a63f0154 by David LaPalomento

Merge pull request #397 from dmlap/live-variant-syncing

Live variant syncing
2 parents 83b4302b 27758be3
1 /** 1 /**
2 * playlist-loader
3 *
2 * A state machine that manages the loading, caching, and updating of 4 * A state machine that manages the loading, caching, and updating of
3 * M3U8 playlists. When tracking a live playlist, loaders will keep 5 * M3U8 playlists. When tracking a live playlist, loaders will keep
4 * track of the duration of content that expired since the loader was 6 * track of the duration of content that expired since the loader was
5 * initialized and when the current discontinuity sequence was 7 * initialized and when the current discontinuity sequence was
6 * encountered. A complete media timeline for a live playlist with 8 * encountered. A complete media timeline for a live playlist with
7 * expiring segments and discontinuities looks like this: 9 * expiring segments looks like this:
8 * 10 *
9 * |-- expiredPreDiscontinuity --|-- expiredPostDiscontinuity --|-- segments --| 11 * |-- expired --|-- segments --|
10 * 12 *
11 * You can use these values to calculate how much time has elapsed
12 * since the stream began loading or how long it has been since the
13 * most recent discontinuity was encountered, for instance.
14 */ 13 */
15 (function(window, videojs) { 14 (function(window, videojs) {
16 'use strict'; 15 'use strict';
...@@ -159,20 +158,13 @@ ...@@ -159,20 +158,13 @@
159 // initialize the loader state 158 // initialize the loader state
160 loader.state = 'HAVE_NOTHING'; 159 loader.state = 'HAVE_NOTHING';
161 160
162 // the total duration of all segments that expired and have been 161 // The total duration of all segments that expired and have been
163 // removed from the current playlist after the last 162 // removed from the current playlist, in seconds. This property
164 // #EXT-X-DISCONTINUITY. In a live playlist without 163 // should always be zero for non-live playlists. In a live
165 // discontinuities, this is the total amount of time that has 164 // playlist, this is the total amount of time that has been
166 // been removed from the stream since the playlist loader began 165 // removed from the stream since the playlist loader began
167 // tracking it. 166 // tracking it.
168 loader.expiredPostDiscontinuity_ = 0; 167 loader.expired_ = 0;
169
170 // the total duration of all segments that expired and have been
171 // removed from the current playlist before the last
172 // #EXT-X-DISCONTINUITY. The total amount of time that has
173 // expired is always the sum of expiredPreDiscontinuity_ and
174 // expiredPostDiscontinuity_.
175 loader.expiredPreDiscontinuity_ = 0;
176 168
177 // capture the prototype dispose function 169 // capture the prototype dispose function
178 dispose = this.dispose; 170 dispose = this.dispose;
...@@ -364,35 +356,14 @@ ...@@ -364,35 +356,14 @@
364 * @param update {object} the updated media playlist object 356 * @param update {object} the updated media playlist object
365 */ 357 */
366 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { 358 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
367 var lastDiscontinuity, expiredCount, i; 359 var expiredCount;
368 360
369 if (this.media_) { 361 if (this.media_) {
370 expiredCount = update.mediaSequence - this.media_.mediaSequence; 362 expiredCount = update.mediaSequence - this.media_.mediaSequence;
371 363
372 // setup the index for duration calculations so that the newly 364 // update the expired time count
373 // expired time will be accumulated after the last 365 this.expired_ += Playlist.duration(this.media_,
374 // discontinuity, unless we discover otherwise
375 lastDiscontinuity = this.media_.mediaSequence;
376
377 if (this.media_.discontinuitySequence !== update.discontinuitySequence) {
378 i = expiredCount;
379 while (i--) {
380 if (this.media_.segments[i].discontinuity) {
381 // a segment that begins a new discontinuity sequence has expired
382 lastDiscontinuity = i + this.media_.mediaSequence;
383 this.expiredPreDiscontinuity_ += this.expiredPostDiscontinuity_;
384 this.expiredPostDiscontinuity_ = 0;
385 break;
386 }
387 }
388 }
389
390 // update the expirated durations
391 this.expiredPreDiscontinuity_ += Playlist.duration(this.media_,
392 this.media_.mediaSequence, 366 this.media_.mediaSequence,
393 lastDiscontinuity);
394 this.expiredPostDiscontinuity_ += Playlist.duration(this.media_,
395 lastDiscontinuity,
396 update.mediaSequence); 367 update.mediaSequence);
397 } 368 }
398 369
...@@ -400,6 +371,28 @@ ...@@ -400,6 +371,28 @@
400 }; 371 };
401 372
402 /** 373 /**
374 * When switching variant playlists in a live stream, the player may
375 * discover that the new set of available segments is shifted in
376 * time relative to the old playlist. If that is the case, you can
377 * call this method to synchronize the playlist loader so that
378 * subsequent calls to getMediaIndexForTime_() return values
379 * appropriate for the new playlist.
380 *
381 * @param mediaIndex {integer} the index of the segment that will be
382 * the used to base timeline calculations on
383 * @param startTime {number} the media timeline position of the
384 * first moment of video data for the specified segment. That is,
385 * data from the specified segment will first be displayed when
386 * `currentTime` is equal to `startTime`.
387 */
388 PlaylistLoader.prototype.updateTimelineOffset = function(mediaIndex, startingTime) {
389 var segmentOffset = Playlist.duration(this.media_,
390 this.media_.mediaSequence,
391 this.media_.mediaSequence + mediaIndex);
392 this.expired_ = startingTime - segmentOffset;
393 };
394
395 /**
403 * Determine the index of the segment that contains a specified 396 * Determine the index of the segment that contains a specified
404 * playback position in the current media playlist. Early versions 397 * playback position in the current media playlist. Early versions
405 * of the HLS specification require segment durations to be rounded 398 * of the HLS specification require segment durations to be rounded
...@@ -426,7 +419,7 @@ ...@@ -426,7 +419,7 @@
426 419
427 // when the requested position is earlier than the current set of 420 // when the requested position is earlier than the current set of
428 // segments, return the earliest segment index 421 // segments, return the earliest segment index
429 time -= this.expiredPreDiscontinuity_ + this.expiredPostDiscontinuity_; 422 time -= this.expired_;
430 if (time < 0) { 423 if (time < 0) {
431 return 0; 424 return 0;
432 } 425 }
......
...@@ -46,6 +46,8 @@ videojs.Hls = videojs.extend(Component, { ...@@ -46,6 +46,8 @@ videojs.Hls = videojs.extend(Component, {
46 this.tech_ = tech; 46 this.tech_ = tech;
47 this.source_ = options.source; 47 this.source_ = options.source;
48 this.mode_ = options.mode; 48 this.mode_ = options.mode;
49 this.pendingSegment_ = null;
50
49 this.bytesReceived = 0; 51 this.bytesReceived = 0;
50 52
51 // loadingState_ tracks how far along the buffering process we 53 // loadingState_ tracks how far along the buffering process we
...@@ -311,6 +313,8 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { ...@@ -311,6 +313,8 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
311 return; 313 return;
312 } 314 }
313 315
316 // if the codecs were explicitly specified, pass them along to the
317 // source buffer
314 mimeType = 'video/mp2t'; 318 mimeType = 'video/mp2t';
315 if (media.attributes && media.attributes.CODECS) { 319 if (media.attributes && media.attributes.CODECS) {
316 mimeType += '; codecs="' + media.attributes.CODECS + '"'; 320 mimeType += '; codecs="' + media.attributes.CODECS + '"';
...@@ -320,10 +324,33 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { ...@@ -320,10 +324,33 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
320 // transition the sourcebuffer to the ended state if we've hit the end of 324 // transition the sourcebuffer to the ended state if we've hit the end of
321 // the playlist 325 // the playlist
322 this.sourceBuffer.addEventListener('updateend', function() { 326 this.sourceBuffer.addEventListener('updateend', function() {
327 var segmentInfo = this.pendingSegment_, i, currentBuffered;
328
329 this.pendingSegment_ = null;
330
323 if (this.duration() !== Infinity && 331 if (this.duration() !== Infinity &&
324 this.mediaIndex === this.playlists.media().segments.length) { 332 this.mediaIndex === this.playlists.media().segments.length) {
325 this.mediaSource.endOfStream(); 333 this.mediaSource.endOfStream();
326 } 334 }
335
336 // When switching renditions or seeking, we may misjudge the media
337 // index to request to continue playback. Check after each append
338 // that a gap hasn't appeared in the buffered region and adjust
339 // the media index to fill it if necessary
340 if (this.tech_.buffered().length === 2 &&
341 segmentInfo.playlist === this.playlists.media()) {
342 i = this.tech_.buffered().length;
343 while (i--) {
344 if (this.tech_.currentTime() < this.tech_.buffered().start(i)) {
345 // found the misidentified segment's buffered time range
346 // adjust the media index to fill the gap
347 currentBuffered = this.findCurrentBuffered_();
348 this.playlists.updateTimelineOffset(segmentInfo.mediaIndex, this.tech_.buffered().start(i));
349 this.mediaIndex = this.playlists.getMediaIndexForTime_(currentBuffered.end(0) + 1);
350 break;
351 }
352 }
353 }
327 }.bind(this)); 354 }.bind(this));
328 }; 355 };
329 356
...@@ -369,8 +396,10 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { ...@@ -369,8 +396,10 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
369 return; 396 return;
370 } 397 }
371 media = this.playlists.media(); 398 media = this.playlists.media();
372 startTime = this.tech_.playlists.expiredPreDiscontinuity_ + this.tech_.playlists.expiredPostDiscontinuity_; 399 startTime = this.tech_.playlists.expired_;
373 startTime += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + this.tech_.mediaIndex); 400 startTime += videojs.Hls.Playlist.duration(media,
401 media.mediaSequence,
402 media.mediaSequence + this.tech_.mediaIndex);
374 403
375 i = textTrack.cues.length; 404 i = textTrack.cues.length;
376 while (i--) { 405 while (i--) {
...@@ -383,8 +412,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { ...@@ -383,8 +412,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
383 412
384 videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { 413 videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
385 var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time; 414 var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time;
386 segmentOffset = this.playlists.expiredPreDiscontinuity_; 415 segmentOffset = this.playlists.expired_;
387 segmentOffset += this.playlists.expiredPostDiscontinuity_;
388 segmentOffset += videojs.Hls.Playlist.duration(segmentInfo.playlist, 416 segmentOffset += videojs.Hls.Playlist.duration(segmentInfo.playlist,
389 segmentInfo.playlist.mediaSequence, 417 segmentInfo.playlist.mediaSequence,
390 segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex); 418 segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex);
...@@ -531,7 +559,7 @@ videojs.Hls.prototype.seekable = function() { ...@@ -531,7 +559,7 @@ videojs.Hls.prototype.seekable = function() {
531 return currentSeekable; 559 return currentSeekable;
532 } 560 }
533 561
534 startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_; 562 startOffset = this.playlists.expired_;
535 return videojs.createTimeRanges(startOffset, 563 return videojs.createTimeRanges(startOffset,
536 startOffset + (currentSeekable.end(0) - currentSeekable.start(0))); 564 startOffset + (currentSeekable.end(0) - currentSeekable.start(0)));
537 }; 565 };
...@@ -1068,10 +1096,9 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1068,10 +1096,9 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1068 this.sourceBuffer.timestampOffset = currentBuffered.end(0); 1096 this.sourceBuffer.timestampOffset = currentBuffered.end(0);
1069 } 1097 }
1070 1098
1099 // the segment is asynchronously added to the current buffered data
1071 this.sourceBuffer.appendBuffer(bytes); 1100 this.sourceBuffer.appendBuffer(bytes);
1072 1101 this.pendingSegment_ = segmentBuffer.shift();
1073 // we're done processing this segment
1074 segmentBuffer.shift();
1075 }; 1102 };
1076 1103
1077 /** 1104 /**
......
...@@ -59,12 +59,7 @@ ...@@ -59,12 +59,7 @@
59 '#EXTM3U\n' + 59 '#EXTM3U\n' +
60 '#EXTINF:10,\n' + 60 '#EXTINF:10,\n' +
61 '0.ts\n'); 61 '0.ts\n');
62 equal(loader.expiredPreDiscontinuity_, 62 equal(loader.expired_, 0, 'zero seconds expired');
63 0,
64 'zero seconds expired pre-discontinuity');
65 equal(loader.expiredPostDiscontinuity_,
66 0,
67 'zero seconds expired post-discontinuity');
68 }); 63 });
69 64
70 test('requests the initial playlist immediately', function() { 65 test('requests the initial playlist immediately', function() {
...@@ -202,7 +197,7 @@ ...@@ -202,7 +197,7 @@
202 '3.ts\n' + 197 '3.ts\n' +
203 '#EXTINF:10,\n' + 198 '#EXTINF:10,\n' +
204 '4.ts\n'); 199 '4.ts\n');
205 equal(loader.expiredPostDiscontinuity_, 10, 'expired one segment'); 200 equal(loader.expired_, 10, 'expired one segment');
206 }); 201 });
207 202
208 test('increments expired seconds after a discontinuity', function() { 203 test('increments expired seconds after a discontinuity', function() {
...@@ -226,8 +221,7 @@ ...@@ -226,8 +221,7 @@
226 '#EXT-X-DISCONTINUITY\n' + 221 '#EXT-X-DISCONTINUITY\n' +
227 '#EXTINF:4,\n' + 222 '#EXTINF:4,\n' +
228 '2.ts\n'); 223 '2.ts\n');
229 equal(loader.expiredPreDiscontinuity_, 0, 'identifies pre-discontinuity time'); 224 equal(loader.expired_, 10, 'expired one segment');
230 equal(loader.expiredPostDiscontinuity_, 10, 'expired one segment');
231 225
232 clock.tick(10 * 1000); // 10s, one target duration 226 clock.tick(10 * 1000); // 10s, one target duration
233 requests.pop().respond(200, null, 227 requests.pop().respond(200, null,
...@@ -236,8 +230,7 @@ ...@@ -236,8 +230,7 @@
236 '#EXT-X-DISCONTINUITY\n' + 230 '#EXT-X-DISCONTINUITY\n' +
237 '#EXTINF:4,\n' + 231 '#EXTINF:4,\n' +
238 '2.ts\n'); 232 '2.ts\n');
239 equal(loader.expiredPreDiscontinuity_, 0, 'tracked time across the discontinuity'); 233 equal(loader.expired_, 13, 'no expirations after the discontinuity yet');
240 equal(loader.expiredPostDiscontinuity_, 13, 'no expirations after the discontinuity yet');
241 234
242 clock.tick(10 * 1000); // 10s, one target duration 235 clock.tick(10 * 1000); // 10s, one target duration
243 requests.pop().respond(200, null, 236 requests.pop().respond(200, null,
...@@ -246,8 +239,7 @@ ...@@ -246,8 +239,7 @@
246 '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + 239 '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' +
247 '#EXTINF:10,\n' + 240 '#EXTINF:10,\n' +
248 '3.ts\n'); 241 '3.ts\n');
249 equal(loader.expiredPreDiscontinuity_, 13, 'did not increment pre-discontinuity'); 242 equal(loader.expired_, 13 + 4, 'tracked expired prior to the discontinuity');
250 equal(loader.expiredPostDiscontinuity_, 4, 'expired post-discontinuity');
251 }); 243 });
252 244
253 test('tracks expired seconds properly when two discontinuities expire at once', function() { 245 test('tracks expired seconds properly when two discontinuities expire at once', function() {
...@@ -272,8 +264,7 @@ ...@@ -272,8 +264,7 @@
272 '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + 264 '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' +
273 '#EXTINF:7,\n' + 265 '#EXTINF:7,\n' +
274 '3.ts\n'); 266 '3.ts\n');
275 equal(loader.expiredPreDiscontinuity_, 4 + 5, 'tracked pre-discontinuity time'); 267 equal(loader.expired_, 4 + 5 + 6, 'tracked both expired discontinuities');
276 equal(loader.expiredPostDiscontinuity_, 6, 'tracked post-discontinuity time');
277 }); 268 });
278 269
279 test('emits an error when an initial playlist request fails', function() { 270 test('emits an error when an initial playlist request fails', function() {
...@@ -782,8 +773,7 @@ ...@@ -782,8 +773,7 @@
782 '1001.ts\n' + 773 '1001.ts\n' +
783 '#EXTINF:5,\n' + 774 '#EXTINF:5,\n' +
784 '1002.ts\n'); 775 '1002.ts\n');
785 loader.expiredPreDiscontinuity_ = 50; 776 loader.expired_ = 150;
786 loader.expiredPostDiscontinuity_ = 100;
787 777
788 equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); 778 equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero');
789 equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); 779 equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero');
...@@ -795,6 +785,30 @@ ...@@ -795,6 +785,30 @@
795 equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); 785 equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment');
796 }); 786 });
797 787
788 test('updating the timeline offset adjusts results from getMediaIndexForTime_', function() {
789 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
790 requests.pop().respond(200, null,
791 '#EXTM3U\n' +
792 '#EXT-X-MEDIA-SEQUENCE:23\n' +
793 '#EXTINF:4,\n' +
794 '23.ts\n' +
795 '#EXTINF:5,\n' +
796 '24.ts\n' +
797 '#EXTINF:6,\n' +
798 '25.ts\n' +
799 '#EXTINF:7,\n' +
800 '26.ts\n');
801 loader.updateTimelineOffset(0, 150);
802 equal(loader.getMediaIndexForTime_(150), 0, 'translated the first segment');
803 equal(loader.getMediaIndexForTime_(130), 0, 'clamps the index to zero');
804 equal(loader.getMediaIndexForTime_(155), 1, 'translated the second segment');
805
806 loader.updateTimelineOffset(2, 30);
807 equal(loader.getMediaIndexForTime_(30 - 5 - 1), 0, 'translated the first segment');
808 equal(loader.getMediaIndexForTime_(30 + 7), 3, 'translated the last segment');
809 equal(loader.getMediaIndexForTime_(30 - 3), 1, 'translated an earlier segment');
810 });
811
798 test('does not misintrepret playlists missing newlines at the end', function() { 812 test('does not misintrepret playlists missing newlines at the end', function() {
799 var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); 813 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
800 requests.shift().respond(200, null, 814 requests.shift().respond(200, null,
......
...@@ -23,7 +23,9 @@ ...@@ -23,7 +23,9 @@
23 23
24 <!-- Media Sources plugin --> 24 <!-- Media Sources plugin -->
25 <script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> 25 <script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
26 26 <script>
27 videojs.MediaSource.webWorkerURI = '../../node_modules/videojs-contrib-media-sources/src/transmuxer_worker.js';
28 </script>
27 <!-- HLS plugin --> 29 <!-- HLS plugin -->
28 <script src="../../src/videojs-hls.js"></script> 30 <script src="../../src/videojs-hls.js"></script>
29 31
...@@ -77,12 +79,11 @@ ...@@ -77,12 +79,11 @@
77 </video> 79 </video>
78 <section class="stats"> 80 <section class="stats">
79 <h2>Player Stats</h2> 81 <h2>Player Stats</h2>
80 <div class="segment-timeline"></div>
81 <dl> 82 <dl>
82 <dt>Current Time:</dt> 83 <dt>Current Time:</dt>
83 <dd class="current-time-stat">0</dd> 84 <dd class="current-time-stat">0</dd>
84 <dt>Buffered:</dt> 85 <dt>Buffered:</dt>
85 <dd><span class="buffered-start-stat">-</span> - <span class="buffered-end-stat">-</span></dd> 86 <dd class="buffered-stat">-</dd>
86 <dt>Seekable:</dt> 87 <dt>Seekable:</dt>
87 <dd><span class="seekable-start-stat">-</span> - <span class="seekable-end-stat">-</span></dd> 88 <dd><span class="seekable-start-stat">-</span> - <span class="seekable-end-stat">-</span></dd>
88 <dt>Video Bitrate:</dt> 89 <dt>Video Bitrate:</dt>
...@@ -90,10 +91,13 @@ ...@@ -90,10 +91,13 @@
90 <dt>Measured Bitrate:</dt> 91 <dt>Measured Bitrate:</dt>
91 <dd class="measured-bitrate-stat">0 kbps</dd> 92 <dd class="measured-bitrate-stat">0 kbps</dd>
92 </dl> 93 </dl>
94 <h3>Bitrate Switching</h3>
93 <div class="switching-stats"> 95 <div class="switching-stats">
94 Once the player begins loading, you'll see information about the 96 Once the player begins loading, you'll see information about the
95 operation of the adaptive quality switching here. 97 operation of the adaptive quality switching here.
96 </div> 98 </div>
99 <h3>Timed Metadata</h3>
100 <div class="segment-timeline"></div>
97 </section> 101 </section>
98 102
99 <script src="stats.js"></script> 103 <script src="stats.js"></script>
...@@ -107,8 +111,7 @@ ...@@ -107,8 +111,7 @@
107 // ------------ 111 // ------------
108 112
109 var currentTimeStat = document.querySelector('.current-time-stat'); 113 var currentTimeStat = document.querySelector('.current-time-stat');
110 var bufferedStartStat = document.querySelector('.buffered-start-stat'); 114 var bufferedStat = document.querySelector('.buffered-stat');
111 var bufferedEndStat = document.querySelector('.buffered-end-stat');
112 var seekableStartStat = document.querySelector('.seekable-start-stat'); 115 var seekableStartStat = document.querySelector('.seekable-start-stat');
113 var seekableEndStat = document.querySelector('.seekable-end-stat'); 116 var seekableEndStat = document.querySelector('.seekable-end-stat');
114 var videoBitrateState = document.querySelector('.video-bitrate-stat'); 117 var videoBitrateState = document.querySelector('.video-bitrate-stat');
...@@ -119,20 +122,17 @@ ...@@ -119,20 +122,17 @@
119 }); 122 });
120 123
121 player.on('progress', function() { 124 player.on('progress', function() {
122 var oldStart, oldEnd; 125 var bufferedText = '', oldStart, oldEnd, i;
126
123 // buffered 127 // buffered
124 var buffered = player.buffered(); 128 var buffered = player.buffered();
125 if (buffered.length) { 129 if (buffered.length) {
126 130 bufferedText += buffered.start(0) + ' - ' + buffered.end(0);
127 oldStart = bufferedStartStat.textContent;
128 if (buffered.start(0).toFixed(1) !== oldStart) {
129 bufferedStartStat.textContent = buffered.start(0).toFixed(1);
130 }
131 oldEnd = bufferedEndStat.textContent;
132 if (buffered.end(0).toFixed(1) !== oldEnd) {
133 bufferedEndStat.textContent = buffered.end(0).toFixed(1);
134 } 131 }
132 for (i = 1; i < buffered.length; i++) {
133 bufferedText += ', ' + buffered.start(i) + ' - ' + buffered.end(i);
135 } 134 }
135 bufferedStat.textContent = bufferedText;
136 136
137 // seekable 137 // seekable
138 var seekable = player.seekable(); 138 var seekable = player.seekable();
...@@ -149,14 +149,14 @@ ...@@ -149,14 +149,14 @@
149 } 149 }
150 150
151 // bitrates 151 // bitrates
152 var playlist = player.tech.hls.playlists.media(); 152 var playlist = player.tech_.hls.playlists.media();
153 if (playlist && playlist.attributes && playlist.attributes.BANDWIDTH) { 153 if (playlist && playlist.attributes && playlist.attributes.BANDWIDTH) {
154 videoBitrateState.textContent = (playlist.attributes.BANDWIDTH / 1024).toLocaleString(undefined, { 154 videoBitrateState.textContent = (playlist.attributes.BANDWIDTH / 1024).toLocaleString(undefined, {
155 maximumFractionDigits: 1 155 maximumFractionDigits: 1
156 }) + ' kbps'; 156 }) + ' kbps';
157 } 157 }
158 if (player.tech.hls.bandwidth) { 158 if (player.tech_.hls.bandwidth) {
159 measuredBitrateStat.textContent = (player.tech.hls.bandwidth / 1024).toLocaleString(undefined, { 159 measuredBitrateStat.textContent = (player.tech_.hls.bandwidth / 1024).toLocaleString(undefined, {
160 maximumFractionDigits: 1 160 maximumFractionDigits: 1
161 }) + ' kbps'; 161 }) + ' kbps';
162 } 162 }
......
...@@ -4,10 +4,15 @@ ...@@ -4,10 +4,15 @@
4 } 4 }
5 5
6 .axis line, 6 .axis line,
7 .axis path, 7 .axis path {
8 .intersect { 8 fill: none;
9 stroke: #111;
10 }
11
12 .bitrates {
9 fill: none; 13 fill: none;
10 stroke: #000; 14 stroke: steelblue;
15 stroke-width: 3px;
11 } 16 }
12 17
13 .cue { 18 .cue {
...@@ -23,6 +28,6 @@ ...@@ -23,6 +28,6 @@
23 28
24 .intersect { 29 .intersect {
25 fill: none; 30 fill: none;
26 stroke: #000; 31 stroke: #111;
27 stroke-dasharray: 2,2; 32 stroke-dasharray: 2,2;
28 } 33 }
......
...@@ -7,9 +7,35 @@ ...@@ -7,9 +7,35 @@
7 7
8 var d3 = window.d3; 8 var d3 = window.d3;
9 9
10 var setupGraph = function(element) { 10 var bitrateTickFormatter = d3.format(',.0f');
11 element.innerHTML = '';
12 11
12 var updateBitrateAxes = function(svg, xScale, yScale) {
13 var xAxis = d3.svg.axis().scale(xScale).orient('bottom');
14 svg.select('.axis.x')
15 .transition().duration(500)
16 .call(xAxis);
17
18 var yAxis = d3.svg.axis().scale(yScale)
19 .tickFormat(function(value) {
20 return bitrateTickFormatter(value / 1024);
21 }).orient('left');
22 svg.select('.axis.y')
23 .transition().duration(500)
24 .call(yAxis);
25 };
26
27 var updateBitrates = function(svg, x, y, measuredBitrateKbps) {
28 var bitrates, line;
29
30 bitrates = svg.selectAll('.bitrates').datum(measuredBitrateKbps);
31 line = d3.svg.line()
32 .x(function(bitrate) { return x(bitrate.time); })
33 .y(function(bitrate) { return y(bitrate.value); });
34
35 bitrates.transition().duration(500).attr('d', line);
36 };
37
38 var setupGraph = function(element, player) {
13 // setup the display 39 // setup the display
14 var margin = { 40 var margin = {
15 top: 20, 41 top: 20,
...@@ -30,15 +56,14 @@ ...@@ -30,15 +56,14 @@
30 var x = d3.time.scale().range([0, width]); // d3.scale.linear().range([0, width]); 56 var x = d3.time.scale().range([0, width]); // d3.scale.linear().range([0, width]);
31 var y = d3.scale.linear().range([height, 0]); 57 var y = d3.scale.linear().range([height, 0]);
32 58
33 x.domain([new Date(), new Date(Date.now() + (5 * 60 * 1000))]); 59 x.domain([new Date(), new Date(Date.now() + (1 * 60 * 1000))]);
34 y.domain([0, 5 * 1024 * 1024 * 8]); 60 y.domain([0, 5 * 1024 * 1024 * 8]);
35 61
36 var timeAxis = d3.svg.axis().scale(x).orient('bottom'); 62 var timeAxis = d3.svg.axis().scale(x).orient('bottom');
37 var tickFormatter = d3.format(',.0f');
38 var bitrateAxis = d3.svg.axis() 63 var bitrateAxis = d3.svg.axis()
39 .scale(y) 64 .scale(y)
40 .tickFormat(function(value) { 65 .tickFormat(function(value) {
41 return tickFormatter(value / 1024); 66 return bitrateTickFormatter(value / 1024);
42 }) 67 })
43 .orient('left'); 68 .orient('left');
44 69
...@@ -60,6 +85,26 @@ ...@@ -60,6 +85,26 @@
60 .style('text-anchor', 'end') 85 .style('text-anchor', 'end')
61 .text('Bitrate (kb/s)'); 86 .text('Bitrate (kb/s)');
62 87
88 svg.append('path')
89 .attr('class', 'bitrates');
90
91 var measuredBitrateKbps = [{
92 time: new Date(),
93 value: player.tech_.hls.bandwidth || 0
94 }];
95
96 player.on('progress', function() {
97 measuredBitrateKbps.push({
98 time: new Date(),
99 value: player.tech_.hls.bandwidth || 0
100 });
101 x.domain([x.domain()[0], new Date()]);
102 y.domain([0, d3.max(measuredBitrateKbps, function(bitrate) {
103 return bitrate.value;
104 })]);
105 updateBitrateAxes(svg, x, y);
106 updateBitrates(svg, x, y, measuredBitrateKbps);
107 });
63 }; 108 };
64 109
65 // --------------- 110 // ---------------
...@@ -86,8 +131,8 @@ ...@@ -86,8 +131,8 @@
86 131
87 var mediaDomain = function(media, player) { 132 var mediaDomain = function(media, player) {
88 var segments = media.segments; 133 var segments = media.segments;
89 var end = player.tech.hls.playlists.expiredPreDiscontinuity_; 134 var end = player.tech_.hls.playlists.expiredPreDiscontinuity_;
90 end += player.tech.hls.playlists.expiredPostDiscontinuity_; 135 end += player.tech_.hls.playlists.expiredPostDiscontinuity_;
91 end += Playlist.duration(media, 136 end += Playlist.duration(media,
92 media.mediaSequence, 137 media.mediaSequence,
93 media.mediaSequence + segments.length); 138 media.mediaSequence + segments.length);
...@@ -160,7 +205,7 @@ ...@@ -160,7 +205,7 @@
160 .call(ptsAxis); 205 .call(ptsAxis);
161 }; 206 };
162 var svgRenderSegmentTimeline = function(container, player) { 207 var svgRenderSegmentTimeline = function(container, player) {
163 var media = player.tech.hls.playlists.media(); 208 var media = player.tech_.hls.playlists.media();
164 var segments = media.segments; // media.segments.slice(0, count); 209 var segments = media.segments; // media.segments.slice(0, count);
165 210
166 // setup the display 211 // setup the display
...@@ -196,7 +241,7 @@ ...@@ -196,7 +241,7 @@
196 241
197 // update everything on progress 242 // update everything on progress
198 player.on('progress', function() { 243 player.on('progress', function() {
199 var updatedMedia = player.tech.hls.playlists.media(); 244 var updatedMedia = player.tech_.hls.playlists.media();
200 var segments = updatedMedia.segments; // updatedMedia.segments.slice(currentIndex, currentIndex + count); 245 var segments = updatedMedia.segments; // updatedMedia.segments.slice(currentIndex, currentIndex + count);
201 246
202 if (updatedMedia.mediaSequence !== media.mediaSequence) { 247 if (updatedMedia.mediaSequence !== media.mediaSequence) {
...@@ -220,7 +265,7 @@ ...@@ -220,7 +265,7 @@
220 }; 265 };
221 266
222 var displayCues = function(container, player) { 267 var displayCues = function(container, player) {
223 var media = player.tech.hls.playlists.media(); 268 var media = player.tech_.hls.playlists.media();
224 if (media && media.segments) { 269 if (media && media.segments) {
225 svgRenderSegmentTimeline(container, player); 270 svgRenderSegmentTimeline(container, player);
226 } else { 271 } else {
......
...@@ -42,7 +42,25 @@ var ...@@ -42,7 +42,25 @@ var
42 // patch over some methods of the provided tech so it can be tested 42 // patch over some methods of the provided tech so it can be tested
43 // synchronously with sinon's fake timers 43 // synchronously with sinon's fake timers
44 mockTech = function(tech) { 44 mockTech = function(tech) {
45 if (tech.isMocked_) {
46 // make this function idempotent because HTML and Flash based
47 // playback have very different lifecycles. For HTML, the tech
48 // is available on player creation. For Flash, the tech isn't
49 // ready until the source has been loaded and one tick has
50 // expired.
51 return;
52 }
53
54 tech.isMocked_ = true;
55
56 tech.paused_ = !tech.autoplay();
57 tech.paused = function() {
58 return tech.paused_;
59 };
60
61 if (!tech.currentTime_) {
45 tech.currentTime_ = tech.currentTime; 62 tech.currentTime_ = tech.currentTime;
63 }
46 tech.currentTime = function() { 64 tech.currentTime = function() {
47 return tech.time_ === undefined ? tech.currentTime_() : tech.time_; 65 return tech.time_ === undefined ? tech.currentTime_() : tech.time_;
48 }; 66 };
...@@ -61,6 +79,19 @@ var ...@@ -61,6 +79,19 @@ var
61 return tech.src_ === undefined ? tech.currentSrc_() : tech.src_; 79 return tech.src_ === undefined ? tech.currentSrc_() : tech.src_;
62 }; 80 };
63 81
82 tech.play_ = tech.play;
83 tech.play = function() {
84 tech.play_();
85 tech.paused_ = false;
86 tech.trigger('play');
87 };
88 tech.pause_ = tech.pause_;
89 tech.pause = function() {
90 tech.pause_();
91 tech.paused_ = true;
92 tech.trigger('pause');
93 };
94
64 tech.setCurrentTime = function(time) { 95 tech.setCurrentTime = function(time) {
65 tech.time_ = time; 96 tech.time_ = time;
66 97
...@@ -95,6 +126,7 @@ var ...@@ -95,6 +126,7 @@ var
95 // ensure the Flash tech is ready 126 // ensure the Flash tech is ready
96 player.tech_.triggerReady(); 127 player.tech_.triggerReady();
97 clock.tick(1); 128 clock.tick(1);
129 mockTech(player.tech_);
98 130
99 // simulate the sourceopen event 131 // simulate the sourceopen event
100 player.tech_.hls.mediaSource.readyState = 'open'; 132 player.tech_.hls.mediaSource.readyState = 'open';
...@@ -197,9 +229,11 @@ var ...@@ -197,9 +229,11 @@ var
197 constructor: function() {}, 229 constructor: function() {},
198 abort: function() {}, 230 abort: function() {},
199 buffered: videojs.createTimeRange(), 231 buffered: videojs.createTimeRange(),
200 appendBuffer: function() {} 232 appendBuffer: function() {},
233 remove: function() {}
201 }))(); 234 }))();
202 }, 235 },
236 endOfStream: function() {}
203 }), 237 }),
204 238
205 // do a shallow copy of the properties of source onto the target object 239 // do a shallow copy of the properties of source onto the target object
...@@ -882,6 +916,57 @@ test('moves to the next segment if there is a network error', function() { ...@@ -882,6 +916,57 @@ test('moves to the next segment if there is a network error', function() {
882 strictEqual(mediaIndex + 1, player.tech_.hls.mediaIndex, 'media index is incremented'); 916 strictEqual(mediaIndex + 1, player.tech_.hls.mediaIndex, 'media index is incremented');
883 }); 917 });
884 918
919 test('updates playlist timeline offsets if it detects a desynchronization', function() {
920 var buffered = [], currentTime = 0;
921
922 player.src({
923 src: 'manifest/master.m3u8',
924 type: 'application/vnd.apple.mpegurl'
925 });
926 openMediaSource(player);
927 standardXHRResponse(requests.shift()); // master
928 requests.shift().respond(200, null,
929 '#EXTM3U\n' +
930 '#EXT-X-MEDIA-SEQUENCE:2\n' +
931 '#EXTINF:10,\n' +
932 '2.ts\n' +
933 '#EXTINF:10,\n' +
934 '3.ts\n'); // media
935 player.tech_.buffered = function() { return videojs.createTimeRange(buffered); };
936 player.tech_.currentTime = function() { return currentTime; };
937 player.tech_.paused = function() { return false; };
938 player.tech_.trigger('play');
939 clock.tick(1);
940 standardXHRResponse(requests.shift()); // segment 0
941 equal(player.tech_.hls.mediaIndex, 1, 'incremented mediaIndex');
942
943 player.tech_.hls.sourceBuffer.trigger('updateend');
944 buffered.push([0, 10]);
945
946 // force a playlist switch
947 player.tech_.hls.playlists.media('media1.m3u8');
948 requests = requests.filter(function(request) {
949 return !request.aborted;
950 });
951 requests.shift().respond(200, null,
952 '#EXTM3U\n' +
953 '#EXT-X-MEDIA-SEQUENCE:9999\n' +
954 '#EXTINF:10,\n' +
955 '3.ts\n' +
956 '#EXTINF:10,\n' +
957 '4.ts\n' +
958 '#EXTINF:10,\n' +
959 '5.ts\n'); // media1
960 player.tech_.hls.checkBuffer_();
961 standardXHRResponse(requests.shift());
962
963 buffered.push([20, 30]);
964 currentTime = 8;
965
966 player.tech_.hls.sourceBuffer.trigger('updateend');
967 equal(player.tech_.hls.mediaIndex, 0, 'prepared to request the missing segment');
968 });
969
885 test('updates the duration after switching playlists', function() { 970 test('updates the duration after switching playlists', function() {
886 var selectedPlaylist = false; 971 var selectedPlaylist = false;
887 player.src({ 972 player.src({
...@@ -1172,33 +1257,14 @@ test('buffers based on the correct TimeRange if multiple ranges exist', function ...@@ -1172,33 +1257,14 @@ test('buffers based on the correct TimeRange if multiple ranges exist', function
1172 return 8; 1257 return 8;
1173 }; 1258 };
1174 1259
1175 player.tech_.buffered = function() {
1176 return {
1177 start: function(num) {
1178 switch (num) {
1179 case 0:
1180 return 0;
1181 case 1:
1182 return 50;
1183 }
1184 },
1185 end: function(num) {
1186 switch (num) {
1187 case 0:
1188 return 10;
1189 case 1:
1190 return 160;
1191 }
1192 },
1193 length: 2
1194 };
1195 };
1196
1197 player.src({ 1260 player.src({
1198 src: 'manifest/media.m3u8', 1261 src: 'manifest/media.m3u8',
1199 type: 'application/vnd.apple.mpegurl' 1262 type: 'application/vnd.apple.mpegurl'
1200 }); 1263 });
1201 openMediaSource(player); 1264 openMediaSource(player);
1265 player.tech_.buffered = function() {
1266 return videojs.createTimeRange([[0, 10], [50, 160]]);
1267 };
1202 1268
1203 standardXHRResponse(requests[0]); 1269 standardXHRResponse(requests[0]);
1204 standardXHRResponse(requests[1]); 1270 standardXHRResponse(requests[1]);
......