Browse Source

Fixed mediaIndex tracking so that it is consistent when the playlist updates during a live stream (#977)

* Fixed mediaIndex tracking so that it is consistent when the playlist updates during a live stream
* Removed any code in SegmentLoader#handleUpdateEnd_ that changed the mediaIndex
* Reordered SegmentLoader#playlist to make it easier to follow
* All changes to both mediaIndexes (SegmentLoader's and segmentInfo's) now happen in SegmentLoader#playlist
* Added tests for proper mediaIndex tracking with live playlists
pull/6/head
Jon-Carlos Rivera 9 years ago
committed by Matthew Neil
parent
commit
5113058c9e
  1. 88
      src/segment-loader.js
  2. 99
      test/segment-loader.test.js
  3. 2
      test/test-helpers.js

88
src/segment-loader.js

@ -270,32 +270,13 @@ export default class SegmentLoader extends videojs.EventTarget {
let oldPlaylist = this.playlist_;
let segmentInfo = this.pendingSegment_;
if (this.mediaIndex !== null) {
// We reloaded the same playlist so we are in a live scenario
// and we will likely need to adjust the mediaIndex
if (oldPlaylist &&
oldPlaylist.uri === newPlaylist.uri) {
let mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
this.mediaIndex -= mediaSequenceDiff;
if (segmentInfo) {
segmentInfo.mediaIndex -= mediaSequenceDiff;
// we need to update the referenced segment so that timing information is
// saved for the new playlist's segment, however, if the segment fell off the
// playlist, we can leave the old reference and just lose the timing info
if (segmentInfo.mediaIndex >= 0) {
segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
}
}
this.playlist_ = newPlaylist;
this.xhrOptions_ = options;
this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
} else {
// We must "resync" the fetcher when we switch renditions
this.resyncLoader();
}
} else if (!this.hasPlayed_()) {
// when we haven't started playing yet, the start of a live playlist
// is always our zero-time so force a sync update each time the playlist
// is refreshed from the server
if (!this.hasPlayed_()) {
newPlaylist.syncInfo = {
mediaSequence: newPlaylist.mediaSequence,
time: 0
@ -303,14 +284,51 @@ export default class SegmentLoader extends videojs.EventTarget {
this.trigger('syncinfoupdate');
}
this.playlist_ = newPlaylist;
this.xhrOptions_ = options;
// if we were unpaused but waiting for a playlist, start
// buffering now
if (this.mimeType_ && this.state === 'INIT' && !this.paused()) {
return this.init_();
}
if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
if (this.mediaIndex !== null) {
// we must "resync" the segment loader when we switch renditions and
// the segment loader is already synced to the previous rendition
this.resyncLoader();
}
// the rest of this function depends on `oldPlaylist` being defined
return;
}
// we reloaded the same playlist so we are in a live scenario
// and we will likely need to adjust the mediaIndex
let mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
log('mediaSequenceDiff', mediaSequenceDiff);
// update the mediaIndex on the SegmentLoader
// this is important because we can abort a request and this value must be
// equal to the last appended mediaIndex
if (this.mediaIndex !== null) {
this.mediaIndex -= mediaSequenceDiff;
}
// update the mediaIndex on the SegmentInfo object
// this is important because we will update this.mediaIndex with this value
// in `handleUpdateEnd_` after the segment has been successfully appended
if (segmentInfo) {
segmentInfo.mediaIndex -= mediaSequenceDiff;
// we need to update the referenced segment so that timing information is
// saved for the new playlist's segment, however, if the segment fell off the
// playlist, we can leave the old reference and just lose the timing info
if (segmentInfo.mediaIndex >= 0) {
segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
}
}
this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
}
/**
@ -1038,23 +1056,17 @@ export default class SegmentLoader extends videojs.EventTarget {
this.pendingSegment_ = null;
this.recordThroughput_(segmentInfo);
log('handleUpdateEnd_');
let currentMediaIndex = segmentInfo.mediaIndex;
currentMediaIndex +=
segmentInfo.playlist.mediaSequence - this.playlist_.mediaSequence;
this.mediaIndex = currentMediaIndex;
this.mediaIndex = segmentInfo.mediaIndex;
this.fetchAtBuffer_ = true;
log('handleUpdateEnd_', this.mediaIndex);
// any time an update finishes and the last segment is in the
// buffer, end the stream. this ensures the "ended" event will
// fire if playback reaches that point.
let isEndOfStream = detectEndOfStream(segmentInfo.playlist,
this.mediaSource_,
currentMediaIndex + 1);
this.mediaIndex + 1);
if (isEndOfStream) {
this.mediaSource_.endOfStream();

99
test/segment-loader.test.js

@ -631,6 +631,105 @@ QUnit.test('abort does not cancel segment processing in progress', function(asse
assert.equal(loader.mediaRequests, 1, '1 request');
});
QUnit.test('SegmentLoader.mediaIndex is adjusted when live playlist is updated', function(assert) {
loader.playlist(playlistWithDuration(50, {
mediaSequence: 0,
endList: false
}));
loader.mimeType(this.mimeType);
loader.load();
// Start at mediaIndex 2 which means that the next segment we request
// should mediaIndex 3
loader.mediaIndex = 2;
this.clock.tick(1);
assert.equal(loader.mediaIndex, 2, 'SegmentLoader.mediaIndex starts at 2');
assert.equal(this.requests[0].url, '3.ts', 'requesting the segment at mediaIndex 3');
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
this.clock.tick(1);
mediaSource.sourceBuffers[0].trigger('updateend');
assert.equal(loader.mediaIndex, 3, 'mediaIndex ends at 3');
this.clock.tick(1);
assert.equal(loader.mediaIndex, 3, 'SegmentLoader.mediaIndex starts at 3');
assert.equal(this.requests[0].url, '4.ts', 'requesting the segment at mediaIndex 4');
// Update the playlist shifting the mediaSequence by 2 which will result
// in a decrement of the mediaIndex by 2 to 1
loader.playlist(playlistWithDuration(50, {
mediaSequence: 2,
endList: false
}));
assert.equal(loader.mediaIndex, 1, 'SegmentLoader.mediaIndex is updated to 1');
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
this.clock.tick(1);
mediaSource.sourceBuffers[0].trigger('updateend');
assert.equal(loader.mediaIndex, 2, 'SegmentLoader.mediaIndex ends at 2');
});
QUnit.test('segmentInfo.mediaIndex is adjusted when live playlist is updated', function(assert) {
// Setting currentTime to 31 so that we start requesting at segment #3
currentTime = 31;
loader.playlist(playlistWithDuration(50, {
mediaSequence: 0,
endList: false
}));
loader.mimeType(this.mimeType);
loader.load();
// Start at mediaIndex null which means that the next segment we request
// should be based on currentTime (mediaIndex 3)
loader.mediaIndex = null;
loader.syncPoint_ = {
segmentIndex: 0,
time: 0
};
this.clock.tick(1);
let segmentInfo = loader.pendingSegment_;
assert.equal(segmentInfo.mediaIndex, 3, 'segmentInfo.mediaIndex starts at 3');
assert.equal(this.requests[0].url, '3.ts', 'requesting the segment at mediaIndex 3');
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
this.clock.tick(1);
mediaSource.sourceBuffers[0].trigger('updateend');
assert.equal(loader.mediaIndex, 3, 'SegmentLoader.mediaIndex ends at 3');
loader.mediaIndex = null;
loader.fetchAtBuffer_ = false;
this.clock.tick(1);
segmentInfo = loader.pendingSegment_;
assert.equal(segmentInfo.mediaIndex, 3, 'segmentInfo.mediaIndex starts at 3');
assert.equal(this.requests[0].url, '3.ts', 'requesting the segment at mediaIndex 3');
// Update the playlist shifting the mediaSequence by 2 which will result
// in a decrement of the mediaIndex by 2 to 1
loader.playlist(playlistWithDuration(50, {
mediaSequence: 2,
endList: false
}));
assert.equal(segmentInfo.mediaIndex, 1, 'segmentInfo.mediaIndex is updated to 1');
this.requests[0].response = new Uint8Array(10).buffer;
this.requests.shift().respond(200, null, '');
this.clock.tick(1);
mediaSource.sourceBuffers[0].trigger('updateend');
assert.equal(loader.mediaIndex, 1, 'SegmentLoader.mediaIndex ends at 1');
});
QUnit.test('sets the timestampOffset on timeline change', function(assert) {
let playlist = playlistWithDuration(40);

2
test/test-helpers.js

@ -313,7 +313,7 @@ export const playlistWithDuration = function(time, conf) {
mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0,
discontinuityStarts: [],
segments: [],
endList: true
endList: conf && typeof conf.endList !== 'undefined' ? !!conf.endList : true
};
let count = Math.floor(time / 10);
let remainder = time % 10;

Loading…
Cancel
Save