f9633470 by David LaPalomento

autoplay at the live point. fix live id3 cue insertion.. Closes #353

2 parents 7aabe429 9dce22d1
...@@ -2,7 +2,7 @@ CHANGELOG ...@@ -2,7 +2,7 @@ CHANGELOG
2 ========= 2 =========
3 3
4 ## HEAD (Unreleased) 4 ## HEAD (Unreleased)
5 _(none)_ 5 * autoplay at the live point. fix live id3 cue insertion. ([view](https://github.com/videojs/videojs-contrib-hls/pull/353))
6 6
7 -------------------- 7 --------------------
8 8
......
...@@ -73,6 +73,7 @@ ...@@ -73,6 +73,7 @@
73 </video> 73 </video>
74 <script> 74 <script>
75 videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf'; 75 videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
76
76 // initialize the player 77 // initialize the player
77 var player = videojs('video'); 78 var player = videojs('video');
78 </script> 79 </script>
......
...@@ -389,8 +389,8 @@ ...@@ -389,8 +389,8 @@
389 this.media_.mediaSequence, 389 this.media_.mediaSequence,
390 lastDiscontinuity); 390 lastDiscontinuity);
391 this.expiredPostDiscontinuity_ += Playlist.duration(this.media_, 391 this.expiredPostDiscontinuity_ += Playlist.duration(this.media_,
392 lastDiscontinuity, 392 lastDiscontinuity,
393 this.media_.mediaSequence + expiredCount); 393 update.mediaSequence);
394 } 394 }
395 395
396 this.media_ = this.master.playlists[update.uri]; 396 this.media_ = this.master.playlists[update.uri];
......
...@@ -35,6 +35,14 @@ videojs.Hls = videojs.Flash.extend({ ...@@ -35,6 +35,14 @@ videojs.Hls = videojs.Flash.extend({
35 options.source = source; 35 options.source = source;
36 this.bytesReceived = 0; 36 this.bytesReceived = 0;
37 37
38 this.hasPlayed_ = false;
39 this.on(player, 'loadstart', function() {
40 this.hasPlayed_ = false;
41 this.one(this.mediaSource, 'sourceopen', this.setupFirstPlay);
42 });
43 this.on(player, ['play', 'loadedmetadata'], this.setupFirstPlay);
44
45
38 // TODO: After video.js#1347 is pulled in remove these lines 46 // TODO: After video.js#1347 is pulled in remove these lines
39 this.currentTime = videojs.Hls.prototype.currentTime; 47 this.currentTime = videojs.Hls.prototype.currentTime;
40 this.setCurrentTime = videojs.Hls.prototype.setCurrentTime; 48 this.setCurrentTime = videojs.Hls.prototype.setCurrentTime;
...@@ -109,12 +117,7 @@ videojs.Hls.prototype.src = function(src) { ...@@ -109,12 +117,7 @@ videojs.Hls.prototype.src = function(src) {
109 117
110 this.playlists.on('loadedmetadata', videojs.bind(this, function() { 118 this.playlists.on('loadedmetadata', videojs.bind(this, function() {
111 var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration, 119 var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration,
112 segmentDlTime, setupEvents, threshold; 120 segmentDlTime, threshold;
113
114 setupEvents = function() {
115 this.fillBuffer();
116 player.trigger('loadedmetadata');
117 };
118 121
119 oldMediaPlaylist = this.playlists.media(); 122 oldMediaPlaylist = this.playlists.media();
120 123
...@@ -155,12 +158,16 @@ videojs.Hls.prototype.src = function(src) { ...@@ -155,12 +158,16 @@ videojs.Hls.prototype.src = function(src) {
155 if (newBitrate > oldBitrate && segmentDlTime <= threshold) { 158 if (newBitrate > oldBitrate && segmentDlTime <= threshold) {
156 this.playlists.media(selectedPlaylist); 159 this.playlists.media(selectedPlaylist);
157 loaderHandler = videojs.bind(this, function() { 160 loaderHandler = videojs.bind(this, function() {
158 setupEvents.call(this); 161 this.setupFirstPlay();
162 this.fillBuffer();
163 player.trigger('loadedmetadata');
159 this.playlists.off('loadedplaylist', loaderHandler); 164 this.playlists.off('loadedplaylist', loaderHandler);
160 }); 165 });
161 this.playlists.on('loadedplaylist', loaderHandler); 166 this.playlists.on('loadedplaylist', loaderHandler);
162 } else { 167 } else {
163 setupEvents.call(this); 168 this.setupFirstPlay();
169 this.fillBuffer();
170 player.trigger('loadedmetadata');
164 } 171 }
165 })); 172 }));
166 173
...@@ -306,11 +313,14 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { ...@@ -306,11 +313,14 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
306 313
307 videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { 314 videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
308 var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time; 315 var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time;
309 segmentOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, 316 segmentOffset = this.playlists.expiredPreDiscontinuity_;
310 segmentInfo.playlist.mediaSequence, 317 segmentOffset += this.playlists.expiredPostDiscontinuity_;
311 segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex); 318 segmentOffset += videojs.Hls.Playlist.duration(segmentInfo.playlist,
319 segmentInfo.playlist.mediaSequence,
320 segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex);
312 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; 321 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
313 minPts = Math.min(segment.minVideoPts, segment.minAudioPts); 322 minPts = Math.min(isFinite(segment.minVideoPts) ? segment.minVideoPts : Infinity,
323 isFinite(segment.minAudioPts) ? segment.minAudioPts : Infinity);
314 324
315 while (segmentInfo.pendingMetadata.length) { 325 while (segmentInfo.pendingMetadata.length) {
316 metadata = segmentInfo.pendingMetadata[0].metadata; 326 metadata = segmentInfo.pendingMetadata[0].metadata;
...@@ -322,6 +332,7 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { ...@@ -322,6 +332,7 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
322 time = segmentOffset + ((metadata.pts - minPts) * 0.001); 332 time = segmentOffset + ((metadata.pts - minPts) * 0.001);
323 cue = new window.VTTCue(time, time, frame.value || frame.url || ''); 333 cue = new window.VTTCue(time, time, frame.value || frame.url || '');
324 cue.frame = frame; 334 cue.frame = frame;
335 cue.pts_ = metadata.pts;
325 textTrack.addCue(cue); 336 textTrack.addCue(cue);
326 } 337 }
327 segmentInfo.pendingMetadata.shift(); 338 segmentInfo.pendingMetadata.shift();
...@@ -329,6 +340,33 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { ...@@ -329,6 +340,33 @@ videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
329 }; 340 };
330 341
331 /** 342 /**
343 * Seek to the latest media position if this is a live video and the
344 * player and video are loaded and initialized.
345 */
346 videojs.Hls.prototype.setupFirstPlay = function() {
347 var seekable, media;
348 media = this.playlists.media();
349
350 // check that everything is ready to begin buffering
351 if (!this.hasPlayed_ &&
352 this.sourceBuffer &&
353 media &&
354 this.paused() === false) {
355
356 // only run this block once per video
357 this.hasPlayed_ = true;
358
359 if (this.duration() === Infinity) {
360 // seek to the latest media position for live videos
361 seekable = this.seekable();
362 if (seekable.length) {
363 this.setCurrentTime(seekable.end(0));
364 }
365 }
366 }
367 };
368
369 /**
332 * Reset the mediaIndex if play() is called after the video has 370 * Reset the mediaIndex if play() is called after the video has
333 * ended. 371 * ended.
334 */ 372 */
...@@ -337,25 +375,20 @@ videojs.Hls.prototype.play = function() { ...@@ -337,25 +375,20 @@ videojs.Hls.prototype.play = function() {
337 this.mediaIndex = 0; 375 this.mediaIndex = 0;
338 } 376 }
339 377
340 // we may need to seek to begin playing safely for live playlists 378 if (!this.hasPlayed_) {
341 if (this.duration() === Infinity) { 379 videojs.Flash.prototype.play.apply(this, arguments);
342 380 return this.setupFirstPlay();
343 // if this is the first time we're playing the stream or we're 381 }
344 // ahead of the latest safe playback position, seek to the live
345 // point
346 if (!this.player().hasClass('vjs-has-started') ||
347 this.currentTime() > this.seekable().end(0)) {
348 this.setCurrentTime(this.seekable().end(0));
349 382
350 } else if (this.currentTime() < this.seekable().start(0)) { 383 // if the viewer has paused and we fell out of the live window,
351 // if the viewer has paused and we fell out of the live window, 384 // seek forward to the earliest available position
352 // seek forward to the earliest available position 385 if (this.duration() === Infinity &&
353 this.setCurrentTime(this.seekable().start(0)); 386 this.currentTime() < this.seekable().start(0)) {
354 } 387 this.setCurrentTime(this.seekable().start(0));
355 } 388 }
356 389
357 // delegate back to the Flash implementation 390 // delegate back to the Flash implementation
358 return videojs.Flash.prototype.play.apply(this, arguments); 391 videojs.Flash.prototype.play.apply(this, arguments);
359 }; 392 };
360 393
361 videojs.Hls.prototype.currentTime = function() { 394 videojs.Hls.prototype.currentTime = function() {
...@@ -396,7 +429,9 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { ...@@ -396,7 +429,9 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
396 this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); 429 this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime);
397 430
398 // abort any segments still being decoded 431 // abort any segments still being decoded
399 this.sourceBuffer.abort(); 432 if (this.sourceBuffer) {
433 this.sourceBuffer.abort();
434 }
400 435
401 // cancel outstanding requests and buffer appends 436 // cancel outstanding requests and buffer appends
402 this.cancelSegmentXhr(); 437 this.cancelSegmentXhr();
...@@ -436,6 +471,10 @@ videojs.Hls.prototype.seekable = function() { ...@@ -436,6 +471,10 @@ videojs.Hls.prototype.seekable = function() {
436 // report the seekable range relative to the earliest possible 471 // report the seekable range relative to the earliest possible
437 // position when the stream was first loaded 472 // position when the stream was first loaded
438 currentSeekable = videojs.Hls.Playlist.seekable(media); 473 currentSeekable = videojs.Hls.Playlist.seekable(media);
474 if (!currentSeekable.length) {
475 return currentSeekable;
476 }
477
439 startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_; 478 startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_;
440 return videojs.createTimeRange(startOffset, 479 return videojs.createTimeRange(startOffset,
441 startOffset + (currentSeekable.end(0) - currentSeekable.start(0))); 480 startOffset + (currentSeekable.end(0) - currentSeekable.start(0)));
...@@ -679,7 +718,7 @@ videojs.Hls.prototype.fillBuffer = function(offset) { ...@@ -679,7 +718,7 @@ videojs.Hls.prototype.fillBuffer = function(offset) {
679 // being buffering so we don't preload data that will never be 718 // being buffering so we don't preload data that will never be
680 // played 719 // played
681 if (!this.playlists.media().endList && 720 if (!this.playlists.media().endList &&
682 !this.player().hasClass('vjs-has-started') && 721 !player.hasClass('vjs-has-started') &&
683 offset === undefined) { 722 offset === undefined) {
684 return; 723 return;
685 } 724 }
...@@ -920,22 +959,24 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -920,22 +959,24 @@ videojs.Hls.prototype.drainBuffer = function(event) {
920 // FLV tags until we find the one that is closest to the desired 959 // FLV tags until we find the one that is closest to the desired
921 // playback time 960 // playback time
922 if (typeof offset === 'number') { 961 if (typeof offset === 'number') {
923 // determine the offset within this segment we're seeking to 962 if (tags.length) {
924 segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_; 963 // determine the offset within this segment we're seeking to
925 segmentOffset += videojs.Hls.Playlist.duration(playlist, 964 segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_;
926 playlist.mediaSequence, 965 segmentOffset += videojs.Hls.Playlist.duration(playlist,
927 playlist.mediaSequence + mediaIndex); 966 playlist.mediaSequence,
928 segmentOffset = offset - (segmentOffset * 1000); 967 playlist.mediaSequence + mediaIndex);
929 ptsTime = segmentOffset + tags[0].pts; 968 segmentOffset = offset - (segmentOffset * 1000);
930 969 ptsTime = segmentOffset + tags[0].pts;
931 while (tags[i + 1] && tags[i].pts < ptsTime) { 970
932 i++; 971 while (tags[i + 1] && tags[i].pts < ptsTime) {
933 } 972 i++;
973 }
934 974
935 // tell the SWF the media position of the first tag we'll be delivering 975 // tell the SWF the media position of the first tag we'll be delivering
936 this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001)); 976 this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001));
937 977
938 tags = tags.slice(i); 978 tags = tags.slice(i);
979 }
939 980
940 this.lastSeekedTime_ = null; 981 this.lastSeekedTime_ = null;
941 } 982 }
......
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta charset="utf-8">
5 <title>video.js HLS Stats</title>
6
7 <link href="../../node_modules/video.js/dist/video-js/video-js.css" rel="stylesheet">
8
9 <!-- video.js -->
10 <script src="../../node_modules/video.js/dist/video-js/video.dev.js"></script>
11
12 <!-- Media Sources plugin -->
13 <script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
14
15 <!-- HLS plugin -->
16 <script src="../../src/videojs-hls.js"></script>
17
18 <!-- segment handling -->
19 <script src="../../src/xhr.js"></script>
20 <script src="../../src/flv-tag.js"></script>
21 <script src="../../src/stream.js"></script>
22 <script src="../../src/exp-golomb.js"></script>
23 <script src="../../src/h264-extradata.js"></script>
24 <script src="../../src/h264-stream.js"></script>
25 <script src="../../src/aac-stream.js"></script>
26 <script src="../../src/metadata-stream.js"></script>
27 <script src="../../src/segment-parser.js"></script>
28
29 <!-- m3u8 handling -->
30 <script src="../../src/m3u8/m3u8-parser.js"></script>
31 <script src="../../src/playlist.js"></script>
32 <script src="../../src/playlist-loader.js"></script>
33
34 <script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
35 <script src="../../src/decrypter.js"></script>
36
37
38 <!-- player stats visualization -->
39 <link href="stats.css" rel="stylesheet">
40 <script src="../switcher/js/vendor/d3.min.js"></script>
41
42 <!-- debugging -->
43 <script src="../../src/bin-utils.js"></script>
44 <style>
45 body {
46 font-family: Arial, sans-serif;
47 margin: 20px;
48 }
49 .info {
50 background-color: #eee;
51 border: thin solid #333;
52 border-radius: 3px;
53 padding: 0 5px;
54 margin: 20px 0;
55 }
56 </style>
57
58 </head>
59 <body>
60 <div class="info">
61 <p>The video below is an <a href="https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1">HTTP Live Stream</a>. On desktop browsers other than Safari, the HLS plugin will polyfill support for the format on top of the video.js Flash tech.</p>
62 <p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p>
63 </div>
64 <video id="video"
65 class="video-js vjs-default-skin"
66 height="300"
67 width="600"
68 controls>
69 <source
70 src="http://s3.amazonaws.com/_bc_dml/example-content/bipbop-id3/index.m3u8"
71 type="application/x-mpegURL">
72 </video>
73 <section class="stats">
74 <h2>Player Stats</h2>
75 <div class="segment-timeline"></div>
76 <dl>
77 <dt>Current Time:</dt>
78 <dd class="current-time-stat">0</dd>
79 <dt>Buffered:</dt>
80 <dd><span class="buffered-start-stat">-</span> - <span class="buffered-end-stat">-</span></dd>
81 <dt>Seekable:</dt>
82 <dd><span class="seekable-start-stat">-</span> - <span class="seekable-end-stat">-</span></dd>
83 <dt>Video Bitrate:</dt>
84 <dd class="video-bitrate-stat">0 kbps</dd>
85 <dt>Measured Bitrate:</dt>
86 <dd class="measured-bitrate-stat">0 kbps</dd>
87 </dl>
88 <div class="switching-stats">
89 Once the player begins loading, you'll see information about the
90 operation of the adaptive quality switching here.
91 </div>
92 </section>
93
94 <script src="stats.js"></script>
95 <script>
96 videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf';
97 // initialize the player
98 var player = videojs('video');
99
100 // ------------
101 // Player Stats
102 // ------------
103
104 var currentTimeStat = document.querySelector('.current-time-stat');
105 var bufferedStartStat = document.querySelector('.buffered-start-stat');
106 var bufferedEndStat = document.querySelector('.buffered-end-stat');
107 var seekableStartStat = document.querySelector('.seekable-start-stat');
108 var seekableEndStat = document.querySelector('.seekable-end-stat');
109 var videoBitrateState = document.querySelector('.video-bitrate-stat');
110 var measuredBitrateStat = document.querySelector('.measured-bitrate-stat');
111
112 player.on('timeupdate', function() {
113 currentTimeStat.textContent = player.currentTime().toFixed(1);
114 });
115
116 player.on('progress', function() {
117 var oldStart, oldEnd;
118 // buffered
119 var buffered = player.buffered();
120 if (buffered.length) {
121
122 oldStart = bufferedStartStat.textContent;
123 if (buffered.start(0).toFixed(1) !== oldStart) {
124 bufferedStartStat.textContent = buffered.start(0).toFixed(1);
125 }
126 oldEnd = bufferedEndStat.textContent;
127 if (buffered.end(0).toFixed(1) !== oldEnd) {
128 bufferedEndStat.textContent = buffered.end(0).toFixed(1);
129 }
130 }
131
132 // seekable
133 var seekable = player.seekable();
134 if (seekable && seekable.length) {
135
136 oldStart = seekableStartStat.textContent;
137 if (seekable.start(0).toFixed(1) !== oldStart) {
138 seekableStartStat.textContent = seekable.start(0).toFixed(1);
139 }
140 oldEnd = seekableEndStat.textContent;
141 if (seekable.end(0).toFixed(1) !== oldEnd) {
142 seekableEndStat.textContent = seekable.end(0).toFixed(1);
143 }
144 }
145
146 // bitrates
147 var playlist = player.hls.playlists.media();
148 if (playlist && playlist.attributes && playlist.attributes.BANDWIDTH) {
149 videoBitrateState.textContent = (playlist.attributes.BANDWIDTH / 1024).toLocaleString(undefined, {
150 maximumFractionDigits: 1
151 }) + ' kbps';
152 }
153 if (player.hls.bandwidth) {
154 measuredBitrateStat.textContent = (player.hls.bandwidth / 1024).toLocaleString(undefined, {
155 maximumFractionDigits: 1
156 }) + ' kbps';
157 }
158 });
159
160 videojs.Hls.displayStats(document.querySelector('.switching-stats'), player);
161 videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player);
162 </script>
163 </body>
164 </html>
1 .axis text,
2 .cue text {
3 font: 12px sans-serif;
4 }
5
6 .axis line,
7 .axis path,
8 .intersect {
9 fill: none;
10 stroke: #000;
11 }
12
13 .cue {
14 width: 20px;
15 height: 20px;
16 }
17 .cue text {
18 display: none;
19 }
20 .cue:hover text {
21 display: block;
22 }
23
24 .intersect {
25 fill: none;
26 stroke: #000;
27 stroke-dasharray: 2,2;
28 }
1 (function(window, videojs, undefined) {
2 'use strict';
3
4 // -------------
5 // Initial Setup
6 // -------------
7
8 var d3 = window.d3;
9
10 var setupGraph = function(element) {
11 element.innerHTML = '';
12
13 // setup the display
14 var margin = {
15 top: 20,
16 right: 80,
17 bottom: 30,
18 left: 50
19 };
20 var width = 600 - margin.left - margin.right;
21 var height = 300 - margin.top - margin.bottom;
22 var svg = d3.select(element)
23 .append('svg')
24 .attr('width', width + margin.left + margin.right)
25 .attr('height', height + margin.top + margin.bottom)
26 .append('g')
27 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
28
29 // setup the timeline
30 var x = d3.time.scale().range([0, width]); // d3.scale.linear().range([0, width]);
31 var y = d3.scale.linear().range([height, 0]);
32
33 x.domain([new Date(), new Date(Date.now() + (5 * 60 * 1000))]);
34 y.domain([0, 5 * 1024 * 1024 * 8]);
35
36 var timeAxis = d3.svg.axis().scale(x).orient('bottom');
37 var tickFormatter = d3.format(',.0f');
38 var bitrateAxis = d3.svg.axis()
39 .scale(y)
40 .tickFormat(function(value) {
41 return tickFormatter(value / 1024);
42 })
43 .orient('left');
44
45 // time axis
46 svg.selectAll('.axis').remove();
47 svg.append('g')
48 .attr('class', 'x axis')
49 .attr('transform', 'translate(0,' + height + ')')
50 .call(timeAxis);
51
52 // bitrate axis
53 svg.append('g')
54 .attr('class', 'y axis')
55 .call(bitrateAxis)
56 .append('text')
57 .attr('transform', 'rotate(-90)')
58 .attr('y', 6)
59 .attr('dy', '.71em')
60 .style('text-anchor', 'end')
61 .text('Bitrate (kb/s)');
62
63 };
64
65 // ---------------
66 // Dynamic Updates
67 // ---------------
68
69 var displayStats = function(element, player) {
70 setupGraph(element, player);
71 };
72
73 // -----------------
74 // Cue Visualization
75 // -----------------
76
77 var Playlist = videojs.Hls.Playlist;
78 var margin = {
79 top: 8,
80 right: 8,
81 bottom: 20,
82 left: 80
83 };
84 var width = 600 - margin.left - margin.right;
85 var height = 600 - margin.top - margin.bottom;
86
87 var mediaDomain = function(media, player) {
88 var segments = media.segments;
89 var end = player.hls.playlists.expiredPreDiscontinuity_;
90 end += player.hls.playlists.expiredPostDiscontinuity_;
91 end += Playlist.duration(media,
92 media.mediaSequence,
93 media.mediaSequence + segments.length);
94 return [0, end];
95 };
96 var ptsDomain = function(segments, mediaScale, mediaOffset) {
97 mediaOffset = mediaOffset * 1000 || 0;
98 var start = mediaScale.domain()[0] * 1000;
99 var segment = segments[0];
100
101 if (segment &&
102 segment.minAudioPts !== undefined ||
103 segment.minVideoPts !== undefined) {
104 start = Math.min(segment.minAudioPts || Infinity,
105 segment.minVideoPts || Infinity);
106 }
107 start -= mediaOffset;
108 return [
109 start,
110 (mediaScale.domain()[1] - mediaScale.domain()[0]) * 1000 + start
111 ];
112 };
113 var svgUpdateCues = function(svg, mediaScale, ptsScale, y, cues) {
114 cues = Array.prototype.slice.call(cues).filter(function(cue) {
115 return cue.startTime > mediaScale.domain()[0] &&
116 cue.startTime < mediaScale.domain()[1];
117 });
118 var points = svg.selectAll('.cue').data(cues, function(cue) {
119 return cue.pts_ + ' -> ' + cue.startTime;
120 });
121 points.attr('transform', function(cue) {
122 return 'translate(' + mediaScale(cue.startTime) + ',' + ptsScale(cue.pts_) + ')';
123 });
124 var enter = points.enter().append('g')
125 .attr('class', 'cue');
126 enter.append('circle')
127 .attr('r', 5)
128 .attr('data-time', function(cue) {
129 return cue.startTime;
130 })
131 .attr('data-pts', function(cue) {
132 return cue.pts_;
133 });
134 enter.append('text')
135 .attr('transform', 'translate(8,0)')
136 .text(function(cue) {
137 return 'time: ' + videojs.formatTime(cue.startTime);
138 });
139 enter.append('text')
140 .attr('transform', 'translate(8,16)')
141 .text(function(cue) {
142 return 'pts: ' + cue.pts_;
143 });
144 points.exit().remove();
145 };
146 var svgUpdateAxes = function(svg, mediaScale, ptsScale) {
147 // media timeline axis
148 var mediaAxis = d3.svg.axis().scale(mediaScale).orient('bottom');
149 svg.select('.axis.media')
150 .transition().duration(500)
151 .call(mediaAxis);
152
153 // presentation timeline axis
154 if (!isFinite(ptsScale.domain()[0]) || !isFinite(ptsScale.domain()[1])) {
155 return;
156 }
157 var ptsAxis = d3.svg.axis().scale(ptsScale).orient('left');
158 svg.select('.axis.presentation')
159 .transition().duration(500)
160 .call(ptsAxis);
161 };
162 var svgRenderSegmentTimeline = function(container, player) {
163 var media = player.hls.playlists.media();
164 var segments = media.segments; // media.segments.slice(0, count);
165
166 // setup the display
167 var svg = d3.select(container)
168 .append('svg')
169 .attr('width', width + margin.left + margin.right)
170 .attr('height', height + margin.top + margin.bottom)
171 .append('g')
172 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
173
174 // setup the scales
175 var mediaScale = d3.scale.linear().range([0, width]);
176 mediaScale.domain(mediaDomain(media, player));
177 var ptsScale = d3.scale.linear().range([height, 0]);
178 ptsScale.domain(ptsDomain(segments, mediaScale));
179
180 // render
181 var mediaAxis = d3.svg.axis().scale(mediaScale).orient('bottom');
182 svg.append('g')
183 .attr('class', 'x axis media')
184 .attr('transform', 'translate(0,' + height + ')')
185 .call(mediaAxis);
186 var ptsAxis = d3.svg.axis().scale(ptsScale).orient('left');
187 svg.append('g')
188 .attr('class', 'y axis presentation')
189 .call(ptsAxis);
190
191 svg.append('path')
192 .attr('class', 'intersect')
193 .attr('d', 'M0,' + height + 'L' + width +',0');
194
195 var mediaOffset = 0;
196
197 // update everything on progress
198 player.on('progress', function() {
199 var updatedMedia = player.hls.playlists.media();
200 var segments = updatedMedia.segments; // updatedMedia.segments.slice(currentIndex, currentIndex + count);
201
202 if (updatedMedia.mediaSequence !== media.mediaSequence) {
203 mediaOffset += Playlist.duration(media,
204 media.mediaSequence,
205 updatedMedia.mediaSequence);
206 media = updatedMedia;
207 }
208
209 mediaScale.domain(mediaDomain(updatedMedia, player));
210 ptsScale.domain(ptsDomain(segments, mediaScale, mediaOffset));
211 svgUpdateAxes(svg, mediaScale, ptsScale, updatedMedia, segments);
212 if (!isFinite(ptsScale.domain()[0]) || !isFinite(ptsScale.domain()[1])) {
213 return;
214 }
215 for (var i = 0; i < player.textTracks().length; i++) {
216 var track = player.textTracks()[i];
217 svgUpdateCues(svg, mediaScale, ptsScale, ptsScale, track.cues);
218 }
219 });
220 };
221
222 var displayCues = function(container, player) {
223 var media = player.hls.playlists.media();
224 if (media && media.segments) {
225 svgRenderSegmentTimeline(container, player);
226 } else {
227 player.one('loadedmetadata', function() {
228 svgRenderSegmentTimeline(container, player);
229 });
230 }
231 };
232
233
234 // export
235 videojs.Hls.displayStats = displayStats;
236 videojs.Hls.displayCues = displayCues;
237
238 })(window, window.videojs);
...@@ -50,10 +50,19 @@ var ...@@ -50,10 +50,19 @@ var
50 }; 50 };
51 51
52 tech = player.el().querySelector('.vjs-tech'); 52 tech = player.el().querySelector('.vjs-tech');
53 tech.vjs_getProperty = function() {}; 53 tech.vjs_getProperty = function(name) {
54 if (name === 'paused') {
55 return this.paused_;
56 }
57 };
54 tech.vjs_setProperty = function() {}; 58 tech.vjs_setProperty = function() {};
55 tech.vjs_src = function() {}; 59 tech.vjs_src = function() {};
56 tech.vjs_play = function() {}; 60 tech.vjs_play = function() {
61 this.paused_ = false;
62 };
63 tech.vjs_pause = function() {
64 this.paused_ = true;
65 };
57 tech.vjs_discontinuity = function() {}; 66 tech.vjs_discontinuity = function() {};
58 videojs.Flash.onReady(tech.id); 67 videojs.Flash.onReady(tech.id);
59 68
...@@ -226,6 +235,46 @@ test('starts playing if autoplay is specified', function() { ...@@ -226,6 +235,46 @@ test('starts playing if autoplay is specified', function() {
226 strictEqual(1, plays, 'play was called'); 235 strictEqual(1, plays, 'play was called');
227 }); 236 });
228 237
238 test('autoplay seeks to the live point after playlist load', function() {
239 var currentTime = 0;
240 player.options().autoplay = true;
241 player.hls.setCurrentTime = function(time) {
242 currentTime = time;
243 return currentTime;
244 };
245 player.hls.currentTime = function() {
246 return currentTime;
247 };
248 player.src({
249 src: 'liveStart30sBefore.m3u8',
250 type: 'application/vnd.apple.mpegurl'
251 });
252 openMediaSource(player);
253 standardXHRResponse(requests.shift());
254
255 notEqual(currentTime, 0, 'seeked on autoplay');
256 });
257
258 test('autoplay seeks to the live point after media source open', function() {
259 var currentTime = 0;
260 player.options().autoplay = true;
261 player.hls.setCurrentTime = function(time) {
262 currentTime = time;
263 return currentTime;
264 };
265 player.hls.currentTime = function() {
266 return currentTime;
267 };
268 player.src({
269 src: 'liveStart30sBefore.m3u8',
270 type: 'application/vnd.apple.mpegurl'
271 });
272 standardXHRResponse(requests.shift());
273 openMediaSource(player);
274
275 notEqual(currentTime, 0, 'seeked on autoplay');
276 });
277
229 test('creates a PlaylistLoader on init', function() { 278 test('creates a PlaylistLoader on init', function() {
230 var loadedmetadata = false; 279 var loadedmetadata = false;
231 player.on('loadedmetadata', function() { 280 player.on('loadedmetadata', function() {
...@@ -1424,6 +1473,73 @@ test('translates ID3 PTS values to cue media timeline positions', function() { ...@@ -1424,6 +1473,73 @@ test('translates ID3 PTS values to cue media timeline positions', function() {
1424 equal(track.cues[0].endTime, 1, 'translated startTime'); 1473 equal(track.cues[0].endTime, 1, 'translated startTime');
1425 }); 1474 });
1426 1475
1476 test('translates ID3 PTS values with expired segments', function() {
1477 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1478 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1479 player.src({
1480 src: 'live.m3u8',
1481 type: 'application/vnd.apple.mpegurl'
1482 });
1483 openMediaSource(player);
1484 player.play();
1485
1486 // 20.9 seconds of content have expired
1487 player.hls.playlists.expiredPostDiscontinuity_ = 20.9;
1488
1489 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1490 // trigger a metadata event
1491 player.hls.segmentParser_.metadataStream.trigger('data', {
1492 pts: 5 * 1000,
1493 data: new Uint8Array([]),
1494 frames: [{
1495 id: 'TXXX',
1496 value: 'cue text'
1497 }]
1498 });
1499 };
1500 requests.shift().respond(200, null,
1501 '#EXTM3U\n' +
1502 '#EXT-X-MEDIA-SEQUENCE:2\n' +
1503 '#EXTINF:10,\n' +
1504 '2.ts\n' +
1505 '#EXTINF:10,\n' +
1506 '3.ts\n'); // media
1507 standardXHRResponse(requests.shift()); // segment 0
1508
1509 track = player.textTracks()[0];
1510 equal(track.cues[0].startTime, 20.9 + 1, 'translated startTime');
1511 equal(track.cues[0].endTime, 20.9 + 1, 'translated startTime');
1512 });
1513
1514 test('translates id3 PTS values for audio-only media', function() {
1515 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1516 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1517 player.src({
1518 src: 'manifest/media.m3u8',
1519 type: 'application/vnd.apple.mpegurl'
1520 });
1521 openMediaSource(player);
1522
1523 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1524 // trigger a metadata event
1525 player.hls.segmentParser_.metadataStream.trigger('data', {
1526 pts: 5 * 1000,
1527 data: new Uint8Array([]),
1528 frames: [{
1529 id: 'TXXX',
1530 value: 'cue text'
1531 }]
1532 });
1533 };
1534 player.hls.segmentParser_.stats.h264Tags = function() { return 0; };
1535 player.hls.segmentParser_.stats.minVideoPts = null;
1536 standardXHRResponse(requests.shift()); // media
1537 standardXHRResponse(requests.shift()); // segment 0
1538
1539 track = player.textTracks()[0];
1540 equal(track.cues[0].startTime, 1, 'translated startTime');
1541 });
1542
1427 test('translates ID3 PTS values across discontinuities', function() { 1543 test('translates ID3 PTS values across discontinuities', function() {
1428 var tags = [], events = [], track; 1544 var tags = [], events = [], track;
1429 videojs.Hls.SegmentParser = mockSegmentParser(tags); 1545 videojs.Hls.SegmentParser = mockSegmentParser(tags);
...@@ -1699,6 +1815,7 @@ test('live playlist starts three target durations before live', function() { ...@@ -1699,6 +1815,7 @@ test('live playlist starts three target durations before live', function() {
1699 equal(player.hls.mediaIndex, 0, 'waits for the first play to start buffering'); 1815 equal(player.hls.mediaIndex, 0, 'waits for the first play to start buffering');
1700 equal(requests.length, 0, 'no outstanding segment request'); 1816 equal(requests.length, 0, 'no outstanding segment request');
1701 1817
1818 player.hls.paused = function() { return false; };
1702 player.play(); 1819 player.play();
1703 mediaPlaylist = player.hls.playlists.media(); 1820 mediaPlaylist = player.hls.playlists.media();
1704 equal(player.hls.mediaIndex, 1, 'mediaIndex is updated at play'); 1821 equal(player.hls.mediaIndex, 1, 'mediaIndex is updated at play');
...@@ -1758,7 +1875,7 @@ test('resets the time to a seekable position when resuming a live stream ' + ...@@ -1758,7 +1875,7 @@ test('resets the time to a seekable position when resuming a live stream ' +
1758 '16.ts\n'); 1875 '16.ts\n');
1759 // mock out the player to simulate a live stream that has been 1876 // mock out the player to simulate a live stream that has been
1760 // playing for awhile 1877 // playing for awhile
1761 player.addClass('vjs-has-started'); 1878 player.hls.hasPlayed_ = true;
1762 player.hls.seekable = function() { 1879 player.hls.seekable = function() {
1763 return { 1880 return {
1764 start: function() { 1881 start: function() {
...@@ -1766,7 +1883,8 @@ test('resets the time to a seekable position when resuming a live stream ' + ...@@ -1766,7 +1883,8 @@ test('resets the time to a seekable position when resuming a live stream ' +
1766 }, 1883 },
1767 end: function() { 1884 end: function() {
1768 return 170; 1885 return 170;
1769 } 1886 },
1887 length: 1
1770 }; 1888 };
1771 }; 1889 };
1772 player.hls.currentTime = function() { 1890 player.hls.currentTime = function() {
...@@ -1780,12 +1898,6 @@ test('resets the time to a seekable position when resuming a live stream ' + ...@@ -1780,12 +1898,6 @@ test('resets the time to a seekable position when resuming a live stream ' +
1780 1898
1781 player.play(); 1899 player.play();
1782 equal(seekTarget, player.seekable().start(0), 'seeked to the start of seekable'); 1900 equal(seekTarget, player.seekable().start(0), 'seeked to the start of seekable');
1783
1784 player.hls.currentTime = function() {
1785 return 180;
1786 };
1787 player.play();
1788 equal(seekTarget, player.seekable().end(0), 'seeked to the end of seekable');
1789 }); 1901 });
1790 1902
1791 test('clamps seeks to the seekable window', function() { 1903 test('clamps seeks to the seekable window', function() {
...@@ -2015,6 +2127,18 @@ test('clears the segment buffer on seek', function() { ...@@ -2015,6 +2127,18 @@ test('clears the segment buffer on seek', function() {
2015 strictEqual(aborts, 1, 'cleared the segment buffer on a seek'); 2127 strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
2016 }); 2128 });
2017 2129
2130 test('can seek before the source buffer opens', function() {
2131 player.src({
2132 src: 'media.m3u8',
2133 type: 'application/vnd.apple.mpegurl'
2134 });
2135 standardXHRResponse(requests.shift());
2136 player.triggerReady();
2137
2138 player.currentTime(1);
2139 equal(player.currentTime(), 1, 'seeked');
2140 });
2141
2018 test('continues playing after seek to discontinuity', function() { 2142 test('continues playing after seek to discontinuity', function() {
2019 var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime; 2143 var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime;
2020 2144
......