2cce0c18 by Jon-Carlos Rivera

Multiple audio track support (#681)

* Support for multiple alternate audio tracks

* Separated segment loading logic into a reusable piece of functionality so that we can have more than one segment loader and they can manage the segment fetch behavior unique to each playlist they are loading from

* Introduced the MasterPlaylistController to coordinate the loading of the master playlist and separate the behavior of the player from videojs-contrib-hls.js leaving the latter to be the glue between HLS and the video element's events

* Added the SourceUpdater to manage the asynchronous bahavior of SourceBuffers and MediaSource objects so that we can treat it as a non-blocking work queue

* Added parsing for MediaGroups in master playlists

* Added support for AudioTrackList objects and events

* Add support for every HLS mime type possible (#684)

* Flash live fixes (#682)
1 parent d33786f1
Showing 93 changed files with 2975 additions and 172 deletions
1 sudo: false 1 sudo: required
2 dist: trusty
2 language: node_js 3 language: node_js
3 addons:
4 firefox: "latest"
5 node_js: 4 node_js:
6 - "stable" 5 - "stable"
7 notifications: 6 notifications:
...@@ -14,6 +13,7 @@ notifications: ...@@ -14,6 +13,7 @@ notifications:
14 use_notice: true 13 use_notice: true
15 # Set up a virtual screen for Firefox. 14 # Set up a virtual screen for Firefox.
16 before_script: 15 before_script:
16 - export CHROME_BIN=/usr/bin/google-chrome
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: 19 env:
...@@ -22,4 +22,8 @@ env: ...@@ -22,4 +22,8 @@ env:
22 - secure: AnduYGXka5ft1x7V3SuVYqvlKLvJGhUaRNFdy4UDJr3ZVuwpQjE4TMDG8REmJIJvXfHbh4qY4N1cFSGnXkZ4bH21Xk0v9DLhsxbarKz+X2BvPgXs+Af9EQ6vLEy/5S1vMLxfT5+y+Ec5bVNGOsdUZby8Y21CRzSg6ADN9kwPGlE= 22 - secure: AnduYGXka5ft1x7V3SuVYqvlKLvJGhUaRNFdy4UDJr3ZVuwpQjE4TMDG8REmJIJvXfHbh4qY4N1cFSGnXkZ4bH21Xk0v9DLhsxbarKz+X2BvPgXs+Af9EQ6vLEy/5S1vMLxfT5+y+Ec5bVNGOsdUZby8Y21CRzSg6ADN9kwPGlE=
23 addons: 23 addons:
24 sauce_connect: true 24 sauce_connect: true
25 firefox: latest 25 apt:
26 sources:
27 - google-chrome
28 packages:
29 - google-chrome-stable
......
...@@ -9,6 +9,8 @@ Play back HLS with video.js, even where it's not natively supported. ...@@ -9,6 +9,8 @@ Play back HLS with video.js, even where it's not natively supported.
9 **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 9 **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
10 10
11 - [Getting Started](#getting-started) 11 - [Getting Started](#getting-started)
12 - [Known Issues](#known-issues)
13 - [IE11](#ie11)
12 - [Documentation](#documentation) 14 - [Documentation](#documentation)
13 - [Options](#options) 15 - [Options](#options)
14 - [withCredentials](#withcredentials) 16 - [withCredentials](#withcredentials)
...@@ -44,7 +46,7 @@ and include it in your page along with video.js: ...@@ -44,7 +46,7 @@ and include it in your page along with video.js:
44 type="application/x-mpegURL"> 46 type="application/x-mpegURL">
45 </video> 47 </video>
46 <script src="video.js"></script> 48 <script src="video.js"></script>
47 <script src="videojs-hls.min.js"></script> 49 <script src="videojs-contrib-hls.min.js"></script>
48 <script> 50 <script>
49 var player = videojs('example-video'); 51 var player = videojs('example-video');
50 player.play(); 52 player.play();
...@@ -53,6 +55,16 @@ player.play(); ...@@ -53,6 +55,16 @@ player.play();
53 55
54 Check out our [live example](http://videojs.github.io/videojs-contrib-hls/) if you're having trouble. 56 Check out our [live example](http://videojs.github.io/videojs-contrib-hls/) if you're having trouble.
55 57
58 ## Known Issues
59 Issues that are currenty know about with workarounds. If you want to
60 help find a solution that would be appreciated!
61
62 ### IE11
63 In some IE11 setups there are issues working with it's native HTML
64 SourceBuffers functionality. This leads to various issues, such as
65 videos stopping playback with media decode errors. The known workaround
66 for this issues is to force the player to use flash when running on IE11.
67
56 ## Documentation 68 ## Documentation
57 [HTTP Live Streaming](https://developer.apple.com/streaming/) (HLS) has 69 [HTTP Live Streaming](https://developer.apple.com/streaming/) (HLS) has
58 become a de-facto standard for streaming video on mobile devices 70 become a de-facto standard for streaming video on mobile devices
...@@ -89,8 +101,7 @@ are some highlights: ...@@ -89,8 +101,7 @@ are some highlights:
89 - mid-segment quality switching 101 - mid-segment quality switching
90 - AES-128 segment encryption 102 - AES-128 segment encryption
91 - CEA-608 captions are automatically translated into standard HTML5 103 - CEA-608 captions are automatically translated into standard HTML5
92 [caption text 104 [caption text tracks][0]
93 tracks](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track)
94 - Timed ID3 Metadata is automatically translated into HTML5 metedata 105 - Timed ID3 Metadata is automatically translated into HTML5 metedata
95 text tracks 106 text tracks
96 - Highly customizable adaptive bitrate selection 107 - Highly customizable adaptive bitrate selection
...@@ -98,6 +109,10 @@ are some highlights: ...@@ -98,6 +109,10 @@ are some highlights:
98 - Cross-domain credentials support with CORS 109 - Cross-domain credentials support with CORS
99 - Tight integration with video.js and a philosophy of exposing as much 110 - Tight integration with video.js and a philosophy of exposing as much
100 as possible with standard HTML APIs 111 as possible with standard HTML APIs
112 - Stream with multiple audio tracks and switching to those audio tracks
113 (see the docs folder) for info
114
115 [0]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track
101 116
102 ### Options 117 ### Options
103 118
...@@ -106,11 +121,22 @@ initialization. You can pass in options just like you would for other ...@@ -106,11 +121,22 @@ initialization. You can pass in options just like you would for other
106 parts of video.js: 121 parts of video.js:
107 122
108 ```javascript 123 ```javascript
109 videojs(video, { 124 // html5 for html hls
125 videojs(video, {html5: {
110 hls: { 126 hls: {
111 withCredentials: true 127 withCredentials: true
112 } 128 }
113 }); 129 }});
130
131 // or
132
133 // flash for flash hls
134 videojs(video, {flash: {
135 hls: {
136 withCredentials: true
137 }
138 }});
139
114 ``` 140 ```
115 141
116 #### withCredentials 142 #### withCredentials
...@@ -289,26 +315,8 @@ and most CDNs should have no trouble turning CORS on for your account. ...@@ -289,26 +315,8 @@ and most CDNs should have no trouble turning CORS on for your account.
289 315
290 ### Testing 316 ### Testing
291 317
292 For testing, you can either run `npm test` or use `grunt` directly. 318 For testing, you run `npm run test`. This will run tests using any of the
293 If you use `npm test`, it will only run the karma and end-to-end tests using chrome. 319 browsers that karma-detect-browsers detects on your machine.
294 You can specify which browsers you want the tests to run via grunt's `test` task.
295 You can use either grunt-style arguments or comma separated arguments:
296 ```
297 grunt test:chrome:firefox # grunt-style
298 grunt test:chrome,firefox # comma-separated
299 ```
300 Possible options are:
301 * `chromecanary`
302 * `phantomjs`
303 * `opera`
304 * `chrome`<sup>1</sup>
305 * `safari`<sup>1, 2</sup>
306 * `firefox`<sup>1</sup>
307 * `ie`<sup>1</sup>
308
309
310 _<sup>1</sup>supported end-to-end browsers_<br />
311 _<sup>2</sup>requires the [SafariDriver extension]( https://code.google.com/p/selenium/wiki/SafariDriver) to be installed_
312 320
313 ## Release History 321 ## Release History
314 Check out the [changelog](CHANGELOG.md) for a summary of each release. 322 Check out the [changelog](CHANGELOG.md) for a summary of each release.
......
1 # Multiple Alternative Audio Tracks
2 ## General
3 m3u8 manifests with multiple audio streams will have those streams added to `video.js` in an `AudioTrackList`. The `AudioTrackList` can be accessed using `player.audioTracks()` or `tech.audioTracks()`.
4
5 ## Mapping m3u8 metadata to AudioTracks
6 The mapping between `AudioTrack` and the parsed m3u8 file is fairly straight forward. The table below shows the mapping
7
8 | m3u8 | AudioTrack |
9 |---------|------------|
10 | label | label |
11 | lang | language |
12 | default | enabled |
13 | ??? | kind |
14 | ??? | id |
15
16 As you can see m3u8's do not have a property for `AudioTrack.id`, which means that we let `video.js` randomly generate the id for `AudioTrack`s. This will have no real impact on any part of the system as we do not use the `id` anywhere.
17
18 The other property that does not have a mapping in the m3u8 is `AudioTrack.kind`. It was decided that we would set the `kind` to `main` when `default` is set to `true` and in all other cases we set it to `alternative`
19
20 Below is a basic example of a mapping
21 m3u8 layout
22 ``` JavaScript
23 {
24 'media-group-1': [{
25 'audio-track-1': {
26 default: true,
27 lang: 'eng'
28 },
29 'audio-track-2': {
30 default: true,
31 lang: 'fr'
32 }
33 }]
34 }
35 ```
36
37 Corresponding AudioTrackList when media-group-1 is used (before any tracks have been changed)
38 ``` JavaScript
39 [{
40 label: 'audio-tracks-1',
41 enabled: true,
42 language: 'eng',
43 kind: 'main',
44 id: 'random'
45 }, {
46 label: 'audio-tracks-2',
47 enabled: false,
48 language: 'fr',
49 kind: 'alternative',
50 id: 'random'
51 }]
52 ```
53
54 ## Startup (how tracks are added and used)
55 > AudioTrack & AudioTrackList live in video.js
56
57 1. `HLS` creates a `MasterPlaylistController` and watches for the `loadedmetadata` event
58 1. `HLS` parses the m3u8 using the `MasterPlaylistController`
59 1. `MasterPlaylistController` creates a `PlaylistLoader` for the master m3u8
60 1. `MasterPlaylistController` creates `PlaylistLoader`s for every audio playlist
61 1. `MasterPlaylistController` creates a `SegmentLoader` for the main m3u8
62 1. `MasterPlaylistController` creates a `SegmentLoader` for a potential audio playlist
63 1. `HLS` sees the `loadedmetadata` and finds the currently selected MediaGroup and all the metadata
64 1. `HLS` removes all `AudioTrack`s from the `AudioTrackList`
65 1. `HLS` created `AudioTrack`s for the MediaGroup and adds them to the `AudioTrackList`
66 1. `HLS` calls `MasterPlaylistController`s `useAudio` with no arguments (causes it to use the currently enabled audio)
67 1. `MasterPlaylistController` turns off the current audio `PlaylistLoader` if it is on
68 1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
69 1. `MasterPlaylistController` turns on that `PlaylistLoader` and the Corresponding `SegmentLoader` (master or audio only)
70 1. `MediaSource`/`mux.js` determine how to mux
71
72 ## How tracks are switched
73 > AudioTrack & AudioTrackList live in video.js
74
75 1. `HLS` is setup to watch for the `changed` event on the `AudioTrackList`
76 1. User selects a new `AudioTrack` from a menu (where only one track can be enabled)
77 1. `AudioTrackList` enables the new `Audiotrack` and disables all others
78 1. `AudioTrackList` triggers a `changed` event
79 1. `HLS` sees the `changed` event and finds the newly enabled `AudioTrack`
80 1. `HLS` sends the `label` for the new `AudioTrack` to `MasterPlaylistController`s `useAudio` function
81 1. `MasterPlaylistController` turns off the current audio `PlaylistLoader` if it is on
82 1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
83 1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
84 1. `MasterPlaylistController` turns on that `PlaylistLoader` and the Corresponding `SegmentLoader` (master or audio only)
85 1. `MediaSource`/`mux.js` determine how to mux
86
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta charset="utf-8" />
5 <title>Index</title>
6 </head>
7 <body>
8 <ul>
9 <li><a href="multiple-alternative-audio-tracks">Multiple Alternative Audio Tracks</a></li>
10 </ul>
11 </body>
12 </html>
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta charset="utf-8">
5 <title>Multiple Alternative Audio Tracks - Example</title>
6 <link href="/node_modules/video.js/dist/video-js.css" rel="stylesheet">
7 </head>
8 <body>
9 <h1>Multiple Alternative Audio Tracks</h1>
10 <p>Check the source of this page and the console for detailed information on this example</p>
11 <video id="maat-player" class="video-js vjs-default-skin" controls>
12 <source src="https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8" type="application/x-mpegURL">
13 </video>
14 <div id="audioTrackChoice">
15 <select id="enabled-audio-track" name="enabled-audio-track">
16 </select>
17 </div>
18 <script src="/node_modules/video.js/dist/video.js"></script>
19 <script src="/dist/videojs-contrib-hls.js"></script>
20 <script>
21 (function(window, videojs) {
22 var player = window.player = videojs('maat-player');
23 var audioTrackList = player.audioTracks();
24 var audioTrackSelect = document.getElementById("enabled-audio-track");
25 // watch for a change on the select element
26 // then change the enabled audio track
27 // only one can be enabled at a time, but video.js will
28 // handle that for us, all we need to do is enable the new
29 // track
30 audioTrackSelect.addEventListener('change', function() {
31 var track = audioTrackList[this.selectedIndex];
32 console.log('User switched to track ' + track.label);
33 track.enabled = true;
34 });
35
36 // watch for changes that will be triggered by any change
37 // to enabled on any audio track. Manually or through the
38 // select element
39 audioTrackList.on('change', function() {
40 for (var i = 0; i < audioTrackList.length; i++) {
41 var track = audioTrackList[i];
42 if (track.enabled) {
43 console.log('A new ' + track.label + ' has been enabled!');
44 }
45 }
46 });
47
48 // will be fired twice in this example
49 audioTrackList.on('addtrack', function() {
50 console.log('a track has been added to the audio track list');
51 });
52
53 // will not be fired at all unless you call
54 // audioTrackList.removeTrack(trackObj)
55 // we typically will not need to do this unless we have to load
56 // another video for some reason
57 audioTrackList.on('removetrack', function() {
58 console.log('a track has been removed from the audio track list');
59 });
60
61 // getting all the possible audio tracks from the track list
62 // get all of thier properties
63 // add each track to the select on the page
64 // this is all filled out by HLS when it parses the m3u8
65 player.on('loadeddata', function() {
66 console.log('There are ' + audioTrackList.length + ' audio tracks');
67 for (var i = 0; i < audioTrackList.length; i++) {
68 var track = audioTrackList[i];
69 var option = document.createElement("option");
70 option.text = track.label;
71 if (track.enabled) {
72 option.selected = true;
73 }
74 audioTrackSelect.add(option, i);
75 console.log('Track ' + (i + 1));
76 ['label', 'enabled', 'language', 'id', 'kind'].forEach(function(prop) {
77 console.log(" " + prop + ": " + track[prop]);
78 });
79 }
80 });
81 }(window, window.videojs));
82 </script>
83 </body>
84 </html>
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
43 <ul> 43 <ul>
44 <li><a href="/test/">Run unit tests in browser.</a></li> 44 <li><a href="/test/">Run unit tests in browser.</a></li>
45 <li><a href="/docs/api/">Read generated docs.</a></li> 45 <li><a href="/docs/api/">Read generated docs.</a></li>
46 <li><a href="/examples">Browse Examples</a></li>
46 </ul> 47 </ul>
47 48
48 <script src="/node_modules/video.js/dist/video.js"></script> 49 <script src="/node_modules/video.js/dist/video.js"></script>
...@@ -50,6 +51,7 @@ ...@@ -50,6 +51,7 @@
50 <script> 51 <script>
51 (function(window, videojs) { 52 (function(window, videojs) {
52 var player = window.player = videojs('videojs-contrib-hls-player'); 53 var player = window.player = videojs('videojs-contrib-hls-player');
54
53 // hook up the video switcher 55 // hook up the video switcher
54 var loadUrl = document.getElementById('load-url'); 56 var loadUrl = document.getElementById('load-url');
55 var url = document.getElementById('url'); 57 var url = document.getElementById('url');
......
...@@ -28,8 +28,7 @@ ...@@ -28,8 +28,7 @@
28 "docs:api": "jsdoc src -r -d docs/api", 28 "docs:api": "jsdoc src -r -d docs/api",
29 "docs:toc": "doctoc README.md", 29 "docs:toc": "doctoc README.md",
30 "lint": "vjsstandard", 30 "lint": "vjsstandard",
31 "prestart": "npm-run-all docs build", 31 "start": "npm-run-all -p watch start:*",
32 "start": "npm-run-all -p start:* watch:*",
33 "start:serve": "babel-node scripts/server.js", 32 "start:serve": "babel-node scripts/server.js",
34 "pretest": "npm-run-all lint build", 33 "pretest": "npm-run-all lint build",
35 "test": "karma start test/karma/detected.js", 34 "test": "karma start test/karma/detected.js",
...@@ -40,7 +39,10 @@ ...@@ -40,7 +39,10 @@
40 "preversion": "npm test", 39 "preversion": "npm test",
41 "version": "npm run build", 40 "version": "npm run build",
42 "watch": "npm-run-all -p watch:*", 41 "watch": "npm-run-all -p watch:*",
43 "watch:js": "watchify src/videojs-contrib-hls.js -t babelify -v -o dist/videojs-contrib-hls.js", 42 "watch:docs": "nodemon --watch src/ --exec npm run docs",
43 "watch:js": "npm-run-all -p watch:js:babel watch:js:browserify",
44 "watch:js:babel": "npm run build:js:babel -- --watch",
45 "watch:js:browserify": "watchify . -v -o dist/videojs-contrib-hls.js",
44 "watch:test": "npm-run-all -p watch:test:*", 46 "watch:test": "npm-run-all -p watch:test:*",
45 "watch:test:js": "node scripts/watch-test.js", 47 "watch:test:js": "node scripts/watch-test.js",
46 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"", 48 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
...@@ -73,21 +75,19 @@ ...@@ -73,21 +75,19 @@
73 }, 75 },
74 "files": [ 76 "files": [
75 "CONTRIBUTING.md", 77 "CONTRIBUTING.md",
76 "dist-test/",
77 "dist/", 78 "dist/",
78 "docs/", 79 "docs/",
79 "es5/", 80 "es5/",
80 "index.html", 81 "index.html",
81 "scripts/", 82 "scripts/",
82 "src/", 83 "src/",
83 "test/", 84 "test/"
84 "utils/"
85 ], 85 ],
86 "dependencies": { 86 "dependencies": {
87 "pkcs7": "^0.2.2", 87 "pkcs7": "^0.2.2",
88 "video.js": "^5.2.1", 88 "video.js": "^5.10.1",
89 "videojs-contrib-media-sources": "^3.0.0", 89 "videojs-contrib-media-sources": "^3.1.0",
90 "videojs-swf": "^5.0.0" 90 "videojs-swf": "^5.0.2"
91 }, 91 },
92 "devDependencies": { 92 "devDependencies": {
93 "babel": "^5.8.0", 93 "babel": "^5.8.0",
...@@ -111,6 +111,7 @@ ...@@ -111,6 +111,7 @@
111 "karma-safari-launcher": "^0.1.0", 111 "karma-safari-launcher": "^0.1.0",
112 "lodash-compat": "^3.10.0", 112 "lodash-compat": "^3.10.0",
113 "minimist": "^1.2.0", 113 "minimist": "^1.2.0",
114 "nodemon": "^1.9.1",
114 "npm-run-all": "^1.2.0", 115 "npm-run-all": "^1.2.0",
115 "portscanner": "^1.0.0", 116 "portscanner": "^1.0.0",
116 "qunitjs": "^1.18.0", 117 "qunitjs": "^1.18.0",
......
...@@ -17,7 +17,7 @@ glob('test/**/*.test.js', function(err, files) { ...@@ -17,7 +17,7 @@ glob('test/**/*.test.js', function(err, files) {
17 }; 17 };
18 18
19 b.on('log', function(msg) { 19 b.on('log', function(msg) {
20 process.stdout.write(msg + '\n'); 20 process.stdout.write(msg + ' dist-test/videojs-contrib-hls.js\n');
21 }); 21 });
22 22
23 b.on('update', bundle); 23 b.on('update', bundle);
......
1 /**
2 * @file bin-utils.js
3 */
4
5 /**
6 * convert a TimeRange to text
7 *
8 * @param {TimeRange} range the timerange to use for conversion
9 * @param {Number} i the iterator on the range to convert
10 */
1 const textRange = function(range, i) { 11 const textRange = function(range, i) {
2 return range.start(i) + '-' + range.end(i); 12 return range.start(i) + '-' + range.end(i);
3 }; 13 };
4 14
15 /**
16 * format a number as hex string
17 *
18 * @param {Number} e The number
19 * @param {Number} i the iterator
20 */
5 const formatHexString = function(e, i) { 21 const formatHexString = function(e, i) {
6 let value = e.toString(16); 22 let value = e.toString(16);
7 23
...@@ -14,6 +30,9 @@ const formatAsciiString = function(e) { ...@@ -14,6 +30,9 @@ const formatAsciiString = function(e) {
14 return '.'; 30 return '.';
15 }; 31 };
16 32
33 /**
34 * utils to help dump binary data to the console
35 */
17 const utils = { 36 const utils = {
18 hexDump(data) { 37 hexDump(data) {
19 let bytes = Array.prototype.slice.call(data); 38 let bytes = Array.prototype.slice.call(data);
......
1 /* 1 /**
2 * aes.js 2 * @file decrypter/aes.js
3 * 3 *
4 * This file contains an adaptation of the AES decryption algorithm 4 * This file contains an adaptation of the AES decryption algorithm
5 * from the Standford Javascript Cryptography Library. That work is 5 * from the Standford Javascript Cryptography Library. That work is
...@@ -96,7 +96,7 @@ let aesTables = null; ...@@ -96,7 +96,7 @@ let aesTables = null;
96 * Schedule out an AES key for both encryption and decryption. This 96 * Schedule out an AES key for both encryption and decryption. This
97 * is a low-level class. Use a cipher mode to do bulk encryption. 97 * is a low-level class. Use a cipher mode to do bulk encryption.
98 * 98 *
99 * @constructor 99 * @class AES
100 * @param key {Array} The key as an array of 4, 6 or 8 words. 100 * @param key {Array} The key as an array of 4, 6 or 8 words.
101 */ 101 */
102 export default class AES { 102 export default class AES {
...@@ -184,13 +184,14 @@ export default class AES { ...@@ -184,13 +184,14 @@ export default class AES {
184 184
185 /** 185 /**
186 * Decrypt 16 bytes, specified as four 32-bit words. 186 * Decrypt 16 bytes, specified as four 32-bit words.
187 * @param encrypted0 {number} the first word to decrypt 187 *
188 * @param encrypted1 {number} the second word to decrypt 188 * @param {Number} encrypted0 the first word to decrypt
189 * @param encrypted2 {number} the third word to decrypt 189 * @param {Number} encrypted1 the second word to decrypt
190 * @param encrypted3 {number} the fourth word to decrypt 190 * @param {Number} encrypted2 the third word to decrypt
191 * @param out {Int32Array} the array to write the decrypted words 191 * @param {Number} encrypted3 the fourth word to decrypt
192 * @param {Int32Array} out the array to write the decrypted words
192 * into 193 * into
193 * @param offset {number} the offset into the output array to start 194 * @param {Number} offset the offset into the output array to start
194 * writing results 195 * writing results
195 * @return {Array} The plaintext. 196 * @return {Array} The plaintext.
196 */ 197 */
......
1 /**
2 * @file decrypter/async-stream.js
3 */
1 import Stream from '../stream'; 4 import Stream from '../stream';
2 5
3 /** 6 /**
4 * A wrapper around the Stream class to use setTiemout 7 * A wrapper around the Stream class to use setTiemout
5 * and run stream "jobs" Asynchronously 8 * and run stream "jobs" Asynchronously
9 *
10 * @class AsyncStream
11 * @extends Stream
6 */ 12 */
7 export default class AsyncStream extends Stream { 13 export default class AsyncStream extends Stream {
8 constructor() { 14 constructor() {
...@@ -11,6 +17,12 @@ export default class AsyncStream extends Stream { ...@@ -11,6 +17,12 @@ export default class AsyncStream extends Stream {
11 this.delay = 1; 17 this.delay = 1;
12 this.timeout_ = null; 18 this.timeout_ = null;
13 } 19 }
20
21 /**
22 * process an async job
23 *
24 * @private
25 */
14 processJob_() { 26 processJob_() {
15 this.jobs.shift()(); 27 this.jobs.shift()();
16 if (this.jobs.length) { 28 if (this.jobs.length) {
...@@ -20,6 +32,12 @@ export default class AsyncStream extends Stream { ...@@ -20,6 +32,12 @@ export default class AsyncStream extends Stream {
20 this.timeout_ = null; 32 this.timeout_ = null;
21 } 33 }
22 } 34 }
35
36 /**
37 * push a job into the stream
38 *
39 * @param {Function} job the job to push into the stream
40 */
23 push(job) { 41 push(job) {
24 this.jobs.push(job); 42 this.jobs.push(job);
25 if (!this.timeout_) { 43 if (!this.timeout_) {
......
1 /* 1 /**
2 * decrypter.js 2 * @file decrypter/decrypter.js
3 * 3 *
4 * An asynchronous implementation of AES-128 CBC decryption with 4 * An asynchronous implementation of AES-128 CBC decryption with
5 * PKCS#7 padding. 5 * PKCS#7 padding.
...@@ -20,12 +20,12 @@ const ntoh = function(word) { ...@@ -20,12 +20,12 @@ const ntoh = function(word) {
20 (word >>> 24); 20 (word >>> 24);
21 }; 21 };
22 22
23 /* eslint-disable max-len */
24 /** 23 /**
25 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 24 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding.
26 * @param encrypted {Uint8Array} the encrypted bytes 25 *
27 * @param key {Uint32Array} the bytes of the decryption key 26 * @param {Uint8Array} encrypted the encrypted bytes
28 * @param initVector {Uint32Array} the initialization vector (IV) to 27 * @param {Uint32Array} key the bytes of the decryption key
28 * @param {Uint32Array} initVector the initialization vector (IV) to
29 * use for the first round of CBC. 29 * use for the first round of CBC.
30 * @return {Uint8Array} the decrypted bytes 30 * @return {Uint8Array} the decrypted bytes
31 * 31 *
...@@ -33,7 +33,6 @@ const ntoh = function(word) { ...@@ -33,7 +33,6 @@ const ntoh = function(word) {
33 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 33 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29
34 * @see https://tools.ietf.org/html/rfc2315 34 * @see https://tools.ietf.org/html/rfc2315
35 */ 35 */
36 /* eslint-enable max-len */
37 export const decrypt = function(encrypted, key, initVector) { 36 export const decrypt = function(encrypted, key, initVector) {
38 // word-level access to the encrypted bytes 37 // word-level access to the encrypted bytes
39 let encrypted32 = new Int32Array(encrypted.buffer, 38 let encrypted32 = new Int32Array(encrypted.buffer,
...@@ -106,6 +105,12 @@ export const decrypt = function(encrypted, key, initVector) { ...@@ -106,6 +105,12 @@ export const decrypt = function(encrypted, key, initVector) {
106 * The `Decrypter` class that manages decryption of AES 105 * The `Decrypter` class that manages decryption of AES
107 * data through `AsyncStream` objects and the `decrypt` 106 * data through `AsyncStream` objects and the `decrypt`
108 * function 107 * function
108 *
109 * @param {Uint8Array} encrypted the encrypted bytes
110 * @param {Uint32Array} key the bytes of the decryption key
111 * @param {Uint32Array} initVector the initialization vector (IV) to
112 * @param {Function} done the function to run when done
113 * @class Decrypter
109 */ 114 */
110 export class Decrypter { 115 export class Decrypter {
111 constructor(encrypted, key, initVector, done) { 116 constructor(encrypted, key, initVector, done) {
...@@ -137,6 +142,20 @@ export class Decrypter { ...@@ -137,6 +142,20 @@ export class Decrypter {
137 done(null, unpad(decrypted)); 142 done(null, unpad(decrypted));
138 }); 143 });
139 } 144 }
145
146 /**
147 * a getter for step the maximum number of bytes to process at one time
148 *
149 * @return {Number} the value of step 32000
150 */
151 static get STEP() {
152 // 4 * 8000;
153 return 32000;
154 }
155
156 /**
157 * @private
158 */
140 decryptChunk_(encrypted, key, initVector, decrypted) { 159 decryptChunk_(encrypted, key, initVector, decrypted) {
141 return function() { 160 return function() {
142 let bytes = decrypt(encrypted, key, initVector); 161 let bytes = decrypt(encrypted, key, initVector);
...@@ -146,10 +165,6 @@ export class Decrypter { ...@@ -146,10 +165,6 @@ export class Decrypter {
146 } 165 }
147 } 166 }
148 167
149 // the maximum number of bytes to process at one time
150 // 4 * 8000;
151 Decrypter.STEP = 32000;
152
153 export default { 168 export default {
154 Decrypter, 169 Decrypter,
155 decrypt 170 decrypt
......
1 /* 1 /**
2 * index.js 2 * @file decrypter/index.js
3 * 3 *
4 * Index module to easily import the primary components of AES-128 4 * Index module to easily import the primary components of AES-128
5 * decryption. Like this: 5 * decryption. Like this:
......
1 /**
2 * @file hls-audio-track.js
3 */
4 import {AudioTrack} from 'video.js';
5 import PlaylistLoader from './playlist-loader';
6
7 /**
8 * HlsAudioTrack extends video.js audio tracks but adds HLS
9 * specific data storage such as playlist loaders, mediaGroups
10 * and default/autoselect
11 *
12 * @param {Object} options options to create HlsAudioTrack with
13 * @class HlsAudioTrack
14 * @extends AudioTrack
15 */
16 export default class HlsAudioTrack extends AudioTrack {
17 constructor(options) {
18 super({
19 kind: options.default ? 'main' : 'alternative',
20 enabled: options.default || false,
21 language: options.language,
22 label: options.label
23 });
24
25 this.hls = options.hls;
26 this.autoselect = options.autoselect || false;
27 this.default = options.default || false;
28 this.withCredentials = options.withCredentials || false;
29 this.mediaGroups_ = [];
30 this.addLoader(options.mediaGroup, options.resolvedUri);
31 }
32
33 /**
34 * get a PlaylistLoader from this track given a mediaGroup name
35 *
36 * @param {String} mediaGroup the mediaGroup to get the loader for
37 * @return {PlaylistLoader|Null} the PlaylistLoader or null
38 */
39 getLoader(mediaGroup) {
40 for (let i = 0; i < this.mediaGroups_.length; i++) {
41 let mgl = this.mediaGroups_[i];
42
43 if (mgl.mediaGroup === mediaGroup) {
44 return mgl.loader;
45 }
46 }
47 }
48
49 /**
50 * add a PlaylistLoader given a mediaGroup, and a uri. for a combined track
51 * we store null for the playlistloader
52 *
53 * @param {String} mediaGroup the mediaGroup to get the loader for
54 * @param {String} uri the uri to get the audio track/mediaGroup from
55 */
56 addLoader(mediaGroup, uri = null) {
57 let loader = null;
58
59 if (uri) {
60 // TODO: this should probably happen upstream in Master Playlist
61 // Controller when we can switch PlaylistLoader sources
62 // then we can just store the uri here instead
63 loader = new PlaylistLoader(uri, this.hls, this.withCredentials);
64 }
65 this.mediaGroups_.push({mediaGroup, loader});
66 }
67
68 /**
69 * remove a playlist loader from a track given the mediaGroup
70 *
71 * @param {String} mediaGroup the mediaGroup to remove
72 */
73 removeLoader(mediaGroup) {
74 for (let i = 0; i < this.mediaGroups_.length; i++) {
75 let mgl = this.mediaGroups_[i];
76
77 if (mgl.mediaGroup === mediaGroup) {
78 if (mgl.loader) {
79 mgl.loader.dispose();
80 }
81 this.mediaGroups_.splice(i, 1);
82 return;
83 }
84 }
85 }
86
87 /**
88 * Dispose of this audio track and
89 * the playlist loader that it holds inside
90 */
91 dispose() {
92 let i = this.mediaGroups_.length;
93
94 while (i--) {
95 this.removeLoader(this.mediaGroups_[i].mediaGroup);
96 }
97 }
98 }
1 /** 1 /**
2 * @file m3u8/index.js
3 *
2 * Utilities for parsing M3U8 files. If the entire manifest is available, 4 * Utilities for parsing M3U8 files. If the entire manifest is available,
3 * `Parser` will create an object representation with enough detail for managing 5 * `Parser` will create an object representation with enough detail for managing
4 * playback. `ParseStream` and `LineStream` are lower-level parsing primitives 6 * playback. `ParseStream` and `LineStream` are lower-level parsing primitives
......
1 /**
2 * @file m3u8/line-stream.js
3 */
1 import Stream from '../stream'; 4 import Stream from '../stream';
5
2 /** 6 /**
3 * A stream that buffers string input and generates a `data` event for each 7 * A stream that buffers string input and generates a `data` event for each
4 * line. 8 * line.
9 *
10 * @class LineStream
11 * @extends Stream
5 */ 12 */
6 export default class LineStream extends Stream { 13 export default class LineStream extends Stream {
7 constructor() { 14 constructor() {
...@@ -11,7 +18,8 @@ export default class LineStream extends Stream { ...@@ -11,7 +18,8 @@ export default class LineStream extends Stream {
11 18
12 /** 19 /**
13 * Add new data to be parsed. 20 * Add new data to be parsed.
14 * @param data {string} the text to process 21 *
22 * @param {String} data the text to process
15 */ 23 */
16 push(data) { 24 push(data) {
17 let nextNewline; 25 let nextNewline;
......
1 /**
2 * @file m3u8/parse-stream.js
3 */
1 import Stream from '../stream'; 4 import Stream from '../stream';
2 5
3 // "forgiving" attribute list psuedo-grammar: 6 /**
4 // attributes -> keyvalue (',' keyvalue)* 7 * "forgiving" attribute list psuedo-grammar:
5 // keyvalue -> key '=' value 8 * attributes -> keyvalue (',' keyvalue)*
6 // key -> [^=]* 9 * keyvalue -> key '=' value
7 // value -> '"' [^"]* '"' | [^,]* 10 * key -> [^=]*
11 * value -> '"' [^"]* '"' | [^,]*
12 */
8 const attributeSeparator = function() { 13 const attributeSeparator = function() {
9 let key = '[^=]*'; 14 let key = '[^=]*';
10 let value = '"[^"]*"|[^,]*'; 15 let value = '"[^"]*"|[^,]*';
...@@ -13,6 +18,11 @@ const attributeSeparator = function() { ...@@ -13,6 +18,11 @@ const attributeSeparator = function() {
13 return new RegExp('(?:^|,)(' + keyvalue + ')'); 18 return new RegExp('(?:^|,)(' + keyvalue + ')');
14 }; 19 };
15 20
21 /**
22 * Parse attributes from a line given the seperator
23 *
24 * @param {String} attributes the attibute line to parse
25 */
16 const parseAttributes = function(attributes) { 26 const parseAttributes = function(attributes) {
17 // split the string using attributes as the separator 27 // split the string using attributes as the separator
18 let attrs = attributes.split(attributeSeparator()); 28 let attrs = attributes.split(attributeSeparator());
...@@ -57,6 +67,9 @@ const parseAttributes = function(attributes) { ...@@ -57,6 +67,9 @@ const parseAttributes = function(attributes) {
57 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized 67 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
58 * tags are given the tag type `unknown` and a single additional property 68 * tags are given the tag type `unknown` and a single additional property
59 * `data` with the remainder of the input. 69 * `data` with the remainder of the input.
70 *
71 * @class ParseStream
72 * @extends Stream
60 */ 73 */
61 export default class ParseStream extends Stream { 74 export default class ParseStream extends Stream {
62 constructor() { 75 constructor() {
...@@ -65,7 +78,8 @@ export default class ParseStream extends Stream { ...@@ -65,7 +78,8 @@ export default class ParseStream extends Stream {
65 78
66 /** 79 /**
67 * Parses an additional line of input. 80 * Parses an additional line of input.
68 * @param line {string} a single line of an M3U8 file to parse 81 *
82 * @param {String} line a single line of an M3U8 file to parse
69 */ 83 */
70 push(line) { 84 push(line) {
71 let match; 85 let match;
...@@ -254,6 +268,18 @@ export default class ParseStream extends Stream { ...@@ -254,6 +268,18 @@ export default class ParseStream extends Stream {
254 this.trigger('data', event); 268 this.trigger('data', event);
255 return; 269 return;
256 } 270 }
271 match = (/^#EXT-X-MEDIA:?(.*)$/).exec(line);
272 if (match) {
273 event = {
274 type: 'tag',
275 tagType: 'media'
276 };
277 if (match[1]) {
278 event.attributes = parseAttributes(match[1]);
279 }
280 this.trigger('data', event);
281 return;
282 }
257 match = (/^#EXT-X-ENDLIST/).exec(line); 283 match = (/^#EXT-X-ENDLIST/).exec(line);
258 if (match) { 284 if (match) {
259 this.trigger('data', { 285 this.trigger('data', {
......
1 /**
2 * @file m3u8/parser.js
3 */
1 import Stream from '../stream' ; 4 import Stream from '../stream' ;
2 import LineStream from './line-stream'; 5 import LineStream from './line-stream';
3 import ParseStream from './parse-stream'; 6 import ParseStream from './parse-stream';
...@@ -20,6 +23,9 @@ import {mergeOptions} from 'video.js'; ...@@ -20,6 +23,9 @@ import {mergeOptions} from 'video.js';
20 * underlying input is somewhat nonsensical. It emits `info` and `warning` 23 * underlying input is somewhat nonsensical. It emits `info` and `warning`
21 * events during the parse if it encounters input that seems invalid or 24 * events during the parse if it encounters input that seems invalid or
22 * requires some property of the manifest object to be defaulted. 25 * requires some property of the manifest object to be defaulted.
26 *
27 * @class Parser
28 * @extends Stream
23 */ 29 */
24 export default class Parser extends Stream { 30 export default class Parser extends Stream {
25 constructor() { 31 constructor() {
...@@ -34,6 +40,14 @@ export default class Parser extends Stream { ...@@ -34,6 +40,14 @@ export default class Parser extends Stream {
34 let currentUri = {}; 40 let currentUri = {};
35 let key; 41 let key;
36 let noop = function() {}; 42 let noop = function() {};
43 let defaultMediaGroups = {
44 'AUDIO': {},
45 'VIDEO': {},
46 'CLOSED-CAPTIONS': {},
47 'SUBTITLES': {}
48 };
49 // group segments into numbered timelines delineated by discontinuities
50 let currentTimeline = 0;
37 51
38 // the manifest is empty until the parse stream begins delivering data 52 // the manifest is empty until the parse stream begins delivering data
39 this.manifest = { 53 this.manifest = {
...@@ -43,6 +57,9 @@ export default class Parser extends Stream { ...@@ -43,6 +57,9 @@ export default class Parser extends Stream {
43 57
44 // update the manifest with the m3u8 entry from the parse stream 58 // update the manifest with the m3u8 entry from the parse stream
45 this.parseStream.on('data', function(entry) { 59 this.parseStream.on('data', function(entry) {
60 let mediaGroup;
61 let rendition;
62
46 ({ 63 ({
47 tag() { 64 tag() {
48 // switch based on the tag type 65 // switch based on the tag type
...@@ -96,7 +113,6 @@ export default class Parser extends Stream { ...@@ -96,7 +113,6 @@ export default class Parser extends Stream {
96 } 113 }
97 114
98 this.manifest.segments = uris; 115 this.manifest.segments = uris;
99
100 }, 116 },
101 key() { 117 key() {
102 if (!entry.attributes) { 118 if (!entry.attributes) {
...@@ -149,6 +165,7 @@ export default class Parser extends Stream { ...@@ -149,6 +165,7 @@ export default class Parser extends Stream {
149 return; 165 return;
150 } 166 }
151 this.manifest.discontinuitySequence = entry.number; 167 this.manifest.discontinuitySequence = entry.number;
168 currentTimeline = entry.number;
152 }, 169 },
153 'playlist-type'() { 170 'playlist-type'() {
154 if (!(/VOD|EVENT/).test(entry.playlistType)) { 171 if (!(/VOD|EVENT/).test(entry.playlistType)) {
...@@ -161,6 +178,8 @@ export default class Parser extends Stream { ...@@ -161,6 +178,8 @@ export default class Parser extends Stream {
161 }, 178 },
162 'stream-inf'() { 179 'stream-inf'() {
163 this.manifest.playlists = uris; 180 this.manifest.playlists = uris;
181 this.manifest.mediaGroups =
182 this.manifest.mediaGroups || defaultMediaGroups;
164 183
165 if (!entry.attributes) { 184 if (!entry.attributes) {
166 this.trigger('warn', { 185 this.trigger('warn', {
...@@ -175,7 +194,48 @@ export default class Parser extends Stream { ...@@ -175,7 +194,48 @@ export default class Parser extends Stream {
175 currentUri.attributes = mergeOptions(currentUri.attributes, 194 currentUri.attributes = mergeOptions(currentUri.attributes,
176 entry.attributes); 195 entry.attributes);
177 }, 196 },
197 media() {
198 this.manifest.mediaGroups =
199 this.manifest.mediaGroups || defaultMediaGroups;
200
201 if (!(entry.attributes &&
202 entry.attributes.TYPE &&
203 entry.attributes['GROUP-ID'] &&
204 entry.attributes.NAME)) {
205 this.trigger('warn', {
206 message: 'ignoring incomplete or missing media group'
207 });
208 return;
209 }
210
211 // find the media group, creating defaults as necessary
212 let mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
213
214 mediaGroupType[entry.attributes['GROUP-ID']] =
215 mediaGroupType[entry.attributes['GROUP-ID']] || {};
216 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']];
217
218 // collect the rendition metadata
219 rendition = {
220 default: (/yes/i).test(entry.attributes.DEFAULT)
221 };
222 if (rendition.default) {
223 rendition.autoselect = true;
224 } else {
225 rendition.autoselect = (/yes/i).test(entry.attributes.AUTOSELECT);
226 }
227 if (entry.attributes.LANGUAGE) {
228 rendition.language = entry.attributes.LANGUAGE;
229 }
230 if (entry.attributes.URI) {
231 rendition.uri = entry.attributes.URI;
232 }
233
234 // insert the new rendition
235 mediaGroup[entry.attributes.NAME] = rendition;
236 },
178 discontinuity() { 237 discontinuity() {
238 currentTimeline += 1;
179 currentUri.discontinuity = true; 239 currentUri.discontinuity = true;
180 this.manifest.discontinuityStarts.push(uris.length); 240 this.manifest.discontinuityStarts.push(uris.length);
181 }, 241 },
...@@ -215,6 +275,7 @@ export default class Parser extends Stream { ...@@ -215,6 +275,7 @@ export default class Parser extends Stream {
215 if (key) { 275 if (key) {
216 currentUri.key = key; 276 currentUri.key = key;
217 } 277 }
278 currentUri.timeline = currentTimeline;
218 279
219 // prepare for the next URI 280 // prepare for the next URI
220 currentUri = {}; 281 currentUri = {};
...@@ -229,7 +290,8 @@ export default class Parser extends Stream { ...@@ -229,7 +290,8 @@ export default class Parser extends Stream {
229 290
230 /** 291 /**
231 * Parse the input string and update the manifest object. 292 * Parse the input string and update the manifest object.
232 * @param chunk {string} a potentially incomplete portion of the manifest 293 *
294 * @param {String} chunk a potentially incomplete portion of the manifest
233 */ 295 */
234 push(chunk) { 296 push(chunk) {
235 this.lineStream.push(chunk); 297 this.lineStream.push(chunk);
...@@ -246,4 +308,3 @@ export default class Parser extends Stream { ...@@ -246,4 +308,3 @@ export default class Parser extends Stream {
246 } 308 }
247 309
248 } 310 }
249
......
1 /** 1 /**
2 * @file playlist.js
3 *
2 * Playlist related utilities. 4 * Playlist related utilities.
3 */ 5 */
4 import {createTimeRange} from 'video.js'; 6 import {createTimeRange} from 'video.js';
...@@ -13,6 +15,14 @@ let Playlist = { ...@@ -13,6 +15,14 @@ let Playlist = {
13 UNSAFE_LIVE_SEGMENTS: 3 15 UNSAFE_LIVE_SEGMENTS: 3
14 }; 16 };
15 17
18 /**
19 * walk backward until we find a duration we can use
20 * or return a failure
21 *
22 * @param {Playlist} playlist the playlist to walk through
23 * @param {Number} endSequence the mediaSequence to stop walking on
24 */
25
16 const backwardDuration = function(playlist, endSequence) { 26 const backwardDuration = function(playlist, endSequence) {
17 let result = 0; 27 let result = 0;
18 let i = endSequence - playlist.mediaSequence; 28 let i = endSequence - playlist.mediaSequence;
...@@ -48,6 +58,13 @@ const backwardDuration = function(playlist, endSequence) { ...@@ -48,6 +58,13 @@ const backwardDuration = function(playlist, endSequence) {
48 return { result, precise: false }; 58 return { result, precise: false };
49 }; 59 };
50 60
61 /**
62 * walk forward until we find a duration we can use
63 * or return a failure
64 *
65 * @param {Playlist} playlist the playlist to walk through
66 * @param {Number} endSequence the mediaSequence to stop walking on
67 */
51 const forwardDuration = function(playlist, endSequence) { 68 const forwardDuration = function(playlist, endSequence) {
52 let result = 0; 69 let result = 0;
53 let segment; 70 let segment;
...@@ -83,13 +100,15 @@ const forwardDuration = function(playlist, endSequence) { ...@@ -83,13 +100,15 @@ const forwardDuration = function(playlist, endSequence) {
83 * playlist. The duration of a subinterval of the available segments 100 * playlist. The duration of a subinterval of the available segments
84 * may be calculated by specifying an end index. 101 * may be calculated by specifying an end index.
85 * 102 *
86 * @param playlist {object} a media playlist object 103 * @param {Object} playlist a media playlist object
87 * @param endSequence {number} (optional) an exclusive upper boundary 104 * @param {Number=} endSequence an exclusive upper boundary
88 * for the playlist. Defaults to playlist length. 105 * for the playlist. Defaults to playlist length.
89 * @return {number} the duration between the first available segment 106 * @param {Number} expired the amount of time that has dropped
107 * off the front of the playlist in a live scenario
108 * @return {Number} the duration between the first available segment
90 * and end index. 109 * and end index.
91 */ 110 */
92 const intervalDuration = function(playlist, endSequence) { 111 const intervalDuration = function(playlist, endSequence, expired) {
93 let backward; 112 let backward;
94 let forward; 113 let forward;
95 114
...@@ -120,7 +139,7 @@ const intervalDuration = function(playlist, endSequence) { ...@@ -120,7 +139,7 @@ const intervalDuration = function(playlist, endSequence) {
120 } 139 }
121 140
122 // return the less-precise, playlist-based duration estimate 141 // return the less-precise, playlist-based duration estimate
123 return backward.result; 142 return backward.result + expired;
124 }; 143 };
125 144
126 /** 145 /**
...@@ -128,23 +147,23 @@ const intervalDuration = function(playlist, endSequence) { ...@@ -128,23 +147,23 @@ const intervalDuration = function(playlist, endSequence) {
128 * are specified, the duration will be for the subset of the media 147 * are specified, the duration will be for the subset of the media
129 * timeline between those two indices. The total duration for live 148 * timeline between those two indices. The total duration for live
130 * playlists is always Infinity. 149 * playlists is always Infinity.
131 * @param playlist {object} a media playlist object 150 *
132 * @param endSequence {number} (optional) an exclusive upper 151 * @param {Object} playlist a media playlist object
152 * @param {Number=} endSequence an exclusive upper
133 * boundary for the playlist. Defaults to the playlist media 153 * boundary for the playlist. Defaults to the playlist media
134 * sequence number plus its length. 154 * sequence number plus its length.
135 * @param includeTrailingTime {boolean} (optional) if false, the 155 * @param {Number=} expired the amount of time that has
136 * interval between the final segment and the subsequent segment 156 * dropped off the front of the playlist in a live scenario
137 * will not be included in the result 157 * @return {Number} the duration between the start index and end
138 * @return {number} the duration between the start index and end
139 * index. 158 * index.
140 */ 159 */
141 export const duration = function(playlist, endSequence, includeTrailingTime) { 160 export const duration = function(playlist, endSequence, expired) {
142 if (!playlist) { 161 if (!playlist) {
143 return 0; 162 return 0;
144 } 163 }
145 164
146 if (typeof includeTrailingTime === 'undefined') { 165 if (typeof expired !== 'number') {
147 includeTrailingTime = true; 166 expired = 0;
148 } 167 }
149 168
150 // if a slice of the total duration is not requested, use 169 // if a slice of the total duration is not requested, use
...@@ -164,7 +183,7 @@ export const duration = function(playlist, endSequence, includeTrailingTime) { ...@@ -164,7 +183,7 @@ export const duration = function(playlist, endSequence, includeTrailingTime) {
164 // calculate the total duration based on the segment durations 183 // calculate the total duration based on the segment durations
165 return intervalDuration(playlist, 184 return intervalDuration(playlist,
166 endSequence, 185 endSequence,
167 includeTrailingTime); 186 expired);
168 }; 187 };
169 188
170 /** 189 /**
...@@ -174,16 +193,24 @@ export const duration = function(playlist, endSequence, includeTrailingTime) { ...@@ -174,16 +193,24 @@ export const duration = function(playlist, endSequence, includeTrailingTime) {
174 * seekable implementation for live streams would need to offset 193 * seekable implementation for live streams would need to offset
175 * these values by the duration of content that has expired from the 194 * these values by the duration of content that has expired from the
176 * stream. 195 * stream.
177 * @param playlist {object} a media playlist object 196 *
197 * @param {Object} playlist a media playlist object
198 * @param {Number=} expired the amount of time that has
199 * dropped off the front of the playlist in a live scenario
178 * @return {TimeRanges} the periods of time that are valid targets 200 * @return {TimeRanges} the periods of time that are valid targets
179 * for seeking 201 * for seeking
180 */ 202 */
181 export const seekable = function(playlist) { 203 export const seekable = function(playlist, expired) {
182 let start; 204 let start;
183 let end; 205 let end;
206 let endSequence;
207
208 if (typeof expired !== 'number') {
209 expired = 0;
210 }
184 211
185 // without segments, there are no seekable ranges 212 // without segments, there are no seekable ranges
186 if (!playlist.segments) { 213 if (!playlist || !playlist.segments) {
187 return createTimeRange(); 214 return createTimeRange();
188 } 215 }
189 // when the playlist is complete, the entire duration is seekable 216 // when the playlist is complete, the entire duration is seekable
...@@ -194,15 +221,142 @@ export const seekable = function(playlist) { ...@@ -194,15 +221,142 @@ export const seekable = function(playlist) {
194 // live playlists should not expose three segment durations worth 221 // live playlists should not expose three segment durations worth
195 // of content from the end of the playlist 222 // of content from the end of the playlist
196 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 223 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
197 start = intervalDuration(playlist, playlist.mediaSequence); 224 start = intervalDuration(playlist, playlist.mediaSequence, expired);
225 endSequence = Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS);
198 end = intervalDuration(playlist, 226 end = intervalDuration(playlist,
199 playlist.mediaSequence + 227 playlist.mediaSequence + endSequence,
200 Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS)); 228 expired);
201 return createTimeRange(start, end); 229 return createTimeRange(start, end);
202 }; 230 };
203 231
232 /**
233 * Determine the index of the segment that contains a specified
234 * playback position in a media playlist.
235 *
236 * @param {Object} playlist the media playlist to query
237 * @param {Number} time The number of seconds since the earliest
238 * possible position to determine the containing segment for
239 * @param {Number=} expired the duration of content, in
240 * seconds, that has been removed from this playlist because it
241 * expired
242 * @return {Number} The number of the media segment that contains
243 * that time position.
244 */
245 export const getMediaIndexForTime_ = function(playlist, time, expired) {
246 let i;
247 let segment;
248 let originalTime = time;
249 let numSegments = playlist.segments.length;
250 let lastSegment = numSegments - 1;
251 let startIndex;
252 let endIndex;
253 let knownStart;
254 let knownEnd;
255
256 if (!playlist) {
257 return 0;
258 }
259
260 // when the requested position is earlier than the current set of
261 // segments, return the earliest segment index
262 if (time < 0) {
263 return 0;
264 }
265
266 expired = expired || 0;
267
268 // find segments with known timing information that bound the
269 // target time
270 for (i = 0; i < numSegments; i++) {
271 segment = playlist.segments[i];
272 if (segment.end) {
273 if (segment.end > time) {
274 knownEnd = segment.end;
275 endIndex = i;
276 break;
277 } else {
278 knownStart = segment.end;
279 startIndex = i + 1;
280 }
281 }
282 }
283
284 // time was equal to or past the end of the last segment in the playlist
285 if (startIndex === numSegments) {
286 return numSegments;
287 }
288
289 // use the bounds we just found and playlist information to
290 // estimate the segment that contains the time we are looking for
291 if (typeof startIndex !== 'undefined') {
292 // We have a known-start point that is before our desired time so
293 // walk from that point forwards
294 time = time - knownStart;
295 for (i = startIndex; i < (endIndex || numSegments); i++) {
296 segment = playlist.segments[i];
297 time -= segment.duration;
298
299 if (time < 0) {
300 return i;
301 }
302 }
303
304 if (i >= endIndex) {
305 // We haven't found a segment but we did hit a known end point
306 // so fallback to interpolating between the segment index
307 // based on the known span of the timeline we are dealing with
308 // and the number of segments inside that span
309 return startIndex + Math.floor(
310 ((originalTime - knownStart) / (knownEnd - knownStart)) *
311 (endIndex - startIndex));
312 }
313
314 // We _still_ haven't found a segment so load the last one
315 return lastSegment;
316 } else if (typeof endIndex !== 'undefined') {
317 // We _only_ have a known-end point that is after our desired time so
318 // walk from that point backwards
319 time = knownEnd - time;
320 for (i = endIndex; i >= 0; i--) {
321 segment = playlist.segments[i];
322 time -= segment.duration;
323
324 if (time < 0) {
325 return i;
326 }
327 }
328
329 // We haven't found a segment so load the first one if time is zero
330 if (time === 0) {
331 return 0;
332 }
333 return -1;
334 }
335 // We known nothing so walk from the front of the playlist,
336 // subtracting durations until we find a segment that contains
337 // time and return it
338 time = time - expired;
339
340 if (time < 0) {
341 return -1;
342 }
343
344 for (i = 0; i < numSegments; i++) {
345 segment = playlist.segments[i];
346 time -= segment.duration;
347 if (time < 0) {
348 return i;
349 }
350 }
351 // We are out of possible candidates so load the last one...
352 // The last one is the least likely to overlap a buffer and therefore
353 // the one most likely to tell us something about the timeline
354 return lastSegment;
355 };
356
204 Playlist.duration = duration; 357 Playlist.duration = duration;
205 Playlist.seekable = seekable; 358 Playlist.seekable = seekable;
359 Playlist.getMediaIndexForTime_ = getMediaIndexForTime_;
206 360
207 // exports 361 // exports
208 export default Playlist; 362 export default Playlist;
......
1 /**
2 * ranges
3 *
4 * Utilities for working with TimeRanges.
5 *
6 */
7
8 import videojs from 'video.js';
9
10 // Fudge factor to account for TimeRanges rounding
11 const TIME_FUDGE_FACTOR = 1 / 30;
12
13 const filterRanges = function(timeRanges, predicate) {
14 let results = [];
15 let i;
16
17 if (timeRanges && timeRanges.length) {
18 // Search for ranges that match the predicate
19 for (i = 0; i < timeRanges.length; i++) {
20 if (predicate(timeRanges.start(i), timeRanges.end(i))) {
21 results.push([timeRanges.start(i), timeRanges.end(i)]);
22 }
23 }
24 }
25
26 return videojs.createTimeRanges(results);
27 };
28
29 /**
30 * Attempts to find the buffered TimeRange that contains the specified
31 * time.
32 * @param {TimeRanges} buffered - the TimeRanges object to query
33 * @param {number} time - the time to filter on.
34 * @returns {TimeRanges} a new TimeRanges object
35 */
36 const findRange = function(buffered, time) {
37 return filterRanges(buffered, function(start, end) {
38 return start - TIME_FUDGE_FACTOR <= time &&
39 end + TIME_FUDGE_FACTOR >= time;
40 });
41 };
42
43 /**
44 * Returns the TimeRanges that begin at or later than the specified
45 * time.
46 * @param {TimeRanges} timeRanges - the TimeRanges object to query
47 * @param {number} time - the time to filter on.
48 * @returns {TimeRanges} a new TimeRanges object.
49 */
50 const findNextRange = function(timeRanges, time) {
51 return filterRanges(timeRanges, function(start) {
52 return start - TIME_FUDGE_FACTOR >= time;
53 });
54 };
55
56 /**
57 * Search for a likely end time for the segment that was just appened
58 * based on the state of the `buffered` property before and after the
59 * append. If we fin only one such uncommon end-point return it.
60 * @param {TimeRanges} original - the buffered time ranges before the update
61 * @param {TimeRanges} update - the buffered time ranges after the update
62 * @returns {Number|null} the end time added between `original` and `update`,
63 * or null if one cannot be unambiguously determined.
64 */
65 const findSoleUncommonTimeRangesEnd = function(original, update) {
66 let i;
67 let start;
68 let end;
69 let result = [];
70 let edges = [];
71
72 // In order to qualify as a possible candidate, the end point must:
73 // 1) Not have already existed in the `original` ranges
74 // 2) Not result from the shrinking of a range that already existed
75 // in the `original` ranges
76 // 3) Not be contained inside of a range that existed in `original`
77 const overlapsCurrentEnd = function(span) {
78 return (span[0] <= end && span[1] >= end);
79 };
80
81 if (original) {
82 // Save all the edges in the `original` TimeRanges object
83 for (i = 0; i < original.length; i++) {
84 start = original.start(i);
85 end = original.end(i);
86
87 edges.push([start, end]);
88 }
89 }
90
91 if (update) {
92 // Save any end-points in `update` that are not in the `original`
93 // TimeRanges object
94 for (i = 0; i < update.length; i++) {
95 start = update.start(i);
96 end = update.end(i);
97
98 if (edges.some(overlapsCurrentEnd)) {
99 continue;
100 }
101
102 // at this point it must be a unique non-shrinking end edge
103 result.push(end);
104 }
105 }
106
107 // we err on the side of caution and return null if didn't find
108 // exactly *one* differing end edge in the search above
109 if (result.length !== 1) {
110 return null;
111 }
112
113 return result[0];
114 };
115
116 /**
117 * Calculate the intersection of two TimeRanges
118 * @param {TimeRanges} bufferA
119 * @param {TimeRanges} bufferB
120 * @returns {TimeRanges} The interesection of `bufferA` with `bufferB`
121 */
122 const bufferIntersection = function(bufferA, bufferB) {
123 let start = null;
124 let end = null;
125 let arity = 0;
126 let extents = [];
127 let ranges = [];
128
129 if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
130 return videojs.createTimeRange();
131 }
132
133 // Handle the case where we have both buffers and create an
134 // intersection of the two
135 let count = bufferA.length;
136
137 // A) Gather up all start and end times
138 while (count--) {
139 extents.push({time: bufferA.start(count), type: 'start'});
140 extents.push({time: bufferA.end(count), type: 'end'});
141 }
142 count = bufferB.length;
143 while (count--) {
144 extents.push({time: bufferB.start(count), type: 'start'});
145 extents.push({time: bufferB.end(count), type: 'end'});
146 }
147 // B) Sort them by time
148 extents.sort(function(a, b) {
149 return a.time - b.time;
150 });
151
152 // C) Go along one by one incrementing arity for start and decrementing
153 // arity for ends
154 for (count = 0; count < extents.length; count++) {
155 if (extents[count].type === 'start') {
156 arity++;
157
158 // D) If arity is ever incremented to 2 we are entering an
159 // overlapping range
160 if (arity === 2) {
161 start = extents[count].time;
162 }
163 } else if (extents[count].type === 'end') {
164 arity--;
165
166 // E) If arity is ever decremented to 1 we leaving an
167 // overlapping range
168 if (arity === 1) {
169 end = extents[count].time;
170 }
171 }
172
173 // F) Record overlapping ranges
174 if (start !== null && end !== null) {
175 ranges.push([start, end]);
176 start = null;
177 end = null;
178 }
179 }
180
181 return videojs.createTimeRanges(ranges);
182 };
183
184 /**
185 * Calculates the percentage of `segmentRange` that overlaps the
186 * `buffered` time ranges.
187 * @param {TimeRanges} segmentRange - the time range that the segment covers
188 * @param {TimeRanges} buffered - the currently buffered time ranges
189 * @returns {Number} percent of the segment currently buffered
190 */
191 const calculateBufferedPercent = function(segmentRange, buffered) {
192 let segmentDuration = segmentRange.end(0) - segmentRange.start(0);
193 let intersection = bufferIntersection(segmentRange, buffered);
194 let overlapDuration = 0;
195 let count = intersection.length;
196
197 while (count--) {
198 overlapDuration += intersection.end(count) - intersection.start(count);
199 }
200
201 return (overlapDuration / segmentDuration) * 100;
202 };
203
204 export default {
205 findRange,
206 findNextRange,
207 findSoleUncommonTimeRangesEnd,
208 calculateBufferedPercent,
209 TIME_FUDGE_FACTOR
210 };
1 /**
2 * @file resolve-url.js
3 */
1 import document from 'global/document'; 4 import document from 'global/document';
2 /* eslint-disable max-len */
3 /** 5 /**
4 * Constructs a new URI by interpreting a path relative to another 6 * Constructs a new URI by interpreting a path relative to another
5 * URI. 7 * URI.
6 * @param basePath {string} a relative or absolute URI 8 *
7 * @param path {string} a path part to combine with the base
8 * @return {string} a URI that is equivalent to composing `base`
9 * with `path`
10 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue 9 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
10 * @param {String} basePath a relative or absolute URI
11 * @param {String} path a path part to combine with the base
12 * @return {String} a URI that is equivalent to composing `base`
13 * with `path`
11 */ 14 */
12 /* eslint-enable max-len */
13 const resolveUrl = function(basePath, path) { 15 const resolveUrl = function(basePath, path) {
14 // use the base element to get the browser to handle URI resolution 16 // use the base element to get the browser to handle URI resolution
15 let oldBase = document.querySelector('base'); 17 let oldBase = document.querySelector('base');
......
1 /**
2 * @file source-updater.js
3 */
4 import videojs from 'video.js';
5
6 /**
7 * A queue of callbacks to be serialized and applied when a
8 * MediaSource and its associated SourceBuffers are not in the
9 * updating state. It is used by the segment loader to update the
10 * underlying SourceBuffers when new data is loaded, for instance.
11 *
12 * @class SourceUpdater
13 * @param {MediaSource} mediaSource the MediaSource to create the
14 * SourceBuffer from
15 * @param {String} mimeType the desired MIME type of the underlying
16 * SourceBuffer
17 */
18 export default class SourceUpdater {
19 constructor(mediaSource, mimeType) {
20 let createSourceBuffer = () => {
21 this.sourceBuffer_ = mediaSource.addSourceBuffer(mimeType);
22
23 // run completion handlers and process callbacks as updateend
24 // events fire
25 this.sourceBuffer_.addEventListener('updateend', () => {
26 let pendingCallback = this.pendingCallback_;
27
28 this.pendingCallback_ = null;
29
30 if (pendingCallback) {
31 pendingCallback();
32 }
33 });
34 this.sourceBuffer_.addEventListener('updateend',
35 this.runCallback_.bind(this));
36
37 this.runCallback_();
38 };
39
40 this.callbacks_ = [];
41 this.pendingCallback_ = null;
42 this.timestampOffset_ = 0;
43 this.mediaSource = mediaSource;
44
45 if (mediaSource.readyState === 'closed') {
46 mediaSource.addEventListener('sourceopen', createSourceBuffer);
47 } else {
48 createSourceBuffer();
49 }
50 }
51
52 /**
53 * Aborts the current segment and resets the segment parser.
54 *
55 * @param {Function} done function to call when done
56 * @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void
57 */
58 abort(done) {
59 this.queueCallback_(() => {
60 this.sourceBuffer_.abort();
61 }, done);
62 }
63
64 /**
65 * Queue an update to append an ArrayBuffer.
66 *
67 * @param {ArrayBuffer} bytes
68 * @param {Function} done the function to call when done
69 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data
70 */
71 appendBuffer(bytes, done) {
72 this.queueCallback_(() => {
73 this.sourceBuffer_.appendBuffer(bytes);
74 }, done);
75 }
76
77 /**
78 * Indicates what TimeRanges are buffered in the managed SourceBuffer.
79 *
80 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered
81 */
82 buffered() {
83 if (!this.sourceBuffer_) {
84 return videojs.createTimeRanges();
85 }
86 return this.sourceBuffer_.buffered;
87 }
88
89 /**
90 * Queue an update to set the duration.
91 *
92 * @param {Double} duration what to set the duration to
93 * @see http://www.w3.org/TR/media-source/#widl-MediaSource-duration
94 */
95 duration(duration) {
96 this.queueCallback_(() => {
97 this.sourceBuffer_.duration = duration;
98 });
99 }
100
101 /**
102 * Queue an update to remove a time range from the buffer.
103 *
104 * @param {Number} start where to start the removal
105 * @param {Number} end where to end the removal
106 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
107 */
108 remove(start, end) {
109 this.queueCallback_(() => {
110 this.sourceBuffer_.remove(start, end);
111 });
112 }
113
114 /**
115 * wether the underlying sourceBuffer is updating or not
116 *
117 * @return {Boolean} the updating status of the SourceBuffer
118 */
119 updating() {
120 return !this.sourceBuffer_ || this.sourceBuffer_.updating;
121 }
122
123 /**
124 * Set/get the timestampoffset on the SourceBuffer
125 *
126 * @return {Number} the timestamp offset
127 */
128 timestampOffset(offset) {
129 if (typeof offset !== 'undefined') {
130 this.queueCallback_(() => {
131 this.sourceBuffer_.timestampOffset = offset;
132 });
133 this.timestampOffset_ = offset;
134 }
135 return this.timestampOffset_;
136 }
137
138 /**
139 * que a callback to run
140 */
141 queueCallback_(callback, done) {
142 this.callbacks_.push([callback.bind(this), done]);
143 this.runCallback_();
144 }
145
146 /**
147 * run a queued callback
148 */
149 runCallback_() {
150 let callbacks;
151
152 if (this.sourceBuffer_ &&
153 !this.sourceBuffer_.updating &&
154 this.callbacks_.length) {
155 callbacks = this.callbacks_.shift();
156 this.pendingCallback_ = callbacks[1];
157 callbacks[0]();
158 }
159 }
160
161 /**
162 * dispose of the source updater and the underlying sourceBuffer
163 */
164 dispose() {
165 if (this.sourceBuffer_ && this.mediaSource.readyState === 'open') {
166 this.sourceBuffer_.abort();
167 }
168 }
169 }
1 /** 1 /**
2 * @file stream.js
3 */
4 /**
2 * A lightweight readable stream implemention that handles event dispatching. 5 * A lightweight readable stream implemention that handles event dispatching.
6 *
7 * @class Stream
3 */ 8 */
4 export default class Stream { 9 export default class Stream {
5 constructor() { 10 constructor() {
...@@ -8,8 +13,9 @@ export default class Stream { ...@@ -8,8 +13,9 @@ export default class Stream {
8 13
9 /** 14 /**
10 * Add a listener for a specified event type. 15 * Add a listener for a specified event type.
11 * @param type {string} the event name 16 *
12 * @param listener {function} the callback to be invoked when an event of 17 * @param {String} type the event name
18 * @param {Function} listener the callback to be invoked when an event of
13 * the specified type occurs 19 * the specified type occurs
14 */ 20 */
15 on(type, listener) { 21 on(type, listener) {
...@@ -21,9 +27,11 @@ export default class Stream { ...@@ -21,9 +27,11 @@ export default class Stream {
21 27
22 /** 28 /**
23 * Remove a listener for a specified event type. 29 * Remove a listener for a specified event type.
24 * @param type {string} the event name 30 *
25 * @param listener {function} a function previously registered for this 31 * @param {String} type the event name
32 * @param {Function} listener a function previously registered for this
26 * type of event through `on` 33 * type of event through `on`
34 * @return {Boolean} if we could turn it off or not
27 */ 35 */
28 off(type, listener) { 36 off(type, listener) {
29 let index; 37 let index;
...@@ -39,7 +47,8 @@ export default class Stream { ...@@ -39,7 +47,8 @@ export default class Stream {
39 /** 47 /**
40 * Trigger an event of the specified type on this stream. Any additional 48 * Trigger an event of the specified type on this stream. Any additional
41 * arguments to this function are passed as parameters to event listeners. 49 * arguments to this function are passed as parameters to event listeners.
42 * @param type {string} the event name 50 *
51 * @param {String} type the event name
43 */ 52 */
44 trigger(type) { 53 trigger(type) {
45 let callbacks; 54 let callbacks;
...@@ -79,7 +88,8 @@ export default class Stream { ...@@ -79,7 +88,8 @@ export default class Stream {
79 * Forwards all `data` events on this stream to the destination stream. The 88 * Forwards all `data` events on this stream to the destination stream. The
80 * destination stream should provide a method `push` to receive the data 89 * destination stream should provide a method `push` to receive the data
81 * events as they arrive. 90 * events as they arrive.
82 * @param destination {stream} the stream that will receive all `data` events 91 *
92 * @param {Stream} destination the stream that will receive all `data` events
83 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 93 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
84 */ 94 */
85 pipe(destination) { 95 pipe(destination) {
......
1 /** 1 /**
2 * @file xhr.js
3 */
4
5 /**
2 * A wrapper for videojs.xhr that tracks bandwidth. 6 * A wrapper for videojs.xhr that tracks bandwidth.
7 *
8 * @param {Object} options options for the XHR
9 * @param {Function} callback the callback to call when done
10 * @return {Request} the xhr request that is going to be made
3 */ 11 */
4 import {xhr as videojsXHR, mergeOptions} from 'video.js'; 12 import {xhr as videojsXHR, mergeOptions} from 'video.js';
5 13
......
1 import HlsAudioTrack from '../src/hls-audio-track';
2 import QUnit from 'qunit';
3
4 // Most of these tests will be done in video.js.AudioTrack unit tests
5 QUnit.module('HlsAudioTrack - Props');
6
7 QUnit.test('verify that props are readonly and can be set', function() {
8 let props = {
9 default: true,
10 language: 'en',
11 label: 'English',
12 autoselect: true,
13 withCredentials: true,
14 // below props won't be used, its used for checking
15 kind: 'main'
16 };
17
18 let track = new HlsAudioTrack(props);
19
20 for (let k in props) {
21 QUnit.equal(track[k], props[k], `${k} should be stored in track`);
22 }
23 });
24
25 QUnit.test('can start with a mediaGroup that has a uri', function() {
26 let props = {
27 default: true,
28 language: 'en',
29 label: 'English',
30 autoselect: true,
31 mediaGroup: 'foo',
32 withCredentials: true,
33 resolvedUri: 'http://some.test.url/playlist.m3u8',
34 // below props won't be used, its used for checking
35 enabled: true,
36 kind: 'main'
37 };
38 let track = new HlsAudioTrack(props);
39
40 QUnit.equal(track.mediaGroups_.length, 1, 'loader was created');
41 let loader = track.getLoader('foo');
42
43 QUnit.ok(loader, 'can getLoader on foo');
44
45 track.dispose();
46 QUnit.equal(track.mediaGroups_.length, 0, 'loader disposed');
47 });
48
49 QUnit.test('can start with a mediaGroup that has no uri', function() {
50 let props = {
51 default: true,
52 language: 'en',
53 label: 'English',
54 autoselect: true,
55 mediaGroup: 'foo',
56 withCredentials: true,
57 // below props won't be used, its used for checking
58 enabled: true,
59 kind: 'main'
60 };
61 let track = new HlsAudioTrack(props);
62
63 QUnit.equal(track.mediaGroups_.length, 1, 'mediaGroupLoader was created for foo');
64 QUnit.ok(!track.getLoader('foo'), 'can getLoader on foo, but it is undefined');
65
66 track.dispose();
67 QUnit.equal(track.mediaGroups_.length, 0, 'loaders disposed');
68 });
69
70 QUnit.module('HlsAudioTrack - Loader', {
71 beforeEach() {
72 this.track = new HlsAudioTrack({
73 mediaGroup: 'default',
74 default: true,
75 language: 'en',
76 label: 'English',
77 autoselect: true,
78 withCredentials: true
79 });
80 },
81 afterEach() {
82 this.track.dispose();
83 QUnit.equal(this.track.mediaGroups_.length, 0, 'zero loaders after dispose');
84 }
85 });
86
87 QUnit.test('can add a playlist loader', function() {
88 QUnit.equal(this.track.mediaGroups_.length, 1, '1 loader to start');
89
90 this.track.addLoader('foo', 'someurl');
91 this.track.addLoader('bar', 'someurl');
92 this.track.addLoader('baz', 'someurl');
93
94 QUnit.equal(this.track.mediaGroups_.length, 4, 'now has four loaders');
95 });
96
97 QUnit.test('can remove playlist loader', function() {
98 QUnit.equal(this.track.mediaGroups_.length, 1, 'one loaders to start');
99
100 this.track.addLoader('foo', 'someurl');
101 this.track.addLoader('baz', 'someurl');
102
103 QUnit.equal(this.track.mediaGroups_.length, 3, 'now has three loaders');
104
105 this.track.removeLoader('baz');
106 QUnit.equal(this.track.mediaGroups_.length, 2, 'now has two loaders');
107
108 });
...@@ -10,13 +10,10 @@ var DEFAULTS = { ...@@ -10,13 +10,10 @@ var DEFAULTS = {
10 'node_modules/sinon/pkg/sinon-ie.js', 10 'node_modules/sinon/pkg/sinon-ie.js',
11 'node_modules/video.js/dist/video.js', 11 'node_modules/video.js/dist/video.js',
12 'node_modules/video.js/dist/video-js.css', 12 'node_modules/video.js/dist/video-js.css',
13
14 'test/**/*.test.js' 13 'test/**/*.test.js'
15 ], 14 ],
16 15
17 exclude: [ 16 exclude: [],
18 'test/data/**'
19 ],
20 17
21 plugins: [ 18 plugins: [
22 'karma-browserify', 19 'karma-browserify',
...@@ -43,6 +40,13 @@ var DEFAULTS = { ...@@ -43,6 +40,13 @@ var DEFAULTS = {
43 noParse: [ 40 noParse: [
44 'test/data/**', 41 'test/data/**',
45 ] 42 ]
43 },
44
45 customLaunchers: {
46 travisChrome: {
47 base: 'Chrome',
48 flags: ['--no-sandbox']
49 }
46 } 50 }
47 }; 51 };
48 52
......
...@@ -4,12 +4,10 @@ var common = require('./common'); ...@@ -4,12 +4,10 @@ var common = require('./common');
4 4
5 module.exports = function(config) { 5 module.exports = function(config) {
6 6
7 // Travis CI should run in its available Firefox headless browser.
8 if (process.env.TRAVIS) { 7 if (process.env.TRAVIS) {
9
10 config.set(common({ 8 config.set(common({
11 browsers: ['Firefox'], 9 browsers: ['travisChrome'],
12 plugins: ['karma-firefox-launcher'] 10 plugins: ['karma-chrome-launcher']
13 })) 11 }))
14 } else { 12 } else {
15 config.set(common({ 13 config.set(common({
......
1 import Playlist from '../src/playlist'; 1 import Playlist from '../src/playlist';
2 import PlaylistLoader from '../src/playlist-loader';
2 import QUnit from 'qunit'; 3 import QUnit from 'qunit';
4 import xhrFactory from '../src/xhr';
5 import { useFakeEnvironment } from './test-helpers';
6
3 QUnit.module('Playlist Duration'); 7 QUnit.module('Playlist Duration');
4 8
5 QUnit.test('total duration for live playlists is Infinity', function() { 9 QUnit.test('total duration for live playlists is Infinity', function() {
...@@ -376,3 +380,251 @@ QUnit.test('seekable end accounts for non-standard target durations', function() ...@@ -376,3 +380,251 @@ QUnit.test('seekable end accounts for non-standard target durations', function()
376 9 - (2 + 2 + 1), 380 9 - (2 + 2 + 1),
377 'allows seeking no further than three segments from the end'); 381 'allows seeking no further than three segments from the end');
378 }); 382 });
383
384 QUnit.module('Playlist Media Index For Time', {
385 beforeEach() {
386 this.env = useFakeEnvironment();
387 this.clock = this.env.clock;
388 this.requests = this.env.requests;
389 this.fakeHls = {
390 xhr: xhrFactory()
391 };
392 },
393 afterEach() {
394 this.env.restore();
395 }
396 });
397
398 QUnit.test('can get media index by playback position for non-live videos', function() {
399 let media;
400 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
401
402 loader.load();
403
404 this.requests.shift().respond(200, null,
405 '#EXTM3U\n' +
406 '#EXT-X-MEDIA-SEQUENCE:0\n' +
407 '#EXTINF:4,\n' +
408 '0.ts\n' +
409 '#EXTINF:5,\n' +
410 '1.ts\n' +
411 '#EXTINF:6,\n' +
412 '2.ts\n' +
413 '#EXT-X-ENDLIST\n'
414 );
415
416 media = loader.media();
417
418 QUnit.equal(Playlist.getMediaIndexForTime_(media, -1), 0,
419 'the index is never less than zero');
420 QUnit.equal(Playlist.getMediaIndexForTime_(media, 0), 0, 'time zero is index zero');
421 QUnit.equal(Playlist.getMediaIndexForTime_(media, 3), 0, 'time three is index zero');
422 QUnit.equal(Playlist.getMediaIndexForTime_(media, 10), 2, 'time 10 is index 2');
423 QUnit.equal(Playlist.getMediaIndexForTime_(media, 22), 2,
424 'time greater than the length is index 2');
425 });
426
427 QUnit.test('returns the lower index when calculating for a segment boundary', function() {
428 let media;
429 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
430
431 loader.load();
432
433 this.requests.shift().respond(200, null,
434 '#EXTM3U\n' +
435 '#EXT-X-MEDIA-SEQUENCE:0\n' +
436 '#EXTINF:4,\n' +
437 '0.ts\n' +
438 '#EXTINF:5,\n' +
439 '1.ts\n' +
440 '#EXT-X-ENDLIST\n'
441 );
442
443 media = loader.media();
444
445 QUnit.equal(Playlist.getMediaIndexForTime_(media, 4), 1, 'rounds up exact matches');
446 QUnit.equal(Playlist.getMediaIndexForTime_(media, 3.7), 0, 'rounds down');
447 QUnit.equal(Playlist.getMediaIndexForTime_(media, 4.5), 1, 'rounds up at 0.5');
448 });
449
450 QUnit.test(
451 'accounts for non-zero starting segment time when calculating media index',
452 function() {
453 let media;
454 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
455
456 loader.load();
457
458 this.requests.shift().respond(200, null,
459 '#EXTM3U\n' +
460 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
461 '#EXTINF:4,\n' +
462 '1001.ts\n' +
463 '#EXTINF:5,\n' +
464 '1002.ts\n'
465 );
466 loader.media().segments[0].end = 154;
467
468 media = loader.media();
469
470 QUnit.equal(
471 Playlist.getMediaIndexForTime_(media, 0),
472 -1,
473 'the lowest returned value is negative one'
474 );
475 QUnit.equal(
476 Playlist.getMediaIndexForTime_(media, 45),
477 -1,
478 'expired content returns negative one'
479 );
480 QUnit.equal(
481 Playlist.getMediaIndexForTime_(media, 75),
482 -1,
483 'expired content returns negative one'
484 );
485 QUnit.equal(
486 Playlist.getMediaIndexForTime_(media, 50 + 100),
487 0,
488 'calculates the earliest available position'
489 );
490 QUnit.equal(
491 Playlist.getMediaIndexForTime_(media, 50 + 100 + 2),
492 0,
493 'calculates within the first segment'
494 );
495 QUnit.equal(
496 Playlist.getMediaIndexForTime_(media, 50 + 100 + 2),
497 0,
498 'calculates within the first segment'
499 );
500 QUnit.equal(
501 Playlist.getMediaIndexForTime_(media, 50 + 100 + 4),
502 1,
503 'calculates within the second segment'
504 );
505 QUnit.equal(
506 Playlist.getMediaIndexForTime_(media, 50 + 100 + 4.5),
507 1,
508 'calculates within the second segment'
509 );
510 QUnit.equal(
511 Playlist.getMediaIndexForTime_(media, 50 + 100 + 6),
512 1,
513 'calculates within the second segment'
514 );
515
516 loader.media().segments[1].end = 159;
517 QUnit.equal(
518 Playlist.getMediaIndexForTime_(media, 159),
519 2,
520 'returns number of segments when time is equal to end of last segment'
521 );
522 QUnit.equal(
523 Playlist.getMediaIndexForTime_(media, 159.1),
524 2,
525 'returns number of segments when time is past end of last segment'
526 );
527 });
528
529 QUnit.test('prefers precise segment timing when tracking expired time', function() {
530 let media;
531 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
532
533 loader.load();
534
535 loader.trigger('firstplay');
536 this.requests.shift().respond(200, null,
537 '#EXTM3U\n' +
538 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
539 '#EXTINF:4,\n' +
540 '1001.ts\n' +
541 '#EXTINF:5,\n' +
542 '1002.ts\n'
543 );
544 // setup the loader with an "imprecise" value as if it had been
545 // accumulating segment durations as they expire
546 loader.expired_ = 160;
547 // annotate the first segment with a start time
548 // this number would be coming from the Source Buffer in practice
549 loader.media().segments[0].end = 150;
550
551 media = loader.media();
552
553 QUnit.equal(
554 Playlist.getMediaIndexForTime_(media, 149),
555 0,
556 'prefers the value on the first segment'
557 );
558
559 // trigger a playlist refresh
560 this.clock.tick(10 * 1000);
561 this.requests.shift().respond(200, null,
562 '#EXTM3U\n' +
563 '#EXT-X-MEDIA-SEQUENCE:1002\n' +
564 '#EXTINF:5,\n' +
565 '1002.ts\n'
566 );
567
568 media = loader.media();
569
570 QUnit.equal(
571 Playlist.getMediaIndexForTime_(media, 150 + 4 + 1),
572 0,
573 'tracks precise expired times'
574 );
575 });
576
577 QUnit.test('accounts for expired time when calculating media index', function() {
578 let media;
579 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
580 let expired = 150;
581
582 loader.load();
583
584 this.requests.shift().respond(200, null,
585 '#EXTM3U\n' +
586 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
587 '#EXTINF:4,\n' +
588 '1001.ts\n' +
589 '#EXTINF:5,\n' +
590 '1002.ts\n'
591 );
592
593 media = loader.media();
594
595 QUnit.equal(
596 Playlist.getMediaIndexForTime_(media, 0, expired),
597 -1,
598 'expired content returns a negative index'
599 );
600 QUnit.equal(
601 Playlist.getMediaIndexForTime_(media, 75, expired),
602 -1,
603 'expired content returns a negative index'
604 );
605 QUnit.equal(
606 Playlist.getMediaIndexForTime_(media, 50 + 100, expired),
607 0,
608 'calculates the earliest available position'
609 );
610 QUnit.equal(
611 Playlist.getMediaIndexForTime_(media, 50 + 100 + 2, expired),
612 0,
613 'calculates within the first segment'
614 );
615 QUnit.equal(
616 Playlist.getMediaIndexForTime_(media, 50 + 100 + 2, expired),
617 0,
618 'calculates within the first segment'
619 );
620 QUnit.equal(
621 Playlist.getMediaIndexForTime_(media, 50 + 100 + 4.5, expired),
622 1,
623 'calculates within the second segment'
624 );
625 QUnit.equal(
626 Playlist.getMediaIndexForTime_(media, 50 + 100 + 6, expired),
627 1,
628 'calculates within the second segment'
629 );
630 });
......
1 import Ranges from '../src/ranges';
2 import {createTimeRanges} from 'video.js';
3 import QUnit from 'qunit';
4
5 QUnit.module('TimeRanges Utilities');
6
7 QUnit.test('finds the overlapping time range', function() {
8 let range = Ranges.findRange(createTimeRanges([[0, 5], [6, 12]]), 3);
9
10 QUnit.equal(range.length, 1, 'found one range');
11 QUnit.equal(range.end(0), 5, 'inside the first buffered region');
12
13 range = Ranges.findRange(createTimeRanges([[0, 5], [6, 12]]), 6);
14 QUnit.equal(range.length, 1, 'found one range');
15 QUnit.equal(range.end(0), 12, 'inside the second buffered region');
16 });
17
18 QUnit.module('Buffer Inpsection');
19
20 QUnit.test('detects time range end-point changed by updates', function() {
21 let edge;
22
23 // Single-range changes
24 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
25 createTimeRanges([[0, 11]]));
26 QUnit.strictEqual(edge, 11, 'detected a forward addition');
27
28 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[5, 10]]),
29 createTimeRanges([[0, 10]]));
30 QUnit.strictEqual(edge, null, 'ignores backward addition');
31
32 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[5, 10]]),
33 createTimeRanges([[0, 11]]));
34 QUnit.strictEqual(edge, 11,
35 'detected a forward addition & ignores a backward addition');
36
37 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
38 createTimeRanges([[0, 9]]));
39 QUnit.strictEqual(edge, null,
40 'ignores a backwards addition resulting from a shrinking range');
41
42 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
43 createTimeRanges([[2, 7]]));
44 QUnit.strictEqual(edge, null,
45 'ignores a forward & backwards addition resulting from a shrinking ' +
46 'range');
47
48 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[2, 10]]),
49 createTimeRanges([[0, 7]]));
50 QUnit.strictEqual(
51 edge,
52 null,
53 'ignores a forward & backwards addition resulting from a range shifted backward'
54 );
55
56 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[2, 10]]),
57 createTimeRanges([[5, 15]]));
58 QUnit.strictEqual(edge, 15,
59 'detected a forwards addition resulting from a range shifted foward');
60
61 // Multiple-range changes
62 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
63 createTimeRanges([[0, 11], [12, 15]]));
64 QUnit.strictEqual(edge, null, 'ignores multiple new forward additions');
65
66 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
67 createTimeRanges([[20, 50]]));
68 QUnit.strictEqual(edge, 50, 'detected a forward addition & ignores range removal');
69
70 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
71 createTimeRanges([[0, 50]]));
72 QUnit.strictEqual(edge, 50, 'detected a forward addition & ignores merges');
73
74 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
75 createTimeRanges([[0, 40]]));
76 QUnit.strictEqual(edge, null, 'ignores merges');
77
78 // Empty input
79 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges(),
80 createTimeRanges([[0, 11]]));
81 QUnit.strictEqual(edge, 11, 'handle an empty original TimeRanges object');
82
83 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 11]]),
84 createTimeRanges());
85 QUnit.strictEqual(edge, null, 'handle an empty update TimeRanges object');
86
87 // Null input
88 edge = Ranges.findSoleUncommonTimeRangesEnd(null, createTimeRanges([[0, 11]]));
89 QUnit.strictEqual(edge, 11, 'treat null original buffer as an empty TimeRanges object');
90
91 edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 11]]), null);
92 QUnit.strictEqual(edge, null, 'treat null update buffer as an empty TimeRanges object');
93 });
1 import SourceUpdater from '../src/source-updater';
2 import QUnit from 'qunit';
3 import videojs from 'video.js';
4 import { useFakeMediaSource } from './test-helpers';
5
6 QUnit.module('Source Updater', {
7 beforeEach() {
8 this.mse = useFakeMediaSource();
9 this.mediaSource = new videojs.MediaSource();
10 },
11 afterEach() {
12 this.mse.restore();
13 }
14 });
15
16 QUnit.test('waits for sourceopen to create a source buffer', function() {
17 new SourceUpdater(this.mediaSource, 'video/mp2t'); // eslint-disable-line no-new
18
19 QUnit.equal(this.mediaSource.sourceBuffers.length, 0,
20 'waited to create the source buffer');
21
22 this.mediaSource.trigger('sourceopen');
23
24 QUnit.equal(this.mediaSource.sourceBuffers.length, 1, 'created one source buffer');
25 QUnit.equal(this.mediaSource.sourceBuffers[0].mimeType_, 'video/mp2t',
26 'assigned the correct MIME type');
27 });
28
29 QUnit.test('runs a callback when the source buffer is created', function() {
30 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
31 let sourceBuffer;
32
33 updater.appendBuffer(new Uint8Array([0, 1, 2]));
34
35 this.mediaSource.trigger('sourceopen');
36 sourceBuffer = this.mediaSource.sourceBuffers[0];
37 QUnit.equal(sourceBuffer.updates_.length, 1, 'called the source buffer once');
38 QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0, 1, 2]),
39 'appended the bytes');
40 });
41
42 QUnit.test('runs the completion callback when updateend fires', function() {
43 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
44 let updateends = 0;
45 let sourceBuffer;
46
47 this.mediaSource.trigger('sourceopen');
48 sourceBuffer = this.mediaSource.sourceBuffers[0];
49 updater.appendBuffer(new Uint8Array([0, 1, 2]), function() {
50 updateends++;
51 });
52 updater.appendBuffer(new Uint8Array([2, 3, 4]), function() {
53 throw new Error('Wrong completion callback invoked!');
54 });
55
56 QUnit.equal(updateends, 0, 'no completions yet');
57 sourceBuffer.trigger('updateend');
58 QUnit.equal(updateends, 1, 'ran the completion callback');
59 });
60
61 QUnit.test('runs the next callback after updateend fires', function() {
62 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
63 let sourceBuffer;
64
65 updater.appendBuffer(new Uint8Array([0, 1, 2]));
66 this.mediaSource.trigger('sourceopen');
67 sourceBuffer = this.mediaSource.sourceBuffers[0];
68
69 updater.appendBuffer(new Uint8Array([2, 3, 4]));
70 QUnit.equal(sourceBuffer.updates_.length, 1, 'delayed the update');
71
72 sourceBuffer.trigger('updateend');
73 QUnit.equal(sourceBuffer.updates_.length, 2, 'updated twice');
74 QUnit.deepEqual(sourceBuffer.updates_[1].append, new Uint8Array([2, 3, 4]),
75 'appended the bytes');
76 });
77
78 QUnit.test('runs only one callback at a time', function() {
79 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
80 let sourceBuffer;
81
82 updater.appendBuffer(new Uint8Array([0]));
83 updater.appendBuffer(new Uint8Array([1]));
84 this.mediaSource.trigger('sourceopen');
85 sourceBuffer = this.mediaSource.sourceBuffers[0];
86
87 updater.appendBuffer(new Uint8Array([2]));
88 QUnit.equal(sourceBuffer.updates_.length, 1, 'queued some updates');
89 QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0]),
90 'ran the first update');
91
92 sourceBuffer.trigger('updateend');
93 QUnit.equal(sourceBuffer.updates_.length, 2, 'queued some updates');
94 QUnit.deepEqual(sourceBuffer.updates_[1].append, new Uint8Array([1]),
95 'ran the second update');
96
97 updater.appendBuffer(new Uint8Array([3]));
98 sourceBuffer.trigger('updateend');
99 QUnit.equal(sourceBuffer.updates_.length, 3, 'queued the updates');
100 QUnit.deepEqual(sourceBuffer.updates_[2].append, new Uint8Array([2]),
101 'ran the third update');
102
103 sourceBuffer.trigger('updateend');
104 QUnit.equal(sourceBuffer.updates_.length, 4, 'finished the updates');
105 QUnit.deepEqual(sourceBuffer.updates_[3].append, new Uint8Array([3]),
106 'ran the fourth update');
107 });
108
109 QUnit.test('runs updates immediately if possible', function() {
110 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
111 let sourceBuffer;
112
113 this.mediaSource.trigger('sourceopen');
114 sourceBuffer = this.mediaSource.sourceBuffers[0];
115 updater.appendBuffer(new Uint8Array([0]));
116 QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
117 QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0]),
118 'appended the bytes');
119 });
120
121 QUnit.test('supports abort', function() {
122 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
123 let sourceBuffer;
124
125 updater.abort();
126 this.mediaSource.trigger('sourceopen');
127
128 sourceBuffer = this.mediaSource.sourceBuffers[0];
129 QUnit.ok(sourceBuffer.updates_[0].abort, 'aborted the source buffer');
130 });
131
132 QUnit.test('supports buffered', function() {
133 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
134
135 QUnit.equal(updater.buffered().length, 0, 'buffered is empty');
136
137 this.mediaSource.trigger('sourceopen');
138 QUnit.ok(updater.buffered(), 'buffered is defined');
139 });
140
141 QUnit.test('supports removeBuffer', function() {
142 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
143 let sourceBuffer;
144
145 this.mediaSource.trigger('sourceopen');
146 sourceBuffer = this.mediaSource.sourceBuffers[0];
147 updater.remove(1, 14);
148
149 QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
150 QUnit.deepEqual(sourceBuffer.updates_[0].remove, [1, 14], 'removed the time range');
151 });
152
153 QUnit.test('supports setting duration', function() {
154 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
155 let sourceBuffer;
156
157 this.mediaSource.trigger('sourceopen');
158 sourceBuffer = this.mediaSource.sourceBuffers[0];
159 updater.duration(21);
160
161 QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
162 QUnit.deepEqual(sourceBuffer.updates_[0].duration, 21, 'changed duration');
163 });
164
165 QUnit.test('supports timestampOffset', function() {
166 let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
167 let sourceBuffer;
168
169 this.mediaSource.trigger('sourceopen');
170 sourceBuffer = this.mediaSource.sourceBuffers[0];
171
172 QUnit.equal(updater.timestampOffset(), 0, 'intialized to zero');
173 updater.timestampOffset(21);
174 QUnit.equal(updater.timestampOffset(), 21, 'reflects changes immediately');
175 QUnit.equal(sourceBuffer.timestampOffset, 21, 'applied the update');
176
177 updater.appendBuffer(new Uint8Array(2));
178 updater.timestampOffset(14);
179 QUnit.equal(updater.timestampOffset(), 14, 'reflects changes immediately');
180 QUnit.equal(sourceBuffer.timestampOffset, 21, 'queues application after updates');
181
182 sourceBuffer.trigger('updateend');
183 QUnit.equal(sourceBuffer.timestampOffset, 14, 'applied the update');
184 });
1 import document from 'global/document';
2 import sinon from 'sinon';
3 import videojs from 'video.js';
4 import QUnit from 'qunit';
5 /* eslint-disable no-unused-vars */
6 // needed so MediaSource can be registered with videojs
7 import MediaSource from 'videojs-contrib-media-sources';
8 /* eslint-enable */
9 import testDataManifests from './test-manifests.js';
10 import xhrFactory from '../src/xhr';
11
12 // a SourceBuffer that tracks updates but otherwise is a noop
13 class MockSourceBuffer extends videojs.EventTarget {
14 constructor() {
15 super();
16 this.updates_ = [];
17
18 this.updating = false;
19 this.on('updateend', function() {
20 this.updating = false;
21 });
22
23 this.buffered = videojs.createTimeRanges();
24 this.duration_ = NaN;
25
26 Object.defineProperty(this, 'duration', {
27 get() {
28 return this.duration_;
29 },
30 set(duration) {
31 this.updates_.push({
32 duration
33 });
34 this.duration_ = duration;
35 }
36 });
37 }
38
39 abort() {
40 this.updates_.push({
41 abort: true
42 });
43 }
44
45 appendBuffer(bytes) {
46 this.updates_.push({
47 append: bytes
48 });
49 this.updating = true;
50 }
51
52 remove(start, end) {
53 this.updates_.push({
54 remove: [start, end]
55 });
56 }
57 }
58
59 class MockMediaSource extends videojs.EventTarget {
60 constructor() {
61 super();
62 this.readyState = 'closed';
63 this.on('sourceopen', function() {
64 this.readyState = 'open';
65 });
66
67 this.sourceBuffers = [];
68 this.duration = NaN;
69 this.seekable = videojs.createTimeRange();
70 }
71
72 addSeekableRange_(start, end) {
73 this.seekable = videojs.createTimeRange(start, end);
74 }
75
76 addSourceBuffer(mime) {
77 let sourceBuffer = new MockSourceBuffer();
78
79 sourceBuffer.mimeType_ = mime;
80 this.sourceBuffers.push(sourceBuffer);
81 return sourceBuffer;
82 }
83
84 endOfStream(error) {
85 this.readyState = 'closed';
86 this.error_ = error;
87 }
88 }
89
90 export const useFakeMediaSource = function() {
91 let RealMediaSource = videojs.MediaSource;
92 let realCreateObjectURL = window.URL.createObjectURL;
93 let id = 0;
94
95 videojs.MediaSource = MockMediaSource;
96 videojs.MediaSource.supportsNativeMediaSources =
97 RealMediaSource.supportsNativeMediaSources;
98 videojs.URL.createObjectURL = function() {
99 id++;
100 return 'blob:videojs-contrib-hls-mock-url' + id;
101 };
102
103 return {
104 restore() {
105 videojs.MediaSource = RealMediaSource;
106 videojs.URL.createObjectURL = realCreateObjectURL;
107 }
108 };
109 };
110
111 let fakeEnvironment = {
112 requests: [],
113 restore() {
114 this.clock.restore();
115 videojs.xhr.XMLHttpRequest = window.XMLHttpRequest;
116 this.xhr.restore();
117 ['warn', 'error'].forEach((level) => {
118 if (this.log && this.log[level] && this.log[level].restore) {
119 QUnit.equal(this.log[level].callCount, 0, `no unexpected logs on ${level}`);
120 this.log[level].restore();
121 }
122 });
123 }
124 };
125
126 export const useFakeEnvironment = function() {
127 fakeEnvironment.log = {};
128 ['warn', 'error'].forEach((level) => {
129 // you can use .log[level].args to get args
130 sinon.stub(videojs.log, level);
131 fakeEnvironment.log[level] = videojs.log[level];
132 Object.defineProperty(videojs.log[level], 'calls', {
133 get() {
134 // reset callCount to 0 so they don't have to
135 let callCount = this.callCount;
136
137 this.callCount = 0;
138 return callCount;
139 }
140 });
141 });
142 fakeEnvironment.clock = sinon.useFakeTimers();
143 fakeEnvironment.xhr = sinon.useFakeXMLHttpRequest();
144 fakeEnvironment.requests.length = 0;
145 fakeEnvironment.xhr.onCreate = function(xhr) {
146 fakeEnvironment.requests.push(xhr);
147 };
148 videojs.xhr.XMLHttpRequest = fakeEnvironment.xhr;
149
150 return fakeEnvironment;
151 };
152
153 // patch over some methods of the provided tech so it can be tested
154 // synchronously with sinon's fake timers
155 export const mockTech = function(tech) {
156 if (tech.isMocked_) {
157 // make this function idempotent because HTML and Flash based
158 // playback have very different lifecycles. For HTML, the tech
159 // is available on player creation. For Flash, the tech isn't
160 // ready until the source has been loaded and one tick has
161 // expired.
162 return;
163 }
164
165 tech.isMocked_ = true;
166 tech.src_ = null;
167 tech.time_ = null;
168
169 tech.paused_ = !tech.autoplay();
170 tech.paused = function() {
171 return tech.paused_;
172 };
173
174 if (!tech.currentTime_) {
175 tech.currentTime_ = tech.currentTime;
176 }
177 tech.currentTime = function() {
178 return tech.time_ === null ? tech.currentTime_() : tech.time_;
179 };
180
181 tech.setSrc = function(src) {
182 tech.src_ = src;
183 };
184 tech.src = function(src) {
185 if (src !== null) {
186 return tech.setSrc(src);
187 }
188 return tech.src_ === null ? tech.src : tech.src_;
189 };
190 tech.currentSrc_ = tech.currentSrc;
191 tech.currentSrc = function() {
192 return tech.src_ === null ? tech.currentSrc_() : tech.src_;
193 };
194
195 tech.play_ = tech.play;
196 tech.play = function() {
197 tech.play_();
198 tech.paused_ = false;
199 tech.trigger('play');
200 };
201 tech.pause_ = tech.pause_;
202 tech.pause = function() {
203 tech.pause_();
204 tech.paused_ = true;
205 tech.trigger('pause');
206 };
207
208 tech.setCurrentTime = function(time) {
209 tech.time_ = time;
210
211 setTimeout(function() {
212 tech.trigger('seeking');
213 setTimeout(function() {
214 tech.trigger('seeked');
215 }, 1);
216 }, 1);
217 };
218 };
219
220 export const createPlayer = function(options) {
221 let video;
222 let player;
223
224 video = document.createElement('video');
225 video.className = 'video-js';
226 document.querySelector('#qunit-fixture').appendChild(video);
227 player = videojs(video, options || {
228 flash: {
229 swf: ''
230 }
231 });
232
233 player.buffered = function() {
234 return videojs.createTimeRange(0, 0);
235 };
236 mockTech(player.tech_);
237
238 return player;
239 };
240
241 export const openMediaSource = function(player, clock) {
242 // ensure the Flash tech is ready
243 player.tech_.triggerReady();
244 clock.tick(1);
245 // mock the tech *after* it has finished loading so that we don't
246 // mock a tech that will be unloaded on the next tick
247 mockTech(player.tech_);
248 player.tech_.hls.xhr = xhrFactory();
249
250 // simulate the sourceopen event
251 player.tech_.hls.mediaSource.readyState = 'open';
252 player.tech_.hls.mediaSource.dispatchEvent({
253 type: 'sourceopen',
254 swfId: player.tech_.el().id
255 });
256 };
257
258 export const standardXHRResponse = function(request) {
259 if (!request.url) {
260 return;
261 }
262
263 let contentType = 'application/json';
264 // contents off the global object
265 let manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
266
267 if (manifestName) {
268 manifestName = manifestName[1];
269 } else {
270 manifestName = request.url;
271 }
272
273 if (/\.m3u8?/.test(request.url)) {
274 contentType = 'application/vnd.apple.mpegurl';
275 } else if (/\.ts/.test(request.url)) {
276 contentType = 'video/MP2T';
277 }
278
279 request.response = new Uint8Array(16).buffer;
280 request.respond(200, { 'Content-Type': contentType },
281 testDataManifests[manifestName]);
282 };
283
284 // return an absolute version of a page-relative URL
285 export const absoluteUrl = function(relativeUrl) {
286 return window.location.protocol + '//' +
287 window.location.host +
288 (window.location.pathname
289 .split('/')
290 .slice(0, -1)
291 .concat(relativeUrl)
292 .join('/')
293 );
294 };
This diff could not be displayed because it is too large.
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "http://example.com/00001.ts" 9 "uri": "http://example.com/00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "https://example.com/00002.ts" 14 "uri": "https://example.com/00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "//example.com/00003.ts" 19 "uri": "//example.com/00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "http://example.com/00004.ts" 24 "uri": "http://example.com/00004.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 }, 14 },
14 { 15 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "offset": 522828 18 "offset": 522828
18 }, 19 },
19 "duration": 10, 20 "duration": 10,
21 "timeline": 0,
20 "uri": "hls_450k_video.ts" 22 "uri": "hls_450k_video.ts"
21 }, 23 },
22 { 24 {
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
25 "offset": 1110328 27 "offset": 1110328
26 }, 28 },
27 "duration": 10, 29 "duration": 10,
30 "timeline": 0,
28 "uri": "hls_450k_video.ts" 31 "uri": "hls_450k_video.ts"
29 }, 32 },
30 { 33 {
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
33 "offset": 1823412 36 "offset": 1823412
34 }, 37 },
35 "duration": 10, 38 "duration": 10,
39 "timeline": 0,
36 "uri": "hls_450k_video.ts" 40 "uri": "hls_450k_video.ts"
37 }, 41 },
38 { 42 {
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
41 "offset": 2299992 45 "offset": 2299992
42 }, 46 },
43 "duration": 10, 47 "duration": 10,
48 "timeline": 0,
44 "uri": "hls_450k_video.ts" 49 "uri": "hls_450k_video.ts"
45 }, 50 },
46 { 51 {
...@@ -49,6 +54,7 @@ ...@@ -49,6 +54,7 @@
49 "offset": 2835604 54 "offset": 2835604
50 }, 55 },
51 "duration": 10, 56 "duration": 10,
57 "timeline": 0,
52 "uri": "hls_450k_video.ts" 58 "uri": "hls_450k_video.ts"
53 }, 59 },
54 { 60 {
...@@ -57,6 +63,7 @@ ...@@ -57,6 +63,7 @@
57 "offset": 3042780 63 "offset": 3042780
58 }, 64 },
59 "duration": 10, 65 "duration": 10,
66 "timeline": 0,
60 "uri": "hls_450k_video.ts" 67 "uri": "hls_450k_video.ts"
61 }, 68 },
62 { 69 {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
65 "offset": 3498680 72 "offset": 3498680
66 }, 73 },
67 "duration": 10, 74 "duration": 10,
75 "timeline": 0,
68 "uri": "hls_450k_video.ts" 76 "uri": "hls_450k_video.ts"
69 }, 77 },
70 { 78 {
...@@ -73,6 +81,7 @@ ...@@ -73,6 +81,7 @@
73 "offset": 4155928 81 "offset": 4155928
74 }, 82 },
75 "duration": 10, 83 "duration": 10,
84 "timeline": 0,
76 "uri": "hls_450k_video.ts" 85 "uri": "hls_450k_video.ts"
77 }, 86 },
78 { 87 {
...@@ -81,6 +90,7 @@ ...@@ -81,6 +90,7 @@
81 "offset": 4727636 90 "offset": 4727636
82 }, 91 },
83 "duration": 10, 92 "duration": 10,
93 "timeline": 0,
84 "uri": "hls_450k_video.ts" 94 "uri": "hls_450k_video.ts"
85 }, 95 },
86 { 96 {
...@@ -89,6 +99,7 @@ ...@@ -89,6 +99,7 @@
89 "offset": 5212676 99 "offset": 5212676
90 }, 100 },
91 "duration": 10, 101 "duration": 10,
102 "timeline": 0,
92 "uri": "hls_450k_video.ts" 103 "uri": "hls_450k_video.ts"
93 }, 104 },
94 { 105 {
...@@ -97,6 +108,7 @@ ...@@ -97,6 +108,7 @@
97 "offset": 5921812 108 "offset": 5921812
98 }, 109 },
99 "duration": 10, 110 "duration": 10,
111 "timeline": 0,
100 "uri": "hls_450k_video.ts" 112 "uri": "hls_450k_video.ts"
101 }, 113 },
102 { 114 {
...@@ -105,6 +117,7 @@ ...@@ -105,6 +117,7 @@
105 "offset": 6651816 117 "offset": 6651816
106 }, 118 },
107 "duration": 10, 119 "duration": 10,
120 "timeline": 0,
108 "uri": "hls_450k_video.ts" 121 "uri": "hls_450k_video.ts"
109 }, 122 },
110 { 123 {
...@@ -113,6 +126,7 @@ ...@@ -113,6 +126,7 @@
113 "offset": 7108092 126 "offset": 7108092
114 }, 127 },
115 "duration": 10, 128 "duration": 10,
129 "timeline": 0,
116 "uri": "hls_450k_video.ts" 130 "uri": "hls_450k_video.ts"
117 }, 131 },
118 { 132 {
...@@ -121,6 +135,7 @@ ...@@ -121,6 +135,7 @@
121 "offset": 7576776 135 "offset": 7576776
122 }, 136 },
123 "duration": 10, 137 "duration": 10,
138 "timeline": 0,
124 "uri": "hls_450k_video.ts" 139 "uri": "hls_450k_video.ts"
125 }, 140 },
126 { 141 {
...@@ -129,6 +144,7 @@ ...@@ -129,6 +144,7 @@
129 "offset": 8021772 144 "offset": 8021772
130 }, 145 },
131 "duration": 10, 146 "duration": 10,
147 "timeline": 0,
132 "uri": "hls_450k_video.ts" 148 "uri": "hls_450k_video.ts"
133 }, 149 },
134 { 150 {
...@@ -137,6 +153,7 @@ ...@@ -137,6 +153,7 @@
137 "offset": 8353216 153 "offset": 8353216
138 }, 154 },
139 "duration": 1.4167, 155 "duration": 1.4167,
156 "timeline": 0,
140 "uri": "hls_450k_video.ts" 157 "uri": "hls_450k_video.ts"
141 } 158 }
142 ], 159 ],
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 } 14 }
14 ], 15 ],
......
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 // TYPE
6 AUDIO: {
7 // GROUP-ID
8 "audio": {
9 // NAME
10 "English": {
11 language: 'eng',
12 autoselect: true,
13 default: true,
14 uri: "eng/prog_index.m3u8"
15 },
16 // NAME
17 "Français": {
18 language: "fre",
19 autoselect: true,
20 default: false,
21 uri: "fre/prog_index.m3u8"
22 },
23 // NAME
24 "Espanol": {
25 language: "sp",
26 autoselect: true,
27 default: false,
28 uri: "sp/prog_index.m3u8"
29 }
30 }
31 },
32 VIDEO: {},
33 "CLOSED-CAPTIONS": {},
34 SUBTITLES: {}
35 },
36 playlists: [{
37 attributes: {
38 "PROGRAM-ID": 1,
39 BANDWIDTH: 195023,
40 CODECS: "avc1.42e00a,mp4a.40.2",
41 AUDIO: 'audio'
42 },
43 timeline: 0,
44 uri: "lo/prog_index.m3u8"
45 }, {
46 attributes: {
47 "PROGRAM-ID": 1,
48 BANDWIDTH: 591680,
49 CODECS: "avc1.42e01e,mp4a.40.2",
50 AUDIO: 'audio'
51 },
52 timeline: 0,
53 uri: "hi/prog_index.m3u8"
54 }]
55 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
3 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
5
6 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="avc1.42e00a,mp4a.40.2",AUDIO="audio"
7 lo/prog_index.m3u8
8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio"
9 hi/prog_index.m3u8
...\ No newline at end of file ...\ No newline at end of file
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 AUDIO: {
6 aac: {
7 English: {
8 autoselect: true,
9 default: true,
10 language: "eng",
11 uri: "eng/prog_index.m3u8"
12 }
13 }
14 },
15 VIDEO: {
16 "500kbs": {
17 Angle1: {
18 autoselect: true,
19 default: true
20 },
21 Angle2: {
22 autoselect: true,
23 default: false,
24 uri: "Angle2/500kbs/prog_index.m3u8"
25 },
26 Angle3: {
27 autoselect: true,
28 default: false,
29 uri: "Angle3/500kbs/prog_index.m3u8"
30 }
31 }
32 },
33 "CLOSED-CAPTIONS": {},
34 SUBTITLES: {}
35 },
36 playlists: [{
37 attributes: {
38 "PROGRAM-ID": 1,
39 BANDWIDTH: 754857,
40 CODECS: "mp4a.40.2,avc1.4d401e",
41 AUDIO: "aac",
42 VIDEO: "500kbs"
43 },
44 timeline: 0,
45 uri: "Angle1/500kbs/prog_index.m3u8"
46 }]
47 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
3 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8"
5
6 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8"
7 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac"
8 Angle1/500kbs/prog_index.m3u8
...\ No newline at end of file ...\ No newline at end of file
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
10 "height": 224 10 "height": 224
11 } 11 }
12 }, 12 },
13 "timeline": 0,
13 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" 14 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
14 }, 15 },
15 { 16 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "PROGRAM-ID": 1, 18 "PROGRAM-ID": 1,
18 "BANDWIDTH": 40000 19 "BANDWIDTH": 40000
19 }, 20 },
21 "timeline": 0,
20 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" 22 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
21 }, 23 },
22 { 24 {
...@@ -28,6 +30,7 @@ ...@@ -28,6 +30,7 @@
28 "height": 224 30 "height": 224
29 } 31 }
30 }, 32 },
33 "timeline": 0,
31 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" 34 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
32 }, 35 },
33 { 36 {
...@@ -39,8 +42,15 @@ ...@@ -39,8 +42,15 @@
39 "height": 540 42 "height": 540
40 } 43 }
41 }, 44 },
45 "timeline": 0,
42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 46 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
43 } 47 }
44 ], 48 ],
45 "discontinuityStarts": [] 49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
46 } 56 }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "hls_450k_video.ts" 9 "uri": "hls_450k_video.ts"
9 }, 10 },
10 { 11 {
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
13 "offset": 522828 14 "offset": 522828
14 }, 15 },
15 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
16 "uri": "hls_450k_video.ts" 18 "uri": "hls_450k_video.ts"
17 }, 19 },
18 { 20 {
...@@ -21,6 +23,7 @@ ...@@ -21,6 +23,7 @@
21 "offset": 0 23 "offset": 0
22 }, 24 },
23 "duration": 10, 25 "duration": 10,
26 "timeline": 0,
24 "uri": "hls_450k_video2.ts" 27 "uri": "hls_450k_video2.ts"
25 }, 28 },
26 { 29 {
...@@ -29,6 +32,7 @@ ...@@ -29,6 +32,7 @@
29 "offset": 1823412 32 "offset": 1823412
30 }, 33 },
31 "duration": 10, 34 "duration": 10,
35 "timeline": 0,
32 "uri": "hls_450k_video.ts" 36 "uri": "hls_450k_video.ts"
33 }, 37 },
34 { 38 {
...@@ -37,6 +41,7 @@ ...@@ -37,6 +41,7 @@
37 "offset": 2299992 41 "offset": 2299992
38 }, 42 },
39 "duration": 10, 43 "duration": 10,
44 "timeline": 0,
40 "uri": "hls_450k_video.ts" 45 "uri": "hls_450k_video.ts"
41 }, 46 },
42 { 47 {
...@@ -45,6 +50,7 @@ ...@@ -45,6 +50,7 @@
45 "offset": 2835604 50 "offset": 2835604
46 }, 51 },
47 "duration": 10, 52 "duration": 10,
53 "timeline": 0,
48 "uri": "hls_450k_video.ts" 54 "uri": "hls_450k_video.ts"
49 }, 55 },
50 { 56 {
...@@ -53,6 +59,7 @@ ...@@ -53,6 +59,7 @@
53 "offset": 3042780 59 "offset": 3042780
54 }, 60 },
55 "duration": 10, 61 "duration": 10,
62 "timeline": 0,
56 "uri": "hls_450k_video.ts" 63 "uri": "hls_450k_video.ts"
57 }, 64 },
58 { 65 {
...@@ -61,6 +68,7 @@ ...@@ -61,6 +68,7 @@
61 "offset": 3498680 68 "offset": 3498680
62 }, 69 },
63 "duration": 10, 70 "duration": 10,
71 "timeline": 0,
64 "uri": "hls_450k_video.ts" 72 "uri": "hls_450k_video.ts"
65 }, 73 },
66 { 74 {
...@@ -69,6 +77,7 @@ ...@@ -69,6 +77,7 @@
69 "offset": 4155928 77 "offset": 4155928
70 }, 78 },
71 "duration": 10, 79 "duration": 10,
80 "timeline": 0,
72 "uri": "hls_450k_video.ts" 81 "uri": "hls_450k_video.ts"
73 }, 82 },
74 { 83 {
...@@ -77,6 +86,7 @@ ...@@ -77,6 +86,7 @@
77 "offset": 4727636 86 "offset": 4727636
78 }, 87 },
79 "duration": 10, 88 "duration": 10,
89 "timeline": 0,
80 "uri": "hls_450k_video.ts" 90 "uri": "hls_450k_video.ts"
81 }, 91 },
82 { 92 {
...@@ -85,6 +95,7 @@ ...@@ -85,6 +95,7 @@
85 "offset": 5212676 95 "offset": 5212676
86 }, 96 },
87 "duration": 10, 97 "duration": 10,
98 "timeline": 0,
88 "uri": "hls_450k_video.ts" 99 "uri": "hls_450k_video.ts"
89 }, 100 },
90 { 101 {
...@@ -93,6 +104,7 @@ ...@@ -93,6 +104,7 @@
93 "offset": 5921812 104 "offset": 5921812
94 }, 105 },
95 "duration": 10, 106 "duration": 10,
107 "timeline": 0,
96 "uri": "hls_450k_video.ts" 108 "uri": "hls_450k_video.ts"
97 }, 109 },
98 { 110 {
...@@ -101,6 +113,7 @@ ...@@ -101,6 +113,7 @@
101 "offset": 6651816 113 "offset": 6651816
102 }, 114 },
103 "duration": 10, 115 "duration": 10,
116 "timeline": 0,
104 "uri": "hls_450k_video.ts" 117 "uri": "hls_450k_video.ts"
105 }, 118 },
106 { 119 {
...@@ -109,6 +122,7 @@ ...@@ -109,6 +122,7 @@
109 "offset": 7108092 122 "offset": 7108092
110 }, 123 },
111 "duration": 10, 124 "duration": 10,
125 "timeline": 0,
112 "uri": "hls_450k_video.ts" 126 "uri": "hls_450k_video.ts"
113 }, 127 },
114 { 128 {
...@@ -117,6 +131,7 @@ ...@@ -117,6 +131,7 @@
117 "offset": 7576776 131 "offset": 7576776
118 }, 132 },
119 "duration": 10, 133 "duration": 10,
134 "timeline": 0,
120 "uri": "hls_450k_video.ts" 135 "uri": "hls_450k_video.ts"
121 }, 136 },
122 { 137 {
...@@ -125,6 +140,7 @@ ...@@ -125,6 +140,7 @@
125 "offset": 8021772 140 "offset": 8021772
126 }, 141 },
127 "duration": 10, 142 "duration": 10,
143 "timeline": 0,
128 "uri": "hls_450k_video.ts" 144 "uri": "hls_450k_video.ts"
129 }, 145 },
130 { 146 {
...@@ -133,6 +149,7 @@ ...@@ -133,6 +149,7 @@
133 "offset": 8353216 149 "offset": 8353216
134 }, 150 },
135 "duration": 1.4167, 151 "duration": 1.4167,
152 "timeline": 0,
136 "uri": "hls_450k_video.ts" 153 "uri": "hls_450k_video.ts"
137 } 154 }
138 ], 155 ],
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 } 14 }
14 ], 15 ],
......
...@@ -5,19 +5,23 @@ ...@@ -5,19 +5,23 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 3,
8 "uri": "001.ts" 9 "uri": "001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 19, 12 "duration": 19,
13 "timeline": 3,
12 "uri": "002.ts" 14 "uri": "002.ts"
13 }, 15 },
14 { 16 {
15 "discontinuity": true, 17 "discontinuity": true,
16 "duration": 10, 18 "duration": 10,
19 "timeline": 4,
17 "uri": "003.ts" 20 "uri": "003.ts"
18 }, 21 },
19 { 22 {
20 "duration": 11, 23 "duration": 11,
24 "timeline": 4,
21 "uri": "004.ts" 25 "uri": "004.ts"
22 } 26 }
23 ], 27 ],
......
...@@ -5,41 +5,50 @@ ...@@ -5,41 +5,50 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "001.ts" 9 "uri": "001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 19, 12 "duration": 19,
13 "timeline": 0,
12 "uri": "002.ts" 14 "uri": "002.ts"
13 }, 15 },
14 { 16 {
15 "discontinuity": true, 17 "discontinuity": true,
16 "duration": 10, 18 "duration": 10,
19 "timeline": 1,
17 "uri": "003.ts" 20 "uri": "003.ts"
18 }, 21 },
19 { 22 {
20 "duration": 11, 23 "duration": 11,
24 "timeline": 1,
21 "uri": "004.ts" 25 "uri": "004.ts"
22 }, 26 },
23 { 27 {
24 "discontinuity": true, 28 "discontinuity": true,
25 "duration": 10, 29 "duration": 10,
30 "timeline": 2,
26 "uri": "005.ts" 31 "uri": "005.ts"
27 }, 32 },
28 { 33 {
29 "duration": 10, 34 "duration": 10,
35 "timeline": 2,
30 "uri": "006.ts" 36 "uri": "006.ts"
31 }, 37 },
32 { 38 {
33 "duration": 10, 39 "duration": 10,
40 "timeline": 2,
34 "uri": "007.ts" 41 "uri": "007.ts"
35 }, 42 },
36 { 43 {
37 "discontinuity": true, 44 "discontinuity": true,
38 "duration": 10, 45 "duration": 10,
46 "timeline": 3,
39 "uri": "008.ts" 47 "uri": "008.ts"
40 }, 48 },
41 { 49 {
42 "duration": 16, 50 "duration": 16,
51 "timeline": 3,
43 "uri": "009.ts" 52 "uri": "009.ts"
44 } 53 }
45 ], 54 ],
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "/00001.ts" 9 "uri": "/00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "/subdir/00002.ts" 14 "uri": "/subdir/00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "/00003.ts" 19 "uri": "/00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "/00004.ts" 24 "uri": "/00004.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 } 14 }
14 ], 15 ],
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -4,26 +4,32 @@ ...@@ -4,26 +4,32 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts" 8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
11 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts" 13 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
12 }, 14 },
13 { 15 {
14 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
15 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts" 18 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
16 }, 19 },
17 { 20 {
18 "duration": 10, 21 "duration": 10,
22 "timeline": 0,
19 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts" 23 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
20 }, 24 },
21 { 25 {
22 "duration": 10, 26 "duration": 10,
27 "timeline": 0,
23 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts" 28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
24 }, 29 },
25 { 30 {
26 "duration": 8, 31 "duration": 8,
32 "timeline": 0,
27 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 33 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
28 } 34 }
29 ], 35 ],
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
10 "height": 224 10 "height": 224
11 } 11 }
12 }, 12 },
13 "timeline": 0,
13 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" 14 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
14 }, 15 },
15 { 16 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "PROGRAM-ID": 1, 18 "PROGRAM-ID": 1,
18 "BANDWIDTH": 40000 19 "BANDWIDTH": 40000
19 }, 20 },
21 "timeline": 0,
20 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" 22 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
21 }, 23 },
22 { 24 {
...@@ -28,6 +30,7 @@ ...@@ -28,6 +30,7 @@
28 "height": 224 30 "height": 224
29 } 31 }
30 }, 32 },
33 "timeline": 0,
31 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" 34 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
32 }, 35 },
33 { 36 {
...@@ -39,8 +42,15 @@ ...@@ -39,8 +42,15 @@
39 "height": 540 42 "height": 540
40 } 43 }
41 }, 44 },
45 "timeline": 0,
42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 46 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
43 } 47 }
44 ], 48 ],
45 "discontinuityStarts": [] 49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
46 } 56 }
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
6 "segments": [ 6 "segments": [
7 { 7 {
8 "duration": 2.833, 8 "duration": 2.833,
9 "timeline": 0,
9 "key": { 10 "key": {
10 "method": "AES-128", 11 "method": "AES-128",
11 "uri": "https://priv.example.com/key.php?r=52" 12 "uri": "https://priv.example.com/key.php?r=52"
...@@ -14,6 +15,7 @@ ...@@ -14,6 +15,7 @@
14 }, 15 },
15 { 16 {
16 "duration": 15, 17 "duration": 15,
18 "timeline": 0,
17 "key": { 19 "key": {
18 "method": "AES-128", 20 "method": "AES-128",
19 "uri": "https://priv.example.com/key.php?r=52" 21 "uri": "https://priv.example.com/key.php?r=52"
...@@ -22,6 +24,7 @@ ...@@ -22,6 +24,7 @@
22 }, 24 },
23 { 25 {
24 "duration": 13.333, 26 "duration": 13.333,
27 "timeline": 0,
25 "key": { 28 "key": {
26 "method": "AES-128", 29 "method": "AES-128",
27 "uri": "https://priv.example.com/key.php?r=52" 30 "uri": "https://priv.example.com/key.php?r=52"
...@@ -30,6 +33,7 @@ ...@@ -30,6 +33,7 @@
30 }, 33 },
31 { 34 {
32 "duration": 15, 35 "duration": 15,
36 "timeline": 0,
33 "key": { 37 "key": {
34 "method": "AES-128", 38 "method": "AES-128",
35 "uri": "https://priv.example.com/key.php?r=53" 39 "uri": "https://priv.example.com/key.php?r=53"
...@@ -38,6 +42,7 @@ ...@@ -38,6 +42,7 @@
38 }, 42 },
39 { 43 {
40 "duration": 14, 44 "duration": 14,
45 "timeline": 0,
41 "key": { 46 "key": {
42 "method": "AES-128", 47 "method": "AES-128",
43 "uri": "https://priv.example.com/key.php?r=54", 48 "uri": "https://priv.example.com/key.php?r=54",
...@@ -47,6 +52,7 @@ ...@@ -47,6 +52,7 @@
47 }, 52 },
48 { 53 {
49 "duration": 15, 54 "duration": 15,
55 "timeline": 0,
50 "uri": "http://media.example.com/fileSequence53-B.ts" 56 "uri": "http://media.example.com/fileSequence53-B.ts"
51 } 57 }
52 ], 58 ],
......
...@@ -5,26 +5,32 @@ ...@@ -5,26 +5,32 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts" 9 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts" 14 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts" 19 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts" 24 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
21 }, 25 },
22 { 26 {
23 "duration": 10, 27 "duration": 10,
28 "timeline": 0,
24 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts" 29 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
25 }, 30 },
26 { 31 {
27 "duration": 8, 32 "duration": 8,
33 "timeline": 0,
28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 34 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
29 } 35 }
30 ], 36 ],
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 6.64, 6 "duration": 6.64,
7 "timeline": 0,
7 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
8 } 9 }
9 ], 10 ],
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 }, 14 },
14 { 15 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "offset": 522828 18 "offset": 522828
18 }, 19 },
19 "duration": 10, 20 "duration": 10,
21 "timeline": 0,
20 "uri": "hls_450k_video.ts" 22 "uri": "hls_450k_video.ts"
21 }, 23 },
22 { 24 {
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
25 "offset": 1110328 27 "offset": 1110328
26 }, 28 },
27 "duration": 5, 29 "duration": 5,
30 "timeline": 0,
28 "uri": "hls_450k_video.ts" 31 "uri": "hls_450k_video.ts"
29 }, 32 },
30 { 33 {
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
33 "offset": 1823412 36 "offset": 1823412
34 }, 37 },
35 "duration": 9.7, 38 "duration": 9.7,
39 "timeline": 0,
36 "uri": "hls_450k_video.ts" 40 "uri": "hls_450k_video.ts"
37 }, 41 },
38 { 42 {
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
41 "offset": 2299992 45 "offset": 2299992
42 }, 46 },
43 "duration": 10, 47 "duration": 10,
48 "timeline": 0,
44 "uri": "hls_450k_video.ts" 49 "uri": "hls_450k_video.ts"
45 }, 50 },
46 { 51 {
...@@ -49,6 +54,7 @@ ...@@ -49,6 +54,7 @@
49 "offset": 2835604 54 "offset": 2835604
50 }, 55 },
51 "duration": 10, 56 "duration": 10,
57 "timeline": 0,
52 "uri": "hls_450k_video.ts" 58 "uri": "hls_450k_video.ts"
53 }, 59 },
54 { 60 {
...@@ -57,6 +63,7 @@ ...@@ -57,6 +63,7 @@
57 "offset": 3042780 63 "offset": 3042780
58 }, 64 },
59 "duration": 10, 65 "duration": 10,
66 "timeline": 0,
60 "uri": "hls_450k_video.ts" 67 "uri": "hls_450k_video.ts"
61 }, 68 },
62 { 69 {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
65 "offset": 3498680 72 "offset": 3498680
66 }, 73 },
67 "duration": 10, 74 "duration": 10,
75 "timeline": 0,
68 "uri": "hls_450k_video.ts" 76 "uri": "hls_450k_video.ts"
69 }, 77 },
70 { 78 {
...@@ -73,6 +81,7 @@ ...@@ -73,6 +81,7 @@
73 "offset": 4155928 81 "offset": 4155928
74 }, 82 },
75 "duration": 10, 83 "duration": 10,
84 "timeline": 0,
76 "uri": "hls_450k_video.ts" 85 "uri": "hls_450k_video.ts"
77 }, 86 },
78 { 87 {
...@@ -81,6 +90,7 @@ ...@@ -81,6 +90,7 @@
81 "offset": 4727636 90 "offset": 4727636
82 }, 91 },
83 "duration": 10, 92 "duration": 10,
93 "timeline": 0,
84 "uri": "hls_450k_video.ts" 94 "uri": "hls_450k_video.ts"
85 }, 95 },
86 { 96 {
...@@ -89,6 +99,7 @@ ...@@ -89,6 +99,7 @@
89 "offset": 5212676 99 "offset": 5212676
90 }, 100 },
91 "duration": 10, 101 "duration": 10,
102 "timeline": 0,
92 "uri": "hls_450k_video.ts" 103 "uri": "hls_450k_video.ts"
93 }, 104 },
94 { 105 {
...@@ -97,6 +108,7 @@ ...@@ -97,6 +108,7 @@
97 "offset": 5921812 108 "offset": 5921812
98 }, 109 },
99 "duration": 10, 110 "duration": 10,
111 "timeline": 0,
100 "uri": "hls_450k_video.ts" 112 "uri": "hls_450k_video.ts"
101 }, 113 },
102 { 114 {
...@@ -105,6 +117,7 @@ ...@@ -105,6 +117,7 @@
105 "offset": 6651816 117 "offset": 6651816
106 }, 118 },
107 "duration": 10, 119 "duration": 10,
120 "timeline": 0,
108 "uri": "hls_450k_video.ts" 121 "uri": "hls_450k_video.ts"
109 }, 122 },
110 { 123 {
...@@ -113,6 +126,7 @@ ...@@ -113,6 +126,7 @@
113 "offset": 7108092 126 "offset": 7108092
114 }, 127 },
115 "duration": 10, 128 "duration": 10,
129 "timeline": 0,
116 "uri": "hls_450k_video.ts" 130 "uri": "hls_450k_video.ts"
117 }, 131 },
118 { 132 {
...@@ -121,6 +135,7 @@ ...@@ -121,6 +135,7 @@
121 "offset": 7576776 135 "offset": 7576776
122 }, 136 },
123 "duration": 10, 137 "duration": 10,
138 "timeline": 0,
124 "uri": "hls_450k_video.ts" 139 "uri": "hls_450k_video.ts"
125 }, 140 },
126 { 141 {
...@@ -129,6 +144,7 @@ ...@@ -129,6 +144,7 @@
129 "offset": 8021772 144 "offset": 8021772
130 }, 145 },
131 "duration": 10, 146 "duration": 10,
147 "timeline": 0,
132 "uri": "hls_450k_video.ts" 148 "uri": "hls_450k_video.ts"
133 }, 149 },
134 { 150 {
...@@ -137,6 +153,7 @@ ...@@ -137,6 +153,7 @@
137 "offset": 8353216 153 "offset": 8353216
138 }, 154 },
139 "duration": 10, 155 "duration": 10,
156 "timeline": 0,
140 "uri": "hls_450k_video.ts" 157 "uri": "hls_450k_video.ts"
141 } 158 }
142 ], 159 ],
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 } 14 }
14 ], 15 ],
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -4,26 +4,32 @@ ...@@ -4,26 +4,32 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts" 8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
11 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts" 13 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
12 }, 14 },
13 { 15 {
14 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
15 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts" 18 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
16 }, 19 },
17 { 20 {
18 "duration": 10, 21 "duration": 10,
22 "timeline": 0,
19 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts" 23 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
20 }, 24 },
21 { 25 {
22 "duration": 10, 26 "duration": 10,
27 "timeline": 0,
23 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts" 28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
24 }, 29 },
25 { 30 {
26 "duration": 8, 31 "duration": 8,
32 "timeline": 0,
27 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts" 33 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
28 } 34 }
29 ], 35 ],
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 }, 14 },
14 { 15 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "offset": 522828 18 "offset": 522828
18 }, 19 },
19 "duration": 10, 20 "duration": 10,
21 "timeline": 0,
20 "uri": "hls_450k_video.ts" 22 "uri": "hls_450k_video.ts"
21 }, 23 },
22 { 24 {
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
25 "offset": 1110328 27 "offset": 1110328
26 }, 28 },
27 "duration": 10, 29 "duration": 10,
30 "timeline": 0,
28 "uri": "hls_450k_video.ts" 31 "uri": "hls_450k_video.ts"
29 }, 32 },
30 { 33 {
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
33 "offset": 1823412 36 "offset": 1823412
34 }, 37 },
35 "duration": 10, 38 "duration": 10,
39 "timeline": 0,
36 "uri": "hls_450k_video.ts" 40 "uri": "hls_450k_video.ts"
37 }, 41 },
38 { 42 {
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
41 "offset": 2299992 45 "offset": 2299992
42 }, 46 },
43 "duration": 10, 47 "duration": 10,
48 "timeline": 0,
44 "uri": "hls_450k_video.ts" 49 "uri": "hls_450k_video.ts"
45 }, 50 },
46 { 51 {
...@@ -49,6 +54,7 @@ ...@@ -49,6 +54,7 @@
49 "offset": 2835604 54 "offset": 2835604
50 }, 55 },
51 "duration": 10, 56 "duration": 10,
57 "timeline": 0,
52 "uri": "hls_450k_video.ts" 58 "uri": "hls_450k_video.ts"
53 }, 59 },
54 { 60 {
...@@ -57,6 +63,7 @@ ...@@ -57,6 +63,7 @@
57 "offset": 3042780 63 "offset": 3042780
58 }, 64 },
59 "duration": 10, 65 "duration": 10,
66 "timeline": 0,
60 "uri": "hls_450k_video.ts" 67 "uri": "hls_450k_video.ts"
61 }, 68 },
62 { 69 {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
65 "offset": 3498680 72 "offset": 3498680
66 }, 73 },
67 "duration": 10, 74 "duration": 10,
75 "timeline": 0,
68 "uri": "hls_450k_video.ts" 76 "uri": "hls_450k_video.ts"
69 }, 77 },
70 { 78 {
...@@ -73,6 +81,7 @@ ...@@ -73,6 +81,7 @@
73 "offset": 4155928 81 "offset": 4155928
74 }, 82 },
75 "duration": 10, 83 "duration": 10,
84 "timeline": 0,
76 "uri": "hls_450k_video.ts" 85 "uri": "hls_450k_video.ts"
77 }, 86 },
78 { 87 {
...@@ -81,6 +90,7 @@ ...@@ -81,6 +90,7 @@
81 "offset": 4727636 90 "offset": 4727636
82 }, 91 },
83 "duration": 10, 92 "duration": 10,
93 "timeline": 0,
84 "uri": "hls_450k_video.ts" 94 "uri": "hls_450k_video.ts"
85 }, 95 },
86 { 96 {
...@@ -89,6 +99,7 @@ ...@@ -89,6 +99,7 @@
89 "offset": 5212676 99 "offset": 5212676
90 }, 100 },
91 "duration": 10, 101 "duration": 10,
102 "timeline": 0,
92 "uri": "hls_450k_video.ts" 103 "uri": "hls_450k_video.ts"
93 }, 104 },
94 { 105 {
...@@ -97,6 +108,7 @@ ...@@ -97,6 +108,7 @@
97 "offset": 5921812 108 "offset": 5921812
98 }, 109 },
99 "duration": 10, 110 "duration": 10,
111 "timeline": 0,
100 "uri": "hls_450k_video.ts" 112 "uri": "hls_450k_video.ts"
101 }, 113 },
102 { 114 {
...@@ -105,6 +117,7 @@ ...@@ -105,6 +117,7 @@
105 "offset": 6651816 117 "offset": 6651816
106 }, 118 },
107 "duration": 10, 119 "duration": 10,
120 "timeline": 0,
108 "uri": "hls_450k_video.ts" 121 "uri": "hls_450k_video.ts"
109 }, 122 },
110 { 123 {
...@@ -113,6 +126,7 @@ ...@@ -113,6 +126,7 @@
113 "offset": 7108092 126 "offset": 7108092
114 }, 127 },
115 "duration": 10, 128 "duration": 10,
129 "timeline": 0,
116 "uri": "hls_450k_video.ts" 130 "uri": "hls_450k_video.ts"
117 }, 131 },
118 { 132 {
...@@ -121,6 +135,7 @@ ...@@ -121,6 +135,7 @@
121 "offset": 7576776 135 "offset": 7576776
122 }, 136 },
123 "duration": 10, 137 "duration": 10,
138 "timeline": 0,
124 "uri": "hls_450k_video.ts" 139 "uri": "hls_450k_video.ts"
125 }, 140 },
126 { 141 {
...@@ -129,6 +144,7 @@ ...@@ -129,6 +144,7 @@
129 "offset": 8021772 144 "offset": 8021772
130 }, 145 },
131 "duration": 10, 146 "duration": 10,
147 "timeline": 0,
132 "uri": "hls_450k_video.ts" 148 "uri": "hls_450k_video.ts"
133 }, 149 },
134 { 150 {
...@@ -137,6 +153,7 @@ ...@@ -137,6 +153,7 @@
137 "offset": 8353216 153 "offset": 8353216
138 }, 154 },
139 "duration": 1.4167, 155 "duration": 1.4167,
156 "timeline": 0,
140 "uri": "hls_450k_video.ts" 157 "uri": "hls_450k_video.ts"
141 } 158 }
142 ], 159 ],
......
...@@ -5,14 +5,17 @@ ...@@ -5,14 +5,17 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 8, 12 "duration": 8,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 8, 17 "duration": 8,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 } 20 }
18 ], 21 ],
......
...@@ -4,38 +4,47 @@ ...@@ -4,38 +4,47 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "001.ts" 8 "uri": "001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 19, 11 "duration": 19,
12 "timeline": 0,
11 "uri": "002.ts" 13 "uri": "002.ts"
12 }, 14 },
13 { 15 {
14 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
15 "uri": "003.ts" 18 "uri": "003.ts"
16 }, 19 },
17 { 20 {
18 "duration": 11, 21 "duration": 11,
22 "timeline": 0,
19 "uri": "004.ts" 23 "uri": "004.ts"
20 }, 24 },
21 { 25 {
22 "duration": 10, 26 "duration": 10,
27 "timeline": 0,
23 "uri": "005.ts" 28 "uri": "005.ts"
24 }, 29 },
25 { 30 {
26 "duration": 10, 31 "duration": 10,
32 "timeline": 0,
27 "uri": "006.ts" 33 "uri": "006.ts"
28 }, 34 },
29 { 35 {
30 "duration": 10, 36 "duration": 10,
37 "timeline": 0,
31 "uri": "007.ts" 38 "uri": "007.ts"
32 }, 39 },
33 { 40 {
34 "duration": 10, 41 "duration": 10,
42 "timeline": 0,
35 "uri": "008.ts" 43 "uri": "008.ts"
36 }, 44 },
37 { 45 {
38 "duration": 16, 46 "duration": 16,
47 "timeline": 0,
39 "uri": "009.ts" 48 "uri": "009.ts"
40 } 49 }
41 ], 50 ],
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/gogo/00001.ts" 8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
8 } 9 }
9 ], 10 ],
......
...@@ -4,22 +4,27 @@ ...@@ -4,22 +4,27 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/gogo/00001.ts" 8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
11 "uri": "/test/ts-files/zencoder/gogo/00002.ts" 13 "uri": "/test/ts-files/zencoder/gogo/00002.ts"
12 }, 14 },
13 { 15 {
14 "duration": 10, 16 "duration": 10,
17 "timeline": 0,
15 "uri": "/test/ts-files/zencoder/gogo/00003.ts" 18 "uri": "/test/ts-files/zencoder/gogo/00003.ts"
16 }, 19 },
17 { 20 {
18 "duration": 10, 21 "duration": 10,
22 "timeline": 0,
19 "uri": "/test/ts-files/zencoder/gogo/00004.ts" 23 "uri": "/test/ts-files/zencoder/gogo/00004.ts"
20 }, 24 },
21 { 25 {
22 "duration": 10, 26 "duration": 10,
27 "timeline": 0,
23 "uri": "/test/ts-files/zencoder/gogo/00005.ts" 28 "uri": "/test/ts-files/zencoder/gogo/00005.ts"
24 } 29 }
25 ], 30 ],
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "/test/ts-files/zencoder/gogo/00001.ts" 8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
8 } 9 }
9 ], 10 ],
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
10 "height": 224 10 "height": 224
11 } 11 }
12 }, 12 },
13 "timeline": 0,
13 "uri": "media.m3u8" 14 "uri": "media.m3u8"
14 }, 15 },
15 { 16 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "PROGRAM-ID": 1, 18 "PROGRAM-ID": 1,
18 "BANDWIDTH": 40000 19 "BANDWIDTH": 40000
19 }, 20 },
21 "timeline": 0,
20 "uri": "media1.m3u8" 22 "uri": "media1.m3u8"
21 }, 23 },
22 { 24 {
...@@ -28,6 +30,7 @@ ...@@ -28,6 +30,7 @@
28 "height": 224 30 "height": 224
29 } 31 }
30 }, 32 },
33 "timeline": 0,
31 "uri": "media2.m3u8" 34 "uri": "media2.m3u8"
32 }, 35 },
33 { 36 {
...@@ -39,8 +42,15 @@ ...@@ -39,8 +42,15 @@
39 "height": 540 42 "height": 540
40 } 43 }
41 }, 44 },
45 "timeline": 0,
42 "uri": "media3.m3u8" 46 "uri": "media3.m3u8"
43 } 47 }
44 ], 48 ],
45 "discontinuityStarts": [] 49 "discontinuityStarts": [],
50 "mediaGroups": {
51 "VIDEO": {},
52 "AUDIO": {},
53 "CLOSED-CAPTIONS": {},
54 "SUBTITLES": {}
55 }
46 } 56 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "media-00001.ts" 9 "uri": "media-00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "media-00002.ts" 14 "uri": "media-00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "media-00003.ts" 19 "uri": "media-00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "media-00004.ts" 24 "uri": "media-00004.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -4,10 +4,12 @@ ...@@ -4,10 +4,12 @@
4 "segments": [ 4 "segments": [
5 { 5 {
6 "duration": 10, 6 "duration": 10,
7 "timeline": 0,
7 "uri": "00001.ts" 8 "uri": "00001.ts"
8 }, 9 },
9 { 10 {
10 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
11 "uri": "00002.ts" 13 "uri": "00002.ts"
12 } 14 }
13 ], 15 ],
......
...@@ -5,14 +5,17 @@ ...@@ -5,14 +5,17 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "hls_450k_video.ts" 9 "uri": "hls_450k_video.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "hls_450k_video.ts" 14 "uri": "hls_450k_video.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "hls_450k_video.ts" 19 "uri": "hls_450k_video.ts"
17 } 20 }
18 ], 21 ],
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 8, 12 "duration": 8,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 8, 17 "duration": 8,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 8, 22 "duration": 8,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
......
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 AUDIO: {
6 "audio-lo": {
7 "English": {
8 autoselect: true,
9 default: true,
10 language: "eng",
11 uri: "englo/prog_index.m3u8"
12 },
13 "Français": {
14 autoselect: true,
15 default: false,
16 language: "fre",
17 uri: "frelo/prog_index.m3u8"
18 },
19 "Espanol": {
20 autoselect: true,
21 default: false,
22 language: "sp",
23 uri: "splo/prog_index.m3u8"
24 }
25 },
26 "audio-hi": {
27 "English": {
28 autoselect: true,
29 default: true,
30 language: "eng",
31 uri: "eng/prog_index.m3u8"
32 },
33 "Français": {
34 autoselect: true,
35 default: false,
36 language: "fre",
37 uri: "fre/prog_index.m3u8"
38 },
39 "Espanol": {
40 autoselect: true,
41 default: false,
42 language: "sp",
43 uri: "sp/prog_index.m3u8"
44 }
45 }
46 },
47 VIDEO: {},
48 "CLOSED-CAPTIONS": {},
49 SUBTITLES: {}
50 },
51 playlists: [{
52 attributes: {
53 "PROGRAM-ID": 1,
54 BANDWIDTH: 195023,
55 CODECS: "mp4a.40.5",
56 AUDIO: "audio-lo",
57 },
58 timeline: 0,
59 uri: "lo/prog_index.m3u8"
60 }, {
61 attributes: {
62 "PROGRAM-ID": 1,
63 BANDWIDTH: 260000,
64 CODECS: "avc1.42e01e,mp4a.40.2",
65 AUDIO: "audio-lo"
66 },
67 timeline: 0,
68 uri: "lo2/prog_index.m3u8"
69 }, {
70 attributes: {
71 "PROGRAM-ID": 1,
72 BANDWIDTH: 591680,
73 CODECS: "mp4a.40.2, avc1.64001e",
74 AUDIO: "audio-hi"
75 },
76 timeline: 0,
77 uri: "hi/prog_index.m3u8"
78 }, {
79 attributes: {
80 "PROGRAM-ID": 1,
81 BANDWIDTH: 650000,
82 CODECS: "avc1.42e01e,mp4a.40.2",
83 AUDIO: "audio-hi"
84 },
85 timeline: 0,
86 uri: "hi2/prog_index.m3u8"
87 }]
88 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="englo/prog_index.m3u8"
3 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="frelo/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="splo/prog_index.m3u8"
5
6 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
7 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
9
10 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5", AUDIO="audio-lo"
11 lo/prog_index.m3u8
12 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-lo"
13 lo2/prog_index.m3u8
14 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e", AUDIO="audio-hi"
15 hi/prog_index.m3u8
16 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-hi"
17 hi2/prog_index.m3u8
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 AUDIO: {
6 "audio-lo": {
7 "English": {
8 autoselect: true,
9 default: true,
10 language: "eng",
11 },
12 "Français": {
13 autoselect: true,
14 default: false,
15 language: "fre",
16 uri: "frelo/prog_index.m3u8"
17 },
18 "Espanol": {
19 autoselect: true,
20 default: false,
21 language: "sp",
22 uri: "splo/prog_index.m3u8"
23 }
24 },
25 "audio-hi": {
26 "English": {
27 autoselect: true,
28 default: true,
29 language: "eng",
30 uri: "eng/prog_index.m3u8"
31 },
32 "Français": {
33 autoselect: true,
34 default: false,
35 language: "fre",
36 uri: "fre/prog_index.m3u8"
37 },
38 "Espanol": {
39 autoselect: true,
40 default: false,
41 language: "sp",
42 uri: "sp/prog_index.m3u8"
43 }
44 }
45 },
46 VIDEO: {},
47 "CLOSED-CAPTIONS": {},
48 SUBTITLES: {}
49 },
50 playlists: [{
51 attributes: {
52 "PROGRAM-ID": 1,
53 BANDWIDTH: 195023,
54 CODECS: "mp4a.40.5",
55 AUDIO: "audio-lo",
56 },
57 timeline: 0,
58 uri: "lo/prog_index.m3u8"
59 }, {
60 attributes: {
61 "PROGRAM-ID": 1,
62 BANDWIDTH: 260000,
63 CODECS: "avc1.42e01e,mp4a.40.2",
64 AUDIO: "audio-lo"
65 },
66 timeline: 0,
67 uri: "lo2/prog_index.m3u8"
68 }, {
69 attributes: {
70 "PROGRAM-ID": 1,
71 BANDWIDTH: 591680,
72 CODECS: "mp4a.40.2, avc1.64001e",
73 AUDIO: "audio-hi"
74 },
75 timeline: 0,
76 uri: "hi/prog_index.m3u8"
77 }, {
78 attributes: {
79 "PROGRAM-ID": 1,
80 BANDWIDTH: 650000,
81 CODECS: "avc1.42e01e,mp4a.40.2",
82 AUDIO: "audio-hi"
83 },
84 timeline: 0,
85 uri: "hi2/prog_index.m3u8"
86 }]
87 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES
3 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="frelo/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="splo/prog_index.m3u8"
5
6 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
7 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
9
10 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5", AUDIO="audio-lo"
11 lo/prog_index.m3u8
12 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-lo"
13 lo2/prog_index.m3u8
14 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e", AUDIO="audio-hi"
15 hi/prog_index.m3u8
16 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-hi"
17 hi2/prog_index.m3u8
...@@ -4,19 +4,23 @@ ...@@ -4,19 +4,23 @@
4 "targetDuration": 10, 4 "targetDuration": 10,
5 "segments": [ 5 "segments": [
6 { 6 {
7 "uri": "001.ts" 7 "uri": "001.ts",
8 "timeline": 0
8 }, 9 },
9 { 10 {
10 "uri": "002.ts", 11 "uri": "002.ts",
11 "duration": 9 12 "duration": 9,
13 "timeline": 0
12 }, 14 },
13 { 15 {
14 "uri": "003.ts", 16 "uri": "003.ts",
15 "duration": 7 17 "duration": 7,
18 "timeline": 0
16 }, 19 },
17 { 20 {
18 "uri": "004.ts", 21 "uri": "004.ts",
19 "duration": 10 22 "duration": 10,
23 "timeline": 0
20 } 24 }
21 ], 25 ],
22 "discontinuitySequence": 0, 26 "discontinuitySequence": 0,
......
1 {
2 allowCache: true,
3 discontinuityStarts: [],
4 mediaGroups: {
5 AUDIO: {
6 aac: {
7 English: {
8 autoselect: true,
9 default: true,
10 language: "eng",
11 uri: "eng/prog_index.m3u8"
12 }
13 }
14 },
15 VIDEO: {
16 "200kbs": {
17 Angle1: {
18 autoselect: true,
19 default: true
20 },
21 Angle2: {
22 autoselect: true,
23 default: false,
24 uri: "Angle2/200kbs/prog_index.m3u8"
25 },
26 Angle3: {
27 autoselect: true,
28 default: false,
29 uri: "Angle3/200kbs/prog_index.m3u8"
30 }
31 },
32 "500kbs": {
33 Angle1: {
34 autoselect: true,
35 default: true
36 },
37 Angle2: {
38 autoselect: true,
39 default: false,
40 uri: "Angle2/500kbs/prog_index.m3u8"
41 },
42 Angle3: {
43 autoselect: true,
44 default: false,
45 uri: "Angle3/500kbs/prog_index.m3u8"
46 }
47 }
48 },
49 "CLOSED-CAPTIONS": {},
50 SUBTITLES: {}
51 },
52 playlists: [{
53 attributes: {
54 "PROGRAM-ID": 1,
55 BANDWIDTH: 300000,
56 CODECS: "mp4a.40.2,avc1.4d401e",
57 AUDIO: "aac",
58 VIDEO: "200kbs"
59 },
60 timeline: 0,
61 uri: "Angle1/200kbs/prog_index.m3u"
62 }, {
63 attributes: {
64 "PROGRAM-ID": 1,
65 BANDWIDTH: 754857,
66 CODECS: "mp4a.40.2,avc1.4d401e",
67 AUDIO: "aac",
68 VIDEO: "500kbs"
69 },
70 timeline: 0,
71 uri: "Angle1/500kbs/prog_index.m3u8"
72 }]
73 }
1 #EXTM3U
2 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
3 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/200kbs/prog_index.m3u8"
4 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/200kbs/prog_index.m3u8"
5
6 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
7 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8"
8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8"
9
10 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8"
11
12 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="200kbs",AUDIO="aac"
13 Angle1/200kbs/prog_index.m3u
14
15 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac"
16 Angle1/500kbs/prog_index.m3u8
...\ No newline at end of file ...\ No newline at end of file
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
9 "offset": 0 9 "offset": 0
10 }, 10 },
11 "duration": 10, 11 "duration": 10,
12 "timeline": 0,
12 "uri": "hls_450k_video.ts" 13 "uri": "hls_450k_video.ts"
13 }, 14 },
14 { 15 {
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
17 "offset": 522828 18 "offset": 522828
18 }, 19 },
19 "duration": 10, 20 "duration": 10,
21 "timeline": 0,
20 "uri": "hls_450k_video.ts" 22 "uri": "hls_450k_video.ts"
21 }, 23 },
22 { 24 {
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
25 "offset": 1110328 27 "offset": 1110328
26 }, 28 },
27 "duration": 10, 29 "duration": 10,
30 "timeline": 0,
28 "uri": "hls_450k_video.ts" 31 "uri": "hls_450k_video.ts"
29 }, 32 },
30 { 33 {
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
33 "offset": 1823412 36 "offset": 1823412
34 }, 37 },
35 "duration": 10, 38 "duration": 10,
39 "timeline": 0,
36 "uri": "hls_450k_video.ts" 40 "uri": "hls_450k_video.ts"
37 }, 41 },
38 { 42 {
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
41 "offset": 2299992 45 "offset": 2299992
42 }, 46 },
43 "duration": 10, 47 "duration": 10,
48 "timeline": 0,
44 "uri": "hls_450k_video.ts" 49 "uri": "hls_450k_video.ts"
45 }, 50 },
46 { 51 {
...@@ -49,6 +54,7 @@ ...@@ -49,6 +54,7 @@
49 "offset": 2835604 54 "offset": 2835604
50 }, 55 },
51 "duration": 10, 56 "duration": 10,
57 "timeline": 0,
52 "uri": "hls_450k_video.ts" 58 "uri": "hls_450k_video.ts"
53 }, 59 },
54 { 60 {
...@@ -57,6 +63,7 @@ ...@@ -57,6 +63,7 @@
57 "offset": 3042780 63 "offset": 3042780
58 }, 64 },
59 "duration": 10, 65 "duration": 10,
66 "timeline": 0,
60 "uri": "hls_450k_video.ts" 67 "uri": "hls_450k_video.ts"
61 }, 68 },
62 { 69 {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
65 "offset": 3498680 72 "offset": 3498680
66 }, 73 },
67 "duration": 10, 74 "duration": 10,
75 "timeline": 0,
68 "uri": "hls_450k_video.ts" 76 "uri": "hls_450k_video.ts"
69 }, 77 },
70 { 78 {
...@@ -73,6 +81,7 @@ ...@@ -73,6 +81,7 @@
73 "offset": 4155928 81 "offset": 4155928
74 }, 82 },
75 "duration": 10, 83 "duration": 10,
84 "timeline": 0,
76 "uri": "hls_450k_video.ts" 85 "uri": "hls_450k_video.ts"
77 }, 86 },
78 { 87 {
...@@ -81,6 +90,7 @@ ...@@ -81,6 +90,7 @@
81 "offset": 4727636 90 "offset": 4727636
82 }, 91 },
83 "duration": 10, 92 "duration": 10,
93 "timeline": 0,
84 "uri": "hls_450k_video.ts" 94 "uri": "hls_450k_video.ts"
85 }, 95 },
86 { 96 {
...@@ -89,6 +99,7 @@ ...@@ -89,6 +99,7 @@
89 "offset": 5212676 99 "offset": 5212676
90 }, 100 },
91 "duration": 10, 101 "duration": 10,
102 "timeline": 0,
92 "uri": "hls_450k_video.ts" 103 "uri": "hls_450k_video.ts"
93 }, 104 },
94 { 105 {
...@@ -97,6 +108,7 @@ ...@@ -97,6 +108,7 @@
97 "offset": 5921812 108 "offset": 5921812
98 }, 109 },
99 "duration": 10, 110 "duration": 10,
111 "timeline": 0,
100 "uri": "hls_450k_video.ts" 112 "uri": "hls_450k_video.ts"
101 }, 113 },
102 { 114 {
...@@ -105,6 +117,7 @@ ...@@ -105,6 +117,7 @@
105 "offset": 6651816 117 "offset": 6651816
106 }, 118 },
107 "duration": 10, 119 "duration": 10,
120 "timeline": 0,
108 "uri": "hls_450k_video.ts" 121 "uri": "hls_450k_video.ts"
109 }, 122 },
110 { 123 {
...@@ -113,6 +126,7 @@ ...@@ -113,6 +126,7 @@
113 "offset": 7108092 126 "offset": 7108092
114 }, 127 },
115 "duration": 10, 128 "duration": 10,
129 "timeline": 0,
116 "uri": "hls_450k_video.ts" 130 "uri": "hls_450k_video.ts"
117 }, 131 },
118 { 132 {
...@@ -121,6 +135,7 @@ ...@@ -121,6 +135,7 @@
121 "offset": 7576776 135 "offset": 7576776
122 }, 136 },
123 "duration": 10, 137 "duration": 10,
138 "timeline": 0,
124 "uri": "hls_450k_video.ts" 139 "uri": "hls_450k_video.ts"
125 }, 140 },
126 { 141 {
...@@ -129,6 +144,7 @@ ...@@ -129,6 +144,7 @@
129 "offset": 8021772 144 "offset": 8021772
130 }, 145 },
131 "duration": 10, 146 "duration": 10,
147 "timeline": 0,
132 "uri": "hls_450k_video.ts" 148 "uri": "hls_450k_video.ts"
133 }, 149 },
134 { 150 {
...@@ -137,6 +153,7 @@ ...@@ -137,6 +153,7 @@
137 "offset": 8353216 153 "offset": 8353216
138 }, 154 },
139 "duration": 1.4167, 155 "duration": 1.4167,
156 "timeline": 0,
140 "uri": "hls_450k_video.ts" 157 "uri": "hls_450k_video.ts"
141 } 158 }
142 ], 159 ],
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 } 10 }
10 ], 11 ],
......
...@@ -5,11 +5,19 @@ ...@@ -5,11 +5,19 @@
5 "attributes": { 5 "attributes": {
6 "PROGRAM-ID": 1 6 "PROGRAM-ID": 1
7 }, 7 },
8 "timeline": 0,
8 "uri": "media.m3u8" 9 "uri": "media.m3u8"
9 }, 10 },
10 { 11 {
12 "timeline": 0,
11 "uri": "media1.m3u8" 13 "uri": "media1.m3u8"
12 } 14 }
13 ], 15 ],
14 "discontinuityStarts": [] 16 "discontinuityStarts": [],
17 "mediaGroups": {
18 "VIDEO": {},
19 "AUDIO": {},
20 "CLOSED-CAPTIONS": {},
21 "SUBTITLES": {}
22 }
15 } 23 }
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 6.64, 7 "duration": 6.64,
8 "timeline": 0,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts" 9 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }, 10 },
10 { 11 {
11 "duration": 6.08, 12 "duration": 6.08,
13 "timeline": 0,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts" 14 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 }, 15 },
14 { 16 {
15 "duration": 6.6, 17 "duration": 6.6,
18 "timeline": 0,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts" 19 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }, 20 },
18 { 21 {
19 "duration": 5, 22 "duration": 5,
23 "timeline": 0,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts" 24 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 } 25 }
22 ], 26 ],
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "hls_450k_video.ts" 9 "uri": "hls_450k_video.ts"
9 } 10 }
10 ], 11 ],
......
...@@ -5,18 +5,22 @@ ...@@ -5,18 +5,22 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "timeline": 0,
8 "uri": "http://example.com/00001.ts" 9 "uri": "http://example.com/00001.ts"
9 }, 10 },
10 { 11 {
11 "duration": 10, 12 "duration": 10,
13 "timeline": 0,
12 "uri": "https://example.com/00002.ts" 14 "uri": "https://example.com/00002.ts"
13 }, 15 },
14 { 16 {
15 "duration": 10, 17 "duration": 10,
18 "timeline": 0,
16 "uri": "//example.com/00003.ts" 19 "uri": "//example.com/00003.ts"
17 }, 20 },
18 { 21 {
19 "duration": 10, 22 "duration": 10,
23 "timeline": 0,
20 "uri": "http://example.com/00004.ts" 24 "uri": "http://example.com/00004.ts"
21 } 25 }
22 ], 26 ],
......
1 (function(videojs) {
2 var Component = videojs.getComponent('Component');
3
4 // -----------------
5 // AudioTrackMenuItem
6 // -----------------
7 //
8 var MenuItem = videojs.getComponent('MenuItem');
9
10 var AudioTrackMenuItem = videojs.extend(MenuItem, {
11 constructor: function(player, options) {
12 var track = options.track;
13 var tracks = player.audioTracks();
14
15 options.label = track.label || track.language || 'Unknown';
16 options.selected = track.enabled;
17
18 MenuItem.call(this, player, options);
19
20 this.track = track;
21
22 if (tracks) {
23 var changeHandler = videojs.bind(this, this.handleTracksChange);
24
25 tracks.addEventListener('change', changeHandler);
26 this.on('dispose', function() {
27 tracks.removeEventListener('change', changeHandler);
28 });
29 }
30 },
31
32 handleClick: function(event) {
33 var kind = this.track.kind;
34 var tracks = this.player_.audioTracks();
35
36 MenuItem.prototype.handleClick.call(this, event);
37
38 if (!tracks) return;
39
40 for (var i = 0; i < tracks.length; i++) {
41 var track = tracks[i];
42
43 if (track === this.track) {
44 track.enabled = true;
45 }
46 }
47 },
48
49 handleTracksChange: function(event) {
50 this.selected(this.track.enabled);
51 }
52 });
53
54 Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
55
56 // -----------------
57 // AudioTrackButton
58 // -----------------
59 //
60 var MenuButton = videojs.getComponent('MenuButton');
61
62 var AudioTrackButton = videojs.extend(MenuButton, {
63 constructor: function(player, options) {
64 MenuButton.call(this, player, options);
65 this.el_.setAttribute('aria-label','Audio Menu');
66
67 var tracks = this.player_.audioTracks();
68
69 if (this.items.length <= 1) {
70 this.hide();
71 }
72
73 if (!tracks) {
74 return;
75 }
76
77 var updateHandler = videojs.bind(this, this.update);
78 tracks.addEventListener('removetrack', updateHandler);
79 tracks.addEventListener('addtrack', updateHandler);
80
81 this.player_.on('dispose', function() {
82 tracks.removeEventListener('removetrack', updateHandler);
83 tracks.removeEventListener('addtrack', updateHandler);
84 });
85 },
86
87 buildCSSClass() {
88 return 'vjs-subtitles-button ' + MenuButton.prototype.buildCSSClass.call(this);
89 },
90
91 createItems: function(items) {
92 items = items || [];
93
94 var tracks = this.player_.audioTracks();
95
96 if (!tracks) {
97 return items;
98 }
99
100 for (var i = 0; i < tracks.length; i++) {
101 var track = tracks[i];
102
103 items.push(new AudioTrackMenuItem(this.player_, {
104 'selectable': true,
105 'track': track
106 }));
107 }
108
109 return items;
110 }
111 });
112
113 Component.registerComponent('AudioTrackButton', AudioTrackButton);
114 })(window.videojs);
...@@ -9,28 +9,16 @@ ...@@ -9,28 +9,16 @@
9 <!-- video.js --> 9 <!-- video.js -->
10 <script src="../../node_modules/video.js/dist/video.js"></script> 10 <script src="../../node_modules/video.js/dist/video.js"></script>
11 11
12 <!-- Media Sources plugin -->
13 <script src="../../node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
14
15 <!-- HLS plugin --> 12 <!-- HLS plugin -->
16 <script src="../../src/videojs-hls.js"></script> 13 <script src="../../dist/videojs-contrib-hls.js"></script>
17
18 <!-- m3u8 handling -->
19 <script src="../../src/xhr.js"></script>
20 <script src="../../src/stream.js"></script>
21 <script src="../../src/m3u8/m3u8-parser.js"></script>
22 <script src="../../src/playlist.js"></script>
23 <script src="../../src/playlist-loader.js"></script>
24 14
25 <script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script> 15 <!-- Track Selector plugin -->
26 <script src="../../src/decrypter.js"></script> 16 <script src="audio-track-selector.js"></script>
27 17
28 <!-- player stats visualization --> 18 <!-- player stats visualization -->
29 <link href="stats.css" rel="stylesheet"> 19 <link href="stats.css" rel="stylesheet">
30 <script src="../switcher/js/vendor/d3.min.js"></script> 20 <script src="../switcher/js/vendor/d3.min.js"></script>
31 21
32 <!-- debugging -->
33 <script src="../../src/bin-utils.js"></script>
34 <style> 22 <style>
35 body { 23 body {
36 font-family: Arial, sans-serif; 24 font-family: Arial, sans-serif;
...@@ -43,13 +31,12 @@ ...@@ -43,13 +31,12 @@
43 padding: 0 5px; 31 padding: 0 5px;
44 margin: 20px 0; 32 margin: 20px 0;
45 } 33 }
46 </style> 34 input {
47 35 margin-top: 15px;
48 <script> 36 min-width: 450px;
49 if (window.location.search === '?flash') { 37 padding: 5px;
50 videojs.options.techOrder = ['flash'];
51 } 38 }
52 </script> 39 </style>
53 40
54 </head> 41 </head>
55 <body> 42 <body>
...@@ -57,18 +44,50 @@ ...@@ -57,18 +44,50 @@
57 <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> 44 <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>
58 <p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p> 45 <p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p>
59 </div> 46 </div>
60 <video id="video" 47
61 class="video-js vjs-default-skin" 48 <div id="fixture">
62 height="300" 49 </div>
63 width="600" 50
64 controls> 51 <form id="load-url">
65 <source 52 <label>
66 src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8" 53 URL to Load:
67 type="application/x-mpegURL"> 54 <input id="url-to-load" type="url" value="https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8">
68 <source 55 </label>
69 src="http://s3.amazonaws.com/_bc_dml/example-content/bipbop-id3/index.m3u8" 56 <br>
70 type="application/x-mpegURL"> 57 <label>
71 </video> 58 Player Options:
59 <input id="player-options" type="text" value='{}'>
60 </label>
61 <br>
62 <label>
63 URL Content Type:
64 <input id="url-content-type" type="text" value="application/x-mpegURL">
65 </label>
66 <br>
67 <label>
68 Default Caption Track Label (blank for none):
69 <input id="caption-track" type="text" value="">
70 </label>
71 <br>
72 <label>
73 Technology Mode:
74 <input type="radio" name="technology-mode" value="auto" checked> Auto
75 <input type="radio" name="technology-mode" value="html"> HTML
76 <input type="radio" name="technology-mode" value="flash"> Flash
77 </label>
78 <br>
79 <label>
80 Autoplay:
81 <input type="radio" name="autoplay" value="on"> On
82 <input type="radio" name="autoplay" value="off" checked> Off
83 </label>
84
85 <br>
86 <br>
87 <button type="submit">Load</button>
88 </form>
89 <br>
90
72 <section class="stats"> 91 <section class="stats">
73 <div class="player-stats"> 92 <div class="player-stats">
74 <h2>Player Stats</h2> 93 <h2>Player Stats</h2>
...@@ -109,9 +128,97 @@ ...@@ -109,9 +128,97 @@
109 128
110 <script src="stats.js"></script> 129 <script src="stats.js"></script>
111 <script> 130 <script>
112 videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf'; 131 function getCheckedValue(name) {
113 // initialize the player 132 var radios = document.getElementsByName(name);
114 var player = videojs('video').ready(function() { 133 var value;
134 for (var i = 0, length = radios.length; i < length; i++) {
135 if (radios[i].checked) {
136 value = radios[i].value;
137 break;
138 }
139 }
140 return value;
141 }
142
143 function createPlayer(cb) {
144 if (window.stats_timer) {
145 clearInterval(window.stats_timer);
146 }
147 // dispose of existing player
148 if(window.player) {
149 window.player.dispose();
150 }
151
152 // create video element in the dom
153 var video = document.createElement('video');
154 video.id = 'videojs-contrib-hls-player';
155 video.className = 'video-js vjs-default-skin';
156 video.setAttribute('controls', true);
157 video.setAttribute('height', 300);
158 video.setAttribute('width', 600);
159 document.querySelector('#fixture').appendChild(video);
160 var techRadios = document.getElementsByName('technology-mode');
161 var techMode = getCheckedValue('technology-mode') || 'auto';
162 var autoplay = getCheckedValue('autoplay') || 'off';
163 var captionTrack = document.getElementById('caption-track').value || "";
164 var url = document.getElementById('url-to-load').value || "";
165 var options = {};
166 // try to parse options from the form
167 try {
168 options = JSON.parse(document.getElementById('player-options').value);
169 } catch(err) {
170 console.log("Reseting options to {}, JSON Parse Error:", err);
171 }
172 if(typeof options.techOrder === 'undefined') {
173 if (techMode === 'html') {
174 options.techOrder = ['html5'];
175 } else if (techMode === 'flash') {
176 options.techOrder = ['flash'];
177 }
178 }
179 if(typeof options.autoplay === 'undefined') {
180 if (autoplay === 'on') {
181 options.autoplay = true;
182 } else if (techMode === 'off') {
183 options.autoplay = false;
184 }
185 }
186 var type = document.getElementById('url-content-type').value;
187 // use the form data to add a src to the player
188 try {
189 window.player = videojs(video.id, options);
190 if(captionTrack) {
191 // hackey way to show captions
192 window.player.on('loadeddata', function(event) {
193 textTracks = this.textTracks().tracks_;
194 for(var i = 0; i < textTracks.length; i++) {
195 if(textTracks[i].label === captionTrack) {
196 console.log("Found matching track, going to show " + captionTrack);
197 textTracks[i].mode = "showing";
198 break;
199 }
200 }
201 });
202 }
203 window.player.src({
204 src: url,
205 type: type
206 });
207
208 cb(player);
209 } catch(err) {
210 console.log("caught an error trying to create and add src to player:", err);
211 }
212 }
213
214 function setup(player) {
215 player.ready(function() {
216
217 // ------------
218 // Audio Track Switcher
219 // ------------
220
221 player.controlBar.addChild('AudioTrackButton', {}, 13);
115 222
116 // ------------ 223 // ------------
117 // Player Stats 224 // Player Stats
...@@ -128,7 +235,7 @@ ...@@ -128,7 +235,7 @@
128 currentTimeStat.textContent = player.currentTime().toFixed(1); 235 currentTimeStat.textContent = player.currentTime().toFixed(1);
129 }); 236 });
130 237
131 window.setInterval(function() { 238 window.stats_timer = window.setInterval(function() {
132 var bufferedText = '', oldStart, oldEnd, i; 239 var bufferedText = '', oldStart, oldEnd, i;
133 240
134 // buffered 241 // buffered
...@@ -184,23 +291,18 @@ ...@@ -184,23 +291,18 @@
184 videojs.Hls.displayStats(document.querySelector('.switching-stats'), player); 291 videojs.Hls.displayStats(document.querySelector('.switching-stats'), player);
185 videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player); 292 videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player);
186 }); 293 });
187
188 // -----------
189 // Tech Switch
190 // -----------
191
192 var techSwitch = document.createElement('a');
193 techSwitch.className = 'tech-switch';
194 if (player.el().querySelector('video')) {
195 techSwitch.href = window.location.origin + window.location.pathname + '?flash';
196 techSwitch.appendChild(document.createTextNode('Switch to the Flash tech'));
197 } else {
198 techSwitch.href = window.location.origin + window.location.pathname;
199 techSwitch.appendChild(document.createTextNode('Stop forcing Flash'));
200 } 294 }
201 295
202 document.body.insertBefore(techSwitch, document.querySelector('.stats')); 296 (function(window, videojs) {
203 297 videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf';
298 createPlayer(setup);
299 // hook up the video switcher
300 document.getElementById('load-url').addEventListener('submit', function(event) {
301 event.preventDefault();
302 createPlayer(setup);
303 return false;
304 });
305 }(window, window.videojs));
204 </script> 306 </script>
205 </body> 307 </body>
206 </html> 308 </html>
......