signal-desktop/ts/services/globalMessageAudio.ts

120 lines
2.8 KiB
TypeScript
Raw Normal View History

// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { noop } from 'lodash';
2023-03-15 17:48:38 +00:00
function isAbortError(error: unknown) {
return error instanceof DOMException && error.name === 'AbortError';
}
/**
* Wrapper around a global HTMLAudioElement that can update the
* source and callbacks without requiring removeEventListener
*/
class GlobalMessageAudio {
#audio: HTMLAudioElement = new Audio();
2023-02-28 13:07:40 +00:00
#url: string | undefined;
// true immediately after play() is called, even if still loading
#playing = false;
#onLoadedMetadata = noop;
#onTimeUpdate = noop;
#onEnded = noop;
#onDurationChange = noop;
2023-02-28 13:07:40 +00:00
#onError = noop;
constructor() {
// callbacks must be wrapped by function (not attached directly)
// so changes to the callbacks are effected
this.#audio.addEventListener('loadedmetadata', () =>
this.#onLoadedMetadata()
);
this.#audio.addEventListener('timeupdate', () => this.#onTimeUpdate());
this.#audio.addEventListener('durationchange', () =>
this.#onDurationChange()
);
this.#audio.addEventListener('ended', () => this.#onEnded());
}
load({
2023-02-28 13:07:40 +00:00
url,
2023-02-24 23:18:57 +00:00
playbackRate,
onLoadedMetadata,
onTimeUpdate,
onDurationChange,
onEnded,
2023-02-28 13:07:40 +00:00
onError,
}: {
2023-02-28 13:07:40 +00:00
url: string;
2023-02-24 23:18:57 +00:00
playbackRate: number;
onLoadedMetadata: () => void;
onTimeUpdate: () => void;
onDurationChange: () => void;
onEnded: () => void;
2023-02-28 13:07:40 +00:00
onError: (error: unknown) => void;
}) {
2023-02-28 13:07:40 +00:00
this.#url = url;
// update callbacks
this.#onLoadedMetadata = onLoadedMetadata;
this.#onTimeUpdate = onTimeUpdate;
this.#onDurationChange = onDurationChange;
this.#onEnded = onEnded;
2023-02-28 13:07:40 +00:00
this.#onError = onError;
2023-02-24 23:18:57 +00:00
// changing src resets the playback rate
2023-02-28 13:07:40 +00:00
this.#audio.src = this.#url;
2023-02-24 23:18:57 +00:00
this.#audio.playbackRate = playbackRate;
}
2023-02-28 13:07:40 +00:00
play(): void {
this.#playing = true;
this.#audio.play().catch(error => {
2023-03-15 17:48:38 +00:00
// If `audio.pause()` is called before `audio.play()` resolves
if (!isAbortError(error)) {
this.#onError(error);
}
2023-02-28 13:07:40 +00:00
});
}
pause(): void {
this.#audio.pause();
2023-02-28 13:07:40 +00:00
this.#playing = false;
}
get playbackRate() {
return this.#audio.playbackRate;
}
set playbackRate(rate: number) {
this.#audio.playbackRate = rate;
}
2023-02-28 13:07:40 +00:00
get playing() {
return this.#playing;
}
get url() {
return this.#url;
}
2023-03-02 20:55:40 +00:00
get duration(): number | undefined {
// the underlying Audio element can return NaN if the audio hasn't loaded
// we filter out 0 or NaN as they are not useful values downstream
return Number.isNaN(this.#audio.duration) || this.#audio.duration === 0
? undefined
: this.#audio.duration;
}
get currentTime() {
return this.#audio.currentTime;
}
set currentTime(value: number) {
this.#audio.currentTime = value;
}
}
export const globalMessageAudio = new GlobalMessageAudio();