Skip to content

Commit

Permalink
Merge pull request #192 from jwplayer/bugfix/live-vtt-synchronization
Browse files Browse the repository at this point in the history
[JW8-2659] Implement live VTT subtitle loading
  • Loading branch information
egreaves committed Feb 12, 2019
2 parents e45f8a6 + 8654222 commit 194bb41
Show file tree
Hide file tree
Showing 14 changed files with 625 additions and 383 deletions.
29 changes: 0 additions & 29 deletions src/controller/audio-stream-controller.js
Expand Up @@ -46,17 +46,6 @@ class AudioStreamController extends BaseStreamController {
this.videoTrackCC = null;
}

onHandlerDestroying () {
this.stopLoad();
super.onHandlerDestroying();
}

onHandlerDestroyed () {
this.state = State.STOPPED;
this.fragmentTracker = null;
super.onHandlerDestroyed();
}

// Signal that video PTS was found
onInitPtsFound (data) {
let demuxerId = data.id, cc = data.frag.cc, initPTS = data.initPTS;
Expand Down Expand Up @@ -96,24 +85,6 @@ class AudioStreamController extends BaseStreamController {
}
}

stopLoad () {
let frag = this.fragCurrent;
if (frag) {
if (frag.loader) {
frag.loader.abort();
}

this.fragmentTracker.removeFragment(frag);
this.fragCurrent = null;
}
this.fragPrevious = null;
if (this.demuxer) {
this.demuxer.destroy();
this.demuxer = null;
}
this.state = State.STOPPED;
}

