diff --git a/app/src/gfn/cloud_session_protocol.cpp b/app/src/gfn/cloud_session_protocol.cpp index 94ff617..3ff47b3 100644 --- a/app/src/gfn/cloud_session_protocol.cpp +++ b/app/src/gfn/cloud_session_protocol.cpp @@ -390,6 +390,11 @@ std::string BuildSessionBody( json_object_set_new(features, "hudStreamingMode", json_integer(0)); json_object_set_new(features, "sdrColorSpace", json_integer(2)); json_object_set_new(features, "hdrColorSpace", json_integer(0)); + json_object_set_new(features, "maxBitrateKbps", json_integer(stream_settings.bitrate_kbps)); + json_object_set_new(features, "codec", json_integer(1)); + json_object_set_new(features, "vsync", json_false()); + json_object_set_new(features, "dynamicStreamingMode", json_integer(3)); + json_object_set_new(features, "audioChannelCount", json_integer(2)); json_object_set_new(req, "requestedStreamingFeatures", features); json_t* meta = json_array(); diff --git a/app/src/video_quality_policy.hpp b/app/src/video_quality_policy.hpp index 12bd60b..0643851 100644 --- a/app/src/video_quality_policy.hpp +++ b/app/src/video_quality_policy.hpp @@ -7,8 +7,6 @@ namespace opennow::video struct QualityTuning { - int minimum_bitrate_percent; - int initial_bitrate_percent; int fec_repair_min_percent; int fec_repair_percent; int fec_repair_max_percent; @@ -23,14 +21,14 @@ struct QualityTuning inline QualityTuning ResolveQualityTuning(const std::string& mode) { if (mode == "Original") - return {35, 70, 5, 5, 35, 5, 15, 1000, 0.0f, 0.0f, false}; + return {5, 5, 35, 5, 15, 1000, 0.0f, 0.0f, false}; if (mode == "Clarity") - return {50, 88, 8, 10, 30, 8, 8, 1500, 0.13f, 0.20f, true}; + return {8, 10, 30, 8, 8, 1500, 0.13f, 0.20f, true}; // A small resilience budget and a single spatial shader pass improve // dynamic scenes without adding a queued frame or raising max bitrate. - return {45, 82, 6, 8, 30, 6, 10, 1250, 0.10f, 0.14f, true}; + return {6, 8, 30, 6, 10, 1250, 0.10f, 0.14f, true}; } inline std::string NextQualityMode(const std::string& mode) diff --git a/app/src/webrtc/negotiation.cpp b/app/src/webrtc/negotiation.cpp index 4ae51e3..2a8f584 100644 --- a/app/src/webrtc/negotiation.cpp +++ b/app/src/webrtc/negotiation.cpp @@ -1,6 +1,6 @@ #include "webrtc_session.hpp" +#include "nvst_sdp.hpp" #include "stream/RemoteCandidatePolicy.hpp" -#include "video_quality_policy.hpp" #include "internal.hpp" #include @@ -87,16 +87,9 @@ int ParseIntegerAttribute(const std::string& sdp, const std::string& attribute, return fallback; } -struct RiInputCapabilities { - int partial_reliable_threshold_ms = 16; - uint32_t hid_device_mask = 0xffffffffu; - uint32_t partial_reliable_gamepad_mask = 0x0fu; - uint32_t partial_reliable_hid_mask = 0xffffffffu; -}; - -RiInputCapabilities ParseRiInputCapabilities(const std::string& offer_sdp) +opennow::webrtc::RiInputCapabilities ParseRiInputCapabilities(const std::string& offer_sdp) { - RiInputCapabilities caps; + opennow::webrtc::RiInputCapabilities caps; const int threshold = ParseIntegerAttribute(offer_sdp, "ri.partialReliableThresholdMs", caps.partial_reliable_threshold_ms); if (threshold > 0) caps.partial_reliable_threshold_ms = std::max(1, std::min(5000, threshold)); @@ -539,108 +532,6 @@ int ExtractNvstIntValue(const std::string& nvst_sdp, const std::string& prefix) return 0; } -std::string BuildNvstSdp( - const std::string& answer_sdp, - const opennow::StreamSettings& settings, - const RiInputCapabilities& ri_caps) -{ - const std::string ice_ufrag = ExtractSdpValue(answer_sdp, "a=ice-ufrag:"); - const std::string ice_pwd = ExtractSdpValue(answer_sdp, "a=ice-pwd:"); - const std::string fingerprint = ExtractSdpValue(answer_sdp, "a=fingerprint:sha-256 "); - const auto tuning = opennow::video::ResolveQualityTuning(settings.image_quality_mode); - const int min_bitrate = std::max( - 5000, (settings.bitrate_kbps * tuning.minimum_bitrate_percent) / 100); - const int initial_bitrate = std::max( - min_bitrate, (settings.bitrate_kbps * tuning.initial_bitrate_percent) / 100); - - std::vector lines = { - "v=0", - "o=SdpTest test_id_13 14 IN IPv4 127.0.0.1", - "s=-", - "t=0 0", - "a=general.icePassword:" + ice_pwd, - "a=general.iceUserNameFragment:" + ice_ufrag, - "a=general.dtlsFingerprint:" + fingerprint, - "m=video 0 RTP/AVP", - "a=msid:fbc-video-0", - "a=vqos.fec.rateDropWindow:10", - "a=vqos.fec.minRequiredFecPackets:2", - "a=vqos.fec.repairMinPercent:" + std::to_string(tuning.fec_repair_min_percent), - "a=vqos.fec.repairPercent:" + std::to_string(tuning.fec_repair_percent), - "a=vqos.fec.repairMaxPercent:" + std::to_string(tuning.fec_repair_max_percent), - "a=vqos.dynamicStreamingMode:0", - "a=vqos.drc.enable:0", - "a=vqos.dfc.enable:0", - "a=vqos.dfc.adjustResAndFps:0", - "a=video.dx9EnableNv12:1", - "a=video.dx9EnableHdr:1", - "a=vqos.qpg.enable:1", - "a=vqos.resControl.qp.qpg.featureSetting:7", - "a=bwe.useOwdCongestionControl:1", - "a=video.enableRtpNack:1", - "a=vqos.bw.txRxLag.minFeedbackTxDeltaMs:200", - "a=vqos.drc.bitrateIirFilterFactor:18", - "a=video.packetSize:1140", - "a=packetPacing.minNumPacketsPerGroup:" + - std::to_string(tuning.pacing_min_packets_per_group), - "a=vqos.adjustStreamingFpsDuringOutOfFocus:1", - "a=vqos.resControl.cpmRtc.ignoreOutOfFocusWindowState:1", - "a=vqos.resControl.perfHistory.rtcIgnoreOutOfFocusWindowState:1", - "a=vqos.resControl.cpmRtc.featureMask:0", - "a=vqos.resControl.cpmRtc.enable:0", - "a=vqos.resControl.cpmRtc.minResolutionPercent:100", - "a=vqos.resControl.cpmRtc.resolutionChangeHoldonMs:999999", - "a=packetPacing.numGroups:" + std::to_string(tuning.pacing_groups), - "a=packetPacing.maxDelayUs:" + std::to_string(tuning.pacing_max_delay_us), - "a=packetPacing.minNumPacketsFrame:10", - "a=video.rtpNackQueueLength:1024", - "a=video.rtpNackQueueMaxPackets:512", - "a=video.rtpNackMaxPacketCount:25", - "a=vqos.drc.qpMaxResThresholdAdj:4", - "a=vqos.grc.qpMaxResThresholdAdj:4", - "a=vqos.drc.iirFilterFactor:100", - "a=video.clientViewportWd:" + std::to_string(settings.width), - "a=video.clientViewportHt:" + std::to_string(settings.height), - "a=video.maxFPS:" + std::to_string(settings.fps), - "a=video.initialBitrateKbps:" + std::to_string(initial_bitrate), - "a=video.initialPeakBitrateKbps:" + std::to_string(settings.bitrate_kbps), - "a=vqos.bw.maximumBitrateKbps:" + std::to_string(settings.bitrate_kbps), - "a=vqos.bw.minimumBitrateKbps:" + std::to_string(min_bitrate), - "a=vqos.bw.peakBitrateKbps:" + std::to_string(settings.bitrate_kbps), - "a=vqos.bw.serverPeakBitrateKbps:" + std::to_string(settings.bitrate_kbps), - "a=vqos.bw.enableBandwidthEstimation:1", - "a=vqos.bw.disableBitrateLimit:0", - "a=vqos.grc.maximumBitrateKbps:" + std::to_string(settings.bitrate_kbps), - "a=vqos.grc.enable:0", - "a=video.maxNumReferenceFrames:4", - "a=video.mapRtpTimestampsToFrames:1", - "a=video.encoderCscMode:3", - "a=video.dynamicRangeMode:0", - "a=video.bitDepth:8", - "a=video.scalingFeature1:0", - "a=video.prefilterParams.prefilterModel:0", - "m=audio 0 RTP/AVP", - "a=msid:audio", - "m=mic 0 RTP/AVP", - "a=msid:mic", - "a=rtpmap:0 PCMU/8000", - "m=application 0 RTP/AVP", - "a=msid:input_1", - "a=ri.partialReliableThresholdMs:" + std::to_string(ri_caps.partial_reliable_threshold_ms), - "a=ri.hidDeviceMask:" + std::to_string(ri_caps.hid_device_mask), - "a=ri.enablePartiallyReliableTransferGamepad:" + std::to_string(ri_caps.partial_reliable_gamepad_mask), - "a=ri.enablePartiallyReliableTransferHid:" + std::to_string(ri_caps.partial_reliable_hid_mask), - "", - }; - - std::string result; - for (const auto& line : lines) { - result += line; - result += "\n"; - } - return result; -} - std::string ExtractSignalingHost(const std::string& url) { size_t start = url.find("://"); @@ -881,7 +772,8 @@ void WebRtcSession::handle_signaling_message(const std::string& msg) { if (!nvst_offer_sdp.empty()) AppendTraceBlock("OFFER NVST SDP", nvst_offer_sdp); server_ice_ufrag_ = ExtractSdpValue(offer_sdp, "a=ice-ufrag:"); - const RiInputCapabilities ri_caps = ParseRiInputCapabilities(offer_sdp); + const opennow::webrtc::RiInputCapabilities ri_caps = + ParseRiInputCapabilities(offer_sdp); partial_reliable_threshold_ms_ = ri_caps.partial_reliable_threshold_ms; const int offer_video_port = ExtractOfferMediaPort(offer_sdp, "video"); const int selected_h264_pt = SelectOfferH264PayloadType(offer_sdp); @@ -905,7 +797,8 @@ void WebRtcSession::handle_signaling_message(const std::string& msg) { if (answer_sdp) { AppendTraceBlock("LIBPEER RAW ANSWER SDP", answer_sdp); const std::string adapted_answer_sdp = AdaptAnswerSdpToOffer(answer_sdp, offer_sdp, settings_); - const std::string nvst_sdp = BuildNvstSdp(adapted_answer_sdp, settings_, ri_caps); + const std::string nvst_sdp = opennow::webrtc::BuildNvstSdp( + adapted_answer_sdp, settings_, ri_caps); AppendTraceBlock("ADAPTED ANSWER SDP", adapted_answer_sdp); AppendTraceBlock("ANSWER NVST SDP", nvst_sdp); AppendStreamLog("SDP answer created localUfrag=" + diff --git a/app/src/webrtc/nvst_sdp.cpp b/app/src/webrtc/nvst_sdp.cpp new file mode 100644 index 0000000..8d1160f --- /dev/null +++ b/app/src/webrtc/nvst_sdp.cpp @@ -0,0 +1,129 @@ +#include "nvst_sdp.hpp" + +#include "../video_quality_policy.hpp" + +#include +#include +#include + +namespace opennow::webrtc +{ +namespace +{ + +std::string ExtractSdpValue(const std::string& sdp, const std::string& prefix) +{ + size_t start = 0; + while (start < sdp.size()) { + size_t end = sdp.find('\n', start); + const size_t length = end == std::string::npos ? std::string::npos : end - start; + std::string line = sdp.substr(start, length); + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (line.rfind(prefix, 0) == 0) + return line.substr(prefix.size()); + if (end == std::string::npos) + break; + start = end + 1; + } + return ""; +} + +} // namespace + +std::string BuildNvstSdp( + const std::string& answer_sdp, + const StreamSettings& settings, + const RiInputCapabilities& ri_caps) +{ + constexpr int kOfficialMinimumBitrateKbps = 4000; + const std::string ice_ufrag = ExtractSdpValue(answer_sdp, "a=ice-ufrag:"); + const std::string ice_pwd = ExtractSdpValue(answer_sdp, "a=ice-pwd:"); + const std::string fingerprint = ExtractSdpValue(answer_sdp, "a=fingerprint:sha-256 "); + const auto tuning = video::ResolveQualityTuning(settings.image_quality_mode); + const int max_bitrate = std::max(kOfficialMinimumBitrateKbps, settings.bitrate_kbps); + const int initial_bitrate = std::max(kOfficialMinimumBitrateKbps, max_bitrate / 4); + + const std::vector lines = { + "v=0", + "o=SdpTest test_id_13 14 IN IPv4 127.0.0.1", + "s=-", + "t=0 0", + "a=general.icePassword:" + ice_pwd, + "a=general.iceUserNameFragment:" + ice_ufrag, + "a=general.dtlsFingerprint:" + fingerprint, + "m=video 0 RTP/AVP", + "a=msid:fbc-video-0", + "a=vqos.fec.rateDropWindow:10", + "a=vqos.fec.minRequiredFecPackets:2", + "a=vqos.drc.minRequiredBitrateCheckEnabled:1", + "a=vqos.fec.repairMinPercent:" + std::to_string(tuning.fec_repair_min_percent), + "a=vqos.fec.repairPercent:" + std::to_string(tuning.fec_repair_percent), + "a=vqos.fec.repairMaxPercent:" + std::to_string(tuning.fec_repair_max_percent), + "a=vqos.dynamicStreamingMode:3", + "a=vqos.bllFec.enable:0", + "a=vqos.drc.enable:1", + "a=video.dx9EnableNv12:1", + "a=video.dx9EnableHdr:1", + "a=vqos.qpg.enable:1", + "a=vqos.resControl.qp.qpg.featureSetting:7", + "a=bwe.useOwdCongestionControl:1", + "a=video.enableRtpNack:1", + "a=vqos.bw.txRxLag.minFeedbackTxDeltaMs:200", + "a=vqos.drc.bitrateIirFilterFactor:18", + "a=video.packetSize:1140", + "a=packetPacing.minNumPacketsPerGroup:" + + std::to_string(tuning.pacing_min_packets_per_group), + "a=vqos.adjustStreamingFpsDuringOutOfFocus:1", + "a=vqos.resControl.cpmRtc.ignoreOutOfFocusWindowState:1", + "a=vqos.resControl.perfHistory.rtcIgnoreOutOfFocusWindowState:1", + "a=vqos.resControl.cpmRtc.featureMask:3", + "a=packetPacing.numGroups:" + std::to_string(tuning.pacing_groups), + "a=packetPacing.maxDelayUs:" + std::to_string(tuning.pacing_max_delay_us), + "a=packetPacing.minNumPacketsFrame:10", + "a=video.rtpNackQueueLength:1024", + "a=video.rtpNackQueueMaxPackets:512", + "a=video.rtpNackMaxPacketCount:25", + "a=video.clientViewportWd:" + std::to_string(settings.width), + "a=video.clientViewportHt:" + std::to_string(settings.height), + "a=video.maxFPS:" + std::to_string(settings.fps), + "a=video.initialBitrateKbps:" + std::to_string(initial_bitrate), + "a=video.initialPeakBitrateKbps:" + std::to_string(initial_bitrate), + "a=vqos.bw.maximumBitrateKbps:" + std::to_string(max_bitrate), + "a=vqos.bw.minimumBitrateKbps:" + std::to_string(kOfficialMinimumBitrateKbps), + "a=video.maxNumReferenceFrames:4", + "a=video.mapRtpTimestampsToFrames:1", + "a=video.encoderCscMode:3", + "a=video.encoderHdrCscMode:4", + "a=video.dynamicRangeMode:0", + "a=video.bitDepth:8", + "a=video.scalingFeature1:0", + "a=video.prefilterParams.prefilterMode:0", + "a=video.prefilterParams.prefilterModel:0", + "a=video.prefilterParams.denoiseLevel:0", + "a=video.prefilterParams.sharpnessLevel:0", + "m=audio 0 RTP/AVP", + "a=msid:audio", + "m=mic 0 RTP/AVP", + "a=msid:mic", + "a=rtpmap:0 PCMU/8000", + "m=application 0 RTP/AVP", + "a=msid:input_1", + "a=ri.partialReliableThresholdMs:" + std::to_string(ri_caps.partial_reliable_threshold_ms), + "a=ri.hidDeviceMask:" + std::to_string(ri_caps.hid_device_mask), + "a=ri.enablePartiallyReliableTransferGamepad:" + + std::to_string(ri_caps.partial_reliable_gamepad_mask), + "a=ri.enablePartiallyReliableTransferHid:" + + std::to_string(ri_caps.partial_reliable_hid_mask), + "", + }; + + std::string result; + for (const auto& line : lines) { + result += line; + result += "\n"; + } + return result; +} + +} // namespace opennow::webrtc diff --git a/app/src/webrtc/nvst_sdp.hpp b/app/src/webrtc/nvst_sdp.hpp new file mode 100644 index 0000000..752a45b --- /dev/null +++ b/app/src/webrtc/nvst_sdp.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "../stream_settings.hpp" + +#include +#include + +namespace opennow::webrtc +{ + +struct RiInputCapabilities +{ + int partial_reliable_threshold_ms = 16; + std::uint32_t hid_device_mask = 0xffffffffu; + std::uint32_t partial_reliable_gamepad_mask = 0x0fu; + std::uint32_t partial_reliable_hid_mask = 0xffffffffu; +}; + +std::string BuildNvstSdp( + const std::string& answer_sdp, + const StreamSettings& settings, + const RiInputCapabilities& ri_caps); + +} // namespace opennow::webrtc diff --git a/tests/cloud_session_protocol_test.cpp b/tests/cloud_session_protocol_test.cpp new file mode 100644 index 0000000..9f201cb --- /dev/null +++ b/tests/cloud_session_protocol_test.cpp @@ -0,0 +1,34 @@ +#include "gfn/cloud_session_internal.hpp" + +#include + +#include +#include +#include + +int main() +{ + opennow::StreamSettings settings; + settings.width = 1280; + settings.height = 720; + settings.fps = 60; + settings.bitrate_kbps = 12000; + + const std::string body = opennow::gfn::cloud_session::BuildSessionBody( + "app-id", "title", "device-id", "sub-session-id", "network-test-id", settings); + json_error_t error {}; + std::unique_ptr root( + json_loads(body.c_str(), 0, &error), &json_decref); + assert(root); + + json_t* session_request = json_object_get(root.get(), "sessionRequestData"); + assert(json_is_object(session_request)); + json_t* features = json_object_get(session_request, "requestedStreamingFeatures"); + assert(json_is_object(features)); + assert(json_integer_value(json_object_get(features, "maxBitrateKbps")) == 12000); + assert(json_integer_value(json_object_get(features, "codec")) == 1); + assert(json_is_false(json_object_get(features, "vsync"))); + assert(json_integer_value(json_object_get(features, "dynamicStreamingMode")) == 3); + assert(json_integer_value(json_object_get(features, "audioChannelCount")) == 2); + return 0; +} diff --git a/tests/nvst_sdp_test.cpp b/tests/nvst_sdp_test.cpp new file mode 100644 index 0000000..b9f2800 --- /dev/null +++ b/tests/nvst_sdp_test.cpp @@ -0,0 +1,73 @@ +#include "webrtc/nvst_sdp.hpp" + +#include +#include + +namespace +{ + +bool HasLine(const std::string& sdp, const std::string& line) +{ + return sdp.find(line + "\n") != std::string::npos; +} + +bool HasAttribute(const std::string& sdp, const std::string& attribute) +{ + return sdp.find("a=" + attribute) != std::string::npos; +} + +} // namespace + +int main() +{ + opennow::StreamSettings settings; + settings.width = 1280; + settings.height = 720; + settings.fps = 60; + settings.bitrate_kbps = 12000; + settings.image_quality_mode = "Adaptive"; + + const std::string answer = + "v=0\r\n" + "a=ice-ufrag:test-ufrag\r\n" + "a=ice-pwd:test-password\r\n" + "a=fingerprint:sha-256 AA:BB:CC\r\n"; + const std::string sdp = opennow::webrtc::BuildNvstSdp( + answer, settings, opennow::webrtc::RiInputCapabilities {}); + + for (const auto& line : { + "a=video.maxFPS:60", + "a=video.initialBitrateKbps:4000", + "a=video.initialPeakBitrateKbps:4000", + "a=vqos.bw.maximumBitrateKbps:12000", + "a=vqos.bw.minimumBitrateKbps:4000", + "a=vqos.dynamicStreamingMode:3", + "a=vqos.drc.enable:1", + "a=vqos.resControl.cpmRtc.featureMask:3", + }) { + assert(HasLine(sdp, line)); + } + + for (const auto& attribute : { + "vqos.bw.peakBitrateKbps:", + "vqos.bw.serverPeakBitrateKbps:", + "vqos.bw.enableBandwidthEstimation:", + "vqos.bw.disableBitrateLimit:", + "vqos.grc.maximumBitrateKbps:", + "vqos.grc.enable:", + "vqos.dfc.enable:", + "vqos.dfc.adjustResAndFps:", + "vqos.resControl.cpmRtc.enable:", + "vqos.resControl.cpmRtc.minResolutionPercent:", + "vqos.resControl.cpmRtc.resolutionChangeHoldonMs:", + }) { + assert(!HasAttribute(sdp, attribute)); + } + + settings.bitrate_kbps = 20000; + const std::string quality_sdp = opennow::webrtc::BuildNvstSdp( + answer, settings, opennow::webrtc::RiInputCapabilities {}); + assert(HasLine(quality_sdp, "a=video.initialBitrateKbps:5000")); + assert(HasLine(quality_sdp, "a=vqos.bw.maximumBitrateKbps:20000")); + return 0; +} diff --git a/tests/video_quality_policy_test.cpp b/tests/video_quality_policy_test.cpp index 9cd780e..ec89bdd 100644 --- a/tests/video_quality_policy_test.cpp +++ b/tests/video_quality_policy_test.cpp @@ -8,12 +8,9 @@ int main() const auto adaptive = opennow::video::ResolveQualityTuning("Adaptive"); const auto clarity = opennow::video::ResolveQualityTuning("Clarity"); - assert(original.initial_bitrate_percent == 70); assert(original.denoise_strength == 0.0f); - assert(adaptive.initial_bitrate_percent > original.initial_bitrate_percent); assert(adaptive.fec_repair_percent == 8); assert(adaptive.sharpen_strength > 0.0f); - assert(clarity.initial_bitrate_percent >= adaptive.initial_bitrate_percent); assert(clarity.preserve_reference_deblocking); assert(opennow::video::NextQualityMode("Adaptive") == "Clarity"); assert(opennow::video::NextQualityMode("Clarity") == "Original");