216ff405 by David LaPalomento

Merge pull request #7 from brightcove/feature/basic-playback

Single-bitrate playback
2 parents 7c5d2ac4 2d70b254
Showing 103 changed files with 5291 additions and 1022 deletions
1 /node_modules/ 1 /node_modules/
2 *~ 2 *~
3 *.iml 3 *.iml
4 *.swp
5 tmp/**
...\ No newline at end of file ...\ No newline at end of file
......
1 language: node_js
2 before_script:
3 - npm install -g grunt-cli
4 notifications:
5 hipchat:
6 rooms:
7 secure: l5TTd5JuPAW883PtcyaIBcJI9Chr9JpsZPQAEUBKAgIEwzuS6y7t5arlkS1PwH6gi1FADzYDf+OXSIou4GkTSrIetnBcT/SAgF0gBKgIhj+eRkuCfZ4VaC7BPhfZ0hgYRE+5Ejf5BM2MJafRm0pj7OlqG4xKrQZwtuV1te5r3JY=
1 'use strict'; 1 'use strict';
2 2
3 var basename = require('path').basename;
4
3 module.exports = function(grunt) { 5 module.exports = function(grunt) {
4 6
5 // Project configuration. 7 // Project configuration.
...@@ -12,7 +14,7 @@ module.exports = function(grunt) { ...@@ -12,7 +14,7 @@ module.exports = function(grunt) {
12 ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', 14 ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n',
13 // Task configuration. 15 // Task configuration.
14 clean: { 16 clean: {
15 files: ['dist'] 17 files: ['build', 'dist', 'tmp']
16 }, 18 },
17 concat: { 19 concat: {
18 options: { 20 options: {
...@@ -26,13 +28,8 @@ module.exports = function(grunt) { ...@@ -26,13 +28,8 @@ module.exports = function(grunt) {
26 'src/h264-stream.js', 28 'src/h264-stream.js',
27 'src/aac-stream.js', 29 'src/aac-stream.js',
28 'src/segment-parser.js', 30 'src/segment-parser.js',
29 'src/segment-controller.js', 31 'src/m3u8/m3u8-parser.js'
30 'src/m3u8/m3u8.js', 32 ],
31 'src/m3u8/m3u8-tag-types.js',
32 'src/m3u8/m3u8-parser.js',
33 'src/manifest-controller.js',
34 'src/segment-controller.js',
35 'src/hls-playback-controller.js'],
36 dest: 'dist/videojs.hls.js' 33 dest: 'dist/videojs.hls.js'
37 }, 34 },
38 }, 35 },
...@@ -65,7 +62,10 @@ module.exports = function(grunt) { ...@@ -65,7 +62,10 @@ module.exports = function(grunt) {
65 options: { 62 options: {
66 jshintrc: 'test/.jshintrc' 63 jshintrc: 'test/.jshintrc'
67 }, 64 },
68 src: ['test/**/*.js', '!test/tsSegment.js', '!test/fixtures/*.js'] 65 src: ['test/**/*.js',
66 '!test/tsSegment.js',
67 '!test/fixtures/*.js',
68 '!test/manifest/**']
69 }, 69 },
70 }, 70 },
71 watch: { 71 watch: {
...@@ -92,8 +92,57 @@ module.exports = function(grunt) { ...@@ -92,8 +92,57 @@ module.exports = function(grunt) {
92 grunt.loadNpmTasks('grunt-contrib-jshint'); 92 grunt.loadNpmTasks('grunt-contrib-jshint');
93 grunt.loadNpmTasks('grunt-contrib-watch'); 93 grunt.loadNpmTasks('grunt-contrib-watch');
94 94
95 grunt.registerTask('manifests-to-js', 'Wrap the test fixtures and output' +
96 ' so they can be loaded in a browser',
97 function() {
98 var
99 jsManifests = 'window.manifests = {\n',
100 jsExpected = 'window.expected = {\n';
101 grunt.file.recurse('test/manifest/',
102 function(abspath, root, sub, filename) {
103 if ((/\.m3u8$/).test(abspath)) {
104
105 // translate this manifest
106 jsManifests += ' \'' + basename(filename, '.m3u8') + '\': ' +
107 grunt.file.read(abspath)
108 .split('\n')
109
110 // quote and concatenate
111 .map(function(line) {
112 return ' \'' + line + '\\n\' +\n';
113 }).join('')
114
115 // strip leading spaces and the trailing '+'
116 .slice(4, -3);
117 jsManifests += ',\n';
118 }
119
120 if ((/\.json$/).test(abspath)) {
121
122 // append the JSON
123 jsExpected += ' "' + basename(filename, '.json') + '": ' +
124 grunt.file.read(abspath) + ',\n';
125 }
126 });
127
128 // clean up and close the objects
129 jsManifests = jsManifests.slice(0, -2);
130 jsManifests += '\n};\n';
131 jsExpected = jsExpected.slice(0, -2);
132 jsExpected += '\n};\n';
133
134 // write out the manifests
135 grunt.file.write('tmp/manifests.js', jsManifests);
136 grunt.file.write('tmp/expected.js', jsExpected);
137 });
138
95 // Default task. 139 // Default task.
96 grunt.registerTask('default', 140 grunt.registerTask('default',
97 ['jshint', 'qunit', 'clean', 'concat', 'uglify']); 141 ['clean',
142 'jshint',
143 'manifests-to-js',
144 'qunit',
145 'concat',
146 'uglify']);
98 147
99 }; 148 };
......
1 [![Build Status](https://travis-ci.org/brightcove/videojs-contrib-hls.png)](https://travis-ci.org/brightcove/videojs-contrib-hls)
2
1 # video.js HLS Plugin 3 # video.js HLS Plugin
2 4
3 A video.js plugin that plays HLS video on platforms that don't support it but have Flash. 5 A video.js plugin that plays HLS video on platforms that don't support it but have Flash.
4 6
5 ## Getting Started 7 ## Getting Started
6 Download the [production version][min] or the [development version][max]. 8 Download the [plugin](https://raw.github.com/videojs/videojs-contrib-hls/master/dist/videojs-hls.min.js). On your web page:
7
8 [min]: https://raw.bithub.com/dlapalomento/video-js-hls/master/dist/videojs-hls.min.js
9 [max]: https://raw.bithub.com/dlapalomento/video-js-hls/master/dist/videojs-hls.js
10
11 In your web page:
12 9
13 ```html 10 ```html
14 <script src="video.js"></script> 11 <script src="video.js"></script>
15 <script src="dist/videojs-hls.min.js"></script> 12 <script src="videojs-hls.min.js"></script>
16 <script> 13 <script>
17 var player = videojs('video'); 14 var player = videojs('video');
18 player.hls(); 15 player.hls('http://example.com/video.m3u8');
16 player.play();
19 </script> 17 </script>
20 ``` 18 ```
21 19
22 ## Documentation 20 ## Documentation
23 _(Coming soon)_ 21 [HTTP Live Streaming](https://developer.apple.com/streaming/) (HLS) has
22 become a de-facto standard for streaming video on mobile devices
23 thanks to its native support on iOS and Android. There are a number of
24 reasons independent of platform to recommend the format, though:
25
26 - Supports (client-driven) adaptive bitrate selection
27 - Delivered over standard HTTP ports
28 - Simple, text-based manifest format
29 - No proprietary streaming servers required
30
31 Unfortunately, all the major desktop browsers except for Safari are
32 missing HLS support. That leaves web developers in the unfortunate
33 position of having to maintain alternate renditions of the same video
34 and potentially having to forego HTML-based video entirely to provide
35 the best desktop viewing experience.
36
37 This plugin attempts to address that situation by providing a polyfill
38 for HLS on browsers that have Flash support. You can deploy a single
39 HLS stream, code against the regular HTML5 video APIs, and create a
40 fast, high-quality video experience across all the big web device
41 categories.
42
43 The videojs-hls plugin is still working towards a 1.0 release so it
44 may not fit your requirements today. Specifically, there is _no_
45 support for:
46
47 - Alternate audio and video tracks
48 - Subtitles
49 - Segment codecs _other than_ H.264 with AAC audio
50 - Live streams
51 - Internet Explorer < 10
52
53 ### Runtime Properties
54 #### player.hls.master
55 Type: `object`
56
57 An object representing the parsed master playlist. If a media playlist
58 is loaded directly, a master playlist with only one entry will be
59 created.
60
61 #### player.hls.media
62 Type: `object`
63
64 An object representing the currently selected media playlist. This is
65 the playlist that is being referred to when a additional video data
66 needs to be downloaded.
67
68 #### player.hls.mediaIndex
69 Type: `number`
70
71 The index of the next video segment to be downloaded from
72 `player.hls.media`.
73
74 #### player.hls.selectPlaylist
75 Type: `function`
76
77 A function that returns the media playlist object to use to download
78 the next segment. It is invoked by the plugin immediately before a new
79 segment is downloaded. You can override this function to provide your
80 adaptive streaming logic. You must, however, be sure to return a valid
81 media playlist object that is present in `player.hls.master`.
82
83 ### Events
84 #### loadedmetadata
85
86 Fired after the first media playlist is downloaded for a stream.
87
88 #### loadedmanifest
89
90 Fired immediately after a new master or media playlist has been
91 downloaded. By default, the plugin only downloads playlists as they
92 are needed.
93
94 ## Hosting Considerations
95 Unlike a native HLS implementation, the HLS plugin has to comply with
96 the browser's security policies. That means that all the files that
97 make up the stream must be served from the same domain as the page
98 hosting the video player or from a server that has appropriate [CORS
99 headers](https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS)
100 configured. Easy [instructions are
101 available](http://enable-cors.org/server.html) for popular webservers
102 and most CDNs should have no trouble turning CORS on for your account.
103
104 ## MBR Rendition Selection Logic
105 In situations where manifests have multiple renditions, the player will
106 go through the following algorithm to determine the best rendition by
107 bandwidth and viewport dimensions.
24 108
25 ## Examples 109 - Start on index 0 as defined in the HLS Spec (link above)
26 _(Coming soon)_ 110 - On a successful load complete per segment determine the following;
111 - player.hls.bandwidth set to value as segment byte size over download time
112 - Viewport width/height as determined by player.width()/player.height()
113 - Playlists mapped and sorted by BANDWIDTH less than or equal to 1.1x player.hls.bandwidth
114 - Best playlist variant by BANDWIDTH determined
115 - Subset of bandwidth appropriate renditions mapped
116 - Subset validated for RESOLUTION attributes less than or equal to player dimensions
117 - Best playlist variant by RESOLUTION determined
118 - Result is as follows;
119 - [Best RESOLUTION variant] OR [Best BANDWIDTH variant] OR [inital playlist in manifest]
27 120
28 ## Release History 121 ## Release History
29 _(Nothing yet)_ 122 _(Nothing yet)_
......
...@@ -4,16 +4,16 @@ ...@@ -4,16 +4,16 @@
4 <meta charset="utf-8"> 4 <meta charset="utf-8">
5 <title>video.js HLS Plugin Example</title> 5 <title>video.js HLS Plugin Example</title>
6 6
7 <link href="node_modules/video.js/video-js.css" rel="stylesheet"> 7 <link href="node_modules/video.js/dist/video-js/video-js.css" rel="stylesheet">
8 8
9 <!-- video.js --> 9 <!-- video.js -->
10 <script src="node_modules/video.js/video.dev.js"></script> 10 <script src="node_modules/video.js/dist/video-js/video.js"></script>
11 11
12 <!-- Media Sources plugin --> 12 <!-- Media Sources plugin -->
13 <script src="node_modules/videojs-media-sources/videojs-media-sources.js"></script> 13 <script src="node_modules/videojs-contrib-media-sources/videojs-media-sources.js"></script>
14 14
15 <!-- HLS plugin --> 15 <!-- HLS plugin -->
16 <script src="src/video-js-hls.js"></script> 16 <script src="src/videojs-hls.js"></script>
17 17
18 <!-- segment handling --> 18 <!-- segment handling -->
19 <script src="src/flv-tag.js"></script> 19 <script src="src/flv-tag.js"></script>
...@@ -21,15 +21,10 @@ ...@@ -21,15 +21,10 @@
21 <script src="src/h264-stream.js"></script> 21 <script src="src/h264-stream.js"></script>
22 <script src="src/aac-stream.js"></script> 22 <script src="src/aac-stream.js"></script>
23 <script src="src/segment-parser.js"></script> 23 <script src="src/segment-parser.js"></script>
24 <script src="src/segment-controller.js"></script>
25 24
26 <!-- m3u8 handling --> 25 <!-- m3u8 handling -->
27 <script src="src/m3u8/m3u8.js"></script> 26 <script src="src/stream.js"></script>
28 <script src="src/m3u8/m3u8-tag-types.js"></script>
29 <script src="src/m3u8/m3u8-parser.js"></script> 27 <script src="src/m3u8/m3u8-parser.js"></script>
30 <script src="src/manifest-controller.js"></script>
31 <script src="src/segment-controller.js"></script>
32 <script src="src/hls-playback-controller.js"></script>
33 28
34 <!-- example MPEG2-TS segments --> 29 <!-- example MPEG2-TS segments -->
35 <!-- bipbop --> 30 <!-- bipbop -->
...@@ -44,20 +39,17 @@ ...@@ -44,20 +39,17 @@
44 height="300" 39 height="300"
45 width="600" 40 width="600"
46 controls> 41 controls>
42 <source
43 src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"
44 type="application/x-mpegURL">
47 </video> 45 </video>
48 <script> 46 <script>
49 var video, mediaSource; 47 videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
50
51 // initialize the player 48 // initialize the player
52 videojs.options.flash.swf = 'node_modules/videojs-media-sources/video-js-with-mse.swf'; 49 var player = videojs('video');
53 video = videojs('video',{},function(){
54 this.playbackController = new window.videojs.hls.HLSPlaybackController(this);
55 this.playbackController.loadManifest('test/fixtures/prog_index.m3u8', function(data) {
56 console.log(data);
57 });
58 });
59 </script>
60
61 50
51 // initialize the plugin
52 player.hls();
53 </script>
62 </body> 54 </body>
63 </html> 55 </html>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 }, 6 },
7 "license": "Apache 2", 7 "license": "Apache 2",
8 "scripts": { 8 "scripts": {
9 "test": "grunt qunit" 9 "test": "grunt"
10 }, 10 },
11 "devDependencies": { 11 "devDependencies": {
12 "grunt-contrib-jshint": "~0.6.0", 12 "grunt-contrib-jshint": "~0.6.0",
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
18 "grunt": "~0.4.1" 18 "grunt": "~0.4.1"
19 }, 19 },
20 "dependencies": { 20 "dependencies": {
21 "video.js": "~4.2.2", 21 "video.js": "git+https://github.com/dmlap/video-js.git#v4.3.0-10",
22 "videojs-contrib-media-sources": "git+ssh://git@github.com/videojs/videojs-contrib-media-sources.git" 22 "videojs-contrib-media-sources": "git+https://github.com/dmlap/videojs-contrib-media-sources.git#hotfix/misc-fixes"
23 } 23 }
24 } 24 }
......
...@@ -296,8 +296,8 @@ hls.FlvTag = function(type, extraData) { ...@@ -296,8 +296,8 @@ hls.FlvTag = function(type, extraData) {
296 296
297 // trim down the byte buffer to what is actually being used 297 // trim down the byte buffer to what is actually being used
298 this.bytes = this.bytes.subarray(0, this.length); 298 this.bytes = this.bytes.subarray(0, this.length);
299 this.frameTime = hls.FlvTag.frameTime(this.bytes);
299 console.assert(this.bytes.byteLength === this.length); 300 console.assert(this.bytes.byteLength === this.length);
300
301 return this; 301 return this;
302 }; 302 };
303 }; 303 };
......
1 (function(window) {
2 var
3 ManifestController = window.videojs.hls.ManifestController,
4 SegmentController = window.videojs.hls.SegmentController,
5 MediaSource = window.videojs.MediaSource,
6 SegmentParser = window.videojs.hls.SegmentParser;
7
8
9 window.videojs.hls.HLSPlaybackController = function(player) {
10
11 var self = this;
12
13 self.player = player;
14 self.mediaSource = new MediaSource();
15 self.parser = new SegmentParser();
16
17 self.manifestLoaded = false;
18 self.currentSegment = 0;
19
20 // register external callbacks
21 self.rendition = function(rendition) {
22 self.currentRendition = rendition;
23 self.loadManifestWithMediaSources(self.currentRendition.url, self.onM3U8LoadComplete, self.onM3U8LoadError, self.onM3U8Update);
24 };
25
26 self.loadManifestWithMediaSources = function(manifestUrl,onDataCallback) {
27 self.manifestController = new ManifestController();
28 self.manifestController.loadManifest(manifestUrl, self.onM3U8LoadComplete, self.onM3U8LoadError, self.onM3U8Update);
29 if (onDataCallback) {
30 self.manifestLoadCompleteCallback = onDataCallback;
31 }
32 };
33
34 self.loadManifest = function(manifestUrl, onDataCallback) {
35 self.mediaSource.addEventListener('sourceopen', function() {
36 // feed parsed bytes into the player
37 self.sourceBuffer = self.mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
38
39 self.parser = new SegmentParser();
40
41 self.sourceBuffer.appendBuffer(self.parser.getFlvHeader(), self.player);
42
43 if (onDataCallback) {
44 self.manifestLoadCompleteCallback = onDataCallback;
45 }
46
47 self.manifestController = new ManifestController();
48 self.manifestController.loadManifest(manifestUrl, self.onM3U8LoadComplete, self.onM3U8LoadError, self.onM3U8Update);
49
50 }, false);
51
52 self.player.src({
53 src: window.videojs.URL.createObjectURL(self.mediaSource),
54 type: "video/flv"
55 });
56 };
57
58 self.onM3U8LoadComplete = function(m3u8) {
59 if (m3u8.invalidReasons.length === 0) {
60 if (m3u8.isPlaylist) {
61 self.currentPlaylist = m3u8;
62 self.rendition(self.currentPlaylist.playlistItems[0]);
63 } else {
64 self.currentManifest = m3u8;
65 self.manifestLoaded = true;
66
67 self.loadSegment(self.currentManifest.mediaItems[0]);
68
69 if (self.manifestLoadCompleteCallback) {
70 self.manifestLoadCompleteCallback(m3u8);
71 }
72 }
73 }
74 };
75
76 self.onM3U8LoadError = function() {};
77 self.onM3U8Update = function() {};
78
79 self.loadSegment = function(segment) {
80 self.segmentController = new SegmentController();
81 self.segmentController.loadSegment(segment.url, self.onSegmentLoadComplete, self.onSegmentLoadError);
82 };
83
84 self.onSegmentLoadComplete = function(segment) {
85 self.parser.parseSegmentBinaryData(segment.binaryData);
86
87 while (self.parser.tagsAvailable()) {
88 self.sourceBuffer.appendBuffer(self.parser.getNextTag().bytes, self.player);
89 }
90
91 if (self.currentSegment < self.currentManifest.mediaItems.length-1) {
92 self.loadNextSegment();
93 }
94 };
95
96 self.loadNextSegment = function() {
97 self.currentSegment++;
98 self.loadSegment(self.currentManifest.mediaItems[self.currentSegment]);
99 };
100
101 self.onSegmentLoadError = function() {};
102
103 };
104 })(this);
1 (function(window) { 1 /**
2 var M3U8 = window.videojs.hls.M3U8; 2 * Utilities for parsing M3U8 files. If the entire manifest is available,
3 * `Parser` will create a object representation with enough detail for managing
4 * playback. `ParseStream` and `LineStream` are lower-level parsing primitives
5 * that do not assume the entirety of the manifest is ready and expose a
6 * ReadableStream-like interface.
7 */
8 (function(videojs, parseInt, isFinite, mergeOptions, undefined) {
9 var
10 noop = function() {},
11 parseAttributes = function(attributes) {
12 var
13 attrs = attributes.split(','),
14 i = attrs.length,
15 result = {},
16 attr;
3 17
4 window.videojs.hls.M3U8Parser = function() { 18 while (i--) {
5 var 19 attr = attrs[i].split('=');
6 self = this, 20 attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
7 tagTypes = window.videojs.hls.m3u8TagType, 21
8 lines = [], 22 // This is not sexy, but gives us the resulting object we want.
9 data; 23 if (attr[1]) {
10 24 attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
11 self.getTagType = function(lineData) { 25 if (attr[1].indexOf('"') !== -1) {
12 for (var s in tagTypes) { 26 attr[1] = attr[1].split('"')[1];
13 if (lineData.indexOf(tagTypes[s]) === 0) { 27 }
14 return tagTypes[s]; 28 result[attr[0]] = attr[1];
29 } else {
30 attrs[i - 1] = attrs[i - 1] + ',' + attr[0];
15 } 31 }
16 } 32 }
17 }; 33 return result;
34 },
35 Stream = videojs.hls.Stream,
36 LineStream,
37 ParseStream,
38 Parser;
18 39
19 self.getTagValue = function(lineData) { 40 /**
20 for (var s in tagTypes) { 41 * A stream that buffers string input and generates a `data` event for each
21 if (lineData.indexOf(tagTypes[s]) === 0) { 42 * line.
22 return lineData.substr(tagTypes[s].length); 43 */
23 } 44 LineStream = function() {
45 var buffer = '';
46 LineStream.prototype.init.call(this);
47
48 /**
49 * Add new data to be parsed.
50 * @param data {string} the text to process
51 */
52 this.push = function(data) {
53 var nextNewline;
54
55 buffer += data;
56 nextNewline = buffer.indexOf('\n');
57
58 for (; nextNewline > -1; nextNewline = buffer.indexOf('\n')) {
59 this.trigger('data', buffer.substring(0, nextNewline));
60 buffer = buffer.substring(nextNewline + 1);
24 } 61 }
25 }; 62 };
63 };
64 LineStream.prototype = new Stream();
26 65
27 self.parse = function(rawDataString) { 66 /**
28 data = new M3U8(); 67 * A line-level M3U8 parser event stream. It expects to receive input one
68 * line at a time and performs a context-free parse of its contents. A stream
69 * interpretation of a manifest can be useful if the manifest is expected to
70 * be too large to fit comfortably into memory or the entirety of the input
71 * is not immediately available. Otherwise, it's probably much easier to work
72 * with a regular `Parser` object.
73 *
74 * Produces `data` events with an object that captures the parser's
75 * interpretation of the input. That object has a property `tag` that is one
76 * of `uri`, `comment`, or `tag`. URIs only have a single additional
77 * property, `line`, which captures the entirety of the input without
78 * interpretation. Comments similarly have a single additional property
79 * `text` which is the input without the leading `#`.
80 *
81 * Tags always have a property `tagType` which is the lower-cased version of
82 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
83 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
84 * tags are given the tag type `unknown` and a single additional property
85 * `data` with the remainder of the input.
86 */
87 ParseStream = function() {
88 ParseStream.prototype.init.call(this);
89 };
90 ParseStream.prototype = new Stream();
91 /**
92 * Parses an additional line of input.
93 * @param line {string} a single line of an M3U8 file to parse
94 */
95 ParseStream.prototype.push = function(line) {
96 var match, event;
97 if (line.length === 0) {
98 // ignore empty lines
99 return;
100 }
29 101
30 if (self.directory) { 102 // URIs
31 data.directory = self.directory; 103 if (line[0] !== '#') {
32 } 104 this.trigger('data', {
105 type: 'uri',
106 uri: line
107 });
108 return;
109 }
110
111 // Comments
112 if (line.indexOf('#EXT') !== 0) {
113 this.trigger('data', {
114 type: 'comment',
115 text: line.slice(1)
116 });
117 return;
118 }
33 119
34 if (rawDataString === undefined || rawDataString.length <= 0) { 120 // Tags
35 data.invalidReasons.push("Empty Manifest"); 121 match = /^#EXTM3U/.exec(line);
36 return; 122 if (match) {
123 this.trigger('data', {
124 type: 'tag',
125 tagType: 'm3u'
126 });
127 return;
128 }
129 match = (/^#EXTINF:?([0-9\.]*)?,?(.*)?$/).exec(line);
130 if (match) {
131 event = {
132 type: 'tag',
133 tagType: 'inf'
134 };
135 if (match[1]) {
136 event.duration = parseFloat(match[1], 10);
37 } 137 }
38 lines = rawDataString.split('\n'); 138 if (match[2]) {
139 event.title = match[2];
140 }
141 this.trigger('data', event);
142 return;
143 }
144 match = (/^#EXT-X-TARGETDURATION:?([0-9.]*)?/).exec(line);
145 if (match) {
146 event = {
147 type: 'tag',
148 tagType: 'targetduration'
149 };
150 if (match[1]) {
151 event.duration = parseInt(match[1], 10);
152 }
153 this.trigger('data', event);
154 return;
155 }
156 match = (/^#ZEN-TOTAL-DURATION:?([0-9.]*)?/).exec(line);
157 if (match) {
158 event = {
159 type: 'tag',
160 tagType: 'totalduration'
161 };
162 if (match[1]) {
163 event.duration = parseInt(match[1], 10);
164 }
165 this.trigger('data', event);
166 return;
167 }
168 match = (/^#EXT-X-VERSION:?([0-9.]*)?/).exec(line);
169 if (match) {
170 event = {
171 type: 'tag',
172 tagType: 'version'
173 };
174 if (match[1]) {
175 event.version = parseInt(match[1], 10);
176 }
177 this.trigger('data', event);
178 return;
179 }
180 match = (/^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/).exec(line);
181 if (match) {
182 event = {
183 type: 'tag',
184 tagType: 'media-sequence'
185 };
186 if (match[1]) {
187 event.number = parseInt(match[1], 10);
188 }
189 this.trigger('data', event);
190 return;
191 }
192 match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line);
193 if (match) {
194 event = {
195 type: 'tag',
196 tagType: 'playlist-type'
197 };
198 if (match[1]) {
199 event.playlistType = match[1];
200 }
201 this.trigger('data', event);
202 return;
203 }
204 match = (/^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/).exec(line);
205 if (match) {
206 event = {
207 type: 'tag',
208 tagType: 'byterange'
209 };
210 if (match[1]) {
211 event.length = parseInt(match[1], 10);
212 }
213 if (match[2]) {
214 event.offset = parseInt(match[2], 10);
215 }
216 this.trigger('data', event);
217 return;
218 }
219 match = (/^#EXT-X-ALLOW-CACHE:?(YES|NO)?/).exec(line);
220 if (match) {
221 event = {
222 type: 'tag',
223 tagType: 'allow-cache'
224 };
225 if (match[1]) {
226 event.allowed = !(/NO/).test(match[1]);
227 }
228 this.trigger('data', event);
229 return;
230 }
231 match = (/^#EXT-X-STREAM-INF:?(.*)$/).exec(line);
232 if (match) {
233 event = {
234 type: 'tag',
235 tagType: 'stream-inf'
236 };
237 if (match[1]) {
238 event.attributes = parseAttributes(match[1]);
39 239
40 lines.forEach(function(value,index) { 240 if (event.attributes.RESOLUTION) {
41 var segment, rendition, attributes; 241 (function() {
242 var
243 split = event.attributes.RESOLUTION.split('x'),
244 resolution = {};
245 if (split[0]) {
246 resolution.width = parseInt(split[0], 10);
247 }
248 if (split[1]) {
249 resolution.height = parseInt(split[1], 10);
250 }
251 event.attributes.RESOLUTION = resolution;
252 })();
253 }
254 if (event.attributes.BANDWIDTH) {
255 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
256 }
257 if (event.attributes['PROGRAM-ID']) {
258 event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
259 }
260 }
261 this.trigger('data', event);
262 return;
263 }
264 match = (/^#EXT-X-ENDLIST/).exec(line);
265 if (match) {
266 this.trigger('data', {
267 type: 'tag',
268 tagType: 'endlist'
269 });
270 return;
271 }
42 272
43 switch (self.getTagType(value)) { 273 // unknown tag type
44 case tagTypes.EXTM3U: 274 this.trigger('data', {
45 data.hasValidM3UTag = (index === 0); 275 type: 'tag',
46 if (!data.hasValidM3UTag) { 276 data: line.slice(4, line.length)
47 data.invalidReasons.push("Invalid EXTM3U Tag"); 277 });
48 } 278 };
49 break;
50 279
51 case tagTypes.DISCONTINUITY: 280 /**
52 break; 281 * A parser for M3U8 files. The current interpretation of the input is
282 * exposed as a property `manifest` on parser objects. It's just two lines to
283 * create and parse a manifest once you have the contents available as a string:
284 *
285 * ```js
286 * var parser = new videojs.m3u8.Parser();
287 * parser.push(xhr.responseText);
288 * ```
289 *
290 * New input can later be applied to update the manifest object by calling
291 * `push` again.
292 *
293 * The parser attempts to create a usable manifest object even if the
294 * underlying input is somewhat nonsensical. It emits `info` and `warning`
295 * events during the parse if it encounters input that seems invalid or
296 * requires some property of the manifest object to be defaulted.
297 */
298 Parser = function() {
299 var
300 self = this,
301 uris = [],
302 currentUri = {};
303 Parser.prototype.init.call(this);
53 304
54 case tagTypes.PLAYLIST_TYPE: 305 this.lineStream = new LineStream();
55 if (self.getTagValue(value) === "VOD" || 306 this.parseStream = new ParseStream();
56 self.getTagValue(value) === "EVENT") { 307 this.lineStream.pipe(this.parseStream);
57 data.playlistType = self.getTagValue(value);
58 308
59 } else { 309 // the manifest is empty until the parse stream begins delivering data
60 data.invalidReasons.push("Invalid Playlist Type Value"); 310 this.manifest = {
61 } 311 allowCache: true
62 break; 312 };
63
64 case tagTypes.EXTINF:
65 segment = {
66 url: "unknown",
67 byterange: -1,
68 targetDuration: data.targetDuration
69 };
70
71 if (self.getTagType(lines[index + 1]) === tagTypes.BYTERANGE) {
72 segment.byterange = self.getTagValue(lines[index + 1]).split('@');
73 segment.url = lines[index + 2];
74 } else {
75 segment.url = lines[index + 1];
76 }
77 313
78 if (segment.url.indexOf("http") === -1 && self.directory) { 314 // update the manifest with the m3u8 entry from the parse stream
79 if (data.directory[data.directory.length-1] === segment.url[0] && 315 this.parseStream.on('data', function(entry) {
80 segment.url[0] === "/") { 316 ({
81 segment.url = segment.url.substr(1); 317 tag: function() {
82 } 318 // switch based on the tag type
83 segment.url = self.directory + segment.url; 319 (({
84 } 320 'allow-cache': function() {
85 data.mediaItems.push(segment); 321 this.manifest.allowCache = entry.allowed;
86 break; 322 if (!('allowed' in entry)) {
87 323 this.trigger('info', {
88 case tagTypes.STREAM_INF: 324 message: 'defaulting allowCache to YES'
89 rendition = {}; 325 });
90 attributes = value.substr(tagTypes.STREAM_INF.length).split(','); 326 this.manifest.allowCache = true;
91
92 attributes.forEach(function(attrValue) {
93 if (isNaN(attrValue.split('=')[1])) {
94 rendition[attrValue.split('=')[0].toLowerCase()] = attrValue.split('=')[1];
95
96 if (rendition[attrValue.split('=')[0].toLowerCase()].split('x').length === 2) {
97 rendition.resolution = {
98 width: parseInt(rendition[attrValue.split('=')[0].toLowerCase()].split('x')[0],10),
99 height: parseInt(rendition[attrValue.split('=')[0].toLowerCase()].split('x')[1],10)
100 };
101 } 327 }
102 } else { 328 },
103 rendition[attrValue.split('=')[0].toLowerCase()] = parseInt(attrValue.split('=')[1],10); 329 'byterange': function() {
104 } 330 var byterange = {};
105 }); 331 if ('length' in entry) {
332 currentUri.byterange = byterange;
333 byterange.length = entry.length;
106 334
107 if (self.getTagType(lines[index + 1]) === tagTypes.BYTERANGE) { 335 if (!('offset' in entry)) {
108 rendition.byterange = self.getTagValue(lines[index + 1]).split('@'); 336 this.trigger('info', {
109 rendition.url = lines[index + 2]; 337 message: 'defaulting offset to zero'
110 } else { 338 });
111 rendition.url = lines[index + 1]; 339 entry.offset = 0;
112 } 340 }
113 341 }
114 data.isPlaylist = true; 342 if ('offset' in entry) {
115 data.playlistItems.push(rendition); 343 currentUri.byterange = byterange;
116 break; 344 byterange.offset = entry.offset;
345 }
346 },
347 'inf': function() {
348 if (!this.manifest.playlistType) {
349 this.manifest.playlistType = 'VOD';
350 this.trigger('info', {
351 message: 'defaulting playlist type to VOD'
352 });
353 }
354 if (!('mediaSequence' in this.manifest)) {
355 this.manifest.mediaSequence = 0;
356 this.trigger('info', {
357 message: 'defaulting media sequence to zero'
358 });
359 }
360 if (entry.duration >= 0) {
361 currentUri.duration = entry.duration;
362 }
117 363
118 case tagTypes.TARGETDURATION: 364 this.manifest.segments = uris;
119 data.targetDuration = parseFloat(self.getTagValue(value).split(',')[0]);
120 break;
121 365
122 case tagTypes.ZEN_TOTAL_DURATION: 366 },
123 data.totalDuration = parseFloat(self.getTagValue(value)); 367 'media-sequence': function() {
124 break; 368 if (!isFinite(entry.number)) {
369 this.trigger('warn', {
370 message: 'ignoring invalid media sequence: ' + entry.number
371 });
372 return;
373 }
374 this.manifest.mediaSequence = entry.number;
375 },
376 'playlist-type': function() {
377 if (!(/VOD|EVENT/).test(entry.playlistType)) {
378 this.trigger('warn', {
379 message: 'ignoring unknown playlist type: ' + entry.playlist
380 });
381 return;
382 }
383 this.manifest.playlistType = entry.playlistType;
384 },
385 'stream-inf': function() {
386 this.manifest.playlists = uris;
125 387
126 case tagTypes.VERSION: 388 if (!entry.attributes) {
127 data.version = parseFloat(self.getTagValue(value)); 389 this.trigger('warn', {
128 break; 390 message: 'ignoring empty stream-inf attributes'
391 });
392 return;
393 }
129 394
130 case tagTypes.MEDIA_SEQUENCE: 395 if (!currentUri.attributes) {
131 data.mediaSequence = parseInt(self.getTagValue(value),10); 396 currentUri.attributes = {};
132 break; 397 }
398 currentUri.attributes = mergeOptions(currentUri.attributes,
399 entry.attributes);
400 },
401 'targetduration': function() {
402 if (!isFinite(entry.duration) || entry.duration < 0) {
403 this.trigger('warn', {
404 message: 'ignoring invalid target duration: ' + entry.duration
405 });
406 return;
407 }
408 this.manifest.targetDuration = entry.duration;
409 },
410 'totalduration': function() {
411 if (!isFinite(entry.duration) || entry.duration < 0) {
412 this.trigger('warn', {
413 message: 'ignoring invalid total duration: ' + entry.duration
414 });
415 return;
416 }
417 this.manifest.totalDuration = entry.duration;
418 }
419 })[entry.tagType] || noop).call(self);
420 },
421 uri: function() {
422 currentUri.uri = entry.uri;
423 uris.push(currentUri);
133 424
134 case tagTypes.ALLOW_CACHE: 425 // if no explicit duration was declared, use the target duration
135 if (self.getTagValue(value) === "YES" || self.getTagValue(value) === "NO") { 426 if (this.manifest.targetDuration &&
136 data.allowCache = self.getTagValue(value); 427 !('duration' in currentUri)) {
137 } else { 428 this.trigger('warn', {
138 data.invalidReasons.push("Invalid ALLOW_CACHE Value"); 429 message: 'defaulting segment duration to the target duration'
430 });
431 currentUri.duration = this.manifest.targetDuration;
139 } 432 }
140 break;
141 433
142 case tagTypes.ENDLIST: 434 // prepare for the next URI
143 data.hasEndTag = true; 435 currentUri = {};
144 break; 436 },
437 comment: function() {
438 // comments are not important for playback
145 } 439 }
146 }); 440 })[entry.type].call(self);
441 });
442 };
443 Parser.prototype = new Stream();
444 /**
445 * Parse the input string and update the manifest object.
446 * @param chunk {string} a potentially incomplete portion of the manifest
447 */
448 Parser.prototype.push = function(chunk) {
449 this.lineStream.push(chunk);
450 };
451 /**
452 * Flush any remaining input. This can be handy if the last line of an M3U8
453 * manifest did not contain a trailing newline but the file has been
454 * completely received.
455 */
456 Parser.prototype.end = function() {
457 // flush any buffered input
458 this.lineStream.push('\n');
459 };
147 460
148 return data; 461 window.videojs.m3u8 = {
149 }; 462 LineStream: LineStream,
463 ParseStream: ParseStream,
464 Parser: Parser
150 }; 465 };
151 })(this); 466 })(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
......
1 (function(window) {
2 window.videojs.hls.m3u8TagType = {
3 /*
4 * Derived from the HTTP Live Streaming Spec V8
5 * http://tools.ietf.org/html/draft-pantos-http-live-streaming-08
6 */
7
8 /**
9 * Identifies manifest as Extended M3U - must be present on first line!
10 */
11 EXTM3U:"#EXTM3U",
12
13 /**
14 * Specifies duration.
15 * Syntax: #EXTINF:<duration>,<title>
16 * Example: #EXTINF:10,
17 */
18 EXTINF:"#EXTINF:",
19
20 /**
21 * Indicates that a media segment is a sub-range of the resource identified by its media URI.
22 * Syntax: #EXT-X-BYTERANGE:<n>[@o]
23 */
24 BYTERANGE:"#EXT-X-BYTERANGE:",
25
26 /**
27 * Specifies the maximum media segment duration - applies to entire manifest.
28 * Syntax: #EXT-X-TARGETDURATION:<s>
29 * Example: #EXT-X-TARGETDURATION:10
30 */
31 TARGETDURATION:"#EXT-X-TARGETDURATION:",
32
33 /**
34 * Specifies the sequence number of the first URI in a manifest.
35 * Syntax: #EXT-X-MEDIA-SEQUENCE:<i>
36 * Example: #EXT-X-MEDIA-SEQUENCE:50
37 */
38 MEDIA_SEQUENCE:"#EXT-X-MEDIA-SEQUENCE:",
39
40 /**
41 * Specifies a method by which media segments can be decrypted, if encryption is present.
42 * Syntax: #EXT-X-KEY:<attribute-list>
43 * Note: This is likely irrelevant in the context of the Flash Player.
44 */
45 KEY:"#EXT-X-KEY:",
46
47 /**
48 * Associates the first sample of a media segment with an absolute date and/or time. Applies only to the next media URI.
49 * Syntax: #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>
50 * Example: #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00
51 */
52 PROGRAM_DATE_TIME:"#EXT-X-PROGRAM-DATE-TIME:",
53
54 /**
55 * Indicates whether the client MAY or MUST NOT cache downloaded media segments for later replay.
56 * Syntax: #EXT-X-ALLOW-CACHE:<YES|NO>
57 * Note: This is likely irrelevant in the context of the Flash Player.
58 */
59 ALLOW_CACHE:"#EXT-X-ALLOW_CACHE:",
60
61 /**
62 * Provides mutability information about the manifest.
63 * Syntax: #EXT-X-PLAYLIST-TYPE:<EVENT|VOD>
64 */
65 PLAYLIST_TYPE:"#EXT-X-PLAYLIST-TYPE:",
66
67 /**
68 * Indicates that no more media segments will be added to the manifest. May occur ONCE, anywhere in the mainfest file.
69 */
70 ENDLIST:"#EXT-X-ENDLIST",
71
72 /**
73 * Used to relate Playlists that contain alternative renditions of the same content.
74 * Syntax: #EXT-X-MEDIA:<attribute-list>
75 */
76 MEDIA:"#EXT-X-MEDIA:",
77
78 /**
79 * Identifies a media URI as a Playlist file containing a multimedia presentation and provides information about that presentation.
80 * Syntax: #EXT-X-STREAM-INF:<attribute-list>
81 * <URI>
82 */
83 STREAM_INF:"#EXT-X-STREAM-INF:",
84
85 /**
86 * Indicates an encoding discontinuity between the media segment that follows it and the one that preceded it.
87 */
88 DISCONTINUITY:"#EXT-X-DISCONTINUITY",
89
90 /**
91 * Indicates that each media segment in the manifest describes a single I-frame.
92 */
93 I_FRAMES_ONLY:"#EXT-X-I-FRAMES-ONLY",
94
95 /**
96 * Identifies a manifest file containing the I-frames of a multimedia presentation. It stands alone, in that it does not apply to a particular URI in the manifest.
97 * Syntax: #EXT-X-I-FRAME-STREAM-INF:<attribute-list>
98 */
99 I_FRAME_STREAM_INF:"#EXT-X-I-FRAME-STREAM-INF:",
100
101 /**
102 * Indicates the compatibility version of the Playlist file.
103 * Syntax: #EXT-X-VERSION:<n>
104 */
105 VERSION:"#EXT-X-VERSION:",
106
107 /**
108 * Indicates the total duration as reported by Zencoder.
109 * Syntax: #ZEN-TOTAL-DURATION:<n>
110 */
111 ZEN_TOTAL_DURATION: "#ZEN-TOTAL-DURATION:"
112
113 };
114 })(this);
1 (function (window) {
2 window.videojs.hls.M3U8 = function () {
3 this.directory = "";
4 this.allowCache = "NO";
5 this.playlistItems = [];
6 this.mediaItems = [];
7 this.iFrameItems = [];
8 this.invalidReasons = [];
9 this.hasValidM3UTag = false;
10 this.hasEndTag = false;
11 this.targetDuration = -1;
12 this.totalDuration = -1;
13 this.isPlaylist = false;
14 this.playlistType = "";
15 this.mediaSequence = -1;
16 this.version = -1;
17 };
18 })(this);
1 (function (window) {
2 var
3 M3U8Parser = window.videojs.hls.M3U8Parser;
4
5 window.videojs.hls.ManifestController = function() {
6 var self = this;
7
8 self.loadManifest = function(manifestUrl, onDataCallback, onErrorCallback, onUpdateCallback) {
9 self.url = manifestUrl;
10
11 if (onDataCallback) {
12 self.onDataCallback = onDataCallback;
13 }
14 if (onErrorCallback) {
15 self.onErrorCallback = onErrorCallback;
16 }
17
18 if (onUpdateCallback) {
19 self.onUpdateCallback = onUpdateCallback;
20 }
21
22 window.vjs.get(manifestUrl, self.onManifestLoadComplete, self.onManifestLoadError);
23 };
24
25 self.parseManifest = function(dataAsString) {
26 self.parser = new M3U8Parser();
27 self.parser.directory = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(self.url).slice(1)[1];
28 self.data = self.parser.parse(dataAsString);
29
30 return self.data;
31 };
32
33 self.onManifestLoadComplete = function(response) {
34 var output = self.parseManifest(response);
35
36 if (self.onDataCallback !== undefined) {
37 self.onDataCallback(output);
38 }
39 };
40
41 self.onManifestLoadError = function(err) {
42 if (self.onErrorCallback !== undefined) {
43 self.onErrorCallback((err !== undefined) ? err : null);
44 }
45 };
46 };
47 })(this);
1 (function(window) {
2
3 window.videojs.hls.SegmentController = function() {
4 var self = this;
5
6 self.loadSegment = function(segmentUrl, onDataCallback, onErrorCallback, onUpdateCallback) {
7 var request = new XMLHttpRequest();
8
9 self.url = segmentUrl;
10 self.onDataCallback = onDataCallback;
11 self.onErrorCallback = onErrorCallback;
12 self.onUpdateCallback = onUpdateCallback;
13 self.requestTimestamp = +new Date();
14
15 request.open('GET', segmentUrl, true);
16 request.responseType = 'arraybuffer';
17 request.onload = function() {
18 self.onSegmentLoadComplete(new Uint8Array(request.response));
19 };
20
21 request.send(null);
22 };
23
24 self.parseSegment = function(incomingData) {
25 self.data = {};
26 self.data.binaryData = incomingData;
27 self.data.url = self.url;
28 self.data.isCached = false;
29 self.data.requestTimestamp = self.requestTimestamp;
30 self.data.responseTimestamp = self.responseTimestamp;
31 self.data.byteLength = incomingData.byteLength;
32 self.data.isCached = parseInt(self.responseTimestamp - self.requestTimestamp,10) < 75;
33 self.data.throughput = self.calculateThroughput(self.data.byteLength, self.requestTimestamp ,self.responseTimestamp);
34
35 return self.data;
36 };
37
38 self.calculateThroughput = function(dataAmount, startTime, endTime) {
39 return Math.round(dataAmount / (endTime - startTime) * 1000) * 8;
40 };
41
42 self.onSegmentLoadComplete = function(response) {
43 var output;
44
45 self.responseTimestamp = +new Date();
46
47 output = self.parseSegment(response);
48
49 if (self.onDataCallback !== undefined) {
50 self.onDataCallback(output);
51 }
52 };
53
54 self.onSegmentLoadError = function(error) {
55 if (error) {
56 throw error;
57 }
58
59 if (self.onErrorCallback !== undefined) {
60 self.onErrorCallback(error);
61 }
62 };
63 };
64 })(this);
...@@ -4,24 +4,30 @@ ...@@ -4,24 +4,30 @@
4 FlvTag = videojs.hls.FlvTag, 4 FlvTag = videojs.hls.FlvTag,
5 H264Stream = videojs.hls.H264Stream, 5 H264Stream = videojs.hls.H264Stream,
6 AacStream = videojs.hls.AacStream, 6 AacStream = videojs.hls.AacStream,
7 m2tsPacketSize = 188; 7 MP2T_PACKET_LENGTH,
8 8 STREAM_TYPES;
9 console.assert(H264Stream); 9
10 console.assert(AacStream); 10 /**
11 11 * An object that incrementally transmuxes MPEG2 Trasport Stream
12 window.videojs.hls.SegmentParser = function() { 12 * chunks into an FLV.
13 */
14 videojs.hls.SegmentParser = function() {
13 var 15 var
14 self = this, 16 self = this,
15 parseTSPacket, 17 parseTSPacket,
16 pmtPid, 18 streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH),
17 streamBuffer = new Uint8Array(m2tsPacketSize),
18 streamBufferByteCount = 0, 19 streamBufferByteCount = 0,
19 videoPid,
20 h264Stream = new H264Stream(), 20 h264Stream = new H264Stream(),
21 audioPid,
22 aacStream = new AacStream(), 21 aacStream = new AacStream(),
23 seekToKeyFrame = false; 22 seekToKeyFrame = false;
24 23
24 // expose the stream metadata
25 self.stream = {
26 // the mapping between transport stream programs and the PIDs
27 // that form their elementary streams
28 programMapTable: {}
29 };
30
25 // For information on the FLV format, see 31 // For information on the FLV format, see
26 // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. 32 // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
27 // Technically, this function returns the header and a metadata FLV tag 33 // Technically, this function returns the header and a metadata FLV tag
...@@ -146,24 +152,24 @@ ...@@ -146,24 +152,24 @@
146 // reconstruct the first packet. The rest of the packets will be 152 // reconstruct the first packet. The rest of the packets will be
147 // parsed directly from data 153 // parsed directly from data
148 if (streamBufferByteCount > 0) { 154 if (streamBufferByteCount > 0) {
149 if (data.byteLength + streamBufferByteCount < m2tsPacketSize) { 155 if (data.byteLength + streamBufferByteCount < MP2T_PACKET_LENGTH) {
150 // the current data is less than a single m2ts packet, so stash it 156 // the current data is less than a single m2ts packet, so stash it
151 // until we receive more 157 // until we receive more
152 158
153 // ?? this seems to append streamBuffer onto data and then just give up. I'm not sure why that would be interesting. 159 // ?? this seems to append streamBuffer onto data and then just give up. I'm not sure why that would be interesting.
154 videojs.log('data.length + streamBuffer.length < m2tsPacketSize ??'); 160 videojs.log('data.length + streamBuffer.length < MP2T_PACKET_LENGTH ??');
155 streamBuffer.readBytes(data, data.length, streamBuffer.length); 161 streamBuffer.readBytes(data, data.length, streamBuffer.length);
156 return; 162 return;
157 } else { 163 } else {
158 // we have enough data for an m2ts packet 164 // we have enough data for an m2ts packet
159 // process it immediately 165 // process it immediately
160 dataSlice = data.subarray(0, m2tsPacketSize - streamBufferByteCount); 166 dataSlice = data.subarray(0, MP2T_PACKET_LENGTH - streamBufferByteCount);
161 streamBuffer.set(dataSlice, streamBufferByteCount); 167 streamBuffer.set(dataSlice, streamBufferByteCount);
162 168
163 parseTSPacket(streamBuffer); 169 parseTSPacket(streamBuffer);
164 170
165 // reset the buffer 171 // reset the buffer
166 streamBuffer = new Uint8Array(m2tsPacketSize); 172 streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH);
167 streamBufferByteCount = 0; 173 streamBufferByteCount = 0;
168 } 174 }
169 } 175 }
...@@ -178,7 +184,7 @@ ...@@ -178,7 +184,7 @@
178 } 184 }
179 185
180 // base case: not enough data to parse a m2ts packet 186 // base case: not enough data to parse a m2ts packet
181 if (data.byteLength - dataPosition < m2tsPacketSize) { 187 if (data.byteLength - dataPosition < MP2T_PACKET_LENGTH) {
182 if (data.byteLength - dataPosition > 0) { 188 if (data.byteLength - dataPosition > 0) {
183 // there are bytes remaining, save them for next time 189 // there are bytes remaining, save them for next time
184 streamBuffer.set(data.subarray(dataPosition), 190 streamBuffer.set(data.subarray(dataPosition),
...@@ -189,8 +195,8 @@ ...@@ -189,8 +195,8 @@
189 } 195 }
190 196
191 // attempt to parse a m2ts packet 197 // attempt to parse a m2ts packet
192 if (parseTSPacket(data.subarray(dataPosition, dataPosition + m2tsPacketSize))) { 198 if (parseTSPacket(data.subarray(dataPosition, dataPosition + MP2T_PACKET_LENGTH))) {
193 dataPosition += m2tsPacketSize; 199 dataPosition += MP2T_PACKET_LENGTH;
194 } else { 200 } else {
195 // If there was an error parsing a TS packet. it could be 201 // If there was an error parsing a TS packet. it could be
196 // because we are not TS packet aligned. Step one forward by 202 // because we are not TS packet aligned. Step one forward by
...@@ -201,24 +207,31 @@ ...@@ -201,24 +207,31 @@
201 } 207 }
202 }; 208 };
203 209
210 /**
211 * Parses a video/mp2t packet and appends the underlying video and
212 * audio data onto h264stream and aacStream, respectively.
213 * @param data {Uint8Array} the bytes of an MPEG2-TS packet,
214 * including the sync byte.
215 * @return {boolean} whether a valid packet was encountered
216 */
204 // TODO add more testing to make sure we dont walk past the end of a TS 217 // TODO add more testing to make sure we dont walk past the end of a TS
205 // packet! 218 // packet!
206 parseTSPacket = function(data) { // :ByteArray):Boolean { 219 parseTSPacket = function(data) { // :ByteArray):Boolean {
207 var 220 var
208 offset = 0, // :uint 221 offset = 0, // :uint
209 end = offset + m2tsPacketSize, // :uint 222 end = offset + MP2T_PACKET_LENGTH, // :uint
210
211 // Don't look for a sync byte. We handle that in
212 // parseSegmentBinaryData()
213 223
214 // Payload Unit Start Indicator 224 // Payload Unit Start Indicator
215 pusi = !!(data[offset + 1] & 0x40), // mask: 0100 0000 225 pusi = !!(data[offset + 1] & 0x40), // mask: 0100 0000
216 226
217 // PacketId 227 // packet identifier (PID), a unique identifier for the elementary
228 // stream this packet describes
218 pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2], // mask: 0001 1111 229 pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2], // mask: 0001 1111
230
231 // adaptation_field_control, whether this header is followed by an
232 // adaptation field, a payload, or both
219 afflag = (data[offset + 3] & 0x30 ) >>> 4, 233 afflag = (data[offset + 3] & 0x30 ) >>> 4,
220 234
221 aflen, // :uint
222 patTableId, // :int 235 patTableId, // :int
223 patCurrentNextIndicator, // Boolean 236 patCurrentNextIndicator, // Boolean
224 patSectionLength, // :uint 237 patSectionLength, // :uint
...@@ -231,8 +244,8 @@ ...@@ -231,8 +244,8 @@
231 pts, // :uint 244 pts, // :uint
232 dts, // :uint 245 dts, // :uint
233 246
234 pmtTableId, // :int
235 pmtCurrentNextIndicator, // :Boolean 247 pmtCurrentNextIndicator, // :Boolean
248 pmtProgramDescriptorsLength,
236 pmtSectionLength, // :uint 249 pmtSectionLength, // :uint
237 250
238 streamType, // :int 251 streamType, // :int
...@@ -243,42 +256,64 @@ ...@@ -243,42 +256,64 @@
243 // corrupt stream detection 256 // corrupt stream detection
244 // cc = (data[offset + 3] & 0x0F); 257 // cc = (data[offset + 3] & 0x0F);
245 258
246 // Done with TS header 259 // move past the header
247 offset += 4; 260 offset += 4;
248 261
249 if (afflag > 0x01) { // skip most of the adaption field 262 // if an adaption field is present, its length is specified by
250 aflen = data[offset]; 263 // the fifth byte of the PES header. The adaptation field is
251 offset += aflen + 1; 264 // used to specify some forms of timing and control data that we
265 // do not currently use.
266 if (afflag > 0x01) {
267 offset += data[offset] + 1;
252 } 268 }
253 269
270 // Handle a Program Association Table (PAT). PATs map PIDs to
271 // individual programs. If this transport stream was being used
272 // for television broadcast, a program would probably be
273 // equivalent to a channel. In HLS, it would be very unusual to
274 // create an mp2t stream with multiple programs.
254 if (0x0000 === pid) { 275 if (0x0000 === pid) {
255 // always test for PMT first! (becuse other variables default to 0) 276 // The PAT may be split into multiple sections and those
256 277 // sections may be split into multiple packets. If a PAT
257 // if pusi is set we must skip X bytes (PSI pointer field) 278 // section starts in this packet, PUSI will be true and the
258 offset += pusi ? 1 + data[offset] : 0; 279 // first byte of the playload will indicate the offset from
280 // the current position to the start of the section.
281 if (pusi) {
282 offset += 1 + data[offset];
283 }
259 patTableId = data[offset]; 284 patTableId = data[offset];
260 285
261 console.assert(0x00 === patTableId, 'patTableId should be 0x00'); 286 if (patTableId !== 0x00) {
287 videojs.log('the table_id of the PAT should be 0x00 but was' +
288 patTableId.toString(16));
289 }
262 290
291 // the current_next_indicator specifies whether this PAT is
292 // currently applicable or is part of the next table to become
293 // active
263 patCurrentNextIndicator = !!(data[offset + 5] & 0x01); 294 patCurrentNextIndicator = !!(data[offset + 5] & 0x01);
264 if (patCurrentNextIndicator) { 295 if (patCurrentNextIndicator) {
296 // section_length specifies the number of bytes following
297 // its position to the end of this section
265 patSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2]; 298 patSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2];
266 offset += 8; // skip past PSI header 299 // move past the rest of the PSI header to the first program
267 300 // map table entry
268 // We currently only support streams with 1 program 301 offset += 8;
269 patSectionLength = (patSectionLength - 9) / 4; 302
270 if (1 !== patSectionLength) { 303 // we don't handle streams with more than one program, so
304 // raise an exception if we encounter one
305 // section_length = rest of header + (n * entry length) + CRC
306 // = 5 + (n * 4) + 4
307 if ((patSectionLength - 5 - 4) / 4 !== 1) {
271 throw new Error("TS has more that 1 program"); 308 throw new Error("TS has more that 1 program");
272 } 309 }
273 310
274 // if we ever support more that 1 program (unlikely) loop over them here 311 // the Program Map Table (PMT) associates the underlying
275 // var programNumber = data[offset + 0] << 8 | data[offset + 1]; 312 // video and audio streams with a unique PID
276 // var programId = (data[offset+2] & 0x1F) << 8 | data[offset + 3]; 313 self.stream.pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
277 pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
278 } 314 }
279 315 } else if (pid === self.stream.programMapTable[STREAM_TYPES.h264] ||
280 // We could test the CRC here to detect corruption with extra CPU cost 316 pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
281 } else if (videoPid === pid || audioPid === pid) {
282 if (pusi) { 317 if (pusi) {
283 // comment out for speed 318 // comment out for speed
284 if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) { 319 if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) {
...@@ -322,60 +357,81 @@ ...@@ -322,60 +357,81 @@
322 // Skip past "optional" portion of PTS header 357 // Skip past "optional" portion of PTS header
323 offset += pesHeaderLength; 358 offset += pesHeaderLength;
324 359
325 if (videoPid === pid) { 360 if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
326 // Stash this frame for future use. 361 // Stash this frame for future use.
327 // console.assert(videoFrames.length < 3); 362 // console.assert(videoFrames.length < 3);
328 363
329 h264Stream.setNextTimeStamp(pts, 364 h264Stream.setNextTimeStamp(pts,
330 dts, 365 dts,
331 dataAlignmentIndicator); 366 dataAlignmentIndicator);
332 } else if (audioPid === pid) { 367 } else if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
333 aacStream.setNextTimeStamp(pts, 368 aacStream.setNextTimeStamp(pts,
334 pesPacketSize, 369 pesPacketSize,
335 dataAlignmentIndicator); 370 dataAlignmentIndicator);
336 } 371 }
337 } 372 }
338 373
339 if (audioPid === pid) { 374 if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
340 aacStream.writeBytes(data, offset, end - offset); 375 aacStream.writeBytes(data, offset, end - offset);
341 } else if (videoPid === pid) { 376 } else if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
342 h264Stream.writeBytes(data, offset, end - offset); 377 h264Stream.writeBytes(data, offset, end - offset);
343 } 378 }
344 } else if (pmtPid === pid) { 379 } else if (self.stream.pmtPid === pid) {
345 // TODO sanity check data[offset] 380 // similarly to the PAT, jump to the first byte of the section
346 // if pusi is set we must skip X bytes (PSI pointer field) 381 if (pusi) {
347 offset += (pusi ? 1 + data[offset] : 0); 382 offset += 1 + data[offset];
348 pmtTableId = data[offset]; 383 }
349 384 if (data[offset] !== 0x02) {
350 console.assert(0x02 === pmtTableId); 385 videojs.log('The table_id of a PMT should be 0x02 but was ' +
386 data[offset].toString(16));
387 }
351 388
389 // whether this PMT is currently applicable or is part of the
390 // next table to become active
352 pmtCurrentNextIndicator = !!(data[offset + 5] & 0x01); 391 pmtCurrentNextIndicator = !!(data[offset + 5] & 0x01);
353 if (pmtCurrentNextIndicator) { 392 if (pmtCurrentNextIndicator) {
354 audioPid = videoPid = 0; 393 // overwrite any existing program map table
355 pmtSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2]; 394 self.stream.programMapTable = {};
395 // section_length specifies the number of bytes following
396 // its position to the end of this section
397 pmtSectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2];
398 // subtract the length of the program info descriptors
399 pmtProgramDescriptorsLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11];
400 pmtSectionLength -= pmtProgramDescriptorsLength;
356 // skip CRC and PSI data we dont care about 401 // skip CRC and PSI data we dont care about
402 // rest of header + CRC = 9 + 4
357 pmtSectionLength -= 13; 403 pmtSectionLength -= 13;
358 404
359 offset += 12; // skip past PSI header and some PMT data 405 // align offset to the first entry in the PMT
406 offset += 12 + pmtProgramDescriptorsLength;
407
408 // iterate through the entries
360 while (0 < pmtSectionLength) { 409 while (0 < pmtSectionLength) {
410 // the type of data carried in the PID this entry describes
361 streamType = data[offset + 0]; 411 streamType = data[offset + 0];
412 // the PID for this entry
362 elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2]; 413 elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
363 ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
364 offset += 5 + ESInfolength;
365 pmtSectionLength -= 5 + ESInfolength;
366 414
367 if (0x1B === streamType) { 415 if (streamType === STREAM_TYPES.h264) {
368 if (0 !== videoPid) { 416 if (self.stream.programMapTable[streamType] &&
417 self.stream.programMapTable[streamType] !== elementaryPID) {
369 throw new Error("Program has more than 1 video stream"); 418 throw new Error("Program has more than 1 video stream");
370 } 419 }
371 videoPid = elementaryPID; 420 self.stream.programMapTable[streamType] = elementaryPID;
372 } else if (0x0F === streamType) { 421 } else if (streamType === STREAM_TYPES.adts) {
373 if (0 !== audioPid) { 422 if (self.stream.programMapTable[streamType] &&
423 self.stream.programMapTable[streamType] !== elementaryPID) {
374 throw new Error("Program has more than 1 audio Stream"); 424 throw new Error("Program has more than 1 audio Stream");
375 } 425 }
376 audioPid = elementaryPID; 426 self.stream.programMapTable[streamType] = elementaryPID;
377 } 427 }
378 // TODO add support for MP3 audio 428 // TODO add support for MP3 audio
429
430 // the length of the entry descriptor
431 ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
432 // move to the first byte after the end of this entry
433 offset += 5 + ESInfolength;
434 pmtSectionLength -= 5 + ESInfolength;
379 } 435 }
380 } 436 }
381 // We could test the CRC here to detect corruption with extra CPU cost 437 // We could test the CRC here to detect corruption with extra CPU cost
...@@ -390,6 +446,10 @@ ...@@ -390,6 +446,10 @@
390 return true; 446 return true;
391 }; 447 };
392 448
449 self.getTags = function() {
450 return h264Stream.tags;
451 };
452
393 self.stats = { 453 self.stats = {
394 h264Tags: function() { 454 h264Tags: function() {
395 return h264Stream.tags.length; 455 return h264Stream.tags.length;
...@@ -399,4 +459,12 @@ ...@@ -399,4 +459,12 @@
399 } 459 }
400 }; 460 };
401 }; 461 };
462
463 // MPEG2-TS constants
464 videojs.hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188;
465 videojs.hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = {
466 h264: 0x1b,
467 adts: 0x0f
468 };
469
402 })(window); 470 })(window);
......
1 /**
2 * A lightweight readable stream implemention that handles event dispatching.
3 * Objects that inherit from streams should call init in their constructors.
4 */
5 (function(videojs, undefined) {
6 var Stream = function() {
7 this.init = function() {
8 var listeners = {};
9 /**
10 * Add a listener for a specified event type.
11 * @param type {string} the event name
12 * @param listener {function} the callback to be invoked when an event of
13 * the specified type occurs
14 */
15 this.on = function(type, listener) {
16 if (!listeners[type]) {
17 listeners[type] = [];
18 }
19 listeners[type].push(listener);
20 };
21 /**
22 * Remove a listener for a specified event type.
23 * @param type {string} the event name
24 * @param listener {function} a function previously registered for this
25 * type of event through `on`
26 */
27 this.off = function(type, listener) {
28 var index;
29 if (!listeners[type]) {
30 return false;
31 }
32 index = listeners[type].indexOf(listener);
33 listeners[type].splice(index, 1);
34 return index > -1;
35 };
36 /**
37 * Trigger an event of the specified type on this stream. Any additional
38 * arguments to this function are passed as parameters to event listeners.
39 * @param type {string} the event name
40 */
41 this.trigger = function(type) {
42 var callbacks, i, length, args;
43 callbacks = listeners[type];
44 if (!callbacks) {
45 return;
46 }
47 args = Array.prototype.slice.call(arguments, 1);
48 length = callbacks.length;
49 for (i = 0; i < length; ++i) {
50 callbacks[i].apply(this, args);
51 }
52 };
53 };
54 };
55 /**
56 * Forwards all `data` events on this stream to the destination stream. The
57 * destination stream should provide a method `push` to receive the data
58 * events as they arrive.
59 * @param destination {stream} the stream that will receive all `data` events
60 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
61 */
62 Stream.prototype.pipe = function(destination) {
63 this.on('data', function(data) {
64 destination.push(data);
65 });
66 };
67
68 videojs.hls.Stream = Stream;
69 })(window.videojs);
1 /*
2 * video-js-hls
3 *
4 *
5 * Copyright (c) 2013 Brightcove
6 * All rights reserved.
7 */
8
9 (function(window) {
10
11 window.videojs.hls = {};
12
13 })(this);
1 /*
2 * video-js-hls
3 *
4 *
5 * Copyright (c) 2013 Brightcove
6 * All rights reserved.
7 */
8
9 (function(window, videojs, document, undefined) {
10
11 videojs.hls = {
12 /**
13 * Whether the browser has built-in HLS support.
14 */
15 supportsNativeHls: (function() {
16 var
17 video = document.createElement('video'),
18 xMpegUrl,
19 vndMpeg;
20
21 // native HLS is definitely not supported if HTML5 video isn't
22 if (!videojs.Html5.isSupported()) {
23 return false;
24 }
25
26 xMpegUrl = video.canPlayType('application/x-mpegURL');
27 vndMpeg = video.canPlayType('application/vnd.apple.mpegURL');
28 return (/probably|maybe/).test(xMpegUrl) ||
29 (/probably|maybe/).test(vndMpeg);
30 })()
31 };
32
33 var
34 // the desired length of video to maintain in the buffer, in seconds
35 goalBufferLength = 5,
36
37 // a fudge factor to apply to advertised playlist bitrates to account for
38 // temporary flucations in client bandwidth
39 bandwidthVariance = 1.1,
40
41 /**
42 * A comparator function to sort two playlist object by bandwidth.
43 * @param left {object} a media playlist object
44 * @param right {object} a media playlist object
45 * @return {number} Greater than zero if the bandwidth attribute of
46 * left is greater than the corresponding attribute of right. Less
47 * than zero if the bandwidth of right is greater than left and
48 * exactly zero if the two are equal.
49 */
50 playlistBandwidth = function(left, right) {
51 var leftBandwidth, rightBandwidth;
52 if (left.attributes && left.attributes.BANDWIDTH) {
53 leftBandwidth = left.attributes.BANDWIDTH;
54 }
55 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
56 if (right.attributes && right.attributes.BANDWIDTH) {
57 rightBandwidth = right.attributes.BANDWIDTH;
58 }
59 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
60
61 return leftBandwidth - rightBandwidth;
62 },
63
64 /**
65 * A comparator function to sort two playlist object by resolution (width).
66 * @param left {object} a media playlist object
67 * @param right {object} a media playlist object
68 * @return {number} Greater than zero if the resolution.width attribute of
69 * left is greater than the corresponding attribute of right. Less
70 * than zero if the resolution.width of right is greater than left and
71 * exactly zero if the two are equal.
72 */
73 playlistResolution = function(left, right) {
74 var leftWidth, rightWidth;
75
76 if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
77 leftWidth = left.attributes.RESOLUTION.width;
78 }
79
80 leftWidth = leftWidth || window.Number.MAX_VALUE;
81
82 if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
83 rightWidth = right.attributes.RESOLUTION.width;
84 }
85
86 rightWidth = rightWidth || window.Number.MAX_VALUE;
87
88 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
89 // have the same media dimensions/ resolution
90 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
91 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
92 } else {
93 return leftWidth - rightWidth;
94 }
95 },
96
97 /**
98 * TODO - Document this great feature.
99 *
100 * @param playlist
101 * @param time
102 * @returns int
103 */
104 getMediaIndexByTime = function(playlist, time) {
105 var index, counter, timeRanges, currentSegmentRange;
106
107 timeRanges = [];
108 for (index = 0; index < playlist.segments.length; index++) {
109 currentSegmentRange = {};
110 currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end;
111 currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration;
112 timeRanges.push(currentSegmentRange);
113 }
114
115 for (counter = 0; counter < timeRanges.length; counter++) {
116 if (time >= timeRanges[counter].start && time < timeRanges[counter].end) {
117 return counter;
118 }
119 }
120
121 return -1;
122
123 },
124
125 /**
126 * Calculate the total duration for a playlist based on segment metadata.
127 * @param playlist {object} a media playlist object
128 * @return {number} the currently known duration, in seconds
129 */
130 totalDuration = function(playlist) {
131 var
132 duration = 0,
133 i = playlist.segments.length,
134 segment;
135 while (i--) {
136 segment = playlist.segments[i];
137 duration += segment.duration || playlist.targetDuration || 0;
138 }
139 return duration;
140 },
141
142 /**
143 * Constructs a new URI by interpreting a path relative to another
144 * URI.
145 * @param basePath {string} a relative or absolute URI
146 * @param path {string} a path part to combine with the base
147 * @return {string} a URI that is equivalent to composing `base`
148 * with `path`
149 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
150 */
151 resolveUrl = function(basePath, path) {
152 // use the base element to get the browser to handle URI resolution
153 var
154 oldBase = document.querySelector('base'),
155 docHead = document.querySelector('head'),
156 a = document.createElement('a'),
157 base = oldBase,
158 oldHref,
159 result;
160
161 // prep the document
162 if (oldBase) {
163 oldHref = oldBase.href;
164 } else {
165 base = docHead.appendChild(document.createElement('base'));
166 }
167
168 base.href = basePath;
169 a.href = path;
170 result = a.href;
171
172 // clean up
173 if (oldBase) {
174 oldBase.href = oldHref;
175 } else {
176 docHead.removeChild(base);
177 }
178 return result;
179 },
180
181 /**
182 * Initializes the HLS plugin.
183 * @param options {mixed} the URL to an HLS playlist
184 */
185 init = function(options) {
186 var
187 mediaSource = new videojs.MediaSource(),
188 segmentParser = new videojs.hls.SegmentParser(),
189 player = this,
190 srcUrl,
191
192 segmentXhr,
193 downloadPlaylist,
194 fillBuffer;
195
196 // if the video element supports HLS natively, do nothing
197 if (videojs.hls.supportsNativeHls) {
198 return;
199 }
200
201 srcUrl = (function() {
202 var
203 extname,
204 i = 0,
205 j = 0,
206 src = player.el().querySelector('.vjs-tech').src,
207 sources = player.options().sources,
208 techName,
209 length = sources.length;
210
211 // use the URL specified in options if one was provided
212 if (typeof options === 'string') {
213 return options;
214 } else if (options) {
215 return options.url;
216 }
217
218 // src attributes take precedence over source children
219 if (src) {
220
221 // assume files with the m3u8 extension are HLS
222 extname = (/[^#?]*(?:\/[^#?]*\.([^#?]*))/).exec(src);
223 if (extname && extname[1] === 'm3u8') {
224 return src;
225 }
226 return;
227 }
228
229 // find the first playable source
230 for (; i < length; i++) {
231
232 // ignore sources without a specified type
233 if (!sources[i].type) {
234 continue;
235 }
236
237 // do nothing if the source is handled by one of the standard techs
238 for (j in player.options().techOrder) {
239 techName = player.options().techOrder[j];
240 techName = techName[0].toUpperCase() + techName.substring(1);
241 if (videojs[techName].canPlaySource({ type: sources[i].type })) {
242 return;
243 }
244 }
245
246 // use the plugin if the MIME type specifies HLS
247 if ((/application\/x-mpegURL/).test(sources[i].type) ||
248 (/application\/vnd\.apple\.mpegURL/).test(sources[i].type)) {
249 return sources[i].src;
250 }
251 }
252 })();
253
254 if (!srcUrl) {
255 // do nothing until the plugin is initialized with a valid URL
256 videojs.log('hls: no valid playlist URL specified');
257 return;
258 }
259
260 // expose the HLS plugin state
261 player.hls.readyState = function() {
262 if (!player.hls.media) {
263 return 0; // HAVE_NOTHING
264 }
265 return 1; // HAVE_METADATA
266 };
267
268 player.on('seeking', function() {
269 var currentTime = player.currentTime();
270 player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime);
271 if (segmentXhr) {
272 segmentXhr.abort();
273 }
274 fillBuffer(currentTime * 1000);
275 });
276
277
278 /**
279 * Chooses the appropriate media playlist based on the current
280 * bandwidth estimate and the player size.
281 * @return the highest bitrate playlist less than the currently detected
282 * bandwidth, accounting for some amount of bandwidth variance
283 */
284 player.hls.selectPlaylist = function () {
285 var
286 effectiveBitrate,
287 sortedPlaylists = player.hls.master.playlists.slice(),
288 bandwidthPlaylists = [],
289 i = sortedPlaylists.length,
290 variant,
291 bandwidthBestVariant,
292 resolutionBestVariant;
293
294 sortedPlaylists.sort(playlistBandwidth);
295
296 // map playlist options by bandwidth and select
297 // best variant as appropriate
298 while (i--) {
299 variant = sortedPlaylists[i];
300
301 // ignore playlists without bandwidth information
302 if (!variant.attributes || !variant.attributes.BANDWIDTH) {
303 continue;
304 }
305
306 effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
307
308 // since the playlists are sorted in ascending order by bandwidth, the
309 // current variant is the best as long as its effective bitrate is
310 // below the current bandwidth estimate
311 // NOTE - only set once
312 if (effectiveBitrate < player.hls.bandwidth) {
313 bandwidthPlaylists.push(variant);
314 if (!bandwidthBestVariant) {
315 bandwidthBestVariant = variant;
316 }
317 }
318 }
319
320 // set index to the available bandwidth mapped renditions
321 i = bandwidthPlaylists.length;
322
323 // sort those by resolution [currently widths]
324 bandwidthPlaylists.sort(playlistResolution);
325
326 // iterate through bandwidth related playlists and find
327 // best rendition by player dimension
328
329 // Tests
330 // Seeking - find if you've seeked correctly?
331 // SelectPlaylist -
332 while (i--) {
333 variant = bandwidthPlaylists[i];
334
335 // ignored playlists without resolution information
336 if (!variant.attributes || !variant.attributes.RESOLUTION ||
337 !variant.attributes.RESOLUTION.width || !variant.attributes.RESOLUTION.height) {
338 continue;
339 }
340
341 if (variant.attributes.RESOLUTION.width <= player.width() &&
342 variant.attributes.RESOLUTION.height <= player.height()) {
343 resolutionBestVariant = variant;
344 break;
345 }
346 }
347
348 // fallback chain of variants
349 return resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0];
350 };
351
352 /**
353 * Download an M3U8 and update the current manifest object. If the provided
354 * URL is a master playlist, the default variant will be downloaded and
355 * parsed as well. Triggers `loadedmanifest` once for each playlist that is
356 * downloaded and `loadedmetadata` after at least one media playlist has
357 * been parsed. Whether multiple playlists were downloaded or not, when
358 * `loadedmetadata` fires a parsed or inferred master playlist object will
359 * be available as `player.hls.master`.
360 *
361 * @param url {string} a URL to the M3U8 file to process
362 */
363 downloadPlaylist = function(url) {
364 var xhr = new window.XMLHttpRequest();
365 xhr.open('GET', url);
366 xhr.onreadystatechange = function() {
367 var i, parser, playlist, playlistUri;
368
369 if (xhr.readyState === 4) {
370 if (xhr.status >= 400 || this.status === 0) {
371 player.hls.error = {
372 status: xhr.status,
373 message: 'HLS playlist request error at URL: ' + url,
374 code: (xhr.status >= 500) ? 4 : 2
375 };
376 player.trigger('error');
377 return;
378 }
379
380 // readystate DONE
381 parser = new videojs.m3u8.Parser();
382 parser.push(xhr.responseText);
383
384 // master playlists
385 if (parser.manifest.playlists) {
386 player.hls.master = parser.manifest;
387 downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri));
388 player.trigger('loadedmanifest');
389 return;
390 }
391
392 // media playlists
393 if (player.hls.master) {
394 // merge this playlist into the master
395 i = player.hls.master.playlists.length;
396
397 while (i--) {
398 playlist = player.hls.master.playlists[i];
399 playlistUri = resolveUrl(srcUrl, playlist.uri);
400 if (playlistUri === url) {
401 player.hls.master.playlists[i] =
402 videojs.util.mergeOptions(playlist, parser.manifest);
403 }
404 }
405 } else {
406 // infer a master playlist if none was previously requested
407 player.hls.master = {
408 playlists: [parser.manifest]
409 };
410 }
411
412 // always start playback with the default rendition
413 if (!player.hls.media) {
414 player.hls.media = player.hls.master.playlists[0];
415
416 // update the duration
417 if (parser.manifest.totalDuration) {
418 player.duration(parser.manifest.totalDuration);
419 } else {
420 player.duration(totalDuration(parser.manifest));
421 }
422
423 // periodicaly check if the buffer needs to be refilled
424 player.on('timeupdate', fillBuffer);
425
426 player.trigger('loadedmanifest');
427 player.trigger('loadedmetadata');
428 fillBuffer();
429 return;
430 }
431
432 // select a playlist and download its metadata if necessary
433 playlist = player.hls.selectPlaylist();
434 if (!playlist.segments) {
435 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
436 } else {
437 player.hls.media = playlist;
438
439 // update the duration
440 if (player.hls.media.totalDuration) {
441 player.duration(player.hls.media.totalDuration);
442 } else {
443 player.duration(totalDuration(player.hls.media));
444 }
445 }
446
447 player.trigger('loadedmanifest');
448 }
449 };
450 xhr.send(null);
451 };
452
453 /**
454 * Determines whether there is enough video data currently in the buffer
455 * and downloads a new segment if the buffered time is less than the goal.
456 * @param offset (optional) {number} the offset into the downloaded segment
457 * to seek to, in milliseconds
458 */
459 fillBuffer = function(offset) {
460 var
461 buffered = player.buffered(),
462 bufferedTime = 0,
463 segment = player.hls.media.segments[player.hls.mediaIndex],
464 segmentUri,
465 startTime;
466
467 // if there is a request already in flight, do nothing
468 if (segmentXhr) {
469 return;
470 }
471
472 // if the video has finished downloading, stop trying to buffer
473 if (!segment) {
474 return;
475 }
476
477 if (buffered) {
478 // assuming a single, contiguous buffer region
479 bufferedTime = player.buffered().end(0) - player.currentTime();
480 }
481
482 // if there is plenty of content in the buffer, relax for awhile
483 if (bufferedTime >= goalBufferLength) {
484 return;
485 }
486
487 segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''),
488 segment.uri);
489
490 // request the next segment
491 segmentXhr = new window.XMLHttpRequest();
492 segmentXhr.open('GET', segmentUri);
493 segmentXhr.responseType = 'arraybuffer';
494 segmentXhr.onreadystatechange = function() {
495 var playlist;
496
497 // wait until the request completes
498 if (this.readyState !== 4) {
499 return;
500 }
501
502 // the segment request is no longer outstanding
503 segmentXhr = null;
504
505 // trigger an error if the request was not successful
506 if (this.status >= 400) {
507 player.hls.error = {
508 status: this.status,
509 message: 'HLS segment request error at URL: ' + segmentUri,
510 code: (this.status >= 500) ? 4 : 2
511 };
512
513 // try moving on to the next segment
514 player.hls.mediaIndex++;
515 return;
516 }
517
518 // stop processing if the request was aborted
519 if (!this.response) {
520 return;
521 }
522
523 // calculate the download bandwidth
524 player.hls.segmentXhrTime = (+new Date()) - startTime;
525 player.hls.bandwidth = (this.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000;
526
527 // transmux the segment data from MP2T to FLV
528 segmentParser.parseSegmentBinaryData(new Uint8Array(this.response));
529
530 // if we're refilling the buffer after a seek, scan through the muxed
531 // FLV tags until we find the one that is closest to the desired
532 // playback time
533 if (offset !== undefined && typeof offset === "number") {
534 while (segmentParser.getTags()[0].pts < offset) {
535 // we're seeking past this tag, so ignore it
536 segmentParser.getNextTag();
537 }
538 }
539
540 while (segmentParser.tagsAvailable()) {
541 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes, player);
542 }
543
544 player.hls.mediaIndex++;
545
546 if (player.hls.mediaIndex === player.hls.media.segments.length) {
547 mediaSource.endOfStream();
548 return;
549 }
550
551 // figure out what stream the next segment should be downloaded from
552 // with the updated bandwidth information
553 playlist = player.hls.selectPlaylist();
554 if (!playlist.segments) {
555 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
556 } else {
557 player.hls.media = playlist;
558 }
559 };
560 startTime = +new Date();
561 segmentXhr.send(null);
562 };
563
564 // load the MediaSource into the player
565 mediaSource.addEventListener('sourceopen', function() {
566 // construct the video data buffer and set the appropriate MIME type
567 var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
568 player.hls.sourceBuffer = sourceBuffer;
569 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
570
571 player.hls.mediaIndex = 0;
572 downloadPlaylist(srcUrl);
573 });
574 player.src([{
575 src: videojs.URL.createObjectURL(mediaSource),
576 type: "video/flv"
577 }]);
578 };
579
580 videojs.plugin('hls', function() {
581 var initialize = function() {
582 return function() {
583 this.hls = initialize();
584 init.apply(this, arguments);
585 };
586 };
587 initialize().apply(this, arguments);
588 });
589
590 })(window, window.videojs, document);
1 #EXTM3U 1 #EXTM3U
2 #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000 2 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000
3 prog_index.m3u8 3 prog_index.m3u8
4 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
5 prog_index1.m3u8
6 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000
7 prog_index2.m3u8
8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000
9 prog_index3.m3u8
......
1 (function(window, undefined) {
2 var
3 //manifestController = this.manifestController,
4 ParseStream = window.videojs.m3u8.ParseStream,
5 parseStream,
6 LineStream = window.videojs.m3u8.LineStream,
7 lineStream,
8 Parser = window.videojs.m3u8.Parser,
9 parser;
10
11 /*
12 M3U8 Test Suite
13 */
14
15 module('LineStream', {
16 setup: function() {
17 lineStream = new LineStream();
18 }
19 });
20 test('empty inputs produce no tokens', function() {
21 var data = false;
22 lineStream.on('data', function() {
23 data = true;
24 });
25 lineStream.push('');
26 ok(!data, 'no tokens were produced');
27 });
28 test('splits on newlines', function() {
29 var lines = [];
30 lineStream.on('data', function(line) {
31 lines.push(line);
32 });
33 lineStream.push('#EXTM3U\nmovie.ts\n');
34
35 strictEqual(2, lines.length, 'two lines are ready');
36 strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
37 strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
38 });
39 test('empty lines become empty strings', function() {
40 var lines = [];
41 lineStream.on('data', function(line) {
42 lines.push(line);
43 });
44 lineStream.push('\n\n');
45
46 strictEqual(2, lines.length, 'two lines are ready');
47 strictEqual('', lines.shift(), 'the first line is empty');
48 strictEqual('', lines.shift(), 'the second line is empty');
49 });
50 test('handles lines broken across appends', function() {
51 var lines = [];
52 lineStream.on('data', function(line) {
53 lines.push(line);
54 });
55 lineStream.push('#EXTM');
56 strictEqual(0, lines.length, 'no lines are ready');
57
58 lineStream.push('3U\nmovie.ts\n');
59 strictEqual(2, lines.length, 'two lines are ready');
60 strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
61 strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
62 });
63 test('stops sending events after deregistering', function() {
64 var
65 temporaryLines = [],
66 temporary = function(line) {
67 temporaryLines.push(line);
68 },
69 permanentLines = [],
70 permanent = function(line) {
71 permanentLines.push(line);
72 };
73
74 lineStream.on('data', temporary);
75 lineStream.on('data', permanent);
76 lineStream.push('line one\n');
77 strictEqual(temporaryLines.length, permanentLines.length, 'both callbacks receive the event');
78
79 ok(lineStream.off('data', temporary), 'a listener was removed');
80 lineStream.push('line two\n');
81 strictEqual(1, temporaryLines.length, 'no new events are received');
82 strictEqual(2, permanentLines.length, 'new events are still received');
83 });
84
85 module('ParseStream', {
86 setup: function() {
87 lineStream = new LineStream();
88 parseStream = new ParseStream();
89 lineStream.pipe(parseStream);
90 }
91 });
92 test('parses comment lines', function() {
93 var
94 manifest = '# a line that starts with a hash mark without "EXT" is a comment\n',
95 element;
96 parseStream.on('data', function(elem) {
97 element = elem;
98 });
99 lineStream.push(manifest);
100
101 ok(element, 'an event was triggered');
102 strictEqual(element.type, 'comment', 'the type is comment');
103 strictEqual(element.text,
104 manifest.slice(1, manifest.length - 1),
105 'the comment text is parsed');
106 });
107 test('parses uri lines', function() {
108 var
109 manifest = 'any non-blank line that does not start with a hash-mark is a URI\n',
110 element;
111 parseStream.on('data', function(elem) {
112 element = elem;
113 });
114 lineStream.push(manifest);
115
116 ok(element, 'an event was triggered');
117 strictEqual(element.type, 'uri', 'the type is uri');
118 strictEqual(element.uri,
119 manifest.substring(0, manifest.length - 1),
120 'the uri text is parsed');
121 });
122 test('parses unknown tag types', function() {
123 var
124 manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n',
125 element;
126 parseStream.on('data', function(elem) {
127 element = elem;
128 });
129 lineStream.push(manifest);
130
131 ok(element, 'an event was triggered');
132 strictEqual(element.type, 'tag', 'the type is tag');
133 strictEqual(element.data,
134 manifest.slice(4, manifest.length - 1),
135 'unknown tag data is preserved');
136 });
137
138 // #EXTM3U
139 test('parses #EXTM3U tags', function() {
140 var
141 manifest = '#EXTM3U\n',
142 element;
143 parseStream.on('data', function(elem) {
144 element = elem;
145 });
146 lineStream.push(manifest);
147
148 ok(element, 'an event was triggered');
149 strictEqual(element.type, 'tag', 'the line type is tag');
150 strictEqual(element.tagType, 'm3u', 'the tag type is m3u');
151 });
152
153 // #EXTINF
154 test('parses minimal #EXTINF tags', function() {
155 var
156 manifest = '#EXTINF\n',
157 element;
158 parseStream.on('data', function(elem) {
159 element = elem;
160 });
161 lineStream.push(manifest);
162
163 ok(element, 'an event was triggered');
164 strictEqual(element.type, 'tag', 'the line type is tag');
165 strictEqual(element.tagType, 'inf', 'the tag type is inf');
166 });
167 test('parses #EXTINF tags with durations', function() {
168 var
169 manifest = '#EXTINF:15\n',
170 element;
171 parseStream.on('data', function(elem) {
172 element = elem;
173 });
174 lineStream.push(manifest);
175
176 ok(element, 'an event was triggered');
177 strictEqual(element.type, 'tag', 'the line type is tag');
178 strictEqual(element.tagType, 'inf', 'the tag type is inf');
179 strictEqual(element.duration, 15, 'the duration is parsed');
180 ok(!('title' in element), 'no title is parsed');
181
182 manifest = '#EXTINF:21,\n';
183 lineStream.push(manifest);
184
185 ok(element, 'an event was triggered');
186 strictEqual(element.type, 'tag', 'the line type is tag');
187 strictEqual(element.tagType, 'inf', 'the tag type is inf');
188 strictEqual(element.duration, 21, 'the duration is parsed');
189 ok(!('title' in element), 'no title is parsed');
190 });
191 test('parses #EXTINF tags with a duration and title', function() {
192 var
193 manifest = '#EXTINF:13,Does anyone really use the title attribute?\n',
194 element;
195 parseStream.on('data', function(elem) {
196 element = elem;
197 });
198 lineStream.push(manifest);
199
200 ok(element, 'an event was triggered');
201 strictEqual(element.type, 'tag', 'the line type is tag');
202 strictEqual(element.tagType, 'inf', 'the tag type is inf');
203 strictEqual(element.duration, 13, 'the duration is parsed');
204 strictEqual(element.title,
205 manifest.substring(manifest.indexOf(',') + 1, manifest.length - 1),
206 'the title is parsed');
207 });
208
209 // #EXT-X-TARGETDURATION
210 test('parses minimal #EXT-X-TARGETDURATION tags', function() {
211 var
212 manifest = '#EXT-X-TARGETDURATION\n',
213 element;
214 parseStream.on('data', function(elem) {
215 element = elem;
216 });
217 lineStream.push(manifest);
218
219 ok(element, 'an event was triggered');
220 strictEqual(element.type, 'tag', 'the line type is tag');
221 strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
222 ok(!('duration' in element), 'no duration is parsed');
223 });
224 test('parses #EXT-X-TARGETDURATION with duration', function() {
225 var
226 manifest = '#EXT-X-TARGETDURATION:47\n',
227 element;
228 parseStream.on('data', function(elem) {
229 element = elem;
230 });
231 lineStream.push(manifest);
232
233 ok(element, 'an event was triggered');
234 strictEqual(element.type, 'tag', 'the line type is tag');
235 strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
236 strictEqual(element.duration, 47, 'the duration is parsed');
237 });
238
239 // #EXT-X-VERSION
240 test('parses minimal #EXT-X-VERSION tags', function() {
241 var
242 manifest = '#EXT-X-VERSION:\n',
243 element;
244 parseStream.on('data', function(elem) {
245 element = elem;
246 });
247 lineStream.push(manifest);
248
249 ok(element, 'an event was triggered');
250 strictEqual(element.type, 'tag', 'the line type is tag');
251 strictEqual(element.tagType, 'version', 'the tag type is version');
252 ok(!('version' in element), 'no version is present');
253 });
254 test('parses #EXT-X-VERSION with a version', function() {
255 var
256 manifest = '#EXT-X-VERSION:99\n',
257 element;
258 parseStream.on('data', function(elem) {
259 element = elem;
260 });
261 lineStream.push(manifest);
262
263 ok(element, 'an event was triggered');
264 strictEqual(element.type, 'tag', 'the line type is tag');
265 strictEqual(element.tagType, 'version', 'the tag type is version');
266 strictEqual(element.version, 99, 'the version is parsed');
267 });
268
269 // #EXT-X-MEDIA-SEQUENCE
270 test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() {
271 var
272 manifest = '#EXT-X-MEDIA-SEQUENCE\n',
273 element;
274 parseStream.on('data', function(elem) {
275 element = elem;
276 });
277 lineStream.push(manifest);
278
279 ok(element, 'an event was triggered');
280 strictEqual(element.type, 'tag', 'the line type is tag');
281 strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
282 ok(!('number' in element), 'no number is present');
283 });
284 test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() {
285 var
286 manifest = '#EXT-X-MEDIA-SEQUENCE:109\n',
287 element;
288 parseStream.on('data', function(elem) {
289 element = elem;
290 });
291 lineStream.push(manifest);
292
293 ok(element, 'an event was triggered');
294 strictEqual(element.type, 'tag', 'the line type is tag');
295 strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
296 ok(element.number, 109, 'the number is parsed');
297 });
298
299 // #EXT-X-PLAYLIST-TYPE
300 test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() {
301 var
302 manifest = '#EXT-X-PLAYLIST-TYPE:\n',
303 element;
304 parseStream.on('data', function(elem) {
305 element = elem;
306 });
307 lineStream.push(manifest);
308
309 ok(element, 'an event was triggered');
310 strictEqual(element.type, 'tag', 'the line type is tag');
311 strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
312 ok(!('playlistType' in element), 'no playlist type is present');
313 });
314 test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() {
315 var
316 manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n',
317 element;
318 parseStream.on('data', function(elem) {
319 element = elem;
320 });
321 lineStream.push(manifest);
322
323 ok(element, 'an event was triggered');
324 strictEqual(element.type, 'tag', 'the line type is tag');
325 strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
326 strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT');
327
328 manifest = '#EXT-X-PLAYLIST-TYPE:VOD\n';
329 lineStream.push(manifest);
330 ok(element, 'an event was triggered');
331 strictEqual(element.type, 'tag', 'the line type is tag');
332 strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
333 strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD');
334
335 manifest = '#EXT-X-PLAYLIST-TYPE:nonsense\n';
336 lineStream.push(manifest);
337 ok(element, 'an event was triggered');
338 strictEqual(element.type, 'tag', 'the line type is tag');
339 strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
340 strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed');
341 });
342
343 // #EXT-X-BYTERANGE
344 test('parses minimal #EXT-X-BYTERANGE tags', function() {
345 var
346 manifest = '#EXT-X-BYTERANGE\n',
347 element;
348 parseStream.on('data', function(elem) {
349 element = elem;
350 });
351 lineStream.push(manifest);
352
353 ok(element, 'an event was triggered');
354 strictEqual(element.type, 'tag', 'the line type is tag');
355 strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
356 ok(!('length' in element), 'no length is present');
357 ok(!('offset' in element), 'no offset is present');
358 });
359 test('parses #EXT-X-BYTERANGE with length and offset', function() {
360 var
361 manifest = '#EXT-X-BYTERANGE:45\n',
362 element;
363 parseStream.on('data', function(elem) {
364 element = elem;
365 });
366 lineStream.push(manifest);
367
368 ok(element, 'an event was triggered');
369 strictEqual(element.type, 'tag', 'the line type is tag');
370 strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
371 strictEqual(element.length, 45, 'length is parsed');
372 ok(!('offset' in element), 'no offset is present');
373
374 manifest = '#EXT-X-BYTERANGE:108@16\n';
375 lineStream.push(manifest);
376 ok(element, 'an event was triggered');
377 strictEqual(element.type, 'tag', 'the line type is tag');
378 strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
379 strictEqual(element.length, 108, 'length is parsed');
380 strictEqual(element.offset, 16, 'offset is parsed');
381 });
382
383 // #EXT-X-ALLOW-CACHE
384 test('parses minimal #EXT-X-ALLOW-CACHE tags', function() {
385 var
386 manifest = '#EXT-X-ALLOW-CACHE:\n',
387 element;
388 parseStream.on('data', function(elem) {
389 element = elem;
390 });
391 lineStream.push(manifest);
392
393 ok(element, 'an event was triggered');
394 strictEqual(element.type, 'tag', 'the line type is tag');
395 strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
396 ok(!('allowed' in element), 'no allowed is present');
397 });
398 test('parses valid #EXT-X-ALLOW-CACHE tags', function() {
399 var
400 manifest = '#EXT-X-ALLOW-CACHE:YES\n',
401 element;
402 parseStream.on('data', function(elem) {
403 element = elem;
404 });
405 lineStream.push(manifest);
406
407 ok(element, 'an event was triggered');
408 strictEqual(element.type, 'tag', 'the line type is tag');
409 strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
410 ok(element.allowed, 'allowed is parsed');
411
412 manifest = '#EXT-X-ALLOW-CACHE:NO\n';
413 lineStream.push(manifest);
414
415 ok(element, 'an event was triggered');
416 strictEqual(element.type, 'tag', 'the line type is tag');
417 strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
418 ok(!element.allowed, 'allowed is parsed');
419 });
420 // #EXT-X-STREAM-INF
421 test('parses minimal #EXT-X-STREAM-INF tags', function() {
422 var
423 manifest = '#EXT-X-STREAM-INF\n',
424 element;
425 parseStream.on('data', function(elem) {
426 element = elem;
427 });
428 lineStream.push(manifest);
429
430 ok(element, 'an event was triggered');
431 strictEqual(element.type, 'tag', 'the line type is tag');
432 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
433 ok(!('attributes' in element), 'no attributes are present');
434 });
435 test('parses #EXT-X-STREAM-INF with common attributes', function() {
436 var
437 manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n',
438 element;
439 parseStream.on('data', function(elem) {
440 element = elem;
441 });
442 lineStream.push(manifest);
443
444 ok(element, 'an event was triggered');
445 strictEqual(element.type, 'tag', 'the line type is tag');
446 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
447 strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed');
448
449 manifest = '#EXT-X-STREAM-INF:PROGRAM-ID=7\n';
450 lineStream.push(manifest);
451
452 ok(element, 'an event was triggered');
453 strictEqual(element.type, 'tag', 'the line type is tag');
454 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
455 strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed');
456
457 manifest = '#EXT-X-STREAM-INF:RESOLUTION=396x224\n';
458 lineStream.push(manifest);
459
460 ok(element, 'an event was triggered');
461 strictEqual(element.type, 'tag', 'the line type is tag');
462 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
463 strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed');
464 strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed');
465 });
466 test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
467 var
468 manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n',
469 element;
470 parseStream.on('data', function(elem) {
471 element = elem;
472 });
473 lineStream.push(manifest);
474
475 ok(element, 'an event was triggered');
476 strictEqual(element.type, 'tag', 'the line type is tag');
477 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
478 strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed');
479 strictEqual(element.attributes.ALPHA, 'Value', 'alphabetic attributes are parsed');
480 strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed');
481 });
482 // #EXT-X-ENDLIST
483 test('parses #EXT-X-ENDLIST tags', function() {
484 var
485 manifest = '#EXT-X-ENDLIST\n',
486 element;
487 parseStream.on('data', function(elem) {
488 element = elem;
489 });
490 lineStream.push(manifest);
491
492 ok(element, 'an event was triggered');
493 strictEqual(element.type, 'tag', 'the line type is tag');
494 strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf');
495 });
496
497 test('ignores empty lines', function() {
498 var
499 manifest = '\n',
500 event = false;
501 parseStream.on('data', function() {
502 event = true;
503 });
504 lineStream.push(manifest);
505
506 ok(!event, 'no event is triggered');
507 });
508
509 module('m3u8 parser', {
510 setup: function() {
511 parser = new Parser();
512 }
513 });
514
515 test('should create a parser', function() {
516 notStrictEqual(parser, undefined, 'parser is defined');
517 });
518
519 module('m3u8s');
520
521 test('parses the example manifests as expected', function() {
522 var key;
523 for (key in window.manifests) {
524 if (window.expected[key]) {
525 parser = new Parser();
526 parser.push(window.manifests[key]);
527 deepEqual(parser.manifest,
528 window.expected[key],
529 key + '.m3u8 was parsed correctly');
530 }
531 }
532 });
533
534 })(window, window.console);
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "http://example.com/00001.ts"
9 },
10 {
11 "duration": 10,
12 "uri": "https://example.com/00002.ts"
13 },
14 {
15 "duration": 10,
16 "uri": "//example.com/00003.ts"
17 },
18 {
19 "duration": 10,
20 "uri": "http://example.com/00004.ts"
21 }
22 ],
23 "targetDuration": 10
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 http://example.com/00001.ts
6 #EXTINF:10,
7 https://example.com/00002.ts
8 #EXTINF:10,
9 //example.com/00003.ts
10 #EXTINF:10,
11 http://example.com/00004.ts
12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "byterange": {
8 "length": 522828,
9 "offset": 0
10 },
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 },
14 {
15 "byterange": {
16 "length": 587500,
17 "offset": 522828
18 },
19 "duration": 10,
20 "uri": "hls_450k_video.ts"
21 },
22 {
23 "byterange": {
24 "length": 713084,
25 "offset": 1110328
26 },
27 "duration": 10,
28 "uri": "hls_450k_video.ts"
29 },
30 {
31 "byterange": {
32 "length": 476580,
33 "offset": 1823412
34 },
35 "duration": 10,
36 "uri": "hls_450k_video.ts"
37 },
38 {
39 "byterange": {
40 "length": 535612,
41 "offset": 2299992
42 },
43 "duration": 10,
44 "uri": "hls_450k_video.ts"
45 },
46 {
47 "byterange": {
48 "length": 207176,
49 "offset": 2835604
50 },
51 "duration": 10,
52 "uri": "hls_450k_video.ts"
53 },
54 {
55 "byterange": {
56 "length": 455900,
57 "offset": 3042780
58 },
59 "duration": 10,
60 "uri": "hls_450k_video.ts"
61 },
62 {
63 "byterange": {
64 "length": 657248,
65 "offset": 3498680
66 },
67 "duration": 10,
68 "uri": "hls_450k_video.ts"
69 },
70 {
71 "byterange": {
72 "length": 571708,
73 "offset": 4155928
74 },
75 "duration": 10,
76 "uri": "hls_450k_video.ts"
77 },
78 {
79 "byterange": {
80 "length": 485040,
81 "offset": 4727636
82 },
83 "duration": 10,
84 "uri": "hls_450k_video.ts"
85 },
86 {
87 "byterange": {
88 "length": 709136,
89 "offset": 5212676
90 },
91 "duration": 10,
92 "uri": "hls_450k_video.ts"
93 },
94 {
95 "byterange": {
96 "length": 730004,
97 "offset": 5921812
98 },
99 "duration": 10,
100 "uri": "hls_450k_video.ts"
101 },
102 {
103 "byterange": {
104 "length": 456276,
105 "offset": 6651816
106 },
107 "duration": 10,
108 "uri": "hls_450k_video.ts"
109 },
110 {
111 "byterange": {
112 "length": 468684,
113 "offset": 7108092
114 },
115 "duration": 10,
116 "uri": "hls_450k_video.ts"
117 },
118 {
119 "byterange": {
120 "length": 444996,
121 "offset": 7576776
122 },
123 "duration": 10,
124 "uri": "hls_450k_video.ts"
125 },
126 {
127 "byterange": {
128 "length": 331444,
129 "offset": 8021772
130 },
131 "duration": 10,
132 "uri": "hls_450k_video.ts"
133 },
134 {
135 "byterange": {
136 "length": 44556,
137 "offset": 8353216
138 },
139 "duration": 1.4167,
140 "uri": "hls_450k_video.ts"
141 }
142 ],
143 "targetDuration": 10
144 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:4
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-MEDIA-SEQUENCE:0
6 #EXT-X-PLAYLIST-TYPE:VOD
7 #EXTINF:10,
8 #EXT-X-BYTERANGE:522828@0
9 hls_450k_video.ts
10 #EXTINF:10,
11 #EXT-X-BYTERANGE:587500@522828
12 hls_450k_video.ts
13 #EXTINF:10,
14 #EXT-X-BYTERANGE:713084@1110328
15 hls_450k_video.ts
16 #EXTINF:10,
17 #EXT-X-BYTERANGE:476580@1823412
18 hls_450k_video.ts
19 #EXTINF:10,
20 #EXT-X-BYTERANGE:535612@2299992
21 hls_450k_video.ts
22 #EXTINF:10,
23 #EXT-X-BYTERANGE:207176@2835604
24 hls_450k_video.ts
25 #EXTINF:10,
26 #EXT-X-BYTERANGE:455900@3042780
27 hls_450k_video.ts
28 #EXTINF:10,
29 #EXT-X-BYTERANGE:657248@3498680
30 hls_450k_video.ts
31 #EXTINF:10,
32 #EXT-X-BYTERANGE:571708@4155928
33 hls_450k_video.ts
34 #EXTINF:10,
35 #EXT-X-BYTERANGE:485040@4727636
36 hls_450k_video.ts
37 #EXTINF:10,
38 #EXT-X-BYTERANGE:709136@5212676
39 hls_450k_video.ts
40 #EXTINF:10,
41 #EXT-X-BYTERANGE:730004@5921812
42 hls_450k_video.ts
43 #EXTINF:10,
44 #EXT-X-BYTERANGE:456276@6651816
45 hls_450k_video.ts
46 #EXTINF:10,
47 #EXT-X-BYTERANGE:468684@7108092
48 hls_450k_video.ts
49 #EXTINF:10,
50 #EXT-X-BYTERANGE:444996@7576776
51 hls_450k_video.ts
52 #EXTINF:10,
53 #EXT-X-BYTERANGE:331444@8021772
54 hls_450k_video.ts
55 #EXTINF:1.4167,
56 #EXT-X-BYTERANGE:44556@8353216
57 hls_450k_video.ts
58 #EXT-X-ENDLIST
...\ No newline at end of file ...\ No newline at end of file
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "byterange": {
8 "length": 522828,
9 "offset": 0
10 },
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 }
14 ],
15 "targetDuration": 10
16 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:4
4 #EXT-X-ALLOW-CACHE:0
5 #EXT-X-MEDIA-SEQUENCE:0
6 #EXT-X-PLAYLIST-TYPE:VOD
7 #EXTINF:10,
8 #EXT-X-BYTERANGE:522828@0
9 hls_450k_video.ts
10 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "playlists": [{
4 "attributes": {
5 "PROGRAM-ID": 1,
6 "BANDWIDTH": 240000,
7 "RESOLUTION": {
8 "width": 396,
9 "height": 224
10 }
11 },
12 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
13 }, {
14 "attributes": {
15 "PROGRAM-ID": 1,
16 "BANDWIDTH": 40000
17 },
18 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
19 }, {
20 "attributes": {
21 "PROGRAM-ID": 1,
22 "BANDWIDTH": 440000,
23 "RESOLUTION": {
24 "width": 396,
25 "height": 224
26 }
27 },
28 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
29 }, {
30 "attributes": {
31 "PROGRAM-ID": 1,
32 "BANDWIDTH": 1928000,
33 "RESOLUTION": {
34 "width": 960,
35 "height": 540
36 }
37 },
38 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
39 }]
40 }
1 #EXTM3U
2 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224
3 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001
4 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
5 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001
6 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224
7 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001
8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540
9 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001
1 window.brightcove_playlist_data = '#EXTM3U\n'+
2 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224\n'+
3 'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001\n'+
4 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000\n'+
5 'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001\n'+
6 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224\n'+
7 'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001\n'+
8 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540\n'+
9 'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001';
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "hls_450k_video.ts"
9 },
10 {
11 "byterange": {
12 "length": 587500,
13 "offset": 522828
14 },
15 "duration": 10,
16 "uri": "hls_450k_video.ts"
17 },
18 {
19 "byterange": {
20 "length": 713084,
21 "offset": 0
22 },
23 "duration": 10,
24 "uri": "hls_450k_video2.ts"
25 },
26 {
27 "byterange": {
28 "length": 476580,
29 "offset": 1823412
30 },
31 "duration": 10,
32 "uri": "hls_450k_video.ts"
33 },
34 {
35 "byterange": {
36 "length": 535612,
37 "offset": 2299992
38 },
39 "duration": 10,
40 "uri": "hls_450k_video.ts"
41 },
42 {
43 "byterange": {
44 "length": 207176,
45 "offset": 2835604
46 },
47 "duration": 10,
48 "uri": "hls_450k_video.ts"
49 },
50 {
51 "byterange": {
52 "length": 455900,
53 "offset": 3042780
54 },
55 "duration": 10,
56 "uri": "hls_450k_video.ts"
57 },
58 {
59 "byterange": {
60 "length": 657248,
61 "offset": 3498680
62 },
63 "duration": 10,
64 "uri": "hls_450k_video.ts"
65 },
66 {
67 "byterange": {
68 "length": 571708,
69 "offset": 4155928
70 },
71 "duration": 10,
72 "uri": "hls_450k_video.ts"
73 },
74 {
75 "byterange": {
76 "length": 485040,
77 "offset": 4727636
78 },
79 "duration": 10,
80 "uri": "hls_450k_video.ts"
81 },
82 {
83 "byterange": {
84 "length": 709136,
85 "offset": 5212676
86 },
87 "duration": 10,
88 "uri": "hls_450k_video.ts"
89 },
90 {
91 "byterange": {
92 "length": 730004,
93 "offset": 5921812
94 },
95 "duration": 10,
96 "uri": "hls_450k_video.ts"
97 },
98 {
99 "byterange": {
100 "length": 456276,
101 "offset": 6651816
102 },
103 "duration": 10,
104 "uri": "hls_450k_video.ts"
105 },
106 {
107 "byterange": {
108 "length": 468684,
109 "offset": 7108092
110 },
111 "duration": 10,
112 "uri": "hls_450k_video.ts"
113 },
114 {
115 "byterange": {
116 "length": 444996,
117 "offset": 7576776
118 },
119 "duration": 10,
120 "uri": "hls_450k_video.ts"
121 },
122 {
123 "byterange": {
124 "length": 331444,
125 "offset": 8021772
126 },
127 "duration": 10,
128 "uri": "hls_450k_video.ts"
129 },
130 {
131 "byterange": {
132 "length": 44556,
133 "offset": 8353216
134 },
135 "duration": 1.4167,
136 "uri": "hls_450k_video.ts"
137 }
138 ],
139 "targetDuration": 10
140 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:3
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXT-X-PLAYLIST-TYPE:VOD
6 #EXTINF:10,
7 hls_450k_video.ts
8 #EXTINF:10,
9 #EXT-X-BYTERANGE:587500@522828
10 hls_450k_video.ts
11 #EXTINF:10,
12 #EXT-X-BYTERANGE:713084
13 hls_450k_video2.ts
14 #EXTINF:10,
15 #EXT-X-BYTERANGE:476580@1823412
16 hls_450k_video.ts
17 #EXTINF:10,
18 #EXT-X-BYTERANGE:535612@2299992
19 hls_450k_video.ts
20 #EXTINF:10,
21 #EXT-X-BYTERANGE:207176@2835604
22 hls_450k_video.ts
23 #EXTINF:10,
24 #EXT-X-BYTERANGE:455900@3042780
25 hls_450k_video.ts
26 #EXTINF:10,
27 #EXT-X-BYTERANGE:657248@3498680
28 hls_450k_video.ts
29 #EXTINF:10,
30 #EXT-X-BYTERANGE:571708@4155928
31 hls_450k_video.ts
32 #EXTINF:10,
33 #EXT-X-BYTERANGE:485040@4727636
34 hls_450k_video.ts
35 #EXTINF:10,
36 #EXT-X-BYTERANGE:709136@5212676
37 hls_450k_video.ts
38 #EXTINF:10,
39 #EXT-X-BYTERANGE:730004@5921812
40 hls_450k_video.ts
41 #EXTINF:10,
42 #EXT-X-BYTERANGE:456276@6651816
43 hls_450k_video.ts
44 #EXTINF:10,
45 #EXT-X-BYTERANGE:468684@7108092
46 hls_450k_video.ts
47 #EXTINF:10,
48 #EXT-X-BYTERANGE:444996@7576776
49 hls_450k_video.ts
50 #EXTINF:10,
51 #EXT-X-BYTERANGE:331444@8021772
52 hls_450k_video.ts
53 #EXTINF:1.4167,
54 #EXT-X-BYTERANGE:44556@8353216
55 hls_450k_video.ts
56 #EXT-X-ENDLIST
...\ No newline at end of file ...\ No newline at end of file
1 {
2 "allowCache": false,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "byterange": {
8 "length": 522828,
9 "offset": 0
10 },
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 }
14 ],
15 "targetDuration": 10
16 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:4
4 #EXT-X-ALLOW-CACHE:NO
5 #EXT-X-MEDIA-SEQUENCE:0
6 #EXT-X-PLAYLIST-TYPE:VOD
7 #EXTINF:10,
8 #EXT-X-BYTERANGE:522828@0
9 hls_450k_video.ts
10 #EXT-X-ENDLIST
...\ No newline at end of file ...\ No newline at end of file
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "/00001.ts"
9 },
10 {
11 "duration": 10,
12 "uri": "/subdir/00002.ts"
13 },
14 {
15 "duration": 10,
16 "uri": "/00003.ts"
17 },
18 {
19 "duration": 10,
20 "uri": "/00004.ts"
21 }
22 ],
23 "targetDuration": 10
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 /00001.ts
6 #EXTINF:10,
7 /subdir/00002.ts
8 #EXTINF:10,
9 /00003.ts
10 #EXTINF:10,
11 /00004.ts
12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "byterange": {
8 "length": 522828,
9 "offset": 0
10 },
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 }
14 ],
15 "targetDuration": 10
16 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:4
4 #EXT-X-ALLOW-CACHE:
5 #EXT-X-MEDIA-SEQUENCE:0
6 #EXT-X-PLAYLIST-TYPE:VOD
7 #EXTINF:10,
8 #EXT-X-BYTERANGE:522828@0
9 hls_450k_video.ts
10 #EXT-X-ENDLIST
...\ No newline at end of file ...\ No newline at end of file
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 },
10 {
11 "duration": 6.08,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 },
14 {
15 "duration": 6.6,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 },
18 {
19 "duration": 5,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 }
22 ],
23 "targetDuration": 8
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-TARGETDURATION:8
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 #EXTINF:6.080,{}
9 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
10 #EXTINF:6.600,{}
11 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
12 #EXTINF:5.000,{}
13 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
14 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
9 },
10 {
11 "duration": 10,
12 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
13 },
14 {
15 "duration": 10,
16 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
17 },
18 {
19 "duration": 10,
20 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
21 },
22 {
23 "duration": 10,
24 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
25 },
26 {
27 "duration": 8,
28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
29 }
30 ],
31 "targetDuration": 10
32 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts
6 #EXTINF:10,
7 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts
8 #EXTINF:10,
9 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts
10 #EXTINF:10,
11 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts
12 #EXTINF:10,
13 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts
14 #EXTINF:8,
15 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts
16 #ZEN-TOTAL-DURATION:57.9911
17 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "playlists": [{
4 "attributes": {
5 "PROGRAM-ID": 1,
6 "BANDWIDTH": 240000,
7 "RESOLUTION": {
8 "width": 396,
9 "height": 224
10 }
11 },
12 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
13 }, {
14 "attributes": {
15 "PROGRAM-ID": 1,
16 "BANDWIDTH": 40000
17 },
18 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
19 }, {
20 "attributes": {
21 "PROGRAM-ID": 1,
22 "BANDWIDTH": 440000,
23 "RESOLUTION": {
24 "width": 396,
25 "height": 224
26 }
27 },
28 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
29 }, {
30 "attributes": {
31 "PROGRAM-ID": 1,
32 "BANDWIDTH": 1928000,
33 "RESOLUTION": {
34 "width": 960,
35 "height": 540
36 }
37 },
38 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
39 }]
40 }
1 #EXTM3U
2 #EXT-X-TARGETDURATION:
3 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224
4 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001
5 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
6 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001
7 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224
8 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001
9 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540
10 http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "EVENT",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
9 },
10 {
11 "duration": 10,
12 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
13 },
14 {
15 "duration": 10,
16 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
17 },
18 {
19 "duration": 10,
20 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
21 },
22 {
23 "duration": 10,
24 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
25 },
26 {
27 "duration": 8,
28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
29 }
30 ],
31 "targetDuration": 10
32 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:EVENT
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts
6 #EXTINF:10,
7 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts
8 #EXTINF:10,
9 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts
10 #EXTINF:10,
11 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts
12 #EXTINF:10,
13 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts
14 #EXTINF:8,
15 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts
16 #ZEN-TOTAL-DURATION:57.9911
17 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 1,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }
10 ],
11 "targetDuration": 8
12 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:STRING
3 #EXT-X-MEDIA-SEQUENCE:1
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-TARGETDURATION:8
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "byterange": {
8 "length": 522828,
9 "offset": 0
10 },
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 },
14 {
15 "byterange": {
16 "length": 587500,
17 "offset": 522828
18 },
19 "duration": 10,
20 "uri": "hls_450k_video.ts"
21 },
22 {
23 "byterange": {
24 "length": 713084,
25 "offset": 1110328
26 },
27 "duration": 5,
28 "uri": "hls_450k_video.ts"
29 },
30 {
31 "byterange": {
32 "length": 476580,
33 "offset": 1823412
34 },
35 "duration": 9.7,
36 "uri": "hls_450k_video.ts"
37 },
38 {
39 "byterange": {
40 "length": 535612,
41 "offset": 2299992
42 },
43 "duration": 10,
44 "uri": "hls_450k_video.ts"
45 },
46 {
47 "byterange": {
48 "length": 207176,
49 "offset": 2835604
50 },
51 "duration": 10,
52 "uri": "hls_450k_video.ts"
53 },
54 {
55 "byterange": {
56 "length": 455900,
57 "offset": 3042780
58 },
59 "duration": 10,
60 "uri": "hls_450k_video.ts"
61 },
62 {
63 "byterange": {
64 "length": 657248,
65 "offset": 3498680
66 },
67 "duration": 10,
68 "uri": "hls_450k_video.ts"
69 },
70 {
71 "byterange": {
72 "length": 571708,
73 "offset": 4155928
74 },
75 "duration": 10,
76 "uri": "hls_450k_video.ts"
77 },
78 {
79 "byterange": {
80 "length": 485040,
81 "offset": 4727636
82 },
83 "duration": 10,
84 "uri": "hls_450k_video.ts"
85 },
86 {
87 "byterange": {
88 "length": 709136,
89 "offset": 5212676
90 },
91 "duration": 10,
92 "uri": "hls_450k_video.ts"
93 },
94 {
95 "byterange": {
96 "length": 730004,
97 "offset": 5921812
98 },
99 "duration": 10,
100 "uri": "hls_450k_video.ts"
101 },
102 {
103 "byterange": {
104 "length": 456276,
105 "offset": 6651816
106 },
107 "duration": 10,
108 "uri": "hls_450k_video.ts"
109 },
110 {
111 "byterange": {
112 "length": 468684,
113 "offset": 7108092
114 },
115 "duration": 10,
116 "uri": "hls_450k_video.ts"
117 },
118 {
119 "byterange": {
120 "length": 444996,
121 "offset": 7576776
122 },
123 "duration": 10,
124 "uri": "hls_450k_video.ts"
125 },
126 {
127 "byterange": {
128 "length": 331444,
129 "offset": 8021772
130 },
131 "duration": 10,
132 "uri": "hls_450k_video.ts"
133 },
134 {
135 "byterange": {
136 "length": 44556,
137 "offset": 8353216
138 },
139 "duration": 10,
140 "uri": "hls_450k_video.ts"
141 }
142 ],
143 "targetDuration": 10
144 }
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:3
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXT-X-PLAYLIST-TYPE:VOD
6 #EXTINF:10
7 #EXT-X-BYTERANGE:522828@0
8 hls_450k_video.ts
9 #EXTINF:;asljasdfii11)))00,
10 #EXT-X-BYTERANGE:587500@522828
11 hls_450k_video.ts
12 #EXTINF:5,
13 #EXT-X-BYTERANGE:713084@1110328
14 hls_450k_video.ts
15 #EXTINF:9.7,
16 #EXT-X-BYTERANGE:476580@1823412
17 hls_450k_video.ts
18 #EXTINF:10,
19 #EXT-X-BYTERANGE:535612@2299992
20 hls_450k_video.ts
21 #EXTINF:10,
22 #EXT-X-BYTERANGE:207176@2835604
23 hls_450k_video.ts
24 #EXTINF:10,
25 #EXT-X-BYTERANGE:455900@3042780
26 hls_450k_video.ts
27 #EXTINF:10,
28 #EXT-X-BYTERANGE:657248@3498680
29 hls_450k_video.ts
30 #EXTINF:10,
31 #EXT-X-BYTERANGE:571708@4155928
32 hls_450k_video.ts
33 #EXTINF:10,
34 #EXT-X-BYTERANGE:485040@4727636
35 hls_450k_video.ts
36 #EXTINF:10,
37 #EXT-X-BYTERANGE:709136@5212676
38 hls_450k_video.ts
39 #EXTINF:10,
40 #EXT-X-BYTERANGE:730004@5921812
41 hls_450k_video.ts
42 #EXTINF:10,
43 #EXT-X-BYTERANGE:456276@6651816
44 hls_450k_video.ts
45 #EXTINF:10,
46 #EXT-X-BYTERANGE:468684@7108092
47 hls_450k_video.ts
48 #EXTINF:10,
49 #EXT-X-BYTERANGE:444996@7576776
50 hls_450k_video.ts
51 #EXTINF:22,
52 #EXTINF:10,
53 #EXT-X-BYTERANGE:331444@8021772
54 hls_450k_video.ts
55 #EXT-X-BYTERANGE:44556@8353216
56 hls_450k_video.ts
57 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "byterange": {
8 "length": 522828,
9 "offset": 0
10 },
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 }
14 ],
15 "targetDuration": 10
16 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:4
4 #EXT-X-ALLOW-CACHE:MAYBE
5 #EXT-X-MEDIA-SEQUENCE:0
6 #EXT-X-PLAYLIST-TYPE:VOD
7 #EXTINF:10,
8 #EXT-X-BYTERANGE:522828@0
9 hls_450k_video.ts
10 #EXT-X-ENDLIST
...\ No newline at end of file ...\ No newline at end of file
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 },
10 {
11 "duration": 6.08,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 },
14 {
15 "duration": 6.6,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 },
18 {
19 "duration": 5,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 }
22 ],
23 "targetDuration": 8
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:gobblegobble
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-TARGETDURATION:8
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 #EXTINF:6.080,{}
9 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
10 #EXTINF:6.600,{}
11 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
12 #EXTINF:5.000,{}
13 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
14 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
9 },
10 {
11 "duration": 10,
12 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
13 },
14 {
15 "duration": 10,
16 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
17 },
18 {
19 "duration": 10,
20 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
21 },
22 {
23 "duration": 10,
24 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
25 },
26 {
27 "duration": 8,
28 "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
29 }
30 ],
31 "targetDuration": 10
32 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:asdRASDfasdR
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts
6 #EXTINF:10,
7 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts
8 #EXTINF:10,
9 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts
10 #EXTINF:10,
11 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts
12 #EXTINF:10,
13 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts
14 #EXTINF:8,
15 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts
16 #ZEN-TOTAL-DURATION:57.9911
17 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "byterange": {
8 "length": 522828,
9 "offset": 0
10 },
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 },
14 {
15 "byterange": {
16 "length": 587500,
17 "offset": 522828
18 },
19 "duration": 10,
20 "uri": "hls_450k_video.ts"
21 },
22 {
23 "byterange": {
24 "length": 713084,
25 "offset": 1110328
26 },
27 "duration": 10,
28 "uri": "hls_450k_video.ts"
29 },
30 {
31 "byterange": {
32 "length": 476580,
33 "offset": 1823412
34 },
35 "duration": 10,
36 "uri": "hls_450k_video.ts"
37 },
38 {
39 "byterange": {
40 "length": 535612,
41 "offset": 2299992
42 },
43 "duration": 10,
44 "uri": "hls_450k_video.ts"
45 },
46 {
47 "byterange": {
48 "length": 207176,
49 "offset": 2835604
50 },
51 "duration": 10,
52 "uri": "hls_450k_video.ts"
53 },
54 {
55 "byterange": {
56 "length": 455900,
57 "offset": 3042780
58 },
59 "duration": 10,
60 "uri": "hls_450k_video.ts"
61 },
62 {
63 "byterange": {
64 "length": 657248,
65 "offset": 3498680
66 },
67 "duration": 10,
68 "uri": "hls_450k_video.ts"
69 },
70 {
71 "byterange": {
72 "length": 571708,
73 "offset": 4155928
74 },
75 "duration": 10,
76 "uri": "hls_450k_video.ts"
77 },
78 {
79 "byterange": {
80 "length": 485040,
81 "offset": 4727636
82 },
83 "duration": 10,
84 "uri": "hls_450k_video.ts"
85 },
86 {
87 "byterange": {
88 "length": 709136,
89 "offset": 5212676
90 },
91 "duration": 10,
92 "uri": "hls_450k_video.ts"
93 },
94 {
95 "byterange": {
96 "length": 730004,
97 "offset": 5921812
98 },
99 "duration": 10,
100 "uri": "hls_450k_video.ts"
101 },
102 {
103 "byterange": {
104 "length": 456276,
105 "offset": 6651816
106 },
107 "duration": 10,
108 "uri": "hls_450k_video.ts"
109 },
110 {
111 "byterange": {
112 "length": 468684,
113 "offset": 7108092
114 },
115 "duration": 10,
116 "uri": "hls_450k_video.ts"
117 },
118 {
119 "byterange": {
120 "length": 444996,
121 "offset": 7576776
122 },
123 "duration": 10,
124 "uri": "hls_450k_video.ts"
125 },
126 {
127 "byterange": {
128 "length": 331444,
129 "offset": 8021772
130 },
131 "duration": 10,
132 "uri": "hls_450k_video.ts"
133 },
134 {
135 "byterange": {
136 "length": 44556,
137 "offset": 8353216
138 },
139 "duration": 1.4167,
140 "uri": "hls_450k_video.ts"
141 }
142 ]
143 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:NaN
3 #EXT-X-VERSION:4
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXT-X-PLAYLIST-TYPE:VOD
6 #EXTINF:10,
7 #EXT-X-BYTERANGE:522828@0
8 hls_450k_video.ts
9 #EXTINF:10,
10 #EXT-X-BYTERANGE:587500@522828
11 hls_450k_video.ts
12 #EXTINF:10,
13 #EXT-X-BYTERANGE:713084@1110328
14 hls_450k_video.ts
15 #EXTINF:10,
16 #EXT-X-BYTERANGE:476580@1823412
17 hls_450k_video.ts
18 #EXTINF:10,
19 #EXT-X-BYTERANGE:535612@2299992
20 hls_450k_video.ts
21 #EXTINF:10,
22 #EXT-X-BYTERANGE:207176@2835604
23 hls_450k_video.ts
24 #EXTINF:10,
25 #EXT-X-BYTERANGE:455900@3042780
26 hls_450k_video.ts
27 #EXTINF:10,
28 #EXT-X-BYTERANGE:657248@3498680
29 hls_450k_video.ts
30 #EXTINF:10,
31 #EXT-X-BYTERANGE:571708@4155928
32 hls_450k_video.ts
33 #EXTINF:10,
34 #EXT-X-BYTERANGE:485040@4727636
35 hls_450k_video.ts
36 #EXTINF:10,
37 #EXT-X-BYTERANGE:709136@5212676
38 hls_450k_video.ts
39 #EXTINF:10,
40 #EXT-X-BYTERANGE:730004@5921812
41 hls_450k_video.ts
42 #EXTINF:10,
43 #EXT-X-BYTERANGE:456276@6651816
44 hls_450k_video.ts
45 #EXTINF:10,
46 #EXT-X-BYTERANGE:468684@7108092
47 hls_450k_video.ts
48 #EXTINF:10,
49 #EXT-X-BYTERANGE:444996@7576776
50 hls_450k_video.ts
51 #EXTINF:10,
52 #EXT-X-BYTERANGE:331444@8021772
53 hls_450k_video.ts
54 #EXTINF:1.4167,
55 #EXT-X-BYTERANGE:44556@8353216
56 hls_450k_video.ts
57 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 },
10 {
11 "duration": 8,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 },
14 {
15 "duration": 8,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 }
18 ],
19 "targetDuration": 8
20 }
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:0
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-TARGETDURATION:8
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
9 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
9 }
10 ]
11 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #ZEN-TOTAL-DURATION:50
3 #EXT-X-TARGETDURATION:-10
4 #EXTINF:10,
5 /test/ts-files/zencoder/gogo/00001.ts
6 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
9 },
10 {
11 "duration": 10,
12 "uri": "/test/ts-files/zencoder/gogo/00002.ts"
13 },
14 {
15 "duration": 10,
16 "uri": "/test/ts-files/zencoder/gogo/00003.ts"
17 },
18 {
19 "duration": 10,
20 "uri": "/test/ts-files/zencoder/gogo/00004.ts"
21 },
22 {
23 "duration": 10,
24 "uri": "/test/ts-files/zencoder/gogo/00005.ts"
25 }
26 ],
27 "targetDuration": 10
28 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #ZEN-TOTAL-DURATION:50
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 /test/ts-files/zencoder/gogo/00001.ts
6 #EXTINF:10,
7 /test/ts-files/zencoder/gogo/00002.ts
8 #EXTINF:10,
9 /test/ts-files/zencoder/gogo/00003.ts
10 #EXT-X-ENDLIST
11 #EXTINF:10,
12 /test/ts-files/zencoder/gogo/00004.ts
13 #EXTINF:10,
14 /test/ts-files/zencoder/gogo/00005.ts
15
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "/test/ts-files/zencoder/gogo/00001.ts"
9 }
10 ],
11 "targetDuration": 10
12 }
...\ No newline at end of file ...\ No newline at end of file
1 #ZEN-TOTAL-DURATION:10
2 #EXT-X-TARGETDURATION:10
3 #EXTINF:10,
4 /test/ts-files/zencoder/gogo/00001.ts
5 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "playlists": [{
4 "attributes": {
5 "PROGRAM-ID": 1,
6 "BANDWIDTH": 240000,
7 "RESOLUTION": {
8 "width": 396,
9 "height": 224
10 }
11 },
12 "uri": "media.m3u8"
13 }, {
14 "attributes": {
15 "PROGRAM-ID": 1,
16 "BANDWIDTH": 40000
17 },
18 "uri": "media1.m3u8"
19 }, {
20 "attributes": {
21 "PROGRAM-ID": 1,
22 "BANDWIDTH": 440000,
23 "RESOLUTION": {
24 "width": 396,
25 "height": 224
26 }
27 },
28 "uri": "media2.m3u8"
29 }, {
30 "attributes": {
31 "PROGRAM-ID": 1,
32 "BANDWIDTH": 1928000,
33 "RESOLUTION": {
34 "width": 960,
35 "height": 540
36 }
37 },
38 "uri": "media3.m3u8"
39 }]
40 }
1 # A simple master playlist with multiple variant streams
2 #EXTM3U
3 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224
4 media.m3u8
5 #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=40000
6 media1.m3u8
7 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224
8 media2.m3u8
9 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540
10 media3.m3u8
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "00001.ts"
9 },
10 {
11 "duration": 10,
12 "uri": "00002.ts"
13 },
14 {
15 "duration": 10,
16 "uri": "00003.ts"
17 },
18 {
19 "duration": 10,
20 "uri": "00004.ts"
21 }
22 ],
23 "targetDuration": 10
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 00001.ts
6 #EXTINF:10,
7 00002.ts
8 #EXTINF:10,
9 00003.ts
10 #EXTINF:10,
11 00004.ts
12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 00001.ts
6 #EXTINF:10,
7 00002.ts
8 #EXTINF:10,
9 00003.ts
10 #EXTINF:10,
11 00004.ts
12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 00001.ts
6 #EXTINF:10,
7 00002.ts
8 #EXTINF:10,
9 00003.ts
10 #EXTINF:10,
11 00004.ts
12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 },
10 {
11 "duration": 6.08,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 },
14 {
15 "duration": 6.6,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 },
18 {
19 "duration": 5,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 }
22 ],
23 "targetDuration": 8
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:0
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-TARGETDURATION:8
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 #EXTINF:6.080,{}
9 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
10 #EXTINF:6.600,{}
11 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
12 #EXTINF:5.000,{}
13 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
14 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "hls_450k_video.ts"
9 },
10 {
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 },
14 {
15 "duration": 10,
16 "uri": "hls_450k_video.ts"
17 }
18 ],
19 "targetDuration": 10
20 }
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:3
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXT-X-PLAYLIST-TYPE:VOD
6 #EXTINF:10
7 hls_450k_video.ts
8 hls_450k_video.ts
9 #EXTINF:10,
10 hls_450k_video.ts
11 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 },
10 {
11 "duration": 6.08,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 },
14 {
15 "duration": 6.6,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 },
18 {
19 "duration": 5,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 }
22 ],
23 "targetDuration": 8
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-ALLOW-CACHE:YES
4 #EXT-X-TARGETDURATION:8
5 #EXTINF:6.640,{}
6 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
7 #EXTINF:6.080,{}
8 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
9 #EXTINF:6.600,{}
10 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
11 #EXTINF:5.000,{}
12 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
13 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 },
10 {
11 "duration": 8,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 },
14 {
15 "duration": 8,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 },
18 {
19 "duration": 8,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 }
22 ],
23 "targetDuration": 8
24 }
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:0
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-TARGETDURATION:8
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
9 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
10 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
11 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "targetDuration": 10,
6 "segments": [{
7 "uri": "001.ts"
8 }, {
9 "uri": "002.ts",
10 "duration": 9
11 }, {
12 "uri": "003.ts",
13 "duration": 7
14 }, {
15 "uri": "004.ts",
16 "duration": 10
17 }]
18 }
1 #EXTM3U
2 001.ts
3 #EXT-X-TARGETDURATION:9
4 002.ts
5 #EXTINF:7
6 003.ts
7 #EXT-X-TARGETDURATION:10
8 004.ts
...\ No newline at end of file ...\ No newline at end of file
1 {
2 "allowCache": true,
3 "mediaSequence": -11,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 },
10 {
11 "duration": 6.08,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 },
14 {
15 "duration": 6.6,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 },
18 {
19 "duration": 5,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 }
22 ],
23 "targetDuration": 8
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:-11
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-TARGETDURATION:8
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 #EXTINF:6.080,{}
9 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
10 #EXTINF:6.600,{}
11 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
12 #EXTINF:5.000,{}
13 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
14 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "byterange": {
8 "length": 522828,
9 "offset": 0
10 },
11 "duration": 10,
12 "uri": "hls_450k_video.ts"
13 },
14 {
15 "byterange": {
16 "length": 587500,
17 "offset": 522828
18 },
19 "duration": 10,
20 "uri": "hls_450k_video.ts"
21 },
22 {
23 "byterange": {
24 "length": 713084,
25 "offset": 1110328
26 },
27 "duration": 10,
28 "uri": "hls_450k_video.ts"
29 },
30 {
31 "byterange": {
32 "length": 476580,
33 "offset": 1823412
34 },
35 "duration": 10,
36 "uri": "hls_450k_video.ts"
37 },
38 {
39 "byterange": {
40 "length": 535612,
41 "offset": 2299992
42 },
43 "duration": 10,
44 "uri": "hls_450k_video.ts"
45 },
46 {
47 "byterange": {
48 "length": 207176,
49 "offset": 2835604
50 },
51 "duration": 10,
52 "uri": "hls_450k_video.ts"
53 },
54 {
55 "byterange": {
56 "length": 455900,
57 "offset": 3042780
58 },
59 "duration": 10,
60 "uri": "hls_450k_video.ts"
61 },
62 {
63 "byterange": {
64 "length": 657248,
65 "offset": 3498680
66 },
67 "duration": 10,
68 "uri": "hls_450k_video.ts"
69 },
70 {
71 "byterange": {
72 "length": 571708,
73 "offset": 4155928
74 },
75 "duration": 10,
76 "uri": "hls_450k_video.ts"
77 },
78 {
79 "byterange": {
80 "length": 485040,
81 "offset": 4727636
82 },
83 "duration": 10,
84 "uri": "hls_450k_video.ts"
85 },
86 {
87 "byterange": {
88 "length": 709136,
89 "offset": 5212676
90 },
91 "duration": 10,
92 "uri": "hls_450k_video.ts"
93 },
94 {
95 "byterange": {
96 "length": 730004,
97 "offset": 5921812
98 },
99 "duration": 10,
100 "uri": "hls_450k_video.ts"
101 },
102 {
103 "byterange": {
104 "length": 456276,
105 "offset": 6651816
106 },
107 "duration": 10,
108 "uri": "hls_450k_video.ts"
109 },
110 {
111 "byterange": {
112 "length": 468684,
113 "offset": 7108092
114 },
115 "duration": 10,
116 "uri": "hls_450k_video.ts"
117 },
118 {
119 "byterange": {
120 "length": 444996,
121 "offset": 7576776
122 },
123 "duration": 10,
124 "uri": "hls_450k_video.ts"
125 },
126 {
127 "byterange": {
128 "length": 331444,
129 "offset": 8021772
130 },
131 "duration": 10,
132 "uri": "hls_450k_video.ts"
133 },
134 {
135 "byterange": {
136 "length": 44556,
137 "offset": 8353216
138 },
139 "duration": 1.4167,
140 "uri": "hls_450k_video.ts"
141 }
142 ],
143 "targetDuration": 10
144 }
...\ No newline at end of file ...\ No newline at end of file
1 window.playlistData = '#EXTM3U\n'+
2 '#EXT-X-TARGETDURATION:10\n' +
3 '#EXT-X-VERSION:4\n' +
4 '#EXT-X-MEDIA-SEQUENCE:0\n' +
5 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
6 '#EXTINF:10,\n' +
7 '#EXT-X-BYTERANGE:522828@0\n' +
8 'hls_450k_video.ts\n' +
9 '#EXTINF:10,\n' +
10 '#EXT-X-BYTERANGE:587500@522828\n' +
11 'hls_450k_video.ts\n' +
12 '#EXTINF:10,\n' +
13 '#EXT-X-BYTERANGE:713084@1110328\n' +
14 'hls_450k_video.ts\n' +
15 '#EXTINF:10,\n' +
16 '#EXT-X-BYTERANGE:476580@1823412\n' +
17 'hls_450k_video.ts\n' +
18 '#EXTINF:10,\n' +
19 '#EXT-X-BYTERANGE:535612@2299992\n' +
20 'hls_450k_video.ts\n' +
21 '#EXTINF:10,\n' +
22 '#EXT-X-BYTERANGE:207176@2835604\n' +
23 'hls_450k_video.ts\n' +
24 '#EXTINF:10,\n' +
25 '#EXT-X-BYTERANGE:455900@3042780\n' +
26 'hls_450k_video.ts\n' +
27 '#EXTINF:10,\n' +
28 '#EXT-X-BYTERANGE:657248@3498680\n' +
29 'hls_450k_video.ts\n' +
30 '#EXTINF:10,\n' +
31 '#EXT-X-BYTERANGE:571708@4155928\n' +
32 'hls_450k_video.ts\n' +
33 '#EXTINF:10,\n' +
34 '#EXT-X-BYTERANGE:485040@4727636\n' +
35 'hls_450k_video.ts\n' +
36 '#EXTINF:10,\n' +
37 '#EXT-X-BYTERANGE:709136@5212676\n' +
38 'hls_450k_video.ts\n' +
39 '#EXTINF:10,\n' +
40 '#EXT-X-BYTERANGE:730004@5921812\n' +
41 'hls_450k_video.ts\n' +
42 '#EXTINF:10,\n' +
43 '#EXT-X-BYTERANGE:456276@6651816\n' +
44 'hls_450k_video.ts\n' +
45 '#EXTINF:10,\n' +
46 '#EXT-X-BYTERANGE:468684@7108092\n' +
47 'hls_450k_video.ts' +
48 '#EXTINF:10,\n' +
49 '#EXT-X-BYTERANGE:444996@7576776\n' +
50 'hls_450k_video.ts\n' +
51 '#EXTINF:10,\n' +
52 '#EXT-X-BYTERANGE:331444@8021772\n' +
53 'hls_450k_video.ts\n' +
54 '#EXTINF:1.4167,\n' +
55 '#EXT-X-BYTERANGE:44556@8353216\n' +
56 'hls_450k_video.ts\n' +
57 '#EXT-X-ENDLIST';
1 {
2 "allowCache": true,
3 "mediaSequence": 17,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 }
10 ],
11 "targetDuration": 8
12 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:17
4 #EXT-X-ALLOW-CACHE:YES
5 #EXT-X-TARGETDURATION:8
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 #EXT-X-ENDLIST
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:{{{version}}}
4 {{#if allowCache}}#EXT-X-ALLOW-CACHE:{{{allowCache}}}{{/if}}
5 #EXT-X-MEDIA-SEQUENCE:0
6 #EXT-X-PLAYLIST-TYPE:VOD
7 #EXTINF:10,
8 #EXT-X-BYTERANGE:522828@0
9 hls_450k_video.ts
10 #EXTINF:10,
11 #EXT-X-BYTERANGE:587500@522828
12 hls_450k_video.ts
13 #EXTINF:10,
14 #EXT-X-BYTERANGE:713084@1110328
15 hls_450k_video.ts
16 #EXTINF:10,
17 #EXT-X-BYTERANGE:476580@1823412
18 hls_450k_video.ts
19 #EXTINF:10,
20 #EXT-X-BYTERANGE:535612@2299992
21 hls_450k_video.ts
22 #EXTINF:10,
23 #EXT-X-BYTERANGE:207176@2835604
24 hls_450k_video.ts
25 #EXTINF:10,
26 #EXT-X-BYTERANGE:455900@3042780
27 hls_450k_video.ts
28 #EXTINF:10,
29 #EXT-X-BYTERANGE:657248@3498680
30 hls_450k_video.ts
31 #EXTINF:10,
32 #EXT-X-BYTERANGE:571708@4155928
33 hls_450k_video.ts
34 #EXTINF:10,
35 #EXT-X-BYTERANGE:485040@4727636
36 hls_450k_video.ts
37 #EXTINF:10,
38 #EXT-X-BYTERANGE:709136@5212676
39 hls_450k_video.ts
40 #EXTINF:10,
41 #EXT-X-BYTERANGE:730004@5921812
42 hls_450k_video.ts
43 #EXTINF:10,
44 #EXT-X-BYTERANGE:456276@6651816
45 hls_450k_video.ts
46 #EXTINF:10,
47 #EXT-X-BYTERANGE:468684@7108092
48 hls_450k_video.ts
49 #EXTINF:10,
50 #EXT-X-BYTERANGE:444996@7576776
51 hls_450k_video.ts
52 #EXTINF:10,
53 #EXT-X-BYTERANGE:331444@8021772
54 hls_450k_video.ts
55 #EXTINF:1.4167,
56 #EXT-X-BYTERANGE:44556@8353216
57 hls_450k_video.ts
58 #EXT-X-ENDLIST
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:{{{version}}}
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXT-X-PLAYLIST-TYPE:VOD
6 #EXTINF:10,
7 {{#if byteRange}}#EXT-X-BYTERANGE:{{{byteRange}}}{{/if}}
8 //#EXT-X-BYTERANGE:522828@0
9 hls_450k_video.ts
10 #EXTINF:10,
11 {{#if byteRange1}}#EXT-X-BYTERANGE:{{{byteRange1}}}{{/if}}
12 //#EXT-X-BYTERANGE:587500@522828
13 hls_450k_video.ts
14 #EXTINF:10,
15 #EXT-X-BYTERANGE:713084@1110328
16 hls_450k_video.ts
17 #EXTINF:10,
18 #EXT-X-BYTERANGE:476580@1823412
19 hls_450k_video.ts
20 #EXTINF:10,
21 #EXT-X-BYTERANGE:535612@2299992
22 hls_450k_video.ts
23 #EXTINF:10,
24 #EXT-X-BYTERANGE:207176@2835604
25 hls_450k_video.ts
26 #EXTINF:10,
27 #EXT-X-BYTERANGE:455900@3042780
28 hls_450k_video.ts
29 #EXTINF:10,
30 #EXT-X-BYTERANGE:657248@3498680
31 hls_450k_video.ts
32 #EXTINF:10,
33 #EXT-X-BYTERANGE:571708@4155928
34 hls_450k_video.ts
35 #EXTINF:10,
36 #EXT-X-BYTERANGE:485040@4727636
37 hls_450k_video.ts
38 #EXTINF:10,
39 #EXT-X-BYTERANGE:709136@5212676
40 hls_450k_video.ts
41 #EXTINF:10,
42 #EXT-X-BYTERANGE:730004@5921812
43 hls_450k_video.ts
44 #EXTINF:10,
45 #EXT-X-BYTERANGE:456276@6651816
46 hls_450k_video.ts
47 #EXTINF:10,
48 #EXT-X-BYTERANGE:468684@7108092
49 hls_450k_video.ts
50 #EXTINF:10,
51 #EXT-X-BYTERANGE:444996@7576776
52 hls_450k_video.ts
53 #EXTINF:10,
54 #EXT-X-BYTERANGE:331444@8021772
55 hls_450k_video.ts
56 #EXTINF:1.4167,
57 {{#if byteRange2}}#EXT-X-BYTERANGE:{{{byteRange2}}}{{/if}}
58 //#EXT-X-BYTERANGE:44556@8353216
59 hls_450k_video.ts
60 #EXT-X-ENDLIST
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:{{{version}}}
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXT-X-PLAYLIST-TYPE:VOD
6 {{#if extInf}}#EXTINF:{{{extInf}}}{{/if}}
7 #EXT-X-BYTERANGE:522828@0
8 {{#if segment}}{{{segment}}}\n{{/if}}
9 {{#if extInf1}}#EXTINF:{{{extInf1}}}{{/if}}
10 #EXT-X-BYTERANGE:587500@522828
11 hls_450k_video.ts
12 #EXTINF:10,
13 #EXT-X-BYTERANGE:713084@1110328
14 hls_450k_video.ts
15 #EXTINF:10,
16 #EXT-X-BYTERANGE:476580@1823412
17 hls_450k_video.ts
18 #EXTINF:10,
19 #EXT-X-BYTERANGE:535612@2299992
20 hls_450k_video.ts
21 #EXTINF:10,
22 #EXT-X-BYTERANGE:207176@2835604
23 hls_450k_video.ts
24 #EXTINF:10,
25 #EXT-X-BYTERANGE:455900@3042780
26 hls_450k_video.ts
27 #EXTINF:10,
28 #EXT-X-BYTERANGE:657248@3498680
29 hls_450k_video.ts
30 #EXTINF:10,
31 #EXT-X-BYTERANGE:571708@4155928
32 hls_450k_video.ts
33 #EXTINF:10,
34 #EXT-X-BYTERANGE:485040@4727636
35 hls_450k_video.ts
36 #EXTINF:10,
37 #EXT-X-BYTERANGE:709136@5212676
38 hls_450k_video.ts
39 #EXTINF:10,
40 #EXT-X-BYTERANGE:730004@5921812
41 hls_450k_video.ts
42 #EXTINF:10,
43 #EXT-X-BYTERANGE:456276@6651816
44 hls_450k_video.ts
45 #EXTINF:10,
46 #EXT-X-BYTERANGE:468684@7108092
47 hls_450k_video.ts
48 #EXTINF:10,
49 #EXT-X-BYTERANGE:444996@7576776
50 hls_450k_video.ts
51 #EXTINF:10,
52 #EXT-X-BYTERANGE:331444@8021772
53 hls_450k_video.ts
54 {{#if extInf2}}#EXTINF:{{{extInf2}}}{{/if}}
55 #EXT-X-BYTERANGE:44556@8353216
56 hls_450k_video.ts
57 #EXT-X-ENDLIST
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 {{#if mediaSequence}}#EXT-X-MEDIA-SEQUENCE:{{{mediaSequence}}}{{/if}}
4 {{#if mediaSequence1}}#EXT-X-MEDIA-SEQUENCE:{{{mediaSequence2}}}{{/if}}
5 #EXT-X-ALLOW-CACHE:YES
6 #EXT-X-TARGETDURATION:8
7 #EXTINF:6.640,{}
8 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
9 #EXTINF:6.080,{}
10 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
11 #EXTINF:6.600,{}
12 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
13 #EXTINF:5.000,{}
14 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
15 #EXT-X-ENDLIST
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:0
4 #EXT-X-ALLOW-CACHE:YES
5 {{#if targetDuration}}#EXT-X-TARGETDURATION:{{{targetDuration}}}{{/if}}
6 #EXTINF:6.640,{}
7 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
8 #EXTINF:6.080,{}
9 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
10 #EXTINF:6.600,{}
11 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
12 #EXTINF:5.000,{}
13 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
14 #EXT-X-ENDLIST
1 #EXTM3U
2 {{#if playlistType}}#EXT-X-PLAYLIST-TYPE:{{{playlistType}}}{{/if}}
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts
6 #EXTINF:10,
7 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts
8 #EXTINF:10,
9 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts
10 #EXTINF:10,
11 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts
12 #EXTINF:10,
13 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts
14 #EXTINF:8,
15 /test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts
16 #ZEN-TOTAL-DURATION:57.9911
17 #EXT-X-ENDLIST
1 var grunt = require('grunt'),
2 extname = require('path').extname;
3
4 grunt.file.recurse(process.cwd(), function(path) {
5 var json;
6 if (extname(path) === '.json') {
7 json = grunt.file.readJSON(path);
8 if (json.totalDuration) {
9 delete json.totalDuration;
10 grunt.file.write(path, JSON.stringify(json, null, ' '));
11 }
12 }
13 });
1 {
2 "allowCache": true,
3 "playlists": [
4 {
5 "attributes": {
6 "PROGRAM-ID": 1
7 },
8 "uri": "media.m3u8"
9 },
10 {
11 "uri": "media1.m3u8"
12 }
13 ]
14 }
1 # A simple master playlist with multiple variant streams
2 #EXTM3U
3 #EXT-X-STREAM-INF:PROGRAM-ID=1
4 media.m3u8
5 #EXT-X-STREAM-INF:
6 media1.m3u8
1 {
2 "allowCache": true,
3 "mediaSequence": 11,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 6.64,
8 "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
9 },
10 {
11 "duration": 6.08,
12 "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
13 },
14 {
15 "duration": 6.6,
16 "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
17 },
18 {
19 "duration": 5,
20 "uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
21 }
22 ],
23 "targetDuration": 8
24 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-MEDIA-SEQUENCE:0
4 #EXT-X-MEDIA-SEQUENCE:11
5 #EXT-X-ALLOW-CACHE:YES
6 #EXT-X-TARGETDURATION:8
7 #EXTINF:6.640,{}
8 /test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
9 #EXTINF:6.080,{}
10 /test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
11 #EXTINF:6.600,{}
12 /test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
13 #EXTINF:5.000,{}
14 /test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
15 #EXT-X-ENDLIST
1 {
2 "allowCache": true,
3 "mediaSequence": 0,
4 "playlistType": "VOD",
5 "segments": [
6 {
7 "duration": 10,
8 "uri": "hls_450k_video.ts"
9 }
10 ],
11 "targetDuration": 10
12 }
...\ No newline at end of file ...\ No newline at end of file
1 #EXTM3U
2 #EXT-X-TARGETDURATION:10
3 #EXT-X-VERSION:NaN
4 #EXT-X-MEDIA-SEQUENCE:0
5 #EXT-X-PLAYLIST-TYPE:VOD
6 #EXTINF:10,
7 hls_450k_video.ts
8 #EXT-X-ENDLIST
1 (function(window) {
2 /*
3 ======== A Handy Little QUnit Reference ========
4 http://api.qunitjs.com/
5
6 Test methods:
7 module(name, {[setup][ ,teardown]})
8 test(name, callback)
9 expect(numberOfAssertions)
10 stop(increment)
11 start(decrement)
12 Test assertions:
13 ok(value, [message])
14 equal(actual, expected, [message])
15 notEqual(actual, expected, [message])
16 deepEqual(actual, expected, [message])
17 notDeepEqual(actual, expected, [message])
18 strictEqual(actual, expected, [message])
19 notStrictEqual(actual, expected, [message])
20 throws(block, [expected], [message])
21 */
22 var
23 parser,
24
25 expectedHeader = [
26 0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00,
27 0x09, 0x00, 0x00, 0x00, 0x00
28 ],
29
30 extend = window.videojs.util.mergeOptions,
31
32 testAudioTag,
33 testVideoTag,
34 testScriptTag,
35 asciiFromBytes,
36 testScriptString,
37 testScriptEcmaArray,
38 testNalUnit;
39
40 module('segment parser', {
41 setup: function() {
42 parser = new window.videojs.hls.SegmentParser();
43 }
44 });
45
46 test('creates an flv header', function() {
47 var header = Array.prototype.slice.call(parser.getFlvHeader());
48 ok(header, 'the header is truthy');
49 equal(9 + 4, header.length, 'the header length is correct');
50 equal(header[0], 'F'.charCodeAt(0), 'the first character is "F"');
51 equal(header[1], 'L'.charCodeAt(0), 'the second character is "L"');
52 equal(header[2], 'V'.charCodeAt(0), 'the third character is "V"');
53
54 deepEqual(expectedHeader, header, 'the rest of the header is correct');
55 });
56
57 test('parses PMTs with program descriptors', function() {
58 var
59 makePmt = function(options) {
60 var
61 result = [],
62 entryCount = 0,
63 k,
64 sectionLength;
65 for (k in options.pids) {
66 entryCount++;
67 }
68 // table_id
69 result.push(0x02);
70 // section_syntax_indicator '0' reserved section_length
71 // 13 + (program_info_length) + (n * 5 + ES_info_length[n])
72 sectionLength = 13 + (5 * entryCount) + 17;
73 result.push(0x80 | (0xF00 & sectionLength >>> 8));
74 result.push(sectionLength & 0xFF);
75 // program_number
76 result.push(0x00);
77 result.push(0x01);
78 // reserved version_number current_next_indicator
79 result.push(0x01);
80 // section_number
81 result.push(0x00);
82 // last_section_number
83 result.push(0x00);
84 // reserved PCR_PID
85 result.push(0xe1);
86 result.push(0x00);
87 // reserved program_info_length
88 result.push(0xf0);
89 result.push(0x11); // hard-coded 17 byte descriptor
90 // program descriptors
91 result = result.concat([
92 0x25, 0x0f, 0xff, 0xff,
93 0x49, 0x44, 0x33, 0x20,
94 0xff, 0x49, 0x44, 0x33,
95 0x20, 0x00, 0x1f, 0x00,
96 0x01
97 ]);
98 for (k in options.pids) {
99 // stream_type
100 result.push(options.pids[k]);
101 // reserved elementary_PID
102 result.push(0xe0 | (k & 0x1f00) >>> 8);
103 result.push(k & 0xff);
104 // reserved ES_info_length
105 result.push(0xf0);
106 result.push(0x00); // ES_info_length = 0
107 }
108 // CRC_32
109 result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
110 return result;
111 },
112 makePat = function(options) {
113 var
114 result = [],
115 k;
116 // table_id
117 result.push(0x00);
118 // section_syntax_indicator '0' reserved section_length
119 result.push(0x80);
120 result.push(0x0d); // section_length for one program
121 // transport_stream_id
122 result.push(0x00);
123 result.push(0x00);
124 // reserved version_number current_next_indicator
125 result.push(0x01); // current_next_indicator is 1
126 // section_number
127 result.push(0x00);
128 // last_section_number
129 result.push(0x00);
130 for (k in options.programs) {
131 // program_number
132 result.push((k & 0xFF00) >>> 8);
133 result.push(k & 0x00FF);
134 // reserved program_map_pid
135 result.push((options.programs[k] & 0x1f00) >>> 8);
136 result.push(options.programs[k] & 0xff);
137 }
138 return result;
139 },
140 makePsi = function(options) {
141 var result = [];
142
143 // pointer_field
144 if (options.payloadUnitStartIndicator) {
145 result.push(0x00);
146 }
147 if (options.programs) {
148 return result.concat(makePat(options));
149 }
150 return result.concat(makePmt(options));
151 },
152 makePacket = function(options) {
153 var
154 result = [],
155 settings = extend({
156 payloadUnitStartIndicator: true,
157 pid: 0x00
158 }, options);
159
160 // header
161 // sync_byte
162 result.push(0x47);
163 // transport_error_indicator payload_unit_start_indicator transport_priority PID
164 result.push((settings.pid & 0x1f) << 8 | 0x40);
165 result.push(settings.pid & 0xff);
166 // transport_scrambling_control adaptation_field_control continuity_counter
167 result.push(0x10);
168 result = result.concat(makePsi(settings));
169
170 // ensure the resulting packet is the correct size
171 result.length = window.videojs.hls.SegmentParser.MP2T_PACKET_LENGTH;
172 return result;
173 },
174 h264Type = window.videojs.hls.SegmentParser.STREAM_TYPES.h264,
175 adtsType = window.videojs.hls.SegmentParser.STREAM_TYPES.adts;
176
177 parser.parseSegmentBinaryData(new Uint8Array(makePacket({
178 programs: {
179 0x01: [0x01]
180 }
181 }).concat(makePacket({
182 pid: 0x01,
183 pids: {
184 0x02: h264Type, // h264 video
185 0x03: adtsType // adts audio
186 }
187 }))));
188
189 strictEqual(parser.stream.pmtPid, 0x01, 'PMT PID is 1');
190 strictEqual(parser.stream.programMapTable[h264Type], 0x02, 'video is PID 2');
191 strictEqual(parser.stream.programMapTable[adtsType], 0x03, 'audio is PID 3');
192 });
193
194 test('parses the first bipbop segment', function() {
195 parser.parseSegmentBinaryData(window.bcSegment);
196
197 ok(parser.tagsAvailable(), 'tags are available');
198 });
199
200 testAudioTag = function(tag) {
201 var
202 byte = tag.bytes[11],
203 format = (byte & 0xF0) >>> 4,
204 soundRate = byte & 0x03,
205 soundSize = (byte & 0x2) >>> 1,
206 soundType = byte & 0x1,
207 aacPacketType = tag.bytes[12];
208
209 equal(10, format, 'the audio format is aac');
210 equal(3, soundRate, 'the sound rate is 44kHhz');
211 equal(1, soundSize, 'the sound size is 16-bit samples');
212 equal(1, soundType, 'the sound type is stereo');
213
214 ok(aacPacketType === 0 || aacPacketType === 1, 'aac packets should have a valid type');
215 };
216
217 testVideoTag = function (tag) {
218 var
219 byte = tag.bytes[11],
220 frameType = (byte & 0xF0) >>> 4,
221 codecId = byte & 0x0F,
222 packetType = tag.bytes[12],
223 compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8;
224
225 // payload starts at tag.bytes[16]
226
227 // XXX: I'm not sure that frame types 3-5 are invalid
228 ok(frameType === 1 || frameType === 2,
229 'the frame type should be valid');
230
231 equal(7, codecId, 'the codec ID is AVC for h264');
232 ok(packetType <= 2 && packetType >= 0, 'the packet type is within [0, 2]');
233 if (packetType !== 1) {
234 equal(0,
235 compositionTime,
236 'the composition time is zero for non-NALU packets');
237 }
238
239 // TODO: the rest of the bytes are an NLU unit
240 if (packetType === 0) {
241 // AVC decoder configuration record
242 } else {
243 // NAL units
244 testNalUnit(tag.bytes.subarray(16));
245 }
246 };
247
248 testNalUnit = function(bytes) {
249 var
250 nalHeader = bytes[0];
251 // unitType = nalHeader & 0x1F;
252
253 equal(0, (nalHeader & 0x80) >>> 7, 'the first bit is always 0');
254 // equal(90, (nalHeader & 0x60) >>> 5, 'the NAL reference indicator is something');
255 // ok(unitType > 0, 'NAL unit type ' + unitType + ' is greater than 0');
256 // ok(unitType < 22 , 'NAL unit type ' + unitType + ' is less than 22');
257 };
258
259
260 asciiFromBytes = function(bytes) {
261 var
262 string = [],
263 i = bytes.byteLength;
264
265 while (i--) {
266 string[i] = String.fromCharCode(bytes[i]);
267 }
268 return string.join('');
269 };
270
271 testScriptString = function(tag, offset, expected) {
272 var
273 type = tag.bytes[offset],
274 stringLength = tag.view.getUint16(offset + 1),
275 string;
276
277 equal(2, type, 'the script element is of string type');
278 equal(stringLength, expected.length, 'the script string length is correct');
279 string = asciiFromBytes(tag.bytes.subarray(offset + 3,
280 offset + 3 + stringLength));
281 equal(expected, string, 'the string value is "' + expected + '"');
282 };
283
284 testScriptEcmaArray = function(tag, start) {
285 var
286 numItems = tag.view.getUint32(start),
287 i = numItems,
288 offset = start + 4,
289 length,
290 type;
291
292 while (i--) {
293 length = tag.view.getUint16(offset);
294
295 // advance offset to the property value
296 offset += 2 + length;
297
298 type = tag.bytes[offset];
299 ok(type === 1 || type === 0,
300 'the ecma array property value type is number or boolean');
301 offset++;
302 if (type) {
303 // boolean
304 ok(tag.bytes[offset] === 0 || tag.bytes[offset] === 1,
305 'the script boolean value is 0 or 1');
306 offset++;
307 } else {
308 // number
309 ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN');
310 offset += 8;
311 }
312 }
313 equal(tag.bytes[offset], 0, 'the property array terminator is valid');
314 equal(tag.bytes[offset + 1], 0, 'the property array terminator is valid');
315 equal(tag.bytes[offset + 2], 9, 'the property array terminator is valid');
316 };
317
318 testScriptTag = function(tag) {
319 testScriptString(tag, 11, 'onMetaData');
320
321 // the onMetaData object is stored as an 'ecma array', an array with non-
322 // integer indices (i.e. a dictionary or hash-map).
323 equal(8, tag.bytes[24], 'onMetaData is of ecma array type');
324 testScriptEcmaArray(tag, 25);
325 };
326
327 test('the flv tags are well-formed', function() {
328 var
329 byte,
330 tag,
331 type,
332 currentPts = 0,
333 lastTime = 0;
334 parser.parseSegmentBinaryData(window.bcSegment);
335
336 while (parser.tagsAvailable()) {
337 tag = parser.getNextTag();
338 type = tag.bytes[0];
339
340 ok(tag.pts >= currentPts, 'presentation time stamps are increasing');
341 currentPts = tag.pts;
342
343 // generic flv headers
344 ok(type === 8 || type === 9 || type === 18,
345 'the type field specifies audio, video or script');
346
347 byte = (tag.view.getUint32(1) & 0xFFFFFF00) >>> 8;
348 equal(tag.bytes.byteLength - 11 - 4, byte, 'the size field is correct');
349
350 byte = tag.view.getUint32(5) & 0xFFFFFF00;
351 ok(byte >= lastTime, 'the timestamp for the tag is greater than zero');
352 lastTime = byte;
353
354 // tag type-specific headers
355 ({
356 8: testAudioTag,
357 9: testVideoTag,
358 18: testScriptTag
359 })[type](tag);
360
361 // previous tag size
362 equal(tag.bytes.byteLength - 4,
363 tag.view.getUint32(tag.bytes.byteLength - 4),
364 'the size of the previous tag is correct');
365 }
366 });
367 })(window);
1 (function(window) {
2 /*
3 ======== A Handy Little QUnit Reference ========
4 http://api.qunitjs.com/
5
6 Test methods:
7 module(name, {[setup][ ,teardown]})
8 test(name, callback)
9 expect(numberOfAssertions)
10 stop(increment)
11 start(decrement)
12 Test assertions:
13 ok(value, [message])
14 equal(actual, expected, [message])
15 notEqual(actual, expected, [message])
16 deepEqual(actual, expected, [message])
17 notDeepEqual(actual, expected, [message])
18 strictEqual(actual, expected, [message])
19 notStrictEqual(actual, expected, [message])
20 throws(block, [expected], [message])
21 */
22 var
23 manifestController,
24 segmentController,
25 m3u8parser,
26 parser,
27
28 expectedHeader = [
29 0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00,
30 0x09, 0x00, 0x00, 0x00, 0x00
31 ],
32 testAudioTag,
33 testVideoTag,
34 testScriptTag,
35 asciiFromBytes,
36 testScriptString,
37 testScriptEcmaArray,
38 testNalUnit;
39
40 module('environment');
41
42 test('is sane', function() {
43 expect(1);
44 ok(true);
45 });
46
47 module('segment parser', {
48 setup: function() {
49 parser = new window.videojs.hls.SegmentParser();
50 }
51 });
52
53 test('creates an flv header', function() {
54 var header = Array.prototype.slice.call(parser.getFlvHeader());
55 ok(header, 'the header is truthy');
56 equal(9 + 4, header.length, 'the header length is correct');
57 equal(header[0], 'F'.charCodeAt(0), 'the first character is "F"');
58 equal(header[1], 'L'.charCodeAt(0), 'the second character is "L"');
59 equal(header[2], 'V'.charCodeAt(0), 'the third character is "V"');
60
61 deepEqual(expectedHeader, header, 'the rest of the header is correct');
62 });
63
64 test('parses the first bipbop segment', function() {
65 parser.parseSegmentBinaryData(window.bcSegment);
66
67 ok(parser.tagsAvailable(), 'tags are available');
68 });
69
70 testAudioTag = function(tag) {
71 var
72 byte = tag.bytes[11],
73 format = (byte & 0xF0) >>> 4,
74 soundRate = byte & 0x03,
75 soundSize = (byte & 0x2) >>> 1,
76 soundType = byte & 0x1,
77 aacPacketType = tag.bytes[12];
78
79 equal(10, format, 'the audio format is aac');
80 equal(3, soundRate, 'the sound rate is 44kHhz');
81 equal(1, soundSize, 'the sound size is 16-bit samples');
82 equal(1, soundType, 'the sound type is stereo');
83
84 ok(aacPacketType === 0 || aacPacketType === 1, 'aac packets should have a valid type');
85 };
86
87 testVideoTag = function (tag) {
88 var
89 byte = tag.bytes[11],
90 frameType = (byte & 0xF0) >>> 4,
91 codecId = byte & 0x0F,
92 packetType = tag.bytes[12],
93 compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8;
94
95 // payload starts at tag.bytes[16]
96
97 // XXX: I'm not sure that frame types 3-5 are invalid
98 ok(frameType === 1 || frameType === 2,
99 'the frame type should be valid');
100
101 equal(7, codecId, 'the codec ID is AVC for h264');
102 ok(packetType <= 2 && packetType >= 0, 'the packet type is within [0, 2]');
103 if (packetType !== 1) {
104 equal(0,
105 compositionTime,
106 'the composition time is zero for non-NALU packets');
107 }
108
109 // TODO: the rest of the bytes are an NLU unit
110 if (packetType === 0) {
111 // AVC decoder configuration record
112 } else {
113 // NAL units
114 testNalUnit(tag.bytes.subarray(16));
115 }
116 };
117
118 testNalUnit = function(bytes) {
119 var
120 nalHeader = bytes[0];
121 // unitType = nalHeader & 0x1F;
122
123 equal(0, (nalHeader & 0x80) >>> 7, 'the first bit is always 0');
124 // equal(90, (nalHeader & 0x60) >>> 5, 'the NAL reference indicator is something');
125 // ok(unitType > 0, 'NAL unit type ' + unitType + ' is greater than 0');
126 // ok(unitType < 22 , 'NAL unit type ' + unitType + ' is less than 22');
127 };
128
129
130 asciiFromBytes = function(bytes) {
131 var
132 string = [],
133 i = bytes.byteLength;
134
135 while (i--) {
136 string[i] = String.fromCharCode(bytes[i]);
137 }
138 return string.join('');
139 };
140
141 testScriptString = function(tag, offset, expected) {
142 var
143 type = tag.bytes[offset],
144 stringLength = tag.view.getUint16(offset + 1),
145 string;
146
147 equal(2, type, 'the script element is of string type');
148 equal(stringLength, expected.length, 'the script string length is correct');
149 string = asciiFromBytes(tag.bytes.subarray(offset + 3,
150 offset + 3 + stringLength));
151 equal(expected, string, 'the string value is "' + expected + '"');
152 };
153
154 testScriptEcmaArray = function(tag, start) {
155 var
156 numItems = tag.view.getUint32(start),
157 i = numItems,
158 offset = start + 4,
159 length,
160 type;
161
162 while (i--) {
163 length = tag.view.getUint16(offset);
164
165 // advance offset to the property value
166 offset += 2 + length;
167
168 type = tag.bytes[offset];
169 ok(type === 1 || type === 0,
170 'the ecma array property value type is number or boolean');
171 offset++;
172 if (type) {
173 // boolean
174 ok(tag.bytes[offset] === 0 || tag.bytes[offset] === 1,
175 'the script boolean value is 0 or 1');
176 offset++;
177 } else {
178 // number
179 ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN');
180 offset += 8;
181 }
182 }
183 equal(tag.bytes[offset], 0, 'the property array terminator is valid');
184 equal(tag.bytes[offset + 1], 0, 'the property array terminator is valid');
185 equal(tag.bytes[offset + 2], 9, 'the property array terminator is valid');
186 };
187
188 testScriptTag = function(tag) {
189 testScriptString(tag, 11, 'onMetaData');
190
191 // the onMetaData object is stored as an 'ecma array', an array with non-
192 // integer indices (i.e. a dictionary or hash-map).
193 equal(8, tag.bytes[24], 'onMetaData is of ecma array type');
194 testScriptEcmaArray(tag, 25);
195 };
196
197 test('the flv tags are well-formed', function() {
198 var
199 tag,
200 byte,
201 type,
202 lastTime = 0;
203 parser.parseSegmentBinaryData(window.bcSegment);
204
205 while (parser.tagsAvailable()) {
206 tag = parser.getNextTag();
207 type = tag.bytes[0];
208
209 // generic flv headers
210 ok(type === 8 || type === 9 || type === 18,
211 'the type field specifies audio, video or script');
212
213 byte = (tag.view.getUint32(1) & 0xFFFFFF00) >>> 8;
214 equal(tag.bytes.byteLength - 11 - 4, byte, 'the size field is correct');
215
216 byte = tag.view.getUint32(5) & 0xFFFFFF00;
217 ok(byte >= lastTime, 'the timestamp for the tag is greater than zero');
218 lastTime = byte;
219
220 // tag type-specific headers
221 ({
222 8: testAudioTag,
223 9: testVideoTag,
224 18: testScriptTag
225 })[type](tag);
226
227 // previous tag size
228 equal(tag.bytes.byteLength - 4,
229 tag.view.getUint32(tag.bytes.byteLength - 4),
230 'the size of the previous tag is correct');
231 }
232 });
233
234 /*
235 M3U8 Test Suite
236 */
237
238 module('m3u8 parser', {
239 setup: function() {
240 m3u8parser = new window.videojs.hls.M3U8Parser();
241 }
242 });
243
244 test('should create my parser', function() {
245 ok(m3u8parser !== undefined);
246 });
247
248 test('should successfully parse manifest data', function() {
249 var parsedData = m3u8parser.parse(window.playlistData);
250 ok(parsedData);
251 });
252
253 test('test for expected results', function() {
254 var data = m3u8parser.parse(window.playlistData);
255
256 notEqual(data, null, 'data is not NULL');
257 equal(data.invalidReasons.length, 0, 'data has 0 invalid reasons');
258 equal(data.hasValidM3UTag, true, 'data has valid EXTM3U');
259 equal(data.targetDuration, 10, 'data has correct TARGET DURATION');
260 equal(data.allowCache, "NO", 'acceptable ALLOW CACHE');
261 equal(data.isPlaylist, false, 'data is parsed as a PLAYLIST as expected');
262 equal(data.playlistType, "VOD", 'acceptable PLAYLIST TYPE');
263 equal(data.mediaItems.length, 16, 'acceptable mediaItem count');
264 equal(data.mediaSequence, 0, 'MEDIA SEQUENCE is correct');
265 equal(data.totalDuration, -1, "ZEN TOTAL DURATION is unknown as expected");
266 equal(data.hasEndTag, true, 'should have ENDLIST tag');
267 });
268
269 module('brightcove playlist', {
270 setup: function() {
271 m3u8parser = new window.videojs.hls.M3U8Parser();
272 }
273 });
274
275 test('should parse a brightcove manifest data', function() {
276 var data = m3u8parser.parse(window.brightcove_playlist_data);
277
278 ok(data);
279 equal(data.playlistItems.length, 4, 'Has correct rendition count');
280 equal(data.isPlaylist, true, 'data is parsed as a PLAYLIST as expected');
281 equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct');
282 equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct');
283 equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct');
284 equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct');
285
286 }
287 );
288
289 module('manifest controller', {
290 setup: function() {
291 manifestController = new window.videojs.hls.ManifestController();
292 this.vjsget = window.videojs.get;
293 window.videojs.get = function(url, success) {
294 success(window.brightcove_playlist_data);
295 };
296 },
297 teardown: function() {
298 window.videojs.get = this.vjsget;
299 }
300 });
301
302 test('should create', function() {
303 ok(manifestController);
304 });
305
306 test('should return a parsed object', function() {
307 var data = manifestController.parseManifest(window.brightcove_playlist_data);
308
309 ok(data);
310 equal(data.playlistItems.length, 4, 'Has correct rendition count');
311 equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct');
312 equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct');
313 equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct');
314 equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct');
315 });
316
317 test('should get a manifest from hermes', function() {
318 manifestController.loadManifest('http://example.com/16x9-master.m3u8',
319 function(responseData) {
320 ok(responseData);
321 },
322 function() {
323 ok(false, 'does not error');
324 },
325 function() {});
326 });
327
328 module('segment controller', {
329 setup: function() {
330 segmentController = new window.videojs.hls.SegmentController();
331 this.vjsget = window.videojs.get;
332 window.videojs.get = function(url, success) {
333 success(window.bcSegment);
334 };
335 },
336 teardown: function() {
337 window.videojs.get = this.vjsget;
338 }
339 });
340
341 test('bandwidth calulation test', function() {
342 var
343 multiSecondData = segmentController.calculateThroughput(10000, 1000, 2000),
344 subSecondData = segmentController.calculateThroughput(10000, 1000, 1500);
345 equal(multiSecondData, 80000, 'MULTI-Second bits per second calculation');
346 equal(subSecondData, 160000, 'SUB-Second bits per second calculation');
347 });
348 })(this);
...@@ -8,10 +8,11 @@ ...@@ -8,10 +8,11 @@
8 <script src="../libs/qunit/qunit.js"></script> 8 <script src="../libs/qunit/qunit.js"></script>
9 9
10 <!-- video.js --> 10 <!-- video.js -->
11 <script src="../node_modules/video.js/video.dev.js"></script> 11 <script src="../node_modules/video.js/dist/video-js/video.js"></script>
12 <script src="../node_modules/videojs-contrib-media-sources/videojs-media-sources.js"></script>
12 13
13 <!-- HLS plugin --> 14 <!-- HLS plugin -->
14 <script src="../src/video-js-hls.js"></script> 15 <script src="../src/videojs-hls.js"></script>
15 <script src="../src/flv-tag.js"></script> 16 <script src="../src/flv-tag.js"></script>
16 <script src="../src/exp-golomb.js"></script> 17 <script src="../src/exp-golomb.js"></script>
17 <script src="../src/h264-stream.js"></script> 18 <script src="../src/h264-stream.js"></script>
...@@ -19,23 +20,30 @@ ...@@ -19,23 +20,30 @@
19 <script src="../src/segment-parser.js"></script> 20 <script src="../src/segment-parser.js"></script>
20 21
21 <!-- M3U8 --> 22 <!-- M3U8 -->
22 <script src="../src/m3u8/m3u8.js"></script> 23 <script src="../src/stream.js"></script>
23 <script src="../src/m3u8/m3u8-tag-types.js"></script>
24 <script src="../src/m3u8/m3u8-parser.js"></script> 24 <script src="../src/m3u8/m3u8-parser.js"></script>
25 <script src="../src/manifest-controller.js"></script>
26 <!-- M3U8 TEST DATA --> 25 <!-- M3U8 TEST DATA -->
27 <script src="manifest/playlistM3U8data.js"></script> 26 <script src="../tmp/manifests.js"></script>
28 <script src="manifest/brightcove_playlist_m3u8.js"></script> 27 <script src="../tmp/expected.js"></script>
28
29 <!-- M3U8 --> 29 <!-- M3U8 -->
30 <!-- SEGMENT --> 30 <!-- SEGMENT -->
31 <script src="tsSegment-bc.js"></script> 31 <script src="tsSegment-bc.js"></script>
32 <script src="../src/segment-controller.js"></script>
33
34 <script src="../src/bin-utils.js"></script> 32 <script src="../src/bin-utils.js"></script>
35 33
36 <script src="video-js-hls_test.js"></script> 34 <!-- Test cases -->
35 <script>
36 module('environment');
37 test('is sane', function() {
38 expect(1);
39 ok(true);
40 });
41 </script>
42 <script src="videojs-hls_test.js"></script>
43 <script src="segment-parser.js"></script>
37 <script src="exp-golomb_test.js"></script> 44 <script src="exp-golomb_test.js"></script>
38 <script src="flv-tag_test.js"></script> 45 <script src="flv-tag_test.js"></script>
46 <script src="m3u8_test.js"></script>
39 </head> 47 </head>
40 <body> 48 <body>
41 <div id="qunit"></div> 49 <div id="qunit"></div>
......
1 (function(window, videojs, undefined) {
2 /*
3 ======== A Handy Little QUnit Reference ========
4 http://api.qunitjs.com/
5
6 Test methods:
7 module(name, {[setup][ ,teardown]})
8 test(name, callback)
9 expect(numberOfAssertions)
10 stop(increment)
11 start(decrement)
12 Test assertions:
13 ok(value, [message])
14 equal(actual, expected, [message])
15 notEqual(actual, expected, [message])
16 deepEqual(actual, expected, [message])
17 notDeepEqual(actual, expected, [message])
18 strictEqual(actual, expected, [message])
19 notStrictEqual(actual, expected, [message])
20 throws(block, [expected], [message])
21 */
22
23 var
24 player,
25 oldFlashSupported,
26 oldXhr,
27 oldSourceBuffer,
28 oldSupportsNativeHls,
29 xhrUrls;
30
31 module('HLS', {
32 setup: function() {
33
34 // mock out Flash feature for phantomjs
35 oldFlashSupported = videojs.Flash.isSupported;
36 videojs.Flash.isSupported = function() {
37 return true;
38 };
39 oldSourceBuffer = window.videojs.SourceBuffer;
40 window.videojs.SourceBuffer = function() {
41 this.appendBuffer = function() {};
42 };
43
44 // force native HLS to be ignored
45 oldSupportsNativeHls = videojs.hls.supportsNativeHls;
46 videojs.hls.supportsNativeHls = false;
47
48 // create the test player
49 var video = document.createElement('video');
50 document.querySelector('#qunit-fixture').appendChild(video);
51 player = videojs(video, {
52 flash: {
53 swf: '../node_modules/video.js/dist/video-js/video-js.swf'
54 },
55 techOrder: ['flash']
56 });
57 player.buffered = function() {
58 return videojs.createTimeRange(0, 0);
59 };
60
61 // make XHR synchronous
62 oldXhr = window.XMLHttpRequest;
63 window.XMLHttpRequest = function() {
64 this.open = function(method, url) {
65 xhrUrls.push(url);
66 };
67 this.send = function() {
68 // if the request URL looks like one of the test manifests, grab the
69 // contents off the global object
70 var manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]);
71 if (manifestName) {
72 manifestName = manifestName[1];
73 }
74 this.responseText = window.manifests[manifestName || xhrUrls.slice(-1)[0]];
75 this.response = new Uint8Array([1]).buffer;
76
77 this.readyState = 4;
78 this.onreadystatechange();
79 };
80 };
81 xhrUrls = [];
82 },
83 teardown: function() {
84 videojs.Flash.isSupported = oldFlashSupported;
85 videojs.hls.supportsNativeHls = oldSupportsNativeHls;
86 window.videojs.SourceBuffer = oldSourceBuffer;
87 window.XMLHttpRequest = oldXhr;
88 }
89 });
90
91 test('loads the specified manifest URL on init', function() {
92 var loadedmanifest = false, loadedmetadata = false;
93 player.on('loadedmanifest', function() {
94 loadedmanifest = true;
95 });
96 player.on('loadedmetadata', function() {
97 loadedmetadata = true;
98 });
99
100 player.hls('manifest/playlist.m3u8');
101 strictEqual(player.hls.readyState(), 0, 'the readyState is HAVE_NOTHING');
102 videojs.mediaSources[player.currentSrc()].trigger({
103 type: 'sourceopen'
104 });
105 ok(loadedmanifest, 'loadedmanifest fires');
106 ok(loadedmetadata, 'loadedmetadata fires');
107 ok(player.hls.master, 'a master is inferred');
108 ok(player.hls.media, 'the manifest is available');
109 ok(player.hls.media.segments, 'the segment entries are parsed');
110 strictEqual(player.hls.master.playlists[0],
111 player.hls.media,
112 'the playlist is selected');
113 strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA');
114 });
115
116 test('sets the duration if one is available on the playlist', function() {
117 var calls = 0;
118 player.duration = function(value) {
119 if (value === undefined) {
120 return 0;
121 }
122 calls++;
123 };
124 player.hls('manifest/media.m3u8');
125 videojs.mediaSources[player.currentSrc()].trigger({
126 type: 'sourceopen'
127 });
128
129 strictEqual(1, calls, 'duration is set');
130 });
131
132 test('calculates the duration if needed', function() {
133 var durations = [];
134 player.duration = function(duration) {
135 if (duration === undefined) {
136 return 0;
137 }
138 durations.push(duration);
139 };
140 player.hls('manifest/liveMissingSegmentDuration.m3u8');
141 videojs.mediaSources[player.currentSrc()].trigger({
142 type: 'sourceopen'
143 });
144
145 strictEqual(durations.length, 1, 'duration is set');
146 strictEqual(durations[0], 6.64 + (2 * 8), 'duration is calculated');
147 });
148
149 test('starts downloading a segment on loadedmetadata', function() {
150 player.hls('manifest/media.m3u8');
151 player.buffered = function() {
152 return videojs.createTimeRange(0, 0);
153 };
154 videojs.mediaSources[player.currentSrc()].trigger({
155 type: 'sourceopen'
156 });
157
158 strictEqual(xhrUrls[1],
159 window.location.origin +
160 window.location.pathname.split('/').slice(0, -1).join('/') +
161 '/manifest/00001.ts',
162 'the first segment is requested');
163 });
164
165 test('recognizes absolute URIs and requests them unmodified', function() {
166 player.hls('manifest/absoluteUris.m3u8');
167 videojs.mediaSources[player.currentSrc()].trigger({
168 type: 'sourceopen'
169 });
170
171 strictEqual(xhrUrls[1],
172 'http://example.com/00001.ts',
173 'the first segment is requested');
174 });
175
176 test('recognizes domain-relative URLs', function() {
177 player.hls('manifest/domainUris.m3u8');
178 videojs.mediaSources[player.currentSrc()].trigger({
179 type: 'sourceopen'
180 });
181
182 strictEqual(xhrUrls[1],
183 window.location.origin + '/00001.ts',
184 'the first segment is requested');
185 });
186
187 test('re-initializes the plugin for each source', function() {
188 var firstInit, secondInit;
189 player.hls('manifest/master.m3u8');
190 firstInit = player.hls;
191 player.hls('manifest/master.m3u8');
192 secondInit = player.hls;
193
194 notStrictEqual(firstInit, secondInit, 'the plugin object is replaced');
195 });
196
197 test('triggers an error when a master playlist request errors', function() {
198 var
199 status = 0,
200 error;
201 window.XMLHttpRequest = function() {
202 this.open = function() {};
203 this.send = function() {
204 this.readyState = 4;
205 this.status = status;
206 this.onreadystatechange();
207 };
208 };
209
210 player.on('error', function() {
211 error = player.hls.error;
212 });
213 player.hls('manifest/master.m3u8');
214 videojs.mediaSources[player.currentSrc()].trigger({
215 type: 'sourceopen'
216 });
217
218 ok(error, 'an error is triggered');
219 strictEqual(2, error.code, 'a network error is triggered');
220 });
221
222 test('downloads media playlists after loading the master', function() {
223 player.hls('manifest/master.m3u8');
224 videojs.mediaSources[player.currentSrc()].trigger({
225 type: 'sourceopen'
226 });
227
228 strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested');
229 strictEqual(xhrUrls[1],
230 window.location.origin +
231 window.location.pathname.split('/').slice(0, -1).join('/') +
232 '/manifest/media.m3u8',
233 'media playlist requested');
234 strictEqual(xhrUrls[2],
235 window.location.origin +
236 window.location.pathname.split('/').slice(0, -1).join('/') +
237 '/manifest/00001.ts',
238 'first segment requested');
239 });
240
241 test('timeupdates do not check to fill the buffer until a media playlist is ready', function() {
242 var urls = [];
243 window.XMLHttpRequest = function() {
244 this.open = function(method, url) {
245 urls.push(url);
246 };
247 this.send = function() {};
248 };
249 player.hls('manifest/media.m3u8');
250 videojs.mediaSources[player.currentSrc()].trigger({
251 type: 'sourceopen'
252 });
253 player.trigger('timeupdate');
254
255 strictEqual(1, urls.length, 'one request was made');
256 strictEqual('manifest/media.m3u8', urls[0], 'media playlist requested');
257 });
258
259 test('calculates the bandwidth after downloading a segment', function() {
260 player.hls('manifest/media.m3u8');
261 videojs.mediaSources[player.currentSrc()].trigger({
262 type: 'sourceopen'
263 });
264
265 ok(player.hls.bandwidth, 'bandwidth is calculated');
266 ok(player.hls.bandwidth > 0,
267 'bandwidth is positive: ' + player.hls.bandwidth);
268 ok(player.hls.segmentXhrTime >= 0,
269 'saves segment request time: ' + player.hls.segmentXhrTime + 's');
270 });
271
272 test('selects a playlist after segment downloads', function() {
273 var calls = 0;
274 player.hls('manifest/master.m3u8');
275 player.hls.selectPlaylist = function() {
276 calls++;
277 return player.hls.master.playlists[0];
278 };
279 videojs.mediaSources[player.currentSrc()].trigger({
280 type: 'sourceopen'
281 });
282
283 strictEqual(calls, 1, 'selects after the initial segment');
284 player.currentTime = function() {
285 return 1;
286 };
287 player.buffered = function() {
288 return videojs.createTimeRange(0, 2);
289 };
290 player.trigger('timeupdate');
291 strictEqual(calls, 2, 'selects after additional segments');
292 });
293
294 test('moves to the next segment if there is a network error', function() {
295 var mediaIndex;
296 player.hls('manifest/master.m3u8');
297 videojs.mediaSources[player.currentSrc()].trigger({
298 type: 'sourceopen'
299 });
300
301 // fail the next segment request
302 window.XMLHttpRequest = function() {
303 this.open = function() {};
304 this.send = function() {
305 this.readyState = 4;
306 this.status = 400;
307 this.onreadystatechange();
308 };
309 };
310 mediaIndex = player.hls.mediaIndex;
311 player.trigger('timeupdate');
312
313 strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented');
314 });
315
316 test('updates the duration after switching playlists', function() {
317 var
318 calls = 0,
319 selectedPlaylist = false;
320 player.hls('manifest/master.m3u8');
321 player.hls.selectPlaylist = function() {
322 selectedPlaylist = true;
323 return player.hls.master.playlists[1];
324 };
325 player.duration = function(duration) {
326 if (duration === undefined) {
327 return 0;
328 }
329 // only track calls that occur after the playlist has been switched
330 if (player.hls.media === player.hls.master.playlists[1]) {
331 calls++;
332 }
333 };
334 videojs.mediaSources[player.currentSrc()].trigger({
335 type: 'sourceopen'
336 });
337
338 ok(selectedPlaylist, 'selected playlist');
339 strictEqual(calls, 1, 'updates the duration');
340 });
341
342 test('downloads additional playlists if required', function() {
343 var
344 called = false,
345 playlist = {
346 uri: 'media3.m3u8'
347 };
348 player.hls('manifest/master.m3u8');
349 videojs.mediaSources[player.currentSrc()].trigger({
350 type: 'sourceopen'
351 });
352
353 // before an m3u8 is downloaded, no segments are available
354 player.hls.selectPlaylist = function() {
355 if (!called) {
356 called = true;
357 return playlist;
358 }
359 playlist.segments = [];
360 return playlist;
361 };
362 xhrUrls = [];
363
364 // the playlist selection is revisited after a new segment is downloaded
365 player.currentTime = function() {
366 return 1;
367 };
368 player.trigger('timeupdate');
369
370 strictEqual(2, xhrUrls.length, 'requests were made');
371 strictEqual(xhrUrls[1],
372 window.location.origin +
373 window.location.pathname.split('/').slice(0, -1).join('/') +
374 '/manifest/' +
375 playlist.uri,
376 'made playlist request');
377 strictEqual(playlist, player.hls.media, 'a new playlists was selected');
378 ok(player.hls.media.segments, 'segments are now available');
379 });
380
381 test('selects a playlist below the current bandwidth', function() {
382 var playlist;
383 player.hls('manifest/master.m3u8');
384 videojs.mediaSources[player.currentSrc()].trigger({
385 type: 'sourceopen'
386 });
387
388 // the default playlist has a really high bitrate
389 player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10;
390 // playlist 1 has a very low bitrate
391 player.hls.master.playlists[1].attributes.BANDWIDTH = 1;
392 // but the detected client bandwidth is really low
393 player.hls.bandwidth = 10;
394
395 playlist = player.hls.selectPlaylist();
396 strictEqual(playlist,
397 player.hls.master.playlists[1],
398 'the low bitrate stream is selected');
399 });
400
401 test('raises the minimum bitrate for a stream proportionially', function() {
402 var playlist;
403 player.hls('manifest/master.m3u8');
404 videojs.mediaSources[player.currentSrc()].trigger({
405 type: 'sourceopen'
406 });
407
408 // the default playlist's bandwidth + 10% is equal to the current bandwidth
409 player.hls.master.playlists[0].attributes.BANDWIDTH = 10;
410 player.hls.bandwidth = 11;
411
412 // 9.9 * 1.1 < 11
413 player.hls.master.playlists[1].attributes.BANDWIDTH = 9.9;
414 playlist = player.hls.selectPlaylist();
415
416 strictEqual(playlist,
417 player.hls.master.playlists[1],
418 'a lower bitrate stream is selected');
419 });
420
421 test('uses the lowest bitrate if no other is suitable', function() {
422 var playlist;
423 player.hls('manifest/master.m3u8');
424 videojs.mediaSources[player.currentSrc()].trigger({
425 type: 'sourceopen'
426 });
427
428 // the lowest bitrate playlist is much greater than 1b/s
429 player.hls.bandwidth = 1;
430 playlist = player.hls.selectPlaylist();
431
432 // playlist 1 has the lowest advertised bitrate
433 strictEqual(playlist,
434 player.hls.master.playlists[1],
435 'the lowest bitrate stream is selected');
436 });
437
438 test('selects the correct rendition by player dimensions', function() {
439 var playlist;
440
441 player.hls('manifest/master.m3u8');
442
443 videojs.mediaSources[player.currentSrc()].trigger({
444 type: 'sourceopen'
445 });
446
447 player.width(640);
448 player.height(360);
449 player.hls.bandwidth = 3000000;
450
451 playlist = player.hls.selectPlaylist();
452
453 deepEqual(playlist.attributes.RESOLUTION, {width:396,height:224},'should return the correct resolution by player dimensions');
454 equal(playlist.attributes.BANDWIDTH, 440000, 'should have the expected bandwidth in case of multiple');
455
456 player.width(1920);
457 player.height(1080);
458 player.hls.bandwidth = 3000000;
459
460 playlist = player.hls.selectPlaylist();
461
462 deepEqual(playlist.attributes.RESOLUTION, {width:960,height:540},'should return the correct resolution by player dimensions');
463 equal(playlist.attributes.BANDWIDTH, 1928000, 'should have the expected bandwidth in case of multiple');
464
465 });
466
467
468 test('does not download the next segment if the buffer is full', function() {
469 player.hls('manifest/media.m3u8');
470 player.currentTime = function() {
471 return 15;
472 };
473 player.buffered = function() {
474 return videojs.createTimeRange(0, 20);
475 };
476 videojs.mediaSources[player.currentSrc()].trigger({
477 type: 'sourceopen'
478 });
479 player.trigger('timeupdate');
480
481 strictEqual(xhrUrls.length, 1, 'no segment request was made');
482 });
483
484 test('downloads the next segment if the buffer is getting low', function() {
485 player.hls('manifest/media.m3u8');
486 videojs.mediaSources[player.currentSrc()].trigger({
487 type: 'sourceopen'
488 });
489 strictEqual(xhrUrls.length, 2, 'did not make a request');
490 player.currentTime = function() {
491 return 15;
492 };
493 player.buffered = function() {
494 return videojs.createTimeRange(0, 19.999);
495 };
496 player.trigger('timeupdate');
497
498 strictEqual(xhrUrls.length, 3, 'made a request');
499 strictEqual(xhrUrls[2],
500 window.location.origin +
501 window.location.pathname.split('/').slice(0, -1).join('/') +
502 '/manifest/00002.ts',
503 'made segment request');
504 });
505
506 test('stops downloading segments at the end of the playlist', function() {
507 player.hls('manifest/media.m3u8');
508 videojs.mediaSources[player.currentSrc()].trigger({
509 type: 'sourceopen'
510 });
511 xhrUrls = [];
512 player.hls.mediaIndex = 4;
513 player.trigger('timeupdate');
514
515 strictEqual(xhrUrls.length, 0, 'no request is made');
516 });
517
518 test('only makes one segment request at a time', function() {
519 var openedXhrs = 0;
520 player.hls('manifest/media.m3u8');
521 videojs.mediaSources[player.currentSrc()].trigger({
522 type: 'sourceopen'
523 });
524 // mock out a long-running XHR
525 window.XMLHttpRequest = function() {
526 this.send = function() {};
527 this.open = function() {
528 openedXhrs++;
529 };
530 };
531 player.trigger('timeupdate');
532
533 strictEqual(1, openedXhrs, 'one XHR is made');
534 player.trigger('timeupdate');
535 strictEqual(1, openedXhrs, 'only one XHR is made');
536 });
537
538 test('uses the src attribute if no options are provided and it ends in ".m3u8"', function() {
539 var url = 'http://example.com/services/mobile/streaming/index/master.m3u8?videoId=1824650741001';
540 player.el().querySelector('.vjs-tech').src = url;
541 player.hls();
542 videojs.mediaSources[player.currentSrc()].trigger({
543 type: 'sourceopen'
544 });
545
546 strictEqual(url, xhrUrls[0], 'currentSrc is used');
547 });
548
549 test('ignores src attribute if it doesn\'t have the "m3u8" extension', function() {
550 var tech = player.el().querySelector('.vjs-tech');
551 tech.src = 'basdfasdfasdfliel//.m3u9';
552 player.hls();
553 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
554 strictEqual(xhrUrls.length, 0, 'no request is made');
555
556 tech.src = '';
557 player.hls();
558 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
559 strictEqual(xhrUrls.length, 0, 'no request is made');
560
561 tech.src = 'http://example.com/movie.mp4?q=why.m3u8';
562 player.hls();
563 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
564 strictEqual(xhrUrls.length, 0, 'no request is made');
565
566 tech.src = 'http://example.m3u8/movie.mp4';
567 player.hls();
568 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
569 strictEqual(xhrUrls.length, 0, 'no request is made');
570
571 tech.src = '//example.com/movie.mp4#http://tricky.com/master.m3u8';
572 player.hls();
573 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
574 strictEqual(xhrUrls.length, 0, 'no request is made');
575 });
576
577 test('activates if the first playable source is HLS', function() {
578 var video;
579 document.querySelector('#qunit-fixture').innerHTML =
580 '<video controls>' +
581 '<source type="slartibartfast$%" src="movie.slarti">' +
582 '<source type="application/x-mpegURL" src="movie.m3u8">' +
583 '<source type="video/mp4" src="movie.mp4">' +
584 '</video>';
585 video = document.querySelector('#qunit-fixture video');
586 player = videojs(video, {
587 flash: {
588 swf: '../node_modules/video.js/dist/video-js/video-js.swf'
589 },
590 techOrder: ['flash']
591 });
592 player.hls();
593
594 ok(player.currentSrc() in videojs.mediaSources, 'media source created');
595 });
596
597 test('cancels outstanding XHRs when seeking', function() {
598 var
599 aborted = false,
600 opened = 0;
601 player.hls('manifest/media.m3u8');
602 videojs.mediaSources[player.currentSrc()].trigger({
603 type: 'sourceopen'
604 });
605 player.hls.media = {
606 segments: [{
607 uri: '0.ts',
608 duration: 10
609 }, {
610 uri: '1.ts',
611 duration: 10
612 }]
613 };
614
615 // XHR requests will never complete
616 window.XMLHttpRequest = function() {
617 this.open = function() {
618 opened++;
619 };
620 this.send = function() {};
621 this.abort = function() {
622 aborted = true;
623 this.readyState = 4;
624 this.status = 0;
625 this.onreadystatechange();
626 };
627 };
628 // trigger a segment download request
629 player.trigger('timeupdate');
630 opened = 0;
631 // attempt to seek while the download is in progress
632 player.trigger('seeking');
633
634 ok(aborted, 'XHR aborted');
635 strictEqual(1, opened, 'opened new XHR');
636 });
637
638 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
639 var errorTriggered = false;
640
641 window.XMLHttpRequest = function() {
642 this.open = function(method, url) {
643 xhrUrls.push(url);
644 };
645 this.send = function() {
646 this.readyState = 4;
647 this.status = 404;
648 this.onreadystatechange();
649 };
650 };
651
652 player.hls('manifest/media.m3u8');
653
654 player.on('error', function() {
655 errorTriggered = true;
656 });
657
658 videojs.mediaSources[player.currentSrc()].trigger({
659 type: 'sourceopen'
660 });
661
662 equal(true, errorTriggered, 'Missing Playlist error event should trigger');
663 equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
664 ok(player.hls.error.message, 'Player error type should inform user correctly');
665 });
666
667 test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
668 player.hls('manifest/media.m3u8');
669
670 player.on('loadedmanifest', function () {
671 window.XMLHttpRequest = function () {
672 this.open = function (method, url) {
673 xhrUrls.push(url);
674 };
675 this.send = function () {
676 this.readyState = 4;
677 this.status = 404;
678 this.onreadystatechange();
679 };
680 };
681 });
682
683 videojs.mediaSources[player.currentSrc()].trigger({
684 type: 'sourceopen'
685 });
686
687 ok(player.hls.error.message, 'an error message is available');
688 equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
689 });
690
691 test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
692 player.hls('manifest/media.m3u8');
693
694 player.on('loadedmanifest', function () {
695 window.XMLHttpRequest = function () {
696 this.open = function (method, url) {
697 xhrUrls.push(url);
698 };
699 this.send = function () {
700 this.readyState = 4;
701 this.status = 500;
702 this.onreadystatechange();
703 };
704 };
705 });
706
707 videojs.mediaSources[player.currentSrc()].trigger({
708 type: 'sourceopen'
709 });
710
711 ok(player.hls.error.message, 'an error message is available');
712 equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
713 });
714
715 test('has no effect if native HLS is available', function() {
716 videojs.hls.supportsNativeHls = true;
717 player.hls('manifest/master.m3u8');
718
719 ok(!(player.currentSrc() in videojs.mediaSources),
720 'no media source was opened');
721 });
722
723 })(window, window.videojs);