2020-08-21 07:37:10 -04:00
|
|
|
import {
|
|
|
|
AbstractVoiceConnection,
|
2020-09-24 16:06:52 -04:00
|
|
|
VoiceConnectionStatus,
|
|
|
|
WhisperSessionInitializer
|
2020-08-21 07:37:10 -04:00
|
|
|
} from "tc-shared/connection/VoiceConnection";
|
2021-04-19 14:27:12 -04:00
|
|
|
import {ConnectionRecorderProfileOwner, RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
2020-12-02 15:00:51 -05:00
|
|
|
import {NativeServerConnection, NativeVoiceClient, NativeVoiceConnection, PlayerState} from "tc-native/connection";
|
2020-09-24 16:06:52 -04:00
|
|
|
import {ServerConnection} from "./ServerConnection";
|
|
|
|
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
|
|
|
import {WhisperSession, WhisperTarget} from "tc-shared/voice/VoiceWhisper";
|
2020-04-01 15:56:23 -04:00
|
|
|
import {NativeInput} from "../audio/AudioRecorder";
|
2021-04-19 14:27:12 -04:00
|
|
|
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
2020-09-24 16:06:52 -04:00
|
|
|
import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
|
|
|
import {Registry} from "tc-shared/events";
|
2021-04-19 14:27:12 -04:00
|
|
|
import {LogCategory, logError, logInfo, logWarn} from "tc-shared/log";
|
2020-09-24 16:06:52 -04:00
|
|
|
import {tr} from "tc-shared/i18n/localize";
|
2020-11-29 15:06:57 -05:00
|
|
|
import {ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
|
2021-04-19 14:27:12 -04:00
|
|
|
import {AbstractInput} from "tc-shared/voice/RecorderBase";
|
|
|
|
import {crashOnThrow, ignorePromise} from "tc-shared/proto";
|
2020-04-01 15:56:23 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
export class NativeVoiceConnectionWrapper extends AbstractVoiceConnection {
|
|
|
|
private readonly serverConnectionStateChangedListener;
|
|
|
|
private readonly native: NativeVoiceConnection;
|
2020-04-01 15:56:23 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
private localAudioStarted = false;
|
|
|
|
private connectionState: VoiceConnectionStatus;
|
|
|
|
private currentRecorder: RecorderProfile;
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
private ignoreRecorderUnmount: boolean;
|
|
|
|
private listenerRecorder: (() => void)[];
|
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
private registeredVoiceClients: {[key: number]: NativeVoiceClientWrapper} = {};
|
|
|
|
|
|
|
|
private currentlyReplayingAudio = false;
|
|
|
|
private readonly voiceClientStateChangedEventListener;
|
2020-04-01 15:56:23 -04:00
|
|
|
|
|
|
|
constructor(connection: ServerConnection, voice: NativeVoiceConnection) {
|
|
|
|
super(connection);
|
2020-09-24 16:06:52 -04:00
|
|
|
this.native = voice;
|
2021-04-19 14:27:12 -04:00
|
|
|
this.ignoreRecorderUnmount = false;
|
2020-09-24 16:06:52 -04:00
|
|
|
|
|
|
|
this.serverConnectionStateChangedListener = () => {
|
|
|
|
if(this.connection.getConnectionState() === ConnectionState.CONNECTED) {
|
|
|
|
this.setConnectionState(VoiceConnectionStatus.Connected);
|
|
|
|
} else {
|
|
|
|
this.setConnectionState(VoiceConnectionStatus.Disconnected);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.connection.events.on("notify_connection_state_changed", this.serverConnectionStateChangedListener);
|
|
|
|
this.connectionState = VoiceConnectionStatus.Disconnected;
|
|
|
|
|
|
|
|
this.voiceClientStateChangedEventListener = this.handleVoiceClientStateChange.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
this.connection.events.off("notify_connection_state_changed", this.serverConnectionStateChangedListener);
|
|
|
|
}
|
|
|
|
|
|
|
|
getConnectionState(): VoiceConnectionStatus {
|
|
|
|
return this.connectionState;
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
getFailedMessage(): string {
|
|
|
|
/* the native voice connection can't fail */
|
|
|
|
return "this message should never appear";
|
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
private setConnectionState(state: VoiceConnectionStatus) {
|
|
|
|
if(this.connectionState === state) {
|
2020-04-01 15:56:23 -04:00
|
|
|
return;
|
2020-09-24 16:06:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const oldState = this.connectionState;
|
|
|
|
this.connectionState = state;
|
|
|
|
this.events.fire("notify_connection_status_changed", { oldStatus: oldState, newStatus: state });
|
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
encodingSupported(codec: number): boolean {
|
|
|
|
return this.native.encoding_supported(codec);
|
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
decodingSupported(codec: number): boolean {
|
|
|
|
return this.native.decoding_supported(codec);
|
|
|
|
}
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
async acquireVoiceRecorder(recorder: RecorderProfile | undefined, enforce?: boolean): Promise<void> {
|
|
|
|
if(this.currentRecorder === recorder && !enforce) {
|
2020-09-24 16:06:52 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
this.listenerRecorder?.forEach(callback => callback());
|
|
|
|
this.listenerRecorder = undefined;
|
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
if(this.currentRecorder) {
|
2021-04-19 14:27:12 -04:00
|
|
|
this.ignoreRecorderUnmount = true;
|
|
|
|
this.ignoreRecorderUnmount = false;
|
2020-10-01 04:56:22 -04:00
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
this.native.set_audio_source(undefined);
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2021-01-08 16:04:27 -05:00
|
|
|
const oldRecorder = recorder;
|
2020-09-24 16:06:52 -04:00
|
|
|
this.currentRecorder = recorder;
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
if(this.currentRecorder) {
|
|
|
|
const connection = this;
|
|
|
|
await recorder.ownRecorder(new class extends ConnectionRecorderProfileOwner {
|
|
|
|
getConnection(): ConnectionHandler {
|
|
|
|
return connection.connection.client;
|
2020-09-24 16:06:52 -04:00
|
|
|
}
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
protected handleRecorderInput(input: AbstractInput): any {
|
|
|
|
if(!(input instanceof NativeInput)) {
|
|
|
|
logError(LogCategory.VOICE, tr("Recorder input isn't an instance of NativeInput. Ignoring recorder input."));
|
|
|
|
return;
|
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
connection.native.set_audio_source(input.getNativeConsumer());
|
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
protected handleUnmount(): any {
|
|
|
|
if(connection.ignoreRecorderUnmount) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
connection.currentRecorder = undefined;
|
|
|
|
ignorePromise(crashOnThrow(connection.acquireVoiceRecorder(undefined, true)));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.listenerRecorder = [];
|
|
|
|
this.listenerRecorder.push(recorder.events.on("notify_voice_start", () => this.handleVoiceStartEvent()));
|
|
|
|
this.listenerRecorder.push(recorder.events.on("notify_voice_end", () => this.handleVoiceEndEvent(tr("recorder event"))));
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2021-04-19 14:27:12 -04:00
|
|
|
|
|
|
|
if(this.currentRecorder?.isInputActive()) {
|
|
|
|
this.handleVoiceStartEvent();
|
|
|
|
} else {
|
|
|
|
this.handleVoiceEndEvent(tr("recorder change"));
|
|
|
|
}
|
|
|
|
|
2021-01-08 16:04:27 -05:00
|
|
|
this.events.fire("notify_recorder_changed", {
|
|
|
|
oldRecorder,
|
|
|
|
newRecorder: recorder
|
|
|
|
});
|
2020-09-24 16:06:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
voiceRecorder(): RecorderProfile {
|
|
|
|
return this.currentRecorder;
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
getEncoderCodec(): number {
|
|
|
|
return this.native.get_encoder_codec();
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
setEncoderCodec(codec: number) {
|
|
|
|
this.native.set_encoder_codec(codec);
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
isReplayingVoice(): boolean {
|
|
|
|
return this.currentlyReplayingAudio;
|
|
|
|
}
|
|
|
|
|
|
|
|
private setReplayingVoice(status: boolean) {
|
|
|
|
if(status === this.currentlyReplayingAudio) {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-19 14:27:12 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
this.currentlyReplayingAudio = status;
|
|
|
|
this.events.fire("notify_voice_replay_state_change", { replaying: status });
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleVoiceClientStateChange() {
|
|
|
|
this.setReplayingVoice(this.availableVoiceClients().findIndex(client => client.getState() === VoicePlayerState.PLAYING || client.getState() === VoicePlayerState.BUFFERING) !== -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleVoiceStartEvent() {
|
2020-04-01 15:56:23 -04:00
|
|
|
const chandler = this.connection.client;
|
2020-09-24 16:06:52 -04:00
|
|
|
if(chandler.isMicrophoneMuted()) {
|
|
|
|
logWarn(LogCategory.VOICE, tr("Received local voice started event, even thou we're muted!"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.native.enable_voice_send(true);
|
|
|
|
this.localAudioStarted = true;
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
logInfo(LogCategory.VOICE, tr("Local voice started"));
|
|
|
|
chandler.getClient()?.setSpeaking(true);
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
private handleVoiceEndEvent(reason: string) {
|
2020-09-24 16:06:52 -04:00
|
|
|
this.native.enable_voice_send(false);
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
if(!this.localAudioStarted) {
|
|
|
|
return;
|
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
const chandler = this.connection.client;
|
|
|
|
chandler.getClient()?.setSpeaking(false);
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
logInfo(LogCategory.VOICE, tr("Local voice ended (%s)"), reason);
|
2020-09-24 16:06:52 -04:00
|
|
|
this.localAudioStarted = false;
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
availableVoiceClients(): NativeVoiceClientWrapper[] {
|
|
|
|
return Object.keys(this.registeredVoiceClients).map(clientId => this.registeredVoiceClients[clientId]);
|
|
|
|
}
|
|
|
|
|
|
|
|
registerVoiceClient(clientId: number) {
|
|
|
|
const client = new NativeVoiceClientWrapper(this.native.register_client(clientId));
|
|
|
|
client.events.on("notify_state_changed", this.voiceClientStateChangedEventListener);
|
|
|
|
this.registeredVoiceClients[clientId] = client;
|
|
|
|
return client;
|
|
|
|
}
|
|
|
|
|
|
|
|
unregisterVoiceClient(client: VoiceClient) {
|
2021-04-19 14:27:12 -04:00
|
|
|
if(!(client instanceof NativeVoiceClientWrapper)) {
|
2020-09-24 16:06:52 -04:00
|
|
|
throw "invalid client type";
|
2021-04-19 14:27:12 -04:00
|
|
|
}
|
2020-09-24 16:06:52 -04:00
|
|
|
|
|
|
|
delete this.registeredVoiceClients[client.getClientId()];
|
|
|
|
this.native.unregister_client(client.getClientId());
|
|
|
|
client.destroy();
|
2021-04-19 14:27:12 -04:00
|
|
|
|
|
|
|
this.handleVoiceClientStateChange();
|
2020-09-24 16:06:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
stopAllVoiceReplays() {
|
|
|
|
this.availableVoiceClients().forEach(client => client.abortReplay());
|
|
|
|
}
|
|
|
|
|
|
|
|
/* whisper API */
|
|
|
|
getWhisperSessionInitializer(): WhisperSessionInitializer | undefined {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
getWhisperSessions(): WhisperSession[] {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
getWhisperTarget(): WhisperTarget | undefined {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
setWhisperSessionInitializer(initializer: WhisperSessionInitializer | undefined) { }
|
2020-09-24 16:06:52 -04:00
|
|
|
|
|
|
|
startWhisper(target: WhisperTarget): Promise<void> {
|
|
|
|
return Promise.resolve(undefined);
|
|
|
|
}
|
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
dropWhisperSession(session: WhisperSession) { }
|
2020-09-24 16:06:52 -04:00
|
|
|
|
2021-04-19 14:27:12 -04:00
|
|
|
stopWhisper() { }
|
2020-11-29 15:06:57 -05:00
|
|
|
|
|
|
|
getConnectionStats(): Promise<ConnectionStatistics> {
|
2020-12-02 15:00:51 -05:00
|
|
|
/* FIXME: This is iffy! */
|
|
|
|
const stats = (this.connection as any as NativeServerConnection)["nativeHandle"]?.statistics();
|
|
|
|
|
2020-11-29 15:06:57 -05:00
|
|
|
return Promise.resolve({
|
2020-12-02 15:00:51 -05:00
|
|
|
bytesSend: stats?.voice_bytes_send ? stats?.voice_bytes_send : 0,
|
|
|
|
bytesReceived: stats?.voice_bytes_received ? stats?.voice_bytes_received : 0
|
2020-11-29 15:06:57 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
getRetryTimestamp(): number | 0 {
|
|
|
|
return Date.now();
|
|
|
|
}
|
2020-09-24 16:06:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
class NativeVoiceClientWrapper implements VoiceClient {
|
|
|
|
private readonly native: NativeVoiceClient;
|
|
|
|
readonly events: Registry<VoicePlayerEvents>;
|
|
|
|
private playerState: VoicePlayerState;
|
|
|
|
|
|
|
|
constructor(native: NativeVoiceClient) {
|
|
|
|
this.events = new Registry<VoicePlayerEvents>();
|
|
|
|
this.native = native;
|
|
|
|
this.playerState = VoicePlayerState.STOPPED;
|
|
|
|
|
|
|
|
this.native.callback_state_changed = state => {
|
|
|
|
switch (state) {
|
|
|
|
case PlayerState.BUFFERING:
|
|
|
|
this.setState(VoicePlayerState.BUFFERING);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PlayerState.PLAYING:
|
|
|
|
this.setState(VoicePlayerState.PLAYING);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PlayerState.STOPPED:
|
|
|
|
this.setState(VoicePlayerState.STOPPED);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PlayerState.STOPPING:
|
|
|
|
this.setState(VoicePlayerState.STOPPING);
|
|
|
|
break;
|
2021-04-19 14:27:12 -04:00
|
|
|
|
|
|
|
default:
|
|
|
|
logError(LogCategory.VOICE, tr("Native audio player has invalid state: %o"), state);
|
|
|
|
break;
|
2019-10-25 19:51:40 -04:00
|
|
|
}
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2020-10-01 04:56:22 -04:00
|
|
|
|
|
|
|
this.resetLatencySettings();
|
2020-09-24 16:06:52 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
destroy() {
|
|
|
|
this.events.destroy();
|
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
abortReplay() {
|
|
|
|
this.native.abort_replay();
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
flushBuffer() {
|
|
|
|
this.native.get_stream().flush_buffer();
|
|
|
|
}
|
2020-04-01 15:56:23 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
getClientId(): number {
|
|
|
|
return this.native.client_id;
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
getState(): VoicePlayerState {
|
|
|
|
return this.playerState;
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
private setState(state: VoicePlayerState) {
|
|
|
|
if(this.playerState === state) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const oldState = this.playerState;
|
|
|
|
this.playerState = state;
|
|
|
|
this.events.fire("notify_state_changed", { oldState: oldState, newState: state });
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
setVolume(volume: number) {
|
|
|
|
this.native.set_volume(volume);
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
getVolume(): number {
|
|
|
|
return this.native.get_volume();
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
resetLatencySettings() {
|
|
|
|
const stream = this.native.get_stream();
|
|
|
|
stream.set_buffer_latency(0.080);
|
|
|
|
stream.set_buffer_max_latency(0.5);
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
setLatencySettings(settings: VoicePlayerLatencySettings) {
|
|
|
|
const stream = this.native.get_stream();
|
|
|
|
stream.set_buffer_latency(settings.minBufferTime / 1000);
|
|
|
|
stream.set_buffer_max_latency(settings.maxBufferTime / 1000);
|
2020-04-01 15:56:23 -04:00
|
|
|
}
|
2019-10-25 19:51:40 -04:00
|
|
|
|
2020-09-24 16:06:52 -04:00
|
|
|
getLatencySettings(): Readonly<VoicePlayerLatencySettings> {
|
|
|
|
const stream = this.native.get_stream();
|
|
|
|
|
|
|
|
return {
|
|
|
|
maxBufferTime: stream.get_buffer_max_latency() * 1000,
|
|
|
|
minBufferTime: stream.get_buffer_latency() * 1000
|
|
|
|
};
|
2019-10-25 19:51:40 -04:00
|
|
|
}
|
|
|
|
}
|