Implemented the voice whisper backend and reenabled the voice echo test for TeaSpeak 1.5.0
This commit is contained in:
parent
18e44faf3c
commit
ee0fee5cf7
8 changed files with 478 additions and 135 deletions
|
@ -403,8 +403,7 @@ export class ConnectionHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(this.serverFeatures.supportsFeature(ServerFeature.WHISPER_ECHO)) {
|
if(this.serverFeatures.supportsFeature(ServerFeature.WHISPER_ECHO)) {
|
||||||
/* FIXME: Reenable */
|
spawnEchoTestModal(this);
|
||||||
//spawnEchoTestModal(this);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -500,7 +500,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
|
|
||||||
private async doStartWhisper(target: CancelableWhisperTarget) {
|
private async doStartWhisper(target: CancelableWhisperTarget) {
|
||||||
if(target.target === "echo") {
|
if(target.target === "echo") {
|
||||||
await this.connection.send_command("setwhispertarget", {
|
await this.connection.send_command("setwhispersession", {
|
||||||
type: 0x10, /* self */
|
type: 0x10, /* self */
|
||||||
target: 0,
|
target: 0,
|
||||||
id: 0
|
id: 0
|
||||||
|
@ -528,7 +528,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
if(this.whisperTarget) {
|
if(this.whisperTarget) {
|
||||||
this.whisperTarget.canceled = true;
|
this.whisperTarget.canceled = true;
|
||||||
this.whisperTargetInitialize = undefined;
|
this.whisperTargetInitialize = undefined;
|
||||||
this.connection.send_command("clearwhispertarget").catch(error => {
|
this.connection.send_command("clearwhispersession").catch(error => {
|
||||||
logWarn(LogCategory.CLIENT, tr("Failed to clear the whisper target: %o"), error);
|
logWarn(LogCategory.CLIENT, tr("Failed to clear the whisper target: %o"), error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,7 @@ export class WebWhisperSession implements WhisperSession {
|
||||||
this.sessionTimeout = data.sessionTimeout;
|
this.sessionTimeout = data.sessionTimeout;
|
||||||
|
|
||||||
this.voicePlayer = new WebVoicePlayer();
|
this.voicePlayer = new WebVoicePlayer();
|
||||||
|
this.voicePlayer.setVolume(data.volume);
|
||||||
this.voicePlayer.events.on("notify_state_changed", event => {
|
this.voicePlayer.events.on("notify_state_changed", event => {
|
||||||
if(event.newState === VoicePlayerState.BUFFERING) {
|
if(event.newState === VoicePlayerState.BUFFERING) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
import {SdpCompressor, SdpProcessor} from "tc-backend/web/rtc/SdpUtils";
|
import {SdpCompressor, SdpProcessor} from "tc-backend/web/rtc/SdpUtils";
|
||||||
import {context} from "tc-backend/web/audio/player";
|
import {context} from "tc-backend/web/audio/player";
|
||||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||||
|
import {WhisperTarget} from "tc-shared/voice/VoiceWhisper";
|
||||||
|
|
||||||
const kSdpCompressionMode = 1;
|
const kSdpCompressionMode = 1;
|
||||||
|
|
||||||
|
@ -628,6 +629,28 @@ export class RTCConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async startWhisper(target: WhisperTarget) : Promise<void> {
|
||||||
|
const transceiver = this.currentTransceiver["audio-whisper"];
|
||||||
|
if(typeof transceiver === "undefined") {
|
||||||
|
throw tr("missing transceiver");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(target.target === "echo") {
|
||||||
|
await this.connection.send_command("whispersessioninitialize", {
|
||||||
|
ssrc: this.sdpProcessor.getLocalSsrcFromFromMediaId(transceiver.mid),
|
||||||
|
type: 0x10, /* self */
|
||||||
|
target: 0,
|
||||||
|
id: 0
|
||||||
|
}, { flagset: ["new"] });
|
||||||
|
} else if(target.target === "channel-clients") {
|
||||||
|
throw "target not yet supported";
|
||||||
|
} else if(target.target === "groups") {
|
||||||
|
throw "target not yet supported";
|
||||||
|
} else {
|
||||||
|
throw "target not yet supported";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public stopTrackBroadcast(type: RTCBroadcastableTrackType) {
|
public stopTrackBroadcast(type: RTCBroadcastableTrackType) {
|
||||||
this.connection.send_command("rtcbroadcast", {
|
this.connection.send_command("rtcbroadcast", {
|
||||||
type: broadcastableTrackTypeToNumber(type),
|
type: broadcastableTrackTypeToNumber(type),
|
||||||
|
|
|
@ -5,17 +5,24 @@ import {
|
||||||
} from "tc-shared/connection/VoiceConnection";
|
} from "tc-shared/connection/VoiceConnection";
|
||||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||||
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
||||||
import {WhisperSession, WhisperSessionState, WhisperTarget} from "tc-shared/voice/VoiceWhisper";
|
import {
|
||||||
|
kUnknownWhisperClientUniqueId,
|
||||||
|
WhisperSession,
|
||||||
|
WhisperSessionState,
|
||||||
|
WhisperTarget
|
||||||
|
} from "tc-shared/voice/VoiceWhisper";
|
||||||
import {RTCConnection, RTCConnectionEvents, RTPConnectionState} from "tc-backend/web/rtc/Connection";
|
import {RTCConnection, RTCConnectionEvents, RTPConnectionState} from "tc-backend/web/rtc/Connection";
|
||||||
import {AbstractServerConnection, ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
|
import {AbstractServerConnection, ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
|
||||||
import {VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
import {VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log";
|
import {LogCategory, logDebug, logError, logTrace, logWarn} from "tc-shared/log";
|
||||||
import * as aplayer from "../../audio/player";
|
import * as aplayer from "../../audio/player";
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
import {RtpVoiceClient} from "tc-backend/web/rtc/voice/VoiceClient";
|
import {RtpVoiceClient} from "tc-backend/web/rtc/voice/VoiceClient";
|
||||||
import {InputConsumerType} from "tc-shared/voice/RecorderBase";
|
import {InputConsumerType} from "tc-shared/voice/RecorderBase";
|
||||||
|
import {RtpWhisperSession} from "tc-backend/web/rtc/voice/WhisperClient";
|
||||||
|
|
||||||
|
type CancelableWhisperTarget = WhisperTarget & { canceled: boolean };
|
||||||
export class RtpVoiceConnection extends AbstractVoiceConnection {
|
export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
private readonly rtcConnection: RTCConnection;
|
private readonly rtcConnection: RTCConnection;
|
||||||
|
|
||||||
|
@ -34,16 +41,21 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
private speakerMuted: boolean;
|
private speakerMuted: boolean;
|
||||||
private voiceClients: RtpVoiceClient[] = [];
|
private voiceClients: RtpVoiceClient[] = [];
|
||||||
|
|
||||||
|
private whisperSessionInitializer: WhisperSessionInitializer | undefined;
|
||||||
|
private whisperSessions: RtpWhisperSession[] = [];
|
||||||
|
private whisperTarget: CancelableWhisperTarget | undefined;
|
||||||
|
private whisperTargetInitialize: Promise<void> | undefined;
|
||||||
|
|
||||||
private currentlyReplayingVoice: boolean = false;
|
private currentlyReplayingVoice: boolean = false;
|
||||||
private readonly voiceClientStateChangedEventListener;
|
private readonly voiceClientStateChangedEventListener;
|
||||||
private readonly whisperSessionStateChangedEventListener;
|
private readonly whisperSessionStateChangedEventListener;
|
||||||
|
|
||||||
|
|
||||||
constructor(connection: AbstractServerConnection, rtcConnection: RTCConnection) {
|
constructor(connection: AbstractServerConnection, rtcConnection: RTCConnection) {
|
||||||
super(connection);
|
super(connection);
|
||||||
|
|
||||||
this.rtcConnection = rtcConnection;
|
this.rtcConnection = rtcConnection;
|
||||||
this.voiceClientStateChangedEventListener = this.handleVoiceClientStateChange.bind(this);
|
this.voiceClientStateChangedEventListener = this.handleVoiceClientStateChange.bind(this);
|
||||||
|
this.whisperSessionStateChangedEventListener = this.handleWhisperSessionStateChange.bind(this);
|
||||||
|
|
||||||
this.rtcConnection.getEvents().on("notify_audio_assignment_changed",
|
this.rtcConnection.getEvents().on("notify_audio_assignment_changed",
|
||||||
this.listenerRtcAudioAssignment = event => this.handleAudioAssignmentChanged(event));
|
this.listenerRtcAudioAssignment = event => this.handleAudioAssignmentChanged(event));
|
||||||
|
@ -78,6 +90,8 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
this.currentAudioSourceNode.connect(this.localAudioDestination);
|
this.currentAudioSourceNode.connect(this.localAudioDestination);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.setWhisperSessionInitializer(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -186,17 +200,23 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
const ch = chandler.getClient();
|
const ch = chandler.getClient();
|
||||||
if(ch) ch.speaking = false;
|
if(ch) ch.speaking = false;
|
||||||
|
|
||||||
if(!chandler.connected)
|
if(!chandler.connected) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if(chandler.isMicrophoneMuted())
|
if(chandler.isMicrophoneMuted()) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
log.info(LogCategory.VOICE, tr("Local voice ended"));
|
log.info(LogCategory.VOICE, tr("Local voice ended"));
|
||||||
|
|
||||||
this.rtcConnection.setTrackSource("audio", null).catch(error => {
|
this.rtcConnection.setTrackSource("audio", null).catch(error => {
|
||||||
logError(LogCategory.AUDIO, tr("Failed to set current audio track: %o"), error);
|
logError(LogCategory.AUDIO, tr("Failed to set current audio track: %o"), error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.rtcConnection.setTrackSource("audio-whisper", null).catch(error => {
|
||||||
|
logError(LogCategory.AUDIO, tr("Failed to set current audio track: %o"), error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleRecorderStart() {
|
private handleRecorderStart() {
|
||||||
|
@ -210,7 +230,8 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
|
|
||||||
const ch = chandler.getClient();
|
const ch = chandler.getClient();
|
||||||
if(ch) { ch.speaking = true; }
|
if(ch) { ch.speaking = true; }
|
||||||
this.rtcConnection.setTrackSource("audio", this.localAudioDestination.stream.getAudioTracks()[0])
|
|
||||||
|
this.rtcConnection.setTrackSource(this.whisperTarget ? "audio-whisper" : "audio", this.localAudioDestination.stream.getAudioTracks()[0])
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logError(LogCategory.AUDIO, tr("Failed to set current audio track: %o"), error);
|
logError(LogCategory.AUDIO, tr("Failed to set current audio track: %o"), error);
|
||||||
});
|
});
|
||||||
|
@ -235,10 +256,12 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
getEncoderCodec(): number {
|
getEncoderCodec(): number {
|
||||||
return 5;
|
return 4;
|
||||||
}
|
}
|
||||||
setEncoderCodec(codec: number) { }
|
|
||||||
|
|
||||||
|
setEncoderCodec(codec: number) {
|
||||||
|
/* TODO: If possible change the payload format? */
|
||||||
|
}
|
||||||
|
|
||||||
availableVoiceClients(): VoiceClient[] {
|
availableVoiceClients(): VoiceClient[] {
|
||||||
return Object.values(this.voiceClients);
|
return Object.values(this.voiceClients);
|
||||||
|
@ -266,33 +289,93 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
client.destroy();
|
client.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAllVoiceReplays() {
|
stopAllVoiceReplays() { }
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
getWhisperSessionInitializer(): WhisperSessionInitializer | undefined {
|
getWhisperSessionInitializer(): WhisperSessionInitializer | undefined {
|
||||||
return undefined;
|
return this.whisperSessionInitializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWhisperSessionInitializer(initializer: WhisperSessionInitializer | undefined) {
|
setWhisperSessionInitializer(initializer: WhisperSessionInitializer | undefined) {
|
||||||
|
this.whisperSessionInitializer = initializer;
|
||||||
|
if(!this.whisperSessionInitializer) {
|
||||||
|
this.whisperSessionInitializer = async session => {
|
||||||
|
logWarn(LogCategory.VOICE, tr("Missing whisper session initializer. Blocking whisper from %d (%s)"), session.getClientId(), session.getClientUniqueId());
|
||||||
|
return {
|
||||||
|
clientName: session.getClientName() || tr("Unknown client"),
|
||||||
|
clientUniqueId: session.getClientUniqueId() || kUnknownWhisperClientUniqueId,
|
||||||
|
|
||||||
|
blocked: true,
|
||||||
|
volume: 1,
|
||||||
|
|
||||||
|
sessionTimeout: 60 * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getWhisperSessions(): WhisperSession[] {
|
getWhisperSessions(): WhisperSession[] {
|
||||||
return [];
|
return this.whisperSessions;
|
||||||
}
|
|
||||||
|
|
||||||
getWhisperTarget(): WhisperTarget | undefined {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dropWhisperSession(session: WhisperSession) {
|
dropWhisperSession(session: WhisperSession) {
|
||||||
|
if(!(session instanceof RtpWhisperSession)) {
|
||||||
|
throw tr("Invalid session type");
|
||||||
}
|
}
|
||||||
|
|
||||||
startWhisper(target: WhisperTarget): Promise<void> {
|
session.events.off("notify_state_changed", this.whisperSessionStateChangedEventListener);
|
||||||
return Promise.resolve(undefined);
|
this.whisperSessions.remove(session);
|
||||||
|
session.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
stopWhisper() { }
|
async startWhisper(target: WhisperTarget): Promise<void> {
|
||||||
|
while(this.whisperTargetInitialize) {
|
||||||
|
this.whisperTarget.canceled = true;
|
||||||
|
await this.whisperTargetInitialize;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.whisperTarget = Object.assign({ canceled: false }, target);
|
||||||
|
try {
|
||||||
|
await (this.whisperTargetInitialize = this.doStartWhisper(this.whisperTarget));
|
||||||
|
} finally {
|
||||||
|
this.whisperTargetInitialize = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doStartWhisper(target: CancelableWhisperTarget) {
|
||||||
|
if(this.rtcConnection.getConnectionState() !== RTPConnectionState.CONNECTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.rtcConnection.startWhisper(target);
|
||||||
|
|
||||||
|
if(target.canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleRecorderStop();
|
||||||
|
if(this.currentAudioSource?.input && !this.currentAudioSource.input.isFiltered()) {
|
||||||
|
this.handleRecorderStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getWhisperTarget(): WhisperTarget | undefined {
|
||||||
|
return this.whisperTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWhisper() {
|
||||||
|
if(this.whisperTarget) {
|
||||||
|
this.whisperTarget.canceled = true;
|
||||||
|
this.whisperTargetInitialize = undefined;
|
||||||
|
this.connection.send_command("whispersessionreset").catch(error => {
|
||||||
|
logWarn(LogCategory.CLIENT, tr("Failed to clear the whisper target: %o"), error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleRecorderStop();
|
||||||
|
if(this.currentAudioSource?.input && !this.currentAudioSource.input.isFiltered()) {
|
||||||
|
this.handleRecorderStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleVoiceClientStateChange(/* event: VoicePlayerEvents["notify_state_changed"] */) {
|
private handleVoiceClientStateChange(/* event: VoicePlayerEvents["notify_state_changed"] */) {
|
||||||
this.updateVoiceReplaying();
|
this.updateVoiceReplaying();
|
||||||
|
@ -303,9 +386,9 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateVoiceReplaying() {
|
private updateVoiceReplaying() {
|
||||||
let replaying = false;
|
let replaying;
|
||||||
|
|
||||||
/* if(this.connectionState === VoiceConnectionStatus.Connected) */ {
|
{
|
||||||
let index = this.availableVoiceClients().findIndex(client => client.getState() === VoicePlayerState.PLAYING || client.getState() === VoicePlayerState.BUFFERING);
|
let index = this.availableVoiceClients().findIndex(client => client.getState() === VoicePlayerState.PLAYING || client.getState() === VoicePlayerState.BUFFERING);
|
||||||
replaying = index !== -1;
|
replaying = index !== -1;
|
||||||
|
|
||||||
|
@ -322,8 +405,6 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private setConnectionState(state: VoiceConnectionStatus) {
|
private setConnectionState(state: VoiceConnectionStatus) {
|
||||||
if(this.connectionState === state)
|
if(this.connectionState === state)
|
||||||
return;
|
return;
|
||||||
|
@ -344,6 +425,12 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
this.localFailedReason = tr("Failed to start audio broadcasting");
|
this.localFailedReason = tr("Failed to start audio broadcasting");
|
||||||
this.setConnectionState(VoiceConnectionStatus.Failed);
|
this.setConnectionState(VoiceConnectionStatus.Failed);
|
||||||
});
|
});
|
||||||
|
if(this.whisperTarget) {
|
||||||
|
this.startWhisper(this.whisperTarget).catch(error => {
|
||||||
|
logError(LogCategory.VOICE, tr("Failed to start voice whisper on connected rtc connection: &o"), error);
|
||||||
|
/* TODO: Somehow abort the whisper and give the user a feedback? */
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RTPConnectionState.CONNECTING:
|
case RTPConnectionState.CONNECTING:
|
||||||
|
@ -366,18 +453,63 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAudioAssignmentChanged(event: RTCConnectionEvents["notify_audio_assignment_changed"]) {
|
private handleAudioAssignmentChanged(event: RTCConnectionEvents["notify_audio_assignment_changed"]) {
|
||||||
const oldClient = Object.values(this.voiceClients).find(client => client.getRtpTrack() === event.track);
|
{
|
||||||
if(oldClient) {
|
let oldClient = Object.values(this.voiceClients).find(client => client.getRtpTrack() === event.track);
|
||||||
oldClient.setRtpTrack(undefined);
|
oldClient?.setRtpTrack(undefined);
|
||||||
|
|
||||||
|
let oldSession = this.whisperSessions.find(e => e.getRtpTrack() === event.track);
|
||||||
|
oldSession?.setRtpTrack(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(event.info) {
|
if(event.info) {
|
||||||
|
if(typeof event.info.media === "undefined") {
|
||||||
|
logWarn(LogCategory.VOICE, tr("Received audio assignment event with missing media type"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.info.media) {
|
||||||
|
case 0: {
|
||||||
const newClient = this.voiceClients[event.info.client_id];
|
const newClient = this.voiceClients[event.info.client_id];
|
||||||
if(newClient) {
|
if(newClient) {
|
||||||
newClient.setRtpTrack(event.track);
|
newClient.setRtpTrack(event.track);
|
||||||
} else {
|
} else {
|
||||||
logWarn(LogCategory.AUDIO, tr("Received audio track assignment for unknown voice client (%o)."), event.info);
|
logWarn(LogCategory.AUDIO, tr("Received audio track assignment for unknown voice client (%o)."), event.info);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
let session = this.whisperSessions.find(e => e.getClientId() === event.info.client_id);
|
||||||
|
if(typeof session === "undefined") {
|
||||||
|
logDebug(LogCategory.VOICE, tr("Received new whisper from %d (%s)"), event.info.client_id, event.info.client_name);
|
||||||
|
session = new RtpWhisperSession(event.track, event.info);
|
||||||
|
session.setGloballyMuted(this.speakerMuted);
|
||||||
|
this.whisperSessions.push(session);
|
||||||
|
session.events.on("notify_state_changed", this.whisperSessionStateChangedEventListener);
|
||||||
|
this.whisperSessionInitializer(session).then(result => {
|
||||||
|
try {
|
||||||
|
session.initializeFromData(result);
|
||||||
|
this.events.fire("notify_whisper_initialized", { session });
|
||||||
|
} catch (error) {
|
||||||
|
logError(LogCategory.VOICE, tr("Failed to internally initialize a voice whisper session: %o"), error);
|
||||||
|
session.setSessionState(WhisperSessionState.INITIALIZE_FAILED);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
logError(LogCategory.VOICE, tr("Failed to initialize whisper session: %o."), error);
|
||||||
|
session.initializeFailed();
|
||||||
|
});
|
||||||
|
|
||||||
|
session.events.on("notify_timed_out", () => {
|
||||||
|
logTrace(LogCategory.VOICE, tr("Whisper session %d timed out. Dropping session."), session.getClientId());
|
||||||
|
this.dropWhisperSession(session);
|
||||||
|
});
|
||||||
|
this.events.fire("notify_whisper_created", { session: session });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logWarn(LogCategory.VOICE, tr("Received audio assignment event with invalid media type (%o)"), event.info.media);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,6 +528,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
|
|
||||||
this.speakerMuted = newState;
|
this.speakerMuted = newState;
|
||||||
this.voiceClients.forEach(client => client.setGloballyMuted(this.speakerMuted));
|
this.voiceClients.forEach(client => client.setGloballyMuted(this.speakerMuted));
|
||||||
|
this.whisperSessions.forEach(session => session.setGloballyMuted(this.speakerMuted));
|
||||||
}
|
}
|
||||||
|
|
||||||
getRetryTimestamp(): number | 0 {
|
getRetryTimestamp(): number | 0 {
|
||||||
|
|
115
web/app/rtc/voice/RtpVoicePlayer.ts
Normal file
115
web/app/rtc/voice/RtpVoicePlayer.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import {
|
||||||
|
VoicePlayer,
|
||||||
|
VoicePlayerEvents,
|
||||||
|
VoicePlayerLatencySettings,
|
||||||
|
VoicePlayerState
|
||||||
|
} from "tc-shared/voice/VoicePlayer";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {LogCategory, logWarn} from "tc-shared/log";
|
||||||
|
import {RemoteRTPAudioTrack, RemoteRTPTrackState} from "tc-backend/web/rtc/RemoteTrack";
|
||||||
|
|
||||||
|
export interface RtpVoicePlayerEvents {
|
||||||
|
notify_state_changed: { oldState: VoicePlayerState, newState: VoicePlayerState }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RtpVoicePlayer implements VoicePlayer {
|
||||||
|
readonly events: Registry<VoicePlayerEvents>;
|
||||||
|
private readonly listenerTrackStateChanged;
|
||||||
|
|
||||||
|
private globallyMuted: boolean;
|
||||||
|
|
||||||
|
private volume: number;
|
||||||
|
private currentState: VoicePlayerState;
|
||||||
|
private currentRtpTrack: RemoteRTPAudioTrack;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.listenerTrackStateChanged = event => this.handleTrackStateChanged(event.newState);
|
||||||
|
this.events = new Registry<VoicePlayerEvents>();
|
||||||
|
this.currentState = VoicePlayerState.STOPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.events.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
setGloballyMuted(muted: boolean) {
|
||||||
|
if(this.globallyMuted === muted) { return; }
|
||||||
|
|
||||||
|
this.globallyMuted = muted;
|
||||||
|
this.updateVolume();
|
||||||
|
}
|
||||||
|
|
||||||
|
abortReplay() {
|
||||||
|
this.currentRtpTrack?.abortCurrentReplay();
|
||||||
|
this.setState(VoicePlayerState.STOPPED);
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): VoicePlayerState {
|
||||||
|
return this.currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setState(state: VoicePlayerState) {
|
||||||
|
if(this.currentState === state) { return; }
|
||||||
|
|
||||||
|
const oldState = this.currentState;
|
||||||
|
this.currentState = state;
|
||||||
|
this.events.fire("notify_state_changed", { oldState: oldState, newState: state });
|
||||||
|
}
|
||||||
|
|
||||||
|
getVolume(): number {
|
||||||
|
return this.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume(volume: number) {
|
||||||
|
this.volume = volume;
|
||||||
|
this.updateVolume();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRtpTrack(track: RemoteRTPAudioTrack | undefined) {
|
||||||
|
if(this.currentRtpTrack) {
|
||||||
|
this.currentRtpTrack.setGain(0);
|
||||||
|
this.currentRtpTrack.getEvents().off("notify_state_changed", this.listenerTrackStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentRtpTrack = track;
|
||||||
|
if(this.currentRtpTrack) {
|
||||||
|
this.currentRtpTrack.getEvents().on("notify_state_changed", this.listenerTrackStateChanged);
|
||||||
|
this.updateVolume();
|
||||||
|
this.handleTrackStateChanged(this.currentRtpTrack.getState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRtpTrack() {
|
||||||
|
return this.currentRtpTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTrackStateChanged(newState: RemoteRTPTrackState) {
|
||||||
|
switch (newState) {
|
||||||
|
case RemoteRTPTrackState.Bound:
|
||||||
|
case RemoteRTPTrackState.Unbound:
|
||||||
|
this.setState(VoicePlayerState.STOPPED);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RemoteRTPTrackState.Started:
|
||||||
|
this.setState(VoicePlayerState.PLAYING);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RemoteRTPTrackState.Destroyed:
|
||||||
|
logWarn(LogCategory.AUDIO, tr("Received new track state 'Destroyed' which should never happen."));
|
||||||
|
this.setState(VoicePlayerState.STOPPED);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateVolume() {
|
||||||
|
this.currentRtpTrack?.setGain(this.globallyMuted ? 0 : this.volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
flushBuffer() { /* not supported */ }
|
||||||
|
|
||||||
|
getLatencySettings(): Readonly<VoicePlayerLatencySettings> {
|
||||||
|
return { minBufferTime: 0, maxBufferTime: 0 };
|
||||||
|
}
|
||||||
|
resetLatencySettings() { }
|
||||||
|
setLatencySettings(settings: VoicePlayerLatencySettings) { }
|
||||||
|
}
|
|
@ -1,111 +1,16 @@
|
||||||
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
||||||
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
import {RtpVoicePlayer} from "./RtpVoicePlayer";
|
||||||
import {Registry} from "tc-shared/events";
|
|
||||||
import {LogCategory, logWarn} from "tc-shared/log";
|
|
||||||
import {RemoteRTPAudioTrack, RemoteRTPTrackState} from "tc-backend/web/rtc/RemoteTrack";
|
|
||||||
|
|
||||||
export class RtpVoiceClient implements VoiceClient {
|
export class RtpVoiceClient extends RtpVoicePlayer implements VoiceClient {
|
||||||
readonly events: Registry<VoicePlayerEvents>;
|
|
||||||
private readonly listenerTrackStateChanged;
|
|
||||||
private readonly clientId: number;
|
private readonly clientId: number;
|
||||||
|
|
||||||
private globallyMuted: boolean;
|
|
||||||
|
|
||||||
private volume: number;
|
|
||||||
private currentState: VoicePlayerState;
|
|
||||||
private currentRtpTrack: RemoteRTPAudioTrack;
|
|
||||||
|
|
||||||
constructor(clientId: number) {
|
constructor(clientId: number) {
|
||||||
|
super();
|
||||||
|
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.listenerTrackStateChanged = event => this.handleTrackStateChanged(event.newState);
|
|
||||||
this.events = new Registry<VoicePlayerEvents>();
|
|
||||||
this.currentState = VoicePlayerState.STOPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.events.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
setGloballyMuted(muted: boolean) {
|
|
||||||
if(this.globallyMuted === muted) { return; }
|
|
||||||
|
|
||||||
this.globallyMuted = muted;
|
|
||||||
this.updateVolume();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientId(): number {
|
getClientId(): number {
|
||||||
return this.clientId;
|
return this.clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
abortReplay() {
|
|
||||||
this.currentRtpTrack?.abortCurrentReplay();
|
|
||||||
this.setState(VoicePlayerState.STOPPED);
|
|
||||||
}
|
|
||||||
|
|
||||||
flushBuffer() { /* not possible */ }
|
|
||||||
|
|
||||||
getState(): VoicePlayerState {
|
|
||||||
return this.currentState;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected setState(state: VoicePlayerState) {
|
|
||||||
if(this.currentState === state) { return; }
|
|
||||||
const oldState = this.currentState;
|
|
||||||
this.currentState = state;
|
|
||||||
this.events.fire("notify_state_changed", { oldState: oldState, newState: state });
|
|
||||||
}
|
|
||||||
|
|
||||||
getVolume(): number {
|
|
||||||
return this.volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
setVolume(volume: number) {
|
|
||||||
this.volume = volume;
|
|
||||||
this.updateVolume();
|
|
||||||
}
|
|
||||||
|
|
||||||
getLatencySettings(): Readonly<VoicePlayerLatencySettings> {
|
|
||||||
return { minBufferTime: 0, maxBufferTime: 0 };
|
|
||||||
}
|
|
||||||
resetLatencySettings() { }
|
|
||||||
setLatencySettings(settings: VoicePlayerLatencySettings) { }
|
|
||||||
|
|
||||||
setRtpTrack(track: RemoteRTPAudioTrack | undefined) {
|
|
||||||
if(this.currentRtpTrack) {
|
|
||||||
this.currentRtpTrack.setGain(0);
|
|
||||||
this.currentRtpTrack.getEvents().off("notify_state_changed", this.listenerTrackStateChanged);
|
|
||||||
}
|
|
||||||
this.currentRtpTrack = track;
|
|
||||||
if(this.currentRtpTrack) {
|
|
||||||
this.currentRtpTrack.setGain(this.volume);
|
|
||||||
this.currentRtpTrack.getEvents().on("notify_state_changed", this.listenerTrackStateChanged);
|
|
||||||
this.handleTrackStateChanged(this.currentRtpTrack.getState());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getRtpTrack() {
|
|
||||||
return this.currentRtpTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleTrackStateChanged(newState: RemoteRTPTrackState) {
|
|
||||||
switch (newState) {
|
|
||||||
case RemoteRTPTrackState.Bound:
|
|
||||||
case RemoteRTPTrackState.Unbound:
|
|
||||||
this.setState(VoicePlayerState.STOPPED);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RemoteRTPTrackState.Started:
|
|
||||||
this.setState(VoicePlayerState.PLAYING);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RemoteRTPTrackState.Destroyed:
|
|
||||||
logWarn(LogCategory.AUDIO, tr("Received new track state 'Destroyed' which should never happen."));
|
|
||||||
this.setState(VoicePlayerState.STOPPED);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateVolume() {
|
|
||||||
this.currentRtpTrack?.setGain(this.globallyMuted ? 0 : this.volume);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
import {WhisperSession, WhisperSessionEvents, WhisperSessionState} from "tc-shared/voice/VoiceWhisper";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {VoicePlayer, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
||||||
|
import {WhisperSessionInitializeData} from "tc-shared/connection/VoiceConnection";
|
||||||
|
import {RtpVoicePlayer} from "./RtpVoicePlayer";
|
||||||
|
import {RemoteRTPAudioTrack, TrackClientInfo} from "tc-backend/web/rtc/RemoteTrack";
|
||||||
|
|
||||||
|
export class RtpWhisperSession implements WhisperSession {
|
||||||
|
readonly events: Registry<WhisperSessionEvents>;
|
||||||
|
private readonly assignmentInfo: TrackClientInfo;
|
||||||
|
|
||||||
|
private track: RemoteRTPAudioTrack;
|
||||||
|
private globallyMuted: boolean;
|
||||||
|
|
||||||
|
private sessionState: WhisperSessionState;
|
||||||
|
private sessionBlocked: boolean;
|
||||||
|
|
||||||
|
private sessionTimeout: number;
|
||||||
|
private sessionTimeoutId: number;
|
||||||
|
|
||||||
|
private lastWhisperTimestamp: number;
|
||||||
|
private voicePlayer: RtpVoicePlayer;
|
||||||
|
|
||||||
|
constructor(track: RemoteRTPAudioTrack, info: TrackClientInfo) {
|
||||||
|
this.events = new Registry<WhisperSessionEvents>();
|
||||||
|
this.track = track;
|
||||||
|
this.assignmentInfo = info;
|
||||||
|
this.sessionState = WhisperSessionState.INITIALIZING;
|
||||||
|
|
||||||
|
this.globallyMuted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getClientId(): number {
|
||||||
|
return this.assignmentInfo.client_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getClientName(): string | undefined {
|
||||||
|
return this.assignmentInfo.client_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getClientUniqueId(): string | undefined {
|
||||||
|
return this.assignmentInfo.client_unique_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastWhisperTimestamp(): number {
|
||||||
|
return this.lastWhisperTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionState(): WhisperSessionState {
|
||||||
|
return this.sessionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionTimeout(): number {
|
||||||
|
return this.sessionTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVoicePlayer(): VoicePlayer | undefined {
|
||||||
|
return this.voicePlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionTimeout(timeout: number) {
|
||||||
|
this.sessionTimeout = timeout;
|
||||||
|
this.resetSessionTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
isBlocked(): boolean {
|
||||||
|
return this.sessionBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlocked(blocked: boolean) {
|
||||||
|
this.sessionBlocked = blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeFromData(data: WhisperSessionInitializeData) {
|
||||||
|
this.assignmentInfo.client_name = data.clientName;
|
||||||
|
this.assignmentInfo.client_unique_id = data.clientUniqueId;
|
||||||
|
|
||||||
|
this.sessionBlocked = data.blocked;
|
||||||
|
this.sessionTimeout = data.sessionTimeout;
|
||||||
|
|
||||||
|
this.voicePlayer = new RtpVoicePlayer();
|
||||||
|
this.voicePlayer.setRtpTrack(this.track);
|
||||||
|
this.voicePlayer.setGloballyMuted(this.globallyMuted);
|
||||||
|
this.voicePlayer.setVolume(data.volume);
|
||||||
|
this.voicePlayer.events.on("notify_state_changed", event => {
|
||||||
|
if(event.newState === VoicePlayerState.BUFFERING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetSessionTimeout();
|
||||||
|
if(event.newState === VoicePlayerState.PLAYING || event.newState === VoicePlayerState.STOPPING) {
|
||||||
|
this.setSessionState(WhisperSessionState.PLAYING);
|
||||||
|
} else {
|
||||||
|
this.setSessionState(WhisperSessionState.PAUSED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.setSessionState(WhisperSessionState.PAUSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeFailed() {
|
||||||
|
this.setSessionState(WhisperSessionState.INITIALIZE_FAILED);
|
||||||
|
|
||||||
|
/* if we're receiving nothing for more than 5 seconds we can try it again */
|
||||||
|
this.sessionTimeout = 5000;
|
||||||
|
this.resetSessionTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
clearTimeout(this.sessionTimeoutId);
|
||||||
|
this.events.destroy();
|
||||||
|
this.voicePlayer?.destroy();
|
||||||
|
this.voicePlayer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionState(state: WhisperSessionState) {
|
||||||
|
if(this.sessionState === state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldState = this.sessionState;
|
||||||
|
this.sessionState = state;
|
||||||
|
this.events.fire("notify_state_changed", { oldState: oldState, newState: state });
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetSessionTimeout() {
|
||||||
|
clearTimeout(this.sessionTimeoutId);
|
||||||
|
if(this.sessionState === WhisperSessionState.PLAYING) {
|
||||||
|
/* no need to reschedule a session timeout if we're currently playing */
|
||||||
|
return;
|
||||||
|
} else if(this.sessionState === WhisperSessionState.INITIALIZING) {
|
||||||
|
/* we're still initializing; a session timeout hasn't been set */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessionTimeoutId = setTimeout(() => {
|
||||||
|
this.events.fire("notify_timed_out");
|
||||||
|
}, Math.max(this.sessionTimeout, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
setRtpTrack(track: RemoteRTPAudioTrack | undefined) {
|
||||||
|
if(track) {
|
||||||
|
const info = track.getCurrentAssignment();
|
||||||
|
if(!info) {
|
||||||
|
throw tr("Target track missing an assignment");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(info.client_id !== this.assignmentInfo.client_id) {
|
||||||
|
throw tra("Target track does not belong to current whisper session owner (Owner: {}, Track owner: {})", this.assignmentInfo.client_id, info.client_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assignmentInfo.client_name = info.client_name;
|
||||||
|
this.assignmentInfo.client_unique_id = info.client_unique_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.track = track;
|
||||||
|
this.voicePlayer?.setRtpTrack(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRtpTrack() {
|
||||||
|
return this.voicePlayer.getRtpTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
setGloballyMuted(muted: boolean) {
|
||||||
|
this.globallyMuted = muted;
|
||||||
|
this.voicePlayer?.setGloballyMuted(muted);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue