Sloppy allowing the user to controll it's max bandwidth used

master
WolverinDEV 2021-01-04 21:28:47 +01:00
parent b162668090
commit 2a120987cf
11 changed files with 290 additions and 89 deletions

View File

@ -77,7 +77,7 @@ export type LocalVideoBroadcastState = {
state: "broadcasting"
}
export interface BroadcastConstraints {
export interface VideoBroadcastConfig {
/**
* Ideal and max video width
*/
@ -94,9 +94,16 @@ export interface BroadcastConstraints {
dynamicQuality: boolean,
/**
* Max bandwidth which should be used (in bits/second)
* Max bandwidth which should be used (in bits/second).
* `0` indicates no bandwidth limit.
*/
maxBandwidth: number,
maxBandwidth: number | 0,
/**
* Interval of enforcing keyframes.
* Zero means that no keyframes will be enforced.
*/
keyframeInterval: number | 0,
/**
* Maximal frame rate for the video.
@ -124,16 +131,14 @@ export interface LocalVideoBroadcast {
* @param source The source of the broadcast (No ownership will be taken. The voice connection must ref the source by itself!)
* @param constraints
*/
startBroadcasting(source: VideoSource, constraints: BroadcastConstraints) : Promise<void>;
startBroadcasting(source: VideoSource, constraints: VideoBroadcastConfig) : Promise<void>;
/**
* @param source The source of the broadcast (No ownership will be taken. The voice connection must ref the source by itself!)
* @param constraints
*/
changeSource(source: VideoSource, constraints: BroadcastConstraints) : Promise<void>;
getConstraints() : BroadcastConstraints | undefined;
applyConstraints(constraints: BroadcastConstraints) : Promise<void>;
changeSource(source: VideoSource, constraints: VideoBroadcastConfig) : Promise<void>;
getConstraints() : VideoBroadcastConfig | undefined;
stopBroadcasting();
}

View File

@ -11,6 +11,7 @@ import {SdpCompressor, SdpProcessor} from "./SdpUtils";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
import {WhisperTarget} from "tc-shared/voice/VoiceWhisper";
import {globalAudioContext} from "tc-backend/audio/player";
import {VideoBroadcastConfig, VideoBroadcastType} from "tc-shared/connection/VideoConnection";
const kSdpCompressionMode = 1;
@ -665,37 +666,90 @@ export class RTCConnection {
return oldTrack;
}
/**
* @param type
* @throws a string on error
*/
public async startTrackBroadcast(type: RTCBroadcastableTrackType) : Promise<void> {
if(typeof this.currentTransceiver[type] !== "object") {
throw tr("missing transceiver");
}
public async startVideoBroadcast(type: VideoBroadcastType, config: VideoBroadcastConfig) {
let track: RTCBroadcastableTrackType;
let broadcastType: number;
switch (type) {
case "audio":
if(!this.audioSupport) {
throw tr("audio support isn't enabled");
}
case "camera":
broadcastType = 0;
track = "video";
break;
case "video":
case "video-screen":
case "screen":
broadcastType = 1;
track = "video-screen";
break;
default:
throw tr("invalid broadcast type");
throw tr("invalid video broadcast type");
}
let payload = {};
payload["broadcast_keyframe_interval"] = config.keyframeInterval;
payload["broadcast_bitrate_max"] = config.maxBandwidth;
payload["ssrc"] = this.sdpProcessor.getLocalSsrcFromFromMediaId(this.currentTransceiver[track].mid);
payload["type"] = broadcastType;
try {
await this.connection.send_command("rtcbroadcast", {
type: broadcastableTrackTypeToNumber(type),
ssrc: this.sdpProcessor.getLocalSsrcFromFromMediaId(this.currentTransceiver[type].mid)
await this.connection.send_command("broadcastvideo", payload);
} catch (error) {
if(error instanceof CommandResult) {
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
throw tr("failed on permission") + " " + this.connection.client.permissions.getFailedPermission(error);
}
error = error.formattedMessage();
}
logError(LogCategory.WEBRTC, tr("failed to start %s broadcast: %o"), type, error);
throw tr("failed to signal broadcast start");
}
}
public async changeVideoBroadcastConfig(type: VideoBroadcastType, config: VideoBroadcastConfig) {
let track: RTCBroadcastableTrackType;
let broadcastType: number;
switch (type) {
case "camera":
broadcastType = 0;
track = "video";
break;
case "screen":
broadcastType = 1;
track = "video-screen";
break;
default:
throw tr("invalid video broadcast type");
}
let payload = {};
payload["broadcast_keyframe_interval"] = config.keyframeInterval;
payload["broadcast_bitrate_max"] = config.maxBandwidth;
payload["bt"] = broadcastType;
try {
await this.connection.send_command("broadcastvideoconfigure", payload);
} catch (error) {
if(error instanceof CommandResult) {
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
throw tr("failed on permission") + " " + this.connection.client.permissions.getFailedPermission(error);
}
error = error.formattedMessage();
}
logError(LogCategory.WEBRTC, tr("failed to update %s broadcast: %o"), type, error);
throw tr("failed to update broadcast config");
}
}
public async startAudioBroadcast() {
try {
await this.connection.send_command("broadcastaudio", {
ssrc: this.sdpProcessor.getLocalSsrcFromFromMediaId(this.currentTransceiver["audio"].mid)
});
} catch (error) {
logError(LogCategory.WEBRTC, tr("failed to start %s broadcast: %o"), type, error);
logError(LogCategory.WEBRTC, tr("failed to start %s broadcast: %o"), "audio", error);
throw tr("failed to signal broadcast start");
}
}
@ -727,10 +781,30 @@ export class RTCConnection {
}
public stopTrackBroadcast(type: RTCBroadcastableTrackType) {
this.connection.send_command("rtcbroadcast", {
type: broadcastableTrackTypeToNumber(type),
ssrc: 0
}).catch(error => {
let promise: Promise<any>;
switch (type) {
case "audio":
promise = this.connection.send_command("broadcastaudio", {
ssrc: 0
});
break;
case "video-screen":
promise = this.connection.send_command("broadcastvideo", {
type: 1,
ssrc: 0
});
break;
case "video":
promise = this.connection.send_command("broadcastvideo", {
type: 0,
ssrc: 0
});
break;
}
promise.catch(error => {
logWarn(LogCategory.WEBRTC, tr("Failed to signal track broadcast stop: %o"), error);
});
}

View File

@ -1,5 +1,5 @@
import {
BroadcastConstraints,
VideoBroadcastConfig,
LocalVideoBroadcast,
LocalVideoBroadcastEvents,
LocalVideoBroadcastState,
@ -28,7 +28,8 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
private state: LocalVideoBroadcastState;
private currentSource: VideoSource;
private currentConstrints: BroadcastConstraints;
private currentConfig: VideoBroadcastConfig;
private signaledConfig: VideoBroadcastConfig | undefined;
private broadcastStartId: number;
private localStartPromise: Promise<void>;
@ -72,7 +73,7 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
return Promise.resolve(undefined);
}
async changeSource(source: VideoSource, constraints: BroadcastConstraints): Promise<void> {
async changeSource(source: VideoSource, constraints: VideoBroadcastConfig): Promise<void> {
let sourceRef = source.ref();
try {
if(this.currentSource !== source) {
@ -91,7 +92,7 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
}
/* Apply the constraints to the current source */
await this.doApplyConstraints(constraints, source);
await this.doApplyLocalConstraints(constraints, source);
const startId = ++this.broadcastStartId;
let rtcBroadcastType: RTCBroadcastableTrackType = this.type === "camera" ? "video" : "video-screen";
@ -108,7 +109,7 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
}
this.setCurrentSource(sourceRef);
} else if(!_.isEqual(this.currentConstrints, constraints)) {
} else if(!_.isEqual(this.currentConfig, constraints)) {
console.error("Constraints changed");
await this.applyConstraints(constraints);
}
@ -120,12 +121,12 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
private setCurrentSource(source: VideoSource | undefined) {
if(this.currentSource) {
this.currentSource.deref();
this.currentConstrints = undefined;
this.currentConfig = undefined;
}
this.currentSource = source?.ref();
}
async startBroadcasting(source: VideoSource, constraints: BroadcastConstraints): Promise<void> {
async startBroadcasting(source: VideoSource, constraints: VideoBroadcastConfig): Promise<void> {
const sourceRef = source.ref();
while(this.localStartPromise) {
await this.localStartPromise;
@ -141,7 +142,7 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
}
}
private async doStartBroadcast(source: VideoSource, constraints: BroadcastConstraints) {
private async doStartBroadcast(source: VideoSource, constraints: VideoBroadcastConfig) {
const videoTracks = source.getStream().getVideoTracks();
if(videoTracks.length === 0) {
throw tr("missing video stream track");
@ -157,7 +158,7 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
}
try {
await this.applyConstraints(constraints);
await this.doApplyLocalConstraints(constraints, this.currentSource);
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
@ -194,7 +195,7 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
}
try {
await this.handle.getRTCConnection().startTrackBroadcast(rtcBroadcastType);
await this.handle.getRTCConnection().startVideoBroadcast(this.type, this.currentConfig);
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
@ -210,14 +211,27 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
return;
}
this.signaledConfig = Object.assign({}, this.currentConfig);
this.setState({ state: "broadcasting" });
}
async applyConstraints(constraints: BroadcastConstraints): Promise<void> {
await this.doApplyConstraints(constraints, this.currentSource);
async applyConstraints(constraints: VideoBroadcastConfig): Promise<void> {
await this.doApplyLocalConstraints(constraints, this.currentSource);
if(this.signaledConfig?.keyframeInterval !== constraints.keyframeInterval ||
this.signaledConfig?.maxBandwidth !== constraints.maxBandwidth
) {
try {
await this.handle.getRTCConnection().changeVideoBroadcastConfig(this.type, constraints);
this.signaledConfig = Object.assign({}, constraints);
} catch (error) {
/* Really rethrow it? */
throw error;
}
}
}
private async doApplyConstraints(constraints: BroadcastConstraints, source: VideoSource): Promise<void> {
private async doApplyLocalConstraints(constraints: VideoBroadcastConfig, source: VideoSource): Promise<void> {
const capabilities = source.getCapabilities();
const videoConstraints: MediaTrackConstraints = {};
@ -249,9 +263,7 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
}
await source.getStream().getVideoTracks()[0]?.applyConstraints(constraints);
this.currentConstrints = constraints;
/* TODO: Bandwidth update? */
this.currentConfig = constraints;
}
stopBroadcasting(skipRtcStop?: boolean, stopState?: LocalVideoBroadcastState) {
@ -296,11 +308,10 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
}
this.setState({ state: "initializing" });
let rtcBroadcastType: RTCBroadcastableTrackType = this.type === "camera" ? "video" : "video-screen";
const startId = ++this.broadcastStartId;
try {
await this.handle.getRTCConnection().startTrackBroadcast(rtcBroadcastType);
await this.handle.getRTCConnection().startVideoBroadcast(this.type, this.currentConfig);
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
@ -313,8 +324,8 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
})();
}
getConstraints(): BroadcastConstraints | undefined {
return this.currentConstrints;
getConstraints(): VideoBroadcastConfig | undefined {
return this.currentConfig;
}
}

View File

@ -193,14 +193,14 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
}
spawnVideoSourceSelectModal(event.broadcastType, event.quickSelect ? { mode: "select-quick", defaultDevice: event.defaultDevice } : { mode: "select-default", defaultDevice: event.defaultDevice })
.then(async ({ source, constraints }) => {
.then(async ({ source, config }) => {
if(!source) { return; }
try {
const broadcast = connection.getServerConnection().getVideoConnection().getLocalBroadcast(event.broadcastType);
if(broadcast.getState().state === "initializing" || broadcast.getState().state === "broadcasting") {
console.error("Change source");
broadcast.changeSource(source, constraints).catch(error => {
broadcast.changeSource(source, config).catch(error => {
logError(LogCategory.VIDEO, tr("Failed to change broadcast source: %o"), event.broadcastType, error);
if(typeof error !== "string") {
error = tr("lookup the console for detail");
@ -214,7 +214,7 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
});
} else {
console.error("Start broadcast");
broadcast.startBroadcasting(source, constraints).catch(error => {
broadcast.startBroadcasting(source, config).catch(error => {
logError(LogCategory.VIDEO, tr("Failed to start %s broadcasting: %o"), event.broadcastType, error);
if(typeof error !== "string") {
error = tr("lookup the console for detail");
@ -252,7 +252,7 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
}
spawnVideoSourceSelectModal(event.broadcastType, { mode: "edit", source: broadcast.getSource(), broadcastConstraints: Object.assign({}, broadcast.getConstraints()) })
.then(async ({ source, constraints }) => {
.then(async ({ source, config }) => {
if (!source) {
return;
}
@ -262,7 +262,7 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
return;
}
await broadcast.changeSource(source, constraints);
await broadcast.changeSource(source, config);
}).catch(error => {
logWarn(LogCategory.VIDEO, tr("Failed to edit video broadcast: %o"), error);
createErrorModal(tr("Broadcast update failed"), tr("We failed to update the current video broadcast settings.\nThe old settings will be used.")).open();

View File

@ -555,6 +555,20 @@ export class Settings extends StaticSettings {
valueType: "number",
};
static readonly KEY_VIDEO_DYNAMIC_QUALITY: ValuedSettingsKey<boolean> = {
key: 'video_dynamic_quality',
defaultValue: true,
description: "Dynamically decrease video quality in order to archive a higher framerate.",
valueType: "boolean",
};
static readonly KEY_VIDEO_DYNAMIC_FRAME_RATE: ValuedSettingsKey<boolean> = {
key: 'video_dynamic_frame_rate',
defaultValue: true,
description: "Dynamically decrease video framerate to allow higher video resolutions.",
valueType: "boolean",
};
static readonly FN_LOG_ENABLED: (category: string) => SettingsKey<boolean> = category => {
return {
key: "log." + category.toLowerCase() + ".enabled",

View File

@ -204,12 +204,10 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
createServerModal(this, properties => {
log.info(LogCategory.SERVER, tr("Changing server properties %o"), properties);
console.log(tr("Changed properties: %o"), properties);
if (properties) {
if(Object.keys(properties)) {
return this.channelTree.client.serverConnection.send_command("serveredit", properties).then(() => {
this.channelTree.client.sound.play(Sound.SERVER_EDITED_SELF);
});
}
if (Object.keys(properties || {}).length > 0) {
return this.channelTree.client.serverConnection.send_command("serveredit", properties).then(() => {
this.channelTree.client.sound.play(Sound.SERVER_EDITED_SELF);
});
}
return Promise.resolve();
});

View File

@ -4,7 +4,7 @@ import {ModalVideoSourceEvents} from "tc-shared/ui/modal/video-source/Definition
import {ModalVideoSource} from "tc-shared/ui/modal/video-source/Renderer";
import {getVideoDriver, VideoPermissionStatus, VideoSource} from "tc-shared/video/VideoSource";
import {LogCategory, logError, logWarn} from "tc-shared/log";
import {BroadcastConstraints, VideoBroadcastType} from "tc-shared/connection/VideoConnection";
import {VideoBroadcastConfig, VideoBroadcastType} from "tc-shared/connection/VideoConnection";
import {Settings, settings} from "tc-shared/settings";
import {tr} from "tc-shared/i18n/localize";
@ -19,14 +19,14 @@ export type VideoSourceModalAction = {
} | {
mode: "edit",
source: VideoSource,
broadcastConstraints: BroadcastConstraints
broadcastConstraints: VideoBroadcastConfig
};
/**
* @param type The video type which should be prompted
* @param mode
*/
export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, mode: VideoSourceModalAction) : Promise<{ source: VideoSource | undefined, constraints: BroadcastConstraints | undefined }> {
export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, mode: VideoSourceModalAction) : Promise<{ source: VideoSource | undefined, config: VideoBroadcastConfig | undefined }> {
const controller = new VideoSourceController(type);
let defaultSelectDevice: string | true;
@ -41,7 +41,7 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, mode
controller.destroy();
return {
source: resultSource,
constraints: resultConstraints
config: resultConstraints
};
} else {
/* Select failed. We'll open the modal and show the error. */
@ -91,11 +91,11 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, mode
controller.destroy();
return {
source: resultSource,
constraints: resultConstraints
config: resultConstraints
};
}
function updateBroadcastConstraintsFromSource(source: VideoSource, constraints: BroadcastConstraints) {
function updateBroadcastConfigFromSource(source: VideoSource, constraints: VideoBroadcastConfig) {
const videoTrack = source.getStream().getVideoTracks()[0];
const trackSettings = videoTrack.getSettings();
@ -104,7 +104,7 @@ function updateBroadcastConstraintsFromSource(source: VideoSource, constraints:
constraints.maxFrameRate = trackSettings.frameRate;
}
async function generateAndApplyDefaultConstraints(source: VideoSource) : Promise<BroadcastConstraints> {
async function generateAndApplyDefaultConfig(source: VideoSource) : Promise<VideoBroadcastConfig> {
const videoTrack = source.getStream().getVideoTracks()[0];
let maxHeight = settings.static_global(Settings.KEY_VIDEO_DEFAULT_MAX_HEIGHT);
@ -116,7 +116,12 @@ async function generateAndApplyDefaultConstraints(source: VideoSource) : Promise
maxHeight = Math.min(maxHeight, capabilities.maxHeight);
maxWidth = Math.min(maxWidth, capabilities.maxWidth);
const broadcastConstraints: BroadcastConstraints = {} as any;
/* FIXME: Get these values somewhere else! */
const broadcastConstraints: VideoBroadcastConfig = {
maxBandwidth: 1_600_000,
keyframeInterval: 0
} as VideoBroadcastConfig;
{
let ratio = 1;
@ -137,23 +142,22 @@ async function generateAndApplyDefaultConstraints(source: VideoSource) : Promise
}
}
broadcastConstraints.dynamicQuality = true;
broadcastConstraints.dynamicFrameRate = true;
broadcastConstraints.maxBandwidth = 10_000_000;
broadcastConstraints.dynamicQuality = settings.static_global(Settings.KEY_VIDEO_DYNAMIC_QUALITY);
broadcastConstraints.dynamicFrameRate = settings.static_global(Settings.KEY_VIDEO_DYNAMIC_FRAME_RATE);
try {
await applyBroadcastConstraints(source, broadcastConstraints);
await applyBroadcastConfig(source, broadcastConstraints);
} catch (error) {
logWarn(LogCategory.VIDEO, tr("Failed to apply initial default broadcast constraints: %o"), error);
logWarn(LogCategory.VIDEO, tr("Failed to apply initial default broadcast config: %o"), error);
}
updateBroadcastConstraintsFromSource(source, broadcastConstraints);
updateBroadcastConfigFromSource(source, broadcastConstraints);
return broadcastConstraints;
}
/* May throws an overconstraint error */
async function applyBroadcastConstraints(source: VideoSource, constraints: BroadcastConstraints) {
async function applyBroadcastConfig(source: VideoSource, constraints: VideoBroadcastConfig) {
const videoTrack = source.getStream().getVideoTracks()[0];
if(!videoTrack) { return; }
@ -183,7 +187,7 @@ class VideoSourceController {
private readonly type: VideoBroadcastType;
private currentSource: VideoSource | string;
private currentConstraints: BroadcastConstraints;
private currentConstraints: VideoBroadcastConfig;
/* preselected current source id */
private currentSourceId: string;
@ -204,6 +208,8 @@ class VideoSourceController {
this.events.on("query_start_button", () => this.notifyStartButton());
this.events.on("query_setting_dimension", () => this.notifySettingDimension());
this.events.on("query_setting_framerate", () => this.notifySettingFramerate());
this.events.on("query_setting_bitrate_max", () => this.notifySettingBitrate());
this.events.on("query_setting_keyframe_sender", () => this.notifySettingKeyframeInterval());
this.events.on("action_request_permissions", () => {
getVideoDriver().requestPermissions().then(result => {
@ -289,6 +295,14 @@ class VideoSourceController {
this.events.on("action_setting_framerate", event => {
this.currentConstraints.maxFrameRate = event.frameRate;
});
this.events.on("action_setting_bitrate_max", event => {
this.currentConstraints.maxBandwidth = event.bitrate;
});
this.events.on("action_setting_keyframe_sender", event => {
this.currentConstraints.keyframeInterval = event.interval;
});
}
destroy() {
@ -310,7 +324,7 @@ class VideoSourceController {
if(this.currentConstraints) {
try {
/* TODO: Automatically scale down resolution if new one isn't capable of supplying our current resolution */
await applyBroadcastConstraints(source, this.currentConstraints);
await applyBroadcastConfig(source, this.currentConstraints);
} catch (error) {
logWarn(LogCategory.VIDEO, tr("Failed to apply broadcast constraints to new source: %o"), error);
this.currentConstraints = undefined;
@ -318,7 +332,7 @@ class VideoSourceController {
}
if(!this.currentConstraints) {
this.currentConstraints = await generateAndApplyDefaultConstraints(source);
this.currentConstraints = await generateAndApplyDefaultConfig(source);
}
}
@ -328,9 +342,11 @@ class VideoSourceController {
this.notifyCurrentSource();
this.notifySettingDimension();
this.notifySettingFramerate();
this.notifySettingBitrate();
this.notifySettingKeyframeInterval();
}
async useSettings(source: VideoSource, constraints: BroadcastConstraints) {
async useSettings(source: VideoSource, constraints: VideoBroadcastConfig) {
if(typeof this.currentSource === "object") {
this.currentSource.deref();
}
@ -342,6 +358,8 @@ class VideoSourceController {
this.notifyCurrentSource();
this.notifySettingDimension();
this.notifySettingFramerate();
this.notifySettingBitrate();
this.notifySettingKeyframeInterval();
}
async selectSource(sourceId: string) : Promise<boolean> {
@ -387,7 +405,7 @@ class VideoSourceController {
return typeof this.currentSource === "object" ? this.currentSource : undefined;
}
getBroadcastConstraints() : BroadcastConstraints {
getBroadcastConstraints() : VideoBroadcastConfig {
return this.currentConstraints;
}
@ -528,4 +546,23 @@ class VideoSourceController {
this.events.fire_react("notify_settings_framerate", { frameRate: undefined });
}
};
private notifySettingBitrate() {
if(this.currentConstraints) {
this.events.fire_react("notify_setting_bitrate_max", {
bitrate: {
allowedBitrate: 0,
bitrate: this.currentConstraints.maxBandwidth
}
});
} else {
this.events.fire_react("notify_setting_bitrate_max", undefined);
}
}
private notifySettingKeyframeInterval() {
this.events.fire_react("notify_settings_keyframe_sender", {
interval: this.currentConstraints?.keyframeInterval || 0
});
}
}

View File

@ -51,6 +51,11 @@ export type SettingFrameRate = {
current: number
};
export type SettingBitrate = {
allowedBitrate: number | -1
bitrate: number | 0,
};
export interface ModalVideoSourceEvents {
action_cancel: {},
action_start: {},
@ -58,6 +63,8 @@ export interface ModalVideoSourceEvents {
action_select_source: { id: string | undefined },
action_setting_dimension: { width: number, height: number },
action_setting_framerate: { frameRate: number },
action_setting_bitrate_max: { bitrate: number | 0 },
action_setting_keyframe_sender: { interval: number | 0 },
action_toggle_screen_capture_device_select: { shown: boolean },
action_preselect_screen_capture_device: { deviceId: string },
@ -67,7 +74,9 @@ export interface ModalVideoSourceEvents {
query_start_button: {},
query_setting_dimension: {},
query_setting_framerate: {},
query_screen_capture_devices: { }
query_setting_bitrate_max: {},
query_setting_keyframe_sender: {},
query_screen_capture_devices: {}
notify_source: { state: VideoSourceState }
notify_device_list: { status: DeviceListResult },
@ -91,7 +100,13 @@ export interface ModalVideoSourceEvents {
},
notify_screen_capture_devices: {
devices: ScreenCaptureDeviceList
}
},
notify_setting_bitrate_max: {
bitrate: SettingBitrate | undefined
},
notify_settings_keyframe_sender: {
interval: number | 0
},
notify_destroy: {}
}

View File

@ -1,6 +1,8 @@
@import "../../../../css/static/mixin";
@import "../../../../css/static/properties";
//#96903a
.container {
display: flex;
flex-direction: column;

View File

@ -2,12 +2,12 @@ import {Registry} from "tc-shared/events";
import * as React from "react";
import {
DeviceListResult,
ModalVideoSourceEvents, ScreenCaptureDeviceList, SettingFrameRate,
ModalVideoSourceEvents, ScreenCaptureDeviceList, SettingBitrate, SettingFrameRate,
VideoPreviewStatus, VideoSourceState
} from "tc-shared/ui/modal/video-source/Definitions";
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {Select} from "tc-shared/ui/react-elements/InputField";
import {BoxedInputField, Select} from "tc-shared/ui/react-elements/InputField";
import {Button} from "tc-shared/ui/react-elements/Button";
import {useContext, useEffect, useRef, useState} from "react";
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
@ -16,6 +16,7 @@ import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {ScreenCaptureDevice} from "tc-shared/video/VideoSource";
import {useTr} from "tc-shared/ui/react-elements/Helper";
const cssStyle = require("./Renderer.scss");
const ModalEvents = React.createContext<Registry<ModalVideoSourceEvents>>(undefined);
@ -540,6 +541,49 @@ const SettingFramerate = () => {
);
}
const SettingBps = () => {
const events = useContext(ModalEvents);
const [ bitrate, setBitrate ] = useState<SettingBitrate | undefined>(() => {
events.fire("query_setting_bitrate_max");
return undefined;
});
events.reactUse("notify_setting_bitrate_max", event => {
setBitrate(event.bitrate);
setCurrentValue(undefined);
});
const [ currentValue, setCurrentValue ] = useState<string>(undefined);
const advanced = useContext(AdvancedSettings);
if(!advanced) {
return null;
}
return (
<div className={cssStyle.setting + " " + cssStyle.dimensions}>
<div className={cssStyle.title}>
<div><Translatable>Bitrate</Translatable></div>
<div>{bitrate ? (bitrate.bitrate / 1000).toFixed() + " kbps" : ""}</div>
</div>
<div className={cssStyle.body}>
<BoxedInputField
value={bitrate ? typeof currentValue === "string" ? currentValue : (bitrate.bitrate / 1000).toFixed(0) : " "}
placeholder={tr("loading")}
onChange={value => {
const numValue = (parseInt(value) * 1000) || 0;
bitrate.bitrate = numValue;
events.fire("action_setting_bitrate_max", { bitrate: numValue });
setCurrentValue(undefined);
}}
onInput={value => setCurrentValue(value)}
type={"number"}
/>
</div>
</div>
);
}
const calculateBps = (width: number, height: number, frameRate: number) => {
/* Based on the tables showed here: http://www.lighterra.com/papers/videoencodingh264/ */
const estimatedBitsPerPixed = 3.9;
@ -609,6 +653,7 @@ const Settings = React.memo(() => {
<div className={cssStyle.sectionBody}>
<SettingDimension />
<SettingFramerate />
<SettingBps />
<BpsInfo />
</div>
</div>

View File

@ -73,7 +73,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
const localClientId = this.rtcConnection.getConnection().client.getClientId();
for(const data of event.arguments) {
if(parseInt(data["clid"]) === localClientId) {
this.rtcConnection.startTrackBroadcast("audio").catch(error => {
this.rtcConnection.startAudioBroadcast().catch(error => {
logError(LogCategory.VOICE, tr("Failed to start voice audio broadcasting after channel switch: %o"), error);
this.localFailedReason = tr("Failed to start audio broadcasting");
this.setConnectionState(VoiceConnectionStatus.Failed);
@ -422,7 +422,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
private handleRtcConnectionStateChanged(event: RTCConnectionEvents["notify_state_changed"]) {
switch (event.newState) {
case RTPConnectionState.CONNECTED:
this.rtcConnection.startTrackBroadcast("audio").then(() => {
this.rtcConnection.startAudioBroadcast().then(() => {
logTrace(LogCategory.VOICE, tr("Local audio broadcasting has been started successfully"));
this.setConnectionState(VoiceConnectionStatus.Connected);
}).catch(error => {