Merge branch 'master' into development
Showing
5 changed files
with
387 additions
and
37 deletions
... | @@ -16,3 +16,10 @@ notifications: | ... | @@ -16,3 +16,10 @@ notifications: |
16 | before_script: | 16 | before_script: |
17 | - export DISPLAY=:99.0 | 17 | - export DISPLAY=:99.0 |
18 | - sh -e /etc/init.d/xvfb start | 18 | - sh -e /etc/init.d/xvfb start |
19 | env: | ||
20 | global: | ||
21 | - secure: dM7svnHPPu5IiUMeFWW5zg+iuWNpwt6SSDi3MmVvhSclNMRLesQoRB+7Qq5J/LiKhmjpv1/GlNVV0CTsHMRhZNwQ3fo38eEuTXv99aAflEITXwSEh/VntKViHbGFubn06EnVkJoH6MX3zJ6kbiwc2QdSQbywKzS6l6quUEpWpd0= | ||
22 | - secure: AnduYGXka5ft1x7V3SuVYqvlKLvJGhUaRNFdy4UDJr3ZVuwpQjE4TMDG8REmJIJvXfHbh4qY4N1cFSGnXkZ4bH21Xk0v9DLhsxbarKz+X2BvPgXs+Af9EQ6vLEy/5S1vMLxfT5+y+Ec5bVNGOsdUZby8Y21CRzSg6ADN9kwPGlE= | ||
23 | addons: | ||
24 | sauce_connect: true | ||
25 | firefox: latest | ... | ... |
1 | { | 1 | { |
2 | "name": "videojs-contrib-hls", | 2 | "name": "videojs-contrib-hls", |
3 | "version": "1.3.5", | 3 | "version": "1.3.9", |
4 | "description": "Play back HLS with video.js, even where it's not natively supported", | 4 | "description": "Play back HLS with video.js, even where it's not natively supported", |
5 | "main": "es5/videojs-contrib-hls.js", | 5 | "main": "es5/videojs-contrib-hls.js", |
6 | "engines": { | 6 | "engines": { | ... | ... |
... | @@ -3,6 +3,16 @@ | ... | @@ -3,6 +3,16 @@ |
3 | */ | 3 | */ |
4 | import {createTimeRange} from 'video.js'; | 4 | import {createTimeRange} from 'video.js'; |
5 | 5 | ||
6 | let Playlist = { | ||
7 | /** | ||
8 | * The number of segments that are unsafe to start playback at in | ||
9 | * a live stream. Changing this value can cause playback stalls. | ||
10 | * See HTTP Live Streaming, "Playing the Media Playlist File" | ||
11 | * https://tools.ietf.org/html/draft-pantos-http-live-streaming-18#section-6.3.3 | ||
12 | */ | ||
13 | UNSAFE_LIVE_SEGMENTS: 3 | ||
14 | }; | ||
15 | |||
6 | const backwardDuration = function(playlist, endSequence) { | 16 | const backwardDuration = function(playlist, endSequence) { |
7 | let result = 0; | 17 | let result = 0; |
8 | let i = endSequence - playlist.mediaSequence; | 18 | let i = endSequence - playlist.mediaSequence; |
... | @@ -187,12 +197,12 @@ export const seekable = function(playlist) { | ... | @@ -187,12 +197,12 @@ export const seekable = function(playlist) { |
187 | start = intervalDuration(playlist, playlist.mediaSequence); | 197 | start = intervalDuration(playlist, playlist.mediaSequence); |
188 | end = intervalDuration(playlist, | 198 | end = intervalDuration(playlist, |
189 | playlist.mediaSequence + | 199 | playlist.mediaSequence + |
190 | Math.max(0, playlist.segments.length - 3)); | 200 | Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS)); |
191 | return createTimeRange(start, end); | 201 | return createTimeRange(start, end); |
192 | }; | 202 | }; |
193 | 203 | ||
204 | Playlist.duration = duration; | ||
205 | Playlist.seekable = seekable; | ||
206 | |||
194 | // exports | 207 | // exports |
195 | export default { | 208 | export default Playlist; |
196 | duration, | ||
197 | seekable | ||
198 | }; | ... | ... |
... | @@ -197,6 +197,88 @@ const keyFailed = function(key) { | ... | @@ -197,6 +197,88 @@ const keyFailed = function(key) { |
197 | return key.retries && key.retries >= 2; | 197 | return key.retries && key.retries >= 2; |
198 | }; | 198 | }; |
199 | 199 | ||
200 | /** | ||
201 | * Returns the CSS value for the specified property on an element | ||
202 | * using `getComputedStyle`. Firefox has a long-standing issue where | ||
203 | * getComputedStyle() may return null when running in an iframe with | ||
204 | * `display: none`. | ||
205 | * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 | ||
206 | */ | ||
207 | const safeGetComputedStyle = function(el, property) { | ||
208 | let result; | ||
209 | |||
210 | if (!el) { | ||
211 | return ''; | ||
212 | } | ||
213 | |||
214 | result = getComputedStyle(el); | ||
215 | if (!result) { | ||
216 | return ''; | ||
217 | } | ||
218 | |||
219 | return result[property]; | ||
220 | }; | ||
221 | |||
222 | /** | ||
223 | * Updates segment with information about its end-point in time and, optionally, | ||
224 | * the segment duration if we have enough information to determine a segment duration | ||
225 | * accurately. | ||
226 | * @param playlist {object} a media playlist object | ||
227 | * @param segmentIndex {number} the index of segment we last appended | ||
228 | * @param segmentEnd {number} the known of the segment referenced by segmentIndex | ||
229 | */ | ||
230 | const updateSegmentMetadata = function(playlist, segmentIndex, segmentEnd) { | ||
231 | if (!playlist) { | ||
232 | return; | ||
233 | } | ||
234 | |||
235 | let segment = playlist.segments[segmentIndex]; | ||
236 | let previousSegment = playlist.segments[segmentIndex - 1]; | ||
237 | |||
238 | if (segmentEnd && segment) { | ||
239 | segment.end = segmentEnd; | ||
240 | |||
241 | // fix up segment durations based on segment end data | ||
242 | if (!previousSegment) { | ||
243 | // first segment is always has a start time of 0 making its duration | ||
244 | // equal to the segment end | ||
245 | segment.duration = segment.end; | ||
246 | } else if (previousSegment.end) { | ||
247 | segment.duration = segment.end - previousSegment.end; | ||
248 | } | ||
249 | } | ||
250 | }; | ||
251 | |||
252 | /** | ||
253 | * Determines if we should call endOfStream on the media source based on the state | ||
254 | * of the buffer or if appened segment was the final segment in the playlist. | ||
255 | * @param playlist {object} a media playlist object | ||
256 | * @param mediaSource {object} the MediaSource object | ||
257 | * @param segmentIndex {number} the index of segment we last appended | ||
258 | * @param currentBuffered {object} the buffered region that currentTime resides in | ||
259 | * @return {boolean} whether the calling function should call endOfStream on the MediaSource | ||
260 | */ | ||
261 | const detectEndOfStream = function(playlist, mediaSource, segmentIndex, currentBuffered) { | ||
262 | if (!playlist) { | ||
263 | return false; | ||
264 | } | ||
265 | |||
266 | let segments = playlist.segments; | ||
267 | |||
268 | // determine a few boolean values to help make the branch below easier | ||
269 | // to read | ||
270 | let appendedLastSegment = (segmentIndex === segments.length - 1); | ||
271 | let bufferedToEnd = (currentBuffered.length && | ||
272 | segments[segments.length - 1].end <= currentBuffered.end(0)); | ||
273 | |||
274 | // if we've buffered to the end of the video, we need to call endOfStream | ||
275 | // so that MediaSources can trigger the `ended` event when it runs out of | ||
276 | // buffered data instead of waiting for me | ||
277 | return playlist.endList && | ||
278 | mediaSource.readyState === 'open' && | ||
279 | (appendedLastSegment || bufferedToEnd); | ||
280 | }; | ||
281 | |||
200 | const parseCodecs = function(codecs) { | 282 | const parseCodecs = function(codecs) { |
201 | let result = { | 283 | let result = { |
202 | codecCount: 0, | 284 | codecCount: 0, |
... | @@ -592,10 +674,15 @@ export default class HlsHandler extends Component { | ... | @@ -592,10 +674,15 @@ export default class HlsHandler extends Component { |
592 | duration() { | 674 | duration() { |
593 | let playlists = this.playlists; | 675 | let playlists = this.playlists; |
594 | 676 | ||
595 | if (playlists) { | 677 | if (!playlists) { |
596 | return Hls.Playlist.duration(playlists.media()); | 678 | return 0; |
679 | } | ||
680 | |||
681 | if (this.mediaSource) { | ||
682 | return this.mediaSource.duration; | ||
597 | } | 683 | } |
598 | return 0; | 684 | |
685 | return Hls.Playlist.duration(playlists.media()); | ||
599 | } | 686 | } |
600 | 687 | ||
601 | seekable() { | 688 | seekable() { |
... | @@ -635,6 +722,7 @@ export default class HlsHandler extends Component { | ... | @@ -635,6 +722,7 @@ export default class HlsHandler extends Component { |
635 | updateDuration(playlist) { | 722 | updateDuration(playlist) { |
636 | let oldDuration = this.mediaSource.duration; | 723 | let oldDuration = this.mediaSource.duration; |
637 | let newDuration = Hls.Playlist.duration(playlist); | 724 | let newDuration = Hls.Playlist.duration(playlist); |
725 | let buffered = this.tech_.buffered(); | ||
638 | let setDuration = () => { | 726 | let setDuration = () => { |
639 | this.mediaSource.duration = newDuration; | 727 | this.mediaSource.duration = newDuration; |
640 | this.tech_.trigger('durationchange'); | 728 | this.tech_.trigger('durationchange'); |
... | @@ -642,6 +730,10 @@ export default class HlsHandler extends Component { | ... | @@ -642,6 +730,10 @@ export default class HlsHandler extends Component { |
642 | this.mediaSource.removeEventListener('sourceopen', setDuration); | 730 | this.mediaSource.removeEventListener('sourceopen', setDuration); |
643 | }; | 731 | }; |
644 | 732 | ||
733 | if (buffered.length > 0) { | ||
734 | newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); | ||
735 | } | ||
736 | |||
645 | // if the duration has changed, invalidate the cached value | 737 | // if the duration has changed, invalidate the cached value |
646 | if (oldDuration !== newDuration) { | 738 | if (oldDuration !== newDuration) { |
647 | // update the duration | 739 | // update the duration |
... | @@ -767,8 +859,8 @@ export default class HlsHandler extends Component { | ... | @@ -767,8 +859,8 @@ export default class HlsHandler extends Component { |
767 | // (this could be the lowest bitrate rendition as we go through all of them above) | 859 | // (this could be the lowest bitrate rendition as we go through all of them above) |
768 | variant = null; | 860 | variant = null; |
769 | 861 | ||
770 | width = parseInt(getComputedStyle(this.tech_.el()).width, 10); | 862 | width = parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10); |
771 | height = parseInt(getComputedStyle(this.tech_.el()).height, 10); | 863 | height = parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10); |
772 | 864 | ||
773 | // iterate through the bandwidth-filtered playlists and find | 865 | // iterate through the bandwidth-filtered playlists and find |
774 | // best rendition by player dimension | 866 | // best rendition by player dimension |
... | @@ -1094,6 +1186,7 @@ export default class HlsHandler extends Component { | ... | @@ -1094,6 +1186,7 @@ export default class HlsHandler extends Component { |
1094 | let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; | 1186 | let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; |
1095 | let removeToTime = 0; | 1187 | let removeToTime = 0; |
1096 | let seekable = this.seekable(); | 1188 | let seekable = this.seekable(); |
1189 | let currentTime = this.tech_.currentTime(); | ||
1097 | 1190 | ||
1098 | // Chrome has a hard limit of 150mb of | 1191 | // Chrome has a hard limit of 150mb of |
1099 | // buffer and a very conservative "garbage collector" | 1192 | // buffer and a very conservative "garbage collector" |
... | @@ -1103,10 +1196,10 @@ export default class HlsHandler extends Component { | ... | @@ -1103,10 +1196,10 @@ export default class HlsHandler extends Component { |
1103 | if (this.sourceBuffer && !this.sourceBuffer.updating) { | 1196 | if (this.sourceBuffer && !this.sourceBuffer.updating) { |
1104 | // If we have a seekable range use that as the limit for what can be removed safely | 1197 | // If we have a seekable range use that as the limit for what can be removed safely |
1105 | // otherwise remove anything older than 1 minute before the current play head | 1198 | // otherwise remove anything older than 1 minute before the current play head |
1106 | if (seekable.length && seekable.start(0) > 0) { | 1199 | if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) { |
1107 | removeToTime = seekable.start(0); | 1200 | removeToTime = seekable.start(0); |
1108 | } else { | 1201 | } else { |
1109 | removeToTime = this.tech_.currentTime() - 60; | 1202 | removeToTime = currentTime - 60; |
1110 | } | 1203 | } |
1111 | 1204 | ||
1112 | if (removeToTime > 0) { | 1205 | if (removeToTime > 0) { |
... | @@ -1260,37 +1353,43 @@ export default class HlsHandler extends Component { | ... | @@ -1260,37 +1353,43 @@ export default class HlsHandler extends Component { |
1260 | 1353 | ||
1261 | updateEndHandler_() { | 1354 | updateEndHandler_() { |
1262 | let segmentInfo = this.pendingSegment_; | 1355 | let segmentInfo = this.pendingSegment_; |
1263 | let segment; | ||
1264 | let segments; | ||
1265 | let playlist; | 1356 | let playlist; |
1266 | let currentMediaIndex; | 1357 | let currentMediaIndex; |
1267 | let currentBuffered; | 1358 | let currentBuffered; |
1268 | let seekable; | 1359 | let seekable; |
1269 | let timelineUpdate; | 1360 | let timelineUpdate; |
1270 | 1361 | let isEndOfStream; | |
1271 | this.pendingSegment_ = null; | ||
1272 | 1362 | ||
1273 | // stop here if the update errored or was aborted | 1363 | // stop here if the update errored or was aborted |
1274 | if (!segmentInfo) { | 1364 | if (!segmentInfo) { |
1365 | this.pendingSegment_ = null; | ||
1366 | return; | ||
1367 | } | ||
1368 | |||
1369 | // In Firefox, the updateend event is triggered for both removing from the buffer and | ||
1370 | // adding to the buffer. To prevent this code from executing on removals, we wait for | ||
1371 | // segmentInfo to have a filled in buffered value before we continue processing. | ||
1372 | if (!segmentInfo.buffered) { | ||
1275 | return; | 1373 | return; |
1276 | } | 1374 | } |
1277 | 1375 | ||
1278 | playlist = this.playlists.media(); | 1376 | this.pendingSegment_ = null; |
1279 | segments = playlist.segments; | 1377 | |
1378 | playlist = segmentInfo.playlist; | ||
1280 | currentMediaIndex = segmentInfo.mediaIndex + | 1379 | currentMediaIndex = segmentInfo.mediaIndex + |
1281 | (segmentInfo.mediaSequence - playlist.mediaSequence); | 1380 | (segmentInfo.mediaSequence - playlist.mediaSequence); |
1282 | currentBuffered = this.findBufferedRange_(); | 1381 | currentBuffered = this.findBufferedRange_(); |
1382 | isEndOfStream = detectEndOfStream(playlist, this.mediaSource, currentMediaIndex, currentBuffered); | ||
1283 | 1383 | ||
1284 | // if we switched renditions don't try to add segment timeline | 1384 | // if we switched renditions don't try to add segment timeline |
1285 | // information to the playlist | 1385 | // information to the playlist |
1286 | if (segmentInfo.playlist.uri !== this.playlists.media().uri) { | 1386 | if (segmentInfo.playlist.uri !== this.playlists.media().uri) { |
1387 | if (isEndOfStream) { | ||
1388 | return this.mediaSource.endOfStream(); | ||
1389 | } | ||
1287 | return this.fillBuffer(); | 1390 | return this.fillBuffer(); |
1288 | } | 1391 | } |
1289 | 1392 | ||
1290 | // annotate the segment with any start and end time information | ||
1291 | // added by the media processing | ||
1292 | segment = playlist.segments[currentMediaIndex]; | ||
1293 | |||
1294 | // when seeking to the beginning of the seekable range, it's | 1393 | // when seeking to the beginning of the seekable range, it's |
1295 | // possible that imprecise timing information may cause the seek to | 1394 | // possible that imprecise timing information may cause the seek to |
1296 | // end up earlier than the start of the range | 1395 | // end up earlier than the start of the range |
... | @@ -1313,17 +1412,13 @@ export default class HlsHandler extends Component { | ... | @@ -1313,17 +1412,13 @@ export default class HlsHandler extends Component { |
1313 | timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, | 1412 | timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, |
1314 | this.tech_.buffered()); | 1413 | this.tech_.buffered()); |
1315 | 1414 | ||
1316 | if (timelineUpdate && segment) { | 1415 | // Update segment meta-data (duration and end-point) based on timeline |
1317 | segment.end = timelineUpdate; | 1416 | updateSegmentMetadata(playlist, currentMediaIndex, timelineUpdate); |
1318 | } | ||
1319 | 1417 | ||
1320 | // if we've buffered to the end of the video, let the MediaSource know | 1418 | // If we decide to signal the end of stream, then we can return instead |
1321 | if (this.playlists.media().endList && | 1419 | // of trying to fetch more segments |
1322 | currentBuffered.length && | 1420 | if (isEndOfStream) { |
1323 | segments[segments.length - 1].end <= currentBuffered.end(0) && | 1421 | return this.mediaSource.endOfStream(); |
1324 | this.mediaSource.readyState === 'open') { | ||
1325 | this.mediaSource.endOfStream(); | ||
1326 | return; | ||
1327 | } | 1422 | } |
1328 | 1423 | ||
1329 | if (timelineUpdate !== null || | 1424 | if (timelineUpdate !== null || | ... | ... |
... | @@ -436,7 +436,7 @@ QUnit.test('duration is set when the source opens after the playlist is loaded', | ... | @@ -436,7 +436,7 @@ QUnit.test('duration is set when the source opens after the playlist is loaded', |
436 | 'set the duration'); | 436 | 'set the duration'); |
437 | }); | 437 | }); |
438 | 438 | ||
439 | QUnit.test('calls `remove` on sourceBuffer to when loading a live segment', function() { | 439 | QUnit.test('calls `remove` based on seekable when loading a live segment', function() { |
440 | let removes = []; | 440 | let removes = []; |
441 | let seekable = videojs.createTimeRanges([[60, 120]]); | 441 | let seekable = videojs.createTimeRanges([[60, 120]]); |
442 | 442 | ||
... | @@ -487,7 +487,59 @@ QUnit.test('calls `remove` on sourceBuffer to when loading a live segment', func | ... | @@ -487,7 +487,59 @@ QUnit.test('calls `remove` on sourceBuffer to when loading a live segment', func |
487 | 'remove called with the right range'); | 487 | 'remove called with the right range'); |
488 | }); | 488 | }); |
489 | 489 | ||
490 | QUnit.test('calls `remove` on sourceBuffer to when loading a vod segment', function() { | 490 | QUnit.test('calls `remove` based on currentTime when loading a live segment ' + |
491 | 'if seekable start is after currentTime', function() { | ||
492 | let removes = []; | ||
493 | let seekable = videojs.createTimeRanges([[0, 80]]); | ||
494 | |||
495 | this.player.src({ | ||
496 | src: 'liveStart30sBefore.m3u8', | ||
497 | type: 'application/vnd.apple.mpegurl' | ||
498 | }); | ||
499 | openMediaSource(this.player, this.clock); | ||
500 | this.player.tech_.hls.seekable = function() { | ||
501 | return seekable; | ||
502 | }; | ||
503 | |||
504 | openMediaSource(this.player, this.clock); | ||
505 | this.player.tech_.hls.mediaSource.addSourceBuffer = function() { | ||
506 | return new (videojs.extend(videojs.EventTarget, { | ||
507 | constructor() {}, | ||
508 | abort() {}, | ||
509 | buffered: videojs.createTimeRange(), | ||
510 | appendBuffer() {}, | ||
511 | remove(start, end) { | ||
512 | removes.push([start, end]); | ||
513 | } | ||
514 | }))(); | ||
515 | }; | ||
516 | this.player.tech_.hls.bandwidth = 20e10; | ||
517 | this.player.tech_.triggerReady(); | ||
518 | standardXHRResponse(this.requests[0]); | ||
519 | this.player.tech_.hls.playlists.trigger('loadedmetadata'); | ||
520 | this.player.tech_.trigger('canplay'); | ||
521 | |||
522 | this.player.tech_.paused = function() { | ||
523 | return false; | ||
524 | }; | ||
525 | |||
526 | this.player.tech_.readyState = function() { | ||
527 | return 1; | ||
528 | }; | ||
529 | |||
530 | this.player.tech_.trigger('play'); | ||
531 | this.clock.tick(1); | ||
532 | // Change seekable so that it starts *after* the currentTime which was set | ||
533 | // based on the previous seekable range (the end of 80) | ||
534 | seekable = videojs.createTimeRanges([[100, 120]]); | ||
535 | standardXHRResponse(this.requests[1]); | ||
536 | |||
537 | QUnit.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8', 'master playlist requested'); | ||
538 | QUnit.equal(removes.length, 1, 'remove called'); | ||
539 | QUnit.deepEqual(removes[0], [0, 80 - 60], 'remove called with the right range'); | ||
540 | }); | ||
541 | |||
542 | QUnit.test('calls `remove` based on currentTime when loading a vod segment', function() { | ||
491 | let removes = []; | 543 | let removes = []; |
492 | 544 | ||
493 | this.player.src({ | 545 | this.player.src({ |
... | @@ -2268,6 +2320,81 @@ QUnit.test('tracks segment end times as they are buffered', function() { | ... | @@ -2268,6 +2320,81 @@ QUnit.test('tracks segment end times as they are buffered', function() { |
2268 | QUnit.equal(this.player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration'); | 2320 | QUnit.equal(this.player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration'); |
2269 | }); | 2321 | }); |
2270 | 2322 | ||
2323 | QUnit.test('updates first segment duration as it is buffered', function() { | ||
2324 | let bufferEnd = 0; | ||
2325 | |||
2326 | this.player.src({ | ||
2327 | src: 'media.m3u8', | ||
2328 | type: 'application/x-mpegURL' | ||
2329 | }); | ||
2330 | openMediaSource(this.player, this.clock); | ||
2331 | |||
2332 | // as new segments are downloaded, the buffer end is updated | ||
2333 | this.player.tech_.buffered = function() { | ||
2334 | return videojs.createTimeRange(0, bufferEnd); | ||
2335 | }; | ||
2336 | this.requests.shift().respond(200, null, | ||
2337 | '#EXTM3U\n' + | ||
2338 | '#EXTINF:10,\n' + | ||
2339 | '0.ts\n' + | ||
2340 | '#EXTINF:10,\n' + | ||
2341 | '1.ts\n' + | ||
2342 | '#EXT-X-ENDLIST\n'); | ||
2343 | |||
2344 | // 0.ts is shorter than advertised | ||
2345 | standardXHRResponse(this.requests.shift()); | ||
2346 | QUnit.equal(this.player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8'); | ||
2347 | QUnit.equal(this.player.tech_.hls.playlists.media().segments[0].duration, 10, | ||
2348 | 'segment duration initially based on playlist'); | ||
2349 | |||
2350 | bufferEnd = 9.5; | ||
2351 | this.player.tech_.hls.sourceBuffer.trigger('update'); | ||
2352 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2353 | QUnit.equal(this.player.tech_.hls.playlists.media().segments[0].duration, 9.5, | ||
2354 | 'updated segment duration'); | ||
2355 | }); | ||
2356 | |||
2357 | QUnit.test('updates segment durations as they are buffered', function() { | ||
2358 | let bufferEnd = 0; | ||
2359 | |||
2360 | this.player.src({ | ||
2361 | src: 'media.m3u8', | ||
2362 | type: 'application/x-mpegURL' | ||
2363 | }); | ||
2364 | openMediaSource(this.player, this.clock); | ||
2365 | |||
2366 | // as new segments are downloaded, the buffer end is updated | ||
2367 | this.player.tech_.buffered = function() { | ||
2368 | return videojs.createTimeRange(0, bufferEnd); | ||
2369 | }; | ||
2370 | this.requests.shift().respond(200, null, | ||
2371 | '#EXTM3U\n' + | ||
2372 | '#EXTINF:10,\n' + | ||
2373 | '0.ts\n' + | ||
2374 | '#EXTINF:10,\n' + | ||
2375 | '1.ts\n' + | ||
2376 | '#EXT-X-ENDLIST\n'); | ||
2377 | |||
2378 | // 0.ts is shorter than advertised | ||
2379 | standardXHRResponse(this.requests.shift()); | ||
2380 | QUnit.equal(this.player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8'); | ||
2381 | |||
2382 | QUnit.equal(this.player.tech_.hls.playlists.media().segments[1].duration, 10, | ||
2383 | 'segment duration initially based on playlist'); | ||
2384 | |||
2385 | bufferEnd = 9.5; | ||
2386 | this.player.tech_.hls.sourceBuffer.trigger('update'); | ||
2387 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2388 | this.clock.tick(1); | ||
2389 | standardXHRResponse(this.requests.shift()); | ||
2390 | bufferEnd = 19; | ||
2391 | this.player.tech_.hls.sourceBuffer.trigger('update'); | ||
2392 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2393 | |||
2394 | QUnit.equal(this.player.tech_.hls.playlists.media().segments[1].duration, 9.5, | ||
2395 | 'updated segment duration'); | ||
2396 | }); | ||
2397 | |||
2271 | QUnit.skip('seeking does not fail when targeted between segments', function() { | 2398 | QUnit.skip('seeking does not fail when targeted between segments', function() { |
2272 | let currentTime; | 2399 | let currentTime; |
2273 | let segmentUrl; | 2400 | let segmentUrl; |
... | @@ -2576,7 +2703,7 @@ QUnit.test('can be disposed before finishing initialization', function() { | ... | @@ -2576,7 +2703,7 @@ QUnit.test('can be disposed before finishing initialization', function() { |
2576 | } | 2703 | } |
2577 | }); | 2704 | }); |
2578 | 2705 | ||
2579 | QUnit.test('calls ended() on the media source at the end of a playlist', function() { | 2706 | QUnit.test('calls endOfStream on the media source after appending the last segment', function() { |
2580 | let endOfStreams = 0; | 2707 | let endOfStreams = 0; |
2581 | let buffered = [[]]; | 2708 | let buffered = [[]]; |
2582 | 2709 | ||
... | @@ -2591,11 +2718,15 @@ QUnit.test('calls ended() on the media source at the end of a playlist', functio | ... | @@ -2591,11 +2718,15 @@ QUnit.test('calls ended() on the media source at the end of a playlist', functio |
2591 | this.player.tech_.hls.mediaSource.endOfStream = function() { | 2718 | this.player.tech_.hls.mediaSource.endOfStream = function() { |
2592 | endOfStreams++; | 2719 | endOfStreams++; |
2593 | }; | 2720 | }; |
2721 | this.player.currentTime(20); | ||
2722 | this.clock.tick(1); | ||
2594 | // playlist response | 2723 | // playlist response |
2595 | this.requests.shift().respond(200, null, | 2724 | this.requests.shift().respond(200, null, |
2596 | '#EXTM3U\n' + | 2725 | '#EXTM3U\n' + |
2597 | '#EXTINF:10,\n' + | 2726 | '#EXTINF:10,\n' + |
2598 | '0.ts\n' + | 2727 | '0.ts\n' + |
2728 | '#EXTINF:10,\n' + | ||
2729 | '1.ts\n' + | ||
2599 | '#EXT-X-ENDLIST\n'); | 2730 | '#EXT-X-ENDLIST\n'); |
2600 | // segment response | 2731 | // segment response |
2601 | this.requests[0].response = new ArrayBuffer(17); | 2732 | this.requests[0].response = new ArrayBuffer(17); |
... | @@ -2604,7 +2735,52 @@ QUnit.test('calls ended() on the media source at the end of a playlist', functio | ... | @@ -2604,7 +2735,52 @@ QUnit.test('calls ended() on the media source at the end of a playlist', functio |
2604 | 2735 | ||
2605 | buffered = [[0, 10]]; | 2736 | buffered = [[0, 10]]; |
2606 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); | 2737 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); |
2607 | QUnit.strictEqual(endOfStreams, 1, 'ended media source'); | 2738 | QUnit.strictEqual(endOfStreams, 1, 'called endOfStream on the media source'); |
2739 | }); | ||
2740 | |||
2741 | QUnit.test('calls endOfStream on the media source when the current buffer ends at duration', function() { | ||
2742 | let endOfStreams = 0; | ||
2743 | let buffered = [[]]; | ||
2744 | |||
2745 | this.player.src({ | ||
2746 | src: 'http://example.com/media.m3u8', | ||
2747 | type: 'application/vnd.apple.mpegurl' | ||
2748 | }); | ||
2749 | openMediaSource(this.player, this.clock); | ||
2750 | this.player.tech_.buffered = function() { | ||
2751 | return videojs.createTimeRanges(buffered); | ||
2752 | }; | ||
2753 | this.player.tech_.hls.mediaSource.endOfStream = function() { | ||
2754 | endOfStreams++; | ||
2755 | }; | ||
2756 | this.player.currentTime(19); | ||
2757 | this.clock.tick(1); | ||
2758 | // playlist response | ||
2759 | this.requests.shift().respond(200, null, | ||
2760 | '#EXTM3U\n' + | ||
2761 | '#EXTINF:10,\n' + | ||
2762 | '0.ts\n' + | ||
2763 | '#EXTINF:10,\n' + | ||
2764 | '1.ts\n' + | ||
2765 | '#EXT-X-ENDLIST\n'); | ||
2766 | // segment response | ||
2767 | this.requests[0].response = new ArrayBuffer(17); | ||
2768 | this.requests.shift().respond(200, null, ''); | ||
2769 | QUnit.strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); | ||
2770 | |||
2771 | buffered = [[10, 20]]; | ||
2772 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2773 | |||
2774 | this.player.currentTime(5); | ||
2775 | this.clock.tick(1); | ||
2776 | // segment response | ||
2777 | this.requests[0].response = new ArrayBuffer(17); | ||
2778 | this.requests.shift().respond(200, null, ''); | ||
2779 | |||
2780 | buffered = [[0, 20]]; | ||
2781 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2782 | |||
2783 | QUnit.strictEqual(endOfStreams, 2, 'called endOfStream on the media source twice'); | ||
2608 | }); | 2784 | }); |
2609 | 2785 | ||
2610 | QUnit.test('calling play() at the end of a video replays', function() { | 2786 | QUnit.test('calling play() at the end of a video replays', function() { |
... | @@ -3086,6 +3262,68 @@ QUnit.test('does not download segments if preload option set to none', function( | ... | @@ -3086,6 +3262,68 @@ QUnit.test('does not download segments if preload option set to none', function( |
3086 | QUnit.equal(this.requests.length, 0, 'did not download any segments'); | 3262 | QUnit.equal(this.requests.length, 0, 'did not download any segments'); |
3087 | }); | 3263 | }); |
3088 | 3264 | ||
3265 | QUnit.test('does not process update end until buffered value has been set', function() { | ||
3266 | let drainBufferCallCount = 0; | ||
3267 | let origDrainBuffer; | ||
3268 | |||
3269 | this.player.src({ | ||
3270 | src: 'master.m3u8', | ||
3271 | type: 'application/vnd.apple.mpegurl' | ||
3272 | }); | ||
3273 | |||
3274 | openMediaSource(this.player, this.clock); | ||
3275 | origDrainBuffer = this.player.tech_.hls.drainBuffer; | ||
3276 | this.player.tech_.hls.drainBuffer = function() { | ||
3277 | drainBufferCallCount++; | ||
3278 | }; | ||
3279 | |||
3280 | // master | ||
3281 | standardXHRResponse(this.requests.shift()); | ||
3282 | // media | ||
3283 | standardXHRResponse(this.requests.shift()); | ||
3284 | |||
3285 | QUnit.equal(drainBufferCallCount, 0, 'drainBuffer not called yet'); | ||
3286 | |||
3287 | // segment | ||
3288 | standardXHRResponse(this.requests.shift()); | ||
3289 | |||
3290 | QUnit.ok(this.player.tech_.hls.pendingSegment_, 'pending segment exists'); | ||
3291 | QUnit.equal(drainBufferCallCount, 1, 'drainBuffer called'); | ||
3292 | |||
3293 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
3294 | QUnit.ok(this.player.tech_.hls.pendingSegment_, 'pending segment exists'); | ||
3295 | |||
3296 | this.player.tech_.hls.drainBuffer = origDrainBuffer; | ||
3297 | this.player.tech_.hls.drainBuffer(); | ||
3298 | QUnit.ok(this.player.tech_.hls.pendingSegment_, 'pending segment exists'); | ||
3299 | |||
3300 | this.player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
3301 | QUnit.ok(!this.player.tech_.hls.pendingSegment_, 'pending segment cleared out'); | ||
3302 | }); | ||
3303 | |||
3304 | // workaround https://bugzilla.mozilla.org/show_bug.cgi?id=548397 | ||
3305 | QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', function() { | ||
3306 | let oldGetComputedStyle = window.getComputedStyle; | ||
3307 | |||
3308 | window.getComputedStyle = function() { | ||
3309 | return null; | ||
3310 | }; | ||
3311 | |||
3312 | this.player.src({ | ||
3313 | src: 'master.m3u8', | ||
3314 | type: 'application/vnd.apple.mpegurl' | ||
3315 | }); | ||
3316 | openMediaSource(this.player, this.clock); | ||
3317 | // master | ||
3318 | standardXHRResponse(this.requests.shift()); | ||
3319 | // media | ||
3320 | standardXHRResponse(this.requests.shift()); | ||
3321 | |||
3322 | this.player.tech_.hls.selectPlaylist(); | ||
3323 | QUnit.ok(true, 'should not throw'); | ||
3324 | window.getComputedStyle = oldGetComputedStyle; | ||
3325 | }); | ||
3326 | |||
3089 | QUnit.module('Buffer Inspection'); | 3327 | QUnit.module('Buffer Inspection'); |
3090 | QUnit.test('detects time range end-point changed by updates', function() { | 3328 | QUnit.test('detects time range end-point changed by updates', function() { |
3091 | let edge; | 3329 | let edge; | ... | ... |
-
Please register or sign in to post a comment