perf: use FFmpeg fast seek instead of fps filter for frame extraction
- Replace single `ffmpeg -vf fps=1/interval` call (full video decode) with per-frame `ffmpeg -ss <ts> -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
This commit is contained in:
@@ -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 间隔抽帧")
|
||||
|
||||
# 快速 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', '-i', video_path,
|
||||
'-vf', f'fps=1/{interval}',
|
||||
'ffmpeg', '-ss', f'{ts:.1f}',
|
||||
'-i', video_path,
|
||||
'-frames:v', '1',
|
||||
'-q:v', '2',
|
||||
os.path.join(self.frames_dir, 'frame_%04d.jpg')
|
||||
output_path
|
||||
]
|
||||
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
|
||||
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]:
|
||||
|
||||
Reference in New Issue
Block a user