Improved the Recorder API
This commit is contained in:
parent
ae39685a40
commit
d4179db329
15 changed files with 599 additions and 292 deletions
|
@ -1,4 +1,9 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **05.04.21**
|
||||||
|
- Fixed the mute but for the webclient
|
||||||
|
- Fixed that "always active" microphone filter now works reliably
|
||||||
|
- Improved the recorder API
|
||||||
|
|
||||||
* **29.03.21**
|
* **29.03.21**
|
||||||
- Accquiering the default input recorder when opening the settings
|
- Accquiering the default input recorder when opening the settings
|
||||||
- Adding new modal Input Processing Properties for the native client
|
- Adding new modal Input Processing Properties for the native client
|
||||||
|
|
|
@ -151,6 +151,7 @@ export class ConnectionHandler {
|
||||||
sound: SoundManager;
|
sound: SoundManager;
|
||||||
|
|
||||||
serverFeatures: ServerFeatures;
|
serverFeatures: ServerFeatures;
|
||||||
|
log: ServerEventLog;
|
||||||
|
|
||||||
private sideBar: SideBarManager;
|
private sideBar: SideBarManager;
|
||||||
private playlistManager: PlaylistManager;
|
private playlistManager: PlaylistManager;
|
||||||
|
@ -171,7 +172,7 @@ export class ConnectionHandler {
|
||||||
|
|
||||||
private pluginCmdRegistry: PluginCmdRegistry;
|
private pluginCmdRegistry: PluginCmdRegistry;
|
||||||
|
|
||||||
private client_status: LocalClientStatus = {
|
private handlerState: LocalClientStatus = {
|
||||||
input_muted: false,
|
input_muted: false,
|
||||||
|
|
||||||
output_muted: false,
|
output_muted: false,
|
||||||
|
@ -186,8 +187,6 @@ export class ConnectionHandler {
|
||||||
private inputHardwareState: InputHardwareState = InputHardwareState.MISSING;
|
private inputHardwareState: InputHardwareState = InputHardwareState.MISSING;
|
||||||
private listenerRecorderInputDeviceChanged: (() => void);
|
private listenerRecorderInputDeviceChanged: (() => void);
|
||||||
|
|
||||||
log: ServerEventLog;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.handlerId = guid();
|
this.handlerId = guid();
|
||||||
this.events_ = new Registry<ConnectionEvents>();
|
this.events_ = new Registry<ConnectionEvents>();
|
||||||
|
@ -196,7 +195,13 @@ export class ConnectionHandler {
|
||||||
this.settings = new ServerSettings();
|
this.settings = new ServerSettings();
|
||||||
|
|
||||||
this.serverConnection = getServerConnectionFactory().create(this);
|
this.serverConnection = getServerConnectionFactory().create(this);
|
||||||
this.serverConnection.events.on("notify_connection_state_changed", event => this.on_connection_state_changed(event.oldState, event.newState));
|
this.serverConnection.events.on("notify_connection_state_changed", event => {
|
||||||
|
logTrace(LogCategory.CLIENT, tr("From %s to %s"), ConnectionState[event.oldState], ConnectionState[event.newState]);
|
||||||
|
this.events_.fire("notify_connection_state_changed", {
|
||||||
|
oldState: event.oldState,
|
||||||
|
newState: event.newState
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.serverConnection.getVoiceConnection().events.on("notify_recorder_changed", event => {
|
this.serverConnection.getVoiceConnection().events.on("notify_recorder_changed", event => {
|
||||||
this.setInputHardwareState(this.getVoiceRecorder() ? InputHardwareState.VALID : InputHardwareState.MISSING);
|
this.setInputHardwareState(this.getVoiceRecorder() ? InputHardwareState.VALID : InputHardwareState.MISSING);
|
||||||
|
@ -245,14 +250,25 @@ export class ConnectionHandler {
|
||||||
this.events_.fire("notify_handler_initialized");
|
this.events_.fire("notify_handler_initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize_client_state(source?: ConnectionHandler) {
|
initializeHandlerState(source?: ConnectionHandler) {
|
||||||
this.client_status.input_muted = source ? source.client_status.input_muted : settings.getValue(Settings.KEY_CLIENT_STATE_MICROPHONE_MUTED);
|
if(source) {
|
||||||
this.client_status.output_muted = source ? source.client_status.output_muted : settings.getValue(Settings.KEY_CLIENT_STATE_SPEAKER_MUTED);
|
this.handlerState.input_muted = source.handlerState.input_muted;
|
||||||
|
this.handlerState.output_muted = source.handlerState.output_muted;
|
||||||
this.update_voice_status();
|
this.update_voice_status();
|
||||||
|
|
||||||
this.setSubscribeToAllChannels(source ? source.client_status.channel_subscribe_all : settings.getValue(Settings.KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS));
|
this.setAway(source.handlerState.away);
|
||||||
this.doSetAway(source ? source.client_status.away : (settings.getValue(Settings.KEY_CLIENT_STATE_AWAY) ? settings.getValue(Settings.KEY_CLIENT_AWAY_MESSAGE) : false), false);
|
this.setQueriesShown(source.handlerState.queries_visible);
|
||||||
this.setQueriesShown(source ? source.client_status.queries_visible : settings.getValue(Settings.KEY_CLIENT_STATE_QUERY_SHOWN));
|
this.setSubscribeToAllChannels(source.handlerState.channel_subscribe_all);
|
||||||
|
/* Ignore lastChannelCodecWarned */
|
||||||
|
} else {
|
||||||
|
this.handlerState.input_muted = settings.getValue(Settings.KEY_CLIENT_STATE_MICROPHONE_MUTED);
|
||||||
|
this.handlerState.output_muted = settings.getValue(Settings.KEY_CLIENT_STATE_SPEAKER_MUTED);
|
||||||
|
this.update_voice_status();
|
||||||
|
|
||||||
|
this.setSubscribeToAllChannels(settings.getValue(Settings.KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS));
|
||||||
|
this.doSetAway(settings.getValue(Settings.KEY_CLIENT_STATE_AWAY) ? settings.getValue(Settings.KEY_CLIENT_AWAY_MESSAGE) : false, false);
|
||||||
|
this.setQueriesShown(settings.getValue(Settings.KEY_CLIENT_STATE_QUERY_SHOWN));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events() : Registry<ConnectionEvents> {
|
events() : Registry<ConnectionEvents> {
|
||||||
|
@ -386,7 +402,9 @@ export class ConnectionHandler {
|
||||||
|
|
||||||
async disconnectFromServer(reason?: string) {
|
async disconnectFromServer(reason?: string) {
|
||||||
this.cancelAutoReconnect(true);
|
this.cancelAutoReconnect(true);
|
||||||
if(!this.connected) return;
|
if(!this.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.handleDisconnect(DisconnectReason.REQUESTED);
|
this.handleDisconnect(DisconnectReason.REQUESTED);
|
||||||
try {
|
try {
|
||||||
|
@ -458,12 +476,12 @@ export class ConnectionHandler {
|
||||||
this.settings.setServer(this.channelTree.server.properties.virtualserver_unique_identifier);
|
this.settings.setServer(this.channelTree.server.properties.virtualserver_unique_identifier);
|
||||||
|
|
||||||
/* apply the server settings */
|
/* apply the server settings */
|
||||||
if(this.client_status.channel_subscribe_all) {
|
if(this.handlerState.channel_subscribe_all) {
|
||||||
this.channelTree.subscribe_all_channels();
|
this.channelTree.subscribe_all_channels();
|
||||||
} else {
|
} else {
|
||||||
this.channelTree.unsubscribe_all_channels();
|
this.channelTree.unsubscribe_all_channels();
|
||||||
}
|
}
|
||||||
this.channelTree.toggle_server_queries(this.client_status.queries_visible);
|
this.channelTree.toggle_server_queries(this.handlerState.queries_visible);
|
||||||
|
|
||||||
this.sync_status_with_server();
|
this.sync_status_with_server();
|
||||||
this.channelTree.server.updateProperties();
|
this.channelTree.server.updateProperties();
|
||||||
|
@ -740,7 +758,7 @@ export class ConnectionHandler {
|
||||||
this.serverConnection.disconnect();
|
this.serverConnection.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client_status.lastChannelCodecWarned = 0;
|
this.handlerState.lastChannelCodecWarned = 0;
|
||||||
|
|
||||||
if(autoReconnect) {
|
if(autoReconnect) {
|
||||||
if(!this.serverConnection) {
|
if(!this.serverConnection) {
|
||||||
|
@ -776,14 +794,6 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private on_connection_state_changed(old_state: ConnectionState, new_state: ConnectionState) {
|
|
||||||
logTrace(LogCategory.CLIENT, tr("From %s to %s"), ConnectionState[old_state], ConnectionState[new_state]);
|
|
||||||
this.events_.fire("notify_connection_state_changed", {
|
|
||||||
oldState: old_state,
|
|
||||||
newState: new_state
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateVoiceStatus() {
|
private updateVoiceStatus() {
|
||||||
if(!this.localClient) {
|
if(!this.localClient) {
|
||||||
/* we've been destroyed */
|
/* we've been destroyed */
|
||||||
|
@ -816,8 +826,8 @@ export class ConnectionHandler {
|
||||||
localClientUpdates.client_input_hardware = codecSupportEncode;
|
localClientUpdates.client_input_hardware = codecSupportEncode;
|
||||||
localClientUpdates.client_output_hardware = codecSupportDecode;
|
localClientUpdates.client_output_hardware = codecSupportDecode;
|
||||||
|
|
||||||
if(this.client_status.lastChannelCodecWarned !== currentChannel.getChannelId()) {
|
if(this.handlerState.lastChannelCodecWarned !== currentChannel.getChannelId()) {
|
||||||
this.client_status.lastChannelCodecWarned = currentChannel.getChannelId();
|
this.handlerState.lastChannelCodecWarned = currentChannel.getChannelId();
|
||||||
|
|
||||||
if(!codecSupportEncode || !codecSupportDecode) {
|
if(!codecSupportEncode || !codecSupportDecode) {
|
||||||
let message;
|
let message;
|
||||||
|
@ -837,8 +847,8 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
localClientUpdates.client_input_hardware = localClientUpdates.client_input_hardware && this.inputHardwareState === InputHardwareState.VALID;
|
localClientUpdates.client_input_hardware = localClientUpdates.client_input_hardware && this.inputHardwareState === InputHardwareState.VALID;
|
||||||
localClientUpdates.client_output_muted = this.client_status.output_muted;
|
localClientUpdates.client_output_muted = this.handlerState.output_muted;
|
||||||
localClientUpdates.client_input_muted = this.client_status.input_muted;
|
localClientUpdates.client_input_muted = this.handlerState.input_muted;
|
||||||
if(localClientUpdates.client_input_muted || localClientUpdates.client_output_muted) {
|
if(localClientUpdates.client_input_muted || localClientUpdates.client_output_muted) {
|
||||||
shouldRecord = false;
|
shouldRecord = false;
|
||||||
}
|
}
|
||||||
|
@ -863,6 +873,7 @@ export class ConnectionHandler {
|
||||||
this.getClient().updateVariables(...updates);
|
this.getClient().updateVariables(...updates);
|
||||||
|
|
||||||
this.clientStatusSync = true;
|
this.clientStatusSync = true;
|
||||||
|
if(this.connected) {
|
||||||
this.serverConnection.send_command("clientupdate", localClientUpdates).catch(error => {
|
this.serverConnection.send_command("clientupdate", localClientUpdates).catch(error => {
|
||||||
logWarn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error);
|
logWarn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error);
|
||||||
this.log.log("error.custom", { message: tr("Failed to update audio hardware properties.") });
|
this.log.log("error.custom", { message: tr("Failed to update audio hardware properties.") });
|
||||||
|
@ -870,6 +881,7 @@ export class ConnectionHandler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
/* we're not connect, so we should not record either */
|
/* we're not connect, so we should not record either */
|
||||||
}
|
}
|
||||||
|
@ -902,10 +914,10 @@ export class ConnectionHandler {
|
||||||
sync_status_with_server() {
|
sync_status_with_server() {
|
||||||
if(this.serverConnection.connected())
|
if(this.serverConnection.connected())
|
||||||
this.serverConnection.send_command("clientupdate", {
|
this.serverConnection.send_command("clientupdate", {
|
||||||
client_input_muted: this.client_status.input_muted,
|
client_input_muted: this.handlerState.input_muted,
|
||||||
client_output_muted: this.client_status.output_muted,
|
client_output_muted: this.handlerState.output_muted,
|
||||||
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
|
client_away: typeof(this.handlerState.away) === "string" || this.handlerState.away,
|
||||||
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
|
client_away_message: typeof(this.handlerState.away) === "string" ? this.handlerState.away : "",
|
||||||
/* TODO: Somehow store this? */
|
/* TODO: Somehow store this? */
|
||||||
//client_input_hardware: this.client_status.sound_record_supported && this.getInputHardwareState() === InputHardwareState.VALID,
|
//client_input_hardware: this.client_status.sound_record_supported && this.getInputHardwareState() === InputHardwareState.VALID,
|
||||||
//client_output_hardware: this.client_status.sound_playback_supported
|
//client_output_hardware: this.client_status.sound_playback_supported
|
||||||
|
@ -917,11 +929,8 @@ export class ConnectionHandler {
|
||||||
|
|
||||||
/* can be called as much as you want, does nothing if nothing changed */
|
/* can be called as much as you want, does nothing if nothing changed */
|
||||||
async acquireInputHardware() {
|
async acquireInputHardware() {
|
||||||
/* if we're having multiple recorders, try to get the right one */
|
|
||||||
let recorder: RecorderProfile = defaultRecorder;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.serverConnection.getVoiceConnection().acquireVoiceRecorder(recorder);
|
await this.serverConnection.getVoiceConnection().acquireVoiceRecorder(defaultRecorder);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(LogCategory.AUDIO, tr("Failed to acquire recorder: %o"), error);
|
logError(LogCategory.AUDIO, tr("Failed to acquire recorder: %o"), error);
|
||||||
createErrorModal(tr("Failed to acquire recorder"), tr("Failed to acquire recorder.\nLookup the console for more details.")).open();
|
createErrorModal(tr("Failed to acquire recorder"), tr("Failed to acquire recorder.\nLookup the console for more details.")).open();
|
||||||
|
@ -983,7 +992,7 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getVoiceRecorder() : RecorderProfile | undefined { return this.serverConnection.getVoiceConnection().voiceRecorder(); }
|
getVoiceRecorder() : RecorderProfile | undefined { return this.serverConnection?.getVoiceConnection().voiceRecorder(); }
|
||||||
|
|
||||||
|
|
||||||
reconnect_properties(profile?: ConnectionProfile) : ConnectParametersOld {
|
reconnect_properties(profile?: ConnectionProfile) : ConnectParametersOld {
|
||||||
|
@ -1098,11 +1107,11 @@ export class ConnectionHandler {
|
||||||
|
|
||||||
/* state changing methods */
|
/* state changing methods */
|
||||||
setMicrophoneMuted(muted: boolean, dontPlaySound?: boolean) {
|
setMicrophoneMuted(muted: boolean, dontPlaySound?: boolean) {
|
||||||
if(this.client_status.input_muted === muted) {
|
if(this.handlerState.input_muted === muted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client_status.input_muted = muted;
|
this.handlerState.input_muted = muted;
|
||||||
if(!dontPlaySound) {
|
if(!dontPlaySound) {
|
||||||
this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
|
this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
|
||||||
}
|
}
|
||||||
|
@ -1111,21 +1120,30 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
toggleMicrophone() { this.setMicrophoneMuted(!this.isMicrophoneMuted()); }
|
toggleMicrophone() { this.setMicrophoneMuted(!this.isMicrophoneMuted()); }
|
||||||
|
|
||||||
isMicrophoneMuted() { return this.client_status.input_muted; }
|
isMicrophoneMuted() { return this.handlerState.input_muted; }
|
||||||
isMicrophoneDisabled() { return this.inputHardwareState !== InputHardwareState.VALID; }
|
isMicrophoneDisabled() { return this.inputHardwareState !== InputHardwareState.VALID; }
|
||||||
|
|
||||||
setSpeakerMuted(muted: boolean, dontPlaySound?: boolean) {
|
setSpeakerMuted(muted: boolean, dontPlaySound?: boolean) {
|
||||||
if(this.client_status.output_muted === muted) return;
|
if(this.handlerState.output_muted === muted) {
|
||||||
if(muted && !dontPlaySound) this.sound.play(Sound.SOUND_MUTED); /* play the sound *before* we're setting the muted state */
|
return;
|
||||||
this.client_status.output_muted = muted;
|
}
|
||||||
|
|
||||||
|
if(muted && !dontPlaySound) {
|
||||||
|
/* play the sound *before* we're setting the muted state */
|
||||||
|
this.sound.play(Sound.SOUND_MUTED);
|
||||||
|
}
|
||||||
|
this.handlerState.output_muted = muted;
|
||||||
this.events_.fire("notify_state_updated", { state: "speaker" });
|
this.events_.fire("notify_state_updated", { state: "speaker" });
|
||||||
if(!muted && !dontPlaySound) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */
|
if(!muted && !dontPlaySound) {
|
||||||
|
/* play the sound *after* we're setting we've unmuted the sound */
|
||||||
|
this.sound.play(Sound.SOUND_ACTIVATED);
|
||||||
|
}
|
||||||
this.update_voice_status();
|
this.update_voice_status();
|
||||||
this.serverConnection.getVoiceConnection().stopAllVoiceReplays();
|
this.serverConnection.getVoiceConnection().stopAllVoiceReplays();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSpeakerMuted() { this.setSpeakerMuted(!this.isSpeakerMuted()); }
|
toggleSpeakerMuted() { this.setSpeakerMuted(!this.isSpeakerMuted()); }
|
||||||
isSpeakerMuted() { return this.client_status.output_muted; }
|
isSpeakerMuted() { return this.handlerState.output_muted; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns whatever the client is able to playback sound (voice). Reasons for returning true could be:
|
* Returns whatever the client is able to playback sound (voice). Reasons for returning true could be:
|
||||||
|
@ -1136,8 +1154,11 @@ export class ConnectionHandler {
|
||||||
isSpeakerDisabled() : boolean { return false; }
|
isSpeakerDisabled() : boolean { return false; }
|
||||||
|
|
||||||
setSubscribeToAllChannels(flag: boolean) {
|
setSubscribeToAllChannels(flag: boolean) {
|
||||||
if(this.client_status.channel_subscribe_all === flag) return;
|
if(this.handlerState.channel_subscribe_all === flag) {
|
||||||
this.client_status.channel_subscribe_all = flag;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlerState.channel_subscribe_all = flag;
|
||||||
if(flag) {
|
if(flag) {
|
||||||
this.channelTree.subscribe_all_channels();
|
this.channelTree.subscribe_all_channels();
|
||||||
} else {
|
} else {
|
||||||
|
@ -1146,25 +1167,27 @@ export class ConnectionHandler {
|
||||||
this.events_.fire("notify_state_updated", { state: "subscribe" });
|
this.events_.fire("notify_state_updated", { state: "subscribe" });
|
||||||
}
|
}
|
||||||
|
|
||||||
isSubscribeToAllChannels() : boolean { return this.client_status.channel_subscribe_all; }
|
isSubscribeToAllChannels() : boolean { return this.handlerState.channel_subscribe_all; }
|
||||||
|
|
||||||
setAway(state: boolean | string) {
|
setAway(state: boolean | string) {
|
||||||
this.doSetAway(state, true);
|
this.doSetAway(state, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private doSetAway(state: boolean | string, play_sound: boolean) {
|
private doSetAway(state: boolean | string, playSound: boolean) {
|
||||||
if(this.client_status.away === state)
|
if(this.handlerState.away === state) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const was_away = this.isAway();
|
const wasAway = this.isAway();
|
||||||
const will_away = typeof state === "boolean" ? state : true;
|
const willAway = typeof state === "boolean" ? state : true;
|
||||||
if(was_away != will_away && play_sound)
|
if(wasAway != willAway && playSound) {
|
||||||
this.sound.play(will_away ? Sound.AWAY_ACTIVATED : Sound.AWAY_DEACTIVATED);
|
this.sound.play(willAway ? Sound.AWAY_ACTIVATED : Sound.AWAY_DEACTIVATED);
|
||||||
|
}
|
||||||
|
|
||||||
this.client_status.away = state;
|
this.handlerState.away = state;
|
||||||
this.serverConnection.send_command("clientupdate", {
|
this.serverConnection.send_command("clientupdate", {
|
||||||
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
|
client_away: typeof(this.handlerState.away) === "string" || this.handlerState.away,
|
||||||
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
|
client_away_message: typeof(this.handlerState.away) === "string" ? this.handlerState.away : "",
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
logWarn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
|
logWarn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
|
||||||
this.log.log("error.custom", {message: tr("Failed to update away status.")});
|
this.log.log("error.custom", {message: tr("Failed to update away status.")});
|
||||||
|
@ -1175,11 +1198,13 @@ export class ConnectionHandler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toggleAway() { this.setAway(!this.isAway()); }
|
toggleAway() { this.setAway(!this.isAway()); }
|
||||||
isAway() : boolean { return typeof this.client_status.away !== "boolean" || this.client_status.away; }
|
isAway() : boolean { return typeof this.handlerState.away !== "boolean" || this.handlerState.away; }
|
||||||
|
|
||||||
setQueriesShown(flag: boolean) {
|
setQueriesShown(flag: boolean) {
|
||||||
if(this.client_status.queries_visible === flag) return;
|
if(this.handlerState.queries_visible === flag) {
|
||||||
this.client_status.queries_visible = flag;
|
return;
|
||||||
|
}
|
||||||
|
this.handlerState.queries_visible = flag;
|
||||||
this.channelTree.toggle_server_queries(flag);
|
this.channelTree.toggle_server_queries(flag);
|
||||||
|
|
||||||
this.events_.fire("notify_state_updated", {
|
this.events_.fire("notify_state_updated", {
|
||||||
|
@ -1188,7 +1213,7 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
areQueriesShown() : boolean {
|
areQueriesShown() : boolean {
|
||||||
return this.client_status.queries_visible;
|
return this.handlerState.queries_visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputHardwareState() : InputHardwareState { return this.inputHardwareState; }
|
getInputHardwareState() : InputHardwareState { return this.inputHardwareState; }
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class ConnectionManager {
|
||||||
|
|
||||||
spawnConnectionHandler() : ConnectionHandler {
|
spawnConnectionHandler() : ConnectionHandler {
|
||||||
const handler = new ConnectionHandler();
|
const handler = new ConnectionHandler();
|
||||||
handler.initialize_client_state(this.activeConnectionHandler);
|
handler.initializeHandlerState(this.activeConnectionHandler);
|
||||||
this.connectionHandlers.push(handler);
|
this.connectionHandlers.push(handler);
|
||||||
|
|
||||||
this.events_.fire("notify_handler_created", { handler: handler, handlerId: handler.handlerId });
|
this.events_.fire("notify_handler_created", { handler: handler, handlerId: handler.handlerId });
|
||||||
|
|
|
@ -667,7 +667,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
entry.getVoiceClient()?.abortReplay();
|
entry.getVoiceClient()?.abortReplay();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client.speaking = false;
|
client.getVoiceClient()?.abortReplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
const own_channel = this.connection.client.getClient().currentChannel();
|
const own_channel = this.connection.client.getClient().currentChannel();
|
||||||
|
|
|
@ -2,13 +2,15 @@ import {
|
||||||
AbstractVoiceConnection,
|
AbstractVoiceConnection,
|
||||||
VoiceConnectionStatus, WhisperSessionInitializer
|
VoiceConnectionStatus, WhisperSessionInitializer
|
||||||
} from "../connection/VoiceConnection";
|
} from "../connection/VoiceConnection";
|
||||||
import {RecorderProfile} from "../voice/RecorderProfile";
|
import {RecorderProfile, RecorderProfileOwner} from "../voice/RecorderProfile";
|
||||||
import {AbstractServerConnection, ConnectionStatistics} from "../connection/ConnectionBase";
|
import {AbstractServerConnection, ConnectionStatistics} from "../connection/ConnectionBase";
|
||||||
import {VoiceClient} from "../voice/VoiceClient";
|
import {VoiceClient} from "../voice/VoiceClient";
|
||||||
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "../voice/VoicePlayer";
|
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "../voice/VoicePlayer";
|
||||||
import {WhisperSession, WhisperTarget} from "../voice/VoiceWhisper";
|
import {WhisperSession, WhisperTarget} from "../voice/VoiceWhisper";
|
||||||
import {Registry} from "../events";
|
import {Registry} from "../events";
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
import { tr } from "tc-shared/i18n/localize";
|
||||||
|
import {AbstractInput} from "tc-shared/voice/RecorderBase";
|
||||||
|
import {crashOnThrow} from "tc-shared/proto";
|
||||||
|
|
||||||
class DummyVoiceClient implements VoiceClient {
|
class DummyVoiceClient implements VoiceClient {
|
||||||
readonly events: Registry<VoicePlayerEvents>;
|
readonly events: Registry<VoicePlayerEvents>;
|
||||||
|
@ -54,9 +56,11 @@ class DummyVoiceClient implements VoiceClient {
|
||||||
export class DummyVoiceConnection extends AbstractVoiceConnection {
|
export class DummyVoiceConnection extends AbstractVoiceConnection {
|
||||||
private recorder: RecorderProfile;
|
private recorder: RecorderProfile;
|
||||||
private voiceClients: DummyVoiceClient[] = [];
|
private voiceClients: DummyVoiceClient[] = [];
|
||||||
|
private triggerUnmountEvent: boolean;
|
||||||
|
|
||||||
constructor(connection: AbstractServerConnection) {
|
constructor(connection: AbstractServerConnection) {
|
||||||
super(connection);
|
super(connection);
|
||||||
|
this.triggerUnmountEvent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async acquireVoiceRecorder(recorder: RecorderProfile | undefined): Promise<void> {
|
async acquireVoiceRecorder(recorder: RecorderProfile | undefined): Promise<void> {
|
||||||
|
@ -64,21 +68,29 @@ export class DummyVoiceConnection extends AbstractVoiceConnection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.recorder) {
|
|
||||||
this.recorder.callback_unmount = undefined;
|
|
||||||
await this.recorder.unmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
await recorder?.unmount();
|
|
||||||
const oldRecorder = this.recorder;
|
const oldRecorder = this.recorder;
|
||||||
|
await crashOnThrow(async () => {
|
||||||
|
this.triggerUnmountEvent = false;
|
||||||
|
await this.recorder?.ownRecorder(undefined);
|
||||||
|
this.triggerUnmountEvent = true;
|
||||||
|
|
||||||
this.recorder = recorder;
|
this.recorder = recorder;
|
||||||
|
|
||||||
if(this.recorder) {
|
const voiceConnection = this;
|
||||||
this.recorder.callback_unmount = () => {
|
await this.recorder?.ownRecorder(new class extends RecorderProfileOwner {
|
||||||
this.recorder = undefined;
|
protected handleRecorderInput(input: AbstractInput) { }
|
||||||
this.events.fire("notify_recorder_changed");
|
|
||||||
|
protected handleUnmount() {
|
||||||
|
if(!voiceConnection.triggerUnmountEvent) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldRecorder = voiceConnection.recorder;
|
||||||
|
voiceConnection.recorder = undefined;
|
||||||
|
voiceConnection.events.fire("notify_recorder_changed", { oldRecorder: oldRecorder, newRecorder: undefined })
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.events.fire("notify_recorder_changed", {
|
this.events.fire("notify_recorder_changed", {
|
||||||
oldRecorder,
|
oldRecorder,
|
||||||
|
|
|
@ -25,18 +25,38 @@ export abstract class PluginCmdHandler {
|
||||||
|
|
||||||
abstract handlePluginCommand(data: string, invoker: PluginCommandInvoker);
|
abstract handlePluginCommand(data: string, invoker: PluginCommandInvoker);
|
||||||
|
|
||||||
protected sendPluginCommand(data: string, mode: "server" | "view" | "channel" | "private", clientId?: number) : Promise<CommandResult> {
|
protected sendPluginCommand(data: string, mode: "server" | "view" | "channel" | "private", clientOrChannelId?: number) : Promise<CommandResult> {
|
||||||
if(!this.currentServerConnection)
|
if(!this.currentServerConnection) {
|
||||||
throw "plugin command handler not registered";
|
throw "plugin command handler not registered";
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetMode: number;
|
||||||
|
switch (mode) {
|
||||||
|
case "channel":
|
||||||
|
targetMode = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "server":
|
||||||
|
targetMode = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "private":
|
||||||
|
targetMode = 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "view":
|
||||||
|
targetMode = 3;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw tr("invalid plugin message target");
|
||||||
|
}
|
||||||
|
|
||||||
return this.currentServerConnection.send_command("plugincmd", {
|
return this.currentServerConnection.send_command("plugincmd", {
|
||||||
data: data,
|
data: data,
|
||||||
name: this.channel,
|
name: this.channel,
|
||||||
targetmode: mode === "server" ? 1 :
|
targetmode: targetMode,
|
||||||
mode === "view" ? 3 :
|
target: clientOrChannelId
|
||||||
mode === "channel" ? 0 :
|
|
||||||
2,
|
|
||||||
target: clientId
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -661,6 +661,25 @@ export class RTCConnection {
|
||||||
return oldTrack;
|
return oldTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearTrackSources(types: RTCSourceTrackType[]) : Promise<MediaStreamTrack[]> {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for(const type of types) {
|
||||||
|
if(this.currentTracks[type]) {
|
||||||
|
result.push(this.currentTracks[type]);
|
||||||
|
this.currentTracks[type] = null;
|
||||||
|
} else {
|
||||||
|
result.push(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result.find(entry => typeof entry !== "undefined") !== -1) {
|
||||||
|
await this.updateTracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async startVideoBroadcast(type: VideoBroadcastType, config: VideoBroadcastConfig) {
|
public async startVideoBroadcast(type: VideoBroadcastType, config: VideoBroadcastConfig) {
|
||||||
let track: RTCBroadcastableTrackType;
|
let track: RTCBroadcastableTrackType;
|
||||||
let broadcastType: number;
|
let broadcastType: number;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* setup jsrenderer */
|
/* setup jsrenderer */
|
||||||
import "jsrender";
|
import "jsrender";
|
||||||
import {tr} from "./i18n/localize";
|
import {tr} from "./i18n/localize";
|
||||||
import {LogCategory, logTrace} from "tc-shared/log";
|
import {LogCategory, logError, logTrace} from "tc-shared/log";
|
||||||
|
|
||||||
if(__build.target === "web") {
|
if(__build.target === "web") {
|
||||||
(window as any).$ = require("jquery");
|
(window as any).$ = require("jquery");
|
||||||
|
@ -301,3 +301,152 @@ if(typeof ($) !== "undefined") {
|
||||||
if(!Object.values) {
|
if(!Object.values) {
|
||||||
Object.values = object => Object.keys(object).map(e => object[e]);
|
Object.values = object => Object.keys(object).map(e => object[e]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function crashOnThrow<T>(promise: Promise<T> | (() => Promise<T>)) : Promise<T> {
|
||||||
|
if(typeof promise === "function") {
|
||||||
|
try {
|
||||||
|
promise = promise();
|
||||||
|
} catch (error) {
|
||||||
|
promise = Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.catch(error => {
|
||||||
|
/* TODO: Crash screen of the app? */
|
||||||
|
logError(LogCategory.GENERAL, tr("Critical app error: %o"), error);
|
||||||
|
|
||||||
|
/* Lets make this promise stuck for ever */
|
||||||
|
return new Promise(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ignorePromise<T>(_promise: Promise<T>) {}
|
||||||
|
|
||||||
|
export function NoThrow(target: any, methodName: string, descriptor: PropertyDescriptor) {
|
||||||
|
const crashApp = error => {
|
||||||
|
/* TODO: Crash screen of the app? */
|
||||||
|
logError(LogCategory.GENERAL, tr("Critical app error: %o"), error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const promiseAccepted = { value: false };
|
||||||
|
|
||||||
|
const originalMethod: Function = descriptor.value;
|
||||||
|
descriptor.value = function () {
|
||||||
|
try {
|
||||||
|
const result = originalMethod.apply(this, arguments);
|
||||||
|
if(result instanceof Promise) {
|
||||||
|
promiseAccepted.value = true;
|
||||||
|
return result.catch(error => {
|
||||||
|
crashApp(error);
|
||||||
|
|
||||||
|
/* Lets make this promise stuck for ever since we're in a not well defined state */
|
||||||
|
return new Promise(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
crashApp(error);
|
||||||
|
|
||||||
|
if(!promiseAccepted.value) {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* We don't know if we can return a promise or if just the object is expected.
|
||||||
|
* Since we don't know that, we're just rethrowing the error for now.
|
||||||
|
*/
|
||||||
|
return new Promise(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const kCallOnceSymbol = Symbol("call-once-data");
|
||||||
|
export function CallOnce(target: any, methodName: string, descriptor: PropertyDescriptor) {
|
||||||
|
const callOnceData = target[kCallOnceSymbol] || (target[kCallOnceSymbol] = {});
|
||||||
|
|
||||||
|
const originalMethod: Function = descriptor.value;
|
||||||
|
descriptor.value = function () {
|
||||||
|
if(callOnceData[methodName]) {
|
||||||
|
debugger;
|
||||||
|
throw "method " + methodName + " has already been called";
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const kNonNullSymbol = Symbol("non-null-data");
|
||||||
|
export function NonNull(target: any, methodName: string, parameterIndex: number) {
|
||||||
|
const nonNullInfo = target[kNonNullSymbol] || (target[kNonNullSymbol] = {});
|
||||||
|
const methodInfo = nonNullInfo[methodName] || (nonNullInfo[methodName] = {});
|
||||||
|
if(!Array.isArray(methodInfo.indexes)) {
|
||||||
|
/* Initialize method info */
|
||||||
|
methodInfo.overloaded = false;
|
||||||
|
methodInfo.indexes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
methodInfo.indexes.push(parameterIndex);
|
||||||
|
setImmediate(() => {
|
||||||
|
if(methodInfo.overloaded || methodInfo.missingWarned) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
methodInfo.missingWarned = true;
|
||||||
|
logError(LogCategory.GENERAL, "Method %s has been constrained but the @Constrained decoration is missing.");
|
||||||
|
debugger;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class or method has been constrained
|
||||||
|
*/
|
||||||
|
export function ParameterConstrained(target: any, methodName: string, descriptor: PropertyDescriptor) {
|
||||||
|
const nonNullInfo = target[kNonNullSymbol];
|
||||||
|
if(!nonNullInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodInfo = nonNullInfo[methodName] || (nonNullInfo[methodName] = {});
|
||||||
|
if(!methodInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
methodInfo.overloaded = true;
|
||||||
|
const originalMethod: Function = descriptor.value;
|
||||||
|
descriptor.value = function () {
|
||||||
|
for(let index = 0; index < methodInfo.indexes.length; index++) {
|
||||||
|
const argument = arguments[methodInfo.indexes[index]];
|
||||||
|
if(typeof argument === undefined || typeof argument === null) {
|
||||||
|
throw "parameter " + methodInfo.indexes[index] + " should not be null or undefined";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMethod.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestClass {
|
||||||
|
@NoThrow
|
||||||
|
noThrow0() { }
|
||||||
|
|
||||||
|
@NoThrow
|
||||||
|
async noThrow1() {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NoThrow
|
||||||
|
noThrow2() { throw "expected"; }
|
||||||
|
|
||||||
|
@NoThrow
|
||||||
|
async noThrow3() {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1));
|
||||||
|
throw "expected";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterConstrained
|
||||||
|
nonNull0(@NonNull value: number) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(window as any).TestClass = TestClass;
|
|
@ -260,12 +260,13 @@ export class ClientEntry<Events extends ClientEvents = ClientEvents> extends Cha
|
||||||
switch (event.newState) {
|
switch (event.newState) {
|
||||||
case VoicePlayerState.PLAYING:
|
case VoicePlayerState.PLAYING:
|
||||||
case VoicePlayerState.STOPPING:
|
case VoicePlayerState.STOPPING:
|
||||||
this.speaking = true;
|
this.setSpeaking(true);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case VoicePlayerState.STOPPED:
|
case VoicePlayerState.STOPPED:
|
||||||
case VoicePlayerState.INITIALIZING:
|
case VoicePlayerState.INITIALIZING:
|
||||||
this.speaking = false;
|
default:
|
||||||
|
this.setSpeaking(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -698,14 +699,22 @@ export class ClientEntry<Events extends ClientEvents = ClientEvents> extends Cha
|
||||||
return ClientEntry.chatTag(this.clientId(), this.clientNickName(), this.clientUid(), braces);
|
return ClientEntry.chatTag(this.clientId(), this.clientNickName(), this.clientUid(), braces);
|
||||||
}
|
}
|
||||||
|
|
||||||
set speaking(flag) {
|
/** @deprecated Don't use this any more! */
|
||||||
if(flag === this._speaking) return;
|
set speaking(flag: boolean) {
|
||||||
this._speaking = flag;
|
this.setSpeaking(!!flag);
|
||||||
this.events.fire("notify_speak_state_change", { speaking: flag });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isSpeaking() { return this._speaking; }
|
isSpeaking() { return this._speaking; }
|
||||||
|
|
||||||
|
protected setSpeaking(flag: boolean) {
|
||||||
|
if(this._speaking === flag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._speaking = flag;
|
||||||
|
this.events.fire("notify_speak_state_change", { speaking: flag });
|
||||||
|
}
|
||||||
|
|
||||||
updateVariables(...variables: {key: string, value: string}[]) {
|
updateVariables(...variables: {key: string, value: string}[]) {
|
||||||
|
|
||||||
let reorder_channel = false;
|
let reorder_channel = false;
|
||||||
|
@ -914,6 +923,10 @@ export class LocalClientEntry extends ClientEntry {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSpeaking(flag: boolean) {
|
||||||
|
super.setSpeaking(flag);
|
||||||
|
}
|
||||||
|
|
||||||
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
|
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
|
||||||
contextmenu.spawn_context_menu(x, y,
|
contextmenu.spawn_context_menu(x, y,
|
||||||
...this.contextmenu_info(), {
|
...this.contextmenu_info(), {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer";
|
||||||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||||
import {ImagePreviewHook} from "tc-shared/ui/frames/ImagePreview";
|
import {ImagePreviewHook} from "tc-shared/ui/frames/ImagePreview";
|
||||||
import {InternalModalHook} from "tc-shared/ui/react-elements/modal/internal";
|
import {InternalModalHook} from "tc-shared/ui/react-elements/modal/internal";
|
||||||
|
import {TooltipHook} from "tc-shared/ui/react-elements/Tooltip";
|
||||||
|
|
||||||
const cssStyle = require("./AppRenderer.scss");
|
const cssStyle = require("./AppRenderer.scss");
|
||||||
const VideoFrame = React.memo((props: { events: Registry<AppUiEvents> }) => {
|
const VideoFrame = React.memo((props: { events: Registry<AppUiEvents> }) => {
|
||||||
|
@ -110,6 +111,10 @@ export const TeaAppMainView = (props: {
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<InternalModalHook />
|
<InternalModalHook />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
|
<ErrorBoundary>
|
||||||
|
<TooltipHook />
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {AbstractInput, FilterMode, LevelMeter} from "tc-shared/voice/RecorderBase";
|
import {AbstractInput, FilterMode, LevelMeter} from "tc-shared/voice/RecorderBase";
|
||||||
import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log";
|
import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log";
|
||||||
import {defaultRecorder} from "tc-shared/voice/RecorderProfile";
|
import {ConnectionRecorderProfileOwner, defaultRecorder, RecorderProfileOwner} from "tc-shared/voice/RecorderProfile";
|
||||||
import {getRecorderBackend, InputDevice} from "tc-shared/audio/Recorder";
|
import {getRecorderBackend, InputDevice} from "tc-shared/audio/Recorder";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
import {getBackend} from "tc-shared/backend";
|
import {getBackend} from "tc-shared/backend";
|
||||||
|
@ -16,6 +16,7 @@ import {
|
||||||
import {spawnInputProcessorModal} from "tc-shared/ui/modal/input-processor/Controller";
|
import {spawnInputProcessorModal} from "tc-shared/ui/modal/input-processor/Controller";
|
||||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
|
import {ignorePromise} from "tc-shared/proto";
|
||||||
|
|
||||||
export function initialize_audio_microphone_controller(events: Registry<MicrophoneSettingsEvents>) {
|
export function initialize_audio_microphone_controller(events: Registry<MicrophoneSettingsEvents>) {
|
||||||
const recorderBackend = getRecorderBackend();
|
const recorderBackend = getRecorderBackend();
|
||||||
|
@ -438,25 +439,26 @@ export function initialize_audio_microphone_controller(events: Registry<Micropho
|
||||||
|
|
||||||
/* TODO: Only do this on user request? */
|
/* TODO: Only do this on user request? */
|
||||||
{
|
{
|
||||||
const ownDefaultRecorder = () => {
|
const oldOwner = defaultRecorder.getOwner();
|
||||||
const originalHandlerId = defaultRecorder.current_handler?.handlerId;
|
let originalHandlerId = oldOwner instanceof ConnectionRecorderProfileOwner ? oldOwner.getConnection().handlerId : undefined;
|
||||||
defaultRecorder.unmount().then(() => {
|
|
||||||
defaultRecorder.input.start().catch(error => {
|
ignorePromise(defaultRecorder.ownRecorder(new class extends RecorderProfileOwner {
|
||||||
|
protected handleRecorderInput(input: AbstractInput) {
|
||||||
|
input.start().catch(error => {
|
||||||
logError(LogCategory.AUDIO, tr("Failed to start default input: %o"), error);
|
logError(LogCategory.AUDIO, tr("Failed to start default input: %o"), error);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
protected handleUnmount() {
|
||||||
|
/* We've been passed to somewhere else */
|
||||||
|
originalHandlerId = undefined;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
events.on("notify_destroy", () => {
|
events.on("notify_destroy", () => {
|
||||||
server_connections.findConnection(originalHandlerId)?.acquireInputHardware().catch(error => {
|
server_connections.findConnection(originalHandlerId)?.acquireInputHardware().catch(error => {
|
||||||
logError(LogCategory.GENERAL, tr("Failed to acquire microphone after settings detach: %o"), error);
|
logError(LogCategory.GENERAL, tr("Failed to acquire microphone after settings detach: %o"), error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
if(defaultRecorder.input) {
|
|
||||||
ownDefaultRecorder();
|
|
||||||
} else {
|
|
||||||
events.on("notify_destroy", defaultRecorder.events.one("notify_input_initialized", () => ownDefaultRecorder()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,6 +11,7 @@ interface GlobalTooltipState {
|
||||||
tooltipId: string;
|
tooltipId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globalTooltipRef = React.createRef<GlobalTooltip>();
|
||||||
class GlobalTooltip extends React.Component<{}, GlobalTooltipState> {
|
class GlobalTooltip extends React.Component<{}, GlobalTooltipState> {
|
||||||
private currentTooltip_: Tooltip;
|
private currentTooltip_: Tooltip;
|
||||||
private isUnmount: boolean;
|
private isUnmount: boolean;
|
||||||
|
@ -160,7 +161,4 @@ export const IconTooltip = (props: { children?: React.ReactElement | React.React
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
const globalTooltipRef = React.createRef<GlobalTooltip>();
|
export const TooltipHook = React.memo(() => <GlobalTooltip ref={globalTooltipRef} />);
|
||||||
const tooltipContainer = document.createElement("div");
|
|
||||||
document.body.appendChild(tooltipContainer);
|
|
||||||
ReactDOM.render(<GlobalTooltip ref={globalTooltipRef} />, tooltipContainer);
|
|
|
@ -8,12 +8,22 @@ import {
|
||||||
} from "tc-shared/ui/react-elements/modal/Renderer";
|
} from "tc-shared/ui/react-elements/modal/Renderer";
|
||||||
|
|
||||||
import "./ModalRenderer.scss";
|
import "./ModalRenderer.scss";
|
||||||
|
import {TooltipHook} from "tc-shared/ui/react-elements/Tooltip";
|
||||||
|
import {ImagePreviewHook} from "tc-shared/ui/frames/ImagePreview";
|
||||||
|
|
||||||
export interface ModalControlFunctions {
|
export interface ModalControlFunctions {
|
||||||
close();
|
close();
|
||||||
minimize();
|
minimize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GlobalHooks = React.memo((props: { children }) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<ImagePreviewHook />
|
||||||
|
<TooltipHook />
|
||||||
|
<React.Fragment>{props.children}</React.Fragment>
|
||||||
|
</React.Fragment>
|
||||||
|
));
|
||||||
|
|
||||||
export class ModalRenderer {
|
export class ModalRenderer {
|
||||||
private readonly functionController: ModalControlFunctions;
|
private readonly functionController: ModalControlFunctions;
|
||||||
private readonly container: HTMLDivElement;
|
private readonly container: HTMLDivElement;
|
||||||
|
@ -33,6 +43,7 @@ export class ModalRenderer {
|
||||||
|
|
||||||
if(__build.target === "client") {
|
if(__build.target === "client") {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
<GlobalHooks>
|
||||||
<ModalFrameRenderer windowed={true}>
|
<ModalFrameRenderer windowed={true}>
|
||||||
<ModalFrameTopRenderer
|
<ModalFrameTopRenderer
|
||||||
replacePageTitle={true}
|
replacePageTitle={true}
|
||||||
|
@ -42,11 +53,13 @@ export class ModalRenderer {
|
||||||
onMinimize={() => this.functionController.minimize()}
|
onMinimize={() => this.functionController.minimize()}
|
||||||
/>
|
/>
|
||||||
<ModalBodyRenderer modalInstance={modal} />
|
<ModalBodyRenderer modalInstance={modal} />
|
||||||
</ModalFrameRenderer>,
|
</ModalFrameRenderer>
|
||||||
|
</GlobalHooks>,
|
||||||
this.container
|
this.container
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
<GlobalHooks>
|
||||||
<WindowModalRenderer>
|
<WindowModalRenderer>
|
||||||
<ModalFrameTopRenderer
|
<ModalFrameTopRenderer
|
||||||
replacePageTitle={true}
|
replacePageTitle={true}
|
||||||
|
@ -56,7 +69,8 @@ export class ModalRenderer {
|
||||||
onMinimize={() => this.functionController.minimize()}
|
onMinimize={() => this.functionController.minimize()}
|
||||||
/>
|
/>
|
||||||
<ModalBodyRenderer modalInstance={modal} />
|
<ModalBodyRenderer modalInstance={modal} />
|
||||||
</WindowModalRenderer>,
|
</WindowModalRenderer>
|
||||||
|
</GlobalHooks>,
|
||||||
this.container
|
this.container
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,11 @@ import {Settings, settings} from "../settings";
|
||||||
import {ConnectionHandler} from "../ConnectionHandler";
|
import {ConnectionHandler} from "../ConnectionHandler";
|
||||||
import {getRecorderBackend, InputDevice} from "../audio/Recorder";
|
import {getRecorderBackend, InputDevice} from "../audio/Recorder";
|
||||||
import {FilterType, StateFilter, ThresholdFilter} from "../voice/Filter";
|
import {FilterType, StateFilter, ThresholdFilter} from "../voice/Filter";
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {getAudioBackend} from "tc-shared/audio/Player";
|
import {getAudioBackend} from "tc-shared/audio/Player";
|
||||||
|
import {Mutex} from "tc-shared/Mutex";
|
||||||
|
import {NoThrow} from "tc-shared/proto";
|
||||||
|
|
||||||
export type VadType = "threshold" | "push_to_talk" | "active";
|
export type VadType = "threshold" | "push_to_talk" | "active";
|
||||||
export interface RecorderProfileConfig {
|
export interface RecorderProfileConfig {
|
||||||
|
@ -57,23 +59,42 @@ export interface RecorderProfileEvents {
|
||||||
notify_input_initialized: { },
|
notify_input_initialized: { },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class RecorderProfileOwner {
|
||||||
|
/**
|
||||||
|
* This method will be called from the recorder profile.
|
||||||
|
*/
|
||||||
|
protected abstract handleUnmount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This callback will be called when the recorder audio input has
|
||||||
|
* been initialized.
|
||||||
|
* Note: This method might be called within ownRecorder().
|
||||||
|
* If this method has been called, handleUnmount will be called.
|
||||||
|
*
|
||||||
|
* @param input The target input.
|
||||||
|
*/
|
||||||
|
protected abstract handleRecorderInput(input: AbstractInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class ConnectionRecorderProfileOwner extends RecorderProfileOwner {
|
||||||
|
abstract getConnection() : ConnectionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
export class RecorderProfile {
|
export class RecorderProfile {
|
||||||
readonly events: Registry<RecorderProfileEvents>;
|
readonly events: Registry<RecorderProfileEvents>;
|
||||||
readonly name;
|
readonly name;
|
||||||
readonly volatile; /* not saving profile */
|
readonly volatile; /* not saving profile */
|
||||||
|
|
||||||
config: RecorderProfileConfig;
|
config: RecorderProfileConfig;
|
||||||
input: AbstractInput;
|
/* TODO! */
|
||||||
|
/* private */input: AbstractInput;
|
||||||
|
|
||||||
|
private currentOwner: RecorderProfileOwner;
|
||||||
|
private currentOwnerMutex: Mutex<void>;
|
||||||
|
|
||||||
|
/* FIXME: Remove this! */
|
||||||
current_handler: ConnectionHandler;
|
current_handler: ConnectionHandler;
|
||||||
|
|
||||||
/* attention: this callback will only be called when the audio input hasn't been initialized! */
|
|
||||||
callback_input_initialized: (input: AbstractInput) => void;
|
|
||||||
callback_start: () => any;
|
|
||||||
callback_stop: () => any;
|
|
||||||
|
|
||||||
callback_unmount: () => any; /* called if somebody else takes the ownership */
|
|
||||||
|
|
||||||
private readonly pptHook: KeyHook;
|
private readonly pptHook: KeyHook;
|
||||||
private pptTimeout: number;
|
private pptTimeout: number;
|
||||||
private pptHookRegistered: boolean;
|
private pptHookRegistered: boolean;
|
||||||
|
@ -87,6 +108,7 @@ export class RecorderProfile {
|
||||||
this.events = new Registry<RecorderProfileEvents>();
|
this.events = new Registry<RecorderProfileEvents>();
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.volatile = typeof(volatile) === "boolean" ? volatile : false;
|
this.volatile = typeof(volatile) === "boolean" ? volatile : false;
|
||||||
|
this.currentOwnerMutex = new Mutex<void>(void 0);
|
||||||
|
|
||||||
this.pptHook = {
|
this.pptHook = {
|
||||||
callbackRelease: () => {
|
callbackRelease: () => {
|
||||||
|
@ -161,18 +183,12 @@ export class RecorderProfile {
|
||||||
this.input = getRecorderBackend().createInput();
|
this.input = getRecorderBackend().createInput();
|
||||||
|
|
||||||
this.input.events.on("notify_voice_start", () => {
|
this.input.events.on("notify_voice_start", () => {
|
||||||
logDebug(LogCategory.VOICE, "Voice start");
|
logDebug(LogCategory.VOICE, tr("Voice recorder %s: Voice started."), this.name);
|
||||||
if(this.callback_start) {
|
|
||||||
this.callback_start();
|
|
||||||
}
|
|
||||||
this.events.fire("notify_voice_start");
|
this.events.fire("notify_voice_start");
|
||||||
});
|
});
|
||||||
|
|
||||||
this.input.events.on("notify_voice_end", () => {
|
this.input.events.on("notify_voice_end", () => {
|
||||||
logDebug(LogCategory.VOICE, "Voice end");
|
logDebug(LogCategory.VOICE, tr("Voice recorder %s: Voice stopped."), this.name);
|
||||||
if(this.callback_stop) {
|
|
||||||
this.callback_stop();
|
|
||||||
}
|
|
||||||
this.events.fire("notify_voice_end");
|
this.events.fire("notify_voice_end");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -183,12 +199,8 @@ export class RecorderProfile {
|
||||||
this.registeredFilter["threshold"] = this.input.createFilter(FilterType.THRESHOLD, 100);
|
this.registeredFilter["threshold"] = this.input.createFilter(FilterType.THRESHOLD, 100);
|
||||||
this.registeredFilter["threshold"].setEnabled(false);
|
this.registeredFilter["threshold"].setEnabled(false);
|
||||||
|
|
||||||
if(this.callback_input_initialized) {
|
|
||||||
this.callback_input_initialized(this.input);
|
|
||||||
}
|
|
||||||
this.events.fire("notify_input_initialized");
|
this.events.fire("notify_input_initialized");
|
||||||
|
|
||||||
|
|
||||||
/* apply initial config values */
|
/* apply initial config values */
|
||||||
this.input.setVolume(this.config.volume / 100);
|
this.input.setVolume(this.config.volume / 100);
|
||||||
if(this.config.device_id) {
|
if(this.config.device_id) {
|
||||||
|
@ -267,26 +279,47 @@ export class RecorderProfile {
|
||||||
this.input.setFilterMode(FilterMode.Filter);
|
this.input.setFilterMode(FilterMode.Filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async unmount() : Promise<void> {
|
/**
|
||||||
if(this.callback_unmount) {
|
* Own the recorder.
|
||||||
this.callback_unmount();
|
*/
|
||||||
}
|
@NoThrow
|
||||||
|
async ownRecorder(target: RecorderProfileOwner | undefined) {
|
||||||
if(this.input) {
|
await this.currentOwnerMutex.execute(async () => {
|
||||||
|
if(this.currentOwner) {
|
||||||
try {
|
try {
|
||||||
|
this.currentOwner["handleUnmount"]();
|
||||||
|
} catch (error) {
|
||||||
|
logError(LogCategory.AUDIO, tr("Failed to invoke unmount method on the current owner: %o"), error);
|
||||||
|
}
|
||||||
|
this.currentOwner = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentOwner = target;
|
||||||
|
if(this.input) {
|
||||||
await this.input.setConsumer(undefined);
|
await this.input.setConsumer(undefined);
|
||||||
} catch(error) {
|
|
||||||
logWarn(LogCategory.VOICE, tr("Failed to unmount input consumer for profile (%o)"), error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* this.input.setFilterMode(FilterMode.Block); */
|
if(this.currentOwner && this.input) {
|
||||||
|
try {
|
||||||
|
this.currentOwner["handleRecorderInput"](this.input);
|
||||||
|
} catch (error) {
|
||||||
|
logError(LogCategory.AUDIO, tr("Failed to call handleRecorderInput on the current owner: %o"), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.callback_input_initialized = undefined;
|
getOwner() : RecorderProfileOwner | undefined {
|
||||||
this.callback_start = undefined;
|
return this.currentOwner;
|
||||||
this.callback_stop = undefined;
|
}
|
||||||
this.callback_unmount = undefined;
|
|
||||||
this.current_handler = undefined;
|
isInputActive() : boolean {
|
||||||
|
return typeof this.input !== "undefined" && !this.input.isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use `ownRecorder(undefined)` */
|
||||||
|
async unmount() : Promise<void> {
|
||||||
|
await this.ownRecorder(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
getVadType() { return this.config.vad_type; }
|
getVadType() { return this.config.vad_type; }
|
||||||
|
@ -343,8 +376,9 @@ export class RecorderProfile {
|
||||||
|
|
||||||
getPushToTalkDelay() { return this.config.vad_push_to_talk.delay; }
|
getPushToTalkDelay() { return this.config.vad_push_to_talk.delay; }
|
||||||
setPushToTalkDelay(value: number) {
|
setPushToTalkDelay(value: number) {
|
||||||
if(this.config.vad_push_to_talk.delay === value)
|
if(this.config.vad_push_to_talk.delay === value) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.config.vad_push_to_talk.delay = value;
|
this.config.vad_push_to_talk.delay = value;
|
||||||
this.save();
|
this.save();
|
||||||
|
@ -371,8 +405,9 @@ export class RecorderProfile {
|
||||||
|
|
||||||
getVolume() : number { return this.input ? (this.input.getVolume() * 100) : this.config.volume; }
|
getVolume() : number { return this.input ? (this.input.getVolume() * 100) : this.config.volume; }
|
||||||
setVolume(volume: number) {
|
setVolume(volume: number) {
|
||||||
if(this.config.volume === volume)
|
if(this.config.volume === volume) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.config.volume = volume;
|
this.config.volume = volume;
|
||||||
this.input?.setVolume(volume / 100);
|
this.input?.setVolume(volume / 100);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
VoiceConnectionStatus,
|
VoiceConnectionStatus,
|
||||||
WhisperSessionInitializer
|
WhisperSessionInitializer
|
||||||
} from "tc-shared/connection/VoiceConnection";
|
} from "tc-shared/connection/VoiceConnection";
|
||||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
import {ConnectionRecorderProfileOwner, RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||||
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
||||||
import {
|
import {
|
||||||
kUnknownWhisperClientUniqueId,
|
kUnknownWhisperClientUniqueId,
|
||||||
|
@ -16,19 +16,18 @@ import {AbstractServerConnection, ConnectionStatistics} from "tc-shared/connecti
|
||||||
import {VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
import {VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
||||||
import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log";
|
import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log";
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
import {InputConsumerType} from "tc-shared/voice/RecorderBase";
|
import {AbstractInput, InputConsumerType} from "tc-shared/voice/RecorderBase";
|
||||||
import {getAudioBackend} from "tc-shared/audio/Player";
|
import {getAudioBackend} from "tc-shared/audio/Player";
|
||||||
import {RtpVoiceClient} from "./VoiceClient";
|
import {RtpVoiceClient} from "./VoiceClient";
|
||||||
import {RtpWhisperSession} from "./WhisperClient";
|
import {RtpWhisperSession} from "./WhisperClient";
|
||||||
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
|
import {CallOnce, crashOnThrow, ignorePromise} from "tc-shared/proto";
|
||||||
|
|
||||||
type CancelableWhisperTarget = WhisperTarget & { canceled: boolean };
|
type CancelableWhisperTarget = WhisperTarget & { canceled: boolean };
|
||||||
export class RtpVoiceConnection extends AbstractVoiceConnection {
|
export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
private readonly rtcConnection: RTCConnection;
|
private readonly rtcConnection: RTCConnection;
|
||||||
|
|
||||||
private readonly listenerRtcAudioAssignment;
|
private listenerCallbacks: (() => void)[];
|
||||||
private readonly listenerRtcStateChanged;
|
|
||||||
private listenerClientMoved;
|
|
||||||
private listenerSpeakerStateChanged;
|
|
||||||
|
|
||||||
private connectionState: VoiceConnectionStatus;
|
private connectionState: VoiceConnectionStatus;
|
||||||
private localFailedReason: string;
|
private localFailedReason: string;
|
||||||
|
@ -36,9 +35,11 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
private localAudioDestination: MediaStreamAudioDestinationNode;
|
private localAudioDestination: MediaStreamAudioDestinationNode;
|
||||||
private currentAudioSourceNode: AudioNode;
|
private currentAudioSourceNode: AudioNode;
|
||||||
private currentAudioSource: RecorderProfile;
|
private currentAudioSource: RecorderProfile;
|
||||||
|
private ignoreRecorderUnmount: boolean;
|
||||||
|
private currentAudioListener: (() => void)[];
|
||||||
|
|
||||||
private speakerMuted: boolean;
|
private speakerMuted: boolean;
|
||||||
private voiceClients: RtpVoiceClient[] = [];
|
private voiceClients: {[T: number]: RtpVoiceClient} = {};
|
||||||
|
|
||||||
private whisperSessionInitializer: WhisperSessionInitializer | undefined;
|
private whisperSessionInitializer: WhisperSessionInitializer | undefined;
|
||||||
private whisperSessions: RtpWhisperSession[] = [];
|
private whisperSessions: RtpWhisperSession[] = [];
|
||||||
|
@ -56,19 +57,25 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
this.voiceClientStateChangedEventListener = this.handleVoiceClientStateChange.bind(this);
|
this.voiceClientStateChangedEventListener = this.handleVoiceClientStateChange.bind(this);
|
||||||
this.whisperSessionStateChangedEventListener = this.handleWhisperSessionStateChange.bind(this);
|
this.whisperSessionStateChangedEventListener = this.handleWhisperSessionStateChange.bind(this);
|
||||||
|
|
||||||
this.rtcConnection.getEvents().on("notify_audio_assignment_changed",
|
this.listenerCallbacks = [];
|
||||||
this.listenerRtcAudioAssignment = event => this.handleAudioAssignmentChanged(event));
|
this.listenerCallbacks.push(
|
||||||
|
this.rtcConnection.getEvents().on("notify_audio_assignment_changed", event => this.handleAudioAssignmentChanged(event))
|
||||||
|
);
|
||||||
|
|
||||||
this.rtcConnection.getEvents().on("notify_state_changed",
|
this.listenerCallbacks.push(
|
||||||
this.listenerRtcStateChanged = event => this.handleRtcConnectionStateChanged(event));
|
this.rtcConnection.getEvents().on("notify_state_changed", event => this.handleRtcConnectionStateChanged(event))
|
||||||
|
);
|
||||||
|
|
||||||
this.listenerSpeakerStateChanged = connection.client.events().on("notify_state_updated", event => {
|
this.listenerCallbacks.push(
|
||||||
|
connection.client.events().on("notify_state_updated", event => {
|
||||||
if(event.state === "speaker") {
|
if(event.state === "speaker") {
|
||||||
this.updateSpeakerState();
|
this.updateSpeakerState();
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.listenerClientMoved = this.rtcConnection.getConnection().command_handler_boss().register_explicit_handler("notifyclientmoved", event => {
|
this.listenerCallbacks.push(
|
||||||
|
this.rtcConnection.getConnection().command_handler_boss().register_explicit_handler("notifyclientmoved", event => {
|
||||||
const localClientId = this.rtcConnection.getConnection().client.getClientId();
|
const localClientId = this.rtcConnection.getConnection().client.getClientId();
|
||||||
for(const data of event.arguments) {
|
for(const data of event.arguments) {
|
||||||
if(parseInt(data["clid"]) === localClientId) {
|
if(parseInt(data["clid"]) === localClientId) {
|
||||||
|
@ -81,7 +88,8 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.speakerMuted = connection.client.isSpeakerMuted() || connection.client.isSpeakerDisabled();
|
this.speakerMuted = connection.client.isSpeakerMuted() || connection.client.isSpeakerDisabled();
|
||||||
|
|
||||||
|
@ -96,37 +104,32 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
this.setWhisperSessionInitializer(undefined);
|
this.setWhisperSessionInitializer(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CallOnce
|
||||||
destroy() {
|
destroy() {
|
||||||
if(this.listenerClientMoved) {
|
this.listenerCallbacks?.forEach(callback => callback());
|
||||||
this.listenerClientMoved();
|
this.listenerCallbacks = undefined;
|
||||||
this.listenerClientMoved = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.listenerSpeakerStateChanged) {
|
this.ignoreRecorderUnmount = true;
|
||||||
this.listenerSpeakerStateChanged();
|
this.acquireVoiceRecorder(undefined).catch(error => {
|
||||||
this.listenerSpeakerStateChanged = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rtcConnection.getEvents().off("notify_audio_assignment_changed", this.listenerRtcAudioAssignment);
|
|
||||||
this.rtcConnection.getEvents().off("notify_state_changed", this.listenerRtcStateChanged);
|
|
||||||
|
|
||||||
this.acquireVoiceRecorder(undefined, true).catch(error => {
|
|
||||||
logWarn(LogCategory.VOICE, tr("Failed to release voice recorder: %o"), error);
|
logWarn(LogCategory.VOICE, tr("Failed to release voice recorder: %o"), error);
|
||||||
}).then(() => {
|
|
||||||
for(const client of Object.values(this.voiceClients)) {
|
|
||||||
client.abortReplay();
|
|
||||||
}
|
|
||||||
this.voiceClients = undefined;
|
|
||||||
this.currentAudioSource = undefined;
|
|
||||||
});
|
});
|
||||||
if(Object.keys(this.voiceClients).length !== 0) {
|
|
||||||
logWarn(LogCategory.AUDIO, tr("Voice connection will be destroyed, but some voice clients are still left (%d)."), Object.keys(this.voiceClients).length);
|
for(const key of Object.keys(this.voiceClients)) {
|
||||||
|
const client = this.voiceClients[key];
|
||||||
|
delete this.voiceClients[key];
|
||||||
|
|
||||||
|
client.abortReplay();
|
||||||
|
client.destroy();
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
const whisperSessions = Object.keys(this.whisperSessions);
|
this.currentAudioSource = undefined;
|
||||||
whisperSessions.forEach(session => this.whisperSessions[session].destroy());
|
|
||||||
this.whisperSessions = {};
|
for(const client of this.whisperSessions) {
|
||||||
*/
|
client.getVoicePlayer()?.abortReplay();
|
||||||
|
client.destroy();
|
||||||
|
}
|
||||||
|
this.whisperSessions = [];
|
||||||
|
|
||||||
this.events.destroy();
|
this.events.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,52 +150,73 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentAudioListener?.forEach(callback => callback());
|
||||||
|
this.currentAudioListener = undefined;
|
||||||
|
|
||||||
if(this.currentAudioSource) {
|
if(this.currentAudioSource) {
|
||||||
this.currentAudioSourceNode?.disconnect(this.localAudioDestination);
|
this.currentAudioSourceNode?.disconnect(this.localAudioDestination);
|
||||||
this.currentAudioSourceNode = undefined;
|
this.currentAudioSourceNode = undefined;
|
||||||
|
|
||||||
this.currentAudioSource.callback_unmount = undefined;
|
this.ignoreRecorderUnmount = true;
|
||||||
await this.currentAudioSource.unmount();
|
await this.currentAudioSource.ownRecorder(undefined);
|
||||||
|
this.ignoreRecorderUnmount = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* unmount our target recorder */
|
|
||||||
await recorder?.unmount();
|
|
||||||
|
|
||||||
this.handleRecorderStop();
|
|
||||||
const oldRecorder = recorder;
|
const oldRecorder = recorder;
|
||||||
this.currentAudioSource = recorder;
|
this.currentAudioSource = recorder;
|
||||||
|
|
||||||
if(recorder) {
|
if(recorder) {
|
||||||
recorder.current_handler = this.connection.client;
|
const rtpConnection = this;
|
||||||
|
await recorder.ownRecorder(new class extends ConnectionRecorderProfileOwner {
|
||||||
|
getConnection(): ConnectionHandler {
|
||||||
|
return rtpConnection.connection.client;
|
||||||
|
}
|
||||||
|
|
||||||
recorder.callback_unmount = this.handleRecorderUnmount.bind(this);
|
protected handleRecorderInput(input: AbstractInput) {
|
||||||
recorder.callback_start = this.handleRecorderStart.bind(this);
|
input.setConsumer({
|
||||||
recorder.callback_stop = this.handleRecorderStop.bind(this);
|
|
||||||
|
|
||||||
recorder.callback_input_initialized = async input => {
|
|
||||||
await input.setConsumer({
|
|
||||||
type: InputConsumerType.NODE,
|
type: InputConsumerType.NODE,
|
||||||
callbackDisconnect: node => {
|
callbackDisconnect: node => {
|
||||||
this.currentAudioSourceNode = undefined;
|
if(rtpConnection.currentAudioSourceNode !== node) {
|
||||||
node.disconnect(this.localAudioDestination);
|
/* We're not connected */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rtpConnection.currentAudioSourceNode = undefined;
|
||||||
|
if(rtpConnection.localAudioDestination) {
|
||||||
|
node.disconnect(rtpConnection.localAudioDestination);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
callbackNode: node => {
|
callbackNode: node => {
|
||||||
this.currentAudioSourceNode = node;
|
if(rtpConnection.currentAudioSourceNode === node) {
|
||||||
if(this.localAudioDestination) {
|
return;
|
||||||
node.connect(this.localAudioDestination);
|
}
|
||||||
|
|
||||||
|
if(rtpConnection.localAudioDestination) {
|
||||||
|
rtpConnection.currentAudioSourceNode?.disconnect(rtpConnection.localAudioDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
rtpConnection.currentAudioSourceNode = node;
|
||||||
|
if(rtpConnection.localAudioDestination) {
|
||||||
|
node.connect(rtpConnection.localAudioDestination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
if(recorder.input) {
|
|
||||||
recorder.callback_input_initialized(recorder.input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!recorder.input || recorder.input.isFiltered()) {
|
protected handleUnmount() {
|
||||||
this.handleRecorderStop();
|
rtpConnection.handleRecorderUnmount();
|
||||||
} else {
|
|
||||||
this.handleRecorderStart();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentAudioListener = [];
|
||||||
|
this.currentAudioListener.push(recorder.events.on("notify_voice_start", () => this.handleRecorderStart()));
|
||||||
|
this.currentAudioListener.push(recorder.events.on("notify_voice_end", () => this.handleRecorderStop(tr("recorder event"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.currentAudioSource?.isInputActive()) {
|
||||||
|
this.handleRecorderStart();
|
||||||
|
} else {
|
||||||
|
this.handleRecorderStop(tr("recorder change"));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.events.fire("notify_recorder_changed", {
|
this.events.fire("notify_recorder_changed", {
|
||||||
|
@ -201,27 +225,13 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleRecorderStop() {
|
private handleRecorderStop(reason: string) {
|
||||||
const chandler = this.connection.client;
|
const chandler = this.connection.client;
|
||||||
const ch = chandler.getClient();
|
chandler.getClient()?.setSpeaking(false);
|
||||||
if(ch) ch.speaking = false;
|
|
||||||
|
|
||||||
if(!chandler.connected) {
|
logInfo(LogCategory.VOICE, tr("Received local voice end signal (%s)"), reason);
|
||||||
return false;
|
this.rtcConnection.clearTrackSources(["audio", "audio-whisper"]).catch(error => {
|
||||||
}
|
logError(LogCategory.AUDIO, tr("Failed to stop/remove audio RTC tracks: %o"), error);
|
||||||
|
|
||||||
if(chandler.isMicrophoneMuted()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logInfo(LogCategory.VOICE, tr("Local voice ended"));
|
|
||||||
|
|
||||||
this.rtcConnection.setTrackSource("audio", null).catch(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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,11 +244,11 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
|
|
||||||
logInfo(LogCategory.VOICE, tr("Local voice started"));
|
logInfo(LogCategory.VOICE, tr("Local voice started"));
|
||||||
|
|
||||||
const ch = chandler.getClient();
|
chandler.getClient()?.setSpeaking(true);
|
||||||
if(ch) { ch.speaking = true; }
|
|
||||||
|
|
||||||
this.rtcConnection.setTrackSource(this.whisperTarget ? "audio-whisper" : "audio", this.localAudioDestination.stream.getAudioTracks()[0])
|
const audioTrack = this.localAudioDestination.stream.getAudioTracks()[0];
|
||||||
.catch(error => {
|
const audioTarget = this.whisperTarget ? "audio-whisper" : "audio";
|
||||||
|
this.rtcConnection.setTrackSource(audioTarget, audioTrack).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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -246,7 +256,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
private handleRecorderUnmount() {
|
private handleRecorderUnmount() {
|
||||||
logInfo(LogCategory.VOICE, "Lost recorder!");
|
logInfo(LogCategory.VOICE, "Lost recorder!");
|
||||||
this.currentAudioSource = undefined;
|
this.currentAudioSource = undefined;
|
||||||
this.acquireVoiceRecorder(undefined, true); /* we can ignore the promise because we should finish this directly */
|
ignorePromise(crashOnThrow(this.acquireVoiceRecorder(undefined, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
isReplayingVoice(): boolean {
|
isReplayingVoice(): boolean {
|
||||||
|
@ -359,7 +369,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleRecorderStop();
|
this.handleRecorderStop(tr("whisper start"));
|
||||||
if(this.currentAudioSource?.input && !this.currentAudioSource.input.isFiltered()) {
|
if(this.currentAudioSource?.input && !this.currentAudioSource.input.isFiltered()) {
|
||||||
this.handleRecorderStart();
|
this.handleRecorderStart();
|
||||||
}
|
}
|
||||||
|
@ -379,7 +389,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleRecorderStop();
|
this.handleRecorderStop(tr("whisper stop"));
|
||||||
if(this.currentAudioSource?.input && !this.currentAudioSource.input.isFiltered()) {
|
if(this.currentAudioSource?.input && !this.currentAudioSource.input.isFiltered()) {
|
||||||
this.handleRecorderStart();
|
this.handleRecorderStart();
|
||||||
}
|
}
|
||||||
|
@ -538,7 +548,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
if(this.speakerMuted === newState) { return; }
|
if(this.speakerMuted === newState) { return; }
|
||||||
|
|
||||||
this.speakerMuted = newState;
|
this.speakerMuted = newState;
|
||||||
this.voiceClients.forEach(client => client.setGloballyMuted(this.speakerMuted));
|
Object.values(this.voiceClients).forEach(client => client.setGloballyMuted(this.speakerMuted));
|
||||||
this.whisperSessions.forEach(session => session.setGloballyMuted(this.speakerMuted));
|
this.whisperSessions.forEach(session => session.setGloballyMuted(this.speakerMuted));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue