-
Notifications
You must be signed in to change notification settings - Fork 426
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
feat: use serverControl and preloadSegment llhls features behind a flag #1078
Changes from 20 commits
123aba7
32de23e
3f8ad12
9c9493a
73278fa
e7598c6
d3236ca
c2ebf7b
a8957ff
2ce6e59
0fa6dee
b4e219c
a58bcef
83bf171
43720f0
6ce8c1e
c3bef56
5d78a1f
467832f
d35fa4d
4374c0e
e80c0fb
b77c94c
dffa291
3aa8582
1aa7785
b381d96
088ee6e
d7c30d2
01a6763
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,7 +83,7 @@ export const updateSegments = (original, update, offset) => { | |
}; | ||
|
||
export const resolveSegmentUris = (segment, baseUri) => { | ||
// preloadSegments will not have a uri at all | ||
// preloadSegment will not have a uri at all | ||
// as the segment isn't actually in the manifest yet, only parts | ||
if (!segment.resolvedUri && segment.uri) { | ||
segment.resolvedUri = resolveUrl(baseUri, segment.uri); | ||
|
@@ -113,6 +113,19 @@ export const resolveSegmentUris = (segment, baseUri) => { | |
} | ||
}; | ||
|
||
const getAllSegments = function(media) { | ||
const segments = media.segments || []; | ||
|
||
// a preloadSegment with only preloadHints is not currently | ||
// a usable segment, only include a preloadSegment that has | ||
// parts. | ||
if (media.preloadSegment && media.preloadSegment.parts) { | ||
segments.push(media.preloadSegment); | ||
} | ||
|
||
return segments; | ||
}; | ||
|
||
// consider the playlist unchanged if the playlist object is the same or | ||
// the number of segments is equal, the media sequence number is unchanged, | ||
// and this playlist hasn't become the end of the playlist | ||
|
@@ -147,6 +160,8 @@ export const updateMaster = (master, media, unchangedCheck = isPlaylistUnchanged | |
|
||
const mergedPlaylist = mergeOptions(playlist, media); | ||
|
||
media.segments = getAllSegments(media); | ||
|
||
// if the update could overlap existing segment information, merge the two segment lists | ||
if (playlist.segments) { | ||
mergedPlaylist.segments = updateSegments( | ||
|
@@ -188,16 +203,16 @@ export const updateMaster = (master, media, unchangedCheck = isPlaylistUnchanged | |
*/ | ||
export const refreshDelay = (media, update) => { | ||
const lastSegment = media.segments[media.segments.length - 1]; | ||
let delay; | ||
|
||
if (update && lastSegment && lastSegment.duration) { | ||
delay = lastSegment.duration * 1000; | ||
} else { | ||
// if the playlist is unchanged since the last reload or last segment duration | ||
// cannot be determined, try again after half the target duration | ||
delay = (media.targetDuration || 10) * 500; | ||
const lastPart = lastSegment && lastSegment.parts && lastSegment.parts[lastSegment.parts.length - 1]; | ||
const lastDuration = lastPart && lastPart.duration || lastSegment && lastSegment.duration; | ||
|
||
if (update && lastDuration) { | ||
return lastDuration * 1000; | ||
} | ||
return delay; | ||
|
||
// if the playlist is unchanged since the last reload or last segment duration | ||
// cannot be determined, try again after half the target duration | ||
return (media.partTargetDuration || media.targetDuration || 10) * 500; | ||
}; | ||
|
||
/** | ||
|
@@ -326,7 +341,7 @@ export default class PlaylistLoader extends EventTarget { | |
// merge this playlist into the master | ||
const update = updateMaster(this.master, playlist); | ||
|
||
this.targetDuration = playlist.targetDuration; | ||
this.targetDuration = playlist.partTargetDuration || playlist.targetDuration; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use a playlists targetDuration in a few places outside of these files. In the case where we have partTargetDuration that is always what we will use as out "true" targetDuration, I understand that this is ambiguous though and I think we might want to rename |
||
|
||
if (update) { | ||
this.master = update; | ||
|
@@ -405,7 +420,7 @@ export default class PlaylistLoader extends EventTarget { | |
window.clearTimeout(this.finalRenditionTimeout); | ||
|
||
if (shouldDelay) { | ||
const delay = (playlist.targetDuration / 2) * 1000 || 5 * 1000; | ||
const delay = ((playlist.partTargetDuration || playlist.targetDuration) / 2) * 1000 || 5 * 1000; | ||
|
||
this.finalRenditionTimeout = | ||
window.setTimeout(this.media.bind(this, playlist, false), delay); | ||
|
@@ -538,7 +553,7 @@ export default class PlaylistLoader extends EventTarget { | |
const media = this.media(); | ||
|
||
if (shouldDelay) { | ||
const delay = media ? (media.targetDuration / 2) * 1000 : 5 * 1000; | ||
const delay = media ? ((media.partTargetDuration || media.targetDuration) / 2) * 1000 : 5 * 1000; | ||
|
||
this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay); | ||
return; | ||
|
@@ -660,11 +675,11 @@ export default class PlaylistLoader extends EventTarget { | |
// then resolve URIs in advance, as they are usually done after a playlist request, | ||
// which may not happen if the playlist is resolved. | ||
manifest.playlists.forEach((playlist) => { | ||
if (playlist.segments) { | ||
playlist.segments.forEach((segment) => { | ||
resolveSegmentUris(segment, playlist.resolvedUri); | ||
}); | ||
} | ||
playlist.segments = getAllSegments(playlist); | ||
|
||
playlist.segments.forEach((segment) => { | ||
resolveSegmentUris(segment, playlist.resolvedUri); | ||
}); | ||
}); | ||
this.trigger('loadedplaylist'); | ||
if (!this.request) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,96 @@ import {TIME_FUDGE_FACTOR} from './ranges.js'; | |
|
||
const {createTimeRange} = videojs; | ||
|
||
/** | ||
* A function to get a combined list of parts and segments with durations | ||
* and indexes. | ||
* | ||
* @param {Playlist} playlist the playlist to get the list for. | ||
* | ||
* @return {Array} The part/segment list. | ||
*/ | ||
const getPartsAndSegments = (playlist) => (playlist.segments || []).reduce((acc, segment, si) => { | ||
if (segment.parts) { | ||
segment.parts.forEach(function(part, pi) { | ||
acc.push({duration: part.duration, segmentIndex: si, partIndex: pi}); | ||
|
||
brandonocasey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
} else { | ||
acc.push({duration: segment.duration, segmentIndex: si, partIndex: null}); | ||
} | ||
return acc; | ||
}, []); | ||
|
||
const sumLastThreeDurations = function(partsAndSegments) { | ||
// get the last three part/segment durations | ||
let lastThreeDurations = 0; | ||
|
||
if (partsAndSegments.length >= 3) { | ||
for (let i = partsAndSegments.length - 1; i > partsAndSegments.length - 4; i--) { | ||
// segment missing a duration, we cannot calculate | ||
if (!partsAndSegments[i].duration) { | ||
lastThreeDurations = 0; | ||
break; | ||
} | ||
lastThreeDurations += partsAndSegments[i].duration; | ||
} | ||
} | ||
|
||
return lastThreeDurations; | ||
}; | ||
|
||
/** | ||
* Get the number of seconds to delay from the end of a | ||
* live playlist. | ||
* | ||
* @param {Playlist} master the master playlist | ||
* @param {Playlist} media the media playlist | ||
* @return {number} the hold back in seconds. | ||
*/ | ||
export const liveEdgeDelay = (master, media) => { | ||
if (media.endList) { | ||
return 0; | ||
} | ||
|
||
const partsAndSegments = getPartsAndSegments(media); | ||
const hasParts = partsAndSegments.length && | ||
typeof partsAndSegments[partsAndSegments.length - 1].partIndex === 'number'; | ||
// by default we use the last three durations of segments if | ||
// part target duration or target duration isn't found. | ||
// see: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.3.8 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I may be missing something, but I don't think that section of the spec, if referring to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes we use three segment durations in case target durations are not available for whatever reason. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if it's worth it to add the logic for this here if those are required attributes. It adds logic to the code that we shouldn't really encounter, and if we were to, then it's something that should be resolved by the creator of the stream, since target duration is always required (https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.3.1) and part target duration is required when parts are used (https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.3.7). I think if we're trying to account for missing target durations we should do that at a higher level, when we first parse the manifest. It would help to simplify the code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok I will remove the last three durations logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about just default to 10s target duration in case somehow it isn't set or whatever? While I agree that bad streams are bad, we should still try to play as much as possible and having a default, if bad value, is worth it. Plus, it should make things simpler than trying to figure out segment duration and using that for target duation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think having a sort of default may be appropriate (along with a warning). We should probably add it at the point where we first parse the stream (i.e., in the loaders).
brandonocasey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const lastThreeDurations = sumLastThreeDurations(partsAndSegments); | ||
|
||
// dash suggestedPresentationDelay trumps everything | ||
if (master && master.suggestedPresentationDelay) { | ||
return master.suggestedPresentationDelay; | ||
Comment on lines
+44
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth pulling this up to short circuit the rest of the function. |
||
// look for "part" delays from ll-hls first | ||
} else if (hasParts && media.serverControl && media.serverControl.partHoldBack) { | ||
return media.serverControl.partHoldBack; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same with this, might be easier to read if exiting before going through other logic and calculations. |
||
} else if (hasParts && media.partTargetDuration) { | ||
return media.partTargetDuration * 3; | ||
} else if (hasParts && lastThreeDurations) { | ||
return lastThreeDurations; | ||
|
||
// finally look for full segment delays | ||
} else if (media.serverControl && media.serverControl.holdBack) { | ||
return media.serverControl.holdBack; | ||
} else if (media.targetDuration) { | ||
brandonocasey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// TODO: this should probably be targetDuration * 3 | ||
// but we use this for backwards compatability. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably worth adding to next major to change this. |
||
const lastPartSegment = partsAndSegments[partsAndSegments.length - 1]; | ||
const lastPartDuration = lastPartSegment && lastPartSegment.duration || media.targetDuration; | ||
|
||
return lastPartDuration + media.targetDuration * 2; | ||
// we shouldn't ever end up using lastThreeDurations as targetDuration | ||
// is usually required, but if we somehow get here, with a missing | ||
// targetDuration we should handle it | ||
} else if (lastThreeDurations) { | ||
return lastThreeDurations; | ||
} | ||
|
||
return 0; | ||
}; | ||
|
||
/** | ||
* walk backward until we find a duration we can use | ||
* or return a failure | ||
|
@@ -233,31 +323,29 @@ export const sumDurations = function(playlist, startIndex, endIndex) { | |
* @function safeLiveIndex | ||
*/ | ||
export const safeLiveIndex = function(playlist, liveEdgePadding) { | ||
if (!playlist.segments.length) { | ||
const partsAndSegments = getPartsAndSegments(playlist); | ||
|
||
if (!partsAndSegments.length) { | ||
return 0; | ||
} | ||
|
||
let i = playlist.segments.length; | ||
const lastSegmentDuration = playlist.segments[i - 1].duration || playlist.targetDuration; | ||
const safeDistance = typeof liveEdgePadding === 'number' ? | ||
liveEdgePadding : | ||
lastSegmentDuration + playlist.targetDuration * 2; | ||
|
||
if (safeDistance === 0) { | ||
return i; | ||
if (typeof liveEdgePadding !== 'number') { | ||
liveEdgePadding = liveEdgeDelay(null, playlist); | ||
} | ||
|
||
let i = partsAndSegments.length; | ||
let distanceFromEnd = 0; | ||
|
||
while (i--) { | ||
distanceFromEnd += playlist.segments[i].duration; | ||
distanceFromEnd += partsAndSegments[i].duration; | ||
|
||
if (distanceFromEnd >= safeDistance) { | ||
break; | ||
if (distanceFromEnd >= liveEdgePadding) { | ||
return partsAndSegments[i].segmentIndex; | ||
} | ||
} | ||
|
||
return Math.max(0, i); | ||
// there is nowhere in the playlist that is a safe distance from live. | ||
return 0; | ||
}; | ||
|
||
/** | ||
|
@@ -347,31 +435,34 @@ export const getMediaInfoForTime = function( | |
startIndex, | ||
startTime | ||
) { | ||
let i; | ||
let segment; | ||
const numSegments = playlist.segments.length; | ||
|
||
const partsAndSegments = getPartsAndSegments(playlist); | ||
let time = currentTime - startTime; | ||
|
||
if (time < 0) { | ||
// Walk backward from startIndex in the playlist, adding durations | ||
// until we find a segment that contains `time` and return it | ||
if (startIndex > 0) { | ||
for (i = startIndex - 1; i >= 0; i--) { | ||
segment = playlist.segments[i]; | ||
for (let i = startIndex - 1; i >= 0; i--) { | ||
const segment = partsAndSegments[i]; | ||
|
||
time += (segment.duration + TIME_FUDGE_FACTOR); | ||
|
||
if (time > 0) { | ||
return { | ||
mediaIndex: i, | ||
startTime: startTime - sumDurations(playlist, startIndex, i) | ||
mediaIndex: segment.segmentIndex, | ||
startTime: startTime - sumDurations(playlist, startIndex, segment.segmentIndex), | ||
partIndex: segment.partIndex | ||
}; | ||
} | ||
} | ||
} | ||
|
||
// We were unable to find a good segment within the playlist | ||
// so select the first segment | ||
return { | ||
mediaIndex: 0, | ||
mediaIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, | ||
partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, | ||
startTime: currentTime | ||
}; | ||
} | ||
|
@@ -380,11 +471,11 @@ export const getMediaInfoForTime = function( | |
// adding target durations. If we "run out of time" before getting to | ||
// the first segment, return the first segment | ||
if (startIndex < 0) { | ||
for (i = startIndex; i < 0; i++) { | ||
for (let i = startIndex; i < 0; i++) { | ||
time -= playlist.targetDuration; | ||
if (time < 0) { | ||
return { | ||
mediaIndex: 0, | ||
mediaIndex: partsAndSegments[0].segmentIndex, | ||
startTime: currentTime | ||
}; | ||
} | ||
|
@@ -394,20 +485,24 @@ export const getMediaInfoForTime = function( | |
|
||
// Walk forward from startIndex in the playlist, subtracting durations | ||
// until we find a segment that contains `time` and return it | ||
for (i = startIndex; i < numSegments; i++) { | ||
segment = playlist.segments[i]; | ||
time -= segment.duration + TIME_FUDGE_FACTOR; | ||
for (let i = startIndex; i < partsAndSegments.length; i++) { | ||
const partSegment = partsAndSegments[i]; | ||
|
||
time -= partSegment.duration + TIME_FUDGE_FACTOR; | ||
|
||
if (time < 0) { | ||
return { | ||
mediaIndex: i, | ||
startTime: startTime + sumDurations(playlist, startIndex, i) | ||
mediaIndex: partSegment.segmentIndex, | ||
startTime: startTime + sumDurations(playlist, startIndex, partSegment.segmentIndex), | ||
partIndex: partSegment.partIndex | ||
}; | ||
} | ||
} | ||
|
||
// We are out of possible candidates so load the last one... | ||
return { | ||
mediaIndex: numSegments - 1, | ||
mediaIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex, | ||
partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex, | ||
startTime: currentTime | ||
}; | ||
}; | ||
|
@@ -543,6 +638,7 @@ export const isLowestEnabledRendition = (master, media) => { | |
|
||
// exports | ||
export default { | ||
liveEdgeDelay, | ||
duration, | ||
seekable, | ||
safeLiveIndex, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use partTargetDuration or targetDuration