1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-30 07:43:03 +04:00

Merge pull request #28833 from 0lekW:avfoundation-fractional-fps-seek-fix

Avfoundation fractional fps seek fix #28833

Accompanied by PR on [opencv_extra](https://github.com/opencv/opencv_extra/pull/1345)

Fixes https://github.com/opencv/opencv/issues/28831

AVFoundation backend seeking slowly drifts for any video which has a non-integer frame rate.   
Narrowing of a float `mAssetTrack.nominalFrameRate` to int32_t means fractional rates are treated as integer when seeking by frame number. 

Solution implemented in this branch is to use the tracks native rational frame duration when seeking (and available).
I've also included an AVF only test case which uses a new test file in opencv_extra. This test just compares the video caps expected ms vs actual for a known rate file (23.976). 

Could consider extending this test to more then just AVF, as from what I can see there are no fractional rate tests / files.

### 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
- [x] 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:
Alex
2026-04-20 23:58:58 +12:00
committed by GitHub
parent 590b99f478
commit 30178be800
3 changed files with 64 additions and 5 deletions
+13 -3
View File
@@ -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
#pragma clang diagnostic pop
+12 -2
View File
@@ -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);
+39
View File
@@ -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<VideoCaptureAPIs> 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