├── .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 |
33 | 34 | 35 |
36 | 37 | 69 | 70 | 71 | 72 | ~~~ html 73 |
74 | 75 | 76 |
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 | 153 | 154 | * 155 | GET 156 | 157 | 158 | HEAD 159 | 160 | 3000 161 | * 162 | 163 | 164 | ~~~ 165 | 166 | For other server configurations, please check [enable-cors.org](https://enable-cors.org/) . 167 | 168 | ### Loading the assets 169 | 170 | The only requirement is to load the plugin after the Flowplayer script. 171 | 172 | ~~~ html 173 | 174 | 175 | 176 | 177 | ~~~ 178 | 179 | 180 | To speed up page loading a 'light' version of the plugin can be used: 181 | 182 | ~~~ html 183 | 184 | 185 | 186 | 187 | ~~~ 188 | 189 | 190 | The full plugin is required for support of: 191 | 192 | - [alternate audio renditions](https://developer.apple.com/library/content/referencelibrary/GettingStarted/AboutHTTPLiveStreaming/about/about.html#//apple_ref/doc/uid/TP40013978-CH3-SW17) 193 | - manual audio track selection 194 | - HLS subtitle support 195 | - ID3 tag processing 196 | 197 | [notice=note] 198 | The development of the client library for this plugin is a fast moving target. Loading the latest version of the plugin as above is recommended. It packs a recent and tested release of the client library. 199 | [/notice] 200 | 201 | 202 | ### Configuration 203 | 204 | The plugin can be configured on the [clip](../setup#clip-options), 205 | [player](../setup#player-options) and [global](../setup#global-configuration) level of the Flowplayer configuration. 206 | 207 | 208 | All configuration is done by the `hlsjs` option: 209 | 210 | | option | kind | description | 211 | |--------|------|-------------| 212 | | hlsjs | Boolean | Players can be told to disable the plugin by setting this to `false`. Normally used only for testing [Flash HLS](../setup#flash-hls).
Can be set on [clip level](../setup#clip-option) for convenience, but as it decides what engine to load it will only have an effect if configured for a single clip or the first clip of a playlist. | 213 | | hlsjs | Object | The plugin behavior can be fine tuned in the `hlsjs` [configuration object](#hlsjs-options).
This option cannot be set as [HTML data-attribute](../setup#html-configuration). | 214 | 215 | 216 | #### hlsjs options 217 | 218 | The 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.
Requires the [full plugin build](#hlsjs-assets). | 227 | | autoLevelCapping | -1 | Forbids the player to pick a higher clip resolution/bitrate than specified when in ABR mode. Accepts an index number from `0` (lowest) to highest. The default value `-1` means no capping, and may also be specified as boolean `false`. | 228 | | bufferWhilePaused | true | If set to `false` the player does not buffer segments in paused state. Be careful, this may affect playback quality after pausing and seeking performance in paused state.
For bandwidth saving it is recommended to compare the effect with the generic hls.js buffer related options, especially [maxMaxBufferLength](https://github.com/video-dev/hls.js/blob/v0.8.4/doc/API.md#maxmaxbufferlength). | 229 | | listeners | | An array of [hls.js runtime events](https://github.com/video-dev/hls.js/blob/v0.8.4/doc/API.md#runtime-events) to be exposed via the player API. Refer to [hlsjs events](#hlsjs-events) for details. | 230 | | recover | | _Deprecated_ - use `recoverMediaError` or/and `recoverNetworkError` instead.
Maximum attempts to recover from [network and media errors](https://github.com/video-dev/hls.js/blob/v0.8.4/doc/API.md#errors) which are considered fatal by hls.js. If set to `-1`, recovery is always tried. | 231 | | recoverMediaError | true | When `true`, the hlsjs engine will try to recover from otherwise fatal decoding errors if possible. | 232 | | recoverNetworkError | false | When `true`, the hlsjs engine will try to recover from otherwise fatal network errors if possible.
_Note:_ Enabling network error recovery changes player behaviour, and only for the hlsjs engine. | 233 | | safari | false | If set to `true` the plugin is enabled in Safari. Please read the section on [browser support](#hlsjs-browser-support) before enabling this option. | 234 | | smoothSwitching | true | Whether manual [HLS quality switching](../setup#hls-quality-selection) should be smooth - level change with begin of next segment - or instant. Setting this to `false` can cause a playback pause on switch. | 235 | | startLevel | firstLevel | Tells the player which clip resolution/bitrate to pick initially. Accepts an index number from `0` (lowest) to highest. Defaults to the level listed first in the variant (master) playlist, as with generic HLS playback. Set to `-1` or `"auto"` for automatic selection of suitable level after a bandwidth check. | 236 | | strict | false | Set to `true` if you want non fatal hls.js playback errors to trigger Flowplayer errors. Useful for debugging streams and live stream maintenance. | 237 | | subtitles | false | If set to `true` HLS subtitles are shown. If there are multiple subtitle tracks available, they can be selected from the CC menu. Native subtitle display can be configured via the `nativesubtitles` [player option](../subtitles#player-options).
Requires the [full plugin build](#hlsjs-assets). | 238 | 239 | 240 | ### JavaScript API 241 | 242 | The plugin provides complete access to the [hls.js client API](https://github.com/video-dev/hls.js/blob/master/doc/API.md) 243 | via the `engine.hlsjs` [property](../api#engines). 244 | 245 | Simple example: 246 | 247 | ~~~ js 248 | // switch to first hls level 249 | flowplayer(0).engine.hlsjs.nextLevel = 0; 250 | ~~~ 251 | 252 | 253 | 254 | #### Video object 255 | 256 | If several video quality levels are available and [hls quality selection](../setup#hls-quality-selection) is enabled, the 257 | current [video object](../api#video-object) features these additional 258 | properties: 259 | 260 | | property | kind | description | 261 | |----------|------|-------------| 262 | | quality | integer | The currently selected video quality; `-1` stands for adaptive selection. | 263 | | qualities | array | Lists all qualities available for manual selection. | 264 | 265 | 266 | 267 | 268 | #### Events 269 | 270 | [hls.js client runtime events](https://github.com/video-dev/hls.js/blob/master/doc/API.md#runtime-events) 271 | which are listed in the `listeners` [hlsjs configuration](#hlsjs-options) are exposed to the Flowplayer API. The third argument of the event handle functions gives access to the event's data. 272 | 273 | ~~~ js 274 | // expose hls.js LEVEL_SWITCH event to Flowplayer API 275 | flowplayer.conf.hlsjs = { 276 | listeners: ["hlsLevelSwitch"] 277 | }; 278 | 279 | flowplayer(function (api) { 280 | api.on("hlsLevelSwitch", function (e, api, data) { 281 | // listen to Hls.Events.LEVEL_SWITCH 282 | var level = api.engine.hlsjs.levels[data.level]; 283 | 284 | console.info("switched to hls level index:", data.level); 285 | console.info("width:", level.width, "height": level.height); 286 | 287 | }); 288 | }); 289 | ~~~ 290 | 291 | The mappings of hls.js event names to their respective constants are listed [here](https://github.com/video-dev/hls.js/blob/master/src/events.js). 292 | 293 | 294 | 295 | #### Migration from Flowplayer Version 6 296 | 297 | `hlsQualities` is now a core [option](../setup#player-options). *Note:* 298 | The adaptive bitrate level `-1` must now be specified explicitly as first item 299 | of the `hlsQualities` array. 300 | 301 | No additonal CSS resources have to be loaded for manual HLS level selection, the 302 | builtin HD menu is used to present the choices. 303 | 304 | ### Links 305 | 306 | - [GitHub code repository](https://github.com/flowplayer/flowplayer-hlsjs) 307 | - [complete demo](https://demos.flowplayer.com/plugins/hlsjs.html) with detailed 308 | explanations 309 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flowplayer-hlsjs", 3 | "version": "1.1.1", 4 | "description": "Flowplayer HLS.js plugin", 5 | "main": "flowplayer.hlsjs.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "jslint --edition=latest --devel flowplayer.hlsjs.js flowplayer.hlsjs.light.js", 9 | "build": "webpack", 10 | "light": "webpack --config ./webpack.light.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/flowplayer/flowplayer-hlsjs" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/flowplayer/flowplayer-hlsjs/issues" 20 | }, 21 | "homepage": "https://github.com/flowplayer/flowplayer-hlsjs", 22 | "dependencies": { 23 | "hls.js": "0.8.4", 24 | "text-encoding": "https://github.com/aiham/text-encoding#remove-encoding-indexes", 25 | "webworkify-webpack": "^1.0.4" 26 | }, 27 | "peerDependencies": { 28 | "flowplayer": "^6.0.3 || ^7.0.2" 29 | }, 30 | "devDependencies": { 31 | "babel-core": "^6.5.1", 32 | "babel-loader": "^6.2.2", 33 | "babel-preset-es2015": "^6.5.0", 34 | "jslint": "^0.9.5", 35 | "url-toolkit": "^1.0.4", 36 | "webpack": "^1.12.13", 37 | "wrapper-webpack-plugin": "^0.1.7" 38 | }, 39 | "jshintConfig": { 40 | "undef": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /standalone.js: -------------------------------------------------------------------------------- 1 | /*global flowplayer */ 2 | 3 | var engine = require('./flowplayer.hlsjs'); 4 | engine(flowplayer); 5 | -------------------------------------------------------------------------------- /standalone.light.js: -------------------------------------------------------------------------------- 1 | /*global flowplayer */ 2 | 3 | var engine = require('./flowplayer.hlsjs.light'); 4 | engine(flowplayer); 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs') 4 | , path = require('path') 5 | , webpack = require('webpack') 6 | , console = require('console') 7 | , WrapperPlugin = require('wrapper-webpack-plugin') 8 | , headerComment = fs.readFileSync('./headConditionalComment.js') 9 | , footerComment = fs.readFileSync('./footConditionalComment.js') 10 | , exec = require('child_process').execSync 11 | , gitDesc 12 | , banner = '' 13 | , bannerAppend = false 14 | , lines = fs.readFileSync('./flowplayer.hlsjs.js', 'utf8').split('\n'); 15 | 16 | try { 17 | gitDesc = exec('git describe').toString('utf8').trim(); 18 | } catch (ignore) { 19 | console.warn('unable to determine git description'); 20 | } 21 | 22 | lines.forEach(function (line) { 23 | if (line === '/*!') { 24 | bannerAppend = true; 25 | } 26 | if (bannerAppend) { 27 | bannerAppend = line.indexOf('$GIT_DESC$') < 0; 28 | if (gitDesc) { 29 | line = line.replace('$GIT_DESC$', gitDesc); 30 | } 31 | banner += line + (bannerAppend ? '\n' : '\n\n*/'); 32 | } 33 | }); 34 | 35 | module.exports = { 36 | entry: {'flowplayer.hlsjs.min': ['./standalone.js']}, 37 | externals: { 38 | flowplayer: 'flowplayer' 39 | }, 40 | output: { 41 | path: path.join(__dirname, 'dist'), 42 | filename: '[name].js' 43 | }, 44 | plugins: [ 45 | new webpack.optimize.OccurrenceOrderPlugin(true), 46 | new webpack.optimize.UglifyJsPlugin({ 47 | include: /\.min\.js$/, 48 | mangle: true, 49 | output: { comments: false } 50 | }), 51 | new WrapperPlugin({header: headerComment, footer: footerComment}), 52 | new webpack.BannerPlugin(banner, {raw: true}) 53 | ] 54 | }; 55 | -------------------------------------------------------------------------------- /webpack.light.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs') 4 | , path = require('path') 5 | , webpack = require('webpack') 6 | , console = require('console') 7 | , WrapperPlugin = require('wrapper-webpack-plugin') 8 | , headerComment = fs.readFileSync('./headConditionalComment.js') 9 | , footerComment = fs.readFileSync('./footConditionalComment.js') 10 | , exec = require('child_process').execSync 11 | , gitDesc 12 | , banner = '' 13 | , bannerAppend = false 14 | , lines = fs.readFileSync('./flowplayer.hlsjs.light.js', 'utf8').split('\n'); 15 | 16 | try { 17 | gitDesc = exec('git describe').toString('utf8').trim(); 18 | } catch (ignore) { 19 | console.warn('unable to determine git description'); 20 | } 21 | 22 | lines.forEach(function (line) { 23 | if (line === '/*!') { 24 | bannerAppend = true; 25 | } 26 | if (bannerAppend) { 27 | bannerAppend = line.indexOf('$GIT_DESC$') < 0; 28 | if (gitDesc) { 29 | line = line.replace('$GIT_DESC$', gitDesc); 30 | } 31 | banner += line + (bannerAppend ? '\n' : '\n\n*/'); 32 | } 33 | }); 34 | 35 | module.exports = { 36 | entry: {'flowplayer.hlsjs.light.min': ['./standalone.light.js']}, 37 | externals: { 38 | flowplayer: 'flowplayer' 39 | }, 40 | output: { 41 | path: path.join(__dirname, 'dist'), 42 | filename: '[name].js' 43 | }, 44 | plugins: [ 45 | new webpack.optimize.OccurrenceOrderPlugin(true), 46 | new webpack.optimize.UglifyJsPlugin({ 47 | include: /\.light\.min\.js$/, 48 | mangle: true, 49 | output: { comments: false } 50 | }), 51 | new WrapperPlugin({header: headerComment, footer: footerComment}), 52 | new webpack.BannerPlugin(banner, {raw: true}) 53 | ] 54 | }; 55 | --------------------------------------------------------------------------------