diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index b8882c0e..e5fbad2f 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -38,6 +38,7 @@ import {ChannelTree} from "./tree/ChannelTree"; import {LocalClientEntry} from "./tree/Client"; import {ServerAddress} from "./tree/Server"; import {ChannelVideoFrame} from "tc-shared/ui/frames/video/Controller"; +import {global_client_actions} from "tc-shared/events/GlobalEvents"; export enum InputHardwareState { MISSING, @@ -392,18 +393,13 @@ export class ConnectionHandler { control_bar.apply_server_voice_state(); */ - /* - this.serverConnection.getVoiceConnection().startWhisper({ target: "echo" }).catch(error => { - logError(LogCategory.CLIENT, tr("Failed to start local echo: %o"), error); - }); - */ - if(__build.target === "web") { + if(__build.target === "web" && settings.static_global(Settings.KEY_VOICE_ECHO_TEST_ENABLED)) { this.serverFeatures.awaitFeatures().then(result => { if(!result) { return; } if(this.serverFeatures.supportsFeature(ServerFeature.WHISPER_ECHO)) { - spawnEchoTestModal(this); + global_client_actions.fire("action_open_window", { window: "server-echo-test", connection: this }); } }); } diff --git a/shared/js/events/ClientGlobalControlHandler.ts b/shared/js/events/ClientGlobalControlHandler.ts index fca41d03..b64fdd16 100644 --- a/shared/js/events/ClientGlobalControlHandler.ts +++ b/shared/js/events/ClientGlobalControlHandler.ts @@ -19,6 +19,7 @@ import {spawnAbout} from "tc-shared/ui/modal/ModalAbout"; import {spawnVideoSourceSelectModal} from "tc-shared/ui/modal/video-source/Controller"; import {LogCategory, logError} from "tc-shared/log"; import {getVideoDriver} from "tc-shared/video/VideoSource"; +import {spawnEchoTestModal} from "tc-shared/ui/modal/echo-test/Controller"; /* function initialize_sounds(event_registry: Registry) { @@ -158,6 +159,13 @@ export function initialize(event_registry: Registry) spawnAbout(); break; + case "server-echo-test": + const connection = event.connection || server_connections.active_connection(); + if(connection) { + spawnEchoTestModal(connection); + } + break; + default: console.warn(tr("Received open window event for an unknown window: %s"), event.window); } diff --git a/shared/js/events/GlobalEvents.ts b/shared/js/events/GlobalEvents.ts index 403595eb..6bd9a675 100644 --- a/shared/js/events/GlobalEvents.ts +++ b/shared/js/events/GlobalEvents.ts @@ -17,7 +17,8 @@ export interface ClientGlobalControlEvents { "ban-list" | "permissions" | "token-list" | - "token-use", + "token-use" | + "server-echo-test", connection?: ConnectionHandler }, diff --git a/web/app/rtc/Connection.ts b/web/app/rtc/Connection.ts index 52677341..6e64d94f 100644 --- a/web/app/rtc/Connection.ts +++ b/web/app/rtc/Connection.ts @@ -217,7 +217,7 @@ class CommandHandler extends AbstractCommandHandler { }).then(() => this.handle["peer"].createAnswer()) .then(async answer => { if(RTCConnection.kEnableSdpTrace) { - logTrace(LogCategory.WEBRTC, tr("Applying local description from remote %s:\n%s"), data.mode, answer.sdp); + logTrace(LogCategory.WEBRTC, tr("Generated local answer due to remote %s:\n%s"), data.mode, answer.sdp); } answer.sdp = this.sdpProcessor.processOutgoingSdp(answer.sdp, "answer"); @@ -782,6 +782,10 @@ export class RTCConnection { private async updateTracks() { for(const type of kRtcSourceTrackTypes) { + if(!this.currentTransceiver[type]?.sender) { + continue; + } + let fallback; switch (type) { case "audio": @@ -794,7 +798,15 @@ export class RTCConnection { fallback = getIdleTrack("video"); break; } - await this.currentTransceiver[type]?.sender.replaceTrack(this.currentTracks[type] || fallback); + + let target = this.currentTracks[type] || fallback; + if(this.currentTransceiver[type].sender.track === target) { + continue; + } + + await this.currentTransceiver[type].sender.replaceTrack(target); + target.enabled = true; + console.error("Replaced track for %o (Fallback: %o)", type, target === fallback); } } diff --git a/web/app/rtc/SdpUtils.ts b/web/app/rtc/SdpUtils.ts index e0a24198..8e23e325 100644 --- a/web/app/rtc/SdpUtils.ts +++ b/web/app/rtc/SdpUtils.ts @@ -11,7 +11,7 @@ interface SdpCodec { } /* For optimal performance these payload formats should match the servers one */ -const OPUS_VOICE_PAYLOAD_TYPE = 111 +const OPUS_VOICE_PAYLOAD_TYPE = 111; const OPUS_MUSIC_PAYLOAD_TYPE = 112; const H264_PAYLOAD_TYPE = 126; @@ -24,25 +24,27 @@ type SdpMedia = { export class SdpProcessor { private static readonly kAudioCodecs: SdpCodec[] = [ - /* Primary audio format */ + // Primary audio format + // Attention, the payload id should be the same as the server once. + // If not, Firefox start to make trouble and isn't replaying the sound... { - /* Opus Mono/Opus Voice */ + // Opus Mono/Opus Voice payload: OPUS_VOICE_PAYLOAD_TYPE, codec: "opus", rate: 48000, encoding: 2, - fmtp: { minptime: 1, maxptime: 20, useinbandfec: 1, stereo: 0 }, + fmtp: { minptime: 1, maxptime: 20, useinbandfec: 1, usedtx: 1, stereo: 0, "sprop-stereo": 0 }, rtcpFb: [ "transport-cc" ] }, { - /* Opus Stereo/Opus Music */ + // Opus Stereo/Opus Music payload: OPUS_MUSIC_PAYLOAD_TYPE, codec: "opus", rate: 48000, encoding: 2, - fmtp: { minptime: 1, maxptime: 20, useinbandfec: 1, stereo: 1 }, + fmtp: { minptime: 1, maxptime: 20, useinbandfec: 1, usedtx: 1, stereo: 1, "sprop-stereo": 1 }, rtcpFb: [ "transport-cc" ] - } + }, ]; private static readonly kVideoCodecs: SdpCodec[] = [ @@ -193,6 +195,7 @@ export class SdpProcessor { * This causes us only to add one, the primary, codec and hope for the best * (Opus Stereo and Mono mixing seem to work right now 11.2020) */ + /* TODO: Test this again since we're not sending a end signal via RTCP. This might change the behaviour? */ break; } } diff --git a/web/app/rtc/voice/Connection.ts b/web/app/rtc/voice/Connection.ts index 9646a1b2..4d154e4e 100644 --- a/web/app/rtc/voice/Connection.ts +++ b/web/app/rtc/voice/Connection.ts @@ -365,6 +365,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection { stopWhisper() { if(this.whisperTarget) { this.whisperTarget.canceled = true; + this.whisperTarget = undefined; this.whisperTargetInitialize = undefined; this.connection.send_command("whispersessionreset").catch(error => { logWarn(LogCategory.CLIENT, tr("Failed to clear the whisper target: %o"), error);