From 00274e948342279fe46099c42f8a68f8fd03689e Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 21 Jan 2018 21:44:34 -0800 Subject: [PATCH] Standardize on elementSource --- js/config.js | 1 - js/media/README.md | 112 ----------------- js/media/bufferSource.js | 253 -------------------------------------- js/media/elementSource.js | 23 +++- js/media/emitter.js | 22 ---- js/media/index.js | 6 +- 6 files changed, 20 insertions(+), 397 deletions(-) delete mode 100644 js/media/README.md delete mode 100644 js/media/bufferSource.js delete mode 100644 js/media/emitter.js diff --git a/js/config.js b/js/config.js index 76bd43d2..5059c83a 100644 --- a/js/config.js +++ b/js/config.js @@ -17,6 +17,5 @@ if (hash) { export const skinUrl = config.skinUrl === undefined ? skin : config.skinUrl; export const audioUrl = config.audioUrl === undefined ? audio : config.audioUrl; export const hideAbout = config.hideAbout || false; -export const elementSource = config.elementSource || true; export const initialState = config.initialState || undefined; export const sentryDsn = SENTRY_DSN; diff --git a/js/media/README.md b/js/media/README.md deleted file mode 100644 index ec1a3c7a..00000000 --- a/js/media/README.md +++ /dev/null @@ -1,112 +0,0 @@ -# Audio Source Abstraction - -An attempt to build a common interface for a media file source node. The goal is to have two implementations one that uses `MediaElementAudioSourceNode` and one that uses `AudioBufferSourceNode`. - -See `./elementSource.js` and `./bufferSource.js`. - -## Interface methods - -### `constructor(audioContext, destinationNode)` - -Construct a new instance: - - const source = new Source(audoContext, context.destination); - -### `disconnect()` - -Disconnect the source from the destination and clean up. The source cannot be reused. - -### `loadFile(file)` - -Load a file supplied by the user from a ``. - -Returns a Promise so that users can play when the file is done loading. - -TODO: Throw if cannot play - -### `loadUrl(url: string)` - -Load a url. Note that this URL is subject to CORS. - -Returns a Promise so that users can play when the file is done loading. - -TODO: Throw if cannot play - -### `play()` - -Start playing the source. - -* If already playing, start again at the begining. -* If paused, resume. -* If user has called `seekToTime()`, starts at that time. -* Has no affect if waiting. - -Returns a Promise - -### `pause()` - -* If already puased, does nothing. -* If stopped, does nothing. -* If waiting, puts us in paused mode. -* If playing, pauses the playback at the current time. - -### `stop()` - -Stops playback, reverts the current time to 0. - -### `seekToTime(seconds)` - -Updates the current time to `seconds`. - -Does not change the current status. If already playing, media will continue to play from the new time. - -### `getStatus(): STATUS` - -Get the current status. One of: - -* "PLAYING" -* "PAUSED" -* "STOPPED" - -### `getDuration(): seconds | null` - -Get the duration of the current track in seconds, or `null` if no track is loaded. - -### `getStalled(): boolean` - -Are we buffering/loading? - -### `getTimeElapsed(): seconds | null` - -How many secons have elapsed? - -### `getNumberOfChannels(): number | null` - -### `getSampleRate(): number | null` - -### `getLoop(): boolean` - -### `setLoop(shouldLoop: boolean)` - -### `setAutoPlay(shouldAutoPlay: boolean)` - -### `on(EVENT: string, callback: () => {})` - -Subscribe to events - -Options: - -* "statusChange": Our play status has changed. -* "positionChange": Our position changed. -* "stallChanged": We either started or stopped stalling. - -## Manual test plan: - -* Play a track on loop. Ensure the position indicator also loops. -* Pause a track after looping. Resume that track. -* Pause a track. Load a large file. Assert that the loading indicator shows. -* Pause a track. Resume it with play. -* Pause a track. Resume it with pause. -* Play a track. Part way through press play again. Asserrt that the track starts over. -* While a track is loading: Press play. Nothing should happen. -* Load a large file while another file is playing. Assert that the first file stops playing while we wait. \ No newline at end of file diff --git a/js/media/bufferSource.js b/js/media/bufferSource.js deleted file mode 100644 index 66be96fc..00000000 --- a/js/media/bufferSource.js +++ /dev/null @@ -1,253 +0,0 @@ -import Emitter from "./emitter"; - -const invariant = (assertion, message) => { - if (!assertion) { - console.error(message); - } -}; - -async function readUrlAsArrayBuffer(url) { - return new Promise((resolve, reject) => { - const oReq = new XMLHttpRequest(); - oReq.open("GET", url, true); - oReq.responseType = "arraybuffer"; - - oReq.onload = () => { - const arrayBuffer = oReq.response; // Note: not oReq.responseText - resolve(arrayBuffer); - }; - - oReq.onerror = e => { - reject(e); - }; - - oReq.send(null); - return true; - }); -} - -const STATUS = { - PLAYING: "PLAYING", - STOPPED: "STOPPED", - PAUSED: "PAUSED" -}; - -export default class BufferSource extends Emitter { - constructor(context, destination) { - super(); - this._context = context; - this._destination = destination; - //this._loop = false; - this._buffer = null; - this._source = null; - this._status = STATUS.STOPPED; - this._stalled = false; - this._startTime = null; - this._pausedAtTime = null; - this._onEnded = this._onEnded.bind(this); - } - - disconnect() { - if (this._source != null) { - this._source.disconnect(); - this._source = null; - } - } - - // Just adapt the callback into a promise. - _decodeAudioData(buffer) { - return new Promise((resolve, reject) => { - this._context.decodeAudioData(buffer, resolve, reject); - }); - } - - _startLoading() { - this.stop(); - // Ensure we don't start playing while we are waiting to load. - this._buffer = null; - this._setStalled(true); - } - - async loadUrl(url) { - this._startLoading(); - const arrayBuffer = await readUrlAsArrayBuffer(url); - return this._loadArrayBuffer(arrayBuffer); - } - - async _loadArrayBuffer(arrayBuffer) { - this._buffer = await this._decodeAudioData(arrayBuffer); - this.trigger("loaded"); - this._setStalled(false); - this._setStatus(STATUS.STOPPED); - } - - play() { - if (this._stalled) { - return; - } - switch (this._status) { - case STATUS.PLAYING: - case STATUS.STOPPED: - this._start(0); - break; - case STATUS.PAUSED: - invariant( - this._pausedAtTime != null, - "Status is paused, but there is no _pausedAtTime" - ); - this._start(this._pausedAtTime); - break; - } - } - - pause() { - this._pausedAtTime = this.getTimeElapsed(); - this._silence(STATUS.PAUSED); - } - - stop() { - this._silence(STATUS.STOPPED); - } - - seekToTime(time) { - if (!this.getDuration()) { - return; - } - time = Math.min(time, this.getDuration()); - time = Math.max(time, 0); - this._start(time); - } - - getStatus() { - return this._status; - } - - getStalled() { - return this._stalled; - } - - getDuration() { - if (this._buffer == null) { - return 0; - } - return this._buffer.duration; - } - - getTimeElapsed() { - switch (this._status) { - case STATUS.PLAYING: - invariant( - this._startTime != null, - "status is playing, but there is no _startTime" - ); - const totalTime = this._context.currentTime - this._startTime; - // If looping, the time may be greater than the song's duration. - return totalTime % this.getDuration(); - case STATUS.STOPPED: - return 0; - case STATUS.PAUSED: - invariant( - this._pausedAtTime != null, - "Status is paused, but there is no _pausedAtTime" - ); - return this._pausedAtTime; - } - throw new Error("Invalid status"); - } - - getNumberOfChannels() { - if (this._buffer == null) { - return null; - } - // TODO: This is actually the channels of the audio node, not the source media. - return this._buffer.numberOfChannels; - } - - getSampleRate() { - if (this._buffer == null) { - return null; - } - // TODO: This is actually the sample rate of the audio node, not the source media. - return this._buffer.sampleRate; - } - - /* - getLoop() { - return this._loop; - } - - setLoop(shouldLoop) { - this._loop = shouldLoop; - if (this._source) { - this._source.loop = this._loop; - } - } - */ - - _start(position) { - invariant(!isNaN(position)); - if (this._buffer == null) { - return; - } - if (this._source) { - // Does this work? - this.disconnect(); - } - this._source = this._context.createBufferSource(); - this._source.buffer = this._buffer; - //this._source.loop = this._loop; - this._source.connect(this._destination); - this._source.onended = this._onEnded; - this._startTime = this._context.currentTime - position; - this._setStatus(STATUS.PLAYING); - this._source.start(0, position); - } - - _silence(reason) { - if (!reason) { - throw new Error("Tried to silence the audio without a reason"); - } - // We must always update the status before ending the audio, so that onEnded - // can distinguish between audio that got to the end of the track, and audio - // that the user stopped/paused - this._setStatus(reason); - if (this._source == null) { - return; - } - this._source.stop(0); - this._source = null; - } - - _startMonitoringPosition() { - this._nextPositionUpdate = setTimeout(() => { - this.trigger("positionChange"); - this._startMonitoringPosition(); - }, 50); - } - - _stopMonitoringPosition() { - clearTimeout(this._nextPositionUpdate); - } - - _setStatus(status) { - this._status = status; - if (status === STATUS.PLAYING) { - this._startMonitoringPosition(); - } else { - this._stopMonitoringPosition(); - } - this.trigger("statusChange"); - } - - _setStalled(stalled) { - this._stalled = stalled; - this.trigger("statusChange"); - } - - _onEnded() { - if (this._status === STATUS.PLAYING) { - this.trigger("ended"); - this._setStatus(STATUS.STOPPED); - } - } -} diff --git a/js/media/elementSource.js b/js/media/elementSource.js index fd9edc4f..02e0c242 100644 --- a/js/media/elementSource.js +++ b/js/media/elementSource.js @@ -1,14 +1,29 @@ -import Emitter from "./emitter"; - const STATUS = { PLAYING: "PLAYING", STOPPED: "STOPPED", PAUSED: "PAUSED" }; -export default class ElementSource extends Emitter { +export default class ElementSource { + on(event, callback) { + const eventListeners = this._listeners[event] || []; + eventListeners.push(callback); + this._listeners[event] = eventListeners; + const unsubscribe = () => { + this._listeners[event] = eventListeners.filter(cb => cb !== callback); + }; + return unsubscribe; + } + + trigger(event) { + const callbacks = this._listeners[event]; + if (callbacks) { + callbacks.forEach(cb => cb()); + } + } + constructor(context, destination) { - super(); + this._listeners = {}; this._context = context; this._destination = destination; this._audio = document.createElement("audio"); diff --git a/js/media/emitter.js b/js/media/emitter.js deleted file mode 100644 index 3b3c0d2b..00000000 --- a/js/media/emitter.js +++ /dev/null @@ -1,22 +0,0 @@ -export default class Emitter { - constructor() { - this._listeners = {}; - } - - on(event, callback) { - const eventListeners = this._listeners[event] || []; - eventListeners.push(callback); - this._listeners[event] = eventListeners; - const unsubscribe = () => { - this._listeners[event] = eventListeners.filter(cb => cb !== callback); - }; - return unsubscribe; - } - - trigger(event) { - const callbacks = this._listeners[event]; - if (callbacks) { - callbacks.forEach(cb => cb()); - } - } -} diff --git a/js/media/index.js b/js/media/index.js index 5707fb59..acc3129a 100644 --- a/js/media/index.js +++ b/js/media/index.js @@ -1,7 +1,5 @@ /* Emulate the native