TeaWeb/web/app/voice/VoiceClient.ts

292 lines
9.7 KiB
TypeScript
Raw Normal View History

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";
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);
}
2020-09-01 12:53:42 +02:00
support_flush(): boolean {
return true;
2020-03-30 13:44:18 +02: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
}
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
}
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
}
2020-09-01 12:53:42 +02:00
support_latency_settings(): boolean {
return true;
2019-04-04 21:47:52 +02:00
}
}