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 <noreply@anthropic.com>
This commit is contained in:
henryruhs
2026-07-22 12:54:37 +02:00
co-authored by Claude Opus 4.8
parent d88c1f6060
commit f7dd894241
4 changed files with 22 additions and 60 deletions
+2
View File
@@ -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' ])
+2 -4
View File
@@ -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]
+8 -30
View File
@@ -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()
+10 -26
View File
@@ -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