Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
9.7 KiB
323 lines
9.7 KiB
/* |
|
* 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(); |
|
} |
|
} |
|
} |