Skip to content

Commit bf4a458

Browse files
fix: Various fixes for llhls so that we start closer to live, and stay closer to live (#1201)
* Don't switch renditions when the pending rendition is the rendition we would switch to * Don't switch renditions before playback starts for llhls * Don't set seekable until all source buffers have been created * Take into account parts and preload segments when during duration calculations * Reset the segment loader on rendition change for live streams, still resync for vod * Try to choose an independent first part if we have no buffered data * Determine if we made a bad part guess for our segment download
1 parent 1fe2df1 commit bf4a458

12 files changed

Lines changed: 875 additions & 57 deletions

src/master-playlist-controller.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ const sumLoaderStat = function(stat) {
4747
};
4848
const shouldSwitchToMedia = function({
4949
currentPlaylist,
50+
buffered,
51+
currentTime,
5052
nextPlaylist,
51-
forwardBuffer,
5253
bufferLowWaterLine,
5354
bufferHighWaterLine,
5455
duration,
@@ -73,15 +74,25 @@ const shouldSwitchToMedia = function({
7374
return false;
7475
}
7576

77+
// determine if current time is in a buffered range.
78+
const isBuffered = Boolean(Ranges.findRange(buffered, currentTime).length);
79+
7680
// If the playlist is live, then we want to not take low water line into account.
7781
// This is because in LIVE, the player plays 3 segments from the end of the
7882
// playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
7983
// in those segments, a viewer will never experience a rendition upswitch.
8084
if (!currentPlaylist.endList) {
85+
// For LLHLS live streams, don't switch renditions before playback has started, as it almost
86+
// doubles the time to first playback.
87+
if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') {
88+
log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`);
89+
return false;
90+
}
8191
log(`${sharedLogLine} as current playlist is live`);
8292
return true;
8393
}
8494

95+
const forwardBuffer = Ranges.timeAheadOf(buffered, currentTime);
8596
const maxBufferLowWaterLine = experimentalBufferBasedABR ?
8697
Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE;
8798

@@ -732,18 +743,18 @@ export class MasterPlaylistController extends videojs.EventTarget {
732743
}
733744

734745
shouldSwitchToMedia_(nextPlaylist) {
735-
const currentPlaylist = this.masterPlaylistLoader_.media();
736-
const buffered = this.tech_.buffered();
737-
const forwardBuffer = buffered.length ?
738-
buffered.end(buffered.length - 1) - this.tech_.currentTime() : 0;
739-
746+
const currentPlaylist = this.masterPlaylistLoader_.media() ||
747+
this.masterPlaylistLoader_.pendingMedia_;
748+
const currentTime = this.tech_.currentTime();
740749
const bufferLowWaterLine = this.bufferLowWaterLine();
741750
const bufferHighWaterLine = this.bufferHighWaterLine();
751+
const buffered = this.tech_.buffered();
742752

743753
return shouldSwitchToMedia({
754+
buffered,
755+
currentTime,
744756
currentPlaylist,
745757
nextPlaylist,
746-
forwardBuffer,
747758
bufferLowWaterLine,
748759
bufferHighWaterLine,
749760
duration: this.duration(),
@@ -1434,7 +1445,9 @@ export class MasterPlaylistController extends videojs.EventTarget {
14341445
onSyncInfoUpdate_() {
14351446
let audioSeekable;
14361447

1437-
if (!this.masterPlaylistLoader_) {
1448+
// If we have two source buffers and only one is created then the seekable range will be incorrect.
1449+
// We should wait until all source buffers are created.
1450+
if (!this.masterPlaylistLoader_ || this.sourceUpdater_.hasCreatedSourceBuffers()) {
14381451
return;
14391452
}
14401453

src/playback-watcher.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,15 @@ export default class PlaybackWatcher {
351351
const buffered = this.tech_.buffered();
352352
const audioBuffered = sourceUpdater.audioBuffer ? sourceUpdater.audioBuffered() : null;
353353
const videoBuffered = sourceUpdater.videoBuffer ? sourceUpdater.videoBuffered() : null;
354+
const media = this.media();
355+
356+
// verify that at least two segment durations or one part duration have been
357+
// appended before checking for a gap.
358+
const minAppendedDuration = media.partTargetDuration ? media.partTargetDuration :
359+
(media.targetDuration - Ranges.TIME_FUDGE_FACTOR) * 2;
354360

355361
// verify that at least two segment durations have been
356362
// appended before checking for a gap.
357-
const twoSegmentDurations = (this.media().targetDuration - Ranges.TIME_FUDGE_FACTOR) * 2;
358363
const bufferedToCheck = [audioBuffered, videoBuffered];
359364

360365
for (let i = 0; i < bufferedToCheck.length; i++) {
@@ -365,9 +370,9 @@ export default class PlaybackWatcher {
365370

366371
const timeAhead = Ranges.timeAheadOf(bufferedToCheck[i], currentTime);
367372

368-
// if we are less than two video/audio segment durations behind,
369-
// we haven't appended enough to call this a bad seek.
370-
if (timeAhead < twoSegmentDurations) {
373+
// if we are less than two video/audio segment durations or one part
374+
// duration behind we haven't appended enough to call this a bad seek.
375+
if (timeAhead < minAppendedDuration) {
371376
return false;
372377
}
373378
}

src/playlist-loader.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ const getAllSegments = function(media) {
257257
export const isPlaylistUnchanged = (a, b) => a === b ||
258258
(a.segments && b.segments && a.segments.length === b.segments.length &&
259259
a.endList === b.endList &&
260-
a.mediaSequence === b.mediaSequence);
260+
a.mediaSequence === b.mediaSequence &&
261+
a.preloadSegment === b.preloadSegment);
261262

262263
/**
263264
* Returns a new master playlist that is the result of merging an
@@ -516,6 +517,8 @@ export default class PlaylistLoader extends EventTarget {
516517

517518
this.targetDuration = playlist.partTargetDuration || playlist.targetDuration;
518519

520+
this.pendingMedia_ = null;
521+
519522
if (update) {
520523
this.master = update;
521524
this.media_ = this.master.playlists[id];
@@ -662,6 +665,8 @@ export default class PlaylistLoader extends EventTarget {
662665
this.trigger('mediachanging');
663666
}
664667

668+
this.pendingMedia_ = playlist;
669+
665670
this.request = this.vhs_.xhr({
666671
uri: playlist.resolvedUri,
667672
withCredentials: this.withCredentials

src/playlist.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,43 @@ import {TIME_FUDGE_FACTOR} from './ranges.js';
1010

1111
const {createTimeRange} = videojs;
1212

13+
/**
14+
* Get the duration of a segment, with special cases for
15+
* llhls segments that do not have a duration yet.
16+
*
17+
* @param {Object} playlist
18+
* the playlist that the segment belongs to.
19+
* @param {Object} segment
20+
* the segment to get a duration for.
21+
*
22+
* @return {number}
23+
* the segment duration
24+
*/
25+
export const segmentDurationWithParts = (playlist, segment) => {
26+
// if this isn't a preload segment
27+
// then we will have a segment duration that is accurate.
28+
if (!segment.preload) {
29+
return segment.duration;
30+
}
31+
32+
// otherwise we have to add up parts and preload hints
33+
// to get an up to date duration.
34+
let result = 0;
35+
36+
(segment.parts || []).forEach(function(p) {
37+
result += p.duration;
38+
});
39+
40+
// for preload hints we have to use partTargetDuration
41+
// as they won't even have a duration yet.
42+
(segment.preloadHints || []).forEach(function(p) {
43+
if (p.type === 'PART') {
44+
result += playlist.partTargetDuration;
45+
}
46+
});
47+
48+
return result;
49+
};
1350
/**
1451
* A function to get a combined list of parts and segments with durations
1552
* and indexes.
@@ -117,7 +154,7 @@ const backwardDuration = function(playlist, endSequence) {
117154
return { result: result + segment.end, precise: true };
118155
}
119156

120-
result += segment.duration;
157+
result += segmentDurationWithParts(playlist, segment);
121158

122159
if (typeof segment.start !== 'undefined') {
123160
return { result: result + segment.start, precise: true };
@@ -149,7 +186,7 @@ const forwardDuration = function(playlist, endSequence) {
149186
};
150187
}
151188

152-
result += segment.duration;
189+
result += segmentDurationWithParts(playlist, segment);
153190

154191
if (typeof segment.end !== 'undefined') {
155192
return {
@@ -321,19 +358,19 @@ export const playlistEnd = function(playlist, expired, useSafeLiveEnd, liveEdgeP
321358

322359
expired = expired || 0;
323360

324-
let lastSegmentTime = intervalDuration(
361+
let lastSegmentEndTime = intervalDuration(
325362
playlist,
326363
playlist.mediaSequence + playlist.segments.length,
327364
expired
328365
);
329366

330367
if (useSafeLiveEnd) {
331368
liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist);
332-
lastSegmentTime -= liveEdgePadding;
369+
lastSegmentEndTime -= liveEdgePadding;
333370
}
334371

335372
// don't return a time less than zero
336-
return Math.max(0, lastSegmentTime);
373+
return Math.max(0, lastSegmentEndTime);
337374
};
338375

339376
/**
@@ -737,5 +774,6 @@ export default {
737774
estimateSegmentRequestTime,
738775
isLowestEnabledRendition,
739776
isAudioOnly,
740-
playlistMatch
777+
playlistMatch,
778+
segmentDurationWithParts
741779
};

src/segment-loader.js

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ import {
2222
import { gopsSafeToAlignWith, removeGopBuffer, updateGopBuffer } from './util/gops';
2323
import shallowEqual from './util/shallow-equal.js';
2424
import { QUOTA_EXCEEDED_ERR } from './error-codes';
25-
import { timeRangesToArray } from './ranges';
26-
import {lastBufferedEnd} from './ranges.js';
25+
import {timeRangesToArray, lastBufferedEnd, timeAheadOf} from './ranges.js';
2726
import {getKnownPartCount} from './playlist.js';
2827

2928
/**
@@ -135,7 +134,7 @@ export const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) =>
135134
return Math.min(maxTrimTime, trimTime);
136135
};
137136

138-
const segmentInfoString = (segmentInfo) => {
137+
export const segmentInfoString = (segmentInfo) => {
139138
const {
140139
startOfSegment,
141140
duration,
@@ -160,6 +159,10 @@ const segmentInfoString = (segmentInfo) => {
160159
selection = 'getSyncSegmentCandidate (isSyncRequest)';
161160
}
162161

162+
if (segmentInfo.independent) {
163+
selection += ` with independent ${segmentInfo.independent}`;
164+
}
165+
163166
const hasPartIndex = typeof partIndex === 'number';
164167
const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment';
165168
const zeroBasedPartCount = hasPartIndex ? getKnownPartCount({preloadSegment: segment}) - 1 : 0;
@@ -1024,9 +1027,20 @@ export default class SegmentLoader extends videojs.EventTarget {
10241027

10251028
if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
10261029
if (this.mediaIndex !== null) {
1027-
// we must "resync" the segment loader when we switch renditions and
1030+
// we must reset/resync the segment loader when we switch renditions and
10281031
// the segment loader is already synced to the previous rendition
1029-
this.resyncLoader();
1032+
1033+
// on playlist changes we want it to be possible to fetch
1034+
// at the buffer for vod but not for live. So we use resetLoader
1035+
// for live and resyncLoader for vod. We want this because
1036+
// if a playlist uses independent and non-independent segments/parts the
1037+
// buffer may not accurately reflect the next segment that we should try
1038+
// downloading.
1039+
if (!newPlaylist.endList) {
1040+
this.resetLoader();
1041+
} else {
1042+
this.resyncLoader();
1043+
}
10301044
}
10311045
this.currentMediaInfo_ = void 0;
10321046
this.trigger('playlistupdate');
@@ -1366,8 +1380,9 @@ export default class SegmentLoader extends videojs.EventTarget {
13661380
* @return {Object} a request object that describes the segment/part to load
13671381
*/
13681382
chooseNextRequest_() {
1369-
const bufferedEnd = lastBufferedEnd(this.buffered_()) || 0;
1370-
const bufferedTime = Math.max(0, bufferedEnd - this.currentTime_());
1383+
const buffered = this.buffered_();
1384+
const bufferedEnd = lastBufferedEnd(buffered) || 0;
1385+
const bufferedTime = timeAheadOf(buffered, this.currentTime_());
13711386
const preloaded = !this.hasPlayed_() && bufferedTime >= 1;
13721387
const haveEnoughBuffer = bufferedTime >= this.goalBufferLength_();
13731388
const segments = this.playlist_.segments;
@@ -1420,14 +1435,15 @@ export default class SegmentLoader extends videojs.EventTarget {
14201435
startTime: this.syncPoint_.time
14211436
});
14221437

1423-
next.getMediaInfoForTime = this.fetchAtBuffer_ ? 'bufferedEnd' : 'currentTime';
1438+
next.getMediaInfoForTime = this.fetchAtBuffer_ ?
1439+
`bufferedEnd ${bufferedEnd}` : `currentTime ${this.currentTime_()}`;
14241440
next.mediaIndex = segmentIndex;
14251441
next.startOfSegment = startTime;
14261442
next.partIndex = partIndex;
14271443
}
14281444

14291445
const nextSegment = segments[next.mediaIndex];
1430-
const nextPart = nextSegment &&
1446+
let nextPart = nextSegment &&
14311447
typeof next.partIndex === 'number' &&
14321448
nextSegment.parts &&
14331449
nextSegment.parts[next.partIndex];
@@ -1442,6 +1458,28 @@ export default class SegmentLoader extends videojs.EventTarget {
14421458
// Set partIndex to 0
14431459
if (typeof next.partIndex !== 'number' && nextSegment.parts) {
14441460
next.partIndex = 0;
1461+
nextPart = nextSegment.parts[0];
1462+
}
1463+
1464+
// if we have no buffered data then we need to make sure
1465+
// that the next part we append is "independent" if possible.
1466+
// So we check if the previous part is independent, and request
1467+
// it if it is.
1468+
if (!bufferedTime && nextPart && !nextPart.independent) {
1469+
1470+
if (next.partIndex === 0) {
1471+
const lastSegment = segments[next.mediaIndex - 1];
1472+
const lastSegmentLastPart = lastSegment.parts && lastSegment.parts.length && lastSegment.parts[lastSegment.parts.length - 1];
1473+
1474+
if (lastSegmentLastPart && lastSegmentLastPart.independent) {
1475+
next.mediaIndex -= 1;
1476+
next.partIndex = lastSegment.parts.length - 1;
1477+
next.independent = 'previous segment';
1478+
}
1479+
} else if (nextSegment.parts[next.partIndex - 1].independent) {
1480+
next.partIndex -= 1;
1481+
next.independent = 'previous part';
1482+
}
14451483
}
14461484

14471485
const ended = this.mediaSource_ && this.mediaSource_.readyState === 'ended';
@@ -1459,6 +1497,7 @@ export default class SegmentLoader extends videojs.EventTarget {
14591497

14601498
generateSegmentInfo_(options) {
14611499
const {
1500+
independent,
14621501
playlist,
14631502
mediaIndex,
14641503
startOfSegment,
@@ -1499,7 +1538,8 @@ export default class SegmentLoader extends videojs.EventTarget {
14991538
byteLength: 0,
15001539
transmuxer: this.transmuxer_,
15011540
// type of getMediaInfoForTime that was used to get this segment
1502-
getMediaInfoForTime
1541+
getMediaInfoForTime,
1542+
independent
15031543
};
15041544

15051545
const overrideCheck =
@@ -1991,7 +2031,7 @@ export default class SegmentLoader extends videojs.EventTarget {
19912031
this.setTimeMapping_(segmentInfo.timeline);
19922032

19932033
// for tracking overall stats
1994-
this.updateMediaSecondsLoaded_(segmentInfo.segment);
2034+
this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment);
19952035

19962036
// Note that the state isn't changed from loading to appending. This is because abort
19972037
// logic may change behavior depending on the state, and changing state too early may
@@ -2995,15 +3035,19 @@ export default class SegmentLoader extends videojs.EventTarget {
29953035
// and attempt to resync when the post-update seekable window and live
29963036
// point would mean that this was the perfect segment to fetch
29973037
this.trigger('syncinfoupdate');
2998-
29993038
const segment = segmentInfo.segment;
3039+
const part = segmentInfo.part;
3040+
const badSegmentGuess = segment.end &&
3041+
this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3;
3042+
const badPartGuess = part &&
3043+
part.end && this.currentTime_() - part.end > segmentInfo.playlist.partTargetDuration * 3;
30003044

3001-
// If we previously appended a segment that ends more than 3 targetDurations before
3045+
// If we previously appended a segment/part that ends more than 3 part/targetDurations before
30023046
// the currentTime_ that means that our conservative guess was too conservative.
30033047
// In that case, reset the loader state so that we try to use any information gained
30043048
// from the previous request to create a new, more accurate, sync-point.
3005-
if (segment.end &&
3006-
this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3) {
3049+
if (badSegmentGuess || badPartGuess) {
3050+
this.logger_(`bad ${badSegmentGuess ? 'segment' : 'part'} ${segmentInfoString(segmentInfo)}`);
30073051
this.resetEverything();
30083052
return;
30093053
}

0 commit comments

Comments
 (0)