From 64be9fcda6096c77bf86c2af2464fbaeee2eb136 Mon Sep 17 00:00:00 2001 From: jcm <6864788+jcm93@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:25:43 -0600 Subject: [PATCH] add FPS fallback --- plugins/mac-avcapture/OBSAVCapture.h | 12 +++++++ plugins/mac-avcapture/OBSAVCapture.m | 53 +++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/plugins/mac-avcapture/OBSAVCapture.h b/plugins/mac-avcapture/OBSAVCapture.h index 877b646dc648ad..f85e8f6ce329c5 100644 --- a/plugins/mac-avcapture/OBSAVCapture.h +++ b/plugins/mac-avcapture/OBSAVCapture.h @@ -25,6 +25,7 @@ typedef struct obs_source_frame OBSAVCaptureVideoFrame; typedef struct obs_source_audio OBSAVCaptureAudioFrame; typedef struct gs_texture OBSAVCaptureTexture; typedef struct gs_effect OBSAVCaptureEffect; +typedef struct media_frames_per_second OBSAVCaptureMediaFPS; /// C struct for errors encountered in capture callback typedef enum : NSUInteger { @@ -152,6 +153,17 @@ typedef struct av_capture_info { /// - Returns: [CMVideoDimensions](https://developer.apple.com/documentation/coremedia/cmvideodimensions?language=objc) struct with resolution from user settings + (CMVideoDimensions)legacyDimensionsFromSettings:(void *)settings; +/// Generates an appropriate frame rate value to fall back to (based on OBS's configured output framerate) when the user selects a format that does not support their previously configured frame rate. +/// +/// This function fetches OBS's configured output frame rate and uses it to determine the appropriate default frame rate supported by the format. It will return: +/// +/// * The frame rate nearest up to and including OBS's configured output frame rate. +/// * If that does not exist on the format, the frame rate nearest above OBS's configured output frame rate. +/// * If that does not exist, a struct representing an invalid frame rate. +/// - Parameter format: [AVCaptureDeviceFormat](https://developer.apple.com/documentation/avfoundation/avcapturedevice/format?language=objc) instance that we are determining a fallback FPS for. +/// - Returns: Struct representing a frames per second value as defined in ``libobs``. ++ (OBSAVCaptureMediaFPS)fallbackFrameRateForFormat:(AVCaptureDeviceFormat *)format; + /// Generates a new [NSString](https://developer.apple.com/documentation/foundation/nsstring?language=objc) instance containing a human-readable aspect ratio for a given pixel width and height. /// - Parameter dimensions: [CMVideoDimensions](https://developer.apple.com/documentation/coremedia/cmvideodimensions?language=objc) struct containing the width and height in pixels. /// - Returns: New [NSString](https://developer.apple.com/documentation/foundation/nsstring?language=objc) instance containing the aspect ratio description. diff --git a/plugins/mac-avcapture/OBSAVCapture.m b/plugins/mac-avcapture/OBSAVCapture.m index 4415ec9bd3c260..9813e55663a687 100644 --- a/plugins/mac-avcapture/OBSAVCapture.m +++ b/plugins/mac-avcapture/OBSAVCapture.m @@ -411,7 +411,7 @@ - (BOOL)configureSessionWithPreset:(AVCaptureSessionPreset)preset withError:(NSE - (BOOL)configureSession:(NSError *__autoreleasing *)error { - struct media_frames_per_second fps; + OBSAVCaptureMediaFPS fps; if (!obs_data_get_frames_per_second(self.captureInfo->settings, "frame_rate", &fps, NULL)) { [self AVCaptureLog:LOG_DEBUG withFormat:@"No valid framerate found in settings"]; return NO; @@ -477,9 +477,23 @@ - (BOOL)configureSession:(NSError *__autoreleasing *)error } if (!fpsSupported) { - [self AVCaptureLog:LOG_WARNING withFormat:@"Frame rate is not supported: %g FPS (%u/%u)", - media_frames_per_second_to_fps(fps), fps.numerator, fps.denominator]; - return NO; + OBSAVCaptureMediaFPS fallbackFPS = [OBSAVCapture fallbackFrameRateForFormat:format]; + if (fallbackFPS.denominator > 0 && fallbackFPS.numerator > 0) { + [self AVCaptureLog:LOG_WARNING withFormat:@"Frame rate is not supported: %g FPS (%u/%u), \n" + " falling back to value supported by device: %G FPS (%u/%u)", + media_frames_per_second_to_fps(fps), fps.numerator, + fps.denominator, media_frames_per_second_to_fps(fallbackFPS), + fallbackFPS.numerator, fallbackFPS.denominator]; + obs_data_set_frames_per_second(self.captureInfo->settings, "frame_rate", fallbackFPS, NULL); + time.value = fallbackFPS.denominator; + time.timescale = fallbackFPS.numerator; + } else { + [self AVCaptureLog:LOG_WARNING + withFormat:@"Frame rate is not supported: %g FPS (%u/%u), \n" + " no supported fallback FPS found", + media_frames_per_second_to_fps(fps), fps.numerator, fps.denominator]; + return NO; + } } [self.session beginConfiguration]; @@ -643,6 +657,37 @@ + (CMVideoDimensions)legacyDimensionsFromSettings:(void *)settings return dimensions; } ++ (OBSAVCaptureMediaFPS)fallbackFrameRateForFormat:(AVCaptureDeviceFormat *)format +{ + struct obs_video_info video_info; + bool result = obs_get_video_info(&video_info); + + double outputFPS = result ? ((double) video_info.fps_num / (double) video_info.fps_den) : 0; + + double closestUpTo = 0; + double closestAbove = DBL_MAX; + OBSAVCaptureMediaFPS closestUpToMFPS = {}; + OBSAVCaptureMediaFPS closestAboveMFPS = {}; + + for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) { + if (range.maxFrameRate > closestUpTo && range.maxFrameRate <= outputFPS) { + closestUpTo = range.maxFrameRate; + closestUpToMFPS.numerator = (uint32_t) clamp_Uint(range.minFrameDuration.timescale, 0, UINT32_MAX); + closestUpToMFPS.denominator = (uint32_t) clamp_Uint(range.minFrameDuration.value, 0, UINT32_MAX); + } + if (range.minFrameRate > outputFPS && range.minFrameRate < closestAbove) { + closestAbove = range.minFrameRate; + closestAboveMFPS.numerator = (uint32_t) clamp_Uint(range.maxFrameDuration.timescale, 0, UINT32_MAX); + closestAboveMFPS.denominator = (uint32_t) clamp_Uint(range.maxFrameDuration.value, 0, UINT32_MAX); + } + } + if (closestUpTo > 0) { + return closestUpToMFPS; + } else { + return closestAboveMFPS; + } +} + + (NSString *)aspectRatioStringFromDimensions:(CMVideoDimensions)dimensions { if (dimensions.width <= 0 || dimensions.height <= 0) {