set state (nextState) {
if (this.state !== nextState) {
const previousState = this.state;
Expand Down
31 changes: 31 additions & 0 deletions src/controller/base-stream-controller.js
Expand Up @@ -24,6 +24,27 @@ export const State = {
export default class BaseStreamController extends TaskLoop {
doTick () {}

startLoad () {}

stopLoad () {
let frag = this.fragCurrent;
if (frag) {
if (frag.loader) {
frag.loader.abort();
}
this.fragmentTracker.removeFragment(frag);
}
if (this.demuxer) {
this.demuxer.destroy();
this.demuxer = null;
}
this.fragCurrent = null;
this.fragPrevious = null;
this.clearInterval();
this.clearNextTick();
this.state = State.STOPPED;
}

_streamEnded (bufferInfo, levelDetails) {
const { fragCurrent, fragmentTracker } = this;
// we just got done loading the final fragment and there is no other buffered range after ...
Expand Down Expand Up @@ -94,4 +115,14 @@ export default class BaseStreamController extends TaskLoop {
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
this.startPosition = this.lastCurrentTime = 0;
}

onHandlerDestroying () {
this.stopLoad();
super.onHandlerDestroying();
}

onHandlerDestroyed () {
this.state = State.STOPPED;
this.fragmentTracker = null;
}
}
14 changes: 8 additions & 6 deletions src/controller/fragment-tracker.js
Expand Up @@ -234,13 +234,15 @@ export class FragmentTracker extends EventHandler {
const fragment = e.frag;
// don't track initsegment (for which sn is not a number)
// don't track frags used for bitrateTest, they're irrelevant.
if (Number.isFinite(fragment.sn) && !fragment.bitrateTest) {
this.fragments[this.getFragmentKey(fragment)] = {
body: fragment,
range: Object.create(null),
buffered: false
};
if (!Number.isFinite(fragment.sn) || fragment.bitrateTest) {
return;
}

this.fragments[this.getFragmentKey(fragment)] = {
body: fragment,
range: Object.create(null),
buffered: false
};
}

/**
Expand Down
26 changes: 6 additions & 20 deletions src/controller/level-controller.js
Expand Up @@ -7,7 +7,7 @@ import EventHandler from '../event-handler';
import { logger } from '../utils/logger';
import { ErrorTypes, ErrorDetails } from '../errors';
import { isCodecSupportedInMp4 } from '../utils/codecs';
import { addGroupId } from './level-helper';
import { addGroupId, computeReloadInterval } from './level-helper';

const { performance } = window;
let chromeOrFirefox;
Expand Down Expand Up @@ -382,35 +382,21 @@ export default class LevelController extends EventHandler {
}

onLevelLoaded (data) {
const levelId = data.level;
const { level, details } = data;
// only process level loaded events matching with expected level
if (levelId !== this.currentLevelIndex) {
if (level !== this.currentLevelIndex) {
return;
}

const curLevel = this._levels[levelId];
const curLevel = this._levels[level];
// reset level load error counter on successful level loaded only if there is no issues with fragments
if (!curLevel.fragmentError) {
curLevel.loadError = 0;
this.levelRetryCount = 0;
}
let newDetails = data.details;
// if current playlist is a live playlist, arm a timer to reload it
if (newDetails.live) {
const targetdurationMs = 1000 * (newDetails.averagetargetduration ? newDetails.averagetargetduration : newDetails.targetduration);
let reloadInterval = targetdurationMs,
curDetails = curLevel.details;
if (curDetails && newDetails.endSN === curDetails.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 /= 2;
logger.log('same live playlist, reload twice faster');
}
// decrement reloadInterval with level loading delay
reloadInterval -= performance.now() - data.stats.trequest;
// in any case, don't reload more than half of target duration
reloadInterval = Math.max(targetdurationMs / 2, Math.round(reloadInterval));
if (details.live) {
const reloadInterval = computeReloadInterval(curLevel.details, details, data.stats.trequest);
logger.log(`live playlist, reload in ${Math.round(reloadInterval)} ms`);
this.timer = setTimeout(() => this.loadLevel(), reloadInterval);
} else {
Expand Down
126 changes: 91 additions & 35 deletions src/controller/level-helper.js
Expand Up @@ -110,64 +110,120 @@ export function updateFragPTSDTS (details, frag, startPTS, endPTS, startDTS, end
}

export function mergeDetails (oldDetails, newDetails) {
let start = Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN,
end = Math.min(oldDetails.endSN, newDetails.endSN) - newDetails.startSN,
delta = newDetails.startSN - oldDetails.startSN,
oldfragments = oldDetails.fragments,
newfragments = newDetails.fragments,
ccOffset = 0,
PTSFrag;

// potentially retrieve cached initsegment
if (newDetails.initSegment && oldDetails.initSegment) {
newDetails.initSegment = oldDetails.initSegment;
}

// check if old/new playlists have fragments in common
if (end < start) {
newDetails.PTSKnown = false;
return;
}
// loop through overlapping SN and update startPTS , cc, and duration if any found
for (var i = start; i <= end; i++) {
let oldFrag = oldfragments[delta + i],
newFrag = newfragments[i];
if (newFrag && oldFrag) {
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;
}
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');
for (i = 0; i < newfragments.length; i++) {
newfragments[i].cc += ccOffset;
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
// 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
if (delta >= 0 && delta < oldfragments.length) {
// adjust start by sliding offset
let sliding = oldfragments[delta].start;
for (i = 0; i < newfragments.length; i++) {
newfragments[i].start += sliding;
}
}
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);
}
28 changes: 1 addition & 27 deletions src/controller/stream-controller.js
Expand Up @@ -52,17 +52,6 @@ class StreamController extends BaseStreamController {
this._state = State.STOPPED;
}

onHandlerDestroying () {
this.stopLoad();
super.onHandlerDestroying();
}

onHandlerDestroyed () {
this.state = State.STOPPED;
this.fragmentTracker = null;
super.onHandlerDestroyed();
}

startLoad (startPosition) {
if (this.levels) {
const { lastCurrentTime, hls } = this;
Expand Down Expand Up @@ -102,23 +91,8 @@ class StreamController extends BaseStreamController {
}

stopLoad () {
let frag = this.fragCurrent;
if (frag) {
if (frag.loader) {
frag.loader.abort();
}

this.fragmentTracker.removeFragment(frag);
this.fragCurrent = null;
}
this.fragPrevious = null;
if (this.demuxer) {
this.demuxer.destroy();
this.demuxer = null;
}
this.clearInterval();
this.state = State.STOPPED;
this.forceStartLoad = false;
super.stopLoad();
}

doTick () {
Expand Down

0 comments on commit 194bb41

Please sign in to comment.