├── .gitignore
├── .jshintrc
├── .npmignore
├── Gruntfile.js
├── LICENSE-MIT
├── README.md
├── demo
└── index.html
├── dist
├── videojs-hlsjs.js
└── videojs-hlsjs.min.js
├── index.html
├── package.json
└── src
└── videojs-hlsjs.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | .DS_Store
3 | *.log
4 | *~
5 |
6 | # User-specific stuff:
7 | .idea/
8 | *.iml
9 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "eqeqeq": true,
4 | "immed": true,
5 | "latedef": true,
6 | "newcap": true,
7 | "noarg": true,
8 | "sub": true,
9 | "undef": true,
10 | "boss": true,
11 | "eqnull": true,
12 | "browser": true,
13 | "smarttabs": true
14 | }
15 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Intentionally left blank, so that npm does not ignore anything by default,
2 | # but relies on the package.json "files" array to explicitly define what ends
3 | # up in the package.
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (grunt) {
4 | require('load-grunt-tasks')(grunt);
5 |
6 | grunt.initConfig({
7 | pkg: grunt.file.readJSON('package.json'),
8 | banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
9 | '<%= grunt.template.today("yyyy-mm-dd") %>*/\n',
10 | clean: {
11 | files: ['dist']
12 | },
13 | connect: {
14 | main: {
15 | options: {
16 | port: 9000,
17 | protocol: 'http',
18 | hostname: '*'
19 | }
20 | }
21 | },
22 | concat: {
23 | options: {
24 | banner: '<%= banner %>',
25 | stripBanners: true
26 | },
27 | dist: {
28 | src: ['src/**/*.js'],
29 | dest: 'dist/<%= pkg.name %>.js'
30 | }
31 | },
32 | uglify: {
33 | options: {
34 | banner: '<%= banner %>'
35 | },
36 | dist: {
37 | src: '<%= concat.dist.dest %>',
38 | dest: 'dist/<%= pkg.name %>.min.js'
39 | }
40 | },
41 | jshint: {
42 | gruntfile: {
43 | options: {
44 | node: true
45 | },
46 | src: 'Gruntfile.js'
47 | },
48 | src: {
49 | options: {
50 | jshintrc: '.jshintrc'
51 | },
52 | src: ['src/**/*.js']
53 | },
54 | },
55 | watch: {
56 | gruntfile: {
57 | files: '<%= jshint.gruntfile.src %>',
58 | tasks: ['jshint:gruntfile']
59 | },
60 | src: {
61 | files: '<%= jshint.src.src %>',
62 | tasks: ['jshint:src']
63 | }
64 | }
65 | });
66 |
67 | grunt.registerTask('build', ['jshint', 'concat', 'uglify']);
68 | grunt.registerTask('serve', ['jshint', 'connect', 'watch']);
69 | grunt.registerTask('default', ['clean', 'build']);
70 | };
71 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 SRGSSR
4 |
5 | Permission is hereby granted, free of charge, to any person
6 | obtaining a copy of this software and associated documentation
7 | files (the "Software"), to deal in the Software without
8 | restriction, including without limitation the rights to use,
9 | copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the
11 | Software is furnished to do so, subject to the following
12 | conditions:
13 |
14 | The above copyright notice and this permission notice shall be
15 | included in all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Videojs hls.js Plugin
2 |
3 |
4 |
5 | > An HLS plugin for video.jas based on hls.js
6 |
7 | Videojs hls.js offers hls playback using [hls.js](https://github.com/dailymotion/hls.js). For more details on browser compatibility see th hls.js github page.
8 |
9 | - [Getting Started](#getting-started)
10 | - [Documentation](#documentation)
11 | - [Dependencies](#dependencies)
12 | - [CORS Considerations](#cors-considerations)
13 | - [Options](#options)
14 | - [Event Listeners](#event-listeners)
15 | - [Original Author](#original-author)
16 |
17 | ## Getting Started
18 |
19 | Download videojs-hlsjs and include it in your page along with video.js:
20 |
21 | ```html
22 |
23 |
24 |
25 |
26 |
27 |
28 |
35 | ```
36 |
37 | There's also a [demo](https://srgssr.github.io/videojs-hlsjs/demo) of the plugin that you can check out.
38 |
39 | ## Changelog
40 |
41 | - 1.4.5: Added text and audio tracks compatibility.
42 |
43 | ## Documentation
44 |
45 | ### Dependencies
46 | This project depends on:
47 |
48 | - [video.js](https://github.com/videojs/video.js) 5.8.5+.
49 | - [hls.js](https://github.com/dailymotion/hls.js) 0.7.0+.
50 |
51 | ### CORS Considerations
52 |
53 | All HLS resources must be delivered with
54 | [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) allowing GET requests.
55 |
56 | ### Options
57 |
58 | You may pass in an options object to the hls playback technology at player initialization.
59 |
60 | #### hlsjs.favorNativeHLS
61 | Type: `Boolean`
62 |
63 | When the `favorNativeHLS` property is set to `true`, the plugin will prioritize native hls
64 | over MSE. Note that in the case native streaming is available other options won't have any effect.
65 |
66 | #### hlsjs.disableAutoLevel
67 | Type: `Boolean`
68 |
69 | When the `disableAutoLevel` property is set to `true`, the plugin will completely disable auto leveling based on bandwidth and remove it from the list of available level options.
70 | If no level is specified in `hlsjs.startLevelByHeight` or `hlsjs.setLevelByHeight` the plugin will start with the best quality available when this property is set to true.
71 | Useful for browsers that have trouble switching between different qualities.
72 |
73 | #### hlsjs.startLevelByHeight
74 | Type: `Number`
75 |
76 | When the `startLevelByHeight` property is present, the plugin will start the video on the closest quality to the
77 | specified height but the auto leveling will still be enabled unless `hlsjs.disableAutoLevel` was set to `true`. If height metadata is not present in the HLS playlist this property will be ignored.
78 |
79 | #### hlsjs.setLevelByHeight
80 | Type: `Number`
81 |
82 | When the `setLevelByHeight` property is present, the plugin will start the video on the closest quality to the
83 | specified height. The auto leveling will be disabled but it will still be selectable unless `hlsjs.disableAutoLevel` was set to `true`. If height metadata is not present in the HLS playlist this property will be ignored.
84 |
85 | This property takes precedence over `hlsjs.startLevelByHeight`.
86 |
87 | #### hlsjs.hls
88 | Type `object`
89 |
90 | An object containing hls.js configuration parameters, see in detail:
91 | [Hls.js Fine Tuning](https://github.com/dailymotion/hls.js/blob/master/doc/API.md#fine-tuning).
92 |
93 | **Exceptions:**
94 |
95 | * `autoStartLoad` the loading is done through the `preload` attribute of the video tag. This property is always set to `false` when using this plugin.
96 | * `startLevel` if you set any of the level options above this property will be ignored.
97 |
98 | ### Event listeners
99 |
100 | This plugin offers the possibility to attach a callback to any hls.js runtime event, see the documetation
101 | about the different events here: [Hls.js Runtime Events](https://github.com/dailymotion/hls.js/blob/master/doc/API.md#runtime-events). Simply precede the name of the event in camel case by `on`, see an example:
102 |
103 | ```js
104 | var player = videojs('video', {
105 | hlsjs: {
106 | /**
107 | * Will be called on Hls.Events.MEDIA_ATTACHED.
108 | *
109 | * @param {Hls} hls The hls instance from hls.js
110 | * @param {Object} data The data from this HLS runtime event
111 | */
112 | onMediaAttached: function(hls, data) {
113 | // do stuff...
114 | }
115 | }
116 | });
117 | ```
118 |
119 | ## Original Author
120 |
121 | This project was orginally forked from: [videojs-hlsjs](https://github.com/benjipott/videojs-hlsjs), credits to the
122 | original author.
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Videojs HlsJs plugin
6 |
7 |
8 |
35 |
36 |
37 |
38 |
39 | Videojs-hlsjs demo page.
40 |
41 |
42 |
43 |
50 |
51 | Your browser doesn't support video. Please upgrade your browser to see the
52 | example.
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/dist/videojs-hlsjs.js:
--------------------------------------------------------------------------------
1 | /*! videojs-hlsjs - v1.4.8 - 2017-06-06*/
2 | (function (window, videojs, Hls) {
3 | 'use strict';
4 |
5 | /**
6 | * Initialize the plugin.
7 | * @param options (optional) {object} configuration for the plugin
8 | */
9 | var Component = videojs.getComponent('Component'),
10 | Tech = videojs.getTech('Tech'),
11 | Html5 = videojs.getComponent('Html5');
12 |
13 | var Hlsjs = videojs.extend(Html5, {
14 | initHls_: function() {
15 | this.options_.hls.autoStartLoad = false;
16 | this.hls_ = new Hls(this.options_.hls);
17 |
18 | this.bindExternalCallbacks_();
19 |
20 | this.hls_.on(Hls.Events.MEDIA_ATTACHED, videojs.bind(this, this.onMediaAttached_));
21 | this.hls_.on(Hls.Events.MANIFEST_PARSED, videojs.bind(this, this.onManifestParsed_));
22 | this.hls_.on(Hls.Events.MANIFEST_LOADED, videojs.bind(this, this.initAudioTracks_));
23 | this.hls_.on(Hls.Events.MANIFEST_LOADED, videojs.bind(this, this.initTextTracks_));
24 | this.hls_.on(Hls.Events.LEVEL_UPDATE, videojs.bind(this, this.updateTimeRange_));
25 | this.hls_.on(Hls.Events.ERROR, videojs.bind(this, this.onError_));
26 |
27 | this.el_.addEventListener('error', videojs.bind(this, this.onMediaError_));
28 |
29 | this.currentLevel_ = undefined;
30 | this.setLevelOnLoad_ = undefined;
31 | this.lastLevel_ = undefined;
32 | this.timeRange_ = undefined;
33 | this.starttime_ = -1;
34 | this.levels_ = [];
35 |
36 | this.hls_.attachMedia(this.el_);
37 | },
38 |
39 | bindExternalCallbacks_: function() {
40 | var resolveCallbackFromOptions = function(evt, options, hls) {
41 | var capitalize = function(str) {
42 | return str.charAt(0).toUpperCase() + str.slice(1);
43 | }, createCallback = function(callback, hls) {
44 | return function(evt, data) {
45 | callback(hls, data);
46 | };
47 | }, callback = options['on' + capitalize(evt)];
48 |
49 | if (callback && typeof callback === 'function') {
50 | return createCallback(callback, hls);
51 | }
52 | }, key;
53 |
54 | for(key in Hls.Events) {
55 | if (Object.prototype.hasOwnProperty.call(Hls.Events, key)) {
56 | var evt = Hls.Events[key],
57 | callback = resolveCallbackFromOptions(evt, this.options_, this.hls_);
58 |
59 | if (callback) {
60 | this.hls_.on(evt, videojs.bind(this, callback));
61 | }
62 | }
63 | }
64 | },
65 |
66 | onMediaAttached_: function() {
67 | this.triggerReady();
68 | },
69 |
70 | updateTimeRange_: function() {
71 | var range;
72 |
73 | if (this.hls_ && this.hls_.currentLevel >= 0) {
74 | var details = this.hls_.levels[this.hls_.currentLevel].details;
75 |
76 | if (details) {
77 | var fragments = details.fragments, isLive = details.live,
78 | firstFragmentIndex = !isLive ? 0 : 2,
79 | firstFragment = fragments[firstFragmentIndex > fragments.length ? 0 : firstFragmentIndex],
80 | liveSyncDurationCount = this.hls_.config.liveSyncDurationCount,
81 | lastFragmentIndex = !isLive ? fragments.length - 1 : fragments.length - liveSyncDurationCount,
82 | lastFragment = fragments[lastFragmentIndex < 0 ? 0 : lastFragmentIndex];
83 |
84 | range = {
85 | start: firstFragment.start,
86 | end: lastFragment.start + lastFragment.duration
87 | };
88 | }
89 | }
90 |
91 | if (!range && !this.timeRange_) {
92 | var duration = Html5.prototype.duration.apply(this);
93 | if (duration && !isNaN(duration)) {
94 | range = {start: 0, end: duration};
95 | }
96 | } else if (!range) {
97 | range = this.timeRange_;
98 | }
99 |
100 | this.timeRange_ = range;
101 | },
102 |
103 | play: function() {
104 | if (this.preload() === 'none' && !this.hasStarted_) {
105 | if (this.setLevelOnLoad_) {
106 | this.setLevel(this.setLevelOnLoad_);
107 | }
108 | this.hls_.startLoad(this.starttime());
109 | }
110 |
111 | Html5.prototype.play.apply(this);
112 | },
113 |
114 | duration: function() {
115 | this.updateTimeRange_();
116 | return (this.timeRange_) ? this.timeRange_.end - this.timeRange_.start : undefined;
117 | },
118 |
119 | currentTime: function() {
120 | this.updateTimeRange_();
121 | if (this.hls_.currentLevel !== this.lastLevel_) {
122 | this.trigger('levelswitched');
123 | }
124 |
125 | this.lastLevel_ = this.hls_.currentLevel;
126 | return Html5.prototype.currentTime.apply(this);
127 | },
128 |
129 | seekable: function() {
130 | if (this.timeRange_) {
131 | return {
132 | start: function() { return this.timeRange_.start; }.bind(this),
133 | end: function() { return this.timeRange_.end; }.bind(this),
134 | length: 1
135 | };
136 | } else {
137 | return {length: 0};
138 | }
139 | },
140 |
141 | onManifestParsed_: function() {
142 | var hasAutoLevel = !this.options_.disableAutoLevel, startLevel, autoLevel;
143 |
144 | this.parseLevels_();
145 |
146 | if (this.levels_.length > 0) {
147 | if (this.options_.setLevelByHeight) {
148 | startLevel = this.getLevelByHeight_(this.options_.setLevelByHeight);
149 | autoLevel = false;
150 | } else if (this.options_.startLevelByHeight) {
151 | startLevel = this.getLevelByHeight_(this.options_.startLevelByHeight);
152 | autoLevel = hasAutoLevel;
153 | }
154 |
155 | if (!hasAutoLevel && (!startLevel || startLevel.index === -1)) {
156 | startLevel = this.levels_[this.levels_.length-1];
157 | autoLevel = false;
158 | }
159 | } else if (!hasAutoLevel) {
160 | startLevel = {index: this.hls_.levels.length-1};
161 | autoLevel = false;
162 | }
163 |
164 | if (startLevel) {
165 | this.hls_.startLevel = startLevel.index;
166 | }
167 |
168 | if (this.preload() !== 'none') {
169 | if (!autoLevel && startLevel) {
170 | this.setLevel(startLevel);
171 | }
172 | this.hls_.startLoad(this.starttime());
173 | } else if (!autoLevel && startLevel) {
174 | this.setLevelOnLoad_ = startLevel;
175 | this.currentLevel_ = startLevel;
176 | }
177 |
178 | if (this.autoplay() && this.paused()) {
179 | this.play();
180 | }
181 |
182 | this.trigger('levelsloaded');
183 | },
184 |
185 | initAudioTracks_: function() {
186 | var i, toRemove = [], vjsTracks = this.audioTracks(),
187 | hlsTracks = this.hls_.audioTracks,
188 | hlsGroups = [],
189 | hlsGroupTracks = [],
190 | isEnabled = function(track) {
191 | var hls = this.hls_;
192 | return track.groups.reduce(function (acc, g) {
193 | return acc || g.id === hls.audioTrack;
194 | }, false);
195 | },
196 | modeChanged = function(tech) {
197 | if (this.enabled) {
198 | var level = tech.currentLevel();
199 | var id = this.__hlsGroups.reduce(function(acc, group){
200 | if (group.groupId === level.audio) {
201 | acc = group.id;
202 | }
203 | return acc;
204 | }, this.__hlsTrackId);
205 | if (id !== this.__hlsTrackId) {
206 | tech.hls_.audioTrack = id;
207 | }
208 |
209 | }
210 | };
211 |
212 | var g = 0;
213 | hlsTracks.forEach(function(track){
214 | var name = (typeof track.groupId !== 'undefined') ? track.name : 'no-groups';
215 | var group = { id: track.id, groupId: track.groupId };
216 | if (typeof hlsGroups[name] === 'undefined') {
217 | hlsGroups[name] = g;
218 | hlsGroupTracks[g] = [];
219 | var t = track;
220 | t.groups = [];
221 | t.groups.push(group);
222 | hlsGroupTracks[g] = t;
223 | g++;
224 | } else {
225 | hlsGroupTracks[hlsGroups[track.name]].groups.push(group);
226 | }
227 | });
228 |
229 | for (i = 0; i < vjsTracks.length; i++) {
230 | var track = vjsTracks[i];
231 | if (track.__hlsTrackId !== undefined) {
232 | toRemove.push(track);
233 | }
234 | }
235 |
236 | for (i = 0; i < toRemove.length; i++) {
237 | vjsTracks.removeTrack_(toRemove[i]);
238 | }
239 |
240 | for (i = 0; i < hlsGroupTracks.length; i++) {
241 | var hlsTrack = hlsGroupTracks[i];
242 | var vjsTrack = new videojs.AudioTrack({
243 | type: hlsTrack.type,
244 | language: hlsTrack.lang,
245 | label: hlsTrack.name,
246 | enabled: isEnabled.bind(this, hlsTrack)()
247 | });
248 |
249 | vjsTrack.__hlsTrackId = hlsTrack.id;
250 | vjsTrack.__hlsGroups = hlsTrack.groups;
251 | vjsTrack.addEventListener('enabledchange', modeChanged.bind(vjsTrack, this));
252 | vjsTracks.addTrack(vjsTrack);
253 | }
254 | },
255 |
256 | initTextTracks_: function() {
257 | var i, toRemove = [], vjsTracks = this.textTracks(),
258 | hlsTracks = this.hls_.subtitleTracks,
259 | modeChanged = function() {
260 | this.tech_.el_.textTracks[this.__hlsTrack.vjsId].mode = this.mode;
261 | };
262 | for (i = 0; i < vjsTracks.length; i++) {
263 | var track = vjsTracks[i];
264 | if (track.__hlsTrack !== undefined) {
265 | toRemove.push(track);
266 | }
267 | }
268 |
269 | for (i = 0; i < toRemove.length; i++) {
270 | vjsTracks.removeTrack_(toRemove[i]);
271 | }
272 | var hlsHasDefaultTrack = false;
273 | for (i = 0; i < hlsTracks.length; i++) {
274 | var hlsTrack = hlsTracks[i],
275 | vjsTrack = new videojs.TextTrack({
276 | srclang: hlsTrack.lang,
277 | label: hlsTrack.name,
278 | mode: ((typeof hlsTrack.default !== 'undefined') && hlsTrack.default && !hlsHasDefaultTrack) ? 'showing' : 'hidden',
279 | tech: this
280 | });
281 | if ((typeof hlsTrack.default !== 'undefined') && hlsTrack.default) {
282 | hlsHasDefaultTrack = true;
283 | }
284 | vjsTrack.__hlsTrack = hlsTrack;
285 | vjsTrack.__hlsTrack.vjsId = i+1;
286 | vjsTrack.addEventListener('modechange', modeChanged);
287 | vjsTracks.addTrack_(vjsTrack);
288 | }
289 | if (hlsHasDefaultTrack) {
290 | this.trigger('texttrackchange');
291 | }
292 | },
293 |
294 | getLevelByHeight_: function (h) {
295 | var i, result;
296 | for (i = 0; i < this.levels_.length; i++) {
297 | var cLevel = this.levels_[i],
298 | cDiff = Math.abs(h - cLevel.height),
299 | pLevel = result,
300 | pDiff = (pLevel !== undefined) ? Math.abs(h - pLevel.height) : undefined;
301 |
302 | if (pDiff === undefined || (pDiff > cDiff)) {
303 | result = this.levels_[i];
304 | }
305 | }
306 | return result;
307 | },
308 |
309 | parseLevels_: function() {
310 | this.levels_ = [];
311 | this.currentLevel_ = undefined;
312 |
313 | if (this.hls_.levels) {
314 | var i;
315 |
316 | if (!this.options_.disableAutoLevel) {
317 | this.levels_.push({
318 | label: 'auto',
319 | index: -1,
320 | height: -1
321 | });
322 | this.currentLevel_ = this.levels_[0];
323 | }
324 |
325 | for (i = 0; i < this.hls_.levels.length; i++) {
326 | var level = this.hls_.levels[i];
327 | var lvl = null;
328 | if (level.height) {
329 | lvl = {
330 | label: level.height + 'p',
331 | index: i,
332 | height: level.height
333 | };
334 | }
335 | if (typeof level.attrs.AUDIO !== 'undefined') {
336 | lvl = lvl || {};
337 | lvl.index = i;
338 | lvl.audio = level.attrs.AUDIO;
339 | }
340 | if (lvl) {
341 | this.levels_.push(lvl);
342 | }
343 | }
344 |
345 | if (this.levels_.length <= 1) {
346 | this.levels_ = [];
347 | this.currentLevel_ = undefined;
348 | }
349 | }
350 | },
351 |
352 | setSrc: function(src) {
353 | if (this.hls_) {
354 | this.hls_.destroy();
355 | }
356 |
357 | if (this.currentLevel_) {
358 | this.options_.setLevelByHeight = this.currentLevel_.height;
359 | }
360 |
361 | this.initHls_();
362 | this.hls_.loadSource(src);
363 | },
364 |
365 | onMediaError_: function(event) {
366 | var error = event.currentTarget.error;
367 | if (error && error.code === error.MEDIA_ERR_DECODE) {
368 | var data = {
369 | type: Hls.ErrorTypes.MEDIA_ERROR,
370 | fatal: true,
371 | details: 'mediaErrorDecode'
372 | };
373 |
374 | this.onError_(event, data);
375 | }
376 | },
377 |
378 | onError_: function(event, data) {
379 | var abort = [Hls.ErrorDetails.MANIFEST_LOAD_ERROR,
380 | Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT,
381 | Hls.ErrorDetails.MANIFEST_PARSING_ERROR];
382 |
383 | if (abort.indexOf(data.details) >= 0) {
384 | videojs.log.error('HLSJS: Fatal error: "' + data.details + '", aborting playback.');
385 | this.hls_.destroy();
386 | this.error = function() {
387 | return {code: 3};
388 | };
389 | this.trigger('error');
390 | } else {
391 | if (data.fatal) {
392 | switch (data.type) {
393 | case Hls.ErrorTypes.NETWORK_ERROR:
394 | videojs.log.warn('HLSJS: Network error: "' + data.details + '", trying to recover...');
395 | this.hls_.startLoad();
396 | this.trigger('waiting');
397 | break;
398 |
399 | case Hls.ErrorTypes.MEDIA_ERROR:
400 | var startLoad = function() {
401 | this.hls_.startLoad();
402 | this.hls_.off(Hls.Events.MEDIA_ATTACHED, startLoad);
403 | }.bind(this);
404 |
405 | videojs.log.warn('HLSJS: Media error: "' + data.details + '", trying to recover...');
406 | this.hls_.swapAudioCodec();
407 | this.hls_.recoverMediaError();
408 | this.hls_.on(Hls.Events.MEDIA_ATTACHED, startLoad);
409 |
410 | this.trigger('waiting');
411 | break;
412 | default:
413 | videojs.log.error('HLSJS: Fatal error: "' + data.details + '", aborting playback.');
414 | this.hls_.destroy();
415 | this.error = function() {
416 | return {code: 3};
417 | };
418 | this.trigger('error');
419 | break;
420 | }
421 | }
422 | }
423 | },
424 |
425 | currentLevel: function() {
426 | var hasAutoLevel = !this.options_.disableAutoLevel;
427 | return (this.currentLevel_ && this.currentLevel_.index === -1) ?
428 | this.levels_[(hasAutoLevel) ? this.hls_.currentLevel+1 : this.hls_.currentLevel] :
429 | this.currentLevel_;
430 | },
431 |
432 | isAutoLevel: function() {
433 | return this.currentLevel_ && this.currentLevel_.index === -1;
434 | },
435 |
436 | setLevel: function(level) {
437 | this.currentLevel_ = level;
438 | this.setLevelOnLoad_ = undefined;
439 | this.hls_.currentLevel = level.index;
440 | this.hls_.loadLevel = level.index;
441 | },
442 |
443 | getLevels: function() {
444 | return this.levels_;
445 | },
446 |
447 | supportsStarttime: function() {
448 | return true;
449 | },
450 |
451 | starttime: function(starttime) {
452 | if (starttime) {
453 | this.starttime_ = starttime;
454 | } else {
455 | return this.starttime_;
456 | }
457 | },
458 |
459 | dispose: function() {
460 | if (this.hls_) {
461 | this.hls_.destroy();
462 | }
463 | return Html5.prototype.dispose.apply(this);
464 | }
465 | });
466 |
467 | Hlsjs.isSupported = function() {
468 | return Hls.isSupported();
469 | };
470 |
471 | Hlsjs.canPlaySource = function(source) {
472 | return !(videojs.options.hlsjs.favorNativeHLS && Html5.canPlaySource(source)) &&
473 | (source.type && /^application\/(?:x-|vnd\.apple\.)mpegurl/i.test(source.type)) &&
474 | Hls.isSupported();
475 | };
476 |
477 | videojs.options.hlsjs = {
478 | /**
479 | * Whether to favor native HLS playback or not.
480 | * @type {boolean}
481 | * @default true
482 | */
483 | favorNativeHLS: true,
484 | hls: {}
485 | };
486 |
487 | Component.registerComponent('Hlsjs', Hlsjs);
488 | Tech.registerTech('hlsjs', Hlsjs);
489 | videojs.options.techOrder.push('hlsjs');
490 |
491 | })(window, window.videojs, window.Hls);
492 |
--------------------------------------------------------------------------------
/dist/videojs-hlsjs.min.js:
--------------------------------------------------------------------------------
1 | /*! videojs-hlsjs - v1.4.8 - 2017-06-06*/
2 |
3 | !function(a,b,c){"use strict";var d=b.getComponent("Component"),e=b.getTech("Tech"),f=b.getComponent("Html5"),g=b.extend(f,{initHls_:function(){this.options_.hls.autoStartLoad=!1,this.hls_=new c(this.options_.hls),this.bindExternalCallbacks_(),this.hls_.on(c.Events.MEDIA_ATTACHED,b.bind(this,this.onMediaAttached_)),this.hls_.on(c.Events.MANIFEST_PARSED,b.bind(this,this.onManifestParsed_)),this.hls_.on(c.Events.MANIFEST_LOADED,b.bind(this,this.initAudioTracks_)),this.hls_.on(c.Events.MANIFEST_LOADED,b.bind(this,this.initTextTracks_)),this.hls_.on(c.Events.LEVEL_UPDATE,b.bind(this,this.updateTimeRange_)),this.hls_.on(c.Events.ERROR,b.bind(this,this.onError_)),this.el_.addEventListener("error",b.bind(this,this.onMediaError_)),this.currentLevel_=void 0,this.setLevelOnLoad_=void 0,this.lastLevel_=void 0,this.timeRange_=void 0,this.starttime_=-1,this.levels_=[],this.hls_.attachMedia(this.el_)},bindExternalCallbacks_:function(){var a;for(a in c.Events)if(Object.prototype.hasOwnProperty.call(c.Events,a)){var d=c.Events[a],e=function(a,b,c){var d=b["on"+function(a){return a.charAt(0).toUpperCase()+a.slice(1)}(a)];if(d&&"function"==typeof d)return function(a,b){return function(c,d){a(b,d)}}(d,c)}(d,this.options_,this.hls_);e&&this.hls_.on(d,b.bind(this,e))}},onMediaAttached_:function(){this.triggerReady()},updateTimeRange_:function(){var a;if(this.hls_&&this.hls_.currentLevel>=0){var b=this.hls_.levels[this.hls_.currentLevel].details;if(b){var c=b.fragments,d=b.live,e=d?2:0,g=c[e>c.length?0:e],h=this.hls_.config.liveSyncDurationCount,i=d?c.length-h:c.length-1,j=c[i<0?0:i];a={start:g.start,end:j.start+j.duration}}}if(a||this.timeRange_)a||(a=this.timeRange_);else{var k=f.prototype.duration.apply(this);k&&!isNaN(k)&&(a={start:0,end:k})}this.timeRange_=a},play:function(){"none"!==this.preload()||this.hasStarted_||(this.setLevelOnLoad_&&this.setLevel(this.setLevelOnLoad_),this.hls_.startLoad(this.starttime())),f.prototype.play.apply(this)},duration:function(){return this.updateTimeRange_(),this.timeRange_?this.timeRange_.end-this.timeRange_.start:void 0},currentTime:function(){return this.updateTimeRange_(),this.hls_.currentLevel!==this.lastLevel_&&this.trigger("levelswitched"),this.lastLevel_=this.hls_.currentLevel,f.prototype.currentTime.apply(this)},seekable:function(){return this.timeRange_?{start:function(){return this.timeRange_.start}.bind(this),end:function(){return this.timeRange_.end}.bind(this),length:1}:{length:0}},onManifestParsed_:function(){var a,b,c=!this.options_.disableAutoLevel;this.parseLevels_(),this.levels_.length>0?(this.options_.setLevelByHeight?(a=this.getLevelByHeight_(this.options_.setLevelByHeight),b=!1):this.options_.startLevelByHeight&&(a=this.getLevelByHeight_(this.options_.startLevelByHeight),b=c),c||a&&-1!==a.index||(a=this.levels_[this.levels_.length-1],b=!1)):c||(a={index:this.hls_.levels.length-1},b=!1),a&&(this.hls_.startLevel=a.index),"none"!==this.preload()?(!b&&a&&this.setLevel(a),this.hls_.startLoad(this.starttime())):!b&&a&&(this.setLevelOnLoad_=a,this.currentLevel_=a),this.autoplay()&&this.paused()&&this.play(),this.trigger("levelsloaded")},initAudioTracks_:function(){var a,c=[],d=this.audioTracks(),e=this.hls_.audioTracks,f=[],g=[],h=function(a){var b=this.hls_;return a.groups.reduce(function(a,c){return a||c.id===b.audioTrack},!1)},i=function(a){if(this.enabled){var b=a.currentLevel(),c=this.__hlsGroups.reduce(function(a,c){return c.groupId===b.audio&&(a=c.id),a},this.__hlsTrackId);c!==this.__hlsTrackId&&(a.hls_.audioTrack=c)}},j=0;for(e.forEach(function(a){var b=void 0!==a.groupId?a.name:"no-groups",c={id:a.id,groupId:a.groupId};if(void 0===f[b]){f[b]=j,g[j]=[];var d=a;d.groups=[],d.groups.push(c),g[j]=d,j++}else g[f[a.name]].groups.push(c)}),a=0;ae)&&(c=this.levels_[b])}return c},parseLevels_:function(){if(this.levels_=[],this.currentLevel_=void 0,this.hls_.levels){var a;for(this.options_.disableAutoLevel||(this.levels_.push({label:"auto",index:-1,height:-1}),this.currentLevel_=this.levels_[0]),a=0;a=0)b.log.error('HLSJS: Fatal error: "'+d.details+'", aborting playback.'),this.hls_.destroy(),this.error=function(){return{code:3}},this.trigger("error");else if(d.fatal)switch(d.type){case c.ErrorTypes.NETWORK_ERROR:b.log.warn('HLSJS: Network error: "'+d.details+'", trying to recover...'),this.hls_.startLoad(),this.trigger("waiting");break;case c.ErrorTypes.MEDIA_ERROR:var e=function(){this.hls_.startLoad(),this.hls_.off(c.Events.MEDIA_ATTACHED,e)}.bind(this);b.log.warn('HLSJS: Media error: "'+d.details+'", trying to recover...'),this.hls_.swapAudioCodec(),this.hls_.recoverMediaError(),this.hls_.on(c.Events.MEDIA_ATTACHED,e),this.trigger("waiting");break;default:b.log.error('HLSJS: Fatal error: "'+d.details+'", aborting playback.'),this.hls_.destroy(),this.error=function(){return{code:3}},this.trigger("error")}},currentLevel:function(){var a=!this.options_.disableAutoLevel;return this.currentLevel_&&-1===this.currentLevel_.index?this.levels_[a?this.hls_.currentLevel+1:this.hls_.currentLevel]:this.currentLevel_},isAutoLevel:function(){return this.currentLevel_&&-1===this.currentLevel_.index},setLevel:function(a){this.currentLevel_=a,this.setLevelOnLoad_=void 0,this.hls_.currentLevel=a.index,this.hls_.loadLevel=a.index},getLevels:function(){return this.levels_},supportsStarttime:function(){return!0},starttime:function(a){if(!a)return this.starttime_;this.starttime_=a},dispose:function(){return this.hls_&&this.hls_.destroy(),f.prototype.dispose.apply(this)}});g.isSupported=function(){return c.isSupported()},g.canPlaySource=function(a){return!(b.options.hlsjs.favorNativeHLS&&f.canPlaySource(a))&&a.type&&/^application\/(?:x-|vnd\.apple\.)mpegurl/i.test(a.type)&&c.isSupported()},b.options.hlsjs={favorNativeHLS:!0,hls:{}},d.registerComponent("Hlsjs",g),e.registerTech("hlsjs",g),b.options.techOrder.push("hlsjs")}(window,window.videojs,window.Hls);
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Videojs HlsJs plugin
6 |
7 |
8 |
9 |
26 |
27 |
28 |
29 |
30 | You can see the VideoJs HlsJs plugin in action below.
31 | Look at the source of this page to see how to use it with your videos.
32 |
33 |
34 |
41 |
42 | Your browser doesn't support video. Please upgrade your browser to see the
43 | example.
44 |
45 |
46 |
47 |
48 |
49 |
50 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "videojs-hlsjs",
3 | "version": "1.4.8",
4 | "description": "hls.js playback plugin for videojs",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/SRGSSR/videojs-hlsjs.git"
8 | },
9 | "keywords": ["videojs", "videojs-plugin", "hls", "hls.js" ],
10 | "license": "MIT",
11 | "author": "SRGSSR",
12 | "files": [
13 | "dist/",
14 | "src/",
15 | "README.md",
16 | "LICENSE-MIT"
17 | ],
18 | "dependencies": {
19 | "video.js": "^5.8.5",
20 | "hls.js": "^0.7.0"
21 | },
22 | "devDependencies": {
23 | "grunt": "^1.0.1",
24 | "grunt-contrib-clean": "^1.0.0",
25 | "grunt-contrib-concat": "^1.0.1",
26 | "grunt-contrib-jshint": "^1.1.0",
27 | "grunt-contrib-uglify": "^2.0.0",
28 | "grunt-contrib-connect": "^1.0.2",
29 | "grunt-contrib-watch": "^1.0.0",
30 | "load-grunt-tasks": "^3.5.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/videojs-hlsjs.js:
--------------------------------------------------------------------------------
1 | (function (window, videojs, Hls) {
2 | 'use strict';
3 |
4 | /**
5 | * Initialize the plugin.
6 | * @param options (optional) {object} configuration for the plugin
7 | */
8 | var Component = videojs.getComponent('Component'),
9 | Tech = videojs.getTech('Tech'),
10 | Html5 = videojs.getComponent('Html5');
11 |
12 | var Hlsjs = videojs.extend(Html5, {
13 | initHls_: function() {
14 | this.options_.hls.autoStartLoad = false;
15 | this.hls_ = new Hls(this.options_.hls);
16 |
17 | this.bindExternalCallbacks_();
18 |
19 | this.hls_.on(Hls.Events.MEDIA_ATTACHED, videojs.bind(this, this.onMediaAttached_));
20 | this.hls_.on(Hls.Events.MANIFEST_PARSED, videojs.bind(this, this.onManifestParsed_));
21 | this.hls_.on(Hls.Events.MANIFEST_LOADED, videojs.bind(this, this.initAudioTracks_));
22 | this.hls_.on(Hls.Events.MANIFEST_LOADED, videojs.bind(this, this.initTextTracks_));
23 | this.hls_.on(Hls.Events.LEVEL_UPDATE, videojs.bind(this, this.updateTimeRange_));
24 | this.hls_.on(Hls.Events.ERROR, videojs.bind(this, this.onError_));
25 |
26 | this.el_.addEventListener('error', videojs.bind(this, this.onMediaError_));
27 |
28 | this.currentLevel_ = undefined;
29 | this.setLevelOnLoad_ = undefined;
30 | this.lastLevel_ = undefined;
31 | this.timeRange_ = undefined;
32 | this.starttime_ = -1;
33 | this.levels_ = [];
34 |
35 | this.hls_.attachMedia(this.el_);
36 | },
37 |
38 | bindExternalCallbacks_: function() {
39 | var resolveCallbackFromOptions = function(evt, options, hls) {
40 | var capitalize = function(str) {
41 | return str.charAt(0).toUpperCase() + str.slice(1);
42 | }, createCallback = function(callback, hls) {
43 | return function(evt, data) {
44 | callback(hls, data);
45 | };
46 | }, callback = options['on' + capitalize(evt)];
47 |
48 | if (callback && typeof callback === 'function') {
49 | return createCallback(callback, hls);
50 | }
51 | }, key;
52 |
53 | for(key in Hls.Events) {
54 | if (Object.prototype.hasOwnProperty.call(Hls.Events, key)) {
55 | var evt = Hls.Events[key],
56 | callback = resolveCallbackFromOptions(evt, this.options_, this.hls_);
57 |
58 | if (callback) {
59 | this.hls_.on(evt, videojs.bind(this, callback));
60 | }
61 | }
62 | }
63 | },
64 |
65 | onMediaAttached_: function() {
66 | this.triggerReady();
67 | },
68 |
69 | updateTimeRange_: function() {
70 | var range;
71 |
72 | if (this.hls_ && this.hls_.currentLevel >= 0) {
73 | var details = this.hls_.levels[this.hls_.currentLevel].details;
74 |
75 | if (details) {
76 | var fragments = details.fragments, isLive = details.live,
77 | firstFragmentIndex = !isLive ? 0 : 2,
78 | firstFragment = fragments[firstFragmentIndex > fragments.length ? 0 : firstFragmentIndex],
79 | liveSyncDurationCount = this.hls_.config.liveSyncDurationCount,
80 | lastFragmentIndex = !isLive ? fragments.length - 1 : fragments.length - liveSyncDurationCount,
81 | lastFragment = fragments[lastFragmentIndex < 0 ? 0 : lastFragmentIndex];
82 |
83 | range = {
84 | start: firstFragment.start,
85 | end: lastFragment.start + lastFragment.duration
86 | };
87 | }
88 | }
89 |
90 | if (!range && !this.timeRange_) {
91 | var duration = Html5.prototype.duration.apply(this);
92 | if (duration && !isNaN(duration)) {
93 | range = {start: 0, end: duration};
94 | }
95 | } else if (!range) {
96 | range = this.timeRange_;
97 | }
98 |
99 | this.timeRange_ = range;
100 | },
101 |
102 | play: function() {
103 | if (this.preload() === 'none' && !this.hasStarted_) {
104 | if (this.setLevelOnLoad_) {
105 | this.setLevel(this.setLevelOnLoad_);
106 | }
107 | this.hls_.startLoad(this.starttime());
108 | }
109 |
110 | Html5.prototype.play.apply(this);
111 | },
112 |
113 | duration: function() {
114 | this.updateTimeRange_();
115 | return (this.timeRange_) ? this.timeRange_.end - this.timeRange_.start : undefined;
116 | },
117 |
118 | currentTime: function() {
119 | this.updateTimeRange_();
120 | if (this.hls_.currentLevel !== this.lastLevel_) {
121 | this.trigger('levelswitched');
122 | }
123 |
124 | this.lastLevel_ = this.hls_.currentLevel;
125 | return Html5.prototype.currentTime.apply(this);
126 | },
127 |
128 | seekable: function() {
129 | if (this.timeRange_) {
130 | return {
131 | start: function() { return this.timeRange_.start; }.bind(this),
132 | end: function() { return this.timeRange_.end; }.bind(this),
133 | length: 1
134 | };
135 | } else {
136 | return {length: 0};
137 | }
138 | },
139 |
140 | onManifestParsed_: function() {
141 | var hasAutoLevel = !this.options_.disableAutoLevel, startLevel, autoLevel;
142 |
143 | this.parseLevels_();
144 |
145 | if (this.levels_.length > 0) {
146 | if (this.options_.setLevelByHeight) {
147 | startLevel = this.getLevelByHeight_(this.options_.setLevelByHeight);
148 | autoLevel = false;
149 | } else if (this.options_.startLevelByHeight) {
150 | startLevel = this.getLevelByHeight_(this.options_.startLevelByHeight);
151 | autoLevel = hasAutoLevel;
152 | }
153 |
154 | if (!hasAutoLevel && (!startLevel || startLevel.index === -1)) {
155 | startLevel = this.levels_[this.levels_.length-1];
156 | autoLevel = false;
157 | }
158 | } else if (!hasAutoLevel) {
159 | startLevel = {index: this.hls_.levels.length-1};
160 | autoLevel = false;
161 | }
162 |
163 | if (startLevel) {
164 | this.hls_.startLevel = startLevel.index;
165 | }
166 |
167 | if (this.preload() !== 'none') {
168 | if (!autoLevel && startLevel) {
169 | this.setLevel(startLevel);
170 | }
171 | this.hls_.startLoad(this.starttime());
172 | } else if (!autoLevel && startLevel) {
173 | this.setLevelOnLoad_ = startLevel;
174 | this.currentLevel_ = startLevel;
175 | }
176 |
177 | if (this.autoplay() && this.paused()) {
178 | this.play();
179 | }
180 |
181 | this.trigger('levelsloaded');
182 | },
183 |
184 | initAudioTracks_: function() {
185 | var i, toRemove = [], vjsTracks = this.audioTracks(),
186 | hlsTracks = this.hls_.audioTracks,
187 | hlsGroups = [],
188 | hlsGroupTracks = [],
189 | isEnabled = function(track) {
190 | var hls = this.hls_;
191 | return track.groups.reduce(function (acc, g) {
192 | return acc || g.id === hls.audioTrack;
193 | }, false);
194 | },
195 | modeChanged = function(tech) {
196 | if (this.enabled) {
197 | var level = tech.currentLevel();
198 | var id = this.__hlsGroups.reduce(function(acc, group){
199 | if (group.groupId === level.audio) {
200 | acc = group.id;
201 | }
202 | return acc;
203 | }, this.__hlsTrackId);
204 | if (id !== this.__hlsTrackId) {
205 | tech.hls_.audioTrack = id;
206 | }
207 |
208 | }
209 | };
210 |
211 | var g = 0;
212 | hlsTracks.forEach(function(track){
213 | var name = (typeof track.groupId !== 'undefined') ? track.name : 'no-groups';
214 | var group = { id: track.id, groupId: track.groupId };
215 | if (typeof hlsGroups[name] === 'undefined') {
216 | hlsGroups[name] = g;
217 | hlsGroupTracks[g] = [];
218 | var t = track;
219 | t.groups = [];
220 | t.groups.push(group);
221 | hlsGroupTracks[g] = t;
222 | g++;
223 | } else {
224 | hlsGroupTracks[hlsGroups[track.name]].groups.push(group);
225 | }
226 | });
227 |
228 | for (i = 0; i < vjsTracks.length; i++) {
229 | var track = vjsTracks[i];
230 | if (track.__hlsTrackId !== undefined) {
231 | toRemove.push(track);
232 | }
233 | }
234 |
235 | for (i = 0; i < toRemove.length; i++) {
236 | vjsTracks.removeTrack_(toRemove[i]);
237 | }
238 |
239 | for (i = 0; i < hlsGroupTracks.length; i++) {
240 | var hlsTrack = hlsGroupTracks[i];
241 | var vjsTrack = new videojs.AudioTrack({
242 | type: hlsTrack.type,
243 | language: hlsTrack.lang,
244 | label: hlsTrack.name,
245 | enabled: isEnabled.bind(this, hlsTrack)()
246 | });
247 |
248 | vjsTrack.__hlsTrackId = hlsTrack.id;
249 | vjsTrack.__hlsGroups = hlsTrack.groups;
250 | vjsTrack.addEventListener('enabledchange', modeChanged.bind(vjsTrack, this));
251 | vjsTracks.addTrack(vjsTrack);
252 | }
253 | },
254 |
255 | initTextTracks_: function() {
256 | var i, toRemove = [], vjsTracks = this.textTracks(),
257 | hlsTracks = this.hls_.subtitleTracks,
258 | modeChanged = function() {
259 | this.tech_.el_.textTracks[this.__hlsTrack.vjsId].mode = this.mode;
260 | };
261 | for (i = 0; i < vjsTracks.length; i++) {
262 | var track = vjsTracks[i];
263 | if (track.__hlsTrack !== undefined) {
264 | toRemove.push(track);
265 | }
266 | }
267 |
268 | for (i = 0; i < toRemove.length; i++) {
269 | vjsTracks.removeTrack_(toRemove[i]);
270 | }
271 | var hlsHasDefaultTrack = false;
272 | for (i = 0; i < hlsTracks.length; i++) {
273 | var hlsTrack = hlsTracks[i],
274 | vjsTrack = new videojs.TextTrack({
275 | srclang: hlsTrack.lang,
276 | label: hlsTrack.name,
277 | mode: ((typeof hlsTrack.default !== 'undefined') && hlsTrack.default && !hlsHasDefaultTrack) ? 'showing' : 'hidden',
278 | tech: this
279 | });
280 | if ((typeof hlsTrack.default !== 'undefined') && hlsTrack.default) {
281 | hlsHasDefaultTrack = true;
282 | }
283 | vjsTrack.__hlsTrack = hlsTrack;
284 | vjsTrack.__hlsTrack.vjsId = i+1;
285 | vjsTrack.addEventListener('modechange', modeChanged);
286 | vjsTracks.addTrack_(vjsTrack);
287 | }
288 | if (hlsHasDefaultTrack) {
289 | this.trigger('texttrackchange');
290 | }
291 | },
292 |
293 | getLevelByHeight_: function (h) {
294 | var i, result;
295 | for (i = 0; i < this.levels_.length; i++) {
296 | var cLevel = this.levels_[i],
297 | cDiff = Math.abs(h - cLevel.height),
298 | pLevel = result,
299 | pDiff = (pLevel !== undefined) ? Math.abs(h - pLevel.height) : undefined;
300 |
301 | if (pDiff === undefined || (pDiff > cDiff)) {
302 | result = this.levels_[i];
303 | }
304 | }
305 | return result;
306 | },
307 |
308 | parseLevels_: function() {
309 | this.levels_ = [];
310 | this.currentLevel_ = undefined;
311 |
312 | if (this.hls_.levels) {
313 | var i;
314 |
315 | if (!this.options_.disableAutoLevel) {
316 | this.levels_.push({
317 | label: 'auto',
318 | index: -1,
319 | height: -1
320 | });
321 | this.currentLevel_ = this.levels_[0];
322 | }
323 |
324 | for (i = 0; i < this.hls_.levels.length; i++) {
325 | var level = this.hls_.levels[i];
326 | var lvl = null;
327 | if (level.height) {
328 | lvl = {
329 | label: level.height + 'p',
330 | index: i,
331 | height: level.height
332 | };
333 | }
334 | if (typeof level.attrs.AUDIO !== 'undefined') {
335 | lvl = lvl || {};
336 | lvl.index = i;
337 | lvl.audio = level.attrs.AUDIO;
338 | }
339 | if (lvl) {
340 | this.levels_.push(lvl);
341 | }
342 | }
343 |
344 | if (this.levels_.length <= 1) {
345 | this.levels_ = [];
346 | this.currentLevel_ = undefined;
347 | }
348 | }
349 | },
350 |
351 | setSrc: function(src) {
352 | if (this.hls_) {
353 | this.hls_.destroy();
354 | }
355 |
356 | if (this.currentLevel_) {
357 | this.options_.setLevelByHeight = this.currentLevel_.height;
358 | }
359 |
360 | this.initHls_();
361 | this.hls_.loadSource(src);
362 | },
363 |
364 | onMediaError_: function(event) {
365 | var error = event.currentTarget.error;
366 | if (error && error.code === error.MEDIA_ERR_DECODE) {
367 | var data = {
368 | type: Hls.ErrorTypes.MEDIA_ERROR,
369 | fatal: true,
370 | details: 'mediaErrorDecode'
371 | };
372 |
373 | this.onError_(event, data);
374 | }
375 | },
376 |
377 | onError_: function(event, data) {
378 | var abort = [Hls.ErrorDetails.MANIFEST_LOAD_ERROR,
379 | Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT,
380 | Hls.ErrorDetails.MANIFEST_PARSING_ERROR];
381 |
382 | if (abort.indexOf(data.details) >= 0) {
383 | videojs.log.error('HLSJS: Fatal error: "' + data.details + '", aborting playback.');
384 | this.hls_.destroy();
385 | this.error = function() {
386 | return {code: 3};
387 | };
388 | this.trigger('error');
389 | } else {
390 | if (data.fatal) {
391 | switch (data.type) {
392 | case Hls.ErrorTypes.NETWORK_ERROR:
393 | videojs.log.warn('HLSJS: Network error: "' + data.details + '", trying to recover...');
394 | this.hls_.startLoad();
395 | this.trigger('waiting');
396 | break;
397 |
398 | case Hls.ErrorTypes.MEDIA_ERROR:
399 | var startLoad = function() {
400 | this.hls_.startLoad();
401 | this.hls_.off(Hls.Events.MEDIA_ATTACHED, startLoad);
402 | }.bind(this);
403 |
404 | videojs.log.warn('HLSJS: Media error: "' + data.details + '", trying to recover...');
405 | this.hls_.swapAudioCodec();
406 | this.hls_.recoverMediaError();
407 | this.hls_.on(Hls.Events.MEDIA_ATTACHED, startLoad);
408 |
409 | this.trigger('waiting');
410 | break;
411 | default:
412 | videojs.log.error('HLSJS: Fatal error: "' + data.details + '", aborting playback.');
413 | this.hls_.destroy();
414 | this.error = function() {
415 | return {code: 3};
416 | };
417 | this.trigger('error');
418 | break;
419 | }
420 | }
421 | }
422 | },
423 |
424 | currentLevel: function() {
425 | var hasAutoLevel = !this.options_.disableAutoLevel;
426 | return (this.currentLevel_ && this.currentLevel_.index === -1) ?
427 | this.levels_[(hasAutoLevel) ? this.hls_.currentLevel+1 : this.hls_.currentLevel] :
428 | this.currentLevel_;
429 | },
430 |
431 | isAutoLevel: function() {
432 | return this.currentLevel_ && this.currentLevel_.index === -1;
433 | },
434 |
435 | setLevel: function(level) {
436 | this.currentLevel_ = level;
437 | this.setLevelOnLoad_ = undefined;
438 | this.hls_.currentLevel = level.index;
439 | this.hls_.loadLevel = level.index;
440 | },
441 |
442 | getLevels: function() {
443 | return this.levels_;
444 | },
445 |
446 | supportsStarttime: function() {
447 | return true;
448 | },
449 |
450 | starttime: function(starttime) {
451 | if (starttime) {
452 | this.starttime_ = starttime;
453 | } else {
454 | return this.starttime_;
455 | }
456 | },
457 |
458 | dispose: function() {
459 | if (this.hls_) {
460 | this.hls_.destroy();
461 | }
462 | return Html5.prototype.dispose.apply(this);
463 | }
464 | });
465 |
466 | Hlsjs.isSupported = function() {
467 | return Hls.isSupported();
468 | };
469 |
470 | Hlsjs.canPlaySource = function(source) {
471 | return !(videojs.options.hlsjs.favorNativeHLS && Html5.canPlaySource(source)) &&
472 | (source.type && /^application\/(?:x-|vnd\.apple\.)mpegurl/i.test(source.type)) &&
473 | Hls.isSupported();
474 | };
475 |
476 | videojs.options.hlsjs = {
477 | /**
478 | * Whether to favor native HLS playback or not.
479 | * @type {boolean}
480 | * @default true
481 | */
482 | favorNativeHLS: true,
483 | hls: {}
484 | };
485 |
486 | Component.registerComponent('Hlsjs', Hlsjs);
487 | Tech.registerTech('hlsjs', Hlsjs);
488 | videojs.options.techOrder.push('hlsjs');
489 |
490 | })(window, window.videojs, window.Hls);
491 |
--------------------------------------------------------------------------------