Merge pull request #7 from brightcove/feature/basic-playback
Single-bitrate playback
Showing
103 changed files
with
5291 additions
and
1022 deletions
.travis.yml
0 → 100644
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 | }; | ... | ... |
src/hls-playback-controller.js
deleted
100644 → 0
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); | ... | ... |
src/m3u8/m3u8-tag-types.js
deleted
100644 → 0
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); |
src/m3u8/m3u8.js
deleted
100644 → 0
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); |
src/manifest-controller.js
deleted
100644 → 0
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); |
src/segment-controller.js
deleted
100644 → 0
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); | ... | ... |
src/stream.js
0 → 100644
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); |
src/video-js-hls.js
deleted
100644 → 0
src/videojs-hls.js
0 → 100644
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 | ... | ... |
test/m3u8_test.js
0 → 100644
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); |
test/manifest/absoluteUris.json
0 → 100644
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 |
test/manifest/absoluteUris.m3u8
0 → 100644
test/manifest/allowCache.json
0 → 100644
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 |
test/manifest/allowCache.m3u8
0 → 100644
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 |
test/manifest/allowCacheInvalid.json
0 → 100644
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 |
test/manifest/allowCacheInvalid.m3u8
0 → 100644
test/manifest/brightcove.json
0 → 100644
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 | } |
test/manifest/brightcove.m3u8
0 → 100644
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'; |
test/manifest/byteRange.json
0 → 100644
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 |
test/manifest/byteRange.m3u8
0 → 100644
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 |
test/manifest/disallowCache.json
0 → 100644
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 |
test/manifest/disallowCache.m3u8
0 → 100644
test/manifest/domainUris.json
0 → 100644
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 |
test/manifest/domainUris.m3u8
0 → 100644
test/manifest/emptyAllowCache.json
0 → 100644
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 |
test/manifest/emptyAllowCache.m3u8
0 → 100644
test/manifest/emptyMediaSequence.json
0 → 100644
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 |
test/manifest/emptyMediaSequence.m3u8
0 → 100644
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 |
test/manifest/emptyPlaylistType.json
0 → 100644
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 |
test/manifest/emptyPlaylistType.m3u8
0 → 100644
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 |
test/manifest/emptyTargetDuration.json
0 → 100644
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 | } |
test/manifest/emptyTargetDuration.m3u8
0 → 100644
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 |
test/manifest/event.json
0 → 100644
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 |
test/manifest/event.m3u8
0 → 100644
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 |
test/manifest/extinf.json
0 → 100644
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 | } |
test/manifest/extinf.m3u8
0 → 100644
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 |
test/manifest/invalidAllowCache.json
0 → 100644
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 |
test/manifest/invalidAllowCache.m3u8
0 → 100644
test/manifest/invalidMediaSequence.json
0 → 100644
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 |
test/manifest/invalidMediaSequence.m3u8
0 → 100644
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 |
test/manifest/invalidPlaylistType.json
0 → 100644
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 |
test/manifest/invalidPlaylistType.m3u8
0 → 100644
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 |
test/manifest/invalidTargetDuration.json
0 → 100644
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 |
test/manifest/invalidTargetDuration.m3u8
0 → 100644
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 |
test/manifest/manifestExtXEndlistEarly.json
0 → 100644
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 |
test/manifest/manifestExtXEndlistEarly.m3u8
0 → 100644
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 |
test/manifest/manifestNoExtM3u.json
0 → 100644
test/manifest/manifestNoExtM3u.m3u8
0 → 100644
test/manifest/master.json
0 → 100644
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 | } |
test/manifest/master.m3u8
0 → 100644
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 |
test/manifest/media.json
0 → 100644
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 |
test/manifest/media.m3u8
0 → 100644
test/manifest/media1.m3u8
0 → 100644
test/manifest/media3.m3u8
0 → 100644
test/manifest/mediaSequence.json
0 → 100644
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 |
test/manifest/mediaSequence.m3u8
0 → 100644
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 |
test/manifest/missingExtinf.json
0 → 100644
test/manifest/missingExtinf.m3u8
0 → 100644
test/manifest/missingMediaSequence.json
0 → 100644
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 |
test/manifest/missingMediaSequence.m3u8
0 → 100644
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 |
test/manifest/missingSegmentDuration.json
0 → 100644
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 | } |
test/manifest/missingSegmentDuration.m3u8
0 → 100644
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 |
test/manifest/multipleTargetDurations.json
0 → 100644
test/manifest/multipleTargetDurations.m3u8
0 → 100644
test/manifest/negativeMediaSequence.json
0 → 100644
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 |
test/manifest/negativeMediaSequence.m3u8
0 → 100644
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 |
test/manifest/playlist.json
0 → 100644
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 |
test/manifest/playlistM3U8data.js
deleted
100644 → 0
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 | #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 |
test/manifest/playlist_extinf_template.m3u8
0 → 100644
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 |
test/manifest/playlist_type_template.m3u8
0 → 100644
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 |
test/manifest/remove-trs.js
0 → 100644
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 | }); |
test/manifest/streamInfInvalid.json
0 → 100644
test/manifest/streamInfInvalid.m3u8
0 → 100644
test/manifest/twoMediaSequences.json
0 → 100644
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 |
test/manifest/twoMediaSequences.m3u8
0 → 100644
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 |
test/manifest/versionInvalid.json
0 → 100644
test/manifest/versionInvalid.m3u8
0 → 100644
test/segment-parser.js
0 → 100644
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); |
test/video-js-hls_test.js
deleted
100644 → 0
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> | ... | ... |
test/videojs-hls_test.js
0 → 100644
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); |
-
Please register or sign in to post a comment