@firstcoders/hls-web-audio
Version:
`@firstcoders/hls-web-audio` plays multiple streamed audio tracks in sync using the Web Audio API. It is designed for stem-like playback where each track can be independently loaded, buffered, scheduled, and mixed.
206 lines (179 loc) • 4.96 kB
JavaScript
import Timeframe from './Timeframe.js';
/**
* Tracks playback bounds, seek state, and the mapping between audio context time and track time.
*/
export default class PlaybackTimeline {
#offset = 0;
#playDuration;
/**
* @param {import('./AudioController.js').default} controller
*/
constructor(controller) {
this.controller = controller; // Reference back to dispatcher context
this.timeframe = new Timeframe();
}
/**
* @returns {number|undefined}
*/
get anchor() {
return this.timeframe.anchor;
}
/**
* @param {number|undefined} v
*/
set anchor(v) {
this.timeframe.anchor = v;
}
/**
* @returns {number|undefined}
*/
get audioDuration() {
return this.controller.trackGroup.audioDuration;
}
/**
* Sets the active play duration bound and notifies listeners.
*
* @param {number|undefined} duration
*/
set playDuration(duration) {
if (duration && typeof duration !== 'number')
throw new TypeError('The property "playDuration" must be of type number');
this.#playDuration = duration;
this.controller.notifyUpdated('playDuration', this.playDuration);
}
/**
* @returns {number|undefined}
*/
get playDuration() {
return this.#playDuration || this.audioDuration;
}
/**
* Sets the playback offset and notifies listeners.
*
* @param {number} offset
*/
set offset(offset = 0) {
if (typeof offset !== 'number')
throw new TypeError('The property "offset" must be of type number');
this.#offset = offset;
this.controller.notifyUpdated('offset', this.offset);
}
/**
* @returns {number}
*/
get offset() {
return this.#offset;
}
/**
* Updates offset and duration together to avoid intermediate invalid bounds.
*
* @param {number} offset
* @param {number|undefined} playDuration
*/
setRegion(offset, playDuration) {
if (typeof offset !== 'number')
throw new TypeError('The property "offset" must be of type number');
if (playDuration && typeof playDuration !== 'number')
throw new TypeError('The property "playDuration" must be of type number');
const offsetChanged = this.#offset !== offset;
const durationChanged = this.#playDuration !== playDuration;
this.#offset = offset;
this.#playDuration = playDuration;
// notify after both have been set, so they don't trigger intermediate incomplete bounds
if (offsetChanged) this.controller.notifyUpdated('offset', this.#offset);
if (durationChanged) this.controller.notifyUpdated('playDuration', this.playDuration);
}
/**
* Returns current track time without any boundary adjustments.
*
* @returns {number|undefined}
*/
get rawCurrentTime() {
return this.anchor !== undefined ? this.controller.ac.currentTime - this.anchor : undefined;
}
/**
* Returns the current track time.
*
* @returns {number|undefined}
*/
get currentTime() {
return this.rawCurrentTime;
}
/**
* Seeks to a track time and reanchors the playback timeframe.
*
* @param {number} t
*/
setCurrentTime(t) {
if (typeof this.audioDuration !== 'number' || t < 0 || t > this.audioDuration)
throw new Error(`CurrentTime ${t} should be between 0 and duration ${this.audioDuration}`);
let seekTo = t;
if (seekTo < this.offset || seekTo > this.offset + this.playDuration) {
seekTo = this.offset;
}
this.fixAnchor(seekTo);
this.controller.ac.suspend().then(() => {
if (this.controller.desiredState === 'resumed' && !this.controller.engine.isBuffering) {
this.controller.ac.resume();
}
});
}
/**
* Builds the current timeframe snapshot used by the scheduler.
*
* @returns {Timeframe}
*/
get currentTimeframe() {
return this.timeframe.update({
anchor: this.anchor,
realEnd: this.realEnd,
currentTime: this.currentTime,
playDuration: this.playDuration,
offset: this.offset,
});
}
/**
* Reanchors the track timeline to the given track time.
*
* @param {number} t
*/
fixAnchor(t) {
this.timeframe.setAnchor(this.controller.ac.currentTime, t);
this.controller.fireEvent('seek', {
t,
pct: t / this.audioDuration,
remaining: this.audioDuration - t,
});
}
/**
* Sets current time as a fraction of the audio duration.
*
* @param {number} n
*/
set pct(n) {
if (this.audioDuration) {
let factor = n;
if (factor < 0) factor = 0;
if (factor > 1) factor = 1;
this.setCurrentTime(factor * this.audioDuration);
}
}
/**
* @returns {number}
*/
get pct() {
return this.currentTime / this.audioDuration;
}
/**
* @returns {number}
*/
get remaining() {
return this.audioDuration - this.currentTime;
}
/**
* @returns {number}
*/
get realEnd() {
return this.anchor + this.offset + this.playDuration;
}
}