From f7dd89424104663c187587f6253a818c3338be00 Mon Sep 17 00:00:00 2001 From: henryruhs Date: Wed, 22 Jul 2026 12:54:37 +0200 Subject: [PATCH] Drop the cv2 capture pool; all video reads go through ffmpeg/ffprobe video_manager now pools only the ffmpeg reader and writer. read_video_frame seeks and reads through the reader; count_video_frame_total / detect_video_fps / detect_video_resolution read from ffprobe metadata (cached via lru_cache). Removes get_video_capture, conditional_set_video_frame_position, the cv2 import and the 'capture' pool key (and VideoCaptureSet). Inline the reader buffer margin as a local (buffer_margin = 16) instead of a module constant. Side effect: reference face-selector mode and the NSFW analyser now decode AV1 correctly, since they read frames through the reader instead of cv2. Co-Authored-By: Claude Opus 4.8 --- facefusion/ffprobe.py | 2 ++ facefusion/types.py | 6 ++---- facefusion/video_manager.py | 38 ++++++++----------------------------- facefusion/vision.py | 36 ++++++++++------------------------- 4 files changed, 22 insertions(+), 60 deletions(-) diff --git a/facefusion/ffprobe.py b/facefusion/ffprobe.py index 17147cf5..ebdb5375 100644 --- a/facefusion/ffprobe.py +++ b/facefusion/ffprobe.py @@ -1,4 +1,5 @@ import subprocess +from functools import lru_cache from typing import Dict, List from facefusion import ffprobe_builder @@ -47,6 +48,7 @@ def probe_video_entries(video_path : str, entries : List[str]) -> Dict[str, str] return parse_entries(output) +@lru_cache(maxsize = 128) def extract_video_metadata(video_path : str) -> VideoMetadata: video_entries = probe_video_entries(video_path, [ 'duration', 'width', 'height', 'r_frame_rate', 'bit_rate' ]) diff --git a/facefusion/types.py b/facefusion/types.py index edb0f1ef..e4390908 100755 --- a/facefusion/types.py +++ b/facefusion/types.py @@ -64,7 +64,6 @@ Language = Literal['en'] Locales : TypeAlias = Dict[Language, Dict[str, Any]] LocalePoolSet : TypeAlias = Dict[str, Locales] -VideoCaptureSet : TypeAlias = Dict[str, cv2.VideoCapture] VideoWriterSet : TypeAlias = Dict[str, subprocess.Popen] CameraCaptureSet : TypeAlias = Dict[str, cv2.VideoCapture] CameraPoolSet = TypedDict('CameraPoolSet', @@ -111,9 +110,8 @@ VideoReader = TypedDict('VideoReader', VideoReaderSet : TypeAlias = Dict[str, VideoReader] VideoPoolSet = TypedDict('VideoPoolSet', { - 'capture' : VideoCaptureSet, - 'writer' : VideoWriterSet, - 'reader' : VideoReaderSet + 'reader' : VideoReaderSet, + 'writer' : VideoWriterSet }) Color : TypeAlias = Tuple[int, int, int, int] Padding : TypeAlias = Tuple[int, int, int, int] diff --git a/facefusion/video_manager.py b/facefusion/video_manager.py index 846df288..c86cd6d5 100644 --- a/facefusion/video_manager.py +++ b/facefusion/video_manager.py @@ -1,7 +1,6 @@ import subprocess from typing import Optional, Tuple, cast -import cv2 import numpy from facefusion import ffmpeg_builder, state_manager @@ -12,27 +11,9 @@ from facefusion.types import Fps, Resolution, VideoFormat, VideoPoolSet, VideoRe VIDEO_POOL_SET : VideoPoolSet =\ { - 'capture': {}, - 'writer': {}, - 'reader': {} + 'reader': {}, + 'writer': {} } -VIDEO_READER_BUFFER_MARGIN = 16 - - -def get_video_capture(video_path : str) -> cv2.VideoCapture: - if video_path not in VIDEO_POOL_SET.get('capture'): - video_capture = cv2.VideoCapture(video_path) - - if video_capture.isOpened(): - VIDEO_POOL_SET['capture'][video_path] = video_capture - - return VIDEO_POOL_SET.get('capture').get(video_path) - - -def conditional_set_video_frame_position(video_capture : cv2.VideoCapture, frame_position : int) -> bool: - if not video_capture.get(cv2.CAP_PROP_POS_FRAMES) == frame_position: - return video_capture.set(cv2.CAP_PROP_POS_FRAMES, frame_position) - return True def create_video_reader_process(video_path : str, frame_position : int, video_fps : Fps) -> subprocess.Popen[bytes]: @@ -90,22 +71,23 @@ def read_video_reader_frame(video_reader : VideoReader) -> Tuple[bool, Optional[ return False, None -def evict_video_reader_buffer(video_reader : VideoReader, frame_start : int) -> None: +def evict_video_reader_buffer(video_reader : VideoReader, frame_start : int, buffer_margin : int) -> None: frame_buffer = video_reader.get('frame_buffer') - for frame_number in [ key for key in frame_buffer if key < frame_start - VIDEO_READER_BUFFER_MARGIN ]: + for frame_number in [ key for key in frame_buffer if key < frame_start - buffer_margin ]: del frame_buffer[frame_number] def read_video_reader_window(video_reader : VideoReader, frame_start : int, frame_end : int) -> VideoReaderBuffer: frame_buffer = video_reader.get('frame_buffer') + buffer_margin = 16 read_start = max(frame_start, 0) read_end = frame_end if video_reader.get('frame_total') > 0: read_end = min(read_end, video_reader.get('frame_total') - 1) - if read_start not in frame_buffer and (read_start < video_reader.get('position') or read_start > video_reader.get('position') + VIDEO_READER_BUFFER_MARGIN): + if read_start not in frame_buffer and (read_start < video_reader.get('position') or read_start > video_reader.get('position') + buffer_margin): restart_video_reader(video_reader, read_start) reading = video_reader.get('position') <= read_end @@ -117,7 +99,7 @@ def read_video_reader_window(video_reader : VideoReader, frame_start : int, fram frame_buffer[video_reader.get('position') - 1] = vision_frame reading = has_vision_frame and video_reader.get('position') <= read_end - evict_video_reader_buffer(video_reader, read_start) + evict_video_reader_buffer(video_reader, read_start, buffer_margin) return frame_buffer @@ -170,15 +152,11 @@ def close_video_writer(video_writer : subprocess.Popen[bytes]) -> bool: def clear_video_pool() -> None: - for video_capture in VIDEO_POOL_SET.get('capture').values(): - video_capture.release() - for video_writer in VIDEO_POOL_SET.get('writer').values(): video_writer.terminate() for video_reader in VIDEO_POOL_SET.get('reader').values(): video_reader.get('process').terminate() - VIDEO_POOL_SET['capture'].clear() - VIDEO_POOL_SET['writer'].clear() VIDEO_POOL_SET['reader'].clear() + VIDEO_POOL_SET['writer'].clear() diff --git a/facefusion/vision.py b/facefusion/vision.py index dfe32648..b007f903 100644 --- a/facefusion/vision.py +++ b/facefusion/vision.py @@ -7,10 +7,11 @@ import numpy from cv2.typing import Size from facefusion.common_helper import is_windows +from facefusion.ffprobe import extract_video_metadata from facefusion.filesystem import get_file_extension, is_image, is_video from facefusion.thread_helper import thread_lock, thread_semaphore from facefusion.types import ColorMode, Duration, Fps, Mask, Orientation, Resolution, Scale, VisionFrame -from facefusion.video_manager import conditional_set_video_frame_position, conditional_set_video_reader_position, get_video_capture, get_video_reader, read_video_reader_frame, read_video_reader_window +from facefusion.video_manager import conditional_set_video_reader_position, get_video_reader, read_video_reader_frame, read_video_reader_window def read_static_images(image_paths : List[str], color_mode : ColorMode = 'rgb') -> List[VisionFrame]: @@ -77,15 +78,14 @@ def read_static_video_frame(video_path : str, frame_number : int = 0) -> Optiona def read_video_frame(video_path : str, frame_number : int = 0) -> Optional[VisionFrame]: if is_video(video_path): - video_capture = get_video_capture(video_path) + video_reader = get_video_reader(video_path) - if video_capture and video_capture.isOpened(): - video_frame_total = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT)) - video_frame_position = min(video_frame_total, frame_number) + if video_reader: + video_frame_position = min(video_reader.get('frame_total'), frame_number) with thread_semaphore(): - conditional_set_video_frame_position(video_capture, video_frame_position) - has_vision_frame, vision_frame = video_capture.read() + conditional_set_video_reader_position(video_reader, video_frame_position) + has_vision_frame, vision_frame = read_video_reader_frame(video_reader) if has_vision_frame: return vision_frame @@ -146,12 +146,7 @@ def select_video_frames(video_path : str, frame_number : int = 0, frame_offset : def count_video_frame_total(video_path : str) -> int: if is_video(video_path): - video_capture = get_video_capture(video_path) - - if video_capture and video_capture.isOpened(): - with thread_semaphore(): - video_frame_total = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT)) - return video_frame_total + return extract_video_metadata(video_path).get('frame_total') return 0 @@ -166,12 +161,7 @@ def predict_video_frame_total(video_path : str, fps : Fps, trim_frame_start : in def detect_video_fps(video_path : str) -> Optional[float]: if is_video(video_path): - video_capture = get_video_capture(video_path) - - if video_capture and video_capture.isOpened(): - with thread_semaphore(): - video_fps = video_capture.get(cv2.CAP_PROP_FPS) - return video_fps + return extract_video_metadata(video_path).get('fps') return None @@ -219,13 +209,7 @@ def restrict_trim_frame(video_path : str, trim_frame_start : Optional[int], trim def detect_video_resolution(video_path : str) -> Optional[Resolution]: if is_video(video_path): - video_capture = get_video_capture(video_path) - - if video_capture and video_capture.isOpened(): - with thread_semaphore(): - width = video_capture.get(cv2.CAP_PROP_FRAME_WIDTH) - height = video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT) - return int(width), int(height) + return extract_video_metadata(video_path).get('resolution') return None