diff --git a/src/Limelight.h b/src/Limelight.h index 3d8aded5..4f80ef4b 100644 --- a/src/Limelight.h +++ b/src/Limelight.h @@ -100,11 +100,23 @@ typedef struct _STREAM_CONFIGURATION { // in /launch and /resume requests. char remoteInputAesKey[16]; char remoteInputAesIv[16]; + + // Fractional frame rate of the video stream (in the form of numerator + // and denominator). Supported by recent versions of Sunshine. + // The non-fractional frame rate still needs to be specified for backward + // compatibility. LiConvertFloatingPointFrameRateToFraction() can be used for + // converting floating-point frame rates to this representation. + int fpsNum; + int fpsDen; } STREAM_CONFIGURATION, *PSTREAM_CONFIGURATION; // Use this function to zero the stream configuration when allocated on the stack or heap void LiInitializeStreamConfiguration(PSTREAM_CONFIGURATION streamConfig); +// Use this function to convert floating-point frame rates to fractional frame +// rates (in the form of numerator and denominator) while trying to maximize precision. +void LiConvertFloatingPointFrameRateToFraction(double value, int* outNum, int* outDen); + // These identify codec configuration data in the buffer lists // of frames identified as IDR frames for H.264 and HEVC formats. // For other codecs, all data is marked as BUFFER_TYPE_PICDATA. diff --git a/src/Misc.c b/src/Misc.c index 4988b125..82d7764e 100644 --- a/src/Misc.c +++ b/src/Misc.c @@ -1,5 +1,7 @@ #include "Limelight-internal.h" +#include + #define ENET_INTERNAL_TIMEOUT_MS 100 // This function wraps enet_host_service() and hides the fact that it must be called @@ -128,6 +130,33 @@ void LiInitializeStreamConfiguration(PSTREAM_CONFIGURATION streamConfig) { memset(streamConfig, 0, sizeof(*streamConfig)); } +void LiConvertFloatingPointFrameRateToFraction(double value, int* outNum, int* outDen) { + if (fabs(value) < 1.0) { + // Unrealistic scenario, we don't care about perfect precision + *outNum = (int)round(INT32_MAX * value); + *outDen = INT32_MAX; + } + else if (fabs(value) > 1000.0) { + // Unrealistic scenario, we don't care about perfect precision + *outNum = INT32_MAX; + *outDen = (int)round(INT32_MAX / value); + } + else { + // Try different numerators for the best precision + double minError = 1.0; + for (int i = 0; i < value; ++i) { + int num = INT32_MAX - i; + int den = (int)round(num / value); + double error = fabs((double)num / den - value); + if (error < minError) { + minError = error; + *outNum = num; + *outDen = den; + } + } + } +} + void LiInitializeVideoCallbacks(PDECODER_RENDERER_CALLBACKS drCallbacks) { memset(drCallbacks, 0, sizeof(*drCallbacks)); } diff --git a/src/SdpGenerator.c b/src/SdpGenerator.c index a2f9789f..defe63fc 100644 --- a/src/SdpGenerator.c +++ b/src/SdpGenerator.c @@ -309,6 +309,15 @@ static PSDP_OPTION getAttributesList(char*urlSafeAddr) { else { err |= addAttributeString(&optionHead, "x-ss-video[0].chromaSamplingType", "0"); } + + // Specify fractional frame rate if requested + if (StreamConfig.fpsNum > 0 && StreamConfig.fpsDen > 0) { + snprintf(payloadStr, sizeof(payloadStr), "%d", StreamConfig.fpsNum); + err |= addAttributeString(&optionHead, "x-ml-video.targetFrameRateNum", payloadStr); + + snprintf(payloadStr, sizeof(payloadStr), "%d", StreamConfig.fpsDen); + err |= addAttributeString(&optionHead, "x-ml-video.targetFrameRateDen", payloadStr); + } } snprintf(payloadStr, sizeof(payloadStr), "%d", StreamConfig.width);