mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-19 09:33:47 +00:00
Standardize on elementSource
This commit is contained in:
parent
a08ed96ab5
commit
00274e9483
6 changed files with 20 additions and 397 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 `<inpyt type='file'>`.
|
||||
|
||||
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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
/* Emulate the native <audio> element with Web Audio API */
|
||||
import { elementSource } from "../config";
|
||||
import { BANDS } from "../constants";
|
||||
import BufferSource from "./bufferSource";
|
||||
import ElementSource from "./elementSource";
|
||||
|
||||
export default class Media {
|
||||
|
|
@ -76,9 +74,7 @@ export default class Media {
|
|||
// |
|
||||
// <destination>
|
||||
|
||||
this._source = elementSource
|
||||
? new ElementSource(this._context, this._staticSource)
|
||||
: new BufferSource(this._context, this._staticSource);
|
||||
this._source = new ElementSource(this._context, this._staticSource);
|
||||
|
||||
this._source.on("positionChange", () => {
|
||||
this._callbacks.timeupdate();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue