blob: 63f5aaed9f2de0868ff03d2409324cc5f9bc5e92 [file] [log] [blame]
import {DownloadBuffer} from './download_buffer';
import {Media} from './media';
const UNPLAYED_BUFFER_MAX_SIZE = 10; // Only buffer 10 seconds maximum.
/**
* A source buffer wrapper that limits the array buffers that can be added to
* the source buffer. If the bufferred length exceeds the limit, it will
* automatically pauses and waits until the remaining playback time is below the
* limit.
*/
export class LimitedSourceBuffer {
private scheduleTimeoutId = -1;
private currentMediaIndex = 0;
constructor(
private readonly videoEl: HTMLVideoElement,
private readonly sourceBuffer: SourceBuffer,
private readonly mediaList: Media[],
private readonly downloadBuffer: DownloadBuffer) {
this.appendNext();
}
/** Appends the next array buffer to the source buffer. */
async appendNext() {
const unplayedBufferLength = this.unplayedBufferLength();
if (unplayedBufferLength > UNPLAYED_BUFFER_MAX_SIZE) {
// Buffered too many content. Wait until we need to add buffer again.
const timeout = unplayedBufferLength - UNPLAYED_BUFFER_MAX_SIZE;
clearTimeout(this.scheduleTimeoutId);
console.log(`Schedule appendNext in ${timeout} sec.`);
this.scheduleTimeoutId =
setTimeout(() => this.appendNext(), timeout * 1000);
return;
}
const media = this.mediaList[this.currentMediaIndex];
if (!media) {
// All media are appended.
console.log('All media are appended');
return;
}
const chunk = await this.downloadBuffer.shift(media);
if (!chunk) {
// No more buffer left to append in the current media. Move on to the next
// one.
console.log(`${media.url} finished appending. Move on to the next one.`);
this.currentMediaIndex++;
const bufferedLength = this.sourceBuffer.buffered.end(0);
this.sourceBuffer.timestampOffset = bufferedLength;
this.appendNext();
return;
}
console.log(`Appending new chunk from ${media.url}`);
await this.appendChunkToBuffer(chunk);
this.appendNext();
}
/**
* Appends a chunk to the source buffer. Waits until the update is completed.
*/
private async appendChunkToBuffer(chunk: ArrayBuffer) {
return new Promise((resolve, reject) => {
this.sourceBuffer.appendBuffer(chunk);
const unsubscribe = () => {
this.sourceBuffer.removeEventListener('updateend', onupdateend);
this.sourceBuffer.removeEventListener('error', onerror);
};
const onerror = () => {
unsubscribe();
reject(new Error('Append to buffer failed.'));
};
const onupdateend = () => {
unsubscribe();
resolve();
};
this.sourceBuffer.addEventListener('updateend', onupdateend);
this.sourceBuffer.addEventListener('error', onerror);
});
}
private unplayedBufferLength() {
if (this.sourceBuffer.buffered.length === 0) {
return 0;
}
return this.sourceBuffer.buffered.end(0) - this.videoEl.currentTime;
}
}