-
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 ll-hls query directives and support skipping segments #1079
Changes from 12 commits
3d39612
5e46a1e
df957d4
ff40332
53ec980
addf841
285a4ae
a46f0a7
9d1fc06
00a8e25
1ff48b0
3ff694a
d3fb477
2854967
a842b84
7898b4f
3ff79bc
8ca1629
941881f
9288f11
adb47d2
9744999
1d2b8a2
ee09077
a1d3176
c555607
44b8830
2aa89ab
bba29c4
e8cd59d
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 |
---|---|---|
|
@@ -49,6 +49,12 @@ export const updateSegment = (a, b) => { | |
} | ||
} | ||
|
||
// set skipped to false for segments that have | ||
// had information merged into them. | ||
if (!a.skipped && b.skipped) { | ||
result.skipped = false; | ||
} | ||
|
||
return result; | ||
}; | ||
|
||
|
@@ -69,15 +75,30 @@ export const updateSegment = (a, b) => { | |
*/ | ||
export const updateSegments = (original, update, offset) => { | ||
const oldSegments = original.slice(); | ||
const result = update.slice(); | ||
const newSegments = update.slice(); | ||
|
||
offset = offset || 0; | ||
const length = Math.min(original.length, update.length + offset); | ||
const result = []; | ||
|
||
let currentMap; | ||
|
||
for (let i = offset; i < length; i++) { | ||
const newIndex = i - offset; | ||
for (let newIndex = 0; newIndex < newSegments.length; newIndex++) { | ||
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. This function was completely changed. Previously we used to create a new array from Now create an empty array for the result. Loop over every new segment and merge the accompanying old segment if we have one. Otherwise we just push the new segment into the list. |
||
const oldSegment = oldSegments[newIndex + offset]; | ||
const newSegment = newSegments[newIndex]; | ||
|
||
result[newIndex] = updateSegment(oldSegments[i], result[newIndex]); | ||
if (oldSegment) { | ||
currentMap = oldSegment.map || currentMap; | ||
|
||
result.push(updateSegment(oldSegment, newSegment)); | ||
} else { | ||
// carry over map to new segment if it is missing | ||
if (currentMap && !newSegment.map) { | ||
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. The big push for the refactor of this function was: using |
||
newSegment.map = currentMap; | ||
} | ||
|
||
result.push(newSegment); | ||
|
||
} | ||
} | ||
return result; | ||
}; | ||
|
@@ -115,12 +136,22 @@ export const resolveSegmentUris = (segment, baseUri) => { | |
|
||
const getAllSegments = function(media) { | ||
const segments = media.segments || []; | ||
const preloadSegment = media.preloadSegment; | ||
|
||
if (preloadSegment && preloadSegment.parts && preloadSegment.parts.length) { | ||
// if preloadHints has a MAP that means that the | ||
// init segment is going to change. We cannot use any of the parts | ||
if (preloadSegment.preloadHints) { | ||
for (let i = 0; i < preloadSegment.preloadHints.length; i++) { | ||
if (preloadSegment.preloadHints[i].type === 'MAP') { | ||
return segments; | ||
gkatsev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
// set the duration for our preload segment to target duration. | ||
preloadSegment.duration = media.targetDuration; | ||
gesinger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// 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); | ||
segments.push(preloadSegment); | ||
} | ||
|
||
return segments; | ||
|
@@ -146,28 +177,36 @@ export const isPlaylistUnchanged = (a, b) => a === b || | |
* master playlist with the updated media playlist merged in, or | ||
* null if the merge produced no change. | ||
*/ | ||
export const updateMaster = (master, media, unchangedCheck = isPlaylistUnchanged) => { | ||
export const updateMaster = (master, newMedia, unchangedCheck = isPlaylistUnchanged) => { | ||
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 continuously got confused by 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. thought (non-blocking): yeah, I'm sure have a lot of places where the terminology is confusing. We probably want to write up some guidelines around naming, particularly around words like |
||
const result = mergeOptions(master, {}); | ||
const playlist = result.playlists[media.id]; | ||
const oldMedia = result.playlists[newMedia.id]; | ||
|
||
if (!playlist) { | ||
if (!oldMedia) { | ||
return null; | ||
} | ||
|
||
if (unchangedCheck(playlist, media)) { | ||
if (unchangedCheck(oldMedia, newMedia)) { | ||
return null; | ||
} | ||
|
||
const mergedPlaylist = mergeOptions(playlist, media); | ||
newMedia.segments = getAllSegments(newMedia); | ||
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. preserve |
||
|
||
media.segments = getAllSegments(media); | ||
const mergedPlaylist = mergeOptions(oldMedia, newMedia); | ||
|
||
// if the update could overlap existing segment information, merge the two segment lists | ||
if (playlist.segments) { | ||
if (oldMedia.segments) { | ||
if (newMedia.skip) { | ||
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. if we have skipped segments, add them back in so that we can merge information from the old media segments into them. |
||
newMedia.segments = newMedia.segments || []; | ||
// add back in objects for skipped segments, so that we merge | ||
// old properties into this new segment | ||
brandonocasey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (let i = 0; i < newMedia.skip.skippedSegments; i++) { | ||
newMedia.segments.unshift({skipped: true}); | ||
gesinger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
mergedPlaylist.segments = updateSegments( | ||
playlist.segments, | ||
media.segments, | ||
media.mediaSequence - playlist.mediaSequence | ||
oldMedia.segments, | ||
newMedia.segments, | ||
newMedia.mediaSequence - oldMedia.mediaSequence | ||
); | ||
} | ||
|
||
|
@@ -180,13 +219,13 @@ export const updateMaster = (master, media, unchangedCheck = isPlaylistUnchanged | |
// that is referenced by index, and one by URI. The index reference may no longer be | ||
// necessary. | ||
for (let i = 0; i < result.playlists.length; i++) { | ||
if (result.playlists[i].id === media.id) { | ||
if (result.playlists[i].id === newMedia.id) { | ||
result.playlists[i] = mergedPlaylist; | ||
} | ||
} | ||
result.playlists[media.id] = mergedPlaylist; | ||
result.playlists[newMedia.id] = mergedPlaylist; | ||
// URI reference added for backwards compatibility | ||
result.playlists[media.uri] = mergedPlaylist; | ||
result.playlists[newMedia.uri] = mergedPlaylist; | ||
|
||
return result; | ||
}; | ||
|
@@ -255,11 +294,48 @@ export default class PlaylistLoader extends EventTarget { | |
// only refresh the media playlist if no other activity is going on | ||
return; | ||
} | ||
const media = this.media(); | ||
|
||
let uri = resolveUrl(this.master.uri, media.uri); | ||
|
||
if (this.experimentalLLHLS) { | ||
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. This is all new, basically use query directives and server side blocking if available. 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. question: do we want to skip these two server-control checks if we have an end list tag? The spec says that the server MUST ignore these, so, we may as well not do the calculations in those cases, if we don't already. 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. sure I will make this an |
||
const query = []; | ||
|
||
if (media.serverControl && media.serverControl.canSkipUntil) { | ||
query.push('_HLS_skip=' + (media.serverControl.canSkipDateranges ? 'v2' : 'YES')); | ||
} | ||
|
||
if (media.serverControl && media.serverControl.canBlockReload) { | ||
const preloadSegment = media.preloadSegment; | ||
// next msn will be for preload segment, if we have a preload segment. | ||
let nextMSN = media.mediaSequence + media.segments.length; | ||
let nextPart; | ||
|
||
if (preloadSegment && preloadSegment.parts && preloadSegment.parts.length) { | ||
nextPart = preloadSegment.parts.length + 1; | ||
nextMSN--; | ||
} else { | ||
nextMSN++; | ||
} | ||
|
||
query.push(`_HLS_msn=${nextMSN}`); | ||
|
||
if (nextPart) { | ||
query.push(`_HLS_part=${nextPart}`); | ||
} | ||
} | ||
|
||
query.forEach(function(str, i) { | ||
const symbol = i === 0 ? '?' : '&'; | ||
|
||
uri += `${symbol}${str}`; | ||
}); | ||
|
||
} | ||
this.state = 'HAVE_CURRENT_METADATA'; | ||
|
||
this.request = this.vhs_.xhr({ | ||
uri: resolveUrl(this.master.uri, this.media().uri), | ||
uri, | ||
withCredentials: this.withCredentials | ||
}, (error, req) => { | ||
// disposed | ||
|
@@ -304,6 +380,17 @@ export default class PlaylistLoader extends EventTarget { | |
this.trigger('error'); | ||
} | ||
|
||
parseManifest_({url, manifestString}) { | ||
return parseManifest({ | ||
onwarn: ({message}) => this.logger_(`m3u8-parser warn for ${url}: ${message}`), | ||
oninfo: ({message}) => this.logger_(`m3u8-parser info for ${url}: ${message}`), | ||
manifestString, | ||
customTagParsers: this.customTagParsers, | ||
customTagMappers: this.customTagMappers, | ||
experimentalLLHLS: this.experimentalLLHLS | ||
}); | ||
} | ||
|
||
/** | ||
* Update the playlist loader's state in response to a new or updated playlist. | ||
* | ||
|
@@ -321,13 +408,9 @@ export default class PlaylistLoader extends EventTarget { | |
this.request = null; | ||
this.state = 'HAVE_METADATA'; | ||
|
||
const playlist = playlistObject || parseManifest({ | ||
onwarn: ({message}) => this.logger_(`m3u8-parser warn for ${id}: ${message}`), | ||
oninfo: ({message}) => this.logger_(`m3u8-parser info for ${id}: ${message}`), | ||
manifestString: playlistString, | ||
customTagParsers: this.customTagParsers, | ||
customTagMappers: this.customTagMappers, | ||
experimentalLLHLS: this.experimentalLLHLS | ||
const playlist = playlistObject || this.parseManifest_({ | ||
url, | ||
manifestString: playlistString | ||
}); | ||
|
||
playlist.lastRequest = Date.now(); | ||
|
@@ -632,11 +715,9 @@ export default class PlaylistLoader extends EventTarget { | |
|
||
this.src = resolveManifestRedirect(this.handleManifestRedirects, this.src, req); | ||
|
||
const manifest = parseManifest({ | ||
const manifest = this.parseManifest_({ | ||
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. move parseManifest to |
||
manifestString: req.responseText, | ||
customTagParsers: this.customTagParsers, | ||
customTagMappers: this.customTagMappers, | ||
experimentalLLHLS: this.experimentalLLHLS | ||
url: this.src | ||
}); | ||
|
||
this.setupInitialPlaylist(manifest); | ||
|
@@ -677,7 +758,7 @@ export default class PlaylistLoader extends EventTarget { | |
manifest.playlists.forEach((playlist) => { | ||
playlist.segments = getAllSegments(playlist); | ||
|
||
playlist.segments.forEach((segment) => { | ||
playlist.segments.forEach(function(segment) { | ||
resolveSegmentUris(segment, playlist.resolvedUri); | ||
}); | ||
}); | ||
|
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.
set a default part target duration for manifests with parts that do not have one.