8718c2e2 by jrivera

Merge branch 'development'

Conflicts:
	.travis.yml
	package.json
	src/playlist.js
	src/videojs-hls.js
	test/videojs-hls_test.js
2 parents 2f10ecfc b5e60aba
Showing 181 changed files with 4200 additions and 4270 deletions
1 # http://editorconfig.org
2 root = true
3
4 [*]
5 charset = utf-8
6 end_of_line = lf
7 indent_style = space
8 indent_size = 2
9 insert_final_newline = true
10 trim_trailing_whitespace = true
11
12 [*.md]
13 trim_trailing_whitespace = false
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
......
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 *~ 1 # Intentionally left blank, so that npm does not ignore anything by default,
2 *.iml 2 # but relies on the package.json "files" array to explicitly define what ends
3 *.swp 3 # up in the package.
4 tmp/**
5 test/**
......
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
......
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 }
......
1 /**
2 * <%- pkg.name %>
3 * @version <%- pkg.version %>
4 * @copyright <%- date.getFullYear() %> <%- pkg.author %>
5 * @license <%- pkg.license %>
6 */
1 var browserify = require('browserify');
2 var fs = require('fs');
3 var glob = require('glob');
4
5 glob('test/**/*.test.js', function(err, files) {
6 browserify(files)
7 .transform('babelify')
8 .bundle()
9 .pipe(fs.createWriteStream('dist-test/videojs-contrib-hls.js'));
10 });
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 };
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 });
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 });
1 {
2 "curly": true,
3 "eqeqeq": true,
4 "globals": {
5 "console": true
6 },
7 "immed": true,
8 "latedef": true,
9 "newcap": true,
10 "noarg": true,
11 "sub": true,
12 "undef": true,
13 "unused": true,
14 "boss": true,
15 "eqnull": true,
16 "browser": true
17 }
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);
......
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);
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 }
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
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 };
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 };
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 };
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);
......
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;
......
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;
......
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 }
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 });
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);
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>
1
2
3 var fixture = document.createElement('div');
4 fixture.id = 'qunit-fixture';
5 document.body.appendChild(fixture);
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 };
1 var common = require('./common');
2
3 module.exports = function(config) {
4 config.set(common({
5 plugins: ['karma-chrome-launcher'],
6 browsers: ['Chrome']
7 }));
8 };
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 };
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 };
1 var common = require('./common');
2
3 module.exports = function(config) {
4 config.set(common({
5 plugins: ['karma-firefox-launcher'],
6 browsers: ['Firefox']
7 }));
8 };
1 var common = require('./common');
2
3 module.exports = function(config) {
4 config.set(common({
5 plugins: ['karma-ie-launcher'],
6 browsers: ['IE']
7 }));
8 };
1 var common = require('./common');
2
3 module.exports = function(config) {
4 config.set(common({
5 plugins: ['karma-safari-launcher'],
6 browsers: ['Safari']
7 }));
8 };
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 };
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 });
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);
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>
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 });
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);
......
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 });
This diff could not be displayed because it is too large.
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>
This diff could not be displayed because it is too large.
...@@ -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';
......
...@@ -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() {
......