Merge pull request #568 from videojs/browserify-p5
Browserify p5
Showing
16 changed files
with
1283 additions
and
1324 deletions
... | @@ -46,10 +46,7 @@ | ... | @@ -46,10 +46,7 @@ |
46 | </ul> | 46 | </ul> |
47 | 47 | ||
48 | <script src="/node_modules/video.js/dist/video.js"></script> | 48 | <script src="/node_modules/video.js/dist/video.js"></script> |
49 | <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> | ||
50 | <script src="/src/videojs-contrib-hls.js"></script> | ||
51 | <script src="/dist/videojs-contrib-hls.js"></script> | 49 | <script src="/dist/videojs-contrib-hls.js"></script> |
52 | <script src="/src/bin-utils.js"></script> | ||
53 | <script> | 50 | <script> |
54 | (function(window, videojs) { | 51 | (function(window, videojs) { |
55 | var player = window.player = videojs('videojs-contrib-hls-player'); | 52 | var player = window.player = videojs('videojs-contrib-hls-player'); | ... | ... |
... | @@ -2,7 +2,7 @@ | ... | @@ -2,7 +2,7 @@ |
2 | "name": "videojs-contrib-hls", | 2 | "name": "videojs-contrib-hls", |
3 | "version": "1.3.5", | 3 | "version": "1.3.5", |
4 | "description": "Play back HLS with video.js, even where it's not natively supported", | 4 | "description": "Play back HLS with video.js, even where it's not natively supported", |
5 | "main": "es5/stub.js", | 5 | "main": "es5/videojs-contrib-hls.js", |
6 | "engines": { | 6 | "engines": { |
7 | "node": ">= 0.10.12" | 7 | "node": ">= 0.10.12" |
8 | }, | 8 | }, |
... | @@ -27,7 +27,7 @@ | ... | @@ -27,7 +27,7 @@ |
27 | "docs": "npm-run-all docs:*", | 27 | "docs": "npm-run-all docs:*", |
28 | "docs:api": "jsdoc src -r -d docs/api", | 28 | "docs:api": "jsdoc src -r -d docs/api", |
29 | "docs:toc": "doctoc README.md", | 29 | "docs:toc": "doctoc README.md", |
30 | "lint": "vjsstandard :", | 30 | "lint": "vjsstandard", |
31 | "prestart": "npm-run-all docs build", | 31 | "prestart": "npm-run-all docs build", |
32 | "start": "npm-run-all -p start:* watch:*", | 32 | "start": "npm-run-all -p start:* watch:*", |
33 | "start:serve": "babel-node scripts/server.js", | 33 | "start:serve": "babel-node scripts/server.js", |
... | @@ -40,7 +40,7 @@ | ... | @@ -40,7 +40,7 @@ |
40 | "preversion": "npm test", | 40 | "preversion": "npm test", |
41 | "version": "npm run build", | 41 | "version": "npm run build", |
42 | "watch": "npm-run-all -p watch:*", | 42 | "watch": "npm-run-all -p watch:*", |
43 | "watch:js": "watchify src/stub.js -t babelify -v -o dist/videojs-contrib-hls.js", | 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:*", | 44 | "watch:test": "npm-run-all -p watch:test:*", |
45 | "watch:test:js": "node scripts/watch-test.js", | 45 | "watch:test:js": "node scripts/watch-test.js", |
46 | "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"", | 46 | "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"", |
... | @@ -72,7 +72,8 @@ | ... | @@ -72,7 +72,8 @@ |
72 | "scripts", | 72 | "scripts", |
73 | "utils", | 73 | "utils", |
74 | "test/test-manifests.js", | 74 | "test/test-manifests.js", |
75 | "test/test-expected.js" | 75 | "test/test-expected.js", |
76 | "src/playlist-loader.js" | ||
76 | ] | 77 | ] |
77 | }, | 78 | }, |
78 | "files": [ | 79 | "files": [ |
... | @@ -88,10 +89,10 @@ | ... | @@ -88,10 +89,10 @@ |
88 | "utils/" | 89 | "utils/" |
89 | ], | 90 | ], |
90 | "dependencies": { | 91 | "dependencies": { |
91 | "pkcs7": "^0.2.3", | 92 | "pkcs7": "^0.2.2", |
92 | "video.js": "^5.0.0", | 93 | "video.js": "^5.2.1", |
93 | "videojs-contrib-media-sources": "^2.4.4", | 94 | "videojs-contrib-media-sources": "^3.0.0", |
94 | "videojs-swf": "^5.0.1" | 95 | "videojs-swf": "^5.0.0" |
95 | }, | 96 | }, |
96 | "devDependencies": { | 97 | "devDependencies": { |
97 | "babel": "^5.8.0", | 98 | "babel": "^5.8.0", |
... | @@ -117,12 +118,13 @@ | ... | @@ -117,12 +118,13 @@ |
117 | "minimist": "^1.2.0", | 118 | "minimist": "^1.2.0", |
118 | "npm-run-all": "^1.2.0", | 119 | "npm-run-all": "^1.2.0", |
119 | "portscanner": "^1.0.0", | 120 | "portscanner": "^1.0.0", |
120 | "qunitjs": "^1.0.0", | 121 | "qunitjs": "^1.18.0", |
121 | "serve-static": "^1.10.0", | 122 | "serve-static": "^1.10.0", |
122 | "shelljs": "^0.5.3", | 123 | "shelljs": "^0.5.3", |
123 | "sinon": "1.10.2", | 124 | "sinon": "1.10.2", |
124 | "uglify-js": "^2.5.0", | 125 | "uglify-js": "^2.5.0", |
125 | "videojs-standard": "^4.0.0", | 126 | "videojs-standard": "^4.0.0", |
126 | "watchify": "^3.6.0" | 127 | "watchify": "^3.6.0", |
128 | "webworkify": "^1.1.0" | ||
127 | } | 129 | } |
128 | } | 130 | } | ... | ... |
... | @@ -2,7 +2,7 @@ var browserify = require('browserify'); | ... | @@ -2,7 +2,7 @@ var browserify = require('browserify'); |
2 | var fs = require('fs'); | 2 | var fs = require('fs'); |
3 | var glob = require('glob'); | 3 | var glob = require('glob'); |
4 | 4 | ||
5 | glob('test/{playlist*,decryper,m3u8,stub}.test.js', function(err, files) { | 5 | glob('test/**/*.test.js', function(err, files) { |
6 | browserify(files) | 6 | browserify(files) |
7 | .transform('babelify') | 7 | .transform('babelify') |
8 | .bundle() | 8 | .bundle() | ... | ... |
... | @@ -3,7 +3,7 @@ var fs = require('fs'); | ... | @@ -3,7 +3,7 @@ var fs = require('fs'); |
3 | var glob = require('glob'); | 3 | var glob = require('glob'); |
4 | var watchify = require('watchify'); | 4 | var watchify = require('watchify'); |
5 | 5 | ||
6 | glob('test/{playlist*,decrypter,m3u8,stub}.test.js', function(err, files) { | 6 | glob('test/**/*.test.js', function(err, files) { |
7 | var b = browserify(files, { | 7 | var b = browserify(files, { |
8 | cache: {}, | 8 | cache: {}, |
9 | packageCache: {}, | 9 | packageCache: {}, | ... | ... |
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); | ... | ... |
... | @@ -89,10 +89,9 @@ const precompute = function() { | ... | @@ -89,10 +89,9 @@ const precompute = function() { |
89 | decTable[i] = decTable[i].slice(0); | 89 | decTable[i] = decTable[i].slice(0); |
90 | } | 90 | } |
91 | return tables; | 91 | return tables; |
92 | } | 92 | }; |
93 | |||
94 | |||
95 | let aesTables = null; | 93 | let aesTables = null; |
94 | |||
96 | /** | 95 | /** |
97 | * Schedule out an AES key for both encryption and decryption. This | 96 | * Schedule out an AES key for both encryption and decryption. This |
98 | * is a low-level class. Use a cipher mode to do bulk encryption. | 97 | * is a low-level class. Use a cipher mode to do bulk encryption. |
... | @@ -116,7 +115,7 @@ export default class AES { | ... | @@ -116,7 +115,7 @@ export default class AES { |
116 | */ | 115 | */ |
117 | // if we have yet to precompute the S-box tables | 116 | // if we have yet to precompute the S-box tables |
118 | // do so now | 117 | // do so now |
119 | if(!aesTables) { | 118 | if (!aesTables) { |
120 | aesTables = precompute(); | 119 | aesTables = precompute(); |
121 | } | 120 | } |
122 | // then make a copy of that object for use | 121 | // then make a copy of that object for use | ... | ... |
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 | export default class Stream { | 4 | export default class Stream { |
6 | constructor() { | 5 | constructor() { |
7 | this.init(); | ||
8 | } | ||
9 | |||
10 | init() { | ||
11 | this.listeners = {}; | 6 | this.listeners = {}; |
12 | } | 7 | } |
13 | 8 | ... | ... |
src/stub.js
deleted
100644 → 0
1 | import m3u8 from './m3u8'; | ||
2 | import Stream from './stream'; | ||
3 | import videojs from 'video.js'; | ||
4 | import {Decrypter, decrypt, AsyncStream} from './decrypter'; | ||
5 | import Playlist from './playlist'; | ||
6 | import PlaylistLoader from './playlist-loader'; | ||
7 | import xhr from './xhr'; | ||
8 | |||
9 | |||
10 | if(typeof window.videojs.Hls === 'undefined') { | ||
11 | videojs.Hls = {}; | ||
12 | } | ||
13 | videojs.Hls.Stream = Stream; | ||
14 | videojs.m3u8 = m3u8; | ||
15 | videojs.Hls.decrypt = decrypt; | ||
16 | videojs.Hls.Decrypter = Decrypter; | ||
17 | videojs.Hls.AsyncStream = AsyncStream; | ||
18 | videojs.Hls.xhr = xhr; | ||
19 | videojs.Hls.Playlist = Playlist; | ||
20 | videojs.Hls.PlaylistLoader = PlaylistLoader; |
1 | /* | 1 | /** |
2 | * videojs-hls | 2 | * videojs-hls |
3 | * The main file for the HLS project. | 3 | * The main file for the HLS project. |
4 | * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE | 4 | * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE |
5 | */ | 5 | */ |
6 | (function(window, videojs, document, undefined) { | 6 | import PlaylistLoader from './playlist-loader'; |
7 | 'use strict'; | 7 | import Playlist from './playlist'; |
8 | import xhr from './xhr'; | ||
9 | import {Decrypter, AsyncStream, decrypt} from './decrypter'; | ||
10 | import utils from './bin-utils'; | ||
11 | import {MediaSource, URL} from 'videojs-contrib-media-sources'; | ||
12 | import m3u8 from './m3u8'; | ||
13 | import videojs from 'video.js'; | ||
14 | import resolveUrl from './resolve-url'; | ||
15 | |||
16 | const Hls = { | ||
17 | PlaylistLoader, | ||
18 | Playlist, | ||
19 | Decrypter, | ||
20 | AsyncStream, | ||
21 | decrypt, | ||
22 | utils, | ||
23 | xhr | ||
24 | }; | ||
25 | |||
26 | // the desired length of video to maintain in the buffer, in seconds | ||
27 | Hls.GOAL_BUFFER_LENGTH = 30; | ||
28 | |||
29 | // HLS is a source handler, not a tech. Make sure attempts to use it | ||
30 | // as one do not cause exceptions. | ||
31 | Hls.canPlaySource = function() { | ||
32 | return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + | ||
33 | 'your player\'s techOrder.'); | ||
34 | }; | ||
35 | |||
36 | // Search for a likely end time for the segment that was just appened | ||
37 | // based on the state of the `buffered` property before and after the | ||
38 | // append. | ||
39 | // If we found only one such uncommon end-point return it. | ||
40 | Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { | ||
41 | let i; | ||
42 | let start; | ||
43 | let end; | ||
44 | let result = []; | ||
45 | let edges = []; | ||
46 | |||
47 | // In order to qualify as a possible candidate, the end point must: | ||
48 | // 1) Not have already existed in the `original` ranges | ||
49 | // 2) Not result from the shrinking of a range that already existed | ||
50 | // in the `original` ranges | ||
51 | // 3) Not be contained inside of a range that existed in `original` | ||
52 | let overlapsCurrentEnd = function(span) { | ||
53 | return (span[0] <= end && span[1] >= end); | ||
54 | }; | ||
55 | |||
56 | if (original) { | ||
57 | // Save all the edges in the `original` TimeRanges object | ||
58 | for (i = 0; i < original.length; i++) { | ||
59 | start = original.start(i); | ||
60 | end = original.end(i); | ||
61 | |||
62 | edges.push([start, end]); | ||
63 | } | ||
64 | } | ||
65 | |||
66 | if (update) { | ||
67 | // Save any end-points in `update` that are not in the `original` | ||
68 | // TimeRanges object | ||
69 | for (i = 0; i < update.length; i++) { | ||
70 | start = update.start(i); | ||
71 | end = update.end(i); | ||
8 | 72 | ||
9 | var | 73 | if (edges.some(overlapsCurrentEnd)) { |
10 | // A fudge factor to apply to advertised playlist bitrates to account for | 74 | continue; |
11 | // temporary flucations in client bandwidth | 75 | } |
12 | bandwidthVariance = 1.2, | ||
13 | blacklistDuration = 5 * 60 * 1000, // 5 minute blacklist | ||
14 | TIME_FUDGE_FACTOR = 1 / 30, // Fudge factor to account for TimeRanges rounding | ||
15 | Component = videojs.getComponent('Component'), | ||
16 | 76 | ||
17 | // The amount of time to wait between checking the state of the buffer | 77 | // at this point it must be a unique non-shrinking end edge |
18 | bufferCheckInterval = 500, | 78 | result.push(end); |
79 | } | ||
80 | } | ||
19 | 81 | ||
20 | keyFailed, | 82 | // we err on the side of caution and return null if didn't find |
21 | resolveUrl; | 83 | // exactly *one* differing end edge in the search above |
84 | if (result.length !== 1) { | ||
85 | return null; | ||
86 | } | ||
87 | |||
88 | return result[0]; | ||
89 | }; | ||
90 | |||
91 | /** | ||
92 | * Whether the browser has built-in HLS support. | ||
93 | */ | ||
94 | Hls.supportsNativeHls = (function() { | ||
95 | let video = document.createElement('video'); | ||
96 | let xMpegUrl; | ||
97 | let vndMpeg; | ||
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; | ||
22 | 194 | ||
23 | // returns true if a key has failed to download within a certain amount of retries | 195 | // returns true if a key has failed to download within a certain amount of retries |
24 | keyFailed = function(key) { | 196 | const keyFailed = function(key) { |
25 | return key.retries && key.retries >= 2; | 197 | return key.retries && key.retries >= 2; |
26 | }; | 198 | }; |
27 | 199 | ||
28 | videojs.Hls = {}; | 200 | const parseCodecs = function(codecs) { |
29 | videojs.HlsHandler = videojs.extend(Component, { | 201 | let result = { |
30 | constructor: function(tech, options) { | 202 | codecCount: 0, |
31 | var self = this, _player; | 203 | videoCodec: null, |
204 | audioProfile: null | ||
205 | }; | ||
32 | 206 | ||
33 | Component.call(this, tech); | 207 | result.codecCount = codecs.split(',').length; |
208 | result.codecCount = result.codecCount || 2; | ||
209 | |||
210 | // parse the video codec but ignore the version | ||
211 | result.videoCodec = (/(^|\s|,)+(avc1)[^ ,]*/i).exec(codecs); | ||
212 | result.videoCodec = result.videoCodec && result.videoCodec[2]; | ||
213 | |||
214 | // parse the last field of the audio codec | ||
215 | result.audioProfile = (/(^|\s|,)+mp4a.\d+\.(\d+)/i).exec(codecs); | ||
216 | result.audioProfile = result.audioProfile && result.audioProfile[2]; | ||
217 | |||
218 | return result; | ||
219 | }; | ||
220 | |||
221 | const filterBufferedRanges = function(predicate) { | ||
222 | return function(time) { | ||
223 | let i; | ||
224 | let ranges = []; | ||
225 | let tech = this.tech_; | ||
226 | // !!The order of the next two assignments is important!! | ||
227 | // `currentTime` must be equal-to or greater-than the start of the | ||
228 | // buffered range. Flash executes out-of-process so, every value can | ||
229 | // change behind the scenes from line-to-line. By reading `currentTime` | ||
230 | // after `buffered`, we ensure that it is always a current or later | ||
231 | // value during playback. | ||
232 | let buffered = tech.buffered(); | ||
233 | |||
234 | if (typeof time === 'undefined') { | ||
235 | time = tech.currentTime(); | ||
236 | } | ||
237 | |||
238 | if (buffered && buffered.length) { | ||
239 | // Search for a range containing the play-head | ||
240 | for (i = 0; i < buffered.length; i++) { | ||
241 | if (predicate(buffered.start(i), buffered.end(i), time)) { | ||
242 | ranges.push([buffered.start(i), buffered.end(i)]); | ||
243 | } | ||
244 | } | ||
245 | } | ||
246 | |||
247 | return videojs.createTimeRanges(ranges); | ||
248 | }; | ||
249 | }; | ||
250 | |||
251 | export default class HlsHandler extends Component { | ||
252 | constructor(tech, options) { | ||
253 | super(tech); | ||
254 | let _player; | ||
34 | 255 | ||
35 | // tech.player() is deprecated but setup a reference to HLS for | 256 | // tech.player() is deprecated but setup a reference to HLS for |
36 | // backwards-compatibility | 257 | // backwards-compatibility |
... | @@ -38,9 +259,9 @@ videojs.HlsHandler = videojs.extend(Component, { | ... | @@ -38,9 +259,9 @@ videojs.HlsHandler = videojs.extend(Component, { |
38 | _player = videojs(tech.options_.playerId); | 259 | _player = videojs(tech.options_.playerId); |
39 | if (!_player.hls) { | 260 | if (!_player.hls) { |
40 | Object.defineProperty(_player, 'hls', { | 261 | Object.defineProperty(_player, 'hls', { |
41 | get: function() { | 262 | get: () => { |
42 | videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.'); | 263 | videojs.log.warn('player.hls is deprecated. Use player.tech.hls instead.'); |
43 | return self; | 264 | return this; |
44 | } | 265 | } |
45 | }); | 266 | }); |
46 | } | 267 | } |
... | @@ -54,7 +275,8 @@ videojs.HlsHandler = videojs.extend(Component, { | ... | @@ -54,7 +275,8 @@ videojs.HlsHandler = videojs.extend(Component, { |
54 | 275 | ||
55 | // start playlist selection at a reasonable bandwidth for | 276 | // start playlist selection at a reasonable bandwidth for |
56 | // broadband internet | 277 | // broadband internet |
57 | this.bandwidth = options.bandwidth || 4194304; // 0.5 Mbps | 278 | // 0.5 Mbps |
279 | this.bandwidth = options.bandwidth || 4194304; | ||
58 | this.bytesReceived = 0; | 280 | this.bytesReceived = 0; |
59 | 281 | ||
60 | // loadingState_ tracks how far along the buffering process we | 282 | // loadingState_ tracks how far along the buffering process we |
... | @@ -81,1409 +303,1200 @@ videojs.HlsHandler = videojs.extend(Component, { | ... | @@ -81,1409 +303,1200 @@ videojs.HlsHandler = videojs.extend(Component, { |
81 | 303 | ||
82 | this.on(this.tech_, 'play', this.play); | 304 | this.on(this.tech_, 'play', this.play); |
83 | } | 305 | } |
84 | }); | 306 | src(src) { |
85 | 307 | let oldMediaPlaylist; | |
86 | // HLS is a source handler, not a tech. Make sure attempts to use it | ||
87 | // as one do not cause exceptions. | ||
88 | videojs.Hls.canPlaySource = function() { | ||
89 | return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + | ||
90 | 'your player\'s techOrder.'); | ||
91 | }; | ||
92 | 308 | ||
93 | /** | 309 | // do nothing if the src is falsey |
94 | * The Source Handler object, which informs video.js what additional | 310 | if (!src) { |
95 | * MIME types are supported and sets up playback. It is registered | 311 | return; |
96 | * automatically to the appropriate tech based on the capabilities of | ||
97 | * the browser it is running in. It is not necessary to use or modify | ||
98 | * this object in normal usage. | ||
99 | */ | ||
100 | videojs.HlsSourceHandler = function(mode) { | ||
101 | return { | ||
102 | canHandleSource: function(srcObj) { | ||
103 | return videojs.HlsSourceHandler.canPlayType(srcObj.type); | ||
104 | }, | ||
105 | handleSource: function(source, tech) { | ||
106 | if (mode === 'flash') { | ||
107 | // We need to trigger this asynchronously to give others the chance | ||
108 | // to bind to the event when a source is set at player creation | ||
109 | tech.setTimeout(function() { | ||
110 | tech.trigger('loadstart'); | ||
111 | }, 1); | ||
112 | } | ||
113 | tech.hls = new videojs.HlsHandler(tech, { | ||
114 | source: source, | ||
115 | mode: mode | ||
116 | }); | ||
117 | tech.hls.src(source.src); | ||
118 | return tech.hls; | ||
119 | }, | ||
120 | canPlayType: function(type) { | ||
121 | return videojs.HlsSourceHandler.canPlayType(type); | ||
122 | } | 312 | } |
123 | }; | ||
124 | }; | ||
125 | 313 | ||
126 | videojs.HlsSourceHandler.canPlayType = function(type) { | 314 | this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); |
127 | var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; | ||
128 | 315 | ||
129 | // favor native HLS support if it's available | 316 | // load the MediaSource into the player |
130 | if (videojs.Hls.supportsNativeHls) { | 317 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); |
131 | return false; | ||
132 | } | ||
133 | return mpegurlRE.test(type); | ||
134 | }; | ||
135 | 318 | ||
136 | // register source handlers with the appropriate techs | 319 | this.options_ = {}; |
137 | if (videojs.MediaSource.supportsNativeMediaSources()) { | 320 | if (typeof this.source_.withCredentials !== 'undefined') { |
138 | videojs.getComponent('Html5').registerSourceHandler(videojs.HlsSourceHandler('html5')); | 321 | this.options_.withCredentials = this.source_.withCredentials; |
139 | } | 322 | } else if (videojs.options.hls) { |
140 | if (window.Uint8Array) { | 323 | this.options_.withCredentials = videojs.options.hls.withCredentials; |
141 | videojs.getComponent('Flash').registerSourceHandler(videojs.HlsSourceHandler('flash')); | 324 | } |
142 | } | 325 | this.playlists = new Hls.PlaylistLoader(this.source_.src, |
143 | 326 | this.options_.withCredentials); | |
144 | // the desired length of video to maintain in the buffer, in seconds | ||
145 | videojs.Hls.GOAL_BUFFER_LENGTH = 30; | ||
146 | 327 | ||
147 | videojs.HlsHandler.prototype.src = function(src) { | 328 | this.tech_.one('canplay', this.setupFirstPlay.bind(this)); |
148 | var oldMediaPlaylist; | ||
149 | 329 | ||
150 | // do nothing if the src is falsey | 330 | this.playlists.on('loadedmetadata', () => { |
151 | if (!src) { | 331 | oldMediaPlaylist = this.playlists.media(); |
152 | return; | ||
153 | } | ||
154 | 332 | ||
155 | this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); | 333 | // if this isn't a live video and preload permits, start |
334 | // downloading segments | ||
335 | if (oldMediaPlaylist.endList && | ||
336 | this.tech_.preload() !== 'metadata' && | ||
337 | this.tech_.preload() !== 'none') { | ||
338 | this.loadingState_ = 'segments'; | ||
339 | } | ||
156 | 340 | ||
157 | // load the MediaSource into the player | 341 | this.setupSourceBuffer_(); |
158 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); | 342 | this.setupFirstPlay(); |
343 | this.fillBuffer(); | ||
344 | this.tech_.trigger('loadedmetadata'); | ||
345 | }); | ||
159 | 346 | ||
160 | this.options_ = {}; | 347 | this.playlists.on('error', () => { |
161 | if (this.source_.withCredentials !== undefined) { | 348 | this.blacklistCurrentPlaylist_(this.playlists.error); |
162 | this.options_.withCredentials = this.source_.withCredentials; | 349 | }); |
163 | } else if (videojs.options.hls) { | ||
164 | this.options_.withCredentials = videojs.options.hls.withCredentials; | ||
165 | } | ||
166 | this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials); | ||
167 | 350 | ||
168 | this.tech_.one('canplay', this.setupFirstPlay.bind(this)); | 351 | this.playlists.on('loadedplaylist', () => { |
352 | let updatedPlaylist = this.playlists.media(); | ||
353 | let seekable; | ||
169 | 354 | ||
170 | this.playlists.on('loadedmetadata', function() { | 355 | if (!updatedPlaylist) { |
171 | oldMediaPlaylist = this.playlists.media(); | 356 | // select the initial variant |
357 | this.playlists.media(this.selectPlaylist()); | ||
358 | return; | ||
359 | } | ||
172 | 360 | ||
173 | // if this isn't a live video and preload permits, start | 361 | this.updateDuration(this.playlists.media()); |
174 | // downloading segments | ||
175 | if (oldMediaPlaylist.endList && | ||
176 | this.tech_.preload() !== 'metadata' && | ||
177 | this.tech_.preload() !== 'none') { | ||
178 | this.loadingState_ = 'segments'; | ||
179 | } | ||
180 | 362 | ||
181 | this.setupSourceBuffer_(); | 363 | // update seekable |
182 | this.setupFirstPlay(); | 364 | seekable = this.seekable(); |
183 | this.fillBuffer(); | 365 | if (this.duration() === Infinity && |
184 | this.tech_.trigger('loadedmetadata'); | 366 | seekable.length !== 0) { |
185 | }.bind(this)); | 367 | this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0)); |
368 | } | ||
186 | 369 | ||
187 | this.playlists.on('error', function() { | 370 | oldMediaPlaylist = updatedPlaylist; |
188 | this.blacklistCurrentPlaylist_(this.playlists.error); | 371 | }); |
189 | }.bind(this)); | ||
190 | 372 | ||
191 | this.playlists.on('loadedplaylist', function() { | 373 | this.playlists.on('mediachange', () => { |
192 | var updatedPlaylist = this.playlists.media(), seekable; | 374 | this.tech_.trigger({ |
375 | type: 'mediachange', | ||
376 | bubbles: true | ||
377 | }); | ||
378 | }); | ||
193 | 379 | ||
194 | if (!updatedPlaylist) { | 380 | // do nothing if the tech has been disposed already |
195 | // select the initial variant | 381 | // this can occur if someone sets the src in player.ready(), for instance |
196 | this.playlists.media(this.selectPlaylist()); | 382 | if (!this.tech_.el()) { |
197 | return; | 383 | return; |
198 | } | 384 | } |
199 | 385 | ||
200 | this.updateDuration(this.playlists.media()); | 386 | this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); |
201 | |||
202 | // update seekable | ||
203 | seekable = this.seekable(); | ||
204 | if (this.duration() === Infinity && | ||
205 | seekable.length !== 0) { | ||
206 | this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0)); | ||
207 | } | ||
208 | |||
209 | oldMediaPlaylist = updatedPlaylist; | ||
210 | }.bind(this)); | ||
211 | |||
212 | this.playlists.on('mediachange', function() { | ||
213 | this.tech_.trigger({ | ||
214 | type: 'mediachange', | ||
215 | bubbles: true | ||
216 | }); | ||
217 | }.bind(this)); | ||
218 | |||
219 | // do nothing if the tech has been disposed already | ||
220 | // this can occur if someone sets the src in player.ready(), for instance | ||
221 | if (!this.tech_.el()) { | ||
222 | return; | ||
223 | } | ||
224 | |||
225 | this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); | ||
226 | }; | ||
227 | |||
228 | videojs.HlsHandler.prototype.handleSourceOpen = function() { | ||
229 | // Only attempt to create the source buffer if none already exist. | ||
230 | // handleSourceOpen is also called when we are "re-opening" a source buffer | ||
231 | // after `endOfStream` has been called (in response to a seek for instance) | ||
232 | if (!this.sourceBuffer) { | ||
233 | this.setupSourceBuffer_(); | ||
234 | } | 387 | } |
388 | handleSourceOpen() { | ||
389 | // Only attempt to create the source buffer if none already exist. | ||
390 | // handleSourceOpen is also called when we are "re-opening" a source buffer | ||
391 | // after `endOfStream` has been called (in response to a seek for instance) | ||
392 | if (!this.sourceBuffer) { | ||
393 | this.setupSourceBuffer_(); | ||
394 | } | ||
235 | 395 | ||
236 | // if autoplay is enabled, begin playback. This is duplicative of | 396 | // if autoplay is enabled, begin playback. This is duplicative of |
237 | // code in video.js but is required because play() must be invoked | 397 | // code in video.js but is required because play() must be invoked |
238 | // *after* the media source has opened. | 398 | // *after* the media source has opened. |
239 | // NOTE: moving this invocation of play() after | 399 | // NOTE: moving this invocation of play() after |
240 | // sourceBuffer.appendBuffer() below caused live streams with | 400 | // sourceBuffer.appendBuffer() below caused live streams with |
241 | // autoplay to stall | 401 | // autoplay to stall |
242 | if (this.tech_.autoplay()) { | 402 | if (this.tech_.autoplay()) { |
243 | this.play(); | 403 | this.play(); |
244 | } | ||
245 | }; | ||
246 | |||
247 | // Search for a likely end time for the segment that was just appened | ||
248 | // based on the state of the `buffered` property before and after the | ||
249 | // append. | ||
250 | // If we found only one such uncommon end-point return it. | ||
251 | videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { | ||
252 | var | ||
253 | i, start, end, | ||
254 | result = [], | ||
255 | edges = [], | ||
256 | // In order to qualify as a possible candidate, the end point must: | ||
257 | // 1) Not have already existed in the `original` ranges | ||
258 | // 2) Not result from the shrinking of a range that already existed | ||
259 | // in the `original` ranges | ||
260 | // 3) Not be contained inside of a range that existed in `original` | ||
261 | overlapsCurrentEnd = function(span) { | ||
262 | return (span[0] <= end && span[1] >= end); | ||
263 | }; | ||
264 | |||
265 | if (original) { | ||
266 | // Save all the edges in the `original` TimeRanges object | ||
267 | for (i = 0; i < original.length; i++) { | ||
268 | start = original.start(i); | ||
269 | end = original.end(i); | ||
270 | |||
271 | edges.push([start, end]); | ||
272 | } | 404 | } |
273 | } | 405 | } |
274 | 406 | ||
275 | if (update) { | 407 | /** |
276 | // Save any end-points in `update` that are not in the `original` | 408 | * Blacklist playlists that are known to be codec or |
277 | // TimeRanges object | 409 | * stream-incompatible with the SourceBuffer configuration. For |
278 | for (i = 0; i < update.length; i++) { | 410 | * instance, Media Source Extensions would cause the video element to |
279 | start = update.start(i); | 411 | * stall waiting for video data if you switched from a variant with |
280 | end = update.end(i); | 412 | * video and audio to an audio-only one. |
281 | 413 | * | |
282 | if (edges.some(overlapsCurrentEnd)) { | 414 | * @param media {object} a media playlist compatible with the current |
283 | continue; | 415 | * set of SourceBuffers. Variants in the current master playlist that |
416 | * do not appear to have compatible codec or stream configurations | ||
417 | * will be excluded from the default playlist selection algorithm | ||
418 | * indefinitely. | ||
419 | */ | ||
420 | excludeIncompatibleVariants_(media) { | ||
421 | let master = this.playlists.master; | ||
422 | let codecCount = 2; | ||
423 | let videoCodec = null; | ||
424 | let audioProfile = null; | ||
425 | let codecs; | ||
426 | |||
427 | if (media.attributes && media.attributes.CODECS) { | ||
428 | codecs = parseCodecs(media.attributes.CODECS); | ||
429 | videoCodec = codecs.videoCodec; | ||
430 | audioProfile = codecs.audioProfile; | ||
431 | codecCount = codecs.codecCount; | ||
432 | } | ||
433 | master.playlists.forEach(function(variant) { | ||
434 | let variantCodecs = { | ||
435 | codecCount: 2, | ||
436 | videoCodec: null, | ||
437 | audioProfile: null | ||
438 | }; | ||
439 | |||
440 | if (variant.attributes && variant.attributes.CODECS) { | ||
441 | variantCodecs = parseCodecs(variant.attributes.CODECS); | ||
284 | } | 442 | } |
285 | 443 | ||
286 | // at this point it must be a unique non-shrinking end edge | 444 | // if the streams differ in the presence or absence of audio or |
287 | result.push(end); | 445 | // video, they are incompatible |
288 | } | 446 | if (variantCodecs.codecCount !== codecCount) { |
289 | } | 447 | variant.excludeUntil = Infinity; |
448 | } | ||
290 | 449 | ||
291 | // we err on the side of caution and return null if didn't find | 450 | // if h.264 is specified on the current playlist, some flavor of |
292 | // exactly *one* differing end edge in the search above | 451 | // it must be specified on all compatible variants |
293 | if (result.length !== 1) { | 452 | if (variantCodecs.videoCodec !== videoCodec) { |
294 | return null; | 453 | variant.excludeUntil = Infinity; |
454 | } | ||
455 | // HE-AAC ("mp4a.40.5") is incompatible with all other versions of | ||
456 | // AAC audio in Chrome 46. Don't mix the two. | ||
457 | if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') || | ||
458 | (audioProfile === '5' && variantCodecs.audioProfile !== '5')) { | ||
459 | variant.excludeUntil = Infinity; | ||
460 | } | ||
461 | }); | ||
295 | } | 462 | } |
296 | 463 | ||
297 | return result[0]; | 464 | setupSourceBuffer_() { |
298 | }; | 465 | let media = this.playlists.media(); |
299 | 466 | let mimeType; | |
300 | var parseCodecs = function(codecs) { | ||
301 | var result = { | ||
302 | codecCount: 0, | ||
303 | videoCodec: null, | ||
304 | audioProfile: null | ||
305 | }; | ||
306 | |||
307 | result.codecCount = codecs.split(',').length; | ||
308 | result.codecCount = result.codecCount || 2; | ||
309 | |||
310 | // parse the video codec but ignore the version | ||
311 | result.videoCodec = /(^|\s|,)+(avc1)[^ ,]*/i.exec(codecs); | ||
312 | result.videoCodec = result.videoCodec && result.videoCodec[2]; | ||
313 | |||
314 | // parse the last field of the audio codec | ||
315 | result.audioProfile = /(^|\s|,)+mp4a.\d+\.(\d+)/i.exec(codecs); | ||
316 | result.audioProfile = result.audioProfile && result.audioProfile[2]; | ||
317 | |||
318 | return result; | ||
319 | }; | ||
320 | |||
321 | /** | ||
322 | * Blacklist playlists that are known to be codec or | ||
323 | * stream-incompatible with the SourceBuffer configuration. For | ||
324 | * instance, Media Source Extensions would cause the video element to | ||
325 | * stall waiting for video data if you switched from a variant with | ||
326 | * video and audio to an audio-only one. | ||
327 | * | ||
328 | * @param media {object} a media playlist compatible with the current | ||
329 | * set of SourceBuffers. Variants in the current master playlist that | ||
330 | * do not appear to have compatible codec or stream configurations | ||
331 | * will be excluded from the default playlist selection algorithm | ||
332 | * indefinitely. | ||
333 | */ | ||
334 | videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) { | ||
335 | var | ||
336 | master = this.playlists.master, | ||
337 | codecCount = 2, | ||
338 | videoCodec = null, | ||
339 | audioProfile = null, | ||
340 | codecs; | ||
341 | |||
342 | if (media.attributes && media.attributes.CODECS) { | ||
343 | codecs = parseCodecs(media.attributes.CODECS); | ||
344 | videoCodec = codecs.videoCodec; | ||
345 | audioProfile = codecs.audioProfile; | ||
346 | codecCount = codecs.codecCount; | ||
347 | } | ||
348 | master.playlists.forEach(function(variant) { | ||
349 | var variantCodecs = { | ||
350 | codecCount: 2, | ||
351 | videoCodec: null, | ||
352 | audioProfile: null | ||
353 | }; | ||
354 | 467 | ||
355 | if (variant.attributes && variant.attributes.CODECS) { | 468 | // wait until a media playlist is available and the Media Source is |
356 | variantCodecs = parseCodecs(variant.attributes.CODECS); | 469 | // attached |
357 | } | 470 | if (!media || this.mediaSource.readyState !== 'open') { |
358 | 471 | return; | |
359 | // if the streams differ in the presence or absence of audio or | ||
360 | // video, they are incompatible | ||
361 | if (variantCodecs.codecCount !== codecCount) { | ||
362 | variant.excludeUntil = Infinity; | ||
363 | } | 472 | } |
364 | 473 | ||
365 | // if h.264 is specified on the current playlist, some flavor of | 474 | // if the codecs were explicitly specified, pass them along to the |
366 | // it must be specified on all compatible variants | 475 | // source buffer |
367 | if (variantCodecs.videoCodec !== videoCodec) { | 476 | mimeType = 'video/mp2t'; |
368 | variant.excludeUntil = Infinity; | 477 | if (media.attributes && media.attributes.CODECS) { |
369 | } | 478 | mimeType += '; codecs="' + media.attributes.CODECS + '"'; |
370 | // HE-AAC ("mp4a.40.5") is incompatible with all other versions of | ||
371 | // AAC audio in Chrome 46. Don't mix the two. | ||
372 | if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') || | ||
373 | (audioProfile === '5' && variantCodecs.audioProfile !== '5')) { | ||
374 | variant.excludeUntil = Infinity; | ||
375 | } | 479 | } |
376 | }); | 480 | this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType); |
377 | }; | ||
378 | 481 | ||
379 | videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { | 482 | // exclude any incompatible variant streams from future playlist |
380 | var media = this.playlists.media(), mimeType; | 483 | // selection |
484 | this.excludeIncompatibleVariants_(media); | ||
381 | 485 | ||
382 | // wait until a media playlist is available and the Media Source is | 486 | // transition the sourcebuffer to the ended state if we've hit the end of |
383 | // attached | 487 | // the playlist |
384 | if (!media || this.mediaSource.readyState !== 'open') { | 488 | this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this)); |
385 | return; | ||
386 | } | 489 | } |
387 | 490 | ||
388 | // if the codecs were explicitly specified, pass them along to the | 491 | /** |
389 | // source buffer | 492 | * Seek to the latest media position if this is a live video and the |
390 | mimeType = 'video/mp2t'; | 493 | * player and video are loaded and initialized. |
391 | if (media.attributes && media.attributes.CODECS) { | 494 | */ |
392 | mimeType += '; codecs="' + media.attributes.CODECS + '"'; | 495 | setupFirstPlay() { |
393 | } | 496 | let seekable; |
394 | this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType); | 497 | let media = this.playlists.media(); |
395 | 498 | ||
396 | // exclude any incompatible variant streams from future playlist | 499 | // check that everything is ready to begin buffering |
397 | // selection | ||
398 | this.excludeIncompatibleVariants_(media); | ||
399 | 500 | ||
400 | // transition the sourcebuffer to the ended state if we've hit the end of | 501 | // 1) the video is a live stream of unknown duration |
401 | // the playlist | 502 | if (this.duration() === Infinity && |
402 | this.sourceBuffer.addEventListener('updateend', this.updateEndHandler_.bind(this)); | ||
403 | }; | ||
404 | 503 | ||
405 | /** | 504 | // 2) the player has not played before and is not paused |
406 | * Seek to the latest media position if this is a live video and the | 505 | this.tech_.played().length === 0 && |
407 | * player and video are loaded and initialized. | 506 | !this.tech_.paused() && |
408 | */ | ||
409 | videojs.HlsHandler.prototype.setupFirstPlay = function() { | ||
410 | var seekable, media; | ||
411 | media = this.playlists.media(); | ||
412 | 507 | ||
508 | // 3) the Media Source and Source Buffers are ready | ||
509 | this.sourceBuffer && | ||
413 | 510 | ||
414 | // check that everything is ready to begin buffering | 511 | // 4) the active media playlist is available |
512 | media && | ||
415 | 513 | ||
416 | // 1) the video is a live stream of unknown duration | 514 | // 5) the video element or flash player is in a readyState of |
417 | if (this.duration() === Infinity && | 515 | // at least HAVE_FUTURE_DATA |
516 | this.tech_.readyState() >= 1) { | ||
418 | 517 | ||
419 | // 2) the player has not played before and is not paused | 518 | // trigger the playlist loader to start "expired time"-tracking |
420 | this.tech_.played().length === 0 && | 519 | this.playlists.trigger('firstplay'); |
421 | !this.tech_.paused() && | ||
422 | 520 | ||
423 | // 3) the Media Source and Source Buffers are ready | 521 | // seek to the latest media position for live videos |
424 | this.sourceBuffer && | 522 | seekable = this.seekable(); |
523 | if (seekable.length) { | ||
524 | this.tech_.setCurrentTime(seekable.end(0)); | ||
525 | } | ||
526 | } | ||
527 | } | ||
425 | 528 | ||
426 | // 4) the active media playlist is available | 529 | /** |
427 | media && | 530 | * Begin playing the video. |
531 | */ | ||
532 | play() { | ||
533 | this.loadingState_ = 'segments'; | ||
428 | 534 | ||
429 | // 5) the video element or flash player is in a readyState of | 535 | if (this.tech_.ended()) { |
430 | // at least HAVE_FUTURE_DATA | 536 | this.tech_.setCurrentTime(0); |
431 | this.tech_.readyState() >= 1) { | 537 | } |
432 | 538 | ||
433 | // trigger the playlist loader to start "expired time"-tracking | 539 | if (this.tech_.played().length === 0) { |
434 | this.playlists.trigger('firstplay'); | 540 | return this.setupFirstPlay(); |
541 | } | ||
435 | 542 | ||
436 | // seek to the latest media position for live videos | 543 | // if the viewer has paused and we fell out of the live window, |
437 | seekable = this.seekable(); | 544 | // seek forward to the earliest available position |
438 | if (seekable.length) { | 545 | if (this.duration() === Infinity) { |
439 | this.tech_.setCurrentTime(seekable.end(0)); | 546 | if (this.tech_.currentTime() < this.seekable().start(0)) { |
547 | this.tech_.setCurrentTime(this.seekable().start(0)); | ||
548 | } | ||
440 | } | 549 | } |
441 | } | 550 | } |
442 | }; | ||
443 | 551 | ||
444 | /** | 552 | setCurrentTime(currentTime) { |
445 | * Begin playing the video. | 553 | let buffered = this.findBufferedRange_(); |
446 | */ | ||
447 | videojs.HlsHandler.prototype.play = function() { | ||
448 | this.loadingState_ = 'segments'; | ||
449 | 554 | ||
450 | if (this.tech_.ended()) { | 555 | if (!(this.playlists && this.playlists.media())) { |
451 | this.tech_.setCurrentTime(0); | 556 | // return immediately if the metadata is not ready yet |
452 | } | 557 | return 0; |
558 | } | ||
453 | 559 | ||
454 | if (this.tech_.played().length === 0) { | 560 | // it's clearly an edge-case but don't thrown an error if asked to |
455 | return this.setupFirstPlay(); | 561 | // seek within an empty playlist |
456 | } | 562 | if (!this.playlists.media().segments) { |
563 | return 0; | ||
564 | } | ||
457 | 565 | ||
458 | // if the viewer has paused and we fell out of the live window, | 566 | // if the seek location is already buffered, continue buffering as |
459 | // seek forward to the earliest available position | 567 | // usual |
460 | if (this.duration() === Infinity) { | 568 | if (buffered && buffered.length) { |
461 | if (this.tech_.currentTime() < this.seekable().start(0)) { | 569 | return currentTime; |
462 | this.tech_.setCurrentTime(this.seekable().start(0)); | ||
463 | } | 570 | } |
464 | } | ||
465 | }; | ||
466 | 571 | ||
467 | videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { | 572 | // if we are in the middle of appending a segment, let it finish up |
468 | var | 573 | if (this.pendingSegment_ && this.pendingSegment_.buffered) { |
469 | buffered = this.findBufferedRange_(); | 574 | return currentTime; |
575 | } | ||
470 | 576 | ||
471 | if (!(this.playlists && this.playlists.media())) { | 577 | this.lastSegmentLoaded_ = null; |
472 | // return immediately if the metadata is not ready yet | ||
473 | return 0; | ||
474 | } | ||
475 | 578 | ||
476 | // it's clearly an edge-case but don't thrown an error if asked to | 579 | // cancel outstanding requests and buffer appends |
477 | // seek within an empty playlist | 580 | this.cancelSegmentXhr(); |
478 | if (!this.playlists.media().segments) { | ||
479 | return 0; | ||
480 | } | ||
481 | 581 | ||
482 | // if the seek location is already buffered, continue buffering as | 582 | // abort outstanding key requests, if necessary |
483 | // usual | 583 | if (this.keyXhr_) { |
484 | if (buffered && buffered.length) { | 584 | this.keyXhr_.aborted = true; |
485 | return currentTime; | 585 | this.cancelKeyXhr(); |
486 | } | 586 | } |
487 | 587 | ||
488 | // if we are in the middle of appending a segment, let it finish up | 588 | // begin filling the buffer at the new position |
489 | if (this.pendingSegment_ && this.pendingSegment_.buffered) { | 589 | this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime)); |
490 | return currentTime; | ||
491 | } | 590 | } |
492 | 591 | ||
493 | this.lastSegmentLoaded_ = null; | 592 | duration() { |
593 | let playlists = this.playlists; | ||
494 | 594 | ||
495 | // cancel outstanding requests and buffer appends | 595 | if (playlists) { |
496 | this.cancelSegmentXhr(); | 596 | return Hls.Playlist.duration(playlists.media()); |
497 | 597 | } | |
498 | // abort outstanding key requests, if necessary | 598 | return 0; |
499 | if (this.keyXhr_) { | ||
500 | this.keyXhr_.aborted = true; | ||
501 | this.cancelKeyXhr(); | ||
502 | } | 599 | } |
503 | 600 | ||
504 | // begin filling the buffer at the new position | 601 | seekable() { |
505 | this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime)); | 602 | let media; |
506 | }; | 603 | let seekable; |
507 | 604 | ||
508 | videojs.HlsHandler.prototype.duration = function() { | 605 | if (!this.playlists) { |
509 | var playlists = this.playlists; | 606 | return videojs.createTimeRanges(); |
510 | if (playlists) { | 607 | } |
511 | return videojs.Hls.Playlist.duration(playlists.media()); | 608 | media = this.playlists.media(); |
512 | } | 609 | if (!media) { |
513 | return 0; | 610 | return videojs.createTimeRanges(); |
514 | }; | 611 | } |
515 | 612 | ||
516 | videojs.HlsHandler.prototype.seekable = function() { | 613 | seekable = Hls.Playlist.seekable(media); |
517 | var media, seekable; | 614 | if (seekable.length === 0) { |
615 | return seekable; | ||
616 | } | ||
518 | 617 | ||
519 | if (!this.playlists) { | 618 | // if the seekable start is zero, it may be because the player has |
520 | return videojs.createTimeRanges(); | 619 | // been paused for a long time and stopped buffering. in that case, |
521 | } | 620 | // fall back to the playlist loader's running estimate of expired |
522 | media = this.playlists.media(); | 621 | // time |
523 | if (!media) { | 622 | if (seekable.start(0) === 0) { |
524 | return videojs.createTimeRanges(); | 623 | return videojs.createTimeRanges([[this.playlists.expired_, |
525 | } | 624 | this.playlists.expired_ + seekable.end(0)]]); |
625 | } | ||
526 | 626 | ||
527 | seekable = videojs.Hls.Playlist.seekable(media); | 627 | // seekable has been calculated based on buffering video data so it |
528 | if (seekable.length === 0) { | 628 | // can be returned directly |
529 | return seekable; | 629 | return seekable; |
530 | } | 630 | } |
531 | 631 | ||
532 | // if the seekable start is zero, it may be because the player has | 632 | /** |
533 | // been paused for a long time and stopped buffering. in that case, | 633 | * Update the player duration |
534 | // fall back to the playlist loader's running estimate of expired | 634 | */ |
535 | // time | 635 | updateDuration(playlist) { |
536 | if (seekable.start(0) === 0) { | 636 | let oldDuration = this.mediaSource.duration; |
537 | return videojs.createTimeRanges([[ | 637 | let newDuration = Hls.Playlist.duration(playlist); |
538 | this.playlists.expired_, | 638 | let setDuration = () => { |
539 | this.playlists.expired_ + seekable.end(0) | 639 | this.mediaSource.duration = newDuration; |
540 | ]]); | 640 | this.tech_.trigger('durationchange'); |
541 | } | ||
542 | 641 | ||
543 | // seekable has been calculated based on buffering video data so it | 642 | this.mediaSource.removeEventListener('sourceopen', setDuration); |
544 | // can be returned directly | 643 | }; |
545 | return seekable; | ||
546 | }; | ||
547 | 644 | ||
548 | /** | 645 | // if the duration has changed, invalidate the cached value |
549 | * Update the player duration | 646 | if (oldDuration !== newDuration) { |
550 | */ | 647 | // update the duration |
551 | videojs.HlsHandler.prototype.updateDuration = function(playlist) { | 648 | if (this.mediaSource.readyState !== 'open') { |
552 | var oldDuration = this.mediaSource.duration, | 649 | this.mediaSource.addEventListener('sourceopen', setDuration); |
553 | newDuration = videojs.Hls.Playlist.duration(playlist), | 650 | } else if (!this.sourceBuffer || !this.sourceBuffer.updating) { |
554 | setDuration = function() { | ||
555 | this.mediaSource.duration = newDuration; | 651 | this.mediaSource.duration = newDuration; |
556 | this.tech_.trigger('durationchange'); | 652 | this.tech_.trigger('durationchange'); |
557 | 653 | } | |
558 | this.mediaSource.removeEventListener('sourceopen', setDuration); | ||
559 | }.bind(this); | ||
560 | |||
561 | // if the duration has changed, invalidate the cached value | ||
562 | if (oldDuration !== newDuration) { | ||
563 | // update the duration | ||
564 | if (this.mediaSource.readyState !== 'open') { | ||
565 | this.mediaSource.addEventListener('sourceopen', setDuration); | ||
566 | } else if (!this.sourceBuffer || !this.sourceBuffer.updating) { | ||
567 | this.mediaSource.duration = newDuration; | ||
568 | this.tech_.trigger('durationchange'); | ||
569 | } | 654 | } |
570 | } | 655 | } |
571 | }; | ||
572 | 656 | ||
573 | /** | 657 | /** |
574 | * Clear all buffers and reset any state relevant to the current | 658 | * Clear all buffers and reset any state relevant to the current |
575 | * source. After this function is called, the tech should be in a | 659 | * source. After this function is called, the tech should be in a |
576 | * state suitable for switching to a different video. | 660 | * state suitable for switching to a different video. |
577 | */ | 661 | */ |
578 | videojs.HlsHandler.prototype.resetSrc_ = function() { | 662 | resetSrc_() { |
579 | this.cancelSegmentXhr(); | 663 | this.cancelSegmentXhr(); |
580 | this.cancelKeyXhr(); | 664 | this.cancelKeyXhr(); |
581 | 665 | ||
582 | if (this.sourceBuffer && this.mediaSource.readyState === 'open') { | 666 | if (this.sourceBuffer && this.mediaSource.readyState === 'open') { |
583 | this.sourceBuffer.abort(); | 667 | this.sourceBuffer.abort(); |
668 | } | ||
584 | } | 669 | } |
585 | }; | ||
586 | 670 | ||
587 | videojs.HlsHandler.prototype.cancelKeyXhr = function() { | 671 | cancelKeyXhr() { |
588 | if (this.keyXhr_) { | 672 | if (this.keyXhr_) { |
589 | this.keyXhr_.onreadystatechange = null; | 673 | this.keyXhr_.onreadystatechange = null; |
590 | this.keyXhr_.abort(); | 674 | this.keyXhr_.abort(); |
591 | this.keyXhr_ = null; | 675 | this.keyXhr_ = null; |
676 | } | ||
592 | } | 677 | } |
593 | }; | ||
594 | 678 | ||
595 | videojs.HlsHandler.prototype.cancelSegmentXhr = function() { | 679 | cancelSegmentXhr() { |
596 | if (this.segmentXhr_) { | 680 | if (this.segmentXhr_) { |
597 | // Prevent error handler from running. | 681 | // Prevent error handler from running. |
598 | this.segmentXhr_.onreadystatechange = null; | 682 | this.segmentXhr_.onreadystatechange = null; |
599 | this.segmentXhr_.abort(); | 683 | this.segmentXhr_.abort(); |
600 | this.segmentXhr_ = null; | 684 | this.segmentXhr_ = null; |
685 | } | ||
686 | |||
687 | // clear out the segment being processed | ||
688 | this.pendingSegment_ = null; | ||
601 | } | 689 | } |
602 | 690 | ||
603 | // clear out the segment being processed | 691 | /** |
604 | this.pendingSegment_ = null; | 692 | * Abort all outstanding work and cleanup. |
605 | }; | 693 | */ |
694 | dispose() { | ||
695 | this.stopCheckingBuffer_(); | ||
606 | 696 | ||
607 | /** | 697 | if (this.playlists) { |
608 | * Abort all outstanding work and cleanup. | 698 | this.playlists.dispose(); |
609 | */ | 699 | } |
610 | videojs.HlsHandler.prototype.dispose = function() { | ||
611 | this.stopCheckingBuffer_(); | ||
612 | 700 | ||
613 | if (this.playlists) { | 701 | this.resetSrc_(); |
614 | this.playlists.dispose(); | 702 | super.dispose(); |
615 | } | 703 | } |
616 | 704 | ||
617 | this.resetSrc_(); | 705 | /** |
618 | Component.prototype.dispose.call(this); | 706 | * Chooses the appropriate media playlist based on the current |
619 | }; | 707 | * bandwidth estimate and the player size. |
620 | 708 | * @return the highest bitrate playlist less than the currently detected | |
621 | /** | 709 | * bandwidth, accounting for some amount of bandwidth variance |
622 | * Chooses the appropriate media playlist based on the current | 710 | */ |
623 | * bandwidth estimate and the player size. | 711 | selectPlaylist() { |
624 | * @return the highest bitrate playlist less than the currently detected | 712 | let effectiveBitrate; |
625 | * bandwidth, accounting for some amount of bandwidth variance | 713 | let sortedPlaylists = this.playlists.master.playlists.slice(); |
626 | */ | 714 | let bandwidthPlaylists = []; |
627 | videojs.HlsHandler.prototype.selectPlaylist = function () { | 715 | let now = +new Date(); |
628 | var | 716 | let i; |
629 | effectiveBitrate, | 717 | let variant; |
630 | sortedPlaylists = this.playlists.master.playlists.slice(), | 718 | let bandwidthBestVariant; |
631 | bandwidthPlaylists = [], | 719 | let resolutionPlusOne; |
632 | now = +new Date(), | 720 | let resolutionPlusOneAttribute; |
633 | i, | 721 | let resolutionBestVariant; |
634 | variant, | 722 | let width; |
635 | bandwidthBestVariant, | 723 | let height; |
636 | resolutionPlusOne, | 724 | |
637 | resolutionBestVariant, | 725 | sortedPlaylists.sort(Hls.comparePlaylistBandwidth); |
638 | width, | 726 | |
639 | height; | 727 | // filter out any playlists that have been excluded due to |
640 | 728 | // incompatible configurations or playback errors | |
641 | sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth); | 729 | sortedPlaylists = sortedPlaylists.filter((localVariant) => { |
642 | 730 | if (typeof localVariant.excludeUntil !== 'undefined') { | |
643 | // filter out any playlists that have been excluded due to | 731 | return now >= localVariant.excludeUntil; |
644 | // incompatible configurations or playback errors | 732 | } |
645 | sortedPlaylists = sortedPlaylists.filter(function(variant) { | 733 | return true; |
646 | if (variant.excludeUntil !== undefined) { | 734 | }); |
647 | return now >= variant.excludeUntil; | ||
648 | } | ||
649 | return true; | ||
650 | }); | ||
651 | 735 | ||
652 | // filter out any variant that has greater effective bitrate | 736 | // filter out any variant that has greater effective bitrate |
653 | // than the current estimated bandwidth | 737 | // than the current estimated bandwidth |
654 | i = sortedPlaylists.length; | 738 | i = sortedPlaylists.length; |
655 | while (i--) { | 739 | while (i--) { |
656 | variant = sortedPlaylists[i]; | 740 | variant = sortedPlaylists[i]; |
657 | 741 | ||
658 | // ignore playlists without bandwidth information | 742 | // ignore playlists without bandwidth information |
659 | if (!variant.attributes || !variant.attributes.BANDWIDTH) { | 743 | if (!variant.attributes || !variant.attributes.BANDWIDTH) { |
660 | continue; | 744 | continue; |
661 | } | 745 | } |
662 | 746 | ||
663 | effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance; | 747 | effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance; |
664 | 748 | ||
665 | if (effectiveBitrate < this.bandwidth) { | 749 | if (effectiveBitrate < this.bandwidth) { |
666 | bandwidthPlaylists.push(variant); | 750 | bandwidthPlaylists.push(variant); |
667 | 751 | ||
668 | // since the playlists are sorted in ascending order by | 752 | // since the playlists are sorted in ascending order by |
669 | // bandwidth, the first viable variant is the best | 753 | // bandwidth, the first viable variant is the best |
670 | if (!bandwidthBestVariant) { | 754 | if (!bandwidthBestVariant) { |
671 | bandwidthBestVariant = variant; | 755 | bandwidthBestVariant = variant; |
756 | } | ||
672 | } | 757 | } |
673 | } | 758 | } |
674 | } | ||
675 | |||
676 | i = bandwidthPlaylists.length; | ||
677 | |||
678 | // sort variants by resolution | ||
679 | bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution); | ||
680 | |||
681 | // forget our old variant from above, or we might choose that in high-bandwidth scenarios | ||
682 | // (this could be the lowest bitrate rendition as we go through all of them above) | ||
683 | variant = null; | ||
684 | |||
685 | width = parseInt(getComputedStyle(this.tech_.el()).width, 10); | ||
686 | height = parseInt(getComputedStyle(this.tech_.el()).height, 10); | ||
687 | |||
688 | // iterate through the bandwidth-filtered playlists and find | ||
689 | // best rendition by player dimension | ||
690 | while (i--) { | ||
691 | variant = bandwidthPlaylists[i]; | ||
692 | |||
693 | // ignore playlists without resolution information | ||
694 | if (!variant.attributes || | ||
695 | !variant.attributes.RESOLUTION || | ||
696 | !variant.attributes.RESOLUTION.width || | ||
697 | !variant.attributes.RESOLUTION.height) { | ||
698 | continue; | ||
699 | } | ||
700 | |||
701 | // since the playlists are sorted, the first variant that has | ||
702 | // dimensions less than or equal to the player size is the best | ||
703 | |||
704 | if (variant.attributes.RESOLUTION.width === width && | ||
705 | variant.attributes.RESOLUTION.height === height) { | ||
706 | // if we have the exact resolution as the player use it | ||
707 | resolutionPlusOne = null; | ||
708 | resolutionBestVariant = variant; | ||
709 | break; | ||
710 | } else if (variant.attributes.RESOLUTION.width < width && | ||
711 | variant.attributes.RESOLUTION.height < height) { | ||
712 | // if both dimensions are less than the player use the | ||
713 | // previous (next-largest) variant | ||
714 | break; | ||
715 | } else if (!resolutionPlusOne || | ||
716 | (variant.attributes.RESOLUTION.width < resolutionPlusOne.attributes.RESOLUTION.width && | ||
717 | variant.attributes.RESOLUTION.height < resolutionPlusOne.attributes.RESOLUTION.height)) { | ||
718 | // If we still haven't found a good match keep a | ||
719 | // reference to the previous variant for the next loop | ||
720 | // iteration | ||
721 | |||
722 | // By only saving variants if they are smaller than the | ||
723 | // previously saved variant, we ensure that we also pick | ||
724 | // the highest bandwidth variant that is just-larger-than | ||
725 | // the video player | ||
726 | resolutionPlusOne = variant; | ||
727 | } | ||
728 | } | ||
729 | |||
730 | // fallback chain of variants | ||
731 | return resolutionPlusOne || resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0]; | ||
732 | }; | ||
733 | 759 | ||
734 | /** | 760 | i = bandwidthPlaylists.length; |
735 | * Periodically request new segments and append video data. | ||
736 | */ | ||
737 | videojs.HlsHandler.prototype.checkBuffer_ = function() { | ||
738 | // calling this method directly resets any outstanding buffer checks | ||
739 | if (this.checkBufferTimeout_) { | ||
740 | window.clearTimeout(this.checkBufferTimeout_); | ||
741 | this.checkBufferTimeout_ = null; | ||
742 | } | ||
743 | 761 | ||
744 | this.fillBuffer(); | 762 | // sort variants by resolution |
745 | this.drainBuffer(); | 763 | bandwidthPlaylists.sort(Hls.comparePlaylistResolution); |
746 | 764 | ||
747 | // wait awhile and try again | 765 | // forget our old variant from above, |
748 | this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this), | 766 | // or we might choose that in high-bandwidth scenarios |
749 | bufferCheckInterval); | 767 | // (this could be the lowest bitrate rendition as we go through all of them above) |
750 | }; | 768 | variant = null; |
751 | 769 | ||
752 | /** | 770 | width = parseInt(getComputedStyle(this.tech_.el()).width, 10); |
753 | * Setup a periodic task to request new segments if necessary and | 771 | height = parseInt(getComputedStyle(this.tech_.el()).height, 10); |
754 | * append bytes into the SourceBuffer. | ||
755 | */ | ||
756 | videojs.HlsHandler.prototype.startCheckingBuffer_ = function() { | ||
757 | this.checkBuffer_(); | ||
758 | }; | ||
759 | 772 | ||
760 | /** | 773 | // iterate through the bandwidth-filtered playlists and find |
761 | * Stop the periodic task requesting new segments and feeding the | 774 | // best rendition by player dimension |
762 | * SourceBuffer. | 775 | while (i--) { |
763 | */ | 776 | variant = bandwidthPlaylists[i]; |
764 | videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() { | ||
765 | if (this.checkBufferTimeout_) { | ||
766 | window.clearTimeout(this.checkBufferTimeout_); | ||
767 | this.checkBufferTimeout_ = null; | ||
768 | } | ||
769 | }; | ||
770 | 777 | ||
771 | var filterBufferedRanges = function(predicate) { | 778 | // ignore playlists without resolution information |
772 | return function(time) { | 779 | if (!variant.attributes || |
773 | var | 780 | !variant.attributes.RESOLUTION || |
774 | i, | 781 | !variant.attributes.RESOLUTION.width || |
775 | ranges = [], | 782 | !variant.attributes.RESOLUTION.height) { |
776 | tech = this.tech_, | 783 | continue; |
777 | // !!The order of the next two assignments is important!! | 784 | } |
778 | // `currentTime` must be equal-to or greater-than the start of the | ||
779 | // buffered range. Flash executes out-of-process so, every value can | ||
780 | // change behind the scenes from line-to-line. By reading `currentTime` | ||
781 | // after `buffered`, we ensure that it is always a current or later | ||
782 | // value during playback. | ||
783 | buffered = tech.buffered(); | ||
784 | |||
785 | |||
786 | if (time === undefined) { | ||
787 | time = tech.currentTime(); | ||
788 | } | ||
789 | 785 | ||
790 | if (buffered && buffered.length) { | 786 | // since the playlists are sorted, the first variant that has |
791 | // Search for a range containing the play-head | 787 | // dimensions less than or equal to the player size is the best |
792 | for (i = 0; i < buffered.length; i++) { | 788 | |
793 | if (predicate(buffered.start(i), buffered.end(i), time)) { | 789 | let variantResolution = variant.attributes.RESOLUTION; |
794 | ranges.push([buffered.start(i), buffered.end(i)]); | 790 | |
795 | } | 791 | if (variantResolution.width === width && |
792 | variantResolution.height === height) { | ||
793 | // if we have the exact resolution as the player use it | ||
794 | resolutionPlusOne = null; | ||
795 | resolutionBestVariant = variant; | ||
796 | break; | ||
797 | } else if (variantResolution.width < width && | ||
798 | variantResolution.height < height) { | ||
799 | // if both dimensions are less than the player use the | ||
800 | // previous (next-largest) variant | ||
801 | break; | ||
802 | } else if (!resolutionPlusOne || | ||
803 | (variantResolution.width < resolutionPlusOneAttribute.width && | ||
804 | variantResolution.height < resolutionPlusOneAttribute.height)) { | ||
805 | // If we still haven't found a good match keep a | ||
806 | // reference to the previous variant for the next loop | ||
807 | // iteration | ||
808 | |||
809 | // By only saving variants if they are smaller than the | ||
810 | // previously saved variant, we ensure that we also pick | ||
811 | // the highest bandwidth variant that is just-larger-than | ||
812 | // the video player | ||
813 | resolutionPlusOne = variant; | ||
814 | resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION; | ||
796 | } | 815 | } |
797 | } | 816 | } |
798 | 817 | ||
799 | return videojs.createTimeRanges(ranges); | 818 | // fallback chain of variants |
800 | }; | 819 | return resolutionPlusOne || |
801 | }; | 820 | resolutionBestVariant || |
821 | bandwidthBestVariant || | ||
822 | sortedPlaylists[0]; | ||
823 | } | ||
802 | 824 | ||
803 | /** | 825 | /** |
804 | * Attempts to find the buffered TimeRange that contains the specified | 826 | * Periodically request new segments and append video data. |
805 | * time, or where playback is currently happening if no specific time | 827 | */ |
806 | * is specified. | 828 | checkBuffer_() { |
807 | * @param time (optional) {number} the time to filter on. Defaults to | 829 | // calling this method directly resets any outstanding buffer checks |
808 | * currentTime. | 830 | if (this.checkBufferTimeout_) { |
809 | * @return a new TimeRanges object. | 831 | window.clearTimeout(this.checkBufferTimeout_); |
810 | */ | 832 | this.checkBufferTimeout_ = null; |
811 | videojs.HlsHandler.prototype.findBufferedRange_ = filterBufferedRanges(function(start, end, time) { | 833 | } |
812 | return start - TIME_FUDGE_FACTOR <= time && | ||
813 | end + TIME_FUDGE_FACTOR >= time; | ||
814 | }); | ||
815 | 834 | ||
816 | /** | 835 | this.fillBuffer(); |
817 | * Returns the TimeRanges that begin at or later than the specified | 836 | this.drainBuffer(); |
818 | * time. | ||
819 | * @param time (optional) {number} the time to filter on. Defaults to | ||
820 | * currentTime. | ||
821 | * @return a new TimeRanges object. | ||
822 | */ | ||
823 | videojs.HlsHandler.prototype.findNextBufferedRange_ = filterBufferedRanges(function(start, end, time) { | ||
824 | return start - TIME_FUDGE_FACTOR >= time; | ||
825 | }); | ||
826 | 837 | ||
827 | /** | 838 | // wait awhile and try again |
828 | * Determines whether there is enough video data currently in the buffer | 839 | this.checkBufferTimeout_ = window.setTimeout((this.checkBuffer_).bind(this), |
829 | * and downloads a new segment if the buffered time is less than the goal. | 840 | bufferCheckInterval); |
830 | * @param seekToTime (optional) {number} the offset into the downloaded segment | ||
831 | * to seek to, in seconds | ||
832 | */ | ||
833 | videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { | ||
834 | var | ||
835 | tech = this.tech_, | ||
836 | currentTime = tech.currentTime(), | ||
837 | hasBufferedContent = (this.tech_.buffered().length !== 0), | ||
838 | currentBuffered = this.findBufferedRange_(), | ||
839 | outsideBufferedRanges = !(currentBuffered && currentBuffered.length), | ||
840 | currentBufferedEnd = 0, | ||
841 | bufferedTime = 0, | ||
842 | segment, | ||
843 | segmentInfo, | ||
844 | segmentTimestampOffset; | ||
845 | |||
846 | // if preload is set to "none", do not download segments until playback is requested | ||
847 | if (this.loadingState_ !== 'segments') { | ||
848 | return; | ||
849 | } | 841 | } |
850 | 842 | ||
851 | // if a video has not been specified, do nothing | 843 | /** |
852 | if (!tech.currentSrc() || !this.playlists) { | 844 | * Setup a periodic task to request new segments if necessary and |
853 | return; | 845 | * append bytes into the SourceBuffer. |
846 | */ | ||
847 | startCheckingBuffer_() { | ||
848 | this.checkBuffer_(); | ||
854 | } | 849 | } |
855 | 850 | ||
856 | // if there is a request already in flight, do nothing | 851 | /** |
857 | if (this.segmentXhr_) { | 852 | * Stop the periodic task requesting new segments and feeding the |
858 | return; | 853 | * SourceBuffer. |
854 | */ | ||
855 | stopCheckingBuffer_() { | ||
856 | if (this.checkBufferTimeout_) { | ||
857 | window.clearTimeout(this.checkBufferTimeout_); | ||
858 | this.checkBufferTimeout_ = null; | ||
859 | } | ||
859 | } | 860 | } |
860 | 861 | ||
861 | // wait until the buffer is up to date | 862 | /** |
862 | if (this.pendingSegment_) { | 863 | * Determines whether there is enough video data currently in the buffer |
863 | return; | 864 | * and downloads a new segment if the buffered time is less than the goal. |
864 | } | 865 | * @param seekToTime (optional) {number} the offset into the downloaded segment |
866 | * to seek to, in seconds | ||
867 | */ | ||
868 | fillBuffer(mediaIndex) { | ||
869 | let tech = this.tech_; | ||
870 | let currentTime = tech.currentTime(); | ||
871 | let hasBufferedContent = (this.tech_.buffered().length !== 0); | ||
872 | let currentBuffered = this.findBufferedRange_(); | ||
873 | let outsideBufferedRanges = !(currentBuffered && currentBuffered.length); | ||
874 | let currentBufferedEnd = 0; | ||
875 | let bufferedTime = 0; | ||
876 | let segment; | ||
877 | let segmentInfo; | ||
878 | let segmentTimestampOffset; | ||
879 | |||
880 | // if preload is set to "none", do not download segments until playback is requested | ||
881 | if (this.loadingState_ !== 'segments') { | ||
882 | return; | ||
883 | } | ||
884 | |||
885 | // if a video has not been specified, do nothing | ||
886 | if (!tech.currentSrc() || !this.playlists) { | ||
887 | return; | ||
888 | } | ||
889 | |||
890 | // if there is a request already in flight, do nothing | ||
891 | if (this.segmentXhr_) { | ||
892 | return; | ||
893 | } | ||
865 | 894 | ||
866 | // if no segments are available, do nothing | 895 | // wait until the buffer is up to date |
867 | if (this.playlists.state === "HAVE_NOTHING" || | 896 | if (this.pendingSegment_) { |
868 | !this.playlists.media() || | 897 | return; |
869 | !this.playlists.media().segments) { | 898 | } |
870 | return; | ||
871 | } | ||
872 | 899 | ||
873 | // if a playlist switch is in progress, wait for it to finish | 900 | // if no segments are available, do nothing |
874 | if (this.playlists.state === 'SWITCHING_MEDIA') { | 901 | if (this.playlists.state === 'HAVE_NOTHING' || |
875 | return; | 902 | !this.playlists.media() || |
876 | } | 903 | !this.playlists.media().segments) { |
904 | return; | ||
905 | } | ||
877 | 906 | ||
878 | if (mediaIndex === undefined) { | 907 | // if a playlist switch is in progress, wait for it to finish |
879 | if (currentBuffered && currentBuffered.length) { | 908 | if (this.playlists.state === 'SWITCHING_MEDIA') { |
880 | currentBufferedEnd = currentBuffered.end(0); | 909 | return; |
881 | mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd); | 910 | } |
882 | bufferedTime = Math.max(0, currentBufferedEnd - currentTime); | ||
883 | 911 | ||
884 | // if there is plenty of content in the buffer and we're not | 912 | if (typeof mediaIndex === 'undefined') { |
885 | // seeking, relax for awhile | 913 | if (currentBuffered && currentBuffered.length) { |
886 | if (bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { | 914 | currentBufferedEnd = currentBuffered.end(0); |
887 | return; | 915 | mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd); |
916 | bufferedTime = Math.max(0, currentBufferedEnd - currentTime); | ||
917 | |||
918 | // if there is plenty of content in the buffer and we're not | ||
919 | // seeking, relax for awhile | ||
920 | if (bufferedTime >= Hls.GOAL_BUFFER_LENGTH) { | ||
921 | return; | ||
922 | } | ||
923 | } else { | ||
924 | mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime()); | ||
888 | } | 925 | } |
889 | } else { | ||
890 | mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime()); | ||
891 | } | 926 | } |
892 | } | 927 | segment = this.playlists.media().segments[mediaIndex]; |
893 | segment = this.playlists.media().segments[mediaIndex]; | ||
894 | 928 | ||
895 | // if the video has finished downloading | 929 | // if the video has finished downloading |
896 | if (!segment) { | 930 | if (!segment) { |
897 | return; | 931 | return; |
898 | } | 932 | } |
899 | 933 | ||
900 | // we have entered a state where we are fetching the same segment, | 934 | // we have entered a state where we are fetching the same segment, |
901 | // try to walk forward | 935 | // try to walk forward |
902 | if (this.lastSegmentLoaded_ && | 936 | if (this.lastSegmentLoaded_ && |
903 | this.playlistUriToUrl(this.lastSegmentLoaded_.uri) === this.playlistUriToUrl(segment.uri) && | 937 | this.playlistUriToUrl(this.lastSegmentLoaded_.uri) === this.playlistUriToUrl(segment.uri) && |
904 | this.lastSegmentLoaded_.byterange === segment.byterange) { | 938 | this.lastSegmentLoaded_.byterange === segment.byterange) { |
905 | return this.fillBuffer(mediaIndex + 1); | 939 | return this.fillBuffer(mediaIndex + 1); |
906 | } | 940 | } |
907 | 941 | ||
908 | // package up all the work to append the segment | 942 | // package up all the work to append the segment |
909 | segmentInfo = { | 943 | segmentInfo = { |
910 | // resolve the segment URL relative to the playlist | 944 | // resolve the segment URL relative to the playlist |
911 | uri: this.playlistUriToUrl(segment.uri), | 945 | uri: this.playlistUriToUrl(segment.uri), |
912 | // the segment's mediaIndex & mediaSequence at the time it was requested | 946 | // the segment's mediaIndex & mediaSequence at the time it was requested |
913 | mediaIndex: mediaIndex, | 947 | mediaIndex, |
914 | mediaSequence: this.playlists.media().mediaSequence, | 948 | mediaSequence: this.playlists.media().mediaSequence, |
915 | // the segment's playlist | 949 | // the segment's playlist |
916 | playlist: this.playlists.media(), | 950 | playlist: this.playlists.media(), |
917 | // The state of the buffer when this segment was requested | 951 | // The state of the buffer when this segment was requested |
918 | currentBufferedEnd: currentBufferedEnd, | 952 | currentBufferedEnd, |
919 | // unencrypted bytes of the segment | 953 | // unencrypted bytes of the segment |
920 | bytes: null, | 954 | bytes: null, |
921 | // when a key is defined for this segment, the encrypted bytes | 955 | // when a key is defined for this segment, the encrypted bytes |
922 | encryptedBytes: null, | 956 | encryptedBytes: null, |
923 | // optionally, the decrypter that is unencrypting the segment | 957 | // optionally, the decrypter that is unencrypting the segment |
924 | decrypter: null, | 958 | decrypter: null, |
925 | // the state of the buffer before a segment is appended will be | 959 | // the state of the buffer before a segment is appended will be |
926 | // stored here so that the actual segment duration can be | 960 | // stored here so that the actual segment duration can be |
927 | // determined after it has been appended | 961 | // determined after it has been appended |
928 | buffered: null, | 962 | buffered: null, |
929 | // The target timestampOffset for this segment when we append it | 963 | // The target timestampOffset for this segment when we append it |
930 | // to the source buffer | 964 | // to the source buffer |
931 | timestampOffset: null | 965 | timestampOffset: null |
932 | }; | 966 | }; |
933 | 967 | ||
934 | if (mediaIndex > 0) { | 968 | if (mediaIndex > 0) { |
935 | segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, | 969 | segmentTimestampOffset = Hls.Playlist.duration(segmentInfo.playlist, |
936 | segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_; | 970 | segmentInfo.playlist.mediaSequence + mediaIndex) + this.playlists.expired_; |
937 | } | 971 | } |
938 | 972 | ||
939 | if (this.tech_.seeking() && outsideBufferedRanges) { | 973 | if (this.tech_.seeking() && outsideBufferedRanges) { |
940 | // If there are discontinuities in the playlist, we can't be sure of anything | 974 | // If there are discontinuities in the playlist, we can't be sure of anything |
941 | // related to time so we reset the timestamp offset and start appending data | 975 | // related to time so we reset the timestamp offset and start appending data |
942 | // anew on every seek | 976 | // anew on every seek |
943 | if (segmentInfo.playlist.discontinuityStarts.length) { | 977 | if (segmentInfo.playlist.discontinuityStarts.length) { |
978 | segmentInfo.timestampOffset = segmentTimestampOffset; | ||
979 | } | ||
980 | } else if (segment.discontinuity && currentBuffered.length) { | ||
981 | // If we aren't seeking and are crossing a discontinuity, we should set | ||
982 | // timestampOffset for new segments to be appended the end of the current | ||
983 | // buffered time-range | ||
984 | segmentInfo.timestampOffset = currentBuffered.end(0); | ||
985 | } else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) { | ||
986 | // If we are trying to play at a position that is not zero but we aren't | ||
987 | // currently seeking according to the video element | ||
944 | segmentInfo.timestampOffset = segmentTimestampOffset; | 988 | segmentInfo.timestampOffset = segmentTimestampOffset; |
945 | } | 989 | } |
946 | } else if (segment.discontinuity && currentBuffered.length) { | 990 | |
947 | // If we aren't seeking and are crossing a discontinuity, we should set | 991 | this.loadSegment(segmentInfo); |
948 | // timestampOffset for new segments to be appended the end of the current | ||
949 | // buffered time-range | ||
950 | segmentInfo.timestampOffset = currentBuffered.end(0); | ||
951 | } else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) { | ||
952 | // If we are trying to play at a position that is not zero but we aren't | ||
953 | // currently seeking according to the video element | ||
954 | segmentInfo.timestampOffset = segmentTimestampOffset; | ||
955 | } | 992 | } |
956 | 993 | ||
957 | this.loadSegment(segmentInfo); | 994 | playlistUriToUrl(segmentRelativeUrl) { |
958 | }; | 995 | let playListUrl; |
959 | 996 | ||
960 | videojs.HlsHandler.prototype.playlistUriToUrl = function(segmentRelativeUrl) { | 997 | // resolve the segment URL relative to the playlist |
961 | var playListUrl; | 998 | if (this.playlists.media().uri === this.source_.src) { |
962 | // resolve the segment URL relative to the playlist | 999 | playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl); |
963 | if (this.playlists.media().uri === this.source_.src) { | 1000 | } else { |
964 | playListUrl = resolveUrl(this.source_.src, segmentRelativeUrl); | 1001 | playListUrl = |
965 | } else { | 1002 | resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''), |
966 | playListUrl = resolveUrl(resolveUrl(this.source_.src, this.playlists.media().uri || ''), segmentRelativeUrl); | 1003 | segmentRelativeUrl); |
1004 | } | ||
1005 | return playListUrl; | ||
967 | } | 1006 | } |
968 | return playListUrl; | ||
969 | }; | ||
970 | 1007 | ||
971 | /* Turns segment byterange into a string suitable for use in | 1008 | /* |
972 | * HTTP Range requests | 1009 | * Turns segment byterange into a string suitable for use in |
973 | */ | 1010 | * HTTP Range requests |
974 | videojs.HlsHandler.prototype.byterangeStr_ = function(byterange) { | 1011 | */ |
975 | var byterangeStart, byterangeEnd; | 1012 | byterangeStr_(byterange) { |
1013 | let byterangeStart; | ||
1014 | let byterangeEnd; | ||
976 | 1015 | ||
977 | // `byterangeEnd` is one less than `offset + length` because the HTTP range | 1016 | // `byterangeEnd` is one less than `offset + length` because the HTTP range |
978 | // header uses inclusive ranges | 1017 | // header uses inclusive ranges |
979 | byterangeEnd = byterange.offset + byterange.length - 1; | 1018 | byterangeEnd = byterange.offset + byterange.length - 1; |
980 | byterangeStart = byterange.offset; | 1019 | byterangeStart = byterange.offset; |
981 | return "bytes=" + byterangeStart + "-" + byterangeEnd; | 1020 | return 'bytes=' + byterangeStart + '-' + byterangeEnd; |
982 | }; | ||
983 | |||
984 | /* Defines headers for use in the xhr request for a particular segment. | ||
985 | */ | ||
986 | videojs.HlsHandler.prototype.segmentXhrHeaders_ = function(segment) { | ||
987 | var headers = {}; | ||
988 | if ('byterange' in segment) { | ||
989 | headers['Range'] = this.byterangeStr_(segment.byterange); | ||
990 | } | 1021 | } |
991 | return headers; | ||
992 | }; | ||
993 | 1022 | ||
994 | /* | 1023 | /* |
995 | * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived. | 1024 | * Defines headers for use in the xhr request for a particular segment. |
996 | * Expects an object with: | 1025 | */ |
997 | * * `roundTripTime` - the round trip time for the request we're setting the time for | 1026 | segmentXhrHeaders_(segment) { |
998 | * * `bandwidth` - the bandwidth we want to set | 1027 | let headers = {}; |
999 | * * `bytesReceived` - amount of bytes downloaded | ||
1000 | * `bandwidth` is the only required property. | ||
1001 | */ | ||
1002 | videojs.HlsHandler.prototype.setBandwidth = function(xhr) { | ||
1003 | // calculate the download bandwidth | ||
1004 | this.segmentXhrTime = xhr.roundTripTime; | ||
1005 | this.bandwidth = xhr.bandwidth; | ||
1006 | this.bytesReceived += xhr.bytesReceived || 0; | ||
1007 | 1028 | ||
1008 | this.tech_.trigger('bandwidthupdate'); | 1029 | if ('byterange' in segment) { |
1009 | }; | 1030 | headers.Range = this.byterangeStr_(segment.byterange); |
1031 | } | ||
1032 | return headers; | ||
1033 | } | ||
1010 | 1034 | ||
1011 | /* | 1035 | /* |
1012 | * Blacklists a playlist when an error occurs for a set amount of time | 1036 | * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived. |
1013 | * making it unavailable for selection by the rendition selection algorithm | 1037 | * Expects an object with: |
1014 | * and then forces a new playlist (rendition) selection. | 1038 | * * `roundTripTime` - the round trip time for the request we're setting the time for |
1015 | */ | 1039 | * * `bandwidth` - the bandwidth we want to set |
1016 | videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) { | 1040 | * * `bytesReceived` - amount of bytes downloaded |
1017 | var currentPlaylist, nextPlaylist; | 1041 | * `bandwidth` is the only required property. |
1018 | 1042 | */ | |
1019 | // If the `error` was generated by the playlist loader, it will contain | 1043 | setBandwidth(localXhr) { |
1020 | // the playlist we were trying to load (but failed) and that should be | 1044 | // calculate the download bandwidth |
1021 | // blacklisted instead of the currently selected playlist which is likely | 1045 | this.segmentXhrTime = localXhr.roundTripTime; |
1022 | // out-of-date in this scenario | 1046 | this.bandwidth = localXhr.bandwidth; |
1023 | currentPlaylist = error.playlist || this.playlists.media(); | 1047 | this.bytesReceived += localXhr.bytesReceived || 0; |
1024 | 1048 | ||
1025 | // If there is no current playlist, then an error occurred while we were | 1049 | this.tech_.trigger('bandwidthupdate'); |
1026 | // trying to load the master OR while we were disposing of the tech | ||
1027 | if (!currentPlaylist) { | ||
1028 | this.error = error; | ||
1029 | return this.mediaSource.endOfStream('network'); | ||
1030 | } | 1050 | } |
1031 | 1051 | ||
1032 | // Blacklist this playlist | 1052 | /* |
1033 | currentPlaylist.excludeUntil = Date.now() + blacklistDuration; | 1053 | * Blacklists a playlist when an error occurs for a set amount of time |
1054 | * making it unavailable for selection by the rendition selection algorithm | ||
1055 | * and then forces a new playlist (rendition) selection. | ||
1056 | */ | ||
1057 | blacklistCurrentPlaylist_(error) { | ||
1058 | let currentPlaylist; | ||
1059 | let nextPlaylist; | ||
1060 | |||
1061 | // If the `error` was generated by the playlist loader, it will contain | ||
1062 | // the playlist we were trying to load (but failed) and that should be | ||
1063 | // blacklisted instead of the currently selected playlist which is likely | ||
1064 | // out-of-date in this scenario | ||
1065 | currentPlaylist = error.playlist || this.playlists.media(); | ||
1066 | |||
1067 | // If there is no current playlist, then an error occurred while we were | ||
1068 | // trying to load the master OR while we were disposing of the tech | ||
1069 | if (!currentPlaylist) { | ||
1070 | this.error = error; | ||
1071 | return this.mediaSource.endOfStream('network'); | ||
1072 | } | ||
1073 | |||
1074 | // Blacklist this playlist | ||
1075 | currentPlaylist.excludeUntil = Date.now() + blacklistDuration; | ||
1034 | 1076 | ||
1035 | // Select a new playlist | 1077 | // Select a new playlist |
1036 | nextPlaylist = this.selectPlaylist(); | 1078 | nextPlaylist = this.selectPlaylist(); |
1037 | 1079 | ||
1038 | if (nextPlaylist) { | 1080 | if (nextPlaylist) { |
1039 | videojs.log.warn('Problem encountered with the current HLS playlist. Switching to another playlist.'); | 1081 | videojs.log.warn('Problem encountered with the current ' + |
1082 | 'HLS playlist. Switching to another playlist.'); | ||
1040 | 1083 | ||
1041 | return this.playlists.media(nextPlaylist); | 1084 | return this.playlists.media(nextPlaylist); |
1042 | } else { | 1085 | } |
1043 | videojs.log.warn('Problem encountered with the current HLS playlist. No suitable alternatives found.'); | 1086 | videojs.log.warn('Problem encountered with the current ' + |
1087 | 'HLS playlist. No suitable alternatives found.'); | ||
1044 | // We have no more playlists we can select so we must fail | 1088 | // We have no more playlists we can select so we must fail |
1045 | this.error = error; | 1089 | this.error = error; |
1046 | return this.mediaSource.endOfStream('network'); | 1090 | return this.mediaSource.endOfStream('network'); |
1047 | } | 1091 | } |
1048 | }; | ||
1049 | 1092 | ||
1050 | videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) { | 1093 | loadSegment(segmentInfo) { |
1051 | var | 1094 | let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; |
1052 | self = this, | 1095 | let removeToTime = 0; |
1053 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex], | 1096 | let seekable = this.seekable(); |
1054 | removeToTime = 0, | 1097 | |
1055 | seekable = this.seekable(); | 1098 | // Chrome has a hard limit of 150mb of |
1056 | 1099 | // buffer and a very conservative "garbage collector" | |
1057 | // Chrome has a hard limit of 150mb of buffer and a very conservative "garbage collector" | 1100 | // We manually clear out the old buffer to ensure |
1058 | // We manually clear out the old buffer to ensure we don't trigger the QuotaExceeded error | 1101 | // we don't trigger the QuotaExceeded error |
1059 | // on the source buffer during subsequent appends | 1102 | // on the source buffer during subsequent appends |
1060 | if (this.sourceBuffer && !this.sourceBuffer.updating) { | 1103 | if (this.sourceBuffer && !this.sourceBuffer.updating) { |
1061 | // If we have a seekable range use that as the limit for what can be removed safely | 1104 | // If we have a seekable range use that as the limit for what can be removed safely |
1062 | // otherwise remove anything older than 1 minute before the current play head | 1105 | // otherwise remove anything older than 1 minute before the current play head |
1063 | if (seekable.length && seekable.start(0) > 0) { | 1106 | if (seekable.length && seekable.start(0) > 0) { |
1064 | removeToTime = seekable.start(0); | 1107 | removeToTime = seekable.start(0); |
1065 | } else { | 1108 | } else { |
1066 | removeToTime = this.tech_.currentTime() - 60; | 1109 | removeToTime = this.tech_.currentTime() - 60; |
1067 | } | 1110 | } |
1068 | 1111 | ||
1069 | if (removeToTime > 0) { | 1112 | if (removeToTime > 0) { |
1070 | this.sourceBuffer.remove(0, removeToTime); | 1113 | this.sourceBuffer.remove(0, removeToTime); |
1114 | } | ||
1071 | } | 1115 | } |
1072 | } | ||
1073 | |||
1074 | // if the segment is encrypted, request the key | ||
1075 | if (segment.key) { | ||
1076 | this.fetchKey_(segment); | ||
1077 | } | ||
1078 | 1116 | ||
1079 | // request the next segment | 1117 | // if the segment is encrypted, request the key |
1080 | this.segmentXhr_ = videojs.Hls.xhr({ | 1118 | if (segment.key) { |
1081 | uri: segmentInfo.uri, | 1119 | this.fetchKey_(segment); |
1082 | responseType: 'arraybuffer', | ||
1083 | withCredentials: this.source_.withCredentials, | ||
1084 | // Set xhr timeout to 150% of the segment duration to allow us | ||
1085 | // some time to switch renditions in the event of a catastrophic | ||
1086 | // decrease in network performance or a server issue. | ||
1087 | timeout: (segment.duration * 1.5) * 1000, | ||
1088 | headers: this.segmentXhrHeaders_(segment) | ||
1089 | }, function(error, request) { | ||
1090 | // This is a timeout of a previously aborted segment request | ||
1091 | // so simply ignore it | ||
1092 | if (!self.segmentXhr_ || request !== self.segmentXhr_) { | ||
1093 | return; | ||
1094 | } | 1120 | } |
1095 | 1121 | ||
1096 | // the segment request is no longer outstanding | 1122 | // request the next segment |
1097 | self.segmentXhr_ = null; | 1123 | this.segmentXhr_ = Hls.xhr({ |
1098 | 1124 | uri: segmentInfo.uri, | |
1099 | // if a segment request times out, we may have better luck with another playlist | 1125 | responseType: 'arraybuffer', |
1100 | if (request.timedout) { | 1126 | withCredentials: this.source_.withCredentials, |
1101 | self.bandwidth = 1; | 1127 | // Set xhr timeout to 150% of the segment duration to allow us |
1102 | return self.playlists.media(self.selectPlaylist()); | 1128 | // some time to switch renditions in the event of a catastrophic |
1103 | } | 1129 | // decrease in network performance or a server issue. |
1130 | timeout: (segment.duration * 1.5) * 1000, | ||
1131 | headers: this.segmentXhrHeaders_(segment) | ||
1132 | }, (error, request) => { | ||
1133 | // This is a timeout of a previously aborted segment request | ||
1134 | // so simply ignore it | ||
1135 | if (!this.segmentXhr_ || request !== this.segmentXhr_) { | ||
1136 | return; | ||
1137 | } | ||
1104 | 1138 | ||
1105 | // otherwise, trigger a network error | 1139 | // the segment request is no longer outstanding |
1106 | if (!request.aborted && error) { | 1140 | this.segmentXhr_ = null; |
1107 | return self.blacklistCurrentPlaylist_({ | ||
1108 | status: request.status, | ||
1109 | message: 'HLS segment request error at URL: ' + segmentInfo.uri, | ||
1110 | code: (request.status >= 500) ? 4 : 2 | ||
1111 | }); | ||
1112 | } | ||
1113 | 1141 | ||
1114 | // stop processing if the request was aborted | 1142 | // if a segment request times out, we may have better luck with another playlist |
1115 | if (!request.response) { | 1143 | if (request.timedout) { |
1116 | return; | 1144 | this.bandwidth = 1; |
1117 | } | 1145 | return this.playlists.media(this.selectPlaylist()); |
1146 | } | ||
1118 | 1147 | ||
1119 | self.lastSegmentLoaded_ = segment; | 1148 | // otherwise, trigger a network error |
1120 | self.setBandwidth(request); | 1149 | if (!request.aborted && error) { |
1150 | return this.blacklistCurrentPlaylist_({ | ||
1151 | status: request.status, | ||
1152 | message: 'HLS segment request error at URL: ' + segmentInfo.uri, | ||
1153 | code: (request.status >= 500) ? 4 : 2 | ||
1154 | }); | ||
1155 | } | ||
1121 | 1156 | ||
1122 | if (segment.key) { | 1157 | // stop processing if the request was aborted |
1123 | segmentInfo.encryptedBytes = new Uint8Array(request.response); | 1158 | if (!request.response) { |
1124 | } else { | 1159 | return; |
1125 | segmentInfo.bytes = new Uint8Array(request.response); | 1160 | } |
1126 | } | ||
1127 | 1161 | ||
1128 | self.pendingSegment_ = segmentInfo; | 1162 | this.lastSegmentLoaded_ = segment; |
1163 | this.setBandwidth(request); | ||
1129 | 1164 | ||
1130 | self.tech_.trigger('progress'); | 1165 | if (segment.key) { |
1131 | self.drainBuffer(); | 1166 | segmentInfo.encryptedBytes = new Uint8Array(request.response); |
1167 | } else { | ||
1168 | segmentInfo.bytes = new Uint8Array(request.response); | ||
1169 | } | ||
1132 | 1170 | ||
1133 | // figure out what stream the next segment should be downloaded from | 1171 | this.pendingSegment_ = segmentInfo; |
1134 | // with the updated bandwidth information | ||
1135 | self.playlists.media(self.selectPlaylist()); | ||
1136 | }); | ||
1137 | 1172 | ||
1138 | }; | 1173 | this.tech_.trigger('progress'); |
1174 | this.drainBuffer(); | ||
1139 | 1175 | ||
1140 | videojs.HlsHandler.prototype.drainBuffer = function() { | 1176 | // figure out what stream the next segment should be downloaded from |
1141 | var | 1177 | // with the updated bandwidth information |
1142 | segmentInfo, | 1178 | this.playlists.media(this.selectPlaylist()); |
1143 | mediaIndex, | 1179 | }); |
1144 | playlist, | ||
1145 | offset, | ||
1146 | bytes, | ||
1147 | segment, | ||
1148 | decrypter, | ||
1149 | segIv; | ||
1150 | |||
1151 | // if the buffer is empty or the source buffer hasn't been created | ||
1152 | // yet, do nothing | ||
1153 | if (!this.pendingSegment_ || !this.sourceBuffer) { | ||
1154 | return; | ||
1155 | } | ||
1156 | 1180 | ||
1157 | // the pending segment has already been appended and we're waiting | ||
1158 | // for updateend to fire | ||
1159 | if (this.pendingSegment_.buffered) { | ||
1160 | return; | ||
1161 | } | 1181 | } |
1162 | 1182 | ||
1163 | // we can't append more data if the source buffer is busy processing | 1183 | drainBuffer() { |
1164 | // what we've already sent | 1184 | let segmentInfo; |
1165 | if (this.sourceBuffer.updating) { | 1185 | let mediaIndex; |
1166 | return; | 1186 | let playlist; |
1167 | } | 1187 | let bytes; |
1188 | let segment; | ||
1189 | let decrypter; | ||
1190 | let segIv; | ||
1168 | 1191 | ||
1169 | segmentInfo = this.pendingSegment_; | 1192 | // if the buffer is empty or the source buffer hasn't been created |
1170 | mediaIndex = segmentInfo.mediaIndex; | 1193 | // yet, do nothing |
1171 | playlist = segmentInfo.playlist; | 1194 | if (!this.pendingSegment_ || !this.sourceBuffer) { |
1172 | offset = segmentInfo.offset; | 1195 | return; |
1173 | bytes = segmentInfo.bytes; | 1196 | } |
1174 | segment = playlist.segments[mediaIndex]; | ||
1175 | |||
1176 | if (segment.key && !bytes) { | ||
1177 | // this is an encrypted segment | ||
1178 | // if the key download failed, we want to skip this segment | ||
1179 | // but if the key hasn't downloaded yet, we want to try again later | ||
1180 | if (keyFailed(segment.key)) { | ||
1181 | return this.blacklistCurrentPlaylist_({ | ||
1182 | message: 'HLS segment key request error.', | ||
1183 | code: 4 | ||
1184 | }); | ||
1185 | } else if (!segment.key.bytes) { | ||
1186 | 1197 | ||
1187 | // waiting for the key bytes, try again later | 1198 | // the pending segment has already been appended and we're waiting |
1199 | // for updateend to fire | ||
1200 | if (this.pendingSegment_.buffered) { | ||
1188 | return; | 1201 | return; |
1189 | } else if (segmentInfo.decrypter) { | 1202 | } |
1190 | 1203 | ||
1191 | // decryption is in progress, try again later | 1204 | // we can't append more data if the source buffer is busy processing |
1205 | // what we've already sent | ||
1206 | if (this.sourceBuffer.updating) { | ||
1192 | return; | 1207 | return; |
1193 | } else { | 1208 | } |
1194 | 1209 | ||
1210 | segmentInfo = this.pendingSegment_; | ||
1211 | mediaIndex = segmentInfo.mediaIndex; | ||
1212 | playlist = segmentInfo.playlist; | ||
1213 | bytes = segmentInfo.bytes; | ||
1214 | segment = playlist.segments[mediaIndex]; | ||
1215 | |||
1216 | if (segment.key && !bytes) { | ||
1217 | // this is an encrypted segment | ||
1218 | // if the key download failed, we want to skip this segment | ||
1219 | // but if the key hasn't downloaded yet, we want to try again later | ||
1220 | if (keyFailed(segment.key)) { | ||
1221 | return this.blacklistCurrentPlaylist_({ | ||
1222 | message: 'HLS segment key request error.', | ||
1223 | code: 4 | ||
1224 | }); | ||
1225 | } else if (!segment.key.bytes) { | ||
1226 | // waiting for the key bytes, try again later | ||
1227 | return; | ||
1228 | } else if (segmentInfo.decrypter) { | ||
1229 | // decryption is in progress, try again later | ||
1230 | return; | ||
1231 | } | ||
1195 | // if the media sequence is greater than 2^32, the IV will be incorrect | 1232 | // if the media sequence is greater than 2^32, the IV will be incorrect |
1196 | // assuming 10s segments, that would be about 1300 years | 1233 | // assuming 10s segments, that would be about 1300 years |
1197 | segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); | 1234 | segIv = segment.key.iv || |
1235 | new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); | ||
1198 | 1236 | ||
1199 | // create a decrypter to incrementally decrypt the segment | 1237 | // create a decrypter to incrementally decrypt the segment |
1200 | decrypter = new videojs.Hls.Decrypter(segmentInfo.encryptedBytes, | 1238 | decrypter = new Hls.Decrypter(segmentInfo.encryptedBytes, |
1201 | segment.key.bytes, | 1239 | segment.key.bytes, |
1202 | segIv, | 1240 | segIv, |
1203 | function(err, bytes) { | 1241 | function(error, localBytes) { |
1204 | segmentInfo.bytes = bytes; | 1242 | if (error) { |
1205 | }); | 1243 | videojs.log.warn(error); |
1244 | } | ||
1245 | segmentInfo.bytes = localBytes; | ||
1246 | }); | ||
1206 | segmentInfo.decrypter = decrypter; | 1247 | segmentInfo.decrypter = decrypter; |
1207 | return; | 1248 | return; |
1208 | } | 1249 | } |
1209 | } | ||
1210 | 1250 | ||
1211 | this.pendingSegment_.buffered = this.tech_.buffered(); | 1251 | this.pendingSegment_.buffered = this.tech_.buffered(); |
1252 | |||
1253 | if (segmentInfo.timestampOffset !== null) { | ||
1254 | this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset; | ||
1255 | } | ||
1212 | 1256 | ||
1213 | if (segmentInfo.timestampOffset !== null) { | 1257 | // the segment is asynchronously added to the current buffered data |
1214 | this.sourceBuffer.timestampOffset = segmentInfo.timestampOffset; | 1258 | this.sourceBuffer.appendBuffer(bytes); |
1215 | } | 1259 | } |
1216 | 1260 | ||
1217 | // the segment is asynchronously added to the current buffered data | 1261 | updateEndHandler_() { |
1218 | this.sourceBuffer.appendBuffer(bytes); | 1262 | let segmentInfo = this.pendingSegment_; |
1219 | }; | 1263 | let segment; |
1264 | let segments; | ||
1265 | let playlist; | ||
1266 | let currentMediaIndex; | ||
1267 | let currentBuffered; | ||
1268 | let seekable; | ||
1269 | let timelineUpdate; | ||
1220 | 1270 | ||
1221 | videojs.HlsHandler.prototype.updateEndHandler_ = function () { | 1271 | this.pendingSegment_ = null; |
1222 | var | 1272 | |
1223 | segmentInfo = this.pendingSegment_, | 1273 | // stop here if the update errored or was aborted |
1224 | segment, | 1274 | if (!segmentInfo) { |
1225 | segments, | 1275 | return; |
1226 | playlist, | 1276 | } |
1227 | currentMediaIndex, | ||
1228 | currentBuffered, | ||
1229 | seekable, | ||
1230 | timelineUpdate; | ||
1231 | |||
1232 | this.pendingSegment_ = null; | ||
1233 | |||
1234 | // stop here if the update errored or was aborted | ||
1235 | if (!segmentInfo) { | ||
1236 | return; | ||
1237 | } | ||
1238 | 1277 | ||
1239 | playlist = this.playlists.media(); | 1278 | playlist = this.playlists.media(); |
1240 | segments = playlist.segments; | 1279 | segments = playlist.segments; |
1241 | currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence); | 1280 | currentMediaIndex = segmentInfo.mediaIndex + |
1242 | currentBuffered = this.findBufferedRange_(); | 1281 | (segmentInfo.mediaSequence - playlist.mediaSequence); |
1282 | currentBuffered = this.findBufferedRange_(); | ||
1243 | 1283 | ||
1244 | // if we switched renditions don't try to add segment timeline | 1284 | // if we switched renditions don't try to add segment timeline |
1245 | // information to the playlist | 1285 | // information to the playlist |
1246 | if (segmentInfo.playlist.uri !== this.playlists.media().uri) { | 1286 | if (segmentInfo.playlist.uri !== this.playlists.media().uri) { |
1247 | return this.fillBuffer(); | 1287 | return this.fillBuffer(); |
1248 | } | 1288 | } |
1249 | 1289 | ||
1250 | // annotate the segment with any start and end time information | 1290 | // annotate the segment with any start and end time information |
1251 | // added by the media processing | 1291 | // added by the media processing |
1252 | segment = playlist.segments[currentMediaIndex]; | 1292 | segment = playlist.segments[currentMediaIndex]; |
1253 | 1293 | ||
1254 | // when seeking to the beginning of the seekable range, it's | 1294 | // when seeking to the beginning of the seekable range, it's |
1255 | // possible that imprecise timing information may cause the seek to | 1295 | // possible that imprecise timing information may cause the seek to |
1256 | // end up earlier than the start of the range | 1296 | // end up earlier than the start of the range |
1257 | // in that case, seek again | 1297 | // in that case, seek again |
1258 | seekable = this.seekable(); | 1298 | seekable = this.seekable(); |
1259 | if (this.tech_.seeking() && | 1299 | if (this.tech_.seeking() && |
1260 | currentBuffered.length === 0) { | 1300 | currentBuffered.length === 0) { |
1261 | if (seekable.length && | 1301 | if (seekable.length && |
1262 | this.tech_.currentTime() < seekable.start(0)) { | 1302 | this.tech_.currentTime() < seekable.start(0)) { |
1263 | var next = this.findNextBufferedRange_(); | 1303 | let next = this.findNextBufferedRange_(); |
1264 | if (next.length) { | 1304 | |
1265 | videojs.log('tried seeking to', this.tech_.currentTime(), 'but that was too early, retrying at', next.start(0)); | 1305 | if (next.length) { |
1266 | this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR); | 1306 | videojs.log('tried seeking to', this.tech_.currentTime(), |
1307 | 'but that was too early, retrying at', next.start(0)); | ||
1308 | this.tech_.setCurrentTime(next.start(0) + TIME_FUDGE_FACTOR); | ||
1309 | } | ||
1267 | } | 1310 | } |
1268 | } | 1311 | } |
1269 | } | ||
1270 | 1312 | ||
1313 | timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, | ||
1314 | this.tech_.buffered()); | ||
1271 | 1315 | ||
1272 | timelineUpdate = videojs.Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, | 1316 | if (timelineUpdate && segment) { |
1273 | this.tech_.buffered()); | 1317 | segment.end = timelineUpdate; |
1318 | } | ||
1274 | 1319 | ||
1275 | if (timelineUpdate && segment) { | 1320 | // if we've buffered to the end of the video, let the MediaSource know |
1276 | segment.end = timelineUpdate; | 1321 | if (this.playlists.media().endList && |
1277 | } | 1322 | currentBuffered.length && |
1323 | segments[segments.length - 1].end <= currentBuffered.end(0) && | ||
1324 | this.mediaSource.readyState === 'open') { | ||
1325 | this.mediaSource.endOfStream(); | ||
1326 | return; | ||
1327 | } | ||
1278 | 1328 | ||
1279 | // if we've buffered to the end of the video, let the MediaSource know | 1329 | if (timelineUpdate !== null || |
1280 | if (this.playlists.media().endList && | 1330 | segmentInfo.buffered.length !== this.tech_.buffered().length) { |
1281 | currentBuffered.length && | 1331 | this.updateDuration(playlist); |
1282 | segments[segments.length - 1].end <= currentBuffered.end(0) && | 1332 | // check if it's time to download the next segment |
1283 | this.mediaSource.readyState === 'open') { | 1333 | this.fillBuffer(); |
1284 | this.mediaSource.endOfStream(); | 1334 | return; |
1285 | return; | 1335 | } |
1286 | } | ||
1287 | 1336 | ||
1288 | if (timelineUpdate !== null || | 1337 | // the last segment append must have been entirely in the |
1289 | segmentInfo.buffered.length !== this.tech_.buffered().length) { | 1338 | // already buffered time ranges. just buffer forward until we |
1290 | this.updateDuration(playlist); | 1339 | // find a segment that adds to the buffered time ranges and |
1291 | // check if it's time to download the next segment | 1340 | // improves subsequent media index calculations. |
1292 | this.fillBuffer(); | 1341 | this.fillBuffer(currentMediaIndex + 1); |
1293 | return; | 1342 | return; |
1294 | } | 1343 | } |
1295 | 1344 | ||
1296 | // the last segment append must have been entirely in the | 1345 | /** |
1297 | // already buffered time ranges. just buffer forward until we | 1346 | * Attempt to retrieve the key for a particular media segment. |
1298 | // find a segment that adds to the buffered time ranges and | 1347 | */ |
1299 | // improves subsequent media index calculations. | 1348 | fetchKey_(segment) { |
1300 | this.fillBuffer(currentMediaIndex + 1); | 1349 | let key; |
1301 | return; | 1350 | let settings; |
1302 | }; | 1351 | let receiveKey; |
1303 | 1352 | ||
1304 | /** | 1353 | // if there is a pending XHR or no segments, don't do anything |
1305 | * Attempt to retrieve the key for a particular media segment. | 1354 | if (this.keyXhr_) { |
1306 | */ | 1355 | return; |
1307 | videojs.HlsHandler.prototype.fetchKey_ = function(segment) { | 1356 | } |
1308 | var key, self, settings, receiveKey; | ||
1309 | 1357 | ||
1310 | // if there is a pending XHR or no segments, don't do anything | 1358 | settings = this.options_; |
1311 | if (this.keyXhr_) { | ||
1312 | return; | ||
1313 | } | ||
1314 | 1359 | ||
1315 | self = this; | 1360 | /** |
1316 | settings = this.options_; | 1361 | * Handle a key XHR response. |
1362 | */ | ||
1363 | receiveKey = (keyRecieved) => { | ||
1364 | return (error, request) => { | ||
1365 | let view; | ||
1317 | 1366 | ||
1318 | /** | 1367 | this.keyXhr_ = null; |
1319 | * Handle a key XHR response. | ||
1320 | */ | ||
1321 | receiveKey = function(key) { | ||
1322 | return function(error, request) { | ||
1323 | var view; | ||
1324 | self.keyXhr_ = null; | ||
1325 | |||
1326 | if (error || !request.response || request.response.byteLength !== 16) { | ||
1327 | key.retries = key.retries || 0; | ||
1328 | key.retries++; | ||
1329 | if (!request.aborted) { | ||
1330 | // try fetching again | ||
1331 | self.fetchKey_(segment); | ||
1332 | } | ||
1333 | return; | ||
1334 | } | ||
1335 | 1368 | ||
1336 | view = new DataView(request.response); | 1369 | if (error || !request.response || request.response.byteLength !== 16) { |
1337 | key.bytes = new Uint32Array([ | 1370 | keyRecieved.retries = keyRecieved.retries || 0; |
1338 | view.getUint32(0), | 1371 | keyRecieved.retries++; |
1339 | view.getUint32(4), | 1372 | if (!request.aborted) { |
1340 | view.getUint32(8), | 1373 | // try fetching again |
1341 | view.getUint32(12) | 1374 | this.fetchKey_(segment); |
1342 | ]); | 1375 | } |
1376 | return; | ||
1377 | } | ||
1343 | 1378 | ||
1344 | // check to see if this allows us to make progress buffering now | 1379 | view = new DataView(request.response); |
1345 | self.checkBuffer_(); | 1380 | keyRecieved.bytes = new Uint32Array([ |
1381 | view.getUint32(0), | ||
1382 | view.getUint32(4), | ||
1383 | view.getUint32(8), | ||
1384 | view.getUint32(12) | ||
1385 | ]); | ||
1386 | |||
1387 | // check to see if this allows us to make progress buffering now | ||
1388 | this.checkBuffer_(); | ||
1389 | }; | ||
1346 | }; | 1390 | }; |
1347 | }; | ||
1348 | 1391 | ||
1349 | key = segment.key; | 1392 | key = segment.key; |
1350 | 1393 | ||
1351 | // nothing to do if this segment is unencrypted | 1394 | // nothing to do if this segment is unencrypted |
1352 | if (!key) { | 1395 | if (!key) { |
1353 | return; | 1396 | return; |
1354 | } | 1397 | } |
1355 | 1398 | ||
1356 | // request the key if the retry limit hasn't been reached | 1399 | // request the key if the retry limit hasn't been reached |
1357 | if (!key.bytes && !keyFailed(key)) { | 1400 | if (!key.bytes && !keyFailed(key)) { |
1358 | this.keyXhr_ = videojs.Hls.xhr({ | 1401 | this.keyXhr_ = Hls.xhr({ |
1359 | uri: this.playlistUriToUrl(key.uri), | 1402 | uri: this.playlistUriToUrl(key.uri), |
1360 | responseType: 'arraybuffer', | 1403 | responseType: 'arraybuffer', |
1361 | withCredentials: settings.withCredentials | 1404 | withCredentials: settings.withCredentials |
1362 | }, receiveKey(key)); | 1405 | }, receiveKey(key)); |
1363 | return; | 1406 | return; |
1407 | } | ||
1364 | } | 1408 | } |
1365 | }; | 1409 | } |
1366 | 1410 | ||
1367 | /** | 1411 | /** |
1368 | * Whether the browser has built-in HLS support. | 1412 | * Attempts to find the buffered TimeRange that contains the specified |
1413 | * time, or where playback is currently happening if no specific time | ||
1414 | * is specified. | ||
1415 | * @param time (optional) {number} the time to filter on. Defaults to | ||
1416 | * currentTime. | ||
1417 | * @return a new TimeRanges object. | ||
1369 | */ | 1418 | */ |
1370 | videojs.Hls.supportsNativeHls = (function() { | 1419 | HlsHandler.prototype.findBufferedRange_ = |
1371 | var | 1420 | filterBufferedRanges(function(start, end, time) { |
1372 | video = document.createElement('video'), | 1421 | return start - TIME_FUDGE_FACTOR <= time && |
1373 | xMpegUrl, | 1422 | end + TIME_FUDGE_FACTOR >= time; |
1374 | vndMpeg; | 1423 | }); |
1375 | |||
1376 | // native HLS is definitely not supported if HTML5 video isn't | ||
1377 | if (!videojs.getComponent('Html5').isSupported()) { | ||
1378 | return false; | ||
1379 | } | ||
1380 | |||
1381 | xMpegUrl = video.canPlayType('application/x-mpegURL'); | ||
1382 | vndMpeg = video.canPlayType('application/vnd.apple.mpegURL'); | ||
1383 | return (/probably|maybe/).test(xMpegUrl) || | ||
1384 | (/probably|maybe/).test(vndMpeg); | ||
1385 | })(); | ||
1386 | |||
1387 | // HLS is a source handler, not a tech. Make sure attempts to use it | ||
1388 | // as one do not cause exceptions. | ||
1389 | videojs.Hls.isSupported = function() { | ||
1390 | return videojs.log.warn('HLS is no longer a tech. Please remove it from ' + | ||
1391 | 'your player\'s techOrder.'); | ||
1392 | }; | ||
1393 | |||
1394 | /** | 1424 | /** |
1395 | * A comparator function to sort two playlist object by bandwidth. | 1425 | * Returns the TimeRanges that begin at or later than the specified |
1396 | * @param left {object} a media playlist object | 1426 | * time. |
1397 | * @param right {object} a media playlist object | 1427 | * @param time (optional) {number} the time to filter on. Defaults to |
1398 | * @return {number} Greater than zero if the bandwidth attribute of | 1428 | * currentTime. |
1399 | * left is greater than the corresponding attribute of right. Less | 1429 | * @return a new TimeRanges object. |
1400 | * than zero if the bandwidth of right is greater than left and | ||
1401 | * exactly zero if the two are equal. | ||
1402 | */ | 1430 | */ |
1403 | videojs.Hls.comparePlaylistBandwidth = function(left, right) { | 1431 | HlsHandler.prototype.findNextBufferedRange_ = |
1404 | var leftBandwidth, rightBandwidth; | 1432 | filterBufferedRanges(function(start, end, time) { |
1405 | if (left.attributes && left.attributes.BANDWIDTH) { | 1433 | return start - TIME_FUDGE_FACTOR >= time; |
1406 | leftBandwidth = left.attributes.BANDWIDTH; | 1434 | }); |
1407 | } | ||
1408 | leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; | ||
1409 | if (right.attributes && right.attributes.BANDWIDTH) { | ||
1410 | rightBandwidth = right.attributes.BANDWIDTH; | ||
1411 | } | ||
1412 | rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; | ||
1413 | |||
1414 | return leftBandwidth - rightBandwidth; | ||
1415 | }; | ||
1416 | 1435 | ||
1417 | /** | 1436 | /** |
1418 | * A comparator function to sort two playlist object by resolution (width). | 1437 | * The Source Handler object, which informs video.js what additional |
1419 | * @param left {object} a media playlist object | 1438 | * MIME types are supported and sets up playback. It is registered |
1420 | * @param right {object} a media playlist object | 1439 | * automatically to the appropriate tech based on the capabilities of |
1421 | * @return {number} Greater than zero if the resolution.width attribute of | 1440 | * the browser it is running in. It is not necessary to use or modify |
1422 | * left is greater than the corresponding attribute of right. Less | 1441 | * this object in normal usage. |
1423 | * than zero if the resolution.width of right is greater than left and | ||
1424 | * exactly zero if the two are equal. | ||
1425 | */ | 1442 | */ |
1426 | videojs.Hls.comparePlaylistResolution = function(left, right) { | 1443 | const HlsSourceHandler = function(mode) { |
1427 | var leftWidth, rightWidth; | 1444 | return { |
1428 | 1445 | canHandleSource(srcObj) { | |
1429 | if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { | 1446 | return HlsSourceHandler.canPlayType(srcObj.type); |
1430 | leftWidth = left.attributes.RESOLUTION.width; | 1447 | }, |
1431 | } | 1448 | handleSource(source, tech) { |
1432 | 1449 | if (mode === 'flash') { | |
1433 | leftWidth = leftWidth || window.Number.MAX_VALUE; | 1450 | // We need to trigger this asynchronously to give others the chance |
1434 | 1451 | // to bind to the event when a source is set at player creation | |
1435 | if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { | 1452 | tech.setTimeout(function() { |
1436 | rightWidth = right.attributes.RESOLUTION.width; | 1453 | tech.trigger('loadstart'); |
1437 | } | 1454 | }, 1); |
1455 | } | ||
1456 | tech.hls = new HlsHandler(tech, { | ||
1457 | source, | ||
1458 | mode | ||
1459 | }); | ||
1460 | tech.hls.src(source.src); | ||
1461 | return tech.hls; | ||
1462 | }, | ||
1463 | canPlayType(type) { | ||
1464 | return HlsSourceHandler.canPlayType(type); | ||
1465 | } | ||
1466 | }; | ||
1467 | }; | ||
1438 | 1468 | ||
1439 | rightWidth = rightWidth || window.Number.MAX_VALUE; | 1469 | HlsSourceHandler.canPlayType = function(type) { |
1470 | let mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; | ||
1440 | 1471 | ||
1441 | // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions | 1472 | // favor native HLS support if it's available |
1442 | // have the same media dimensions/ resolution | 1473 | if (Hls.supportsNativeHls) { |
1443 | if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { | 1474 | return false; |
1444 | return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; | ||
1445 | } else { | ||
1446 | return leftWidth - rightWidth; | ||
1447 | } | 1475 | } |
1476 | return mpegurlRE.test(type); | ||
1448 | }; | 1477 | }; |
1449 | 1478 | ||
1450 | /** | 1479 | if (typeof videojs.MediaSource === 'undefined' || |
1451 | * Constructs a new URI by interpreting a path relative to another | 1480 | typeof videojs.URL === 'undefined') { |
1452 | * URI. | 1481 | videojs.MediaSource = MediaSource; |
1453 | * @param basePath {string} a relative or absolute URI | 1482 | videojs.URL = URL; |
1454 | * @param path {string} a path part to combine with the base | 1483 | } |
1455 | * @return {string} a URI that is equivalent to composing `base` | ||
1456 | * with `path` | ||
1457 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | ||
1458 | */ | ||
1459 | resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) { | ||
1460 | // use the base element to get the browser to handle URI resolution | ||
1461 | var | ||
1462 | oldBase = document.querySelector('base'), | ||
1463 | docHead = document.querySelector('head'), | ||
1464 | a = document.createElement('a'), | ||
1465 | base = oldBase, | ||
1466 | oldHref, | ||
1467 | result; | ||
1468 | |||
1469 | // prep the document | ||
1470 | if (oldBase) { | ||
1471 | oldHref = oldBase.href; | ||
1472 | } else { | ||
1473 | base = docHead.appendChild(document.createElement('base')); | ||
1474 | } | ||
1475 | 1484 | ||
1476 | base.href = basePath; | 1485 | // register source handlers with the appropriate techs |
1477 | a.href = path; | 1486 | if (MediaSource.supportsNativeMediaSources()) { |
1478 | result = a.href; | 1487 | videojs.getComponent('Html5').registerSourceHandler(HlsSourceHandler('html5')); |
1488 | } | ||
1489 | if (window.Uint8Array) { | ||
1490 | videojs.getComponent('Flash').registerSourceHandler(HlsSourceHandler('flash')); | ||
1491 | } | ||
1479 | 1492 | ||
1480 | // clean up | 1493 | videojs.HlsHandler = HlsHandler; |
1481 | if (oldBase) { | 1494 | videojs.HlsSourceHandler = HlsSourceHandler; |
1482 | oldBase.href = oldHref; | 1495 | videojs.Hls = Hls; |
1483 | } else { | 1496 | videojs.m3u8 = m3u8; |
1484 | docHead.removeChild(base); | ||
1485 | } | ||
1486 | return result; | ||
1487 | }; | ||
1488 | 1497 | ||
1489 | })(window, window.videojs, document); | 1498 | export default { |
1499 | Hls, | ||
1500 | HlsHandler, | ||
1501 | HlsSourceHandler | ||
1502 | }; | ... | ... |
... | @@ -90,7 +90,6 @@ function() { | ... | @@ -90,7 +90,6 @@ function() { |
90 | 90 | ||
91 | }); | 91 | }); |
92 | 92 | ||
93 | |||
94 | QUnit.module('Incremental Processing', { | 93 | QUnit.module('Incremental Processing', { |
95 | beforeEach() { | 94 | beforeEach() { |
96 | this.clock = sinon.useFakeTimers(); | 95 | this.clock = sinon.useFakeTimers(); | ... | ... |
... | @@ -13,13 +13,6 @@ | ... | @@ -13,13 +13,6 @@ |
13 | <script src="/node_modules/sinon/pkg/sinon.js"></script> | 13 | <script src="/node_modules/sinon/pkg/sinon.js"></script> |
14 | <script src="/node_modules/qunitjs/qunit/qunit.js"></script> | 14 | <script src="/node_modules/qunitjs/qunit/qunit.js"></script> |
15 | <script src="/node_modules/video.js/dist/video.js"></script> | 15 | <script src="/node_modules/video.js/dist/video.js"></script> |
16 | <script src="/node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> | ||
17 | |||
18 | <script src="/src/videojs-contrib-hls.js"></script> | ||
19 | <script src="/dist/videojs-contrib-hls.js"></script> | ||
20 | <script src="/src/bin-utils.js"></script> | ||
21 | |||
22 | <script src="/test/videojs-contrib-hls.test.js"></script> | ||
23 | <script src="/dist-test/videojs-contrib-hls.js"></script> | 16 | <script src="/dist-test/videojs-contrib-hls.js"></script> |
24 | 17 | ||
25 | </body> | 18 | </body> | ... | ... |
... | @@ -11,29 +11,11 @@ var DEFAULTS = { | ... | @@ -11,29 +11,11 @@ var DEFAULTS = { |
11 | 'node_modules/video.js/dist/video.js', | 11 | 'node_modules/video.js/dist/video.js', |
12 | 'node_modules/video.js/dist/video-js.css', | 12 | 'node_modules/video.js/dist/video-js.css', |
13 | 13 | ||
14 | // REMOVE ME WHEN BROWSERIFIED | 14 | 'test/**/*.test.js' |
15 | 'node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | ||
16 | |||
17 | // these two stub old functionality | ||
18 | 'src/videojs-contrib-hls.js', | ||
19 | 'dist/videojs-contrib-hls.js', | ||
20 | |||
21 | 'src/bin-utils.js', | ||
22 | |||
23 | 'test/stub.test.js', | ||
24 | |||
25 | 'test/videojs-contrib-hls.test.js', | ||
26 | 'test/m3u8.test.js', | ||
27 | 'test/playlist.test.js', | ||
28 | 'test/playlist-loader.test.js', | ||
29 | 'test/decrypter.test.js', | ||
30 | // END REMOVE ME | ||
31 | // 'test/**/*.js' | ||
32 | ], | 15 | ], |
33 | 16 | ||
34 | exclude: [ | 17 | exclude: [ |
35 | 'test/bundle.js', | 18 | 'test/data/**' |
36 | // 'test/data/**' | ||
37 | ], | 19 | ], |
38 | 20 | ||
39 | plugins: [ | 21 | plugins: [ |
... | @@ -42,7 +24,7 @@ var DEFAULTS = { | ... | @@ -42,7 +24,7 @@ var DEFAULTS = { |
42 | ], | 24 | ], |
43 | 25 | ||
44 | preprocessors: { | 26 | preprocessors: { |
45 | 'test/{playlist*,decrypter,stub,m3u8}.test.js': ['browserify'] | 27 | 'test/**/*.test.js': ['browserify'] |
46 | }, | 28 | }, |
47 | 29 | ||
48 | reporters: ['dots'], | 30 | reporters: ['dots'], | ... | ... |
... | @@ -29,9 +29,11 @@ QUnit.test('the environment is sane', function(assert) { | ... | @@ -29,9 +29,11 @@ QUnit.test('the environment is sane', function(assert) { |
29 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); | 29 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); |
30 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); | 30 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); |
31 | assert.strictEqual(typeof videojs, 'function', 'videojs exists'); | 31 | assert.strictEqual(typeof videojs, 'function', 'videojs exists'); |
32 | assert.strictEqual(typeof videojs.MediaSource, 'object', 'MediaSource is an object'); | 32 | assert.strictEqual(typeof videojs.MediaSource, 'function', 'MediaSource is an object'); |
33 | assert.strictEqual(typeof videojs.URL, 'object', 'URL 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'); | 34 | assert.strictEqual(typeof videojs.Hls, 'object', 'Hls is an object'); |
35 | assert.strictEqual(typeof videojs.HlsSourceHandler,'function', 'HlsSourceHandler is a function'); | 35 | assert.strictEqual(typeof videojs.HlsSourceHandler, |
36 | 'function', | ||
37 | 'HlsSourceHandler is a function'); | ||
36 | assert.strictEqual(typeof videojs.HlsHandler, 'function', 'HlsHandler is a function'); | 38 | assert.strictEqual(typeof videojs.HlsHandler, 'function', 'HlsHandler is a function'); |
37 | }); | 39 | }); | ... | ... |
test/stub.test.js
deleted
100644 → 0
This diff could not be displayed because it is too large.
-
Please register or sign in to post a comment