/** * @file Provides an interface for playing transcription audio. * @author Carl Stephens * @module */ class LoadedAudio { /** * Initialises a new instance of the {@link LoadedAudio} class. * @param {String | null} id The ID of the transcription for which this audio belongs to. * @param {String | null} url The audio object URL. */ constructor(id, url) { /** @type {String | null} The ID of the transcription for which this audio belongs to. */ this.id = id; /** @type {String | null} The audio object URL. */ this.url = url; } } /** * Polls an audio element's current time and updates the vuex store accordingly. * @param {Audio} audioElement The audio element. * @param {*} store The vuex store to update. */ function pollAudioTime(audioElement, store) { let lastTime = 0; (function poll() { if (audioElement.currentTime !== lastTime) { lastTime = audioElement.currentTime; store.commit("playbackStateSetTime", { id: store.state.playbackState.id, time: lastTime }); } requestAnimationFrame(poll); })(); } /** * Loads an audio file. * @param {HTMLAudioElement} player The audio player. * @param {TranscriptionViewModel} transcription The name of the requested audio file. * @param {LoadedAudio} current The currently loaded audio. * @returns {LoadedAudio} If a new audio file was loaded, a new audio tracking object, else the current one. */ function loadAudio(player, transcription, current) { // TODO: Need to profile on some larger tracks; this may not be worth it? Better to cache a URL object instead? if (current.url !== null) { URL.revokeObjectURL(current.url); } const urlObject = URL.createObjectURL(transcription.file); player.src = urlObject; player.load(); return new LoadedAudio(transcription.id, urlObject); } export default class AudioPlayback { /** * Initialises the {@link AudioPlayback} class. * @param {*} store The vuex store to update. */ static initialise(store) { /** @type The vuex store. */ this.store = store; this.player = new Audio(); this.loadedAudio = new LoadedAudio(null, null); this.requestedPlaybackTime = 0; pollAudioTime(this.player, store); this.player.addEventListener("ended", function() { store.commit("playbackStateSetIsPlaying", false); }); } static onCanPlayThrough() { AudioPlayback.player.removeEventListener("canplaythrough", AudioPlayback.onCanPlayThrough); AudioPlayback.player.currentTime = AudioPlayback.requestedPlaybackTime; AudioPlayback.store.commit("playbackStateSetLength", { id: AudioPlayback.loadedAudio.id, time: AudioPlayback.player.duration }); } /** * Loads a transcription's audio file. * @param {String} id The ID of the transcription to load the audio of. * @param {Number} startTime The time into the audio to set the current playback time to. Leave negative to select the last value, or zero. */ static async load(id, startTime = -1) { this.player.addEventListener("canplaythrough", this.onCanPlayThrough); const playbackTimes = this.store.state.playbackState.playbackTimes; this.requestedPlaybackTime = startTime; if (playbackTimes.has(id) && startTime < 0) { this.requestedPlaybackTime = playbackTimes.get(id); } if (this.loadedAudio.id !== id) { this.loadedAudio = loadAudio(this.player, this.store.state.rawTranscriptions.get(id), this.loadedAudio); this.store.commit("playbackStateSetID", id); } this.player.currentTime = AudioPlayback.requestedPlaybackTime; this.store.commit("playbackStateSetLength", { id: id, time: this.player.duration }); } /** * Plays a transcription's audio file. * @param {String} id The ID of the transcription to play audio for. * @param {Number} startTime The time into the audio at which to start playing. Leave negative to resume playback if paused. */ static async play(id, startTime = -1) { // Start at the beginning if we've reached the end if (this.player.duration <= this.player.currentTime && startTime < 0) { await this.load(id, 0); } else { await this.load(id, startTime); } await this.player.play(); this.store.commit("playbackStateSetIsPlaying", true); } /** * Sets the current time of the audio player. * @param {Number} time The time to scrub to. * @param {Boolean} fromCurrent Indicates if the time is an offset. */ static scrub(time, fromCurrent) { const newTime = fromCurrent ? this.player.currentTime + time : time; this.player.currentTime = newTime; } /** * Pauses playback. */ static pause() { this.player.pause(); this.store.commit("playbackStateSetIsPlaying", false); } /** * Resumes playback. * @param {Number} startTime The time into the audio at which to resume playback. */ static resume(startTime = -1) { this.play(this.loadedAudio.id, startTime); } }