autoplay at the live point. fix live id3 cue insertion.. Closes #353
Showing
8 changed files
with
652 additions
and
56 deletions
... | @@ -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 | } | ... | ... |
test/stats/index.html
0 → 100644
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> |
test/stats/stats.css
0 → 100644
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 | } |
test/stats/stats.js
0 → 100644
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 | ... | ... |
-
Please register or sign in to post a comment