From 50d2d1446fc7213b00411452f9aca313a1f2c96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Brzezi=C5=84ski?= Date: Mon, 31 Mar 2025 18:54:28 +0200 Subject: [PATCH] avfvideosrc: Guess reasonable framerate values for some 3rd party devices For some third-party devices macOS sometimes reports silly framerates, like 750003/6250 instead of 120/1. To avoid users having to exactly pecify those values, we instead report the closest reasonable value in caps. If it ends up being chosen, the additional logic in setDeviceCaps() will reverse that process and pass the actual supported value back to AVF, as most often the rounding causes us to fall just outside the accepted threshold. Part-of: --- .../sys/applemedia/avfvideosrc.m | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/subprojects/gst-plugins-bad/sys/applemedia/avfvideosrc.m b/subprojects/gst-plugins-bad/sys/applemedia/avfvideosrc.m index bcd94ca567..5a0e7c54b4 100644 --- a/subprojects/gst-plugins-bad/sys/applemedia/avfvideosrc.m +++ b/subprojects/gst-plugins-bad/sys/applemedia/avfvideosrc.m @@ -53,6 +53,9 @@ GST_DEBUG_CATEGORY (gst_avf_video_src_debug); static CMVideoDimensions get_oriented_dimensions(GstAVFVideoSourceOrientation orientation, CMVideoDimensions dimensions); +static CMTime +find_range_bound_close_enough_to_fps (AVFrameRateRange *range, int fps_n, int fps_d); + static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, @@ -662,8 +665,11 @@ G_GNUC_END_IGNORE_DEPRECATIONS device.activeFormat = fmt; for (AVFrameRateRange *range in fmt.videoSupportedFrameRateRanges) { CMTime dur = CMTimeMake (info->fps_d, info->fps_n); - if (CMTIME_COMPARE_INLINE (range.minFrameDuration, <=, dur) && - CMTIME_COMPARE_INLINE (range.maxFrameDuration, >=, dur)) { + // Either the value is directly supported, or we rounded it to the nearest 'sane' value in get_caps(), + // in which case AVF might refuse to accept it, so we try to use one of the range bounds if it's close enough. + if ((CMTIME_COMPARE_INLINE (range.minFrameDuration, <=, dur) && + CMTIME_COMPARE_INLINE (range.maxFrameDuration, >=, dur)) || + CMTIME_IS_VALID (dur = find_range_bound_close_enough_to_fps (range, info->fps_n, info->fps_d))) { device.activeVideoMinFrameDuration = dur; device.activeVideoMaxFrameDuration = dur; found_framerate = TRUE; @@ -1581,16 +1587,20 @@ gst_av_capture_device_get_caps (AVCaptureDevice *device, AVCaptureVideoDataOutpu for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) { int min_fps_n, min_fps_d, max_fps_n, max_fps_d; + GstClockTime min_dur, max_dur; - /* CMTime duration is the inverse of fps*/ - min_fps_n = range.maxFrameDuration.timescale; - min_fps_d = range.maxFrameDuration.value; - max_fps_n = range.minFrameDuration.timescale; - max_fps_d = range.minFrameDuration.value; + // For some third-party devices macOS sometimes reports silly framerates, like 750003/6250 instead of 120/1. + // Let's round all values here just in case, and then reverse that in setDeviceCaps when passing values to AVF. + // Note: In the past, min/maxFrameRate were used instead, but that also came with its own set of rounding issues. + min_dur = gst_util_uint64_scale (GST_SECOND, range.minFrameDuration.value, range.minFrameDuration.timescale); + max_dur = gst_util_uint64_scale (GST_SECOND, range.maxFrameDuration.value, range.maxFrameDuration.timescale); - GST_DEBUG ("dimensions %ix%i fps range is [%i/%i, %i/%i]", - dimensions.width, dimensions.height, min_fps_n, min_fps_d, max_fps_n, - max_fps_d); + gst_video_guess_framerate (min_dur, &max_fps_n, &max_fps_d); + gst_video_guess_framerate (max_dur, &min_fps_n, &min_fps_d); + + GST_DEBUG ("dimensions %ix%i, duration range %s, assuming fps range [%d/%d - %d/%d]", + dimensions.width, dimensions.height, [range description].UTF8String, + min_fps_n, min_fps_d, max_fps_n, max_fps_d); for (NSNumber *pixel_format in output.availableVideoCVPixelFormatTypes) { GstCaps *caps; @@ -1659,3 +1669,26 @@ get_oriented_dimensions (GstAVFVideoSourceOrientation orientation, CMVideoDimens } return orientedDimensions; } + +static CMTime +find_range_bound_close_enough_to_fps (AVFrameRateRange *range, int fps_n, int fps_d) +{ + GstClockTime min_dur, max_dur; + gint guessed_n, guessed_d; + + min_dur = gst_util_uint64_scale (GST_SECOND, range.minFrameDuration.value, range.minFrameDuration.timescale); + gst_video_guess_framerate (min_dur, &guessed_n, &guessed_d); + if (guessed_n == fps_n && guessed_d == fps_d) { + GST_DEBUG ("Using min duration from range %s for fps %d/%d", [range description].UTF8String, fps_n, fps_d); + return range.minFrameDuration; + } + + max_dur = gst_util_uint64_scale (GST_SECOND, range.maxFrameDuration.value, range.maxFrameDuration.timescale); + gst_video_guess_framerate (max_dur, &guessed_n, &guessed_d); + if (guessed_n == fps_n && guessed_d == fps_d) { + GST_DEBUG ("Using max duration from range %s for fps %d/%d", [range description].UTF8String, fps_n, fps_d); + return range.maxFrameDuration; + } + + return kCMTimeInvalid; +}