mirror of https://bitbucket.org/ausocean/av.git
230 lines
7.6 KiB
JavaScript
230 lines
7.6 KiB
JavaScript
/**
|
|
* @module LevelHelper
|
|
*
|
|
* Providing methods dealing with playlist sliding and drift
|
|
*
|
|
* TODO: Create an actual `Level` class/model that deals with all this logic in an object-oriented-manner.
|
|
*
|
|
* */
|
|
|
|
import { logger } from '../utils/logger';
|
|
|
|
export function addGroupId (level, type, id) {
|
|
switch (type) {
|
|
case 'audio':
|
|
if (!level.audioGroupIds) {
|
|
level.audioGroupIds = [];
|
|
}
|
|
level.audioGroupIds.push(id);
|
|
break;
|
|
case 'text':
|
|
if (!level.textGroupIds) {
|
|
level.textGroupIds = [];
|
|
}
|
|
level.textGroupIds.push(id);
|
|
break;
|
|
}
|
|
}
|
|
|
|
export function updatePTS (fragments, fromIdx, toIdx) {
|
|
let fragFrom = fragments[fromIdx], fragTo = fragments[toIdx], fragToPTS = fragTo.startPTS;
|
|
// if we know startPTS[toIdx]
|
|
if (Number.isFinite(fragToPTS)) {
|
|
// update fragment duration.
|
|
// it helps to fix drifts between playlist reported duration and fragment real duration
|
|
if (toIdx > fromIdx) {
|
|
fragFrom.duration = fragToPTS - fragFrom.start;
|
|
if (fragFrom.duration < 0) {
|
|
logger.warn(`negative duration computed for frag ${fragFrom.sn},level ${fragFrom.level}, there should be some duration drift between playlist and fragment!`);
|
|
}
|
|
} else {
|
|
fragTo.duration = fragFrom.start - fragToPTS;
|
|
if (fragTo.duration < 0) {
|
|
logger.warn(`negative duration computed for frag ${fragTo.sn},level ${fragTo.level}, there should be some duration drift between playlist and fragment!`);
|
|
}
|
|
}
|
|
} else {
|
|
// we dont know startPTS[toIdx]
|
|
if (toIdx > fromIdx) {
|
|
fragTo.start = fragFrom.start + fragFrom.duration;
|
|
} else {
|
|
fragTo.start = Math.max(fragFrom.start - fragTo.duration, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function updateFragPTSDTS (details, frag, startPTS, endPTS, startDTS, endDTS) {
|
|
// update frag PTS/DTS
|
|
let maxStartPTS = startPTS;
|
|
if (Number.isFinite(frag.startPTS)) {
|
|
// delta PTS between audio and video
|
|
let deltaPTS = Math.abs(frag.startPTS - startPTS);
|
|
if (!Number.isFinite(frag.deltaPTS)) {
|
|
frag.deltaPTS = deltaPTS;
|
|
} else {
|
|
frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS);
|
|
}
|
|
|
|
maxStartPTS = Math.max(startPTS, frag.startPTS);
|
|
startPTS = Math.min(startPTS, frag.startPTS);
|
|
endPTS = Math.max(endPTS, frag.endPTS);
|
|
startDTS = Math.min(startDTS, frag.startDTS);
|
|
endDTS = Math.max(endDTS, frag.endDTS);
|
|
}
|
|
|
|
const drift = startPTS - frag.start;
|
|
frag.start = frag.startPTS = startPTS;
|
|
frag.maxStartPTS = maxStartPTS;
|
|
frag.endPTS = endPTS;
|
|
frag.startDTS = startDTS;
|
|
frag.endDTS = endDTS;
|
|
frag.duration = endPTS - startPTS;
|
|
|
|
const sn = frag.sn;
|
|
// exit if sn out of range
|
|
if (!details || sn < details.startSN || sn > details.endSN) {
|
|
return 0;
|
|
}
|
|
|
|
let fragIdx, fragments, i;
|
|
fragIdx = sn - details.startSN;
|
|
fragments = details.fragments;
|
|
// update frag reference in fragments array
|
|
// rationale is that fragments array might not contain this frag object.
|
|
// this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS()
|
|
// if we don't update frag, we won't be able to propagate PTS info on the playlist
|
|
// resulting in invalid sliding computation
|
|
fragments[fragIdx] = frag;
|
|
// adjust fragment PTS/duration from seqnum-1 to frag 0
|
|
for (i = fragIdx; i > 0; i--) {
|
|
updatePTS(fragments, i, i - 1);
|
|
}
|
|
|
|
// adjust fragment PTS/duration from seqnum to last frag
|
|
for (i = fragIdx; i < fragments.length - 1; i++) {
|
|
updatePTS(fragments, i, i + 1);
|
|
}
|
|
|
|
details.PTSKnown = true;
|
|
return drift;
|
|
}
|
|
|
|
export function mergeDetails (oldDetails, newDetails) {
|
|
// potentially retrieve cached initsegment
|
|
if (newDetails.initSegment && oldDetails.initSegment) {
|
|
newDetails.initSegment = oldDetails.initSegment;
|
|
}
|
|
|
|
// check if old/new playlists have fragments in common
|
|
// loop through overlapping SN and update startPTS , cc, and duration if any found
|
|
let ccOffset = 0;
|
|
let PTSFrag;
|
|
mapFragmentIntersection(oldDetails, newDetails, (oldFrag, newFrag) => {
|
|
ccOffset = oldFrag.cc - newFrag.cc;
|
|
if (Number.isFinite(oldFrag.startPTS)) {
|
|
newFrag.start = newFrag.startPTS = oldFrag.startPTS;
|
|
newFrag.endPTS = oldFrag.endPTS;
|
|
newFrag.duration = oldFrag.duration;
|
|
newFrag.backtracked = oldFrag.backtracked;
|
|
newFrag.dropped = oldFrag.dropped;
|
|
PTSFrag = newFrag;
|
|
}
|
|
// PTS is known when there are overlapping segments
|
|
newDetails.PTSKnown = true;
|
|
});
|
|
|
|
if (!newDetails.PTSKnown) {
|
|
return;
|
|
}
|
|
|
|
if (ccOffset) {
|
|
logger.log('discontinuity sliding from playlist, take drift into account');
|
|
const newFragments = newDetails.fragments;
|
|
for (let i = 0; i < newFragments.length; i++) {
|
|
newFragments[i].cc += ccOffset;
|
|
}
|
|
}
|
|
|
|
// if at least one fragment contains PTS info, recompute PTS information for all fragments
|
|
if (PTSFrag) {
|
|
updateFragPTSDTS(newDetails, PTSFrag, PTSFrag.startPTS, PTSFrag.endPTS, PTSFrag.startDTS, PTSFrag.endDTS);
|
|
} else {
|
|
// ensure that delta is within oldFragments range
|
|
// also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
|
|
// in that case we also need to adjust start offset of all fragments
|
|
adjustSliding(oldDetails, newDetails);
|
|
}
|
|
// if we are here, it means we have fragments overlapping between
|
|
// old and new level. reliable PTS info is thus relying on old level
|
|
newDetails.PTSKnown = oldDetails.PTSKnown;
|
|
}
|
|
|
|
export function mergeSubtitlePlaylists (oldPlaylist, newPlaylist, referenceStart = 0) {
|
|
let lastIndex = -1;
|
|
mapFragmentIntersection(oldPlaylist, newPlaylist, (oldFrag, newFrag, index) => {
|
|
newFrag.start = oldFrag.start;
|
|
lastIndex = index;
|
|
});
|
|
|
|
const frags = newPlaylist.fragments;
|
|
if (lastIndex < 0) {
|
|
frags.forEach(frag => {
|
|
frag.start += referenceStart;
|
|
});
|
|
return;
|
|
}
|
|
|
|
for (let i = lastIndex + 1; i < frags.length; i++) {
|
|
frags[i].start = (frags[i - 1].start + frags[i - 1].duration);
|
|
}
|
|
}
|
|
|
|
export function mapFragmentIntersection (oldPlaylist, newPlaylist, intersectionFn) {
|
|
if (!oldPlaylist || !newPlaylist) {
|
|
return;
|
|
}
|
|
|
|
const start = Math.max(oldPlaylist.startSN, newPlaylist.startSN) - newPlaylist.startSN;
|
|
const end = Math.min(oldPlaylist.endSN, newPlaylist.endSN) - newPlaylist.startSN;
|
|
const delta = newPlaylist.startSN - oldPlaylist.startSN;
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
const oldFrag = oldPlaylist.fragments[delta + i];
|
|
const newFrag = newPlaylist.fragments[i];
|
|
if (!oldFrag || !newFrag) {
|
|
break;
|
|
}
|
|
intersectionFn(oldFrag, newFrag, i);
|
|
}
|
|
}
|
|
|
|
export function adjustSliding (oldPlaylist, newPlaylist) {
|
|
const delta = newPlaylist.startSN - oldPlaylist.startSN;
|
|
const oldFragments = oldPlaylist.fragments;
|
|
const newFragments = newPlaylist.fragments;
|
|
|
|
if (delta < 0 || delta > oldFragments.length) {
|
|
return;
|
|
}
|
|
for (let i = 0; i < newFragments.length; i++) {
|
|
newFragments[i].start += oldFragments[delta].start;
|
|
}
|
|
}
|
|
|
|
export function computeReloadInterval (currentPlaylist, newPlaylist, lastRequestTime) {
|
|
let reloadInterval = 1000 * (newPlaylist.averagetargetduration ? newPlaylist.averagetargetduration : newPlaylist.targetduration);
|
|
const minReloadInterval = reloadInterval / 2;
|
|
if (currentPlaylist && newPlaylist.endSN === currentPlaylist.endSN) {
|
|
// follow HLS Spec, If the client reloads a Playlist file and finds that it has not
|
|
// changed then it MUST wait for a period of one-half the target
|
|
// duration before retrying.
|
|
reloadInterval = minReloadInterval;
|
|
}
|
|
|
|
if (lastRequestTime) {
|
|
reloadInterval = Math.max(minReloadInterval, reloadInterval - (window.performance.now() - lastRequestTime));
|
|
}
|
|
// in any case, don't reload more than half of target duration
|
|
return Math.round(reloadInterval);
|
|
}
|