-
Notifications
You must be signed in to change notification settings - Fork 425
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
fix: more accurate segment choices and logging #1127
Changes from 8 commits
a4a45df
d2d4aab
127e075
6ddcf4c
fa97a95
eae4f41
17dda63
ad088c5
8ebbc50
c6f36e4
2bd3f53
dd29591
a971ff0
504f434
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 | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -31,30 +31,33 @@ import {lastBufferedEnd} from './ranges.js'; | |||||||||
* generate a syncPoint. This function returns a good candidate index | ||||||||||
* for that process. | ||||||||||
* | ||||||||||
* @param {Object} playlist - the playlist object to look for a | ||||||||||
* @param {Array} segments - the segments array from a playlist. | ||||||||||
* @return {number} An index of a segment from the playlist to load | ||||||||||
*/ | ||||||||||
export const getSyncSegmentCandidate = function(currentTimeline, {segments = []} = {}) { | ||||||||||
// if we don't currently have a real timeline yet. | ||||||||||
if (currentTimeline === -1) { | ||||||||||
return 0; | ||||||||||
} | ||||||||||
export const getSyncSegmentCandidate = function(currentTimeline, segments, targetTime) { | ||||||||||
segments = segments || []; | ||||||||||
const timelineSegments = []; | ||||||||||
let time = 0; | ||||||||||
|
||||||||||
for (let i = 0; i < segments.length; i++) { | ||||||||||
const segment = segments[i]; | ||||||||||
|
||||||||||
if (currentTimeline === segment.timeline) { | ||||||||||
timelineSegments.push(i); | ||||||||||
time += segment.duration; | ||||||||||
|
||||||||||
const segmentIndexArray = segments.reduce((acc, s, i) => { | ||||||||||
if (s.timeline === currentTimeline) { | ||||||||||
acc.push(i); | ||||||||||
if (time > targetTime) { | ||||||||||
return i; | ||||||||||
} | ||||||||||
} | ||||||||||
return acc; | ||||||||||
}, []); | ||||||||||
} | ||||||||||
|
||||||||||
if (segmentIndexArray.length) { | ||||||||||
// TODO: why do we do this? Basically we choose index 0 if | ||||||||||
// segmentIndexArray.length is 1 and index = 1 if segmentIndexArray.length | ||||||||||
// is greater then 1 | ||||||||||
return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)]; | ||||||||||
if (timelineSegments.length === 0) { | ||||||||||
return 0; | ||||||||||
} | ||||||||||
|
||||||||||
return Math.max(segments.length - 1, 0); | ||||||||||
// default to the last timeline segment | ||||||||||
return timelineSegments[timelineSegments.length - 1]; | ||||||||||
}; | ||||||||||
|
||||||||||
// In the event of a quota exceeded error, keep at least one second of back buffer. This | ||||||||||
|
@@ -135,11 +138,8 @@ const segmentInfoString = (segmentInfo) => { | |||||||||
const { | ||||||||||
startOfSegment, | ||||||||||
duration, | ||||||||||
segment: { | ||||||||||
start, | ||||||||||
end, | ||||||||||
parts | ||||||||||
}, | ||||||||||
segment, | ||||||||||
part, | ||||||||||
playlist: { | ||||||||||
mediaSequence: seq, | ||||||||||
id, | ||||||||||
|
@@ -159,12 +159,18 @@ const segmentInfoString = (segmentInfo) => { | |||||||||
selection = 'getSyncSegmentCandidate (isSyncRequest)'; | ||||||||||
} | ||||||||||
|
||||||||||
const hasPartIndex = typeof partIndex === 'number'; | ||||||||||
const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment'; | ||||||||||
|
||||||||||
return `${name} [${index}/${segmentLen}]` + | ||||||||||
(partIndex ? ` part [${partIndex}/${parts.length - 1}]` : '') + | ||||||||||
` mediaSequenceNumber [${seq}/${seq + segmentLen}]` + | ||||||||||
` start/end [${start} => ${end}]` + | ||||||||||
const partCount = segment.parts ? segment.parts.length : 0; | ||||||||||
const preloadPartCount = segment.preloadHints ? | ||||||||||
segment.preloadHints.filter((h) => h.type === 'PART').length : | ||||||||||
0; | ||||||||||
const zeroBasedPartCount = partCount + preloadPartCount - 1 - (preloadPartCount > 0 ? 1 : 0); | ||||||||||
|
||||||||||
return `${name} [${seq + index}/${seq + segmentLen}]` + | ||||||||||
(hasPartIndex ? ` part [${partIndex}/${zeroBasedPartCount}]` : '') + | ||||||||||
` segment start/end [${segment.start} => ${segment.end}]` + | ||||||||||
(hasPartIndex ? ` part start/end [${part.start} => ${part.end}]` : '') + | ||||||||||
` startOfSegment [${startOfSegment}]` + | ||||||||||
` duration [${duration}]` + | ||||||||||
` timeline [${timeline}]` + | ||||||||||
|
@@ -1382,7 +1388,7 @@ export default class SegmentLoader extends videojs.EventTarget { | |||||||||
}; | ||||||||||
|
||||||||||
if (next.isSyncRequest) { | ||||||||||
next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, this.playlist_); | ||||||||||
next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, segments, bufferedEnd); | ||||||||||
} else if (this.mediaIndex !== null) { | ||||||||||
const segment = segments[this.mediaIndex]; | ||||||||||
const partIndex = typeof this.partIndex === 'number' ? this.partIndex : -1; | ||||||||||
|
@@ -2031,6 +2037,27 @@ export default class SegmentLoader extends videojs.EventTarget { | |||||||||
// as we use the start of the segment to offset the best guess (playlist provided) | ||||||||||
// timestamp offset. | ||||||||||
this.updateSourceBufferTimestampOffset_(segmentInfo); | ||||||||||
|
||||||||||
// throw away the isSyncRequest segment if | ||||||||||
// it wouldn't have been the next segment we request. | ||||||||||
if (segmentInfo.isSyncRequest) { | ||||||||||
this.syncController_.saveSegmentTimingInfo({ | ||||||||||
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 may have to be careful here, since we're losing some of our timing logic, which may lead to inaccurate next requests: http-streaming/src/segment-loader.js Lines 2860 to 2863 in 1c7a63b
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. Should we call this function within this block? From a brief glance it seems like it only estimates the segment end point using start/duration unless we have an 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 we should probably call it. On that note we may also want to consider adding a property to the segment whether the last time we requested that segment it was a sync request (and clear it if it's requested on a non sync request). It may help us for debugging, particularly now that it's no longer being appended. 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. called |
||||||||||
segmentInfo, | ||||||||||
shouldSaveTimelineMapping: this.loaderType_ === 'main' | ||||||||||
}); | ||||||||||
|
||||||||||
const next = this.chooseNextRequest_(); | ||||||||||
|
||||||||||
// If the sync request isn't the segment that would be requested next | ||||||||||
// after taking into account its timing info, do not append it. | ||||||||||
if (next.mediaIndex !== segmentInfo.mediaIndex || next.partIndex !== segmentInfo.partIndex) { | ||||||||||
this.logger_('sync segment was incorrect, not appending'); | ||||||||||
return; | ||||||||||
} | ||||||||||
this.logger_('sync segment was correct, appending'); | ||||||||||
|
||||||||||
} | ||||||||||
|
||||||||||
// Save some state so that in the future anything waiting on first append (and/or | ||||||||||
// timestamp offset(s)) can process immediately. While the extra state isn't optimal, | ||||||||||
// we need some notion of whether the timestamp offset or other relevant information | ||||||||||
|
@@ -2885,8 +2912,6 @@ export default class SegmentLoader extends videojs.EventTarget { | |||||||||
}); | ||||||||||
} | ||||||||||
|
||||||||||
this.logger_(`Appended ${segmentInfoString(segmentInfo)}`); | ||||||||||
|
||||||||||
const segmentDurationMessage = | ||||||||||
getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_); | ||||||||||
|
||||||||||
|
@@ -2904,9 +2929,18 @@ export default class SegmentLoader extends videojs.EventTarget { | |||||||||
|
||||||||||
if (segmentInfo.isSyncRequest) { | ||||||||||
this.trigger('syncinfoupdate'); | ||||||||||
return; | ||||||||||
// if the sync request was not appended | ||||||||||
// then it was not the correct segment. | ||||||||||
// throw it away and use the data it gave us | ||||||||||
// to get the correct one. | ||||||||||
if (!segmentInfo.hasAppendedData_) { | ||||||||||
this.logger_(`Throwing away un-appended sync request ${segmentInfoString(segmentInfo)}`); | ||||||||||
return; | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
this.logger_(`Appended ${segmentInfoString(segmentInfo)}`); | ||||||||||
|
||||||||||
this.addSegmentMetadataCue_(segmentInfo); | ||||||||||
this.fetchAtBuffer_ = true; | ||||||||||
if (this.currentTimeline_ !== segmentInfo.timeline) { | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -870,7 +870,7 @@ export const LoaderCommonFactory = ({ | |
}); | ||
|
||
QUnit.test('drops partIndex if playlist update drops parts', function(assert) { | ||
assert.timeout(100000000000000000000); | ||
loader.duration_ = () => Infinity; | ||
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. ie 11 has the duration as NaN here, so sync-controller gets a sync point of |
||
return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => { | ||
loader.playlist(playlistWithDuration(50, { | ||
mediaSequence: 0, | ||
|
@@ -899,8 +899,8 @@ export const LoaderCommonFactory = ({ | |
|
||
assert.equal( | ||
this.requests[0].url, | ||
'1.ts', | ||
'requested mediaIndex 1 only' | ||
'0.ts', | ||
'requested mediaIndex 0 only' | ||
); | ||
}); | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,51 +79,38 @@ SegmentLoader.prototype.addSegmentMetadataCue_ = function() {}; | |
QUnit.module('SegmentLoader Isolated Functions'); | ||
|
||
QUnit.test('getSyncSegmentCandidate works as expected', function(assert) { | ||
assert.equal(getSyncSegmentCandidate(-1, {}), 0, '-1 timeline, try index 0'); | ||
assert.equal(getSyncSegmentCandidate(0, {}), 0, '0 timeline, no segments, try index 0'); | ||
|
||
assert.equal( | ||
getSyncSegmentCandidate(0, {segments: [ | ||
{timeline: 0}, | ||
{timeline: 0} | ||
]}), | ||
1, | ||
'0 timeline, two timeline 0 segments, try index 1' | ||
); | ||
|
||
assert.equal( | ||
getSyncSegmentCandidate(0, {segments: [ | ||
{timeline: 0}, | ||
{timeline: 0}, | ||
{timeline: 0}, | ||
{timeline: 0} | ||
]}), | ||
1, | ||
'0 timeline, four timeline 0 segments, try index 1' | ||
); | ||
|
||
assert.equal( | ||
getSyncSegmentCandidate(0, {segments: [ | ||
{timeline: 0}, | ||
{timeline: 1}, | ||
{timeline: 1}, | ||
{timeline: 1} | ||
]}), | ||
0, | ||
'0 timeline, one timeline 0 segment, three timeline 1 segments, try index 0' | ||
); | ||
|
||
assert.equal( | ||
getSyncSegmentCandidate(0, {segments: [ | ||
{timeline: 1}, | ||
{timeline: 1}, | ||
{timeline: 1}, | ||
{timeline: 1} | ||
]}), | ||
3, | ||
'0 timeline, four timeline 1 segments, try index 3' | ||
); | ||
|
||
let segments = []; | ||
|
||
assert.equal(getSyncSegmentCandidate(-1, segments, 0), 0, '-1 timeline, no segments, 0 target'); | ||
assert.equal(getSyncSegmentCandidate(0, segments, 0), 0, '0 timeline, no segments, 0 target'); | ||
|
||
segments = [ | ||
{timeline: 0, duration: 4}, | ||
{timeline: 0, duration: 4}, | ||
{timeline: 0, duration: 4}, | ||
{timeline: 0, duration: 4} | ||
]; | ||
|
||
assert.equal(getSyncSegmentCandidate(-1, segments, 0), 0, '-1 timeline, 4x 0 segments, 0 target'); | ||
assert.equal(getSyncSegmentCandidate(0, segments, 1), 0, '0 timeline, 4x 0 segments, 1 target'); | ||
assert.equal(getSyncSegmentCandidate(0, segments, 4), 1, '0 timeline, 4x 0 segments, 4 target'); | ||
assert.equal(getSyncSegmentCandidate(-1, segments, 8), 0, '-1 timeline, 4x 0 segments, 8 target'); | ||
assert.equal(getSyncSegmentCandidate(0, segments, 8), 2, '0 timeline, 4x 0 segments, 8 target'); | ||
assert.equal(getSyncSegmentCandidate(0, segments, 20), 3, '0 timeline, 4x 0 segments, 20 target'); | ||
|
||
segments = [ | ||
{timeline: 1, duration: 4}, | ||
{timeline: 0, duration: 4}, | ||
{timeline: 1, duration: 4}, | ||
{timeline: 0, duration: 4}, | ||
{timeline: 2, duration: 4}, | ||
{timeline: 1, duration: 4}, | ||
{timeline: 0, duration: 4} | ||
]; | ||
|
||
assert.equal(getSyncSegmentCandidate(1, segments, 8), 5, '1 timeline, mixed timeline segments, 8 target'); | ||
assert.equal(getSyncSegmentCandidate(0, segments, 8), 6, '0 timeline, mixed timeline segments, 8 target'); | ||
assert.equal(getSyncSegmentCandidate(2, segments, 8), 4, '2 timeline, mixed timeline segments, 8 target'); | ||
}); | ||
|
||
QUnit.test('illegalMediaSwitch detects illegal media switches', function(assert) { | ||
|
@@ -2967,6 +2954,69 @@ QUnit.module('SegmentLoader', function(hooks) { | |
}); | ||
}); | ||
|
||
QUnit.test('sync request can be thrown away', function(assert) { | ||
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. issue: looks like this test is failing under IE11. https://github.com/videojs/http-streaming/pull/1127/checks?check_run_id=2659611689#step:13:478 |
||
const appends = []; | ||
const logs = []; | ||
|
||
return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_, {isVideoOnly: true}).then(() => { | ||
const origAppendToSourceBuffer = loader.appendToSourceBuffer_.bind(loader); | ||
|
||
// set the mediaSource duration as it is usually set by | ||
// master playlist controller, which is not present here | ||
loader.mediaSource_.duration = Infinity; | ||
|
||
loader.appendToSourceBuffer_ = (config) => { | ||
appends.push(config); | ||
origAppendToSourceBuffer(config); | ||
}; | ||
|
||
loader.playlist(playlistWithDuration(20)); | ||
loader.load(); | ||
this.clock.tick(1); | ||
standardXHRResponse(this.requests.shift(), videoSegment()); | ||
|
||
return new Promise((resolve, reject) => { | ||
loader.one('appended', resolve); | ||
loader.one('error', reject); | ||
}); | ||
}).then(() => { | ||
this.clock.tick(1); | ||
|
||
assert.equal(appends.length, 1, 'one append'); | ||
assert.equal(appends[0].type, 'video', 'appended to video buffer'); | ||
assert.ok(appends[0].initSegment, 'appended video init segment'); | ||
|
||
loader.playlist(playlistWithDuration(20, { uri: 'new-playlist.m3u8' })); | ||
// remove old aborted request | ||
this.requests.shift(); | ||
// get the new request | ||
this.clock.tick(1); | ||
loader.chooseNextRequest_ = () => ({partIndex: null, mediaIndex: 1}); | ||
loader.logger_ = (line) => { | ||
logs.push(line); | ||
}; | ||
standardXHRResponse(this.requests.shift(), videoSegment()); | ||
|
||
// since it's a sync request, wait for the syncinfoupdate event (we won't get the | ||
// appended event) | ||
return new Promise((resolve, reject) => { | ||
loader.one('syncinfoupdate', resolve); | ||
loader.one('error', reject); | ||
}); | ||
}).then(() => { | ||
this.clock.tick(1); | ||
assert.equal(appends.length, 1, 'still only one append'); | ||
assert.true( | ||
logs.some((l) => (/^sync segment was incorrect, not appending/).test(l)), | ||
'has log line' | ||
); | ||
assert.true( | ||
logs.some((l) => (/^Throwing away un-appended sync request segment/).test(l)), | ||
'has log line' | ||
); | ||
}); | ||
}); | ||
|
||
QUnit.test('re-appends init segments on discontinuity', function(assert) { | ||
const appends = []; | ||
|
||
|
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.
Should we keep
TIME_FUDGE_FACTOR
at all?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.
question: What is
TIME_FUDGE_FACTOR
used for here? Is using it causing timing issues down the line?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.
I think this may be the wrong place for us to use time fudging. We should probably only ever fudge time in one place: when we are trying to determine the next segment to request and are not simply walking the playlist (mediaIndex++). In that case, all requests to get media info/segment info should be as precise as possible given the current segment information, then, once the final segment is determined, we should decide whether we want to fudge the time at all to conservatively request a segment (i.e., whether to request back 1 segment, or, as an alternative, get media info for time with a fudge factor included).
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.
I think this change basically encompasses what you are saying. We only use
getMediaIndexForTime
when we are not incrementing mediaIndex/partIndex. This change makes it so that cumulatively we can only be off byTIME_FUDGE_FACTOR
rather than every single part/segments duration being incremented or decremented byTIME_FUDGE_FACTOR
. For the sake of time I think that we might want to add a TODO here to see if we should even use TIME_FUDGE_FACTOR at all. I lean towards not using it, but it will require a bit more testing and could easily enough be an isolated change.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.
👍 a TODO would be good for now. This definitely is better than before.
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.
Probably worth making an issue/a note in our major/refactoring epic
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.
added