src/controller/level-helper.js
- /**
- * @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);
- }