From 1dee720f77b9bdee9654af5da798a0d34f769899 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Sun, 12 May 2024 18:05:21 +0700 Subject: [PATCH] Add "Maximum video bitrate" option --- src/assets/css/number-stepper.styl | 2 +- src/modules/ui/global-settings.ts | 3 ++ src/utils/monkey-patches.ts | 17 +++++++++ src/utils/preferences.ts | 24 ++++++++++++ src/utils/sdp.ts | 61 ++++++++++++++++++++++++++++++ src/utils/settings.ts | 26 ++++++++++--- 6 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/utils/sdp.ts diff --git a/src/assets/css/number-stepper.styl b/src/assets/css/number-stepper.styl index 2d6b5ce..c1c154a 100644 --- a/src/assets/css/number-stepper.styl +++ b/src/assets/css/number-stepper.styl @@ -3,7 +3,7 @@ span { display: inline-block; - width: 40px; + min-width: 40px; font-family: var(--bx-monospaced-font); font-size: 14px; } diff --git a/src/modules/ui/global-settings.ts b/src/modules/ui/global-settings.ts index 353e55e..81b78e0 100644 --- a/src/modules/ui/global-settings.ts +++ b/src/modules/ui/global-settings.ts @@ -27,6 +27,9 @@ const SETTINGS_UI = { items: [ PrefKey.STREAM_TARGET_RESOLUTION, PrefKey.STREAM_CODEC_PROFILE, + + PrefKey.BITRATE_VIDEO_MAX, + PrefKey.AUDIO_ENABLE_VOLUME_CONTROL, PrefKey.AUDIO_MIC_ON_PLAYING, PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG, diff --git a/src/utils/monkey-patches.ts b/src/utils/monkey-patches.ts index 32d4290..11bf256 100644 --- a/src/utils/monkey-patches.ts +++ b/src/utils/monkey-patches.ts @@ -2,6 +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"; export function patchVideoApi() { const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO); @@ -96,6 +97,22 @@ export function patchRtcPeerConnection() { return dataChannel; } + const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; + RTCPeerConnection.prototype.setLocalDescription = function(description?: RTCLocalSessionDescriptionInit): Promise { + // set maximum bitrate + try { + const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX); + if (maxVideoBitrate > 0) { + arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, maxVideoBitrate * 1000); + } + } catch (e) { + BxLogger.error('setLocalDescription', e); + } + + // @ts-ignore + return nativeSetLocalDescription.apply(this, arguments); + }; + const OrgRTCPeerConnection = window.RTCPeerConnection; // @ts-ignore window.RTCPeerConnection = function() { diff --git a/src/utils/preferences.ts b/src/utils/preferences.ts index 662754f..5207561 100644 --- a/src/utils/preferences.ts +++ b/src/utils/preferences.ts @@ -32,6 +32,8 @@ export enum PrefKey { STREAM_DISABLE_FEEDBACK_DIALOG = 'stream_disable_feedback_dialog', + BITRATE_VIDEO_MAX = 'bitrate_video_max', + GAME_BAR_POSITION = 'game_bar_position', LOCAL_CO_OP_ENABLED = 'local_co_op_enabled', @@ -314,6 +316,28 @@ export class Preferences { default: false, }, + [PrefKey.BITRATE_VIDEO_MAX]: { + type: SettingElementType.NUMBER_STEPPER, + label: 'Maximum video bitrate', + default: 0, + min: 0, + max: 15, + steps: 1, + params: { + suffix: ' Mb/s', + exactTicks: 5, + customTextValue: (value: any) => { + value = parseInt(value); + + if (value === 0) { + return t('default'); + } + + return null; + }, + }, + }, + [PrefKey.GAME_BAR_POSITION]: { label: t('position'), default: 'bottom-left', diff --git a/src/utils/sdp.ts b/src/utils/sdp.ts new file mode 100644 index 0000000..a8f9510 --- /dev/null +++ b/src/utils/sdp.ts @@ -0,0 +1,61 @@ +export function patchSdpBitrate(sdp: string, video?: number, audio?: number) { + const lines = sdp.split('\n'); + + const mediaSet: Set = new Set(); + !!video && mediaSet.add('video'); + !!audio && mediaSet.add('audio'); + + const bitrate = { + video, + audio, + }; + + for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) { + let media: string = ''; + + let line = lines[lineNumber]; + if (!line.startsWith('m=')) { + continue; + } + + for (const m of mediaSet) { + if (line.startsWith(`m=${m}`)) { + media = m; + // Remove matched media from set + mediaSet.delete(media); + break; + } + } + + // Invalid media, continue looking + if (!media) { + continue; + } + + const bLine = `b=AS:${bitrate[media as keyof typeof bitrate]}`; + + while (lineNumber++, lineNumber < lines.length) { + line = lines[lineNumber]; + // Ignore lines that start with "i=" or "c=" + if (line.startsWith('i=') || line.startsWith('c=')) { + continue; + } + + if (line.startsWith('b=AS:')) { + // Replace bitrate + lines[lineNumber] = bLine; + // Stop lookine for "b=AS:" line + break; + } + + if (line.startsWith('m=')) { + // "b=AS:" line not found, add "b" line before "m=" + lines.splice(lineNumber, 0, bLine); + // Stop + break; + } + } + } + + return lines.join('\n'); +} diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 06a90bd..6d9e578 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -12,6 +12,8 @@ type NumberStepperParams = { ticks?: number; exactTicks?: number; + + customTextValue?: (value: any) => string | null; } export enum SettingElementType { @@ -131,9 +133,24 @@ export class SettingElement { const MAX = setting.max!; const STEPS = Math.max(setting.steps || 1, 1); + const renderTextValue = (value: any) => { + value = parseInt(value as string); + + let textContent = null; + if (options.customTextValue) { + textContent = options.customTextValue(value); + } + + if (textContent === null) { + textContent = value.toString() + options.suffix; + } + + return textContent; + }; + const $wrapper = CE('div', {'class': 'bx-number-stepper'}, $decBtn = CE('button', {'data-type': 'dec'}, '-') as HTMLButtonElement, - $text = CE('span', {}, value + options.suffix) as HTMLSpanElement, + $text = CE('span', {}, renderTextValue(value)) as HTMLSpanElement, $incBtn = CE('button', {'data-type': 'inc'}, '+') as HTMLButtonElement, ); @@ -141,8 +158,7 @@ export class SettingElement { $range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value, 'step': STEPS}) as HTMLInputElement; $range.addEventListener('input', e => { value = parseInt((e.target as HTMLInputElement).value); - - $text.textContent = value + options.suffix; + $text.textContent = renderTextValue(value); onChange && onChange(e, value); }); $wrapper.appendChild($range); @@ -204,7 +220,7 @@ export class SettingElement { value = Math.min(MAX, value + STEPS); } - $text.textContent = value.toString() + options.suffix; + $text.textContent = renderTextValue(value); $range && ($range.value = value.toString()); isHolding = false; @@ -237,7 +253,7 @@ export class SettingElement { // Custom method ($wrapper as any).setValue = (value: any) => { - $text.textContent = value + options.suffix; + $text.textContent = renderTextValue(value); $range && ($range.value = value); };