├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── flowplayer.hlsjs.js ├── flowplayer.hlsjs.light.js ├── footConditionalComment.js ├── headConditionalComment.js ├── olddocs.md ├── package.json ├── standalone.js ├── standalone.light.js ├── webpack.config.js └── webpack.light.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .DS_Store 3 | *~ 4 | *.swp 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017, Flowplayer Drive Oy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PATH := ./node_modules/.bin/:$(PATH) 2 | SHELL := /bin/bash 3 | 4 | DIST=dist 5 | JS=$(DIST)/flowplayer.hlsjs 6 | LJS=$(JS).light 7 | 8 | webpack: 9 | @ npm run build 10 | 11 | light: 12 | @ npm run light 13 | 14 | all: webpack 15 | 16 | debug: 17 | $(eval GIT_DESC = $(shell git describe )) 18 | @ mkdir -p $(DIST) 19 | @ cp LICENSE.md $(DIST)/ 20 | @ sed -e 's/\$$GIT_DESC\$$/$(GIT_DESC)/' flowplayer.hlsjs.js > $(JS).js 21 | @ sed -e 's/\$$GIT_DESC\$$/$(GIT_DESC)/' flowplayer.hlsjs.light.js > $(LJS).js 22 | @ cp node_modules/hls.js/dist/hls.min.js $(DIST)/ 23 | @ cp node_modules/hls.js/dist/hls.light.min.js $(DIST)/ 24 | 25 | dist: clean all debug light 26 | 27 | zip: dist 28 | @ cd $(DIST) && zip flowplayer.hlsjs.zip *.js LICENSE.md 29 | 30 | clean: 31 | @ rm -rf $(DIST) 32 | 33 | lint: 34 | @ npm run -s lint 35 | 36 | deps: 37 | @ npm install 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flowplayer hlsjs plugin 2 | =========================== 3 | 4 | **deprecated** 5 | 6 | The development of this plugins is discontinued. You should use the [`hlsjs-lite`](https://flowplayer.com/docs/player/setup#hlsjs-lite) plugin available in Flowplayer Core now. 7 | 8 | **deprecated** 9 | 10 | This plugin provides the `hlsjs` [engine](https://flowplayer.org/docs/api.html#engines) for 11 | playback of [HLS](https://flowplayer.org/docs/setup.html#hls) streams in browsers which do not 12 | support playback of HLS in a VIDEO tag, and without the need for 13 | [Flash](https://flowplayer.org/docs/setup.html#flash-hls). 14 | 15 | The plugin relies on the [hls.js](https://github.com/video-dev/hls.js) client library. 16 | 17 | Usage 18 | ----- 19 | 20 | See: https://flowplayer.org/docs/plugins.html#hlsjs 21 | 22 | - [compatibility](https://flowplayer.org/docs/plugins.html#hlsjs-compatibility) 23 | - [loading the assets](https://flowplayer.org/docs/plugins.html#hlsjs-assets) 24 | - [configuration](https://flowplayer.org/docs/plugins.html#hlsjs-configuration) 25 | - [hlsjs options](https://flowplayer.org/docs/plugins.html#hlsjs-options) 26 | - [hlsjs API](https://flowplayer.org/docs/plugins.html#hlsjs-api) 27 | 28 | ### Installation 29 | 30 | The plugin can be installed with: 31 | 32 | ``` 33 | npm install --save flowplayer/flowplayer-hlsjs 34 | ``` 35 | 36 | ### CommonJS 37 | 38 | The plugin can be used in a [browserify](http://browserify.org) and/or 39 | [webpack](https://webpack.github.io/) environment with a 40 | [commonjs](http://requirejs.org/docs/commonjs.html) loader: 41 | 42 | ```js 43 | var flowplayer = require('flowplayer'); 44 | var engine = require('flowplayer-hlsjs'); 45 | engine(flowplayer); 46 | 47 | flowplayer('#container', { 48 | clip: { 49 | sources: [{ 50 | type: 'application/x-mpegurl', 51 | src: '//stream.flowplayer.org/bauhaus.m3u8' 52 | }] 53 | } 54 | }); 55 | ``` 56 | 57 | Demo 58 | ---- 59 | 60 | A fully documented demo can be found [here](http://demos.flowplayer.org/api/hlsjs.html). 61 | 62 | Features 63 | -------- 64 | 65 | - packs a compatibility tested version - current: 66 | [v0.8.4](https://github.com/video-dev/hls.js/releases/tag/v0.8.4) - of hls.js 67 | - by default the engine is only loaded if the browser supports 68 | [MediaSource extensions](http://w3c.github.io/media-source/) reliably for playback 69 | - configurable manual HLS quality selection 70 | - manual audio track selection and optional audio ABR 71 | - display of HLS subtitles, Flowplayer style or native 72 | - ID3 metadata processing to string 73 | - optional light build - `flowplayer.hlsjs.light.min.js` - without multiple audio track, subtitles, 74 | and ID3 support 75 | 76 | Debugging 77 | --------- 78 | 79 | A quick way to find out whether there's a problem with the actual plugin component is to 80 | run your stream in the [hls.js demo player](http://streambox.fr/mse/hls.js-0.8.4/demo/). 81 | 82 | For fine grained debugging load the unminified components and turn hlsjs debugging on: 83 | 84 | ```html 85 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | ``` 98 | 99 | If you need to debug features only available from the 100 | [full plugin](https://flowplayer.com/docs/plugins.html#hlsjs-assets) you have to 101 | [build](https://github.com/flowplayer/flowplayer-hlsjs#building-the-plugin) the plugin. 102 | 103 | ### Building the plugin 104 | 105 | Build requirement: 106 | 107 | - [nodejs](https://nodejs.org) with [npm](https://www.npmjs.com) 108 | 109 | ```sh 110 | cd flowplayer-hlsjs 111 | make deps 112 | make 113 | ``` 114 | -------------------------------------------------------------------------------- /flowplayer.hlsjs.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, for: true, node: true */ 2 | /*eslint indent: ["error", 4], no-empty: ["error", { "allowEmptyCatch": true }] */ 3 | /*eslint-disable quotes, no-console */ 4 | /*global window */ 5 | 6 | /*! 7 | 8 | hlsjs engine plugin for Flowplayer HTML5 9 | 10 | Copyright (c) 2015-2017, Flowplayer Drive Oy 11 | 12 | Released under the MIT License: 13 | http://www.opensource.org/licenses/mit-license.php 14 | 15 | Includes hls.js 16 | Copyright (c) 2017 Dailymotion (http://www.dailymotion.com) 17 | https://github.com/video-dev/hls.js/blob/master/LICENSE 18 | 19 | Requires Flowplayer HTML5 version 6 or greater 20 | $GIT_DESC$ 21 | 22 | */ 23 | (function () { 24 | "use strict"; 25 | var extension = function (Hls, flowplayer) { 26 | var engineName = "hlsjs", 27 | hlsconf, 28 | common = flowplayer.common, 29 | extend = flowplayer.extend, 30 | support = flowplayer.support, 31 | brwsr = support.browser, 32 | version = flowplayer.version, 33 | coreV6 = version.indexOf("6.") === 0, 34 | win = window, 35 | mse = win.MediaSource || win.WebKitMediaSource, 36 | performance = win.performance, 37 | 38 | isHlsType = function (typ) { 39 | return typ.toLowerCase().indexOf("mpegurl") > -1; 40 | }, 41 | hlsQualitiesSupport = function (conf) { 42 | var hlsQualities = (conf.clip && conf.clip.hlsQualities) || conf.hlsQualities; 43 | 44 | return support.inlineVideo && 45 | (hlsQualities === true || 46 | (hlsQualities && hlsQualities.length)); 47 | }, 48 | destroyVideoTag = function (root) { 49 | var vtag = common.findDirect("video", root)[0] 50 | || common.find(".fp-player>video", root)[0]; 51 | 52 | if (vtag) { 53 | common.find("source", vtag).forEach(function (source) { 54 | source.removeAttribute("src"); 55 | }); 56 | vtag.removeAttribute("src"); 57 | vtag.load(); 58 | common.removeNode(vtag); 59 | } 60 | }, 61 | 62 | textencoding = require("text-encoding"), 63 | Decoder = new textencoding.TextDecoder("utf-8"), 64 | uint8ArrayToString = function (arr) { 65 | var txt = ""; 66 | 67 | try { 68 | txt = Decoder.decode(arr); 69 | } catch (ignore) { 70 | try { 71 | Decoder = new textencoding.TextDecoder("utf-16be"); 72 | txt = Decoder.decode(arr); 73 | } catch (ignore) { 74 | try { 75 | Decoder = new textencoding.TextDecoder("utf-16le"); 76 | txt = Decoder.decode(arr); 77 | } catch (ignore) {} 78 | } 79 | } 80 | return txt; 81 | }, 82 | 83 | loadHlsSubtitle = function (api, entry, num) { 84 | entry.title = entry.title || num + ""; 85 | 86 | var cue = { 87 | time: entry.startTime, 88 | subtitle: entry, 89 | visible: false 90 | }; 91 | 92 | api.subtitles.push(entry); 93 | api.addCuepoint(cue); 94 | api.addCuepoint({ 95 | time: entry.endTime, 96 | subtitleEnd: entry.title, 97 | visible: false 98 | }); 99 | // initial cuepoint 100 | if (entry.startTime === 0 && !api.video.time && !api.splash) { 101 | api.trigger("cuepoint", [api, cue]); 102 | } 103 | if (api.splash) { 104 | api.one("ready." + engineName, function () { 105 | api.trigger('cuepoint', [api, cue]); 106 | }); 107 | } 108 | }, 109 | 110 | engineImpl = function hlsjsEngine(player, root) { 111 | var bean = flowplayer.bean, 112 | videoTag, 113 | hls, 114 | 115 | recover, // DEPRECATED 116 | recoverMediaErrorDate, 117 | swapAudioCodecDate, 118 | recoveryClass = "is-seeking", 119 | posterClass = "is-poster", 120 | doRecover = function (conf, etype, isNetworkError) { 121 | if (conf.debug) { 122 | console.log("recovery." + engineName, "<-", etype); 123 | } 124 | common.removeClass(root, "is-paused"); 125 | common.addClass(root, recoveryClass); 126 | if (isNetworkError) { 127 | hls.startLoad(); 128 | } else { 129 | var now = performance.now(); 130 | if (!recoverMediaErrorDate || now - recoverMediaErrorDate > 3000) { 131 | recoverMediaErrorDate = performance.now(); 132 | hls.recoverMediaError(); 133 | } else if (!swapAudioCodecDate || (now - swapAudioCodecDate) > 3000) { 134 | swapAudioCodecDate = performance.now(); 135 | hls.swapAudioCodec(); 136 | hls.recoverMediaError(); 137 | } 138 | } 139 | // DEPRECATED 140 | if (recover > 0) { 141 | recover -= 1; 142 | } 143 | bean.one(videoTag, "seeked." + engineName, function () { 144 | if (videoTag.paused) { 145 | common.removeClass(root, posterClass); 146 | player.poster = false; 147 | videoTag.play(); 148 | } 149 | common.removeClass(root, recoveryClass); 150 | }); 151 | }, 152 | handleError = function (errorCode, src, url) { 153 | var errobj = {code: errorCode}; 154 | 155 | if (errorCode > 2) { 156 | errobj.video = extend(player.video, { 157 | src: src, 158 | url: url || src 159 | }); 160 | } 161 | return errobj; 162 | }, 163 | 164 | // pre 6.0.4 poster detection 165 | bc, 166 | has_bg, 167 | 168 | addPoster = function () { 169 | bean.one(videoTag, "timeupdate." + engineName, function () { 170 | common.addClass(root, posterClass); 171 | player.poster = true; 172 | }); 173 | }, 174 | removePoster = function () { 175 | if (coreV6 && player.poster) { 176 | bean.one(videoTag, "timeupdate." + engineName, function () { 177 | common.removeClass(root, posterClass); 178 | player.poster = false; 179 | }); 180 | } 181 | }, 182 | 183 | maxLevel = 0, 184 | 185 | audioGroups, 186 | audioUXGroup, 187 | audioAutoSwitch = function (level) { 188 | if (audioGroups && audioGroups.length > 1) { 189 | var audioTracks = hls.audioTracks, 190 | tracks = audioTracks.filter(function (atrack) { 191 | var attrs = hls.levels[level].attrs; 192 | 193 | return atrack.autoselect && attrs && 194 | atrack.groupId === attrs.AUDIO && 195 | atrack.name === audioTracks[hls.audioTrack].name; 196 | }), 197 | audioTrackId = tracks.length && tracks[0].id; 198 | 199 | if (audioTrackId !== undefined && audioTrackId !== hls.audioTrack) { 200 | hls.audioTrack = audioTrackId; 201 | } 202 | } 203 | }, 204 | selectAudioTrack = function (audioTrack) { 205 | common.find(".fp-audio", root)[0].innerHTML = audioTrack.lang || audioTrack.name; 206 | common.find(".fp-audio-menu a", root).forEach(function (el) { 207 | var adata = el.getAttribute("data-audio"), 208 | isSelected = adata === audioTrack.name; 209 | 210 | common.toggleClass(el, "fp-selected", isSelected); 211 | common.toggleClass(el, "fp-color", isSelected); 212 | }); 213 | }, 214 | removeAudioMenu = function () { 215 | common.find(".fp-audio-menu", root).forEach(common.removeNode); 216 | common.find(".fp-audio", root).forEach(common.removeNode); 217 | }, 218 | 219 | nativeSubs, 220 | hlsSubtitles, 221 | setActiveSubtitleClass = function (idx) { 222 | var menu = common.find(".fp-subtitle-menu", root)[0]; 223 | 224 | common.toggleClass(common.find('a.fp-selected', menu)[0], 'fp-selected'); 225 | common.toggleClass(common.find('a[data-subtitle-index="' + idx + '"]', menu)[0], 'fp-selected'); 226 | }, 227 | updateSubtitles = function (data, conf) { 228 | var entries = uint8ArrayToString(data.payload), 229 | id = data.frag.trackId; 230 | 231 | if (!entries) { 232 | return; 233 | } 234 | if (!hlsSubtitles[id]) { 235 | hlsSubtitles[id] = []; 236 | } 237 | entries = conf.subtitleParser(entries); 238 | entries.forEach(function (entry) { 239 | if (entry.text) { 240 | hlsSubtitles[id].push(entry); 241 | if (player.ready) { 242 | loadHlsSubtitle(player, entry, hlsSubtitles[id].length); 243 | if (player.live) { 244 | var seekOffset = player.video.seekOffset; 245 | 246 | hlsSubtitles[id] = hlsSubtitles[id].filter(function (sub) { 247 | return sub.endTime >= seekOffset; 248 | }); 249 | player.subtitles = player.subtitles.filter(function (sub) { 250 | return sub.endTime >= seekOffset; 251 | }); 252 | player.cuepoints.forEach(function (cue) { 253 | if (cue.subtitle && cue.time < seekOffset) { 254 | player.removeCuepoint(cue); 255 | } 256 | }); 257 | } 258 | } 259 | } 260 | }); 261 | }, 262 | disableSubtitleTracks = function () { 263 | [].forEach.call(videoTag.textTracks, function (track) { 264 | if (track.kind === "subtitles") { 265 | track.mode = "hidden"; 266 | } 267 | }); 268 | }, 269 | initSubtitles = function (data, conf) { 270 | var subtitleTracks = data.subtitleTracks; 271 | 272 | if (!conf.subtitles || !subtitleTracks.length || !support.inlineVideo || coreV6) { 273 | return; 274 | } 275 | // can there be more than 1 groupId? 276 | subtitleTracks = subtitleTracks.filter(function (subtitleTrack) { 277 | return subtitleTrack.groupId === subtitleTracks[0].groupId; 278 | }); 279 | player.video.subtitles = subtitleTracks.map(function (subtitleTrack) { 280 | // fake tracks 281 | var track = { 282 | kind: "subtitles", 283 | id: subtitleTrack.id, 284 | srclang: subtitleTrack.lang, 285 | label: subtitleTrack.name, 286 | "default": subtitleTrack.default 287 | }; 288 | common.append(videoTag, common.createElement("track", track)); 289 | return track; 290 | }); 291 | player.on("ready." + engineName, function (_e, api) { 292 | var tracks = hls.subtitleTracks, 293 | defaultTrack; 294 | 295 | if (!tracks || !tracks.length) { 296 | return; 297 | } 298 | if (nativeSubs) { 299 | common.addClass(videoTag, "native-subtitles"); 300 | } else { 301 | disableSubtitleTracks(); 302 | } 303 | tracks.map(function (sub, idx) { 304 | if (sub.default) { 305 | hls.subtitleTrack = idx; 306 | } 307 | }); 308 | defaultTrack = hls.subtitleTrack; 309 | if (defaultTrack > -1) { 310 | if (!nativeSubs && hlsSubtitles[defaultTrack]) { 311 | hlsSubtitles[defaultTrack].forEach(function (entry, i) { 312 | loadHlsSubtitle(api, entry, i + 1); 313 | }); 314 | } 315 | setActiveSubtitleClass(defaultTrack); 316 | } else { 317 | setActiveSubtitleClass(-1); 318 | } 319 | }); 320 | bean.on(root, "click." + engineName, ".fp-subtitle-menu [data-subtitle-index]", function (e) { 321 | e.preventDefault(); 322 | var idx = e.target.getAttribute("data-subtitle-index"); 323 | 324 | player.disableSubtitles(); 325 | hls.subtitleTrack = idx; 326 | if (idx < 0) { 327 | disableSubtitleTracks(); 328 | return; 329 | } 330 | setActiveSubtitleClass(idx); 331 | if (!nativeSubs && hlsSubtitles[idx]) { 332 | hlsSubtitles[idx].forEach(function (entry, i) { 333 | loadHlsSubtitle(player, entry, i + 1); 334 | }); 335 | } 336 | }); 337 | }, 338 | 339 | initAudio = function (data) { 340 | audioGroups = []; 341 | audioUXGroup = []; 342 | data.levels.forEach(function (level) { 343 | var agroup = level.attrs && level.attrs.AUDIO, 344 | acodec = level.audioCodec; 345 | 346 | if (agroup && audioGroups.indexOf(agroup) < 0 && 347 | (!acodec || mse.isTypeSupported("audio/mp4;codecs=" + acodec))) { 348 | audioGroups.push(agroup); 349 | } 350 | }); 351 | if (audioGroups.length) { 352 | // create sample group 353 | audioUXGroup = data.audioTracks.filter(function (audioTrack) { 354 | return audioTrack.groupId === audioGroups[0]; 355 | }); 356 | } 357 | if (!support.inlineVideo || coreV6 || audioUXGroup.length < 2) { 358 | return; 359 | } 360 | 361 | // audio menu 362 | bean.on(root, "click." + engineName, ".fp-audio", function () { 363 | var menu = common.find(".fp-audio-menu", root)[0]; 364 | 365 | if (common.hasClass(menu, "fp-active")) { 366 | player.hideMenu(); 367 | } else { 368 | player.showMenu(menu); 369 | } 370 | }); 371 | bean.on(root, "click." + engineName, ".fp-audio-menu a", function (e) { 372 | var adata = e.target.getAttribute("data-audio"), 373 | audioTracks = hls.audioTracks, 374 | gid = audioTracks[hls.audioTrack].groupId, 375 | // confine choice to current group 376 | atrack = audioTracks.filter(function (at) { 377 | return at.groupId === gid && (at.name === adata || at.lang === adata); 378 | })[0]; 379 | hls.audioTrack = atrack.id; 380 | selectAudioTrack(atrack); 381 | }); 382 | 383 | player.on("ready." + engineName, function () { 384 | removeAudioMenu(); 385 | if (!hls || !audioUXGroup || audioUXGroup.length < 2) { 386 | return; 387 | } 388 | 389 | var ui = common.find(".fp-ui", root)[0], 390 | controlbar = common.find(".fp-controls", ui)[0], 391 | currentAudioTrack = hls.audioTracks[hls.audioTrack], 392 | menu = common.createElement("div", { 393 | className: "fp-menu fp-audio-menu", 394 | css: {width: "auto"} 395 | }, "Audio"); 396 | 397 | audioUXGroup.forEach(function (audioTrack) { 398 | menu.appendChild(common.createElement("a", { 399 | "data-audio": audioTrack.name 400 | }, audioTrack.name)); 401 | }); 402 | ui.appendChild(menu); 403 | controlbar.appendChild(common.createElement("strong", { 404 | className: "fp-audio" 405 | }, currentAudioTrack)); 406 | 407 | selectAudioTrack(currentAudioTrack); 408 | }); 409 | }, 410 | 411 | // v6 qsel 412 | qActive = "active", 413 | dataQuality = function (quality) { 414 | // e.g. "Level 1" -> "level1" 415 | if (!quality) { 416 | quality = player.quality; 417 | } else if (player.qualities.indexOf(quality) < 0) { 418 | quality = "abr"; 419 | } 420 | return quality.toLowerCase().replace(/\ /g, ""); 421 | }, 422 | removeAllQualityClasses = function () { 423 | var qualities = player.qualities; 424 | 425 | if (qualities) { 426 | common.removeClass(root, "quality-abr"); 427 | qualities.forEach(function (quality) { 428 | common.removeClass(root, "quality-" + dataQuality(quality)); 429 | }); 430 | } 431 | }, 432 | qClean = function () { 433 | if (coreV6) { 434 | delete player.hlsQualities; 435 | removeAllQualityClasses(); 436 | common.find(".fp-quality-selector", root).forEach(common.removeNode); 437 | } 438 | }, 439 | qIndex = function () { 440 | return player.hlsQualities[player.qualities.indexOf(player.quality) + 1]; 441 | }, 442 | 443 | // v7 qsel 444 | lastSelectedLevel = -1, 445 | 446 | // v7 and v6 qsel 447 | initQualitySelection = function (hlsQualitiesConf, conf, data) { 448 | var levels = data.levels, 449 | hlsQualities, 450 | qualities, 451 | getLevel = function (q) { 452 | return isNaN(Number(q)) 453 | ? q.level 454 | : q; 455 | }, 456 | selector; 457 | 458 | qClean(); 459 | if (!hlsQualitiesConf || levels.length < 2) { 460 | return; 461 | } 462 | 463 | if (hlsQualitiesConf === "drive") { 464 | switch (levels.length) { 465 | case 4: 466 | hlsQualities = [1, 2, 3]; 467 | break; 468 | case 5: 469 | hlsQualities = [1, 2, 3, 4]; 470 | break; 471 | case 6: 472 | hlsQualities = [1, 3, 4, 5]; 473 | break; 474 | case 7: 475 | hlsQualities = [1, 3, 5, 6]; 476 | break; 477 | case 8: 478 | hlsQualities = [1, 3, 6, 7]; 479 | break; 480 | default: 481 | if (levels.length < 3 || 482 | (levels[0].height && levels[2].height && levels[0].height === levels[2].height)) { 483 | return; 484 | } 485 | hlsQualities = [1, 2]; 486 | } 487 | hlsQualities.unshift(-1); 488 | } else { 489 | switch (typeof hlsQualitiesConf) { 490 | case "object": 491 | hlsQualities = hlsQualitiesConf.map(getLevel); 492 | break; 493 | case "string": 494 | hlsQualities = hlsQualitiesConf.split(/\s*,\s*/).map(Number); 495 | break; 496 | default: 497 | hlsQualities = levels.map(function (_level, i) { 498 | return i; 499 | }); 500 | hlsQualities.unshift(-1); 501 | } 502 | } 503 | if (coreV6 && hlsQualities.indexOf(-1) < 0) { 504 | hlsQualities.unshift(-1); 505 | } 506 | 507 | hlsQualities = hlsQualities.filter(function (q) { 508 | if (q > -1 && q < levels.length) { 509 | var level = levels[q]; 510 | 511 | // do not check audioCodec, 512 | // as e.g. HE_AAC is decoded as LC_AAC by hls.js on Android 513 | return !level.videoCodec || 514 | (level.videoCodec && 515 | mse.isTypeSupported('video/mp4;codecs=' + level.videoCodec)); 516 | } else { 517 | return q === -1; 518 | } 519 | }); 520 | 521 | qualities = hlsQualities.map(function (idx, i) { 522 | var level = levels[idx], 523 | q = typeof hlsQualitiesConf === "object" 524 | ? hlsQualitiesConf.filter(function (q) { 525 | return getLevel(q) === idx; 526 | })[0] 527 | : idx, 528 | label = "Level " + (i + 1); 529 | 530 | if (idx < 0) { 531 | label = q.label || "Auto"; 532 | } else if (q.label) { 533 | label = q.label; 534 | } else { 535 | if (level.width && level.height) { 536 | label = Math.min(level.width, level.height) + "p"; 537 | } 538 | if (hlsQualitiesConf !== "drive" && level.bitrate) { 539 | label += " (" + Math.round(level.bitrate / 1000) + "k)"; 540 | } 541 | } 542 | if (coreV6) { 543 | return label; 544 | } 545 | return {value: idx, label: label}; 546 | }); 547 | 548 | if (!coreV6) { 549 | player.video.qualities = qualities; 550 | if (lastSelectedLevel > -1 || hlsQualities.indexOf(-1) < 0) { 551 | hls.loadLevel = hlsQualities.indexOf(lastSelectedLevel) < 0 552 | ? hlsQualities[0] 553 | : lastSelectedLevel; 554 | hls.config.startLevel = hls.loadLevel; 555 | player.video.quality = hls.loadLevel; 556 | } else { 557 | player.video.quality = -1; 558 | } 559 | lastSelectedLevel = player.video.quality; 560 | return; 561 | } 562 | 563 | // v6 564 | player.hlsQualities = hlsQualities; 565 | player.qualities = qualities.slice(1); 566 | 567 | selector = common.createElement("ul", { 568 | "class": "fp-quality-selector" 569 | }); 570 | common.find(".fp-ui", root)[0].appendChild(selector); 571 | 572 | if (!player.quality || qualities.indexOf(player.quality) < 1) { 573 | player.quality = "abr"; 574 | } else { 575 | hls.loadLevel = qIndex(); 576 | hls.config.startLevel = hls.loadLevel; 577 | } 578 | 579 | qualities.forEach(function (q) { 580 | selector.appendChild(common.createElement("li", { 581 | "data-quality": dataQuality(q) 582 | }, q)); 583 | }); 584 | 585 | common.addClass(root, "quality-" + dataQuality()); 586 | 587 | bean.on(root, "click." + engineName, ".fp-quality-selector li", function (e) { 588 | var choice = e.currentTarget, 589 | items = common.find(".fp-quality-selector li", root), 590 | smooth = conf.smoothSwitching, 591 | paused = videoTag.paused; 592 | 593 | if (common.hasClass(choice, qActive)) { 594 | return; 595 | } 596 | 597 | if (!paused && !smooth) { 598 | bean.one(videoTag, "pause." + engineName, function () { 599 | common.removeClass(root, "is-paused"); 600 | }); 601 | } 602 | 603 | items.forEach(function (item, i) { 604 | var active = item === choice; 605 | 606 | if (active) { 607 | player.quality = i > 0 608 | ? player.qualities[i - 1] 609 | : "abr"; 610 | if (smooth && !player.poster) { 611 | hls.nextLevel = qIndex(); 612 | } else { 613 | hls.currentLevel = qIndex(); 614 | } 615 | common.addClass(choice, qActive); 616 | if (paused) { 617 | videoTag.play(); 618 | } 619 | } 620 | common.toggleClass(item, qActive, active); 621 | }); 622 | removeAllQualityClasses(); 623 | common.addClass(root, "quality-" + dataQuality()); 624 | }); 625 | }, 626 | 627 | engine = { 628 | engineName: engineName, 629 | 630 | pick: function (sources) { 631 | var source = sources.filter(function (s) { 632 | return isHlsType(s.type); 633 | })[0]; 634 | 635 | if (typeof source.src === "string") { 636 | source.src = common.createAbsoluteUrl(source.src); 637 | } 638 | return source; 639 | }, 640 | 641 | load: function (video) { 642 | var conf = player.conf, 643 | EVENTS = { 644 | ended: "finish", 645 | loadeddata: "ready", 646 | pause: "pause", 647 | play: "resume", 648 | progress: "buffer", 649 | ratechange: "speed", 650 | seeked: "seek", 651 | timeupdate: "progress", 652 | volumechange: "volume", 653 | error: "error" 654 | }, 655 | HLSEVENTS = Hls.Events, 656 | autoplay = !!video.autoplay || !!conf.autoplay || !!conf.splash, 657 | hlsQualitiesConf = video.hlsQualities || conf.hlsQualities, 658 | hlsUpdatedConf = extend(hlsconf, conf.hlsjs, video.hlsjs), 659 | hlsClientConf = extend({}, hlsUpdatedConf); 660 | 661 | // allow disabling level selection for single clips 662 | if (video.hlsQualities === false) { 663 | hlsQualitiesConf = false; 664 | } 665 | nativeSubs = hlsUpdatedConf.subtitles && 666 | support.subtitles && conf.nativesubtitles; 667 | 668 | if (!hls) { 669 | destroyVideoTag(root); 670 | videoTag = common.createElement("video", { 671 | "class": "fp-engine " + engineName + "-engine", 672 | "autoplay": autoplay 673 | ? "autoplay" 674 | : false, 675 | "volume": player.volumeLevel 676 | }); 677 | if (support.mutedAutoplay && !conf.splash && autoplay) { 678 | videoTag.muted = true; 679 | } 680 | 681 | Object.keys(EVENTS).forEach(function (key) { 682 | var flow = EVENTS[key], 683 | type = key + "." + engineName, 684 | arg; 685 | 686 | bean.on(videoTag, type, function (e) { 687 | if (conf.debug && flow.indexOf("progress") < 0) { 688 | console.log(type, "->", flow, e.originalEvent); 689 | } 690 | 691 | var ct = videoTag.currentTime, 692 | seekable = videoTag.seekable, 693 | updatedVideo = player.video, 694 | liveResumePosition = player.dvr 695 | ? updatedVideo.seekOffset 696 | : player.live 697 | ? hls.liveSyncPosition 698 | : 0, 699 | buffered = videoTag.buffered, 700 | i, 701 | buffends = [], 702 | src = updatedVideo.src, 703 | quality = player.quality, 704 | selectorIndex, 705 | errorCode; 706 | 707 | switch (flow) { 708 | case "ready": 709 | arg = extend(updatedVideo, { 710 | duration: videoTag.duration, 711 | seekable: seekable.length && seekable.end(null), 712 | width: videoTag.videoWidth, 713 | height: videoTag.videoHeight, 714 | url: src 715 | }); 716 | break; 717 | case "resume": 718 | removePoster(); 719 | if (!hlsUpdatedConf.bufferWhilePaused) { 720 | hls.startLoad(ct); 721 | } 722 | if (ct < liveResumePosition) { 723 | videoTag.currentTime = liveResumePosition; 724 | } 725 | break; 726 | case "seek": 727 | removePoster(); 728 | if (!hlsUpdatedConf.bufferWhilePaused && videoTag.paused) { 729 | hls.stopLoad(); 730 | } 731 | arg = ct; 732 | break; 733 | case "pause": 734 | if (!hlsUpdatedConf.bufferWhilePaused) { 735 | hls.stopLoad(); 736 | } 737 | break; 738 | case "progress": 739 | arg = ct; 740 | break; 741 | case "speed": 742 | arg = videoTag.playbackRate; 743 | break; 744 | case "volume": 745 | arg = videoTag.volume; 746 | break; 747 | case "buffer": 748 | for (i = 0; i < buffered.length; i += 1) { 749 | buffends.push(buffered.end(i)); 750 | } 751 | arg = buffends.filter(function (b) { 752 | return b >= ct; 753 | }).sort()[0]; 754 | updatedVideo.buffer = arg; 755 | break; 756 | case "finish": 757 | if (hlsUpdatedConf.bufferWhilePaused && hls.autoLevelEnabled && 758 | (updatedVideo.loop || conf.playlist.length < 2 || conf.advance === false)) { 759 | hls.nextLoadLevel = maxLevel; 760 | } 761 | break; 762 | case "error": 763 | errorCode = videoTag.error && videoTag.error.code; 764 | 765 | if ((hlsUpdatedConf.recoverMediaError && (errorCode === 3 || !errorCode)) || 766 | (hlsUpdatedConf.recoverNetworkError && errorCode === 2) || 767 | (hlsUpdatedConf.recover && (errorCode === 2 || errorCode === 3))) { 768 | e.preventDefault(); 769 | doRecover(conf, flow, errorCode === 2); 770 | return; 771 | } 772 | 773 | arg = handleError(errorCode, src); 774 | break; 775 | } 776 | 777 | player.trigger(flow, [player, arg]); 778 | 779 | if (coreV6) { 780 | if (flow === "ready" && quality) { 781 | selectorIndex = quality === "abr" 782 | ? 0 783 | : player.qualities.indexOf(quality) + 1; 784 | common.addClass(common.find(".fp-quality-selector li", root)[selectorIndex], 785 | qActive); 786 | } 787 | } 788 | }); 789 | }); 790 | 791 | player.on("error." + engineName, function () { 792 | if (hls) { 793 | player.engine.unload(); 794 | } 795 | 796 | }).on("beforeseek." + engineName, function (e, api, pos) { 797 | if (pos === undefined) { 798 | e.preventDefault(); 799 | } else if (!hlsUpdatedConf.bufferWhilePaused && api.paused) { 800 | hls.startLoad(pos); 801 | } 802 | }); 803 | 804 | if (!coreV6) { 805 | player.on("quality." + engineName, function (_e, _api, q) { 806 | if (hlsUpdatedConf.smoothSwitching) { 807 | hls.nextLevel = q; 808 | } else { 809 | hls.currentLevel = q; 810 | } 811 | lastSelectedLevel = q; 812 | }); 813 | 814 | } else if (conf.poster) { 815 | // v6 only 816 | // engine too late, poster already removed 817 | // abuse timeupdate to re-instate poster 818 | player.on("stop." + engineName, addPoster); 819 | // re-instate initial poster for live streams 820 | if (player.live && !autoplay && !player.video.autoplay) { 821 | bean.one(videoTag, "seeked." + engineName, addPoster); 822 | } 823 | } 824 | 825 | common.prepend(common.find(".fp-player", root)[0], videoTag); 826 | 827 | } else { 828 | hls.destroy(); 829 | common.find("track", videoTag).forEach(common.removeNode); 830 | common.removeClass(videoTag, "native-subtitles"); 831 | if ((player.video.src && video.src !== player.video.src) || video.index) { 832 | common.attr(videoTag, "autoplay", "autoplay"); 833 | } 834 | } 835 | 836 | // #28 obtain api.video props before ready 837 | player.video = video; 838 | 839 | // reset 840 | maxLevel = 0; 841 | 842 | Object.keys(hlsUpdatedConf).forEach(function (key) { 843 | if (!Hls.DefaultConfig.hasOwnProperty(key)) { 844 | delete hlsClientConf[key]; 845 | } 846 | 847 | var value = hlsUpdatedConf[key]; 848 | 849 | switch (key) { 850 | case "adaptOnStartOnly": 851 | if (value) { 852 | hlsClientConf.startLevel = -1; 853 | } 854 | break; 855 | case "autoLevelCapping": 856 | if (value === false) { 857 | value = -1; 858 | } 859 | hlsClientConf[key] = value; 860 | break; 861 | case "startLevel": 862 | switch (value) { 863 | case "auto": 864 | value = -1; 865 | break; 866 | case "firstLevel": 867 | value = undefined; 868 | break; 869 | } 870 | hlsClientConf[key] = value; 871 | break; 872 | case "recover": // DEPRECATED 873 | hlsUpdatedConf.recoverMediaError = false; 874 | hlsUpdatedConf.recoverNetworkError = false; 875 | recover = value; 876 | break; 877 | case "strict": 878 | if (value) { 879 | hlsUpdatedConf.recoverMediaError = false; 880 | hlsUpdatedConf.recoverNetworkError = false; 881 | recover = 0; 882 | } 883 | break; 884 | 885 | } 886 | }); 887 | 888 | hls = new Hls(hlsClientConf); 889 | player.engine[engineName] = hls; 890 | recoverMediaErrorDate = null; 891 | swapAudioCodecDate = null; 892 | player.disableSubtitles(); 893 | hlsSubtitles = {}; 894 | 895 | Object.keys(HLSEVENTS).forEach(function (key) { 896 | var etype = HLSEVENTS[key], 897 | listeners = hlsUpdatedConf.listeners, 898 | expose = listeners && listeners.indexOf(etype) > -1; 899 | 900 | hls.on(etype, function (e, data) { 901 | var fperr, 902 | errobj = {}, 903 | ERRORTYPES = Hls.ErrorTypes, 904 | ERRORDETAILS = Hls.ErrorDetails, 905 | updatedVideo = player.video, 906 | src = updatedVideo.src; 907 | 908 | switch (key) { 909 | case "MANIFEST_PARSED": 910 | if (hlsQualitiesSupport(conf) && 911 | !(!coreV6 && player.pluginQualitySelectorEnabled)) { 912 | initQualitySelection(hlsQualitiesConf, hlsUpdatedConf, data); 913 | } else if (coreV6) { 914 | delete player.quality; 915 | } 916 | break; 917 | case "MANIFEST_LOADED": 918 | initAudio(data); 919 | break; 920 | case "SUBTITLE_TRACKS_UPDATED": 921 | initSubtitles(data, hlsUpdatedConf); 922 | break; 923 | case "MEDIA_ATTACHED": 924 | hls.loadSource(src); 925 | break; 926 | case "FRAG_LOADED": 927 | if (data.frag.type === "subtitle" && hlsUpdatedConf.subtitles && !nativeSubs) { 928 | updateSubtitles(data, conf); 929 | } 930 | if (hlsUpdatedConf.bufferWhilePaused && !player.live && 931 | hls.autoLevelEnabled && hls.nextLoadLevel > maxLevel) { 932 | maxLevel = hls.nextLoadLevel; 933 | } 934 | break; 935 | case "SUBTITLE_TRACK_SWITCH": 936 | if (nativeSubs) { 937 | [].forEach.call(videoTag.textTracks, function (track) { 938 | track.mode = (hls.subtitleTracks[data.id].lang === track.language && 939 | track.kind === "subtitles") 940 | ? "showing" 941 | : "hidden"; 942 | }); 943 | } 944 | break; 945 | case "FRAG_PARSING_METADATA": 946 | if (coreV6) { 947 | return; 948 | } 949 | data.samples.forEach(function (sample) { 950 | var metadataHandler; 951 | 952 | metadataHandler = function () { 953 | if (videoTag.currentTime < sample.dts) { 954 | return; 955 | } 956 | bean.off(videoTag, 'timeupdate.' + engineName, metadataHandler); 957 | 958 | var txt = uint8ArrayToString(sample.unit || sample.data); 959 | 960 | player.trigger('metadata', [player, { 961 | key: txt.substr(10, 4), 962 | data: txt 963 | }]); 964 | }; 965 | bean.on(videoTag, 'timeupdate.' + engineName, metadataHandler); 966 | }); 967 | break; 968 | case "LEVEL_UPDATED": 969 | if (player.live) { 970 | extend(updatedVideo, { 971 | seekOffset: data.details.fragments[0].start + hls.config.nudgeOffset, 972 | duration: hls.liveSyncPosition 973 | }); 974 | if (player.dvr && player.playing) { 975 | player.trigger('dvrwindow', [player, { 976 | start: updatedVideo.seekOffset, 977 | end: hls.liveSyncPosition 978 | }]); 979 | } 980 | } 981 | break; 982 | case "LEVEL_SWITCHED": 983 | if (hlsUpdatedConf.audioABR) { 984 | player.one("buffer." + engineName, function (_e, api, buffer) { 985 | if (buffer > api.video.time) { 986 | audioAutoSwitch(data.level); 987 | } 988 | }); 989 | } 990 | break; 991 | case "BUFFER_APPENDED": 992 | common.removeClass(root, recoveryClass); 993 | break; 994 | case "ERROR": 995 | if (data.fatal || hlsUpdatedConf.strict) { 996 | switch (data.type) { 997 | case ERRORTYPES.NETWORK_ERROR: 998 | if (hlsUpdatedConf.recoverNetworkError || recover) { 999 | doRecover(conf, data.type, true); 1000 | } else if (data.frag && data.frag.url) { 1001 | errobj.url = data.frag.url; 1002 | fperr = 2; 1003 | } else { 1004 | fperr = 4; 1005 | } 1006 | break; 1007 | case ERRORTYPES.MEDIA_ERROR: 1008 | if (hlsUpdatedConf.recoverMediaError || recover) { 1009 | doRecover(conf, data.type); 1010 | } else { 1011 | fperr = 3; 1012 | } 1013 | break; 1014 | default: 1015 | fperr = 5; 1016 | } 1017 | 1018 | if (fperr !== undefined) { 1019 | errobj = handleError(fperr, src, data.url); 1020 | player.trigger("error", [player, errobj]); 1021 | } 1022 | } else if (data.details === ERRORDETAILS.FRAG_LOOP_LOADING_ERROR || 1023 | data.details === ERRORDETAILS.BUFFER_STALLED_ERROR) { 1024 | common.addClass(root, recoveryClass); 1025 | } 1026 | break; 1027 | } 1028 | 1029 | // memory leak if all these are re-triggered by api #29 1030 | if (expose) { 1031 | player.trigger(e, [player, data]); 1032 | } 1033 | }); 1034 | }); 1035 | 1036 | if (hlsUpdatedConf.adaptOnStartOnly) { 1037 | bean.one(videoTag, "timeupdate." + engineName, function () { 1038 | hls.loadLevel = hls.loadLevel; 1039 | }); 1040 | } 1041 | 1042 | hls.attachMedia(videoTag); 1043 | 1044 | if (autoplay && videoTag.paused) { 1045 | var playPromise = videoTag.play(); 1046 | if (playPromise !== undefined) { 1047 | playPromise.catch(function () { 1048 | if (!support.mutedAutoplay) { 1049 | player.unload(); 1050 | if (!coreV6) { 1051 | player.message("Please click the play button", 3000); 1052 | } 1053 | } 1054 | }); 1055 | } 1056 | } 1057 | }, 1058 | 1059 | resume: function () { 1060 | videoTag.play(); 1061 | }, 1062 | 1063 | pause: function () { 1064 | videoTag.pause(); 1065 | }, 1066 | 1067 | seek: function (time) { 1068 | if (videoTag) { 1069 | videoTag.currentTime = time; 1070 | } 1071 | }, 1072 | 1073 | volume: function (level) { 1074 | if (videoTag) { 1075 | videoTag.volume = level; 1076 | } 1077 | }, 1078 | 1079 | speed: function (val) { 1080 | videoTag.playbackRate = val; 1081 | player.trigger('speed', [player, val]); 1082 | }, 1083 | 1084 | unload: function () { 1085 | if (hls) { 1086 | var listeners = "." + engineName; 1087 | 1088 | player.disableSubtitles(); 1089 | hls.destroy(); 1090 | hls = 0; 1091 | qClean(); 1092 | removeAudioMenu(); 1093 | player.off(listeners); 1094 | bean.off(root, listeners); 1095 | bean.off(videoTag, listeners); 1096 | common.removeNode(videoTag); 1097 | videoTag = 0; 1098 | } 1099 | } 1100 | }; 1101 | 1102 | // pre 6.0.4: no boolean api.conf.poster and no poster with autoplay 1103 | if (/^6\.0\.[0-3]$/.test(version) && 1104 | !player.conf.splash && !player.conf.poster && !player.conf.autoplay) { 1105 | bc = common.css(root, 'backgroundColor'); 1106 | // spaces in rgba arg mandatory for recognition 1107 | has_bg = common.css(root, 'backgroundImage') !== "none" || 1108 | (bc && bc !== "rgba(0, 0, 0, 0)" && bc !== "transparent"); 1109 | if (has_bg) { 1110 | player.conf.poster = true; 1111 | } 1112 | } 1113 | 1114 | return engine; 1115 | }; 1116 | 1117 | if (Hls.isSupported() && version.indexOf("5.") !== 0) { 1118 | // only load engine if it can be used 1119 | engineImpl.engineName = engineName; // must be exposed 1120 | engineImpl[engineName + "ClientVersion"] = Hls.version; 1121 | engineImpl.canPlay = function (type, conf) { 1122 | if (conf[engineName] === false || conf.clip[engineName] === false) { 1123 | // engine disabled for player 1124 | return false; 1125 | } 1126 | 1127 | // merge hlsjs clip config at earliest opportunity 1128 | hlsconf = extend({ 1129 | bufferWhilePaused: true, 1130 | smoothSwitching: true, 1131 | recoverMediaError: true 1132 | }, conf[engineName], conf.clip[engineName]); 1133 | 1134 | // https://github.com/dailymotion/hls.js/issues/9 1135 | return isHlsType(type) && (!(brwsr.safari && support.dataload) || hlsconf.safari); 1136 | }; 1137 | 1138 | flowplayer(function (api, root) { 1139 | var c = api.conf; 1140 | 1141 | if (coreV6) { 1142 | // to take precedence over VOD quality selector 1143 | api.pluginQualitySelectorEnabled = hlsQualitiesSupport(c) && 1144 | engineImpl.canPlay("application/x-mpegurl", c); 1145 | 1146 | } else if (support.mutedAutoplay && !c.splash && !c.autoplay && 1147 | (version === "7.1.0" || version === "7.0.0")) { 1148 | // issue #94 1149 | api.splash = true; 1150 | c.splash = typeof c.poster === "string" 1151 | ? c.poster 1152 | : true; 1153 | c.poster = undefined; 1154 | c.autoplay = true; 1155 | destroyVideoTag(root); 1156 | } 1157 | }); 1158 | 1159 | // put on top of engine stack 1160 | // so hlsjs is tested before html5 video hls and flash hls 1161 | flowplayer.engines.unshift(engineImpl); 1162 | } 1163 | 1164 | }; 1165 | if (typeof module === 'object' && module.exports) { 1166 | module.exports = extension.bind(undefined, require('hls.js')); 1167 | } else if (window.Hls && window.flowplayer) { 1168 | extension(window.Hls, window.flowplayer); 1169 | } 1170 | }()); 1171 | -------------------------------------------------------------------------------- /flowplayer.hlsjs.light.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, for: true, node: true */ 2 | /*eslint indent: ["error", 4], no-empty: ["error", { "allowEmptyCatch": true }] */ 3 | /*eslint-disable quotes, no-console */ 4 | /*global window */ 5 | 6 | /*! 7 | 8 | hlsjs engine plugin (light) for Flowplayer HTML5 9 | 10 | Copyright (c) 2015-2017, Flowplayer Drive Oy 11 | 12 | Released under the MIT License: 13 | http://www.opensource.org/licenses/mit-license.php 14 | 15 | Includes hls.light.js 16 | Copyright (c) 2017 Dailymotion (http://www.dailymotion.com) 17 | https://github.com/video-dev/hls.js/blob/master/LICENSE 18 | 19 | Requires Flowplayer HTML5 version 7 or greater 20 | $GIT_DESC$ 21 | 22 | */ 23 | (function () { 24 | "use strict"; 25 | var extension = function (Hls, flowplayer) { 26 | var engineName = "hlsjs", 27 | hlsconf, 28 | common = flowplayer.common, 29 | extend = flowplayer.extend, 30 | support = flowplayer.support, 31 | brwsr = support.browser, 32 | version = flowplayer.version, 33 | win = window, 34 | mse = win.MediaSource || win.WebKitMediaSource, 35 | performance = win.performance, 36 | 37 | isHlsType = function (typ) { 38 | return typ.toLowerCase().indexOf("mpegurl") > -1; 39 | }, 40 | hlsQualitiesSupport = function (conf) { 41 | var hlsQualities = (conf.clip && conf.clip.hlsQualities) || conf.hlsQualities; 42 | 43 | return support.inlineVideo && 44 | (hlsQualities === true || 45 | (hlsQualities && hlsQualities.length)); 46 | }, 47 | destroyVideoTag = function (root) { 48 | var vtag = common.findDirect("video", root)[0] 49 | || common.find(".fp-player>video", root)[0]; 50 | 51 | if (vtag) { 52 | common.find("source", vtag).forEach(function (source) { 53 | source.removeAttribute("src"); 54 | }); 55 | vtag.removeAttribute("src"); 56 | vtag.load(); 57 | common.removeNode(vtag); 58 | } 59 | }, 60 | 61 | engineImpl = function hlsjsEngine(player, root) { 62 | var bean = flowplayer.bean, 63 | videoTag, 64 | hls, 65 | 66 | recover, // DEPRECATED 67 | recoverMediaErrorDate, 68 | swapAudioCodecDate, 69 | recoveryClass = "is-seeking", 70 | posterClass = "is-poster", 71 | doRecover = function (conf, etype, isNetworkError) { 72 | if (conf.debug) { 73 | console.log("recovery." + engineName, "<-", etype); 74 | } 75 | common.removeClass(root, "is-paused"); 76 | common.addClass(root, recoveryClass); 77 | if (isNetworkError) { 78 | hls.startLoad(); 79 | } else { 80 | var now = performance.now(); 81 | if (!recoverMediaErrorDate || now - recoverMediaErrorDate > 3000) { 82 | recoverMediaErrorDate = performance.now(); 83 | hls.recoverMediaError(); 84 | } else if (!swapAudioCodecDate || (now - swapAudioCodecDate) > 3000) { 85 | swapAudioCodecDate = performance.now(); 86 | hls.swapAudioCodec(); 87 | hls.recoverMediaError(); 88 | } 89 | } 90 | // DEPRECATED 91 | if (recover > 0) { 92 | recover -= 1; 93 | } 94 | bean.one(videoTag, "seeked." + engineName, function () { 95 | if (videoTag.paused) { 96 | common.removeClass(root, posterClass); 97 | player.poster = false; 98 | videoTag.play(); 99 | } 100 | common.removeClass(root, recoveryClass); 101 | }); 102 | }, 103 | handleError = function (errorCode, src, url) { 104 | var errobj = {code: errorCode}; 105 | 106 | if (errorCode > 2) { 107 | errobj.video = extend(player.video, { 108 | src: src, 109 | url: url || src 110 | }); 111 | } 112 | return errobj; 113 | }, 114 | 115 | maxLevel = 0, 116 | lastSelectedLevel = -1, 117 | initQualitySelection = function (hlsQualitiesConf, data) { 118 | var levels = data.levels, 119 | hlsQualities, 120 | getLevel = function (q) { 121 | return isNaN(Number(q)) 122 | ? q.level 123 | : q; 124 | }; 125 | 126 | if (!hlsQualitiesConf || levels.length < 2) { 127 | return; 128 | } 129 | 130 | if (hlsQualitiesConf === "drive") { 131 | switch (levels.length) { 132 | case 4: 133 | hlsQualities = [1, 2, 3]; 134 | break; 135 | case 5: 136 | hlsQualities = [1, 2, 3, 4]; 137 | break; 138 | case 6: 139 | hlsQualities = [1, 3, 4, 5]; 140 | break; 141 | case 7: 142 | hlsQualities = [1, 3, 5, 6]; 143 | break; 144 | case 8: 145 | hlsQualities = [1, 3, 6, 7]; 146 | break; 147 | default: 148 | if (levels.length < 3 || 149 | (levels[0].height && levels[2].height && levels[0].height === levels[2].height)) { 150 | return; 151 | } 152 | hlsQualities = [1, 2]; 153 | } 154 | hlsQualities.unshift(-1); 155 | } else { 156 | switch (typeof hlsQualitiesConf) { 157 | case "object": 158 | hlsQualities = hlsQualitiesConf.map(getLevel); 159 | break; 160 | case "string": 161 | hlsQualities = hlsQualitiesConf.split(/\s*,\s*/).map(Number); 162 | break; 163 | default: 164 | hlsQualities = levels.map(function (_level, i) { 165 | return i; 166 | }); 167 | hlsQualities.unshift(-1); 168 | } 169 | } 170 | 171 | hlsQualities = hlsQualities.filter(function (q) { 172 | if (q > -1 && q < levels.length) { 173 | var level = levels[q]; 174 | 175 | // do not check audioCodec, 176 | // as e.g. HE_AAC is decoded as LC_AAC by hls.js on Android 177 | return !level.videoCodec || 178 | (level.videoCodec && 179 | mse.isTypeSupported('video/mp4;codecs=' + level.videoCodec)); 180 | } else { 181 | return q === -1; 182 | } 183 | }); 184 | 185 | player.video.qualities = hlsQualities.map(function (idx, i) { 186 | var level = levels[idx], 187 | q = typeof hlsQualitiesConf === "object" 188 | ? hlsQualitiesConf.filter(function (q) { 189 | return getLevel(q) === idx; 190 | })[0] 191 | : idx, 192 | label = "Level " + (i + 1); 193 | 194 | if (idx < 0) { 195 | label = q.label || "Auto"; 196 | } else if (q.label) { 197 | label = q.label; 198 | } else { 199 | if (level.width && level.height) { 200 | label = Math.min(level.width, level.height) + "p"; 201 | } 202 | if (hlsQualitiesConf !== "drive" && level.bitrate) { 203 | label += " (" + Math.round(level.bitrate / 1000) + "k)"; 204 | } 205 | } 206 | return {value: idx, label: label}; 207 | }); 208 | 209 | if (lastSelectedLevel > -1 || hlsQualities.indexOf(-1) < 0) { 210 | hls.loadLevel = hlsQualities.indexOf(lastSelectedLevel) < 0 211 | ? hlsQualities[0] 212 | : lastSelectedLevel; 213 | hls.config.startLevel = hls.loadLevel; 214 | player.video.quality = hls.loadLevel; 215 | } else { 216 | player.video.quality = -1; 217 | } 218 | lastSelectedLevel = player.video.quality; 219 | }, 220 | 221 | engine = { 222 | engineName: engineName, 223 | 224 | pick: function (sources) { 225 | var source = sources.filter(function (s) { 226 | return isHlsType(s.type); 227 | })[0]; 228 | 229 | if (typeof source.src === "string") { 230 | source.src = common.createAbsoluteUrl(source.src); 231 | } 232 | return source; 233 | }, 234 | 235 | load: function (video) { 236 | var conf = player.conf, 237 | EVENTS = { 238 | ended: "finish", 239 | loadeddata: "ready", 240 | pause: "pause", 241 | play: "resume", 242 | progress: "buffer", 243 | ratechange: "speed", 244 | seeked: "seek", 245 | timeupdate: "progress", 246 | volumechange: "volume", 247 | error: "error" 248 | }, 249 | HLSEVENTS = Hls.Events, 250 | autoplay = !!video.autoplay || !!conf.autoplay || !!conf.splash, 251 | hlsQualitiesConf = video.hlsQualities || conf.hlsQualities, 252 | hlsUpdatedConf = extend(hlsconf, conf.hlsjs, video.hlsjs), 253 | hlsClientConf = extend({}, hlsUpdatedConf); 254 | 255 | // allow disabling level selection for single clips 256 | if (video.hlsQualities === false) { 257 | hlsQualitiesConf = false; 258 | } 259 | 260 | if (!hls) { 261 | destroyVideoTag(root); 262 | videoTag = common.createElement("video", { 263 | "class": "fp-engine " + engineName + "-engine", 264 | "autoplay": autoplay 265 | ? "autoplay" 266 | : false, 267 | "volume": player.volumeLevel 268 | }); 269 | if (support.mutedAutoplay && !conf.splash && autoplay) { 270 | videoTag.muted = true; 271 | } 272 | 273 | Object.keys(EVENTS).forEach(function (key) { 274 | var flow = EVENTS[key], 275 | type = key + "." + engineName, 276 | arg; 277 | 278 | bean.on(videoTag, type, function (e) { 279 | if (conf.debug && flow.indexOf("progress") < 0) { 280 | console.log(type, "->", flow, e.originalEvent); 281 | } 282 | 283 | var ct = videoTag.currentTime, 284 | seekable = videoTag.seekable, 285 | updatedVideo = player.video, 286 | liveResumePosition = player.dvr 287 | ? updatedVideo.seekOffset 288 | : player.live 289 | ? hls.liveSyncPosition 290 | : 0, 291 | buffered = videoTag.buffered, 292 | i, 293 | buffends = [], 294 | src = updatedVideo.src, 295 | errorCode; 296 | 297 | switch (flow) { 298 | case "ready": 299 | arg = extend(updatedVideo, { 300 | duration: videoTag.duration, 301 | seekable: seekable.length && seekable.end(null), 302 | width: videoTag.videoWidth, 303 | height: videoTag.videoHeight, 304 | url: src 305 | }); 306 | break; 307 | case "resume": 308 | if (!hlsUpdatedConf.bufferWhilePaused) { 309 | hls.startLoad(ct); 310 | } 311 | if (ct < liveResumePosition) { 312 | videoTag.currentTime = liveResumePosition; 313 | } 314 | break; 315 | case "seek": 316 | if (!hlsUpdatedConf.bufferWhilePaused && videoTag.paused) { 317 | hls.stopLoad(); 318 | } 319 | arg = ct; 320 | break; 321 | case "pause": 322 | if (!hlsUpdatedConf.bufferWhilePaused) { 323 | hls.stopLoad(); 324 | } 325 | break; 326 | case "progress": 327 | arg = ct; 328 | break; 329 | case "speed": 330 | arg = videoTag.playbackRate; 331 | break; 332 | case "volume": 333 | arg = videoTag.volume; 334 | break; 335 | case "buffer": 336 | for (i = 0; i < buffered.length; i += 1) { 337 | buffends.push(buffered.end(i)); 338 | } 339 | arg = buffends.filter(function (b) { 340 | return b >= ct; 341 | }).sort()[0]; 342 | updatedVideo.buffer = arg; 343 | break; 344 | case "finish": 345 | if (hlsUpdatedConf.bufferWhilePaused && hls.autoLevelEnabled && 346 | (updatedVideo.loop || conf.playlist.length < 2 || conf.advance === false)) { 347 | hls.nextLoadLevel = maxLevel; 348 | } 349 | break; 350 | case "error": 351 | errorCode = videoTag.error && videoTag.error.code; 352 | 353 | if ((hlsUpdatedConf.recoverMediaError && (errorCode === 3 || !errorCode)) || 354 | (hlsUpdatedConf.recoverNetworkError && errorCode === 2) || 355 | (hlsUpdatedConf.recover && (errorCode === 2 || errorCode === 3))) { 356 | e.preventDefault(); 357 | doRecover(conf, flow, errorCode === 2); 358 | return; 359 | } 360 | 361 | arg = handleError(errorCode, src); 362 | break; 363 | } 364 | 365 | player.trigger(flow, [player, arg]); 366 | }); 367 | }); 368 | 369 | player.on("error." + engineName, function () { 370 | if (hls) { 371 | player.engine.unload(); 372 | } 373 | 374 | }).on("beforeseek." + engineName, function (e, api, pos) { 375 | if (pos === undefined) { 376 | e.preventDefault(); 377 | } else if (!hlsUpdatedConf.bufferWhilePaused && api.paused) { 378 | hls.startLoad(pos); 379 | } 380 | }); 381 | 382 | player.on("quality." + engineName, function (_e, _api, q) { 383 | if (hlsUpdatedConf.smoothSwitching) { 384 | hls.nextLevel = q; 385 | } else { 386 | hls.currentLevel = q; 387 | } 388 | lastSelectedLevel = q; 389 | }); 390 | 391 | common.prepend(common.find(".fp-player", root)[0], videoTag); 392 | 393 | } else { 394 | hls.destroy(); 395 | if ((player.video.src && video.src !== player.video.src) || video.index) { 396 | common.attr(videoTag, "autoplay", "autoplay"); 397 | } 398 | } 399 | 400 | // #28 obtain api.video props before ready 401 | player.video = video; 402 | 403 | // reset 404 | maxLevel = 0; 405 | 406 | Object.keys(hlsUpdatedConf).forEach(function (key) { 407 | if (!Hls.DefaultConfig.hasOwnProperty(key)) { 408 | delete hlsClientConf[key]; 409 | } 410 | 411 | var value = hlsUpdatedConf[key]; 412 | 413 | switch (key) { 414 | case "adaptOnStartOnly": 415 | if (value) { 416 | hlsClientConf.startLevel = -1; 417 | } 418 | break; 419 | case "autoLevelCapping": 420 | if (value === false) { 421 | value = -1; 422 | } 423 | hlsClientConf[key] = value; 424 | break; 425 | case "startLevel": 426 | switch (value) { 427 | case "auto": 428 | value = -1; 429 | break; 430 | case "firstLevel": 431 | value = undefined; 432 | break; 433 | } 434 | hlsClientConf[key] = value; 435 | break; 436 | case "recover": // DEPRECATED 437 | hlsUpdatedConf.recoverMediaError = false; 438 | hlsUpdatedConf.recoverNetworkError = false; 439 | recover = value; 440 | break; 441 | case "strict": 442 | if (value) { 443 | hlsUpdatedConf.recoverMediaError = false; 444 | hlsUpdatedConf.recoverNetworkError = false; 445 | recover = 0; 446 | } 447 | break; 448 | 449 | } 450 | }); 451 | 452 | hls = new Hls(hlsClientConf); 453 | player.engine[engineName] = hls; 454 | recoverMediaErrorDate = null; 455 | swapAudioCodecDate = null; 456 | 457 | Object.keys(HLSEVENTS).forEach(function (key) { 458 | var etype = HLSEVENTS[key], 459 | listeners = hlsUpdatedConf.listeners, 460 | expose = listeners && listeners.indexOf(etype) > -1; 461 | 462 | hls.on(etype, function (e, data) { 463 | var fperr, 464 | errobj = {}, 465 | errors = player.conf.errors, 466 | ERRORTYPES = Hls.ErrorTypes, 467 | ERRORDETAILS = Hls.ErrorDetails, 468 | updatedVideo = player.video, 469 | src = updatedVideo.src; 470 | 471 | switch (key) { 472 | case "MANIFEST_PARSED": 473 | if (hlsQualitiesSupport(conf) && !player.pluginQualitySelectorEnabled) { 474 | initQualitySelection(hlsQualitiesConf, data); 475 | } 476 | break; 477 | case "MANIFEST_LOADED": 478 | if (data.audioTracks && data.audioTracks.length && 479 | (!hls.audioTracks || !hls.audioTracks.length)) { 480 | errors.push("Alternate audio tracks not supported by light plugin build."); 481 | errobj = handleError(errors.length - 1, player.video.src); 482 | player.trigger('error', [player, errobj]); 483 | errors.slice(0, errors.length - 1); 484 | } 485 | break; 486 | case "MEDIA_ATTACHED": 487 | hls.loadSource(src); 488 | break; 489 | case "FRAG_LOADED": 490 | if (hlsUpdatedConf.bufferWhilePaused && !player.live && 491 | hls.autoLevelEnabled && hls.nextLoadLevel > maxLevel) { 492 | maxLevel = hls.nextLoadLevel; 493 | } 494 | break; 495 | case "LEVEL_UPDATED": 496 | if (player.live) { 497 | extend(updatedVideo, { 498 | seekOffset: data.details.fragments[0].start + hls.config.nudgeOffset, 499 | duration: hls.liveSyncPosition 500 | }); 501 | if (player.dvr && player.playing) { 502 | player.trigger('dvrwindow', [player, { 503 | start: updatedVideo.seekOffset, 504 | end: hls.liveSyncPosition 505 | }]); 506 | } 507 | } 508 | break; 509 | case "BUFFER_APPENDED": 510 | common.removeClass(root, recoveryClass); 511 | break; 512 | case "ERROR": 513 | if (data.fatal || hlsUpdatedConf.strict) { 514 | switch (data.type) { 515 | case ERRORTYPES.NETWORK_ERROR: 516 | if (hlsUpdatedConf.recoverNetworkError || recover) { 517 | doRecover(conf, data.type, true); 518 | } else if (data.frag && data.frag.url) { 519 | errobj.url = data.frag.url; 520 | fperr = 2; 521 | } else { 522 | fperr = 4; 523 | } 524 | break; 525 | case ERRORTYPES.MEDIA_ERROR: 526 | if (hlsUpdatedConf.recoverMediaError || recover) { 527 | doRecover(conf, data.type); 528 | } else { 529 | fperr = 3; 530 | } 531 | break; 532 | default: 533 | fperr = 5; 534 | } 535 | 536 | if (fperr !== undefined) { 537 | errobj = handleError(fperr, src, data.url); 538 | player.trigger("error", [player, errobj]); 539 | } 540 | } else if (data.details === ERRORDETAILS.FRAG_LOOP_LOADING_ERROR || 541 | data.details === ERRORDETAILS.BUFFER_STALLED_ERROR) { 542 | common.addClass(root, recoveryClass); 543 | } 544 | break; 545 | } 546 | 547 | // memory leak if all these are re-triggered by api #29 548 | if (expose) { 549 | player.trigger(e, [player, data]); 550 | } 551 | }); 552 | }); 553 | 554 | if (hlsUpdatedConf.adaptOnStartOnly) { 555 | bean.one(videoTag, "timeupdate." + engineName, function () { 556 | hls.loadLevel = hls.loadLevel; 557 | }); 558 | } 559 | 560 | hls.attachMedia(videoTag); 561 | 562 | if (autoplay && videoTag.paused) { 563 | var playPromise = videoTag.play(); 564 | if (playPromise !== undefined) { 565 | playPromise.catch(function () { 566 | if (!support.mutedAutoplay) { 567 | player.unload(); 568 | player.message("Please click the play button", 3000); 569 | } 570 | }); 571 | } 572 | } 573 | }, 574 | 575 | resume: function () { 576 | videoTag.play(); 577 | }, 578 | 579 | pause: function () { 580 | videoTag.pause(); 581 | }, 582 | 583 | seek: function (time) { 584 | if (videoTag) { 585 | videoTag.currentTime = time; 586 | } 587 | }, 588 | 589 | volume: function (level) { 590 | if (videoTag) { 591 | videoTag.volume = level; 592 | } 593 | }, 594 | 595 | speed: function (val) { 596 | videoTag.playbackRate = val; 597 | player.trigger('speed', [player, val]); 598 | }, 599 | 600 | unload: function () { 601 | if (hls) { 602 | var listeners = "." + engineName; 603 | 604 | hls.destroy(); 605 | hls = 0; 606 | player.off(listeners); 607 | bean.off(root, listeners); 608 | bean.off(videoTag, listeners); 609 | common.removeNode(videoTag); 610 | videoTag = 0; 611 | } 612 | } 613 | }; 614 | 615 | return engine; 616 | }; 617 | 618 | if (Hls.isSupported() && 619 | (parseInt(version.split(".")[0]) > 6 || (/adhoc|dev/.test(version)))) { 620 | // only load engine if it can be used 621 | engineImpl.engineName = engineName; // must be exposed 622 | engineImpl[engineName + "ClientVersion"] = Hls.version; 623 | engineImpl.canPlay = function (type, conf) { 624 | if (conf[engineName] === false || conf.clip[engineName] === false) { 625 | // engine disabled for player 626 | return false; 627 | } 628 | 629 | // merge hlsjs clip config at earliest opportunity 630 | hlsconf = extend({ 631 | bufferWhilePaused: true, 632 | smoothSwitching: true, 633 | recoverMediaError: true 634 | }, conf[engineName], conf.clip[engineName]); 635 | 636 | // https://github.com/dailymotion/hls.js/issues/9 637 | return isHlsType(type) && (!(brwsr.safari && support.dataload) || hlsconf.safari); 638 | }; 639 | 640 | // issue #94 641 | if (support.mutedAutoplay && (version === "7.1.1" || version === "7.1.0")) { 642 | flowplayer(function (api, root) { 643 | var c = api.conf; 644 | 645 | if (!c.splash && !c.autoplay) { 646 | api.splash = true; 647 | c.splash = typeof c.poster === "string" 648 | ? c.poster 649 | : true; 650 | c.poster = undefined; 651 | c.autoplay = true; 652 | destroyVideoTag(root); 653 | } 654 | }); 655 | } 656 | 657 | // put on top of engine stack 658 | // so hlsjs is tested before html5 video hls and flash hls 659 | flowplayer.engines.unshift(engineImpl); 660 | } 661 | 662 | }; 663 | if (typeof module === 'object' && module.exports) { 664 | module.exports = extension.bind(undefined, require('hls.js/dist/hls.light.js')); 665 | } else if (window.Hls && window.flowplayer) { 666 | extension(window.Hls, window.flowplayer); 667 | } 668 | }()); 669 | -------------------------------------------------------------------------------- /footConditionalComment.js: -------------------------------------------------------------------------------- 1 | 2 | /*@ 3 | @end 4 | @*/ 5 | -------------------------------------------------------------------------------- /headConditionalComment.js: -------------------------------------------------------------------------------- 1 | /*@cc_on @*/ 2 | /*@ 3 | @if (@_jscript_version > 10) 4 | @*/ 5 | -------------------------------------------------------------------------------- /olddocs.md: -------------------------------------------------------------------------------- 1 | Present video in optimal quality via Adaptive Bit Rate streaming ([ABR][]) in 2 | modern desktop browsers without the need for Flash. 3 | 4 | Get your ticket to the future of [HLS](../setup#hls) with this plugin 5 | developed on top of the [hls.js](https://github.com/video-dev/hls.js) client 6 | library. 7 | 8 | Integrates seamlessly with your HLS streams from Flowplayer Drive. 9 | 10 | [Manual HLS quality level selection](../setup#hls-quality-selection) via the 11 | HD menu is available out of the box. See also the `hlsQualities` 12 | [player](../setup#player-options) and [clip](../setup#clip-options) option. 13 | 14 | If a stream offers 15 | [alternate audio renditions](https://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.1) 16 | (`#EXT-X-MEDIA`) as in the demo below a selection menu is added to the 17 | controlbar (currently not supported by Safari). 18 | 19 | HLS subtitle display and selection can be enabled via the `subtitles` 20 | [hlsjs option](#hlsjs-options). 21 | 22 | The hlsjs plugin is also loaded by [Twitter shared](../sharing#twitter) and 23 | [embedded players](../sharing#embedding). 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
36 | 37 | 69 | 70 | 71 | 72 | ~~~ html 73 | 77 | 78 | 110 | ~~~ 111 | 112 | 113 | [View standalone page](../standalone/plugins/hlsjs.html){.standalone} 114 | 115 | 116 | 117 | ### Compatibility 118 | 119 | #### Browser support 120 | 121 | The `hlsjs` [engine](../api#engines) provided by the plugin is loaded if the browser features the [MediaSource extension](https://developer.mozilla.org/en-US/docs/Web/API/MediaSource), and if the MediaSource implementation __reliably__ handles playback of segmented MPEG-4 video. 122 | 123 | __Note:__ Mac OS Safari's MediaSource implementation has issues with 124 | [remuxed MPEG-4 segments](https://github.com/video-dev/hls.js/issues/9) - for the moment the hlsjs engine will be loaded in Safari only if the `safari` [hlsjs option](#hlsjs-options) is enabled. 125 | 126 | 127 | #### Stream compatibility 128 | 129 | For stream compatibility check the [list of supported m3u8 tags](https://github.com/video-dev/hls.js#supported-m3u8-tags) and the as of yet 130 | [unsupported HLS features](https://github.com/video-dev/hls.js#not-supported-yet). 131 | 132 | [notice] 133 | Chromecast cannot play streams with alternate audio renditions. 134 | [/notice] 135 | 136 | 137 | [notice] 138 | Test your streams in the hls.js demo player. In case of playback issues with the hls.js client, we encourage you to use the hls.js bug tracker as first port of call. 139 | [/notice] 140 | 141 | 142 | #### Server side 143 | 144 | [notice] 145 | The video streams must be served with a cross domain policy (CORS) allowing GET requests. If the segments are not static files, but are retrieved via byte-range requests HEAD and OPTIONS must be allowed as well. 146 | [/notice] 147 | 148 | Sample CORS Configuration for Amazon S3: 149 | 150 | ~~~ xml 151 | 152 |hlsjs
configuration object makes all [hls.js tuning parameters](https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning) available for Flowplayer.
219 |
220 | Additionally hlsjs
accepts the following Flowplayer-specific
221 | properties:
222 |
223 | | option | default value | description |
224 | |--------|---------------|-------------|
225 | | adaptOnStartOnly | false | If set to `true` adaptive bitrate switching is disabled after a suitable HLS level is chosen in an initial bandwidth check before playback. Useful for shorter videos, especially when looping. |
226 | | audioABR | false | If multiple audio [groups](https://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.1.1) are present the hlsjs engine will load only one by default, regardless of the current video level. If this option is set to `true` the corresponding audio track will be loaded on level switch.