Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement live VTT subtitle loading #2148

Merged
merged 5 commits into from
Mar 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 0 additions & 29 deletions src/controller/audio-stream-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,6 @@ class AudioStreamController extends BaseStreamController {
this.videoTrackCC = null;
}

onHandlerDestroying () {
johnBartos marked this conversation as resolved.
Show resolved Hide resolved
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 () {
johnBartos marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
johnBartos marked this conversation as resolved.
Show resolved Hide resolved
}

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
Original file line number Diff line number Diff line change
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;
johnBartos marked this conversation as resolved.
Show resolved Hide resolved
// 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
Original file line number Diff line number Diff line change
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) => {
itsjamie marked this conversation as resolved.
Show resolved Hide resolved
newFrag.start = oldFrag.start;
lastIndex = index;
});

const frags = newPlaylist.fragments;
if (lastIndex < 0) {
itsjamie marked this conversation as resolved.
Show resolved Hide resolved
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) {
itsjamie marked this conversation as resolved.
Show resolved Hide resolved
if (!oldPlaylist || !newPlaylist) {
return;
}

const start = Math.max(oldPlaylist.startSN, newPlaylist.startSN) - newPlaylist.startSN;
itsjamie marked this conversation as resolved.
Show resolved Hide resolved
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is slightly different in the implementation that was here before.

Previously the max was taken between the rounded result in reloadInterval and minReloadInterval, now it's the max of the unrounded reloadInterval and minReloadInterval.

I don't see this resulting in any meaningful impact though. It could lead up to if I recall Javascript rounding right, a reload interval that occurs up to 0.49s early?

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
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,6 @@ class StreamController extends BaseStreamController {
this.gapController = null;
}

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

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

startLoad (startPosition) {
if (this.levels) {
let lastCurrentTime = this.lastCurrentTime, hls = this.hls;
Expand Down Expand Up @@ -95,23 +84,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();
itsjamie marked this conversation as resolved.
Show resolved Hide resolved
}

doTick () {
Expand Down