Merge branch 'development'
Conflicts: .travis.yml package.json src/playlist.js src/videojs-hls.js test/videojs-hls_test.js
Showing
181 changed files
with
4200 additions
and
4270 deletions
.editorconfig
0 → 100644
1 | # OS | ||
2 | Thumbs.db | ||
3 | ehthumbs.db | ||
4 | Desktop.ini | ||
1 | .DS_Store | 5 | .DS_Store |
2 | dist/* | 6 | ._* |
3 | /node_modules/ | 7 | |
8 | # Editors | ||
4 | *~ | 9 | *~ |
5 | *.iml | ||
6 | *.ipr | ||
7 | *.iws | ||
8 | *.swp | 10 | *.swp |
9 | tmp/**.*.swo | 11 | *.tmproj |
12 | *.tmproject | ||
13 | *.sublime-* | ||
14 | .idea/ | ||
15 | .project/ | ||
16 | .settings/ | ||
17 | .vscode/ | ||
18 | |||
19 | # Logs | ||
20 | logs | ||
21 | *.log | ||
22 | npm-debug.log* | ||
23 | |||
24 | # Dependency directories | ||
25 | bower_components/ | ||
26 | node_modules/ | ||
27 | |||
28 | # Yeoman meta-data | ||
29 | .yo-rc.json | ||
30 | |||
31 | # Build-related directories | ||
32 | dist/ | ||
33 | dist-test/ | ||
34 | docs/api/ | ||
35 | es5/ | ||
36 | tmp | ||
37 | test/test-manifests.js | ||
38 | test/test-expected.js | ... | ... |
.jshintrc
deleted
100644 → 0
1 | { | ||
2 | "curly": true, | ||
3 | "eqeqeq": true, | ||
4 | "immed": true, | ||
5 | "latedef": true, | ||
6 | "newcap": true, | ||
7 | "noarg": true, | ||
8 | "sub": true, | ||
9 | "undef": true, | ||
10 | "unused": true, | ||
11 | "boss": true, | ||
12 | "eqnull": true, | ||
13 | "node": true, | ||
14 | |||
15 | "camelcase": true, | ||
16 | "nonew": true, | ||
17 | "quotmark": "single", | ||
18 | "trailing": true, | ||
19 | "maxlen": 80 | ||
20 | } |
1 | language: node_js | ||
2 | sudo: false | 1 | sudo: false |
2 | language: node_js | ||
3 | addons: | ||
4 | firefox: "latest" | ||
3 | node_js: | 5 | node_js: |
4 | - "stable" | 6 | - "stable" |
5 | install: | ||
6 | - npm install -g grunt-cli && npm install | ||
7 | notifications: | 7 | notifications: |
8 | hipchat: | 8 | hipchat: |
9 | rooms: | 9 | rooms: |
... | @@ -12,6 +12,7 @@ notifications: | ... | @@ -12,6 +12,7 @@ notifications: |
12 | channels: | 12 | channels: |
13 | - "chat.freenode.net#videojs" | 13 | - "chat.freenode.net#videojs" |
14 | use_notice: true | 14 | use_notice: true |
15 | # Set up a virtual screen for Firefox. | ||
15 | before_script: | 16 | before_script: |
16 | - export DISPLAY=:99.0 | 17 | - export DISPLAY=:99.0 |
17 | - sh -e /etc/init.d/xvfb start | 18 | - sh -e /etc/init.d/xvfb start | ... | ... |
Gruntfile.js
deleted
100644 → 0
1 | 'use strict'; | ||
2 | |||
3 | var | ||
4 | basename = require('path').basename, | ||
5 | mediaSourcesPath = 'node_modules/videojs-contrib-media-sources/dist/', | ||
6 | mediaSourcesDebug = mediaSourcesPath + 'videojs-media-sources.js'; | ||
7 | |||
8 | module.exports = function(grunt) { | ||
9 | var pkg = grunt.file.readJSON('package.json'); | ||
10 | |||
11 | // Project configuration. | ||
12 | grunt.initConfig({ | ||
13 | // Metadata. | ||
14 | pkg: pkg, | ||
15 | banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' + | ||
16 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + | ||
17 | '* Copyright (c) <%= grunt.template.today("yyyy") %> Brightcove;' + | ||
18 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', | ||
19 | // Task configuration. | ||
20 | clean: { | ||
21 | files: ['build', 'dist', 'tmp'] | ||
22 | }, | ||
23 | concat: { | ||
24 | options: { | ||
25 | banner: '<%= banner %>', | ||
26 | stripBanners: true | ||
27 | }, | ||
28 | dist: { | ||
29 | nonull: true, | ||
30 | src: [ | ||
31 | mediaSourcesDebug, | ||
32 | 'src/videojs-hls.js', | ||
33 | 'src/xhr.js', | ||
34 | 'src/stream.js', | ||
35 | 'src/m3u8/m3u8-parser.js', | ||
36 | 'src/playlist.js', | ||
37 | 'src/playlist-loader.js', | ||
38 | 'node_modules/pkcs7/dist/pkcs7.unpad.js', | ||
39 | 'src/decrypter.js' | ||
40 | ], | ||
41 | dest: 'dist/videojs.hls.js' | ||
42 | } | ||
43 | }, | ||
44 | uglify: { | ||
45 | options: { | ||
46 | banner: '<%= banner %>' | ||
47 | }, | ||
48 | dist: { | ||
49 | src: '<%= concat.dist.dest %>', | ||
50 | dest: 'dist/videojs.hls.min.js' | ||
51 | } | ||
52 | }, | ||
53 | jshint: { | ||
54 | gruntfile: { | ||
55 | options: { | ||
56 | jshintrc: '.jshintrc' | ||
57 | }, | ||
58 | src: 'Gruntfile.js' | ||
59 | }, | ||
60 | src: { | ||
61 | options: { | ||
62 | jshintrc: 'src/.jshintrc' | ||
63 | }, | ||
64 | src: ['src/**/*.js'] | ||
65 | }, | ||
66 | test: { | ||
67 | options: { | ||
68 | jshintrc: 'test/.jshintrc' | ||
69 | }, | ||
70 | src: ['test/**/*.js', | ||
71 | '!test/tsSegment.js', | ||
72 | '!test/fixtures/*.js', | ||
73 | '!test/manifest/**', | ||
74 | '!test/muxer/**', | ||
75 | '!test/switcher/**'] | ||
76 | } | ||
77 | }, | ||
78 | connect: { | ||
79 | dev: { | ||
80 | options: { | ||
81 | hostname: '*', | ||
82 | port: 9999, | ||
83 | keepalive: true | ||
84 | } | ||
85 | }, | ||
86 | test: { | ||
87 | options: { | ||
88 | hostname: '*', | ||
89 | port: 9999 | ||
90 | } | ||
91 | } | ||
92 | }, | ||
93 | open : { | ||
94 | dev : { | ||
95 | path: 'http://127.0.0.1:<%= connect.dev.options.port %>/example.html', | ||
96 | app: 'Google Chrome' | ||
97 | } | ||
98 | }, | ||
99 | watch: { | ||
100 | build: { | ||
101 | files: '<%= concat.dist.src %>', | ||
102 | tasks: ['clean', 'concat', 'uglify'] | ||
103 | }, | ||
104 | gruntfile: { | ||
105 | files: '<%= jshint.gruntfile.src %>', | ||
106 | tasks: ['jshint:gruntfile'] | ||
107 | }, | ||
108 | src: { | ||
109 | files: '<%= jshint.src.src %>', | ||
110 | tasks: ['jshint:src', 'test'] | ||
111 | }, | ||
112 | test: { | ||
113 | files: '<%= jshint.test.src %>', | ||
114 | tasks: ['jshint:test', 'test'] | ||
115 | } | ||
116 | }, | ||
117 | concurrent: { | ||
118 | dev: { | ||
119 | tasks: ['connect', 'open', 'watch'], | ||
120 | options: { | ||
121 | logConcurrentOutput: true | ||
122 | } | ||
123 | } | ||
124 | }, | ||
125 | version: { | ||
126 | project: { | ||
127 | src: ['package.json'] | ||
128 | } | ||
129 | }, | ||
130 | 'github-release': { | ||
131 | options: { | ||
132 | repository: 'videojs/videojs-contrib-hls', | ||
133 | auth: { | ||
134 | user: process.env.VJS_GITHUB_USER, | ||
135 | password: process.env.VJS_GITHUB_TOKEN | ||
136 | }, | ||
137 | release: { | ||
138 | 'tag_name': 'v' + pkg.version, | ||
139 | name: pkg.version, | ||
140 | body: require('chg').find(pkg.version).changesRaw | ||
141 | } | ||
142 | }, | ||
143 | files: { | ||
144 | 'dist': ['videojs.hls.min.js'] | ||
145 | } | ||
146 | }, | ||
147 | karma: { | ||
148 | options: { | ||
149 | frameworks: ['qunit'] | ||
150 | }, | ||
151 | |||
152 | saucelabs: { | ||
153 | configFile: 'test/karma.conf.js', | ||
154 | autoWatch: true | ||
155 | }, | ||
156 | |||
157 | dev: { | ||
158 | browsers: ['Chrome', 'Safari', 'Firefox', | ||
159 | 'Opera', 'IE', 'PhantomJS', 'ChromeCanary'], | ||
160 | configFile: 'test/localkarma.conf.js', | ||
161 | autoWatch: true | ||
162 | }, | ||
163 | |||
164 | chromecanary: { | ||
165 | options: { | ||
166 | browsers: ['ChromeCanary'], | ||
167 | configFile: 'test/localkarma.conf.js', | ||
168 | autoWatch: true | ||
169 | } | ||
170 | }, | ||
171 | |||
172 | phantomjs: { | ||
173 | options: { | ||
174 | browsers: ['PhantomJS'], | ||
175 | configFile: 'test/localkarma.conf.js', | ||
176 | autoWatch: true | ||
177 | } | ||
178 | }, | ||
179 | |||
180 | opera: { | ||
181 | options: { | ||
182 | browsers: ['Opera'], | ||
183 | configFile: 'test/localkarma.conf.js', | ||
184 | autoWatch: true | ||
185 | } | ||
186 | }, | ||
187 | |||
188 | chrome: { | ||
189 | options: { | ||
190 | browsers: ['Chrome'], | ||
191 | configFile: 'test/localkarma.conf.js', | ||
192 | autoWatch: true | ||
193 | } | ||
194 | }, | ||
195 | |||
196 | safari: { | ||
197 | options: { | ||
198 | browsers: ['Safari'], | ||
199 | configFile: 'test/localkarma.conf.js', | ||
200 | autoWatch: true | ||
201 | } | ||
202 | }, | ||
203 | |||
204 | firefox: { | ||
205 | options: { | ||
206 | browsers: ['Firefox'], | ||
207 | configFile: 'test/localkarma.conf.js', | ||
208 | autoWatch: true | ||
209 | } | ||
210 | }, | ||
211 | |||
212 | ie: { | ||
213 | options: { | ||
214 | browsers: ['IE'], | ||
215 | configFile: 'test/localkarma.conf.js', | ||
216 | autoWatch: true | ||
217 | } | ||
218 | }, | ||
219 | |||
220 | ci: { | ||
221 | configFile: 'test/karma.conf.js', | ||
222 | autoWatch: false | ||
223 | } | ||
224 | }, | ||
225 | protractor: { | ||
226 | options: { | ||
227 | configFile: 'test/functional/protractor.config.js', | ||
228 | webdriverManagerUpdate: process.env.TRAVIS ? false : true | ||
229 | }, | ||
230 | |||
231 | chrome: { | ||
232 | options: { | ||
233 | args: { | ||
234 | capabilities: { | ||
235 | browserName: 'chrome' | ||
236 | } | ||
237 | } | ||
238 | } | ||
239 | }, | ||
240 | |||
241 | firefox: { | ||
242 | options: { | ||
243 | args: { | ||
244 | capabilities: { | ||
245 | browserName: 'firefox' | ||
246 | } | ||
247 | } | ||
248 | } | ||
249 | }, | ||
250 | |||
251 | safari: { | ||
252 | options: { | ||
253 | args: { | ||
254 | capabilities: { | ||
255 | browserName: 'safari' | ||
256 | } | ||
257 | } | ||
258 | } | ||
259 | }, | ||
260 | |||
261 | ie: { | ||
262 | options: { | ||
263 | args: { | ||
264 | capabilities: { | ||
265 | browserName: 'internet explorer' | ||
266 | } | ||
267 | } | ||
268 | } | ||
269 | }, | ||
270 | |||
271 | saucelabs:{} | ||
272 | } | ||
273 | }); | ||
274 | |||
275 | // These plugins provide necessary tasks. | ||
276 | grunt.loadNpmTasks('grunt-karma'); | ||
277 | grunt.loadNpmTasks('grunt-contrib-clean'); | ||
278 | grunt.loadNpmTasks('grunt-contrib-concat'); | ||
279 | grunt.loadNpmTasks('grunt-contrib-uglify'); | ||
280 | grunt.loadNpmTasks('grunt-contrib-jshint'); | ||
281 | grunt.loadNpmTasks('grunt-contrib-watch'); | ||
282 | grunt.loadNpmTasks('grunt-contrib-connect'); | ||
283 | grunt.loadNpmTasks('grunt-open'); | ||
284 | grunt.loadNpmTasks('grunt-concurrent'); | ||
285 | grunt.loadNpmTasks('grunt-contrib-watch'); | ||
286 | grunt.loadNpmTasks('grunt-github-releaser'); | ||
287 | grunt.loadNpmTasks('grunt-version'); | ||
288 | grunt.loadNpmTasks('grunt-protractor-runner'); | ||
289 | grunt.loadNpmTasks('chg'); | ||
290 | |||
291 | |||
292 | grunt.registerTask('manifests-to-js', 'Wrap the test fixtures and output' + | ||
293 | ' so they can be loaded in a browser', | ||
294 | function() { | ||
295 | var | ||
296 | jsManifests = 'window.manifests = {\n', | ||
297 | jsExpected = 'window.expected = {\n'; | ||
298 | grunt.file.recurse('test/manifest/', | ||
299 | function(abspath, root, sub, filename) { | ||
300 | if ((/\.m3u8$/).test(abspath)) { | ||
301 | |||
302 | // translate this manifest | ||
303 | jsManifests += ' \'' + basename(filename, '.m3u8') + '\': ' + | ||
304 | grunt.file.read(abspath) | ||
305 | .split(/\r\n|\n/) | ||
306 | |||
307 | // quote and concatenate | ||
308 | .map(function(line) { | ||
309 | return ' \'' + line + '\\n\' +\n'; | ||
310 | }).join('') | ||
311 | |||
312 | // strip leading spaces and the trailing '+' | ||
313 | .slice(4, -3); | ||
314 | jsManifests += ',\n'; | ||
315 | } | ||
316 | |||
317 | if ((/\.js$/).test(abspath)) { | ||
318 | |||
319 | // append the expected parse | ||
320 | jsExpected += ' "' + basename(filename, '.js') + '": ' + | ||
321 | grunt.file.read(abspath) + ',\n'; | ||
322 | } | ||
323 | }); | ||
324 | |||
325 | // clean up and close the objects | ||
326 | jsManifests = jsManifests.slice(0, -2); | ||
327 | jsManifests += '\n};\n'; | ||
328 | jsExpected = jsExpected.slice(0, -2); | ||
329 | jsExpected += '\n};\n'; | ||
330 | |||
331 | // write out the manifests | ||
332 | grunt.file.write('tmp/manifests.js', jsManifests); | ||
333 | grunt.file.write('tmp/expected.js', jsExpected); | ||
334 | }); | ||
335 | |||
336 | // Launch a Development Environment | ||
337 | grunt.registerTask('dev', 'Launching Dev Environment', 'concurrent:dev'); | ||
338 | |||
339 | grunt.registerTask('build', | ||
340 | ['clean', | ||
341 | 'concat', | ||
342 | 'uglify']); | ||
343 | |||
344 | // Default task. | ||
345 | grunt.registerTask('default', | ||
346 | ['test', | ||
347 | 'build']); | ||
348 | |||
349 | // The test task will run `karma:saucelabs` when running in travis, | ||
350 | // otherwise, it'll default to running karma in chrome. | ||
351 | // You can specify which browsers to build with by using grunt-style arguments | ||
352 | // or separating them with a comma: | ||
353 | // grunt test:chrome:firefox # grunt-style | ||
354 | // grunt test:chrome,firefox # comma-separated | ||
355 | grunt.registerTask('test', function() { | ||
356 | var tasks = this.args; | ||
357 | |||
358 | grunt.task.run(['jshint', 'manifests-to-js']); | ||
359 | |||
360 | if (process.env.TRAVIS) { | ||
361 | if (process.env.TRAVIS_PULL_REQUEST === 'false') { | ||
362 | grunt.task.run(['karma:saucelabs']); | ||
363 | grunt.task.run(['connect:test', 'protractor:saucelabs']); | ||
364 | } else { | ||
365 | grunt.task.run(['karma:firefox']); | ||
366 | } | ||
367 | } else { | ||
368 | if (tasks.length === 0) { | ||
369 | tasks.push('chrome'); | ||
370 | } | ||
371 | if (tasks.length === 1) { | ||
372 | tasks = tasks[0].split(','); | ||
373 | } | ||
374 | tasks = tasks.reduce(function(acc, el) { | ||
375 | acc.push('karma:' + el); | ||
376 | if (/chrome|firefox|safari|ie/.test(el)) { | ||
377 | acc.push('protractor:' + el); | ||
378 | } | ||
379 | return acc; | ||
380 | }, ['connect:test']); | ||
381 | |||
382 | grunt.task.run(tasks); | ||
383 | } | ||
384 | }); | ||
385 | }; |
1 | <!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
2 | <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* | ||
4 | |||
5 | - [video.js HLS Source Handler](#videojs-hls-source-handler) | ||
6 | - [Getting Started](#getting-started) | ||
7 | - [Documentation](#documentation) | ||
8 | - [Options](#options) | ||
9 | - [withCredentials](#withcredentials) | ||
10 | - [Runtime Properties](#runtime-properties) | ||
11 | - [hls.playlists.master](#hlsplaylistsmaster) | ||
12 | - [hls.playlists.media](#hlsplaylistsmedia) | ||
13 | - [hls.segmentXhrTime](#hlssegmentxhrtime) | ||
14 | - [hls.bandwidth](#hlsbandwidth) | ||
15 | - [hls.bytesReceived](#hlsbytesreceived) | ||
16 | - [hls.selectPlaylist](#hlsselectplaylist) | ||
17 | - [Events](#events) | ||
18 | - [loadedmetadata](#loadedmetadata) | ||
19 | - [loadedplaylist](#loadedplaylist) | ||
20 | - [mediachange](#mediachange) | ||
21 | - [In-Band Metadata](#in-band-metadata) | ||
22 | - [Hosting Considerations](#hosting-considerations) | ||
23 | - [Testing](#testing) | ||
24 | - [Release History](#release-history) | ||
25 | |||
26 | <!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
27 | |||
1 | # video.js HLS Source Handler | 28 | # video.js HLS Source Handler |
2 | 29 | ||
3 | Play back HLS with video.js, even where it's not natively supported. | 30 | Play back HLS with video.js, even where it's not natively supported. | ... | ... |
... | @@ -2,31 +2,8 @@ | ... | @@ -2,31 +2,8 @@ |
2 | <html> | 2 | <html> |
3 | <head> | 3 | <head> |
4 | <meta charset="utf-8"> | 4 | <meta charset="utf-8"> |
5 | <title>video.js HLS Plugin Example</title> | 5 | <title>videojs-contrib-hls Demo</title> |
6 | 6 | <link href="/node_modules/video.js/dist/video-js.css" rel="stylesheet"> | |
7 | <link href="node_modules/video.js/dist/video-js.css" rel="stylesheet"> | ||
8 | |||
9 | <!-- video.js --> | ||
10 | <script src="node_modules/video.js/dist/video.js"></script> | ||
11 | |||
12 | <!-- Media Sources plugin --> | ||
13 | <script src="node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> | ||
14 | |||
15 | <!-- HLS plugin --> | ||
16 | <script src="src/videojs-hls.js"></script> | ||
17 | |||
18 | <!-- m3u8 handling --> | ||
19 | <script src="src/xhr.js"></script> | ||
20 | <script src="src/stream.js"></script> | ||
21 | <script src="src/m3u8/m3u8-parser.js"></script> | ||
22 | <script src="src/playlist.js"></script> | ||
23 | <script src="src/playlist-loader.js"></script> | ||
24 | |||
25 | <script src="node_modules/pkcs7/dist/pkcs7.unpad.js"></script> | ||
26 | <script src="src/decrypter.js"></script> | ||
27 | |||
28 | <script src="src/bin-utils.js"></script> | ||
29 | |||
30 | <style> | 7 | <style> |
31 | body { | 8 | body { |
32 | font-family: Arial, sans-serif; | 9 | font-family: Arial, sans-serif; |
... | @@ -52,14 +29,8 @@ | ... | @@ -52,14 +29,8 @@ |
52 | <p>The video below is an <a href="https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1">HTTP Live Stream</a>. On desktop browsers other than Safari, the HLS plugin will polyfill support for the format on top of the video.js Flash tech.</p> | 29 | <p>The video below is an <a href="https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1">HTTP Live Stream</a>. On desktop browsers other than Safari, the HLS plugin will polyfill support for the format on top of the video.js Flash tech.</p> |
53 | <p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p> | 30 | <p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p> |
54 | </div> | 31 | </div> |
55 | <video id="video" | 32 | <video id="videojs-contrib-hls-player" class="video-js vjs-default-skin" controls> |
56 | class="video-js vjs-default-skin" | 33 | <source src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8" type="application/x-mpegURL"> |
57 | height="300" | ||
58 | width="600" | ||
59 | controls> | ||
60 | <source | ||
61 | src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8" | ||
62 | type="application/x-mpegURL"> | ||
63 | </video> | 34 | </video> |
64 | 35 | ||
65 | <form id=load-url> | 36 | <form id=load-url> |
... | @@ -69,12 +40,16 @@ | ... | @@ -69,12 +40,16 @@ |
69 | </label> | 40 | </label> |
70 | <button type=submit>Load</button> | 41 | <button type=submit>Load</button> |
71 | </form> | 42 | </form> |
43 | <ul> | ||
44 | <li><a href="/test/">Run unit tests in browser.</a></li> | ||
45 | <li><a href="/docs/api/">Read generated docs.</a></li> | ||
46 | </ul> | ||
72 | 47 | ||
48 | <script src="/node_modules/video.js/dist/video.js"></script> | ||
49 | <script src="/dist/videojs-contrib-hls.js"></script> | ||
73 | <script> | 50 | <script> |
74 | videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf'; | 51 | (function(window, videojs) { |
75 | // initialize the player | 52 | var player = window.player = videojs('videojs-contrib-hls-player'); |
76 | var player = videojs('video'); | ||
77 | |||
78 | // hook up the video switcher | 53 | // hook up the video switcher |
79 | var loadUrl = document.getElementById('load-url'); | 54 | var loadUrl = document.getElementById('load-url'); |
80 | var url = document.getElementById('url'); | 55 | var url = document.getElementById('url'); |
... | @@ -86,6 +61,7 @@ | ... | @@ -86,6 +61,7 @@ |
86 | }); | 61 | }); |
87 | return false; | 62 | return false; |
88 | }); | 63 | }); |
64 | }(window, window.videojs)); | ||
89 | </script> | 65 | </script> |
90 | </body> | 66 | </body> |
91 | </html> | 67 | </html> | ... | ... |
1 | { | 1 | { |
2 | "name": "videojs-contrib-hls", | 2 | "name": "videojs-contrib-hls", |
3 | "version": "1.3.9", | 3 | "version": "1.3.9", |
4 | "description": "Play back HLS with video.js, even where it's not natively supported", | ||
5 | "main": "es5/videojs-contrib-hls.js", | ||
4 | "engines": { | 6 | "engines": { |
5 | "node": ">= 0.10.12" | 7 | "node": ">= 0.10.12" |
6 | }, | 8 | }, |
... | @@ -8,47 +10,121 @@ | ... | @@ -8,47 +10,121 @@ |
8 | "type": "git", | 10 | "type": "git", |
9 | "url": "git@github.com:videojs/videojs-contrib-hls.git" | 11 | "url": "git@github.com:videojs/videojs-contrib-hls.git" |
10 | }, | 12 | }, |
11 | "license": "Apache-2.0", | ||
12 | "scripts": { | 13 | "scripts": { |
13 | "test": "grunt test", | 14 | "prebuild": "npm run clean", |
14 | "prepublish": "if [ -z \"$TRAVIS\" ]; then grunt; fi" | 15 | "build": "npm-run-all -p build:*", |
16 | "build:js": "npm-run-all build:js:babel build:js:browserify build:js:bannerize build:js:uglify", | ||
17 | "build:js:babel": "babel src -d es5", | ||
18 | "build:js:bannerize": "bannerize dist/videojs-contrib-hls.js --banner=scripts/banner.ejs", | ||
19 | "build:js:browserify": "browserify . -s videojs-contrib-hls -o dist/videojs-contrib-hls.js", | ||
20 | "build:js:uglify": "uglifyjs dist/videojs-contrib-hls.js --comments --mangle --compress -o dist/videojs-contrib-hls.min.js", | ||
21 | "build:test": "npm-run-all build:test:manifest build:test:js", | ||
22 | "build:test:js": "node scripts/build-test.js", | ||
23 | "build:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.build();\"", | ||
24 | "clean": "npm-run-all -p clean:*", | ||
25 | "clean:build": "node -e \"var s=require('shelljs'),d=['dist','dist-test','es5'];s.rm('-rf',d);s.mkdir('-p',d);\"", | ||
26 | "clean:test": "node -e \"var b=require('./scripts/manifest-data.js'); b.clean();\"", | ||
27 | "docs": "npm-run-all docs:*", | ||
28 | "docs:api": "jsdoc src -r -d docs/api", | ||
29 | "docs:toc": "doctoc README.md", | ||
30 | "lint": "vjsstandard", | ||
31 | "prestart": "npm-run-all docs build", | ||
32 | "start": "npm-run-all -p start:* watch:*", | ||
33 | "start:serve": "babel-node scripts/server.js", | ||
34 | "pretest": "npm-run-all lint build", | ||
35 | "test": "karma start test/karma/detected.js", | ||
36 | "test:chrome": "npm run pretest && karma start test/karma/chrome.js", | ||
37 | "test:firefox": "npm run pretest && karma start test/karma/firefox.js", | ||
38 | "test:ie": "npm run pretest && karma start test/karma/ie.js", | ||
39 | "test:safari": "npm run pretest && karma start test/karma/safari.js", | ||
40 | "preversion": "npm test", | ||
41 | "version": "npm run build", | ||
42 | "watch": "npm-run-all -p watch:*", | ||
43 | "watch:js": "watchify src/videojs-contrib-hls.js -t babelify -v -o dist/videojs-contrib-hls.js", | ||
44 | "watch:test": "npm-run-all -p watch:test:*", | ||
45 | "watch:test:js": "node scripts/watch-test.js", | ||
46 | "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"", | ||
47 | "prepublish": "npm run build" | ||
15 | }, | 48 | }, |
16 | "keywords": [ | 49 | "keywords": [ |
17 | "videojs", | 50 | "videojs", |
18 | "videojs-plugin" | 51 | "videojs-plugin" |
19 | ], | 52 | ], |
20 | "devDependencies": { | 53 | "author": "Brightcove, Inc", |
21 | "chg": "^0.2.0", | 54 | "license": "Apache-2.0", |
22 | "grunt": "^0.4.5", | 55 | "browserify": { |
23 | "grunt-concurrent": "0.4.3", | 56 | "transform": [ |
24 | "grunt-contrib-clean": "~0.4.0", | 57 | "browserify-shim" |
25 | "grunt-contrib-concat": "~0.3.0", | 58 | ] |
26 | "grunt-contrib-connect": "~0.6.0", | 59 | }, |
27 | "grunt-contrib-jshint": "~0.6.0", | 60 | "browserify-shim": { |
28 | "grunt-contrib-uglify": "~0.2.0", | 61 | "qunit": "global:QUnit", |
29 | "grunt-contrib-watch": "~0.4.0", | 62 | "sinon": "global:sinon", |
30 | "grunt-github-releaser": "^0.1.17", | 63 | "video.js": "global:videojs" |
31 | "grunt-karma": "~0.6.2", | ||
32 | "grunt-open": "0.2.3", | ||
33 | "grunt-protractor-runner": "forbesjo/grunt-protractor-runner.git#webdriverManagerUpdate", | ||
34 | "grunt-shell": "0.6.1", | ||
35 | "grunt-version": "^1.0.0", | ||
36 | "karma": "~0.10.0", | ||
37 | "karma-chrome-launcher": "~0.1.2", | ||
38 | "karma-firefox-launcher": "~0.1.3", | ||
39 | "karma-ie-launcher": "~0.1.1", | ||
40 | "karma-opera-launcher": "~0.1.0", | ||
41 | "karma-phantomjs-launcher": "^0.1.4", | ||
42 | "karma-qunit": "~0.1.1", | ||
43 | "karma-safari-launcher": "~0.1.1", | ||
44 | "karma-sauce-launcher": "~0.1.8", | ||
45 | "qunitjs": "^1.18.0", | ||
46 | "sinon": "1.10.2", | ||
47 | "video.js": "^5.2.1" | ||
48 | }, | 64 | }, |
65 | "vjsstandard": { | ||
66 | "ignore": [ | ||
67 | "dist", | ||
68 | "dist-test", | ||
69 | "docs", | ||
70 | "es5", | ||
71 | "test/karma", | ||
72 | "scripts", | ||
73 | "utils", | ||
74 | "test/test-manifests.js", | ||
75 | "test/test-expected.js", | ||
76 | "src/playlist-loader.js" | ||
77 | ] | ||
78 | }, | ||
79 | "files": [ | ||
80 | "CONTRIBUTING.md", | ||
81 | "dist-test/", | ||
82 | "dist/", | ||
83 | "docs/", | ||
84 | "es5/", | ||
85 | "index.html", | ||
86 | "scripts/", | ||
87 | "src/", | ||
88 | "test/", | ||
89 | "utils/" | ||
90 | ], | ||
49 | "dependencies": { | 91 | "dependencies": { |
50 | "pkcs7": "^0.2.2", | 92 | "pkcs7": "^0.2.2", |
51 | "videojs-contrib-media-sources": "^2.4.0", | 93 | "video.js": "^5.2.1", |
94 | "videojs-contrib-media-sources": "^3.0.0", | ||
52 | "videojs-swf": "^5.0.0" | 95 | "videojs-swf": "^5.0.0" |
96 | }, | ||
97 | "devDependencies": { | ||
98 | "babel": "^5.8.0", | ||
99 | "babelify": "^6.0.0", | ||
100 | "bannerize": "^1.0.0", | ||
101 | "browserify": "^11.0.0", | ||
102 | "browserify-shim": "^3.0.0", | ||
103 | "connect": "^3.4.0", | ||
104 | "cowsay": "^1.1.0", | ||
105 | "doctoc": "^0.15.0", | ||
106 | "glob": "^6.0.3", | ||
107 | "global": "^4.3.0", | ||
108 | "jsdoc": "^3.4.0", | ||
109 | "karma": "^0.13.0", | ||
110 | "karma-browserify": "^4.4.0", | ||
111 | "karma-chrome-launcher": "^0.2.0", | ||
112 | "karma-detect-browsers": "^2.0.0", | ||
113 | "karma-firefox-launcher": "^0.1.0", | ||
114 | "karma-ie-launcher": "^0.2.0", | ||
115 | "karma-qunit": "^0.1.9", | ||
116 | "karma-safari-launcher": "^0.1.0", | ||
117 | "lodash-compat": "^3.10.0", | ||
118 | "minimist": "^1.2.0", | ||
119 | "npm-run-all": "^1.2.0", | ||
120 | "portscanner": "^1.0.0", | ||
121 | "qunitjs": "^1.18.0", | ||
122 | "serve-static": "^1.10.0", | ||
123 | "shelljs": "^0.5.3", | ||
124 | "sinon": "1.10.2", | ||
125 | "uglify-js": "^2.5.0", | ||
126 | "videojs-standard": "^4.0.0", | ||
127 | "watchify": "^3.6.0", | ||
128 | "webworkify": "^1.1.0" | ||
53 | } | 129 | } |
54 | } | 130 | } | ... | ... |
scripts/banner.ejs
0 → 100644
scripts/build-test.js
0 → 100644
scripts/manifest-data.js
0 → 100644
1 | var fs = require('fs'); | ||
2 | var path = require('path'); | ||
3 | |||
4 | var basePath = path.resolve(__dirname, '..'); | ||
5 | var testDataDir = path.join(basePath,'test'); | ||
6 | var manifestDir = path.join(basePath, 'utils', 'manifest'); | ||
7 | var manifestFilepath = path.join(testDataDir, 'test-manifests.js'); | ||
8 | var expectedFilepath = path.join(testDataDir, 'test-expected.js'); | ||
9 | |||
10 | var build = function() { | ||
11 | var manifests = 'export default {\n'; | ||
12 | var expected = 'export default {\n'; | ||
13 | |||
14 | var files = fs.readdirSync(manifestDir); | ||
15 | while (files.length > 0) { | ||
16 | var file = path.resolve(manifestDir, files.shift()); | ||
17 | var extname = path.extname(file); | ||
18 | |||
19 | if (extname === '.m3u8') { | ||
20 | // translate this manifest | ||
21 | manifests += ' \'' + path.basename(file, '.m3u8') + '\': '; | ||
22 | manifests += fs.readFileSync(file, 'utf8') | ||
23 | .split(/\r\n|\n/) | ||
24 | // quote and concatenate | ||
25 | .map(function(line) { | ||
26 | return ' \'' + line + '\\n\' +\n'; | ||
27 | }).join('') | ||
28 | // strip leading spaces and the trailing '+' | ||
29 | .slice(4, -3); | ||
30 | manifests += ',\n'; | ||
31 | } else if (extname === '.js') { | ||
32 | // append the expected parse | ||
33 | expected += ' "' + path.basename(file, '.js') + '": '; | ||
34 | expected += fs.readFileSync(file, 'utf8'); | ||
35 | expected += ',\n'; | ||
36 | } else { | ||
37 | console.log('Unknown file ' + file + ' found in manifest dir ' + manifestDir); | ||
38 | } | ||
39 | |||
40 | } | ||
41 | |||
42 | // clean up and close the objects | ||
43 | manifests = manifests.slice(0, -2); | ||
44 | manifests += '\n};\n'; | ||
45 | expected = expected.slice(0, -2); | ||
46 | expected += '\n};\n'; | ||
47 | |||
48 | fs.writeFileSync(manifestFilepath, manifests); | ||
49 | fs.writeFileSync(expectedFilepath, expected); | ||
50 | console.log('Wrote test data file ' + manifestFilepath); | ||
51 | console.log('Wrote test data file ' + expectedFilepath); | ||
52 | }; | ||
53 | |||
54 | var watch = function() { | ||
55 | build(); | ||
56 | fs.watch(manifestDir, function(event, filename) { | ||
57 | console.log('files in manifest dir were changed rebuilding manifest data'); | ||
58 | build(); | ||
59 | }); | ||
60 | }; | ||
61 | |||
62 | var clean = function() { | ||
63 | try { | ||
64 | fs.unlinkSync(manifestFilepath); | ||
65 | } catch(e) { | ||
66 | console.log(e); | ||
67 | } | ||
68 | try { | ||
69 | fs.unlinkSync(expectedFilepath); | ||
70 | } catch(e) { | ||
71 | console.log(e); | ||
72 | } | ||
73 | } | ||
74 | |||
75 | module.exports = { | ||
76 | build: build, | ||
77 | watch: watch, | ||
78 | clean: clean | ||
79 | }; |
scripts/server.js
0 → 100644
1 | import connect from 'connect'; | ||
2 | import cowsay from 'cowsay'; | ||
3 | import path from 'path'; | ||
4 | import portscanner from 'portscanner'; | ||
5 | import serveStatic from 'serve-static'; | ||
6 | |||
7 | // Configuration for the server. | ||
8 | const PORT = 9999; | ||
9 | const MAX_PORT = PORT + 100; | ||
10 | const HOST = '127.0.0.1'; | ||
11 | |||
12 | const app = connect(); | ||
13 | |||
14 | const verbs = [ | ||
15 | 'Chewing the cud', | ||
16 | 'Grazing', | ||
17 | 'Mooing', | ||
18 | 'Lowing', | ||
19 | 'Churning the cream' | ||
20 | ]; | ||
21 | |||
22 | app.use(serveStatic(path.join(__dirname, '..'))); | ||
23 | |||
24 | portscanner.findAPortNotInUse(PORT, MAX_PORT, HOST, (error, port) => { | ||
25 | if (error) { | ||
26 | throw error; | ||
27 | } | ||
28 | |||
29 | process.stdout.write(cowsay.say({ | ||
30 | text: `${verbs[Math.floor(Math.random() * 5)]} on ${HOST}:${port}` | ||
31 | }) + '\n\n'); | ||
32 | |||
33 | app.listen(port); | ||
34 | }); |
scripts/watch-test.js
0 → 100644
1 | var browserify = require('browserify'); | ||
2 | var fs = require('fs'); | ||
3 | var glob = require('glob'); | ||
4 | var watchify = require('watchify'); | ||
5 | |||
6 | glob('test/**/*.test.js', function(err, files) { | ||
7 | var b = browserify(files, { | ||
8 | cache: {}, | ||
9 | packageCache: {}, | ||
10 | plugin: [watchify] | ||
11 | }).transform('babelify'); | ||
12 | |||
13 | var bundle = function() { | ||
14 | b.bundle().pipe(fs.createWriteStream('dist-test/videojs-contrib-hls.js')); | ||
15 | }; | ||
16 | |||
17 | b.on('log', function(msg) { | ||
18 | process.stdout.write(msg + '\n'); | ||
19 | }); | ||
20 | |||
21 | b.on('update', bundle); | ||
22 | bundle(); | ||
23 | }); |
src/.jshintrc
deleted
100644 → 0
1 | (function(window) { | 1 | const textRange = function(range, i) { |
2 | var textRange = function(range, i) { | ||
3 | return range.start(i) + '-' + range.end(i); | 2 | return range.start(i) + '-' + range.end(i); |
4 | }; | 3 | }; |
5 | var module = { | 4 | |
6 | hexDump: function(data) { | 5 | const formatHexString = function(e, i) { |
7 | var | 6 | let value = e.toString(16); |
8 | bytes = Array.prototype.slice.call(data), | 7 | |
9 | step = 16, | 8 | return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); |
10 | formatHexString = function(e, i) { | 9 | }; |
11 | var value = e.toString(16); | 10 | const formatAsciiString = function(e) { |
12 | return "00".substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); | ||
13 | }, | ||
14 | formatAsciiString = function(e) { | ||
15 | if (e >= 0x20 && e < 0x7e) { | 11 | if (e >= 0x20 && e < 0x7e) { |
16 | return String.fromCharCode(e); | 12 | return String.fromCharCode(e); |
17 | } | 13 | } |
18 | return '.'; | 14 | return '.'; |
19 | }, | 15 | }; |
20 | result = '', | 16 | |
21 | hex, | 17 | const utils = { |
22 | ascii; | 18 | hexDump(data) { |
23 | for (var j = 0; j < bytes.length / step; j++) { | 19 | let bytes = Array.prototype.slice.call(data); |
20 | let step = 16; | ||
21 | let result = ''; | ||
22 | let hex; | ||
23 | let ascii; | ||
24 | |||
25 | for (let j = 0; j < bytes.length / step; j++) { | ||
24 | hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); | 26 | hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); |
25 | ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); | 27 | ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); |
26 | result += hex + ' ' + ascii + '\n'; | 28 | result += hex + ' ' + ascii + '\n'; |
27 | } | 29 | } |
28 | return result; | 30 | return result; |
29 | }, | 31 | }, |
30 | tagDump: function(tag) { | 32 | tagDump(tag) { |
31 | return module.hexDump(tag.bytes); | 33 | return utils.hexDump(tag.bytes); |
32 | }, | 34 | }, |
33 | textRanges: function(ranges) { | 35 | textRanges(ranges) { |
34 | var result = '', i; | 36 | let result = ''; |
37 | let i; | ||
38 | |||
35 | for (i = 0; i < ranges.length; i++) { | 39 | for (i = 0; i < ranges.length; i++) { |
36 | result += textRange(ranges, i) + ' '; | 40 | result += textRange(ranges, i) + ' '; |
37 | } | 41 | } |
38 | return result; | 42 | return result; |
39 | } | 43 | } |
40 | }; | 44 | }; |
41 | 45 | ||
42 | window.videojs.Hls.utils = module; | 46 | export default utils; |
43 | })(this); | ... | ... |
src/decrypter.js
deleted
100644 → 0
1 | /* | ||
2 | * | ||
3 | * This file contains an adaptation of the AES decryption algorithm | ||
4 | * from the Standford Javascript Cryptography Library. That work is | ||
5 | * covered by the following copyright and permissions notice: | ||
6 | * | ||
7 | * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. | ||
8 | * All rights reserved. | ||
9 | * | ||
10 | * Redistribution and use in source and binary forms, with or without | ||
11 | * modification, are permitted provided that the following conditions are | ||
12 | * met: | ||
13 | * | ||
14 | * 1. Redistributions of source code must retain the above copyright | ||
15 | * notice, this list of conditions and the following disclaimer. | ||
16 | * | ||
17 | * 2. Redistributions in binary form must reproduce the above | ||
18 | * copyright notice, this list of conditions and the following | ||
19 | * disclaimer in the documentation and/or other materials provided | ||
20 | * with the distribution. | ||
21 | * | ||
22 | * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR | ||
23 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
24 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
25 | * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE | ||
26 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
27 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
28 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR | ||
29 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | ||
30 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE | ||
31 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN | ||
32 | * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
33 | * | ||
34 | * The views and conclusions contained in the software and documentation | ||
35 | * are those of the authors and should not be interpreted as representing | ||
36 | * official policies, either expressed or implied, of the authors. | ||
37 | */ | ||
38 | (function(window, videojs, unpad) { | ||
39 | 'use strict'; | ||
40 | |||
41 | var AES, AsyncStream, Decrypter, decrypt, ntoh; | ||
42 | |||
43 | /** | ||
44 | * Convert network-order (big-endian) bytes into their little-endian | ||
45 | * representation. | ||
46 | */ | ||
47 | ntoh = function(word) { | ||
48 | return (word << 24) | | ||
49 | ((word & 0xff00) << 8) | | ||
50 | ((word & 0xff0000) >> 8) | | ||
51 | (word >>> 24); | ||
52 | }; | ||
53 | |||
54 | /** | ||
55 | * Schedule out an AES key for both encryption and decryption. This | ||
56 | * is a low-level class. Use a cipher mode to do bulk encryption. | ||
57 | * | ||
58 | * @constructor | ||
59 | * @param key {Array} The key as an array of 4, 6 or 8 words. | ||
60 | */ | ||
61 | AES = function (key) { | ||
62 | this._precompute(); | ||
63 | |||
64 | var i, j, tmp, | ||
65 | encKey, decKey, | ||
66 | sbox = this._tables[0][4], decTable = this._tables[1], | ||
67 | keyLen = key.length, rcon = 1; | ||
68 | |||
69 | if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { | ||
70 | throw new Error("Invalid aes key size"); | ||
71 | } | ||
72 | |||
73 | encKey = key.slice(0); | ||
74 | decKey = []; | ||
75 | this._key = [encKey, decKey]; | ||
76 | |||
77 | // schedule encryption keys | ||
78 | for (i = keyLen; i < 4 * keyLen + 28; i++) { | ||
79 | tmp = encKey[i-1]; | ||
80 | |||
81 | // apply sbox | ||
82 | if (i%keyLen === 0 || (keyLen === 8 && i%keyLen === 4)) { | ||
83 | tmp = sbox[tmp>>>24]<<24 ^ sbox[tmp>>16&255]<<16 ^ sbox[tmp>>8&255]<<8 ^ sbox[tmp&255]; | ||
84 | |||
85 | // shift rows and add rcon | ||
86 | if (i%keyLen === 0) { | ||
87 | tmp = tmp<<8 ^ tmp>>>24 ^ rcon<<24; | ||
88 | rcon = rcon<<1 ^ (rcon>>7)*283; | ||
89 | } | ||
90 | } | ||
91 | |||
92 | encKey[i] = encKey[i-keyLen] ^ tmp; | ||
93 | } | ||
94 | |||
95 | // schedule decryption keys | ||
96 | for (j = 0; i; j++, i--) { | ||
97 | tmp = encKey[j&3 ? i : i - 4]; | ||
98 | if (i<=4 || j<4) { | ||
99 | decKey[j] = tmp; | ||
100 | } else { | ||
101 | decKey[j] = decTable[0][sbox[tmp>>>24 ]] ^ | ||
102 | decTable[1][sbox[tmp>>16 & 255]] ^ | ||
103 | decTable[2][sbox[tmp>>8 & 255]] ^ | ||
104 | decTable[3][sbox[tmp & 255]]; | ||
105 | } | ||
106 | } | ||
107 | }; | ||
108 | |||
109 | AES.prototype = { | ||
110 | /** | ||
111 | * The expanded S-box and inverse S-box tables. These will be computed | ||
112 | * on the client so that we don't have to send them down the wire. | ||
113 | * | ||
114 | * There are two tables, _tables[0] is for encryption and | ||
115 | * _tables[1] is for decryption. | ||
116 | * | ||
117 | * The first 4 sub-tables are the expanded S-box with MixColumns. The | ||
118 | * last (_tables[01][4]) is the S-box itself. | ||
119 | * | ||
120 | * @private | ||
121 | */ | ||
122 | _tables: [[[],[],[],[],[]],[[],[],[],[],[]]], | ||
123 | |||
124 | /** | ||
125 | * Expand the S-box tables. | ||
126 | * | ||
127 | * @private | ||
128 | */ | ||
129 | _precompute: function () { | ||
130 | var encTable = this._tables[0], decTable = this._tables[1], | ||
131 | sbox = encTable[4], sboxInv = decTable[4], | ||
132 | i, x, xInv, d=[], th=[], x2, x4, x8, s, tEnc, tDec; | ||
133 | |||
134 | // Compute double and third tables | ||
135 | for (i = 0; i < 256; i++) { | ||
136 | th[( d[i] = i<<1 ^ (i>>7)*283 )^i]=i; | ||
137 | } | ||
138 | |||
139 | for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { | ||
140 | // Compute sbox | ||
141 | s = xInv ^ xInv<<1 ^ xInv<<2 ^ xInv<<3 ^ xInv<<4; | ||
142 | s = s>>8 ^ s&255 ^ 99; | ||
143 | sbox[x] = s; | ||
144 | sboxInv[s] = x; | ||
145 | |||
146 | // Compute MixColumns | ||
147 | x8 = d[x4 = d[x2 = d[x]]]; | ||
148 | tDec = x8*0x1010101 ^ x4*0x10001 ^ x2*0x101 ^ x*0x1010100; | ||
149 | tEnc = d[s]*0x101 ^ s*0x1010100; | ||
150 | |||
151 | for (i = 0; i < 4; i++) { | ||
152 | encTable[i][x] = tEnc = tEnc<<24 ^ tEnc>>>8; | ||
153 | decTable[i][s] = tDec = tDec<<24 ^ tDec>>>8; | ||
154 | } | ||
155 | } | ||
156 | |||
157 | // Compactify. Considerable speedup on Firefox. | ||
158 | for (i = 0; i < 5; i++) { | ||
159 | encTable[i] = encTable[i].slice(0); | ||
160 | decTable[i] = decTable[i].slice(0); | ||
161 | } | ||
162 | }, | ||
163 | |||
164 | /** | ||
165 | * Decrypt 16 bytes, specified as four 32-bit words. | ||
166 | * @param encrypted0 {number} the first word to decrypt | ||
167 | * @param encrypted1 {number} the second word to decrypt | ||
168 | * @param encrypted2 {number} the third word to decrypt | ||
169 | * @param encrypted3 {number} the fourth word to decrypt | ||
170 | * @param out {Int32Array} the array to write the decrypted words | ||
171 | * into | ||
172 | * @param offset {number} the offset into the output array to start | ||
173 | * writing results | ||
174 | * @return {Array} The plaintext. | ||
175 | */ | ||
176 | decrypt:function (encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { | ||
177 | var key = this._key[1], | ||
178 | // state variables a,b,c,d are loaded with pre-whitened data | ||
179 | a = encrypted0 ^ key[0], | ||
180 | b = encrypted3 ^ key[1], | ||
181 | c = encrypted2 ^ key[2], | ||
182 | d = encrypted1 ^ key[3], | ||
183 | a2, b2, c2, | ||
184 | |||
185 | nInnerRounds = key.length / 4 - 2, // key.length === 2 ? | ||
186 | i, | ||
187 | kIndex = 4, | ||
188 | table = this._tables[1], | ||
189 | |||
190 | // load up the tables | ||
191 | table0 = table[0], | ||
192 | table1 = table[1], | ||
193 | table2 = table[2], | ||
194 | table3 = table[3], | ||
195 | sbox = table[4]; | ||
196 | |||
197 | // Inner rounds. Cribbed from OpenSSL. | ||
198 | for (i = 0; i < nInnerRounds; i++) { | ||
199 | a2 = table0[a>>>24] ^ table1[b>>16 & 255] ^ table2[c>>8 & 255] ^ table3[d & 255] ^ key[kIndex]; | ||
200 | b2 = table0[b>>>24] ^ table1[c>>16 & 255] ^ table2[d>>8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; | ||
201 | c2 = table0[c>>>24] ^ table1[d>>16 & 255] ^ table2[a>>8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; | ||
202 | d = table0[d>>>24] ^ table1[a>>16 & 255] ^ table2[b>>8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; | ||
203 | kIndex += 4; | ||
204 | a=a2; b=b2; c=c2; | ||
205 | } | ||
206 | |||
207 | // Last round. | ||
208 | for (i = 0; i < 4; i++) { | ||
209 | out[(3 & -i) + offset] = | ||
210 | sbox[a>>>24 ]<<24 ^ | ||
211 | sbox[b>>16 & 255]<<16 ^ | ||
212 | sbox[c>>8 & 255]<<8 ^ | ||
213 | sbox[d & 255] ^ | ||
214 | key[kIndex++]; | ||
215 | a2=a; a=b; b=c; c=d; d=a2; | ||
216 | } | ||
217 | } | ||
218 | }; | ||
219 | |||
220 | /** | ||
221 | * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. | ||
222 | * @param encrypted {Uint8Array} the encrypted bytes | ||
223 | * @param key {Uint32Array} the bytes of the decryption key | ||
224 | * @param initVector {Uint32Array} the initialization vector (IV) to | ||
225 | * use for the first round of CBC. | ||
226 | * @return {Uint8Array} the decrypted bytes | ||
227 | * | ||
228 | * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard | ||
229 | * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 | ||
230 | * @see https://tools.ietf.org/html/rfc2315 | ||
231 | */ | ||
232 | decrypt = function(encrypted, key, initVector) { | ||
233 | var | ||
234 | // word-level access to the encrypted bytes | ||
235 | encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2), | ||
236 | |||
237 | decipher = new AES(Array.prototype.slice.call(key)), | ||
238 | |||
239 | // byte and word-level access for the decrypted output | ||
240 | decrypted = new Uint8Array(encrypted.byteLength), | ||
241 | decrypted32 = new Int32Array(decrypted.buffer), | ||
242 | |||
243 | // temporary variables for working with the IV, encrypted, and | ||
244 | // decrypted data | ||
245 | init0, init1, init2, init3, | ||
246 | encrypted0, encrypted1, encrypted2, encrypted3, | ||
247 | |||
248 | // iteration variable | ||
249 | wordIx; | ||
250 | |||
251 | // pull out the words of the IV to ensure we don't modify the | ||
252 | // passed-in reference and easier access | ||
253 | init0 = initVector[0]; | ||
254 | init1 = initVector[1]; | ||
255 | init2 = initVector[2]; | ||
256 | init3 = initVector[3]; | ||
257 | |||
258 | // decrypt four word sequences, applying cipher-block chaining (CBC) | ||
259 | // to each decrypted block | ||
260 | for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { | ||
261 | // convert big-endian (network order) words into little-endian | ||
262 | // (javascript order) | ||
263 | encrypted0 = ntoh(encrypted32[wordIx]); | ||
264 | encrypted1 = ntoh(encrypted32[wordIx + 1]); | ||
265 | encrypted2 = ntoh(encrypted32[wordIx + 2]); | ||
266 | encrypted3 = ntoh(encrypted32[wordIx + 3]); | ||
267 | |||
268 | // decrypt the block | ||
269 | decipher.decrypt(encrypted0, | ||
270 | encrypted1, | ||
271 | encrypted2, | ||
272 | encrypted3, | ||
273 | decrypted32, | ||
274 | wordIx); | ||
275 | |||
276 | // XOR with the IV, and restore network byte-order to obtain the | ||
277 | // plaintext | ||
278 | decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); | ||
279 | decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); | ||
280 | decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); | ||
281 | decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); | ||
282 | |||
283 | // setup the IV for the next round | ||
284 | init0 = encrypted0; | ||
285 | init1 = encrypted1; | ||
286 | init2 = encrypted2; | ||
287 | init3 = encrypted3; | ||
288 | } | ||
289 | |||
290 | return decrypted; | ||
291 | }; | ||
292 | |||
293 | AsyncStream = function() { | ||
294 | this.jobs = []; | ||
295 | this.delay = 1; | ||
296 | this.timeout_ = null; | ||
297 | }; | ||
298 | AsyncStream.prototype = new videojs.Hls.Stream(); | ||
299 | AsyncStream.prototype.processJob_ = function() { | ||
300 | this.jobs.shift()(); | ||
301 | if (this.jobs.length) { | ||
302 | this.timeout_ = setTimeout(this.processJob_.bind(this), | ||
303 | this.delay); | ||
304 | } else { | ||
305 | this.timeout_ = null; | ||
306 | } | ||
307 | }; | ||
308 | AsyncStream.prototype.push = function(job) { | ||
309 | this.jobs.push(job); | ||
310 | if (!this.timeout_) { | ||
311 | this.timeout_ = setTimeout(this.processJob_.bind(this), | ||
312 | this.delay); | ||
313 | } | ||
314 | }; | ||
315 | |||
316 | Decrypter = function(encrypted, key, initVector, done) { | ||
317 | var | ||
318 | step = Decrypter.STEP, | ||
319 | encrypted32 = new Int32Array(encrypted.buffer), | ||
320 | decrypted = new Uint8Array(encrypted.byteLength), | ||
321 | i = 0; | ||
322 | this.asyncStream_ = new AsyncStream(); | ||
323 | |||
324 | // split up the encryption job and do the individual chunks asynchronously | ||
325 | this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), | ||
326 | key, | ||
327 | initVector, | ||
328 | decrypted)); | ||
329 | for (i = step; i < encrypted32.length; i += step) { | ||
330 | initVector = new Uint32Array([ | ||
331 | ntoh(encrypted32[i - 4]), | ||
332 | ntoh(encrypted32[i - 3]), | ||
333 | ntoh(encrypted32[i - 2]), | ||
334 | ntoh(encrypted32[i - 1]) | ||
335 | ]); | ||
336 | this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), | ||
337 | key, | ||
338 | initVector, | ||
339 | decrypted)); | ||
340 | } | ||
341 | // invoke the done() callback when everything is finished | ||
342 | this.asyncStream_.push(function() { | ||
343 | // remove pkcs#7 padding from the decrypted bytes | ||
344 | done(null, unpad(decrypted)); | ||
345 | }); | ||
346 | }; | ||
347 | Decrypter.prototype = new videojs.Hls.Stream(); | ||
348 | Decrypter.prototype.decryptChunk_ = function(encrypted, key, initVector, decrypted) { | ||
349 | return function() { | ||
350 | var bytes = decrypt(encrypted, | ||
351 | key, | ||
352 | initVector); | ||
353 | decrypted.set(bytes, encrypted.byteOffset); | ||
354 | }; | ||
355 | }; | ||
356 | // the maximum number of bytes to process at one time | ||
357 | Decrypter.STEP = 4 * 8000; | ||
358 | |||
359 | // exports | ||
360 | videojs.Hls.decrypt = decrypt; | ||
361 | videojs.Hls.Decrypter = Decrypter; | ||
362 | videojs.Hls.AsyncStream = AsyncStream; | ||
363 | |||
364 | })(window, window.videojs, window.pkcs7.unpad); |
src/decrypter/aes.js
0 → 100644
1 | /* | ||
2 | * aes.js | ||
3 | * | ||
4 | * This file contains an adaptation of the AES decryption algorithm | ||
5 | * from the Standford Javascript Cryptography Library. That work is | ||
6 | * covered by the following copyright and permissions notice: | ||
7 | * | ||
8 | * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. | ||
9 | * All rights reserved. | ||
10 | * | ||
11 | * Redistribution and use in source and binary forms, with or without | ||
12 | * modification, are permitted provided that the following conditions are | ||
13 | * met: | ||
14 | * | ||
15 | * 1. Redistributions of source code must retain the above copyright | ||
16 | * notice, this list of conditions and the following disclaimer. | ||
17 | * | ||
18 | * 2. Redistributions in binary form must reproduce the above | ||
19 | * copyright notice, this list of conditions and the following | ||
20 | * disclaimer in the documentation and/or other materials provided | ||
21 | * with the distribution. | ||
22 | * | ||
23 | * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR | ||
24 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
25 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
26 | * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE | ||
27 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
28 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
29 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR | ||
30 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | ||
31 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE | ||
32 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN | ||
33 | * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
34 | * | ||
35 | * The views and conclusions contained in the software and documentation | ||
36 | * are those of the authors and should not be interpreted as representing | ||
37 | * official policies, either expressed or implied, of the authors. | ||
38 | */ | ||
39 | |||
40 | /** | ||
41 | * Expand the S-box tables. | ||
42 | * | ||
43 | * @private | ||
44 | */ | ||
45 | const precompute = function() { | ||
46 | let tables = [[[], [], [], [], []], [[], [], [], [], []]]; | ||
47 | let encTable = tables[0]; | ||
48 | let decTable = tables[1]; | ||
49 | let sbox = encTable[4]; | ||
50 | let sboxInv = decTable[4]; | ||
51 | let i; | ||
52 | let x; | ||
53 | let xInv; | ||
54 | let d = []; | ||
55 | let th = []; | ||
56 | let x2; | ||
57 | let x4; | ||
58 | let x8; | ||
59 | let s; | ||
60 | let tEnc; | ||
61 | let tDec; | ||
62 | |||
63 | // Compute double and third tables | ||
64 | for (i = 0; i < 256; i++) { | ||
65 | th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; | ||
66 | } | ||
67 | |||
68 | for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { | ||
69 | // Compute sbox | ||
70 | s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; | ||
71 | s = s >> 8 ^ s & 255 ^ 99; | ||
72 | sbox[x] = s; | ||
73 | sboxInv[s] = x; | ||
74 | |||
75 | // Compute MixColumns | ||
76 | x8 = d[x4 = d[x2 = d[x]]]; | ||
77 | tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; | ||
78 | tEnc = d[s] * 0x101 ^ s * 0x1010100; | ||
79 | |||
80 | for (i = 0; i < 4; i++) { | ||
81 | encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; | ||
82 | decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; | ||
83 | } | ||
84 | } | ||
85 | |||
86 | // Compactify. Considerable speedup on Firefox. | ||
87 | for (i = 0; i < 5; i++) { | ||
88 | encTable[i] = encTable[i].slice(0); | ||
89 | decTable[i] = decTable[i].slice(0); | ||
90 | } | ||
91 | return tables; | ||
92 | }; | ||
93 | let aesTables = null; | ||
94 | |||
95 | /** | ||
96 | * Schedule out an AES key for both encryption and decryption. This | ||
97 | * is a low-level class. Use a cipher mode to do bulk encryption. | ||
98 | * | ||
99 | * @constructor | ||
100 | * @param key {Array} The key as an array of 4, 6 or 8 words. | ||
101 | */ | ||
102 | export default class AES { | ||
103 | constructor(key) { | ||
104 | /** | ||
105 | * The expanded S-box and inverse S-box tables. These will be computed | ||
106 | * on the client so that we don't have to send them down the wire. | ||
107 | * | ||
108 | * There are two tables, _tables[0] is for encryption and | ||
109 | * _tables[1] is for decryption. | ||
110 | * | ||
111 | * The first 4 sub-tables are the expanded S-box with MixColumns. The | ||
112 | * last (_tables[01][4]) is the S-box itself. | ||
113 | * | ||
114 | * @private | ||
115 | */ | ||
116 | // if we have yet to precompute the S-box tables | ||
117 | // do so now | ||
118 | if (!aesTables) { | ||
119 | aesTables = precompute(); | ||
120 | } | ||
121 | // then make a copy of that object for use | ||
122 | this._tables = [[aesTables[0][0].slice(), | ||
123 | aesTables[0][1].slice(), | ||
124 | aesTables[0][2].slice(), | ||
125 | aesTables[0][3].slice(), | ||
126 | aesTables[0][4].slice()], | ||
127 | [aesTables[1][0].slice(), | ||
128 | aesTables[1][1].slice(), | ||
129 | aesTables[1][2].slice(), | ||
130 | aesTables[1][3].slice(), | ||
131 | aesTables[1][4].slice()]]; | ||
132 | let i; | ||
133 | let j; | ||
134 | let tmp; | ||
135 | let encKey; | ||
136 | let decKey; | ||
137 | let sbox = this._tables[0][4]; | ||
138 | let decTable = this._tables[1]; | ||
139 | let keyLen = key.length; | ||
140 | let rcon = 1; | ||
141 | |||
142 | if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { | ||
143 | throw new Error('Invalid aes key size'); | ||
144 | } | ||
145 | |||
146 | encKey = key.slice(0); | ||
147 | decKey = []; | ||
148 | this._key = [encKey, decKey]; | ||
149 | |||
150 | // schedule encryption keys | ||
151 | for (i = keyLen; i < 4 * keyLen + 28; i++) { | ||
152 | tmp = encKey[i - 1]; | ||
153 | |||
154 | // apply sbox | ||
155 | if (i % keyLen === 0 || (keyLen === 8 && i % keyLen === 4)) { | ||
156 | tmp = sbox[tmp >>> 24] << 24 ^ | ||
157 | sbox[tmp >> 16 & 255] << 16 ^ | ||
158 | sbox[tmp >> 8 & 255] << 8 ^ | ||
159 | sbox[tmp & 255]; | ||
160 | |||
161 | // shift rows and add rcon | ||
162 | if (i % keyLen === 0) { | ||
163 | tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; | ||
164 | rcon = rcon << 1 ^ (rcon >> 7) * 283; | ||
165 | } | ||
166 | } | ||
167 | |||
168 | encKey[i] = encKey[i - keyLen] ^ tmp; | ||
169 | } | ||
170 | |||
171 | // schedule decryption keys | ||
172 | for (j = 0; i; j++, i--) { | ||
173 | tmp = encKey[j & 3 ? i : i - 4]; | ||
174 | if (i <= 4 || j < 4) { | ||
175 | decKey[j] = tmp; | ||
176 | } else { | ||
177 | decKey[j] = decTable[0][sbox[tmp >>> 24 ]] ^ | ||
178 | decTable[1][sbox[tmp >> 16 & 255]] ^ | ||
179 | decTable[2][sbox[tmp >> 8 & 255]] ^ | ||
180 | decTable[3][sbox[tmp & 255]]; | ||
181 | } | ||
182 | } | ||
183 | } | ||
184 | |||
185 | /** | ||
186 | * Decrypt 16 bytes, specified as four 32-bit words. | ||
187 | * @param encrypted0 {number} the first word to decrypt | ||
188 | * @param encrypted1 {number} the second word to decrypt | ||
189 | * @param encrypted2 {number} the third word to decrypt | ||
190 | * @param encrypted3 {number} the fourth word to decrypt | ||
191 | * @param out {Int32Array} the array to write the decrypted words | ||
192 | * into | ||
193 | * @param offset {number} the offset into the output array to start | ||
194 | * writing results | ||
195 | * @return {Array} The plaintext. | ||
196 | */ | ||
197 | decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { | ||
198 | let key = this._key[1]; | ||
199 | // state variables a,b,c,d are loaded with pre-whitened data | ||
200 | let a = encrypted0 ^ key[0]; | ||
201 | let b = encrypted3 ^ key[1]; | ||
202 | let c = encrypted2 ^ key[2]; | ||
203 | let d = encrypted1 ^ key[3]; | ||
204 | let a2; | ||
205 | let b2; | ||
206 | let c2; | ||
207 | |||
208 | // key.length === 2 ? | ||
209 | let nInnerRounds = key.length / 4 - 2; | ||
210 | let i; | ||
211 | let kIndex = 4; | ||
212 | let table = this._tables[1]; | ||
213 | |||
214 | // load up the tables | ||
215 | let table0 = table[0]; | ||
216 | let table1 = table[1]; | ||
217 | let table2 = table[2]; | ||
218 | let table3 = table[3]; | ||
219 | let sbox = table[4]; | ||
220 | |||
221 | // Inner rounds. Cribbed from OpenSSL. | ||
222 | for (i = 0; i < nInnerRounds; i++) { | ||
223 | a2 = table0[a >>> 24] ^ | ||
224 | table1[b >> 16 & 255] ^ | ||
225 | table2[c >> 8 & 255] ^ | ||
226 | table3[d & 255] ^ | ||
227 | key[kIndex]; | ||
228 | b2 = table0[b >>> 24] ^ | ||
229 | table1[c >> 16 & 255] ^ | ||
230 | table2[d >> 8 & 255] ^ | ||
231 | table3[a & 255] ^ | ||
232 | key[kIndex + 1]; | ||
233 | c2 = table0[c >>> 24] ^ | ||
234 | table1[d >> 16 & 255] ^ | ||
235 | table2[a >> 8 & 255] ^ | ||
236 | table3[b & 255] ^ | ||
237 | key[kIndex + 2]; | ||
238 | d = table0[d >>> 24] ^ | ||
239 | table1[a >> 16 & 255] ^ | ||
240 | table2[b >> 8 & 255] ^ | ||
241 | table3[c & 255] ^ | ||
242 | key[kIndex + 3]; | ||
243 | kIndex += 4; | ||
244 | a = a2; b = b2; c = c2; | ||
245 | } | ||
246 | |||
247 | // Last round. | ||
248 | for (i = 0; i < 4; i++) { | ||
249 | out[(3 & -i) + offset] = | ||
250 | sbox[a >>> 24] << 24 ^ | ||
251 | sbox[b >> 16 & 255] << 16 ^ | ||
252 | sbox[c >> 8 & 255] << 8 ^ | ||
253 | sbox[d & 255] ^ | ||
254 | key[kIndex++]; | ||
255 | a2 = a; a = b; b = c; c = d; d = a2; | ||
256 | } | ||
257 | } | ||
258 | } |
src/decrypter/async-stream.js
0 → 100644
1 | import Stream from '../stream'; | ||
2 | |||
3 | /** | ||
4 | * A wrapper around the Stream class to use setTiemout | ||
5 | * and run stream "jobs" Asynchronously | ||
6 | */ | ||
7 | export default class AsyncStream extends Stream { | ||
8 | constructor() { | ||
9 | super(Stream); | ||
10 | this.jobs = []; | ||
11 | this.delay = 1; | ||
12 | this.timeout_ = null; | ||
13 | } | ||
14 | processJob_() { | ||
15 | this.jobs.shift()(); | ||
16 | if (this.jobs.length) { | ||
17 | this.timeout_ = setTimeout(this.processJob_.bind(this), | ||
18 | this.delay); | ||
19 | } else { | ||
20 | this.timeout_ = null; | ||
21 | } | ||
22 | } | ||
23 | push(job) { | ||
24 | this.jobs.push(job); | ||
25 | if (!this.timeout_) { | ||
26 | this.timeout_ = setTimeout(this.processJob_.bind(this), | ||
27 | this.delay); | ||
28 | } | ||
29 | } | ||
30 | } | ||
31 |
src/decrypter/decrypter.js
0 → 100644
1 | /* | ||
2 | * decrypter.js | ||
3 | * | ||
4 | * An asynchronous implementation of AES-128 CBC decryption with | ||
5 | * PKCS#7 padding. | ||
6 | */ | ||
7 | |||
8 | import AES from './aes'; | ||
9 | import AsyncStream from './async-stream'; | ||
10 | import {unpad} from 'pkcs7'; | ||
11 | |||
12 | /** | ||
13 | * Convert network-order (big-endian) bytes into their little-endian | ||
14 | * representation. | ||
15 | */ | ||
16 | const ntoh = function(word) { | ||
17 | return (word << 24) | | ||
18 | ((word & 0xff00) << 8) | | ||
19 | ((word & 0xff0000) >> 8) | | ||
20 | (word >>> 24); | ||
21 | }; | ||
22 | |||
23 | /* eslint-disable max-len */ | ||
24 | /** | ||
25 | * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. | ||
26 | * @param encrypted {Uint8Array} the encrypted bytes | ||
27 | * @param key {Uint32Array} the bytes of the decryption key | ||
28 | * @param initVector {Uint32Array} the initialization vector (IV) to | ||
29 | * use for the first round of CBC. | ||
30 | * @return {Uint8Array} the decrypted bytes | ||
31 | * | ||
32 | * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard | ||
33 | * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 | ||
34 | * @see https://tools.ietf.org/html/rfc2315 | ||
35 | */ | ||
36 | /* eslint-enable max-len */ | ||
37 | export const decrypt = function(encrypted, key, initVector) { | ||
38 | // word-level access to the encrypted bytes | ||
39 | let encrypted32 = new Int32Array(encrypted.buffer, | ||
40 | encrypted.byteOffset, | ||
41 | encrypted.byteLength >> 2); | ||
42 | |||
43 | let decipher = new AES(Array.prototype.slice.call(key)); | ||
44 | |||
45 | // byte and word-level access for the decrypted output | ||
46 | let decrypted = new Uint8Array(encrypted.byteLength); | ||
47 | let decrypted32 = new Int32Array(decrypted.buffer); | ||
48 | |||
49 | // temporary variables for working with the IV, encrypted, and | ||
50 | // decrypted data | ||
51 | let init0; | ||
52 | let init1; | ||
53 | let init2; | ||
54 | let init3; | ||
55 | let encrypted0; | ||
56 | let encrypted1; | ||
57 | let encrypted2; | ||
58 | let encrypted3; | ||
59 | |||
60 | // iteration variable | ||
61 | let wordIx; | ||
62 | |||
63 | // pull out the words of the IV to ensure we don't modify the | ||
64 | // passed-in reference and easier access | ||
65 | init0 = initVector[0]; | ||
66 | init1 = initVector[1]; | ||
67 | init2 = initVector[2]; | ||
68 | init3 = initVector[3]; | ||
69 | |||
70 | // decrypt four word sequences, applying cipher-block chaining (CBC) | ||
71 | // to each decrypted block | ||
72 | for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { | ||
73 | // convert big-endian (network order) words into little-endian | ||
74 | // (javascript order) | ||
75 | encrypted0 = ntoh(encrypted32[wordIx]); | ||
76 | encrypted1 = ntoh(encrypted32[wordIx + 1]); | ||
77 | encrypted2 = ntoh(encrypted32[wordIx + 2]); | ||
78 | encrypted3 = ntoh(encrypted32[wordIx + 3]); | ||
79 | |||
80 | // decrypt the block | ||
81 | decipher.decrypt(encrypted0, | ||
82 | encrypted1, | ||
83 | encrypted2, | ||
84 | encrypted3, | ||
85 | decrypted32, | ||
86 | wordIx); | ||
87 | |||
88 | // XOR with the IV, and restore network byte-order to obtain the | ||
89 | // plaintext | ||
90 | decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); | ||
91 | decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); | ||
92 | decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); | ||
93 | decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); | ||
94 | |||
95 | // setup the IV for the next round | ||
96 | init0 = encrypted0; | ||
97 | init1 = encrypted1; | ||
98 | init2 = encrypted2; | ||
99 | init3 = encrypted3; | ||
100 | } | ||
101 | |||
102 | return decrypted; | ||
103 | }; | ||
104 | |||
105 | /** | ||
106 | * The `Decrypter` class that manages decryption of AES | ||
107 | * data through `AsyncStream` objects and the `decrypt` | ||
108 | * function | ||
109 | */ | ||
110 | export class Decrypter { | ||
111 | constructor(encrypted, key, initVector, done) { | ||
112 | let step = Decrypter.STEP; | ||
113 | let encrypted32 = new Int32Array(encrypted.buffer); | ||
114 | let decrypted = new Uint8Array(encrypted.byteLength); | ||
115 | let i = 0; | ||
116 | |||
117 | this.asyncStream_ = new AsyncStream(); | ||
118 | |||
119 | // split up the encryption job and do the individual chunks asynchronously | ||
120 | this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), | ||
121 | key, | ||
122 | initVector, | ||
123 | decrypted)); | ||
124 | for (i = step; i < encrypted32.length; i += step) { | ||
125 | initVector = new Uint32Array([ntoh(encrypted32[i - 4]), | ||
126 | ntoh(encrypted32[i - 3]), | ||
127 | ntoh(encrypted32[i - 2]), | ||
128 | ntoh(encrypted32[i - 1])]); | ||
129 | this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), | ||
130 | key, | ||
131 | initVector, | ||
132 | decrypted)); | ||
133 | } | ||
134 | // invoke the done() callback when everything is finished | ||
135 | this.asyncStream_.push(function() { | ||
136 | // remove pkcs#7 padding from the decrypted bytes | ||
137 | done(null, unpad(decrypted)); | ||
138 | }); | ||
139 | } | ||
140 | decryptChunk_(encrypted, key, initVector, decrypted) { | ||
141 | return function() { | ||
142 | let bytes = decrypt(encrypted, key, initVector); | ||
143 | |||
144 | decrypted.set(bytes, encrypted.byteOffset); | ||
145 | }; | ||
146 | } | ||
147 | } | ||
148 | |||
149 | // the maximum number of bytes to process at one time | ||
150 | // 4 * 8000; | ||
151 | Decrypter.STEP = 32000; | ||
152 | |||
153 | export default { | ||
154 | Decrypter, | ||
155 | decrypt | ||
156 | }; |
src/decrypter/index.js
0 → 100644
1 | /* | ||
2 | * index.js | ||
3 | * | ||
4 | * Index module to easily import the primary components of AES-128 | ||
5 | * decryption. Like this: | ||
6 | * | ||
7 | * ```js | ||
8 | * import {Decrypter, decrypt, AsyncStream} from './src/decrypter'; | ||
9 | * ``` | ||
10 | */ | ||
11 | import {decrypt, Decrypter} from './decrypter'; | ||
12 | import AsyncStream from './async-stream'; | ||
13 | |||
14 | export default { | ||
15 | decrypt, | ||
16 | Decrypter, | ||
17 | AsyncStream | ||
18 | }; |
src/m3u8/index.js
0 → 100644
1 | /** | ||
2 | * Utilities for parsing M3U8 files. If the entire manifest is available, | ||
3 | * `Parser` will create an 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 | |||
9 | import LineStream from './line-stream'; | ||
10 | import ParseStream from './parse-stream'; | ||
11 | import Parser from './parser'; | ||
12 | |||
13 | export default { | ||
14 | LineStream, | ||
15 | ParseStream, | ||
16 | Parser | ||
17 | }; |
src/m3u8/line-stream.js
0 → 100644
1 | import Stream from '../stream'; | ||
2 | /** | ||
3 | * A stream that buffers string input and generates a `data` event for each | ||
4 | * line. | ||
5 | */ | ||
6 | export default class LineStream extends Stream { | ||
7 | constructor() { | ||
8 | super(); | ||
9 | this.buffer = ''; | ||
10 | } | ||
11 | |||
12 | /** | ||
13 | * Add new data to be parsed. | ||
14 | * @param data {string} the text to process | ||
15 | */ | ||
16 | push(data) { | ||
17 | let nextNewline; | ||
18 | |||
19 | this.buffer += data; | ||
20 | nextNewline = this.buffer.indexOf('\n'); | ||
21 | |||
22 | for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { | ||
23 | this.trigger('data', this.buffer.substring(0, nextNewline)); | ||
24 | this.buffer = this.buffer.substring(nextNewline + 1); | ||
25 | } | ||
26 | } | ||
27 | } |
1 | /** | 1 | import Stream from '../stream'; |
2 | * Utilities for parsing M3U8 files. If the entire manifest is available, | ||
3 | * `Parser` will create an 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 | 2 | ||
12 | // "forgiving" attribute list psuedo-grammar: | 3 | // "forgiving" attribute list psuedo-grammar: |
13 | // attributes -> keyvalue (',' keyvalue)* | 4 | // attributes -> keyvalue (',' keyvalue)* |
14 | // keyvalue -> key '=' value | 5 | // keyvalue -> key '=' value |
15 | // key -> [^=]* | 6 | // key -> [^=]* |
16 | // value -> '"' [^"]* '"' | [^,]* | 7 | // value -> '"' [^"]* '"' | [^,]* |
17 | attributeSeparator = (function() { | 8 | const attributeSeparator = function() { |
18 | var | 9 | let key = '[^=]*'; |
19 | key = '[^=]*', | 10 | let value = '"[^"]*"|[^,]*'; |
20 | value = '"[^"]*"|[^,]*', | 11 | let keyvalue = '(?:' + key + ')=(?:' + value + ')'; |
21 | keyvalue = '(?:' + key + ')=(?:' + value + ')'; | ||
22 | 12 | ||
23 | return new RegExp('(?:^|,)(' + keyvalue + ')'); | 13 | return new RegExp('(?:^|,)(' + keyvalue + ')'); |
24 | })(), | 14 | }; |
25 | parseAttributes = function(attributes) { | 15 | |
26 | var | 16 | const parseAttributes = function(attributes) { |
27 | // split the string using attributes as the separator | 17 | // split the string using attributes as the separator |
28 | attrs = attributes.split(attributeSeparator), | 18 | let attrs = attributes.split(attributeSeparator()); |
29 | i = attrs.length, | 19 | let i = attrs.length; |
30 | result = {}, | 20 | let result = {}; |
31 | attr; | 21 | let attr; |
32 | 22 | ||
33 | while (i--) { | 23 | while (i--) { |
34 | // filter out unmatched portions of the string | 24 | // filter out unmatched portions of the string |
... | @@ -37,7 +27,7 @@ | ... | @@ -37,7 +27,7 @@ |
37 | } | 27 | } |
38 | 28 | ||
39 | // split the key and value | 29 | // split the key and value |
40 | attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); | 30 | attr = (/([^=]*)=(.*)/).exec(attrs[i]).slice(1); |
41 | // trim whitespace and remove optional quotes around the value | 31 | // trim whitespace and remove optional quotes around the value |
42 | attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); | 32 | attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); |
43 | attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); | 33 | attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); |
... | @@ -45,39 +35,9 @@ | ... | @@ -45,39 +35,9 @@ |
45 | result[attr[0]] = attr[1]; | 35 | result[attr[0]] = attr[1]; |
46 | } | 36 | } |
47 | return result; | 37 | return result; |
48 | }, | 38 | }; |
49 | Stream = videojs.Hls.Stream, | ||
50 | LineStream, | ||
51 | ParseStream, | ||
52 | Parser; | ||
53 | |||
54 | /** | ||
55 | * A stream that buffers string input and generates a `data` event for each | ||
56 | * line. | ||
57 | */ | ||
58 | LineStream = function() { | ||
59 | var buffer = ''; | ||
60 | LineStream.prototype.init.call(this); | ||
61 | 39 | ||
62 | /** | 40 | /** |
63 | * Add new data to be parsed. | ||
64 | * @param data {string} the text to process | ||
65 | */ | ||
66 | this.push = function(data) { | ||
67 | var nextNewline; | ||
68 | |||
69 | buffer += data; | ||
70 | nextNewline = buffer.indexOf('\n'); | ||
71 | |||
72 | for (; nextNewline > -1; nextNewline = buffer.indexOf('\n')) { | ||
73 | this.trigger('data', buffer.substring(0, nextNewline)); | ||
74 | buffer = buffer.substring(nextNewline + 1); | ||
75 | } | ||
76 | }; | ||
77 | }; | ||
78 | LineStream.prototype = new Stream(); | ||
79 | |||
80 | /** | ||
81 | * A line-level M3U8 parser event stream. It expects to receive input one | 41 | * A line-level M3U8 parser event stream. It expects to receive input one |
82 | * line at a time and performs a context-free parse of its contents. A stream | 42 | * line at a time and performs a context-free parse of its contents. A stream |
83 | * interpretation of a manifest can be useful if the manifest is expected to | 43 | * interpretation of a manifest can be useful if the manifest is expected to |
... | @@ -98,18 +58,20 @@ | ... | @@ -98,18 +58,20 @@ |
98 | * tags are given the tag type `unknown` and a single additional property | 58 | * tags are given the tag type `unknown` and a single additional property |
99 | * `data` with the remainder of the input. | 59 | * `data` with the remainder of the input. |
100 | */ | 60 | */ |
101 | ParseStream = function() { | 61 | export default class ParseStream extends Stream { |
102 | ParseStream.prototype.init.call(this); | 62 | constructor() { |
103 | }; | 63 | super(); |
104 | ParseStream.prototype = new Stream(); | 64 | } |
65 | |||
105 | /** | 66 | /** |
106 | * Parses an additional line of input. | 67 | * Parses an additional line of input. |
107 | * @param line {string} a single line of an M3U8 file to parse | 68 | * @param line {string} a single line of an M3U8 file to parse |
108 | */ | 69 | */ |
109 | ParseStream.prototype.push = function(line) { | 70 | push(line) { |
110 | var match, event; | 71 | let match; |
72 | let event; | ||
111 | 73 | ||
112 | //strip whitespace | 74 | // strip whitespace |
113 | line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, ''); | 75 | line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, ''); |
114 | if (line.length === 0) { | 76 | if (line.length === 0) { |
115 | // ignore empty lines | 77 | // ignore empty lines |
... | @@ -134,12 +96,12 @@ | ... | @@ -134,12 +96,12 @@ |
134 | return; | 96 | return; |
135 | } | 97 | } |
136 | 98 | ||
137 | //strip off any carriage returns here so the regex matching | 99 | // strip off any carriage returns here so the regex matching |
138 | //doesn't have to account for them. | 100 | // doesn't have to account for them. |
139 | line = line.replace('\r',''); | 101 | line = line.replace('\r', ''); |
140 | 102 | ||
141 | // Tags | 103 | // Tags |
142 | match = /^#EXTM3U/.exec(line); | 104 | match = (/^#EXTM3U/).exec(line); |
143 | if (match) { | 105 | if (match) { |
144 | this.trigger('data', { | 106 | this.trigger('data', { |
145 | type: 'tag', | 107 | type: 'tag', |
... | @@ -271,10 +233,9 @@ | ... | @@ -271,10 +233,9 @@ |
271 | event.attributes = parseAttributes(match[1]); | 233 | event.attributes = parseAttributes(match[1]); |
272 | 234 | ||
273 | if (event.attributes.RESOLUTION) { | 235 | if (event.attributes.RESOLUTION) { |
274 | (function() { | 236 | let split = event.attributes.RESOLUTION.split('x'); |
275 | var | 237 | let resolution = {}; |
276 | split = event.attributes.RESOLUTION.split('x'), | 238 | |
277 | resolution = {}; | ||
278 | if (split[0]) { | 239 | if (split[0]) { |
279 | resolution.width = parseInt(split[0], 10); | 240 | resolution.width = parseInt(split[0], 10); |
280 | } | 241 | } |
... | @@ -282,7 +243,6 @@ | ... | @@ -282,7 +243,6 @@ |
282 | resolution.height = parseInt(split[1], 10); | 243 | resolution.height = parseInt(split[1], 10); |
283 | } | 244 | } |
284 | event.attributes.RESOLUTION = resolution; | 245 | event.attributes.RESOLUTION = resolution; |
285 | })(); | ||
286 | } | 246 | } |
287 | if (event.attributes.BANDWIDTH) { | 247 | if (event.attributes.BANDWIDTH) { |
288 | event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); | 248 | event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); |
... | @@ -320,7 +280,7 @@ | ... | @@ -320,7 +280,7 @@ |
320 | event.attributes = parseAttributes(match[1]); | 280 | event.attributes = parseAttributes(match[1]); |
321 | // parse the IV string into a Uint32Array | 281 | // parse the IV string into a Uint32Array |
322 | if (event.attributes.IV) { | 282 | if (event.attributes.IV) { |
323 | if (event.attributes.IV.substring(0,2) === '0x') { | 283 | if (event.attributes.IV.substring(0, 2) === '0x') { |
324 | event.attributes.IV = event.attributes.IV.substring(2); | 284 | event.attributes.IV = event.attributes.IV.substring(2); |
325 | } | 285 | } |
326 | 286 | ||
... | @@ -341,248 +301,5 @@ | ... | @@ -341,248 +301,5 @@ |
341 | type: 'tag', | 301 | type: 'tag', |
342 | data: line.slice(4, line.length) | 302 | data: line.slice(4, line.length) |
343 | }); | 303 | }); |
344 | }; | ||
345 | |||
346 | /** | ||
347 | * A parser for M3U8 files. The current interpretation of the input is | ||
348 | * exposed as a property `manifest` on parser objects. It's just two lines to | ||
349 | * create and parse a manifest once you have the contents available as a string: | ||
350 | * | ||
351 | * ```js | ||
352 | * var parser = new videojs.m3u8.Parser(); | ||
353 | * parser.push(xhr.responseText); | ||
354 | * ``` | ||
355 | * | ||
356 | * New input can later be applied to update the manifest object by calling | ||
357 | * `push` again. | ||
358 | * | ||
359 | * The parser attempts to create a usable manifest object even if the | ||
360 | * underlying input is somewhat nonsensical. It emits `info` and `warning` | ||
361 | * events during the parse if it encounters input that seems invalid or | ||
362 | * requires some property of the manifest object to be defaulted. | ||
363 | */ | ||
364 | Parser = function() { | ||
365 | var | ||
366 | self = this, | ||
367 | uris = [], | ||
368 | currentUri = {}, | ||
369 | key; | ||
370 | Parser.prototype.init.call(this); | ||
371 | |||
372 | this.lineStream = new LineStream(); | ||
373 | this.parseStream = new ParseStream(); | ||
374 | this.lineStream.pipe(this.parseStream); | ||
375 | |||
376 | // the manifest is empty until the parse stream begins delivering data | ||
377 | this.manifest = { | ||
378 | allowCache: true, | ||
379 | discontinuityStarts: [] | ||
380 | }; | ||
381 | |||
382 | // update the manifest with the m3u8 entry from the parse stream | ||
383 | this.parseStream.on('data', function(entry) { | ||
384 | ({ | ||
385 | tag: function() { | ||
386 | // switch based on the tag type | ||
387 | (({ | ||
388 | 'allow-cache': function() { | ||
389 | this.manifest.allowCache = entry.allowed; | ||
390 | if (!('allowed' in entry)) { | ||
391 | this.trigger('info', { | ||
392 | message: 'defaulting allowCache to YES' | ||
393 | }); | ||
394 | this.manifest.allowCache = true; | ||
395 | } | ||
396 | }, | ||
397 | 'byterange': function() { | ||
398 | var byterange = {}; | ||
399 | if ('length' in entry) { | ||
400 | currentUri.byterange = byterange; | ||
401 | byterange.length = entry.length; | ||
402 | |||
403 | if (!('offset' in entry)) { | ||
404 | this.trigger('info', { | ||
405 | message: 'defaulting offset to zero' | ||
406 | }); | ||
407 | entry.offset = 0; | ||
408 | } | ||
409 | } | ||
410 | if ('offset' in entry) { | ||
411 | currentUri.byterange = byterange; | ||
412 | byterange.offset = entry.offset; | ||
413 | } | ||
414 | }, | ||
415 | 'endlist': function() { | ||
416 | this.manifest.endList = true; | ||
417 | }, | ||
418 | 'inf': function() { | ||
419 | if (!('mediaSequence' in this.manifest)) { | ||
420 | this.manifest.mediaSequence = 0; | ||
421 | this.trigger('info', { | ||
422 | message: 'defaulting media sequence to zero' | ||
423 | }); | ||
424 | } | ||
425 | if (!('discontinuitySequence' in this.manifest)) { | ||
426 | this.manifest.discontinuitySequence = 0; | ||
427 | this.trigger('info', { | ||
428 | message: 'defaulting discontinuity sequence to zero' | ||
429 | }); | ||
430 | } | ||
431 | if (entry.duration >= 0) { | ||
432 | currentUri.duration = entry.duration; | ||
433 | } | ||
434 | |||
435 | this.manifest.segments = uris; | ||
436 | |||
437 | }, | ||
438 | 'key': function() { | ||
439 | if (!entry.attributes) { | ||
440 | this.trigger('warn', { | ||
441 | message: 'ignoring key declaration without attribute list' | ||
442 | }); | ||
443 | return; | ||
444 | } | ||
445 | // clear the active encryption key | ||
446 | if (entry.attributes.METHOD === 'NONE') { | ||
447 | key = null; | ||
448 | return; | ||
449 | } | ||
450 | if (!entry.attributes.URI) { | ||
451 | this.trigger('warn', { | ||
452 | message: 'ignoring key declaration without URI' | ||
453 | }); | ||
454 | return; | ||
455 | } | ||
456 | if (!entry.attributes.METHOD) { | ||
457 | this.trigger('warn', { | ||
458 | message: 'defaulting key method to AES-128' | ||
459 | }); | ||
460 | } | ||
461 | |||
462 | // setup an encryption key for upcoming segments | ||
463 | key = { | ||
464 | method: entry.attributes.METHOD || 'AES-128', | ||
465 | uri: entry.attributes.URI | ||
466 | }; | ||
467 | |||
468 | if (entry.attributes.IV !== undefined) { | ||
469 | key.iv = entry.attributes.IV; | ||
470 | } | ||
471 | }, | ||
472 | 'media-sequence': function() { | ||
473 | if (!isFinite(entry.number)) { | ||
474 | this.trigger('warn', { | ||
475 | message: 'ignoring invalid media sequence: ' + entry.number | ||
476 | }); | ||
477 | return; | ||
478 | } | 304 | } |
479 | this.manifest.mediaSequence = entry.number; | 305 | } |
480 | }, | ||
481 | 'discontinuity-sequence': function() { | ||
482 | if (!isFinite(entry.number)) { | ||
483 | this.trigger('warn', { | ||
484 | message: 'ignoring invalid discontinuity sequence: ' + entry.number | ||
485 | }); | ||
486 | return; | ||
487 | } | ||
488 | this.manifest.discontinuitySequence = entry.number; | ||
489 | }, | ||
490 | 'playlist-type': function() { | ||
491 | if (!(/VOD|EVENT/).test(entry.playlistType)) { | ||
492 | this.trigger('warn', { | ||
493 | message: 'ignoring unknown playlist type: ' + entry.playlist | ||
494 | }); | ||
495 | return; | ||
496 | } | ||
497 | this.manifest.playlistType = entry.playlistType; | ||
498 | }, | ||
499 | 'stream-inf': function() { | ||
500 | this.manifest.playlists = uris; | ||
501 | |||
502 | if (!entry.attributes) { | ||
503 | this.trigger('warn', { | ||
504 | message: 'ignoring empty stream-inf attributes' | ||
505 | }); | ||
506 | return; | ||
507 | } | ||
508 | |||
509 | if (!currentUri.attributes) { | ||
510 | currentUri.attributes = {}; | ||
511 | } | ||
512 | currentUri.attributes = mergeOptions(currentUri.attributes, | ||
513 | entry.attributes); | ||
514 | }, | ||
515 | 'discontinuity': function() { | ||
516 | currentUri.discontinuity = true; | ||
517 | this.manifest.discontinuityStarts.push(uris.length); | ||
518 | }, | ||
519 | 'targetduration': function() { | ||
520 | if (!isFinite(entry.duration) || entry.duration < 0) { | ||
521 | this.trigger('warn', { | ||
522 | message: 'ignoring invalid target duration: ' + entry.duration | ||
523 | }); | ||
524 | return; | ||
525 | } | ||
526 | this.manifest.targetDuration = entry.duration; | ||
527 | }, | ||
528 | 'totalduration': function() { | ||
529 | if (!isFinite(entry.duration) || entry.duration < 0) { | ||
530 | this.trigger('warn', { | ||
531 | message: 'ignoring invalid total duration: ' + entry.duration | ||
532 | }); | ||
533 | return; | ||
534 | } | ||
535 | this.manifest.totalDuration = entry.duration; | ||
536 | } | ||
537 | })[entry.tagType] || noop).call(self); | ||
538 | }, | ||
539 | uri: function() { | ||
540 | currentUri.uri = entry.uri; | ||
541 | uris.push(currentUri); | ||
542 | |||
543 | // if no explicit duration was declared, use the target duration | ||
544 | if (this.manifest.targetDuration && | ||
545 | !('duration' in currentUri)) { | ||
546 | this.trigger('warn', { | ||
547 | message: 'defaulting segment duration to the target duration' | ||
548 | }); | ||
549 | currentUri.duration = this.manifest.targetDuration; | ||
550 | } | ||
551 | // annotate with encryption information, if necessary | ||
552 | if (key) { | ||
553 | currentUri.key = key; | ||
554 | } | ||
555 | |||
556 | // prepare for the next URI | ||
557 | currentUri = {}; | ||
558 | }, | ||
559 | comment: function() { | ||
560 | // comments are not important for playback | ||
561 | } | ||
562 | })[entry.type].call(self); | ||
563 | }); | ||
564 | }; | ||
565 | Parser.prototype = new Stream(); | ||
566 | /** | ||
567 | * Parse the input string and update the manifest object. | ||
568 | * @param chunk {string} a potentially incomplete portion of the manifest | ||
569 | */ | ||
570 | Parser.prototype.push = function(chunk) { | ||
571 | this.lineStream.push(chunk); | ||
572 | }; | ||
573 | /** | ||
574 | * Flush any remaining input. This can be handy if the last line of an M3U8 | ||
575 | * manifest did not contain a trailing newline but the file has been | ||
576 | * completely received. | ||
577 | */ | ||
578 | Parser.prototype.end = function() { | ||
579 | // flush any buffered input | ||
580 | this.lineStream.push('\n'); | ||
581 | }; | ||
582 | |||
583 | window.videojs.m3u8 = { | ||
584 | LineStream: LineStream, | ||
585 | ParseStream: ParseStream, | ||
586 | Parser: Parser | ||
587 | }; | ||
588 | })(window.videojs, window.parseInt, window.isFinite, window.videojs.mergeOptions); | ... | ... |
src/m3u8/parser.js
0 → 100644
1 | import Stream from '../stream' ; | ||
2 | import LineStream from './line-stream'; | ||
3 | import ParseStream from './parse-stream'; | ||
4 | import {mergeOptions} from 'video.js'; | ||
5 | |||
6 | /** | ||
7 | * A parser for M3U8 files. The current interpretation of the input is | ||
8 | * exposed as a property `manifest` on parser objects. It's just two lines to | ||
9 | * create and parse a manifest once you have the contents available as a string: | ||
10 | * | ||
11 | * ```js | ||
12 | * var parser = new videojs.m3u8.Parser(); | ||
13 | * parser.push(xhr.responseText); | ||
14 | * ``` | ||
15 | * | ||
16 | * New input can later be applied to update the manifest object by calling | ||
17 | * `push` again. | ||
18 | * | ||
19 | * The parser attempts to create a usable manifest object even if the | ||
20 | * underlying input is somewhat nonsensical. It emits `info` and `warning` | ||
21 | * events during the parse if it encounters input that seems invalid or | ||
22 | * requires some property of the manifest object to be defaulted. | ||
23 | */ | ||
24 | export default class Parser extends Stream { | ||
25 | constructor() { | ||
26 | super(); | ||
27 | this.lineStream = new LineStream(); | ||
28 | this.parseStream = new ParseStream(); | ||
29 | this.lineStream.pipe(this.parseStream); | ||
30 | /* eslint-disable consistent-this */ | ||
31 | let self = this; | ||
32 | /* eslint-enable consistent-this */ | ||
33 | let uris = []; | ||
34 | let currentUri = {}; | ||
35 | let key; | ||
36 | let noop = function() {}; | ||
37 | |||
38 | // the manifest is empty until the parse stream begins delivering data | ||
39 | this.manifest = { | ||
40 | allowCache: true, | ||
41 | discontinuityStarts: [] | ||
42 | }; | ||
43 | |||
44 | // update the manifest with the m3u8 entry from the parse stream | ||
45 | this.parseStream.on('data', function(entry) { | ||
46 | ({ | ||
47 | tag() { | ||
48 | // switch based on the tag type | ||
49 | (({ | ||
50 | 'allow-cache'() { | ||
51 | this.manifest.allowCache = entry.allowed; | ||
52 | if (!('allowed' in entry)) { | ||
53 | this.trigger('info', { | ||
54 | message: 'defaulting allowCache to YES' | ||
55 | }); | ||
56 | this.manifest.allowCache = true; | ||
57 | } | ||
58 | }, | ||
59 | byterange() { | ||
60 | let byterange = {}; | ||
61 | |||
62 | if ('length' in entry) { | ||
63 | currentUri.byterange = byterange; | ||
64 | byterange.length = entry.length; | ||
65 | |||
66 | if (!('offset' in entry)) { | ||
67 | this.trigger('info', { | ||
68 | message: 'defaulting offset to zero' | ||
69 | }); | ||
70 | entry.offset = 0; | ||
71 | } | ||
72 | } | ||
73 | if ('offset' in entry) { | ||
74 | currentUri.byterange = byterange; | ||
75 | byterange.offset = entry.offset; | ||
76 | } | ||
77 | }, | ||
78 | endlist() { | ||
79 | this.manifest.endList = true; | ||
80 | }, | ||
81 | inf() { | ||
82 | if (!('mediaSequence' in this.manifest)) { | ||
83 | this.manifest.mediaSequence = 0; | ||
84 | this.trigger('info', { | ||
85 | message: 'defaulting media sequence to zero' | ||
86 | }); | ||
87 | } | ||
88 | if (!('discontinuitySequence' in this.manifest)) { | ||
89 | this.manifest.discontinuitySequence = 0; | ||
90 | this.trigger('info', { | ||
91 | message: 'defaulting discontinuity sequence to zero' | ||
92 | }); | ||
93 | } | ||
94 | if (entry.duration >= 0) { | ||
95 | currentUri.duration = entry.duration; | ||
96 | } | ||
97 | |||
98 | this.manifest.segments = uris; | ||
99 | |||
100 | }, | ||
101 | key() { | ||
102 | if (!entry.attributes) { | ||
103 | this.trigger('warn', { | ||
104 | message: 'ignoring key declaration without attribute list' | ||
105 | }); | ||
106 | return; | ||
107 | } | ||
108 | // clear the active encryption key | ||
109 | if (entry.attributes.METHOD === 'NONE') { | ||
110 | key = null; | ||
111 | return; | ||
112 | } | ||
113 | if (!entry.attributes.URI) { | ||
114 | this.trigger('warn', { | ||
115 | message: 'ignoring key declaration without URI' | ||
116 | }); | ||
117 | return; | ||
118 | } | ||
119 | if (!entry.attributes.METHOD) { | ||
120 | this.trigger('warn', { | ||
121 | message: 'defaulting key method to AES-128' | ||
122 | }); | ||
123 | } | ||
124 | |||
125 | // setup an encryption key for upcoming segments | ||
126 | key = { | ||
127 | method: entry.attributes.METHOD || 'AES-128', | ||
128 | uri: entry.attributes.URI | ||
129 | }; | ||
130 | |||
131 | if (typeof entry.attributes.IV !== 'undefined') { | ||
132 | key.iv = entry.attributes.IV; | ||
133 | } | ||
134 | }, | ||
135 | 'media-sequence'() { | ||
136 | if (!isFinite(entry.number)) { | ||
137 | this.trigger('warn', { | ||
138 | message: 'ignoring invalid media sequence: ' + entry.number | ||
139 | }); | ||
140 | return; | ||
141 | } | ||
142 | this.manifest.mediaSequence = entry.number; | ||
143 | }, | ||
144 | 'discontinuity-sequence'() { | ||
145 | if (!isFinite(entry.number)) { | ||
146 | this.trigger('warn', { | ||
147 | message: 'ignoring invalid discontinuity sequence: ' + entry.number | ||
148 | }); | ||
149 | return; | ||
150 | } | ||
151 | this.manifest.discontinuitySequence = entry.number; | ||
152 | }, | ||
153 | 'playlist-type'() { | ||
154 | if (!(/VOD|EVENT/).test(entry.playlistType)) { | ||
155 | this.trigger('warn', { | ||
156 | message: 'ignoring unknown playlist type: ' + entry.playlist | ||
157 | }); | ||
158 | return; | ||
159 | } | ||
160 | this.manifest.playlistType = entry.playlistType; | ||
161 | }, | ||
162 | 'stream-inf'() { | ||
163 | this.manifest.playlists = uris; | ||
164 | |||
165 | if (!entry.attributes) { | ||
166 | this.trigger('warn', { | ||
167 | message: 'ignoring empty stream-inf attributes' | ||
168 | }); | ||
169 | return; | ||
170 | } | ||
171 | |||
172 | if (!currentUri.attributes) { | ||
173 | currentUri.attributes = {}; | ||
174 | } | ||
175 | currentUri.attributes = mergeOptions(currentUri.attributes, | ||
176 | entry.attributes); | ||
177 | }, | ||
178 | discontinuity() { | ||
179 | currentUri.discontinuity = true; | ||
180 | this.manifest.discontinuityStarts.push(uris.length); | ||
181 | }, | ||
182 | targetduration() { | ||
183 | if (!isFinite(entry.duration) || entry.duration < 0) { | ||
184 | this.trigger('warn', { | ||
185 | message: 'ignoring invalid target duration: ' + entry.duration | ||
186 | }); | ||
187 | return; | ||
188 | } | ||
189 | this.manifest.targetDuration = entry.duration; | ||
190 | }, | ||
191 | totalduration() { | ||
192 | if (!isFinite(entry.duration) || entry.duration < 0) { | ||
193 | this.trigger('warn', { | ||
194 | message: 'ignoring invalid total duration: ' + entry.duration | ||
195 | }); | ||
196 | return; | ||
197 | } | ||
198 | this.manifest.totalDuration = entry.duration; | ||
199 | } | ||
200 | })[entry.tagType] || noop).call(self); | ||
201 | }, | ||
202 | uri() { | ||
203 | currentUri.uri = entry.uri; | ||
204 | uris.push(currentUri); | ||
205 | |||
206 | // if no explicit duration was declared, use the target duration | ||
207 | if (this.manifest.targetDuration && | ||
208 | !('duration' in currentUri)) { | ||
209 | this.trigger('warn', { | ||
210 | message: 'defaulting segment duration to the target duration' | ||
211 | }); | ||
212 | currentUri.duration = this.manifest.targetDuration; | ||
213 | } | ||
214 | // annotate with encryption information, if necessary | ||
215 | if (key) { | ||
216 | currentUri.key = key; | ||
217 | } | ||
218 | |||
219 | // prepare for the next URI | ||
220 | currentUri = {}; | ||
221 | }, | ||
222 | comment() { | ||
223 | // comments are not important for playback | ||
224 | } | ||
225 | })[entry.type].call(self); | ||
226 | }); | ||
227 | |||
228 | } | ||
229 | |||
230 | /** | ||
231 | * Parse the input string and update the manifest object. | ||
232 | * @param chunk {string} a potentially incomplete portion of the manifest | ||
233 | */ | ||
234 | push(chunk) { | ||
235 | this.lineStream.push(chunk); | ||
236 | } | ||
237 | |||
238 | /** | ||
239 | * Flush any remaining input. This can be handy if the last line of an M3U8 | ||
240 | * manifest did not contain a trailing newline but the file has been | ||
241 | * completely received. | ||
242 | */ | ||
243 | end() { | ||
244 | // flush any buffered input | ||
245 | this.lineStream.push('\n'); | ||
246 | } | ||
247 | |||
248 | } | ||
249 |
... | @@ -5,14 +5,13 @@ | ... | @@ -5,14 +5,13 @@ |
5 | * M3U8 playlists. | 5 | * M3U8 playlists. |
6 | * | 6 | * |
7 | */ | 7 | */ |
8 | (function(window, videojs) { | 8 | import resolveUrl from './resolve-url'; |
9 | 'use strict'; | 9 | import XhrModule from './xhr'; |
10 | var | 10 | import {mergeOptions} from 'video.js'; |
11 | resolveUrl = videojs.Hls.resolveUrl, | 11 | import Stream from './stream'; |
12 | xhr = videojs.Hls.xhr, | 12 | import m3u8 from './m3u8'; |
13 | mergeOptions = videojs.mergeOptions, | ||
14 | 13 | ||
15 | /** | 14 | /** |
16 | * Returns a new master playlist that is the result of merging an | 15 | * Returns a new master playlist that is the result of merging an |
17 | * updated media playlist into the original version. If the | 16 | * updated media playlist into the original version. If the |
18 | * updated media playlist does not match any of the playlist | 17 | * updated media playlist does not match any of the playlist |
... | @@ -23,14 +22,12 @@ | ... | @@ -23,14 +22,12 @@ |
23 | * master playlist with the updated media playlist merged in, or | 22 | * master playlist with the updated media playlist merged in, or |
24 | * null if the merge produced no change. | 23 | * null if the merge produced no change. |
25 | */ | 24 | */ |
26 | updateMaster = function(master, media) { | 25 | const updateMaster = function(master, media) { |
27 | var | 26 | let changed = false; |
28 | changed = false, | 27 | let result = mergeOptions(master, {}); |
29 | result = mergeOptions(master, {}), | 28 | let i = master.playlists.length; |
30 | i, | 29 | let playlist; |
31 | playlist; | 30 | |
32 | |||
33 | i = master.playlists.length; | ||
34 | while (i--) { | 31 | while (i--) { |
35 | playlist = result.playlists[i]; | 32 | playlist = result.playlists[i]; |
36 | if (playlist.uri === media.uri) { | 33 | if (playlist.uri === media.uri) { |
... | @@ -51,15 +48,16 @@ | ... | @@ -51,15 +48,16 @@ |
51 | if (playlist.segments) { | 48 | if (playlist.segments) { |
52 | result.playlists[i].segments = updateSegments(playlist.segments, | 49 | result.playlists[i].segments = updateSegments(playlist.segments, |
53 | media.segments, | 50 | media.segments, |
54 | media.mediaSequence - playlist.mediaSequence); | 51 | media.mediaSequence - |
52 | playlist.mediaSequence); | ||
55 | } | 53 | } |
56 | changed = true; | 54 | changed = true; |
57 | } | 55 | } |
58 | } | 56 | } |
59 | return changed ? result : null; | 57 | return changed ? result : null; |
60 | }, | 58 | }; |
61 | 59 | ||
62 | /** | 60 | /** |
63 | * Returns a new array of segments that is the result of merging | 61 | * Returns a new array of segments that is the result of merging |
64 | * properties from an older list of segments onto an updated | 62 | * properties from an older list of segments onto an updated |
65 | * list. No properties on the updated playlist will be overridden. | 63 | * list. No properties on the updated playlist will be overridden. |
... | @@ -73,8 +71,11 @@ | ... | @@ -73,8 +71,11 @@ |
73 | * playlists. | 71 | * playlists. |
74 | * @return a list of merged segment objects | 72 | * @return a list of merged segment objects |
75 | */ | 73 | */ |
76 | updateSegments = function(original, update, offset) { | 74 | const updateSegments = function(original, update, offset) { |
77 | var result = update.slice(), length, i; | 75 | let result = update.slice(); |
76 | let length; | ||
77 | let i; | ||
78 | |||
78 | offset = offset || 0; | 79 | offset = offset || 0; |
79 | length = Math.min(original.length, update.length + offset); | 80 | length = Math.min(original.length, update.length + offset); |
80 | 81 | ||
... | @@ -82,18 +83,17 @@ | ... | @@ -82,18 +83,17 @@ |
82 | result[i - offset] = mergeOptions(original[i], result[i - offset]); | 83 | result[i - offset] = mergeOptions(original[i], result[i - offset]); |
83 | } | 84 | } |
84 | return result; | 85 | return result; |
85 | }, | 86 | }; |
86 | 87 | ||
87 | PlaylistLoader = function(srcUrl, withCredentials) { | 88 | export default class PlaylistLoader extends Stream { |
88 | var | 89 | constructor(srcUrl, withCredentials) { |
89 | loader = this, | 90 | super(); |
90 | dispose, | 91 | let loader = this; |
91 | mediaUpdateTimeout, | 92 | let dispose; |
92 | request, | 93 | let mediaUpdateTimeout; |
93 | playlistRequestError, | 94 | let request; |
94 | haveMetadata; | 95 | let playlistRequestError; |
95 | 96 | let haveMetadata; | |
96 | PlaylistLoader.prototype.init.call(this); | ||
97 | 97 | ||
98 | // a flag that disables "expired time"-tracking this setting has | 98 | // a flag that disables "expired time"-tracking this setting has |
99 | // no effect when not playing a live stream | 99 | // no effect when not playing a live stream |
... | @@ -127,7 +127,9 @@ | ... | @@ -127,7 +127,9 @@ |
127 | // updated playlist. | 127 | // updated playlist. |
128 | 128 | ||
129 | haveMetadata = function(xhr, url) { | 129 | haveMetadata = function(xhr, url) { |
130 | var parser, refreshDelay, update; | 130 | let parser; |
131 | let refreshDelay; | ||
132 | let update; | ||
131 | 133 | ||
132 | loader.setBandwidth(request || xhr); | 134 | loader.setBandwidth(request || xhr); |
133 | 135 | ||
... | @@ -135,7 +137,7 @@ | ... | @@ -135,7 +137,7 @@ |
135 | request = null; | 137 | request = null; |
136 | loader.state = 'HAVE_METADATA'; | 138 | loader.state = 'HAVE_METADATA'; |
137 | 139 | ||
138 | parser = new videojs.m3u8.Parser(); | 140 | parser = new m3u8.Parser(); |
139 | parser.push(xhr.responseText); | 141 | parser.push(xhr.responseText); |
140 | parser.end(); | 142 | parser.end(); |
141 | parser.manifest.uri = url; | 143 | parser.manifest.uri = url; |
... | @@ -198,7 +200,8 @@ | ... | @@ -198,7 +200,8 @@ |
198 | * object to switch to | 200 | * object to switch to |
199 | */ | 201 | */ |
200 | loader.media = function(playlist) { | 202 | loader.media = function(playlist) { |
201 | var startingState = loader.state, mediaChange; | 203 | let startingState = loader.state; |
204 | let mediaChange; | ||
202 | // getter | 205 | // getter |
203 | if (!playlist) { | 206 | if (!playlist) { |
204 | return loader.media_; | 207 | return loader.media_; |
... | @@ -258,9 +261,9 @@ | ... | @@ -258,9 +261,9 @@ |
258 | } | 261 | } |
259 | 262 | ||
260 | // request the new playlist | 263 | // request the new playlist |
261 | request = xhr({ | 264 | request = XhrModule({ |
262 | uri: resolveUrl(loader.master.uri, playlist.uri), | 265 | uri: resolveUrl(loader.master.uri, playlist.uri), |
263 | withCredentials: withCredentials | 266 | withCredentials |
264 | }, function(error, request) { | 267 | }, function(error, request) { |
265 | if (error) { | 268 | if (error) { |
266 | return playlistRequestError(request, playlist.uri, startingState); | 269 | return playlistRequestError(request, playlist.uri, startingState); |
... | @@ -295,9 +298,9 @@ | ... | @@ -295,9 +298,9 @@ |
295 | } | 298 | } |
296 | 299 | ||
297 | loader.state = 'HAVE_CURRENT_METADATA'; | 300 | loader.state = 'HAVE_CURRENT_METADATA'; |
298 | request = xhr({ | 301 | request = XhrModule({ |
299 | uri: resolveUrl(loader.master.uri, loader.media().uri), | 302 | uri: resolveUrl(loader.master.uri, loader.media().uri), |
300 | withCredentials: withCredentials | 303 | withCredentials |
301 | }, function(error, request) { | 304 | }, function(error, request) { |
302 | if (error) { | 305 | if (error) { |
303 | return playlistRequestError(request, loader.media().uri); | 306 | return playlistRequestError(request, loader.media().uri); |
... | @@ -307,11 +310,12 @@ | ... | @@ -307,11 +310,12 @@ |
307 | }); | 310 | }); |
308 | 311 | ||
309 | // request the specified URL | 312 | // request the specified URL |
310 | request = xhr({ | 313 | request = XhrModule({ |
311 | uri: srcUrl, | 314 | uri: srcUrl, |
312 | withCredentials: withCredentials | 315 | withCredentials |
313 | }, function(error, req) { | 316 | }, function(error, req) { |
314 | var parser, i; | 317 | let parser; |
318 | let i; | ||
315 | 319 | ||
316 | // clear the loader's request reference | 320 | // clear the loader's request reference |
317 | request = null; | 321 | request = null; |
... | @@ -321,12 +325,13 @@ | ... | @@ -321,12 +325,13 @@ |
321 | status: req.status, | 325 | status: req.status, |
322 | message: 'HLS playlist request error at URL: ' + srcUrl, | 326 | message: 'HLS playlist request error at URL: ' + srcUrl, |
323 | responseText: req.responseText, | 327 | responseText: req.responseText, |
324 | code: 2 // MEDIA_ERR_NETWORK | 328 | // MEDIA_ERR_NETWORK |
329 | code: 2 | ||
325 | }; | 330 | }; |
326 | return loader.trigger('error'); | 331 | return loader.trigger('error'); |
327 | } | 332 | } |
328 | 333 | ||
329 | parser = new videojs.m3u8.Parser(); | 334 | parser = new m3u8.Parser(); |
330 | parser.push(req.responseText); | 335 | parser.push(req.responseText); |
331 | parser.end(); | 336 | parser.end(); |
332 | 337 | ||
... | @@ -365,16 +370,16 @@ | ... | @@ -365,16 +370,16 @@ |
365 | haveMetadata(req, srcUrl); | 370 | haveMetadata(req, srcUrl); |
366 | return loader.trigger('loadedmetadata'); | 371 | return loader.trigger('loadedmetadata'); |
367 | }); | 372 | }); |
368 | }; | 373 | } |
369 | PlaylistLoader.prototype = new videojs.Hls.Stream(); | ||
370 | |||
371 | /** | 374 | /** |
372 | * Update the PlaylistLoader state to reflect the changes in an | 375 | * Update the PlaylistLoader state to reflect the changes in an |
373 | * update to the current media playlist. | 376 | * update to the current media playlist. |
374 | * @param update {object} the updated media playlist object | 377 | * @param update {object} the updated media playlist object |
375 | */ | 378 | */ |
376 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { | 379 | updateMediaPlaylist_(update) { |
377 | var outdated, i, segment; | 380 | let outdated; |
381 | let i; | ||
382 | let segment; | ||
378 | 383 | ||
379 | outdated = this.media_; | 384 | outdated = this.media_; |
380 | this.media_ = this.master.playlists[update.uri]; | 385 | this.media_ = this.master.playlists[update.uri]; |
... | @@ -432,7 +437,7 @@ | ... | @@ -432,7 +437,7 @@ |
432 | } | 437 | } |
433 | this.expired_ += segment.duration; | 438 | this.expired_ += segment.duration; |
434 | } | 439 | } |
435 | }; | 440 | } |
436 | 441 | ||
437 | /** | 442 | /** |
438 | * Determine the index of the segment that contains a specified | 443 | * Determine the index of the segment that contains a specified |
... | @@ -452,17 +457,16 @@ | ... | @@ -452,17 +457,16 @@ |
452 | * value will be clamped to the index of the segment containing the | 457 | * value will be clamped to the index of the segment containing the |
453 | * closest playback position that is currently available. | 458 | * closest playback position that is currently available. |
454 | */ | 459 | */ |
455 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { | 460 | getMediaIndexForTime_(time) { |
456 | var | 461 | let i; |
457 | i, | 462 | let segment; |
458 | segment, | 463 | let originalTime = time; |
459 | originalTime = time, | 464 | let numSegments = this.media_.segments.length; |
460 | numSegments = this.media_.segments.length, | 465 | let lastSegment = numSegments - 1; |
461 | lastSegment = numSegments - 1, | 466 | let startIndex; |
462 | startIndex, | 467 | let endIndex; |
463 | endIndex, | 468 | let knownStart; |
464 | knownStart, | 469 | let knownEnd; |
465 | knownEnd; | ||
466 | 470 | ||
467 | if (!this.media_) { | 471 | if (!this.media_) { |
468 | return 0; | 472 | return 0; |
... | @@ -558,7 +562,5 @@ | ... | @@ -558,7 +562,5 @@ |
558 | // the one most likely to tell us something about the timeline | 562 | // the one most likely to tell us something about the timeline |
559 | return lastSegment; | 563 | return lastSegment; |
560 | } | 564 | } |
561 | }; | 565 | } |
562 | 566 | } | |
563 | videojs.Hls.PlaylistLoader = PlaylistLoader; | ||
564 | })(window, window.videojs); | ... | ... |
1 | /** | 1 | /** |
2 | * Playlist related utilities. | 2 | * Playlist related utilities. |
3 | */ | 3 | */ |
4 | (function(window, videojs) { | 4 | import {createTimeRange} from 'video.js'; |
5 | 'use strict'; | ||
6 | 5 | ||
7 | var Playlist = { | 6 | let Playlist = { |
8 | /** | 7 | /** |
9 | * The number of segments that are unsafe to start playback at in | 8 | * The number of segments that are unsafe to start playback at in |
10 | * a live stream. Changing this value can cause playback stalls. | 9 | * a live stream. Changing this value can cause playback stalls. |
... | @@ -12,24 +11,22 @@ | ... | @@ -12,24 +11,22 @@ |
12 | * https://tools.ietf.org/html/draft-pantos-http-live-streaming-18#section-6.3.3 | 11 | * https://tools.ietf.org/html/draft-pantos-http-live-streaming-18#section-6.3.3 |
13 | */ | 12 | */ |
14 | UNSAFE_LIVE_SEGMENTS: 3 | 13 | UNSAFE_LIVE_SEGMENTS: 3 |
15 | }; | 14 | }; |
16 | |||
17 | var duration, intervalDuration, backwardDuration, forwardDuration, seekable; | ||
18 | |||
19 | backwardDuration = function(playlist, endSequence) { | ||
20 | var result = 0, segment, i; | ||
21 | 15 | ||
22 | i = endSequence - playlist.mediaSequence; | 16 | const backwardDuration = function(playlist, endSequence) { |
17 | let result = 0; | ||
18 | let i = endSequence - playlist.mediaSequence; | ||
23 | // if a start time is available for segment immediately following | 19 | // if a start time is available for segment immediately following |
24 | // the interval, use it | 20 | // the interval, use it |
25 | segment = playlist.segments[i]; | 21 | let segment = playlist.segments[i]; |
22 | |||
26 | // Walk backward until we find the latest segment with timeline | 23 | // Walk backward until we find the latest segment with timeline |
27 | // information that is earlier than endSequence | 24 | // information that is earlier than endSequence |
28 | if (segment) { | 25 | if (segment) { |
29 | if (segment.start !== undefined) { | 26 | if (typeof segment.start !== 'undefined') { |
30 | return { result: segment.start, precise: true }; | 27 | return { result: segment.start, precise: true }; |
31 | } | 28 | } |
32 | if (segment.end !== undefined) { | 29 | if (typeof segment.end !== 'undefined') { |
33 | return { | 30 | return { |
34 | result: segment.end - segment.duration, | 31 | result: segment.end - segment.duration, |
35 | precise: true | 32 | precise: true |
... | @@ -38,28 +35,29 @@ | ... | @@ -38,28 +35,29 @@ |
38 | } | 35 | } |
39 | while (i--) { | 36 | while (i--) { |
40 | segment = playlist.segments[i]; | 37 | segment = playlist.segments[i]; |
41 | if (segment.end !== undefined) { | 38 | if (typeof segment.end !== 'undefined') { |
42 | return { result: result + segment.end, precise: true }; | 39 | return { result: result + segment.end, precise: true }; |
43 | } | 40 | } |
44 | 41 | ||
45 | result += segment.duration; | 42 | result += segment.duration; |
46 | 43 | ||
47 | if (segment.start !== undefined) { | 44 | if (typeof segment.start !== 'undefined') { |
48 | return { result: result + segment.start, precise: true }; | 45 | return { result: result + segment.start, precise: true }; |
49 | } | 46 | } |
50 | } | 47 | } |
51 | return { result: result, precise: false }; | 48 | return { result, precise: false }; |
52 | }; | 49 | }; |
53 | |||
54 | forwardDuration = function(playlist, endSequence) { | ||
55 | var result = 0, segment, i; | ||
56 | 50 | ||
57 | i = endSequence - playlist.mediaSequence; | 51 | const forwardDuration = function(playlist, endSequence) { |
52 | let result = 0; | ||
53 | let segment; | ||
54 | let i = endSequence - playlist.mediaSequence; | ||
58 | // Walk forward until we find the earliest segment with timeline | 55 | // Walk forward until we find the earliest segment with timeline |
59 | // information | 56 | // information |
57 | |||
60 | for (; i < playlist.segments.length; i++) { | 58 | for (; i < playlist.segments.length; i++) { |
61 | segment = playlist.segments[i]; | 59 | segment = playlist.segments[i]; |
62 | if (segment.start !== undefined) { | 60 | if (typeof segment.start !== 'undefined') { |
63 | return { | 61 | return { |
64 | result: segment.start - result, | 62 | result: segment.start - result, |
65 | precise: true | 63 | precise: true |
... | @@ -68,7 +66,7 @@ | ... | @@ -68,7 +66,7 @@ |
68 | 66 | ||
69 | result += segment.duration; | 67 | result += segment.duration; |
70 | 68 | ||
71 | if (segment.end !== undefined) { | 69 | if (typeof segment.end !== 'undefined') { |
72 | return { | 70 | return { |
73 | result: segment.end - result, | 71 | result: segment.end - result, |
74 | precise: true | 72 | precise: true |
... | @@ -78,9 +76,9 @@ | ... | @@ -78,9 +76,9 @@ |
78 | } | 76 | } |
79 | // indicate we didn't find a useful duration estimate | 77 | // indicate we didn't find a useful duration estimate |
80 | return { result: -1, precise: false }; | 78 | return { result: -1, precise: false }; |
81 | }; | 79 | }; |
82 | 80 | ||
83 | /** | 81 | /** |
84 | * Calculate the media duration from the segments associated with a | 82 | * Calculate the media duration from the segments associated with a |
85 | * playlist. The duration of a subinterval of the available segments | 83 | * playlist. The duration of a subinterval of the available segments |
86 | * may be calculated by specifying an end index. | 84 | * may be calculated by specifying an end index. |
... | @@ -91,10 +89,11 @@ | ... | @@ -91,10 +89,11 @@ |
91 | * @return {number} the duration between the first available segment | 89 | * @return {number} the duration between the first available segment |
92 | * and end index. | 90 | * and end index. |
93 | */ | 91 | */ |
94 | intervalDuration = function(playlist, endSequence) { | 92 | const intervalDuration = function(playlist, endSequence) { |
95 | var backward, forward; | 93 | let backward; |
94 | let forward; | ||
96 | 95 | ||
97 | if (endSequence === undefined) { | 96 | if (typeof endSequence === 'undefined') { |
98 | endSequence = playlist.mediaSequence + playlist.segments.length; | 97 | endSequence = playlist.mediaSequence + playlist.segments.length; |
99 | } | 98 | } |
100 | 99 | ||
... | @@ -122,9 +121,9 @@ | ... | @@ -122,9 +121,9 @@ |
122 | 121 | ||
123 | // return the less-precise, playlist-based duration estimate | 122 | // return the less-precise, playlist-based duration estimate |
124 | return backward.result; | 123 | return backward.result; |
125 | }; | 124 | }; |
126 | 125 | ||
127 | /** | 126 | /** |
128 | * Calculates the duration of a playlist. If a start and end index | 127 | * Calculates the duration of a playlist. If a start and end index |
129 | * are specified, the duration will be for the subset of the media | 128 | * are specified, the duration will be for the subset of the media |
130 | * timeline between those two indices. The total duration for live | 129 | * timeline between those two indices. The total duration for live |
... | @@ -139,18 +138,18 @@ | ... | @@ -139,18 +138,18 @@ |
139 | * @return {number} the duration between the start index and end | 138 | * @return {number} the duration between the start index and end |
140 | * index. | 139 | * index. |
141 | */ | 140 | */ |
142 | duration = function(playlist, endSequence, includeTrailingTime) { | 141 | export const duration = function(playlist, endSequence, includeTrailingTime) { |
143 | if (!playlist) { | 142 | if (!playlist) { |
144 | return 0; | 143 | return 0; |
145 | } | 144 | } |
146 | 145 | ||
147 | if (includeTrailingTime === undefined) { | 146 | if (typeof includeTrailingTime === 'undefined') { |
148 | includeTrailingTime = true; | 147 | includeTrailingTime = true; |
149 | } | 148 | } |
150 | 149 | ||
151 | // if a slice of the total duration is not requested, use | 150 | // if a slice of the total duration is not requested, use |
152 | // playlist-level duration indicators when they're present | 151 | // playlist-level duration indicators when they're present |
153 | if (endSequence === undefined) { | 152 | if (typeof endSequence === 'undefined') { |
154 | // if present, use the duration specified in the playlist | 153 | // if present, use the duration specified in the playlist |
155 | if (playlist.totalDuration) { | 154 | if (playlist.totalDuration) { |
156 | return playlist.totalDuration; | 155 | return playlist.totalDuration; |
... | @@ -166,9 +165,9 @@ | ... | @@ -166,9 +165,9 @@ |
166 | return intervalDuration(playlist, | 165 | return intervalDuration(playlist, |
167 | endSequence, | 166 | endSequence, |
168 | includeTrailingTime); | 167 | includeTrailingTime); |
169 | }; | 168 | }; |
170 | 169 | ||
171 | /** | 170 | /** |
172 | * Calculates the interval of time that is currently seekable in a | 171 | * Calculates the interval of time that is currently seekable in a |
173 | * playlist. The returned time ranges are relative to the earliest | 172 | * playlist. The returned time ranges are relative to the earliest |
174 | * moment in the specified playlist that is still available. A full | 173 | * moment in the specified playlist that is still available. A full |
... | @@ -179,16 +178,17 @@ | ... | @@ -179,16 +178,17 @@ |
179 | * @return {TimeRanges} the periods of time that are valid targets | 178 | * @return {TimeRanges} the periods of time that are valid targets |
180 | * for seeking | 179 | * for seeking |
181 | */ | 180 | */ |
182 | seekable = function(playlist) { | 181 | export const seekable = function(playlist) { |
183 | var start, end; | 182 | let start; |
183 | let end; | ||
184 | 184 | ||
185 | // without segments, there are no seekable ranges | 185 | // without segments, there are no seekable ranges |
186 | if (!playlist.segments) { | 186 | if (!playlist.segments) { |
187 | return videojs.createTimeRange(); | 187 | return createTimeRange(); |
188 | } | 188 | } |
189 | // when the playlist is complete, the entire duration is seekable | 189 | // when the playlist is complete, the entire duration is seekable |
190 | if (playlist.endList) { | 190 | if (playlist.endList) { |
191 | return videojs.createTimeRange(0, duration(playlist)); | 191 | return createTimeRange(0, duration(playlist)); |
192 | } | 192 | } |
193 | 193 | ||
194 | // live playlists should not expose three segment durations worth | 194 | // live playlists should not expose three segment durations worth |
... | @@ -198,12 +198,11 @@ | ... | @@ -198,12 +198,11 @@ |
198 | end = intervalDuration(playlist, | 198 | end = intervalDuration(playlist, |
199 | playlist.mediaSequence + | 199 | playlist.mediaSequence + |
200 | Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS)); | 200 | Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS)); |
201 | return videojs.createTimeRange(start, end); | 201 | return createTimeRange(start, end); |
202 | }; | 202 | }; |
203 | 203 | ||
204 | // exports | 204 | Playlist.duration = duration; |
205 | Playlist.duration = duration; | 205 | Playlist.seekable = seekable; |
206 | Playlist.seekable = seekable; | ||
207 | videojs.Hls.Playlist = Playlist; | ||
208 | 206 | ||
209 | })(window, window.videojs); | 207 | // exports |
208 | export default Playlist; | ... | ... |
src/resolve-url.js
0 → 100644
1 | import document from 'global/document'; | ||
2 | /* eslint-disable max-len */ | ||
3 | /** | ||
4 | * Constructs a new URI by interpreting a path relative to another | ||
5 | * URI. | ||
6 | * @param basePath {string} a relative or absolute URI | ||
7 | * @param path {string} a path part to combine with the base | ||
8 | * @return {string} a URI that is equivalent to composing `base` | ||
9 | * with `path` | ||
10 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | ||
11 | */ | ||
12 | /* eslint-enable max-len */ | ||
13 | const resolveUrl = function(basePath, path) { | ||
14 | // use the base element to get the browser to handle URI resolution | ||
15 | let oldBase = document.querySelector('base'); | ||
16 | let docHead = document.querySelector('head'); | ||
17 | let a = document.createElement('a'); | ||
18 | let base = oldBase; | ||
19 | let oldHref; | ||
20 | let result; | ||
21 | |||
22 | // prep the document | ||
23 | if (oldBase) { | ||
24 | oldHref = oldBase.href; | ||
25 | } else { | ||
26 | base = docHead.appendChild(document.createElement('base')); | ||
27 | } | ||
28 | |||
29 | base.href = basePath; | ||
30 | a.href = path; | ||
31 | result = a.href; | ||
32 | |||
33 | // clean up | ||
34 | if (oldBase) { | ||
35 | oldBase.href = oldHref; | ||
36 | } else { | ||
37 | docHead.removeChild(base); | ||
38 | } | ||
39 | return result; | ||
40 | }; | ||
41 | |||
42 | export default resolveUrl; |
1 | /** | 1 | /** |
2 | * A lightweight readable stream implemention that handles event dispatching. | 2 | * A lightweight readable stream implemention that handles event dispatching. |
3 | * Objects that inherit from streams should call init in their constructors. | ||
4 | */ | 3 | */ |
5 | (function(videojs, undefined) { | 4 | export default class Stream { |
6 | var Stream = function() { | 5 | constructor() { |
7 | this.init = function() { | 6 | this.listeners = {}; |
8 | var listeners = {}; | 7 | } |
8 | |||
9 | /** | 9 | /** |
10 | * Add a listener for a specified event type. | 10 | * Add a listener for a specified event type. |
11 | * @param type {string} the event name | 11 | * @param type {string} the event name |
12 | * @param listener {function} the callback to be invoked when an event of | 12 | * @param listener {function} the callback to be invoked when an event of |
13 | * the specified type occurs | 13 | * the specified type occurs |
14 | */ | 14 | */ |
15 | this.on = function(type, listener) { | 15 | on(type, listener) { |
16 | if (!listeners[type]) { | 16 | if (!this.listeners[type]) { |
17 | listeners[type] = []; | 17 | this.listeners[type] = []; |
18 | } | 18 | } |
19 | listeners[type].push(listener); | 19 | this.listeners[type].push(listener); |
20 | }; | 20 | } |
21 | |||
21 | /** | 22 | /** |
22 | * Remove a listener for a specified event type. | 23 | * Remove a listener for a specified event type. |
23 | * @param type {string} the event name | 24 | * @param type {string} the event name |
24 | * @param listener {function} a function previously registered for this | 25 | * @param listener {function} a function previously registered for this |
25 | * type of event through `on` | 26 | * type of event through `on` |
26 | */ | 27 | */ |
27 | this.off = function(type, listener) { | 28 | off(type, listener) { |
28 | var index; | 29 | let index; |
29 | if (!listeners[type]) { | 30 | |
31 | if (!this.listeners[type]) { | ||
30 | return false; | 32 | return false; |
31 | } | 33 | } |
32 | index = listeners[type].indexOf(listener); | 34 | index = this.listeners[type].indexOf(listener); |
33 | listeners[type].splice(index, 1); | 35 | this.listeners[type].splice(index, 1); |
34 | return index > -1; | 36 | return index > -1; |
35 | }; | 37 | } |
38 | |||
36 | /** | 39 | /** |
37 | * Trigger an event of the specified type on this stream. Any additional | 40 | * Trigger an event of the specified type on this stream. Any additional |
38 | * arguments to this function are passed as parameters to event listeners. | 41 | * arguments to this function are passed as parameters to event listeners. |
39 | * @param type {string} the event name | 42 | * @param type {string} the event name |
40 | */ | 43 | */ |
41 | this.trigger = function(type) { | 44 | trigger(type) { |
42 | var callbacks, i, length, args; | 45 | let callbacks; |
43 | callbacks = listeners[type]; | 46 | let i; |
47 | let length; | ||
48 | let args; | ||
49 | |||
50 | callbacks = this.listeners[type]; | ||
44 | if (!callbacks) { | 51 | if (!callbacks) { |
45 | return; | 52 | return; |
46 | } | 53 | } |
... | @@ -60,15 +67,14 @@ | ... | @@ -60,15 +67,14 @@ |
60 | callbacks[i].apply(this, args); | 67 | callbacks[i].apply(this, args); |
61 | } | 68 | } |
62 | } | 69 | } |
63 | }; | 70 | } |
71 | |||
64 | /** | 72 | /** |
65 | * Destroys the stream and cleans up. | 73 | * Destroys the stream and cleans up. |
66 | */ | 74 | */ |
67 | this.dispose = function() { | 75 | dispose() { |
68 | listeners = {}; | 76 | this.listeners = {}; |
69 | }; | 77 | } |
70 | }; | ||
71 | }; | ||
72 | /** | 78 | /** |
73 | * Forwards all `data` events on this stream to the destination stream. The | 79 | * Forwards all `data` events on this stream to the destination stream. The |
74 | * destination stream should provide a method `push` to receive the data | 80 | * destination stream should provide a method `push` to receive the data |
... | @@ -76,11 +82,9 @@ | ... | @@ -76,11 +82,9 @@ |
76 | * @param destination {stream} the stream that will receive all `data` events | 82 | * @param destination {stream} the stream that will receive all `data` events |
77 | * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options | 83 | * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options |
78 | */ | 84 | */ |
79 | Stream.prototype.pipe = function(destination) { | 85 | pipe(destination) { |
80 | this.on('data', function(data) { | 86 | this.on('data', function(data) { |
81 | destination.push(data); | 87 | destination.push(data); |
82 | }); | 88 | }); |
83 | }; | 89 | } |
84 | 90 | } | |
85 | videojs.Hls.Stream = Stream; | ||
86 | })(window.videojs); | ... | ... |
1 | /* | 1 | /** |
2 | * videojs-hls | 2 | * videojs-hls |
3 | * The main file for the HLS project. | 3 | * The main file for the HLS project. |
4 | * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE | 4 | * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE |
5 | */ | 5 | */ |
6 | (function(window, videojs, document, undefined) { | 6 | import PlaylistLoader from './playlist-loader'; |
7 | 'use strict'; | 7 | import Playlist from './playlist'; |
8 | import xhr from './xhr'; | ||
9 | import {Decrypter, AsyncStream, decrypt} from './decrypter'; | ||
10 | import utils from './bin-utils'; | ||
11 | import {MediaSource, URL} from 'videojs-contrib-media-sources'; | ||
12 | import m3u8 from './m3u8'; | ||
13 | import videojs from 'video.js'; | ||
14 | import resolveUrl from './resolve-url'; | ||
15 | |||
16 | const Hls = { | ||
17 | PlaylistLoader, | ||
18 | Playlist, | ||
19 | Decrypter, | ||
20 | AsyncStream, | ||
21 | decrypt, | ||
22 | utils, | ||
23 | xhr | ||
24 | }; | ||
25 | |||
26 | // the desired length of video to maintain in the buffer, in seconds | ||
27 | Hls.GOAL_BUFFER_LENGTH = 30; | ||
28 | |||
29 | // HLS is a source handler, not a tech. Make sure attempts to use it | ||
30 | // as one do not cause exceptions. | ||
31 | Hls.canPlaySource = function() { | ||
32 | return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + | ||
33 | 'your player\'s techOrder.'); | ||
34 | }; | ||
35 | |||
36 | // Search for a likely end time for the segment that was just appened | ||
37 | // based on the state of the `buffered` property before and after the | ||
38 | // append. | ||
39 | // If we found only one such uncommon end-point return it. | ||
40 | Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { | ||
41 | let i; | ||
42 | let start; | ||
43 | let end; | ||
44 | let result = []; | ||
45 | let edges = []; | ||
46 | |||
47 | // In order to qualify as a possible candidate, the end point must: | ||
48 | // 1) Not have already existed in the `original` ranges | ||
49 | // 2) Not result from the shrinking of a range that already existed | ||
50 | // in the `original` ranges | ||
51 | // 3) Not be contained inside of a range that existed in `original` | ||
52 | let overlapsCurrentEnd = function(span) { | ||
53 | return (span[0] <= end && span[1] >= end); | ||
54 | }; | ||
55 | |||
56 | if (original) { | ||
57 | // Save all the edges in the `original` TimeRanges object | ||
58 | for (i = 0; i < original.length; i++) { | ||
59 | start = original.start(i); | ||
60 | end = original.end(i); | ||
61 | |||
62 | edges.push([start, end]); | ||
63 | } | ||
64 | } | ||
65 | |||
66 | if (update) { | ||
67 | // Save any end-points in `update` that are not in the `original` | ||
68 | // TimeRanges object | ||
69 | for (i = 0; i < update.length; i++) { | ||
70 | start = update.start(i); | ||
71 | end = update.end(i); | ||
72 | |||
73 | if (edges.some(overlapsCurrentEnd)) { | ||
74 | continue; | ||
75 | } | ||
76 | |||
77 | // at this point it must be a unique non-shrinking end edge | ||
78 | result.push(end); | ||
79 | } | ||
80 | } | ||
81 | |||
82 | // we err on the side of caution and return null if didn't find | ||
83 | // exactly *one* differing end edge in the search above | ||
84 | if (result.length !== 1) { | ||
85 | return null; | ||
86 | } | ||
87 | |||
88 | return result[0]; | ||
89 | }; | ||
90 | |||
91 | /** | ||
92 | * Whether the browser has built-in HLS support. | ||
93 | */ | ||
94 | Hls.supportsNativeHls = (function() { | ||
95 | let video = document.createElement('video'); | ||
96 | let xMpegUrl; | ||
97 | let vndMpeg; | ||
8 | 98 | ||
9 | var | 99 | // native HLS is definitely not supported if HTML5 video isn't |
10 | // A fudge factor to apply to advertised playlist bitrates to account for | 100 | if (!videojs.getComponent('Html5').isSupported()) { |
11 | // temporary flucations in client bandwidth | 101 | return false; |
12 | bandwidthVariance = 1.2, | 102 | } |
13 | blacklistDuration = 5 * 60 * 1000, // 5 minute blacklist | ||
14 | TIME_FUDGE_FACTOR = 1 / 30, // Fudge factor to account for TimeRanges rounding | ||
15 | Component = videojs.getComponent('Component'), | ||
16 | 103 | ||
17 | // The amount of time to wait between checking the state of the buffer | 104 | xMpegUrl = video.canPlayType('application/x-mpegURL'); |
18 | bufferCheckInterval = 500, | 105 | vndMpeg = video.canPlayType('application/vnd.apple.mpegURL'); |
106 | return (/probably|maybe/).test(xMpegUrl) || | ||
107 | (/probably|maybe/).test(vndMpeg); | ||
108 | }()); | ||
19 | 109 | ||
20 | safeGetComputedStyle, | 110 | // HLS is a source handler, not a tech. Make sure attempts to use it |
21 | keyFailed, | 111 | // as one do not cause exceptions. |
22 | resolveUrl; | 112 | Hls.isSupported = function() { |
113 | return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + | ||
114 | 'your player\'s techOrder.'); | ||
115 | }; | ||
116 | |||
117 | /** | ||
118 | * A comparator function to sort two playlist object by bandwidth. | ||
119 | * @param left {object} a media playlist object | ||
120 | * @param right {object} a media playlist object | ||
121 | * @return {number} Greater than zero if the bandwidth attribute of | ||
122 | * left is greater than the corresponding attribute of right. Less | ||
123 | * than zero if the bandwidth of right is greater than left and | ||
124 | * exactly zero if the two are equal. | ||
125 | */ | ||
126 | Hls.comparePlaylistBandwidth = function(left, right) { | ||
127 | let leftBandwidth; | ||
128 | let rightBandwidth; | ||
129 | |||
130 | if (left.attributes && left.attributes.BANDWIDTH) { | ||
131 | leftBandwidth = left.attributes.BANDWIDTH; | ||
132 | } | ||
133 | leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; | ||
134 | if (right.attributes && right.attributes.BANDWIDTH) { | ||
135 | rightBandwidth = right.attributes.BANDWIDTH; | ||
136 | } | ||
137 | rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; | ||
138 | |||
139 | return leftBandwidth - rightBandwidth; | ||
140 | }; | ||
141 | |||
142 | /** | ||
143 | * A comparator function to sort two playlist object by resolution (width). | ||
144 | * @param left {object} a media playlist object | ||
145 | * @param right {object} a media playlist object | ||
146 | * @return {number} Greater than zero if the resolution.width attribute of | ||
147 | * left is greater than the corresponding attribute of right. Less | ||
148 | * than zero if the resolution.width of right is greater than left and | ||
149 | * exactly zero if the two are equal. | ||
150 | */ | ||
151 | Hls.comparePlaylistResolution = function(left, right) { | ||
152 | let leftWidth; | ||
153 | let rightWidth; | ||
154 | |||
155 | if (left.attributes && | ||
156 | left.attributes.RESOLUTION && | ||
157 | left.attributes.RESOLUTION.width) { | ||
158 | leftWidth = left.attributes.RESOLUTION.width; | ||
159 | } | ||
160 | |||
161 | leftWidth = leftWidth || window.Number.MAX_VALUE; | ||
162 | |||
163 | if (right.attributes && | ||
164 | right.attributes.RESOLUTION && | ||
165 | right.attributes.RESOLUTION.width) { | ||
166 | rightWidth = right.attributes.RESOLUTION.width; | ||
167 | } | ||
168 | |||
169 | rightWidth = rightWidth || window.Number.MAX_VALUE; | ||
170 | |||
171 | // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions | ||
172 | // have the same media dimensions/ resolution | ||
173 | if (leftWidth === rightWidth && | ||
174 | left.attributes.BANDWIDTH && | ||
175 | right.attributes.BANDWIDTH) { | ||
176 | return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; | ||
177 | } | ||
178 | return leftWidth - rightWidth; | ||
179 | }; | ||
180 | |||
181 | // A fudge factor to apply to advertised playlist bitrates to account for | ||
182 | // temporary flucations in client bandwidth | ||
183 | const bandwidthVariance = 1.2; | ||
184 | |||
185 | // 5 minute blacklist | ||
186 | const blacklistDuration = 5 * 60 * 1000; | ||
187 | |||
188 | // Fudge factor to account for TimeRanges rounding | ||
189 | const TIME_FUDGE_FACTOR = 1 / 30; | ||
190 | const Component = videojs.getComponent('Component'); | ||
191 | |||
192 | // The amount of time to wait between checking the state of the buffer | ||
193 | const bufferCheckInterval = 500; | ||
23 | 194 | ||
24 | // returns true if a key has failed to download within a certain amount of retries | 195 | // returns true if a key has failed to download within a certain amount of retries |
25 | keyFailed = function(key) { | 196 | const keyFailed = function(key) { |
26 | return key.retries && key.retries >= 2; | 197 | return key.retries && key.retries >= 2; |
27 | }; | 198 | }; |
28 | 199 | ||
29 | videojs.Hls = {}; | 200 | /** |
30 | videojs.HlsHandler = videojs.extend(Component, { | 201 | * Returns the CSS value for the specified property on an element |
31 | constructor: function(tech, options) { | 202 | * using `getComputedStyle`. Firefox has a long-standing issue where |
32 | var self = this, _player; | 203 | * getComputedStyle() may return null when running in an iframe with |
204 | * `display: none`. | ||
205 | * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 | ||
206 | */ | ||
207 | const safeGetComputedStyle = function(el, property) { | ||
208 | let result; | ||
209 | |||
210 | if (!el) { | ||
211 | return ''; | ||
212 | } | ||
213 | |||
214 | result = getComputedStyle(el); | ||
215 | if (!result) { | ||
216 | return ''; | ||
217 | } | ||
218 | |||
219 | return result[property]; | ||
220 | }; | ||
221 | |||
222 | /** | ||
223 | * Updates segment with information about its end-point in time and, optionally, | ||
224 | * the segment duration if we have enough information to determine a segment duration | ||
225 | * accurately. | ||
226 | * @param playlist {object} a media playlist object | ||
227 | * @param segmentIndex {number} the index of segment we last appended | ||
228 | * @param segmentEnd {number} the known of the segment referenced by segmentIndex | ||
229 | */ | ||
230 | const updateSegmentMetadata = function(playlist, segmentIndex, segmentEnd) { | ||
231 | if (!playlist) { | ||
232 | return; | ||
233 | } | ||
234 | |||
235 | let segment = playlist.segments[segmentIndex]; | ||
236 | let previousSegment = playlist.segments[segmentIndex - 1]; | ||
237 | |||
238 | if (segmentEnd && segment) { | ||
239 | segment.end = segmentEnd; | ||
240 | |||
241 | // fix up segment durations based on segment end data | ||
242 | if (!previousSegment) { | ||
243 | // first segment is always has a start time of 0 making its duration | ||
244 | // equal to the segment end | ||
245 | segment.duration = segment.end; | ||
246 | } else if (previousSegment.end) { | ||
247 | segment.duration = segment.end - previousSegment.end; | ||
248 | } | ||
249 | } | ||
250 | }; | ||
251 | |||
252 | /** | ||
253 | * Determines if we should call endOfStream on the media source based on the state | ||
254 | * of the buffer or if appened segment was the final segment in the playlist. | ||
255 | * @param playlist {object} a media playlist object | ||
256 | * @param mediaSource {object} the MediaSource object | ||
257 | * @param segmentIndex {number} the index of segment we last appended | ||
258 | * @param currentBuffered {object} the buffered region that currentTime resides in | ||
259 | * @return {boolean} whether the calling function should call endOfStream on the MediaSource | ||
260 | */ | ||
261 | const detectEndOfStream = function(playlist, mediaSource, segmentIndex, currentBuffered) { | ||
262 | if (!playlist) { | ||
263 | return false; | ||
264 | } | ||
265 | |||
266 | let segments = playlist.segments; | ||
267 | |||
268 | // determine a few boolean values to help make the branch below easier | ||
269 | // to read | ||
270 | let appendedLastSegment = (segmentIndex === segments.length - 1); | ||
271 | let bufferedToEnd = (currentBuffered.length && | ||
272 | segments[segments.length - 1].end <= currentBuffered.end(0)); | ||
273 | |||
274 | // if we've buffered to the end of the video, we need to call endOfStream | ||
275 | // so that MediaSources can trigger the `ended` event when it runs out of | ||
276 | // buffered data instead of waiting for me | ||
277 | return playlist.endList && | ||
278 | mediaSource.readyState === 'open' && | ||
279 | (appendedLastSegment || bufferedToEnd); | ||
280 | }; | ||
281 | |||
282 | const parseCodecs = function(codecs) { | ||
283 | let result = { | ||
284 | codecCount: 0, | ||
285 | videoCodec: null, | ||
286 | audioProfile: null | ||
287 | }; | ||
288 | |||
289 | result.codecCount = codecs.split(',').length; | ||
290 | result.codecCount = result.codecCount || 2; | ||
291 | |||
292 | // parse the video codec but ignore the version | ||
293 | result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs); | ||
294 | result.videoCodec = result.videoCodec && result.videoCodec[2]; | ||
295 | |||
296 | // parse the last field of the audio codec | ||
297 | result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs); | ||
298 | result.audioProfile = result.audioProfile && result.audioProfile[2]; | ||
299 | |||
300 | return result; | ||
301 | }; | ||
302 | |||
303 | const filterBufferedRanges = function(predicate) { | ||
304 | return function(time) { | ||
305 | let i; | ||
306 | let ranges = []; | ||
307 | let tech = this.tech_; | ||
308 | // !!The order of the next two assignments is important!! | ||
309 | // `currentTime` must be equal-to or greater-than the start of the | ||
310 | // buffered range. Flash executes out-of-process so, every value can | ||
311 | // change behind the scenes from line-to-line. By reading `currentTime` | ||
312 | // after `buffered`, we ensure that it is always a current or later | ||
313 | // value during playback. | ||
314 | let buffered = tech.buffered(); | ||
33 | 315 | ||
34 | Component.call(this, tech); | 316 | if (typeof time === 'undefined') { |
317 | time = tech.currentTime(); | ||
318 | } | ||
319 | |||
320 | if (buffered && buffered.length) { | ||
321 | // Search for a range containing the play-head | ||
322 | for (i = 0; i < buffered.length; i++) { | ||
323 | if (predicate(buffered.start(i), buffered.end(i), time)) { | ||
324 | ranges.push([buffered.start(i), buffered.end(i)]); | ||
325 | } | ||
326 | } | ||
327 | } | ||
328 | |||
329 | return videojs.createTimeRanges(ranges); | ||
330 | }; | ||
331 | }; | ||
332 | |||
333 | export default class HlsHandler extends Component { | ||
334 | constructor(tech, options) { | ||
335 | super(tech); | ||
336 | let _player; | ||
35 | 337 | ||
36 | // tech.player() is deprecated but setup a reference to HLS for | 338 | // tech.player() is deprecated but setup a reference to HLS for |
37 | // backwards-compatibility | 339 | // backwards-compatibility |
... | @@ -39,9 +341,9 @@ videojs.HlsHandler = videojs.extend(Component, { | ... | @@ -39,9 +341,9 @@ videojs.HlsHandler = videojs.extend(Component, { |
39 | _player = videojs(tech.options_.playerId); | 341 | _player = videojs(tech.options_.playerId); |
40 | if (!_player.hls) { | 342 | if (!_player.hls) { |
41 | Object.defineProperty(_player, 'hls', { | 343 | Object.defineProperty(_player, 'hls', { |
42 | get: function() { | 344 | get: () => { |
43 | videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.'); | 345 | videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.'); |
44 | return self; | 346 | return this; |
45 | } | 347 | } |
46 | }); | 348 | }); |
47 | } | 349 | } |
... | @@ -55,7 +357,8 @@ videojs.HlsHandler = videojs.extend(Component, { | ... | @@ -55,7 +357,8 @@ videojs.HlsHandler = videojs.extend(Component, { |
55 | 357 | ||
56 | // start playlist selection at a reasonable bandwidth for | 358 | // start playlist selection at a reasonable bandwidth for |
57 | // broadband internet | 359 | // broadband internet |
58 | this.bandwidth = options.bandwidth || 4194304; // 0.5 Mbps | 360 | // 0.5 Mbps |
361 | this.bandwidth = options.bandwidth || 4194304; | ||
59 | this.bytesReceived = 0; | 362 | this.bytesReceived = 0; |
60 | 363 | ||
61 | // loadingState_ tracks how far along the buffering process we | 364 | // loadingState_ tracks how far along the buffering process we |
... | @@ -82,71 +385,8 @@ videojs.HlsHandler = videojs.extend(Component, { | ... | @@ -82,71 +385,8 @@ videojs.HlsHandler = videojs.extend(Component, { |
82 | 385 | ||
83 | this.on(this.tech_, 'play', this.play); | 386 | this.on(this.tech_, 'play', this.play); |
84 | } | 387 | } |
85 | }); | 388 | src(src) { |
86 | 389 | let oldMediaPlaylist; | |
87 | // HLS is a source handler, not a tech. Make sure attempts to use it | ||
88 | // as one do not cause exceptions. | ||
89 | videojs.Hls.canPlaySource = function() { | ||
90 | return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + | ||
91 | 'your player\'s techOrder.'); | ||
92 | }; | ||
93 | |||
94 | /** | ||
95 | * The Source Handler object, which informs video.js what additional | ||
96 | * MIME types are supported and sets up playback. It is registered | ||
97 | * automatically to the appropriate tech based on the capabilities of | ||
98 | * the browser it is running in. It is not necessary to use or modify | ||
99 | * this object in normal usage. | ||
100 | */ | ||
101 | videojs.HlsSourceHandler = function(mode) { | ||
102 | return { | ||
103 | canHandleSource: function(srcObj) { | ||
104 | return videojs.HlsSourceHandler.canPlayType(srcObj.type); | ||
105 | }, | ||
106 | handleSource: function(source, tech) { | ||
107 | if (mode === 'flash') { | ||
108 | // We need to trigger this asynchronously to give others the chance | ||
109 | // to bind to the event when a source is set at player creation | ||
110 | tech.setTimeout(function() { | ||
111 | tech.trigger('loadstart'); | ||
112 | }, 1); | ||
113 | } | ||
114 | tech.hls = new videojs.HlsHandler(tech, { | ||
115 | source: source, | ||
116 | mode: mode | ||
117 | }); | ||
118 | tech.hls.src(source.src); | ||
119 | return tech.hls; | ||
120 | }, | ||
121 | canPlayType: function(type) { | ||
122 | return videojs.HlsSourceHandler.canPlayType(type); | ||
123 | } | ||
124 | }; | ||
125 | }; | ||
126 | |||
127 | videojs.HlsSourceHandler.canPlayType = function(type) { | ||
128 | var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; | ||
129 | |||
130 | // favor native HLS support if it's available | ||
131 | if (videojs.Hls.supportsNativeHls) { | ||
132 | return false; | ||
133 | } | ||
134 | return mpegurlRE.test(type); | ||
135 | }; | ||
136 | |||
137 | // register source handlers with the appropriate techs | ||
138 | if (videojs.MediaSource.supportsNativeMediaSources()) { | ||
139 | videojs.getComponent('Html5').registerSourceHandler(videojs.HlsSourceHandler('html5')); | ||
140 | } | ||
141 | if (window.Uint8Array) { | ||
142 | videojs.getComponent('Flash').registerSourceHandler(videojs.HlsSourceHandler('flash')); | ||
143 | } | ||
144 | |||
145 | // the desired length of video to maintain in the buffer, in seconds | ||
146 | videojs.Hls.GOAL_BUFFER_LENGTH = 30; | ||
147 | |||
148 | videojs.HlsHandler.prototype.src = function(src) { | ||
149 | var oldMediaPlaylist; | ||
150 | 390 | ||
151 | // do nothing if the src is falsey | 391 | // do nothing if the src is falsey |
152 | if (!src) { | 392 | if (!src) { |
... | @@ -159,16 +399,17 @@ videojs.HlsHandler.prototype.src = function(src) { | ... | @@ -159,16 +399,17 @@ videojs.HlsHandler.prototype.src = function(src) { |
159 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); | 399 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); |
160 | 400 | ||
161 | this.options_ = {}; | 401 | this.options_ = {}; |
162 | if (this.source_.withCredentials !== undefined) { | 402 | if (typeof this.source_.withCredentials !== 'undefined') { |
163 | this.options_.withCredentials = this.source_.withCredentials; | 403 | this.options_.withCredentials = this.source_.withCredentials; |
164 | } else if (videojs.options.hls) { | 404 | } else if (videojs.options.hls) { |
165 | this.options_.withCredentials = videojs.options.hls.withCredentials; | 405 | this.options_.withCredentials = videojs.options.hls.withCredentials; |
166 | } | 406 | } |
167 | this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials); | 407 | this.playlists = new Hls.PlaylistLoader(this.source_.src, |
408 | this.options_.withCredentials); | ||
168 | 409 | ||
169 | this.tech_.one('canplay', this.setupFirstPlay.bind(this)); | 410 | this.tech_.one('canplay', this.setupFirstPlay.bind(this)); |
170 | 411 | ||
171 | this.playlists.on('loadedmetadata', function() { | 412 | this.playlists.on('loadedmetadata', () => { |
172 | oldMediaPlaylist = this.playlists.media(); | 413 | oldMediaPlaylist = this.playlists.media(); |
173 | 414 | ||
174 | // if this isn't a live video and preload permits, start | 415 | // if this isn't a live video and preload permits, start |
... | @@ -183,14 +424,15 @@ videojs.HlsHandler.prototype.src = function(src) { | ... | @@ -183,14 +424,15 @@ videojs.HlsHandler.prototype.src = function(src) { |
183 | this.setupFirstPlay(); | 424 | this.setupFirstPlay(); |
184 | this.fillBuffer(); | 425 | this.fillBuffer(); |
185 | this.tech_.trigger('loadedmetadata'); | 426 | this.tech_.trigger('loadedmetadata'); |
186 | }.bind(this)); | 427 | }); |
187 | 428 | ||
188 | this.playlists.on('error', function() { | 429 | this.playlists.on('error', () => { |
189 | this.blacklistCurrentPlaylist_(this.playlists.error); | 430 | this.blacklistCurrentPlaylist_(this.playlists.error); |
190 | }.bind(this)); | 431 | }); |
191 | 432 | ||
192 | this.playlists.on('loadedplaylist', function() { | 433 | this.playlists.on('loadedplaylist', () => { |
193 | var updatedPlaylist = this.playlists.media(), seekable; | 434 | let updatedPlaylist = this.playlists.media(); |
435 | let seekable; | ||
194 | 436 | ||
195 | if (!updatedPlaylist) { | 437 | if (!updatedPlaylist) { |
196 | // select the initial variant | 438 | // select the initial variant |
... | @@ -208,14 +450,14 @@ videojs.HlsHandler.prototype.src = function(src) { | ... | @@ -208,14 +450,14 @@ videojs.HlsHandler.prototype.src = function(src) { |
208 | } | 450 | } |
209 | 451 | ||
210 | oldMediaPlaylist = updatedPlaylist; | 452 | oldMediaPlaylist = updatedPlaylist; |
211 | }.bind(this)); | 453 | }); |
212 | 454 | ||
213 | this.playlists.on('mediachange', function() { | 455 | this.playlists.on('mediachange', () => { |
214 | this.tech_.trigger({ | 456 | this.tech_.trigger({ |
215 | type: 'mediachange', | 457 | type: 'mediachange', |
216 | bubbles: true | 458 | bubbles: true |
217 | }); | 459 | }); |
218 | }.bind(this)); | 460 | }); |
219 | 461 | ||
220 | // do nothing if the tech has been disposed already | 462 | // do nothing if the tech has been disposed already |
221 | // this can occur if someone sets the src in player.ready(), for instance | 463 | // this can occur if someone sets the src in player.ready(), for instance |
... | @@ -224,9 +466,8 @@ videojs.HlsHandler.prototype.src = function(src) { | ... | @@ -224,9 +466,8 @@ videojs.HlsHandler.prototype.src = function(src) { |
224 | } | 466 | } |
225 | 467 | ||
226 | this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); | 468 | this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); |
227 | }; | 469 | } |
228 | 470 | handleSourceOpen() { | |
229 | videojs.HlsHandler.prototype.handleSourceOpen = function() { | ||
230 | // Only attempt to create the source buffer if none already exist. | 471 | // Only attempt to create the source buffer if none already exist. |
231 | // handleSourceOpen is also called when we are "re-opening" a source buffer | 472 | // handleSourceOpen is also called when we are "re-opening" a source buffer |
232 | // after `endOfStream` has been called (in response to a seek for instance) | 473 | // after `endOfStream` has been called (in response to a seek for instance) |
... | @@ -243,149 +484,9 @@ videojs.HlsHandler.prototype.handleSourceOpen = function() { | ... | @@ -243,149 +484,9 @@ videojs.HlsHandler.prototype.handleSourceOpen = function() { |
243 | if (this.tech_.autoplay()) { | 484 | if (this.tech_.autoplay()) { |
244 | this.play(); | 485 | this.play(); |
245 | } | 486 | } |
246 | }; | ||
247 | |||
248 | // Search for a likely end time for the segment that was just appened | ||
249 | // based on the state of the `buffered` property before and after the | ||
250 | // append. | ||
251 | // If we found only one such uncommon end-point return it. | ||
252 | videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { | ||
253 | var | ||
254 | i, start, end, | ||
255 | result = [], | ||
256 | edges = [], | ||
257 | // In order to qualify as a possible candidate, the end point must: | ||
258 | // 1) Not have already existed in the `original` ranges | ||
259 | // 2) Not result from the shrinking of a range that already existed | ||
260 | // in the `original` ranges | ||
261 | // 3) Not be contained inside of a range that existed in `original` | ||
262 | overlapsCurrentEnd = function(span) { | ||
263 | return (span[0] <= end && span[1] >= end); | ||
264 | }; | ||
265 | |||
266 | if (original) { | ||
267 | // Save all the edges in the `original` TimeRanges object | ||
268 | for (i = 0; i < original.length; i++) { | ||
269 | start = original.start(i); | ||
270 | end = original.end(i); | ||
271 | |||
272 | edges.push([start, end]); | ||
273 | } | ||
274 | } | 487 | } |
275 | 488 | ||
276 | if (update) { | 489 | /** |
277 | // Save any end-points in `update` that are not in the `original` | ||
278 | // TimeRanges object | ||
279 | for (i = 0; i < update.length; i++) { | ||
280 | start = update.start(i); | ||
281 | end = update.end(i); | ||
282 | |||
283 | if (edges.some(overlapsCurrentEnd)) { | ||
284 | continue; | ||
285 | } | ||
286 | |||
287 | // at this point it must be a unique non-shrinking end edge | ||
288 | result.push(end); | ||
289 | } | ||
290 | } | ||
291 | |||
292 | // we err on the side of caution and return null if didn't find | ||
293 | // exactly *one* differing end edge in the search above | ||
294 | if (result.length !== 1) { | ||
295 | return null; | ||
296 | } | ||
297 | |||
298 | return result[0]; | ||
299 | }; | ||
300 | |||
301 | /** | ||
302 | * Updates segment with information about its end-point in time and, optionally, | ||
303 | * the segment duration if we have enough information to determine a segment duration | ||
304 | * accurately. | ||
305 | * @param playlist {object} a media playlist object | ||
306 | * @param segmentIndex {number} the index of segment we last appended | ||
307 | * @param segmentEnd {number} the known of the segment referenced by segmentIndex | ||
308 | */ | ||
309 | videojs.HlsHandler.prototype.updateSegmentMetadata_ = function(playlist, segmentIndex, segmentEnd) { | ||
310 | var | ||
311 | segment, | ||
312 | previousSegment; | ||
313 | |||
314 | if (!playlist) { | ||
315 | return; | ||
316 | } | ||
317 | |||
318 | segment = playlist.segments[segmentIndex]; | ||
319 | previousSegment = playlist.segments[segmentIndex - 1]; | ||
320 | |||
321 | if (segmentEnd && segment) { | ||
322 | segment.end = segmentEnd; | ||
323 | |||
324 | // fix up segment durations based on segment end data | ||
325 | if (!previousSegment) { | ||
326 | // first segment is always has a start time of 0 making its duration | ||
327 | // equal to the segment end | ||
328 | segment.duration = segment.end; | ||
329 | } else if (previousSegment.end) { | ||
330 | segment.duration = segment.end - previousSegment.end; | ||
331 | } | ||
332 | } | ||
333 | }; | ||
334 | |||
335 | /** | ||
336 | * Determines if we should call endOfStream on the media source based on the state | ||
337 | * of the buffer or if appened segment was the final segment in the playlist. | ||
338 | * @param playlist {object} a media playlist object | ||
339 | * @param segmentIndex {number} the index of segment we last appended | ||
340 | * @param currentBuffered {object} the buffered region that currentTime resides in | ||
341 | * @return {boolean} whether the calling function should call endOfStream on the MediaSource | ||
342 | */ | ||
343 | videojs.HlsHandler.prototype.isEndOfStream_ = function(playlist, segmentIndex, currentBuffered) { | ||
344 | var | ||
345 | segments = playlist.segments, | ||
346 | appendedLastSegment, | ||
347 | bufferedToEnd; | ||
348 | |||
349 | if (!playlist) { | ||
350 | return false; | ||
351 | } | ||
352 | |||
353 | // determine a few boolean values to help make the branch below easier | ||
354 | // to read | ||
355 | appendedLastSegment = (segmentIndex === segments.length - 1); | ||
356 | bufferedToEnd = (currentBuffered.length && | ||
357 | segments[segments.length - 1].end <= currentBuffered.end(0)); | ||
358 | |||
359 | // if we've buffered to the end of the video, we need to call endOfStream | ||
360 | // so that MediaSources can trigger the `ended` event when it runs out of | ||
361 | // buffered data instead of waiting for me | ||
362 | return playlist.endList && | ||
363 | this.mediaSource.readyState === 'open' && | ||
364 | (appendedLastSegment || bufferedToEnd); | ||
365 | }; | ||
366 | |||
367 | var parseCodecs = function(codecs) { | ||
368 | var result = { | ||
369 | codecCount: 0, | ||
370 | videoCodec: null, | ||
371 | audioProfile: null | ||
372 | }; | ||
373 | |||
374 | result.codecCount = codecs.split(',').length; | ||
375 | result.codecCount = result.codecCount || 2; | ||
376 | |||
377 | // parse the video codec but ignore the version | ||
378 | result.videoCodec = /(^|\s|,)+(avc1)[^ ,]*/i.exec(codecs); | ||
379 | result.videoCodec = result.videoCodec && result.videoCodec[2]; | ||
380 | |||
381 | // parse the last field of the audio codec | ||
382 | result.audioProfile = /(^|\s|,)+mp4a.\d+\.(\d+)/i.exec(codecs); | ||
383 | result.audioProfile = result.audioProfile && result.audioProfile[2]; | ||
384 | |||
385 | return result; | ||
386 | }; | ||
387 | |||
388 | /** | ||
389 | * Blacklist playlists that are known to be codec or | 490 | * Blacklist playlists that are known to be codec or |
390 | * stream-incompatible with the SourceBuffer configuration. For | 491 | * stream-incompatible with the SourceBuffer configuration. For |
391 | * instance, Media Source Extensions would cause the video element to | 492 | * instance, Media Source Extensions would cause the video element to |
... | @@ -398,13 +499,12 @@ var parseCodecs = function(codecs) { | ... | @@ -398,13 +499,12 @@ var parseCodecs = function(codecs) { |
398 | * will be excluded from the default playlist selection algorithm | 499 | * will be excluded from the default playlist selection algorithm |
399 | * indefinitely. | 500 | * indefinitely. |
400 | */ | 501 | */ |
401 | videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) { | 502 | excludeIncompatibleVariants_(media) { |
402 | var | 503 | let master = this.playlists.master; |
403 | master = this.playlists.master, | 504 | let codecCount = 2; |
404 | codecCount = 2, | 505 | let videoCodec = null; |
405 | videoCodec = null, | 506 | let audioProfile = null; |
406 | audioProfile = null, | 507 | let codecs; |
407 | codecs; | ||
408 | 508 | ||
409 | if (media.attributes && media.attributes.CODECS) { | 509 | if (media.attributes && media.attributes.CODECS) { |
410 | codecs = parseCodecs(media.attributes.CODECS); | 510 | codecs = parseCodecs(media.attributes.CODECS); |
... | @@ -413,7 +513,7 @@ videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) { | ... | @@ -413,7 +513,7 @@ videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) { |
413 | codecCount = codecs.codecCount; | 513 | codecCount = codecs.codecCount; |
414 | } | 514 | } |
415 | master.playlists.forEach(function(variant) { | 515 | master.playlists.forEach(function(variant) { |
416 | var variantCodecs = { | 516 | let variantCodecs = { |
417 | codecCount: 2, | 517 | codecCount: 2, |
418 | videoCodec: null, | 518 | videoCodec: null, |
419 | audioProfile: null | 519 | audioProfile: null |
... | @@ -441,10 +541,11 @@ videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) { | ... | @@ -441,10 +541,11 @@ videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) { |
441 | variant.excludeUntil = Infinity; | 541 | variant.excludeUntil = Infinity; |
442 | } | 542 | } |
443 | }); | 543 | }); |
444 | }; | 544 | } |
445 | 545 | ||
446 | videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { | 546 | setupSourceBuffer_() { |
447 | var media = this.playlists.media(), mimeType; | 547 | let media = this.playlists.media(); |
548 | let mimeType; | ||
448 | 549 | ||
449 | // wait until a media playlist is available and the Media Source is | 550 | // wait until a media playlist is available and the Media Source is |
450 | // attached | 551 | // attached |
... | @@ -467,16 +568,15 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { | ... | @@ -467,16 +568,15 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { |
467 | // transition the sourcebuffer to the ended state if we've hit the end of | 568 | // transition the sourcebuffer to the ended state if we've hit the end of |
468 | // the playlist | 569 | // the playlist |
469 | this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this)); | 570 | this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this)); |
470 | }; | 571 | } |
471 | 572 | ||
472 | /** | 573 | /** |
473 | * Seek to the latest media position if this is a live video and the | 574 | * Seek to the latest media position if this is a live video and the |
474 | * player and video are loaded and initialized. | 575 | * player and video are loaded and initialized. |
475 | */ | 576 | */ |
476 | videojs.HlsHandler.prototype.setupFirstPlay = function() { | 577 | setupFirstPlay() { |
477 | var seekable, media; | 578 | let seekable; |
478 | media = this.playlists.media(); | 579 | let media = this.playlists.media(); |
479 | |||
480 | 580 | ||
481 | // check that everything is ready to begin buffering | 581 | // check that everything is ready to begin buffering |
482 | 582 | ||
... | @@ -506,12 +606,12 @@ videojs.HlsHandler.prototype.setupFirstPlay = function() { | ... | @@ -506,12 +606,12 @@ videojs.HlsHandler.prototype.setupFirstPlay = function() { |
506 | this.tech_.setCurrentTime(seekable.end(0)); | 606 | this.tech_.setCurrentTime(seekable.end(0)); |
507 | } | 607 | } |
508 | } | 608 | } |
509 | }; | 609 | } |
510 | 610 | ||
511 | /** | 611 | /** |
512 | * Begin playing the video. | 612 | * Begin playing the video. |
513 | */ | 613 | */ |
514 | videojs.HlsHandler.prototype.play = function() { | 614 | play() { |
515 | this.loadingState_ = 'segments'; | 615 | this.loadingState_ = 'segments'; |
516 | 616 | ||
517 | if (this.tech_.ended()) { | 617 | if (this.tech_.ended()) { |
... | @@ -529,11 +629,10 @@ videojs.HlsHandler.prototype.play = function() { | ... | @@ -529,11 +629,10 @@ videojs.HlsHandler.prototype.play = function() { |
529 | this.tech_.setCurrentTime(this.seekable().start(0)); | 629 | this.tech_.setCurrentTime(this.seekable().start(0)); |
530 | } | 630 | } |
531 | } | 631 | } |
532 | }; | 632 | } |
533 | 633 | ||
534 | videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { | 634 | setCurrentTime(currentTime) { |
535 | var | 635 | let buffered = this.findBufferedRange_(); |
536 | buffered = this.findBufferedRange_(); | ||
537 | 636 | ||
538 | if (!(this.playlists && this.playlists.media())) { | 637 | if (!(this.playlists && this.playlists.media())) { |
539 | // return immediately if the metadata is not ready yet | 638 | // return immediately if the metadata is not ready yet |
... | @@ -570,11 +669,10 @@ videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { | ... | @@ -570,11 +669,10 @@ videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { |
570 | 669 | ||
571 | // begin filling the buffer at the new position | 670 | // begin filling the buffer at the new position |
572 | this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime)); | 671 | this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime)); |
573 | }; | 672 | } |
574 | 673 | ||
575 | videojs.HlsHandler.prototype.duration = function() { | 674 | duration() { |
576 | var | 675 | let playlists = this.playlists; |
577 | playlists = this.playlists; | ||
578 | 676 | ||
579 | if (!playlists) { | 677 | if (!playlists) { |
580 | return 0; | 678 | return 0; |
... | @@ -584,11 +682,12 @@ videojs.HlsHandler.prototype.duration = function() { | ... | @@ -584,11 +682,12 @@ videojs.HlsHandler.prototype.duration = function() { |
584 | return this.mediaSource.duration; | 682 | return this.mediaSource.duration; |
585 | } | 683 | } |
586 | 684 | ||
587 | return videojs.Hls.Playlist.duration(playlists.media()); | 685 | return Hls.Playlist.duration(playlists.media()); |
588 | }; | 686 | } |
589 | 687 | ||
590 | videojs.HlsHandler.prototype.seekable = function() { | 688 | seekable() { |
591 | var media, seekable; | 689 | let media; |
690 | let seekable; | ||
592 | 691 | ||
593 | if (!this.playlists) { | 692 | if (!this.playlists) { |
594 | return videojs.createTimeRanges(); | 693 | return videojs.createTimeRanges(); |
... | @@ -598,7 +697,7 @@ videojs.HlsHandler.prototype.seekable = function() { | ... | @@ -598,7 +697,7 @@ videojs.HlsHandler.prototype.seekable = function() { |
598 | return videojs.createTimeRanges(); | 697 | return videojs.createTimeRanges(); |
599 | } | 698 | } |
600 | 699 | ||
601 | seekable = videojs.Hls.Playlist.seekable(media); | 700 | seekable = Hls.Playlist.seekable(media); |
602 | if (seekable.length === 0) { | 701 | if (seekable.length === 0) { |
603 | return seekable; | 702 | return seekable; |
604 | } | 703 | } |
... | @@ -608,30 +707,28 @@ videojs.HlsHandler.prototype.seekable = function() { | ... | @@ -608,30 +707,28 @@ videojs.HlsHandler.prototype.seekable = function() { |
608 | // fall back to the playlist loader's running estimate of expired | 707 | // fall back to the playlist loader's running estimate of expired |
609 | // time | 708 | // time |
610 | if (seekable.start(0) === 0) { | 709 | if (seekable.start(0) === 0) { |
611 | return videojs.createTimeRanges([[ | 710 | return videojs.createTimeRanges([[this.playlists.expired_, |
612 | this.playlists.expired_, | 711 | this.playlists.expired_ + seekable.end(0)]]); |
613 | this.playlists.expired_ + seekable.end(0) | ||
614 | ]]); | ||
615 | } | 712 | } |
616 | 713 | ||
617 | // seekable has been calculated based on buffering video data so it | 714 | // seekable has been calculated based on buffering video data so it |
618 | // can be returned directly | 715 | // can be returned directly |
619 | return seekable; | 716 | return seekable; |
620 | }; | 717 | } |
621 | 718 | ||
622 | /** | 719 | /** |
623 | * Update the player duration | 720 | * Update the player duration |
624 | */ | 721 | */ |
625 | videojs.HlsHandler.prototype.updateDuration = function(playlist) { | 722 | updateDuration(playlist) { |
626 | var oldDuration = this.mediaSource.duration, | 723 | let oldDuration = this.mediaSource.duration; |
627 | newDuration = videojs.Hls.Playlist.duration(playlist), | 724 | let newDuration = Hls.Playlist.duration(playlist); |
628 | buffered = this.tech_.buffered(), | 725 | let buffered = this.tech_.buffered(); |
629 | setDuration = function() { | 726 | let setDuration = () => { |
630 | this.mediaSource.duration = newDuration; | 727 | this.mediaSource.duration = newDuration; |
631 | this.tech_.trigger('durationchange'); | 728 | this.tech_.trigger('durationchange'); |
632 | 729 | ||
633 | this.mediaSource.removeEventListener('sourceopen', setDuration); | 730 | this.mediaSource.removeEventListener('sourceopen', setDuration); |
634 | }.bind(this); | 731 | }; |
635 | 732 | ||
636 | if (buffered.length > 0) { | 733 | if (buffered.length > 0) { |
637 | newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); | 734 | newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); |
... | @@ -647,31 +744,31 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) { | ... | @@ -647,31 +744,31 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) { |
647 | this.tech_.trigger('durationchange'); | 744 | this.tech_.trigger('durationchange'); |
648 | } | 745 | } |
649 | } | 746 | } |
650 | }; | 747 | } |
651 | 748 | ||
652 | /** | 749 | /** |
653 | * Clear all buffers and reset any state relevant to the current | 750 | * Clear all buffers and reset any state relevant to the current |
654 | * source. After this function is called, the tech should be in a | 751 | * source. After this function is called, the tech should be in a |
655 | * state suitable for switching to a different video. | 752 | * state suitable for switching to a different video. |
656 | */ | 753 | */ |
657 | videojs.HlsHandler.prototype.resetSrc_ = function() { | 754 | resetSrc_() { |
658 | this.cancelSegmentXhr(); | 755 | this.cancelSegmentXhr(); |
659 | this.cancelKeyXhr(); | 756 | this.cancelKeyXhr(); |
660 | 757 | ||
661 | if (this.sourceBuffer && this.mediaSource.readyState === 'open') { | 758 | if (this.sourceBuffer && this.mediaSource.readyState === 'open') { |
662 | this.sourceBuffer.abort(); | 759 | this.sourceBuffer.abort(); |
663 | } | 760 | } |
664 | }; | 761 | } |
665 | 762 | ||
666 | videojs.HlsHandler.prototype.cancelKeyXhr = function() { | 763 | cancelKeyXhr() { |
667 | if (this.keyXhr_) { | 764 | if (this.keyXhr_) { |
668 | this.keyXhr_.onreadystatechange = null; | 765 | this.keyXhr_.onreadystatechange = null; |
669 | this.keyXhr_.abort(); | 766 | this.keyXhr_.abort(); |
670 | this.keyXhr_ = null; | 767 | this.keyXhr_ = null; |
671 | } | 768 | } |
672 | }; | 769 | } |
673 | 770 | ||
674 | videojs.HlsHandler.prototype.cancelSegmentXhr = function() { | 771 | cancelSegmentXhr() { |
675 | if (this.segmentXhr_) { | 772 | if (this.segmentXhr_) { |
676 | // Prevent error handler from running. | 773 | // Prevent error handler from running. |
677 | this.segmentXhr_.onreadystatechange = null; | 774 | this.segmentXhr_.onreadystatechange = null; |
... | @@ -681,33 +778,12 @@ videojs.HlsHandler.prototype.cancelSegmentXhr = function() { | ... | @@ -681,33 +778,12 @@ videojs.HlsHandler.prototype.cancelSegmentXhr = function() { |
681 | 778 | ||
682 | // clear out the segment being processed | 779 | // clear out the segment being processed |
683 | this.pendingSegment_ = null; | 780 | this.pendingSegment_ = null; |
684 | }; | ||
685 | |||
686 | /** | ||
687 | * Returns the CSS value for the specified property on an element | ||
688 | * using `getComputedStyle`. Firefox has a long-standing issue where | ||
689 | * getComputedStyle() may return null when running in an iframe with | ||
690 | * `display: none`. | ||
691 | * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 | ||
692 | */ | ||
693 | safeGetComputedStyle = function(el, property) { | ||
694 | var result; | ||
695 | if (!el) { | ||
696 | return ''; | ||
697 | } | ||
698 | |||
699 | result = getComputedStyle(el); | ||
700 | if (!result) { | ||
701 | return ''; | ||
702 | } | 781 | } |
703 | 782 | ||
704 | return result[property]; | 783 | /** |
705 | }; | ||
706 | |||
707 | /** | ||
708 | * Abort all outstanding work and cleanup. | 784 | * Abort all outstanding work and cleanup. |
709 | */ | 785 | */ |
710 | videojs.HlsHandler.prototype.dispose = function() { | 786 | dispose() { |
711 | this.stopCheckingBuffer_(); | 787 | this.stopCheckingBuffer_(); |
712 | 788 | ||
713 | if (this.playlists) { | 789 | if (this.playlists) { |
... | @@ -715,36 +791,36 @@ videojs.HlsHandler.prototype.dispose = function() { | ... | @@ -715,36 +791,36 @@ videojs.HlsHandler.prototype.dispose = function() { |
715 | } | 791 | } |
716 | 792 | ||
717 | this.resetSrc_(); | 793 | this.resetSrc_(); |
718 | Component.prototype.dispose.call(this); | 794 | super.dispose(); |
719 | }; | 795 | } |
720 | 796 | ||
721 | /** | 797 | /** |
722 | * Chooses the appropriate media playlist based on the current | 798 | * Chooses the appropriate media playlist based on the current |
723 | * bandwidth estimate and the player size. | 799 | * bandwidth estimate and the player size. |
724 | * @return the highest bitrate playlist less than the currently detected | 800 | * @return the highest bitrate playlist less than the currently detected |
725 | * bandwidth, accounting for some amount of bandwidth variance | 801 | * bandwidth, accounting for some amount of bandwidth variance |
726 | */ | 802 | */ |
727 | videojs.HlsHandler.prototype.selectPlaylist = function () { | 803 | selectPlaylist() { |
728 | var | 804 | let effectiveBitrate; |
729 | effectiveBitrate, | 805 | let sortedPlaylists = this.playlists.master.playlists.slice(); |
730 | sortedPlaylists = this.playlists.master.playlists.slice(), | 806 | let bandwidthPlaylists = []; |
731 | bandwidthPlaylists = [], | 807 | let now = +new Date(); |
732 | now = +new Date(), | 808 | let i; |
733 | i, | 809 | let variant; |
734 | variant, | 810 | let bandwidthBestVariant; |
735 | bandwidthBestVariant, | 811 | let resolutionPlusOne; |
736 | resolutionPlusOne, | 812 | let resolutionPlusOneAttribute; |
737 | resolutionBestVariant, | 813 | let resolutionBestVariant; |
738 | width, | 814 | let width; |
739 | height; | 815 | let height; |
740 | 816 | ||
741 | sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth); | 817 | sortedPlaylists.sort(Hls.comparePlaylistBandwidth); |
742 | 818 | ||
743 | // filter out any playlists that have been excluded due to | 819 | // filter out any playlists that have been excluded due to |
744 | // incompatible configurations or playback errors | 820 | // incompatible configurations or playback errors |
745 | sortedPlaylists = sortedPlaylists.filter(function(variant) { | 821 | sortedPlaylists = sortedPlaylists.filter((localVariant) => { |
746 | if (variant.excludeUntil !== undefined) { | 822 | if (typeof localVariant.excludeUntil !== 'undefined') { |
747 | return now >= variant.excludeUntil; | 823 | return now >= localVariant.excludeUntil; |
748 | } | 824 | } |
749 | return true; | 825 | return true; |
750 | }); | 826 | }); |
... | @@ -776,9 +852,10 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { | ... | @@ -776,9 +852,10 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { |
776 | i = bandwidthPlaylists.length; | 852 | i = bandwidthPlaylists.length; |
777 | 853 | ||
778 | // sort variants by resolution | 854 | // sort variants by resolution |
779 | bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution); | 855 | bandwidthPlaylists.sort(Hls.comparePlaylistResolution); |
780 | 856 | ||
781 | // forget our old variant from above, or we might choose that in high-bandwidth scenarios | 857 | // forget our old variant from above, |
858 | // or we might choose that in high-bandwidth scenarios | ||
782 | // (this could be the lowest bitrate rendition as we go through all of them above) | 859 | // (this could be the lowest bitrate rendition as we go through all of them above) |
783 | variant = null; | 860 | variant = null; |
784 | 861 | ||
... | @@ -801,20 +878,22 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { | ... | @@ -801,20 +878,22 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { |
801 | // since the playlists are sorted, the first variant that has | 878 | // since the playlists are sorted, the first variant that has |
802 | // dimensions less than or equal to the player size is the best | 879 | // dimensions less than or equal to the player size is the best |
803 | 880 | ||
804 | if (variant.attributes.RESOLUTION.width === width && | 881 | let variantResolution = variant.attributes.RESOLUTION; |
805 | variant.attributes.RESOLUTION.height === height) { | 882 | |
883 | if (variantResolution.width === width && | ||
884 | variantResolution.height === height) { | ||
806 | // if we have the exact resolution as the player use it | 885 | // if we have the exact resolution as the player use it |
807 | resolutionPlusOne = null; | 886 | resolutionPlusOne = null; |
808 | resolutionBestVariant = variant; | 887 | resolutionBestVariant = variant; |
809 | break; | 888 | break; |
810 | } else if (variant.attributes.RESOLUTION.width < width && | 889 | } else if (variantResolution.width < width && |
811 | variant.attributes.RESOLUTION.height < height) { | 890 | variantResolution.height < height) { |
812 | // if both dimensions are less than the player use the | 891 | // if both dimensions are less than the player use the |
813 | // previous (next-largest) variant | 892 | // previous (next-largest) variant |
814 | break; | 893 | break; |
815 | } else if (!resolutionPlusOne || | 894 | } else if (!resolutionPlusOne || |
816 | (variant.attributes.RESOLUTION.width < resolutionPlusOne.attributes.RESOLUTION.width && | 895 | (variantResolution.width < resolutionPlusOneAttribute.width && |
817 | variant.attributes.RESOLUTION.height < resolutionPlusOne.attributes.RESOLUTION.height)) { | 896 | variantResolution.height < resolutionPlusOneAttribute.height)) { |
818 | // If we still haven't found a good match keep a | 897 | // If we still haven't found a good match keep a |
819 | // reference to the previous variant for the next loop | 898 | // reference to the previous variant for the next loop |
820 | // iteration | 899 | // iteration |
... | @@ -824,17 +903,21 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { | ... | @@ -824,17 +903,21 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { |
824 | // the highest bandwidth variant that is just-larger-than | 903 | // the highest bandwidth variant that is just-larger-than |
825 | // the video player | 904 | // the video player |
826 | resolutionPlusOne = variant; | 905 | resolutionPlusOne = variant; |
906 | resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION; | ||
827 | } | 907 | } |
828 | } | 908 | } |
829 | 909 | ||
830 | // fallback chain of variants | 910 | // fallback chain of variants |
831 | return resolutionPlusOne || resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0]; | 911 | return resolutionPlusOne || |
832 | }; | 912 | resolutionBestVariant || |
913 | bandwidthBestVariant || | ||
914 | sortedPlaylists[0]; | ||
915 | } | ||
833 | 916 | ||
834 | /** | 917 | /** |
835 | * Periodically request new segments and append video data. | 918 | * Periodically request new segments and append video data. |
836 | */ | 919 | */ |
837 | videojs.HlsHandler.prototype.checkBuffer_ = function() { | 920 | checkBuffer_() { |
838 | // calling this method directly resets any outstanding buffer checks | 921 | // calling this method directly resets any outstanding buffer checks |
839 | if (this.checkBufferTimeout_) { | 922 | if (this.checkBufferTimeout_) { |
840 | window.clearTimeout(this.checkBufferTimeout_); | 923 | window.clearTimeout(this.checkBufferTimeout_); |
... | @@ -847,101 +930,44 @@ videojs.HlsHandler.prototype.checkBuffer_ = function() { | ... | @@ -847,101 +930,44 @@ videojs.HlsHandler.prototype.checkBuffer_ = function() { |
847 | // wait awhile and try again | 930 | // wait awhile and try again |
848 | this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this), | 931 | this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this), |
849 | bufferCheckInterval); | 932 | bufferCheckInterval); |
850 | }; | 933 | } |
851 | 934 | ||
852 | /** | 935 | /** |
853 | * Setup a periodic task to request new segments if necessary and | 936 | * Setup a periodic task to request new segments if necessary and |
854 | * append bytes into the SourceBuffer. | 937 | * append bytes into the SourceBuffer. |
855 | */ | 938 | */ |
856 | videojs.HlsHandler.prototype.startCheckingBuffer_ = function() { | 939 | startCheckingBuffer_() { |
857 | this.checkBuffer_(); | 940 | this.checkBuffer_(); |
858 | }; | 941 | } |
859 | 942 | ||
860 | /** | 943 | /** |
861 | * Stop the periodic task requesting new segments and feeding the | 944 | * Stop the periodic task requesting new segments and feeding the |
862 | * SourceBuffer. | 945 | * SourceBuffer. |
863 | */ | 946 | */ |
864 | videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() { | 947 | stopCheckingBuffer_() { |
865 | if (this.checkBufferTimeout_) { | 948 | if (this.checkBufferTimeout_) { |
866 | window.clearTimeout(this.checkBufferTimeout_); | 949 | window.clearTimeout(this.checkBufferTimeout_); |
867 | this.checkBufferTimeout_ = null; | 950 | this.checkBufferTimeout_ = null; |
868 | } | 951 | } |
869 | }; | ||
870 | |||
871 | var filterBufferedRanges = function(predicate) { | ||
872 | return function(time) { | ||
873 | var | ||
874 | i, | ||
875 | ranges = [], | ||
876 | tech = this.tech_, | ||
877 | // !!The order of the next two assignments is important!! | ||
878 | // `currentTime` must be equal-to or greater-than the start of the | ||
879 | // buffered range. Flash executes out-of-process so, every value can | ||
880 | // change behind the scenes from line-to-line. By reading `currentTime` | ||
881 | // after `buffered`, we ensure that it is always a current or later | ||
882 | // value during playback. | ||
883 | buffered = tech.buffered(); | ||
884 | |||
885 | |||
886 | if (time === undefined) { | ||
887 | time = tech.currentTime(); | ||
888 | } | ||
889 | |||
890 | if (buffered && buffered.length) { | ||
891 | // Search for a range containing the play-head | ||
892 | for (i = 0; i < buffered.length; i++) { | ||
893 | if (predicate(buffered.start(i), buffered.end(i), time)) { | ||
894 | ranges.push([buffered.start(i), buffered.end(i)]); | ||
895 | } | ||
896 | } | 952 | } |
897 | } | ||
898 | |||
899 | return videojs.createTimeRanges(ranges); | ||
900 | }; | ||
901 | }; | ||
902 | |||
903 | /** | ||
904 | * Attempts to find the buffered TimeRange that contains the specified | ||
905 | * time, or where playback is currently happening if no specific time | ||
906 | * is specified. | ||
907 | * @param time (optional) {number} the time to filter on. Defaults to | ||
908 | * currentTime. | ||
909 | * @return a new TimeRanges object. | ||
910 | */ | ||
911 | videojs.HlsHandler.prototype.findBufferedRange_ = filterBufferedRanges(function(start, end, time) { | ||
912 | return start - TIME_FUDGE_FACTOR <= time && | ||
913 | end + TIME_FUDGE_FACTOR >= time; | ||
914 | }); | ||
915 | |||
916 | /** | ||
917 | * Returns the TimeRanges that begin at or later than the specified | ||
918 | * time. | ||
919 | * @param time (optional) {number} the time to filter on. Defaults to | ||
920 | * currentTime. | ||
921 | * @return a new TimeRanges object. | ||
922 | */ | ||
923 | videojs.HlsHandler.prototype.findNextBufferedRange_ = filterBufferedRanges(function(start, end, time) { | ||
924 | return start - TIME_FUDGE_FACTOR >= time; | ||
925 | }); | ||
926 | 953 | ||
927 | /** | 954 | /** |
928 | * Determines whether there is enough video data currently in the buffer | 955 | * Determines whether there is enough video data currently in the buffer |
929 | * and downloads a new segment if the buffered time is less than the goal. | 956 | * and downloads a new segment if the buffered time is less than the goal. |
930 | * @param seekToTime (optional) {number} the offset into the downloaded segment | 957 | * @param seekToTime (optional) {number} the offset into the downloaded segment |
931 | * to seek to, in seconds | 958 | * to seek to, in seconds |
932 | */ | 959 | */ |
933 | videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { | 960 | fillBuffer(mediaIndex) { |
934 | var | 961 | let tech = this.tech_; |
935 | tech = this.tech_, | 962 | let currentTime = tech.currentTime(); |
936 | currentTime = tech.currentTime(), | 963 | let hasBufferedContent = (this.tech_.buffered().length !== 0); |
937 | hasBufferedContent = (this.tech_.buffered().length !== 0), | 964 | let currentBuffered = this.findBufferedRange_(); |
938 | currentBuffered = this.findBufferedRange_(), | 965 | let outsideBufferedRanges = !(currentBuffered && currentBuffered.length); |
939 | outsideBufferedRanges = !(currentBuffered && currentBuffered.length), | 966 | let currentBufferedEnd = 0; |
940 | currentBufferedEnd = 0, | 967 | let bufferedTime = 0; |
941 | bufferedTime = 0, | 968 | let segment; |
942 | segment, | 969 | let segmentInfo; |
943 | segmentInfo, | 970 | let segmentTimestampOffset; |
944 | segmentTimestampOffset; | ||
945 | 971 | ||
946 | // if preload is set to "none", do not download segments until playback is requested | 972 | // if preload is set to "none", do not download segments until playback is requested |
947 | if (this.loadingState_ !== 'segments') { | 973 | if (this.loadingState_ !== 'segments') { |
... | @@ -964,7 +990,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { | ... | @@ -964,7 +990,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { |
964 | } | 990 | } |
965 | 991 | ||
966 | // if no segments are available, do nothing | 992 | // if no segments are available, do nothing |
967 | if (this.playlists.state === "HAVE_NOTHING" || | 993 | if (this.playlists.state === 'HAVE_NOTHING' || |
968 | !this.playlists.media() || | 994 | !this.playlists.media() || |
969 | !this.playlists.media().segments) { | 995 | !this.playlists.media().segments) { |
970 | return; | 996 | return; |
... | @@ -975,7 +1001,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { | ... | @@ -975,7 +1001,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { |
975 | return; | 1001 | return; |
976 | } | 1002 | } |
977 | 1003 | ||
978 | if (mediaIndex === undefined) { | 1004 | if (typeof mediaIndex === 'undefined') { |
979 | if (currentBuffered && currentBuffered.length) { | 1005 | if (currentBuffered && currentBuffered.length) { |
980 | currentBufferedEnd = currentBuffered.end(0); | 1006 | currentBufferedEnd = currentBuffered.end(0); |
981 | mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd); | 1007 | mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd); |
... | @@ -983,7 +1009,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { | ... | @@ -983,7 +1009,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { |
983 | 1009 | ||
984 | // if there is plenty of content in the buffer and we're not | 1010 | // if there is plenty of content in the buffer and we're not |
985 | // seeking, relax for awhile | 1011 | // seeking, relax for awhile |
986 | if (bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { | 1012 | if (bufferedTime >= Hls.GOAL_BUFFER_LENGTH) { |
987 | return; | 1013 | return; |
988 | } | 1014 | } |
989 | } else { | 1015 | } else { |
... | @@ -1010,12 +1036,12 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { | ... | @@ -1010,12 +1036,12 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { |
1010 | // resolve the segment URL relative to the playlist | 1036 | // resolve the segment URL relative to the playlist |
1011 | uri: this.playlistUriToUrl(segment.uri), | 1037 | uri: this.playlistUriToUrl(segment.uri), |
1012 | // the segment's mediaIndex & mediaSequence at the time it was requested | 1038 | // the segment's mediaIndex & mediaSequence at the time it was requested |
1013 | mediaIndex: mediaIndex, | 1039 | mediaIndex, |
1014 | mediaSequence: this.playlists.media().mediaSequence, | 1040 | mediaSequence: this.playlists.media().mediaSequence, |
1015 | // the segment's playlist | 1041 | // the segment's playlist |
1016 | playlist: this.playlists.media(), | 1042 | playlist: this.playlists.media(), |
1017 | // The state of the buffer when this segment was requested | 1043 | // The state of the buffer when this segment was requested |
1018 | currentBufferedEnd: currentBufferedEnd, | 1044 | currentBufferedEnd, |
1019 | // unencrypted bytes of the segment | 1045 | // unencrypted bytes of the segment |
1020 | bytes: null, | 1046 | bytes: null, |
1021 | // when a key is defined for this segment, the encrypted bytes | 1047 | // when a key is defined for this segment, the encrypted bytes |
... | @@ -1032,7 +1058,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { | ... | @@ -1032,7 +1058,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { |
1032 | }; | 1058 | }; |
1033 | 1059 | ||
1034 | if (mediaIndex > 0) { | 1060 | if (mediaIndex > 0) { |
1035 | segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, | 1061 | segmentTimestampOffset = Hls.Playlist.duration(segmentInfo.playlist, |
1036 | segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_; | 1062 | segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_; |
1037 | } | 1063 | } |
1038 | 1064 | ||
... | @@ -1055,43 +1081,50 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { | ... | @@ -1055,43 +1081,50 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { |
1055 | } | 1081 | } |
1056 | 1082 | ||
1057 | this.loadSegment(segmentInfo); | 1083 | this.loadSegment(segmentInfo); |
1058 | }; | 1084 | } |
1085 | |||
1086 | playlistUriToUrl(segmentRelativeUrl) { | ||
1087 | let playListUrl; | ||
1059 | 1088 | ||
1060 | videojs.HlsHandler.prototype.playlistUriToUrl = function(segmentRelativeUrl) { | ||
1061 | var playListUrl; | ||
1062 | // resolve the segment URL relative to the playlist | 1089 | // resolve the segment URL relative to the playlist |
1063 | if (this.playlists.media().uri === this.source_.src) { | 1090 | if (this.playlists.media().uri === this.source_.src) { |
1064 | playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl); | 1091 | playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl); |
1065 | } else { | 1092 | } else { |
1066 | playListUrl = resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''), segmentRelativeUrl); | 1093 | playListUrl = |
1094 | resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''), | ||
1095 | segmentRelativeUrl); | ||
1067 | } | 1096 | } |
1068 | return playListUrl; | 1097 | return playListUrl; |
1069 | }; | 1098 | } |
1070 | 1099 | ||
1071 | /* Turns segment byterange into a string suitable for use in | 1100 | /* |
1101 | * Turns segment byterange into a string suitable for use in | ||
1072 | * HTTP Range requests | 1102 | * HTTP Range requests |
1073 | */ | 1103 | */ |
1074 | videojs.HlsHandler.prototype.byterangeStr_ = function(byterange) { | 1104 | byterangeStr_(byterange) { |
1075 | var byterangeStart, byterangeEnd; | 1105 | let byterangeStart; |
1106 | let byterangeEnd; | ||
1076 | 1107 | ||
1077 | // `byterangeEnd` is one less than `offset + length` because the HTTP range | 1108 | // `byterangeEnd` is one less than `offset + length` because the HTTP range |
1078 | // header uses inclusive ranges | 1109 | // header uses inclusive ranges |
1079 | byterangeEnd = byterange.offset + byterange.length - 1; | 1110 | byterangeEnd = byterange.offset + byterange.length - 1; |
1080 | byterangeStart = byterange.offset; | 1111 | byterangeStart = byterange.offset; |
1081 | return "bytes=" + byterangeStart + "-" + byterangeEnd; | 1112 | return 'bytes=' + byterangeStart + '-' + byterangeEnd; |
1082 | }; | 1113 | } |
1083 | 1114 | ||
1084 | /* Defines headers for use in the xhr request for a particular segment. | 1115 | /* |
1116 | * Defines headers for use in the xhr request for a particular segment. | ||
1085 | */ | 1117 | */ |
1086 | videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) { | 1118 | segmentXhrHeaders_(segment) { |
1087 | var headers = {}; | 1119 | let headers = {}; |
1120 | |||
1088 | if ('byterange' in segment) { | 1121 | if ('byterange' in segment) { |
1089 | headers['Range'] = this.byterangeStr_(segment.byterange); | 1122 | headers.Range = this.byterangeStr_(segment.byterange); |
1090 | } | 1123 | } |
1091 | return headers; | 1124 | return headers; |
1092 | }; | 1125 | } |
1093 | 1126 | ||
1094 | /* | 1127 | /* |
1095 | * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived. | 1128 | * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived. |
1096 | * Expects an object with: | 1129 | * Expects an object with: |
1097 | * * `roundTripTime` - the round trip time for the request we're setting the time for | 1130 | * * `roundTripTime` - the round trip time for the request we're setting the time for |
... | @@ -1099,22 +1132,23 @@ videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) { | ... | @@ -1099,22 +1132,23 @@ videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) { |
1099 | * * `bytesReceived` - amount of bytes downloaded | 1132 | * * `bytesReceived` - amount of bytes downloaded |
1100 | * `bandwidth` is the only required property. | 1133 | * `bandwidth` is the only required property. |
1101 | */ | 1134 | */ |
1102 | videojs.HlsHandler.prototype.setBandwidth = function(xhr) { | 1135 | setBandwidth(localXhr) { |
1103 | // calculate the download bandwidth | 1136 | // calculate the download bandwidth |
1104 | this.segmentXhrTime = xhr.roundTripTime; | 1137 | this.segmentXhrTime = localXhr.roundTripTime; |
1105 | this.bandwidth = xhr.bandwidth; | 1138 | this.bandwidth = localXhr.bandwidth; |
1106 | this.bytesReceived += xhr.bytesReceived || 0; | 1139 | this.bytesReceived += localXhr.bytesReceived || 0; |
1107 | 1140 | ||
1108 | this.tech_.trigger('bandwidthupdate'); | 1141 | this.tech_.trigger('bandwidthupdate'); |
1109 | }; | 1142 | } |
1110 | 1143 | ||
1111 | /* | 1144 | /* |
1112 | * Blacklists a playlist when an error occurs for a set amount of time | 1145 | * Blacklists a playlist when an error occurs for a set amount of time |
1113 | * making it unavailable for selection by the rendition selection algorithm | 1146 | * making it unavailable for selection by the rendition selection algorithm |
1114 | * and then forces a new playlist (rendition) selection. | 1147 | * and then forces a new playlist (rendition) selection. |
1115 | */ | 1148 | */ |
1116 | videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) { | 1149 | blacklistCurrentPlaylist_(error) { |
1117 | var currentPlaylist, nextPlaylist; | 1150 | let currentPlaylist; |
1151 | let nextPlaylist; | ||
1118 | 1152 | ||
1119 | // If the `error` was generated by the playlist loader, it will contain | 1153 | // If the `error` was generated by the playlist loader, it will contain |
1120 | // the playlist we were trying to load (but failed) and that should be | 1154 | // the playlist we were trying to load (but failed) and that should be |
... | @@ -1136,27 +1170,28 @@ videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) { | ... | @@ -1136,27 +1170,28 @@ videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) { |
1136 | nextPlaylist = this.selectPlaylist(); | 1170 | nextPlaylist = this.selectPlaylist(); |
1137 | 1171 | ||
1138 | if (nextPlaylist) { | 1172 | if (nextPlaylist) { |
1139 | videojs.log.warn('Problem encountered with the current HLS playlist. Switching to another playlist.'); | 1173 | videojs.log.warn('Problem encountered with the current ' + |
1174 | 'HLS playlist. Switching to another playlist.'); | ||
1140 | 1175 | ||
1141 | return this.playlists.media(nextPlaylist); | 1176 | return this.playlists.media(nextPlaylist); |
1142 | } else { | 1177 | } |
1143 | videojs.log.warn('Problem encountered with the current HLS playlist. No suitable alternatives found.'); | 1178 | videojs.log.warn('Problem encountered with the current ' + |
1179 | 'HLS playlist. No suitable alternatives found.'); | ||
1144 | // We have no more playlists we can select so we must fail | 1180 | // We have no more playlists we can select so we must fail |
1145 | this.error = error; | 1181 | this.error = error; |
1146 | return this.mediaSource.endOfStream('network'); | 1182 | return this.mediaSource.endOfStream('network'); |
1147 | } | 1183 | } |
1148 | }; | ||
1149 | 1184 | ||
1150 | videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { | 1185 | loadSegment(segmentInfo) { |
1151 | var | 1186 | let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; |
1152 | self = this, | 1187 | let removeToTime = 0; |
1153 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex], | 1188 | let seekable = this.seekable(); |
1154 | removeToTime = 0, | 1189 | let currentTime = this.tech_.currentTime(); |
1155 | seekable = this.seekable(), | ||
1156 | currentTime = this.tech_.currentTime(); | ||
1157 | 1190 | ||
1158 | // Chrome has a hard limit of 150mb of buffer and a very conservative "garbage collector" | 1191 | // Chrome has a hard limit of 150mb of |
1159 | // We manually clear out the old buffer to ensure we don't trigger the QuotaExceeded error | 1192 | // buffer and a very conservative "garbage collector" |
1193 | // We manually clear out the old buffer to ensure | ||
1194 | // we don't trigger the QuotaExceeded error | ||
1160 | // on the source buffer during subsequent appends | 1195 | // on the source buffer during subsequent appends |
1161 | if (this.sourceBuffer && !this.sourceBuffer.updating) { | 1196 | if (this.sourceBuffer && !this.sourceBuffer.updating) { |
1162 | // If we have a seekable range use that as the limit for what can be removed safely | 1197 | // If we have a seekable range use that as the limit for what can be removed safely |
... | @@ -1178,7 +1213,7 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { | ... | @@ -1178,7 +1213,7 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { |
1178 | } | 1213 | } |
1179 | 1214 | ||
1180 | // request the next segment | 1215 | // request the next segment |
1181 | this.segmentXhr_ = videojs.Hls.xhr({ | 1216 | this.segmentXhr_ = Hls.xhr({ |
1182 | uri: segmentInfo.uri, | 1217 | uri: segmentInfo.uri, |
1183 | responseType: 'arraybuffer', | 1218 | responseType: 'arraybuffer', |
1184 | withCredentials: this.source_.withCredentials, | 1219 | withCredentials: this.source_.withCredentials, |
... | @@ -1187,25 +1222,25 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { | ... | @@ -1187,25 +1222,25 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { |
1187 | // decrease in network performance or a server issue. | 1222 | // decrease in network performance or a server issue. |
1188 | timeout: (segment.duration * 1.5) * 1000, | 1223 | timeout: (segment.duration * 1.5) * 1000, |
1189 | headers: this.segmentXhrHeaders_(segment) | 1224 | headers: this.segmentXhrHeaders_(segment) |
1190 | }, function(error, request) { | 1225 | }, (error, request) => { |
1191 | // This is a timeout of a previously aborted segment request | 1226 | // This is a timeout of a previously aborted segment request |
1192 | // so simply ignore it | 1227 | // so simply ignore it |
1193 | if (!self.segmentXhr_ || request !== self.segmentXhr_) { | 1228 | if (!this.segmentXhr_ || request !== this.segmentXhr_) { |
1194 | return; | 1229 | return; |
1195 | } | 1230 | } |
1196 | 1231 | ||
1197 | // the segment request is no longer outstanding | 1232 | // the segment request is no longer outstanding |
1198 | self.segmentXhr_ = null; | 1233 | this.segmentXhr_ = null; |
1199 | 1234 | ||
1200 | // if a segment request times out, we may have better luck with another playlist | 1235 | // if a segment request times out, we may have better luck with another playlist |
1201 | if (request.timedout) { | 1236 | if (request.timedout) { |
1202 | self.bandwidth = 1; | 1237 | this.bandwidth = 1; |
1203 | return self.playlists.media(self.selectPlaylist()); | 1238 | return this.playlists.media(this.selectPlaylist()); |
1204 | } | 1239 | } |
1205 | 1240 | ||
1206 | // otherwise, trigger a network error | 1241 | // otherwise, trigger a network error |
1207 | if (!request.aborted && error) { | 1242 | if (!request.aborted && error) { |
1208 | return self.blacklistCurrentPlaylist_({ | 1243 | return this.blacklistCurrentPlaylist_({ |
1209 | status: request.status, | 1244 | status: request.status, |
1210 | message: 'HLS segment request error at URL: ' + segmentInfo.uri, | 1245 | message: 'HLS segment request error at URL: ' + segmentInfo.uri, |
1211 | code: (request.status >= 500) ? 4 : 2 | 1246 | code: (request.status >= 500) ? 4 : 2 |
... | @@ -1217,8 +1252,8 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { | ... | @@ -1217,8 +1252,8 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { |
1217 | return; | 1252 | return; |
1218 | } | 1253 | } |
1219 | 1254 | ||
1220 | self.lastSegmentLoaded_ = segment; | 1255 | this.lastSegmentLoaded_ = segment; |
1221 | self.setBandwidth(request); | 1256 | this.setBandwidth(request); |
1222 | 1257 | ||
1223 | if (segment.key) { | 1258 | if (segment.key) { |
1224 | segmentInfo.encryptedBytes = new Uint8Array(request.response); | 1259 | segmentInfo.encryptedBytes = new Uint8Array(request.response); |
... | @@ -1226,28 +1261,26 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { | ... | @@ -1226,28 +1261,26 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { |
1226 | segmentInfo.bytes = new Uint8Array(request.response); | 1261 | segmentInfo.bytes = new Uint8Array(request.response); |
1227 | } | 1262 | } |
1228 | 1263 | ||
1229 | self.pendingSegment_ = segmentInfo; | 1264 | this.pendingSegment_ = segmentInfo; |
1230 | 1265 | ||
1231 | self.tech_.trigger('progress'); | 1266 | this.tech_.trigger('progress'); |
1232 | self.drainBuffer(); | 1267 | this.drainBuffer(); |
1233 | 1268 | ||
1234 | // figure out what stream the next segment should be downloaded from | 1269 | // figure out what stream the next segment should be downloaded from |
1235 | // with the updated bandwidth information | 1270 | // with the updated bandwidth information |
1236 | self.playlists.media(self.selectPlaylist()); | 1271 | this.playlists.media(this.selectPlaylist()); |
1237 | }); | 1272 | }); |
1238 | 1273 | ||
1239 | }; | 1274 | } |
1240 | 1275 | ||
1241 | videojs.HlsHandler.prototype.drainBuffer = function() { | 1276 | drainBuffer() { |
1242 | var | 1277 | let segmentInfo; |
1243 | segmentInfo, | 1278 | let mediaIndex; |
1244 | mediaIndex, | 1279 | let playlist; |
1245 | playlist, | 1280 | let bytes; |
1246 | offset, | 1281 | let segment; |
1247 | bytes, | 1282 | let decrypter; |
1248 | segment, | 1283 | let segIv; |
1249 | decrypter, | ||
1250 | segIv; | ||
1251 | 1284 | ||
1252 | // if the buffer is empty or the source buffer hasn't been created | 1285 | // if the buffer is empty or the source buffer hasn't been created |
1253 | // yet, do nothing | 1286 | // yet, do nothing |
... | @@ -1270,7 +1303,6 @@ videojs.HlsHandler.prototype.drainBuffer = function() { | ... | @@ -1270,7 +1303,6 @@ videojs.HlsHandler.prototype.drainBuffer = function() { |
1270 | segmentInfo = this.pendingSegment_; | 1303 | segmentInfo = this.pendingSegment_; |
1271 | mediaIndex = segmentInfo.mediaIndex; | 1304 | mediaIndex = segmentInfo.mediaIndex; |
1272 | playlist = segmentInfo.playlist; | 1305 | playlist = segmentInfo.playlist; |
1273 | offset = segmentInfo.offset; | ||
1274 | bytes = segmentInfo.bytes; | 1306 | bytes = segmentInfo.bytes; |
1275 | segment = playlist.segments[mediaIndex]; | 1307 | segment = playlist.segments[mediaIndex]; |
1276 | 1308 | ||
... | @@ -1284,30 +1316,30 @@ videojs.HlsHandler.prototype.drainBuffer = function() { | ... | @@ -1284,30 +1316,30 @@ videojs.HlsHandler.prototype.drainBuffer = function() { |
1284 | code: 4 | 1316 | code: 4 |
1285 | }); | 1317 | }); |
1286 | } else if (!segment.key.bytes) { | 1318 | } else if (!segment.key.bytes) { |
1287 | |||
1288 | // waiting for the key bytes, try again later | 1319 | // waiting for the key bytes, try again later |
1289 | return; | 1320 | return; |
1290 | } else if (segmentInfo.decrypter) { | 1321 | } else if (segmentInfo.decrypter) { |
1291 | |||
1292 | // decryption is in progress, try again later | 1322 | // decryption is in progress, try again later |
1293 | return; | 1323 | return; |
1294 | } else { | 1324 | } |
1295 | |||
1296 | // if the media sequence is greater than 2^32, the IV will be incorrect | 1325 | // if the media sequence is greater than 2^32, the IV will be incorrect |
1297 | // assuming 10s segments, that would be about 1300 years | 1326 | // assuming 10s segments, that would be about 1300 years |
1298 | segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); | 1327 | segIv = segment.key.iv || |
1328 | new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); | ||
1299 | 1329 | ||
1300 | // create a decrypter to incrementally decrypt the segment | 1330 | // create a decrypter to incrementally decrypt the segment |
1301 | decrypter = new videojs.Hls.Decrypter(segmentInfo.encryptedBytes, | 1331 | decrypter = new Hls.Decrypter(segmentInfo.encryptedBytes, |
1302 | segment.key.bytes, | 1332 | segment.key.bytes, |
1303 | segIv, | 1333 | segIv, |
1304 | function(err, bytes) { | 1334 | function(error, localBytes) { |
1305 | segmentInfo.bytes = bytes; | 1335 | if (error) { |
1336 | videojs.log.warn(error); | ||
1337 | } | ||
1338 | segmentInfo.bytes = localBytes; | ||
1306 | }); | 1339 | }); |
1307 | segmentInfo.decrypter = decrypter; | 1340 | segmentInfo.decrypter = decrypter; |
1308 | return; | 1341 | return; |
1309 | } | 1342 | } |
1310 | } | ||
1311 | 1343 | ||
1312 | this.pendingSegment_.buffered = this.tech_.buffered(); | 1344 | this.pendingSegment_.buffered = this.tech_.buffered(); |
1313 | 1345 | ||
... | @@ -1317,22 +1349,20 @@ videojs.HlsHandler.prototype.drainBuffer = function() { | ... | @@ -1317,22 +1349,20 @@ videojs.HlsHandler.prototype.drainBuffer = function() { |
1317 | 1349 | ||
1318 | // the segment is asynchronously added to the current buffered data | 1350 | // the segment is asynchronously added to the current buffered data |
1319 | this.sourceBuffer.appendBuffer(bytes); | 1351 | this.sourceBuffer.appendBuffer(bytes); |
1320 | }; | 1352 | } |
1321 | 1353 | ||
1322 | videojs.HlsHandler.prototype.updateEndHandler_ = function () { | 1354 | updateEndHandler_() { |
1323 | var | 1355 | let segmentInfo = this.pendingSegment_; |
1324 | segmentInfo = this.pendingSegment_, | 1356 | let playlist; |
1325 | segment, | 1357 | let currentMediaIndex; |
1326 | segments, | 1358 | let currentBuffered; |
1327 | playlist, | 1359 | let seekable; |
1328 | currentMediaIndex, | 1360 | let timelineUpdate; |
1329 | currentBuffered, | 1361 | let isEndOfStream; |
1330 | seekable, | ||
1331 | timelineUpdate, | ||
1332 | isEndOfStream; | ||
1333 | 1362 | ||
1334 | // stop here if the update errored or was aborted | 1363 | // stop here if the update errored or was aborted |
1335 | if (!segmentInfo) { | 1364 | if (!segmentInfo) { |
1365 | this.pendingSegment_ = null; | ||
1336 | return; | 1366 | return; |
1337 | } | 1367 | } |
1338 | 1368 | ||
... | @@ -1346,10 +1376,10 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { | ... | @@ -1346,10 +1376,10 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { |
1346 | this.pendingSegment_ = null; | 1376 | this.pendingSegment_ = null; |
1347 | 1377 | ||
1348 | playlist = segmentInfo.playlist; | 1378 | playlist = segmentInfo.playlist; |
1349 | segments = playlist.segments; | 1379 | currentMediaIndex = segmentInfo.mediaIndex + |
1350 | currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence); | 1380 | (segmentInfo.mediaSequence - playlist.mediaSequence); |
1351 | currentBuffered = this.findBufferedRange_(); | 1381 | currentBuffered = this.findBufferedRange_(); |
1352 | isEndOfStream = this.isEndOfStream_(playlist, currentMediaIndex, currentBuffered); | 1382 | isEndOfStream = detectEndOfStream(playlist, this.mediaSource, currentMediaIndex, currentBuffered); |
1353 | 1383 | ||
1354 | // if we switched renditions don't try to add segment timeline | 1384 | // if we switched renditions don't try to add segment timeline |
1355 | // information to the playlist | 1385 | // information to the playlist |
... | @@ -1360,10 +1390,6 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { | ... | @@ -1360,10 +1390,6 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { |
1360 | return this.fillBuffer(); | 1390 | return this.fillBuffer(); |
1361 | } | 1391 | } |
1362 | 1392 | ||
1363 | // annotate the segment with any start and end time information | ||
1364 | // added by the media processing | ||
1365 | segment = playlist.segments[currentMediaIndex]; | ||
1366 | |||
1367 | // when seeking to the beginning of the seekable range, it's | 1393 | // when seeking to the beginning of the seekable range, it's |
1368 | // possible that imprecise timing information may cause the seek to | 1394 | // possible that imprecise timing information may cause the seek to |
1369 | // end up earlier than the start of the range | 1395 | // end up earlier than the start of the range |
... | @@ -1373,19 +1399,21 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { | ... | @@ -1373,19 +1399,21 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { |
1373 | currentBuffered.length === 0) { | 1399 | currentBuffered.length === 0) { |
1374 | if (seekable.length && | 1400 | if (seekable.length && |
1375 | this.tech_.currentTime() < seekable.start(0)) { | 1401 | this.tech_.currentTime() < seekable.start(0)) { |
1376 | var next = this.findNextBufferedRange_(); | 1402 | let next = this.findNextBufferedRange_(); |
1403 | |||
1377 | if (next.length) { | 1404 | if (next.length) { |
1378 | videojs.log('tried seeking to', this.tech_.currentTime(), 'but that was too early, retrying at', next.start(0)); | 1405 | videojs.log('tried seeking to', this.tech_.currentTime(), |
1406 | 'but that was too early, retrying at', next.start(0)); | ||
1379 | this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR); | 1407 | this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR); |
1380 | } | 1408 | } |
1381 | } | 1409 | } |
1382 | } | 1410 | } |
1383 | 1411 | ||
1384 | timelineUpdate = videojs.Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, | 1412 | timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, |
1385 | this.tech_.buffered()); | 1413 | this.tech_.buffered()); |
1386 | 1414 | ||
1387 | // Update segment meta-data (duration and end-point) based on timeline | 1415 | // Update segment meta-data (duration and end-point) based on timeline |
1388 | this.updateSegmentMetadata_(playlist, currentMediaIndex, timelineUpdate); | 1416 | updateSegmentMetadata(playlist, currentMediaIndex, timelineUpdate); |
1389 | 1417 | ||
1390 | // If we decide to signal the end of stream, then we can return instead | 1418 | // If we decide to signal the end of stream, then we can return instead |
1391 | // of trying to fetch more segments | 1419 | // of trying to fetch more segments |
... | @@ -1407,42 +1435,44 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { | ... | @@ -1407,42 +1435,44 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { |
1407 | // improves subsequent media index calculations. | 1435 | // improves subsequent media index calculations. |
1408 | this.fillBuffer(currentMediaIndex + 1); | 1436 | this.fillBuffer(currentMediaIndex + 1); |
1409 | return; | 1437 | return; |
1410 | }; | 1438 | } |
1411 | 1439 | ||
1412 | /** | 1440 | /** |
1413 | * Attempt to retrieve the key for a particular media segment. | 1441 | * Attempt to retrieve the key for a particular media segment. |
1414 | */ | 1442 | */ |
1415 | videojs.HlsHandler.prototype.fetchKey_ = function(segment) { | 1443 | fetchKey_(segment) { |
1416 | var key, self, settings, receiveKey; | 1444 | let key; |
1445 | let settings; | ||
1446 | let receiveKey; | ||
1417 | 1447 | ||
1418 | // if there is a pending XHR or no segments, don't do anything | 1448 | // if there is a pending XHR or no segments, don't do anything |
1419 | if (this.keyXhr_) { | 1449 | if (this.keyXhr_) { |
1420 | return; | 1450 | return; |
1421 | } | 1451 | } |
1422 | 1452 | ||
1423 | self = this; | ||
1424 | settings = this.options_; | 1453 | settings = this.options_; |
1425 | 1454 | ||
1426 | /** | 1455 | /** |
1427 | * Handle a key XHR response. | 1456 | * Handle a key XHR response. |
1428 | */ | 1457 | */ |
1429 | receiveKey = function(key) { | 1458 | receiveKey = (keyRecieved) => { |
1430 | return function(error, request) { | 1459 | return (error, request) => { |
1431 | var view; | 1460 | let view; |
1432 | self.keyXhr_ = null; | 1461 | |
1462 | this.keyXhr_ = null; | ||
1433 | 1463 | ||
1434 | if (error || !request.response || request.response.byteLength !== 16) { | 1464 | if (error || !request.response || request.response.byteLength !== 16) { |
1435 | key.retries = key.retries || 0; | 1465 | keyRecieved.retries = keyRecieved.retries || 0; |
1436 | key.retries++; | 1466 | keyRecieved.retries++; |
1437 | if (!request.aborted) { | 1467 | if (!request.aborted) { |
1438 | // try fetching again | 1468 | // try fetching again |
1439 | self.fetchKey_(segment); | 1469 | this.fetchKey_(segment); |
1440 | } | 1470 | } |
1441 | return; | 1471 | return; |
1442 | } | 1472 | } |
1443 | 1473 | ||
1444 | view = new DataView(request.response); | 1474 | view = new DataView(request.response); |
1445 | key.bytes = new Uint32Array([ | 1475 | keyRecieved.bytes = new Uint32Array([ |
1446 | view.getUint32(0), | 1476 | view.getUint32(0), |
1447 | view.getUint32(4), | 1477 | view.getUint32(4), |
1448 | view.getUint32(8), | 1478 | view.getUint32(8), |
... | @@ -1450,7 +1480,7 @@ videojs.HlsHandler.prototype.fetchKey_ = function(segment) { | ... | @@ -1450,7 +1480,7 @@ videojs.HlsHandler.prototype.fetchKey_ = function(segment) { |
1450 | ]); | 1480 | ]); |
1451 | 1481 | ||
1452 | // check to see if this allows us to make progress buffering now | 1482 | // check to see if this allows us to make progress buffering now |
1453 | self.checkBuffer_(); | 1483 | this.checkBuffer_(); |
1454 | }; | 1484 | }; |
1455 | }; | 1485 | }; |
1456 | 1486 | ||
... | @@ -1463,135 +1493,105 @@ videojs.HlsHandler.prototype.fetchKey_ = function(segment) { | ... | @@ -1463,135 +1493,105 @@ videojs.HlsHandler.prototype.fetchKey_ = function(segment) { |
1463 | 1493 | ||
1464 | // request the key if the retry limit hasn't been reached | 1494 | // request the key if the retry limit hasn't been reached |
1465 | if (!key.bytes && !keyFailed(key)) { | 1495 | if (!key.bytes && !keyFailed(key)) { |
1466 | this.keyXhr_ = videojs.Hls.xhr({ | 1496 | this.keyXhr_ = Hls.xhr({ |
1467 | uri: this.playlistUriToUrl(key.uri), | 1497 | uri: this.playlistUriToUrl(key.uri), |
1468 | responseType: 'arraybuffer', | 1498 | responseType: 'arraybuffer', |
1469 | withCredentials: settings.withCredentials | 1499 | withCredentials: settings.withCredentials |
1470 | }, receiveKey(key)); | 1500 | }, receiveKey(key)); |
1471 | return; | 1501 | return; |
1472 | } | 1502 | } |
1473 | }; | 1503 | } |
1504 | } | ||
1474 | 1505 | ||
1475 | /** | 1506 | /** |
1476 | * Whether the browser has built-in HLS support. | 1507 | * Attempts to find the buffered TimeRange that contains the specified |
1508 | * time, or where playback is currently happening if no specific time | ||
1509 | * is specified. | ||
1510 | * @param time (optional) {number} the time to filter on. Defaults to | ||
1511 | * currentTime. | ||
1512 | * @return a new TimeRanges object. | ||
1477 | */ | 1513 | */ |
1478 | videojs.Hls.supportsNativeHls = (function() { | 1514 | HlsHandler.prototype.findBufferedRange_ = |
1479 | var | 1515 | filterBufferedRanges(function(start, end, time) { |
1480 | video = document.createElement('video'), | 1516 | return start - TIME_FUDGE_FACTOR <= time && |
1481 | xMpegUrl, | 1517 | end + TIME_FUDGE_FACTOR >= time; |
1482 | vndMpeg; | 1518 | }); |
1483 | |||
1484 | // native HLS is definitely not supported if HTML5 video isn't | ||
1485 | if (!videojs.getComponent('Html5').isSupported()) { | ||
1486 | return false; | ||
1487 | } | ||
1488 | |||
1489 | xMpegUrl = video.canPlayType('application/x-mpegURL'); | ||
1490 | vndMpeg = video.canPlayType('application/vnd.apple.mpegURL'); | ||
1491 | return (/probably|maybe/).test(xMpegUrl) || | ||
1492 | (/probably|maybe/).test(vndMpeg); | ||
1493 | })(); | ||
1494 | |||
1495 | // HLS is a source handler, not a tech. Make sure attempts to use it | ||
1496 | // as one do not cause exceptions. | ||
1497 | videojs.Hls.isSupported = function() { | ||
1498 | return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + | ||
1499 | 'your player\'s techOrder.'); | ||
1500 | }; | ||
1501 | |||
1502 | /** | 1519 | /** |
1503 | * A comparator function to sort two playlist object by bandwidth. | 1520 | * Returns the TimeRanges that begin at or later than the specified |
1504 | * @param left {object} a media playlist object | 1521 | * time. |
1505 | * @param right {object} a media playlist object | 1522 | * @param time (optional) {number} the time to filter on. Defaults to |
1506 | * @return {number} Greater than zero if the bandwidth attribute of | 1523 | * currentTime. |
1507 | * left is greater than the corresponding attribute of right. Less | 1524 | * @return a new TimeRanges object. |
1508 | * than zero if the bandwidth of right is greater than left and | ||
1509 | * exactly zero if the two are equal. | ||
1510 | */ | 1525 | */ |
1511 | videojs.Hls.comparePlaylistBandwidth = function(left, right) { | 1526 | HlsHandler.prototype.findNextBufferedRange_ = |
1512 | var leftBandwidth, rightBandwidth; | 1527 | filterBufferedRanges(function(start, end, time) { |
1513 | if (left.attributes && left.attributes.BANDWIDTH) { | 1528 | return start - TIME_FUDGE_FACTOR >= time; |
1514 | leftBandwidth = left.attributes.BANDWIDTH; | 1529 | }); |
1515 | } | ||
1516 | leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; | ||
1517 | if (right.attributes && right.attributes.BANDWIDTH) { | ||
1518 | rightBandwidth = right.attributes.BANDWIDTH; | ||
1519 | } | ||
1520 | rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; | ||
1521 | |||
1522 | return leftBandwidth - rightBandwidth; | ||
1523 | }; | ||
1524 | 1530 | ||
1525 | /** | 1531 | /** |
1526 | * A comparator function to sort two playlist object by resolution (width). | 1532 | * The Source Handler object, which informs video.js what additional |
1527 | * @param left {object} a media playlist object | 1533 | * MIME types are supported and sets up playback. It is registered |
1528 | * @param right {object} a media playlist object | 1534 | * automatically to the appropriate tech based on the capabilities of |
1529 | * @return {number} Greater than zero if the resolution.width attribute of | 1535 | * the browser it is running in. It is not necessary to use or modify |
1530 | * left is greater than the corresponding attribute of right. Less | 1536 | * this object in normal usage. |
1531 | * than zero if the resolution.width of right is greater than left and | ||
1532 | * exactly zero if the two are equal. | ||
1533 | */ | 1537 | */ |
1534 | videojs.Hls.comparePlaylistResolution = function(left, right) { | 1538 | const HlsSourceHandler = function(mode) { |
1535 | var leftWidth, rightWidth; | 1539 | return { |
1536 | 1540 | canHandleSource(srcObj) { | |
1537 | if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { | 1541 | return HlsSourceHandler.canPlayType(srcObj.type); |
1538 | leftWidth = left.attributes.RESOLUTION.width; | 1542 | }, |
1543 | handleSource(source, tech) { | ||
1544 | if (mode === 'flash') { | ||
1545 | // We need to trigger this asynchronously to give others the chance | ||
1546 | // to bind to the event when a source is set at player creation | ||
1547 | tech.setTimeout(function() { | ||
1548 | tech.trigger('loadstart'); | ||
1549 | }, 1); | ||
1539 | } | 1550 | } |
1540 | 1551 | tech.hls = new HlsHandler(tech, { | |
1541 | leftWidth = leftWidth || window.Number.MAX_VALUE; | 1552 | source, |
1542 | 1553 | mode | |
1543 | if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { | 1554 | }); |
1544 | rightWidth = right.attributes.RESOLUTION.width; | 1555 | tech.hls.src(source.src); |
1556 | return tech.hls; | ||
1557 | }, | ||
1558 | canPlayType(type) { | ||
1559 | return HlsSourceHandler.canPlayType(type); | ||
1545 | } | 1560 | } |
1561 | }; | ||
1562 | }; | ||
1546 | 1563 | ||
1547 | rightWidth = rightWidth || window.Number.MAX_VALUE; | 1564 | HlsSourceHandler.canPlayType = function(type) { |
1565 | let mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; | ||
1548 | 1566 | ||
1549 | // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions | 1567 | // favor native HLS support if it's available |
1550 | // have the same media dimensions/ resolution | 1568 | if (Hls.supportsNativeHls) { |
1551 | if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { | 1569 | return false; |
1552 | return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; | ||
1553 | } else { | ||
1554 | return leftWidth - rightWidth; | ||
1555 | } | 1570 | } |
1571 | return mpegurlRE.test(type); | ||
1556 | }; | 1572 | }; |
1557 | 1573 | ||
1558 | /** | 1574 | if (typeof videojs.MediaSource === 'undefined' || |
1559 | * Constructs a new URI by interpreting a path relative to another | 1575 | typeof videojs.URL === 'undefined') { |
1560 | * URI. | 1576 | videojs.MediaSource = MediaSource; |
1561 | * @param basePath {string} a relative or absolute URI | 1577 | videojs.URL = URL; |
1562 | * @param path {string} a path part to combine with the base | 1578 | } |
1563 | * @return {string} a URI that is equivalent to composing `base` | 1579 | |
1564 | * with `path` | 1580 | // register source handlers with the appropriate techs |
1565 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | 1581 | if (MediaSource.supportsNativeMediaSources()) { |
1566 | */ | 1582 | videojs.getComponent('Html5').registerSourceHandler(HlsSourceHandler('html5')); |
1567 | resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) { | 1583 | } |
1568 | // use the base element to get the browser to handle URI resolution | 1584 | if (window.Uint8Array) { |
1569 | var | 1585 | videojs.getComponent('Flash').registerSourceHandler(HlsSourceHandler('flash')); |
1570 | oldBase = document.querySelector('base'), | 1586 | } |
1571 | docHead = document.querySelector('head'), | ||
1572 | a = document.createElement('a'), | ||
1573 | base = oldBase, | ||
1574 | oldHref, | ||
1575 | result; | ||
1576 | |||
1577 | // prep the document | ||
1578 | if (oldBase) { | ||
1579 | oldHref = oldBase.href; | ||
1580 | } else { | ||
1581 | base = docHead.appendChild(document.createElement('base')); | ||
1582 | } | ||
1583 | 1587 | ||
1584 | base.href = basePath; | 1588 | videojs.HlsHandler = HlsHandler; |
1585 | a.href = path; | 1589 | videojs.HlsSourceHandler = HlsSourceHandler; |
1586 | result = a.href; | 1590 | videojs.Hls = Hls; |
1591 | videojs.m3u8 = m3u8; | ||
1587 | 1592 | ||
1588 | // clean up | 1593 | export default { |
1589 | if (oldBase) { | 1594 | Hls, |
1590 | oldBase.href = oldHref; | 1595 | HlsHandler, |
1591 | } else { | 1596 | HlsSourceHandler |
1592 | docHead.removeChild(base); | ||
1593 | } | ||
1594 | return result; | ||
1595 | }; | 1597 | }; |
1596 | |||
1597 | })(window, window.videojs, document); | ... | ... |
1 | (function(videojs) { | 1 | /** |
2 | 'use strict'; | ||
3 | |||
4 | /** | ||
5 | * A wrapper for videojs.xhr that tracks bandwidth. | 2 | * A wrapper for videojs.xhr that tracks bandwidth. |
6 | */ | 3 | */ |
7 | videojs.Hls.xhr = function(options, callback) { | 4 | import {xhr as videojsXHR, mergeOptions} from 'video.js'; |
5 | const xhr = function(options, callback) { | ||
8 | // Add a default timeout for all hls requests | 6 | // Add a default timeout for all hls requests |
9 | options = videojs.mergeOptions({ | 7 | options = mergeOptions({ |
10 | timeout: 45e3 | 8 | timeout: 45e3 |
11 | }, options); | 9 | }, options); |
12 | 10 | ||
13 | var request = videojs.xhr(options, function(error, response) { | 11 | let request = videojsXHR(options, function(error, response) { |
14 | if (!error && request.response) { | 12 | if (!error && request.response) { |
15 | request.responseTime = (new Date()).getTime(); | 13 | request.responseTime = (new Date()).getTime(); |
16 | request.roundTripTime = request.responseTime - request.requestTime; | 14 | request.roundTripTime = request.responseTime - request.requestTime; |
17 | request.bytesReceived = request.response.byteLength || request.response.length; | 15 | request.bytesReceived = request.response.byteLength || request.response.length; |
18 | if (!request.bandwidth) { | 16 | if (!request.bandwidth) { |
19 | request.bandwidth = Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); | 17 | request.bandwidth = |
18 | Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); | ||
20 | } | 19 | } |
21 | } | 20 | } |
22 | 21 | ||
23 | // videojs.xhr now uses a specific code on the error object to signal that a request has | 22 | // videojs.xhr now uses a specific code |
23 | // on the error object to signal that a request has | ||
24 | // timed out errors of setting a boolean on the request object | 24 | // timed out errors of setting a boolean on the request object |
25 | if (error || request.timedout) { | 25 | if (error || request.timedout) { |
26 | request.timedout = request.timedout || (error.code === 'ETIMEDOUT'); | 26 | request.timedout = request.timedout || (error.code === 'ETIMEDOUT'); |
... | @@ -44,5 +44,6 @@ | ... | @@ -44,5 +44,6 @@ |
44 | 44 | ||
45 | request.requestTime = (new Date()).getTime(); | 45 | request.requestTime = (new Date()).getTime(); |
46 | return request; | 46 | return request; |
47 | }; | 47 | }; |
48 | })(window.videojs); | 48 | |
49 | export default xhr; | ... | ... |
test/.jshintrc
deleted
100644 → 0
1 | { | ||
2 | "curly": true, | ||
3 | "eqeqeq": true, | ||
4 | "immed": true, | ||
5 | "latedef": true, | ||
6 | "newcap": true, | ||
7 | "noarg": true, | ||
8 | "sub": true, | ||
9 | "undef": true, | ||
10 | "unused": true, | ||
11 | "boss": true, | ||
12 | "eqnull": true, | ||
13 | "browser": true, | ||
14 | "node": true, | ||
15 | "predef": [ | ||
16 | "QUnit", | ||
17 | "module", | ||
18 | "test", | ||
19 | "asyncTest", | ||
20 | "expect", | ||
21 | "start", | ||
22 | "stop", | ||
23 | "ok", | ||
24 | "equal", | ||
25 | "notEqual", | ||
26 | "deepEqual", | ||
27 | "notDeepEqual", | ||
28 | "strictEqual", | ||
29 | "notStrictEqual", | ||
30 | "throws", | ||
31 | "sinon", | ||
32 | "process" | ||
33 | ] | ||
34 | } |
test/decrypter.test.js
0 → 100644
1 | // see docs/hlse.md for instructions on how test data was generated | ||
2 | import QUnit from 'qunit'; | ||
3 | import {unpad} from 'pkcs7'; | ||
4 | import sinon from 'sinon'; | ||
5 | import {decrypt, Decrypter, AsyncStream} from '../src/decrypter'; | ||
6 | |||
7 | // see docs/hlse.md for instructions on how test data was generated | ||
8 | const stringFromBytes = function(bytes) { | ||
9 | let result = ''; | ||
10 | |||
11 | for (let i = 0; i < bytes.length; i++) { | ||
12 | result += String.fromCharCode(bytes[i]); | ||
13 | } | ||
14 | return result; | ||
15 | }; | ||
16 | |||
17 | QUnit.module('Decryption'); | ||
18 | QUnit.test('decrypts a single AES-128 with PKCS7 block', function() { | ||
19 | let key = new Uint32Array([0, 0, 0, 0]); | ||
20 | let initVector = key; | ||
21 | // the string "howdy folks" encrypted | ||
22 | let encrypted = new Uint8Array([ | ||
23 | 0xce, 0x90, 0x97, 0xd0, | ||
24 | 0x08, 0x46, 0x4d, 0x18, | ||
25 | 0x4f, 0xae, 0x01, 0x1c, | ||
26 | 0x82, 0xa8, 0xf0, 0x67 | ||
27 | ]); | ||
28 | |||
29 | QUnit.deepEqual('howdy folks', | ||
30 | stringFromBytes(unpad(decrypt(encrypted, key, initVector))), | ||
31 | 'decrypted with a byte array key' | ||
32 | ); | ||
33 | }); | ||
34 | |||
35 | QUnit.test('decrypts multiple AES-128 blocks with CBC', function() { | ||
36 | let key = new Uint32Array([0, 0, 0, 0]); | ||
37 | let initVector = key; | ||
38 | // the string "0123456789abcdef01234" encrypted | ||
39 | let encrypted = new Uint8Array([ | ||
40 | 0x14, 0xf5, 0xfe, 0x74, | ||
41 | 0x69, 0x66, 0xf2, 0x92, | ||
42 | 0x65, 0x1c, 0x22, 0x88, | ||
43 | 0xbb, 0xff, 0x46, 0x09, | ||
44 | |||
45 | 0x0b, 0xde, 0x5e, 0x71, | ||
46 | 0x77, 0x87, 0xeb, 0x84, | ||
47 | 0xa9, 0x54, 0xc2, 0x45, | ||
48 | 0xe9, 0x4e, 0x29, 0xb3 | ||
49 | ]); | ||
50 | |||
51 | QUnit.deepEqual('0123456789abcdef01234', | ||
52 | stringFromBytes(unpad(decrypt(encrypted, key, initVector))), | ||
53 | 'decrypted multiple blocks'); | ||
54 | }); | ||
55 | |||
56 | QUnit.test( | ||
57 | 'verify that the deepcopy works by doing two decrypts in the same test', | ||
58 | function() { | ||
59 | let key = new Uint32Array([0, 0, 0, 0]); | ||
60 | let initVector = key; | ||
61 | // the string "howdy folks" encrypted | ||
62 | let pkcs7Block = new Uint8Array([ | ||
63 | 0xce, 0x90, 0x97, 0xd0, | ||
64 | 0x08, 0x46, 0x4d, 0x18, | ||
65 | 0x4f, 0xae, 0x01, 0x1c, | ||
66 | 0x82, 0xa8, 0xf0, 0x67 | ||
67 | ]); | ||
68 | |||
69 | QUnit.deepEqual('howdy folks', | ||
70 | stringFromBytes(unpad(decrypt(pkcs7Block, key, initVector))), | ||
71 | 'decrypted with a byte array key' | ||
72 | ); | ||
73 | |||
74 | // the string "0123456789abcdef01234" encrypted | ||
75 | let cbcBlocks = new Uint8Array([ | ||
76 | 0x14, 0xf5, 0xfe, 0x74, | ||
77 | 0x69, 0x66, 0xf2, 0x92, | ||
78 | 0x65, 0x1c, 0x22, 0x88, | ||
79 | 0xbb, 0xff, 0x46, 0x09, | ||
80 | |||
81 | 0x0b, 0xde, 0x5e, 0x71, | ||
82 | 0x77, 0x87, 0xeb, 0x84, | ||
83 | 0xa9, 0x54, 0xc2, 0x45, | ||
84 | 0xe9, 0x4e, 0x29, 0xb3 | ||
85 | ]); | ||
86 | |||
87 | QUnit.deepEqual('0123456789abcdef01234', | ||
88 | stringFromBytes(unpad(decrypt(cbcBlocks, key, initVector))), | ||
89 | 'decrypted multiple blocks'); | ||
90 | |||
91 | }); | ||
92 | |||
93 | QUnit.module('Incremental Processing', { | ||
94 | beforeEach() { | ||
95 | this.clock = sinon.useFakeTimers(); | ||
96 | }, | ||
97 | afterEach() { | ||
98 | this.clock.restore(); | ||
99 | } | ||
100 | }); | ||
101 | |||
102 | QUnit.test('executes a callback after a timeout', function() { | ||
103 | let asyncStream = new AsyncStream(); | ||
104 | let calls = ''; | ||
105 | |||
106 | asyncStream.push(function() { | ||
107 | calls += 'a'; | ||
108 | }); | ||
109 | |||
110 | this.clock.tick(asyncStream.delay); | ||
111 | QUnit.equal(calls, 'a', 'invoked the callback once'); | ||
112 | this.clock.tick(asyncStream.delay); | ||
113 | QUnit.equal(calls, 'a', 'only invoked the callback once'); | ||
114 | }); | ||
115 | |||
116 | QUnit.test('executes callback in series', function() { | ||
117 | let asyncStream = new AsyncStream(); | ||
118 | let calls = ''; | ||
119 | |||
120 | asyncStream.push(function() { | ||
121 | calls += 'a'; | ||
122 | }); | ||
123 | asyncStream.push(function() { | ||
124 | calls += 'b'; | ||
125 | }); | ||
126 | |||
127 | this.clock.tick(asyncStream.delay); | ||
128 | QUnit.equal(calls, 'a', 'invoked the first callback'); | ||
129 | this.clock.tick(asyncStream.delay); | ||
130 | QUnit.equal(calls, 'ab', 'invoked the second'); | ||
131 | }); | ||
132 | |||
133 | QUnit.module('Incremental Decryption', { | ||
134 | beforeEach() { | ||
135 | this.clock = sinon.useFakeTimers(); | ||
136 | }, | ||
137 | afterEach() { | ||
138 | this.clock.restore(); | ||
139 | } | ||
140 | }); | ||
141 | |||
142 | QUnit.test('asynchronously decrypts a 4-word block', function() { | ||
143 | let key = new Uint32Array([0, 0, 0, 0]); | ||
144 | let initVector = key; | ||
145 | // the string "howdy folks" encrypted | ||
146 | let encrypted = new Uint8Array([0xce, 0x90, 0x97, 0xd0, | ||
147 | 0x08, 0x46, 0x4d, 0x18, | ||
148 | 0x4f, 0xae, 0x01, 0x1c, | ||
149 | 0x82, 0xa8, 0xf0, 0x67]); | ||
150 | let decrypted; | ||
151 | let decrypter = new Decrypter(encrypted, | ||
152 | key, | ||
153 | initVector, | ||
154 | function(error, result) { | ||
155 | if (error) { | ||
156 | throw new Error(error); | ||
157 | } | ||
158 | decrypted = result; | ||
159 | }); | ||
160 | |||
161 | QUnit.ok(!decrypted, 'asynchronously decrypts'); | ||
162 | this.clock.tick(decrypter.asyncStream_.delay * 2); | ||
163 | |||
164 | QUnit.ok(decrypted, 'completed decryption'); | ||
165 | QUnit.deepEqual('howdy folks', | ||
166 | stringFromBytes(decrypted), | ||
167 | 'decrypts and unpads the result'); | ||
168 | }); | ||
169 | |||
170 | QUnit.test('breaks up input greater than the step value', function() { | ||
171 | let encrypted = new Int32Array(Decrypter.STEP + 4); | ||
172 | let done = false; | ||
173 | let decrypter = new Decrypter(encrypted, | ||
174 | new Uint32Array(4), | ||
175 | new Uint32Array(4), | ||
176 | function() { | ||
177 | done = true; | ||
178 | }); | ||
179 | |||
180 | this.clock.tick(decrypter.asyncStream_.delay * 2); | ||
181 | QUnit.ok(!done, 'not finished after two ticks'); | ||
182 | |||
183 | this.clock.tick(decrypter.asyncStream_.delay); | ||
184 | QUnit.ok(done, 'finished after the last chunk is decrypted'); | ||
185 | }); |
test/decrypter_test.js
deleted
100644 → 0
1 | (function(window, videojs, unpad, undefined) { | ||
2 | 'use strict'; | ||
3 | /* | ||
4 | ======== A Handy Little QUnit Reference ======== | ||
5 | http://api.qunitjs.com/ | ||
6 | |||
7 | Test methods: | ||
8 | module(name, {[setup][ ,teardown]}) | ||
9 | test(name, callback) | ||
10 | expect(numberOfAssertions) | ||
11 | stop(increment) | ||
12 | start(decrement) | ||
13 | Test assertions: | ||
14 | ok(value, [message]) | ||
15 | equal(actual, expected, [message]) | ||
16 | notEqual(actual, expected, [message]) | ||
17 | deepEqual(actual, expected, [message]) | ||
18 | notDeepEqual(actual, expected, [message]) | ||
19 | strictEqual(actual, expected, [message]) | ||
20 | notStrictEqual(actual, expected, [message]) | ||
21 | throws(block, [expected], [message]) | ||
22 | */ | ||
23 | |||
24 | // see docs/hlse.md for instructions on how test data was generated | ||
25 | |||
26 | var stringFromBytes = function(bytes) { | ||
27 | var result = '', i; | ||
28 | |||
29 | for (i = 0; i < bytes.length; i++) { | ||
30 | result += String.fromCharCode(bytes[i]); | ||
31 | } | ||
32 | return result; | ||
33 | }; | ||
34 | |||
35 | module('Decryption'); | ||
36 | |||
37 | test('decrypts a single AES-128 with PKCS7 block', function() { | ||
38 | var | ||
39 | key = new Uint32Array([0, 0, 0, 0]), | ||
40 | initVector = key, | ||
41 | // the string "howdy folks" encrypted | ||
42 | encrypted = new Uint8Array([ | ||
43 | 0xce, 0x90, 0x97, 0xd0, | ||
44 | 0x08, 0x46, 0x4d, 0x18, | ||
45 | 0x4f, 0xae, 0x01, 0x1c, | ||
46 | 0x82, 0xa8, 0xf0, 0x67]); | ||
47 | |||
48 | deepEqual('howdy folks', | ||
49 | stringFromBytes(unpad(videojs.Hls.decrypt(encrypted, key, initVector))), | ||
50 | 'decrypted with a byte array key'); | ||
51 | }); | ||
52 | |||
53 | test('decrypts multiple AES-128 blocks with CBC', function() { | ||
54 | var | ||
55 | key = new Uint32Array([0, 0, 0, 0]), | ||
56 | initVector = key, | ||
57 | // the string "0123456789abcdef01234" encrypted | ||
58 | encrypted = new Uint8Array([ | ||
59 | 0x14, 0xf5, 0xfe, 0x74, | ||
60 | 0x69, 0x66, 0xf2, 0x92, | ||
61 | 0x65, 0x1c, 0x22, 0x88, | ||
62 | 0xbb, 0xff, 0x46, 0x09, | ||
63 | |||
64 | 0x0b, 0xde, 0x5e, 0x71, | ||
65 | 0x77, 0x87, 0xeb, 0x84, | ||
66 | 0xa9, 0x54, 0xc2, 0x45, | ||
67 | 0xe9, 0x4e, 0x29, 0xb3 | ||
68 | ]); | ||
69 | |||
70 | deepEqual('0123456789abcdef01234', | ||
71 | stringFromBytes(unpad(videojs.Hls.decrypt(encrypted, key, initVector))), | ||
72 | 'decrypted multiple blocks'); | ||
73 | }); | ||
74 | |||
75 | var clock; | ||
76 | |||
77 | module('Incremental Processing', { | ||
78 | setup: function() { | ||
79 | clock = sinon.useFakeTimers(); | ||
80 | }, | ||
81 | teardown: function() { | ||
82 | clock.restore(); | ||
83 | } | ||
84 | }); | ||
85 | |||
86 | test('executes a callback after a timeout', function() { | ||
87 | var asyncStream = new videojs.Hls.AsyncStream(), | ||
88 | calls = ''; | ||
89 | asyncStream.push(function() { | ||
90 | calls += 'a'; | ||
91 | }); | ||
92 | |||
93 | clock.tick(asyncStream.delay); | ||
94 | equal(calls, 'a', 'invoked the callback once'); | ||
95 | clock.tick(asyncStream.delay); | ||
96 | equal(calls, 'a', 'only invoked the callback once'); | ||
97 | }); | ||
98 | |||
99 | test('executes callback in series', function() { | ||
100 | var asyncStream = new videojs.Hls.AsyncStream(), | ||
101 | calls = ''; | ||
102 | asyncStream.push(function() { | ||
103 | calls += 'a'; | ||
104 | }); | ||
105 | asyncStream.push(function() { | ||
106 | calls += 'b'; | ||
107 | }); | ||
108 | |||
109 | clock.tick(asyncStream.delay); | ||
110 | equal(calls, 'a', 'invoked the first callback'); | ||
111 | clock.tick(asyncStream.delay); | ||
112 | equal(calls, 'ab', 'invoked the second'); | ||
113 | }); | ||
114 | |||
115 | var decrypter; | ||
116 | |||
117 | module('Incremental Decryption', { | ||
118 | setup: function() { | ||
119 | clock = sinon.useFakeTimers(); | ||
120 | }, | ||
121 | teardown: function() { | ||
122 | clock.restore(); | ||
123 | } | ||
124 | }); | ||
125 | |||
126 | test('asynchronously decrypts a 4-word block', function() { | ||
127 | var | ||
128 | key = new Uint32Array([0, 0, 0, 0]), | ||
129 | initVector = key, | ||
130 | // the string "howdy folks" encrypted | ||
131 | encrypted = new Uint8Array([ | ||
132 | 0xce, 0x90, 0x97, 0xd0, | ||
133 | 0x08, 0x46, 0x4d, 0x18, | ||
134 | 0x4f, 0xae, 0x01, 0x1c, | ||
135 | 0x82, 0xa8, 0xf0, 0x67]), | ||
136 | decrypted; | ||
137 | |||
138 | decrypter = new videojs.Hls.Decrypter(encrypted, key, initVector, function(error, result) { | ||
139 | decrypted = result; | ||
140 | }); | ||
141 | ok(!decrypted, 'asynchronously decrypts'); | ||
142 | |||
143 | clock.tick(decrypter.asyncStream_.delay * 2); | ||
144 | |||
145 | ok(decrypted, 'completed decryption'); | ||
146 | deepEqual('howdy folks', | ||
147 | stringFromBytes(decrypted), | ||
148 | 'decrypts and unpads the result'); | ||
149 | }); | ||
150 | |||
151 | test('breaks up input greater than the step value', function() { | ||
152 | var encrypted = new Int32Array(videojs.Hls.Decrypter.STEP + 4), | ||
153 | done = false, | ||
154 | decrypter = new videojs.Hls.Decrypter(encrypted, | ||
155 | new Uint32Array(4), | ||
156 | new Uint32Array(4), | ||
157 | function() { | ||
158 | done = true; | ||
159 | }); | ||
160 | clock.tick(decrypter.asyncStream_.delay * 2); | ||
161 | ok(!done, 'not finished after two ticks'); | ||
162 | |||
163 | clock.tick(decrypter.asyncStream_.delay); | ||
164 | ok(done, 'finished after the last chunk is decrypted'); | ||
165 | }); | ||
166 | |||
167 | })(window, window.videojs, window.pkcs7.unpad); |
test/index.html
0 → 100644
1 | <!DOCTYPE html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <meta charset="utf-8"> | ||
5 | <title>video.js HLS Plugin Test Suite</title> | ||
6 | <link rel="stylesheet" href="/node_modules/qunitjs/qunit/qunit.css" media="screen"> | ||
7 | <link rel="stylesheet" href="/node_modules/video.js/dist/video-js.css" media="screen"> | ||
8 | </head> | ||
9 | <body> | ||
10 | <div id="qunit"></div> | ||
11 | <div id="qunit-fixture"></div> | ||
12 | <!-- NOTE in order for test to pass we require sinon 1.10.2 exactly --> | ||
13 | <script src="/node_modules/sinon/pkg/sinon.js"></script> | ||
14 | <script src="/node_modules/qunitjs/qunit/qunit.js"></script> | ||
15 | <script src="/node_modules/video.js/dist/video.js"></script> | ||
16 | <script src="/dist-test/videojs-contrib-hls.js"></script> | ||
17 | |||
18 | </body> | ||
19 | </html> |
test/karma-qunit-shim.js
deleted
100644 → 0
test/karma.conf.js
deleted
100644 → 0
1 | // Karma example configuration file | ||
2 | // NOTE: To configure Karma tests, do the following: | ||
3 | // 1. Copy this file and rename the copy with a .conf.js extension, for example: karma.conf.js | ||
4 | // 2. Configure the properties below in your conf.js copy | ||
5 | // 3. Run your tests | ||
6 | |||
7 | module.exports = function(config) { | ||
8 | var customLaunchers = { | ||
9 | chrome_sl: { | ||
10 | singleRun: true, | ||
11 | base: 'SauceLabs', | ||
12 | browserName: 'chrome', | ||
13 | platform: 'Windows 7' | ||
14 | }, | ||
15 | |||
16 | firefox_sl: { | ||
17 | singleRun: true, | ||
18 | base: 'SauceLabs', | ||
19 | browserName: 'firefox', | ||
20 | platform: 'Windows 8' | ||
21 | }, | ||
22 | |||
23 | safari_sl: { | ||
24 | singleRun: true, | ||
25 | base: 'SauceLabs', | ||
26 | browserName: 'safari', | ||
27 | platform: 'OS X 10.8' | ||
28 | }, | ||
29 | |||
30 | ipad_sl: { | ||
31 | singleRun: true, | ||
32 | base: 'SauceLabs', | ||
33 | browserName: 'ipad', | ||
34 | platform:'OS X 10.9', | ||
35 | version: '7.1' | ||
36 | }, | ||
37 | |||
38 | android_sl: { | ||
39 | singleRun: true, | ||
40 | base: 'SauceLabs', | ||
41 | browserName: 'android', | ||
42 | platform:'Linux' | ||
43 | } | ||
44 | }; | ||
45 | |||
46 | config.set({ | ||
47 | // base path, that will be used to resolve files and exclude | ||
48 | basePath: '', | ||
49 | |||
50 | frameworks: ['qunit'], | ||
51 | |||
52 | // Set autoWatch to true if you plan to run `grunt karma` continuously, to automatically test changes as you make them. | ||
53 | autoWatch: false, | ||
54 | |||
55 | // Setting singleRun to true here will start up your specified browsers, run tests, and then shut down the browsers. Helpful to have in a CI environment, where you don't want to leave browsers running continuously. | ||
56 | singleRun: true, | ||
57 | |||
58 | // custom launchers for sauce labs | ||
59 | //define SL browsers | ||
60 | customLaunchers: customLaunchers, | ||
61 | |||
62 | // Start these browsers | ||
63 | browsers: ['chrome_sl'], //Object.keys(customLaunchers), | ||
64 | |||
65 | // List of files / patterns to load in the browser | ||
66 | // Add any new src files to this list. | ||
67 | // If you add new unit tests, they will be picked up automatically by Karma, | ||
68 | // unless you've added them to a nested directory, in which case you should | ||
69 | // add their paths to this list. | ||
70 | |||
71 | files: [ | ||
72 | '../node_modules/sinon/pkg/sinon.js', | ||
73 | '../node_modules/video.js/dist/video-js.css', | ||
74 | '../node_modules/video.js/dist/video.js', | ||
75 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | ||
76 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', | ||
77 | '../test/karma-qunit-shim.js', | ||
78 | '../src/videojs-hls.js', | ||
79 | '../src/stream.js', | ||
80 | '../src/m3u8/m3u8-parser.js', | ||
81 | '../src/xhr.js', | ||
82 | '../src/playlist.js', | ||
83 | '../src/playlist-loader.js', | ||
84 | '../src/decrypter.js', | ||
85 | '../tmp/manifests.js', | ||
86 | '../tmp/expected.js', | ||
87 | 'tsSegment-bc.js', | ||
88 | '../src/bin-utils.js', | ||
89 | '../test/*.js', | ||
90 | ], | ||
91 | |||
92 | plugins: [ | ||
93 | 'karma-qunit', | ||
94 | 'karma-chrome-launcher', | ||
95 | 'karma-firefox-launcher', | ||
96 | 'karma-ie-launcher', | ||
97 | 'karma-opera-launcher', | ||
98 | 'karma-phantomjs-launcher', | ||
99 | 'karma-safari-launcher', | ||
100 | 'karma-sauce-launcher' | ||
101 | ], | ||
102 | |||
103 | // test results reporter to use | ||
104 | // possible values: 'dots', 'progress', 'junit' | ||
105 | reporters: ['dots', 'progress'], | ||
106 | |||
107 | // web server port | ||
108 | port: 9876, | ||
109 | |||
110 | // cli runner port | ||
111 | runnerPort: 9100, | ||
112 | |||
113 | // enable / disable colors in the output (reporters and logs) | ||
114 | colors: true, | ||
115 | |||
116 | // level of logging | ||
117 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG | ||
118 | //logLevel: config.LOG_INFO, | ||
119 | |||
120 | // If browser does not capture in given timeout [ms], kill it | ||
121 | captureTimeout: 60000, | ||
122 | |||
123 | // global config for SauceLabs | ||
124 | sauceLabs: { | ||
125 | startConnect: false, | ||
126 | tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, | ||
127 | build: process.env.TRAVIS_BUILD_NUMBER, | ||
128 | testName: process.env.TRAVIS_BUILD_NUMBER + process.env.TRAVIS_BRANCH, | ||
129 | recordScreenshots: false | ||
130 | } | ||
131 | }); | ||
132 | }; |
test/karma/chrome.js
0 → 100644
test/karma/common.js
0 → 100644
1 | var merge = require('lodash-compat/object/merge'); | ||
2 | |||
3 | var DEFAULTS = { | ||
4 | basePath: '../..', | ||
5 | frameworks: ['browserify', 'qunit'], | ||
6 | |||
7 | |||
8 | files: [ | ||
9 | 'node_modules/sinon/pkg/sinon.js', | ||
10 | 'node_modules/sinon/pkg/sinon-ie.js', | ||
11 | 'node_modules/video.js/dist/video.js', | ||
12 | 'node_modules/video.js/dist/video-js.css', | ||
13 | |||
14 | 'test/**/*.test.js' | ||
15 | ], | ||
16 | |||
17 | exclude: [ | ||
18 | 'test/data/**' | ||
19 | ], | ||
20 | |||
21 | plugins: [ | ||
22 | 'karma-browserify', | ||
23 | 'karma-qunit' | ||
24 | ], | ||
25 | |||
26 | preprocessors: { | ||
27 | 'test/**/*.test.js': ['browserify'] | ||
28 | }, | ||
29 | |||
30 | reporters: ['dots'], | ||
31 | port: 9876, | ||
32 | colors: true, | ||
33 | autoWatch: false, | ||
34 | singleRun: true, | ||
35 | concurrency: Infinity, | ||
36 | |||
37 | browserify: { | ||
38 | debug: true, | ||
39 | transform: [ | ||
40 | 'babelify', | ||
41 | 'browserify-shim' | ||
42 | ], | ||
43 | noParse: [ | ||
44 | 'test/data/**', | ||
45 | ] | ||
46 | } | ||
47 | }; | ||
48 | |||
49 | /** | ||
50 | * Customizes target/source merging with lodash merge. | ||
51 | * | ||
52 | * @param {Mixed} target | ||
53 | * @param {Mixed} source | ||
54 | * @return {Mixed} | ||
55 | */ | ||
56 | var customizer = function(target, source) { | ||
57 | if (Array.isArray(target)) { | ||
58 | return target.concat(source); | ||
59 | } | ||
60 | }; | ||
61 | |||
62 | /** | ||
63 | * Generates a new Karma config with a common set of base configuration. | ||
64 | * | ||
65 | * @param {Object} custom | ||
66 | * Configuration that will be deep-merged. Arrays will be | ||
67 | * concatenated. | ||
68 | * @return {Object} | ||
69 | */ | ||
70 | module.exports = function(custom) { | ||
71 | return merge({}, custom, DEFAULTS, customizer); | ||
72 | }; |
test/karma/detected.js
0 → 100644
1 | var common = require('./common'); | ||
2 | |||
3 | // Runs default testing configuration in multiple environments. | ||
4 | |||
5 | module.exports = function(config) { | ||
6 | |||
7 | // Travis CI should run in its available Firefox headless browser. | ||
8 | if (process.env.TRAVIS) { | ||
9 | |||
10 | config.set(common({ | ||
11 | browsers: ['Firefox'], | ||
12 | plugins: ['karma-firefox-launcher'] | ||
13 | })) | ||
14 | } else { | ||
15 | config.set(common({ | ||
16 | |||
17 | frameworks: ['detectBrowsers'], | ||
18 | |||
19 | plugins: [ | ||
20 | 'karma-chrome-launcher', | ||
21 | 'karma-detect-browsers', | ||
22 | 'karma-firefox-launcher', | ||
23 | 'karma-ie-launcher', | ||
24 | 'karma-safari-launcher' | ||
25 | ], | ||
26 | |||
27 | detectBrowsers: { | ||
28 | // disable safari as it was not previously supported and causes test failures | ||
29 | postDetection: function(availableBrowsers) { | ||
30 | var safariIndex = availableBrowsers.indexOf('Safari'); | ||
31 | if(safariIndex !== -1) { | ||
32 | console.log("Not running safari it is/was broken"); | ||
33 | availableBrowsers.splice(safariIndex, 1); | ||
34 | } | ||
35 | return availableBrowsers; | ||
36 | }, | ||
37 | usePhantomJS: false | ||
38 | } | ||
39 | })); | ||
40 | } | ||
41 | }; |
test/karma/firefox.js
0 → 100644
test/karma/ie.js
0 → 100644
test/karma/safari.js
0 → 100644
test/localkarma.conf.js
deleted
100644 → 0
1 | // Karma example configuration file | ||
2 | // NOTE: To configure Karma tests, do the following: | ||
3 | // 1. Copy this file and rename the copy with a .conf.js extension, for example: karma.conf.js | ||
4 | // 2. Configure the properties below in your conf.js copy | ||
5 | // 3. Run your tests | ||
6 | |||
7 | module.exports = function(config) { | ||
8 | config.set({ | ||
9 | // base path, that will be used to resolve files and exclude | ||
10 | basePath: '', | ||
11 | |||
12 | frameworks: ['qunit'], | ||
13 | |||
14 | // Set autoWatch to true if you plan to run `grunt karma` continuously, to automatically test changes as you make them. | ||
15 | autoWatch: false, | ||
16 | |||
17 | // Setting singleRun to true here will start up your specified browsers, run tests, and then shut down the browsers. Helpful to have in a CI environment, where you don't want to leave browsers running continuously. | ||
18 | singleRun: true, | ||
19 | |||
20 | // Start these browsers, currently available: | ||
21 | // - Chrome | ||
22 | // - ChromeCanary | ||
23 | // - Firefox | ||
24 | // - Opera | ||
25 | // - Safari (only Mac) | ||
26 | // - PhantomJS | ||
27 | // - IE (only Windows) | ||
28 | // Example usage: | ||
29 | // browsers: [], | ||
30 | // List of files / patterns to load in the browser | ||
31 | // Add any new src files to this list. | ||
32 | // If you add new unit tests, they will be picked up automatically by Karma, | ||
33 | // unless you've added them to a nested directory, in which case you should | ||
34 | // add their paths to this list. | ||
35 | |||
36 | files: [ | ||
37 | '../node_modules/sinon/pkg/sinon.js', | ||
38 | '../node_modules/video.js/dist/video-js.css', | ||
39 | '../node_modules/video.js/dist/video.js', | ||
40 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | ||
41 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', | ||
42 | '../test/karma-qunit-shim.js', | ||
43 | '../src/videojs-hls.js', | ||
44 | '../src/stream.js', | ||
45 | '../src/m3u8/m3u8-parser.js', | ||
46 | '../src/xhr.js', | ||
47 | '../src/playlist.js', | ||
48 | '../src/playlist-loader.js', | ||
49 | '../src/decrypter.js', | ||
50 | '../tmp/manifests.js', | ||
51 | '../tmp/expected.js', | ||
52 | 'tsSegment-bc.js', | ||
53 | '../src/bin-utils.js', | ||
54 | '../test/*.js', | ||
55 | ], | ||
56 | |||
57 | plugins: [ | ||
58 | 'karma-qunit', | ||
59 | 'karma-chrome-launcher', | ||
60 | 'karma-firefox-launcher', | ||
61 | 'karma-ie-launcher', | ||
62 | 'karma-opera-launcher', | ||
63 | 'karma-phantomjs-launcher', | ||
64 | 'karma-safari-launcher' | ||
65 | ], | ||
66 | |||
67 | // list of files to exclude | ||
68 | exclude: [ | ||
69 | |||
70 | ], | ||
71 | |||
72 | |||
73 | // test results reporter to use | ||
74 | // possible values: 'dots', 'progress', 'junit' | ||
75 | reporters: ['progress'], | ||
76 | |||
77 | |||
78 | // web server port | ||
79 | port: 9876, | ||
80 | |||
81 | |||
82 | // cli runner port | ||
83 | runnerPort: 9100, | ||
84 | |||
85 | |||
86 | // enable / disable colors in the output (reporters and logs) | ||
87 | colors: true, | ||
88 | |||
89 | |||
90 | // level of logging | ||
91 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG | ||
92 | logLevel: config.LOG_DISABLE, | ||
93 | |||
94 | // If browser does not capture in given timeout [ms], kill it | ||
95 | captureTimeout: 60000 | ||
96 | }); | ||
97 | }; |
test/m3u8.test.js
0 → 100644
1 | import {ParseStream, LineStream, Parser} from '../src/m3u8'; | ||
2 | import QUnit from 'qunit'; | ||
3 | import testDataExpected from './test-expected.js'; | ||
4 | import testDataManifests from './test-manifests.js'; | ||
5 | |||
6 | QUnit.module('LineStream', { | ||
7 | beforeEach() { | ||
8 | this.lineStream = new LineStream(); | ||
9 | } | ||
10 | }); | ||
11 | QUnit.test('empty inputs produce no tokens', function() { | ||
12 | let data = false; | ||
13 | |||
14 | this.lineStream.on('data', function() { | ||
15 | data = true; | ||
16 | }); | ||
17 | this.lineStream.push(''); | ||
18 | QUnit.ok(!data, 'no tokens were produced'); | ||
19 | }); | ||
20 | QUnit.test('splits on newlines', function() { | ||
21 | let lines = []; | ||
22 | |||
23 | this.lineStream.on('data', function(line) { | ||
24 | lines.push(line); | ||
25 | }); | ||
26 | this.lineStream.push('#EXTM3U\nmovie.ts\n'); | ||
27 | |||
28 | QUnit.strictEqual(2, lines.length, 'two lines are ready'); | ||
29 | QUnit.strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token'); | ||
30 | QUnit.strictEqual('movie.ts', lines.shift(), 'the second line is the second token'); | ||
31 | }); | ||
32 | QUnit.test('empty lines become empty strings', function() { | ||
33 | let lines = []; | ||
34 | |||
35 | this.lineStream.on('data', function(line) { | ||
36 | lines.push(line); | ||
37 | }); | ||
38 | this.lineStream.push('\n\n'); | ||
39 | |||
40 | QUnit.strictEqual(2, lines.length, 'two lines are ready'); | ||
41 | QUnit.strictEqual('', lines.shift(), 'the first line is empty'); | ||
42 | QUnit.strictEqual('', lines.shift(), 'the second line is empty'); | ||
43 | }); | ||
44 | QUnit.test('handles lines broken across appends', function() { | ||
45 | let lines = []; | ||
46 | |||
47 | this.lineStream.on('data', function(line) { | ||
48 | lines.push(line); | ||
49 | }); | ||
50 | this.lineStream.push('#EXTM'); | ||
51 | QUnit.strictEqual(0, lines.length, 'no lines are ready'); | ||
52 | |||
53 | this.lineStream.push('3U\nmovie.ts\n'); | ||
54 | QUnit.strictEqual(2, lines.length, 'two lines are ready'); | ||
55 | QUnit.strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token'); | ||
56 | QUnit.strictEqual('movie.ts', lines.shift(), 'the second line is the second token'); | ||
57 | }); | ||
58 | QUnit.test('stops sending events after deregistering', function() { | ||
59 | let temporaryLines = []; | ||
60 | let temporary = function(line) { | ||
61 | temporaryLines.push(line); | ||
62 | }; | ||
63 | let permanentLines = []; | ||
64 | let permanent = function(line) { | ||
65 | permanentLines.push(line); | ||
66 | }; | ||
67 | |||
68 | this.lineStream.on('data', temporary); | ||
69 | this.lineStream.on('data', permanent); | ||
70 | this.lineStream.push('line one\n'); | ||
71 | QUnit.strictEqual(temporaryLines.length, | ||
72 | permanentLines.length, | ||
73 | 'both callbacks receive the event'); | ||
74 | |||
75 | QUnit.ok(this.lineStream.off('data', temporary), 'a listener was removed'); | ||
76 | this.lineStream.push('line two\n'); | ||
77 | QUnit.strictEqual(1, temporaryLines.length, 'no new events are received'); | ||
78 | QUnit.strictEqual(2, permanentLines.length, 'new events are still received'); | ||
79 | }); | ||
80 | |||
81 | QUnit.module('ParseStream', { | ||
82 | beforeEach() { | ||
83 | this.lineStream = new LineStream(); | ||
84 | this.parseStream = new ParseStream(); | ||
85 | this.lineStream.pipe(this.parseStream); | ||
86 | } | ||
87 | }); | ||
88 | QUnit.test('parses comment lines', function() { | ||
89 | let manifest = '# a line that starts with a hash mark without "EXT" is a comment\n'; | ||
90 | let element; | ||
91 | |||
92 | this.parseStream.on('data', function(elem) { | ||
93 | element = elem; | ||
94 | }); | ||
95 | this.lineStream.push(manifest); | ||
96 | |||
97 | QUnit.ok(element, 'an event was triggered'); | ||
98 | QUnit.strictEqual(element.type, 'comment', 'the type is comment'); | ||
99 | QUnit.strictEqual(element.text, | ||
100 | manifest.slice(1, manifest.length - 1), | ||
101 | 'the comment text is parsed'); | ||
102 | }); | ||
103 | QUnit.test('parses uri lines', function() { | ||
104 | let manifest = 'any non-blank line that does not start with a hash-mark is a URI\n'; | ||
105 | let element; | ||
106 | |||
107 | this.parseStream.on('data', function(elem) { | ||
108 | element = elem; | ||
109 | }); | ||
110 | this.lineStream.push(manifest); | ||
111 | |||
112 | QUnit.ok(element, 'an event was triggered'); | ||
113 | QUnit.strictEqual(element.type, 'uri', 'the type is uri'); | ||
114 | QUnit.strictEqual(element.uri, | ||
115 | manifest.substring(0, manifest.length - 1), | ||
116 | 'the uri text is parsed'); | ||
117 | }); | ||
118 | QUnit.test('parses unknown tag types', function() { | ||
119 | let manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n'; | ||
120 | let element; | ||
121 | |||
122 | this.parseStream.on('data', function(elem) { | ||
123 | element = elem; | ||
124 | }); | ||
125 | this.lineStream.push(manifest); | ||
126 | |||
127 | QUnit.ok(element, 'an event was triggered'); | ||
128 | QUnit.strictEqual(element.type, 'tag', 'the type is tag'); | ||
129 | QUnit.strictEqual(element.data, | ||
130 | manifest.slice(4, manifest.length - 1), | ||
131 | 'unknown tag data is preserved'); | ||
132 | }); | ||
133 | |||
134 | // #EXTM3U | ||
135 | QUnit.test('parses #EXTM3U tags', function() { | ||
136 | let manifest = '#EXTM3U\n'; | ||
137 | let element; | ||
138 | |||
139 | this.parseStream.on('data', function(elem) { | ||
140 | element = elem; | ||
141 | }); | ||
142 | this.lineStream.push(manifest); | ||
143 | |||
144 | QUnit.ok(element, 'an event was triggered'); | ||
145 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
146 | QUnit.strictEqual(element.tagType, 'm3u', 'the tag type is m3u'); | ||
147 | }); | ||
148 | |||
149 | // #EXTINF | ||
150 | QUnit.test('parses minimal #EXTINF tags', function() { | ||
151 | let manifest = '#EXTINF\n'; | ||
152 | let element; | ||
153 | |||
154 | this.parseStream.on('data', function(elem) { | ||
155 | element = elem; | ||
156 | }); | ||
157 | this.lineStream.push(manifest); | ||
158 | |||
159 | QUnit.ok(element, 'an event was triggered'); | ||
160 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
161 | QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
162 | }); | ||
163 | QUnit.test('parses #EXTINF tags with durations', function() { | ||
164 | let manifest = '#EXTINF:15\n'; | ||
165 | let element; | ||
166 | |||
167 | this.parseStream.on('data', function(elem) { | ||
168 | element = elem; | ||
169 | }); | ||
170 | this.lineStream.push(manifest); | ||
171 | |||
172 | QUnit.ok(element, 'an event was triggered'); | ||
173 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
174 | QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
175 | QUnit.strictEqual(element.duration, 15, 'the duration is parsed'); | ||
176 | QUnit.ok(!('title' in element), 'no title is parsed'); | ||
177 | |||
178 | manifest = '#EXTINF:21,\n'; | ||
179 | this.lineStream.push(manifest); | ||
180 | |||
181 | QUnit.ok(element, 'an event was triggered'); | ||
182 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
183 | QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
184 | QUnit.strictEqual(element.duration, 21, 'the duration is parsed'); | ||
185 | QUnit.ok(!('title' in element), 'no title is parsed'); | ||
186 | }); | ||
187 | QUnit.test('parses #EXTINF tags with a duration and title', function() { | ||
188 | let manifest = '#EXTINF:13,Does anyone really use the title attribute?\n'; | ||
189 | let element; | ||
190 | |||
191 | this.parseStream.on('data', function(elem) { | ||
192 | element = elem; | ||
193 | }); | ||
194 | this.lineStream.push(manifest); | ||
195 | |||
196 | QUnit.ok(element, 'an event was triggered'); | ||
197 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
198 | QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
199 | QUnit.strictEqual(element.duration, 13, 'the duration is parsed'); | ||
200 | QUnit.strictEqual(element.title, | ||
201 | manifest.substring(manifest.indexOf(',') + 1, manifest.length - 1), | ||
202 | 'the title is parsed'); | ||
203 | }); | ||
204 | QUnit.test('parses #EXTINF tags with carriage returns', function() { | ||
205 | let manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n'; | ||
206 | let element; | ||
207 | |||
208 | this.parseStream.on('data', function(elem) { | ||
209 | element = elem; | ||
210 | }); | ||
211 | this.lineStream.push(manifest); | ||
212 | |||
213 | QUnit.ok(element, 'an event was triggered'); | ||
214 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
215 | QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
216 | QUnit.strictEqual(element.duration, 13, 'the duration is parsed'); | ||
217 | QUnit.strictEqual(element.title, | ||
218 | manifest.substring(manifest.indexOf(',') + 1, manifest.length - 2), | ||
219 | 'the title is parsed'); | ||
220 | }); | ||
221 | |||
222 | // #EXT-X-TARGETDURATION | ||
223 | QUnit.test('parses minimal #EXT-X-TARGETDURATION tags', function() { | ||
224 | let manifest = '#EXT-X-TARGETDURATION\n'; | ||
225 | let element; | ||
226 | |||
227 | this.parseStream.on('data', function(elem) { | ||
228 | element = elem; | ||
229 | }); | ||
230 | this.lineStream.push(manifest); | ||
231 | |||
232 | QUnit.ok(element, 'an event was triggered'); | ||
233 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
234 | QUnit.strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration'); | ||
235 | QUnit.ok(!('duration' in element), 'no duration is parsed'); | ||
236 | }); | ||
237 | QUnit.test('parses #EXT-X-TARGETDURATION with duration', function() { | ||
238 | let manifest = '#EXT-X-TARGETDURATION:47\n'; | ||
239 | let element; | ||
240 | |||
241 | this.parseStream.on('data', function(elem) { | ||
242 | element = elem; | ||
243 | }); | ||
244 | this.lineStream.push(manifest); | ||
245 | |||
246 | QUnit.ok(element, 'an event was triggered'); | ||
247 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
248 | QUnit.strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration'); | ||
249 | QUnit.strictEqual(element.duration, 47, 'the duration is parsed'); | ||
250 | }); | ||
251 | |||
252 | // #EXT-X-VERSION | ||
253 | QUnit.test('parses minimal #EXT-X-VERSION tags', function() { | ||
254 | let manifest = '#EXT-X-VERSION:\n'; | ||
255 | let element; | ||
256 | |||
257 | this.parseStream.on('data', function(elem) { | ||
258 | element = elem; | ||
259 | }); | ||
260 | this.lineStream.push(manifest); | ||
261 | |||
262 | QUnit.ok(element, 'an event was triggered'); | ||
263 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
264 | QUnit.strictEqual(element.tagType, 'version', 'the tag type is version'); | ||
265 | QUnit.ok(!('version' in element), 'no version is present'); | ||
266 | }); | ||
267 | QUnit.test('parses #EXT-X-VERSION with a version', function() { | ||
268 | let manifest = '#EXT-X-VERSION:99\n'; | ||
269 | let element; | ||
270 | |||
271 | this.parseStream.on('data', function(elem) { | ||
272 | element = elem; | ||
273 | }); | ||
274 | this.lineStream.push(manifest); | ||
275 | |||
276 | QUnit.ok(element, 'an event was triggered'); | ||
277 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
278 | QUnit.strictEqual(element.tagType, 'version', 'the tag type is version'); | ||
279 | QUnit.strictEqual(element.version, 99, 'the version is parsed'); | ||
280 | }); | ||
281 | |||
282 | // #EXT-X-MEDIA-SEQUENCE | ||
283 | QUnit.test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() { | ||
284 | let manifest = '#EXT-X-MEDIA-SEQUENCE\n'; | ||
285 | let element; | ||
286 | |||
287 | this.parseStream.on('data', function(elem) { | ||
288 | element = elem; | ||
289 | }); | ||
290 | this.lineStream.push(manifest); | ||
291 | |||
292 | QUnit.ok(element, 'an event was triggered'); | ||
293 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
294 | QUnit.strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence'); | ||
295 | QUnit.ok(!('number' in element), 'no number is present'); | ||
296 | }); | ||
297 | QUnit.test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() { | ||
298 | let manifest = '#EXT-X-MEDIA-SEQUENCE:109\n'; | ||
299 | let element; | ||
300 | |||
301 | this.parseStream.on('data', function(elem) { | ||
302 | element = elem; | ||
303 | }); | ||
304 | this.lineStream.push(manifest); | ||
305 | |||
306 | QUnit.ok(element, 'an event was triggered'); | ||
307 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
308 | QUnit.strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence'); | ||
309 | QUnit.ok(element.number, 109, 'the number is parsed'); | ||
310 | }); | ||
311 | |||
312 | // #EXT-X-PLAYLIST-TYPE | ||
313 | QUnit.test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() { | ||
314 | let manifest = '#EXT-X-PLAYLIST-TYPE:\n'; | ||
315 | let element; | ||
316 | |||
317 | this.parseStream.on('data', function(elem) { | ||
318 | element = elem; | ||
319 | }); | ||
320 | this.lineStream.push(manifest); | ||
321 | |||
322 | QUnit.ok(element, 'an event was triggered'); | ||
323 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
324 | QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type'); | ||
325 | QUnit.ok(!('playlistType' in element), 'no playlist type is present'); | ||
326 | }); | ||
327 | QUnit.test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() { | ||
328 | let manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n'; | ||
329 | let element; | ||
330 | |||
331 | this.parseStream.on('data', function(elem) { | ||
332 | element = elem; | ||
333 | }); | ||
334 | this.lineStream.push(manifest); | ||
335 | |||
336 | QUnit.ok(element, 'an event was triggered'); | ||
337 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
338 | QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type'); | ||
339 | QUnit.strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT'); | ||
340 | |||
341 | manifest = '#EXT-X-PLAYLIST-TYPE:VOD\n'; | ||
342 | this.lineStream.push(manifest); | ||
343 | QUnit.ok(element, 'an event was triggered'); | ||
344 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
345 | QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type'); | ||
346 | QUnit.strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD'); | ||
347 | |||
348 | manifest = '#EXT-X-PLAYLIST-TYPE:nonsense\n'; | ||
349 | this.lineStream.push(manifest); | ||
350 | QUnit.ok(element, 'an event was triggered'); | ||
351 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
352 | QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type'); | ||
353 | QUnit.strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed'); | ||
354 | }); | ||
355 | |||
356 | // #EXT-X-BYTERANGE | ||
357 | QUnit.test('parses minimal #EXT-X-BYTERANGE tags', function() { | ||
358 | let manifest = '#EXT-X-BYTERANGE\n'; | ||
359 | let element; | ||
360 | |||
361 | this.parseStream.on('data', function(elem) { | ||
362 | element = elem; | ||
363 | }); | ||
364 | this.lineStream.push(manifest); | ||
365 | |||
366 | QUnit.ok(element, 'an event was triggered'); | ||
367 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
368 | QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange'); | ||
369 | QUnit.ok(!('length' in element), 'no length is present'); | ||
370 | QUnit.ok(!('offset' in element), 'no offset is present'); | ||
371 | }); | ||
372 | QUnit.test('parses #EXT-X-BYTERANGE with length and offset', function() { | ||
373 | let manifest = '#EXT-X-BYTERANGE:45\n'; | ||
374 | let element; | ||
375 | |||
376 | this.parseStream.on('data', function(elem) { | ||
377 | element = elem; | ||
378 | }); | ||
379 | this.lineStream.push(manifest); | ||
380 | |||
381 | QUnit.ok(element, 'an event was triggered'); | ||
382 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
383 | QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange'); | ||
384 | QUnit.strictEqual(element.length, 45, 'length is parsed'); | ||
385 | QUnit.ok(!('offset' in element), 'no offset is present'); | ||
386 | |||
387 | manifest = '#EXT-X-BYTERANGE:108@16\n'; | ||
388 | this.lineStream.push(manifest); | ||
389 | QUnit.ok(element, 'an event was triggered'); | ||
390 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
391 | QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange'); | ||
392 | QUnit.strictEqual(element.length, 108, 'length is parsed'); | ||
393 | QUnit.strictEqual(element.offset, 16, 'offset is parsed'); | ||
394 | }); | ||
395 | |||
396 | // #EXT-X-ALLOW-CACHE | ||
397 | QUnit.test('parses minimal #EXT-X-ALLOW-CACHE tags', function() { | ||
398 | let manifest = '#EXT-X-ALLOW-CACHE:\n'; | ||
399 | let element; | ||
400 | |||
401 | this.parseStream.on('data', function(elem) { | ||
402 | element = elem; | ||
403 | }); | ||
404 | this.lineStream.push(manifest); | ||
405 | |||
406 | QUnit.ok(element, 'an event was triggered'); | ||
407 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
408 | QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache'); | ||
409 | QUnit.ok(!('allowed' in element), 'no allowed is present'); | ||
410 | }); | ||
411 | QUnit.test('parses valid #EXT-X-ALLOW-CACHE tags', function() { | ||
412 | let manifest = '#EXT-X-ALLOW-CACHE:YES\n'; | ||
413 | let element; | ||
414 | |||
415 | this.parseStream.on('data', function(elem) { | ||
416 | element = elem; | ||
417 | }); | ||
418 | this.lineStream.push(manifest); | ||
419 | |||
420 | QUnit.ok(element, 'an event was triggered'); | ||
421 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
422 | QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache'); | ||
423 | QUnit.ok(element.allowed, 'allowed is parsed'); | ||
424 | |||
425 | manifest = '#EXT-X-ALLOW-CACHE:NO\n'; | ||
426 | this.lineStream.push(manifest); | ||
427 | |||
428 | QUnit.ok(element, 'an event was triggered'); | ||
429 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
430 | QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache'); | ||
431 | QUnit.ok(!element.allowed, 'allowed is parsed'); | ||
432 | }); | ||
433 | // #EXT-X-STREAM-INF | ||
434 | QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function() { | ||
435 | let manifest = '#EXT-X-STREAM-INF\n'; | ||
436 | let element; | ||
437 | |||
438 | this.parseStream.on('data', function(elem) { | ||
439 | element = elem; | ||
440 | }); | ||
441 | this.lineStream.push(manifest); | ||
442 | |||
443 | QUnit.ok(element, 'an event was triggered'); | ||
444 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
445 | QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
446 | QUnit.ok(!('attributes' in element), 'no attributes are present'); | ||
447 | }); | ||
448 | QUnit.test('parses #EXT-X-STREAM-INF with common attributes', function() { | ||
449 | let manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n'; | ||
450 | let element; | ||
451 | |||
452 | this.parseStream.on('data', function(elem) { | ||
453 | element = elem; | ||
454 | }); | ||
455 | this.lineStream.push(manifest); | ||
456 | |||
457 | QUnit.ok(element, 'an event was triggered'); | ||
458 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
459 | QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
460 | QUnit.strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed'); | ||
461 | |||
462 | manifest = '#EXT-X-STREAM-INF:PROGRAM-ID=7\n'; | ||
463 | this.lineStream.push(manifest); | ||
464 | |||
465 | QUnit.ok(element, 'an event was triggered'); | ||
466 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
467 | QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
468 | QUnit.strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed'); | ||
469 | |||
470 | manifest = '#EXT-X-STREAM-INF:RESOLUTION=396x224\n'; | ||
471 | this.lineStream.push(manifest); | ||
472 | |||
473 | QUnit.ok(element, 'an event was triggered'); | ||
474 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
475 | QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
476 | QUnit.strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed'); | ||
477 | QUnit.strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed'); | ||
478 | |||
479 | manifest = '#EXT-X-STREAM-INF:CODECS="avc1.4d400d, mp4a.40.2"\n'; | ||
480 | this.lineStream.push(manifest); | ||
481 | |||
482 | QUnit.ok(element, 'an event was triggered'); | ||
483 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
484 | QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
485 | QUnit.strictEqual(element.attributes.CODECS, | ||
486 | 'avc1.4d400d, mp4a.40.2', | ||
487 | 'codecs are parsed'); | ||
488 | }); | ||
489 | QUnit.test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() { | ||
490 | let manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n'; | ||
491 | let element; | ||
492 | |||
493 | this.parseStream.on('data', function(elem) { | ||
494 | element = elem; | ||
495 | }); | ||
496 | this.lineStream.push(manifest); | ||
497 | |||
498 | QUnit.ok(element, 'an event was triggered'); | ||
499 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
500 | QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
501 | QUnit.strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed'); | ||
502 | QUnit.strictEqual(element.attributes.ALPHA, | ||
503 | 'Value', | ||
504 | 'alphabetic attributes are parsed'); | ||
505 | QUnit.strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed'); | ||
506 | }); | ||
507 | // #EXT-X-ENDLIST | ||
508 | QUnit.test('parses #EXT-X-ENDLIST tags', function() { | ||
509 | let manifest = '#EXT-X-ENDLIST\n'; | ||
510 | let element; | ||
511 | |||
512 | this.parseStream.on('data', function(elem) { | ||
513 | element = elem; | ||
514 | }); | ||
515 | this.lineStream.push(manifest); | ||
516 | |||
517 | QUnit.ok(element, 'an event was triggered'); | ||
518 | QUnit.strictEqual(element.type, 'tag', 'the line type is tag'); | ||
519 | QUnit.strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf'); | ||
520 | }); | ||
521 | |||
522 | // #EXT-X-KEY | ||
523 | QUnit.test('parses valid #EXT-X-KEY tags', function() { | ||
524 | let manifest = | ||
525 | '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n'; | ||
526 | let element; | ||
527 | |||
528 | this.parseStream.on('data', function(elem) { | ||
529 | element = elem; | ||
530 | }); | ||
531 | this.lineStream.push(manifest); | ||
532 | |||
533 | QUnit.ok(element, 'an event was triggered'); | ||
534 | QUnit.deepEqual(element, { | ||
535 | type: 'tag', | ||
536 | tagType: 'key', | ||
537 | attributes: { | ||
538 | METHOD: 'AES-128', | ||
539 | URI: 'https://priv.example.com/key.php?r=52' | ||
540 | } | ||
541 | }, 'parsed a valid key'); | ||
542 | |||
543 | manifest = '#EXT-X-KEY:URI="https://example.com/key#1",METHOD=FutureType-1024\n'; | ||
544 | this.lineStream.push(manifest); | ||
545 | QUnit.ok(element, 'an event was triggered'); | ||
546 | QUnit.deepEqual(element, { | ||
547 | type: 'tag', | ||
548 | tagType: 'key', | ||
549 | attributes: { | ||
550 | METHOD: 'FutureType-1024', | ||
551 | URI: 'https://example.com/key#1' | ||
552 | } | ||
553 | }, 'parsed the attribute list independent of order'); | ||
554 | |||
555 | manifest = '#EXT-X-KEY:IV=1234567890abcdef1234567890abcdef\n'; | ||
556 | this.lineStream.push(manifest); | ||
557 | QUnit.ok(element.attributes.IV, 'detected an IV attribute'); | ||
558 | QUnit.deepEqual(element.attributes.IV, new Uint32Array([ | ||
559 | 0x12345678, | ||
560 | 0x90abcdef, | ||
561 | 0x12345678, | ||
562 | 0x90abcdef | ||
563 | ]), 'parsed an IV value'); | ||
564 | }); | ||
565 | |||
566 | QUnit.test('parses minimal #EXT-X-KEY tags', function() { | ||
567 | let manifest = '#EXT-X-KEY:\n'; | ||
568 | let element; | ||
569 | |||
570 | this.parseStream.on('data', function(elem) { | ||
571 | element = elem; | ||
572 | }); | ||
573 | this.lineStream.push(manifest); | ||
574 | |||
575 | QUnit.ok(element, 'an event was triggered'); | ||
576 | QUnit.deepEqual(element, { | ||
577 | type: 'tag', | ||
578 | tagType: 'key' | ||
579 | }, 'parsed a minimal key tag'); | ||
580 | }); | ||
581 | |||
582 | QUnit.test('parses lightly-broken #EXT-X-KEY tags', function() { | ||
583 | let manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n'; | ||
584 | let element; | ||
585 | |||
586 | this.parseStream.on('data', function(elem) { | ||
587 | element = elem; | ||
588 | }); | ||
589 | this.lineStream.push(manifest); | ||
590 | |||
591 | QUnit.strictEqual(element.attributes.URI, | ||
592 | 'https://example.com/single-quote', | ||
593 | 'parsed a single-quoted uri'); | ||
594 | |||
595 | element = null; | ||
596 | manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n'; | ||
597 | this.lineStream.push(manifest); | ||
598 | QUnit.strictEqual(element.tagType, 'key', 'parsed the tag type'); | ||
599 | QUnit.strictEqual(element.attributes.URI, | ||
600 | 'https://example.com/key', | ||
601 | 'inferred a colon after the tag type'); | ||
602 | |||
603 | element = null; | ||
604 | manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n'; | ||
605 | this.lineStream.push(manifest); | ||
606 | QUnit.strictEqual(element.attributes.URI, | ||
607 | 'https://example.com/key', | ||
608 | 'trims and removes quotes around the URI'); | ||
609 | }); | ||
610 | |||
611 | QUnit.test('ignores empty lines', function() { | ||
612 | let manifest = '\n'; | ||
613 | let event = false; | ||
614 | |||
615 | this.parseStream.on('data', function() { | ||
616 | event = true; | ||
617 | }); | ||
618 | this.lineStream.push(manifest); | ||
619 | |||
620 | QUnit.ok(!event, 'no event is triggered'); | ||
621 | }); | ||
622 | |||
623 | QUnit.module('m3u8 parser'); | ||
624 | |||
625 | QUnit.test('can be constructed', function() { | ||
626 | QUnit.notStrictEqual(typeof new Parser(), 'undefined', 'parser is defined'); | ||
627 | }); | ||
628 | |||
629 | QUnit.module('m3u8s'); | ||
630 | |||
631 | QUnit.test('parses static manifests as expected', function() { | ||
632 | let key; | ||
633 | |||
634 | for (key in testDataManifests) { | ||
635 | if (testDataExpected[key]) { | ||
636 | let parser = new Parser(); | ||
637 | |||
638 | parser.push(testDataManifests[key]); | ||
639 | QUnit.deepEqual(parser.manifest, | ||
640 | testDataExpected[key], | ||
641 | key + '.m3u8 was parsed correctly' | ||
642 | ); | ||
643 | } | ||
644 | } | ||
645 | }); |
test/m3u8_test.js
deleted
100644 → 0
1 | (function(window, undefined) { | ||
2 | var | ||
3 | //manifestController = this.manifestController, | ||
4 | m3u8 = window.videojs.m3u8, | ||
5 | ParseStream = m3u8.ParseStream, | ||
6 | parseStream, | ||
7 | LineStream = m3u8.LineStream, | ||
8 | lineStream, | ||
9 | Parser = m3u8.Parser, | ||
10 | parser; | ||
11 | |||
12 | /* | ||
13 | M3U8 Test Suite | ||
14 | */ | ||
15 | |||
16 | module('LineStream', { | ||
17 | setup: function() { | ||
18 | lineStream = new LineStream(); | ||
19 | } | ||
20 | }); | ||
21 | test('empty inputs produce no tokens', function() { | ||
22 | var data = false; | ||
23 | lineStream.on('data', function() { | ||
24 | data = true; | ||
25 | }); | ||
26 | lineStream.push(''); | ||
27 | ok(!data, 'no tokens were produced'); | ||
28 | }); | ||
29 | test('splits on newlines', function() { | ||
30 | var lines = []; | ||
31 | lineStream.on('data', function(line) { | ||
32 | lines.push(line); | ||
33 | }); | ||
34 | lineStream.push('#EXTM3U\nmovie.ts\n'); | ||
35 | |||
36 | strictEqual(2, lines.length, 'two lines are ready'); | ||
37 | strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token'); | ||
38 | strictEqual('movie.ts', lines.shift(), 'the second line is the second token'); | ||
39 | }); | ||
40 | test('empty lines become empty strings', function() { | ||
41 | var lines = []; | ||
42 | lineStream.on('data', function(line) { | ||
43 | lines.push(line); | ||
44 | }); | ||
45 | lineStream.push('\n\n'); | ||
46 | |||
47 | strictEqual(2, lines.length, 'two lines are ready'); | ||
48 | strictEqual('', lines.shift(), 'the first line is empty'); | ||
49 | strictEqual('', lines.shift(), 'the second line is empty'); | ||
50 | }); | ||
51 | test('handles lines broken across appends', function() { | ||
52 | var lines = []; | ||
53 | lineStream.on('data', function(line) { | ||
54 | lines.push(line); | ||
55 | }); | ||
56 | lineStream.push('#EXTM'); | ||
57 | strictEqual(0, lines.length, 'no lines are ready'); | ||
58 | |||
59 | lineStream.push('3U\nmovie.ts\n'); | ||
60 | strictEqual(2, lines.length, 'two lines are ready'); | ||
61 | strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token'); | ||
62 | strictEqual('movie.ts', lines.shift(), 'the second line is the second token'); | ||
63 | }); | ||
64 | test('stops sending events after deregistering', function() { | ||
65 | var | ||
66 | temporaryLines = [], | ||
67 | temporary = function(line) { | ||
68 | temporaryLines.push(line); | ||
69 | }, | ||
70 | permanentLines = [], | ||
71 | permanent = function(line) { | ||
72 | permanentLines.push(line); | ||
73 | }; | ||
74 | |||
75 | lineStream.on('data', temporary); | ||
76 | lineStream.on('data', permanent); | ||
77 | lineStream.push('line one\n'); | ||
78 | strictEqual(temporaryLines.length, permanentLines.length, 'both callbacks receive the event'); | ||
79 | |||
80 | ok(lineStream.off('data', temporary), 'a listener was removed'); | ||
81 | lineStream.push('line two\n'); | ||
82 | strictEqual(1, temporaryLines.length, 'no new events are received'); | ||
83 | strictEqual(2, permanentLines.length, 'new events are still received'); | ||
84 | }); | ||
85 | |||
86 | module('ParseStream', { | ||
87 | setup: function() { | ||
88 | lineStream = new LineStream(); | ||
89 | parseStream = new ParseStream(); | ||
90 | lineStream.pipe(parseStream); | ||
91 | } | ||
92 | }); | ||
93 | test('parses comment lines', function() { | ||
94 | var | ||
95 | manifest = '# a line that starts with a hash mark without "EXT" is a comment\n', | ||
96 | element; | ||
97 | parseStream.on('data', function(elem) { | ||
98 | element = elem; | ||
99 | }); | ||
100 | lineStream.push(manifest); | ||
101 | |||
102 | ok(element, 'an event was triggered'); | ||
103 | strictEqual(element.type, 'comment', 'the type is comment'); | ||
104 | strictEqual(element.text, | ||
105 | manifest.slice(1, manifest.length - 1), | ||
106 | 'the comment text is parsed'); | ||
107 | }); | ||
108 | test('parses uri lines', function() { | ||
109 | var | ||
110 | manifest = 'any non-blank line that does not start with a hash-mark is a URI\n', | ||
111 | element; | ||
112 | parseStream.on('data', function(elem) { | ||
113 | element = elem; | ||
114 | }); | ||
115 | lineStream.push(manifest); | ||
116 | |||
117 | ok(element, 'an event was triggered'); | ||
118 | strictEqual(element.type, 'uri', 'the type is uri'); | ||
119 | strictEqual(element.uri, | ||
120 | manifest.substring(0, manifest.length - 1), | ||
121 | 'the uri text is parsed'); | ||
122 | }); | ||
123 | test('parses unknown tag types', function() { | ||
124 | var | ||
125 | manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n', | ||
126 | element; | ||
127 | parseStream.on('data', function(elem) { | ||
128 | element = elem; | ||
129 | }); | ||
130 | lineStream.push(manifest); | ||
131 | |||
132 | ok(element, 'an event was triggered'); | ||
133 | strictEqual(element.type, 'tag', 'the type is tag'); | ||
134 | strictEqual(element.data, | ||
135 | manifest.slice(4, manifest.length - 1), | ||
136 | 'unknown tag data is preserved'); | ||
137 | }); | ||
138 | |||
139 | // #EXTM3U | ||
140 | test('parses #EXTM3U tags', function() { | ||
141 | var | ||
142 | manifest = '#EXTM3U\n', | ||
143 | element; | ||
144 | parseStream.on('data', function(elem) { | ||
145 | element = elem; | ||
146 | }); | ||
147 | lineStream.push(manifest); | ||
148 | |||
149 | ok(element, 'an event was triggered'); | ||
150 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
151 | strictEqual(element.tagType, 'm3u', 'the tag type is m3u'); | ||
152 | }); | ||
153 | |||
154 | // #EXTINF | ||
155 | test('parses minimal #EXTINF tags', function() { | ||
156 | var | ||
157 | manifest = '#EXTINF\n', | ||
158 | element; | ||
159 | parseStream.on('data', function(elem) { | ||
160 | element = elem; | ||
161 | }); | ||
162 | lineStream.push(manifest); | ||
163 | |||
164 | ok(element, 'an event was triggered'); | ||
165 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
166 | strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
167 | }); | ||
168 | test('parses #EXTINF tags with durations', function() { | ||
169 | var | ||
170 | manifest = '#EXTINF:15\n', | ||
171 | element; | ||
172 | parseStream.on('data', function(elem) { | ||
173 | element = elem; | ||
174 | }); | ||
175 | lineStream.push(manifest); | ||
176 | |||
177 | ok(element, 'an event was triggered'); | ||
178 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
179 | strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
180 | strictEqual(element.duration, 15, 'the duration is parsed'); | ||
181 | ok(!('title' in element), 'no title is parsed'); | ||
182 | |||
183 | manifest = '#EXTINF:21,\n'; | ||
184 | lineStream.push(manifest); | ||
185 | |||
186 | ok(element, 'an event was triggered'); | ||
187 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
188 | strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
189 | strictEqual(element.duration, 21, 'the duration is parsed'); | ||
190 | ok(!('title' in element), 'no title is parsed'); | ||
191 | }); | ||
192 | test('parses #EXTINF tags with a duration and title', function() { | ||
193 | var | ||
194 | manifest = '#EXTINF:13,Does anyone really use the title attribute?\n', | ||
195 | element; | ||
196 | parseStream.on('data', function(elem) { | ||
197 | element = elem; | ||
198 | }); | ||
199 | lineStream.push(manifest); | ||
200 | |||
201 | ok(element, 'an event was triggered'); | ||
202 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
203 | strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
204 | strictEqual(element.duration, 13, 'the duration is parsed'); | ||
205 | strictEqual(element.title, | ||
206 | manifest.substring(manifest.indexOf(',') + 1, manifest.length - 1), | ||
207 | 'the title is parsed'); | ||
208 | }); | ||
209 | test('parses #EXTINF tags with carriage returns', function() { | ||
210 | var | ||
211 | manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n', | ||
212 | element; | ||
213 | parseStream.on('data', function(elem) { | ||
214 | element = elem; | ||
215 | }); | ||
216 | lineStream.push(manifest); | ||
217 | |||
218 | ok(element, 'an event was triggered'); | ||
219 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
220 | strictEqual(element.tagType, 'inf', 'the tag type is inf'); | ||
221 | strictEqual(element.duration, 13, 'the duration is parsed'); | ||
222 | strictEqual(element.title, | ||
223 | manifest.substring(manifest.indexOf(',') + 1, manifest.length - 2), | ||
224 | 'the title is parsed'); | ||
225 | }); | ||
226 | |||
227 | // #EXT-X-TARGETDURATION | ||
228 | test('parses minimal #EXT-X-TARGETDURATION tags', function() { | ||
229 | var | ||
230 | manifest = '#EXT-X-TARGETDURATION\n', | ||
231 | element; | ||
232 | parseStream.on('data', function(elem) { | ||
233 | element = elem; | ||
234 | }); | ||
235 | lineStream.push(manifest); | ||
236 | |||
237 | ok(element, 'an event was triggered'); | ||
238 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
239 | strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration'); | ||
240 | ok(!('duration' in element), 'no duration is parsed'); | ||
241 | }); | ||
242 | test('parses #EXT-X-TARGETDURATION with duration', function() { | ||
243 | var | ||
244 | manifest = '#EXT-X-TARGETDURATION:47\n', | ||
245 | element; | ||
246 | parseStream.on('data', function(elem) { | ||
247 | element = elem; | ||
248 | }); | ||
249 | lineStream.push(manifest); | ||
250 | |||
251 | ok(element, 'an event was triggered'); | ||
252 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
253 | strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration'); | ||
254 | strictEqual(element.duration, 47, 'the duration is parsed'); | ||
255 | }); | ||
256 | |||
257 | // #EXT-X-VERSION | ||
258 | test('parses minimal #EXT-X-VERSION tags', function() { | ||
259 | var | ||
260 | manifest = '#EXT-X-VERSION:\n', | ||
261 | element; | ||
262 | parseStream.on('data', function(elem) { | ||
263 | element = elem; | ||
264 | }); | ||
265 | lineStream.push(manifest); | ||
266 | |||
267 | ok(element, 'an event was triggered'); | ||
268 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
269 | strictEqual(element.tagType, 'version', 'the tag type is version'); | ||
270 | ok(!('version' in element), 'no version is present'); | ||
271 | }); | ||
272 | test('parses #EXT-X-VERSION with a version', function() { | ||
273 | var | ||
274 | manifest = '#EXT-X-VERSION:99\n', | ||
275 | element; | ||
276 | parseStream.on('data', function(elem) { | ||
277 | element = elem; | ||
278 | }); | ||
279 | lineStream.push(manifest); | ||
280 | |||
281 | ok(element, 'an event was triggered'); | ||
282 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
283 | strictEqual(element.tagType, 'version', 'the tag type is version'); | ||
284 | strictEqual(element.version, 99, 'the version is parsed'); | ||
285 | }); | ||
286 | |||
287 | // #EXT-X-MEDIA-SEQUENCE | ||
288 | test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() { | ||
289 | var | ||
290 | manifest = '#EXT-X-MEDIA-SEQUENCE\n', | ||
291 | element; | ||
292 | parseStream.on('data', function(elem) { | ||
293 | element = elem; | ||
294 | }); | ||
295 | lineStream.push(manifest); | ||
296 | |||
297 | ok(element, 'an event was triggered'); | ||
298 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
299 | strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence'); | ||
300 | ok(!('number' in element), 'no number is present'); | ||
301 | }); | ||
302 | test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() { | ||
303 | var | ||
304 | manifest = '#EXT-X-MEDIA-SEQUENCE:109\n', | ||
305 | element; | ||
306 | parseStream.on('data', function(elem) { | ||
307 | element = elem; | ||
308 | }); | ||
309 | lineStream.push(manifest); | ||
310 | |||
311 | ok(element, 'an event was triggered'); | ||
312 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
313 | strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence'); | ||
314 | ok(element.number, 109, 'the number is parsed'); | ||
315 | }); | ||
316 | |||
317 | // #EXT-X-PLAYLIST-TYPE | ||
318 | test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() { | ||
319 | var | ||
320 | manifest = '#EXT-X-PLAYLIST-TYPE:\n', | ||
321 | element; | ||
322 | parseStream.on('data', function(elem) { | ||
323 | element = elem; | ||
324 | }); | ||
325 | lineStream.push(manifest); | ||
326 | |||
327 | ok(element, 'an event was triggered'); | ||
328 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
329 | strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type'); | ||
330 | ok(!('playlistType' in element), 'no playlist type is present'); | ||
331 | }); | ||
332 | test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() { | ||
333 | var | ||
334 | manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n', | ||
335 | element; | ||
336 | parseStream.on('data', function(elem) { | ||
337 | element = elem; | ||
338 | }); | ||
339 | lineStream.push(manifest); | ||
340 | |||
341 | ok(element, 'an event was triggered'); | ||
342 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
343 | strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type'); | ||
344 | strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT'); | ||
345 | |||
346 | manifest = '#EXT-X-PLAYLIST-TYPE:VOD\n'; | ||
347 | lineStream.push(manifest); | ||
348 | ok(element, 'an event was triggered'); | ||
349 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
350 | strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type'); | ||
351 | strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD'); | ||
352 | |||
353 | manifest = '#EXT-X-PLAYLIST-TYPE:nonsense\n'; | ||
354 | lineStream.push(manifest); | ||
355 | ok(element, 'an event was triggered'); | ||
356 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
357 | strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type'); | ||
358 | strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed'); | ||
359 | }); | ||
360 | |||
361 | // #EXT-X-BYTERANGE | ||
362 | test('parses minimal #EXT-X-BYTERANGE tags', function() { | ||
363 | var | ||
364 | manifest = '#EXT-X-BYTERANGE\n', | ||
365 | element; | ||
366 | parseStream.on('data', function(elem) { | ||
367 | element = elem; | ||
368 | }); | ||
369 | lineStream.push(manifest); | ||
370 | |||
371 | ok(element, 'an event was triggered'); | ||
372 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
373 | strictEqual(element.tagType, 'byterange', 'the tag type is byterange'); | ||
374 | ok(!('length' in element), 'no length is present'); | ||
375 | ok(!('offset' in element), 'no offset is present'); | ||
376 | }); | ||
377 | test('parses #EXT-X-BYTERANGE with length and offset', function() { | ||
378 | var | ||
379 | manifest = '#EXT-X-BYTERANGE:45\n', | ||
380 | element; | ||
381 | parseStream.on('data', function(elem) { | ||
382 | element = elem; | ||
383 | }); | ||
384 | lineStream.push(manifest); | ||
385 | |||
386 | ok(element, 'an event was triggered'); | ||
387 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
388 | strictEqual(element.tagType, 'byterange', 'the tag type is byterange'); | ||
389 | strictEqual(element.length, 45, 'length is parsed'); | ||
390 | ok(!('offset' in element), 'no offset is present'); | ||
391 | |||
392 | manifest = '#EXT-X-BYTERANGE:108@16\n'; | ||
393 | lineStream.push(manifest); | ||
394 | ok(element, 'an event was triggered'); | ||
395 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
396 | strictEqual(element.tagType, 'byterange', 'the tag type is byterange'); | ||
397 | strictEqual(element.length, 108, 'length is parsed'); | ||
398 | strictEqual(element.offset, 16, 'offset is parsed'); | ||
399 | }); | ||
400 | |||
401 | // #EXT-X-ALLOW-CACHE | ||
402 | test('parses minimal #EXT-X-ALLOW-CACHE tags', function() { | ||
403 | var | ||
404 | manifest = '#EXT-X-ALLOW-CACHE:\n', | ||
405 | element; | ||
406 | parseStream.on('data', function(elem) { | ||
407 | element = elem; | ||
408 | }); | ||
409 | lineStream.push(manifest); | ||
410 | |||
411 | ok(element, 'an event was triggered'); | ||
412 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
413 | strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache'); | ||
414 | ok(!('allowed' in element), 'no allowed is present'); | ||
415 | }); | ||
416 | test('parses valid #EXT-X-ALLOW-CACHE tags', function() { | ||
417 | var | ||
418 | manifest = '#EXT-X-ALLOW-CACHE:YES\n', | ||
419 | element; | ||
420 | parseStream.on('data', function(elem) { | ||
421 | element = elem; | ||
422 | }); | ||
423 | lineStream.push(manifest); | ||
424 | |||
425 | ok(element, 'an event was triggered'); | ||
426 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
427 | strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache'); | ||
428 | ok(element.allowed, 'allowed is parsed'); | ||
429 | |||
430 | manifest = '#EXT-X-ALLOW-CACHE:NO\n'; | ||
431 | lineStream.push(manifest); | ||
432 | |||
433 | ok(element, 'an event was triggered'); | ||
434 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
435 | strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache'); | ||
436 | ok(!element.allowed, 'allowed is parsed'); | ||
437 | }); | ||
438 | // #EXT-X-STREAM-INF | ||
439 | test('parses minimal #EXT-X-STREAM-INF tags', function() { | ||
440 | var | ||
441 | manifest = '#EXT-X-STREAM-INF\n', | ||
442 | element; | ||
443 | parseStream.on('data', function(elem) { | ||
444 | element = elem; | ||
445 | }); | ||
446 | lineStream.push(manifest); | ||
447 | |||
448 | ok(element, 'an event was triggered'); | ||
449 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
450 | strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
451 | ok(!('attributes' in element), 'no attributes are present'); | ||
452 | }); | ||
453 | test('parses #EXT-X-STREAM-INF with common attributes', function() { | ||
454 | var | ||
455 | manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n', | ||
456 | element; | ||
457 | parseStream.on('data', function(elem) { | ||
458 | element = elem; | ||
459 | }); | ||
460 | lineStream.push(manifest); | ||
461 | |||
462 | ok(element, 'an event was triggered'); | ||
463 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
464 | strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
465 | strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed'); | ||
466 | |||
467 | manifest = '#EXT-X-STREAM-INF:PROGRAM-ID=7\n'; | ||
468 | lineStream.push(manifest); | ||
469 | |||
470 | ok(element, 'an event was triggered'); | ||
471 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
472 | strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
473 | strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed'); | ||
474 | |||
475 | manifest = '#EXT-X-STREAM-INF:RESOLUTION=396x224\n'; | ||
476 | lineStream.push(manifest); | ||
477 | |||
478 | ok(element, 'an event was triggered'); | ||
479 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
480 | strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
481 | strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed'); | ||
482 | strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed'); | ||
483 | |||
484 | manifest = '#EXT-X-STREAM-INF:CODECS="avc1.4d400d, mp4a.40.2"\n'; | ||
485 | lineStream.push(manifest); | ||
486 | |||
487 | ok(element, 'an event was triggered'); | ||
488 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
489 | strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
490 | strictEqual(element.attributes.CODECS, | ||
491 | 'avc1.4d400d, mp4a.40.2', | ||
492 | 'codecs are parsed'); | ||
493 | }); | ||
494 | test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() { | ||
495 | var | ||
496 | manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n', | ||
497 | element; | ||
498 | parseStream.on('data', function(elem) { | ||
499 | element = elem; | ||
500 | }); | ||
501 | lineStream.push(manifest); | ||
502 | |||
503 | ok(element, 'an event was triggered'); | ||
504 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
505 | strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); | ||
506 | strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed'); | ||
507 | strictEqual(element.attributes.ALPHA, 'Value', 'alphabetic attributes are parsed'); | ||
508 | strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed'); | ||
509 | }); | ||
510 | // #EXT-X-ENDLIST | ||
511 | test('parses #EXT-X-ENDLIST tags', function() { | ||
512 | var | ||
513 | manifest = '#EXT-X-ENDLIST\n', | ||
514 | element; | ||
515 | parseStream.on('data', function(elem) { | ||
516 | element = elem; | ||
517 | }); | ||
518 | lineStream.push(manifest); | ||
519 | |||
520 | ok(element, 'an event was triggered'); | ||
521 | strictEqual(element.type, 'tag', 'the line type is tag'); | ||
522 | strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf'); | ||
523 | }); | ||
524 | |||
525 | // #EXT-X-KEY | ||
526 | test('parses valid #EXT-X-KEY tags', function() { | ||
527 | var | ||
528 | manifest = '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n', | ||
529 | element; | ||
530 | parseStream.on('data', function(elem) { | ||
531 | element = elem; | ||
532 | }); | ||
533 | lineStream.push(manifest); | ||
534 | |||
535 | ok(element, 'an event was triggered'); | ||
536 | deepEqual(element, { | ||
537 | type: 'tag', | ||
538 | tagType: 'key', | ||
539 | attributes: { | ||
540 | METHOD: 'AES-128', | ||
541 | URI: 'https://priv.example.com/key.php?r=52' | ||
542 | } | ||
543 | }, 'parsed a valid key'); | ||
544 | |||
545 | manifest = '#EXT-X-KEY:URI="https://example.com/key#1",METHOD=FutureType-1024\n'; | ||
546 | lineStream.push(manifest); | ||
547 | ok(element, 'an event was triggered'); | ||
548 | deepEqual(element, { | ||
549 | type: 'tag', | ||
550 | tagType: 'key', | ||
551 | attributes: { | ||
552 | METHOD: 'FutureType-1024', | ||
553 | URI: 'https://example.com/key#1' | ||
554 | } | ||
555 | }, 'parsed the attribute list independent of order'); | ||
556 | |||
557 | manifest = '#EXT-X-KEY:IV=1234567890abcdef1234567890abcdef\n'; | ||
558 | lineStream.push(manifest); | ||
559 | ok(element.attributes.IV, 'detected an IV attribute'); | ||
560 | deepEqual(element.attributes.IV, new Uint32Array([ | ||
561 | 0x12345678, | ||
562 | 0x90abcdef, | ||
563 | 0x12345678, | ||
564 | 0x90abcdef | ||
565 | ]), 'parsed an IV value'); | ||
566 | }); | ||
567 | |||
568 | test('parses minimal #EXT-X-KEY tags', function() { | ||
569 | var | ||
570 | manifest = '#EXT-X-KEY:\n', | ||
571 | element; | ||
572 | parseStream.on('data', function(elem) { | ||
573 | element = elem; | ||
574 | }); | ||
575 | lineStream.push(manifest); | ||
576 | |||
577 | ok(element, 'an event was triggered'); | ||
578 | deepEqual(element, { | ||
579 | type: 'tag', | ||
580 | tagType: 'key' | ||
581 | }, 'parsed a minimal key tag'); | ||
582 | }); | ||
583 | |||
584 | test('parses lightly-broken #EXT-X-KEY tags', function() { | ||
585 | var | ||
586 | manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n', | ||
587 | element; | ||
588 | parseStream.on('data', function(elem) { | ||
589 | element = elem; | ||
590 | }); | ||
591 | lineStream.push(manifest); | ||
592 | |||
593 | strictEqual(element.attributes.URI, | ||
594 | 'https://example.com/single-quote', | ||
595 | 'parsed a single-quoted uri'); | ||
596 | |||
597 | element = null; | ||
598 | manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n'; | ||
599 | lineStream.push(manifest); | ||
600 | strictEqual(element.tagType, 'key', 'parsed the tag type'); | ||
601 | strictEqual(element.attributes.URI, | ||
602 | 'https://example.com/key', | ||
603 | 'inferred a colon after the tag type'); | ||
604 | |||
605 | element = null; | ||
606 | manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n'; | ||
607 | lineStream.push(manifest); | ||
608 | strictEqual(element.attributes.URI, | ||
609 | 'https://example.com/key', | ||
610 | 'trims and removes quotes around the URI'); | ||
611 | }); | ||
612 | |||
613 | test('ignores empty lines', function() { | ||
614 | var | ||
615 | manifest = '\n', | ||
616 | event = false; | ||
617 | parseStream.on('data', function() { | ||
618 | event = true; | ||
619 | }); | ||
620 | lineStream.push(manifest); | ||
621 | |||
622 | ok(!event, 'no event is triggered'); | ||
623 | }); | ||
624 | |||
625 | module('m3u8 parser'); | ||
626 | |||
627 | test('can be constructed', function() { | ||
628 | notStrictEqual(new Parser(), undefined, 'parser is defined'); | ||
629 | }); | ||
630 | |||
631 | module('m3u8s'); | ||
632 | |||
633 | test('parses static manifests as expected', function() { | ||
634 | var key; | ||
635 | for (key in window.manifests) { | ||
636 | if (window.expected[key]) { | ||
637 | parser = new Parser(); | ||
638 | parser.push(window.manifests[key]); | ||
639 | deepEqual(parser.manifest, | ||
640 | window.expected[key], | ||
641 | key + '.m3u8 was parsed correctly'); | ||
642 | } | ||
643 | } | ||
644 | }); | ||
645 | |||
646 | })(window, window.console); |
test/perf.html
deleted
100644 → 0
1 | <!doctype html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <title>MPEG-TS Parser Performance Workbench</title> | ||
5 | <!-- video.js --> | ||
6 | <script src="../node_modules/video.js/video.dev.js"></script> | ||
7 | |||
8 | <!-- HLS plugin --> | ||
9 | <script src="../src/video-js-hls.js"></script> | ||
10 | <script src="../src/flv-tag.js"></script> | ||
11 | <script src="../src/exp-golomb.js"></script> | ||
12 | <script src="../src/h264-stream.js"></script> | ||
13 | <script src="../src/aac-stream.js"></script> | ||
14 | <script src="../src/segment-parser.js"></script> | ||
15 | |||
16 | <!-- MPEG-TS segment --> | ||
17 | <script src="tsSegment-bc.js"></script> | ||
18 | <style> | ||
19 | .desc { | ||
20 | background-color: #ddd; | ||
21 | border: thin solid #333; | ||
22 | padding: 8px; | ||
23 | } | ||
24 | </style> | ||
25 | </head> | ||
26 | <body> | ||
27 | <p class="desc">Select your number of iterations and then press "Run" to begin parsing MPEG-TS packets into FLV tags. This page can be handy for identifying segment parser performance bottlenecks.</p> | ||
28 | <form> | ||
29 | <input name="iterations" min="1" type="number" value="1"> | ||
30 | <button type="sumbit">Run</button> | ||
31 | </form> | ||
32 | <table> | ||
33 | <thead> | ||
34 | <th>Iterations</th><th>Time</th><th>MB/second</th> | ||
35 | </thead> | ||
36 | <tbody class="results"></tbody> | ||
37 | </table> | ||
38 | <script> | ||
39 | var | ||
40 | button = document.querySelector('button'), | ||
41 | input = document.querySelector('input'), | ||
42 | results = document.querySelector('.results'), | ||
43 | |||
44 | reportResults = function(count, elapsed) { | ||
45 | var | ||
46 | row = document.createElement('tr'), | ||
47 | countCell = document.createElement('td'), | ||
48 | elapsedCell = document.createElement('td'), | ||
49 | throughputCell = document.createElement('td'); | ||
50 | |||
51 | countCell.innerText = count; | ||
52 | elapsedCell.innerText = elapsed; | ||
53 | throughputCell.innerText = (((bcSegment.byteLength * count * 1000) / elapsed) / (Math.pow(2, 20))).toFixed(3); | ||
54 | row.appendChild(countCell); | ||
55 | row.appendChild(elapsedCell); | ||
56 | row.appendChild(throughputCell); | ||
57 | |||
58 | results.insertBefore(row, results.firstChild); | ||
59 | }; | ||
60 | |||
61 | button.addEventListener('click', function(event) { | ||
62 | var | ||
63 | iterations = input.value, | ||
64 | parser = new window.videojs.hls.SegmentParser(), | ||
65 | start; | ||
66 | |||
67 | // setup | ||
68 | start = +new Date(); | ||
69 | |||
70 | while (iterations--) { | ||
71 | |||
72 | // parse the segment | ||
73 | parser.parseSegmentBinaryData(window.bcSegment); | ||
74 | |||
75 | // finalize all the FLV tags | ||
76 | while (parser.tagsAvailable()) { | ||
77 | parser.getNextTag(); | ||
78 | } | ||
79 | } | ||
80 | |||
81 | // report | ||
82 | reportResults(input.value, (+new Date()) - start); | ||
83 | |||
84 | // don't actually submit the form | ||
85 | event.preventDefault(); | ||
86 | }, false); | ||
87 | </script> | ||
88 | </body> | ||
89 | </html> |
test/playlist-loader.test.js
0 → 100644
1 | import sinon from 'sinon'; | ||
2 | import QUnit from 'qunit'; | ||
3 | import PlaylistLoader from '../src/playlist-loader'; | ||
4 | import videojs from 'video.js'; | ||
5 | // Attempts to produce an absolute URL to a given relative path | ||
6 | // based on window.location.href | ||
7 | const urlTo = function(path) { | ||
8 | return window.location.href | ||
9 | .split('/') | ||
10 | .slice(0, -1) | ||
11 | .concat([path]) | ||
12 | .join('/'); | ||
13 | }; | ||
14 | |||
15 | QUnit.module('Playlist Loader', { | ||
16 | beforeEach() { | ||
17 | // fake XHRs | ||
18 | this.oldXHR = videojs.xhr.XMLHttpRequest; | ||
19 | this.sinonXhr = sinon.useFakeXMLHttpRequest(); | ||
20 | this.requests = []; | ||
21 | this.sinonXhr.onCreate = (xhr) => { | ||
22 | // force the XHR2 timeout polyfill | ||
23 | xhr.timeout = null; | ||
24 | this.requests.push(xhr); | ||
25 | }; | ||
26 | |||
27 | // fake timers | ||
28 | this.clock = sinon.useFakeTimers(); | ||
29 | videojs.xhr.XMLHttpRequest = this.sinonXhr; | ||
30 | }, | ||
31 | afterEach() { | ||
32 | this.sinonXhr.restore(); | ||
33 | this.clock.restore(); | ||
34 | videojs.xhr.XMLHttpRequest = this.oldXHR; | ||
35 | } | ||
36 | }); | ||
37 | |||
38 | QUnit.test('throws if the playlist url is empty or undefined', function() { | ||
39 | QUnit.throws(function() { | ||
40 | PlaylistLoader(); | ||
41 | }, 'requires an argument'); | ||
42 | QUnit.throws(function() { | ||
43 | PlaylistLoader(''); | ||
44 | }, 'does not accept the empty string'); | ||
45 | }); | ||
46 | |||
47 | QUnit.test('starts without any metadata', function() { | ||
48 | let loader = new PlaylistLoader('master.m3u8'); | ||
49 | |||
50 | QUnit.strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); | ||
51 | }); | ||
52 | |||
53 | QUnit.test('starts with no expired time', function() { | ||
54 | let loader = new PlaylistLoader('media.m3u8'); | ||
55 | |||
56 | this.requests.pop().respond(200, null, | ||
57 | '#EXTM3U\n' + | ||
58 | '#EXTINF:10,\n' + | ||
59 | '0.ts\n'); | ||
60 | QUnit.equal(loader.expired_, | ||
61 | 0, | ||
62 | 'zero seconds expired'); | ||
63 | }); | ||
64 | |||
65 | QUnit.test('requests the initial playlist immediately', function() { | ||
66 | /* eslint-disable no-unused-vars */ | ||
67 | let loader = new PlaylistLoader('master.m3u8'); | ||
68 | /* eslint-enable no-unused-vars */ | ||
69 | |||
70 | QUnit.strictEqual(this.requests.length, 1, 'made a request'); | ||
71 | QUnit.strictEqual(this.requests[0].url, | ||
72 | 'master.m3u8', | ||
73 | 'requested the initial playlist'); | ||
74 | }); | ||
75 | |||
76 | QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() { | ||
77 | let loader = new PlaylistLoader('master.m3u8'); | ||
78 | let state; | ||
79 | |||
80 | loader.on('loadedplaylist', function() { | ||
81 | state = loader.state; | ||
82 | }); | ||
83 | this.requests.pop().respond(200, null, | ||
84 | '#EXTM3U\n' + | ||
85 | '#EXT-X-STREAM-INF:\n' + | ||
86 | 'media.m3u8\n'); | ||
87 | QUnit.ok(loader.master, 'the master playlist is available'); | ||
88 | QUnit.strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct'); | ||
89 | }); | ||
90 | |||
91 | QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', function() { | ||
92 | let loadedmetadatas = 0; | ||
93 | let loader = new PlaylistLoader('media.m3u8'); | ||
94 | |||
95 | loader.on('loadedmetadata', function() { | ||
96 | loadedmetadatas++; | ||
97 | }); | ||
98 | this.requests.pop().respond(200, null, | ||
99 | '#EXTM3U\n' + | ||
100 | '#EXTINF:10,\n' + | ||
101 | '0.ts\n' + | ||
102 | '#EXT-X-ENDLIST\n'); | ||
103 | QUnit.ok(loader.master, 'infers a master playlist'); | ||
104 | QUnit.ok(loader.media(), 'sets the media playlist'); | ||
105 | QUnit.ok(loader.media().uri, 'sets the media playlist URI'); | ||
106 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
107 | QUnit.strictEqual(this.requests.length, 0, 'no more requests are made'); | ||
108 | QUnit.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata'); | ||
109 | }); | ||
110 | |||
111 | QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist', | ||
112 | function() { | ||
113 | let loader = new PlaylistLoader('media.m3u8'); | ||
114 | |||
115 | this.requests.pop().respond(200, null, | ||
116 | '#EXTM3U\n' + | ||
117 | '#EXTINF:10,\n' + | ||
118 | '0.ts\n'); | ||
119 | QUnit.ok(loader.master, 'infers a master playlist'); | ||
120 | QUnit.ok(loader.media(), 'sets the media playlist'); | ||
121 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
122 | }); | ||
123 | |||
124 | QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { | ||
125 | let loadedPlaylist = 0; | ||
126 | let loadedMetadata = 0; | ||
127 | let loader = new PlaylistLoader('master.m3u8'); | ||
128 | |||
129 | loader.on('loadedplaylist', function() { | ||
130 | loadedPlaylist++; | ||
131 | }); | ||
132 | loader.on('loadedmetadata', function() { | ||
133 | loadedMetadata++; | ||
134 | }); | ||
135 | this.requests.pop().respond(200, null, | ||
136 | '#EXTM3U\n' + | ||
137 | '#EXT-X-STREAM-INF:\n' + | ||
138 | 'media.m3u8\n' + | ||
139 | 'alt.m3u8\n'); | ||
140 | QUnit.strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once'); | ||
141 | QUnit.strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata'); | ||
142 | QUnit.strictEqual(this.requests.length, 1, 'requests the media playlist'); | ||
143 | QUnit.strictEqual(this.requests[0].method, 'GET', 'GETs the media playlist'); | ||
144 | QUnit.strictEqual(this.requests[0].url, | ||
145 | urlTo('media.m3u8'), | ||
146 | 'requests the first playlist'); | ||
147 | |||
148 | this.requests.pop().respond(200, null, | ||
149 | '#EXTM3U\n' + | ||
150 | '#EXTINF:10,\n' + | ||
151 | '0.ts\n'); | ||
152 | QUnit.ok(loader.master, 'sets the master playlist'); | ||
153 | QUnit.ok(loader.media(), 'sets the media playlist'); | ||
154 | QUnit.strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice'); | ||
155 | QUnit.strictEqual(loadedMetadata, 1, 'fired loadedmetadata once'); | ||
156 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
157 | }); | ||
158 | |||
159 | QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { | ||
160 | let loader = new PlaylistLoader('live.m3u8'); | ||
161 | |||
162 | this.requests.pop().respond(200, null, | ||
163 | '#EXTM3U\n' + | ||
164 | '#EXTINF:10,\n' + | ||
165 | '0.ts\n'); | ||
166 | // 10s, one target duration | ||
167 | this.clock.tick(10 * 1000); | ||
168 | QUnit.strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); | ||
169 | QUnit.strictEqual(this.requests.length, 1, 'requested playlist'); | ||
170 | QUnit.strictEqual(this.requests[0].url, | ||
171 | urlTo('live.m3u8'), | ||
172 | 'refreshes the media playlist'); | ||
173 | }); | ||
174 | |||
175 | QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() { | ||
176 | let loader = new PlaylistLoader('live.m3u8'); | ||
177 | |||
178 | this.requests.pop().respond(200, null, | ||
179 | '#EXTM3U\n' + | ||
180 | '#EXTINF:10,\n' + | ||
181 | '0.ts\n'); | ||
182 | // 10s, one target duration | ||
183 | this.clock.tick(10 * 1000); | ||
184 | this.requests.pop().respond(200, null, | ||
185 | '#EXTM3U\n' + | ||
186 | '#EXTINF:10,\n' + | ||
187 | '1.ts\n'); | ||
188 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
189 | }); | ||
190 | |||
191 | QUnit.test('does not increment expired seconds before firstplay is triggered', | ||
192 | function() { | ||
193 | let loader = new PlaylistLoader('live.m3u8'); | ||
194 | |||
195 | this.requests.pop().respond(200, null, | ||
196 | '#EXTM3U\n' + | ||
197 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
198 | '#EXTINF:10,\n' + | ||
199 | '0.ts\n' + | ||
200 | '#EXTINF:10,\n' + | ||
201 | '1.ts\n' + | ||
202 | '#EXTINF:10,\n' + | ||
203 | '2.ts\n' + | ||
204 | '#EXTINF:10,\n' + | ||
205 | '3.ts\n'); | ||
206 | // 10s, one target duration | ||
207 | this.clock.tick(10 * 1000); | ||
208 | this.requests.pop().respond(200, null, | ||
209 | '#EXTM3U\n' + | ||
210 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
211 | '#EXTINF:10,\n' + | ||
212 | '1.ts\n' + | ||
213 | '#EXTINF:10,\n' + | ||
214 | '2.ts\n' + | ||
215 | '#EXTINF:10,\n' + | ||
216 | '3.ts\n' + | ||
217 | '#EXTINF:10,\n' + | ||
218 | '4.ts\n'); | ||
219 | QUnit.equal(loader.expired_, 0, 'expired one segment'); | ||
220 | }); | ||
221 | |||
222 | QUnit.test('increments expired seconds after a segment is removed', function() { | ||
223 | let loader = new PlaylistLoader('live.m3u8'); | ||
224 | |||
225 | loader.trigger('firstplay'); | ||
226 | this.requests.pop().respond(200, null, | ||
227 | '#EXTM3U\n' + | ||
228 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
229 | '#EXTINF:10,\n' + | ||
230 | '0.ts\n' + | ||
231 | '#EXTINF:10,\n' + | ||
232 | '1.ts\n' + | ||
233 | '#EXTINF:10,\n' + | ||
234 | '2.ts\n' + | ||
235 | '#EXTINF:10,\n' + | ||
236 | '3.ts\n'); | ||
237 | // 10s, one target duration | ||
238 | this.clock.tick(10 * 1000); | ||
239 | this.requests.pop().respond(200, null, | ||
240 | '#EXTM3U\n' + | ||
241 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
242 | '#EXTINF:10,\n' + | ||
243 | '1.ts\n' + | ||
244 | '#EXTINF:10,\n' + | ||
245 | '2.ts\n' + | ||
246 | '#EXTINF:10,\n' + | ||
247 | '3.ts\n' + | ||
248 | '#EXTINF:10,\n' + | ||
249 | '4.ts\n'); | ||
250 | QUnit.equal(loader.expired_, 10, 'expired one segment'); | ||
251 | }); | ||
252 | |||
253 | QUnit.test('increments expired seconds after a discontinuity', function() { | ||
254 | let loader = new PlaylistLoader('live.m3u8'); | ||
255 | |||
256 | loader.trigger('firstplay'); | ||
257 | this.requests.pop().respond(200, null, | ||
258 | '#EXTM3U\n' + | ||
259 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
260 | '#EXTINF:10,\n' + | ||
261 | '0.ts\n' + | ||
262 | '#EXTINF:3,\n' + | ||
263 | '1.ts\n' + | ||
264 | '#EXT-X-DISCONTINUITY\n' + | ||
265 | '#EXTINF:4,\n' + | ||
266 | '2.ts\n'); | ||
267 | // 10s, one target duration | ||
268 | this.clock.tick(10 * 1000); | ||
269 | this.requests.pop().respond(200, null, | ||
270 | '#EXTM3U\n' + | ||
271 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
272 | '#EXTINF:3,\n' + | ||
273 | '1.ts\n' + | ||
274 | '#EXT-X-DISCONTINUITY\n' + | ||
275 | '#EXTINF:4,\n' + | ||
276 | '2.ts\n'); | ||
277 | QUnit.equal(loader.expired_, 10, 'expired one segment'); | ||
278 | |||
279 | // 10s, one target duration | ||
280 | this.clock.tick(10 * 1000); | ||
281 | this.requests.pop().respond(200, null, | ||
282 | '#EXTM3U\n' + | ||
283 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
284 | '#EXT-X-DISCONTINUITY\n' + | ||
285 | '#EXTINF:4,\n' + | ||
286 | '2.ts\n'); | ||
287 | QUnit.equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); | ||
288 | |||
289 | // 10s, one target duration | ||
290 | this.clock.tick(10 * 1000); | ||
291 | this.requests.pop().respond(200, null, | ||
292 | '#EXTM3U\n' + | ||
293 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
294 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + | ||
295 | '#EXTINF:10,\n' + | ||
296 | '3.ts\n'); | ||
297 | QUnit.equal(loader.expired_, 17, 'tracked expiration across the discontinuity'); | ||
298 | }); | ||
299 | |||
300 | QUnit.test('tracks expired seconds properly when two discontinuities expire at once', | ||
301 | function() { | ||
302 | let loader = new PlaylistLoader('live.m3u8'); | ||
303 | |||
304 | loader.trigger('firstplay'); | ||
305 | this.requests.pop().respond(200, null, | ||
306 | '#EXTM3U\n' + | ||
307 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
308 | '#EXTINF:4,\n' + | ||
309 | '0.ts\n' + | ||
310 | '#EXT-X-DISCONTINUITY\n' + | ||
311 | '#EXTINF:5,\n' + | ||
312 | '1.ts\n' + | ||
313 | '#EXT-X-DISCONTINUITY\n' + | ||
314 | '#EXTINF:6,\n' + | ||
315 | '2.ts\n' + | ||
316 | '#EXTINF:7,\n' + | ||
317 | '3.ts\n'); | ||
318 | this.clock.tick(10 * 1000); | ||
319 | this.requests.pop().respond(200, null, | ||
320 | '#EXTM3U\n' + | ||
321 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
322 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + | ||
323 | '#EXTINF:7,\n' + | ||
324 | '3.ts\n'); | ||
325 | QUnit.equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities'); | ||
326 | }); | ||
327 | |||
328 | QUnit.test('estimates expired if an entire window elapses between live playlist updates', | ||
329 | function() { | ||
330 | let loader = new PlaylistLoader('live.m3u8'); | ||
331 | |||
332 | loader.trigger('firstplay'); | ||
333 | this.requests.pop().respond(200, null, | ||
334 | '#EXTM3U\n' + | ||
335 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
336 | '#EXTINF:4,\n' + | ||
337 | '0.ts\n' + | ||
338 | '#EXTINF:5,\n' + | ||
339 | '1.ts\n'); | ||
340 | |||
341 | this.clock.tick(10 * 1000); | ||
342 | this.requests.pop().respond(200, null, | ||
343 | '#EXTM3U\n' + | ||
344 | '#EXT-X-MEDIA-SEQUENCE:4\n' + | ||
345 | '#EXTINF:6,\n' + | ||
346 | '4.ts\n' + | ||
347 | '#EXTINF:7,\n' + | ||
348 | '5.ts\n'); | ||
349 | |||
350 | QUnit.equal(loader.expired_, | ||
351 | 4 + 5 + (2 * 10), | ||
352 | 'made a very rough estimate of expired time'); | ||
353 | }); | ||
354 | |||
355 | QUnit.test('emits an error when an initial playlist request fails', function() { | ||
356 | let errors = []; | ||
357 | let loader = new PlaylistLoader('master.m3u8'); | ||
358 | |||
359 | loader.on('error', function() { | ||
360 | errors.push(loader.error); | ||
361 | }); | ||
362 | this.requests.pop().respond(500); | ||
363 | |||
364 | QUnit.strictEqual(errors.length, 1, 'emitted one error'); | ||
365 | QUnit.strictEqual(errors[0].status, 500, 'http status is captured'); | ||
366 | }); | ||
367 | |||
368 | QUnit.test('errors when an initial media playlist request fails', function() { | ||
369 | let errors = []; | ||
370 | let loader = new PlaylistLoader('master.m3u8'); | ||
371 | |||
372 | loader.on('error', function() { | ||
373 | errors.push(loader.error); | ||
374 | }); | ||
375 | this.requests.pop().respond(200, null, | ||
376 | '#EXTM3U\n' + | ||
377 | '#EXT-X-STREAM-INF:\n' + | ||
378 | 'media.m3u8\n'); | ||
379 | |||
380 | QUnit.strictEqual(errors.length, 0, 'emitted no errors'); | ||
381 | |||
382 | this.requests.pop().respond(500); | ||
383 | |||
384 | QUnit.strictEqual(errors.length, 1, 'emitted one error'); | ||
385 | QUnit.strictEqual(errors[0].status, 500, 'http status is captured'); | ||
386 | }); | ||
387 | |||
388 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | ||
389 | QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload', | ||
390 | function() { | ||
391 | /* eslint-disable no-unused-vars */ | ||
392 | let loader = new PlaylistLoader('live.m3u8'); | ||
393 | /* eslint-enable no-unused-vars */ | ||
394 | |||
395 | this.requests.pop().respond(200, null, | ||
396 | '#EXTM3U\n' + | ||
397 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
398 | '#EXTINF:10,\n' + | ||
399 | '0.ts\n'); | ||
400 | // trigger a refresh | ||
401 | this.clock.tick(10 * 1000); | ||
402 | this.requests.pop().respond(200, null, | ||
403 | '#EXTM3U\n' + | ||
404 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
405 | '#EXTINF:10,\n' + | ||
406 | '0.ts\n'); | ||
407 | // half the default target-duration | ||
408 | this.clock.tick(5 * 1000); | ||
409 | |||
410 | QUnit.strictEqual(this.requests.length, 1, 'sent a request'); | ||
411 | QUnit.strictEqual(this.requests[0].url, | ||
412 | urlTo('live.m3u8'), | ||
413 | 'requested the media playlist'); | ||
414 | }); | ||
415 | |||
416 | QUnit.test('preserves segment metadata across playlist refreshes', function() { | ||
417 | let loader = new PlaylistLoader('live.m3u8'); | ||
418 | let segment; | ||
419 | |||
420 | this.requests.pop().respond(200, null, | ||
421 | '#EXTM3U\n' + | ||
422 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
423 | '#EXTINF:10,\n' + | ||
424 | '0.ts\n' + | ||
425 | '#EXTINF:10,\n' + | ||
426 | '1.ts\n' + | ||
427 | '#EXTINF:10,\n' + | ||
428 | '2.ts\n'); | ||
429 | // add PTS info to 1.ts | ||
430 | segment = loader.media().segments[1]; | ||
431 | segment.minVideoPts = 14; | ||
432 | segment.maxAudioPts = 27; | ||
433 | segment.preciseDuration = 10.045; | ||
434 | |||
435 | // trigger a refresh | ||
436 | this.clock.tick(10 * 1000); | ||
437 | this.requests.pop().respond(200, null, | ||
438 | '#EXTM3U\n' + | ||
439 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
440 | '#EXTINF:10,\n' + | ||
441 | '1.ts\n' + | ||
442 | '#EXTINF:10,\n' + | ||
443 | '2.ts\n'); | ||
444 | |||
445 | QUnit.deepEqual(loader.media().segments[0], segment, 'preserved segment attributes'); | ||
446 | }); | ||
447 | |||
448 | QUnit.test('clears the update timeout when switching quality', function() { | ||
449 | let loader = new PlaylistLoader('live-master.m3u8'); | ||
450 | let refreshes = 0; | ||
451 | |||
452 | // track the number of playlist refreshes triggered | ||
453 | loader.on('mediaupdatetimeout', function() { | ||
454 | refreshes++; | ||
455 | }); | ||
456 | // deliver the master | ||
457 | this.requests.pop().respond(200, null, | ||
458 | '#EXTM3U\n' + | ||
459 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
460 | 'live-low.m3u8\n' + | ||
461 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
462 | 'live-high.m3u8\n'); | ||
463 | // deliver the low quality playlist | ||
464 | this.requests.pop().respond(200, null, | ||
465 | '#EXTM3U\n' + | ||
466 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
467 | '#EXTINF:10,\n' + | ||
468 | 'low-0.ts\n'); | ||
469 | // change to a higher quality playlist | ||
470 | loader.media('live-high.m3u8'); | ||
471 | this.requests.pop().respond(200, null, | ||
472 | '#EXTM3U\n' + | ||
473 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
474 | '#EXTINF:10,\n' + | ||
475 | 'high-0.ts\n'); | ||
476 | // trigger a refresh | ||
477 | this.clock.tick(10 * 1000); | ||
478 | |||
479 | QUnit.equal(1, refreshes, 'only one refresh was triggered'); | ||
480 | }); | ||
481 | |||
482 | QUnit.test('media-sequence updates are considered a playlist change', function() { | ||
483 | /* eslint-disable no-unused-vars */ | ||
484 | let loader = new PlaylistLoader('live.m3u8'); | ||
485 | /* eslint-enable no-unused-vars */ | ||
486 | |||
487 | this.requests.pop().respond(200, null, | ||
488 | '#EXTM3U\n' + | ||
489 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
490 | '#EXTINF:10,\n' + | ||
491 | '0.ts\n'); | ||
492 | // trigger a refresh | ||
493 | this.clock.tick(10 * 1000); | ||
494 | this.requests.pop().respond(200, null, | ||
495 | '#EXTM3U\n' + | ||
496 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
497 | '#EXTINF:10,\n' + | ||
498 | '0.ts\n'); | ||
499 | // half the default target-duration | ||
500 | this.clock.tick(5 * 1000); | ||
501 | |||
502 | QUnit.strictEqual(this.requests.length, 0, 'no request is sent'); | ||
503 | }); | ||
504 | |||
505 | QUnit.test('emits an error if a media refresh fails', function() { | ||
506 | let errors = 0; | ||
507 | let errorResponseText = 'custom error message'; | ||
508 | let loader = new PlaylistLoader('live.m3u8'); | ||
509 | |||
510 | loader.on('error', function() { | ||
511 | errors++; | ||
512 | }); | ||
513 | this.requests.pop().respond(200, null, | ||
514 | '#EXTM3U\n' + | ||
515 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
516 | '#EXTINF:10,\n' + | ||
517 | '0.ts\n'); | ||
518 | // trigger a refresh | ||
519 | this.clock.tick(10 * 1000); | ||
520 | this.requests.pop().respond(500, null, errorResponseText); | ||
521 | |||
522 | QUnit.strictEqual(errors, 1, 'emitted an error'); | ||
523 | QUnit.strictEqual(loader.error.status, 500, 'captured the status code'); | ||
524 | QUnit.strictEqual(loader.error.responseText, | ||
525 | errorResponseText, | ||
526 | 'captured the responseText'); | ||
527 | }); | ||
528 | |||
529 | QUnit.test('switches media playlists when requested', function() { | ||
530 | let loader = new PlaylistLoader('master.m3u8'); | ||
531 | |||
532 | this.requests.pop().respond(200, null, | ||
533 | '#EXTM3U\n' + | ||
534 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
535 | 'low.m3u8\n' + | ||
536 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
537 | 'high.m3u8\n'); | ||
538 | this.requests.pop().respond(200, null, | ||
539 | '#EXTM3U\n' + | ||
540 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
541 | '#EXTINF:10,\n' + | ||
542 | 'low-0.ts\n'); | ||
543 | |||
544 | loader.media(loader.master.playlists[1]); | ||
545 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
546 | |||
547 | this.requests.pop().respond(200, null, | ||
548 | '#EXTM3U\n' + | ||
549 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
550 | '#EXTINF:10,\n' + | ||
551 | 'high-0.ts\n'); | ||
552 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | ||
553 | QUnit.strictEqual(loader.media(), | ||
554 | loader.master.playlists[1], | ||
555 | 'updated the active media'); | ||
556 | }); | ||
557 | |||
558 | QUnit.test('can switch playlists immediately after the master is downloaded', function() { | ||
559 | let loader = new PlaylistLoader('master.m3u8'); | ||
560 | |||
561 | loader.on('loadedplaylist', function() { | ||
562 | loader.media('high.m3u8'); | ||
563 | }); | ||
564 | this.requests.pop().respond(200, null, | ||
565 | '#EXTM3U\n' + | ||
566 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
567 | 'low.m3u8\n' + | ||
568 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
569 | 'high.m3u8\n'); | ||
570 | QUnit.equal(this.requests[0].url, urlTo('high.m3u8'), 'switched variants immediately'); | ||
571 | }); | ||
572 | |||
573 | QUnit.test('can switch media playlists based on URI', function() { | ||
574 | let loader = new PlaylistLoader('master.m3u8'); | ||
575 | |||
576 | this.requests.pop().respond(200, null, | ||
577 | '#EXTM3U\n' + | ||
578 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
579 | 'low.m3u8\n' + | ||
580 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
581 | 'high.m3u8\n'); | ||
582 | this.requests.pop().respond(200, null, | ||
583 | '#EXTM3U\n' + | ||
584 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
585 | '#EXTINF:10,\n' + | ||
586 | 'low-0.ts\n'); | ||
587 | |||
588 | loader.media('high.m3u8'); | ||
589 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
590 | |||
591 | this.requests.pop().respond(200, null, | ||
592 | '#EXTM3U\n' + | ||
593 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
594 | '#EXTINF:10,\n' + | ||
595 | 'high-0.ts\n'); | ||
596 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | ||
597 | QUnit.strictEqual(loader.media(), | ||
598 | loader.master.playlists[1], | ||
599 | 'updated the active media'); | ||
600 | }); | ||
601 | |||
602 | QUnit.test('aborts in-flight playlist refreshes when switching', function() { | ||
603 | let loader = new PlaylistLoader('master.m3u8'); | ||
604 | |||
605 | this.requests.pop().respond(200, null, | ||
606 | '#EXTM3U\n' + | ||
607 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
608 | 'low.m3u8\n' + | ||
609 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
610 | 'high.m3u8\n'); | ||
611 | this.requests.pop().respond(200, null, | ||
612 | '#EXTM3U\n' + | ||
613 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
614 | '#EXTINF:10,\n' + | ||
615 | 'low-0.ts\n'); | ||
616 | this.clock.tick(10 * 1000); | ||
617 | loader.media('high.m3u8'); | ||
618 | QUnit.strictEqual(this.requests[0].aborted, true, 'aborted refresh request'); | ||
619 | QUnit.ok(!this.requests[0].onreadystatechange, | ||
620 | 'onreadystatechange handlers should be removed on abort'); | ||
621 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
622 | }); | ||
623 | |||
624 | QUnit.test('switching to the active playlist is a no-op', function() { | ||
625 | let loader = new PlaylistLoader('master.m3u8'); | ||
626 | |||
627 | this.requests.pop().respond(200, null, | ||
628 | '#EXTM3U\n' + | ||
629 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
630 | 'low.m3u8\n' + | ||
631 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
632 | 'high.m3u8\n'); | ||
633 | this.requests.pop().respond(200, null, | ||
634 | '#EXTM3U\n' + | ||
635 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
636 | '#EXTINF:10,\n' + | ||
637 | 'low-0.ts\n' + | ||
638 | '#EXT-X-ENDLIST\n'); | ||
639 | loader.media('low.m3u8'); | ||
640 | |||
641 | QUnit.strictEqual(this.requests.length, 0, 'no requests are sent'); | ||
642 | }); | ||
643 | |||
644 | QUnit.test('switching to the active live playlist is a no-op', function() { | ||
645 | let loader = new PlaylistLoader('master.m3u8'); | ||
646 | |||
647 | this.requests.pop().respond(200, null, | ||
648 | '#EXTM3U\n' + | ||
649 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
650 | 'low.m3u8\n' + | ||
651 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
652 | 'high.m3u8\n'); | ||
653 | this.requests.pop().respond(200, null, | ||
654 | '#EXTM3U\n' + | ||
655 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
656 | '#EXTINF:10,\n' + | ||
657 | 'low-0.ts\n'); | ||
658 | loader.media('low.m3u8'); | ||
659 | |||
660 | QUnit.strictEqual(this.requests.length, 0, 'no requests are sent'); | ||
661 | }); | ||
662 | |||
663 | QUnit.test('switches back to loaded playlists without re-requesting them', function() { | ||
664 | let loader = new PlaylistLoader('master.m3u8'); | ||
665 | |||
666 | this.requests.pop().respond(200, null, | ||
667 | '#EXTM3U\n' + | ||
668 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
669 | 'low.m3u8\n' + | ||
670 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
671 | 'high.m3u8\n'); | ||
672 | this.requests.pop().respond(200, null, | ||
673 | '#EXTM3U\n' + | ||
674 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
675 | '#EXTINF:10,\n' + | ||
676 | 'low-0.ts\n' + | ||
677 | '#EXT-X-ENDLIST\n'); | ||
678 | loader.media('high.m3u8'); | ||
679 | this.requests.pop().respond(200, null, | ||
680 | '#EXTM3U\n' + | ||
681 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
682 | '#EXTINF:10,\n' + | ||
683 | 'high-0.ts\n' + | ||
684 | '#EXT-X-ENDLIST\n'); | ||
685 | loader.media('low.m3u8'); | ||
686 | |||
687 | QUnit.strictEqual(this.requests.length, 0, 'no outstanding requests'); | ||
688 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist'); | ||
689 | }); | ||
690 | |||
691 | QUnit.test('aborts outstanding requests if switching back to an already loaded playlist', | ||
692 | function() { | ||
693 | let loader = new PlaylistLoader('master.m3u8'); | ||
694 | |||
695 | this.requests.pop().respond(200, null, | ||
696 | '#EXTM3U\n' + | ||
697 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
698 | 'low.m3u8\n' + | ||
699 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
700 | 'high.m3u8\n'); | ||
701 | this.requests.pop().respond(200, null, | ||
702 | '#EXTM3U\n' + | ||
703 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
704 | '#EXTINF:10,\n' + | ||
705 | 'low-0.ts\n' + | ||
706 | '#EXT-X-ENDLIST\n'); | ||
707 | loader.media('high.m3u8'); | ||
708 | loader.media('low.m3u8'); | ||
709 | |||
710 | QUnit.strictEqual(this.requests.length, | ||
711 | 1, | ||
712 | 'requested high playlist'); | ||
713 | QUnit.ok(this.requests[0].aborted, | ||
714 | 'aborted playlist request'); | ||
715 | QUnit.ok(!this.requests[0].onreadystatechange, | ||
716 | 'onreadystatechange handlers should be removed on abort'); | ||
717 | QUnit.strictEqual(loader.state, | ||
718 | 'HAVE_METADATA', | ||
719 | 'returned to loaded playlist'); | ||
720 | QUnit.strictEqual(loader.media(), | ||
721 | loader.master.playlists[0], | ||
722 | 'switched to loaded playlist'); | ||
723 | }); | ||
724 | |||
725 | QUnit.test('does not abort requests when the same playlist is re-requested', | ||
726 | function() { | ||
727 | let loader = new PlaylistLoader('master.m3u8'); | ||
728 | |||
729 | this.requests.pop().respond(200, null, | ||
730 | '#EXTM3U\n' + | ||
731 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
732 | 'low.m3u8\n' + | ||
733 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
734 | 'high.m3u8\n'); | ||
735 | this.requests.pop().respond(200, null, | ||
736 | '#EXTM3U\n' + | ||
737 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
738 | '#EXTINF:10,\n' + | ||
739 | 'low-0.ts\n' + | ||
740 | '#EXT-X-ENDLIST\n'); | ||
741 | loader.media('high.m3u8'); | ||
742 | loader.media('high.m3u8'); | ||
743 | |||
744 | QUnit.strictEqual(this.requests.length, 1, 'made only one request'); | ||
745 | QUnit.ok(!this.requests[0].aborted, 'request not aborted'); | ||
746 | }); | ||
747 | |||
748 | QUnit.test('throws an error if a media switch is initiated too early', function() { | ||
749 | let loader = new PlaylistLoader('master.m3u8'); | ||
750 | |||
751 | QUnit.throws(function() { | ||
752 | loader.media('high.m3u8'); | ||
753 | }, 'threw an error from HAVE_NOTHING'); | ||
754 | |||
755 | this.requests.pop().respond(200, null, | ||
756 | '#EXTM3U\n' + | ||
757 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
758 | 'low.m3u8\n' + | ||
759 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
760 | 'high.m3u8\n'); | ||
761 | }); | ||
762 | |||
763 | QUnit.test('throws an error if a switch to an unrecognized playlist is requested', | ||
764 | function() { | ||
765 | let loader = new PlaylistLoader('master.m3u8'); | ||
766 | |||
767 | this.requests.pop().respond(200, null, | ||
768 | '#EXTM3U\n' + | ||
769 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
770 | 'media.m3u8\n'); | ||
771 | |||
772 | QUnit.throws(function() { | ||
773 | loader.media('unrecognized.m3u8'); | ||
774 | }, 'throws an error'); | ||
775 | }); | ||
776 | |||
777 | QUnit.test('dispose cancels the refresh timeout', function() { | ||
778 | let loader = new PlaylistLoader('live.m3u8'); | ||
779 | |||
780 | this.requests.pop().respond(200, null, | ||
781 | '#EXTM3U\n' + | ||
782 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
783 | '#EXTINF:10,\n' + | ||
784 | '0.ts\n'); | ||
785 | loader.dispose(); | ||
786 | // a lot of time passes... | ||
787 | this.clock.tick(15 * 1000); | ||
788 | |||
789 | QUnit.strictEqual(this.requests.length, 0, 'no refresh request was made'); | ||
790 | }); | ||
791 | |||
792 | QUnit.test('dispose aborts pending refresh requests', function() { | ||
793 | let loader = new PlaylistLoader('live.m3u8'); | ||
794 | |||
795 | this.requests.pop().respond(200, null, | ||
796 | '#EXTM3U\n' + | ||
797 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
798 | '#EXTINF:10,\n' + | ||
799 | '0.ts\n'); | ||
800 | this.clock.tick(10 * 1000); | ||
801 | |||
802 | loader.dispose(); | ||
803 | QUnit.ok(this.requests[0].aborted, 'refresh request aborted'); | ||
804 | QUnit.ok(!this.requests[0].onreadystatechange, | ||
805 | 'onreadystatechange handler should not exist after dispose called' | ||
806 | ); | ||
807 | }); | ||
808 | |||
809 | QUnit.test('errors if requests take longer than 45s', function() { | ||
810 | let loader = new PlaylistLoader('media.m3u8'); | ||
811 | let errors = 0; | ||
812 | |||
813 | loader.on('error', function() { | ||
814 | errors++; | ||
815 | }); | ||
816 | this.clock.tick(45 * 1000); | ||
817 | |||
818 | QUnit.strictEqual(errors, 1, 'fired one error'); | ||
819 | QUnit.strictEqual(loader.error.code, 2, 'fired a network error'); | ||
820 | }); | ||
821 | |||
822 | QUnit.test('triggers an event when the active media changes', function() { | ||
823 | let loader = new PlaylistLoader('master.m3u8'); | ||
824 | let mediaChanges = 0; | ||
825 | |||
826 | loader.on('mediachange', function() { | ||
827 | mediaChanges++; | ||
828 | }); | ||
829 | this.requests.pop().respond(200, null, | ||
830 | '#EXTM3U\n' + | ||
831 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
832 | 'low.m3u8\n' + | ||
833 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
834 | 'high.m3u8\n'); | ||
835 | this.requests.shift().respond(200, null, | ||
836 | '#EXTM3U\n' + | ||
837 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
838 | '#EXTINF:10,\n' + | ||
839 | 'low-0.ts\n' + | ||
840 | '#EXT-X-ENDLIST\n'); | ||
841 | QUnit.strictEqual(mediaChanges, 0, 'initial selection is not a media change'); | ||
842 | |||
843 | loader.media('high.m3u8'); | ||
844 | QUnit.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately'); | ||
845 | |||
846 | this.requests.shift().respond(200, null, | ||
847 | '#EXTM3U\n' + | ||
848 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
849 | '#EXTINF:10,\n' + | ||
850 | 'high-0.ts\n' + | ||
851 | '#EXT-X-ENDLIST\n'); | ||
852 | QUnit.strictEqual(mediaChanges, 1, 'fired a mediachange'); | ||
853 | |||
854 | // switch back to an already loaded playlist | ||
855 | loader.media('low.m3u8'); | ||
856 | QUnit.strictEqual(mediaChanges, 2, 'fired a mediachange'); | ||
857 | |||
858 | // trigger a no-op switch | ||
859 | loader.media('low.m3u8'); | ||
860 | QUnit.strictEqual(mediaChanges, 2, 'ignored a no-op media change'); | ||
861 | }); | ||
862 | |||
863 | QUnit.test('can get media index by playback position for non-live videos', function() { | ||
864 | let loader = new PlaylistLoader('media.m3u8'); | ||
865 | |||
866 | this.requests.shift().respond(200, null, | ||
867 | '#EXTM3U\n' + | ||
868 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
869 | '#EXTINF:4,\n' + | ||
870 | '0.ts\n' + | ||
871 | '#EXTINF:5,\n' + | ||
872 | '1.ts\n' + | ||
873 | '#EXTINF:6,\n' + | ||
874 | '2.ts\n' + | ||
875 | '#EXT-X-ENDLIST\n'); | ||
876 | |||
877 | QUnit.equal(loader.getMediaIndexForTime_(-1), | ||
878 | 0, | ||
879 | 'the index is never less than zero'); | ||
880 | QUnit.equal(loader.getMediaIndexForTime_(0), 0, 'time zero is index zero'); | ||
881 | QUnit.equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); | ||
882 | QUnit.equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); | ||
883 | QUnit.equal(loader.getMediaIndexForTime_(22), | ||
884 | 2, | ||
885 | 'time greater than the length is index 2'); | ||
886 | }); | ||
887 | |||
888 | QUnit.test('returns the lower index when calculating for a segment boundary', function() { | ||
889 | let loader = new PlaylistLoader('media.m3u8'); | ||
890 | |||
891 | this.requests.shift().respond(200, null, | ||
892 | '#EXTM3U\n' + | ||
893 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
894 | '#EXTINF:4,\n' + | ||
895 | '0.ts\n' + | ||
896 | '#EXTINF:5,\n' + | ||
897 | '1.ts\n' + | ||
898 | '#EXT-X-ENDLIST\n'); | ||
899 | QUnit.equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches'); | ||
900 | QUnit.equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); | ||
901 | QUnit.equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); | ||
902 | }); | ||
903 | |||
904 | QUnit.test('accounts for non-zero starting segment time when calculating media index', | ||
905 | function() { | ||
906 | let loader = new PlaylistLoader('media.m3u8'); | ||
907 | |||
908 | this.requests.shift().respond(200, null, | ||
909 | '#EXTM3U\n' + | ||
910 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | ||
911 | '#EXTINF:4,\n' + | ||
912 | '1001.ts\n' + | ||
913 | '#EXTINF:5,\n' + | ||
914 | '1002.ts\n'); | ||
915 | loader.media().segments[0].end = 154; | ||
916 | |||
917 | QUnit.equal(loader.getMediaIndexForTime_(0), | ||
918 | -1, | ||
919 | 'the lowest returned value is negative one'); | ||
920 | QUnit.equal(loader.getMediaIndexForTime_(45), | ||
921 | -1, | ||
922 | 'expired content returns negative one'); | ||
923 | QUnit.equal(loader.getMediaIndexForTime_(75), | ||
924 | -1, | ||
925 | 'expired content returns negative one'); | ||
926 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100), | ||
927 | 0, | ||
928 | 'calculates the earliest available position'); | ||
929 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | ||
930 | 0, | ||
931 | 'calculates within the first segment'); | ||
932 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4), | ||
933 | 1, | ||
934 | 'calculates within the second segment'); | ||
935 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), | ||
936 | 1, | ||
937 | 'calculates within the second segment'); | ||
938 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 6), | ||
939 | 1, | ||
940 | 'calculates within the second segment'); | ||
941 | }); | ||
942 | |||
943 | QUnit.test('prefers precise segment timing when tracking expired time', function() { | ||
944 | let loader = new PlaylistLoader('media.m3u8'); | ||
945 | |||
946 | loader.trigger('firstplay'); | ||
947 | this.requests.shift().respond(200, null, | ||
948 | '#EXTM3U\n' + | ||
949 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | ||
950 | '#EXTINF:4,\n' + | ||
951 | '1001.ts\n' + | ||
952 | '#EXTINF:5,\n' + | ||
953 | '1002.ts\n'); | ||
954 | // setup the loader with an "imprecise" value as if it had been | ||
955 | // accumulating segment durations as they expire | ||
956 | loader.expired_ = 160; | ||
957 | // annotate the first segment with a start time | ||
958 | // this number would be coming from the Source Buffer in practice | ||
959 | loader.media().segments[0].end = 150; | ||
960 | |||
961 | QUnit.equal(loader.getMediaIndexForTime_(149), | ||
962 | 0, | ||
963 | 'prefers the value on the first segment'); | ||
964 | |||
965 | // trigger a playlist refresh | ||
966 | this.clock.tick(10 * 1000); | ||
967 | this.requests.shift().respond(200, null, | ||
968 | '#EXTM3U\n' + | ||
969 | '#EXT-X-MEDIA-SEQUENCE:1002\n' + | ||
970 | '#EXTINF:5,\n' + | ||
971 | '1002.ts\n'); | ||
972 | QUnit.equal(loader.getMediaIndexForTime_(150 + 4 + 1), | ||
973 | 0, | ||
974 | 'tracks precise expired times'); | ||
975 | }); | ||
976 | |||
977 | QUnit.test('accounts for expired time when calculating media index', function() { | ||
978 | let loader = new PlaylistLoader('media.m3u8'); | ||
979 | |||
980 | this.requests.shift().respond(200, null, | ||
981 | '#EXTM3U\n' + | ||
982 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | ||
983 | '#EXTINF:4,\n' + | ||
984 | '1001.ts\n' + | ||
985 | '#EXTINF:5,\n' + | ||
986 | '1002.ts\n'); | ||
987 | loader.expired_ = 150; | ||
988 | |||
989 | QUnit.equal(loader.getMediaIndexForTime_(0), | ||
990 | -1, | ||
991 | 'expired content returns a negative index'); | ||
992 | QUnit.equal(loader.getMediaIndexForTime_(75), | ||
993 | -1, | ||
994 | 'expired content returns a negative index'); | ||
995 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100), | ||
996 | 0, | ||
997 | 'calculates the earliest available position'); | ||
998 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | ||
999 | 0, | ||
1000 | 'calculates within the first segment'); | ||
1001 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), | ||
1002 | 1, | ||
1003 | 'calculates within the second segment'); | ||
1004 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 6), | ||
1005 | 1, | ||
1006 | 'calculates within the second segment'); | ||
1007 | }); | ||
1008 | |||
1009 | QUnit.test('does not misintrepret playlists missing newlines at the end', function() { | ||
1010 | let loader = new PlaylistLoader('media.m3u8'); | ||
1011 | |||
1012 | // no newline | ||
1013 | this.requests.shift().respond(200, null, | ||
1014 | '#EXTM3U\n' + | ||
1015 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
1016 | '#EXTINF:10,\n' + | ||
1017 | 'low-0.ts\n' + | ||
1018 | '#EXT-X-ENDLIST'); | ||
1019 | QUnit.ok(loader.media().endList, 'flushed the final line of input'); | ||
1020 | }); |
test/playlist-loader_test.js
deleted
100644 → 0
1 | (function(window) { | ||
2 | 'use strict'; | ||
3 | var | ||
4 | sinonXhr, | ||
5 | clock, | ||
6 | requests, | ||
7 | videojs = window.videojs, | ||
8 | |||
9 | // Attempts to produce an absolute URL to a given relative path | ||
10 | // based on window.location.href | ||
11 | urlTo = function(path) { | ||
12 | return window.location.href | ||
13 | .split('/') | ||
14 | .slice(0, -1) | ||
15 | .concat([path]) | ||
16 | .join('/'); | ||
17 | }; | ||
18 | |||
19 | module('Playlist Loader', { | ||
20 | setup: function() { | ||
21 | // fake XHRs | ||
22 | sinonXhr = sinon.useFakeXMLHttpRequest(); | ||
23 | videojs.xhr.XMLHttpRequest = sinonXhr; | ||
24 | |||
25 | requests = []; | ||
26 | sinonXhr.onCreate = function(xhr) { | ||
27 | // force the XHR2 timeout polyfill | ||
28 | xhr.timeout = undefined; | ||
29 | requests.push(xhr); | ||
30 | }; | ||
31 | |||
32 | // fake timers | ||
33 | clock = sinon.useFakeTimers(); | ||
34 | }, | ||
35 | teardown: function() { | ||
36 | sinonXhr.restore(); | ||
37 | videojs.xhr.XMLHttpRequest = window.XMLHttpRequest; | ||
38 | clock.restore(); | ||
39 | } | ||
40 | }); | ||
41 | |||
42 | test('throws if the playlist url is empty or undefined', function() { | ||
43 | throws(function() { | ||
44 | videojs.Hls.PlaylistLoader(); | ||
45 | }, 'requires an argument'); | ||
46 | throws(function() { | ||
47 | videojs.Hls.PlaylistLoader(''); | ||
48 | }, 'does not accept the empty string'); | ||
49 | }); | ||
50 | |||
51 | test('starts without any metadata', function() { | ||
52 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
53 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); | ||
54 | }); | ||
55 | |||
56 | test('starts with no expired time', function() { | ||
57 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
58 | requests.pop().respond(200, null, | ||
59 | '#EXTM3U\n' + | ||
60 | '#EXTINF:10,\n' + | ||
61 | '0.ts\n'); | ||
62 | equal(loader.expired_, | ||
63 | 0, | ||
64 | 'zero seconds expired'); | ||
65 | }); | ||
66 | |||
67 | test('requests the initial playlist immediately', function() { | ||
68 | new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
69 | strictEqual(requests.length, 1, 'made a request'); | ||
70 | strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist'); | ||
71 | }); | ||
72 | |||
73 | test('moves to HAVE_MASTER after loading a master playlist', function() { | ||
74 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'), state; | ||
75 | loader.on('loadedplaylist', function() { | ||
76 | state = loader.state; | ||
77 | }); | ||
78 | requests.pop().respond(200, null, | ||
79 | '#EXTM3U\n' + | ||
80 | '#EXT-X-STREAM-INF:\n' + | ||
81 | 'media.m3u8\n'); | ||
82 | ok(loader.master, 'the master playlist is available'); | ||
83 | strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct'); | ||
84 | }); | ||
85 | |||
86 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { | ||
87 | var | ||
88 | loadedmetadatas = 0, | ||
89 | loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
90 | loader.on('loadedmetadata', function() { | ||
91 | loadedmetadatas++; | ||
92 | }); | ||
93 | requests.pop().respond(200, null, | ||
94 | '#EXTM3U\n' + | ||
95 | '#EXTINF:10,\n' + | ||
96 | '0.ts\n' + | ||
97 | '#EXT-X-ENDLIST\n'); | ||
98 | ok(loader.master, 'infers a master playlist'); | ||
99 | ok(loader.media(), 'sets the media playlist'); | ||
100 | ok(loader.media().uri, 'sets the media playlist URI'); | ||
101 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
102 | strictEqual(requests.length, 0, 'no more requests are made'); | ||
103 | strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata'); | ||
104 | }); | ||
105 | |||
106 | test('jumps to HAVE_METADATA when initialized with a live media playlist', function() { | ||
107 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
108 | requests.pop().respond(200, null, | ||
109 | '#EXTM3U\n' + | ||
110 | '#EXTINF:10,\n' + | ||
111 | '0.ts\n'); | ||
112 | ok(loader.master, 'infers a master playlist'); | ||
113 | ok(loader.media(), 'sets the media playlist'); | ||
114 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
115 | }); | ||
116 | |||
117 | test('moves to HAVE_METADATA after loading a media playlist', function() { | ||
118 | var | ||
119 | loadedPlaylist = 0, | ||
120 | loadedMetadata = 0, | ||
121 | loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
122 | loader.on('loadedplaylist', function() { | ||
123 | loadedPlaylist++; | ||
124 | }); | ||
125 | loader.on('loadedmetadata', function() { | ||
126 | loadedMetadata++; | ||
127 | }); | ||
128 | requests.pop().respond(200, null, | ||
129 | '#EXTM3U\n' + | ||
130 | '#EXT-X-STREAM-INF:\n' + | ||
131 | 'media.m3u8\n' + | ||
132 | 'alt.m3u8\n'); | ||
133 | strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once'); | ||
134 | strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata'); | ||
135 | strictEqual(requests.length, 1, 'requests the media playlist'); | ||
136 | strictEqual(requests[0].method, 'GET', 'GETs the media playlist'); | ||
137 | strictEqual(requests[0].url, | ||
138 | urlTo('media.m3u8'), | ||
139 | 'requests the first playlist'); | ||
140 | |||
141 | requests.pop().respond(200, null, | ||
142 | '#EXTM3U\n' + | ||
143 | '#EXTINF:10,\n' + | ||
144 | '0.ts\n'); | ||
145 | ok(loader.master, 'sets the master playlist'); | ||
146 | ok(loader.media(), 'sets the media playlist'); | ||
147 | strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice'); | ||
148 | strictEqual(loadedMetadata, 1, 'fired loadedmetadata once'); | ||
149 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
150 | }); | ||
151 | |||
152 | test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { | ||
153 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
154 | requests.pop().respond(200, null, | ||
155 | '#EXTM3U\n' + | ||
156 | '#EXTINF:10,\n' + | ||
157 | '0.ts\n'); | ||
158 | clock.tick(10 * 1000); // 10s, one target duration | ||
159 | strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); | ||
160 | strictEqual(requests.length, 1, 'requested playlist'); | ||
161 | strictEqual(requests[0].url, | ||
162 | urlTo('live.m3u8'), | ||
163 | 'refreshes the media playlist'); | ||
164 | }); | ||
165 | |||
166 | test('returns to HAVE_METADATA after refreshing the playlist', function() { | ||
167 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
168 | requests.pop().respond(200, null, | ||
169 | '#EXTM3U\n' + | ||
170 | '#EXTINF:10,\n' + | ||
171 | '0.ts\n'); | ||
172 | clock.tick(10 * 1000); // 10s, one target duration | ||
173 | requests.pop().respond(200, null, | ||
174 | '#EXTM3U\n' + | ||
175 | '#EXTINF:10,\n' + | ||
176 | '1.ts\n'); | ||
177 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
178 | }); | ||
179 | |||
180 | test('does not increment expired seconds before firstplay is triggered', function() { | ||
181 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
182 | requests.pop().respond(200, null, | ||
183 | '#EXTM3U\n' + | ||
184 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
185 | '#EXTINF:10,\n' + | ||
186 | '0.ts\n' + | ||
187 | '#EXTINF:10,\n' + | ||
188 | '1.ts\n' + | ||
189 | '#EXTINF:10,\n' + | ||
190 | '2.ts\n' + | ||
191 | '#EXTINF:10,\n' + | ||
192 | '3.ts\n'); | ||
193 | clock.tick(10 * 1000); // 10s, one target duration | ||
194 | requests.pop().respond(200, null, | ||
195 | '#EXTM3U\n' + | ||
196 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
197 | '#EXTINF:10,\n' + | ||
198 | '1.ts\n' + | ||
199 | '#EXTINF:10,\n' + | ||
200 | '2.ts\n' + | ||
201 | '#EXTINF:10,\n' + | ||
202 | '3.ts\n' + | ||
203 | '#EXTINF:10,\n' + | ||
204 | '4.ts\n'); | ||
205 | equal(loader.expired_, 0, 'expired one segment'); | ||
206 | }); | ||
207 | |||
208 | test('increments expired seconds after a segment is removed', function() { | ||
209 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
210 | loader.trigger('firstplay'); | ||
211 | requests.pop().respond(200, null, | ||
212 | '#EXTM3U\n' + | ||
213 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
214 | '#EXTINF:10,\n' + | ||
215 | '0.ts\n' + | ||
216 | '#EXTINF:10,\n' + | ||
217 | '1.ts\n' + | ||
218 | '#EXTINF:10,\n' + | ||
219 | '2.ts\n' + | ||
220 | '#EXTINF:10,\n' + | ||
221 | '3.ts\n'); | ||
222 | clock.tick(10 * 1000); // 10s, one target duration | ||
223 | requests.pop().respond(200, null, | ||
224 | '#EXTM3U\n' + | ||
225 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
226 | '#EXTINF:10,\n' + | ||
227 | '1.ts\n' + | ||
228 | '#EXTINF:10,\n' + | ||
229 | '2.ts\n' + | ||
230 | '#EXTINF:10,\n' + | ||
231 | '3.ts\n' + | ||
232 | '#EXTINF:10,\n' + | ||
233 | '4.ts\n'); | ||
234 | equal(loader.expired_, 10, 'expired one segment'); | ||
235 | }); | ||
236 | |||
237 | test('increments expired seconds after a discontinuity', function() { | ||
238 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
239 | loader.trigger('firstplay'); | ||
240 | requests.pop().respond(200, null, | ||
241 | '#EXTM3U\n' + | ||
242 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
243 | '#EXTINF:10,\n' + | ||
244 | '0.ts\n' + | ||
245 | '#EXTINF:3,\n' + | ||
246 | '1.ts\n' + | ||
247 | '#EXT-X-DISCONTINUITY\n' + | ||
248 | '#EXTINF:4,\n' + | ||
249 | '2.ts\n'); | ||
250 | clock.tick(10 * 1000); // 10s, one target duration | ||
251 | requests.pop().respond(200, null, | ||
252 | '#EXTM3U\n' + | ||
253 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
254 | '#EXTINF:3,\n' + | ||
255 | '1.ts\n' + | ||
256 | '#EXT-X-DISCONTINUITY\n' + | ||
257 | '#EXTINF:4,\n' + | ||
258 | '2.ts\n'); | ||
259 | equal(loader.expired_, 10, 'expired one segment'); | ||
260 | |||
261 | clock.tick(10 * 1000); // 10s, one target duration | ||
262 | requests.pop().respond(200, null, | ||
263 | '#EXTM3U\n' + | ||
264 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
265 | '#EXT-X-DISCONTINUITY\n' + | ||
266 | '#EXTINF:4,\n' + | ||
267 | '2.ts\n'); | ||
268 | equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); | ||
269 | |||
270 | clock.tick(10 * 1000); // 10s, one target duration | ||
271 | requests.pop().respond(200, null, | ||
272 | '#EXTM3U\n' + | ||
273 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
274 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + | ||
275 | '#EXTINF:10,\n' + | ||
276 | '3.ts\n'); | ||
277 | equal(loader.expired_, 17, 'tracked expiration across the discontinuity'); | ||
278 | }); | ||
279 | |||
280 | test('tracks expired seconds properly when two discontinuities expire at once', function() { | ||
281 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
282 | loader.trigger('firstplay'); | ||
283 | requests.pop().respond(200, null, | ||
284 | '#EXTM3U\n' + | ||
285 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
286 | '#EXTINF:4,\n' + | ||
287 | '0.ts\n' + | ||
288 | '#EXT-X-DISCONTINUITY\n' + | ||
289 | '#EXTINF:5,\n' + | ||
290 | '1.ts\n' + | ||
291 | '#EXT-X-DISCONTINUITY\n' + | ||
292 | '#EXTINF:6,\n' + | ||
293 | '2.ts\n' + | ||
294 | '#EXTINF:7,\n' + | ||
295 | '3.ts\n'); | ||
296 | clock.tick(10 * 1000); | ||
297 | requests.pop().respond(200, null, | ||
298 | '#EXTM3U\n' + | ||
299 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
300 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + | ||
301 | '#EXTINF:7,\n' + | ||
302 | '3.ts\n'); | ||
303 | equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities'); | ||
304 | }); | ||
305 | |||
306 | test('estimates expired if an entire window elapses between live playlist updates', function() { | ||
307 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
308 | loader.trigger('firstplay'); | ||
309 | requests.pop().respond(200, null, | ||
310 | '#EXTM3U\n' + | ||
311 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
312 | '#EXTINF:4,\n' + | ||
313 | '0.ts\n' + | ||
314 | '#EXTINF:5,\n' + | ||
315 | '1.ts\n'); | ||
316 | |||
317 | clock.tick(10 * 1000); | ||
318 | requests.pop().respond(200, null, | ||
319 | '#EXTM3U\n' + | ||
320 | '#EXT-X-MEDIA-SEQUENCE:4\n' + | ||
321 | '#EXTINF:6,\n' + | ||
322 | '4.ts\n' + | ||
323 | '#EXTINF:7,\n' + | ||
324 | '5.ts\n'); | ||
325 | |||
326 | equal(loader.expired_, | ||
327 | 4 + 5 + (2 * 10), | ||
328 | 'made a very rough estimate of expired time'); | ||
329 | }); | ||
330 | |||
331 | test('emits an error when an initial playlist request fails', function() { | ||
332 | var | ||
333 | errors = [], | ||
334 | loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
335 | |||
336 | loader.on('error', function() { | ||
337 | errors.push(loader.error); | ||
338 | }); | ||
339 | requests.pop().respond(500); | ||
340 | |||
341 | strictEqual(errors.length, 1, 'emitted one error'); | ||
342 | strictEqual(errors[0].status, 500, 'http status is captured'); | ||
343 | }); | ||
344 | |||
345 | test('errors when an initial media playlist request fails', function() { | ||
346 | var | ||
347 | errors = [], | ||
348 | loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
349 | |||
350 | loader.on('error', function() { | ||
351 | errors.push(loader.error); | ||
352 | }); | ||
353 | requests.pop().respond(200, null, | ||
354 | '#EXTM3U\n' + | ||
355 | '#EXT-X-STREAM-INF:\n' + | ||
356 | 'media.m3u8\n'); | ||
357 | |||
358 | strictEqual(errors.length, 0, 'emitted no errors'); | ||
359 | |||
360 | requests.pop().respond(500); | ||
361 | |||
362 | strictEqual(errors.length, 1, 'emitted one error'); | ||
363 | strictEqual(errors[0].status, 500, 'http status is captured'); | ||
364 | }); | ||
365 | |||
366 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | ||
367 | test('halves the refresh timeout if a playlist is unchanged' + | ||
368 | 'since the last reload', function() { | ||
369 | new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
370 | requests.pop().respond(200, null, | ||
371 | '#EXTM3U\n' + | ||
372 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
373 | '#EXTINF:10,\n' + | ||
374 | '0.ts\n'); | ||
375 | clock.tick(10 * 1000); // trigger a refresh | ||
376 | requests.pop().respond(200, null, | ||
377 | '#EXTM3U\n' + | ||
378 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
379 | '#EXTINF:10,\n' + | ||
380 | '0.ts\n'); | ||
381 | clock.tick(5 * 1000); // half the default target-duration | ||
382 | |||
383 | strictEqual(requests.length, 1, 'sent a request'); | ||
384 | strictEqual(requests[0].url, | ||
385 | urlTo('live.m3u8'), | ||
386 | 'requested the media playlist'); | ||
387 | }); | ||
388 | |||
389 | test('preserves segment metadata across playlist refreshes', function() { | ||
390 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'), segment; | ||
391 | requests.pop().respond(200, null, | ||
392 | '#EXTM3U\n' + | ||
393 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
394 | '#EXTINF:10,\n' + | ||
395 | '0.ts\n' + | ||
396 | '#EXTINF:10,\n' + | ||
397 | '1.ts\n' + | ||
398 | '#EXTINF:10,\n' + | ||
399 | '2.ts\n'); | ||
400 | // add PTS info to 1.ts | ||
401 | segment = loader.media().segments[1]; | ||
402 | segment.minVideoPts = 14; | ||
403 | segment.maxAudioPts = 27; | ||
404 | segment.preciseDuration = 10.045; | ||
405 | |||
406 | clock.tick(10 * 1000); // trigger a refresh | ||
407 | requests.pop().respond(200, null, | ||
408 | '#EXTM3U\n' + | ||
409 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
410 | '#EXTINF:10,\n' + | ||
411 | '1.ts\n' + | ||
412 | '#EXTINF:10,\n' + | ||
413 | '2.ts\n'); | ||
414 | |||
415 | deepEqual(loader.media().segments[0], segment, 'preserved segment attributes'); | ||
416 | }); | ||
417 | |||
418 | test('clears the update timeout when switching quality', function() { | ||
419 | var loader = new videojs.Hls.PlaylistLoader('live-master.m3u8'), refreshes = 0; | ||
420 | // track the number of playlist refreshes triggered | ||
421 | loader.on('mediaupdatetimeout', function() { | ||
422 | refreshes++; | ||
423 | }); | ||
424 | // deliver the master | ||
425 | requests.pop().respond(200, null, | ||
426 | '#EXTM3U\n' + | ||
427 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
428 | 'live-low.m3u8\n' + | ||
429 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
430 | 'live-high.m3u8\n'); | ||
431 | // deliver the low quality playlist | ||
432 | requests.pop().respond(200, null, | ||
433 | '#EXTM3U\n' + | ||
434 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
435 | '#EXTINF:10,\n' + | ||
436 | 'low-0.ts\n'); | ||
437 | // change to a higher quality playlist | ||
438 | loader.media('live-high.m3u8'); | ||
439 | requests.pop().respond(200, null, | ||
440 | '#EXTM3U\n' + | ||
441 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
442 | '#EXTINF:10,\n' + | ||
443 | 'high-0.ts\n'); | ||
444 | clock.tick(10 * 1000); // trigger a refresh | ||
445 | |||
446 | equal(1, refreshes, 'only one refresh was triggered'); | ||
447 | }); | ||
448 | |||
449 | test('media-sequence updates are considered a playlist change', function() { | ||
450 | new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
451 | requests.pop().respond(200, null, | ||
452 | '#EXTM3U\n' + | ||
453 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
454 | '#EXTINF:10,\n' + | ||
455 | '0.ts\n'); | ||
456 | clock.tick(10 * 1000); // trigger a refresh | ||
457 | requests.pop().respond(200, null, | ||
458 | '#EXTM3U\n' + | ||
459 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
460 | '#EXTINF:10,\n' + | ||
461 | '0.ts\n'); | ||
462 | clock.tick(5 * 1000); // half the default target-duration | ||
463 | |||
464 | strictEqual(requests.length, 0, 'no request is sent'); | ||
465 | }); | ||
466 | |||
467 | test('emits an error if a media refresh fails', function() { | ||
468 | var | ||
469 | errors = 0, | ||
470 | errorResponseText = 'custom error message', | ||
471 | loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
472 | |||
473 | loader.on('error', function() { | ||
474 | errors++; | ||
475 | }); | ||
476 | requests.pop().respond(200, null, | ||
477 | '#EXTM3U\n' + | ||
478 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
479 | '#EXTINF:10,\n' + | ||
480 | '0.ts\n'); | ||
481 | clock.tick(10 * 1000); // trigger a refresh | ||
482 | requests.pop().respond(500, null, errorResponseText); | ||
483 | |||
484 | strictEqual(errors, 1, 'emitted an error'); | ||
485 | strictEqual(loader.error.status, 500, 'captured the status code'); | ||
486 | strictEqual(loader.error.responseText, errorResponseText, 'captured the responseText'); | ||
487 | }); | ||
488 | |||
489 | test('switches media playlists when requested', function() { | ||
490 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
491 | requests.pop().respond(200, null, | ||
492 | '#EXTM3U\n' + | ||
493 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
494 | 'low.m3u8\n' + | ||
495 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
496 | 'high.m3u8\n'); | ||
497 | requests.pop().respond(200, null, | ||
498 | '#EXTM3U\n' + | ||
499 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
500 | '#EXTINF:10,\n' + | ||
501 | 'low-0.ts\n'); | ||
502 | |||
503 | loader.media(loader.master.playlists[1]); | ||
504 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
505 | |||
506 | requests.pop().respond(200, null, | ||
507 | '#EXTM3U\n' + | ||
508 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
509 | '#EXTINF:10,\n' + | ||
510 | 'high-0.ts\n'); | ||
511 | strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | ||
512 | strictEqual(loader.media(), | ||
513 | loader.master.playlists[1], | ||
514 | 'updated the active media'); | ||
515 | }); | ||
516 | |||
517 | test('can switch playlists immediately after the master is downloaded', function() { | ||
518 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
519 | loader.on('loadedplaylist', function() { | ||
520 | loader.media('high.m3u8'); | ||
521 | }); | ||
522 | requests.pop().respond(200, null, | ||
523 | '#EXTM3U\n' + | ||
524 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
525 | 'low.m3u8\n' + | ||
526 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
527 | 'high.m3u8\n'); | ||
528 | equal(requests[0].url, urlTo('high.m3u8'), 'switched variants immediately'); | ||
529 | }); | ||
530 | |||
531 | test('can switch media playlists based on URI', function() { | ||
532 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
533 | requests.pop().respond(200, null, | ||
534 | '#EXTM3U\n' + | ||
535 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
536 | 'low.m3u8\n' + | ||
537 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
538 | 'high.m3u8\n'); | ||
539 | requests.pop().respond(200, null, | ||
540 | '#EXTM3U\n' + | ||
541 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
542 | '#EXTINF:10,\n' + | ||
543 | 'low-0.ts\n'); | ||
544 | |||
545 | loader.media('high.m3u8'); | ||
546 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
547 | |||
548 | requests.pop().respond(200, null, | ||
549 | '#EXTM3U\n' + | ||
550 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
551 | '#EXTINF:10,\n' + | ||
552 | 'high-0.ts\n'); | ||
553 | strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | ||
554 | strictEqual(loader.media(), | ||
555 | loader.master.playlists[1], | ||
556 | 'updated the active media'); | ||
557 | }); | ||
558 | |||
559 | test('aborts in-flight playlist refreshes when switching', function() { | ||
560 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
561 | requests.pop().respond(200, null, | ||
562 | '#EXTM3U\n' + | ||
563 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
564 | 'low.m3u8\n' + | ||
565 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
566 | 'high.m3u8\n'); | ||
567 | requests.pop().respond(200, null, | ||
568 | '#EXTM3U\n' + | ||
569 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
570 | '#EXTINF:10,\n' + | ||
571 | 'low-0.ts\n'); | ||
572 | clock.tick(10 * 1000); | ||
573 | loader.media('high.m3u8'); | ||
574 | strictEqual(requests[0].aborted, true, 'aborted refresh request'); | ||
575 | ok(!requests[0].onreadystatechange, 'onreadystatechange handlers should be removed on abort'); | ||
576 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
577 | }); | ||
578 | |||
579 | test('switching to the active playlist is a no-op', function() { | ||
580 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
581 | requests.pop().respond(200, null, | ||
582 | '#EXTM3U\n' + | ||
583 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
584 | 'low.m3u8\n' + | ||
585 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
586 | 'high.m3u8\n'); | ||
587 | requests.pop().respond(200, null, | ||
588 | '#EXTM3U\n' + | ||
589 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
590 | '#EXTINF:10,\n' + | ||
591 | 'low-0.ts\n' + | ||
592 | '#EXT-X-ENDLIST\n'); | ||
593 | loader.media('low.m3u8'); | ||
594 | |||
595 | strictEqual(requests.length, 0, 'no requests are sent'); | ||
596 | }); | ||
597 | |||
598 | test('switching to the active live playlist is a no-op', function() { | ||
599 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
600 | requests.pop().respond(200, null, | ||
601 | '#EXTM3U\n' + | ||
602 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
603 | 'low.m3u8\n' + | ||
604 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
605 | 'high.m3u8\n'); | ||
606 | requests.pop().respond(200, null, | ||
607 | '#EXTM3U\n' + | ||
608 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
609 | '#EXTINF:10,\n' + | ||
610 | 'low-0.ts\n'); | ||
611 | loader.media('low.m3u8'); | ||
612 | |||
613 | strictEqual(requests.length, 0, 'no requests are sent'); | ||
614 | }); | ||
615 | |||
616 | test('switches back to loaded playlists without re-requesting them', function() { | ||
617 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
618 | requests.pop().respond(200, null, | ||
619 | '#EXTM3U\n' + | ||
620 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
621 | 'low.m3u8\n' + | ||
622 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
623 | 'high.m3u8\n'); | ||
624 | requests.pop().respond(200, null, | ||
625 | '#EXTM3U\n' + | ||
626 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
627 | '#EXTINF:10,\n' + | ||
628 | 'low-0.ts\n' + | ||
629 | '#EXT-X-ENDLIST\n'); | ||
630 | loader.media('high.m3u8'); | ||
631 | requests.pop().respond(200, null, | ||
632 | '#EXTM3U\n' + | ||
633 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
634 | '#EXTINF:10,\n' + | ||
635 | 'high-0.ts\n' + | ||
636 | '#EXT-X-ENDLIST\n'); | ||
637 | loader.media('low.m3u8'); | ||
638 | |||
639 | strictEqual(requests.length, 0, 'no outstanding requests'); | ||
640 | strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist'); | ||
641 | }); | ||
642 | |||
643 | test('aborts outstanding requests if switching back to an already loaded playlist', function() { | ||
644 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
645 | requests.pop().respond(200, null, | ||
646 | '#EXTM3U\n' + | ||
647 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
648 | 'low.m3u8\n' + | ||
649 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
650 | 'high.m3u8\n'); | ||
651 | requests.pop().respond(200, null, | ||
652 | '#EXTM3U\n' + | ||
653 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
654 | '#EXTINF:10,\n' + | ||
655 | 'low-0.ts\n' + | ||
656 | '#EXT-X-ENDLIST\n'); | ||
657 | loader.media('high.m3u8'); | ||
658 | loader.media('low.m3u8'); | ||
659 | |||
660 | strictEqual(requests.length, 1, 'requested high playlist'); | ||
661 | ok(requests[0].aborted, 'aborted playlist request'); | ||
662 | ok(!requests[0].onreadystatechange, 'onreadystatechange handlers should be removed on abort'); | ||
663 | strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist'); | ||
664 | strictEqual(loader.media(), loader.master.playlists[0], 'switched to loaded playlist'); | ||
665 | }); | ||
666 | |||
667 | |||
668 | test('does not abort requests when the same playlist is re-requested', function() { | ||
669 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
670 | requests.pop().respond(200, null, | ||
671 | '#EXTM3U\n' + | ||
672 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
673 | 'low.m3u8\n' + | ||
674 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
675 | 'high.m3u8\n'); | ||
676 | requests.pop().respond(200, null, | ||
677 | '#EXTM3U\n' + | ||
678 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
679 | '#EXTINF:10,\n' + | ||
680 | 'low-0.ts\n' + | ||
681 | '#EXT-X-ENDLIST\n'); | ||
682 | loader.media('high.m3u8'); | ||
683 | loader.media('high.m3u8'); | ||
684 | |||
685 | strictEqual(requests.length, 1, 'made only one request'); | ||
686 | ok(!requests[0].aborted, 'request not aborted'); | ||
687 | }); | ||
688 | |||
689 | test('throws an error if a media switch is initiated too early', function() { | ||
690 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
691 | |||
692 | throws(function() { | ||
693 | loader.media('high.m3u8'); | ||
694 | }, 'threw an error from HAVE_NOTHING'); | ||
695 | |||
696 | requests.pop().respond(200, null, | ||
697 | '#EXTM3U\n' + | ||
698 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
699 | 'low.m3u8\n' + | ||
700 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
701 | 'high.m3u8\n'); | ||
702 | }); | ||
703 | |||
704 | test('throws an error if a switch to an unrecognized playlist is requested', function() { | ||
705 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
706 | requests.pop().respond(200, null, | ||
707 | '#EXTM3U\n' + | ||
708 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
709 | 'media.m3u8\n'); | ||
710 | |||
711 | throws(function() { | ||
712 | loader.media('unrecognized.m3u8'); | ||
713 | }, 'throws an error'); | ||
714 | }); | ||
715 | |||
716 | test('dispose cancels the refresh timeout', function() { | ||
717 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
718 | requests.pop().respond(200, null, | ||
719 | '#EXTM3U\n' + | ||
720 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
721 | '#EXTINF:10,\n' + | ||
722 | '0.ts\n'); | ||
723 | loader.dispose(); | ||
724 | // a lot of time passes... | ||
725 | clock.tick(15 * 1000); | ||
726 | |||
727 | strictEqual(requests.length, 0, 'no refresh request was made'); | ||
728 | }); | ||
729 | |||
730 | test('dispose aborts pending refresh requests', function() { | ||
731 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
732 | requests.pop().respond(200, null, | ||
733 | '#EXTM3U\n' + | ||
734 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
735 | '#EXTINF:10,\n' + | ||
736 | '0.ts\n'); | ||
737 | clock.tick(10 * 1000); | ||
738 | |||
739 | loader.dispose(); | ||
740 | ok(requests[0].aborted, 'refresh request aborted'); | ||
741 | ok(!requests[0].onreadystatechange, 'onreadystatechange handler should not exist after dispose called'); | ||
742 | }); | ||
743 | |||
744 | test('errors if requests take longer than 45s', function() { | ||
745 | var | ||
746 | loader = new videojs.Hls.PlaylistLoader('media.m3u8'), | ||
747 | errors = 0; | ||
748 | loader.on('error', function() { | ||
749 | errors++; | ||
750 | }); | ||
751 | clock.tick(45 * 1000); | ||
752 | |||
753 | strictEqual(errors, 1, 'fired one error'); | ||
754 | strictEqual(loader.error.code, 2, 'fired a network error'); | ||
755 | }); | ||
756 | |||
757 | test('triggers an event when the active media changes', function() { | ||
758 | var | ||
759 | loader = new videojs.Hls.PlaylistLoader('master.m3u8'), | ||
760 | mediaChanges = 0; | ||
761 | loader.on('mediachange', function() { | ||
762 | mediaChanges++; | ||
763 | }); | ||
764 | requests.pop().respond(200, null, | ||
765 | '#EXTM3U\n' + | ||
766 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
767 | 'low.m3u8\n' + | ||
768 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
769 | 'high.m3u8\n'); | ||
770 | requests.shift().respond(200, null, | ||
771 | '#EXTM3U\n' + | ||
772 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
773 | '#EXTINF:10,\n' + | ||
774 | 'low-0.ts\n' + | ||
775 | '#EXT-X-ENDLIST\n'); | ||
776 | strictEqual(mediaChanges, 0, 'initial selection is not a media change'); | ||
777 | |||
778 | loader.media('high.m3u8'); | ||
779 | strictEqual(mediaChanges, 0, 'mediachange does not fire immediately'); | ||
780 | |||
781 | requests.shift().respond(200, null, | ||
782 | '#EXTM3U\n' + | ||
783 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
784 | '#EXTINF:10,\n' + | ||
785 | 'high-0.ts\n' + | ||
786 | '#EXT-X-ENDLIST\n'); | ||
787 | strictEqual(mediaChanges, 1, 'fired a mediachange'); | ||
788 | |||
789 | // switch back to an already loaded playlist | ||
790 | loader.media('low.m3u8'); | ||
791 | strictEqual(mediaChanges, 2, 'fired a mediachange'); | ||
792 | |||
793 | // trigger a no-op switch | ||
794 | loader.media('low.m3u8'); | ||
795 | strictEqual(mediaChanges, 2, 'ignored a no-op media change'); | ||
796 | }); | ||
797 | |||
798 | test('can get media index by playback position for non-live videos', function() { | ||
799 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
800 | requests.shift().respond(200, null, | ||
801 | '#EXTM3U\n' + | ||
802 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
803 | '#EXTINF:4,\n' + | ||
804 | '0.ts\n' + | ||
805 | '#EXTINF:5,\n' + | ||
806 | '1.ts\n' + | ||
807 | '#EXTINF:6,\n' + | ||
808 | '2.ts\n' + | ||
809 | '#EXT-X-ENDLIST\n'); | ||
810 | |||
811 | equal(loader.getMediaIndexForTime_(-1), | ||
812 | 0, | ||
813 | 'the index is never less than zero'); | ||
814 | equal(loader.getMediaIndexForTime_(0), 0, 'time zero is index zero'); | ||
815 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); | ||
816 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); | ||
817 | equal(loader.getMediaIndexForTime_(22), | ||
818 | 2, | ||
819 | 'time greater than the length is index 2'); | ||
820 | }); | ||
821 | |||
822 | test('returns the lower index when calculating for a segment boundary', function() { | ||
823 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
824 | requests.shift().respond(200, null, | ||
825 | '#EXTM3U\n' + | ||
826 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
827 | '#EXTINF:4,\n' + | ||
828 | '0.ts\n' + | ||
829 | '#EXTINF:5,\n' + | ||
830 | '1.ts\n' + | ||
831 | '#EXT-X-ENDLIST\n'); | ||
832 | equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches'); | ||
833 | equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); | ||
834 | equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); | ||
835 | }); | ||
836 | |||
837 | test('accounts for non-zero starting segment time when calculating media index', function() { | ||
838 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
839 | requests.shift().respond(200, null, | ||
840 | '#EXTM3U\n' + | ||
841 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | ||
842 | '#EXTINF:4,\n' + | ||
843 | '1001.ts\n' + | ||
844 | '#EXTINF:5,\n' + | ||
845 | '1002.ts\n'); | ||
846 | loader.media().segments[0].end = 154; | ||
847 | |||
848 | equal(loader.getMediaIndexForTime_(0), -1, 'the lowest returned value is negative one'); | ||
849 | equal(loader.getMediaIndexForTime_(45), -1, 'expired content returns negative one'); | ||
850 | equal(loader.getMediaIndexForTime_(75), -1, 'expired content returns negative one'); | ||
851 | equal(loader.getMediaIndexForTime_(50 + 100), 0, 'calculates the earliest available position'); | ||
852 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | ||
853 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | ||
854 | equal(loader.getMediaIndexForTime_(50 + 100 + 4), 1, 'calculates within the second segment'); | ||
855 | equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), 1, 'calculates within the second segment'); | ||
856 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); | ||
857 | }); | ||
858 | |||
859 | test('prefers precise segment timing when tracking expired time', function() { | ||
860 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
861 | loader.trigger('firstplay'); | ||
862 | requests.shift().respond(200, null, | ||
863 | '#EXTM3U\n' + | ||
864 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | ||
865 | '#EXTINF:4,\n' + | ||
866 | '1001.ts\n' + | ||
867 | '#EXTINF:5,\n' + | ||
868 | '1002.ts\n'); | ||
869 | // setup the loader with an "imprecise" value as if it had been | ||
870 | // accumulating segment durations as they expire | ||
871 | loader.expired_ = 160; | ||
872 | // annotate the first segment with a start time | ||
873 | // this number would be coming from the Source Buffer in practice | ||
874 | loader.media().segments[0].end = 150; | ||
875 | |||
876 | equal(loader.getMediaIndexForTime_(149), 0, 'prefers the value on the first segment'); | ||
877 | |||
878 | clock.tick(10 * 1000); // trigger a playlist refresh | ||
879 | requests.shift().respond(200, null, | ||
880 | '#EXTM3U\n' + | ||
881 | '#EXT-X-MEDIA-SEQUENCE:1002\n' + | ||
882 | '#EXTINF:5,\n' + | ||
883 | '1002.ts\n'); | ||
884 | equal(loader.getMediaIndexForTime_(150 + 4 + 1), 0, 'tracks precise expired times'); | ||
885 | }); | ||
886 | |||
887 | test('accounts for expired time when calculating media index', function() { | ||
888 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
889 | requests.shift().respond(200, null, | ||
890 | '#EXTM3U\n' + | ||
891 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | ||
892 | '#EXTINF:4,\n' + | ||
893 | '1001.ts\n' + | ||
894 | '#EXTINF:5,\n' + | ||
895 | '1002.ts\n'); | ||
896 | loader.expired_ = 150; | ||
897 | |||
898 | equal(loader.getMediaIndexForTime_(0), -1, 'expired content returns a negative index'); | ||
899 | equal(loader.getMediaIndexForTime_(75), -1, 'expired content returns a negative index'); | ||
900 | equal(loader.getMediaIndexForTime_(50 + 100), 0, 'calculates the earliest available position'); | ||
901 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | ||
902 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | ||
903 | equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), 1, 'calculates within the second segment'); | ||
904 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); | ||
905 | }); | ||
906 | |||
907 | test('does not misintrepret playlists missing newlines at the end', function() { | ||
908 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
909 | requests.shift().respond(200, null, | ||
910 | '#EXTM3U\n' + | ||
911 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
912 | '#EXTINF:10,\n' + | ||
913 | 'low-0.ts\n' + | ||
914 | '#EXT-X-ENDLIST'); // no newline | ||
915 | ok(loader.media().endList, 'flushed the final line of input'); | ||
916 | }); | ||
917 | |||
918 | })(window); |
1 | /* Tests for the playlist utilities */ | 1 | import Playlist from '../src/playlist'; |
2 | (function(window, videojs) { | 2 | import QUnit from 'qunit'; |
3 | 'use strict'; | 3 | QUnit.module('Playlist Duration'); |
4 | var Playlist = videojs.Hls.Playlist; | ||
5 | 4 | ||
6 | module('Playlist Duration'); | 5 | QUnit.test('total duration for live playlists is Infinity', function() { |
7 | 6 | let duration = Playlist.duration({ | |
8 | test('total duration for live playlists is Infinity', function() { | ||
9 | var duration = Playlist.duration({ | ||
10 | segments: [{ | 7 | segments: [{ |
11 | duration: 4, | 8 | duration: 4, |
12 | uri: '0.ts' | 9 | uri: '0.ts' |
13 | }] | 10 | }] |
14 | }); | 11 | }); |
15 | 12 | ||
16 | equal(duration, Infinity, 'duration is infinity'); | 13 | QUnit.equal(duration, Infinity, 'duration is infinity'); |
17 | }); | 14 | }); |
18 | 15 | ||
19 | module('Playlist Interval Duration'); | 16 | QUnit.module('Playlist Interval Duration'); |
20 | 17 | ||
21 | test('accounts for non-zero starting VOD media sequences', function() { | 18 | QUnit.test('accounts for non-zero starting VOD media sequences', function() { |
22 | var duration = Playlist.duration({ | 19 | let duration = Playlist.duration({ |
23 | mediaSequence: 10, | 20 | mediaSequence: 10, |
24 | endList: true, | 21 | endList: true, |
25 | segments: [{ | 22 | segments: [{ |
... | @@ -37,11 +34,11 @@ | ... | @@ -37,11 +34,11 @@ |
37 | }] | 34 | }] |
38 | }); | 35 | }); |
39 | 36 | ||
40 | equal(duration, 4 * 10, 'includes only listed segments'); | 37 | QUnit.equal(duration, 4 * 10, 'includes only listed segments'); |
41 | }); | 38 | }); |
42 | 39 | ||
43 | test('uses timeline values when available', function() { | 40 | QUnit.test('uses timeline values when available', function() { |
44 | var duration = Playlist.duration({ | 41 | let duration = Playlist.duration({ |
45 | mediaSequence: 0, | 42 | mediaSequence: 0, |
46 | endList: true, | 43 | endList: true, |
47 | segments: [{ | 44 | segments: [{ |
... | @@ -62,11 +59,11 @@ | ... | @@ -62,11 +59,11 @@ |
62 | }] | 59 | }] |
63 | }, 4); | 60 | }, 4); |
64 | 61 | ||
65 | equal(duration, 4 * 10 + 2, 'used timeline values'); | 62 | QUnit.equal(duration, 4 * 10 + 2, 'used timeline values'); |
66 | }); | 63 | }); |
67 | 64 | ||
68 | test('works when partial timeline information is available', function() { | 65 | QUnit.test('works when partial timeline information is available', function() { |
69 | var duration = Playlist.duration({ | 66 | let duration = Playlist.duration({ |
70 | mediaSequence: 0, | 67 | mediaSequence: 0, |
71 | endList: true, | 68 | endList: true, |
72 | segments: [{ | 69 | segments: [{ |
... | @@ -90,11 +87,11 @@ | ... | @@ -90,11 +87,11 @@ |
90 | }] | 87 | }] |
91 | }, 5); | 88 | }, 5); |
92 | 89 | ||
93 | equal(duration, 50.0002, 'calculated with mixed intervals'); | 90 | QUnit.equal(duration, 50.0002, 'calculated with mixed intervals'); |
94 | }); | 91 | }); |
95 | 92 | ||
96 | test('uses timeline values for the expired duration of live playlists', function() { | 93 | QUnit.test('uses timeline values for the expired duration of live playlists', function() { |
97 | var playlist = { | 94 | let playlist = { |
98 | mediaSequence: 12, | 95 | mediaSequence: 12, |
99 | segments: [{ | 96 | segments: [{ |
100 | duration: 10, | 97 | duration: 10, |
... | @@ -104,18 +101,20 @@ | ... | @@ -104,18 +101,20 @@ |
104 | duration: 9, | 101 | duration: 9, |
105 | uri: '1.ts' | 102 | uri: '1.ts' |
106 | }] | 103 | }] |
107 | }, duration; | 104 | }; |
105 | let duration; | ||
108 | 106 | ||
109 | duration = Playlist.duration(playlist, playlist.mediaSequence); | 107 | duration = Playlist.duration(playlist, playlist.mediaSequence); |
110 | equal(duration, 110.5, 'used segment end time'); | 108 | QUnit.equal(duration, 110.5, 'used segment end time'); |
111 | duration = Playlist.duration(playlist, playlist.mediaSequence + 1); | 109 | duration = Playlist.duration(playlist, playlist.mediaSequence + 1); |
112 | equal(duration, 120.5, 'used segment end time'); | 110 | QUnit.equal(duration, 120.5, 'used segment end time'); |
113 | duration = Playlist.duration(playlist, playlist.mediaSequence + 2); | 111 | duration = Playlist.duration(playlist, playlist.mediaSequence + 2); |
114 | equal(duration, 120.5 + 9, 'used segment end time'); | 112 | QUnit.equal(duration, 120.5 + 9, 'used segment end time'); |
115 | }); | 113 | }); |
116 | 114 | ||
117 | test('looks outside the queried interval for live playlist timeline values', function() { | 115 | QUnit.test('looks outside the queried interval for live playlist timeline values', |
118 | var playlist = { | 116 | function() { |
117 | let playlist = { | ||
119 | mediaSequence: 12, | 118 | mediaSequence: 12, |
120 | segments: [{ | 119 | segments: [{ |
121 | duration: 10, | 120 | duration: 10, |
... | @@ -125,14 +124,15 @@ | ... | @@ -125,14 +124,15 @@ |
125 | end: 120.5, | 124 | end: 120.5, |
126 | uri: '1.ts' | 125 | uri: '1.ts' |
127 | }] | 126 | }] |
128 | }, duration; | 127 | }; |
128 | let duration; | ||
129 | 129 | ||
130 | duration = Playlist.duration(playlist, playlist.mediaSequence); | 130 | duration = Playlist.duration(playlist, playlist.mediaSequence); |
131 | equal(duration, 120.5 - 9 - 10, 'used segment end time'); | 131 | QUnit.equal(duration, 120.5 - 9 - 10, 'used segment end time'); |
132 | }); | 132 | }); |
133 | 133 | ||
134 | test('ignores discontinuity sequences later than the end', function() { | 134 | QUnit.test('ignores discontinuity sequences later than the end', function() { |
135 | var duration = Playlist.duration({ | 135 | let duration = Playlist.duration({ |
136 | mediaSequence: 0, | 136 | mediaSequence: 0, |
137 | discontinuityStarts: [1, 3], | 137 | discontinuityStarts: [1, 3], |
138 | segments: [{ | 138 | segments: [{ |
... | @@ -152,12 +152,12 @@ | ... | @@ -152,12 +152,12 @@ |
152 | }] | 152 | }] |
153 | }, 2); | 153 | }, 2); |
154 | 154 | ||
155 | equal(duration, 19, 'excluded the later segments'); | 155 | QUnit.equal(duration, 19, 'excluded the later segments'); |
156 | }); | 156 | }); |
157 | 157 | ||
158 | test('handles trailing segments without timeline information', function() { | 158 | QUnit.test('handles trailing segments without timeline information', function() { |
159 | var playlist, duration; | 159 | let duration; |
160 | playlist = { | 160 | let playlist = { |
161 | mediaSequence: 0, | 161 | mediaSequence: 0, |
162 | endList: true, | 162 | endList: true, |
163 | segments: [{ | 163 | segments: [{ |
... | @@ -178,15 +178,15 @@ | ... | @@ -178,15 +178,15 @@ |
178 | }; | 178 | }; |
179 | 179 | ||
180 | duration = Playlist.duration(playlist, 3); | 180 | duration = Playlist.duration(playlist, 3); |
181 | equal(duration, 29.45, 'calculated duration'); | 181 | QUnit.equal(duration, 29.45, 'calculated duration'); |
182 | 182 | ||
183 | duration = Playlist.duration(playlist, 2); | 183 | duration = Playlist.duration(playlist, 2); |
184 | equal(duration, 19.5, 'calculated duration'); | 184 | QUnit.equal(duration, 19.5, 'calculated duration'); |
185 | }); | 185 | }); |
186 | 186 | ||
187 | test('uses timeline intervals when segments have them', function() { | 187 | QUnit.test('uses timeline intervals when segments have them', function() { |
188 | var playlist, duration; | 188 | let duration; |
189 | playlist = { | 189 | let playlist = { |
190 | mediaSequence: 0, | 190 | mediaSequence: 0, |
191 | segments: [{ | 191 | segments: [{ |
192 | start: 0, | 192 | start: 0, |
... | @@ -195,23 +195,24 @@ | ... | @@ -195,23 +195,24 @@ |
195 | }, { | 195 | }, { |
196 | duration: 9, | 196 | duration: 9, |
197 | uri: '1.ts' | 197 | uri: '1.ts' |
198 | },{ | 198 | }, { |
199 | start: 20.1, | 199 | start: 20.1, |
200 | end: 30.1, | 200 | end: 30.1, |
201 | duration: 10, | 201 | duration: 10, |
202 | uri: '2.ts' | 202 | uri: '2.ts' |
203 | }] | 203 | }] |
204 | }; | 204 | }; |
205 | duration = Playlist.duration(playlist, 2); | ||
206 | 205 | ||
207 | equal(duration, 20.1, 'used the timeline-based interval'); | 206 | duration = Playlist.duration(playlist, 2); |
207 | QUnit.equal(duration, 20.1, 'used the timeline-based interval'); | ||
208 | 208 | ||
209 | duration = Playlist.duration(playlist, 3); | 209 | duration = Playlist.duration(playlist, 3); |
210 | equal(duration, 30.1, 'used the timeline-based interval'); | 210 | QUnit.equal(duration, 30.1, 'used the timeline-based interval'); |
211 | }); | 211 | }); |
212 | 212 | ||
213 | test('counts the time between segments as part of the earlier segment\'s duration', function() { | 213 | QUnit.test('counts the time between segments as part of the earlier segment\'s duration', |
214 | var duration = Playlist.duration({ | 214 | function() { |
215 | let duration = Playlist.duration({ | ||
215 | mediaSequence: 0, | 216 | mediaSequence: 0, |
216 | endList: true, | 217 | endList: true, |
217 | segments: [{ | 218 | segments: [{ |
... | @@ -226,11 +227,11 @@ | ... | @@ -226,11 +227,11 @@ |
226 | }] | 227 | }] |
227 | }, 1); | 228 | }, 1); |
228 | 229 | ||
229 | equal(duration, 10.1, 'included the segment gap'); | 230 | QUnit.equal(duration, 10.1, 'included the segment gap'); |
230 | }); | 231 | }); |
231 | 232 | ||
232 | test('accounts for discontinuities', function() { | 233 | QUnit.test('accounts for discontinuities', function() { |
233 | var duration = Playlist.duration({ | 234 | let duration = Playlist.duration({ |
234 | mediaSequence: 0, | 235 | mediaSequence: 0, |
235 | endList: true, | 236 | endList: true, |
236 | discontinuityStarts: [1], | 237 | discontinuityStarts: [1], |
... | @@ -244,11 +245,11 @@ | ... | @@ -244,11 +245,11 @@ |
244 | }] | 245 | }] |
245 | }, 2); | 246 | }, 2); |
246 | 247 | ||
247 | equal(duration, 10 + 10, 'handles discontinuities'); | 248 | QUnit.equal(duration, 10 + 10, 'handles discontinuities'); |
248 | }); | 249 | }); |
249 | 250 | ||
250 | test('a non-positive length interval has zero duration', function() { | 251 | QUnit.test('a non-positive length interval has zero duration', function() { |
251 | var playlist = { | 252 | let playlist = { |
252 | mediaSequence: 0, | 253 | mediaSequence: 0, |
253 | discontinuityStarts: [1], | 254 | discontinuityStarts: [1], |
254 | segments: [{ | 255 | segments: [{ |
... | @@ -261,15 +262,15 @@ | ... | @@ -261,15 +262,15 @@ |
261 | }] | 262 | }] |
262 | }; | 263 | }; |
263 | 264 | ||
264 | equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero'); | 265 | QUnit.equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero'); |
265 | equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero'); | 266 | QUnit.equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero'); |
266 | equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero'); | 267 | QUnit.equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero'); |
267 | }); | 268 | }); |
268 | 269 | ||
269 | module('Playlist Seekable'); | 270 | QUnit.module('Playlist Seekable'); |
270 | 271 | ||
271 | test('calculates seekable time ranges from the available segments', function() { | 272 | QUnit.test('calculates seekable time ranges from the available segments', function() { |
272 | var playlist = { | 273 | let playlist = { |
273 | mediaSequence: 0, | 274 | mediaSequence: 0, |
274 | segments: [{ | 275 | segments: [{ |
275 | duration: 10, | 276 | duration: 10, |
... | @@ -279,26 +280,29 @@ | ... | @@ -279,26 +280,29 @@ |
279 | uri: '1.ts' | 280 | uri: '1.ts' |
280 | }], | 281 | }], |
281 | endList: true | 282 | endList: true |
282 | }, seekable = Playlist.seekable(playlist); | 283 | }; |
284 | let seekable = Playlist.seekable(playlist); | ||
283 | 285 | ||
284 | equal(seekable.length, 1, 'there are seekable ranges'); | 286 | QUnit.equal(seekable.length, 1, 'there are seekable ranges'); |
285 | equal(seekable.start(0), 0, 'starts at zero'); | 287 | QUnit.equal(seekable.start(0), 0, 'starts at zero'); |
286 | equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration'); | 288 | QUnit.equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration'); |
287 | }); | 289 | }); |
288 | 290 | ||
289 | test('master playlists have empty seekable ranges', function() { | 291 | QUnit.test('master playlists have empty seekable ranges', function() { |
290 | var seekable = Playlist.seekable({ | 292 | let seekable = Playlist.seekable({ |
291 | playlists: [{ | 293 | playlists: [{ |
292 | uri: 'low.m3u8' | 294 | uri: 'low.m3u8' |
293 | }, { | 295 | }, { |
294 | uri: 'high.m3u8' | 296 | uri: 'high.m3u8' |
295 | }] | 297 | }] |
296 | }); | 298 | }); |
297 | equal(seekable.length, 0, 'no seekable ranges from a master playlist'); | ||
298 | }); | ||
299 | 299 | ||
300 | test('seekable end is three target durations from the actual end of live playlists', function() { | 300 | QUnit.equal(seekable.length, 0, 'no seekable ranges from a master playlist'); |
301 | var seekable = Playlist.seekable({ | 301 | }); |
302 | |||
303 | QUnit.test('seekable end is three target durations from the actual end of live playlists', | ||
304 | function() { | ||
305 | let seekable = Playlist.seekable({ | ||
302 | mediaSequence: 0, | 306 | mediaSequence: 0, |
303 | segments: [{ | 307 | segments: [{ |
304 | duration: 7, | 308 | duration: 7, |
... | @@ -314,13 +318,14 @@ | ... | @@ -314,13 +318,14 @@ |
314 | uri: '3.ts' | 318 | uri: '3.ts' |
315 | }] | 319 | }] |
316 | }); | 320 | }); |
317 | equal(seekable.length, 1, 'there are seekable ranges'); | ||
318 | equal(seekable.start(0), 0, 'starts at zero'); | ||
319 | equal(seekable.end(0), 7, 'ends three target durations from the last segment'); | ||
320 | }); | ||
321 | 321 | ||
322 | test('only considers available segments', function() { | 322 | QUnit.equal(seekable.length, 1, 'there are seekable ranges'); |
323 | var seekable = Playlist.seekable({ | 323 | QUnit.equal(seekable.start(0), 0, 'starts at zero'); |
324 | QUnit.equal(seekable.end(0), 7, 'ends three target durations from the last segment'); | ||
325 | }); | ||
326 | |||
327 | QUnit.test('only considers available segments', function() { | ||
328 | let seekable = Playlist.seekable({ | ||
324 | mediaSequence: 7, | 329 | mediaSequence: 7, |
325 | segments: [{ | 330 | segments: [{ |
326 | uri: '8.ts', | 331 | uri: '8.ts', |
... | @@ -336,13 +341,16 @@ | ... | @@ -336,13 +341,16 @@ |
336 | duration: 10 | 341 | duration: 10 |
337 | }] | 342 | }] |
338 | }); | 343 | }); |
339 | equal(seekable.length, 1, 'there are seekable ranges'); | ||
340 | equal(seekable.start(0), 0, 'starts at the earliest available segment'); | ||
341 | equal(seekable.end(0), 10, 'ends three target durations from the last available segment'); | ||
342 | }); | ||
343 | 344 | ||
344 | test('seekable end accounts for non-standard target durations', function() { | 345 | QUnit.equal(seekable.length, 1, 'there are seekable ranges'); |
345 | var seekable = Playlist.seekable({ | 346 | QUnit.equal(seekable.start(0), 0, 'starts at the earliest available segment'); |
347 | QUnit.equal(seekable.end(0), | ||
348 | 10, | ||
349 | 'ends three target durations from the last available segment'); | ||
350 | }); | ||
351 | |||
352 | QUnit.test('seekable end accounts for non-standard target durations', function() { | ||
353 | let seekable = Playlist.seekable({ | ||
346 | targetDuration: 2, | 354 | targetDuration: 2, |
347 | mediaSequence: 0, | 355 | mediaSequence: 0, |
348 | segments: [{ | 356 | segments: [{ |
... | @@ -362,10 +370,9 @@ | ... | @@ -362,10 +370,9 @@ |
362 | uri: '4.ts' | 370 | uri: '4.ts' |
363 | }] | 371 | }] |
364 | }); | 372 | }); |
365 | equal(seekable.start(0), 0, 'starts at the earliest available segment'); | 373 | |
366 | equal(seekable.end(0), | 374 | QUnit.equal(seekable.start(0), 0, 'starts at the earliest available segment'); |
375 | QUnit.equal(seekable.end(0), | ||
367 | 9 - (2 + 2 + 1), | 376 | 9 - (2 + 2 + 1), |
368 | 'allows seeking no further than three segments from the end'); | 377 | 'allows seeking no further than three segments from the end'); |
369 | }); | 378 | }); |
370 | |||
371 | })(window, window.videojs); | ... | ... |
test/sanity.test.js
0 → 100644
1 | import document from 'global/document'; | ||
2 | |||
3 | import QUnit from 'qunit'; | ||
4 | import sinon from 'sinon'; | ||
5 | import videojs from 'video.js'; | ||
6 | |||
7 | QUnit.module('videojs-contrib-hls - sanity', { | ||
8 | beforeEach() { | ||
9 | this.fixture = document.getElementById('qunit-fixture'); | ||
10 | this.video = document.createElement('video'); | ||
11 | this.fixture.appendChild(this.video); | ||
12 | this.player = videojs(this.video); | ||
13 | |||
14 | // Mock the environment's timers because certain things - particularly | ||
15 | // player readiness - are asynchronous in video.js 5. | ||
16 | this.clock = sinon.useFakeTimers(); | ||
17 | }, | ||
18 | |||
19 | afterEach() { | ||
20 | |||
21 | // The clock _must_ be restored before disposing the player; otherwise, | ||
22 | // certain timeout listeners that happen inside video.js may throw errors. | ||
23 | this.clock.restore(); | ||
24 | this.player.dispose(); | ||
25 | } | ||
26 | }); | ||
27 | |||
28 | QUnit.test('the environment is sane', function(assert) { | ||
29 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); | ||
30 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); | ||
31 | assert.strictEqual(typeof videojs, 'function', 'videojs exists'); | ||
32 | assert.strictEqual(typeof videojs.MediaSource, 'function', 'MediaSource is an object'); | ||
33 | assert.strictEqual(typeof videojs.URL, 'object', 'URL is an object'); | ||
34 | assert.strictEqual(typeof videojs.Hls, 'object', 'Hls is an object'); | ||
35 | assert.strictEqual(typeof videojs.HlsSourceHandler, | ||
36 | 'function', | ||
37 | 'HlsSourceHandler is a function'); | ||
38 | assert.strictEqual(typeof videojs.HlsHandler, 'function', 'HlsHandler is a function'); | ||
39 | }); |
test/videojs-contrib-hls.test.js
0 → 100644
This diff could not be displayed because it is too large.
test/videojs-hls.html
deleted
100644 → 0
1 | <!DOCTYPE html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <meta charset="utf-8"> | ||
5 | <title>video.js HLS Plugin Test Suite</title> | ||
6 | <!-- Load sinon server for fakeXHR --> | ||
7 | <script src="../node_modules/sinon/pkg/sinon.js"></script> | ||
8 | |||
9 | <!-- Load local QUnit. --> | ||
10 | <link rel="stylesheet" href="../node_modules/qunitjs/qunit/qunit.css" media="screen"> | ||
11 | <script src="../node_modules/qunitjs/qunit/qunit.js"></script> | ||
12 | |||
13 | <!-- video.js --> | ||
14 | <script src="../node_modules/video.js/dist/video.js"></script> | ||
15 | <link rel="stylesheet" href="../node_modules/video.js/dist/video-js.css" media="screen"> | ||
16 | <script src="../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> | ||
17 | |||
18 | <!-- HLS plugin --> | ||
19 | <script src="../src/videojs-hls.js"></script> | ||
20 | <script src="../src/xhr.js"></script> | ||
21 | <script src="../src/stream.js"></script> | ||
22 | |||
23 | <!-- M3U8 --> | ||
24 | <script src="../src/m3u8/m3u8-parser.js"></script> | ||
25 | <script src="../src/playlist.js"></script> | ||
26 | <script src="../src/playlist-loader.js"></script> | ||
27 | <script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script> | ||
28 | <script src="../src/decrypter.js"></script> | ||
29 | <!-- M3U8 TEST DATA --> | ||
30 | <script src="../tmp/manifests.js"></script> | ||
31 | <script src="../tmp/expected.js"></script> | ||
32 | |||
33 | <!-- M3U8 --> | ||
34 | <!-- SEGMENT --> | ||
35 | <script src="tsSegment-bc.js"></script> | ||
36 | <script src="../src/bin-utils.js"></script> | ||
37 | |||
38 | <!-- Test cases --> | ||
39 | <script> | ||
40 | module('environment'); | ||
41 | test('is sane', function() { | ||
42 | expect(1); | ||
43 | ok(true); | ||
44 | }); | ||
45 | </script> | ||
46 | <script src="videojs-hls_test.js"></script> | ||
47 | <script src="m3u8_test.js"></script> | ||
48 | <script src="playlist_test.js"></script> | ||
49 | <script src="playlist-loader_test.js"></script> | ||
50 | <script src="decrypter_test.js"></script> | ||
51 | </head> | ||
52 | <body> | ||
53 | <div id="qunit"></div> | ||
54 | <div id="qunit-fixture"> | ||
55 | <span>test markup</span> | ||
56 | </div> | ||
57 | </body> | ||
58 | </html> |
test/videojs-hls_test.js
deleted
100644 → 0
This diff could not be displayed because it is too large.
No preview for this file type
No preview for this file type
No preview for this file type
File moved
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
... | @@ -20,7 +20,7 @@ if (process.env.SAUCE_USERNAME) { | ... | @@ -20,7 +20,7 @@ if (process.env.SAUCE_USERNAME) { |
20 | config.maxDuration = 300; | 20 | config.maxDuration = 300; |
21 | } | 21 | } |
22 | 22 | ||
23 | config.baseUrl = 'http://127.0.0.1:9999/example.html'; | 23 | config.baseUrl = 'http://127.0.0.1:9999/'; |
24 | config.specs = ['spec.js']; | 24 | config.specs = ['spec.js']; |
25 | 25 | ||
26 | config.framework = 'jasmine2'; | 26 | config.framework = 'jasmine2'; | ... | ... |
test/manifest/extXPlaylistTypeInvalidPlaylist.js
→
utils/manifest/extXPlaylistTypeInvalidPlaylist.js
File moved
File moved
File moved
File moved
File moved
File moved
test/manifest/playlist_allow_cache_template.m3u8
→
utils/manifest/playlist_allow_cache_template.m3u8
File moved
File moved
File moved
File moved
File moved
... | @@ -12,7 +12,7 @@ | ... | @@ -12,7 +12,7 @@ |
12 | 12 | ||
13 | <link rel="stylesheet" href="css/normalize.min.css"> | 13 | <link rel="stylesheet" href="css/normalize.min.css"> |
14 | <link rel="stylesheet" href="css/main.css"> | 14 | <link rel="stylesheet" href="css/main.css"> |
15 | <link rel="stylesheet" href="../../node_modules/video.js/dist/video-js/video-js.css"> | 15 | <link rel="stylesheet" href="../../node_modules/video.js/dist/video-js.css"> |
16 | 16 | ||
17 | <script src="js/vendor/modernizr-2.6.2.min.js"></script> | 17 | <script src="js/vendor/modernizr-2.6.2.min.js"></script> |
18 | </head> | 18 | </head> |
... | @@ -120,13 +120,17 @@ | ... | @@ -120,13 +120,17 @@ |
120 | <script src="../../node_modules/sinon/lib/sinon/util/fake_timers.js"></script> | 120 | <script src="../../node_modules/sinon/lib/sinon/util/fake_timers.js"></script> |
121 | <script src="js/vendor/d3.min.js"></script> | 121 | <script src="js/vendor/d3.min.js"></script> |
122 | 122 | ||
123 | <script src="../../node_modules/video.js/dist/video-js/video.js"></script> | 123 | <script src="/node_modules/video.js/dist/video.js"></script> |
124 | <script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> | 124 | <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> |
125 | <script src="../../src/videojs-hls.js"></script> | 125 | <script src="/node_modules/pkcs7/dist/pkcs7.unpad.js"></script> |
126 | <script src="../../src/xhr.js"></script> | 126 | <script src="/src/videojs-hls.js"></script> |
127 | <script src="../../src/stream.js"></script> | 127 | <script src="/src/xhr.js"></script> |
128 | <script src="../../src/m3u8/m3u8-parser.js"></script> | 128 | <script src="/src/stream.js"></script> |
129 | <script src="../../src/playlist-loader.js"></script> | 129 | <script src="/src/m3u8/m3u8-parser.js"></script> |
130 | <script src="/src/playlist.js"></script> | ||
131 | <script src="/src/playlist-loader.js"></script> | ||
132 | <script src="/src/decrypter.js"></script> | ||
133 | <script src="/src/bin-utils.js"></script> | ||
130 | 134 | ||
131 | <script src="js/switcher.js"></script> | 135 | <script src="js/switcher.js"></script> |
132 | </body> | 136 | </body> | ... | ... |
... | @@ -142,6 +142,7 @@ | ... | @@ -142,6 +142,7 @@ |
142 | // mock out the environment | 142 | // mock out the environment |
143 | clock = sinon.useFakeTimers(); | 143 | clock = sinon.useFakeTimers(); |
144 | fakeXhr = sinon.useFakeXMLHttpRequest(); | 144 | fakeXhr = sinon.useFakeXMLHttpRequest(); |
145 | videojs.xhr.XMLHttpRequest = fakeXhr; | ||
145 | requests = []; | 146 | requests = []; |
146 | fakeXhr.onCreate = function(xhr) { | 147 | fakeXhr.onCreate = function(xhr) { |
147 | xhr.startTime = +new Date(); | 148 | xhr.startTime = +new Date(); |
... | @@ -156,7 +157,6 @@ | ... | @@ -156,7 +157,6 @@ |
156 | video.controls = true; | 157 | video.controls = true; |
157 | fixture.appendChild(video); | 158 | fixture.appendChild(video); |
158 | player = videojs(video, { | 159 | player = videojs(video, { |
159 | techOrder: ['hls'], | ||
160 | sources: [{ | 160 | sources: [{ |
161 | src: 'http://example.com/master.m3u8', | 161 | src: 'http://example.com/master.m3u8', |
162 | type: 'application/x-mpegurl' | 162 | type: 'application/x-mpegurl' |
... | @@ -295,6 +295,8 @@ | ... | @@ -295,6 +295,8 @@ |
295 | done(null, results); | 295 | done(null, results); |
296 | }, 0); | 296 | }, 0); |
297 | }); | 297 | }); |
298 | /// trigger the ready function through set timeout | ||
299 | clock.tick(1); | ||
298 | }; | 300 | }; |
299 | runButton = document.getElementById('run-simulation'); | 301 | runButton = document.getElementById('run-simulation'); |
300 | runButton.addEventListener('click', function() { | 302 | runButton.addEventListener('click', function() { | ... | ... |
No preview for this file type
File moved
File moved
-
Please register or sign in to post a comment