2020-03-30 13:44:18 +02:00
|
|
|
import * as aplayer from "../audio/player";
|
2020-09-01 12:53:42 +02:00
|
|
|
import {LogCategory, logDebug, logError, logWarn} from "tc-shared/log";
|
2020-08-10 14:41:34 +02:00
|
|
|
import {LatencySettings, PlayerState, VoiceClient} from "tc-shared/connection/VoiceConnection";
|
2020-09-01 12:53:42 +02:00
|
|
|
import {AudioResampler} from "tc-backend/web/voice/AudioResampler";
|
|
|
|
import {AudioClient} from "tc-backend/web/audio-lib/AudioClient";
|
|
|
|
import {getAudioLibrary} from "tc-backend/web/audio-lib";
|
|
|
|
import {VoicePacket} from "tc-backend/web/voice/bridge/VoiceBridge";
|
2020-03-30 13:44:18 +02:00
|
|
|
|
|
|
|
export class VoiceClientController implements VoiceClient {
|
|
|
|
callback_playback: () => any;
|
|
|
|
callback_state_changed: (new_state: PlayerState) => any;
|
|
|
|
callback_stopped: () => any;
|
|
|
|
client_id: number;
|
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private speakerContext: AudioContext;
|
|
|
|
private gainNode: GainNode;
|
2020-03-30 13:44:18 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private playerState: PlayerState = PlayerState.STOPPED;
|
2020-03-30 13:44:18 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private currentPlaybackTime: number = 0;
|
|
|
|
private bufferTimeout: number;
|
2020-03-30 13:44:18 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private bufferQueueTime: number = 0;
|
|
|
|
private bufferQueue: AudioBuffer[] = [];
|
|
|
|
private playingNodes: AudioBufferSourceNode[] = [];
|
|
|
|
|
|
|
|
private currentVolume: number = 1;
|
|
|
|
private latencySettings: LatencySettings;
|
|
|
|
|
|
|
|
private audioInitializePromise: Promise<void>;
|
|
|
|
private audioClient: AudioClient;
|
|
|
|
private resampler: AudioResampler;
|
2020-03-30 13:44:18 +02:00
|
|
|
|
|
|
|
constructor(client_id: number) {
|
|
|
|
this.client_id = client_id;
|
2020-09-01 12:53:42 +02:00
|
|
|
this.reset_latency_settings();
|
|
|
|
|
|
|
|
this.resampler = new AudioResampler(48000);
|
|
|
|
aplayer.on_ready(() => {
|
|
|
|
this.speakerContext = aplayer.context();
|
|
|
|
this.gainNode = aplayer.context().createGain();
|
|
|
|
this.gainNode.connect(this.speakerContext.destination);
|
|
|
|
this.gainNode.gain.value = this.currentVolume;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private initializeAudio() : Promise<void> {
|
|
|
|
if(this.audioInitializePromise) {
|
|
|
|
return this.audioInitializePromise;
|
|
|
|
}
|
2020-03-30 13:44:18 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
this.audioInitializePromise = (async () => {
|
|
|
|
this.audioClient = await getAudioLibrary().createClient();
|
|
|
|
this.audioClient.callback_decoded = buffer => {
|
|
|
|
this.resampler.resample(buffer).then(buffer => {
|
|
|
|
this.playbackAudioBuffer(buffer);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.audioClient.callback_ended = () => {
|
|
|
|
this.stopAudio(false);
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
return this.audioInitializePromise;
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
public enqueuePacket(packet: VoicePacket) {
|
|
|
|
if(!this.audioClient && packet.payload.length === 0) {
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
this.initializeAudio().then(() => {
|
|
|
|
if(!this.audioClient) {
|
|
|
|
/* we've already been destroyed */
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.audioClient.enqueueBuffer(packet.payload, packet.voiceId, packet.codec);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
public destroy() {
|
|
|
|
this.audioClient?.destroy();
|
|
|
|
this.audioClient = undefined;
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
playbackAudioBuffer(buffer: AudioBuffer) {
|
2020-03-30 13:44:18 +02:00
|
|
|
if(!buffer) {
|
2020-09-01 12:53:42 +02:00
|
|
|
logWarn(LogCategory.VOICE, tr("[AudioController] Got empty or undefined buffer! Dropping it"));
|
2020-03-30 13:44:18 +02:00
|
|
|
return;
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-03-30 13:44:18 +02:00
|
|
|
if(!this.speakerContext) {
|
2020-09-01 12:53:42 +02:00
|
|
|
logWarn(LogCategory.VOICE, tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!"));
|
2020-03-30 13:44:18 +02:00
|
|
|
return;
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
if (buffer.sampleRate != this.speakerContext.sampleRate) {
|
|
|
|
logWarn(LogCategory.VOICE, tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate);
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
if(this.playerState == PlayerState.STOPPED || this.playerState == PlayerState.STOPPING) {
|
|
|
|
logDebug(LogCategory.VOICE, tr("[Audio] Starting new playback"));
|
|
|
|
this.setPlayerState(PlayerState.PREBUFFERING);
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
if(this.playerState === PlayerState.PREBUFFERING || this.playerState === PlayerState.BUFFERING) {
|
|
|
|
this.resetBufferTimeout(true);
|
|
|
|
this.bufferQueue.push(buffer);
|
|
|
|
this.bufferQueueTime += buffer.duration;
|
|
|
|
if(this.bufferQueueTime <= this.latencySettings.min_buffer / 1000) {
|
|
|
|
return;
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
/* finished buffering */
|
|
|
|
if(this.playerState == PlayerState.PREBUFFERING) {
|
|
|
|
logDebug(LogCategory.VOICE, tr("[Audio] Prebuffering succeeded (Replaying now)"));
|
|
|
|
if(this.callback_playback) {
|
|
|
|
this.callback_playback();
|
2019-04-04 21:47:52 +02:00
|
|
|
}
|
2020-09-01 12:53:42 +02:00
|
|
|
} else {
|
|
|
|
logDebug(LogCategory.VOICE, tr("[Audio] Buffering succeeded (Replaying now)"));
|
|
|
|
}
|
|
|
|
|
|
|
|
this.replayBufferQueue();
|
|
|
|
this.setPlayerState(PlayerState.PLAYING);
|
|
|
|
} else if(this.playerState === PlayerState.PLAYING) {
|
|
|
|
const latency = this.getCurrentPlaybackLatency();
|
|
|
|
if(latency > (this.latencySettings.max_buffer / 1000)) {
|
|
|
|
logWarn(LogCategory.VOICE, tr("Dropping replay buffer for client %d because of too high replay latency. (Current: %f, Max: %f)"),
|
|
|
|
this.client_id, latency.toFixed(3), (this.latencySettings.max_buffer / 1000).toFixed(3));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.enqueueBufferForPayback(buffer);
|
|
|
|
} else {
|
|
|
|
logError(LogCategory.AUDIO, tr("This block should be unreachable!"));
|
|
|
|
return;
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
getCurrentPlaybackLatency() {
|
|
|
|
return Math.max(this.currentPlaybackTime - this.speakerContext.currentTime, 0);
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
stopAudio(abortPlayback: boolean) {
|
|
|
|
if(abortPlayback) {
|
|
|
|
this.setPlayerState(PlayerState.STOPPED);
|
|
|
|
this.flush();
|
|
|
|
if(this.callback_stopped) {
|
|
|
|
this.callback_stopped();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.setPlayerState(PlayerState.STOPPING);
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
/* replay all pending buffers */
|
|
|
|
this.replayBufferQueue();
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
/* test if there are any buffers which are currently played, if not the state will change to stopped */
|
|
|
|
this.testReplayState();
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private replayBufferQueue() {
|
|
|
|
for(const buffer of this.bufferQueue)
|
|
|
|
this.enqueueBufferForPayback(buffer);
|
|
|
|
this.bufferQueue = [];
|
|
|
|
this.bufferQueueTime = 0;
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private enqueueBufferForPayback(buffer: AudioBuffer) {
|
|
|
|
/* advance the playback time index, we seem to be behind a bit */
|
|
|
|
if(this.currentPlaybackTime < this.speakerContext.currentTime)
|
|
|
|
this.currentPlaybackTime = this.speakerContext.currentTime;
|
2020-03-30 13:44:18 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
const player = this.speakerContext.createBufferSource();
|
|
|
|
player.buffer = buffer;
|
2020-03-30 13:44:18 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
player.onended = () => this.handleBufferPlaybackEnded(player);
|
|
|
|
this.playingNodes.push(player);
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
player.connect(this.gainNode);
|
|
|
|
player.start(this.currentPlaybackTime);
|
2020-03-30 13:44:18 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
this.currentPlaybackTime += buffer.duration;
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private handleBufferPlaybackEnded(node: AudioBufferSourceNode) {
|
|
|
|
this.playingNodes.remove(node);
|
|
|
|
this.testReplayState();
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private testReplayState() {
|
|
|
|
if(this.bufferQueue.length > 0 || this.playingNodes.length > 0) {
|
2020-03-30 13:44:18 +02:00
|
|
|
return;
|
2020-09-01 12:53:42 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
if(this.playerState === PlayerState.STOPPING) {
|
|
|
|
/* All buffers have been replayed successfully */
|
|
|
|
this.setPlayerState(PlayerState.STOPPED);
|
|
|
|
if(this.callback_stopped) {
|
|
|
|
this.callback_stopped();
|
2019-04-04 21:47:52 +02:00
|
|
|
}
|
2020-09-01 12:53:42 +02:00
|
|
|
} else if(this.playerState === PlayerState.PLAYING) {
|
|
|
|
logDebug(LogCategory.VOICE, tr("Client %d has a buffer underflow. Changing state to buffering."), this.client_id);
|
|
|
|
this.setPlayerState(PlayerState.BUFFERING);
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
/***
|
|
|
|
* Schedule a new buffer timeout.
|
|
|
|
* The buffer timeout is used to playback even small amounts of audio, which are less than the min. buffer size.
|
|
|
|
* @param scheduleNewTimeout
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private resetBufferTimeout(scheduleNewTimeout: boolean) {
|
|
|
|
clearTimeout(this.bufferTimeout);
|
|
|
|
|
|
|
|
if(scheduleNewTimeout) {
|
|
|
|
this.bufferTimeout = setTimeout(() => {
|
|
|
|
if(this.playerState == PlayerState.PREBUFFERING || this.playerState == PlayerState.BUFFERING) {
|
|
|
|
logWarn(LogCategory.VOICE, tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay."));
|
|
|
|
this.stopAudio(false);
|
|
|
|
}
|
|
|
|
this.bufferTimeout = undefined;
|
|
|
|
}, 1000);
|
|
|
|
}
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
private setPlayerState(state: PlayerState) {
|
|
|
|
if(this.playerState === state) {
|
|
|
|
return;
|
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
this.playerState = state;
|
|
|
|
if(this.callback_state_changed) {
|
|
|
|
this.callback_state_changed(this.playerState);
|
|
|
|
}
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-03-30 13:44:18 +02:00
|
|
|
get_state(): PlayerState {
|
2020-09-01 12:53:42 +02:00
|
|
|
return this.playerState;
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-03-30 13:44:18 +02:00
|
|
|
get_volume(): number {
|
2020-09-01 12:53:42 +02:00
|
|
|
return this.currentVolume;
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-03-30 13:44:18 +02:00
|
|
|
set_volume(volume: number): void {
|
2020-09-01 12:53:42 +02:00
|
|
|
if(this.currentVolume == volume)
|
2020-03-30 13:44:18 +02:00
|
|
|
return;
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
this.currentVolume = volume;
|
|
|
|
if(this.gainNode) {
|
|
|
|
this.gainNode.gain.value = volume;
|
|
|
|
}
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-04-04 21:47:52 +02:00
|
|
|
|
2020-03-30 13:44:18 +02:00
|
|
|
abort_replay() {
|
|
|
|
this.stopAudio(true);
|
|
|
|
}
|
2019-11-09 15:56:01 +01:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
support_flush(): boolean {
|
|
|
|
return true;
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-11-09 15:56:01 +01:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
flush() {
|
|
|
|
this.bufferQueue = [];
|
|
|
|
this.bufferQueueTime = 0;
|
|
|
|
|
|
|
|
for(const entry of this.playingNodes) {
|
|
|
|
entry.stop(0);
|
|
|
|
}
|
|
|
|
this.playingNodes = [];
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-11-09 15:56:01 +01:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
latency_settings(settings?: LatencySettings): LatencySettings {
|
|
|
|
if(typeof settings !== "undefined") {
|
|
|
|
this.latencySettings = settings;
|
|
|
|
}
|
|
|
|
return this.latencySettings;
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-11-09 16:17:48 +01:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
reset_latency_settings() {
|
|
|
|
this.latencySettings = {
|
|
|
|
min_buffer: 60,
|
|
|
|
max_buffer: 400
|
|
|
|
};
|
2020-03-30 13:44:18 +02:00
|
|
|
}
|
2019-11-09 16:17:48 +01:00
|
|
|
|
2020-09-01 12:53:42 +02:00
|
|
|
support_latency_settings(): boolean {
|
|
|
|
return true;
|
2019-04-04 21:47:52 +02:00
|
|
|
}
|
|
|
|
}
|