videoio(ffmpeg): guard seek() against AV_NOPTS_VALUE start_time

CvCapture_FFMPEG::seek() seeds the seek target with the stream start_time
without checking for AV_NOPTS_VALUE, while dts_to_sec() right above it does
perform that check.

Some containers do not let FFmpeg establish a start time (for instance
Matroska files whose H.264 track is declared through the legacy VfW wrapper,
V_MS/VFW/FOURCC with a BITMAPINFOHEADER, instead of V_MPEG4/ISO/AVC with an
avcC record). For those, start_time is AV_NOPTS_VALUE, the computed target
becomes INT64_MIN plus an offset, and av_seek_frame() with AVSEEK_FLAG_BACKWARD
lands at position 0. The refinement loop below then decodes every single frame
up to the requested one, so every seek degrades to a linear scan. The returned
frame is correct, which makes the failure silent.

Measured on a 15 h H.264 recording (1628443 frames) with OpenCV 5.1.0-dev:

  target      before      after
   10 s      0.247 s     0.137 s
   60 s      1.488 s     0.047 s
  300 s      7.559 s     0.068 s
  54000 s    ~22 min     0.051 s

Seek cost grew strictly linearly with the target position, at roughly 25 ms
per second of video.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loic Goossens
2026-09-21 17:04:22 +02:00
co-authored by Claude Opus 5
parent e0cb212451
commit 7de7f882d3
+2
View File
@@ -2321,6 +2321,8 @@ void CvCapture_FFMPEG::seek(int64_t _frame_number)
AVStream* st = ic->streams[video_stream];
int64_t time_stamp = st->start_time;
if (time_stamp == AV_NOPTS_VALUE_) // same guard as dts_to_sec(): without it the target is INT64_MIN + offset,
time_stamp = 0; // av_seek_frame() lands at position 0 and the loop below decodes every frame
double time_base = r2d(st->time_base);
int64_t ts_norm = (int64_t)(sec / time_base + 0.5);