Add "Maximum video bitrate" option

This commit is contained in:
redphx 2024-05-12 18:05:21 +07:00
parent c1b41663db
commit 1dee720f77
6 changed files with 127 additions and 6 deletions

View File

@ -3,7 +3,7 @@
span {
display: inline-block;
width: 40px;
min-width: 40px;
font-family: var(--bx-monospaced-font);
font-size: 14px;
}

View File

@ -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,

View File

@ -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<void> {
// 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() {

View File

@ -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',

61
src/utils/sdp.ts Normal file
View File

@ -0,0 +1,61 @@
export function patchSdpBitrate(sdp: string, video?: number, audio?: number) {
const lines = sdp.split('\n');
const mediaSet: Set<string> = 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');
}

View File

@ -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);
};