├── .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 | 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 | 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 | 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 | --------------------------------------------------------------------------------