diff --git a/src/utils/monkey-patches.ts b/src/utils/monkey-patches.ts index 2cba1d0..11ad2ad 100644 --- a/src/utils/monkey-patches.ts +++ b/src/utils/monkey-patches.ts @@ -2,7 +2,7 @@ import { BxEvent } from "@utils/bx-event"; import { getPref, PrefKey } from "@utils/preferences"; import { STATES } from "@utils/global"; import { BxLogger } from "@utils/bx-logger"; -import { patchSdpBitrate } from "./sdp"; +import { patchSdpBitrate, setCodecPreferences } from "./sdp"; import { StreamPlayer, type StreamPlayerOptions } from "@/modules/stream-player"; export function patchVideoApi() { @@ -66,33 +66,6 @@ export function patchRtcCodecs() { if (typeof RTCRtpTransceiver === 'undefined' || !('setCodecPreferences' in RTCRtpTransceiver.prototype)) { return false; } - - const profilePrefix = codecProfile === 'high' ? '4d' : (codecProfile === 'low' ? '420' : '42e'); - const profileLevelId = `profile-level-id=${profilePrefix}`; - - const nativeSetCodecPreferences = RTCRtpTransceiver.prototype.setCodecPreferences; - RTCRtpTransceiver.prototype.setCodecPreferences = function(codecs) { - // Use the same codecs as desktop - const newCodecs = codecs.slice(); - let pos = 0; - newCodecs.forEach((codec, i) => { - // Find high-quality codecs - if (codec.sdpFmtpLine && codec.sdpFmtpLine.includes(profileLevelId)) { - // Move it to the top of the array - newCodecs.splice(i, 1); - newCodecs.splice(pos, 0, codec); - ++pos; - } - }); - - try { - nativeSetCodecPreferences.apply(this, [newCodecs]); - } catch (e) { - // Didn't work -> use default codecs - BxLogger.error('setCodecPreferences', e); - nativeSetCodecPreferences.apply(this, [codecs]); - } - } } export function patchRtcPeerConnection() { @@ -109,18 +82,27 @@ export function patchRtcPeerConnection() { } const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX); - if (maxVideoBitrate > 0) { + const codec = getPref(PrefKey.STREAM_CODEC_PROFILE); + + if (codec !== 'default' || maxVideoBitrate > 0) { const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; RTCPeerConnection.prototype.setLocalDescription = function(description?: RTCLocalSessionDescriptionInit): Promise { + // Set preferred codec profile + if (codec !== 'default') { + arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); + } + // set maximum bitrate try { - if (description) { + if (maxVideoBitrate > 0 && description) { arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); } } catch (e) { BxLogger.error('setLocalDescription', e); } + BxLogger.info('setLocalDescription', arguments[0].sdp); + // @ts-ignore return nativeSetLocalDescription.apply(this, arguments); }; diff --git a/src/utils/network.ts b/src/utils/network.ts index 15c14a1..719571b 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -456,9 +456,6 @@ class XcloudInterceptor { }); } - overrides.videoConfiguration = overrides.videoConfiguration || {}; - overrides.videoConfiguration.setCodecPreferences = true; - // Enable touch controller if (TouchController.isEnabled()) { overrides.inputConfiguration.enableTouchInput = true; diff --git a/src/utils/preferences.ts b/src/utils/preferences.ts index c061a1c..212f65d 100644 --- a/src/utils/preferences.ts +++ b/src/utils/preferences.ts @@ -170,7 +170,7 @@ export class Preferences { default: t('default'), }; - if (!('getCapabilities' in RTCRtpReceiver) || typeof RTCRtpTransceiver === 'undefined' || !('setCodecPreferences' in RTCRtpTransceiver.prototype)) { + if (!('getCapabilities' in RTCRtpReceiver)) { return options; } diff --git a/src/utils/sdp.ts b/src/utils/sdp.ts index a8f9510..3ef13a3 100644 --- a/src/utils/sdp.ts +++ b/src/utils/sdp.ts @@ -1,5 +1,59 @@ +export function setCodecPreferences(sdp: string, preferredCodec: string) { + const h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g; + const profilePrefix = preferredCodec === 'high' ? '4d' : (preferredCodec === 'low' ? '420' : '42e'); + + const preferredCodecIds: string[] = []; + + // Find all H.264 codec profile IDs + const matches = sdp.matchAll(h264Pattern) || []; + for (const match of matches) { + const id = match[1]; + const profileId = match[2]; + + if (profileId.startsWith(profilePrefix)) { + preferredCodecIds.push(id); + } + } + + // No preferred IDs found + if (!preferredCodecIds.length) { + return sdp; + } + + const lines = sdp.split('\r\n'); + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + if (!line.startsWith('m=video')) { + continue; + } + + // https://datatracker.ietf.org/doc/html/rfc4566#section-5.14 + // m= + // m=video 9 UDP/TLS/RTP/SAVPF 127 39 102 104 106 108 + const tmp = line.trim().split(' '); + + // Get array of + // ['127', '39', '102', '104', '106', '108'] + let ids = tmp.slice(3); + + // Remove preferred IDs in the original array + ids = ids.filter(item => !preferredCodecIds.includes(item)); + + // Put preferred IDs at the beginning + ids = preferredCodecIds.concat(ids); + + // Update line's content + lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(' '); + + break; + } + + return lines.join('\r\n'); +} + + export function patchSdpBitrate(sdp: string, video?: number, audio?: number) { - const lines = sdp.split('\n'); + const lines = sdp.split('\r\n'); const mediaSet: Set = new Set(); !!video && mediaSet.add('video'); @@ -57,5 +111,5 @@ export function patchSdpBitrate(sdp: string, video?: number, audio?: number) { } } - return lines.join('\n'); + return lines.join('\r\n'); }