mirror of
https://github.com/opencv/opencv.git
synced 2026-07-30 07:43:03 +04:00
Merge pull request #27878 from 0lekW:ffmpeg-negative-dts-seeking-fix
Fix frame seeking with negative DTS values in FFMPEG backend #27878 **Merge with extra**: https://github.com/opencv/opencv_extra/pull/1289 Fixes https://github.com/opencv/opencv/issues/27819 Fixes https://github.com/opencv/opencv/issues/23472 Accompanied by PR on https://github.com/opencv/opencv_extra/pull/1289 The FFmpeg backend fails to correctly seek in H.264 videos that contain negative DTS values in their initial frames. This is a valid encoding practice used by modern video encoders (such as DaVinci Resolve's current export) where B-frame reordering causes the first few frames to have negative DTS values. When picture_pts is unavailable (AV_NOPTS_VALUE), the code falls back to using pkt_dts: `picture_pts = packet_raw.pts != AV_NOPTS_VALUE_ ? packet_raw.pts : packet_raw.dts;` If this DTS value is negative (which is legal per H.264 spec), it propagates through the frame number calculation: `frame_number = dts_to_frame_number(picture_pts) - first_frame_number;` This results in negative frame numbers, messing up seeking operations. Solution implemented in this branch is a timestamp normalization similar to FFmpegs -avoid_negative_ts_make_zero flag: - Calculate a global offset once on the first decoded frame by getting the minimum timestamp in either: - Container start_time - Stream start_time - First observed timestamp (PTS, then DTS). - Apply the offset consistently to all timestamps, shifting negative values to begin at 0 while keeping relative timing. - Simplify timestamp converters to remove `start_time` subtractions since timestamps are pre-normalized. This also includes a new test `videoio_ffmpeg.seek_with_negative_dts` This test verifies that seeking behavior performs as expected on a file which has negative DTS values in the first frames. A PR on opencv_extra accompanies this one with that testing file: https://github.com/opencv/opencv_extra/pull/1279 ``` opencv_extra=ffmpeg-videoio-negative-dts-test-data ``` <cut/> ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
@@ -1055,6 +1055,63 @@ TEST(ffmpeg_cap_properties, set_pos_get_msec)
|
||||
EXPECT_EQ(cap.get(CAP_PROP_POS_MSEC), 0.0);
|
||||
}
|
||||
|
||||
// Test that seeking twice to the same frame in videos with negative DTS
|
||||
// does not result in negative position or timestamp values
|
||||
// related issue: https://github.com/opencv/opencv/issues/27819
|
||||
TEST(videoio_ffmpeg, seek_with_negative_dts)
|
||||
{
|
||||
if (!videoio_registry::hasBackend(CAP_FFMPEG))
|
||||
throw SkipTestException("FFmpeg backend was not found");
|
||||
|
||||
const std::string filename = findDataFile("video/negdts_h264.mp4");
|
||||
VideoCapture cap(filename, CAP_FFMPEG);
|
||||
|
||||
if (!cap.isOpened())
|
||||
throw SkipTestException("Video stream is not supported");
|
||||
|
||||
// after open, a single grab() should not yield negative POS_MSEC.
|
||||
ASSERT_TRUE(cap.grab());
|
||||
EXPECT_GE(cap.get(CAP_PROP_POS_MSEC), 0.0) << "Negative ts immediately after open+grab()";
|
||||
|
||||
ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES, 0));
|
||||
(void)cap.get(CAP_PROP_POS_FRAMES);
|
||||
|
||||
const int framesToProbe[] = {2, 3, 4, 5};
|
||||
|
||||
for (int f : framesToProbe)
|
||||
{
|
||||
// Reset to frame 0
|
||||
ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES, 0));
|
||||
cap.get(CAP_PROP_POS_FRAMES);
|
||||
|
||||
// Seek to target frame
|
||||
ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES, f));
|
||||
const double posAfterFirstSeek = cap.get(CAP_PROP_POS_FRAMES);
|
||||
|
||||
// Seek to the same frame again
|
||||
ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES, f));
|
||||
const double posAfterSecondSeek = cap.get(CAP_PROP_POS_FRAMES);
|
||||
const double tsAfterSecondSeek = cap.get(CAP_PROP_POS_MSEC);
|
||||
|
||||
EXPECT_GE(posAfterSecondSeek, 0)
|
||||
<< "Frame index became negative after second seek to frame " << f
|
||||
<< " (first seek gave " << posAfterFirstSeek << ")";
|
||||
EXPECT_GE(tsAfterSecondSeek, 0.0)
|
||||
<< "Timestamp became negative after second seek to frame " << f;
|
||||
|
||||
// Per-iteration decode check: grab() + ts non-negative
|
||||
ASSERT_TRUE(cap.grab());
|
||||
EXPECT_GE(cap.get(CAP_PROP_POS_MSEC), 0.0) << "Negative timestamp after grab() at frame " << f;
|
||||
|
||||
// Verify that reading a frame works and position advances
|
||||
Mat frame;
|
||||
ASSERT_TRUE(cap.read(frame));
|
||||
ASSERT_FALSE(frame.empty());
|
||||
EXPECT_GE(cap.get(CAP_PROP_POS_FRAMES), f);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // WIN32
|
||||
|
||||
|
||||
}} // namespace
|
||||
|
||||
Reference in New Issue
Block a user