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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user