diff --git a/modules/videoio/src/cap_avfoundation.mm b/modules/videoio/src/cap_avfoundation.mm index 4f74ddfaab..aa7ee57142 100644 --- a/modules/videoio/src/cap_avfoundation.mm +++ b/modules/videoio/src/cap_avfoundation.mm @@ -1104,9 +1104,19 @@ bool CvCaptureFile::setProperty(int property_id, double value) { t.value = value * t.timescale / 1000; retval = setupReadingAt(t); break; - case cv::CAP_PROP_POS_FRAMES: - retval = mAssetTrack.nominalFrameRate > 0 ? setupReadingAt(CMTimeMake(value, mAssetTrack.nominalFrameRate)) : false; + case cv::CAP_PROP_POS_FRAMES: { + // Build the seek CMTime from the track's native rational frame + // duration so non-integer rates (e.g. 23.976 = 24000/1001) are exact. + CMTime frameDuration = mAssetTrack.minFrameDuration; + if (frameDuration.timescale > 0 && frameDuration.value > 0) { + retval = setupReadingAt(CMTimeMultiply(frameDuration, (int32_t)value)); + } else if (mAssetTrack.nominalFrameRate > 0) { + retval = setupReadingAt(CMTimeMake(value, mAssetTrack.nominalFrameRate)); + } else { + retval = false; + } break; + } case cv::CAP_PROP_POS_AVI_RATIO: t = mAsset.duration; t.value = round(t.value * value); @@ -1349,4 +1359,4 @@ void CvVideoWriter_AVFoundation::write(cv::InputArray image) { } } -#pragma clang diagnostic pop \ No newline at end of file +#pragma clang diagnostic pop diff --git a/modules/videoio/src/cap_avfoundation_mac.mm b/modules/videoio/src/cap_avfoundation_mac.mm index 5ba1c01c60..3bcd0928de 100644 --- a/modules/videoio/src/cap_avfoundation_mac.mm +++ b/modules/videoio/src/cap_avfoundation_mac.mm @@ -1033,9 +1033,19 @@ bool CvCaptureFile::setProperty_(int property_id, double value) { t.value = value * t.timescale / 1000; retval = setupReadingAt(t); break; - case cv::CAP_PROP_POS_FRAMES: - retval = mAssetTrack.nominalFrameRate > 0 ? setupReadingAt(CMTimeMake(value, mAssetTrack.nominalFrameRate)) : false; + case cv::CAP_PROP_POS_FRAMES: { + // Build the seek CMTime from the track's native rational frame + // duration so non-integer rates (e.g. 23.976 = 24000/1001) are exact. + CMTime frameDuration = mAssetTrack.minFrameDuration; + if (frameDuration.timescale > 0 && frameDuration.value > 0) { + retval = setupReadingAt(CMTimeMultiply(frameDuration, (int32_t)value)); + } else if (mAssetTrack.nominalFrameRate > 0) { + retval = setupReadingAt(CMTimeMake(value, mAssetTrack.nominalFrameRate)); + } else { + retval = false; + } break; + } case cv::CAP_PROP_POS_AVI_RATIO: t = mAsset.duration; t.value = round(t.value * value); diff --git a/modules/videoio/test/test_video_io.cpp b/modules/videoio/test/test_video_io.cpp index 9c1b5cb209..784b89d600 100644 --- a/modules/videoio/test/test_video_io.cpp +++ b/modules/videoio/test/test_video_io.cpp @@ -1238,4 +1238,43 @@ inline static std::string stream_capture_ffmpeg_name_printer(const testing::Test INSTANTIATE_TEST_CASE_P(videoio, stream_capture_ffmpeg, testing::Values("h264", "h265", "mjpg.avi"), stream_capture_ffmpeg_name_printer); +// Seek by frame index must be accurate on non-integer frame rates. +typedef testing::TestWithParam PreciseSeekingTest; + +TEST_P(PreciseSeekingTest, seek_nonInteger_fps_frame_accurate) +{ + VideoCaptureAPIs apiPref = GetParam(); + if (!videoio_registry::hasBackend(apiPref)) + throw SkipTestException(cv::String("Backend is not available/disabled: ") + cv::videoio_registry::getBackendName(apiPref)); + + const std::string file = findDataFile("video/sample_23976fps.mp4"); + + VideoCapture cap; + ASSERT_TRUE(cap.open(file, apiPref)) + << "AVFoundation could not open " << file; + + const double fps_num = 24000.0; + const double fps_den = 1001.0; + const double frame_period_ms = fps_den * 1000.0 / fps_num; // ~41.7083 + const double tol_ms = frame_period_ms * 0.5; + + const int targets[] = { 0, 1, 24, 50, 99 }; + for (int N : targets) + { + ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES, N)) + << "seek to frame " << N << " failed"; + Mat img; + ASSERT_TRUE(cap.read(img)) + << "read after seek to frame " << N << " failed"; + const double got_ms = cap.get(CAP_PROP_POS_MSEC); + const double expect_ms = N * frame_period_ms; + EXPECT_NEAR(got_ms, expect_ms, tol_ms) + << "non-integer fps seek drifted at frame N=" << N; + } +} + +VideoCaptureAPIs seekable_backeinds[] = {CAP_FFMPEG, CAP_MSMF, CAP_AVFOUNDATION}; + +INSTANTIATE_TEST_CASE_P(videoio, PreciseSeekingTest, testing::ValuesIn(seekable_backeinds), safe_capture_name_printer); + } // namespace