mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
update hls playback
This commit is contained in:
parent
7919706532
commit
396b125d66
27 changed files with 438 additions and 728 deletions
|
@ -132,164 +132,135 @@ class MP4Remuxer {
|
|||
}
|
||||
|
||||
remuxVideo(track, timeOffset, contiguous) {
|
||||
var offset = 8,
|
||||
var view,
|
||||
offset = 8,
|
||||
pesTimeScale = this.PES_TIMESCALE,
|
||||
pes2mp4ScaleFactor = this.PES2MP4SCALEFACTOR,
|
||||
mp4SampleDuration,
|
||||
avcSample,
|
||||
mp4Sample,
|
||||
mp4SampleLength,
|
||||
unit,
|
||||
mdat, moof,
|
||||
firstPTS, firstDTS,
|
||||
nextDTS,
|
||||
lastPTS, lastDTS,
|
||||
inputSamples = track.samples,
|
||||
outputSamples = [];
|
||||
|
||||
// PTS is coded on 33bits, and can loop from -2^32 to 2^32
|
||||
// PTSNormalize will make PTS/DTS value monotonic, we use last known DTS value as reference value
|
||||
let nextAvcDts;
|
||||
if (contiguous) {
|
||||
// if parsed fragment is contiguous with last one, let's use last DTS value as reference
|
||||
nextAvcDts = this.nextAvcDts;
|
||||
} else {
|
||||
// if not contiguous, let's use target timeOffset
|
||||
nextAvcDts = timeOffset*pesTimeScale;
|
||||
}
|
||||
|
||||
// compute first DTS and last DTS, normalize them against reference value
|
||||
let sample = inputSamples[0];
|
||||
firstDTS = Math.max(this._PTSNormalize(sample.dts,nextAvcDts) - this._initDTS,0);
|
||||
firstPTS = Math.max(this._PTSNormalize(sample.pts,nextAvcDts) - this._initDTS,0);
|
||||
|
||||
// check timestamp continuity accross consecutive fragments (this is to remove inter-fragment gap/hole)
|
||||
let delta = Math.round((firstDTS - nextAvcDts) / 90);
|
||||
// if fragment are contiguous, or if there is a huge delta (more than 10s) between expected PTS and sample PTS
|
||||
if (contiguous || Math.abs(delta) > 10000) {
|
||||
if (delta) {
|
||||
if (delta > 1) {
|
||||
logger.log(`AVC:${delta} ms hole between fragments detected,filling it`);
|
||||
} else if (delta < -1) {
|
||||
logger.log(`AVC:${(-delta)} ms overlapping between fragments detected`);
|
||||
}
|
||||
// remove hole/gap : set DTS to next expected DTS
|
||||
firstDTS = nextAvcDts;
|
||||
inputSamples[0].dts = firstDTS + this._initDTS;
|
||||
// offset PTS as well, ensure that PTS is smaller or equal than new DTS
|
||||
firstPTS = Math.max(firstPTS - delta, nextAvcDts);
|
||||
inputSamples[0].pts = firstPTS + this._initDTS;
|
||||
logger.log(`Video/PTS/DTS adjusted: ${firstPTS}/${firstDTS},delta:${delta}`);
|
||||
}
|
||||
}
|
||||
nextDTS = firstDTS;
|
||||
|
||||
// compute lastPTS/lastDTS
|
||||
sample = inputSamples[inputSamples.length-1];
|
||||
lastDTS = Math.max(this._PTSNormalize(sample.dts,nextAvcDts) - this._initDTS,0);
|
||||
lastPTS = Math.max(this._PTSNormalize(sample.pts,nextAvcDts) - this._initDTS,0);
|
||||
lastPTS = Math.max(lastPTS, lastDTS);
|
||||
|
||||
let vendor = navigator.vendor, userAgent = navigator.userAgent,
|
||||
isSafari = vendor && vendor.indexOf('Apple') > -1 && userAgent && !userAgent.match('CriOS');
|
||||
|
||||
// on Safari let's signal the same sample duration for all samples
|
||||
// sample duration (as expected by trun MP4 boxes), should be the delta between sample DTS
|
||||
// set this constant duration as being the avg delta between consecutive DTS.
|
||||
if (isSafari) {
|
||||
mp4SampleDuration = Math.round((lastDTS-firstDTS)/(pes2mp4ScaleFactor*(inputSamples.length-1)));
|
||||
}
|
||||
|
||||
// normalize all PTS/DTS now ...
|
||||
for (let i = 0; i < inputSamples.length; i++) {
|
||||
let sample = inputSamples[i];
|
||||
if (isSafari) {
|
||||
// sample DTS is computed using a constant decoding offset (mp4SampleDuration) between samples
|
||||
sample.dts = firstDTS + i*pes2mp4ScaleFactor*mp4SampleDuration;
|
||||
} else {
|
||||
// ensure sample monotonic DTS
|
||||
sample.dts = Math.max(this._PTSNormalize(sample.dts, nextAvcDts) - this._initDTS,firstDTS);
|
||||
// ensure dts is a multiple of scale factor to avoid rounding issues
|
||||
sample.dts = Math.round(sample.dts/pes2mp4ScaleFactor)*pes2mp4ScaleFactor;
|
||||
}
|
||||
// we normalize PTS against nextAvcDts, we also substract initDTS (some streams don't start @ PTS O)
|
||||
// and we ensure that computed value is greater or equal than sample DTS
|
||||
sample.pts = Math.max(this._PTSNormalize(sample.pts,nextAvcDts) - this._initDTS, sample.dts);
|
||||
// ensure pts is a multiple of scale factor to avoid rounding issues
|
||||
sample.pts = Math.round(sample.pts/pes2mp4ScaleFactor)*pes2mp4ScaleFactor;
|
||||
}
|
||||
|
||||
firstPTS, firstDTS, lastDTS,
|
||||
pts, dts, ptsnorm, dtsnorm,
|
||||
flags,
|
||||
samples = [];
|
||||
/* concatenate the video data and construct the mdat in place
|
||||
(need 8 more bytes to fill length and mpdat type) */
|
||||
mdat = new Uint8Array(track.len + (4 * track.nbNalu) + 8);
|
||||
let view = new DataView(mdat.buffer);
|
||||
view = new DataView(mdat.buffer);
|
||||
view.setUint32(0, mdat.byteLength);
|
||||
mdat.set(MP4.types.mdat, 4);
|
||||
|
||||
for (let i = 0; i < inputSamples.length; i++) {
|
||||
let avcSample = inputSamples[i],
|
||||
mp4SampleLength = 0,
|
||||
compositionTimeOffset;
|
||||
while (track.samples.length) {
|
||||
avcSample = track.samples.shift();
|
||||
mp4SampleLength = 0;
|
||||
// convert NALU bitstream to MP4 format (prepend NALU with size field)
|
||||
while (avcSample.units.units.length) {
|
||||
let unit = avcSample.units.units.shift();
|
||||
unit = avcSample.units.units.shift();
|
||||
view.setUint32(offset, unit.data.byteLength);
|
||||
offset += 4;
|
||||
mdat.set(unit.data, offset);
|
||||
offset += unit.data.byteLength;
|
||||
mp4SampleLength += 4 + unit.data.byteLength;
|
||||
}
|
||||
|
||||
if(!isSafari) {
|
||||
// expected sample duration is the Decoding Timestamp diff of consecutive samples
|
||||
if (i < inputSamples.length - 1) {
|
||||
mp4SampleDuration = inputSamples[i+1].dts - avcSample.dts;
|
||||
} else {
|
||||
// last sample duration is same than previous one
|
||||
mp4SampleDuration = avcSample.dts - inputSamples[i-1].dts;
|
||||
pts = avcSample.pts - this._initDTS;
|
||||
dts = avcSample.dts - this._initDTS;
|
||||
// ensure DTS is not bigger than PTS
|
||||
dts = Math.min(pts,dts);
|
||||
//logger.log(`Video/PTS/DTS:${Math.round(pts/90)}/${Math.round(dts/90)}`);
|
||||
// if not first AVC sample of video track, normalize PTS/DTS with previous sample value
|
||||
// and ensure that sample duration is positive
|
||||
if (lastDTS !== undefined) {
|
||||
ptsnorm = this._PTSNormalize(pts, lastDTS);
|
||||
dtsnorm = this._PTSNormalize(dts, lastDTS);
|
||||
var sampleDuration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
|
||||
if (sampleDuration <= 0) {
|
||||
logger.log(`invalid sample duration at PTS/DTS: ${avcSample.pts}/${avcSample.dts}:${sampleDuration}`);
|
||||
sampleDuration = 1;
|
||||
}
|
||||
mp4SampleDuration /= pes2mp4ScaleFactor;
|
||||
compositionTimeOffset = Math.round((avcSample.pts - avcSample.dts) / pes2mp4ScaleFactor);
|
||||
mp4Sample.duration = sampleDuration;
|
||||
} else {
|
||||
compositionTimeOffset = Math.max(0,mp4SampleDuration*Math.round((avcSample.pts - avcSample.dts)/(pes2mp4ScaleFactor*mp4SampleDuration)));
|
||||
let nextAvcDts, delta;
|
||||
if (contiguous) {
|
||||
nextAvcDts = this.nextAvcDts;
|
||||
} else {
|
||||
nextAvcDts = timeOffset*pesTimeScale;
|
||||
}
|
||||
// first AVC sample of video track, normalize PTS/DTS
|
||||
ptsnorm = this._PTSNormalize(pts, nextAvcDts);
|
||||
dtsnorm = this._PTSNormalize(dts, nextAvcDts);
|
||||
delta = Math.round((dtsnorm - nextAvcDts) / 90);
|
||||
// if fragment are contiguous, detect hole/overlapping between fragments
|
||||
if (contiguous) {
|
||||
if (delta) {
|
||||
if (delta > 1) {
|
||||
logger.log(`AVC:${delta} ms hole between fragments detected,filling it`);
|
||||
} else if (delta < -1) {
|
||||
logger.log(`AVC:${(-delta)} ms overlapping between fragments detected`);
|
||||
}
|
||||
// set DTS to next DTS
|
||||
dtsnorm = nextAvcDts;
|
||||
// offset PTS as well, ensure that PTS is smaller or equal than new DTS
|
||||
ptsnorm = Math.max(ptsnorm - delta, dtsnorm);
|
||||
logger.log(`Video/PTS/DTS adjusted: ${ptsnorm}/${dtsnorm},delta:${delta}`);
|
||||
}
|
||||
}
|
||||
// remember first PTS of our avcSamples, ensure value is positive
|
||||
firstPTS = Math.max(0, ptsnorm);
|
||||
firstDTS = Math.max(0, dtsnorm);
|
||||
}
|
||||
|
||||
|
||||
//console.log('PTS/DTS/initDTS/normPTS/normDTS/relative PTS : ${avcSample.pts}/${avcSample.dts}/${this._initDTS}/${ptsnorm}/${dtsnorm}/${(avcSample.pts/4294967296).toFixed(3)}');
|
||||
outputSamples.push({
|
||||
mp4Sample = {
|
||||
size: mp4SampleLength,
|
||||
// constant duration
|
||||
duration: mp4SampleDuration,
|
||||
cts: compositionTimeOffset,
|
||||
duration: 0,
|
||||
cts: (ptsnorm - dtsnorm) / pes2mp4ScaleFactor,
|
||||
flags: {
|
||||
isLeading: 0,
|
||||
isDependedOn: 0,
|
||||
hasRedundancy: 0,
|
||||
degradPrio: 0,
|
||||
dependsOn : avcSample.key ? 2 : 1,
|
||||
isNonSync : avcSample.key ? 0 : 1
|
||||
degradPrio: 0
|
||||
}
|
||||
});
|
||||
};
|
||||
flags = mp4Sample.flags;
|
||||
if (avcSample.key === true) {
|
||||
// the current sample is a key frame
|
||||
flags.dependsOn = 2;
|
||||
flags.isNonSync = 0;
|
||||
} else {
|
||||
flags.dependsOn = 1;
|
||||
flags.isNonSync = 1;
|
||||
}
|
||||
samples.push(mp4Sample);
|
||||
lastDTS = dtsnorm;
|
||||
}
|
||||
// next AVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale)
|
||||
this.nextAvcDts = lastDTS + mp4SampleDuration*pes2mp4ScaleFactor;
|
||||
var lastSampleDuration = 0;
|
||||
if (samples.length >= 2) {
|
||||
lastSampleDuration = samples[samples.length - 2].duration;
|
||||
mp4Sample.duration = lastSampleDuration;
|
||||
}
|
||||
// next AVC sample DTS should be equal to last sample DTS + last sample duration
|
||||
this.nextAvcDts = dtsnorm + lastSampleDuration * pes2mp4ScaleFactor;
|
||||
track.len = 0;
|
||||
track.nbNalu = 0;
|
||||
if(outputSamples.length && navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
|
||||
let flags = outputSamples[0].flags;
|
||||
if(samples.length && navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
|
||||
flags = samples[0].flags;
|
||||
// chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue
|
||||
// https://code.google.com/p/chromium/issues/detail?id=229412
|
||||
flags.dependsOn = 2;
|
||||
flags.isNonSync = 0;
|
||||
}
|
||||
track.samples = outputSamples;
|
||||
track.samples = samples;
|
||||
moof = MP4.moof(track.sequenceNumber++, firstDTS / pes2mp4ScaleFactor, track);
|
||||
track.samples = [];
|
||||
this.observer.trigger(Event.FRAG_PARSING_DATA, {
|
||||
data1: moof,
|
||||
data2: mdat,
|
||||
startPTS: firstPTS / pesTimeScale,
|
||||
endPTS: (lastPTS + pes2mp4ScaleFactor * mp4SampleDuration) / pesTimeScale,
|
||||
endPTS: (ptsnorm + pes2mp4ScaleFactor * lastSampleDuration) / pesTimeScale,
|
||||
startDTS: firstDTS / pesTimeScale,
|
||||
endDTS: this.nextAvcDts / pesTimeScale,
|
||||
type: 'video',
|
||||
nb: outputSamples.length
|
||||
nb: samples.length
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -328,7 +299,7 @@ class MP4Remuxer {
|
|||
mp4Sample.duration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
|
||||
if(Math.abs(mp4Sample.duration - expectedSampleDuration) > expectedSampleDuration/10) {
|
||||
// more than 10% diff between sample duration and expectedSampleDuration .... lets log that
|
||||
logger.trace(`invalid AAC sample duration at PTS ${Math.round(pts/90)},should be 1024,found :${Math.round(mp4Sample.duration*track.audiosamplerate/track.timescale)}`);
|
||||
logger.log(`invalid AAC sample duration at PTS ${Math.round(pts/90)},should be 1024,found :${Math.round(mp4Sample.duration*track.audiosamplerate/track.timescale)}`);
|
||||
}
|
||||
// always adjust sample duration to avoid av sync issue
|
||||
mp4Sample.duration = expectedSampleDuration;
|
||||
|
@ -343,20 +314,20 @@ class MP4Remuxer {
|
|||
ptsnorm = this._PTSNormalize(pts, nextAacPts);
|
||||
dtsnorm = this._PTSNormalize(dts, nextAacPts);
|
||||
delta = Math.round(1000 * (ptsnorm - nextAacPts) / pesTimeScale);
|
||||
// if fragment are contiguous, or if there is a huge delta (more than 10s) between expected PTS and sample PTS
|
||||
if (contiguous || Math.abs(delta) > 10000) {
|
||||
// if fragment are contiguous, detect hole/overlapping between fragments
|
||||
if (contiguous) {
|
||||
// log delta
|
||||
if (delta) {
|
||||
if (delta > 0) {
|
||||
logger.log(`${delta} ms hole between AAC samples detected,filling it`);
|
||||
// if we have frame overlap, overlapping for more than half a frame duraion
|
||||
// if we have frame overlap, overlapping for more than half a frame duration
|
||||
} else if (delta < -12) {
|
||||
// drop overlapping audio frames... browser will deal with it
|
||||
logger.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`);
|
||||
track.len -= unit.byteLength;
|
||||
continue;
|
||||
}
|
||||
// set PTS/DTS to expected PTS/DTS
|
||||
// set PTS/DTS to next PTS/DTS
|
||||
ptsnorm = dtsnorm = nextAacPts;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue