From 7ce8aff67c54d1f7e6eac4b3df84a6b7240e3032 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Thu, 20 Aug 2026 02:30:17 +0800 Subject: [PATCH] perf: use FFmpeg fast seek instead of fps filter for frame extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace single `ffmpeg -vf fps=1/interval` call (full video decode) with per-frame `ffmpeg -ss -frames:v 1` calls (keyframe seek) - 6-8x faster on ARM: 180s+ → 33s for 60 frames from 30min video - Per-frame timeout 30s (was 120s single call), failed seeks logged and skipped - Verified end-to-end: 30min 360MB video → 60 candidates → 12 keyframes in 39s --- .../video_preprocessor/preprocessor.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/fam-edge/src/fam_edge/video_preprocessor/preprocessor.py b/fam-edge/src/fam_edge/video_preprocessor/preprocessor.py index 3bc136d..8a1e548 100644 --- a/fam-edge/src/fam_edge/video_preprocessor/preprocessor.py +++ b/fam-edge/src/fam_edge/video_preprocessor/preprocessor.py @@ -109,7 +109,7 @@ class VideoPreprocessor: return 0.0 def extract_candidate_frames(self, video_path: str) -> List[str]: - """等距粗抽候选帧(数量随视频时长自适应)""" + """等距粗抽候选帧(数量随视频时长自适应,使用快速 seek)""" os.makedirs(self.frames_dir, exist_ok=True) duration = self._get_video_duration(video_path) self.video_duration = duration @@ -128,17 +128,25 @@ class VideoPreprocessor: candidate_count = 0 logger.warning(f"[task_id={self.task_id}] ffprobe 失败,退化为 60s 间隔抽帧") - cmd = [ - 'ffmpeg', '-i', video_path, - '-vf', f'fps=1/{interval}', - '-q:v', '2', - os.path.join(self.frames_dir, 'frame_%04d.jpg') - ] - try: - subprocess.run(cmd, capture_output=True, timeout=120, check=True) - except subprocess.CalledProcessError as e: - logger.error(f"[task_id={self.task_id}] FFmpeg 抽帧失败: {e}") - raise + # 快速 seek 逐帧提取(比 fps 滤镜快 6-8 倍,ARM CPU 上尤甚) + timestamps = [i * interval for i in range(candidate_count)] if candidate_count > 0 else [] + if not timestamps: + # 兜底: 未知时长,用 ffprobe 不可用时按 60s 间隔 + timestamps = [i * 60 for i in range(30)] + + for i, ts in enumerate(timestamps): + output_path = os.path.join(self.frames_dir, f'frame_{i+1:04d}.jpg') + cmd = [ + 'ffmpeg', '-ss', f'{ts:.1f}', + '-i', video_path, + '-frames:v', '1', + '-q:v', '2', + output_path + ] + try: + subprocess.run(cmd, capture_output=True, timeout=30, check=True) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: + logger.warning(f"[task_id={self.task_id}] seek 到 {ts:.1f}s 失败: {e}") # 收集候选帧路径 frames = sorted([ @@ -147,7 +155,7 @@ class VideoPreprocessor: if f.endswith('.jpg') ]) log_task(logger, self.task_id, 'extract', - f'视频时长 {duration:.0f}s, 粗抽 {len(frames)} 张候选帧 (目标 {candidate_count})') + f'视频时长 {duration:.0f}s, 快速 seek 粗抽 {len(frames)} 张候选帧 (目标 {candidate_count})') return frames def _compute_adaptive_key_frame_counts(self) -> Tuple[int, int]: