@firstcoders/hls-web-audio
Version:
223 lines (185 loc) • 4.9 kB
JavaScript
export default class {
/**
* @property {Array} elements - The ordered elements that jointly compose this HLS track
* @private
*/
elements = [];
/**
* @property {Number} startPointer - an internal pointer pointing to where the start of the next element is
*/
startPointer;
/**
* @property {Number} startPointer - the initial start time, if not 0
*/
initialStartTime;
/**
* @property {Number} nextMarginSeconds - a marin, in seconds, that controls a rolling window that checks whether a segment is nearly next
*/
nextMarginSeconds;
/**
* @property {Number|undefined} - a duration set externally rather than derived from loaded audio
*/
#duration;
constructor({ start = 0, nextMarginSeconds = 5 } = {}) {
this.initialStartTime = start;
this.startPointer = start;
this.nextMarginSeconds = nextMarginSeconds;
}
/**
* Destructor
*/
destroy() {
// destroy all elements
this.elements.forEach((element) => element.destroy());
// remove references
this.elements = [];
}
/**
* Add elements to the stack
*
* @param {...any} element
*/
push(...element) {
element.forEach((s) => {
// initialise start time of element
s.start = this.startPointer;
// push to stack
this.elements.push(s);
// increment start pointer
this.startPointer += s.duration;
});
}
/**
* Try to get the next element that is not ready
* @returns {Object|undefined}
*/
consume(timeframe) {
const iCurrent = this.getIndexAt(timeframe.currentTime);
const current = this.elements[iCurrent];
const next = this.elements[iCurrent + 1];
const getNextElement = () => {
if (current && !current.$inTransit && !current.isReady) {
return current;
}
// do not schedule next unless current is ready
if (!current?.isReady) return undefined;
// ensure the next is in the play window (<timeframe.end)
if (next && next.start < timeframe.end && !next.$inTransit && !next.isReady) {
return next;
}
return undefined;
};
const element = getNextElement();
if (element) {
// store a signpost that we're currently $inTransit the element
// so that it wont be loaded again by the next timeupdate event, while it is still being prepared
element.$inTransit = true;
}
return element;
}
/**
* Ack an element, freeing it up for future consumption
*
* @param {Object} element
*/
ack(element) {
element.$inTransit = false;
}
/**
* The default duration as defined by the audio segments
*/
get audioDuration() {
return this.startPointer;
}
/**
* Get the total duration
*
* @returns {Number|undefined}
*/
get duration() {
return this.#duration || this.audioDuration;
}
/**
* Manually set the duration
*
* @param {Number} duration - the duration
*/
set duration(duration) {
this.#duration = duration;
}
/**
* @returns {Object} The first element
*/
get first() {
return this.elements[0];
}
/**
* Handles a controller's "seek" event
*/
disconnectAll() {
// disconnect all elements. A new set will need to be resheduled
this.elements.forEach((element) => {
// cancel any loading in progress
element.cancel();
// disconnect any connected audio nodes
if (element.isReady) element.disconnect();
// ensure element is again available for consumption
this.ack(element);
});
}
/**
* Get the length of the stack
*/
get length() {
return this.elements.length;
}
/**
* Get the index of the current element
* @param {Number} t - the time
* @returns
*/
getIndexAt(t) {
return this.elements.findIndex((s) => t >= s.start && t <= s.end);
}
/**
* Get the current element
* @param {Number} t - the time
* @returns
*/
getAt(t) {
return this.elements.find((s) => t >= s.start && t <= s.end);
}
/**
* Recalculates the start times, taking into account any later adjustments from learning the real durations
* of a segment after decoding the audio data.
*/
recalculateStartTimes() {
this.startPointer = this.initialStartTime;
this.elements.forEach((s, i) => {
const start = this.elements[i - 1]?.end || this.startPointer;
// initialise start time of element
s.start = start;
// increment start pointer
this.startPointer += s.duration;
});
}
/**
* @deprecated
*/
set start(start) {
this.initialStartTime = start;
this.disconnectAll();
this.recalculateStartTimes();
}
get start() {
return this.initialStartTime;
}
set offset(offset) {
this._offset = offset;
this.disconnectAll();
this.recalculateStartTimes();
}
get offset() {
return this._offset;
}
}