Merge pull request #34 from videojs/feature/live-um
Experimental Live HLS support
Showing
41 changed files
with
766 additions
and
287 deletions
docs/live.md
0 → 100644
1 | # Live HLS Research | ||
2 | This document is a collection of notes on Live HLS implementations in the wild. | ||
3 | |||
4 | There are two varieties of Live HLS. In the first, playlists are | ||
5 | persistent and strictly appended to. In the alternative form, the | ||
6 | maximum number of segments in a playlist is relatively stable and an | ||
7 | old segment is removed every time a new segment becomes available. | ||
8 | |||
9 | On iOS devices, both stream types report a duration of `Infinity`. The | ||
10 | `currentTime` is equal to the amount of the stream that has been | ||
11 | played back on the device. | ||
12 | |||
13 | ## Akamai HD2 | ||
14 | |||
15 | ## OnceLIVE | ||
16 | "Sliding window" live streams. | ||
17 | |||
18 | ### Variant Playlists | ||
19 | Once variant playlists look like standard HLS variant playlists. | ||
20 | |||
21 | ### Media Playlists | ||
22 | OnceLIVE uses "sliding window" manifests for live playback. The media | ||
23 | playlists do not have an `EXT-X-ENDLIST` and don't declare a | ||
24 | `EXT-X-PLAYLIST-TYPE`. On first request, the stream media playlist | ||
25 | returned four segment URLs with a starting media sequence of one, | ||
26 | preceded by a `EXT-X-DISCONTINUITY` tag. As playback progressed, that | ||
27 | number grew to 13 segment URLs, at which point it stabilized. That | ||
28 | would equate to a steady-state 65 second window at 5 seconds per | ||
29 | segment. | ||
30 | |||
31 | OnceLive documentation is [available on the Unicorn Media | ||
32 | website](http://www.unicornmedia.com/documents/2013/02/oncelive_implementationguide.pdf). | ||
33 | |||
34 | Here's a script to quickly parse out segment URLs: | ||
35 | |||
36 | ```shell | ||
37 | curl $ONCE_MEDIA_PLAYLIST | grep '^http' | ||
38 | ``` | ||
39 | |||
40 | An example media playlist might look something like this: | ||
41 | ```m3u8 | ||
42 | #EXTM3U | ||
43 | #EXT-X-TARGETDURATION:5 | ||
44 | #EXT-X-MEDIA-SEQUENCE:3 | ||
45 | #EXTINF:5,3 | ||
46 | http://example.com/0/1/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg | ||
47 | #EXTINF:5,4 | ||
48 | http://example.com/1/2/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg | ||
49 | #EXTINF:5,5 | ||
50 | http://example.com/2/3/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg | ||
51 | #EXTINF:5,6 | ||
52 | http://example.com/3/4/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg | ||
53 | ``` | ||
54 | |||
55 | ## Zencoder Live |
... | @@ -344,13 +344,10 @@ | ... | @@ -344,13 +344,10 @@ |
344 | byterange.offset = entry.offset; | 344 | byterange.offset = entry.offset; |
345 | } | 345 | } |
346 | }, | 346 | }, |
347 | 'endlist': function() { | ||
348 | this.manifest.endList = true; | ||
349 | }, | ||
347 | 'inf': function() { | 350 | 'inf': function() { |
348 | if (!this.manifest.playlistType) { | ||
349 | this.manifest.playlistType = 'VOD'; | ||
350 | this.trigger('info', { | ||
351 | message: 'defaulting playlist type to VOD' | ||
352 | }); | ||
353 | } | ||
354 | if (!('mediaSequence' in this.manifest)) { | 351 | if (!('mediaSequence' in this.manifest)) { |
355 | this.manifest.mediaSequence = 0; | 352 | this.manifest.mediaSequence = 0; |
356 | this.trigger('info', { | 353 | this.trigger('info', { | ... | ... |
... | @@ -95,6 +95,44 @@ var | ... | @@ -95,6 +95,44 @@ var |
95 | }, | 95 | }, |
96 | 96 | ||
97 | /** | 97 | /** |
98 | * Creates and sends an XMLHttpRequest. | ||
99 | * @param options {string | object} if this argument is a string, it | ||
100 | * is intrepreted as a URL and a simple GET request is | ||
101 | * inititated. If it is an object, it should contain a `url` | ||
102 | * property that indicates the URL to request and optionally a | ||
103 | * `method` which is the type of HTTP request to send. | ||
104 | * @return {object} the XMLHttpRequest that was initiated. | ||
105 | */ | ||
106 | xhr = function(url, callback) { | ||
107 | var | ||
108 | options = { | ||
109 | method: 'GET' | ||
110 | }, | ||
111 | request; | ||
112 | if (typeof url === 'object') { | ||
113 | options = videojs.util.mergeOptions(options, url); | ||
114 | url = options.url; | ||
115 | } | ||
116 | request = new window.XMLHttpRequest(); | ||
117 | request.open(options.method, url); | ||
118 | request.onreadystatechange = function() { | ||
119 | // wait until the request completes | ||
120 | if (this.readyState !== 4) { | ||
121 | return; | ||
122 | } | ||
123 | |||
124 | // request error | ||
125 | if (this.status >= 400 || this.status === 0) { | ||
126 | return callback.call(this, true, url); | ||
127 | } | ||
128 | |||
129 | return callback.call(this, false, url); | ||
130 | }; | ||
131 | request.send(null); | ||
132 | return request; | ||
133 | }, | ||
134 | |||
135 | /** | ||
98 | * TODO - Document this great feature. | 136 | * TODO - Document this great feature. |
99 | * | 137 | * |
100 | * @param playlist | 138 | * @param playlist |
... | @@ -123,6 +161,42 @@ var | ... | @@ -123,6 +161,42 @@ var |
123 | }, | 161 | }, |
124 | 162 | ||
125 | /** | 163 | /** |
164 | * Determine the media index in one playlist that corresponds to a | ||
165 | * specified media index in another. This function can be used to | ||
166 | * calculate a new segment position when a playlist is reloaded or a | ||
167 | * variant playlist is becoming active. | ||
168 | * @param mediaIndex {number} the index into the original playlist | ||
169 | * to translate | ||
170 | * @param original {object} the playlist to translate the media | ||
171 | * index from | ||
172 | * @param update {object} the playlist to translate the media index | ||
173 | * to | ||
174 | * @param {number} the corresponding media index in the updated | ||
175 | * playlist | ||
176 | */ | ||
177 | translateMediaIndex = function(mediaIndex, original, update) { | ||
178 | var | ||
179 | i = update.segments.length, | ||
180 | originalSegment; | ||
181 | |||
182 | // no segments have been loaded from the original playlist | ||
183 | if (mediaIndex === 0) { | ||
184 | return 0; | ||
185 | } | ||
186 | |||
187 | // try to sync based on URI | ||
188 | originalSegment = original.segments[mediaIndex - 1]; | ||
189 | while (i--) { | ||
190 | if (originalSegment.uri === update.segments[i].uri) { | ||
191 | return i + 1; | ||
192 | } | ||
193 | } | ||
194 | |||
195 | // sync on media sequence | ||
196 | return (original.mediaSequence + mediaIndex) - update.mediaSequence; | ||
197 | }, | ||
198 | |||
199 | /** | ||
126 | * Calculate the total duration for a playlist based on segment metadata. | 200 | * Calculate the total duration for a playlist based on segment metadata. |
127 | * @param playlist {object} a media playlist object | 201 | * @param playlist {object} a media playlist object |
128 | * @return {number} the currently known duration, in seconds | 202 | * @return {number} the currently known duration, in seconds |
... | @@ -130,8 +204,24 @@ var | ... | @@ -130,8 +204,24 @@ var |
130 | totalDuration = function(playlist) { | 204 | totalDuration = function(playlist) { |
131 | var | 205 | var |
132 | duration = 0, | 206 | duration = 0, |
133 | i = playlist.segments.length, | 207 | i, |
134 | segment; | 208 | segment; |
209 | |||
210 | if (!playlist.segments) { | ||
211 | return 0; | ||
212 | } | ||
213 | i = playlist.segments.length; | ||
214 | |||
215 | // if present, use the duration specified in the playlist | ||
216 | if (playlist.totalDuration) { | ||
217 | return playlist.totalDuration; | ||
218 | } | ||
219 | |||
220 | // duration should be Infinity for live playlists | ||
221 | if (!playlist.endList) { | ||
222 | return window.Infinity; | ||
223 | } | ||
224 | |||
135 | while (i--) { | 225 | while (i--) { |
136 | segment = playlist.segments[i]; | 226 | segment = playlist.segments[i]; |
137 | duration += segment.duration || playlist.targetDuration || 0; | 227 | duration += segment.duration || playlist.targetDuration || 0; |
... | @@ -198,9 +288,11 @@ var | ... | @@ -198,9 +288,11 @@ var |
198 | }), | 288 | }), |
199 | srcUrl, | 289 | srcUrl, |
200 | 290 | ||
291 | playlistXhr, | ||
201 | segmentXhr, | 292 | segmentXhr, |
202 | downloadPlaylist, | 293 | loadedPlaylist, |
203 | fillBuffer; | 294 | fillBuffer, |
295 | updateCurrentPlaylist; | ||
204 | 296 | ||
205 | // if the video element supports HLS natively, do nothing | 297 | // if the video element supports HLS natively, do nothing |
206 | if (videojs.hls.supportsNativeHls) { | 298 | if (videojs.hls.supportsNativeHls) { |
... | @@ -288,6 +380,36 @@ var | ... | @@ -288,6 +380,36 @@ var |
288 | fillBuffer(currentTime * 1000); | 380 | fillBuffer(currentTime * 1000); |
289 | }); | 381 | }); |
290 | 382 | ||
383 | /** | ||
384 | * Determine whether the current media playlist should be changed | ||
385 | * and trigger a switch if necessary. If a sufficiently fresh | ||
386 | * version of the target playlist is available, the switch will take | ||
387 | * effect immediately. Otherwise, the target playlist will be | ||
388 | * refreshed. | ||
389 | */ | ||
390 | updateCurrentPlaylist = function() { | ||
391 | var playlist, mediaSequence; | ||
392 | playlist = player.hls.selectPlaylist(); | ||
393 | mediaSequence = player.hls.mediaIndex + (player.hls.media.mediaSequence || 0); | ||
394 | if (!playlist.segments || | ||
395 | mediaSequence < (playlist.mediaSequence || 0) || | ||
396 | mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) { | ||
397 | |||
398 | if (playlistXhr) { | ||
399 | playlistXhr.abort(); | ||
400 | } | ||
401 | playlistXhr = xhr(resolveUrl(srcUrl, playlist.uri), loadedPlaylist); | ||
402 | } else { | ||
403 | player.hls.mediaIndex = | ||
404 | translateMediaIndex(player.hls.mediaIndex, | ||
405 | player.hls.media, | ||
406 | playlist); | ||
407 | player.hls.media = playlist; | ||
408 | |||
409 | // update the duration | ||
410 | player.duration(totalDuration(player.hls.media)); | ||
411 | } | ||
412 | }; | ||
291 | 413 | ||
292 | /** | 414 | /** |
293 | * Chooses the appropriate media playlist based on the current | 415 | * Chooses the appropriate media playlist based on the current |
... | @@ -341,8 +463,10 @@ var | ... | @@ -341,8 +463,10 @@ var |
341 | variant = bandwidthPlaylists[i]; | 463 | variant = bandwidthPlaylists[i]; |
342 | 464 | ||
343 | // ignore playlists without resolution information | 465 | // ignore playlists without resolution information |
344 | if (!variant.attributes || !variant.attributes.RESOLUTION || | 466 | if (!variant.attributes || |
345 | !variant.attributes.RESOLUTION.width || !variant.attributes.RESOLUTION.height) { | 467 | !variant.attributes.RESOLUTION || |
468 | !variant.attributes.RESOLUTION.width || | ||
469 | !variant.attributes.RESOLUTION.height) { | ||
346 | continue; | 470 | continue; |
347 | } | 471 | } |
348 | 472 | ||
... | @@ -350,7 +474,7 @@ var | ... | @@ -350,7 +474,7 @@ var |
350 | // dimensions less than or equal to the player size is the | 474 | // dimensions less than or equal to the player size is the |
351 | // best | 475 | // best |
352 | if (variant.attributes.RESOLUTION.width <= player.width() && | 476 | if (variant.attributes.RESOLUTION.width <= player.width() && |
353 | variant.attributes.RESOLUTION.height <= player.height()) { | 477 | variant.attributes.RESOLUTION.height <= player.height()) { |
354 | resolutionBestVariant = variant; | 478 | resolutionBestVariant = variant; |
355 | break; | 479 | break; |
356 | } | 480 | } |
... | @@ -361,104 +485,94 @@ var | ... | @@ -361,104 +485,94 @@ var |
361 | }; | 485 | }; |
362 | 486 | ||
363 | /** | 487 | /** |
364 | * Download an M3U8 and update the current manifest object. If the provided | 488 | * Callback that is invoked when a media playlist finishes |
365 | * URL is a master playlist, the default variant will be downloaded and | 489 | * downloading. Triggers `loadedmanifest` once for each playlist |
366 | * parsed as well. Triggers `loadedmanifest` once for each playlist that is | 490 | * that is downloaded and `loadedmetadata` after at least one |
367 | * downloaded and `loadedmetadata` after at least one media playlist has | 491 | * media playlist has been parsed. |
368 | * been parsed. Whether multiple playlists were downloaded or not, when | ||
369 | * `loadedmetadata` fires a parsed or inferred master playlist object will | ||
370 | * be available as `player.hls.master`. | ||
371 | * | 492 | * |
493 | * @param error {*} truthy if the request was not successful | ||
372 | * @param url {string} a URL to the M3U8 file to process | 494 | * @param url {string} a URL to the M3U8 file to process |
373 | */ | 495 | */ |
374 | downloadPlaylist = function(url) { | 496 | loadedPlaylist = function(error, url) { |
375 | var xhr = new window.XMLHttpRequest(); | 497 | var i, parser, playlist, playlistUri, refreshDelay; |
376 | xhr.open('GET', url); | 498 | |
377 | xhr.onreadystatechange = function() { | 499 | // clear the current playlist XHR |
378 | var i, parser, playlist, playlistUri; | 500 | playlistXhr = null; |
379 | 501 | ||
380 | if (xhr.readyState === 4) { | 502 | if (error) { |
381 | if (xhr.status >= 400 || this.status === 0) { | 503 | player.hls.error = { |
382 | player.hls.error = { | 504 | status: this.status, |
383 | status: xhr.status, | 505 | message: 'HLS playlist request error at URL: ' + url, |
384 | message: 'HLS playlist request error at URL: ' + url, | 506 | code: (this.status >= 500) ? 4 : 2 |
385 | code: (xhr.status >= 500) ? 4 : 2 | 507 | }; |
386 | }; | 508 | return player.trigger('error'); |
387 | player.trigger('error'); | 509 | } |
388 | return; | 510 | |
511 | parser = new videojs.m3u8.Parser(); | ||
512 | parser.push(this.responseText); | ||
513 | |||
514 | // merge this playlist into the master | ||
515 | i = player.hls.master.playlists.length; | ||
516 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | ||
517 | while (i--) { | ||
518 | playlist = player.hls.master.playlists[i]; | ||
519 | playlistUri = resolveUrl(srcUrl, playlist.uri); | ||
520 | if (playlistUri === url) { | ||
521 | // if the playlist is unchanged since the last reload, | ||
522 | // try again after half the target duration | ||
523 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | ||
524 | if (playlist.segments && | ||
525 | playlist.segments.length === parser.manifest.segments.length) { | ||
526 | refreshDelay /= 2; | ||
389 | } | 527 | } |
390 | 528 | ||
391 | // readystate DONE | 529 | player.hls.master.playlists[i] = |
392 | parser = new videojs.m3u8.Parser(); | 530 | videojs.util.mergeOptions(playlist, parser.manifest); |
393 | parser.push(xhr.responseText); | ||
394 | 531 | ||
395 | // master playlists | 532 | if (playlist !== player.hls.media) { |
396 | if (parser.manifest.playlists) { | 533 | continue; |
397 | player.hls.master = parser.manifest; | ||
398 | downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri)); | ||
399 | player.trigger('loadedmanifest'); | ||
400 | return; | ||
401 | } | 534 | } |
402 | 535 | ||
403 | // media playlists | 536 | // determine the new mediaIndex if we're updating the |
404 | if (player.hls.master) { | 537 | // current media playlist |
405 | // merge this playlist into the master | 538 | player.hls.mediaIndex = |
406 | i = player.hls.master.playlists.length; | 539 | translateMediaIndex(player.hls.mediaIndex, |
407 | 540 | playlist, | |
408 | while (i--) { | 541 | parser.manifest); |
409 | playlist = player.hls.master.playlists[i]; | 542 | player.hls.media = parser.manifest; |
410 | playlistUri = resolveUrl(srcUrl, playlist.uri); | 543 | } |
411 | if (playlistUri === url) { | 544 | } |
412 | player.hls.master.playlists[i] = | 545 | |
413 | videojs.util.mergeOptions(playlist, parser.manifest); | 546 | // check the playlist for updates if EXT-X-ENDLIST isn't present |
414 | } | 547 | if (!parser.manifest.endList) { |
415 | } | 548 | window.setTimeout(function() { |
416 | } else { | 549 | if (!playlistXhr && |
417 | // infer a master playlist if none was previously requested | 550 | resolveUrl(srcUrl, player.hls.media.uri) === url) { |
418 | player.hls.master = { | 551 | playlistXhr = xhr(url, loadedPlaylist); |
419 | playlists: [parser.manifest] | ||
420 | }; | ||
421 | } | 552 | } |
553 | }, refreshDelay); | ||
554 | } | ||
422 | 555 | ||
423 | // always start playback with the default rendition | 556 | // always start playback with the default rendition |
424 | if (!player.hls.media) { | 557 | if (!player.hls.media) { |
425 | player.hls.media = player.hls.master.playlists[0]; | 558 | player.hls.media = player.hls.master.playlists[0]; |
426 | 559 | ||
427 | // update the duration | 560 | // update the duration |
428 | if (parser.manifest.totalDuration) { | 561 | player.duration(totalDuration(parser.manifest)); |
429 | player.duration(parser.manifest.totalDuration); | ||
430 | } else { | ||
431 | player.duration(totalDuration(parser.manifest)); | ||
432 | } | ||
433 | 562 | ||
434 | // periodicaly check if the buffer needs to be refilled | 563 | // periodicaly check if the buffer needs to be refilled |
435 | player.on('timeupdate', fillBuffer); | 564 | player.on('timeupdate', fillBuffer); |
436 | 565 | ||
437 | player.trigger('loadedmanifest'); | 566 | player.trigger('loadedmanifest'); |
438 | player.trigger('loadedmetadata'); | 567 | player.trigger('loadedmetadata'); |
439 | fillBuffer(); | 568 | fillBuffer(); |
440 | return; | 569 | return; |
441 | } | 570 | } |
442 | 571 | ||
443 | // select a playlist and download its metadata if necessary | 572 | // select a playlist and download its metadata if necessary |
444 | playlist = player.hls.selectPlaylist(); | 573 | updateCurrentPlaylist(); |
445 | if (!playlist.segments) { | ||
446 | downloadPlaylist(resolveUrl(srcUrl, playlist.uri)); | ||
447 | } else { | ||
448 | player.hls.media = playlist; | ||
449 | |||
450 | // update the duration | ||
451 | if (player.hls.media.totalDuration) { | ||
452 | player.duration(player.hls.media.totalDuration); | ||
453 | } else { | ||
454 | player.duration(totalDuration(player.hls.media)); | ||
455 | } | ||
456 | } | ||
457 | 574 | ||
458 | player.trigger('loadedmanifest'); | 575 | player.trigger('loadedmanifest'); |
459 | } | ||
460 | }; | ||
461 | xhr.send(null); | ||
462 | }; | 576 | }; |
463 | 577 | ||
464 | /** | 578 | /** |
... | @@ -495,16 +609,19 @@ var | ... | @@ -495,16 +609,19 @@ var |
495 | return; | 609 | return; |
496 | } | 610 | } |
497 | 611 | ||
498 | segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''), | 612 | // resolve the segment URL relative to the playlist |
499 | segment.uri); | 613 | if (player.hls.media.uri === srcUrl) { |
614 | segmentUri = resolveUrl(srcUrl, segment.uri); | ||
615 | } else { | ||
616 | segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''), | ||
617 | segment.uri); | ||
618 | } | ||
500 | 619 | ||
501 | // request the next segment | 620 | // request the next segment |
502 | segmentXhr = new window.XMLHttpRequest(); | 621 | segmentXhr = new window.XMLHttpRequest(); |
503 | segmentXhr.open('GET', segmentUri); | 622 | segmentXhr.open('GET', segmentUri); |
504 | segmentXhr.responseType = 'arraybuffer'; | 623 | segmentXhr.responseType = 'arraybuffer'; |
505 | segmentXhr.onreadystatechange = function() { | 624 | segmentXhr.onreadystatechange = function() { |
506 | var playlist; | ||
507 | |||
508 | // wait until the request completes | 625 | // wait until the request completes |
509 | if (this.readyState !== 4) { | 626 | if (this.readyState !== 4) { |
510 | return; | 627 | return; |
... | @@ -559,12 +676,7 @@ var | ... | @@ -559,12 +676,7 @@ var |
559 | 676 | ||
560 | // figure out what stream the next segment should be downloaded from | 677 | // figure out what stream the next segment should be downloaded from |
561 | // with the updated bandwidth information | 678 | // with the updated bandwidth information |
562 | playlist = player.hls.selectPlaylist(); | 679 | updateCurrentPlaylist(); |
563 | if (!playlist.segments) { | ||
564 | downloadPlaylist(resolveUrl(srcUrl, playlist.uri)); | ||
565 | } else { | ||
566 | player.hls.media = playlist; | ||
567 | } | ||
568 | }; | 680 | }; |
569 | startTime = +new Date(); | 681 | startTime = +new Date(); |
570 | segmentXhr.send(null); | 682 | segmentXhr.send(null); |
... | @@ -578,7 +690,26 @@ var | ... | @@ -578,7 +690,26 @@ var |
578 | sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); | 690 | sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); |
579 | 691 | ||
580 | player.hls.mediaIndex = 0; | 692 | player.hls.mediaIndex = 0; |
581 | downloadPlaylist(srcUrl); | 693 | xhr(srcUrl, function(error, url) { |
694 | var uri, parser = new videojs.m3u8.Parser(); | ||
695 | parser.push(this.responseText); | ||
696 | |||
697 | // master playlists | ||
698 | if (parser.manifest.playlists) { | ||
699 | player.hls.master = parser.manifest; | ||
700 | playlistXhr = xhr(resolveUrl(url, parser.manifest.playlists[0].uri), loadedPlaylist); | ||
701 | return player.trigger('loadedmanifest'); | ||
702 | } else { | ||
703 | // infer a master playlist if a media playlist is loaded directly | ||
704 | uri = resolveUrl(window.location.href, url); | ||
705 | player.hls.master = { | ||
706 | playlists: [{ | ||
707 | uri: uri | ||
708 | }] | ||
709 | }; | ||
710 | loadedPlaylist.call(this, error, uri); | ||
711 | } | ||
712 | }); | ||
582 | }); | 713 | }); |
583 | player.src([{ | 714 | player.src([{ |
584 | src: videojs.URL.createObjectURL(mediaSource), | 715 | src: videojs.URL.createObjectURL(mediaSource), | ... | ... |
1 | (function(window, undefined) { | 1 | (function(window, undefined) { |
2 | var | 2 | var |
3 | //manifestController = this.manifestController, | 3 | //manifestController = this.manifestController, |
4 | ParseStream = window.videojs.m3u8.ParseStream, | 4 | m3u8 = window.videojs.m3u8, |
5 | ParseStream = m3u8.ParseStream, | ||
5 | parseStream, | 6 | parseStream, |
6 | LineStream = window.videojs.m3u8.LineStream, | 7 | LineStream = m3u8.LineStream, |
7 | lineStream, | 8 | lineStream, |
8 | Parser = window.videojs.m3u8.Parser, | 9 | Parser = m3u8.Parser, |
9 | parser; | 10 | parser; |
10 | 11 | ||
11 | /* | 12 | /* |
... | @@ -506,19 +507,15 @@ | ... | @@ -506,19 +507,15 @@ |
506 | ok(!event, 'no event is triggered'); | 507 | ok(!event, 'no event is triggered'); |
507 | }); | 508 | }); |
508 | 509 | ||
509 | module('m3u8 parser', { | 510 | module('m3u8 parser'); |
510 | setup: function() { | ||
511 | parser = new Parser(); | ||
512 | } | ||
513 | }); | ||
514 | 511 | ||
515 | test('should create a parser', function() { | 512 | test('can be constructed', function() { |
516 | notStrictEqual(parser, undefined, 'parser is defined'); | 513 | notStrictEqual(new Parser(), undefined, 'parser is defined'); |
517 | }); | 514 | }); |
518 | 515 | ||
519 | module('m3u8s'); | 516 | module('m3u8s'); |
520 | 517 | ||
521 | test('parses the example manifests as expected', function() { | 518 | test('parses static manifests as expected', function() { |
522 | var key; | 519 | var key; |
523 | for (key in window.manifests) { | 520 | for (key in window.manifests) { |
524 | if (window.expected[key]) { | 521 | if (window.expected[key]) { | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "playlists": [{ | 3 | "playlists": [ |
4 | "attributes": { | 4 | { |
5 | "PROGRAM-ID": 1, | 5 | "attributes": { |
6 | "BANDWIDTH": 240000, | 6 | "PROGRAM-ID": 1, |
7 | "RESOLUTION": { | 7 | "BANDWIDTH": 240000, |
8 | "width": 396, | 8 | "RESOLUTION": { |
9 | "height": 224 | 9 | "width": 396, |
10 | } | 10 | "height": 224 |
11 | } | ||
12 | }, | ||
13 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" | ||
11 | }, | 14 | }, |
12 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" | 15 | { |
13 | }, { | 16 | "attributes": { |
14 | "attributes": { | 17 | "PROGRAM-ID": 1, |
15 | "PROGRAM-ID": 1, | 18 | "BANDWIDTH": 40000 |
16 | "BANDWIDTH": 40000 | 19 | }, |
20 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" | ||
17 | }, | 21 | }, |
18 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" | 22 | { |
19 | }, { | 23 | "attributes": { |
20 | "attributes": { | 24 | "PROGRAM-ID": 1, |
21 | "PROGRAM-ID": 1, | 25 | "BANDWIDTH": 440000, |
22 | "BANDWIDTH": 440000, | 26 | "RESOLUTION": { |
23 | "RESOLUTION": { | 27 | "width": 396, |
24 | "width": 396, | 28 | "height": 224 |
25 | "height": 224 | 29 | } |
26 | } | 30 | }, |
31 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" | ||
27 | }, | 32 | }, |
28 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" | 33 | { |
29 | }, { | 34 | "attributes": { |
30 | "attributes": { | 35 | "PROGRAM-ID": 1, |
31 | "PROGRAM-ID": 1, | 36 | "BANDWIDTH": 1928000, |
32 | "BANDWIDTH": 1928000, | 37 | "RESOLUTION": { |
33 | "RESOLUTION": { | 38 | "width": 960, |
34 | "width": 960, | 39 | "height": 540 |
35 | "height": 540 | 40 | } |
36 | } | 41 | }, |
37 | }, | 42 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" |
38 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" | 43 | } |
39 | }] | 44 | ] |
40 | } | 45 | } | ... | ... |
... | @@ -20,5 +20,6 @@ | ... | @@ -20,5 +20,6 @@ |
20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" | 20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" |
21 | } | 21 | } |
22 | ], | 22 | ], |
23 | "targetDuration": 8 | 23 | "targetDuration": 8, |
24 | "endList": true | ||
24 | } | 25 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "mediaSequence": 0, | 3 | "mediaSequence": 0, |
4 | "playlistType": "VOD", | ||
5 | "segments": [ | 4 | "segments": [ |
6 | { | 5 | { |
7 | "duration": 10, | 6 | "duration": 10, |
... | @@ -28,5 +27,6 @@ | ... | @@ -28,5 +27,6 @@ |
28 | "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" | 27 | "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" |
29 | } | 28 | } |
30 | ], | 29 | ], |
31 | "targetDuration": 10 | 30 | "targetDuration": 10, |
31 | "endList": true | ||
32 | } | 32 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "playlists": [{ | 3 | "playlists": [ |
4 | "attributes": { | 4 | { |
5 | "PROGRAM-ID": 1, | 5 | "attributes": { |
6 | "BANDWIDTH": 240000, | 6 | "PROGRAM-ID": 1, |
7 | "RESOLUTION": { | 7 | "BANDWIDTH": 240000, |
8 | "width": 396, | 8 | "RESOLUTION": { |
9 | "height": 224 | 9 | "width": 396, |
10 | } | 10 | "height": 224 |
11 | } | ||
12 | }, | ||
13 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" | ||
11 | }, | 14 | }, |
12 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" | 15 | { |
13 | }, { | 16 | "attributes": { |
14 | "attributes": { | 17 | "PROGRAM-ID": 1, |
15 | "PROGRAM-ID": 1, | 18 | "BANDWIDTH": 40000 |
16 | "BANDWIDTH": 40000 | 19 | }, |
20 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" | ||
17 | }, | 21 | }, |
18 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" | 22 | { |
19 | }, { | 23 | "attributes": { |
20 | "attributes": { | 24 | "PROGRAM-ID": 1, |
21 | "PROGRAM-ID": 1, | 25 | "BANDWIDTH": 440000, |
22 | "BANDWIDTH": 440000, | 26 | "RESOLUTION": { |
23 | "RESOLUTION": { | 27 | "width": 396, |
24 | "width": 396, | 28 | "height": 224 |
25 | "height": 224 | 29 | } |
26 | } | 30 | }, |
31 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" | ||
27 | }, | 32 | }, |
28 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" | 33 | { |
29 | }, { | 34 | "attributes": { |
30 | "attributes": { | 35 | "PROGRAM-ID": 1, |
31 | "PROGRAM-ID": 1, | 36 | "BANDWIDTH": 1928000, |
32 | "BANDWIDTH": 1928000, | 37 | "RESOLUTION": { |
33 | "RESOLUTION": { | 38 | "width": 960, |
34 | "width": 960, | 39 | "height": 540 |
35 | "height": 540 | 40 | } |
36 | } | 41 | }, |
37 | }, | 42 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" |
38 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" | 43 | } |
39 | }] | 44 | ] |
40 | } | 45 | } | ... | ... |
... | @@ -28,5 +28,6 @@ | ... | @@ -28,5 +28,6 @@ |
28 | "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" | 28 | "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" |
29 | } | 29 | } |
30 | ], | 30 | ], |
31 | "targetDuration": 10 | 31 | "targetDuration": 10, |
32 | "endList": true | ||
32 | } | 33 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "mediaSequence": 1, | 3 | "mediaSequence": 1, |
4 | "playlistType": "VOD", | ||
5 | "segments": [ | 4 | "segments": [ |
6 | { | 5 | { |
7 | "duration": 6.64, | 6 | "duration": 6.64, |
8 | "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" | 7 | "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" |
9 | } | 8 | } |
10 | ], | 9 | ], |
11 | "targetDuration": 8 | 10 | "targetDuration": 8, |
11 | "endList": true | ||
12 | } | 12 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -20,5 +20,6 @@ | ... | @@ -20,5 +20,6 @@ |
20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" | 20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" |
21 | } | 21 | } |
22 | ], | 22 | ], |
23 | "targetDuration": 8 | 23 | "targetDuration": 8, |
24 | "endList": true | ||
24 | } | 25 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "mediaSequence": 0, | 3 | "mediaSequence": 0, |
4 | "playlistType": "VOD", | ||
5 | "segments": [ | 4 | "segments": [ |
6 | { | 5 | { |
7 | "duration": 10, | 6 | "duration": 10, |
... | @@ -28,5 +27,6 @@ | ... | @@ -28,5 +27,6 @@ |
28 | "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" | 27 | "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" |
29 | } | 28 | } |
30 | ], | 29 | ], |
31 | "targetDuration": 10 | 30 | "targetDuration": 10, |
31 | "endList": true | ||
32 | } | 32 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "mediaSequence": 0, | 3 | "mediaSequence": 0, |
4 | "playlistType": "VOD", | ||
5 | "segments": [ | 4 | "segments": [ |
6 | { | 5 | { |
7 | "duration": 10, | 6 | "duration": 10, |
8 | "uri": "/test/ts-files/zencoder/gogo/00001.ts" | 7 | "uri": "/test/ts-files/zencoder/gogo/00001.ts" |
9 | } | 8 | } |
10 | ] | 9 | ], |
10 | "endList": true | ||
11 | } | 11 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "mediaSequence": 0, | 3 | "mediaSequence": 0, |
4 | "playlistType": "VOD", | ||
5 | "segments": [ | 4 | "segments": [ |
6 | { | 5 | { |
7 | "duration": 10, | 6 | "duration": 10, |
... | @@ -24,5 +23,6 @@ | ... | @@ -24,5 +23,6 @@ |
24 | "uri": "/test/ts-files/zencoder/gogo/00005.ts" | 23 | "uri": "/test/ts-files/zencoder/gogo/00005.ts" |
25 | } | 24 | } |
26 | ], | 25 | ], |
27 | "targetDuration": 10 | 26 | "targetDuration": 10, |
27 | "endList": true | ||
28 | } | 28 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "mediaSequence": 0, | 3 | "mediaSequence": 0, |
4 | "playlistType": "VOD", | ||
5 | "segments": [ | 4 | "segments": [ |
6 | { | 5 | { |
7 | "duration": 10, | 6 | "duration": 10, |
8 | "uri": "/test/ts-files/zencoder/gogo/00001.ts" | 7 | "uri": "/test/ts-files/zencoder/gogo/00001.ts" |
9 | } | 8 | } |
10 | ], | 9 | ], |
11 | "targetDuration": 10 | 10 | "targetDuration": 10, |
11 | "endList": true | ||
12 | } | 12 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "playlists": [{ | 3 | "playlists": [ |
4 | "attributes": { | 4 | { |
5 | "PROGRAM-ID": 1, | 5 | "attributes": { |
6 | "BANDWIDTH": 240000, | 6 | "PROGRAM-ID": 1, |
7 | "RESOLUTION": { | 7 | "BANDWIDTH": 240000, |
8 | "width": 396, | 8 | "RESOLUTION": { |
9 | "height": 224 | 9 | "width": 396, |
10 | } | 10 | "height": 224 |
11 | } | ||
12 | }, | ||
13 | "uri": "media.m3u8" | ||
11 | }, | 14 | }, |
12 | "uri": "media.m3u8" | 15 | { |
13 | }, { | 16 | "attributes": { |
14 | "attributes": { | 17 | "PROGRAM-ID": 1, |
15 | "PROGRAM-ID": 1, | 18 | "BANDWIDTH": 40000 |
16 | "BANDWIDTH": 40000 | 19 | }, |
20 | "uri": "media1.m3u8" | ||
17 | }, | 21 | }, |
18 | "uri": "media1.m3u8" | 22 | { |
19 | }, { | 23 | "attributes": { |
20 | "attributes": { | 24 | "PROGRAM-ID": 1, |
21 | "PROGRAM-ID": 1, | 25 | "BANDWIDTH": 440000, |
22 | "BANDWIDTH": 440000, | 26 | "RESOLUTION": { |
23 | "RESOLUTION": { | 27 | "width": 396, |
24 | "width": 396, | 28 | "height": 224 |
25 | "height": 224 | 29 | } |
26 | } | 30 | }, |
31 | "uri": "media2.m3u8" | ||
27 | }, | 32 | }, |
28 | "uri": "media2.m3u8" | 33 | { |
29 | }, { | 34 | "attributes": { |
30 | "attributes": { | 35 | "PROGRAM-ID": 1, |
31 | "PROGRAM-ID": 1, | 36 | "BANDWIDTH": 1928000, |
32 | "BANDWIDTH": 1928000, | 37 | "RESOLUTION": { |
33 | "RESOLUTION": { | 38 | "width": 960, |
34 | "width": 960, | 39 | "height": 540 |
35 | "height": 540 | 40 | } |
36 | } | 41 | }, |
37 | }, | 42 | "uri": "media3.m3u8" |
38 | "uri": "media3.m3u8" | 43 | } |
39 | }] | 44 | ] |
40 | } | 45 | } | ... | ... |
... | @@ -20,5 +20,6 @@ | ... | @@ -20,5 +20,6 @@ |
20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" | 20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" |
21 | } | 21 | } |
22 | ], | 22 | ], |
23 | "targetDuration": 8 | 23 | "targetDuration": 8, |
24 | "endList": true | ||
24 | } | 25 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
test/manifest/missingEndlist.json
0 → 100644
test/manifest/missingEndlist.m3u8
0 → 100644
... | @@ -20,5 +20,6 @@ | ... | @@ -20,5 +20,6 @@ |
20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" | 20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" |
21 | } | 21 | } |
22 | ], | 22 | ], |
23 | "targetDuration": 8 | 23 | "targetDuration": 8, |
24 | "endList": true | ||
24 | } | 25 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -20,5 +20,6 @@ | ... | @@ -20,5 +20,6 @@ |
20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" | 20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" |
21 | } | 21 | } |
22 | ], | 22 | ], |
23 | "targetDuration": 8 | 23 | "targetDuration": 8, |
24 | } | 24 | "endList": true |
25 | } | ||
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "mediaSequence": 0, | 3 | "mediaSequence": 0, |
4 | "playlistType": "VOD", | ||
5 | "targetDuration": 10, | 4 | "targetDuration": 10, |
6 | "segments": [{ | 5 | "segments": [ |
7 | "uri": "001.ts" | 6 | { |
8 | }, { | 7 | "uri": "001.ts" |
9 | "uri": "002.ts", | 8 | }, |
10 | "duration": 9 | 9 | { |
11 | }, { | 10 | "uri": "002.ts", |
12 | "uri": "003.ts", | 11 | "duration": 9 |
13 | "duration": 7 | 12 | }, |
14 | }, { | 13 | { |
15 | "uri": "004.ts", | 14 | "uri": "003.ts", |
16 | "duration": 10 | 15 | "duration": 7 |
17 | }] | 16 | }, |
17 | { | ||
18 | "uri": "004.ts", | ||
19 | "duration": 10 | ||
20 | } | ||
21 | ] | ||
18 | } | 22 | } | ... | ... |
... | @@ -20,5 +20,6 @@ | ... | @@ -20,5 +20,6 @@ |
20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" | 20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" |
21 | } | 21 | } |
22 | ], | 22 | ], |
23 | "targetDuration": 8 | 23 | "targetDuration": 8, |
24 | "endList": true | ||
24 | } | 25 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -8,5 +8,6 @@ | ... | @@ -8,5 +8,6 @@ |
8 | "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" | 8 | "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" |
9 | } | 9 | } |
10 | ], | 10 | ], |
11 | "targetDuration": 8 | 11 | "targetDuration": 8, |
12 | "endList": true | ||
12 | } | 13 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
1 | { | 1 | { |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "playlists": [ | 3 | "playlists": [ |
4 | { | 4 | { |
5 | "attributes": { | 5 | "attributes": { |
6 | "PROGRAM-ID": 1 | 6 | "PROGRAM-ID": 1 |
7 | }, | ||
8 | "uri": "media.m3u8" | ||
7 | }, | 9 | }, |
8 | "uri": "media.m3u8" | 10 | { |
9 | }, | 11 | "uri": "media1.m3u8" |
10 | { | 12 | } |
11 | "uri": "media1.m3u8" | ||
12 | } | ||
13 | ] | 13 | ] |
14 | } | 14 | } | ... | ... |
... | @@ -20,5 +20,6 @@ | ... | @@ -20,5 +20,6 @@ |
20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" | 20 | "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" |
21 | } | 21 | } |
22 | ], | 22 | ], |
23 | "targetDuration": 8 | 23 | "targetDuration": 8, |
24 | "endList": true | ||
24 | } | 25 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -105,6 +105,7 @@ module('HLS', { | ... | @@ -105,6 +105,7 @@ module('HLS', { |
105 | this.readyState = 4; | 105 | this.readyState = 4; |
106 | this.onreadystatechange(); | 106 | this.onreadystatechange(); |
107 | }; | 107 | }; |
108 | this.abort = function() {}; | ||
108 | }; | 109 | }; |
109 | xhrUrls = []; | 110 | xhrUrls = []; |
110 | }, | 111 | }, |
... | @@ -170,7 +171,7 @@ test('sets the duration if one is available on the playlist', function() { | ... | @@ -170,7 +171,7 @@ test('sets the duration if one is available on the playlist', function() { |
170 | type: 'sourceopen' | 171 | type: 'sourceopen' |
171 | }); | 172 | }); |
172 | 173 | ||
173 | strictEqual(1, calls, 'duration is set'); | 174 | strictEqual(calls, 2, 'duration is set'); |
174 | }); | 175 | }); |
175 | 176 | ||
176 | test('calculates the duration if needed', function() { | 177 | test('calculates the duration if needed', function() { |
... | @@ -181,13 +182,15 @@ test('calculates the duration if needed', function() { | ... | @@ -181,13 +182,15 @@ test('calculates the duration if needed', function() { |
181 | } | 182 | } |
182 | durations.push(duration); | 183 | durations.push(duration); |
183 | }; | 184 | }; |
184 | player.hls('manifest/liveMissingSegmentDuration.m3u8'); | 185 | player.hls('http://example.com/manifest/missingExtinf.m3u8'); |
185 | videojs.mediaSources[player.currentSrc()].trigger({ | 186 | videojs.mediaSources[player.currentSrc()].trigger({ |
186 | type: 'sourceopen' | 187 | type: 'sourceopen' |
187 | }); | 188 | }); |
188 | 189 | ||
189 | strictEqual(durations.length, 1, 'duration is set'); | 190 | strictEqual(durations.length, 2, 'duration is set'); |
190 | strictEqual(durations[0], 6.64 + (2 * 8), 'duration is calculated'); | 191 | strictEqual(durations[0], |
192 | player.hls.media.segments.length * 10, | ||
193 | 'duration is calculated'); | ||
191 | }); | 194 | }); |
192 | 195 | ||
193 | test('starts downloading a segment on loadedmetadata', function() { | 196 | test('starts downloading a segment on loadedmetadata', function() { |
... | @@ -400,15 +403,12 @@ test('downloads additional playlists if required', function() { | ... | @@ -400,15 +403,12 @@ test('downloads additional playlists if required', function() { |
400 | called = true; | 403 | called = true; |
401 | return playlist; | 404 | return playlist; |
402 | } | 405 | } |
403 | playlist.segments = []; | 406 | playlist.segments = [1, 1, 1]; |
404 | return playlist; | 407 | return playlist; |
405 | }; | 408 | }; |
406 | xhrUrls = []; | 409 | xhrUrls = []; |
407 | 410 | ||
408 | // the playlist selection is revisited after a new segment is downloaded | 411 | // the playlist selection is revisited after a new segment is downloaded |
409 | player.currentTime = function() { | ||
410 | return 1; | ||
411 | }; | ||
412 | player.trigger('timeupdate'); | 412 | player.trigger('timeupdate'); |
413 | 413 | ||
414 | strictEqual(2, xhrUrls.length, 'requests were made'); | 414 | strictEqual(2, xhrUrls.length, 'requests were made'); |
... | @@ -867,10 +867,246 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { | ... | @@ -867,10 +867,246 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { |
867 | 867 | ||
868 | test('has no effect if native HLS is available', function() { | 868 | test('has no effect if native HLS is available', function() { |
869 | videojs.hls.supportsNativeHls = true; | 869 | videojs.hls.supportsNativeHls = true; |
870 | player.hls('manifest/master.m3u8'); | 870 | player.hls('http://example.com/manifest/master.m3u8'); |
871 | 871 | ||
872 | ok(!(player.currentSrc() in videojs.mediaSources), | 872 | ok(!(player.currentSrc() in videojs.mediaSources), |
873 | 'no media source was opened'); | 873 | 'no media source was opened'); |
874 | }); | 874 | }); |
875 | 875 | ||
876 | test('reloads live playlists', function() { | ||
877 | var callbacks = []; | ||
878 | // capture timeouts | ||
879 | window.setTimeout = function(callback, timeout) { | ||
880 | callbacks.push({ callback: callback, timeout: timeout }); | ||
881 | }; | ||
882 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); | ||
883 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
884 | type: 'sourceopen' | ||
885 | }); | ||
886 | |||
887 | strictEqual(1, callbacks.length, 'refresh was scheduled'); | ||
888 | strictEqual(player.hls.media.targetDuration * 1000, | ||
889 | callbacks[0].timeout, | ||
890 | 'waited one target duration'); | ||
891 | }); | ||
892 | |||
893 | test('duration is Infinity for live playlists', function() { | ||
894 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); | ||
895 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
896 | type: 'sourceopen' | ||
897 | }); | ||
898 | |||
899 | strictEqual(Infinity, player.duration(), 'duration is infinity'); | ||
900 | }); | ||
901 | |||
902 | test('does not reload playlists with an endlist tag', function() { | ||
903 | var callbacks = []; | ||
904 | // capture timeouts | ||
905 | window.setTimeout = function(callback, timeout) { | ||
906 | callbacks.push({ callback: callback, timeout: timeout }); | ||
907 | }; | ||
908 | player.hls('manifest/media.m3u8'); | ||
909 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
910 | type: 'sourceopen' | ||
911 | }); | ||
912 | |||
913 | strictEqual(0, callbacks.length, 'no refresh was scheduled'); | ||
914 | }); | ||
915 | |||
916 | test('reloads a live playlist after half a target duration if it has not ' + | ||
917 | 'changed since the last request', function() { | ||
918 | var callbacks = []; | ||
919 | // capture timeouts | ||
920 | window.setTimeout = function(callback, timeout) { | ||
921 | callbacks.push({ callback: callback, timeout: timeout }); | ||
922 | }; | ||
923 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); | ||
924 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
925 | type: 'sourceopen' | ||
926 | }); | ||
927 | |||
928 | strictEqual(callbacks.length, 1, 'full-length refresh scheduled'); | ||
929 | callbacks.pop().callback(); | ||
930 | |||
931 | strictEqual(1, callbacks.length, 'half-length refresh was scheduled'); | ||
932 | strictEqual(callbacks[0].timeout, | ||
933 | player.hls.media.targetDuration / 2 * 1000, | ||
934 | 'waited half a target duration'); | ||
935 | }); | ||
936 | |||
937 | test('merges playlist reloads', function() { | ||
938 | var | ||
939 | oldPlaylist, | ||
940 | callback; | ||
941 | // capture timeouts | ||
942 | window.setTimeout = function(cb) { | ||
943 | callback = cb; | ||
944 | }; | ||
945 | |||
946 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); | ||
947 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
948 | type: 'sourceopen' | ||
949 | }); | ||
950 | oldPlaylist = player.hls.media; | ||
951 | |||
952 | callback(); | ||
953 | ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated'); | ||
954 | }); | ||
955 | |||
956 | test('updates the media index when a playlist reloads', function() { | ||
957 | var callback; | ||
958 | window.setTimeout = function(cb) { | ||
959 | callback = cb; | ||
960 | }; | ||
961 | // the initial playlist | ||
962 | window.manifests['live-updating'] = | ||
963 | '#EXTM3U\n' + | ||
964 | '#EXTINF:10,\n' + | ||
965 | '0.ts\n' + | ||
966 | '#EXTINF:10,\n' + | ||
967 | '1.ts\n' + | ||
968 | '#EXTINF:10,\n' + | ||
969 | '2.ts\n'; | ||
970 | |||
971 | player.hls('http://example.com/live-updating.m3u8'); | ||
972 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
973 | type: 'sourceopen' | ||
974 | }); | ||
975 | |||
976 | // play the stream until 2.ts is playing | ||
977 | player.hls.mediaIndex = 3; | ||
978 | |||
979 | // reload the updated playlist | ||
980 | window.manifests['live-updating'] = | ||
981 | '#EXTM3U\n' + | ||
982 | '#EXTINF:10,\n' + | ||
983 | '1.ts\n' + | ||
984 | '#EXTINF:10,\n' + | ||
985 | '2.ts\n' + | ||
986 | '#EXTINF:10,\n' + | ||
987 | '3.ts\n'; | ||
988 | callback(); | ||
989 | |||
990 | strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload'); | ||
991 | }); | ||
992 | |||
993 | test('mediaIndex is zero before the first segment loads', function() { | ||
994 | window.manifests['first-seg-load'] = | ||
995 | '#EXTM3U\n' + | ||
996 | '#EXTINF:10,\n' + | ||
997 | '0.ts\n'; | ||
998 | window.XMLHttpRequest = function() { | ||
999 | this.open = function() {}; | ||
1000 | this.send = function() {}; | ||
1001 | }; | ||
1002 | player.hls('http://example.com/first-seg-load.m3u8'); | ||
1003 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
1004 | type: 'sourceopen' | ||
1005 | }); | ||
1006 | |||
1007 | strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero'); | ||
1008 | }); | ||
1009 | |||
1010 | test('reloads out-of-date live playlists when switching variants', function() { | ||
1011 | player.hls('http://example.com/master.m3u8'); | ||
1012 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
1013 | type: 'sourceopen' | ||
1014 | }); | ||
1015 | |||
1016 | player.hls.master = { | ||
1017 | playlists: [{ | ||
1018 | mediaSequence: 15, | ||
1019 | segments: [1, 1, 1] | ||
1020 | }, { | ||
1021 | uri: 'http://example.com/variant-update.m3u8', | ||
1022 | mediaSequence: 0, | ||
1023 | segments: [1, 1] | ||
1024 | }] | ||
1025 | }; | ||
1026 | // playing segment 15 on playlist zero | ||
1027 | player.hls.media = player.hls.master.playlists[0]; | ||
1028 | player.mediaIndex = 1; | ||
1029 | window.manifests['variant-update'] = '#EXTM3U\n' + | ||
1030 | '#EXT-X-MEDIA-SEQUENCE:16\n' + | ||
1031 | '#EXTINF:10,\n' + | ||
1032 | '16.ts\n' + | ||
1033 | '#EXTINF:10,\n' + | ||
1034 | '17.ts\n'; | ||
1035 | |||
1036 | // switch playlists | ||
1037 | player.hls.selectPlaylist = function() { | ||
1038 | return player.hls.master.playlists[1]; | ||
1039 | }; | ||
1040 | // timeupdate downloads segment 16 then switches playlists | ||
1041 | player.trigger('timeupdate'); | ||
1042 | |||
1043 | strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment'); | ||
1044 | }); | ||
1045 | |||
1046 | test('does not reload master playlists', function() { | ||
1047 | var callbacks = []; | ||
1048 | window.setTimeout = function(callback) { | ||
1049 | callbacks.push(callback); | ||
1050 | }; | ||
1051 | |||
1052 | player.hls('http://example.com/master.m3u8'); | ||
1053 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
1054 | type: 'sourceopen' | ||
1055 | }); | ||
1056 | |||
1057 | strictEqual(callbacks.length, 0, 'no reload scheduled'); | ||
1058 | }); | ||
1059 | |||
1060 | test('only reloads the active media playlist', function() { | ||
1061 | var callbacks = [], urls = [], responses = []; | ||
1062 | window.setTimeout = function(callback) { | ||
1063 | callbacks.push(callback); | ||
1064 | }; | ||
1065 | |||
1066 | player.hls('http://example.com/missingEndlist.m3u8'); | ||
1067 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
1068 | type: 'sourceopen' | ||
1069 | }); | ||
1070 | |||
1071 | window.XMLHttpRequest = function() { | ||
1072 | this.open = function(method, url) { | ||
1073 | urls.push(url); | ||
1074 | }; | ||
1075 | this.send = function() { | ||
1076 | var xhr = this; | ||
1077 | responses.push(function() { | ||
1078 | xhr.readyState = 4; | ||
1079 | xhr.responseText = '#EXTM3U\n' + | ||
1080 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
1081 | '#EXTINF:10,\n' + | ||
1082 | '1.ts\n'; | ||
1083 | xhr.response = new Uint8Array([1]).buffer; | ||
1084 | xhr.onreadystatechange(); | ||
1085 | }); | ||
1086 | }; | ||
1087 | }; | ||
1088 | player.hls.selectPlaylist = function() { | ||
1089 | return player.hls.master.playlists[1]; | ||
1090 | }; | ||
1091 | player.hls.master.playlists.push({ | ||
1092 | uri: 'http://example.com/switched.m3u8' | ||
1093 | }); | ||
1094 | |||
1095 | player.trigger('timeupdate'); | ||
1096 | strictEqual(callbacks.length, 1, 'a refresh is scheduled'); | ||
1097 | strictEqual(responses.length, 1, 'segment requested'); | ||
1098 | |||
1099 | responses.shift()(); // segment response | ||
1100 | responses.shift()(); // loaded switched.m3u8 | ||
1101 | |||
1102 | urls = []; | ||
1103 | callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8 | ||
1104 | callbacks.shift()(); // refresh switched.m3u8 | ||
1105 | |||
1106 | strictEqual(urls.length, 1, 'one refresh was made'); | ||
1107 | strictEqual(urls[0], | ||
1108 | 'http://example.com/switched.m3u8', | ||
1109 | 'refreshed the active playlist'); | ||
1110 | }); | ||
1111 | |||
876 | })(window, window.videojs); | 1112 | })(window, window.videojs); | ... | ... |
-
Please register or sign in to post a comment