tweb/src/lib/MP4Sourcee.ts

323 lines
9.7 KiB
TypeScript

/*
* Copyright (c) 2018-present, Evgeny Nadymov
*
* This source code is licensed under the GPL v.3.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
// @ts-ignore
import MP4Box from 'mp4box/dist/mp4box.all.min';
import { logger, LogLevels } from './polyfill';
export default class MP4Source {
private mp4file: any;
private nextBufferStart = 0;
private mediaSource: MediaSource = null;
private ready = false;
private bufferedTime = 40;
private beforeMoovBufferSize = 32 * 1024;
private moovBufferSize = 512 * 1024;
private bufferSize = 512 * 1024;
private seekBufferSize = 256 * 1024;
private currentBufferSize = this.beforeMoovBufferSize;
private nbSamples = 10;
private expectedSize: number;
private seeking = false;
private loading = false;
private url: string;
private log = logger('MP4'/* , LogLevels.error */);
//public onLoadBuffer: (offset: number)
constructor(private video: {duration: number, video: {expected_size: number}}, private getBufferAsync: (start: number, end: number) => Promise<ArrayBuffer>) {
this.expectedSize = this.video.video.expected_size;
this.init(video.duration);
}
init(videoDuration: number) {
const mediaSource = new MediaSource();
mediaSource.addEventListener('sourceopen', () => {
this.log('[MediaSource] sourceopen start', this.mediaSource, this);
if(this.mediaSource.sourceBuffers.length > 0) return;
const mp4File = MP4Box.createFile();
mp4File.onMoovStart = () => {
this.log('[MP4Box] onMoovStart');
this.currentBufferSize = this.moovBufferSize;
};
mp4File.onError = (error: Error) => {
this.log('[MP4Box] onError', error);
};
mp4File.onReady = (info: any) => {
this.log('[MP4Box] onReady', info);
this.ready = true;
this.currentBufferSize = this.bufferSize;
const { isFragmented, timescale, fragment_duration, duration } = info;
if(!fragment_duration && !duration) {
this.mediaSource.duration = videoDuration;
this.bufferedTime = videoDuration;
} else {
this.mediaSource.duration = isFragmented
? fragment_duration / timescale
: duration / timescale;
}
this.initializeAllSourceBuffers(info);
};
mp4File.onSegment = (id: number, sb: any, buffer: ArrayBuffer, sampleNum: number, is_last: boolean) => {
const isLast = (sampleNum + this.nbSamples) > sb.nb_samples;
this.log('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sb.nb_samples}`, isLast, sb.timestampOffset, mediaSource, is_last);
sb.segmentIndex++;
sb.pendingAppends.push({ id, buffer, sampleNum, is_last: isLast });
this.onUpdateEnd(sb, true, false);
};
this.mp4file = mp4File;
this.log('[MediaSource] sourceopen end', this, this.mp4file);
this.loadNextBuffer();
});
mediaSource.addEventListener('sourceended', () => {
this.log('[MediaSource] sourceended', mediaSource.readyState);
//this.getBufferAsync = null;
});
mediaSource.addEventListener('sourceclose', () => {
this.log('[MediaSource] sourceclose', mediaSource.readyState);
//this.getBufferAsync = null;
});
this.mediaSource = mediaSource;
}
private onInitAppended(sb: any) {
sb.sampleNum = 0;
sb.addEventListener('updateend', () => this.onUpdateEnd(sb, true, true));
/* In case there are already pending buffers we call onUpdateEnd to start appending them*/
this.onUpdateEnd(sb, false, true);
// @ts-ignore
this.mediaSource.pendingInits--;
// @ts-ignore
if(this.mediaSource.pendingInits === 0) {
this.log('onInitAppended start!');
this.mp4file.start();
if(this.expectedSize > this.bufferSize) {
this.nextBufferStart = this.bufferSize;
} else {
return;
}
/* setInterval(() => {
this.loadNextBuffer();
}, 1e3); */
this.loadNextBuffer();
}
};
private onUpdateEnd(sb: any, isNotInit: boolean, isEndOfAppend: boolean) {
//console.this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last);
if(isEndOfAppend === true) {
if(sb.sampleNum) {
this.mp4file.releaseUsedSamples(sb.id, sb.sampleNum);
delete sb.sampleNum;
}
if(sb.is_last) {
this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last);
this.mediaSource.endOfStream();
}
}
if(this.mediaSource.readyState === "open" && sb.updating === false && sb.pendingAppends.length > 0) {
const obj = sb.pendingAppends.shift();
this.log("MSE - SourceBuffer #"+sb.id, "Appending new buffer, pending: "+sb.pendingAppends.length);
sb.sampleNum = obj.sampleNum;
sb.is_last = obj.is_last;
sb.appendBuffer(obj.buffer);
}
}
private initializeAllSourceBuffers(info: any) {
for(let i = 0; i < info.tracks.length; i++) {
this.addSourceBuffer(info.tracks[i]);
}
this.initializeSourceBuffers();
}
private initializeSourceBuffers() {
const initSegs = this.mp4file.initializeSegmentation();
this.log('[MP4Box] initializeSegmentation', initSegs);
for(let i = 0; i < initSegs.length; i++) {
const sb: any = initSegs[i].user;
if(i === 0) {
// @ts-ignore
this.mediaSource.pendingInits = 0;
}
let onInitAppended = () => {
if(this.mediaSource.readyState === "open") {
sb.removeEventListener('updateend', onInitAppended);
this.onInitAppended(sb);
}
};
sb.addEventListener('updateend', onInitAppended);
sb.appendBuffer(initSegs[i].buffer);
sb.segmentIndex = 0;
// @ts-ignore
this.mediaSource.pendingInits++;
}
}
private addSourceBuffer(track: {id: number, codec: string, type: 'video', nb_samples: number}) {
const file = this.mp4file;
const ms = this.mediaSource;
if(!track) return;
const { id, codec, type: trackType, nb_samples } = track;
const mime = `video/mp4; codecs="${codec}"`;
this.log('mimetype:', mime);
if(!MediaSource.isTypeSupported(mime)) {
this.log('[addSourceBuffer] not supported', mime);
return;
}
const sb: any = ms.addSourceBuffer(mime);
sb.id = id;
sb.pendingAppends = [];
sb.nb_samples = nb_samples;
file.setSegmentOptions(id, sb, { nbSamples: this.nbSamples });
this.log('[addSourceBuffer] add', id, codec, trackType, sb);
sb.addEventListener("error", (e: Event) => {
this.log("MSE SourceBuffer #" + id, e);
});
}
stop() {
this.mp4file.stop();
this.mp4file = null;
this.getBufferAsync = null;
}
getURL() {
return this.url ?? (this.url = URL.createObjectURL(this.mediaSource));
}
seek(currentTime: number/* , buffered: any */) {
const seekInfo: {offset: number, time: number} = this.mp4file.seek(currentTime, true);
this.nextBufferStart = seekInfo.offset;
const loadNextBuffer = true;
/* let loadNextBuffer = buffered.length === 0;
for(let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if(start <= currentTime && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
} */
this.log('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart);
if(loadNextBuffer) {
this.loadNextBuffer(true);
}
return seekInfo.offset;
}
timeUpdate(currentTime: number, duration: number, buffered: any) {
//return;
const ranges = [];
for(let i = 0; i < buffered.length; i++) {
ranges.push({ start: buffered.start(i), end: buffered.end(i)})
}
let loadNextBuffer = buffered.length === 0;
let hasRange = false;
for(let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (start <= currentTime && currentTime <= end) {
hasRange = true;
if (end < duration && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
}
}
if(!hasRange) {
loadNextBuffer = true;
}
this.log('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges));
if(loadNextBuffer) {
this.loadNextBuffer();
}
}
async loadNextBuffer(seek = false) {
const { nextBufferStart, loading, currentBufferSize, mp4file } = this;
this.log('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file);
if(!mp4file) return;
if(nextBufferStart === undefined) return;
if(loading) return;
//return;
this.loading = true;
let bufferSize = seek ? this.seekBufferSize : this.bufferSize;
if(nextBufferStart + bufferSize > this.expectedSize) {
bufferSize = this.expectedSize - nextBufferStart;
}
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize);
// @ts-ignore
nextBuffer.fileStart = nextBufferStart;
const end = (nextBuffer.byteLength !== bufferSize)/* || (nextBuffer.byteLength === this.expectedSize) */;
this.log('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart, end);
if(nextBuffer.byteLength) {
this.nextBufferStart = mp4file.appendBuffer(nextBuffer/* , end */);
} else {
this.nextBufferStart = undefined;
}
if(end) {
this.log('[player] loadNextBuffer flush');
this.mp4file.flush();
}
this.log('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart);
this.loading = false;
if(!this.ready || !end) {
this.log('[player] loadNextBuffer next');
this.loadNextBuffer();
}
}
}