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 5619 additions and 5689 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,23 +40,28 @@ ...@@ -69,23 +40,28 @@
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'); 53 // hook up the video switcher
77 54 var loadUrl = document.getElementById('load-url');
78 // hook up the video switcher 55 var url = document.getElementById('url');
79 var loadUrl = document.getElementById('load-url'); 56 loadUrl.addEventListener('submit', function(event) {
80 var url = document.getElementById('url'); 57 event.preventDefault();
81 loadUrl.addEventListener('submit', function(event) { 58 player.src({
82 event.preventDefault(); 59 src: url.value,
83 player.src({ 60 type: 'application/x-mpegURL'
84 src: url.value, 61 });
85 type: 'application/x-mpegURL' 62 return false;
86 }); 63 });
87 return false; 64 }(window, window.videojs));
88 });
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) { 2 return range.start(i) + '-' + range.end(i);
3 return range.start(i) + '-' + range.end(i); 3 };
4 }; 4
5 var module = { 5 const formatHexString = function(e, i) {
6 hexDump: function(data) { 6 let value = e.toString(16);
7 var 7
8 bytes = Array.prototype.slice.call(data), 8 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
9 step = 16, 9 };
10 formatHexString = function(e, i) { 10 const formatAsciiString = function(e) {
11 var value = e.toString(16); 11 if (e >= 0x20 && e < 0x7e) {
12 return "00".substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 12 return String.fromCharCode(e);
13 }, 13 }
14 formatAsciiString = function(e) { 14 return '.';
15 if (e >= 0x20 && e < 0x7e) { 15 };
16 return String.fromCharCode(e); 16
17 } 17 const utils = {
18 return '.'; 18 hexDump(data) {
19 }, 19 let bytes = Array.prototype.slice.call(data);
20 result = '', 20 let step = 16;
21 hex, 21 let result = '';
22 ascii; 22 let hex;
23 for (var j = 0; j < bytes.length / step; j++) { 23 let ascii;
24 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 24
25 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 25 for (let j = 0; j < bytes.length / step; j++) {
26 result += hex + ' ' + ascii + '\n'; 26 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
27 } 27 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
28 return result; 28 result += hex + ' ' + ascii + '\n';
29 }, 29 }
30 tagDump: function(tag) { 30 return result;
31 return module.hexDump(tag.bytes); 31 },
32 }, 32 tagDump(tag) {
33 textRanges: function(ranges) { 33 return utils.hexDump(tag.bytes);
34 var result = '', i; 34 },
35 for (i = 0; i < ranges.length; i++) { 35 textRanges(ranges) {
36 result += textRange(ranges, i) + ' '; 36 let result = '';
37 } 37 let i;
38 return result; 38
39 for (i = 0; i < ranges.length; i++) {
40 result += textRange(ranges, i) + ' ';
39 } 41 }
40 }; 42 return result;
43 }
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
12 // "forgiving" attribute list psuedo-grammar:
13 // attributes -> keyvalue (',' keyvalue)*
14 // keyvalue -> key '=' value
15 // key -> [^=]*
16 // value -> '"' [^"]* '"' | [^,]*
17 attributeSeparator = (function() {
18 var
19 key = '[^=]*',
20 value = '"[^"]*"|[^,]*',
21 keyvalue = '(?:' + key + ')=(?:' + value + ')';
22 2
23 return new RegExp('(?:^|,)(' + keyvalue + ')'); 3 // "forgiving" attribute list psuedo-grammar:
24 })(), 4 // attributes -> keyvalue (',' keyvalue)*
25 parseAttributes = function(attributes) { 5 // keyvalue -> key '=' value
26 var 6 // key -> [^=]*
27 // split the string using attributes as the separator 7 // value -> '"' [^"]* '"' | [^,]*
28 attrs = attributes.split(attributeSeparator), 8 const attributeSeparator = function() {
29 i = attrs.length, 9 let key = '[^=]*';
30 result = {}, 10 let value = '"[^"]*"|[^,]*';
31 attr; 11 let keyvalue = '(?:' + key + ')=(?:' + value + ')';
32 12
33 while (i--) { 13 return new RegExp('(?:^|,)(' + keyvalue + ')');
34 // filter out unmatched portions of the string 14 };
35 if (attrs[i] === '') {
36 continue;
37 }
38 15
39 // split the key and value 16 const parseAttributes = function(attributes) {
40 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); 17 // split the string using attributes as the separator
41 // trim whitespace and remove optional quotes around the value 18 let attrs = attributes.split(attributeSeparator());
42 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 19 let i = attrs.length;
43 attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); 20 let result = {};
44 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); 21 let attr;
45 result[attr[0]] = attr[1];
46 }
47 return result;
48 },
49 Stream = videojs.Hls.Stream,
50 LineStream,
51 ParseStream,
52 Parser;
53 22
54 /** 23 while (i--) {
55 * A stream that buffers string input and generates a `data` event for each 24 // filter out unmatched portions of the string
56 * line. 25 if (attrs[i] === '') {
57 */ 26 continue;
58 LineStream = function() { 27 }
59 var buffer = '';
60 LineStream.prototype.init.call(this);
61
62 /**
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 28
69 buffer += data; 29 // split the key and value
70 nextNewline = buffer.indexOf('\n'); 30 attr = (/([^=]*)=(.*)/).exec(attrs[i]).slice(1);
31 // trim whitespace and remove optional quotes around the value
32 attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
33 attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
34 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
35 result[attr[0]] = attr[1];
36 }
37 return result;
38 };
71 39
72 for (; nextNewline > -1; nextNewline = buffer.indexOf('\n')) { 40 /**
73 this.trigger('data', buffer.substring(0, nextNewline)); 41 * A line-level M3U8 parser event stream. It expects to receive input one
74 buffer = buffer.substring(nextNewline + 1); 42 * line at a time and performs a context-free parse of its contents. A stream
75 } 43 * interpretation of a manifest can be useful if the manifest is expected to
76 }; 44 * be too large to fit comfortably into memory or the entirety of the input
77 }; 45 * is not immediately available. Otherwise, it's probably much easier to work
78 LineStream.prototype = new Stream(); 46 * with a regular `Parser` object.
47 *
48 * Produces `data` events with an object that captures the parser's
49 * interpretation of the input. That object has a property `tag` that is one
50 * of `uri`, `comment`, or `tag`. URIs only have a single additional
51 * property, `line`, which captures the entirety of the input without
52 * interpretation. Comments similarly have a single additional property
53 * `text` which is the input without the leading `#`.
54 *
55 * Tags always have a property `tagType` which is the lower-cased version of
56 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
57 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
58 * tags are given the tag type `unknown` and a single additional property
59 * `data` with the remainder of the input.
60 */
61 export default class ParseStream extends Stream {
62 constructor() {
63 super();
64 }
79 65
80 /** 66 /**
81 * 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
83 * interpretation of a manifest can be useful if the manifest is expected to
84 * be too large to fit comfortably into memory or the entirety of the input
85 * is not immediately available. Otherwise, it's probably much easier to work
86 * with a regular `Parser` object.
87 *
88 * Produces `data` events with an object that captures the parser's
89 * interpretation of the input. That object has a property `tag` that is one
90 * of `uri`, `comment`, or `tag`. URIs only have a single additional
91 * property, `line`, which captures the entirety of the input without
92 * interpretation. Comments similarly have a single additional property
93 * `text` which is the input without the leading `#`.
94 *
95 * Tags always have a property `tagType` which is the lower-cased version of
96 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
97 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
98 * tags are given the tag type `unknown` and a single additional property
99 * `data` with the remainder of the input.
100 */
101 ParseStream = function() {
102 ParseStream.prototype.init.call(this);
103 };
104 ParseStream.prototype = new Stream();
105 /**
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,18 +233,16 @@ ...@@ -271,18 +233,16 @@
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 = {}; 239 if (split[0]) {
278 if (split[0]) { 240 resolution.width = parseInt(split[0], 10);
279 resolution.width = parseInt(split[0], 10); 241 }
280 } 242 if (split[1]) {
281 if (split[1]) { 243 resolution.height = parseInt(split[1], 10);
282 resolution.height = parseInt(split[1], 10); 244 }
283 } 245 event.attributes.RESOLUTION = resolution;
284 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 }; 304 }
345 305 }
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 }
479 this.manifest.mediaSequence = entry.number;
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,376 +5,381 @@ ...@@ -5,376 +5,381 @@
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
19 * entries in the original master playlist, null is returned. 18 * entries in the original master playlist, null is returned.
20 * @param master {object} a parsed master M3U8 object 19 * @param master {object} a parsed master M3U8 object
21 * @param media {object} a parsed media M3U8 object 20 * @param media {object} a parsed media M3U8 object
22 * @return {object} a new object that represents the original 21 * @return {object} a new object that represents the original
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 31 while (i--) {
33 i = master.playlists.length; 32 playlist = result.playlists[i];
34 while (i--) { 33 if (playlist.uri === media.uri) {
35 playlist = result.playlists[i]; 34 // consider the playlist unchanged if the number of segments
36 if (playlist.uri === media.uri) { 35 // are equal and the media sequence number is unchanged
37 // consider the playlist unchanged if the number of segments 36 if (playlist.segments &&
38 // are equal and the media sequence number is unchanged 37 media.segments &&
39 if (playlist.segments && 38 playlist.segments.length === media.segments.length &&
40 media.segments && 39 playlist.mediaSequence === media.mediaSequence) {
41 playlist.segments.length === media.segments.length && 40 continue;
42 playlist.mediaSequence === media.mediaSequence) {
43 continue;
44 }
45
46 result.playlists[i] = mergeOptions(playlist, media);
47 result.playlists[media.uri] = result.playlists[i];
48
49 // if the update could overlap existing segment information,
50 // merge the two lists
51 if (playlist.segments) {
52 result.playlists[i].segments = updateSegments(playlist.segments,
53 media.segments,
54 media.mediaSequence - playlist.mediaSequence);
55 }
56 changed = true;
57 }
58 } 41 }
59 return changed ? result : null;
60 },
61 42
62 /** 43 result.playlists[i] = mergeOptions(playlist, media);
63 * Returns a new array of segments that is the result of merging 44 result.playlists[media.uri] = result.playlists[i];
64 * properties from an older list of segments onto an updated 45
65 * list. No properties on the updated playlist will be overridden. 46 // if the update could overlap existing segment information,
66 * @param original {array} the outdated list of segments 47 // merge the two lists
67 * @param update {array} the updated list of segments 48 if (playlist.segments) {
68 * @param offset {number} (optional) the index of the first update 49 result.playlists[i].segments = updateSegments(playlist.segments,
69 * segment in the original segment list. For non-live playlists, 50 media.segments,
70 * this should always be zero and does not need to be 51 media.mediaSequence -
71 * specified. For live playlists, it should be the difference 52 playlist.mediaSequence);
72 * between the media sequence numbers in the original and updated
73 * playlists.
74 * @return a list of merged segment objects
75 */
76 updateSegments = function(original, update, offset) {
77 var result = update.slice(), length, i;
78 offset = offset || 0;
79 length = Math.min(original.length, update.length + offset);
80
81 for (i = offset; i < length; i++) {
82 result[i - offset] = mergeOptions(original[i], result[i - offset]);
83 }
84 return result;
85 },
86
87 PlaylistLoader = function(srcUrl, withCredentials) {
88 var
89 loader = this,
90 dispose,
91 mediaUpdateTimeout,
92 request,
93 playlistRequestError,
94 haveMetadata;
95
96 PlaylistLoader.prototype.init.call(this);
97
98 // a flag that disables "expired time"-tracking this setting has
99 // no effect when not playing a live stream
100 this.trackExpiredTime_ = false;
101
102 if (!srcUrl) {
103 throw new Error('A non-empty playlist URL is required');
104 } 53 }
54 changed = true;
55 }
56 }
57 return changed ? result : null;
58 };
105 59
106 playlistRequestError = function(xhr, url, startingState) { 60 /**
107 loader.setBandwidth(request || xhr); 61 * Returns a new array of segments that is the result of merging
62 * properties from an older list of segments onto an updated
63 * list. No properties on the updated playlist will be overridden.
64 * @param original {array} the outdated list of segments
65 * @param update {array} the updated list of segments
66 * @param offset {number} (optional) the index of the first update
67 * segment in the original segment list. For non-live playlists,
68 * this should always be zero and does not need to be
69 * specified. For live playlists, it should be the difference
70 * between the media sequence numbers in the original and updated
71 * playlists.
72 * @return a list of merged segment objects
73 */
74 const updateSegments = function(original, update, offset) {
75 let result = update.slice();
76 let length;
77 let i;
78
79 offset = offset || 0;
80 length = Math.min(original.length, update.length + offset);
81
82 for (i = offset; i < length; i++) {
83 result[i - offset] = mergeOptions(original[i], result[i - offset]);
84 }
85 return result;
86 };
87
88 export default class PlaylistLoader extends Stream {
89 constructor(srcUrl, withCredentials) {
90 super();
91 let loader = this;
92 let dispose;
93 let mediaUpdateTimeout;
94 let request;
95 let playlistRequestError;
96 let haveMetadata;
97
98 // a flag that disables "expired time"-tracking this setting has
99 // no effect when not playing a live stream
100 this.trackExpiredTime_ = false;
101
102 if (!srcUrl) {
103 throw new Error('A non-empty playlist URL is required');
104 }
108 105
109 // any in-flight request is now finished 106 playlistRequestError = function(xhr, url, startingState) {
110 request = null; 107 loader.setBandwidth(request || xhr);
111
112 if (startingState) {
113 loader.state = startingState;
114 }
115 108
116 loader.error = { 109 // any in-flight request is now finished
117 playlist: loader.master.playlists[url], 110 request = null;
118 status: xhr.status,
119 message: 'HLS playlist request error at URL: ' + url,
120 responseText: xhr.responseText,
121 code: (xhr.status >= 500) ? 4 : 2
122 };
123 loader.trigger('error');
124 };
125 111
126 // update the playlist loader's state in response to a new or 112 if (startingState) {
127 // updated playlist. 113 loader.state = startingState;
114 }
128 115
129 haveMetadata = function(xhr, url) { 116 loader.error = {
130 var parser, refreshDelay, update; 117 playlist: loader.master.playlists[url],
118 status: xhr.status,
119 message: 'HLS playlist request error at URL: ' + url,
120 responseText: xhr.responseText,
121 code: (xhr.status >= 500) ? 4 : 2
122 };
123 loader.trigger('error');
124 };
131 125
132 loader.setBandwidth(request || xhr); 126 // update the playlist loader's state in response to a new or
127 // updated playlist.
133 128
134 // any in-flight request is now finished 129 haveMetadata = function(xhr, url) {
135 request = null; 130 let parser;
136 loader.state = 'HAVE_METADATA'; 131 let refreshDelay;
132 let update;
137 133
138 parser = new videojs.m3u8.Parser(); 134 loader.setBandwidth(request || xhr);
139 parser.push(xhr.responseText);
140 parser.end();
141 parser.manifest.uri = url;
142
143 // merge this playlist into the master
144 update = updateMaster(loader.master, parser.manifest);
145 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
146 if (update) {
147 loader.master = update;
148 loader.updateMediaPlaylist_(parser.manifest);
149 } else {
150 // if the playlist is unchanged since the last reload,
151 // try again after half the target duration
152 refreshDelay /= 2;
153 }
154 135
155 // refresh live playlists after a target duration passes 136 // any in-flight request is now finished
156 if (!loader.media().endList) { 137 request = null;
157 window.clearTimeout(mediaUpdateTimeout); 138 loader.state = 'HAVE_METADATA';
158 mediaUpdateTimeout = window.setTimeout(function() {
159 loader.trigger('mediaupdatetimeout');
160 }, refreshDelay);
161 }
162 139
163 loader.trigger('loadedplaylist'); 140 parser = new m3u8.Parser();
164 }; 141 parser.push(xhr.responseText);
142 parser.end();
143 parser.manifest.uri = url;
165 144
166 // initialize the loader state 145 // merge this playlist into the master
167 loader.state = 'HAVE_NOTHING'; 146 update = updateMaster(loader.master, parser.manifest);
147 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
148 if (update) {
149 loader.master = update;
150 loader.updateMediaPlaylist_(parser.manifest);
151 } else {
152 // if the playlist is unchanged since the last reload,
153 // try again after half the target duration
154 refreshDelay /= 2;
155 }
168 156
169 // track the time that has expired from the live window 157 // refresh live playlists after a target duration passes
170 // this allows the seekable start range to be calculated even if 158 if (!loader.media().endList) {
171 // all segments with timing information have expired 159 window.clearTimeout(mediaUpdateTimeout);
172 this.expired_ = 0; 160 mediaUpdateTimeout = window.setTimeout(function() {
161 loader.trigger('mediaupdatetimeout');
162 }, refreshDelay);
163 }
173 164
174 // capture the prototype dispose function 165 loader.trigger('loadedplaylist');
175 dispose = this.dispose; 166 };
176 167
177 /** 168 // initialize the loader state
178 * Abort any outstanding work and clean up. 169 loader.state = 'HAVE_NOTHING';
179 */
180 loader.dispose = function() {
181 if (request) {
182 request.onreadystatechange = null;
183 request.abort();
184 request = null;
185 }
186 window.clearTimeout(mediaUpdateTimeout);
187 dispose.call(this);
188 };
189 170
190 /** 171 // track the time that has expired from the live window
191 * When called without any arguments, returns the currently 172 // this allows the seekable start range to be calculated even if
192 * active media playlist. When called with a single argument, 173 // all segments with timing information have expired
193 * triggers the playlist loader to asynchronously switch to the 174 this.expired_ = 0;
194 * specified media playlist. Calling this method while the
195 * loader is in the HAVE_NOTHING causes an error to be emitted
196 * but otherwise has no effect.
197 * @param playlist (optional) {object} the parsed media playlist
198 * object to switch to
199 */
200 loader.media = function(playlist) {
201 var startingState = loader.state, mediaChange;
202 // getter
203 if (!playlist) {
204 return loader.media_;
205 }
206 175
207 // setter 176 // capture the prototype dispose function
208 if (loader.state === 'HAVE_NOTHING') { 177 dispose = this.dispose;
209 throw new Error('Cannot switch media playlist from ' + loader.state);
210 }
211 178
212 // find the playlist object if the target playlist has been 179 /**
213 // specified by URI 180 * Abort any outstanding work and clean up.
214 if (typeof playlist === 'string') { 181 */
215 if (!loader.master.playlists[playlist]) { 182 loader.dispose = function() {
216 throw new Error('Unknown playlist URI: ' + playlist); 183 if (request) {
217 } 184 request.onreadystatechange = null;
218 playlist = loader.master.playlists[playlist]; 185 request.abort();
219 } 186 request = null;
187 }
188 window.clearTimeout(mediaUpdateTimeout);
189 dispose.call(this);
190 };
220 191
221 mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri; 192 /**
222 193 * When called without any arguments, returns the currently
223 // switch to fully loaded playlists immediately 194 * active media playlist. When called with a single argument,
224 if (loader.master.playlists[playlist.uri].endList) { 195 * triggers the playlist loader to asynchronously switch to the
225 // abort outstanding playlist requests 196 * specified media playlist. Calling this method while the
226 if (request) { 197 * loader is in the HAVE_NOTHING causes an error to be emitted
227 request.onreadystatechange = null; 198 * but otherwise has no effect.
228 request.abort(); 199 * @param playlist (optional) {object} the parsed media playlist
229 request = null; 200 * object to switch to
230 } 201 */
231 loader.state = 'HAVE_METADATA'; 202 loader.media = function(playlist) {
232 loader.media_ = playlist; 203 let startingState = loader.state;
233 204 let mediaChange;
234 // trigger media change if the active media has been updated 205 // getter
235 if (mediaChange) { 206 if (!playlist) {
236 loader.trigger('mediachange'); 207 return loader.media_;
237 } 208 }
238 return;
239 }
240 209
241 // switching to the active playlist is a no-op 210 // setter
242 if (!mediaChange) { 211 if (loader.state === 'HAVE_NOTHING') {
243 return; 212 throw new Error('Cannot switch media playlist from ' + loader.state);
213 }
214
215 // find the playlist object if the target playlist has been
216 // specified by URI
217 if (typeof playlist === 'string') {
218 if (!loader.master.playlists[playlist]) {
219 throw new Error('Unknown playlist URI: ' + playlist);
244 } 220 }
221 playlist = loader.master.playlists[playlist];
222 }
245 223
246 loader.state = 'SWITCHING_MEDIA'; 224 mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri;
247 225
248 // there is already an outstanding playlist request 226 // switch to fully loaded playlists immediately
227 if (loader.master.playlists[playlist.uri].endList) {
228 // abort outstanding playlist requests
249 if (request) { 229 if (request) {
250 if (resolveUrl(loader.master.uri, playlist.uri) === request.url) {
251 // requesting to switch to the same playlist multiple times
252 // has no effect after the first
253 return;
254 }
255 request.onreadystatechange = null; 230 request.onreadystatechange = null;
256 request.abort(); 231 request.abort();
257 request = null; 232 request = null;
258 } 233 }
234 loader.state = 'HAVE_METADATA';
235 loader.media_ = playlist;
259 236
260 // request the new playlist 237 // trigger media change if the active media has been updated
261 request = xhr({ 238 if (mediaChange) {
262 uri: resolveUrl(loader.master.uri, playlist.uri), 239 loader.trigger('mediachange');
263 withCredentials: withCredentials 240 }
264 }, function(error, request) { 241 return;
265 if (error) { 242 }
266 return playlistRequestError(request, playlist.uri, startingState);
267 }
268
269 haveMetadata(request, playlist.uri);
270
271 // fire loadedmetadata the first time a media playlist is loaded
272 if (startingState === 'HAVE_MASTER') {
273 loader.trigger('loadedmetadata');
274 } else {
275 loader.trigger('mediachange');
276 }
277 });
278 };
279 243
280 loader.setBandwidth = function(xhr) { 244 // switching to the active playlist is a no-op
281 loader.bandwidth = xhr.bandwidth; 245 if (!mediaChange) {
282 }; 246 return;
247 }
283 248
284 // In a live list, don't keep track of the expired time until 249 loader.state = 'SWITCHING_MEDIA';
285 // HLS tells us that "first play" has commenced
286 loader.on('firstplay', function() {
287 this.trackExpiredTime_ = true;
288 });
289 250
290 // live playlist staleness timeout 251 // there is already an outstanding playlist request
291 loader.on('mediaupdatetimeout', function() { 252 if (request) {
292 if (loader.state !== 'HAVE_METADATA') { 253 if (resolveUrl(loader.master.uri, playlist.uri) === request.url) {
293 // only refresh the media playlist if no other activity is going on 254 // requesting to switch to the same playlist multiple times
255 // has no effect after the first
294 return; 256 return;
295 } 257 }
258 request.onreadystatechange = null;
259 request.abort();
260 request = null;
261 }
296 262
297 loader.state = 'HAVE_CURRENT_METADATA'; 263 // request the new playlist
298 request = xhr({ 264 request = XhrModule({
299 uri: resolveUrl(loader.master.uri, loader.media().uri), 265 uri: resolveUrl(loader.master.uri, playlist.uri),
300 withCredentials: withCredentials 266 withCredentials
301 }, function(error, request) { 267 }, function(error, request) {
302 if (error) { 268 if (error) {
303 return playlistRequestError(request, loader.media().uri); 269 return playlistRequestError(request, playlist.uri, startingState);
304 } 270 }
305 haveMetadata(request, loader.media().uri); 271
306 }); 272 haveMetadata(request, playlist.uri);
273
274 // fire loadedmetadata the first time a media playlist is loaded
275 if (startingState === 'HAVE_MASTER') {
276 loader.trigger('loadedmetadata');
277 } else {
278 loader.trigger('mediachange');
279 }
307 }); 280 });
281 };
282
283 loader.setBandwidth = function(xhr) {
284 loader.bandwidth = xhr.bandwidth;
285 };
308 286
309 // request the specified URL 287 // In a live list, don't keep track of the expired time until
310 request = xhr({ 288 // HLS tells us that "first play" has commenced
311 uri: srcUrl, 289 loader.on('firstplay', function() {
312 withCredentials: withCredentials 290 this.trackExpiredTime_ = true;
313 }, function(error, req) { 291 });
314 var parser, i;
315 292
316 // clear the loader's request reference 293 // live playlist staleness timeout
317 request = null; 294 loader.on('mediaupdatetimeout', function() {
295 if (loader.state !== 'HAVE_METADATA') {
296 // only refresh the media playlist if no other activity is going on
297 return;
298 }
318 299
300 loader.state = 'HAVE_CURRENT_METADATA';
301 request = XhrModule({
302 uri: resolveUrl(loader.master.uri, loader.media().uri),
303 withCredentials
304 }, function(error, request) {
319 if (error) { 305 if (error) {
320 loader.error = { 306 return playlistRequestError(request, loader.media().uri);
321 status: req.status,
322 message: 'HLS playlist request error at URL: ' + srcUrl,
323 responseText: req.responseText,
324 code: 2 // MEDIA_ERR_NETWORK
325 };
326 return loader.trigger('error');
327 } 307 }
308 haveMetadata(request, loader.media().uri);
309 });
310 });
328 311
329 parser = new videojs.m3u8.Parser(); 312 // request the specified URL
330 parser.push(req.responseText); 313 request = XhrModule({
331 parser.end(); 314 uri: srcUrl,
315 withCredentials
316 }, function(error, req) {
317 let parser;
318 let i;
332 319
333 loader.state = 'HAVE_MASTER'; 320 // clear the loader's request reference
321 request = null;
334 322
335 parser.manifest.uri = srcUrl; 323 if (error) {
324 loader.error = {
325 status: req.status,
326 message: 'HLS playlist request error at URL: ' + srcUrl,
327 responseText: req.responseText,
328 // MEDIA_ERR_NETWORK
329 code: 2
330 };
331 return loader.trigger('error');
332 }
336 333
337 // loaded a master playlist 334 parser = new m3u8.Parser();
338 if (parser.manifest.playlists) { 335 parser.push(req.responseText);
339 loader.master = parser.manifest; 336 parser.end();
340 337
341 // setup by-URI lookups 338 loader.state = 'HAVE_MASTER';
342 i = loader.master.playlists.length;
343 while (i--) {
344 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i];
345 }
346 339
347 loader.trigger('loadedplaylist'); 340 parser.manifest.uri = srcUrl;
348 if (!request) { 341
349 // no media playlist was specifically selected so start 342 // loaded a master playlist
350 // from the first listed one 343 if (parser.manifest.playlists) {
351 loader.media(parser.manifest.playlists[0]); 344 loader.master = parser.manifest;
352 } 345
353 return; 346 // setup by-URI lookups
347 i = loader.master.playlists.length;
348 while (i--) {
349 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i];
354 } 350 }
355 351
356 // loaded a media playlist 352 loader.trigger('loadedplaylist');
357 // infer a master playlist if none was previously requested 353 if (!request) {
358 loader.master = { 354 // no media playlist was specifically selected so start
359 uri: window.location.href, 355 // from the first listed one
360 playlists: [{ 356 loader.media(parser.manifest.playlists[0]);
361 uri: srcUrl 357 }
362 }] 358 return;
363 }; 359 }
364 loader.master.playlists[srcUrl] = loader.master.playlists[0];
365 haveMetadata(req, srcUrl);
366 return loader.trigger('loadedmetadata');
367 });
368 };
369 PlaylistLoader.prototype = new videojs.Hls.Stream();
370 360
361 // loaded a media playlist
362 // infer a master playlist if none was previously requested
363 loader.master = {
364 uri: window.location.href,
365 playlists: [{
366 uri: srcUrl
367 }]
368 };
369 loader.master.playlists[srcUrl] = loader.master.playlists[0];
370 haveMetadata(req, srcUrl);
371 return loader.trigger('loadedmetadata');
372 });
373 }
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
7 var Playlist = {
8 /**
9 * The number of segments that are unsafe to start playback at in
10 * a live stream. Changing this value can cause playback stalls.
11 * See HTTP Live Streaming, "Playing the Media Playlist File"
12 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-18#section-6.3.3
13 */
14 UNSAFE_LIVE_SEGMENTS: 3
15 };
16
17 var duration, intervalDuration, backwardDuration, forwardDuration, seekable;
18
19 backwardDuration = function(playlist, endSequence) {
20 var result = 0, segment, i;
21
22 i = endSequence - playlist.mediaSequence;
23 // if a start time is available for segment immediately following
24 // the interval, use it
25 segment = playlist.segments[i];
26 // Walk backward until we find the latest segment with timeline
27 // information that is earlier than endSequence
28 if (segment) {
29 if (segment.start !== undefined) {
30 return { result: segment.start, precise: true };
31 }
32 if (segment.end !== undefined) {
33 return {
34 result: segment.end - segment.duration,
35 precise: true
36 };
37 }
38 }
39 while (i--) {
40 segment = playlist.segments[i];
41 if (segment.end !== undefined) {
42 return { result: result + segment.end, precise: true };
43 }
44
45 result += segment.duration;
46
47 if (segment.start !== undefined) {
48 return { result: result + segment.start, precise: true };
49 }
50 }
51 return { result: result, precise: false };
52 };
53
54 forwardDuration = function(playlist, endSequence) {
55 var result = 0, segment, i;
56
57 i = endSequence - playlist.mediaSequence;
58 // Walk forward until we find the earliest segment with timeline
59 // information
60 for (; i < playlist.segments.length; i++) {
61 segment = playlist.segments[i];
62 if (segment.start !== undefined) {
63 return {
64 result: segment.start - result,
65 precise: true
66 };
67 }
68
69 result += segment.duration;
70
71 if (segment.end !== undefined) {
72 return {
73 result: segment.end - result,
74 precise: true
75 };
76 }
77
78 }
79 // indicate we didn't find a useful duration estimate
80 return { result: -1, precise: false };
81 };
82 5
6 let Playlist = {
83 /** 7 /**
84 * Calculate the media duration from the segments associated with a 8 * The number of segments that are unsafe to start playback at in
85 * playlist. The duration of a subinterval of the available segments 9 * a live stream. Changing this value can cause playback stalls.
86 * may be calculated by specifying an end index. 10 * See HTTP Live Streaming, "Playing the Media Playlist File"
87 * 11 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-18#section-6.3.3
88 * @param playlist {object} a media playlist object
89 * @param endSequence {number} (optional) an exclusive upper boundary
90 * for the playlist. Defaults to playlist length.
91 * @return {number} the duration between the first available segment
92 * and end index.
93 */ 12 */
94 intervalDuration = function(playlist, endSequence) { 13 UNSAFE_LIVE_SEGMENTS: 3
95 var backward, forward; 14 };
96 15
97 if (endSequence === undefined) { 16 const backwardDuration = function(playlist, endSequence) {
98 endSequence = playlist.mediaSequence + playlist.segments.length; 17 let result = 0;
18 let i = endSequence - playlist.mediaSequence;
19 // if a start time is available for segment immediately following
20 // the interval, use it
21 let segment = playlist.segments[i];
22
23 // Walk backward until we find the latest segment with timeline
24 // information that is earlier than endSequence
25 if (segment) {
26 if (typeof segment.start !== 'undefined') {
27 return { result: segment.start, precise: true };
99 } 28 }
100 29 if (typeof segment.end !== 'undefined') {
101 if (endSequence < playlist.mediaSequence) { 30 return {
102 return 0; 31 result: segment.end - segment.duration,
32 precise: true
33 };
103 } 34 }
104 35 }
105 // do a backward walk to estimate the duration 36 while (i--) {
106 backward = backwardDuration(playlist, endSequence); 37 segment = playlist.segments[i];
107 if (backward.precise) { 38 if (typeof segment.end !== 'undefined') {
108 // if we were able to base our duration estimate on timing 39 return { result: result + segment.end, precise: true };
109 // information provided directly from the Media Source, return
110 // it
111 return backward.result;
112 } 40 }
113 41
114 // walk forward to see if a precise duration estimate can be made 42 result += segment.duration;
115 // that way 43
116 forward = forwardDuration(playlist, endSequence); 44 if (typeof segment.start !== 'undefined') {
117 if (forward.precise) { 45 return { result: result + segment.start, precise: true };
118 // we found a segment that has been buffered and so it's 46 }
119 // position is known precisely 47 }
120 return forward.result; 48 return { result, precise: false };
49 };
50
51 const forwardDuration = function(playlist, endSequence) {
52 let result = 0;
53 let segment;
54 let i = endSequence - playlist.mediaSequence;
55 // Walk forward until we find the earliest segment with timeline
56 // information
57
58 for (; i < playlist.segments.length; i++) {
59 segment = playlist.segments[i];
60 if (typeof segment.start !== 'undefined') {
61 return {
62 result: segment.start - result,
63 precise: true
64 };
121 } 65 }
122 66
123 // return the less-precise, playlist-based duration estimate 67 result += segment.duration;
124 return backward.result;
125 };
126 68
127 /** 69 if (typeof segment.end !== 'undefined') {
128 * Calculates the duration of a playlist. If a start and end index 70 return {
129 * are specified, the duration will be for the subset of the media 71 result: segment.end - result,
130 * timeline between those two indices. The total duration for live 72 precise: true
131 * playlists is always Infinity. 73 };
132 * @param playlist {object} a media playlist object
133 * @param endSequence {number} (optional) an exclusive upper
134 * boundary for the playlist. Defaults to the playlist media
135 * sequence number plus its length.
136 * @param includeTrailingTime {boolean} (optional) if false, the
137 * interval between the final segment and the subsequent segment
138 * will not be included in the result
139 * @return {number} the duration between the start index and end
140 * index.
141 */
142 duration = function(playlist, endSequence, includeTrailingTime) {
143 if (!playlist) {
144 return 0;
145 } 74 }
146 75
147 if (includeTrailingTime === undefined) { 76 }
148 includeTrailingTime = true; 77 // indicate we didn't find a useful duration estimate
149 } 78 return { result: -1, precise: false };
79 };
150 80
151 // if a slice of the total duration is not requested, use 81 /**
152 // playlist-level duration indicators when they're present 82 * Calculate the media duration from the segments associated with a
153 if (endSequence === undefined) { 83 * playlist. The duration of a subinterval of the available segments
154 // if present, use the duration specified in the playlist 84 * may be calculated by specifying an end index.
155 if (playlist.totalDuration) { 85 *
156 return playlist.totalDuration; 86 * @param playlist {object} a media playlist object
157 } 87 * @param endSequence {number} (optional) an exclusive upper boundary
158 88 * for the playlist. Defaults to playlist length.
159 // duration should be Infinity for live playlists 89 * @return {number} the duration between the first available segment
160 if (!playlist.endList) { 90 * and end index.
161 return window.Infinity; 91 */
162 } 92 const intervalDuration = function(playlist, endSequence) {
163 } 93 let backward;
94 let forward;
95
96 if (typeof endSequence === 'undefined') {
97 endSequence = playlist.mediaSequence + playlist.segments.length;
98 }
99
100 if (endSequence < playlist.mediaSequence) {
101 return 0;
102 }
103
104 // do a backward walk to estimate the duration
105 backward = backwardDuration(playlist, endSequence);
106 if (backward.precise) {
107 // if we were able to base our duration estimate on timing
108 // information provided directly from the Media Source, return
109 // it
110 return backward.result;
111 }
164 112
165 // calculate the total duration based on the segment durations 113 // walk forward to see if a precise duration estimate can be made
166 return intervalDuration(playlist, 114 // that way
167 endSequence, 115 forward = forwardDuration(playlist, endSequence);
168 includeTrailingTime); 116 if (forward.precise) {
169 }; 117 // we found a segment that has been buffered and so it's
118 // position is known precisely
119 return forward.result;
120 }
170 121
171 /** 122 // return the less-precise, playlist-based duration estimate
172 * Calculates the interval of time that is currently seekable in a 123 return backward.result;
173 * playlist. The returned time ranges are relative to the earliest 124 };
174 * moment in the specified playlist that is still available. A full
175 * seekable implementation for live streams would need to offset
176 * these values by the duration of content that has expired from the
177 * stream.
178 * @param playlist {object} a media playlist object
179 * @return {TimeRanges} the periods of time that are valid targets
180 * for seeking
181 */
182 seekable = function(playlist) {
183 var start, end;
184 125
185 // without segments, there are no seekable ranges 126 /**
186 if (!playlist.segments) { 127 * Calculates the duration of a playlist. If a start and end index
187 return videojs.createTimeRange(); 128 * are specified, the duration will be for the subset of the media
129 * timeline between those two indices. The total duration for live
130 * playlists is always Infinity.
131 * @param playlist {object} a media playlist object
132 * @param endSequence {number} (optional) an exclusive upper
133 * boundary for the playlist. Defaults to the playlist media
134 * sequence number plus its length.
135 * @param includeTrailingTime {boolean} (optional) if false, the
136 * interval between the final segment and the subsequent segment
137 * will not be included in the result
138 * @return {number} the duration between the start index and end
139 * index.
140 */
141 export const duration = function(playlist, endSequence, includeTrailingTime) {
142 if (!playlist) {
143 return 0;
144 }
145
146 if (typeof includeTrailingTime === 'undefined') {
147 includeTrailingTime = true;
148 }
149
150 // if a slice of the total duration is not requested, use
151 // playlist-level duration indicators when they're present
152 if (typeof endSequence === 'undefined') {
153 // if present, use the duration specified in the playlist
154 if (playlist.totalDuration) {
155 return playlist.totalDuration;
188 } 156 }
189 // when the playlist is complete, the entire duration is seekable 157
190 if (playlist.endList) { 158 // duration should be Infinity for live playlists
191 return videojs.createTimeRange(0, duration(playlist)); 159 if (!playlist.endList) {
160 return window.Infinity;
192 } 161 }
162 }
163
164 // calculate the total duration based on the segment durations
165 return intervalDuration(playlist,
166 endSequence,
167 includeTrailingTime);
168 };
193 169
194 // live playlists should not expose three segment durations worth 170 /**
195 // of content from the end of the playlist 171 * Calculates the interval of time that is currently seekable in a
196 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 172 * playlist. The returned time ranges are relative to the earliest
197 start = intervalDuration(playlist, playlist.mediaSequence); 173 * moment in the specified playlist that is still available. A full
198 end = intervalDuration(playlist, 174 * seekable implementation for live streams would need to offset
199 playlist.mediaSequence + 175 * these values by the duration of content that has expired from the
200 Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS)); 176 * stream.
201 return videojs.createTimeRange(start, end); 177 * @param playlist {object} a media playlist object
202 }; 178 * @return {TimeRanges} the periods of time that are valid targets
203 179 * for seeking
204 // exports 180 */
205 Playlist.duration = duration; 181 export const seekable = function(playlist) {
206 Playlist.seekable = seekable; 182 let start;
207 videojs.Hls.Playlist = Playlist; 183 let end;
208 184
209 })(window, window.videojs); 185 // without segments, there are no seekable ranges
186 if (!playlist.segments) {
187 return createTimeRange();
188 }
189 // when the playlist is complete, the entire duration is seekable
190 if (playlist.endList) {
191 return createTimeRange(0, duration(playlist));
192 }
193
194 // live playlists should not expose three segment durations worth
195 // of content from the end of the playlist
196 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
197 start = intervalDuration(playlist, playlist.mediaSequence);
198 end = intervalDuration(playlist,
199 playlist.mediaSequence +
200 Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS));
201 return createTimeRange(start, end);
202 };
203
204 Playlist.duration = duration;
205 Playlist.seekable = seekable;
206
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 }
9 /** 8
10 * Add a listener for a specified event type. 9 /**
11 * @param type {string} the event name 10 * Add a listener for a specified event type.
12 * @param listener {function} the callback to be invoked when an event of 11 * @param type {string} the event name
13 * the specified type occurs 12 * @param listener {function} the callback to be invoked when an event of
14 */ 13 * the specified type occurs
15 this.on = function(type, listener) { 14 */
16 if (!listeners[type]) { 15 on(type, listener) {
17 listeners[type] = []; 16 if (!this.listeners[type]) {
18 } 17 this.listeners[type] = [];
19 listeners[type].push(listener); 18 }
20 }; 19 this.listeners[type].push(listener);
21 /** 20 }
22 * Remove a listener for a specified event type. 21
23 * @param type {string} the event name 22 /**
24 * @param listener {function} a function previously registered for this 23 * Remove a listener for a specified event type.
25 * type of event through `on` 24 * @param type {string} the event name
26 */ 25 * @param listener {function} a function previously registered for this
27 this.off = function(type, listener) { 26 * type of event through `on`
28 var index; 27 */
29 if (!listeners[type]) { 28 off(type, listener) {
30 return false; 29 let index;
31 } 30
32 index = listeners[type].indexOf(listener); 31 if (!this.listeners[type]) {
33 listeners[type].splice(index, 1); 32 return false;
34 return index > -1; 33 }
35 }; 34 index = this.listeners[type].indexOf(listener);
36 /** 35 this.listeners[type].splice(index, 1);
37 * Trigger an event of the specified type on this stream. Any additional 36 return index > -1;
38 * arguments to this function are passed as parameters to event listeners. 37 }
39 * @param type {string} the event name 38
40 */ 39 /**
41 this.trigger = function(type) { 40 * Trigger an event of the specified type on this stream. Any additional
42 var callbacks, i, length, args; 41 * arguments to this function are passed as parameters to event listeners.
43 callbacks = listeners[type]; 42 * @param type {string} the event name
44 if (!callbacks) { 43 */
45 return; 44 trigger(type) {
46 } 45 let callbacks;
47 // Slicing the arguments on every invocation of this method 46 let i;
48 // can add a significant amount of overhead. Avoid the 47 let length;
49 // intermediate object creation for the common case of a 48 let args;
50 // single callback argument 49
51 if (arguments.length === 2) { 50 callbacks = this.listeners[type];
52 length = callbacks.length; 51 if (!callbacks) {
53 for (i = 0; i < length; ++i) { 52 return;
54 callbacks[i].call(this, arguments[1]); 53 }
55 } 54 // Slicing the arguments on every invocation of this method
56 } else { 55 // can add a significant amount of overhead. Avoid the
57 args = Array.prototype.slice.call(arguments, 1); 56 // intermediate object creation for the common case of a
58 length = callbacks.length; 57 // single callback argument
59 for (i = 0; i < length; ++i) { 58 if (arguments.length === 2) {
60 callbacks[i].apply(this, args); 59 length = callbacks.length;
61 } 60 for (i = 0; i < length; ++i) {
62 } 61 callbacks[i].call(this, arguments[1]);
63 }; 62 }
64 /** 63 } else {
65 * Destroys the stream and cleans up. 64 args = Array.prototype.slice.call(arguments, 1);
66 */ 65 length = callbacks.length;
67 this.dispose = function() { 66 for (i = 0; i < length; ++i) {
68 listeners = {}; 67 callbacks[i].apply(this, args);
69 }; 68 }
70 }; 69 }
71 }; 70 }
71
72 /**
73 * Destroys the stream and cleans up.
74 */
75 dispose() {
76 this.listeners = {};
77 }
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 8 import xhr from './xhr';
9 var 9 import {Decrypter, AsyncStream, decrypt} from './decrypter';
10 // A fudge factor to apply to advertised playlist bitrates to account for 10 import utils from './bin-utils';
11 // temporary flucations in client bandwidth 11 import {MediaSource, URL} from 'videojs-contrib-media-sources';
12 bandwidthVariance = 1.2, 12 import m3u8 from './m3u8';
13 blacklistDuration = 5 * 60 * 1000, // 5 minute blacklist 13 import videojs from 'video.js';
14 TIME_FUDGE_FACTOR = 1 / 30, // Fudge factor to account for TimeRanges rounding 14 import resolveUrl from './resolve-url';
15 Component = videojs.getComponent('Component'), 15
16 16 const Hls = {
17 // The amount of time to wait between checking the state of the buffer 17 PlaylistLoader,
18 bufferCheckInterval = 500, 18 Playlist,
19 19 Decrypter,
20 safeGetComputedStyle, 20 AsyncStream,
21 keyFailed, 21 decrypt,
22 resolveUrl; 22 utils,
23 23 xhr
24 // returns true if a key has failed to download within a certain amount of retries
25 keyFailed = function(key) {
26 return key.retries && key.retries >= 2;
27 }; 24 };
28 25
29 videojs.Hls = {}; 26 // the desired length of video to maintain in the buffer, in seconds
30 videojs.HlsHandler = videojs.extend(Component, { 27 Hls.GOAL_BUFFER_LENGTH = 30;
31 constructor: function(tech, options) {
32 var self = this, _player;
33
34 Component.call(this, tech);
35
36 // tech.player() is deprecated but setup a reference to HLS for
37 // backwards-compatibility
38 if (tech.options_ && tech.options_.playerId) {
39 _player = videojs(tech.options_.playerId);
40 if (!_player.hls) {
41 Object.defineProperty(_player, 'hls', {
42 get: function() {
43 videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.');
44 return self;
45 }
46 });
47 }
48 }
49 this.tech_ = tech;
50 this.source_ = options.source;
51 this.mode_ = options.mode;
52 // the segment info object for a segment that is in the process of
53 // being downloaded or processed
54 this.pendingSegment_ = null;
55
56 // start playlist selection at a reasonable bandwidth for
57 // broadband internet
58 this.bandwidth = options.bandwidth || 4194304; // 0.5 Mbps
59 this.bytesReceived = 0;
60
61 // loadingState_ tracks how far along the buffering process we
62 // have been given permission to proceed. There are three possible
63 // values:
64 // - none: do not load playlists or segments
65 // - meta: load playlists but not segments
66 // - segments: load everything
67 this.loadingState_ = 'none';
68 if (this.tech_.preload() !== 'none') {
69 this.loadingState_ = 'meta';
70 }
71
72 // periodically check if new data needs to be downloaded or
73 // buffered data should be appended to the source buffer
74 this.startCheckingBuffer_();
75
76 this.on(this.tech_, 'seeking', function() {
77 this.setCurrentTime(this.tech_.currentTime());
78 });
79 this.on(this.tech_, 'error', function() {
80 this.stopCheckingBuffer_();
81 });
82
83 this.on(this.tech_, 'play', this.play);
84 }
85 });
86 28
87 // HLS is a source handler, not a tech. Make sure attempts to use it 29 // HLS is a source handler, not a tech. Make sure attempts to use it
88 // as one do not cause exceptions. 30 // as one do not cause exceptions.
89 videojs.Hls.canPlaySource = function() { 31 Hls.canPlaySource = function() {
90 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + 32 return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
91 'your player\'s techOrder.'); 33 'your player\'s techOrder.');
92 }; 34 };
93 35
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
151 // do nothing if the src is falsey
152 if (!src) {
153 return;
154 }
155
156 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ });
157
158 // load the MediaSource into the player
159 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
160
161 this.options_ = {};
162 if (this.source_.withCredentials !== undefined) {
163 this.options_.withCredentials = this.source_.withCredentials;
164 } else if (videojs.options.hls) {
165 this.options_.withCredentials = videojs.options.hls.withCredentials;
166 }
167 this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials);
168
169 this.tech_.one('canplay', this.setupFirstPlay.bind(this));
170
171 this.playlists.on('loadedmetadata', function() {
172 oldMediaPlaylist = this.playlists.media();
173
174 // if this isn't a live video and preload permits, start
175 // downloading segments
176 if (oldMediaPlaylist.endList &&
177 this.tech_.preload() !== 'metadata' &&
178 this.tech_.preload() !== 'none') {
179 this.loadingState_ = 'segments';
180 }
181
182 this.setupSourceBuffer_();
183 this.setupFirstPlay();
184 this.fillBuffer();
185 this.tech_.trigger('loadedmetadata');
186 }.bind(this));
187
188 this.playlists.on('error', function() {
189 this.blacklistCurrentPlaylist_(this.playlists.error);
190 }.bind(this));
191
192 this.playlists.on('loadedplaylist', function() {
193 var updatedPlaylist = this.playlists.media(), seekable;
194
195 if (!updatedPlaylist) {
196 // select the initial variant
197 this.playlists.media(this.selectPlaylist());
198 return;
199 }
200
201 this.updateDuration(this.playlists.media());
202
203 // update seekable
204 seekable = this.seekable();
205 if (this.duration() === Infinity &&
206 seekable.length !== 0) {
207 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
208 }
209
210 oldMediaPlaylist = updatedPlaylist;
211 }.bind(this));
212
213 this.playlists.on('mediachange', function() {
214 this.tech_.trigger({
215 type: 'mediachange',
216 bubbles: true
217 });
218 }.bind(this));
219
220 // do nothing if the tech has been disposed already
221 // this can occur if someone sets the src in player.ready(), for instance
222 if (!this.tech_.el()) {
223 return;
224 }
225
226 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
227 };
228
229 videojs.HlsHandler.prototype.handleSourceOpen = function() {
230 // Only attempt to create the source buffer if none already exist.
231 // 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)
233 if (!this.sourceBuffer) {
234 this.setupSourceBuffer_();
235 }
236
237 // if autoplay is enabled, begin playback. This is duplicative of
238 // code in video.js but is required because play() must be invoked
239 // *after* the media source has opened.
240 // NOTE: moving this invocation of play() after
241 // sourceBuffer.appendBuffer() below caused live streams with
242 // autoplay to stall
243 if (this.tech_.autoplay()) {
244 this.play();
245 }
246 };
247
248 // Search for a likely end time for the segment that was just appened 36 // 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 37 // based on the state of the `buffered` property before and after the
250 // append. 38 // append.
251 // If we found only one such uncommon end-point return it. 39 // If we found only one such uncommon end-point return it.
252 videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { 40 Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) {
253 var 41 let i;
254 i, start, end, 42 let start;
255 result = [], 43 let end;
256 edges = [], 44 let result = [];
257 // In order to qualify as a possible candidate, the end point must: 45 let edges = [];
258 // 1) Not have already existed in the `original` ranges 46
259 // 2) Not result from the shrinking of a range that already existed 47 // In order to qualify as a possible candidate, the end point must:
260 // in the `original` ranges 48 // 1) Not have already existed in the `original` ranges
261 // 3) Not be contained inside of a range that existed in `original` 49 // 2) Not result from the shrinking of a range that already existed
262 overlapsCurrentEnd = function(span) { 50 // in the `original` ranges
263 return (span[0] <= end && span[1] >= end); 51 // 3) Not be contained inside of a range that existed in `original`
264 }; 52 let overlapsCurrentEnd = function(span) {
53 return (span[0] <= end && span[1] >= end);
54 };
265 55
266 if (original) { 56 if (original) {
267 // Save all the edges in the `original` TimeRanges object 57 // Save all the edges in the `original` TimeRanges object
...@@ -299,6 +89,137 @@ videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { ...@@ -299,6 +89,137 @@ videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) {
299 }; 89 };
300 90
301 /** 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;
98
99 // native HLS is definitely not supported if HTML5 video isn't
100 if (!videojs.getComponent('Html5').isSupported()) {
101 return false;
102 }
103
104 xMpegUrl = video.canPlayType('application/x-mpegURL');
105 vndMpeg = video.canPlayType('application/vnd.apple.mpegURL');
106 return (/probably|maybe/).test(xMpegUrl) ||
107 (/probably|maybe/).test(vndMpeg);
108 }());
109
110 // HLS is a source handler, not a tech. Make sure attempts to use it
111 // as one do not cause exceptions.
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;
194
195 // returns true if a key has failed to download within a certain amount of retries
196 const keyFailed = function(key) {
197 return key.retries && key.retries >= 2;
198 };
199
200 /**
201 * Returns the CSS value for the specified property on an element
202 * using `getComputedStyle`. Firefox has a long-standing issue where
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 /**
302 * Updates segment with information about its end-point in time and, optionally, 223 * 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 224 * the segment duration if we have enough information to determine a segment duration
304 * accurately. 225 * accurately.
...@@ -306,17 +227,13 @@ videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { ...@@ -306,17 +227,13 @@ videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) {
306 * @param segmentIndex {number} the index of segment we last appended 227 * @param segmentIndex {number} the index of segment we last appended
307 * @param segmentEnd {number} the known of the segment referenced by segmentIndex 228 * @param segmentEnd {number} the known of the segment referenced by segmentIndex
308 */ 229 */
309 videojs.HlsHandler.prototype.updateSegmentMetadata_ = function(playlist, segmentIndex, segmentEnd) { 230 const updateSegmentMetadata = function(playlist, segmentIndex, segmentEnd) {
310 var
311 segment,
312 previousSegment;
313
314 if (!playlist) { 231 if (!playlist) {
315 return; 232 return;
316 } 233 }
317 234
318 segment = playlist.segments[segmentIndex]; 235 let segment = playlist.segments[segmentIndex];
319 previousSegment = playlist.segments[segmentIndex - 1]; 236 let previousSegment = playlist.segments[segmentIndex - 1];
320 237
321 if (segmentEnd && segment) { 238 if (segmentEnd && segment) {
322 segment.end = segmentEnd; 239 segment.end = segmentEnd;
...@@ -336,36 +253,34 @@ videojs.HlsHandler.prototype.updateSegmentMetadata_ = function(playlist, segment ...@@ -336,36 +253,34 @@ videojs.HlsHandler.prototype.updateSegmentMetadata_ = function(playlist, segment
336 * Determines if we should call endOfStream on the media source based on the state 253 * 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. 254 * of the buffer or if appened segment was the final segment in the playlist.
338 * @param playlist {object} a media playlist object 255 * @param playlist {object} a media playlist object
256 * @param mediaSource {object} the MediaSource object
339 * @param segmentIndex {number} the index of segment we last appended 257 * @param segmentIndex {number} the index of segment we last appended
340 * @param currentBuffered {object} the buffered region that currentTime resides in 258 * @param currentBuffered {object} the buffered region that currentTime resides in
341 * @return {boolean} whether the calling function should call endOfStream on the MediaSource 259 * @return {boolean} whether the calling function should call endOfStream on the MediaSource
342 */ 260 */
343 videojs.HlsHandler.prototype.isEndOfStream_ = function(playlist, segmentIndex, currentBuffered) { 261 const detectEndOfStream = function(playlist, mediaSource, segmentIndex, currentBuffered) {
344 var
345 segments = playlist.segments,
346 appendedLastSegment,
347 bufferedToEnd;
348
349 if (!playlist) { 262 if (!playlist) {
350 return false; 263 return false;
351 } 264 }
352 265
266 let segments = playlist.segments;
267
353 // determine a few boolean values to help make the branch below easier 268 // determine a few boolean values to help make the branch below easier
354 // to read 269 // to read
355 appendedLastSegment = (segmentIndex === segments.length - 1); 270 let appendedLastSegment = (segmentIndex === segments.length - 1);
356 bufferedToEnd = (currentBuffered.length && 271 let bufferedToEnd = (currentBuffered.length &&
357 segments[segments.length - 1].end <= currentBuffered.end(0)); 272 segments[segments.length - 1].end <= currentBuffered.end(0));
358 273
359 // if we've buffered to the end of the video, we need to call endOfStream 274 // 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 275 // so that MediaSources can trigger the `ended` event when it runs out of
361 // buffered data instead of waiting for me 276 // buffered data instead of waiting for me
362 return playlist.endList && 277 return playlist.endList &&
363 this.mediaSource.readyState === 'open' && 278 mediaSource.readyState === 'open' &&
364 (appendedLastSegment || bufferedToEnd); 279 (appendedLastSegment || bufferedToEnd);
365 }; 280 };
366 281
367 var parseCodecs = function(codecs) { 282 const parseCodecs = function(codecs) {
368 var result = { 283 let result = {
369 codecCount: 0, 284 codecCount: 0,
370 videoCodec: null, 285 videoCodec: null,
371 audioProfile: null 286 audioProfile: null
...@@ -375,1223 +290,1308 @@ var parseCodecs = function(codecs) { ...@@ -375,1223 +290,1308 @@ var parseCodecs = function(codecs) {
375 result.codecCount = result.codecCount || 2; 290 result.codecCount = result.codecCount || 2;
376 291
377 // parse the video codec but ignore the version 292 // parse the video codec but ignore the version
378 result.videoCodec = /(^|\s|,)+(avc1)[^ ,]*/i.exec(codecs); 293 result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs);
379 result.videoCodec = result.videoCodec && result.videoCodec[2]; 294 result.videoCodec = result.videoCodec && result.videoCodec[2];
380 295
381 // parse the last field of the audio codec 296 // parse the last field of the audio codec
382 result.audioProfile = /(^|\s|,)+mp4a.\d+\.(\d+)/i.exec(codecs); 297 result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs);
383 result.audioProfile = result.audioProfile && result.audioProfile[2]; 298 result.audioProfile = result.audioProfile && result.audioProfile[2];
384 299
385 return result; 300 return result;
386 }; 301 };
387 302
388 /** 303 const filterBufferedRanges = function(predicate) {
389 * Blacklist playlists that are known to be codec or 304 return function(time) {
390 * stream-incompatible with the SourceBuffer configuration. For 305 let i;
391 * instance, Media Source Extensions would cause the video element to 306 let ranges = [];
392 * stall waiting for video data if you switched from a variant with 307 let tech = this.tech_;
393 * video and audio to an audio-only one. 308 // !!The order of the next two assignments is important!!
394 * 309 // `currentTime` must be equal-to or greater-than the start of the
395 * @param media {object} a media playlist compatible with the current 310 // buffered range. Flash executes out-of-process so, every value can
396 * set of SourceBuffers. Variants in the current master playlist that 311 // change behind the scenes from line-to-line. By reading `currentTime`
397 * do not appear to have compatible codec or stream configurations 312 // after `buffered`, we ensure that it is always a current or later
398 * will be excluded from the default playlist selection algorithm 313 // value during playback.
399 * indefinitely. 314 let buffered = tech.buffered();
400 */ 315
401 videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) { 316 if (typeof time === 'undefined') {
402 var 317 time = tech.currentTime();
403 master = this.playlists.master,
404 codecCount = 2,
405 videoCodec = null,
406 audioProfile = null,
407 codecs;
408
409 if (media.attributes && media.attributes.CODECS) {
410 codecs = parseCodecs(media.attributes.CODECS);
411 videoCodec = codecs.videoCodec;
412 audioProfile = codecs.audioProfile;
413 codecCount = codecs.codecCount;
414 }
415 master.playlists.forEach(function(variant) {
416 var variantCodecs = {
417 codecCount: 2,
418 videoCodec: null,
419 audioProfile: null
420 };
421
422 if (variant.attributes && variant.attributes.CODECS) {
423 variantCodecs = parseCodecs(variant.attributes.CODECS);
424 } 318 }
425 319
426 // if the streams differ in the presence or absence of audio or 320 if (buffered && buffered.length) {
427 // video, they are incompatible 321 // Search for a range containing the play-head
428 if (variantCodecs.codecCount !== codecCount) { 322 for (i = 0; i < buffered.length; i++) {
429 variant.excludeUntil = Infinity; 323 if (predicate(buffered.start(i), buffered.end(i), time)) {
324 ranges.push([buffered.start(i), buffered.end(i)]);
325 }
326 }
430 } 327 }
431 328
432 // if h.264 is specified on the current playlist, some flavor of 329 return videojs.createTimeRanges(ranges);
433 // it must be specified on all compatible variants 330 };
434 if (variantCodecs.videoCodec !== videoCodec) {
435 variant.excludeUntil = Infinity;
436 }
437 // HE-AAC ("mp4a.40.5") is incompatible with all other versions of
438 // AAC audio in Chrome 46. Don't mix the two.
439 if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
440 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
441 variant.excludeUntil = Infinity;
442 }
443 });
444 }; 331 };
445 332
446 videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { 333 export default class HlsHandler extends Component {
447 var media = this.playlists.media(), mimeType; 334 constructor(tech, options) {
335 super(tech);
336 let _player;
448 337
449 // wait until a media playlist is available and the Media Source is 338 // tech.player() is deprecated but setup a reference to HLS for
450 // attached 339 // backwards-compatibility
451 if (!media || this.mediaSource.readyState !== 'open') { 340 if (tech.options_ && tech.options_.playerId) {
452 return; 341 _player = videojs(tech.options_.playerId);
453 } 342 if (!_player.hls) {
454 343 Object.defineProperty(_player, 'hls', {
455 // if the codecs were explicitly specified, pass them along to the 344 get: () => {
456 // source buffer 345 videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.');
457 mimeType = 'video/mp2t'; 346 return this;
458 if (media.attributes && media.attributes.CODECS) { 347 }
459 mimeType += '; codecs="' + media.attributes.CODECS + '"'; 348 });
460 } 349 }
461 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType); 350 }
351 this.tech_ = tech;
352 this.source_ = options.source;
353 this.mode_ = options.mode;
354 // the segment info object for a segment that is in the process of
355 // being downloaded or processed
356 this.pendingSegment_ = null;
462 357
463 // exclude any incompatible variant streams from future playlist 358 // start playlist selection at a reasonable bandwidth for
464 // selection 359 // broadband internet
465 this.excludeIncompatibleVariants_(media); 360 // 0.5 Mbps
361 this.bandwidth = options.bandwidth || 4194304;
362 this.bytesReceived = 0;
466 363
467 // transition the sourcebuffer to the ended state if we've hit the end of 364 // loadingState_ tracks how far along the buffering process we
468 // the playlist 365 // have been given permission to proceed. There are three possible
469 this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this)); 366 // values:
470 }; 367 // - none: do not load playlists or segments
368 // - meta: load playlists but not segments
369 // - segments: load everything
370 this.loadingState_ = 'none';
371 if (this.tech_.preload() !== 'none') {
372 this.loadingState_ = 'meta';
373 }
471 374
472 /** 375 // periodically check if new data needs to be downloaded or
473 * Seek to the latest media position if this is a live video and the 376 // buffered data should be appended to the source buffer
474 * player and video are loaded and initialized. 377 this.startCheckingBuffer_();
475 */
476 videojs.HlsHandler.prototype.setupFirstPlay = function() {
477 var seekable, media;
478 media = this.playlists.media();
479 378
379 this.on(this.tech_, 'seeking', function() {
380 this.setCurrentTime(this.tech_.currentTime());
381 });
382 this.on(this.tech_, 'error', function() {
383 this.stopCheckingBuffer_();
384 });
480 385
481 // check that everything is ready to begin buffering 386 this.on(this.tech_, 'play', this.play);
387 }
388 src(src) {
389 let oldMediaPlaylist;
482 390
483 // 1) the video is a live stream of unknown duration 391 // do nothing if the src is falsey
484 if (this.duration() === Infinity && 392 if (!src) {
393 return;
394 }
485 395
486 // 2) the player has not played before and is not paused 396 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ });
487 this.tech_.played().length === 0 &&
488 !this.tech_.paused() &&
489 397
490 // 3) the Media Source and Source Buffers are ready 398 // load the MediaSource into the player
491 this.sourceBuffer && 399 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
492 400
493 // 4) the active media playlist is available 401 this.options_ = {};
494 media && 402 if (typeof this.source_.withCredentials !== 'undefined') {
403 this.options_.withCredentials = this.source_.withCredentials;
404 } else if (videojs.options.hls) {
405 this.options_.withCredentials = videojs.options.hls.withCredentials;
406 }
407 this.playlists = new Hls.PlaylistLoader(this.source_.src,
408 this.options_.withCredentials);
495 409
496 // 5) the video element or flash player is in a readyState of 410 this.tech_.one('canplay', this.setupFirstPlay.bind(this));
497 // at least HAVE_FUTURE_DATA
498 this.tech_.readyState() >= 1) {
499 411
500 // trigger the playlist loader to start "expired time"-tracking 412 this.playlists.on('loadedmetadata', () => {
501 this.playlists.trigger('firstplay'); 413 oldMediaPlaylist = this.playlists.media();
502 414
503 // seek to the latest media position for live videos 415 // if this isn't a live video and preload permits, start
504 seekable = this.seekable(); 416 // downloading segments
505 if (seekable.length) { 417 if (oldMediaPlaylist.endList &&
506 this.tech_.setCurrentTime(seekable.end(0)); 418 this.tech_.preload() !== 'metadata' &&
507 } 419 this.tech_.preload() !== 'none') {
508 } 420 this.loadingState_ = 'segments';
509 }; 421 }
510 422
511 /** 423 this.setupSourceBuffer_();
512 * Begin playing the video. 424 this.setupFirstPlay();
513 */ 425 this.fillBuffer();
514 videojs.HlsHandler.prototype.play = function() { 426 this.tech_.trigger('loadedmetadata');
515 this.loadingState_ = 'segments'; 427 });
516 428
517 if (this.tech_.ended()) { 429 this.playlists.on('error', () => {
518 this.tech_.setCurrentTime(0); 430 this.blacklistCurrentPlaylist_(this.playlists.error);
519 } 431 });
432
433 this.playlists.on('loadedplaylist', () => {
434 let updatedPlaylist = this.playlists.media();
435 let seekable;
436
437 if (!updatedPlaylist) {
438 // select the initial variant
439 this.playlists.media(this.selectPlaylist());
440 return;
441 }
442
443 this.updateDuration(this.playlists.media());
444
445 // update seekable
446 seekable = this.seekable();
447 if (this.duration() === Infinity &&
448 seekable.length !== 0) {
449 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
450 }
451
452 oldMediaPlaylist = updatedPlaylist;
453 });
520 454
521 if (this.tech_.played().length === 0) { 455 this.playlists.on('mediachange', () => {
522 return this.setupFirstPlay(); 456 this.tech_.trigger({
457 type: 'mediachange',
458 bubbles: true
459 });
460 });
461
462 // do nothing if the tech has been disposed already
463 // this can occur if someone sets the src in player.ready(), for instance
464 if (!this.tech_.el()) {
465 return;
466 }
467
468 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
523 } 469 }
470 handleSourceOpen() {
471 // Only attempt to create the source buffer if none already exist.
472 // handleSourceOpen is also called when we are "re-opening" a source buffer
473 // after `endOfStream` has been called (in response to a seek for instance)
474 if (!this.sourceBuffer) {
475 this.setupSourceBuffer_();
476 }
524 477
525 // if the viewer has paused and we fell out of the live window, 478 // if autoplay is enabled, begin playback. This is duplicative of
526 // seek forward to the earliest available position 479 // code in video.js but is required because play() must be invoked
527 if (this.duration() === Infinity) { 480 // *after* the media source has opened.
528 if (this.tech_.currentTime() < this.seekable().start(0)) { 481 // NOTE: moving this invocation of play() after
529 this.tech_.setCurrentTime(this.seekable().start(0)); 482 // sourceBuffer.appendBuffer() below caused live streams with
483 // autoplay to stall
484 if (this.tech_.autoplay()) {
485 this.play();
530 } 486 }
531 } 487 }
532 };
533 488
534 videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { 489 /**
535 var 490 * Blacklist playlists that are known to be codec or
536 buffered = this.findBufferedRange_(); 491 * stream-incompatible with the SourceBuffer configuration. For
492 * instance, Media Source Extensions would cause the video element to
493 * stall waiting for video data if you switched from a variant with
494 * video and audio to an audio-only one.
495 *
496 * @param media {object} a media playlist compatible with the current
497 * set of SourceBuffers. Variants in the current master playlist that
498 * do not appear to have compatible codec or stream configurations
499 * will be excluded from the default playlist selection algorithm
500 * indefinitely.
501 */
502 excludeIncompatibleVariants_(media) {
503 let master = this.playlists.master;
504 let codecCount = 2;
505 let videoCodec = null;
506 let audioProfile = null;
507 let codecs;
508
509 if (media.attributes && media.attributes.CODECS) {
510 codecs = parseCodecs(media.attributes.CODECS);
511 videoCodec = codecs.videoCodec;
512 audioProfile = codecs.audioProfile;
513 codecCount = codecs.codecCount;
514 }
515 master.playlists.forEach(function(variant) {
516 let variantCodecs = {
517 codecCount: 2,
518 videoCodec: null,
519 audioProfile: null
520 };
521
522 if (variant.attributes && variant.attributes.CODECS) {
523 variantCodecs = parseCodecs(variant.attributes.CODECS);
524 }
537 525
538 if (!(this.playlists && this.playlists.media())) { 526 // if the streams differ in the presence or absence of audio or
539 // return immediately if the metadata is not ready yet 527 // video, they are incompatible
540 return 0; 528 if (variantCodecs.codecCount !== codecCount) {
541 } 529 variant.excludeUntil = Infinity;
530 }
542 531
543 // it's clearly an edge-case but don't thrown an error if asked to 532 // if h.264 is specified on the current playlist, some flavor of
544 // seek within an empty playlist 533 // it must be specified on all compatible variants
545 if (!this.playlists.media().segments) { 534 if (variantCodecs.videoCodec !== videoCodec) {
546 return 0; 535 variant.excludeUntil = Infinity;
536 }
537 // HE-AAC ("mp4a.40.5") is incompatible with all other versions of
538 // AAC audio in Chrome 46. Don't mix the two.
539 if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
540 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
541 variant.excludeUntil = Infinity;
542 }
543 });
547 } 544 }
548 545
549 // if the seek location is already buffered, continue buffering as 546 setupSourceBuffer_() {
550 // usual 547 let media = this.playlists.media();
551 if (buffered && buffered.length) { 548 let mimeType;
552 return currentTime;
553 }
554 549
555 // if we are in the middle of appending a segment, let it finish up 550 // wait until a media playlist is available and the Media Source is
556 if (this.pendingSegment_ && this.pendingSegment_.buffered) { 551 // attached
557 return currentTime; 552 if (!media || this.mediaSource.readyState !== 'open') {
558 } 553 return;
554 }
559 555
560 this.lastSegmentLoaded_ = null; 556 // if the codecs were explicitly specified, pass them along to the
557 // source buffer
558 mimeType = 'video/mp2t';
559 if (media.attributes && media.attributes.CODECS) {
560 mimeType += '; codecs="' + media.attributes.CODECS + '"';
561 }
562 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
561 563
562 // cancel outstanding requests and buffer appends 564 // exclude any incompatible variant streams from future playlist
563 this.cancelSegmentXhr(); 565 // selection
566 this.excludeIncompatibleVariants_(media);
564 567
565 // abort outstanding key requests, if necessary 568 // transition the sourcebuffer to the ended state if we've hit the end of
566 if (this.keyXhr_) { 569 // the playlist
567 this.keyXhr_.aborted = true; 570 this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this));
568 this.cancelKeyXhr();
569 } 571 }
570 572
571 // begin filling the buffer at the new position 573 /**
572 this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime)); 574 * Seek to the latest media position if this is a live video and the
573 }; 575 * player and video are loaded and initialized.
576 */
577 setupFirstPlay() {
578 let seekable;
579 let media = this.playlists.media();
574 580
575 videojs.HlsHandler.prototype.duration = function() { 581 // check that everything is ready to begin buffering
576 var
577 playlists = this.playlists;
578 582
579 if (!playlists) { 583 // 1) the video is a live stream of unknown duration
580 return 0; 584 if (this.duration() === Infinity &&
581 }
582 585
583 if (this.mediaSource) { 586 // 2) the player has not played before and is not paused
584 return this.mediaSource.duration; 587 this.tech_.played().length === 0 &&
585 } 588 !this.tech_.paused() &&
586 589
587 return videojs.Hls.Playlist.duration(playlists.media()); 590 // 3) the Media Source and Source Buffers are ready
588 }; 591 this.sourceBuffer &&
589 592
590 videojs.HlsHandler.prototype.seekable = function() { 593 // 4) the active media playlist is available
591 var media, seekable; 594 media &&
592 595
593 if (!this.playlists) { 596 // 5) the video element or flash player is in a readyState of
594 return videojs.createTimeRanges(); 597 // at least HAVE_FUTURE_DATA
595 } 598 this.tech_.readyState() >= 1) {
596 media = this.playlists.media();
597 if (!media) {
598 return videojs.createTimeRanges();
599 }
600 599
601 seekable = videojs.Hls.Playlist.seekable(media); 600 // trigger the playlist loader to start "expired time"-tracking
602 if (seekable.length === 0) { 601 this.playlists.trigger('firstplay');
603 return seekable;
604 }
605 602
606 // if the seekable start is zero, it may be because the player has 603 // seek to the latest media position for live videos
607 // been paused for a long time and stopped buffering. in that case, 604 seekable = this.seekable();
608 // fall back to the playlist loader's running estimate of expired 605 if (seekable.length) {
609 // time 606 this.tech_.setCurrentTime(seekable.end(0));
610 if (seekable.start(0) === 0) { 607 }
611 return videojs.createTimeRanges([[ 608 }
612 this.playlists.expired_,
613 this.playlists.expired_ + seekable.end(0)
614 ]]);
615 } 609 }
616 610
617 // seekable has been calculated based on buffering video data so it 611 /**
618 // can be returned directly 612 * Begin playing the video.
619 return seekable; 613 */
620 }; 614 play() {
615 this.loadingState_ = 'segments';
621 616
622 /** 617 if (this.tech_.ended()) {
623 * Update the player duration 618 this.tech_.setCurrentTime(0);
624 */ 619 }
625 videojs.HlsHandler.prototype.updateDuration = function(playlist) {
626 var oldDuration = this.mediaSource.duration,
627 newDuration = videojs.Hls.Playlist.duration(playlist),
628 buffered = this.tech_.buffered(),
629 setDuration = function() {
630 this.mediaSource.duration = newDuration;
631 this.tech_.trigger('durationchange');
632 620
633 this.mediaSource.removeEventListener('sourceopen', setDuration); 621 if (this.tech_.played().length === 0) {
634 }.bind(this); 622 return this.setupFirstPlay();
623 }
635 624
636 if (buffered.length > 0) { 625 // if the viewer has paused and we fell out of the live window,
637 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); 626 // seek forward to the earliest available position
627 if (this.duration() === Infinity) {
628 if (this.tech_.currentTime() < this.seekable().start(0)) {
629 this.tech_.setCurrentTime(this.seekable().start(0));
630 }
631 }
638 } 632 }
639 633
640 // if the duration has changed, invalidate the cached value 634 setCurrentTime(currentTime) {
641 if (oldDuration !== newDuration) { 635 let buffered = this.findBufferedRange_();
642 // update the duration 636
643 if (this.mediaSource.readyState !== 'open') { 637 if (!(this.playlists && this.playlists.media())) {
644 this.mediaSource.addEventListener('sourceopen', setDuration); 638 // return immediately if the metadata is not ready yet
645 } else if (!this.sourceBuffer || !this.sourceBuffer.updating) { 639 return 0;
646 this.mediaSource.duration = newDuration;
647 this.tech_.trigger('durationchange');
648 } 640 }
649 }
650 };
651 641
652 /** 642 // it's clearly an edge-case but don't thrown an error if asked to
653 * Clear all buffers and reset any state relevant to the current 643 // seek within an empty playlist
654 * source. After this function is called, the tech should be in a 644 if (!this.playlists.media().segments) {
655 * state suitable for switching to a different video. 645 return 0;
656 */ 646 }
657 videojs.HlsHandler.prototype.resetSrc_ = function() {
658 this.cancelSegmentXhr();
659 this.cancelKeyXhr();
660 647
661 if (this.sourceBuffer && this.mediaSource.readyState === 'open') { 648 // if the seek location is already buffered, continue buffering as
662 this.sourceBuffer.abort(); 649 // usual
663 } 650 if (buffered && buffered.length) {
664 }; 651 return currentTime;
652 }
665 653
666 videojs.HlsHandler.prototype.cancelKeyXhr = function() { 654 // if we are in the middle of appending a segment, let it finish up
667 if (this.keyXhr_) { 655 if (this.pendingSegment_ && this.pendingSegment_.buffered) {
668 this.keyXhr_.onreadystatechange = null; 656 return currentTime;
669 this.keyXhr_.abort(); 657 }
670 this.keyXhr_ = null;
671 }
672 };
673 658
674 videojs.HlsHandler.prototype.cancelSegmentXhr = function() { 659 this.lastSegmentLoaded_ = null;
675 if (this.segmentXhr_) {
676 // Prevent error handler from running.
677 this.segmentXhr_.onreadystatechange = null;
678 this.segmentXhr_.abort();
679 this.segmentXhr_ = null;
680 }
681 660
682 // clear out the segment being processed 661 // cancel outstanding requests and buffer appends
683 this.pendingSegment_ = null; 662 this.cancelSegmentXhr();
684 };
685 663
686 /** 664 // abort outstanding key requests, if necessary
687 * Returns the CSS value for the specified property on an element 665 if (this.keyXhr_) {
688 * using `getComputedStyle`. Firefox has a long-standing issue where 666 this.keyXhr_.aborted = true;
689 * getComputedStyle() may return null when running in an iframe with 667 this.cancelKeyXhr();
690 * `display: none`. 668 }
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 669
699 result = getComputedStyle(el); 670 // begin filling the buffer at the new position
700 if (!result) { 671 this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime));
701 return '';
702 } 672 }
703 673
704 return result[property]; 674 duration() {
705 }; 675 let playlists = this.playlists;
706 676
707 /** 677 if (!playlists) {
708 * Abort all outstanding work and cleanup. 678 return 0;
709 */ 679 }
710 videojs.HlsHandler.prototype.dispose = function() { 680
711 this.stopCheckingBuffer_(); 681 if (this.mediaSource) {
682 return this.mediaSource.duration;
683 }
712 684
713 if (this.playlists) { 685 return Hls.Playlist.duration(playlists.media());
714 this.playlists.dispose();
715 } 686 }
716 687
717 this.resetSrc_(); 688 seekable() {
718 Component.prototype.dispose.call(this); 689 let media;
719 }; 690 let seekable;
720 691
721 /** 692 if (!this.playlists) {
722 * Chooses the appropriate media playlist based on the current 693 return videojs.createTimeRanges();
723 * bandwidth estimate and the player size. 694 }
724 * @return the highest bitrate playlist less than the currently detected 695 media = this.playlists.media();
725 * bandwidth, accounting for some amount of bandwidth variance 696 if (!media) {
726 */ 697 return videojs.createTimeRanges();
727 videojs.HlsHandler.prototype.selectPlaylist = function () { 698 }
728 var
729 effectiveBitrate,
730 sortedPlaylists = this.playlists.master.playlists.slice(),
731 bandwidthPlaylists = [],
732 now = +new Date(),
733 i,
734 variant,
735 bandwidthBestVariant,
736 resolutionPlusOne,
737 resolutionBestVariant,
738 width,
739 height;
740
741 sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth);
742
743 // filter out any playlists that have been excluded due to
744 // incompatible configurations or playback errors
745 sortedPlaylists = sortedPlaylists.filter(function(variant) {
746 if (variant.excludeUntil !== undefined) {
747 return now >= variant.excludeUntil;
748 }
749 return true;
750 });
751 699
752 // filter out any variant that has greater effective bitrate 700 seekable = Hls.Playlist.seekable(media);
753 // than the current estimated bandwidth 701 if (seekable.length === 0) {
754 i = sortedPlaylists.length; 702 return seekable;
755 while (i--) { 703 }
756 variant = sortedPlaylists[i];
757 704
758 // ignore playlists without bandwidth information 705 // if the seekable start is zero, it may be because the player has
759 if (!variant.attributes || !variant.attributes.BANDWIDTH) { 706 // been paused for a long time and stopped buffering. in that case,
760 continue; 707 // fall back to the playlist loader's running estimate of expired
708 // time
709 if (seekable.start(0) === 0) {
710 return videojs.createTimeRanges([[this.playlists.expired_,
711 this.playlists.expired_ + seekable.end(0)]]);
761 } 712 }
762 713
763 effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance; 714 // seekable has been calculated based on buffering video data so it
715 // can be returned directly
716 return seekable;
717 }
718
719 /**
720 * Update the player duration
721 */
722 updateDuration(playlist) {
723 let oldDuration = this.mediaSource.duration;
724 let newDuration = Hls.Playlist.duration(playlist);
725 let buffered = this.tech_.buffered();
726 let setDuration = () => {
727 this.mediaSource.duration = newDuration;
728 this.tech_.trigger('durationchange');
764 729
765 if (effectiveBitrate < this.bandwidth) { 730 this.mediaSource.removeEventListener('sourceopen', setDuration);
766 bandwidthPlaylists.push(variant); 731 };
767 732
768 // since the playlists are sorted in ascending order by 733 if (buffered.length > 0) {
769 // bandwidth, the first viable variant is the best 734 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
770 if (!bandwidthBestVariant) { 735 }
771 bandwidthBestVariant = variant; 736
737 // if the duration has changed, invalidate the cached value
738 if (oldDuration !== newDuration) {
739 // update the duration
740 if (this.mediaSource.readyState !== 'open') {
741 this.mediaSource.addEventListener('sourceopen', setDuration);
742 } else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
743 this.mediaSource.duration = newDuration;
744 this.tech_.trigger('durationchange');
772 } 745 }
773 } 746 }
774 } 747 }
775 748
776 i = bandwidthPlaylists.length; 749 /**
777 750 * Clear all buffers and reset any state relevant to the current
778 // sort variants by resolution 751 * source. After this function is called, the tech should be in a
779 bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution); 752 * state suitable for switching to a different video.
780 753 */
781 // forget our old variant from above, or we might choose that in high-bandwidth scenarios 754 resetSrc_() {
782 // (this could be the lowest bitrate rendition as we go through all of them above) 755 this.cancelSegmentXhr();
783 variant = null; 756 this.cancelKeyXhr();
784 757
785 width = parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10); 758 if (this.sourceBuffer && this.mediaSource.readyState === 'open') {
786 height = parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10); 759 this.sourceBuffer.abort();
787
788 // iterate through the bandwidth-filtered playlists and find
789 // best rendition by player dimension
790 while (i--) {
791 variant = bandwidthPlaylists[i];
792
793 // ignore playlists without resolution information
794 if (!variant.attributes ||
795 !variant.attributes.RESOLUTION ||
796 !variant.attributes.RESOLUTION.width ||
797 !variant.attributes.RESOLUTION.height) {
798 continue;
799 }
800
801 // since the playlists are sorted, the first variant that has
802 // dimensions less than or equal to the player size is the best
803
804 if (variant.attributes.RESOLUTION.width === width &&
805 variant.attributes.RESOLUTION.height === height) {
806 // if we have the exact resolution as the player use it
807 resolutionPlusOne = null;
808 resolutionBestVariant = variant;
809 break;
810 } else if (variant.attributes.RESOLUTION.width < width &&
811 variant.attributes.RESOLUTION.height < height) {
812 // if both dimensions are less than the player use the
813 // previous (next-largest) variant
814 break;
815 } else if (!resolutionPlusOne ||
816 (variant.attributes.RESOLUTION.width < resolutionPlusOne.attributes.RESOLUTION.width &&
817 variant.attributes.RESOLUTION.height < resolutionPlusOne.attributes.RESOLUTION.height)) {
818 // If we still haven't found a good match keep a
819 // reference to the previous variant for the next loop
820 // iteration
821
822 // By only saving variants if they are smaller than the
823 // previously saved variant, we ensure that we also pick
824 // the highest bandwidth variant that is just-larger-than
825 // the video player
826 resolutionPlusOne = variant;
827 } 760 }
828 } 761 }
829 762
830 // fallback chain of variants 763 cancelKeyXhr() {
831 return resolutionPlusOne || resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0]; 764 if (this.keyXhr_) {
832 }; 765 this.keyXhr_.onreadystatechange = null;
833 766 this.keyXhr_.abort();
834 /** 767 this.keyXhr_ = null;
835 * Periodically request new segments and append video data. 768 }
836 */
837 videojs.HlsHandler.prototype.checkBuffer_ = function() {
838 // calling this method directly resets any outstanding buffer checks
839 if (this.checkBufferTimeout_) {
840 window.clearTimeout(this.checkBufferTimeout_);
841 this.checkBufferTimeout_ = null;
842 } 769 }
843 770
844 this.fillBuffer(); 771 cancelSegmentXhr() {
845 this.drainBuffer(); 772 if (this.segmentXhr_) {
773 // Prevent error handler from running.
774 this.segmentXhr_.onreadystatechange = null;
775 this.segmentXhr_.abort();
776 this.segmentXhr_ = null;
777 }
846 778
847 // wait awhile and try again 779 // clear out the segment being processed
848 this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this), 780 this.pendingSegment_ = null;
849 bufferCheckInterval); 781 }
850 };
851 782
852 /** 783 /**
853 * Setup a periodic task to request new segments if necessary and 784 * Abort all outstanding work and cleanup.
854 * append bytes into the SourceBuffer. 785 */
855 */ 786 dispose() {
856 videojs.HlsHandler.prototype.startCheckingBuffer_ = function() { 787 this.stopCheckingBuffer_();
857 this.checkBuffer_();
858 };
859 788
860 /** 789 if (this.playlists) {
861 * Stop the periodic task requesting new segments and feeding the 790 this.playlists.dispose();
862 * SourceBuffer. 791 }
863 */ 792
864 videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() { 793 this.resetSrc_();
865 if (this.checkBufferTimeout_) { 794 super.dispose();
866 window.clearTimeout(this.checkBufferTimeout_);
867 this.checkBufferTimeout_ = null;
868 } 795 }
869 };
870 796
871 var filterBufferedRanges = function(predicate) { 797 /**
872 return function(time) { 798 * Chooses the appropriate media playlist based on the current
873 var 799 * bandwidth estimate and the player size.
874 i, 800 * @return the highest bitrate playlist less than the currently detected
875 ranges = [], 801 * bandwidth, accounting for some amount of bandwidth variance
876 tech = this.tech_, 802 */
877 // !!The order of the next two assignments is important!! 803 selectPlaylist() {
878 // `currentTime` must be equal-to or greater-than the start of the 804 let effectiveBitrate;
879 // buffered range. Flash executes out-of-process so, every value can 805 let sortedPlaylists = this.playlists.master.playlists.slice();
880 // change behind the scenes from line-to-line. By reading `currentTime` 806 let bandwidthPlaylists = [];
881 // after `buffered`, we ensure that it is always a current or later 807 let now = +new Date();
882 // value during playback. 808 let i;
883 buffered = tech.buffered(); 809 let variant;
884 810 let bandwidthBestVariant;
885 811 let resolutionPlusOne;
886 if (time === undefined) { 812 let resolutionPlusOneAttribute;
887 time = tech.currentTime(); 813 let resolutionBestVariant;
888 } 814 let width;
815 let height;
816
817 sortedPlaylists.sort(Hls.comparePlaylistBandwidth);
818
819 // filter out any playlists that have been excluded due to
820 // incompatible configurations or playback errors
821 sortedPlaylists = sortedPlaylists.filter((localVariant) => {
822 if (typeof localVariant.excludeUntil !== 'undefined') {
823 return now >= localVariant.excludeUntil;
824 }
825 return true;
826 });
889 827
890 if (buffered && buffered.length) { 828 // filter out any variant that has greater effective bitrate
891 // Search for a range containing the play-head 829 // than the current estimated bandwidth
892 for (i = 0; i < buffered.length; i++) { 830 i = sortedPlaylists.length;
893 if (predicate(buffered.start(i), buffered.end(i), time)) { 831 while (i--) {
894 ranges.push([buffered.start(i), buffered.end(i)]); 832 variant = sortedPlaylists[i];
833
834 // ignore playlists without bandwidth information
835 if (!variant.attributes || !variant.attributes.BANDWIDTH) {
836 continue;
837 }
838
839 effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
840
841 if (effectiveBitrate < this.bandwidth) {
842 bandwidthPlaylists.push(variant);
843
844 // since the playlists are sorted in ascending order by
845 // bandwidth, the first viable variant is the best
846 if (!bandwidthBestVariant) {
847 bandwidthBestVariant = variant;
895 } 848 }
896 } 849 }
897 } 850 }
898 851
899 return videojs.createTimeRanges(ranges); 852 i = bandwidthPlaylists.length;
900 };
901 };
902 853
903 /** 854 // sort variants by resolution
904 * Attempts to find the buffered TimeRange that contains the specified 855 bandwidthPlaylists.sort(Hls.comparePlaylistResolution);
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 856
916 /** 857 // forget our old variant from above,
917 * Returns the TimeRanges that begin at or later than the specified 858 // or we might choose that in high-bandwidth scenarios
918 * time. 859 // (this could be the lowest bitrate rendition as we go through all of them above)
919 * @param time (optional) {number} the time to filter on. Defaults to 860 variant = null;
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 861
927 /** 862 width = parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10);
928 * Determines whether there is enough video data currently in the buffer 863 height = parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10);
929 * and downloads a new segment if the buffered time is less than the goal. 864
930 * @param seekToTime (optional) {number} the offset into the downloaded segment 865 // iterate through the bandwidth-filtered playlists and find
931 * to seek to, in seconds 866 // best rendition by player dimension
932 */ 867 while (i--) {
933 videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { 868 variant = bandwidthPlaylists[i];
934 var 869
935 tech = this.tech_, 870 // ignore playlists without resolution information
936 currentTime = tech.currentTime(), 871 if (!variant.attributes ||
937 hasBufferedContent = (this.tech_.buffered().length !== 0), 872 !variant.attributes.RESOLUTION ||
938 currentBuffered = this.findBufferedRange_(), 873 !variant.attributes.RESOLUTION.width ||
939 outsideBufferedRanges = !(currentBuffered && currentBuffered.length), 874 !variant.attributes.RESOLUTION.height) {
940 currentBufferedEnd = 0, 875 continue;
941 bufferedTime = 0, 876 }
942 segment, 877
943 segmentInfo, 878 // since the playlists are sorted, the first variant that has
944 segmentTimestampOffset; 879 // dimensions less than or equal to the player size is the best
945 880
946 // if preload is set to "none", do not download segments until playback is requested 881 let variantResolution = variant.attributes.RESOLUTION;
947 if (this.loadingState_ !== 'segments') { 882
948 return; 883 if (variantResolution.width === width &&
884 variantResolution.height === height) {
885 // if we have the exact resolution as the player use it
886 resolutionPlusOne = null;
887 resolutionBestVariant = variant;
888 break;
889 } else if (variantResolution.width < width &&
890 variantResolution.height < height) {
891 // if both dimensions are less than the player use the
892 // previous (next-largest) variant
893 break;
894 } else if (!resolutionPlusOne ||
895 (variantResolution.width < resolutionPlusOneAttribute.width &&
896 variantResolution.height < resolutionPlusOneAttribute.height)) {
897 // If we still haven't found a good match keep a
898 // reference to the previous variant for the next loop
899 // iteration
900
901 // By only saving variants if they are smaller than the
902 // previously saved variant, we ensure that we also pick
903 // the highest bandwidth variant that is just-larger-than
904 // the video player
905 resolutionPlusOne = variant;
906 resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION;
907 }
908 }
909
910 // fallback chain of variants
911 return resolutionPlusOne ||
912 resolutionBestVariant ||
913 bandwidthBestVariant ||
914 sortedPlaylists[0];
949 } 915 }
950 916
951 // if a video has not been specified, do nothing 917 /**
952 if (!tech.currentSrc() || !this.playlists) { 918 * Periodically request new segments and append video data.
953 return; 919 */
920 checkBuffer_() {
921 // calling this method directly resets any outstanding buffer checks
922 if (this.checkBufferTimeout_) {
923 window.clearTimeout(this.checkBufferTimeout_);
924 this.checkBufferTimeout_ = null;
925 }
926
927 this.fillBuffer();
928 this.drainBuffer();
929
930 // wait awhile and try again
931 this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this),
932 bufferCheckInterval);
954 } 933 }
955 934
956 // if there is a request already in flight, do nothing 935 /**
957 if (this.segmentXhr_) { 936 * Setup a periodic task to request new segments if necessary and
958 return; 937 * append bytes into the SourceBuffer.
938 */
939 startCheckingBuffer_() {
940 this.checkBuffer_();
959 } 941 }
960 942
961 // wait until the buffer is up to date 943 /**
962 if (this.pendingSegment_) { 944 * Stop the periodic task requesting new segments and feeding the
963 return; 945 * SourceBuffer.
946 */
947 stopCheckingBuffer_() {
948 if (this.checkBufferTimeout_) {
949 window.clearTimeout(this.checkBufferTimeout_);
950 this.checkBufferTimeout_ = null;
951 }
964 } 952 }
965 953
966 // if no segments are available, do nothing 954 /**
967 if (this.playlists.state === "HAVE_NOTHING" || 955 * Determines whether there is enough video data currently in the buffer
968 !this.playlists.media() || 956 * and downloads a new segment if the buffered time is less than the goal.
969 !this.playlists.media().segments) { 957 * @param seekToTime (optional) {number} the offset into the downloaded segment
970 return; 958 * to seek to, in seconds
971 } 959 */
960 fillBuffer(mediaIndex) {
961 let tech = this.tech_;
962 let currentTime = tech.currentTime();
963 let hasBufferedContent = (this.tech_.buffered().length !== 0);
964 let currentBuffered = this.findBufferedRange_();
965 let outsideBufferedRanges = !(currentBuffered && currentBuffered.length);
966 let currentBufferedEnd = 0;
967 let bufferedTime = 0;
968 let segment;
969 let segmentInfo;
970 let segmentTimestampOffset;
971
972 // if preload is set to "none", do not download segments until playback is requested
973 if (this.loadingState_ !== 'segments') {
974 return;
975 }
976
977 // if a video has not been specified, do nothing
978 if (!tech.currentSrc() || !this.playlists) {
979 return;
980 }
981
982 // if there is a request already in flight, do nothing
983 if (this.segmentXhr_) {
984 return;
985 }
986
987 // wait until the buffer is up to date
988 if (this.pendingSegment_) {
989 return;
990 }
991
992 // if no segments are available, do nothing
993 if (this.playlists.state === 'HAVE_NOTHING' ||
994 !this.playlists.media() ||
995 !this.playlists.media().segments) {
996 return;
997 }
972 998
973 // if a playlist switch is in progress, wait for it to finish 999 // if a playlist switch is in progress, wait for it to finish
974 if (this.playlists.state === 'SWITCHING_MEDIA') { 1000 if (this.playlists.state === 'SWITCHING_MEDIA') {
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);
982 bufferedTime = Math.max(0, currentBufferedEnd - currentTime); 1008 bufferedTime = Math.max(0, currentBufferedEnd - currentTime);
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;
1014 }
1015 } else {
1016 mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
988 } 1017 }
989 } else {
990 mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
991 } 1018 }
992 } 1019 segment = this.playlists.media().segments[mediaIndex];
993 segment = this.playlists.media().segments[mediaIndex];
994 1020
995 // if the video has finished downloading 1021 // if the video has finished downloading
996 if (!segment) { 1022 if (!segment) {
997 return; 1023 return;
998 } 1024 }
999 1025
1000 // we have entered a state where we are fetching the same segment, 1026 // we have entered a state where we are fetching the same segment,
1001 // try to walk forward 1027 // try to walk forward
1002 if (this.lastSegmentLoaded_ && 1028 if (this.lastSegmentLoaded_ &&
1003 this.playlistUriToUrl(this.lastSegmentLoaded_.uri) === this.playlistUriToUrl(segment.uri) && 1029 this.playlistUriToUrl(this.lastSegmentLoaded_.uri) === this.playlistUriToUrl(segment.uri) &&
1004 this.lastSegmentLoaded_.byterange === segment.byterange) { 1030 this.lastSegmentLoaded_.byterange === segment.byterange) {
1005 return this.fillBuffer(mediaIndex + 1); 1031 return this.fillBuffer(mediaIndex + 1);
1006 } 1032 }
1007 1033
1008 // package up all the work to append the segment 1034 // package up all the work to append the segment
1009 segmentInfo = { 1035 segmentInfo = {
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
1022 encryptedBytes: null, 1048 encryptedBytes: null,
1023 // optionally, the decrypter that is unencrypting the segment 1049 // optionally, the decrypter that is unencrypting the segment
1024 decrypter: null, 1050 decrypter: null,
1025 // the state of the buffer before a segment is appended will be 1051 // the state of the buffer before a segment is appended will be
1026 // stored here so that the actual segment duration can be 1052 // stored here so that the actual segment duration can be
1027 // determined after it has been appended 1053 // determined after it has been appended
1028 buffered: null, 1054 buffered: null,
1029 // The target timestampOffset for this segment when we append it 1055 // The target timestampOffset for this segment when we append it
1030 // to the source buffer 1056 // to the source buffer
1031 timestampOffset: null 1057 timestampOffset: null
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
1039 if (this.tech_.seeking() && outsideBufferedRanges) { 1065 if (this.tech_.seeking() && outsideBufferedRanges) {
1040 // If there are discontinuities in the playlist, we can't be sure of anything 1066 // If there are discontinuities in the playlist, we can't be sure of anything
1041 // related to time so we reset the timestamp offset and start appending data 1067 // related to time so we reset the timestamp offset and start appending data
1042 // anew on every seek 1068 // anew on every seek
1043 if (segmentInfo.playlist.discontinuityStarts.length) { 1069 if (segmentInfo.playlist.discontinuityStarts.length) {
1070 segmentInfo.timestampOffset = segmentTimestampOffset;
1071 }
1072 } else if (segment.discontinuity && currentBuffered.length) {
1073 // If we aren't seeking and are crossing a discontinuity, we should set
1074 // timestampOffset for new segments to be appended the end of the current
1075 // buffered time-range
1076 segmentInfo.timestampOffset = currentBuffered.end(0);
1077 } else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) {
1078 // If we are trying to play at a position that is not zero but we aren't
1079 // currently seeking according to the video element
1044 segmentInfo.timestampOffset = segmentTimestampOffset; 1080 segmentInfo.timestampOffset = segmentTimestampOffset;
1045 } 1081 }
1046 } else if (segment.discontinuity && currentBuffered.length) { 1082
1047 // If we aren't seeking and are crossing a discontinuity, we should set 1083 this.loadSegment(segmentInfo);
1048 // timestampOffset for new segments to be appended the end of the current
1049 // buffered time-range
1050 segmentInfo.timestampOffset = currentBuffered.end(0);
1051 } else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) {
1052 // If we are trying to play at a position that is not zero but we aren't
1053 // currently seeking according to the video element
1054 segmentInfo.timestampOffset = segmentTimestampOffset;
1055 } 1084 }
1056 1085
1057 this.loadSegment(segmentInfo); 1086 playlistUriToUrl(segmentRelativeUrl) {
1058 }; 1087 let playListUrl;
1059 1088
1060 videojs.HlsHandler.prototype.playlistUriToUrl = function(segmentRelativeUrl) { 1089 // resolve the segment URL relative to the playlist
1061 var playListUrl; 1090 if (this.playlists.media().uri === this.source_.src) {
1062 // resolve the segment URL relative to the playlist 1091 playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl);
1063 if (this.playlists.media().uri === this.source_.src) { 1092 } else {
1064 playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl); 1093 playListUrl =
1065 } else { 1094 resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''),
1066 playListUrl = resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''), segmentRelativeUrl); 1095 segmentRelativeUrl);
1096 }
1097 return playListUrl;
1067 } 1098 }
1068 return playListUrl;
1069 };
1070 1099
1071 /* Turns segment byterange into a string suitable for use in 1100 /*
1072 * HTTP Range requests 1101 * Turns segment byterange into a string suitable for use in
1073 */ 1102 * HTTP Range requests
1074 videojs.HlsHandler.prototype.byterangeStr_ = function(byterange) { 1103 */
1075 var byterangeStart, byterangeEnd; 1104 byterangeStr_(byterange) {
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 };
1083
1084 /* Defines headers for use in the xhr request for a particular segment.
1085 */
1086 videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) {
1087 var headers = {};
1088 if ('byterange' in segment) {
1089 headers['Range'] = this.byterangeStr_(segment.byterange);
1090 } 1113 }
1091 return headers;
1092 };
1093 1114
1094 /* 1115 /*
1095 * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived. 1116 * Defines headers for use in the xhr request for a particular segment.
1096 * Expects an object with: 1117 */
1097 * * `roundTripTime` - the round trip time for the request we're setting the time for 1118 segmentXhrHeaders_(segment) {
1098 * * `bandwidth` - the bandwidth we want to set 1119 let headers = {};
1099 * * `bytesReceived` - amount of bytes downloaded
1100 * `bandwidth` is the only required property.
1101 */
1102 videojs.HlsHandler.prototype.setBandwidth = function(xhr) {
1103 // calculate the download bandwidth
1104 this.segmentXhrTime = xhr.roundTripTime;
1105 this.bandwidth = xhr.bandwidth;
1106 this.bytesReceived += xhr.bytesReceived || 0;
1107 1120
1108 this.tech_.trigger('bandwidthupdate'); 1121 if ('byterange' in segment) {
1109 }; 1122 headers.Range = this.byterangeStr_(segment.byterange);
1123 }
1124 return headers;
1125 }
1110 1126
1111 /* 1127 /*
1112 * Blacklists a playlist when an error occurs for a set amount of time 1128 * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived.
1113 * making it unavailable for selection by the rendition selection algorithm 1129 * Expects an object with:
1114 * and then forces a new playlist (rendition) selection. 1130 * * `roundTripTime` - the round trip time for the request we're setting the time for
1115 */ 1131 * * `bandwidth` - the bandwidth we want to set
1116 videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) { 1132 * * `bytesReceived` - amount of bytes downloaded
1117 var currentPlaylist, nextPlaylist; 1133 * `bandwidth` is the only required property.
1118 1134 */
1119 // If the `error` was generated by the playlist loader, it will contain 1135 setBandwidth(localXhr) {
1120 // the playlist we were trying to load (but failed) and that should be 1136 // calculate the download bandwidth
1121 // blacklisted instead of the currently selected playlist which is likely 1137 this.segmentXhrTime = localXhr.roundTripTime;
1122 // out-of-date in this scenario 1138 this.bandwidth = localXhr.bandwidth;
1123 currentPlaylist = error.playlist || this.playlists.media(); 1139 this.bytesReceived += localXhr.bytesReceived || 0;
1124 1140
1125 // If there is no current playlist, then an error occurred while we were 1141 this.tech_.trigger('bandwidthupdate');
1126 // trying to load the master OR while we were disposing of the tech
1127 if (!currentPlaylist) {
1128 this.error = error;
1129 return this.mediaSource.endOfStream('network');
1130 } 1142 }
1131 1143
1132 // Blacklist this playlist 1144 /*
1133 currentPlaylist.excludeUntil = Date.now() + blacklistDuration; 1145 * Blacklists a playlist when an error occurs for a set amount of time
1146 * making it unavailable for selection by the rendition selection algorithm
1147 * and then forces a new playlist (rendition) selection.
1148 */
1149 blacklistCurrentPlaylist_(error) {
1150 let currentPlaylist;
1151 let nextPlaylist;
1152
1153 // If the `error` was generated by the playlist loader, it will contain
1154 // the playlist we were trying to load (but failed) and that should be
1155 // blacklisted instead of the currently selected playlist which is likely
1156 // out-of-date in this scenario
1157 currentPlaylist = error.playlist || this.playlists.media();
1158
1159 // If there is no current playlist, then an error occurred while we were
1160 // trying to load the master OR while we were disposing of the tech
1161 if (!currentPlaylist) {
1162 this.error = error;
1163 return this.mediaSource.endOfStream('network');
1164 }
1165
1166 // Blacklist this playlist
1167 currentPlaylist.excludeUntil = Date.now() + blacklistDuration;
1134 1168
1135 // Select a new playlist 1169 // Select a new playlist
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(), 1190
1156 currentTime = this.tech_.currentTime(); 1191 // Chrome has a hard limit of 150mb of
1157 1192 // buffer and a very conservative "garbage collector"
1158 // Chrome has a hard limit of 150mb of buffer and a very conservative "garbage collector" 1193 // We manually clear out the old buffer to ensure
1159 // We manually clear out the old buffer to ensure we don't trigger the QuotaExceeded error 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
1163 // otherwise remove anything older than 1 minute before the current play head 1198 // otherwise remove anything older than 1 minute before the current play head
1164 if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) { 1199 if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) {
1165 removeToTime = seekable.start(0); 1200 removeToTime = seekable.start(0);
1166 } else { 1201 } else {
1167 removeToTime = currentTime - 60; 1202 removeToTime = currentTime - 60;
1168 } 1203 }
1169 1204
1170 if (removeToTime > 0) { 1205 if (removeToTime > 0) {
1171 this.sourceBuffer.remove(0, removeToTime); 1206 this.sourceBuffer.remove(0, removeToTime);
1207 }
1172 } 1208 }
1173 }
1174
1175 // if the segment is encrypted, request the key
1176 if (segment.key) {
1177 this.fetchKey_(segment);
1178 }
1179 1209
1180 // request the next segment 1210 // if the segment is encrypted, request the key
1181 this.segmentXhr_ = videojs.Hls.xhr({ 1211 if (segment.key) {
1182 uri: segmentInfo.uri, 1212 this.fetchKey_(segment);
1183 responseType: 'arraybuffer',
1184 withCredentials: this.source_.withCredentials,
1185 // Set xhr timeout to 150% of the segment duration to allow us
1186 // some time to switch renditions in the event of a catastrophic
1187 // decrease in network performance or a server issue.
1188 timeout: (segment.duration * 1.5) * 1000,
1189 headers: this.segmentXhrHeaders_(segment)
1190 }, function(error, request) {
1191 // This is a timeout of a previously aborted segment request
1192 // so simply ignore it
1193 if (!self.segmentXhr_ || request !== self.segmentXhr_) {
1194 return;
1195 } 1213 }
1196 1214
1197 // the segment request is no longer outstanding 1215 // request the next segment
1198 self.segmentXhr_ = null; 1216 this.segmentXhr_ = Hls.xhr({
1199 1217 uri: segmentInfo.uri,
1200 // if a segment request times out, we may have better luck with another playlist 1218 responseType: 'arraybuffer',
1201 if (request.timedout) { 1219 withCredentials: this.source_.withCredentials,
1202 self.bandwidth = 1; 1220 // Set xhr timeout to 150% of the segment duration to allow us
1203 return self.playlists.media(self.selectPlaylist()); 1221 // some time to switch renditions in the event of a catastrophic
1204 } 1222 // decrease in network performance or a server issue.
1223 timeout: (segment.duration * 1.5) * 1000,
1224 headers: this.segmentXhrHeaders_(segment)
1225 }, (error, request) => {
1226 // This is a timeout of a previously aborted segment request
1227 // so simply ignore it
1228 if (!this.segmentXhr_ || request !== this.segmentXhr_) {
1229 return;
1230 }
1205 1231
1206 // otherwise, trigger a network error 1232 // the segment request is no longer outstanding
1207 if (!request.aborted && error) { 1233 this.segmentXhr_ = null;
1208 return self.blacklistCurrentPlaylist_({
1209 status: request.status,
1210 message: 'HLS segment request error at URL: ' + segmentInfo.uri,
1211 code: (request.status >= 500) ? 4 : 2
1212 });
1213 }
1214 1234
1215 // stop processing if the request was aborted 1235 // if a segment request times out, we may have better luck with another playlist
1216 if (!request.response) { 1236 if (request.timedout) {
1217 return; 1237 this.bandwidth = 1;
1218 } 1238 return this.playlists.media(this.selectPlaylist());
1239 }
1219 1240
1220 self.lastSegmentLoaded_ = segment; 1241 // otherwise, trigger a network error
1221 self.setBandwidth(request); 1242 if (!request.aborted && error) {
1243 return this.blacklistCurrentPlaylist_({
1244 status: request.status,
1245 message: 'HLS segment request error at URL: ' + segmentInfo.uri,
1246 code: (request.status >= 500) ? 4 : 2
1247 });
1248 }
1222 1249
1223 if (segment.key) { 1250 // stop processing if the request was aborted
1224 segmentInfo.encryptedBytes = new Uint8Array(request.response); 1251 if (!request.response) {
1225 } else { 1252 return;
1226 segmentInfo.bytes = new Uint8Array(request.response); 1253 }
1227 }
1228 1254
1229 self.pendingSegment_ = segmentInfo; 1255 this.lastSegmentLoaded_ = segment;
1256 this.setBandwidth(request);
1230 1257
1231 self.tech_.trigger('progress'); 1258 if (segment.key) {
1232 self.drainBuffer(); 1259 segmentInfo.encryptedBytes = new Uint8Array(request.response);
1260 } else {
1261 segmentInfo.bytes = new Uint8Array(request.response);
1262 }
1233 1263
1234 // figure out what stream the next segment should be downloaded from 1264 this.pendingSegment_ = segmentInfo;
1235 // with the updated bandwidth information
1236 self.playlists.media(self.selectPlaylist());
1237 });
1238 1265
1239 }; 1266 this.tech_.trigger('progress');
1267 this.drainBuffer();
1240 1268
1241 videojs.HlsHandler.prototype.drainBuffer = function() { 1269 // figure out what stream the next segment should be downloaded from
1242 var 1270 // with the updated bandwidth information
1243 segmentInfo, 1271 this.playlists.media(this.selectPlaylist());
1244 mediaIndex, 1272 });
1245 playlist,
1246 offset,
1247 bytes,
1248 segment,
1249 decrypter,
1250 segIv;
1251
1252 // if the buffer is empty or the source buffer hasn't been created
1253 // yet, do nothing
1254 if (!this.pendingSegment_ || !this.sourceBuffer) {
1255 return;
1256 }
1257 1273
1258 // the pending segment has already been appended and we're waiting
1259 // for updateend to fire
1260 if (this.pendingSegment_.buffered) {
1261 return;
1262 } 1274 }
1263 1275
1264 // we can't append more data if the source buffer is busy processing 1276 drainBuffer() {
1265 // what we've already sent 1277 let segmentInfo;
1266 if (this.sourceBuffer.updating) { 1278 let mediaIndex;
1267 return; 1279 let playlist;
1268 } 1280 let bytes;
1281 let segment;
1282 let decrypter;
1283 let segIv;
1269 1284
1270 segmentInfo = this.pendingSegment_; 1285 // if the buffer is empty or the source buffer hasn't been created
1271 mediaIndex = segmentInfo.mediaIndex; 1286 // yet, do nothing
1272 playlist = segmentInfo.playlist; 1287 if (!this.pendingSegment_ || !this.sourceBuffer) {
1273 offset = segmentInfo.offset; 1288 return;
1274 bytes = segmentInfo.bytes; 1289 }
1275 segment = playlist.segments[mediaIndex];
1276
1277 if (segment.key && !bytes) {
1278 // this is an encrypted segment
1279 // if the key download failed, we want to skip this segment
1280 // but if the key hasn't downloaded yet, we want to try again later
1281 if (keyFailed(segment.key)) {
1282 return this.blacklistCurrentPlaylist_({
1283 message: 'HLS segment key request error.',
1284 code: 4
1285 });
1286 } else if (!segment.key.bytes) {
1287 1290
1288 // waiting for the key bytes, try again later 1291 // the pending segment has already been appended and we're waiting
1292 // for updateend to fire
1293 if (this.pendingSegment_.buffered) {
1289 return; 1294 return;
1290 } else if (segmentInfo.decrypter) { 1295 }
1291 1296
1292 // decryption is in progress, try again later 1297 // we can't append more data if the source buffer is busy processing
1298 // what we've already sent
1299 if (this.sourceBuffer.updating) {
1293 return; 1300 return;
1294 } else { 1301 }
1295 1302
1303 segmentInfo = this.pendingSegment_;
1304 mediaIndex = segmentInfo.mediaIndex;
1305 playlist = segmentInfo.playlist;
1306 bytes = segmentInfo.bytes;
1307 segment = playlist.segments[mediaIndex];
1308
1309 if (segment.key && !bytes) {
1310 // this is an encrypted segment
1311 // if the key download failed, we want to skip this segment
1312 // but if the key hasn't downloaded yet, we want to try again later
1313 if (keyFailed(segment.key)) {
1314 return this.blacklistCurrentPlaylist_({
1315 message: 'HLS segment key request error.',
1316 code: 4
1317 });
1318 } else if (!segment.key.bytes) {
1319 // waiting for the key bytes, try again later
1320 return;
1321 } else if (segmentInfo.decrypter) {
1322 // decryption is in progress, try again later
1323 return;
1324 }
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) {
1306 }); 1336 videojs.log.warn(error);
1337 }
1338 segmentInfo.bytes = localBytes;
1339 });
1307 segmentInfo.decrypter = decrypter; 1340 segmentInfo.decrypter = decrypter;
1308 return; 1341 return;
1309 } 1342 }
1310 }
1311
1312 this.pendingSegment_.buffered = this.tech_.buffered();
1313 1343
1314 if (segmentInfo.timestampOffset !== null) { 1344 this.pendingSegment_.buffered = this.tech_.buffered();
1315 this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset;
1316 }
1317 1345
1318 // the segment is asynchronously added to the current buffered data 1346 if (segmentInfo.timestampOffset !== null) {
1319 this.sourceBuffer.appendBuffer(bytes); 1347 this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset;
1320 }; 1348 }
1321 1349
1322 videojs.HlsHandler.prototype.updateEndHandler_ = function () { 1350 // the segment is asynchronously added to the current buffered data
1323 var 1351 this.sourceBuffer.appendBuffer(bytes);
1324 segmentInfo = this.pendingSegment_,
1325 segment,
1326 segments,
1327 playlist,
1328 currentMediaIndex,
1329 currentBuffered,
1330 seekable,
1331 timelineUpdate,
1332 isEndOfStream;
1333
1334 // stop here if the update errored or was aborted
1335 if (!segmentInfo) {
1336 return;
1337 } 1352 }
1338 1353
1339 // In Firefox, the updateend event is triggered for both removing from the buffer and 1354 updateEndHandler_() {
1340 // adding to the buffer. To prevent this code from executing on removals, we wait for 1355 let segmentInfo = this.pendingSegment_;
1341 // segmentInfo to have a filled in buffered value before we continue processing. 1356 let playlist;
1342 if (!segmentInfo.buffered) { 1357 let currentMediaIndex;
1343 return; 1358 let currentBuffered;
1344 } 1359 let seekable;
1360 let timelineUpdate;
1361 let isEndOfStream;
1345 1362
1346 this.pendingSegment_ = null; 1363 // stop here if the update errored or was aborted
1364 if (!segmentInfo) {
1365 this.pendingSegment_ = null;
1366 return;
1367 }
1368
1369 // In Firefox, the updateend event is triggered for both removing from the buffer and
1370 // adding to the buffer. To prevent this code from executing on removals, we wait for
1371 // segmentInfo to have a filled in buffered value before we continue processing.
1372 if (!segmentInfo.buffered) {
1373 return;
1374 }
1347 1375
1348 playlist = segmentInfo.playlist; 1376 this.pendingSegment_ = null;
1349 segments = playlist.segments;
1350 currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence);
1351 currentBuffered = this.findBufferedRange_();
1352 isEndOfStream = this.isEndOfStream_(playlist, currentMediaIndex, currentBuffered);
1353 1377
1354 // if we switched renditions don't try to add segment timeline 1378 playlist = segmentInfo.playlist;
1355 // information to the playlist 1379 currentMediaIndex = segmentInfo.mediaIndex +
1356 if (segmentInfo.playlist.uri !== this.playlists.media().uri) { 1380 (segmentInfo.mediaSequence - playlist.mediaSequence);
1357 if (isEndOfStream) { 1381 currentBuffered = this.findBufferedRange_();
1358 return this.mediaSource.endOfStream(); 1382 isEndOfStream = detectEndOfStream(playlist, this.mediaSource, currentMediaIndex, currentBuffered);
1383
1384 // if we switched renditions don't try to add segment timeline
1385 // information to the playlist
1386 if (segmentInfo.playlist.uri !== this.playlists.media().uri) {
1387 if (isEndOfStream) {
1388 return this.mediaSource.endOfStream();
1389 }
1390 return this.fillBuffer();
1359 } 1391 }
1360 return this.fillBuffer();
1361 }
1362 1392
1363 // annotate the segment with any start and end time information 1393 // when seeking to the beginning of the seekable range, it's
1364 // added by the media processing 1394 // possible that imprecise timing information may cause the seek to
1365 segment = playlist.segments[currentMediaIndex]; 1395 // end up earlier than the start of the range
1366 1396 // in that case, seek again
1367 // when seeking to the beginning of the seekable range, it's 1397 seekable = this.seekable();
1368 // possible that imprecise timing information may cause the seek to 1398 if (this.tech_.seeking() &&
1369 // end up earlier than the start of the range 1399 currentBuffered.length === 0) {
1370 // in that case, seek again 1400 if (seekable.length &&
1371 seekable = this.seekable(); 1401 this.tech_.currentTime() < seekable.start(0)) {
1372 if (this.tech_.seeking() && 1402 let next = this.findNextBufferedRange_();
1373 currentBuffered.length === 0) { 1403
1374 if (seekable.length && 1404 if (next.length) {
1375 this.tech_.currentTime() < seekable.start(0)) { 1405 videojs.log('tried seeking to', this.tech_.currentTime(),
1376 var next = this.findNextBufferedRange_(); 1406 'but that was too early, retrying at', next.start(0));
1377 if (next.length) { 1407 this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR);
1378 videojs.log('tried seeking to', this.tech_.currentTime(), 'but that was too early, retrying at', next.start(0)); 1408 }
1379 this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR);
1380 } 1409 }
1381 } 1410 }
1382 }
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
1392 if (isEndOfStream) { 1420 if (isEndOfStream) {
1393 return this.mediaSource.endOfStream(); 1421 return this.mediaSource.endOfStream();
1394 } 1422 }
1395 1423
1396 if (timelineUpdate !== null || 1424 if (timelineUpdate !== null ||
1397 segmentInfo.buffered.length !== this.tech_.buffered().length) { 1425 segmentInfo.buffered.length !== this.tech_.buffered().length) {
1398 this.updateDuration(playlist); 1426 this.updateDuration(playlist);
1399 // check if it's time to download the next segment 1427 // check if it's time to download the next segment
1400 this.fillBuffer(); 1428 this.fillBuffer();
1429 return;
1430 }
1431
1432 // the last segment append must have been entirely in the
1433 // already buffered time ranges. just buffer forward until we
1434 // find a segment that adds to the buffered time ranges and
1435 // improves subsequent media index calculations.
1436 this.fillBuffer(currentMediaIndex + 1);
1401 return; 1437 return;
1402 } 1438 }
1403 1439
1404 // the last segment append must have been entirely in the 1440 /**
1405 // already buffered time ranges. just buffer forward until we 1441 * Attempt to retrieve the key for a particular media segment.
1406 // find a segment that adds to the buffered time ranges and 1442 */
1407 // improves subsequent media index calculations. 1443 fetchKey_(segment) {
1408 this.fillBuffer(currentMediaIndex + 1); 1444 let key;
1409 return; 1445 let settings;
1410 }; 1446 let receiveKey;
1411 1447
1412 /** 1448 // if there is a pending XHR or no segments, don't do anything
1413 * Attempt to retrieve the key for a particular media segment. 1449 if (this.keyXhr_) {
1414 */ 1450 return;
1415 videojs.HlsHandler.prototype.fetchKey_ = function(segment) { 1451 }
1416 var key, self, settings, receiveKey;
1417 1452
1418 // if there is a pending XHR or no segments, don't do anything 1453 settings = this.options_;
1419 if (this.keyXhr_) {
1420 return;
1421 }
1422 1454
1423 self = this; 1455 /**
1424 settings = this.options_; 1456 * Handle a key XHR response.
1457 */
1458 receiveKey = (keyRecieved) => {
1459 return (error, request) => {
1460 let view;
1425 1461
1426 /** 1462 this.keyXhr_ = null;
1427 * Handle a key XHR response.
1428 */
1429 receiveKey = function(key) {
1430 return function(error, request) {
1431 var view;
1432 self.keyXhr_ = null;
1433
1434 if (error || !request.response || request.response.byteLength !== 16) {
1435 key.retries = key.retries || 0;
1436 key.retries++;
1437 if (!request.aborted) {
1438 // try fetching again
1439 self.fetchKey_(segment);
1440 }
1441 return;
1442 }
1443 1463
1444 view = new DataView(request.response); 1464 if (error || !request.response || request.response.byteLength !== 16) {
1445 key.bytes = new Uint32Array([ 1465 keyRecieved.retries = keyRecieved.retries || 0;
1446 view.getUint32(0), 1466 keyRecieved.retries++;
1447 view.getUint32(4), 1467 if (!request.aborted) {
1448 view.getUint32(8), 1468 // try fetching again
1449 view.getUint32(12) 1469 this.fetchKey_(segment);
1450 ]); 1470 }
1471 return;
1472 }
1451 1473
1452 // check to see if this allows us to make progress buffering now 1474 view = new DataView(request.response);
1453 self.checkBuffer_(); 1475 keyRecieved.bytes = new Uint32Array([
1476 view.getUint32(0),
1477 view.getUint32(4),
1478 view.getUint32(8),
1479 view.getUint32(12)
1480 ]);
1481
1482 // check to see if this allows us to make progress buffering now
1483 this.checkBuffer_();
1484 };
1454 }; 1485 };
1455 };
1456 1486
1457 key = segment.key; 1487 key = segment.key;
1458 1488
1459 // nothing to do if this segment is unencrypted 1489 // nothing to do if this segment is unencrypted
1460 if (!key) { 1490 if (!key) {
1461 return; 1491 return;
1462 } 1492 }
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;
1502 }
1472 } 1503 }
1473 }; 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 },
1539 } 1543 handleSource(source, tech) {
1540 1544 if (mode === 'flash') {
1541 leftWidth = leftWidth || window.Number.MAX_VALUE; 1545 // We need to trigger this asynchronously to give others the chance
1542 1546 // to bind to the event when a source is set at player creation
1543 if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 1547 tech.setTimeout(function() {
1544 rightWidth = right.attributes.RESOLUTION.width; 1548 tech.trigger('loadstart');
1545 } 1549 }, 1);
1550 }
1551 tech.hls = new HlsHandler(tech, {
1552 source,
1553 mode
1554 });
1555 tech.hls.src(source.src);
1556 return tech.hls;
1557 },
1558 canPlayType(type) {
1559 return HlsSourceHandler.canPlayType(type);
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`
1564 * with `path`
1565 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
1566 */
1567 resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) {
1568 // use the base element to get the browser to handle URI resolution
1569 var
1570 oldBase = document.querySelector('base'),
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 1579
1584 base.href = basePath; 1580 // register source handlers with the appropriate techs
1585 a.href = path; 1581 if (MediaSource.supportsNativeMediaSources()) {
1586 result = a.href; 1582 videojs.getComponent('Html5').registerSourceHandler(HlsSourceHandler('html5'));
1583 }
1584 if (window.Uint8Array) {
1585 videojs.getComponent('Flash').registerSourceHandler(HlsSourceHandler('flash'));
1586 }
1587 1587
1588 // clean up 1588 videojs.HlsHandler = HlsHandler;
1589 if (oldBase) { 1589 videojs.HlsSourceHandler = HlsSourceHandler;
1590 oldBase.href = oldHref; 1590 videojs.Hls = Hls;
1591 } else { 1591 videojs.m3u8 = m3u8;
1592 docHead.removeChild(base);
1593 }
1594 return result;
1595 };
1596 1592
1597 })(window, window.videojs, document); 1593 export default {
1594 Hls,
1595 HlsHandler,
1596 HlsSourceHandler
1597 };
......
1 (function(videojs) { 1 /**
2 'use strict'; 2 * A wrapper for videojs.xhr that tracks bandwidth.
3 */
4 import {xhr as videojsXHR, mergeOptions} from 'video.js';
5 const xhr = function(options, callback) {
6 // Add a default timeout for all hls requests
7 options = mergeOptions({
8 timeout: 45e3
9 }, options);
3 10
4 /** 11 let request = videojsXHR(options, function(error, response) {
5 * A wrapper for videojs.xhr that tracks bandwidth. 12 if (!error && request.response) {
6 */ 13 request.responseTime = (new Date()).getTime();
7 videojs.Hls.xhr = function(options, callback) { 14 request.roundTripTime = request.responseTime - request.requestTime;
8 // Add a default timeout for all hls requests 15 request.bytesReceived = request.response.byteLength || request.response.length;
9 options = videojs.mergeOptions({ 16 if (!request.bandwidth) {
10 timeout: 45e3 17 request.bandwidth =
11 }, options); 18 Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000);
12
13 var request = videojs.xhr(options, function(error, response) {
14 if (!error && request.response) {
15 request.responseTime = (new Date()).getTime();
16 request.roundTripTime = request.responseTime - request.requestTime;
17 request.bytesReceived = request.response.byteLength || request.response.length;
18 if (!request.bandwidth) {
19 request.bandwidth = Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000);
20 }
21 } 19 }
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
24 // timed out errors of setting a boolean on the request object 23 // on the error object to signal that a request has
25 if (error || request.timedout) { 24 // timed out errors of setting a boolean on the request object
26 request.timedout = request.timedout || (error.code === 'ETIMEDOUT'); 25 if (error || request.timedout) {
27 } else { 26 request.timedout = request.timedout || (error.code === 'ETIMEDOUT');
28 request.timedout = false; 27 } else {
29 } 28 request.timedout = false;
29 }
30 30
31 // videojs.xhr no longer considers status codes outside of 200 and 0 31 // videojs.xhr no longer considers status codes outside of 200 and 0
32 // (for file uris) to be errors, but the old XHR did, so emulate that 32 // (for file uris) to be errors, but the old XHR did, so emulate that
33 // behavior. Status 206 may be used in response to byterange requests. 33 // behavior. Status 206 may be used in response to byterange requests.
34 if (!error && 34 if (!error &&
35 response.statusCode !== 200 && 35 response.statusCode !== 200 &&
36 response.statusCode !== 206 && 36 response.statusCode !== 206 &&
37 response.statusCode !== 0) { 37 response.statusCode !== 0) {
38 error = new Error('XHR Failed with a response of: ' + 38 error = new Error('XHR Failed with a response of: ' +
39 (request && (request.response || request.responseText))); 39 (request && (request.response || request.responseText)));
40 } 40 }
41
42 callback(error, request);
43 });
41 44
42 callback(error, request); 45 request.requestTime = (new Date()).getTime();
43 }); 46 return request;
47 };
44 48
45 request.requestTime = (new Date()).getTime(); 49 export default xhr;
46 return request;
47 };
48 })(window.videojs);
......
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; 4
5 5 QUnit.test('total duration for live playlists is Infinity', function() {
6 module('Playlist Duration'); 6 let duration = Playlist.duration({
7 7 segments: [{
8 test('total duration for live playlists is Infinity', function() { 8 duration: 4,
9 var duration = Playlist.duration({ 9 uri: '0.ts'
10 segments: [{ 10 }]
11 duration: 4,
12 uri: '0.ts'
13 }]
14 });
15
16 equal(duration, Infinity, 'duration is infinity');
17 }); 11 });
18 12
19 module('Playlist Interval Duration'); 13 QUnit.equal(duration, Infinity, 'duration is infinity');
20 14 });
21 test('accounts for non-zero starting VOD media sequences', function() { 15
22 var duration = Playlist.duration({ 16 QUnit.module('Playlist Interval Duration');
23 mediaSequence: 10, 17
24 endList: true, 18 QUnit.test('accounts for non-zero starting VOD media sequences', function() {
25 segments: [{ 19 let duration = Playlist.duration({
26 duration: 10, 20 mediaSequence: 10,
27 uri: '0.ts' 21 endList: true,
28 }, { 22 segments: [{
29 duration: 10, 23 duration: 10,
30 uri: '1.ts' 24 uri: '0.ts'
31 }, { 25 }, {
32 duration: 10, 26 duration: 10,
33 uri: '2.ts' 27 uri: '1.ts'
34 }, { 28 }, {
35 duration: 10, 29 duration: 10,
36 uri: '3.ts' 30 uri: '2.ts'
37 }] 31 }, {
38 }); 32 duration: 10,
39 33 uri: '3.ts'
40 equal(duration, 4 * 10, 'includes only listed segments'); 34 }]
41 }); 35 });
42 36
43 test('uses timeline values when available', function() { 37 QUnit.equal(duration, 4 * 10, 'includes only listed segments');
44 var duration = Playlist.duration({ 38 });
45 mediaSequence: 0, 39
46 endList: true, 40 QUnit.test('uses timeline values when available', function() {
47 segments: [{ 41 let duration = Playlist.duration({
48 start: 0, 42 mediaSequence: 0,
49 uri: '0.ts' 43 endList: true,
50 }, { 44 segments: [{
51 duration: 10, 45 start: 0,
52 end: 2 * 10 + 2, 46 uri: '0.ts'
53 uri: '1.ts' 47 }, {
54 }, { 48 duration: 10,
55 duration: 10, 49 end: 2 * 10 + 2,
56 end: 3 * 10 + 2, 50 uri: '1.ts'
57 uri: '2.ts' 51 }, {
58 }, { 52 duration: 10,
59 duration: 10, 53 end: 3 * 10 + 2,
60 end: 4 * 10 + 2, 54 uri: '2.ts'
61 uri: '3.ts' 55 }, {
62 }] 56 duration: 10,
63 }, 4); 57 end: 4 * 10 + 2,
64 58 uri: '3.ts'
65 equal(duration, 4 * 10 + 2, 'used timeline values'); 59 }]
60 }, 4);
61
62 QUnit.equal(duration, 4 * 10 + 2, 'used timeline values');
63 });
64
65 QUnit.test('works when partial timeline information is available', function() {
66 let duration = Playlist.duration({
67 mediaSequence: 0,
68 endList: true,
69 segments: [{
70 start: 0,
71 uri: '0.ts'
72 }, {
73 duration: 9,
74 uri: '1.ts'
75 }, {
76 duration: 10,
77 uri: '2.ts'
78 }, {
79 duration: 10,
80 start: 30.007,
81 end: 40.002,
82 uri: '3.ts'
83 }, {
84 duration: 10,
85 end: 50.0002,
86 uri: '4.ts'
87 }]
88 }, 5);
89
90 QUnit.equal(duration, 50.0002, 'calculated with mixed intervals');
91 });
92
93 QUnit.test('uses timeline values for the expired duration of live playlists', function() {
94 let playlist = {
95 mediaSequence: 12,
96 segments: [{
97 duration: 10,
98 end: 120.5,
99 uri: '0.ts'
100 }, {
101 duration: 9,
102 uri: '1.ts'
103 }]
104 };
105 let duration;
106
107 duration = Playlist.duration(playlist, playlist.mediaSequence);
108 QUnit.equal(duration, 110.5, 'used segment end time');
109 duration = Playlist.duration(playlist, playlist.mediaSequence + 1);
110 QUnit.equal(duration, 120.5, 'used segment end time');
111 duration = Playlist.duration(playlist, playlist.mediaSequence + 2);
112 QUnit.equal(duration, 120.5 + 9, 'used segment end time');
113 });
114
115 QUnit.test('looks outside the queried interval for live playlist timeline values',
116 function() {
117 let playlist = {
118 mediaSequence: 12,
119 segments: [{
120 duration: 10,
121 uri: '0.ts'
122 }, {
123 duration: 9,
124 end: 120.5,
125 uri: '1.ts'
126 }]
127 };
128 let duration;
129
130 duration = Playlist.duration(playlist, playlist.mediaSequence);
131 QUnit.equal(duration, 120.5 - 9 - 10, 'used segment end time');
132 });
133
134 QUnit.test('ignores discontinuity sequences later than the end', function() {
135 let duration = Playlist.duration({
136 mediaSequence: 0,
137 discontinuityStarts: [1, 3],
138 segments: [{
139 duration: 10,
140 uri: '0.ts'
141 }, {
142 discontinuity: true,
143 duration: 9,
144 uri: '1.ts'
145 }, {
146 duration: 10,
147 uri: '2.ts'
148 }, {
149 discontinuity: true,
150 duration: 10,
151 uri: '3.ts'
152 }]
153 }, 2);
154
155 QUnit.equal(duration, 19, 'excluded the later segments');
156 });
157
158 QUnit.test('handles trailing segments without timeline information', function() {
159 let duration;
160 let playlist = {
161 mediaSequence: 0,
162 endList: true,
163 segments: [{
164 start: 0,
165 end: 10.5,
166 uri: '0.ts'
167 }, {
168 duration: 9,
169 uri: '1.ts'
170 }, {
171 duration: 10,
172 uri: '2.ts'
173 }, {
174 start: 29.45,
175 end: 39.5,
176 uri: '3.ts'
177 }]
178 };
179
180 duration = Playlist.duration(playlist, 3);
181 QUnit.equal(duration, 29.45, 'calculated duration');
182
183 duration = Playlist.duration(playlist, 2);
184 QUnit.equal(duration, 19.5, 'calculated duration');
185 });
186
187 QUnit.test('uses timeline intervals when segments have them', function() {
188 let duration;
189 let playlist = {
190 mediaSequence: 0,
191 segments: [{
192 start: 0,
193 end: 10,
194 uri: '0.ts'
195 }, {
196 duration: 9,
197 uri: '1.ts'
198 }, {
199 start: 20.1,
200 end: 30.1,
201 duration: 10,
202 uri: '2.ts'
203 }]
204 };
205
206 duration = Playlist.duration(playlist, 2);
207 QUnit.equal(duration, 20.1, 'used the timeline-based interval');
208
209 duration = Playlist.duration(playlist, 3);
210 QUnit.equal(duration, 30.1, 'used the timeline-based interval');
211 });
212
213 QUnit.test('counts the time between segments as part of the earlier segment\'s duration',
214 function() {
215 let duration = Playlist.duration({
216 mediaSequence: 0,
217 endList: true,
218 segments: [{
219 start: 0,
220 end: 10,
221 uri: '0.ts'
222 }, {
223 start: 10.1,
224 end: 20.1,
225 duration: 10,
226 uri: '1.ts'
227 }]
228 }, 1);
229
230 QUnit.equal(duration, 10.1, 'included the segment gap');
231 });
232
233 QUnit.test('accounts for discontinuities', function() {
234 let duration = Playlist.duration({
235 mediaSequence: 0,
236 endList: true,
237 discontinuityStarts: [1],
238 segments: [{
239 duration: 10,
240 uri: '0.ts'
241 }, {
242 discontinuity: true,
243 duration: 10,
244 uri: '1.ts'
245 }]
246 }, 2);
247
248 QUnit.equal(duration, 10 + 10, 'handles discontinuities');
249 });
250
251 QUnit.test('a non-positive length interval has zero duration', function() {
252 let playlist = {
253 mediaSequence: 0,
254 discontinuityStarts: [1],
255 segments: [{
256 duration: 10,
257 uri: '0.ts'
258 }, {
259 discontinuity: true,
260 duration: 10,
261 uri: '1.ts'
262 }]
263 };
264
265 QUnit.equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero');
266 QUnit.equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero');
267 QUnit.equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero');
268 });
269
270 QUnit.module('Playlist Seekable');
271
272 QUnit.test('calculates seekable time ranges from the available segments', function() {
273 let playlist = {
274 mediaSequence: 0,
275 segments: [{
276 duration: 10,
277 uri: '0.ts'
278 }, {
279 duration: 10,
280 uri: '1.ts'
281 }],
282 endList: true
283 };
284 let seekable = Playlist.seekable(playlist);
285
286 QUnit.equal(seekable.length, 1, 'there are seekable ranges');
287 QUnit.equal(seekable.start(0), 0, 'starts at zero');
288 QUnit.equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
289 });
290
291 QUnit.test('master playlists have empty seekable ranges', function() {
292 let seekable = Playlist.seekable({
293 playlists: [{
294 uri: 'low.m3u8'
295 }, {
296 uri: 'high.m3u8'
297 }]
66 }); 298 });
67 299
68 test('works when partial timeline information is available', function() { 300 QUnit.equal(seekable.length, 0, 'no seekable ranges from a master playlist');
69 var duration = Playlist.duration({ 301 });
70 mediaSequence: 0, 302
71 endList: true, 303 QUnit.test('seekable end is three target durations from the actual end of live playlists',
72 segments: [{ 304 function() {
73 start: 0, 305 let seekable = Playlist.seekable({
74 uri: '0.ts' 306 mediaSequence: 0,
75 }, { 307 segments: [{
76 duration: 9, 308 duration: 7,
77 uri: '1.ts' 309 uri: '0.ts'
78 }, { 310 }, {
79 duration: 10, 311 duration: 10,
80 uri: '2.ts' 312 uri: '1.ts'
81 }, { 313 }, {
82 duration: 10, 314 duration: 10,
83 start: 30.007, 315 uri: '2.ts'
84 end: 40.002, 316 }, {
85 uri: '3.ts' 317 duration: 10,
86 }, { 318 uri: '3.ts'
87 duration: 10, 319 }]
88 end: 50.0002,
89 uri: '4.ts'
90 }]
91 }, 5);
92
93 equal(duration, 50.0002, 'calculated with mixed intervals');
94 }); 320 });
95 321
96 test('uses timeline values for the expired duration of live playlists', function() { 322 QUnit.equal(seekable.length, 1, 'there are seekable ranges');
97 var playlist = { 323 QUnit.equal(seekable.start(0), 0, 'starts at zero');
98 mediaSequence: 12, 324 QUnit.equal(seekable.end(0), 7, 'ends three target durations from the last segment');
99 segments: [{ 325 });
100 duration: 10, 326
101 end: 120.5, 327 QUnit.test('only considers available segments', function() {
102 uri: '0.ts' 328 let seekable = Playlist.seekable({
103 }, { 329 mediaSequence: 7,
104 duration: 9, 330 segments: [{
105 uri: '1.ts' 331 uri: '8.ts',
106 }] 332 duration: 10
107 }, duration; 333 }, {
108 334 uri: '9.ts',
109 duration = Playlist.duration(playlist, playlist.mediaSequence); 335 duration: 10
110 equal(duration, 110.5, 'used segment end time'); 336 }, {
111 duration = Playlist.duration(playlist, playlist.mediaSequence + 1); 337 uri: '10.ts',
112 equal(duration, 120.5, 'used segment end time'); 338 duration: 10
113 duration = Playlist.duration(playlist, playlist.mediaSequence + 2); 339 }, {
114 equal(duration, 120.5 + 9, 'used segment end time'); 340 uri: '11.ts',
341 duration: 10
342 }]
115 }); 343 });
116 344
117 test('looks outside the queried interval for live playlist timeline values', function() { 345 QUnit.equal(seekable.length, 1, 'there are seekable ranges');
118 var playlist = { 346 QUnit.equal(seekable.start(0), 0, 'starts at the earliest available segment');
119 mediaSequence: 12, 347 QUnit.equal(seekable.end(0),
120 segments: [{ 348 10,
121 duration: 10, 349 'ends three target durations from the last available segment');
122 uri: '0.ts' 350 });
123 }, { 351
124 duration: 9, 352 QUnit.test('seekable end accounts for non-standard target durations', function() {
125 end: 120.5, 353 let seekable = Playlist.seekable({
126 uri: '1.ts' 354 targetDuration: 2,
127 }] 355 mediaSequence: 0,
128 }, duration; 356 segments: [{
129 357 duration: 2,
130 duration = Playlist.duration(playlist, playlist.mediaSequence); 358 uri: '0.ts'
131 equal(duration, 120.5 - 9 - 10, 'used segment end time'); 359 }, {
360 duration: 2,
361 uri: '1.ts'
362 }, {
363 duration: 1,
364 uri: '2.ts'
365 }, {
366 duration: 2,
367 uri: '3.ts'
368 }, {
369 duration: 2,
370 uri: '4.ts'
371 }]
132 }); 372 });
133 373
134 test('ignores discontinuity sequences later than the end', function() { 374 QUnit.equal(seekable.start(0), 0, 'starts at the earliest available segment');
135 var duration = Playlist.duration({ 375 QUnit.equal(seekable.end(0),
136 mediaSequence: 0, 376 9 - (2 + 2 + 1),
137 discontinuityStarts: [1, 3], 377 'allows seeking no further than three segments from the end');
138 segments: [{ 378 });
139 duration: 10,
140 uri: '0.ts'
141 }, {
142 discontinuity: true,
143 duration: 9,
144 uri: '1.ts'
145 }, {
146 duration: 10,
147 uri: '2.ts'
148 }, {
149 discontinuity: true,
150 duration: 10,
151 uri: '3.ts'
152 }]
153 }, 2);
154
155 equal(duration, 19, 'excluded the later segments');
156 });
157
158 test('handles trailing segments without timeline information', function() {
159 var playlist, duration;
160 playlist = {
161 mediaSequence: 0,
162 endList: true,
163 segments: [{
164 start: 0,
165 end: 10.5,
166 uri: '0.ts'
167 }, {
168 duration: 9,
169 uri: '1.ts'
170 }, {
171 duration: 10,
172 uri: '2.ts'
173 }, {
174 start: 29.45,
175 end: 39.5,
176 uri: '3.ts'
177 }]
178 };
179
180 duration = Playlist.duration(playlist, 3);
181 equal(duration, 29.45, 'calculated duration');
182
183 duration = Playlist.duration(playlist, 2);
184 equal(duration, 19.5, 'calculated duration');
185 });
186
187 test('uses timeline intervals when segments have them', function() {
188 var playlist, duration;
189 playlist = {
190 mediaSequence: 0,
191 segments: [{
192 start: 0,
193 end: 10,
194 uri: '0.ts'
195 }, {
196 duration: 9,
197 uri: '1.ts'
198 },{
199 start: 20.1,
200 end: 30.1,
201 duration: 10,
202 uri: '2.ts'
203 }]
204 };
205 duration = Playlist.duration(playlist, 2);
206
207 equal(duration, 20.1, 'used the timeline-based interval');
208
209 duration = Playlist.duration(playlist, 3);
210 equal(duration, 30.1, 'used the timeline-based interval');
211 });
212
213 test('counts the time between segments as part of the earlier segment\'s duration', function() {
214 var duration = Playlist.duration({
215 mediaSequence: 0,
216 endList: true,
217 segments: [{
218 start: 0,
219 end: 10,
220 uri: '0.ts'
221 }, {
222 start: 10.1,
223 end: 20.1,
224 duration: 10,
225 uri: '1.ts'
226 }]
227 }, 1);
228
229 equal(duration, 10.1, 'included the segment gap');
230 });
231
232 test('accounts for discontinuities', function() {
233 var duration = Playlist.duration({
234 mediaSequence: 0,
235 endList: true,
236 discontinuityStarts: [1],
237 segments: [{
238 duration: 10,
239 uri: '0.ts'
240 }, {
241 discontinuity: true,
242 duration: 10,
243 uri: '1.ts'
244 }]
245 }, 2);
246
247 equal(duration, 10 + 10, 'handles discontinuities');
248 });
249
250 test('a non-positive length interval has zero duration', function() {
251 var playlist = {
252 mediaSequence: 0,
253 discontinuityStarts: [1],
254 segments: [{
255 duration: 10,
256 uri: '0.ts'
257 }, {
258 discontinuity: true,
259 duration: 10,
260 uri: '1.ts'
261 }]
262 };
263
264 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 equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero');
267 });
268
269 module('Playlist Seekable');
270
271 test('calculates seekable time ranges from the available segments', function() {
272 var playlist = {
273 mediaSequence: 0,
274 segments: [{
275 duration: 10,
276 uri: '0.ts'
277 }, {
278 duration: 10,
279 uri: '1.ts'
280 }],
281 endList: true
282 }, seekable = Playlist.seekable(playlist);
283
284 equal(seekable.length, 1, 'there are seekable ranges');
285 equal(seekable.start(0), 0, 'starts at zero');
286 equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
287 });
288
289 test('master playlists have empty seekable ranges', function() {
290 var seekable = Playlist.seekable({
291 playlists: [{
292 uri: 'low.m3u8'
293 }, {
294 uri: 'high.m3u8'
295 }]
296 });
297 equal(seekable.length, 0, 'no seekable ranges from a master playlist');
298 });
299
300 test('seekable end is three target durations from the actual end of live playlists', function() {
301 var seekable = Playlist.seekable({
302 mediaSequence: 0,
303 segments: [{
304 duration: 7,
305 uri: '0.ts'
306 }, {
307 duration: 10,
308 uri: '1.ts'
309 }, {
310 duration: 10,
311 uri: '2.ts'
312 }, {
313 duration: 10,
314 uri: '3.ts'
315 }]
316 });
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
322 test('only considers available segments', function() {
323 var seekable = Playlist.seekable({
324 mediaSequence: 7,
325 segments: [{
326 uri: '8.ts',
327 duration: 10
328 }, {
329 uri: '9.ts',
330 duration: 10
331 }, {
332 uri: '10.ts',
333 duration: 10
334 }, {
335 uri: '11.ts',
336 duration: 10
337 }]
338 });
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 test('seekable end accounts for non-standard target durations', function() {
345 var seekable = Playlist.seekable({
346 targetDuration: 2,
347 mediaSequence: 0,
348 segments: [{
349 duration: 2,
350 uri: '0.ts'
351 }, {
352 duration: 2,
353 uri: '1.ts'
354 }, {
355 duration: 1,
356 uri: '2.ts'
357 }, {
358 duration: 2,
359 uri: '3.ts'
360 }, {
361 duration: 2,
362 uri: '4.ts'
363 }]
364 });
365 equal(seekable.start(0), 0, 'starts at the earliest available segment');
366 equal(seekable.end(0),
367 9 - (2 + 2 + 1),
368 'allows seeking no further than three segments from the end');
369 });
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() {
......