1typeof navigator === "object" && (function (global, factory) { 2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 typeof define === 'function' && define.amd ? define('Plyr', factory) : 4 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory()); 5})(this, (function () { 'use strict'; 6 7 function _defineProperty$1(obj, key, value) { 8 key = _toPropertyKey(key); 9 if (key in obj) { 10 Object.defineProperty(obj, key, { 11 value: value, 12 enumerable: true, 13 configurable: true, 14 writable: true 15 }); 16 } else { 17 obj[key] = value; 18 } 19 return obj; 20 } 21 function _toPrimitive(input, hint) { 22 if (typeof input !== "object" || input === null) return input; 23 var prim = input[Symbol.toPrimitive]; 24 if (prim !== undefined) { 25 var res = prim.call(input, hint || "default"); 26 if (typeof res !== "object") return res; 27 throw new TypeError("@@toPrimitive must return a primitive value."); 28 } 29 return (hint === "string" ? String : Number)(input); 30 } 31 function _toPropertyKey(arg) { 32 var key = _toPrimitive(arg, "string"); 33 return typeof key === "symbol" ? key : String(key); 34 } 35 36 function _classCallCheck(e, t) { 37 if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function"); 38 } 39 function _defineProperties(e, t) { 40 for (var n = 0; n < t.length; n++) { 41 var r = t[n]; 42 r.enumerable = r.enumerable || !1, r.configurable = !0, "value" in r && (r.writable = !0), Object.defineProperty(e, r.key, r); 43 } 44 } 45 function _createClass(e, t, n) { 46 return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e; 47 } 48 function _defineProperty(e, t, n) { 49 return t in e ? Object.defineProperty(e, t, { 50 value: n, 51 enumerable: !0, 52 configurable: !0, 53 writable: !0 54 }) : e[t] = n, e; 55 } 56 function ownKeys(e, t) { 57 var n = Object.keys(e); 58 if (Object.getOwnPropertySymbols) { 59 var r = Object.getOwnPropertySymbols(e); 60 t && (r = r.filter(function (t) { 61 return Object.getOwnPropertyDescriptor(e, t).enumerable; 62 })), n.push.apply(n, r); 63 } 64 return n; 65 } 66 function _objectSpread2(e) { 67 for (var t = 1; t < arguments.length; t++) { 68 var n = null != arguments[t] ? arguments[t] : {}; 69 t % 2 ? ownKeys(Object(n), !0).forEach(function (t) { 70 _defineProperty(e, t, n[t]); 71 }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) { 72 Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t)); 73 }); 74 } 75 return e; 76 } 77 var defaults$1 = { 78 addCSS: !0, 79 thumbWidth: 15, 80 watch: !0 81 }; 82 function matches$1(e, t) { 83 return function () { 84 return Array.from(document.querySelectorAll(t)).includes(this); 85 }.call(e, t); 86 } 87 function trigger(e, t) { 88 if (e && t) { 89 var n = new Event(t, { 90 bubbles: !0 91 }); 92 e.dispatchEvent(n); 93 } 94 } 95 var getConstructor$1 = function (e) { 96 return null != e ? e.constructor : null; 97 }, 98 instanceOf$1 = function (e, t) { 99 return !!(e && t && e instanceof t); 100 }, 101 isNullOrUndefined$1 = function (e) { 102 return null == e; 103 }, 104 isObject$1 = function (e) { 105 return getConstructor$1(e) === Object; 106 }, 107 isNumber$1 = function (e) { 108 return getConstructor$1(e) === Number && !Number.isNaN(e); 109 }, 110 isString$1 = function (e) { 111 return getConstructor$1(e) === String; 112 }, 113 isBoolean$1 = function (e) { 114 return getConstructor$1(e) === Boolean; 115 }, 116 isFunction$1 = function (e) { 117 return getConstructor$1(e) === Function; 118 }, 119 isArray$1 = function (e) { 120 return Array.isArray(e); 121 }, 122 isNodeList$1 = function (e) { 123 return instanceOf$1(e, NodeList); 124 }, 125 isElement$1 = function (e) { 126 return instanceOf$1(e, Element); 127 }, 128 isEvent$1 = function (e) { 129 return instanceOf$1(e, Event); 130 }, 131 isEmpty$1 = function (e) { 132 return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length; 133 }, 134 is$1 = { 135 nullOrUndefined: isNullOrUndefined$1, 136 object: isObject$1, 137 number: isNumber$1,
vendor: 16,290 bytes, lines 138-609
138 string: isString$1, 139 boolean: isBoolean$1, 140 function: isFunction$1, 141 array: isArray$1, 142 nodeList: isNodeList$1, 143 element: isElement$1, 144 event: isEvent$1, 145 empty: isEmpty$1 146 }; 147 function getDecimalPlaces(e) { 148 var t = "".concat(e).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/); 149 return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0; 150 } 151 function round(e, t) { 152 if (1 > t) { 153 var n = getDecimalPlaces(t); 154 return parseFloat(e.toFixed(n)); 155 } 156 return Math.round(e / t) * t; 157 } 158 var RangeTouch = function () { 159 function e(t, n) { 160 _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init()); 161 } 162 return _createClass(e, [{ 163 key: "init", 164 value: function () { 165 e.enabled && (this.config.addCSS && (this.element.style.userSelect = "none", this.element.style.webKitUserSelect = "none", this.element.style.touchAction = "manipulation"), this.listeners(!0), this.element.rangeTouch = this); 166 } 167 }, { 168 key: "destroy", 169 value: function () { 170 e.enabled && (this.config.addCSS && (this.element.style.userSelect = "", this.element.style.webKitUserSelect = "", this.element.style.touchAction = ""), this.listeners(!1), this.element.rangeTouch = null); 171 } 172 }, { 173 key: "listeners", 174 value: function (e) { 175 var t = this, 176 n = e ? "addEventListener" : "removeEventListener"; 177 ["touchstart", "touchmove", "touchend"].forEach(function (e) { 178 t.element[n](e, function (e) { 179 return t.set(e); 180 }, !1); 181 }); 182 } 183 }, { 184 key: "get", 185 value: function (t) { 186 if (!e.enabled || !is$1.event(t)) return null; 187 var n, 188 r = t.target, 189 i = t.changedTouches[0], 190 o = parseFloat(r.getAttribute("min")) || 0, 191 s = parseFloat(r.getAttribute("max")) || 100, 192 u = parseFloat(r.getAttribute("step")) || 1, 193 c = r.getBoundingClientRect(), 194 a = 100 / c.width * (this.config.thumbWidth / 2) / 100; 195 return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u); 196 } 197 }, { 198 key: "set", 199 value: function (t) { 200 e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, "touchend" === t.type ? "change" : "input")); 201 } 202 }], [{ 203 key: "setup", 204 value: function (t) { 205 var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {}, 206 r = null; 207 if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type="range"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null; 208 var i = _objectSpread2({}, defaults$1, {}, n); 209 if (is$1.string(t) && i.watch) { 210 var o = new MutationObserver(function (n) { 211 Array.from(n).forEach(function (n) { 212 Array.from(n.addedNodes).forEach(function (n) { 213 is$1.element(n) && matches$1(n, t) && new e(n, i); 214 }); 215 }); 216 }); 217 o.observe(document.body, { 218 childList: !0, 219 subtree: !0 220 }); 221 } 222 return r.map(function (t) { 223 return new e(t, n); 224 }); 225 } 226 }, { 227 key: "enabled", 228 get: function () { 229 return "ontouchstart" in document.documentElement; 230 } 231 }]), e; 232 }(); 233 234 // ========================================================================== 235 // Type checking utils 236 // ========================================================================== 237 238 const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null; 239 const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor); 240 const isNullOrUndefined = input => input === null || typeof input === 'undefined'; 241 const isObject = input => getConstructor(input) === Object; 242 const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input); 243 const isString = input => getConstructor(input) === String; 244 const isBoolean = input => getConstructor(input) === Boolean; 245 const isFunction = input => typeof input === 'function'; 246 const isArray = input => Array.isArray(input); 247 const isWeakMap = input => instanceOf(input, WeakMap); 248 const isNodeList = input => instanceOf(input, NodeList); 249 const isTextNode = input => getConstructor(input) === Text; 250 const isEvent = input => instanceOf(input, Event); 251 const isKeyboardEvent = input => instanceOf(input, KeyboardEvent); 252 const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); 253 const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind); 254 const isPromise = input => instanceOf(input, Promise) && isFunction(input.then); 255 const isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object'; 256 const isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length; 257 const isUrl = input => { 258 // Accept a URL object 259 if (instanceOf(input, window.URL)) { 260 return true; 261 } 262 263 // Must be string from here 264 if (!isString(input)) { 265 return false; 266 } 267 268 // Add the protocol if required 269 let string = input; 270 if (!input.startsWith('http://') || !input.startsWith('https://')) { 271 string = `http://${input}`; 272 } 273 try { 274 return !isEmpty(new URL(string).hostname); 275 } catch (_) { 276 return false; 277 } 278 }; 279 var is = { 280 nullOrUndefined: isNullOrUndefined, 281 object: isObject, 282 number: isNumber, 283 string: isString, 284 boolean: isBoolean, 285 function: isFunction, 286 array: isArray, 287 weakMap: isWeakMap, 288 nodeList: isNodeList, 289 element: isElement, 290 textNode: isTextNode, 291 event: isEvent, 292 keyboardEvent: isKeyboardEvent, 293 cue: isCue, 294 track: isTrack, 295 promise: isPromise, 296 url: isUrl, 297 empty: isEmpty 298 }; 299 300 // ========================================================================== 301 const transitionEndEvent = (() => { 302 const element = document.createElement('span'); 303 const events = { 304 WebkitTransition: 'webkitTransitionEnd', 305 MozTransition: 'transitionend', 306 OTransition: 'oTransitionEnd otransitionend', 307 transition: 'transitionend' 308 }; 309 const type = Object.keys(events).find(event => element.style[event] !== undefined); 310 return is.string(type) ? events[type] : false; 311 })(); 312 313 // Force repaint of element 314 function repaint(element, delay) { 315 setTimeout(() => { 316 try { 317 // eslint-disable-next-line no-param-reassign 318 element.hidden = true; 319 320 // eslint-disable-next-line no-unused-expressions 321 element.offsetHeight; 322 323 // eslint-disable-next-line no-param-reassign 324 element.hidden = false; 325 } catch (_) { 326 // Do nothing 327 } 328 }, delay); 329 } 330 331 // ========================================================================== 332 // Browser sniffing 333 // Unfortunately, due to mixed support, UA sniffing is required 334 // ========================================================================== 335 336 const isIE = Boolean(window.document.documentMode); 337 const isEdge = /Edge/g.test(navigator.userAgent); 338 const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent); 339 const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1; 340 // navigator.platform may be deprecated but this check is still required 341 const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; 342 const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1; 343 var browser = { 344 isIE, 345 isEdge, 346 isWebKit, 347 isIPhone, 348 isIPadOS, 349 isIos 350 }; 351 352 // ========================================================================== 353 354 // Clone nested objects 355 function cloneDeep(object) { 356 return JSON.parse(JSON.stringify(object)); 357 } 358 359 // Get a nested value in an object 360 function getDeep(object, path) { 361 return path.split('.').reduce((obj, key) => obj && obj[key], object); 362 } 363 364 // Deep extend destination object with N more objects 365 function extend(target = {}, ...sources) { 366 if (!sources.length) { 367 return target; 368 } 369 const source = sources.shift(); 370 if (!is.object(source)) { 371 return target; 372 } 373 Object.keys(source).forEach(key => { 374 if (is.object(source[key])) { 375 if (!Object.keys(target).includes(key)) { 376 Object.assign(target, { 377 [key]: {} 378 }); 379 } 380 extend(target[key], source[key]); 381 } else { 382 Object.assign(target, { 383 [key]: source[key] 384 }); 385 } 386 }); 387 return extend(target, ...sources); 388 } 389 390 // ========================================================================== 391 392 // Wrap an element 393 function wrap(elements, wrapper) { 394 // Convert `elements` to an array, if necessary. 395 const targets = elements.length ? elements : [elements]; 396 397 // Loops backwards to prevent having to clone the wrapper on the 398 // first element (see `child` below). 399 Array.from(targets).reverse().forEach((element, index) => { 400 const child = index > 0 ? wrapper.cloneNode(true) : wrapper; 401 // Cache the current parent and sibling. 402 const parent = element.parentNode; 403 const sibling = element.nextSibling; 404 405 // Wrap the element (is automatically removed from its current 406 // parent). 407 child.appendChild(element); 408 409 // If the element had a sibling, insert the wrapper before 410 // the sibling to maintain the HTML structure; otherwise, just 411 // append it to the parent. 412 if (sibling) { 413 parent.insertBefore(child, sibling); 414 } else { 415 parent.appendChild(child); 416 } 417 }); 418 } 419 420 // Set attributes 421 function setAttributes(element, attributes) { 422 if (!is.element(element) || is.empty(attributes)) return; 423 424 // Assume null and undefined attributes should be left out, 425 // Setting them would otherwise convert them to "null" and "undefined" 426 Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value)); 427 } 428 429 // Create a DocumentFragment 430 function createElement(type, attributes, text) { 431 // Create a new <element> 432 const element = document.createElement(type); 433 434 // Set all passed attributes 435 if (is.object(attributes)) { 436 setAttributes(element, attributes); 437 } 438 439 // Add text node 440 if (is.string(text)) { 441 element.innerText = text; 442 } 443 444 // Return built element 445 return element; 446 } 447 448 // Insert an element after another 449 function insertAfter(element, target) { 450 if (!is.element(element) || !is.element(target)) return; 451 target.parentNode.insertBefore(element, target.nextSibling); 452 } 453 454 // Insert a DocumentFragment 455 function insertElement(type, parent, attributes, text) { 456 if (!is.element(parent)) return; 457 parent.appendChild(createElement(type, attributes, text)); 458 } 459 460 // Remove element(s) 461 function removeElement(element) { 462 if (is.nodeList(element) || is.array(element)) { 463 Array.from(element).forEach(removeElement); 464 return; 465 } 466 if (!is.element(element) || !is.element(element.parentNode)) { 467 return; 468 } 469 element.parentNode.removeChild(element); 470 } 471 472 // Remove all child elements 473 function emptyElement(element) { 474 if (!is.element(element)) return; 475 let { 476 length 477 } = element.childNodes; 478 while (length > 0) { 479 element.removeChild(element.lastChild); 480 length -= 1; 481 } 482 } 483 484 // Replace element 485 function replaceElement(newChild, oldChild) { 486 if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null; 487 oldChild.parentNode.replaceChild(newChild, oldChild); 488 return newChild; 489 } 490 491 // Get an attribute object from a string selector 492 function getAttributesFromSelector(sel, existingAttributes) { 493 // For example: 494 // '.test' to { class: 'test' } 495 // '#test' to { id: 'test' } 496 // '[data-test="test"]' to { 'data-test': 'test' } 497 498 if (!is.string(sel) || is.empty(sel)) return {}; 499 const attributes = {}; 500 const existing = extend({}, existingAttributes); 501 sel.split(',').forEach(s => { 502 // Remove whitespace 503 const selector = s.trim(); 504 const className = selector.replace('.', ''); 505 const stripped = selector.replace(/[[\]]/g, ''); 506 // Get the parts and value 507 const parts = stripped.split('='); 508 const [key] = parts; 509 const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; 510 // Get the first character 511 const start = selector.charAt(0); 512 switch (start) { 513 case '.': 514 // Add to existing classname 515 if (is.string(existing.class)) { 516 attributes.class = `${existing.class} ${className}`; 517 } else { 518 attributes.class = className; 519 } 520 break; 521 case '#': 522 // ID selector 523 attributes.id = selector.replace('#', ''); 524 break; 525 case '[': 526 // Attribute selector 527 attributes[key] = value; 528 break; 529 } 530 }); 531 return extend(existing, attributes); 532 } 533 534 // Toggle hidden 535 function toggleHidden(element, hidden) { 536 if (!is.element(element)) return; 537 let hide = hidden; 538 if (!is.boolean(hide)) { 539 hide = !element.hidden; 540 } 541 542 // eslint-disable-next-line no-param-reassign 543 element.hidden = hide; 544 } 545 546 // Mirror Element.classList.toggle, with IE compatibility for "force" argument 547 function toggleClass(element, className, force) { 548 if (is.nodeList(element)) { 549 return Array.from(element).map(e => toggleClass(e, className, force)); 550 } 551 if (is.element(element)) { 552 let method = 'toggle'; 553 if (typeof force !== 'undefined') { 554 method = force ? 'add' : 'remove'; 555 } 556 element.classList[method](className); 557 return element.classList.contains(className); 558 } 559 return false; 560 } 561 562 // Has class name 563 function hasClass(element, className) { 564 return is.element(element) && element.classList.contains(className); 565 } 566 567 // Element matches selector 568 function matches(element, selector) { 569 const { 570 prototype 571 } = Element; 572 function match() { 573 return Array.from(document.querySelectorAll(selector)).includes(this); 574 } 575 const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; 576 return method.call(element, selector); 577 } 578 579 // Closest ancestor element matching selector (also tests element itself) 580 function closest$1(element, selector) { 581 const { 582 prototype 583 } = Element; 584 585 // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill 586 function closestElement() { 587 let el = this; 588 do { 589 if (matches.matches(el, selector)) return el; 590 el = el.parentElement || el.parentNode; 591 } while (el !== null && el.nodeType === 1); 592 return null; 593 } 594 const method = prototype.closest || closestElement; 595 return method.call(element, selector); 596 } 597 598 // Find all elements 599 function getElements(selector) { 600 return this.elements.container.querySelectorAll(selector); 601 } 602 603 // Find a single element 604 function getElement(selector) { 605 return this.elements.container.querySelector(selector); 606 } 607 608 // Set focus and tab focus class 609 function setFocus(element = null, focusVisible = false) {
610 if (!is.element(element)) return; 611 612 // Set regular focus 613 element.focus({ 614 preventScroll: true, 615 focusVisible 616 }); 617 } 618 619 // ========================================================================== 620 621 // Default codecs for checking mimetype support 622 const defaultCodecs = { 623 'audio/ogg': 'vorbis', 624 'audio/wav': '1', 625 'video/webm': 'vp8, vorbis', 626 'video/mp4': 'avc1.42E01E, mp4a.40.2', 627 'video/ogg': 'theora' 628 }; 629 630 // Check for feature support 631 const support = { 632 // Basic support 633 audio: 'canPlayType' in document.createElement('audio'), 634 video: 'canPlayType' in document.createElement('video'), 635 // Check for support 636 // Basic functionality vs full UI 637 check(type, provider) { 638 const api = support[type] || provider !== 'html5'; 639 const ui = api && support.rangeInput; 640 return { 641 api, 642 ui 643 }; 644 }, 645 // Picture-in-picture support 646 // Safari & Chrome only currently 647 pip: (() => { 648 // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them 649 // It will throw the following error when trying to enter picture-in-picture 650 // `NotSupportedError: The Picture-in-Picture mode is not supported.` 651 if (browser.isIPhone) { 652 return false; 653 } 654 655 // Safari 656 // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls 657 if (is.function(createElement('video').webkitSetPresentationMode)) { 658 return true; 659 } 660 661 // Chrome 662 // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture 663 if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) { 664 return true; 665 } 666 return false; 667 })(), 668 // Airplay support 669 // Safari only currently 670 airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent), 671 // Inline playback support 672 // https://webkit.org/blog/6784/new-video-policies-for-ios/ 673 playsinline: 'playsInline' in document.createElement('video'), 674 // Check for mime type support against a player instance 675 // Credits: http://diveintohtml5.info/everything.html 676 // Related: http://www.leanbackplayer.com/test/h5mt.html 677 mime(input) { 678 if (is.empty(input)) { 679 return false; 680 } 681 const [mediaType] = input.split('/'); 682 let type = input; 683 684 // Verify we're using HTML5 and there's no media type mismatch 685 if (!this.isHTML5 || mediaType !== this.type) { 686 return false; 687 } 688 689 // Add codec if required 690 if (Object.keys(defaultCodecs).includes(type)) { 691 type += `; codecs="${defaultCodecs[input]}"`; 692 } 693 try { 694 return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); 695 } catch (_) { 696 return false; 697 } 698 },
vendor: 41,894 bytes, lines 699-2000
699 // Check for textTracks support 700 textTracks: 'textTracks' in document.createElement('video'), 701 // <input type="range"> Sliders 702 rangeInput: (() => { 703 const range = document.createElement('input'); 704 range.type = 'range'; 705 return range.type === 'range'; 706 })(), 707 // Touch 708 // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event 709 touch: 'ontouchstart' in document.documentElement, 710 // Detect transitions support 711 transitions: transitionEndEvent !== false, 712 // Reduced motion iOS & MacOS setting 713 // https://webkit.org/blog/7551/responsive-design-for-motion/ 714 reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches 715 }; 716 717 // ========================================================================== 718 719 // Check for passive event listener support 720 // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md 721 // https://www.youtube.com/watch?v=NPM6172J22g 722 const supportsPassiveListeners = (() => { 723 // Test via a getter in the options object to see if the passive property is accessed 724 let supported = false; 725 try { 726 const options = Object.defineProperty({}, 'passive', { 727 get() { 728 supported = true; 729 return null; 730 } 731 }); 732 window.addEventListener('test', null, options); 733 window.removeEventListener('test', null, options); 734 } catch (_) { 735 // Do nothing 736 } 737 return supported; 738 })(); 739 740 // Toggle event listener 741 function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { 742 // Bail if no element, event, or callback 743 if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { 744 return; 745 } 746 747 // Allow multiple events 748 const events = event.split(' '); 749 // Build options 750 // Default to just the capture boolean for browsers with no passive listener support 751 let options = capture; 752 753 // If passive events listeners are supported 754 if (supportsPassiveListeners) { 755 options = { 756 // Whether the listener can be passive (i.e. default never prevented) 757 passive, 758 // Whether the listener is a capturing listener or not 759 capture 760 }; 761 } 762 763 // If a single node is passed, bind the event listener 764 events.forEach(type => { 765 if (this && this.eventListeners && toggle) { 766 // Cache event listener 767 this.eventListeners.push({ 768 element, 769 type, 770 callback, 771 options 772 }); 773 } 774 element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); 775 }); 776 } 777 778 // Bind event handler 779 function on(element, events = '', callback, passive = true, capture = false) { 780 toggleListener.call(this, element, events, callback, true, passive, capture); 781 } 782 783 // Unbind event handler 784 function off(element, events = '', callback, passive = true, capture = false) { 785 toggleListener.call(this, element, events, callback, false, passive, capture); 786 } 787 788 // Bind once-only event handler 789 function once(element, events = '', callback, passive = true, capture = false) { 790 const onceCallback = (...args) => { 791 off(element, events, onceCallback, passive, capture); 792 callback.apply(this, args); 793 }; 794 toggleListener.call(this, element, events, onceCallback, true, passive, capture); 795 } 796 797 // Trigger event 798 function triggerEvent(element, type = '', bubbles = false, detail = {}) { 799 // Bail if no element 800 if (!is.element(element) || is.empty(type)) { 801 return; 802 } 803 804 // Create and dispatch the event 805 const event = new CustomEvent(type, { 806 bubbles, 807 detail: { 808 ...detail, 809 plyr: this 810 } 811 }); 812 813 // Dispatch the event 814 element.dispatchEvent(event); 815 } 816 817 // Unbind all cached event listeners 818 function unbindListeners() { 819 if (this && this.eventListeners) { 820 this.eventListeners.forEach(item => { 821 const { 822 element, 823 type, 824 callback, 825 options 826 } = item; 827 element.removeEventListener(type, callback, options); 828 }); 829 this.eventListeners = []; 830 } 831 } 832 833 // Run method when / if player is ready 834 function ready() { 835 return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {}); 836 } 837 838 /** 839 * Silence a Promise-like object. 840 * This is useful for avoiding non-harmful, but potentially confusing "uncaught 841 * play promise" rejection error messages. 842 * @param {Object} value An object that may or may not be `Promise`-like. 843 */ 844 function silencePromise(value) { 845 if (is.promise(value)) { 846 value.then(null, () => {}); 847 } 848 } 849 850 // ========================================================================== 851 852 // Remove duplicates in an array 853 function dedupe(array) { 854 if (!is.array(array)) { 855 return array; 856 } 857 return array.filter((item, index) => array.indexOf(item) === index); 858 } 859 860 // Get the closest value in an array 861 function closest(array, value) { 862 if (!is.array(array) || !array.length) { 863 return null; 864 } 865 return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev); 866 } 867 868 // ========================================================================== 869 870 // Check support for a CSS declaration 871 function supportsCSS(declaration) { 872 if (!window || !window.CSS) { 873 return false; 874 } 875 return window.CSS.supports(declaration); 876 } 877 878 // Standard/common aspect ratios 879 const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({ 880 ...out, 881 [x / y]: [x, y] 882 }), {}); 883 884 // Validate an aspect ratio 885 function validateAspectRatio(input) { 886 if (!is.array(input) && (!is.string(input) || !input.includes(':'))) { 887 return false; 888 } 889 const ratio = is.array(input) ? input : input.split(':'); 890 return ratio.map(Number).every(is.number); 891 } 892 893 // Reduce an aspect ratio to it's lowest form 894 function reduceAspectRatio(ratio) { 895 if (!is.array(ratio) || !ratio.every(is.number)) { 896 return null; 897 } 898 const [width, height] = ratio; 899 const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h); 900 const divider = getDivider(width, height); 901 return [width / divider, height / divider]; 902 } 903 904 // Calculate an aspect ratio 905 function getAspectRatio(input) { 906 const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null; 907 // Try provided ratio 908 let ratio = parse(input); 909 910 // Get from config 911 if (ratio === null) { 912 ratio = parse(this.config.ratio); 913 } 914 915 // Get from embed 916 if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) { 917 ({ 918 ratio 919 } = this.embed); 920 } 921 922 // Get from HTML5 video 923 if (ratio === null && this.isHTML5) { 924 const { 925 videoWidth, 926 videoHeight 927 } = this.media; 928 ratio = [videoWidth, videoHeight]; 929 } 930 return reduceAspectRatio(ratio); 931 } 932 933 // Set aspect ratio for responsive container 934 function setAspectRatio(input) { 935 if (!this.isVideo) { 936 return {}; 937 } 938 const { 939 wrapper 940 } = this.elements; 941 const ratio = getAspectRatio.call(this, input); 942 if (!is.array(ratio)) { 943 return {}; 944 } 945 const [x, y] = reduceAspectRatio(ratio); 946 const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`); 947 const padding = 100 / x * y; 948 if (useNative) { 949 wrapper.style.aspectRatio = `${x}/${y}`; 950 } else { 951 wrapper.style.paddingBottom = `${padding}%`; 952 } 953 954 // For Vimeo we have an extra <div> to hide the standard controls and UI 955 if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) { 956 const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10); 957 const offset = (height - padding) / (height / 50); 958 if (this.fullscreen.active) { 959 wrapper.style.paddingBottom = null; 960 } else { 961 this.media.style.transform = `translateY(-${offset}%)`; 962 } 963 } else if (this.isHTML5) { 964 wrapper.classList.add(this.config.classNames.videoFixedRatio); 965 } 966 return { 967 padding, 968 ratio 969 }; 970 } 971 972 // Round an aspect ratio to closest standard ratio 973 function roundAspectRatio(x, y, tolerance = 0.05) { 974 const ratio = x / y; 975 const closestRatio = closest(Object.keys(standardRatios), ratio); 976 977 // Check match is within tolerance 978 if (Math.abs(closestRatio - ratio) <= tolerance) { 979 return standardRatios[closestRatio]; 980 } 981 982 // No match 983 return [x, y]; 984 } 985 986 // Get the size of the viewport 987 // https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions 988 function getViewportSize() { 989 const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); 990 const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); 991 return [width, height]; 992 } 993 994 // ========================================================================== 995 const html5 = { 996 getSources() { 997 if (!this.isHTML5) { 998 return []; 999 } 1000 const sources = Array.from(this.media.querySelectorAll('source')); 1001 1002 // Filter out unsupported sources (if type is specified) 1003 return sources.filter(source => { 1004 const type = source.getAttribute('type'); 1005 if (is.empty(type)) { 1006 return true; 1007 } 1008 return support.mime.call(this, type); 1009 }); 1010 }, 1011 // Get quality levels 1012 getQualityOptions() { 1013 // Whether we're forcing all options (e.g. for streaming) 1014 if (this.config.quality.forced) { 1015 return this.config.quality.options; 1016 } 1017 1018 // Get sizes from <source> elements 1019 return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean); 1020 }, 1021 setup() { 1022 if (!this.isHTML5) { 1023 return; 1024 } 1025 const player = this; 1026 1027 // Set speed options from config 1028 player.options.speed = player.config.speed.options; 1029 1030 // Set aspect ratio if fixed 1031 if (!is.empty(this.config.ratio)) { 1032 setAspectRatio.call(player); 1033 } 1034 1035 // Quality 1036 Object.defineProperty(player.media, 'quality', { 1037 get() { 1038 // Get sources 1039 const sources = html5.getSources.call(player); 1040 const source = sources.find(s => s.getAttribute('src') === player.source); 1041 1042 // Return size, if match is found 1043 return source && Number(source.getAttribute('size')); 1044 }, 1045 set(input) { 1046 if (player.quality === input) { 1047 return; 1048 } 1049 1050 // If we're using an external handler... 1051 if (player.config.quality.forced && is.function(player.config.quality.onChange)) { 1052 player.config.quality.onChange(input); 1053 } else { 1054 // Get sources 1055 const sources = html5.getSources.call(player); 1056 // Get first match for requested size 1057 const source = sources.find(s => Number(s.getAttribute('size')) === input); 1058 1059 // No matching source found 1060 if (!source) { 1061 return; 1062 } 1063 1064 // Get current state 1065 const { 1066 currentTime, 1067 paused, 1068 preload, 1069 readyState, 1070 playbackRate 1071 } = player.media; 1072 1073 // Set new source 1074 player.media.src = source.getAttribute('src'); 1075 1076 // Prevent loading if preload="none" and the current source isn't loaded (#1044) 1077 if (preload !== 'none' || readyState) { 1078 // Restore time 1079 player.once('loadedmetadata', () => { 1080 player.speed = playbackRate; 1081 player.currentTime = currentTime; 1082 1083 // Resume playing 1084 if (!paused) { 1085 silencePromise(player.play()); 1086 } 1087 }); 1088 1089 // Load new source 1090 player.media.load(); 1091 } 1092 } 1093 1094 // Trigger change event 1095 triggerEvent.call(player, player.media, 'qualitychange', false, { 1096 quality: input 1097 }); 1098 } 1099 }); 1100 }, 1101 // Cancel current network requests 1102 // See https://github.com/sampotts/plyr/issues/174 1103 cancelRequests() { 1104 if (!this.isHTML5) { 1105 return; 1106 } 1107 1108 // Remove child sources 1109 removeElement(html5.getSources.call(this)); 1110 1111 // Set blank video src attribute 1112 // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error 1113 // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection 1114 this.media.setAttribute('src', this.config.blankVideo); 1115 1116 // Load the new empty source 1117 // This will cancel existing requests 1118 // See https://github.com/sampotts/plyr/issues/174 1119 this.media.load(); 1120 1121 // Debugging 1122 this.debug.log('Cancelled network requests'); 1123 } 1124 }; 1125 1126 // ========================================================================== 1127 1128 // Generate a random ID 1129 function generateId(prefix) { 1130 return `${prefix}-${Math.floor(Math.random() * 10000)}`; 1131 } 1132 1133 // Format string 1134 function format(input, ...args) { 1135 if (is.empty(input)) return input; 1136 return input.toString().replace(/{(\d+)}/g, (_, i) => args[i].toString()); 1137 } 1138 1139 // Get percentage 1140 function getPercentage(current, max) { 1141 if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { 1142 return 0; 1143 } 1144 return (current / max * 100).toFixed(2); 1145 } 1146 1147 // Replace all occurrences of a string in a string 1148 const replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); 1149 1150 // Convert to title case 1151 const toTitleCase = (input = '') => input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()); 1152 1153 // Convert string to pascalCase 1154 function toPascalCase(input = '') { 1155 let string = input.toString(); 1156 1157 // Convert kebab case 1158 string = replaceAll(string, '-', ' '); 1159 1160 // Convert snake case 1161 string = replaceAll(string, '_', ' '); 1162 1163 // Convert to title case 1164 string = toTitleCase(string); 1165 1166 // Convert to pascal case 1167 return replaceAll(string, ' ', ''); 1168 } 1169 1170 // Convert string to pascalCase 1171 function toCamelCase(input = '') { 1172 let string = input.toString(); 1173 1174 // Convert to pascal case 1175 string = toPascalCase(string); 1176 1177 // Convert first character to lowercase 1178 return string.charAt(0).toLowerCase() + string.slice(1); 1179 } 1180 1181 // Remove HTML from a string 1182 function stripHTML(source) { 1183 const fragment = document.createDocumentFragment(); 1184 const element = document.createElement('div'); 1185 fragment.appendChild(element); 1186 element.innerHTML = source; 1187 return fragment.firstChild.innerText; 1188 } 1189 1190 // Like outerHTML, but also works for DocumentFragment 1191 function getHTML(element) { 1192 const wrapper = document.createElement('div'); 1193 wrapper.appendChild(element); 1194 return wrapper.innerHTML; 1195 } 1196 1197 // ========================================================================== 1198 1199 // Skip i18n for abbreviations and brand names 1200 const resources = { 1201 pip: 'PIP', 1202 airplay: 'AirPlay', 1203 html5: 'HTML5', 1204 vimeo: 'Vimeo', 1205 youtube: 'YouTube' 1206 }; 1207 const i18n = { 1208 get(key = '', config = {}) { 1209 if (is.empty(key) || is.empty(config)) { 1210 return ''; 1211 } 1212 let string = getDeep(config.i18n, key); 1213 if (is.empty(string)) { 1214 if (Object.keys(resources).includes(key)) { 1215 return resources[key]; 1216 } 1217 return ''; 1218 } 1219 const replace = { 1220 '{seektime}': config.seekTime, 1221 '{title}': config.title 1222 }; 1223 Object.entries(replace).forEach(([k, v]) => { 1224 string = replaceAll(string, k, v); 1225 }); 1226 return string; 1227 } 1228 }; 1229 1230 class Storage { 1231 constructor(player) { 1232 _defineProperty$1(this, "get", key => { 1233 if (!Storage.supported || !this.enabled) { 1234 return null; 1235 } 1236 const store = window.localStorage.getItem(this.key); 1237 if (is.empty(store)) { 1238 return null; 1239 } 1240 const json = JSON.parse(store); 1241 return is.string(key) && key.length ? json[key] : json; 1242 }); 1243 _defineProperty$1(this, "set", object => { 1244 // Bail if we don't have localStorage support or it's disabled 1245 if (!Storage.supported || !this.enabled) { 1246 return; 1247 } 1248 1249 // Can only store objectst 1250 if (!is.object(object)) { 1251 return; 1252 } 1253 1254 // Get current storage 1255 let storage = this.get(); 1256 1257 // Default to empty object 1258 if (is.empty(storage)) { 1259 storage = {}; 1260 } 1261 1262 // Update the working copy of the values 1263 extend(storage, object); 1264 1265 // Update storage 1266 try { 1267 window.localStorage.setItem(this.key, JSON.stringify(storage)); 1268 } catch (_) { 1269 // Do nothing 1270 } 1271 }); 1272 this.enabled = player.config.storage.enabled; 1273 this.key = player.config.storage.key; 1274 } 1275 1276 // Check for actual support (see if we can use it) 1277 static get supported() { 1278 try { 1279 if (!('localStorage' in window)) { 1280 return false; 1281 } 1282 const test = '___test'; 1283 1284 // Try to use it (it might be disabled, e.g. user is in private mode) 1285 // see: https://github.com/sampotts/plyr/issues/131 1286 window.localStorage.setItem(test, test); 1287 window.localStorage.removeItem(test); 1288 return true; 1289 } catch (_) { 1290 return false; 1291 } 1292 } 1293 } 1294 1295 // ========================================================================== 1296 // Fetch wrapper 1297 // Using XHR to avoid issues with older browsers 1298 // ========================================================================== 1299 1300 function fetch(url, responseType = 'text') { 1301 return new Promise((resolve, reject) => { 1302 try { 1303 const request = new XMLHttpRequest(); 1304 1305 // Check for CORS support 1306 if (!('withCredentials' in request)) { 1307 return; 1308 } 1309 request.addEventListener('load', () => { 1310 if (responseType === 'text') { 1311 try { 1312 resolve(JSON.parse(request.responseText)); 1313 } catch (_) { 1314 resolve(request.responseText); 1315 } 1316 } else { 1317 resolve(request.response); 1318 } 1319 }); 1320 request.addEventListener('error', () => { 1321 throw new Error(request.status); 1322 }); 1323 request.open('GET', url, true); 1324 1325 // Set the required response type 1326 request.responseType = responseType; 1327 request.send(); 1328 } catch (error) { 1329 reject(error); 1330 } 1331 }); 1332 } 1333 1334 // ========================================================================== 1335 1336 // Load an external SVG sprite 1337 function loadSprite(url, id) { 1338 if (!is.string(url)) { 1339 return; 1340 } 1341 const prefix = 'cache'; 1342 const hasId = is.string(id); 1343 let isCached = false; 1344 const exists = () => document.getElementById(id) !== null; 1345 const update = (container, data) => { 1346 // eslint-disable-next-line no-param-reassign 1347 container.innerHTML = data; 1348 1349 // Check again incase of race condition 1350 if (hasId && exists()) { 1351 return; 1352 } 1353 1354 // Inject the SVG to the body 1355 document.body.insertAdjacentElement('afterbegin', container); 1356 }; 1357 1358 // Only load once if ID set 1359 if (!hasId || !exists()) { 1360 const useStorage = Storage.supported; 1361 // Create container 1362 const container = document.createElement('div'); 1363 container.setAttribute('hidden', ''); 1364 if (hasId) { 1365 container.setAttribute('id', id); 1366 } 1367 1368 // Check in cache 1369 if (useStorage) { 1370 const cached = window.localStorage.getItem(`${prefix}-${id}`); 1371 isCached = cached !== null; 1372 if (isCached) { 1373 const data = JSON.parse(cached); 1374 update(container, data.content); 1375 } 1376 } 1377 1378 // Get the sprite 1379 fetch(url).then(result => { 1380 if (is.empty(result)) { 1381 return; 1382 } 1383 if (useStorage) { 1384 try { 1385 window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({ 1386 content: result 1387 })); 1388 } catch (_) { 1389 // Do nothing 1390 } 1391 } 1392 update(container, result); 1393 }).catch(() => {}); 1394 } 1395 } 1396 1397 // ========================================================================== 1398 1399 // Time helpers 1400 const getHours = value => Math.trunc(value / 60 / 60 % 60, 10); 1401 const getMinutes = value => Math.trunc(value / 60 % 60, 10); 1402 const getSeconds = value => Math.trunc(value % 60, 10); 1403 1404 // Format time to UI friendly string 1405 function formatTime(time = 0, displayHours = false, inverted = false) { 1406 // Bail if the value isn't a number 1407 if (!is.number(time)) { 1408 return formatTime(undefined, displayHours, inverted); 1409 } 1410 1411 // Format time component to add leading zero 1412 const format = value => `0${value}`.slice(-2); 1413 // Breakdown to hours, mins, secs 1414 let hours = getHours(time); 1415 const mins = getMinutes(time); 1416 const secs = getSeconds(time); 1417 1418 // Do we need to display hours? 1419 if (displayHours || hours > 0) { 1420 hours = `${hours}:`; 1421 } else { 1422 hours = ''; 1423 } 1424 1425 // Render 1426 return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`; 1427 } 1428 1429 // ========================================================================== 1430 1431 // TODO: Don't export a massive object - break down and create class 1432 const controls = { 1433 // Get icon URL 1434 getIconUrl() { 1435 const url = new URL(this.config.iconUrl, window.location); 1436 const host = window.location.host ? window.location.host : window.top.location.host; 1437 const cors = url.host !== host || browser.isIE && !window.svg4everybody; 1438 return { 1439 url: this.config.iconUrl, 1440 cors 1441 }; 1442 }, 1443 // Find the UI controls 1444 findElements() { 1445 try { 1446 this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); 1447 1448 // Buttons 1449 this.elements.buttons = { 1450 play: getElements.call(this, this.config.selectors.buttons.play), 1451 pause: getElement.call(this, this.config.selectors.buttons.pause), 1452 restart: getElement.call(this, this.config.selectors.buttons.restart), 1453 rewind: getElement.call(this, this.config.selectors.buttons.rewind), 1454 fastForward: getElement.call(this, this.config.selectors.buttons.fastForward), 1455 mute: getElement.call(this, this.config.selectors.buttons.mute), 1456 pip: getElement.call(this, this.config.selectors.buttons.pip), 1457 airplay: getElement.call(this, this.config.selectors.buttons.airplay), 1458 settings: getElement.call(this, this.config.selectors.buttons.settings), 1459 captions: getElement.call(this, this.config.selectors.buttons.captions), 1460 fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen) 1461 }; 1462 1463 // Progress 1464 this.elements.progress = getElement.call(this, this.config.selectors.progress); 1465 1466 // Inputs 1467 this.elements.inputs = { 1468 seek: getElement.call(this, this.config.selectors.inputs.seek), 1469 volume: getElement.call(this, this.config.selectors.inputs.volume) 1470 }; 1471 1472 // Display 1473 this.elements.display = { 1474 buffer: getElement.call(this, this.config.selectors.display.buffer), 1475 currentTime: getElement.call(this, this.config.selectors.display.currentTime), 1476 duration: getElement.call(this, this.config.selectors.display.duration) 1477 }; 1478 1479 // Seek tooltip 1480 if (is.element(this.elements.progress)) { 1481 this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`); 1482 } 1483 return true; 1484 } catch (error) { 1485 // Log it 1486 this.debug.warn('It looks like there is a problem with your custom controls HTML', error); 1487 1488 // Restore native video controls 1489 this.toggleNativeControls(true); 1490 return false; 1491 } 1492 }, 1493 // Create <svg> icon 1494 createIcon(type, attributes) { 1495 const namespace = 'http://www.w3.org/2000/svg'; 1496 const iconUrl = controls.getIconUrl.call(this); 1497 const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; 1498 // Create <svg> 1499 const icon = document.createElementNS(namespace, 'svg'); 1500 setAttributes(icon, extend(attributes, { 1501 'aria-hidden': 'true', 1502 focusable: 'false' 1503 })); 1504 1505 // Create the <use> to reference sprite 1506 const use = document.createElementNS(namespace, 'use'); 1507 const path = `${iconPath}-${type}`; 1508 1509 // Set `href` attributes 1510 // https://github.com/sampotts/plyr/issues/460 1511 // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href 1512 if ('href' in use) { 1513 use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); 1514 } 1515 1516 // Always set the older attribute even though it's "deprecated" (it'll be around for ages) 1517 use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); 1518 1519 // Add <use> to <svg> 1520 icon.appendChild(use); 1521 return icon; 1522 }, 1523 // Create hidden text label 1524 createLabel(key, attr = {}) { 1525 const text = i18n.get(key, this.config); 1526 const attributes = { 1527 ...attr, 1528 class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') 1529 }; 1530 return createElement('span', attributes, text); 1531 }, 1532 // Create a badge 1533 createBadge(text) { 1534 if (is.empty(text)) { 1535 return null; 1536 } 1537 const badge = createElement('span', { 1538 class: this.config.classNames.menu.value 1539 }); 1540 badge.appendChild(createElement('span', { 1541 class: this.config.classNames.menu.badge 1542 }, text)); 1543 return badge; 1544 }, 1545 // Create a <button> 1546 createButton(buttonType, attr) { 1547 const attributes = extend({}, attr); 1548 let type = toCamelCase(buttonType); 1549 const props = { 1550 element: 'button', 1551 toggle: false, 1552 label: null, 1553 icon: null, 1554 labelPressed: null, 1555 iconPressed: null 1556 }; 1557 ['element', 'icon', 'label'].forEach(key => { 1558 if (Object.keys(attributes).includes(key)) { 1559 props[key] = attributes[key]; 1560 delete attributes[key]; 1561 } 1562 }); 1563 1564 // Default to 'button' type to prevent form submission 1565 if (props.element === 'button' && !Object.keys(attributes).includes('type')) { 1566 attributes.type = 'button'; 1567 } 1568 1569 // Set class name 1570 if (Object.keys(attributes).includes('class')) { 1571 if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) { 1572 extend(attributes, { 1573 class: `${attributes.class} ${this.config.classNames.control}` 1574 }); 1575 } 1576 } else { 1577 attributes.class = this.config.classNames.control; 1578 } 1579 1580 // Large play button 1581 switch (buttonType) { 1582 case 'play': 1583 props.toggle = true; 1584 props.label = 'play'; 1585 props.labelPressed = 'pause'; 1586 props.icon = 'play'; 1587 props.iconPressed = 'pause'; 1588 break; 1589 case 'mute': 1590 props.toggle = true; 1591 props.label = 'mute'; 1592 props.labelPressed = 'unmute'; 1593 props.icon = 'volume'; 1594 props.iconPressed = 'muted'; 1595 break; 1596 case 'captions': 1597 props.toggle = true; 1598 props.label = 'enableCaptions'; 1599 props.labelPressed = 'disableCaptions'; 1600 props.icon = 'captions-off'; 1601 props.iconPressed = 'captions-on'; 1602 break; 1603 case 'fullscreen': 1604 props.toggle = true; 1605 props.label = 'enterFullscreen'; 1606 props.labelPressed = 'exitFullscreen'; 1607 props.icon = 'enter-fullscreen'; 1608 props.iconPressed = 'exit-fullscreen'; 1609 break; 1610 case 'play-large': 1611 attributes.class += ` ${this.config.classNames.control}--overlaid`; 1612 type = 'play'; 1613 props.label = 'play'; 1614 props.icon = 'play'; 1615 break; 1616 default: 1617 if (is.empty(props.label)) { 1618 props.label = type; 1619 } 1620 if (is.empty(props.icon)) { 1621 props.icon = buttonType; 1622 } 1623 } 1624 const button = createElement(props.element); 1625 1626 // Setup toggle icon and labels 1627 if (props.toggle) { 1628 // Icon 1629 button.appendChild(controls.createIcon.call(this, props.iconPressed, { 1630 class: 'icon--pressed' 1631 })); 1632 button.appendChild(controls.createIcon.call(this, props.icon, { 1633 class: 'icon--not-pressed' 1634 })); 1635 1636 // Label/Tooltip 1637 button.appendChild(controls.createLabel.call(this, props.labelPressed, { 1638 class: 'label--pressed' 1639 })); 1640 button.appendChild(controls.createLabel.call(this, props.label, { 1641 class: 'label--not-pressed' 1642 })); 1643 } else { 1644 button.appendChild(controls.createIcon.call(this, props.icon)); 1645 button.appendChild(controls.createLabel.call(this, props.label)); 1646 } 1647 1648 // Merge and set attributes 1649 extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); 1650 setAttributes(button, attributes); 1651 1652 // We have multiple play buttons 1653 if (type === 'play') { 1654 if (!is.array(this.elements.buttons[type])) { 1655 this.elements.buttons[type] = []; 1656 } 1657 this.elements.buttons[type].push(button); 1658 } else { 1659 this.elements.buttons[type] = button; 1660 } 1661 return button; 1662 }, 1663 // Create an <input type='range'> 1664 createRange(type, attributes) { 1665 // Seek input 1666 const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { 1667 type: 'range', 1668 min: 0, 1669 max: 100, 1670 step: 0.01, 1671 value: 0, 1672 autocomplete: 'off', 1673 // A11y fixes for https://github.com/sampotts/plyr/issues/905 1674 role: 'slider', 1675 'aria-label': i18n.get(type, this.config), 1676 'aria-valuemin': 0, 1677 'aria-valuemax': 100, 1678 'aria-valuenow': 0 1679 }, attributes)); 1680 this.elements.inputs[type] = input; 1681 1682 // Set the fill for webkit now 1683 controls.updateRangeFill.call(this, input); 1684 1685 // Improve support on touch devices 1686 RangeTouch.setup(input); 1687 return input; 1688 }, 1689 // Create a <progress> 1690 createProgress(type, attributes) { 1691 const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), { 1692 min: 0, 1693 max: 100, 1694 value: 0, 1695 role: 'progressbar', 1696 'aria-hidden': true 1697 }, attributes)); 1698 1699 // Create the label inside 1700 if (type !== 'volume') { 1701 progress.appendChild(createElement('span', null, '0')); 1702 const suffixKey = { 1703 played: 'played', 1704 buffer: 'buffered' 1705 }[type]; 1706 const suffix = suffixKey ? i18n.get(suffixKey, this.config) : ''; 1707 progress.innerText = `% ${suffix.toLowerCase()}`; 1708 } 1709 this.elements.display[type] = progress; 1710 return progress; 1711 }, 1712 // Create time display 1713 createTime(type, attrs) { 1714 const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs); 1715 const container = createElement('div', extend(attributes, { 1716 class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(), 1717 'aria-label': i18n.get(type, this.config), 1718 role: 'timer' 1719 }), '00:00'); 1720 1721 // Reference for updates 1722 this.elements.display[type] = container; 1723 return container; 1724 }, 1725 // Bind keyboard shortcuts for a menu item 1726 // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus 1727 // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 1728 bindMenuItemShortcuts(menuItem, type) { 1729 // Navigate through menus via arrow keys and space 1730 on.call(this, menuItem, 'keydown keyup', event => { 1731 // We only care about space and â¬ï¸ â¬ï¸ï¸ â¡ï¸ 1732 if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) { 1733 return; 1734 } 1735 1736 // Prevent play / seek 1737 event.preventDefault(); 1738 event.stopPropagation(); 1739 1740 // We're just here to prevent the keydown bubbling 1741 if (event.type === 'keydown') { 1742 return; 1743 } 1744 const isRadioButton = matches(menuItem, '[role="menuitemradio"]'); 1745 1746 // Show the respective menu 1747 if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) { 1748 controls.showMenuPanel.call(this, type, true); 1749 } else { 1750 let target; 1751 if (event.key !== ' ') { 1752 if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') { 1753 target = menuItem.nextElementSibling; 1754 if (!is.element(target)) { 1755 target = menuItem.parentNode.firstElementChild; 1756 } 1757 } else { 1758 target = menuItem.previousElementSibling; 1759 if (!is.element(target)) { 1760 target = menuItem.parentNode.lastElementChild; 1761 } 1762 } 1763 setFocus.call(this, target, true); 1764 } 1765 } 1766 }, false); 1767 1768 // Enter will fire a `click` event but we still need to manage focus 1769 // So we bind to keyup which fires after and set focus here 1770 on.call(this, menuItem, 'keyup', event => { 1771 if (event.key !== 'Return') return; 1772 controls.focusFirstMenuItem.call(this, null, true); 1773 }); 1774 }, 1775 // Create a settings menu item 1776 createMenuItem({ 1777 value, 1778 list, 1779 type, 1780 title, 1781 badge = null, 1782 checked = false 1783 }) { 1784 const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); 1785 const menuItem = createElement('button', extend(attributes, { 1786 type: 'button', 1787 role: 'menuitemradio', 1788 class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(), 1789 'aria-checked': checked, 1790 value 1791 })); 1792 const flex = createElement('span'); 1793 1794 // We have to set as HTML incase of special characters 1795 flex.innerHTML = title; 1796 if (is.element(badge)) { 1797 flex.appendChild(badge); 1798 } 1799 menuItem.appendChild(flex); 1800 1801 // Replicate radio button behavior 1802 Object.defineProperty(menuItem, 'checked', { 1803 enumerable: true, 1804 get() { 1805 return menuItem.getAttribute('aria-checked') === 'true'; 1806 }, 1807 set(check) { 1808 // Ensure exclusivity 1809 if (check) { 1810 Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role="menuitemradio"]')).forEach(node => node.setAttribute('aria-checked', 'false')); 1811 } 1812 menuItem.setAttribute('aria-checked', check ? 'true' : 'false'); 1813 } 1814 }); 1815 this.listeners.bind(menuItem, 'click keyup', event => { 1816 if (is.keyboardEvent(event) && event.key !== ' ') { 1817 return; 1818 } 1819 event.preventDefault(); 1820 event.stopPropagation(); 1821 menuItem.checked = true; 1822 switch (type) { 1823 case 'language': 1824 this.currentTrack = Number(value); 1825 break; 1826 case 'quality': 1827 this.quality = value; 1828 break; 1829 case 'speed': 1830 this.speed = parseFloat(value); 1831 break; 1832 } 1833 controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event)); 1834 }, type, false); 1835 controls.bindMenuItemShortcuts.call(this, menuItem, type); 1836 list.appendChild(menuItem); 1837 }, 1838 // Format a time for display 1839 formatTime(time = 0, inverted = false) { 1840 // Bail if the value isn't a number 1841 if (!is.number(time)) { 1842 return time; 1843 } 1844 1845 // Always display hours if duration is over an hour 1846 const forceHours = getHours(this.duration) > 0; 1847 return formatTime(time, forceHours, inverted); 1848 }, 1849 // Update the displayed time 1850 updateTimeDisplay(target = null, time = 0, inverted = false) { 1851 // Bail if there's no element to display or the value isn't a number 1852 if (!is.element(target) || !is.number(time)) { 1853 return; 1854 } 1855 1856 // eslint-disable-next-line no-param-reassign 1857 target.innerText = controls.formatTime(time, inverted); 1858 }, 1859 // Update volume UI and storage 1860 updateVolume() { 1861 if (!this.supported.ui) { 1862 return; 1863 } 1864 1865 // Update range 1866 if (is.element(this.elements.inputs.volume)) { 1867 controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume); 1868 } 1869 1870 // Update mute state 1871 if (is.element(this.elements.buttons.mute)) { 1872 this.elements.buttons.mute.pressed = this.muted || this.volume === 0; 1873 } 1874 }, 1875 // Update seek value and lower fill 1876 setRange(target, value = 0) { 1877 if (!is.element(target)) { 1878 return; 1879 } 1880 1881 // eslint-disable-next-line 1882 target.value = value; 1883 1884 // Webkit range fill 1885 controls.updateRangeFill.call(this, target); 1886 }, 1887 // Update <progress> elements 1888 updateProgress(event) { 1889 if (!this.supported.ui || !is.event(event)) { 1890 return; 1891 } 1892 let value = 0; 1893 const setProgress = (target, input) => { 1894 const val = is.number(input) ? input : 0; 1895 const progress = is.element(target) ? target : this.elements.display.buffer; 1896 1897 // Update value and label 1898 if (is.element(progress)) { 1899 progress.value = val; 1900 1901 // Update text label inside 1902 const label = progress.getElementsByTagName('span')[0]; 1903 if (is.element(label)) { 1904 label.childNodes[0].nodeValue = val; 1905 } 1906 } 1907 }; 1908 if (event) { 1909 switch (event.type) { 1910 // Video playing 1911 case 'timeupdate': 1912 case 'seeking': 1913 case 'seeked': 1914 value = getPercentage(this.currentTime, this.duration); 1915 1916 // Set seek range value only if it's a 'natural' time event 1917 if (event.type === 'timeupdate') { 1918 controls.setRange.call(this, this.elements.inputs.seek, value); 1919 } 1920 break; 1921 1922 // Check buffer status 1923 case 'playing': 1924 case 'progress': 1925 setProgress(this.elements.display.buffer, this.buffered * 100); 1926 break; 1927 } 1928 } 1929 }, 1930 // Webkit polyfill for lower fill range 1931 updateRangeFill(target) { 1932 // Get range from event if event passed 1933 const range = is.event(target) ? target.target : target; 1934 1935 // Needs to be a valid <input type='range'> 1936 if (!is.element(range) || range.getAttribute('type') !== 'range') { 1937 return; 1938 } 1939 1940 // Set aria values for https://github.com/sampotts/plyr/issues/905 1941 if (matches(range, this.config.selectors.inputs.seek)) { 1942 range.setAttribute('aria-valuenow', this.currentTime); 1943 const currentTime = controls.formatTime(this.currentTime); 1944 const duration = controls.formatTime(this.duration); 1945 const format = i18n.get('seekLabel', this.config); 1946 range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration)); 1947 } else if (matches(range, this.config.selectors.inputs.volume)) { 1948 const percent = range.value * 100; 1949 range.setAttribute('aria-valuenow', percent); 1950 range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`); 1951 } else { 1952 range.setAttribute('aria-valuenow', range.value); 1953 } 1954 1955 // WebKit only 1956 if (!browser.isWebKit && !browser.isIPadOS) { 1957 return; 1958 } 1959 1960 // Set CSS custom property 1961 range.style.setProperty('--value', `${range.value / range.max * 100}%`); 1962 }, 1963 // Update hover tooltip for seeking 1964 updateSeekTooltip(event) { 1965 var _this$config$markers, _this$config$markers$; 1966 // Bail if setting not true 1967 if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) { 1968 return; 1969 } 1970 const tipElement = this.elements.display.seekTooltip; 1971 const visible = `${this.config.classNames.tooltip}--visible`; 1972 const toggle = show => toggleClass(tipElement, visible, show); 1973 1974 // Hide on touch 1975 if (this.touch) { 1976 toggle(false); 1977 return; 1978 } 1979 1980 // Determine percentage, if already visible 1981 let percent = 0; 1982 const clientRect = this.elements.progress.getBoundingClientRect(); 1983 if (is.event(event)) { 1984 percent = 100 / clientRect.width * (event.pageX - clientRect.left); 1985 } else if (hasClass(tipElement, visible)) { 1986 percent = parseFloat(tipElement.style.left, 10); 1987 } else { 1988 return; 1989 } 1990 1991 // Set bounds 1992 if (percent < 0) { 1993 percent = 0; 1994 } else if (percent > 100) { 1995 percent = 100; 1996 } 1997 const time = this.duration / 100 * percent; 1998 1999 // Display the time a click would seek to 2000 tipElement.innerTex
vendor: 22,956 bytes, lines 2000-2655
2000t = controls.formatTime(time); 2001 2002 // Get marker point for time 2003 const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({ 2004 time: t 2005 }) => t === Math.round(time)); 2006 2007 // Append the point label to the tooltip 2008 if (point) { 2009 tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`); 2010 } 2011 2012 // Set position 2013 tipElement.style.left = `${percent}%`; 2014 2015 // Show/hide the tooltip 2016 // If the event is a moues in/out and percentage is inside bounds 2017 if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) { 2018 toggle(event.type === 'mouseenter'); 2019 } 2020 }, 2021 // Handle time change event 2022 timeUpdate(event) { 2023 // Only invert if only one time element is displayed and used for both duration and currentTime 2024 const invert = !is.element(this.elements.display.duration) && this.config.invertTime; 2025 2026 // Duration 2027 controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); 2028 2029 // Ignore updates while seeking 2030 if (event && event.type === 'timeupdate' && this.media.seeking) { 2031 return; 2032 } 2033 2034 // Playing progress 2035 controls.updateProgress.call(this, event); 2036 }, 2037 // Show the duration on metadataloaded or durationchange events 2038 durationUpdate() { 2039 // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false 2040 if (!this.supported.ui || !this.config.invertTime && this.currentTime) { 2041 return; 2042 } 2043 2044 // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar. 2045 // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415 2046 // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062 2047 // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338 2048 if (this.duration >= 2 ** 32) { 2049 toggleHidden(this.elements.display.currentTime, true); 2050 toggleHidden(this.elements.progress, true); 2051 return; 2052 } 2053 2054 // Update ARIA values 2055 if (is.element(this.elements.inputs.seek)) { 2056 this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration); 2057 } 2058 2059 // If there's a spot to display duration 2060 const hasDuration = is.element(this.elements.display.duration); 2061 2062 // If there's only one time display, display duration there 2063 if (!hasDuration && this.config.displayDuration && this.paused) { 2064 controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration); 2065 } 2066 2067 // If there's a duration element, update content 2068 if (hasDuration) { 2069 controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); 2070 } 2071 if (this.config.markers.enabled) { 2072 controls.setMarkers.call(this); 2073 } 2074 2075 // Update the tooltip (if visible) 2076 controls.updateSeekTooltip.call(this); 2077 }, 2078 // Hide/show a tab 2079 toggleMenuButton(setting, toggle) { 2080 toggleHidden(this.elements.settings.buttons[setting], !toggle); 2081 }, 2082 // Update the selected setting 2083 updateSetting(setting, container, input) { 2084 const pane = this.elements.settings.panels[setting]; 2085 let value = null; 2086 let list = container; 2087 if (setting === 'captions') { 2088 value = this.currentTrack; 2089 } else { 2090 value = !is.empty(input) ? input : this[setting]; 2091 2092 // Get default 2093 if (is.empty(value)) { 2094 value = this.config[setting].default; 2095 } 2096 2097 // Unsupported value 2098 if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { 2099 this.debug.warn(`Unsupported value of '${value}' for ${setting}`); 2100 return; 2101 } 2102 2103 // Disabled value 2104 if (!this.config[setting].options.includes(value)) { 2105 this.debug.warn(`Disabled value of '${value}' for ${setting}`); 2106 return; 2107 } 2108 } 2109 2110 // Get the list if we need to 2111 if (!is.element(list)) { 2112 list = pane && pane.querySelector('[role="menu"]'); 2113 } 2114 2115 // If there's no list it means it's not been rendered... 2116 if (!is.element(list)) { 2117 return; 2118 } 2119 2120 // Update the label 2121 const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`); 2122 label.innerHTML = controls.getLabel.call(this, setting, value); 2123 2124 // Find the radio option and check it 2125 const target = list && list.querySelector(`[value="${value}"]`); 2126 if (is.element(target)) { 2127 target.checked = true; 2128 } 2129 }, 2130 // Translate a value into a nice label 2131 getLabel(setting, value) { 2132 switch (setting) { 2133 case 'speed': 2134 return value === 1 ? i18n.get('normal', this.config) : `${value}×`; 2135 case 'quality': 2136 if (is.number(value)) { 2137 const label = i18n.get(`qualityLabel.${value}`, this.config); 2138 if (!label.length) { 2139 return `${value}p`; 2140 } 2141 return label; 2142 } 2143 return toTitleCase(value); 2144 case 'captions': 2145 return captions.getLabel.call(this); 2146 default: 2147 return null; 2148 } 2149 }, 2150 // Set the quality menu 2151 setQualityMenu(options) { 2152 // Menu required 2153 if (!is.element(this.elements.settings.panels.quality)) { 2154 return; 2155 } 2156 const type = 'quality'; 2157 const list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); 2158 2159 // Set options if passed and filter based on uniqueness and config 2160 if (is.array(options)) { 2161 this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality)); 2162 } 2163 2164 // Toggle the pane and tab 2165 const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; 2166 controls.toggleMenuButton.call(this, type, toggle); 2167 2168 // Empty the menu 2169 emptyElement(list); 2170 2171 // Check if we need to toggle the parent 2172 controls.checkMenu.call(this); 2173 2174 // If we're hiding, nothing more to do 2175 if (!toggle) { 2176 return; 2177 } 2178 2179 // Get the badge HTML for HD, 4K etc 2180 const getBadge = quality => { 2181 const label = i18n.get(`qualityBadge.${quality}`, this.config); 2182 if (!label.length) { 2183 return null; 2184 } 2185 return controls.createBadge.call(this, label); 2186 }; 2187 2188 // Sort options by the config and then render options 2189 this.options.quality.sort((a, b) => { 2190 const sorting = this.config.quality.options; 2191 return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; 2192 }).forEach(quality => { 2193 controls.createMenuItem.call(this, { 2194 value: quality, 2195 list, 2196 type, 2197 title: controls.getLabel.call(this, 'quality', quality), 2198 badge: getBadge(quality) 2199 }); 2200 }); 2201 controls.updateSetting.call(this, type, list); 2202 }, 2203 // Set the looping options 2204 /* setLoopMenu() { 2205 // Menu required 2206 if (!is.element(this.elements.settings.panels.loop)) { 2207 return; 2208 } 2209 const options = ['start', 'end', 'all', 'reset']; 2210 const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); 2211 // Show the pane and tab 2212 toggleHidden(this.elements.settings.buttons.loop, false); 2213 toggleHidden(this.elements.settings.panels.loop, false); 2214 // Toggle the pane and tab 2215 const toggle = !is.empty(this.loop.options); 2216 controls.toggleMenuButton.call(this, 'loop', toggle); 2217 // Empty the menu 2218 emptyElement(list); 2219 options.forEach(option => { 2220 const item = createElement('li'); 2221 const button = createElement( 2222 'button', 2223 extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { 2224 type: 'button', 2225 class: this.config.classNames.control, 2226 'data-plyr-loop-action': option, 2227 }), 2228 i18n.get(option, this.config) 2229 ); 2230 if (['start', 'end'].includes(option)) { 2231 const badge = controls.createBadge.call(this, '00:00'); 2232 button.appendChild(badge); 2233 } 2234 item.appendChild(button); 2235 list.appendChild(item); 2236 }); 2237 }, */ 2238 2239 // Get current selected caption language 2240 // TODO: rework this to user the getter in the API? 2241 2242 // Set a list of available captions languages 2243 setCaptionsMenu() { 2244 // Menu required 2245 if (!is.element(this.elements.settings.panels.captions)) { 2246 return; 2247 } 2248 2249 // TODO: Captions or language? Currently it's mixed 2250 const type = 'captions'; 2251 const list = this.elements.settings.panels.captions.querySelector('[role="menu"]'); 2252 const tracks = captions.getTracks.call(this); 2253 const toggle = Boolean(tracks.length); 2254 2255 // Toggle the pane and tab 2256 controls.toggleMenuButton.call(this, type, toggle); 2257 2258 // Empty the menu 2259 emptyElement(list); 2260 2261 // Check if we need to toggle the parent 2262 controls.checkMenu.call(this); 2263 2264 // If there's no captions, bail 2265 if (!toggle) { 2266 return; 2267 } 2268 2269 // Generate options data 2270 const options = tracks.map((track, value) => ({ 2271 value, 2272 checked: this.captions.toggled && this.currentTrack === value, 2273 title: captions.getLabel.call(this, track), 2274 badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()), 2275 list, 2276 type: 'language' 2277 })); 2278 2279 // Add the "Disabled" option to turn off captions 2280 options.unshift({ 2281 value: -1, 2282 checked: !this.captions.toggled, 2283 title: i18n.get('disabled', this.config), 2284 list, 2285 type: 'language' 2286 }); 2287 2288 // Generate options 2289 options.forEach(controls.createMenuItem.bind(this)); 2290 controls.updateSetting.call(this, type, list); 2291 }, 2292 // Set a list of available captions languages 2293 setSpeedMenu() { 2294 // Menu required 2295 if (!is.element(this.elements.settings.panels.speed)) { 2296 return; 2297 } 2298 const type = 'speed'; 2299 const list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); 2300 2301 // Filter out invalid speeds 2302 this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed); 2303 2304 // Toggle the pane and tab 2305 const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; 2306 controls.toggleMenuButton.call(this, type, toggle); 2307 2308 // Empty the menu 2309 emptyElement(list); 2310 2311 // Check if we need to toggle the parent 2312 controls.checkMenu.call(this); 2313 2314 // If we're hiding, nothing more to do 2315 if (!toggle) { 2316 return; 2317 } 2318 2319 // Create items 2320 this.options.speed.forEach(speed => { 2321 controls.createMenuItem.call(this, { 2322 value: speed, 2323 list, 2324 type, 2325 title: controls.getLabel.call(this, 'speed', speed) 2326 }); 2327 }); 2328 controls.updateSetting.call(this, type, list); 2329 }, 2330 // Check if we need to hide/show the settings menu 2331 checkMenu() { 2332 const { 2333 buttons 2334 } = this.elements.settings; 2335 const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden); 2336 toggleHidden(this.elements.settings.menu, !visible); 2337 }, 2338 // Focus the first menu item in a given (or visible) menu 2339 focusFirstMenuItem(pane, focusVisible = false) { 2340 if (this.elements.settings.popup.hidden) { 2341 return; 2342 } 2343 let target = pane; 2344 if (!is.element(target)) { 2345 target = Object.values(this.elements.settings.panels).find(p => !p.hidden); 2346 } 2347 const firstItem = target.querySelector('[role^="menuitem"]'); 2348 setFocus.call(this, firstItem, focusVisible); 2349 }, 2350 // Show/hide menu 2351 toggleMenu(input) { 2352 const { 2353 popup 2354 } = this.elements.settings; 2355 const button = this.elements.buttons.settings; 2356 2357 // Menu and button are required 2358 if (!is.element(popup) || !is.element(button)) { 2359 return; 2360 } 2361 2362 // True toggle by default 2363 const { 2364 hidden 2365 } = popup; 2366 let show = hidden; 2367 if (is.boolean(input)) { 2368 show = input; 2369 } else if (is.keyboardEvent(input) && input.key === 'Escape') { 2370 show = false; 2371 } else if (is.event(input)) { 2372 // If Plyr is in a shadowDOM, the event target is set to the component, instead of the 2373 // Element in the shadowDOM. The path, if available, is complete. 2374 const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target; 2375 const isMenuItem = popup.contains(target); 2376 2377 // If the click was inside the menu or if the click 2378 // wasn't the button or menu item and we're trying to 2379 // show the menu (a doc click shouldn't show the menu) 2380 if (isMenuItem || !isMenuItem && input.target !== button && show) { 2381 return; 2382 } 2383 } 2384 2385 // Set button attributes 2386 button.setAttribute('aria-expanded', show); 2387 2388 // Show the actual popup 2389 toggleHidden(popup, !show); 2390 2391 // Add class hook 2392 toggleClass(this.elements.container, this.config.classNames.menu.open, show); 2393 2394 // Focus the first item if key interaction 2395 if (show && is.keyboardEvent(input)) { 2396 controls.focusFirstMenuItem.call(this, null, true); 2397 } else if (!show && !hidden) { 2398 // If closing, re-focus the button 2399 setFocus.call(this, button, is.keyboardEvent(input)); 2400 } 2401 }, 2402 // Get the natural size of a menu panel 2403 getMenuSize(tab) { 2404 const clone = tab.cloneNode(true); 2405 clone.style.position = 'absolute'; 2406 clone.style.opacity = 0; 2407 clone.removeAttribute('hidden'); 2408 2409 // Append to parent so we get the "real" size 2410 tab.parentNode.appendChild(clone); 2411 2412 // Get the sizes before we remove 2413 const width = clone.scrollWidth; 2414 const height = clone.scrollHeight; 2415 2416 // Remove from the DOM 2417 removeElement(clone); 2418 return { 2419 width, 2420 height 2421 }; 2422 }, 2423 // Show a panel in the menu 2424 showMenuPanel(type = '', focusVisible = false) { 2425 const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`); 2426 2427 // Nothing to show, bail 2428 if (!is.element(target)) { 2429 return; 2430 } 2431 2432 // Hide all other panels 2433 const container = target.parentNode; 2434 const current = Array.from(container.children).find(node => !node.hidden); 2435 2436 // If we can do fancy animations, we'll animate the height/width 2437 if (support.transitions && !support.reducedMotion) { 2438 // Set the current width as a base 2439 container.style.width = `${current.scrollWidth}px`; 2440 container.style.height = `${current.scrollHeight}px`; 2441 2442 // Get potential sizes 2443 const size = controls.getMenuSize.call(this, target); 2444 2445 // Restore auto height/width 2446 const restore = event => { 2447 // We're only bothered about height and width on the container 2448 if (event.target !== container || !['width', 'height'].includes(event.propertyName)) { 2449 return; 2450 } 2451 2452 // Revert back to auto 2453 container.style.width = ''; 2454 container.style.height = ''; 2455 2456 // Only listen once 2457 off.call(this, container, transitionEndEvent, restore); 2458 }; 2459 2460 // Listen for the transition finishing and restore auto height/width 2461 on.call(this, container, transitionEndEvent, restore); 2462 2463 // Set dimensions to target 2464 container.style.width = `${size.width}px`; 2465 container.style.height = `${size.height}px`; 2466 } 2467 2468 // Set attributes on current tab 2469 toggleHidden(current, true); 2470 2471 // Set attributes on target 2472 toggleHidden(target, false); 2473 2474 // Focus the first item 2475 controls.focusFirstMenuItem.call(this, target, focusVisible); 2476 }, 2477 // Set the download URL 2478 setDownloadUrl() { 2479 const button = this.elements.buttons.download; 2480 2481 // Bail if no button 2482 if (!is.element(button)) { 2483 return; 2484 } 2485 2486 // Set attribute 2487 button.setAttribute('href', this.download); 2488 }, 2489 // Build the default HTML 2490 create(data) { 2491 const { 2492 bindMenuItemShortcuts, 2493 createButton, 2494 createProgress, 2495 createRange, 2496 createTime, 2497 setQualityMenu, 2498 setSpeedMenu, 2499 showMenuPanel 2500 } = controls; 2501 this.elements.controls = null; 2502 2503 // Larger overlaid play button 2504 if (is.array(this.config.controls) && this.config.controls.includes('play-large')) { 2505 this.elements.container.appendChild(createButton.call(this, 'play-large')); 2506 } 2507 2508 // Create the container 2509 const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); 2510 this.elements.controls = container; 2511 2512 // Default item attributes 2513 const defaultAttributes = { 2514 class: 'plyr__controls__item' 2515 }; 2516 2517 // Loop through controls in order 2518 dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => { 2519 // Restart button 2520 if (control === 'restart') { 2521 container.appendChild(createButton.call(this, 'restart', defaultAttributes)); 2522 } 2523 2524 // Rewind button 2525 if (control === 'rewind') { 2526 container.appendChild(createButton.call(this, 'rewind', defaultAttributes)); 2527 } 2528 2529 // Play/Pause button 2530 if (control === 'play') { 2531 container.appendChild(createButton.call(this, 'play', defaultAttributes)); 2532 } 2533 2534 // Fast forward button 2535 if (control === 'fast-forward') { 2536 container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes)); 2537 } 2538 2539 // Progress 2540 if (control === 'progress') { 2541 const progressContainer = createElement('div', { 2542 class: `${defaultAttributes.class} plyr__progress__container` 2543 }); 2544 const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); 2545 2546 // Seek range slider 2547 progress.appendChild(createRange.call(this, 'seek', { 2548 id: `plyr-seek-${data.id}` 2549 })); 2550 2551 // Buffer progress 2552 progress.appendChild(createProgress.call(this, 'buffer')); 2553 2554 // TODO: Add loop display indicator 2555 2556 // Seek tooltip 2557 if (this.config.tooltips.seek) { 2558 const tooltip = createElement('span', { 2559 class: this.config.classNames.tooltip 2560 }, '00:00'); 2561 progress.appendChild(tooltip); 2562 this.elements.display.seekTooltip = tooltip; 2563 } 2564 this.elements.progress = progress; 2565 progressContainer.appendChild(this.elements.progress); 2566 container.appendChild(progressContainer); 2567 } 2568 2569 // Media current time display 2570 if (control === 'current-time') { 2571 container.appendChild(createTime.call(this, 'currentTime', defaultAttributes)); 2572 } 2573 2574 // Media duration display 2575 if (control === 'duration') { 2576 container.appendChild(createTime.call(this, 'duration', defaultAttributes)); 2577 } 2578 2579 // Volume controls 2580 if (control === 'mute' || control === 'volume') { 2581 let { 2582 volume 2583 } = this.elements; 2584 2585 // Create the volume container if needed 2586 if (!is.element(volume) || !container.contains(volume)) { 2587 volume = createElement('div', extend({}, defaultAttributes, { 2588 class: `${defaultAttributes.class} plyr__volume`.trim() 2589 })); 2590 this.elements.volume = volume; 2591 container.appendChild(volume); 2592 } 2593 2594 // Toggle mute button 2595 if (control === 'mute') { 2596 volume.appendChild(createButton.call(this, 'mute')); 2597 } 2598 2599 // Volume range control 2600 // Ignored on iOS as it's handled globally 2601 // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html 2602 if (control === 'volume' && !browser.isIos && !browser.isIPadOS) { 2603 // Set the attributes 2604 const attributes = { 2605 max: 1, 2606 step: 0.05, 2607 value: this.config.volume 2608 }; 2609 2610 // Create the volume range slider 2611 volume.appendChild(createRange.call(this, 'volume', extend(attributes, { 2612 id: `plyr-volume-${data.id}` 2613 }))); 2614 } 2615 } 2616 2617 // Toggle captions button 2618 if (control === 'captions') { 2619 container.appendChild(createButton.call(this, 'captions', defaultAttributes)); 2620 } 2621 2622 // Settings button / menu 2623 if (control === 'settings' && !is.empty(this.config.settings)) { 2624 const wrapper = createElement('div', extend({}, defaultAttributes, { 2625 class: `${defaultAttributes.class} plyr__menu`.trim(), 2626 hidden: '' 2627 })); 2628 wrapper.appendChild(createButton.call(this, 'settings', { 2629 'aria-haspopup': true, 2630 'aria-controls': `plyr-settings-${data.id}`, 2631 'aria-expanded': false 2632 })); 2633 const popup = createElement('div', { 2634 class: 'plyr__menu__container', 2635 id: `plyr-settings-${data.id}`, 2636 hidden: '' 2637 }); 2638 const inner = createElement('div'); 2639 const home = createElement('div', { 2640 id: `plyr-settings-${data.id}-home` 2641 }); 2642 2643 // Create the menu 2644 const menu = createElement('div', { 2645 role: 'menu' 2646 }); 2647 home.appendChild(menu); 2648 inner.appendChild(home); 2649 this.elements.settings.panels.home = home; 2650 2651 // Build the menu items 2652 this.config.settings.forEach(type => { 2653 // TODO: bundle this with the createMenuItem helper and bindings 2654 const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { 2655
vendor: 28,614 bytes, lines 2655-3476
2655type: 'button', 2656 class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, 2657 role: 'menuitem', 2658 'aria-haspopup': true, 2659 hidden: '' 2660 })); 2661 2662 // Bind menu shortcuts for keyboard users 2663 bindMenuItemShortcuts.call(this, menuItem, type); 2664 2665 // Show menu on click 2666 on.call(this, menuItem, 'click', () => { 2667 showMenuPanel.call(this, type, false); 2668 }); 2669 const flex = createElement('span', null, i18n.get(type, this.config)); 2670 const value = createElement('span', { 2671 class: this.config.classNames.menu.value 2672 }); 2673 2674 // Speed contains HTML entities 2675 value.innerHTML = data[type]; 2676 flex.appendChild(value); 2677 menuItem.appendChild(flex); 2678 menu.appendChild(menuItem); 2679 2680 // Build the panes 2681 const pane = createElement('div', { 2682 id: `plyr-settings-${data.id}-${type}`, 2683 hidden: '' 2684 }); 2685 2686 // Back button 2687 const backButton = createElement('button', { 2688 type: 'button', 2689 class: `${this.config.classNames.control} ${this.config.classNames.control}--back` 2690 }); 2691 2692 // Visible label 2693 backButton.appendChild(createElement('span', { 2694 'aria-hidden': true 2695 }, i18n.get(type, this.config))); 2696 2697 // Screen reader label 2698 backButton.appendChild(createElement('span', { 2699 class: this.config.classNames.hidden 2700 }, i18n.get('menuBack', this.config))); 2701 2702 // Go back via keyboard 2703 on.call(this, pane, 'keydown', event => { 2704 if (event.key !== 'ArrowLeft') return; 2705 2706 // Prevent seek 2707 event.preventDefault(); 2708 event.stopPropagation(); 2709 2710 // Show the respective menu 2711 showMenuPanel.call(this, 'home', true); 2712 }, false); 2713 2714 // Go back via button click 2715 on.call(this, backButton, 'click', () => { 2716 showMenuPanel.call(this, 'home', false); 2717 }); 2718 2719 // Add to pane 2720 pane.appendChild(backButton); 2721 2722 // Menu 2723 pane.appendChild(createElement('div', { 2724 role: 'menu' 2725 })); 2726 inner.appendChild(pane); 2727 this.elements.settings.buttons[type] = menuItem; 2728 this.elements.settings.panels[type] = pane; 2729 }); 2730 popup.appendChild(inner); 2731 wrapper.appendChild(popup); 2732 container.appendChild(wrapper); 2733 this.elements.settings.popup = popup; 2734 this.elements.settings.menu = wrapper; 2735 } 2736 2737 // Picture in picture button 2738 if (control === 'pip' && support.pip) { 2739 container.appendChild(createButton.call(this, 'pip', defaultAttributes)); 2740 } 2741 2742 // Airplay button 2743 if (control === 'airplay' && support.airplay) { 2744 container.appendChild(createButton.call(this, 'airplay', defaultAttributes)); 2745 } 2746 2747 // Download button 2748 if (control === 'download') { 2749 const attributes = extend({}, defaultAttributes, { 2750 element: 'a', 2751 href: this.download, 2752 target: '_blank' 2753 }); 2754 2755 // Set download attribute for HTML5 only 2756 if (this.isHTML5) { 2757 attributes.download = ''; 2758 } 2759 const { 2760 download 2761 } = this.config.urls; 2762 if (!is.url(download) && this.isEmbed) { 2763 extend(attributes, { 2764 icon: `logo-${this.provider}`, 2765 label: this.provider 2766 }); 2767 } 2768 container.appendChild(createButton.call(this, 'download', attributes)); 2769 } 2770 2771 // Toggle fullscreen button 2772 if (control === 'fullscreen') { 2773 container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes)); 2774 } 2775 }); 2776 2777 // Set available quality levels 2778 if (this.isHTML5) { 2779 setQualityMenu.call(this, html5.getQualityOptions.call(this)); 2780 } 2781 setSpeedMenu.call(this); 2782 return container; 2783 }, 2784 // Insert controls 2785 inject() { 2786 // Sprite 2787 if (this.config.loadSprite) { 2788 const icon = controls.getIconUrl.call(this); 2789 2790 // Only load external sprite using AJAX 2791 if (icon.cors) { 2792 loadSprite(icon.url, 'sprite-plyr'); 2793 } 2794 } 2795 2796 // Create a unique ID 2797 this.id = Math.floor(Math.random() * 10000); 2798 2799 // Null by default 2800 let container = null; 2801 this.elements.controls = null; 2802 2803 // Set template properties 2804 const props = { 2805 id: this.id, 2806 seektime: this.config.seekTime, 2807 title: this.config.title 2808 }; 2809 let update = true; 2810 2811 // If function, run it and use output 2812 if (is.function(this.config.controls)) { 2813 this.config.controls = this.config.controls.call(this, props); 2814 } 2815 2816 // Convert falsy controls to empty array (primarily for empty strings) 2817 if (!this.config.controls) { 2818 this.config.controls = []; 2819 } 2820 if (is.element(this.config.controls) || is.string(this.config.controls)) { 2821 // HTMLElement or Non-empty string passed as the option 2822 container = this.config.controls; 2823 } else { 2824 // Create controls 2825 container = controls.create.call(this, { 2826 id: this.id, 2827 seektime: this.config.seekTime, 2828 speed: this.speed, 2829 quality: this.quality, 2830 captions: captions.getLabel.call(this) 2831 // TODO: Looping 2832 // loop: 'None', 2833 }); 2834 2835 update = false; 2836 } 2837 2838 // Replace props with their value 2839 const replace = input => { 2840 let result = input; 2841 Object.entries(props).forEach(([key, value]) => { 2842 result = replaceAll(result, `{${key}}`, value); 2843 }); 2844 return result; 2845 }; 2846 2847 // Update markup 2848 if (update) { 2849 if (is.string(this.config.controls)) { 2850 container = replace(container); 2851 } 2852 } 2853 2854 // Controls container 2855 let target; 2856 2857 // Inject to custom location 2858 if (is.string(this.config.selectors.controls.container)) { 2859 target = document.querySelector(this.config.selectors.controls.container); 2860 } 2861 2862 // Inject into the container by default 2863 if (!is.element(target)) { 2864 target = this.elements.container; 2865 } 2866 2867 // Inject controls HTML (needs to be before captions, hence "afterbegin") 2868 const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML'; 2869 target[insertMethod]('afterbegin', container); 2870 2871 // Find the elements if need be 2872 if (!is.element(this.elements.controls)) { 2873 controls.findElements.call(this); 2874 } 2875 2876 // Add pressed property to buttons 2877 if (!is.empty(this.elements.buttons)) { 2878 const addProperty = button => { 2879 const className = this.config.classNames.controlPressed; 2880 button.setAttribute('aria-pressed', 'false'); 2881 Object.defineProperty(button, 'pressed', { 2882 configurable: true, 2883 enumerable: true, 2884 get() { 2885 return hasClass(button, className); 2886 }, 2887 set(pressed = false) { 2888 toggleClass(button, className, pressed); 2889 button.setAttribute('aria-pressed', pressed ? 'true' : 'false'); 2890 } 2891 }); 2892 }; 2893 2894 // Toggle classname when pressed property is set 2895 Object.values(this.elements.buttons).filter(Boolean).forEach(button => { 2896 if (is.array(button) || is.nodeList(button)) { 2897 Array.from(button).filter(Boolean).forEach(addProperty); 2898 } else { 2899 addProperty(button); 2900 } 2901 }); 2902 } 2903 2904 // Edge sometimes doesn't finish the paint so force a repaint 2905 if (browser.isEdge) { 2906 repaint(target); 2907 } 2908 2909 // Setup tooltips 2910 if (this.config.tooltips.controls) { 2911 const { 2912 classNames, 2913 selectors 2914 } = this.config; 2915 const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`; 2916 const labels = getElements.call(this, selector); 2917 Array.from(labels).forEach(label => { 2918 toggleClass(label, this.config.classNames.hidden, false); 2919 toggleClass(label, this.config.classNames.tooltip, true); 2920 }); 2921 } 2922 }, 2923 // Set media metadata 2924 setMediaMetadata() { 2925 try { 2926 if ('mediaSession' in navigator) { 2927 navigator.mediaSession.metadata = new window.MediaMetadata({ 2928 title: this.config.mediaMetadata.title, 2929 artist: this.config.mediaMetadata.artist, 2930 album: this.config.mediaMetadata.album, 2931 artwork: this.config.mediaMetadata.artwork 2932 }); 2933 } 2934 } catch (_) { 2935 // Do nothing 2936 } 2937 }, 2938 // Add markers 2939 setMarkers() { 2940 var _this$config$markers2, _this$config$markers3; 2941 if (!this.duration || this.elements.markers) return; 2942 2943 // Get valid points 2944 const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({ 2945 time 2946 }) => time > 0 && time < this.duration); 2947 if (!(points !== null && points !== void 0 && points.length)) return; 2948 const containerFragment = document.createDocumentFragment(); 2949 const pointsFragment = document.createDocumentFragment(); 2950 let tipElement = null; 2951 const tipVisible = `${this.config.classNames.tooltip}--visible`; 2952 const toggleTip = show => toggleClass(tipElement, tipVisible, show); 2953 2954 // Inject markers to progress container 2955 points.forEach(point => { 2956 const markerElement = createElement('span', { 2957 class: this.config.classNames.marker 2958 }, ''); 2959 const left = `${point.time / this.duration * 100}%`; 2960 if (tipElement) { 2961 // Show on hover 2962 markerElement.addEventListener('mouseenter', () => { 2963 if (point.label) return; 2964 tipElement.style.left = left; 2965 tipElement.innerHTML = point.label; 2966 toggleTip(true); 2967 }); 2968 2969 // Hide on leave 2970 markerElement.addEventListener('mouseleave', () => { 2971 toggleTip(false); 2972 }); 2973 } 2974 markerElement.addEventListener('click', () => { 2975 this.currentTime = point.time; 2976 }); 2977 markerElement.style.left = left; 2978 pointsFragment.appendChild(markerElement); 2979 }); 2980 containerFragment.appendChild(pointsFragment); 2981 2982 // Inject a tooltip if needed 2983 if (!this.config.tooltips.seek) { 2984 tipElement = createElement('span', { 2985 class: this.config.classNames.tooltip 2986 }, ''); 2987 containerFragment.appendChild(tipElement); 2988 } 2989 this.elements.markers = { 2990 points: pointsFragment, 2991 tip: tipElement 2992 }; 2993 this.elements.progress.appendChild(containerFragment); 2994 } 2995 }; 2996 2997 // ========================================================================== 2998 2999 /** 3000 * Parse a string to a URL object 3001 * @param {String} input - the URL to be parsed 3002 * @param {Boolean} safe - failsafe parsing 3003 */ 3004 function parseUrl(input, safe = true) { 3005 let url = input; 3006 if (safe) { 3007 const parser = document.createElement('a'); 3008 parser.href = url; 3009 url = parser.href; 3010 } 3011 try { 3012 return new URL(url); 3013 } catch (_) { 3014 return null; 3015 } 3016 } 3017 3018 // Convert object to URLSearchParams 3019 function buildUrlParams(input) { 3020 const params = new URLSearchParams(); 3021 if (is.object(input)) { 3022 Object.entries(input).forEach(([key, value]) => { 3023 params.set(key, value); 3024 }); 3025 } 3026 return params; 3027 } 3028 3029 // ========================================================================== 3030 const captions = { 3031 // Setup captions 3032 setup() { 3033 // Requires UI support 3034 if (!this.supported.ui) { 3035 return; 3036 } 3037 3038 // Only Vimeo and HTML5 video supported at this point 3039 if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) { 3040 // Clear menu and hide 3041 if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { 3042 controls.setCaptionsMenu.call(this); 3043 } 3044 return; 3045 } 3046 3047 // Inject the container 3048 if (!is.element(this.elements.captions)) { 3049 this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); 3050 this.elements.captions.setAttribute('dir', 'auto'); 3051 insertAfter(this.elements.captions, this.elements.wrapper); 3052 } 3053 3054 // Fix IE captions if CORS is used 3055 // Fetch captions and inject as blobs instead (data URIs not supported!) 3056 if (browser.isIE && window.URL) { 3057 const elements = this.media.querySelectorAll('track'); 3058 Array.from(elements).forEach(track => { 3059 const src = track.getAttribute('src'); 3060 const url = parseUrl(src); 3061 if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) { 3062 fetch(src, 'blob').then(blob => { 3063 track.setAttribute('src', window.URL.createObjectURL(blob)); 3064 }).catch(() => { 3065 removeElement(track); 3066 }); 3067 } 3068 }); 3069 } 3070 3071 // Get and set initial data 3072 // The "preferred" options are not realized unless / until the wanted language has a match 3073 // * languages: Array of user's browser languages. 3074 // * language: The language preferred by user settings or config 3075 // * active: The state preferred by user settings or config 3076 // * toggled: The real captions state 3077 3078 const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; 3079 const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); 3080 let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); 3081 3082 // Use first browser language when language is 'auto' 3083 if (language === 'auto') { 3084 [language] = languages; 3085 } 3086 let active = this.storage.get('captions'); 3087 if (!is.boolean(active)) { 3088 ({ 3089 active 3090 } = this.config.captions); 3091 } 3092 Object.assign(this.captions, { 3093 toggled: false, 3094 active, 3095 language, 3096 languages 3097 }); 3098 3099 // Watch changes to textTracks and update captions menu 3100 if (this.isHTML5) { 3101 const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; 3102 on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this)); 3103 } 3104 3105 // Update available languages in list next tick (the event must not be triggered before the listeners) 3106 setTimeout(captions.update.bind(this), 0); 3107 }, 3108 // Update available language options in settings based on tracks 3109 update() { 3110 const tracks = captions.getTracks.call(this, true); 3111 // Get the wanted language 3112 const { 3113 active, 3114 language, 3115 meta, 3116 currentTrackNode 3117 } = this.captions; 3118 const languageExists = Boolean(tracks.find(track => track.language === language)); 3119 3120 // Handle tracks (add event listener and "pseudo"-default) 3121 if (this.isHTML5 && this.isVideo) { 3122 tracks.filter(track => !meta.get(track)).forEach(track => { 3123 this.debug.log('Track added', track); 3124 3125 // Attempt to store if the original dom element was "default" 3126 meta.set(track, { 3127 default: track.mode === 'showing' 3128 }); 3129 3130 // Turn off native caption rendering to avoid double captions 3131 // Note: mode='hidden' forces a track to download. To ensure every track 3132 // isn't downloaded at once, only 'showing' tracks should be reassigned 3133 // eslint-disable-next-line no-param-reassign 3134 if (track.mode === 'showing') { 3135 // eslint-disable-next-line no-param-reassign 3136 track.mode = 'hidden'; 3137 } 3138 3139 // Add event listener for cue changes 3140 on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); 3141 }); 3142 } 3143 3144 // Update language first time it matches, or if the previous matching track was removed 3145 if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) { 3146 captions.setLanguage.call(this, language); 3147 captions.toggle.call(this, active && languageExists); 3148 } 3149 3150 // Enable or disable captions based on track length 3151 if (this.elements) { 3152 toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); 3153 } 3154 3155 // Update available languages in list 3156 if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { 3157 controls.setCaptionsMenu.call(this); 3158 } 3159 }, 3160 // Toggle captions display 3161 // Used internally for the toggleCaptions method, with the passive option forced to false 3162 toggle(input, passive = true) { 3163 // If there's no full support 3164 if (!this.supported.ui) { 3165 return; 3166 } 3167 const { 3168 toggled 3169 } = this.captions; // Current state 3170 const activeClass = this.config.classNames.captions.active; 3171 // Get the next state 3172 // If the method is called without parameter, toggle based on current value 3173 const active = is.nullOrUndefined(input) ? !toggled : input; 3174 3175 // Update state and trigger event 3176 if (active !== toggled) { 3177 // When passive, don't override user preferences 3178 if (!passive) { 3179 this.captions.active = active; 3180 this.storage.set({ 3181 captions: active 3182 }); 3183 } 3184 3185 // Force language if the call isn't passive and there is no matching language to toggle to 3186 if (!this.language && active && !passive) { 3187 const tracks = captions.getTracks.call(this); 3188 const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true); 3189 3190 // Override user preferences to avoid switching languages if a matching track is added 3191 this.captions.language = track.language; 3192 3193 // Set caption, but don't store in localStorage as user preference 3194 captions.set.call(this, tracks.indexOf(track)); 3195 return; 3196 } 3197 3198 // Toggle button if it's enabled 3199 if (this.elements.buttons.captions) { 3200 this.elements.buttons.captions.pressed = active; 3201 } 3202 3203 // Add class hook 3204 toggleClass(this.elements.container, activeClass, active); 3205 this.captions.toggled = active; 3206 3207 // Update settings menu 3208 controls.updateSetting.call(this, 'captions'); 3209 3210 // Trigger event (not used internally) 3211 triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); 3212 } 3213 3214 // Wait for the call stack to clear before setting mode='hidden' 3215 // on the active track - forcing the browser to download it 3216 setTimeout(() => { 3217 if (active && this.captions.toggled) { 3218 this.captions.currentTrackNode.mode = 'hidden'; 3219 } 3220 }); 3221 }, 3222 // Set captions by track index 3223 // Used internally for the currentTrack setter with the passive option forced to false 3224 set(index, passive = true) { 3225 const tracks = captions.getTracks.call(this); 3226 3227 // Disable captions if setting to -1 3228 if (index === -1) { 3229 captions.toggle.call(this, false, passive); 3230 return; 3231 } 3232 if (!is.number(index)) { 3233 this.debug.warn('Invalid caption argument', index); 3234 return; 3235 } 3236 if (!(index in tracks)) { 3237 this.debug.warn('Track not found', index); 3238 return; 3239 } 3240 if (this.captions.currentTrack !== index) { 3241 this.captions.currentTrack = index; 3242 const track = tracks[index]; 3243 const { 3244 language 3245 } = track || {}; 3246 3247 // Store reference to node for invalidation on remove 3248 this.captions.currentTrackNode = track; 3249 3250 // Update settings menu 3251 controls.updateSetting.call(this, 'captions'); 3252 3253 // When passive, don't override user preferences 3254 if (!passive) { 3255 this.captions.language = language; 3256 this.storage.set({ 3257 language 3258 }); 3259 } 3260 3261 // Handle Vimeo captions 3262 if (this.isVimeo) { 3263 this.embed.enableTextTrack(language); 3264 } 3265 3266 // Trigger event 3267 triggerEvent.call(this, this.media, 'languagechange'); 3268 } 3269 3270 // Show captions 3271 captions.toggle.call(this, true, passive); 3272 if (this.isHTML5 && this.isVideo) { 3273 // If we change the active track while a cue is already displayed we need to update it 3274 captions.updateCues.call(this); 3275 } 3276 }, 3277 // Set captions by language 3278 // Used internally for the language setter with the passive option forced to false 3279 setLanguage(input, passive = true) { 3280 if (!is.string(input)) { 3281 this.debug.warn('Invalid language argument', input); 3282 return; 3283 } 3284 // Normalize 3285 const language = input.toLowerCase(); 3286 this.captions.language = language; 3287 3288 // Set currentTrack 3289 const tracks = captions.getTracks.call(this); 3290 const track = captions.findTrack.call(this, [language]); 3291 captions.set.call(this, tracks.indexOf(track), passive); 3292 }, 3293 // Get current valid caption tracks 3294 // If update is false it will also ignore tracks without metadata 3295 // This is used to "freeze" the language options when captions.update is false 3296 getTracks(update = false) { 3297 // Handle media or textTracks missing or null 3298 const tracks = Array.from((this.media || {}).textTracks || []); 3299 // For HTML5, use cache instead of current tracks when it exists (if captions.update is false) 3300 // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) 3301 return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind)); 3302 }, 3303 // Match tracks based on languages and get the first 3304 findTrack(languages, force = false) { 3305 const tracks = captions.getTracks.call(this); 3306 const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); 3307 const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); 3308 let track; 3309 languages.every(language => { 3310 track = sorted.find(t => t.language === language); 3311 return !track; // Break iteration if there is a match 3312 }); 3313 3314 // If no match is found but is required, get first 3315 return track || (force ? sorted[0] : undefined); 3316 }, 3317 // Get the current track 3318 getCurrentTrack() { 3319 return captions.getTracks.call(this)[this.currentTrack]; 3320 }, 3321 // Get UI label for track 3322 getLabel(track) { 3323 let currentTrack = track; 3324 if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { 3325 currentTrack = captions.getCurrentTrack.call(this); 3326 } 3327 if (is.track(currentTrack)) { 3328 if (!is.empty(currentTrack.label)) { 3329 return currentTrack.label; 3330 } 3331 if (!is.empty(currentTrack.language)) { 3332 return track.language.toUpperCase(); 3333 } 3334 return i18n.get('enabled', this.config); 3335 } 3336 return i18n.get('disabled', this.config); 3337 }, 3338 // Update captions using current track's active cues 3339 // Also optional array argument in case there isn't any track (ex: vimeo) 3340 updateCues(input) { 3341 // Requires UI 3342 if (!this.supported.ui) { 3343 return; 3344 } 3345 if (!is.element(this.elements.captions)) { 3346 this.debug.warn('No captions element to render to'); 3347 return; 3348 } 3349 3350 // Only accept array or empty input 3351 if (!is.nullOrUndefined(input) && !Array.isArray(input)) { 3352 this.debug.warn('updateCues: Invalid input', input); 3353 return; 3354 } 3355 let cues = input; 3356 3357 // Get cues from track 3358 if (!cues) { 3359 const track = captions.getCurrentTrack.call(this); 3360 cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML); 3361 } 3362 3363 // Set new caption text 3364 const content = cues.map(cueText => cueText.trim()).join('\n'); 3365 const changed = content !== this.elements.captions.innerHTML; 3366 if (changed) { 3367 // Empty the container and create a new child element 3368 emptyElement(this.elements.captions); 3369 const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); 3370 caption.innerHTML = content; 3371 this.elements.captions.appendChild(caption); 3372 3373 // Trigger event 3374 triggerEvent.call(this, this.media, 'cuechange'); 3375 } 3376 } 3377 }; 3378 3379 // ========================================================================== 3380 // Plyr default config 3381 // ========================================================================== 3382 3383 const defaults = { 3384 // Disable 3385 enabled: true, 3386 // Custom media title 3387 title: '', 3388 // Logging to console 3389 debug: false, 3390 // Auto play (if supported) 3391 autoplay: false, 3392 // Only allow one media playing at once (vimeo only) 3393 autopause: true, 3394 // Allow inline playback on iOS 3395 playsinline: true, 3396 // Default time to skip when rewind/fast forward 3397 seekTime: 10, 3398 // Default volume 3399 volume: 1, 3400 muted: false, 3401 // Pass a custom duration 3402 duration: null, 3403 // Display the media duration on load in the current time position 3404 // If you have opted to display both duration and currentTime, this is ignored 3405 displayDuration: true, 3406 // Invert the current time to be a countdown 3407 invertTime: true, 3408 // Clicking the currentTime inverts it's value to show time left rather than elapsed 3409 toggleInvert: true, 3410 // Force an aspect ratio 3411 // The format must be `'w:h'` (e.g. `'16:9'`) 3412 ratio: null, 3413 // Click video container to play/pause 3414 clickToPlay: true, 3415 // Auto hide the controls 3416 hideControls: true, 3417 // Reset to start when playback ended 3418 resetOnEnd: false, 3419 // Disable the standard context menu 3420 disableContextMenu: true, 3421 // Sprite (for icons) 3422 loadSprite: true, 3423 iconPrefix: 'plyr', 3424 iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg', 3425 // Blank video (used to prevent errors on source change) 3426 blankVideo: 'https://cdn.plyr.io/static/blank.mp4', 3427 // Quality default 3428 quality: { 3429 default: 576, 3430 // The options to display in the UI, if available for the source media 3431 options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240], 3432 forced: false, 3433 onChange: null 3434 }, 3435 // Set loops 3436 loop: { 3437 active: false 3438 // start: null, 3439 // end: null, 3440 }, 3441 3442 // Speed default and options to display 3443 speed: { 3444 selected: 1, 3445 // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x) 3446 options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4] 3447 }, 3448 // Keyboard shortcut settings 3449 keyboard: { 3450 focused: true, 3451 global: false 3452 }, 3453 // Display tooltips 3454 tooltips: { 3455 controls: false, 3456 seek: true 3457 }, 3458 // Captions settings 3459 captions: { 3460 active: false, 3461 language: 'auto', 3462 // Listen to new tracks added after Plyr is initialized. 3463 // This is needed for streaming captions, but may result in unselectable options 3464 update: false 3465 }, 3466 // Fullscreen settings 3467 fullscreen: { 3468 enabled: true, 3469 // Allow fullscreen? 3470 fallback: true, 3471 // Fallback using full viewport/window 3472 iosNative: false // Use the native fullscreen in iOS (disables custom controls) 3473 // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode 3474 // Non-ancestors of the player element will be ignored 3475 // container: null, // defaults to the player element 3476 },
vendor: 7,248 bytes, lines 3477-3713
3477 3478 // Local storage 3479 storage: { 3480 enabled: true, 3481 key: 'plyr' 3482 }, 3483 // Default controls 3484 controls: ['play-large', 3485 // 'restart', 3486 // 'rewind', 3487 'play', 3488 // 'fast-forward', 3489 'progress', 'current-time', 3490 // 'duration', 3491 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 3492 // 'download', 3493 'fullscreen'], 3494 settings: ['captions', 'quality', 'speed'], 3495 // Localisation 3496 i18n: { 3497 restart: 'Restart', 3498 rewind: 'Rewind {seektime}s', 3499 play: 'Play', 3500 pause: 'Pause', 3501 fastForward: 'Forward {seektime}s', 3502 seek: 'Seek', 3503 seekLabel: '{currentTime} of {duration}', 3504 played: 'Played', 3505 buffered: 'Buffered', 3506 currentTime: 'Current time', 3507 duration: 'Duration', 3508 volume: 'Volume', 3509 mute: 'Mute', 3510 unmute: 'Unmute', 3511 enableCaptions: 'Enable captions', 3512 disableCaptions: 'Disable captions', 3513 download: 'Download', 3514 enterFullscreen: 'Enter fullscreen', 3515 exitFullscreen: 'Exit fullscreen', 3516 frameTitle: 'Player for {title}', 3517 captions: 'Captions', 3518 settings: 'Settings', 3519 pip: 'PIP', 3520 menuBack: 'Go back to previous menu', 3521 speed: 'Speed', 3522 normal: 'Normal', 3523 quality: 'Quality', 3524 loop: 'Loop', 3525 start: 'Start', 3526 end: 'End', 3527 all: 'All', 3528 reset: 'Reset', 3529 disabled: 'Disabled', 3530 enabled: 'Enabled', 3531 advertisement: 'Ad', 3532 qualityBadge: { 3533 2160: '4K', 3534 1440: 'HD', 3535 1080: 'HD', 3536 720: 'HD', 3537 576: 'SD', 3538 480: 'SD' 3539 } 3540 }, 3541 // URLs 3542 urls: { 3543 download: null, 3544 vimeo: { 3545 sdk: 'https://player.vimeo.com/api/player.js', 3546 iframe: 'https://player.vimeo.com/video/{0}?{1}', 3547 api: 'https://vimeo.com/api/oembed.json?url={0}' 3548 }, 3549 youtube: { 3550 sdk: 'https://www.youtube.com/iframe_api', 3551 api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}' 3552 }, 3553 googleIMA: { 3554 sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js' 3555 } 3556 }, 3557 // Custom control listeners 3558 listeners: { 3559 seek: null, 3560 play: null, 3561 pause: null, 3562 restart: null, 3563 rewind: null, 3564 fastForward: null, 3565 mute: null, 3566 volume: null, 3567 captions: null, 3568 download: null, 3569 fullscreen: null, 3570 pip: null, 3571 airplay: null, 3572 speed: null, 3573 quality: null, 3574 loop: null, 3575 language: null 3576 }, 3577 // Events to watch and bubble 3578 events: [ 3579 // Events to watch on HTML5 media elements and bubble 3580 // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events 3581 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', 3582 // Custom events 3583 'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', 3584 // YouTube 3585 'statechange', 3586 // Quality 3587 'qualitychange', 3588 // Ads 3589 'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'], 3590 // Selectors 3591 // Change these to match your template if using custom HTML 3592 selectors: { 3593 editable: 'input, textarea, select, [contenteditable]', 3594 container: '.plyr', 3595 controls: { 3596 container: null, 3597 wrapper: '.plyr__controls' 3598 }, 3599 labels: '[data-plyr]', 3600 buttons: { 3601 play: '[data-plyr="play"]', 3602 pause: '[data-plyr="pause"]', 3603 restart: '[data-plyr="restart"]', 3604 rewind: '[data-plyr="rewind"]', 3605 fastForward: '[data-plyr="fast-forward"]', 3606 mute: '[data-plyr="mute"]', 3607 captions: '[data-plyr="captions"]', 3608 download: '[data-plyr="download"]', 3609 fullscreen: '[data-plyr="fullscreen"]', 3610 pip: '[data-plyr="pip"]', 3611 airplay: '[data-plyr="airplay"]', 3612 settings: '[data-plyr="settings"]', 3613 loop: '[data-plyr="loop"]' 3614 }, 3615 inputs: { 3616 seek: '[data-plyr="seek"]', 3617 volume: '[data-plyr="volume"]', 3618 speed: '[data-plyr="speed"]', 3619 language: '[data-plyr="language"]', 3620 quality: '[data-plyr="quality"]' 3621 }, 3622 display: { 3623 currentTime: '.plyr__time--current', 3624 duration: '.plyr__time--duration', 3625 buffer: '.plyr__progress__buffer', 3626 loop: '.plyr__progress__loop', 3627 // Used later 3628 volume: '.plyr__volume--display' 3629 }, 3630 progress: '.plyr__progress', 3631 captions: '.plyr__captions', 3632 caption: '.plyr__caption' 3633 }, 3634 // Class hooks added to the player in different states 3635 classNames: { 3636 type: 'plyr--{0}', 3637 provider: 'plyr--{0}', 3638 video: 'plyr__video-wrapper', 3639 embed: 'plyr__video-embed', 3640 videoFixedRatio: 'plyr__video-wrapper--fixed-ratio', 3641 embedContainer: 'plyr__video-embed__container', 3642 poster: 'plyr__poster', 3643 posterEnabled: 'plyr__poster-enabled', 3644 ads: 'plyr__ads', 3645 control: 'plyr__control', 3646 controlPressed: 'plyr__control--pressed', 3647 playing: 'plyr--playing', 3648 paused: 'plyr--paused', 3649 stopped: 'plyr--stopped', 3650 loading: 'plyr--loading', 3651 hover: 'plyr--hover', 3652 tooltip: 'plyr__tooltip', 3653 cues: 'plyr__cues', 3654 marker: 'plyr__progress__marker', 3655 hidden: 'plyr__sr-only', 3656 hideControls: 'plyr--hide-controls', 3657 isTouch: 'plyr--is-touch', 3658 uiSupported: 'plyr--full-ui', 3659 noTransition: 'plyr--no-transition', 3660 display: { 3661 time: 'plyr__time' 3662 }, 3663 menu: { 3664 value: 'plyr__menu__value', 3665 badge: 'plyr__badge', 3666 open: 'plyr--menu-open' 3667 }, 3668 captions: { 3669 enabled: 'plyr--captions-enabled', 3670 active: 'plyr--captions-active' 3671 }, 3672 fullscreen: { 3673 enabled: 'plyr--fullscreen-enabled', 3674 fallback: 'plyr--fullscreen-fallback' 3675 }, 3676 pip: { 3677 supported: 'plyr--pip-supported', 3678 active: 'plyr--pip-active' 3679 }, 3680 airplay: { 3681 supported: 'plyr--airplay-supported', 3682 active: 'plyr--airplay-active' 3683 }, 3684 previewThumbnails: { 3685 // Tooltip thumbs 3686 thumbContainer: 'plyr__preview-thumb', 3687 thumbContainerShown: 'plyr__preview-thumb--is-shown', 3688 imageContainer: 'plyr__preview-thumb__image-container', 3689 timeContainer: 'plyr__preview-thumb__time-container', 3690 // Scrubbing 3691 scrubbingContainer: 'plyr__preview-scrubbing', 3692 scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown' 3693 } 3694 }, 3695 // Embed attributes 3696 attributes: { 3697 embed: { 3698 provider: 'data-plyr-provider', 3699 id: 'data-plyr-embed-id', 3700 hash: 'data-plyr-embed-hash' 3701 } 3702 }, 3703 // Advertisements plugin 3704 // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio 3705 ads: { 3706 enabled: false, 3707 publisherId: '', 3708 tagUrl: '' 3709 }, 3710 // Preview Thumbnails plugin 3711 previewThumbnails: { 3712 enabled: false, 3713 src: ''
vendor: 3,931 bytes, lines 3713-3846
3713 3714 }, 3715 // Vimeo plugin 3716 vimeo: { 3717 byline: false, 3718 portrait: false, 3719 title: false, 3720 speed: true, 3721 transparent: false, 3722 // Custom settings from Plyr 3723 customControls: true, 3724 referrerPolicy: null, 3725 // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy 3726 // Whether the owner of the video has a Pro or Business account 3727 // (which allows us to properly hide controls without CSS hacks, etc) 3728 premium: false 3729 }, 3730 // YouTube plugin 3731 youtube: { 3732 rel: 0, 3733 // No related vids 3734 showinfo: 0, 3735 // Hide info 3736 iv_load_policy: 3, 3737 // Hide annotations 3738 modestbranding: 1, 3739 // Hide logos as much as possible (they still show one in the corner when paused) 3740 // Custom settings from Plyr 3741 customControls: true, 3742 noCookie: false // Whether to use an alternative version of YouTube without cookies 3743 }, 3744 3745 // Media Metadata 3746 mediaMetadata: { 3747 title: '', 3748 artist: '', 3749 album: '', 3750 artwork: [] 3751 }, 3752 // Markers 3753 markers: { 3754 enabled: false, 3755 points: [] 3756 } 3757 }; 3758 3759 // ========================================================================== 3760 // Plyr states 3761 // ========================================================================== 3762 3763 const pip = { 3764 active: 'picture-in-picture', 3765 inactive: 'inline' 3766 }; 3767 3768 // ========================================================================== 3769 // Plyr supported types and providers 3770 // ========================================================================== 3771 3772 const providers = { 3773 html5: 'html5', 3774 youtube: 'youtube', 3775 vimeo: 'vimeo' 3776 }; 3777 const types = { 3778 audio: 'audio', 3779 video: 'video' 3780 }; 3781 3782 /** 3783 * Get provider by URL 3784 * @param {String} url 3785 */ 3786 function getProviderByUrl(url) { 3787 // YouTube 3788 if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) { 3789 return providers.youtube; 3790 } 3791 3792 // Vimeo 3793 if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { 3794 return providers.vimeo; 3795 } 3796 return null; 3797 } 3798 3799 // ========================================================================== 3800 // Console wrapper 3801 // ========================================================================== 3802 3803 const noop = () => {}; 3804 class Console { 3805 constructor(enabled = false) { 3806 this.enabled = window.console && enabled; 3807 if (this.enabled) { 3808 this.log('Debugging enabled'); 3809 } 3810 } 3811 get log() { 3812 // eslint-disable-next-line no-console 3813 return this.enabled ? Function.prototype.bind.call(console.log, console) : noop; 3814 } 3815 get warn() { 3816 // eslint-disable-next-line no-console 3817 return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; 3818 } 3819 get error() { 3820 // eslint-disable-next-line no-console 3821 return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; 3822 } 3823 } 3824 3825 class Fullscreen { 3826 constructor(player) { 3827 _defineProperty$1(this, "onChange", () => { 3828 if (!this.supported) return; 3829 3830 // Update toggle button 3831 const button = this.player.elements.buttons.fullscreen; 3832 if (is.element(button)) { 3833 button.pressed = this.active; 3834 } 3835 3836 // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up 3837 const target = this.target === this.player.media ? this.target : this.player.elements.container; 3838 // Trigger an event 3839 triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); 3840 }); 3841 _defineProperty$1(this, "toggleFallback", (toggle = false) => { 3842 // Store or restore scroll position 3843 if (toggle) { 3844 this.scrollPosition = { 3845 x: window.scrollX ?? 0, 3846 y: window.scrollY ??
vendor: 8,437 bytes, lines 3846-4063
3846 0 3847 }; 3848 } else { 3849 window.scrollTo(this.scrollPosition.x, this.scrollPosition.y); 3850 } 3851 3852 // Toggle scroll 3853 document.body.style.overflow = toggle ? 'hidden' : ''; 3854 3855 // Toggle class hook 3856 toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); 3857 3858 // Force full viewport on iPhone X+ 3859 if (browser.isIos) { 3860 let viewport = document.head.querySelector('meta[name="viewport"]'); 3861 const property = 'viewport-fit=cover'; 3862 3863 // Inject the viewport meta if required 3864 if (!viewport) { 3865 viewport = document.createElement('meta'); 3866 viewport.setAttribute('name', 'viewport'); 3867 } 3868 3869 // Check if the property already exists 3870 const hasProperty = is.string(viewport.content) && viewport.content.includes(property); 3871 if (toggle) { 3872 this.cleanupViewport = !hasProperty; 3873 if (!hasProperty) viewport.content += `,${property}`; 3874 } else if (this.cleanupViewport) { 3875 viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(','); 3876 } 3877 } 3878 3879 // Toggle button and fire events 3880 this.onChange(); 3881 }); 3882 // Trap focus inside container 3883 _defineProperty$1(this, "trapFocus", event => { 3884 // Bail if iOS/iPadOS, not active, not the tab key 3885 if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return; 3886 3887 // Get the current focused element 3888 const focused = document.activeElement; 3889 const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]'); 3890 const [first] = focusable; 3891 const last = focusable[focusable.length - 1]; 3892 if (focused === last && !event.shiftKey) { 3893 // Move focus to first element that can be tabbed if Shift isn't used 3894 first.focus(); 3895 event.preventDefault(); 3896 } else if (focused === first && event.shiftKey) { 3897 // Move focus to last element that can be tabbed if Shift is used 3898 last.focus(); 3899 event.preventDefault(); 3900 } 3901 }); 3902 // Update UI 3903 _defineProperty$1(this, "update", () => { 3904 if (this.supported) { 3905 let mode; 3906 if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback'; 3907 this.player.debug.log(`${mode} fullscreen enabled`); 3908 } else { 3909 this.player.debug.log('Fullscreen not supported and fallback disabled'); 3910 } 3911 3912 // Add styling hook to show button 3913 toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported); 3914 }); 3915 // Make an element fullscreen 3916 _defineProperty$1(this, "enter", () => { 3917 if (!this.supported) return; 3918 3919 // iOS native fullscreen doesn't need the request step 3920 if (browser.isIos && this.player.config.fullscreen.iosNative) { 3921 if (this.player.isVimeo) { 3922 this.player.embed.requestFullscreen(); 3923 } else { 3924 this.target.webkitEnterFullscreen(); 3925 } 3926 } else if (!Fullscreen.nativeSupported || this.forceFallback) { 3927 this.toggleFallback(true); 3928 } else if (!this.prefix) { 3929 this.target.requestFullscreen({ 3930 navigationUI: 'hide' 3931 }); 3932 } else if (!is.empty(this.prefix)) { 3933 this.target[`${this.prefix}Request${this.property}`](); 3934 } 3935 }); 3936 // Bail from fullscreen 3937 _defineProperty$1(this, "exit", () => { 3938 if (!this.supported) return; 3939 3940 // iOS native fullscreen 3941 if (browser.isIos && this.player.config.fullscreen.iosNative) { 3942 if (this.player.isVimeo) { 3943 this.player.embed.exitFullscreen(); 3944 } else { 3945 this.target.webkitEnterFullscreen(); 3946 } 3947 silencePromise(this.player.play()); 3948 } else if (!Fullscreen.nativeSupported || this.forceFallback) { 3949 this.toggleFallback(false); 3950 } else if (!this.prefix) { 3951 (document.cancelFullScreen || document.exitFullscreen).call(document); 3952 } else if (!is.empty(this.prefix)) { 3953 const action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; 3954 document[`${this.prefix}${action}${this.property}`](); 3955 } 3956 }); 3957 // Toggle state 3958 _defineProperty$1(this, "toggle", () => { 3959 if (!this.active) this.enter();else this.exit(); 3960 }); 3961 // Keep reference to parent 3962 this.player = player; 3963 3964 // Get prefix 3965 this.prefix = Fullscreen.prefix; 3966 this.property = Fullscreen.property; 3967 3968 // Scroll position 3969 this.scrollPosition = { 3970 x: 0, 3971 y: 0 3972 }; 3973 3974 // Force the use of 'full window/browser' rather than fullscreen 3975 this.forceFallback = player.config.fullscreen.fallback === 'force'; 3976 3977 // Get the fullscreen element 3978 // Checks container is an ancestor, defaults to null 3979 this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container); 3980 3981 // Register event listeners 3982 // Handle event (incase user presses escape etc) 3983 on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { 3984 // TODO: Filter for target?? 3985 this.onChange(); 3986 }); 3987 3988 // Fullscreen toggle on double click 3989 on.call(this.player, this.player.elements.container, 'dblclick', event => { 3990 // Ignore double click in controls 3991 if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { 3992 return; 3993 } 3994 this.player.listeners.proxy(event, this.toggle, 'fullscreen'); 3995 }); 3996 3997 // Tap focus when in fullscreen 3998 on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event)); 3999 4000 // Update the UI 4001 this.update(); 4002 } 4003 4004 // Determine if native supported 4005 static get nativeSupported() { 4006 return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); 4007 } 4008 4009 // If we're actually using native 4010 get useNative() { 4011 return Fullscreen.nativeSupported && !this.forceFallback; 4012 } 4013 4014 // Get the prefix for handlers 4015 static get prefix() { 4016 // No prefix 4017 if (is.function(document.exitFullscreen)) return ''; 4018 4019 // Check for fullscreen support by vendor prefix 4020 let value = ''; 4021 const prefixes = ['webkit', 'moz', 'ms']; 4022 prefixes.some(pre => { 4023 if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) { 4024 value = pre; 4025 return true; 4026 } 4027 return false; 4028 }); 4029 return value; 4030 } 4031 static get property() { 4032 return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen'; 4033 } 4034 4035 // Determine if fullscreen is supported 4036 get supported() { 4037 return [ 4038 // Fullscreen is enabled in config 4039 this.player.config.fullscreen.enabled, 4040 // Must be a video 4041 this.player.isVideo, 4042 // Either native is supported or fallback enabled 4043 Fullscreen.nativeSupported || this.player.config.fullscreen.fallback, 4044 // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline 4045 // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback 4046 !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean); 4047 } 4048 4049 // Get active state 4050 get active() { 4051 if (!this.supported) return false; 4052 4053 // Fallback using classname 4054 if (!Fullscreen.nativeSupported || this.forceFallback) { 4055 return hasClass(this.target, this.player.config.classNames.fullscreen.fallback); 4056 } 4057 const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`]; 4058 return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target; 4059 } 4060 4061 // Get target element 4062 get target() { 4063
4063return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container; 4064 } 4065 } 4066 4067 // ========================================================================== 4068 // Load image avoiding xhr/fetch CORS issues 4069 // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded 4070 // By default it checks if it is at least 1px, but you can add a second argument to change this 4071 // ========================================================================== 4072 4073 function loadImage(src, minWidth = 1) { 4074 return new Promise((resolve, reject) => { 4075 const image = new Image(); 4076 const handler = () => { 4077 delete image.onload; 4078 delete image.onerror; 4079 (image.naturalWidth >= minWidth ? resolve : reject)(image); 4080 }; 4081 Object.assign(image, { 4082 onload: handler, 4083 onerror: handler, 4084 src 4085 }); 4086 }); 4087 } 4088 4089 // ==========================================================================
vendor: 22,672 bytes, lines 4089-4748
4089 4090 const ui = { 4091 addStyleHook() { 4092 toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); 4093 toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); 4094 }, 4095 // Toggle native HTML5 media controls 4096 toggleNativeControls(toggle = false) { 4097 if (toggle && this.isHTML5) { 4098 this.media.setAttribute('controls', ''); 4099 } else { 4100 this.media.removeAttribute('controls'); 4101 } 4102 }, 4103 // Setup the UI 4104 build() { 4105 // Re-attach media element listeners 4106 // TODO: Use event bubbling? 4107 this.listeners.media(); 4108 4109 // Don't setup interface if no support 4110 if (!this.supported.ui) { 4111 this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); 4112 4113 // Restore native controls 4114 ui.toggleNativeControls.call(this, true); 4115 4116 // Bail 4117 return; 4118 } 4119 4120 // Inject custom controls if not present 4121 if (!is.element(this.elements.controls)) { 4122 // Inject custom controls 4123 controls.inject.call(this); 4124 4125 // Re-attach control listeners 4126 this.listeners.controls(); 4127 } 4128 4129 // Remove native controls 4130 ui.toggleNativeControls.call(this); 4131 4132 // Setup captions for HTML5 4133 if (this.isHTML5) { 4134 captions.setup.call(this); 4135 } 4136 4137 // Reset volume 4138 this.volume = null; 4139 4140 // Reset mute state 4141 this.muted = null; 4142 4143 // Reset loop state 4144 this.loop = null; 4145 4146 // Reset quality setting 4147 this.quality = null; 4148 4149 // Reset speed 4150 this.speed = null; 4151 4152 // Reset volume display 4153 controls.updateVolume.call(this); 4154 4155 // Reset time display 4156 controls.timeUpdate.call(this); 4157 4158 // Reset duration display 4159 controls.durationUpdate.call(this); 4160 4161 // Update the UI 4162 ui.checkPlaying.call(this); 4163 4164 // Check for picture-in-picture support 4165 toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); 4166 4167 // Check for airplay support 4168 toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); 4169 4170 // Add touch class 4171 toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); 4172 4173 // Ready for API calls 4174 this.ready = true; 4175 4176 // Ready event at end of execution stack 4177 setTimeout(() => { 4178 triggerEvent.call(this, this.media, 'ready'); 4179 }, 0); 4180 4181 // Set the title 4182 ui.setTitle.call(this); 4183 4184 // Assure the poster image is set, if the property was added before the element was created 4185 if (this.poster) { 4186 ui.setPoster.call(this, this.poster, false).catch(() => {}); 4187 } 4188 4189 // Manually set the duration if user has overridden it. 4190 // The event listeners for it doesn't get called if preload is disabled (#701) 4191 if (this.config.duration) { 4192 controls.durationUpdate.call(this); 4193 } 4194 4195 // Media metadata 4196 if (this.config.mediaMetadata) { 4197 controls.setMediaMetadata.call(this); 4198 } 4199 }, 4200 // Setup aria attribute for play and iframe title 4201 setTitle() { 4202 // Find the current text 4203 let label = i18n.get('play', this.config); 4204 4205 // If there's a media title set, use that for the label 4206 if (is.string(this.config.title) && !is.empty(this.config.title)) { 4207 label += `, ${this.config.title}`; 4208 } 4209 4210 // If there's a play button, set label 4211 Array.from(this.elements.buttons.play || []).forEach(button => { 4212 button.setAttribute('aria-label', label); 4213 }); 4214 4215 // Set iframe title 4216 // https://github.com/sampotts/plyr/issues/124 4217 if (this.isEmbed) { 4218 const iframe = getElement.call(this, 'iframe'); 4219 if (!is.element(iframe)) { 4220 return; 4221 } 4222 4223 // Default to media type 4224 const title = !is.empty(this.config.title) ? this.config.title : 'video'; 4225 const format = i18n.get('frameTitle', this.config); 4226 iframe.setAttribute('title', format.replace('{title}', title)); 4227 } 4228 }, 4229 // Toggle poster 4230 togglePoster(enable) { 4231 toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); 4232 }, 4233 // Set the poster image (async) 4234 // Used internally for the poster setter, with the passive option forced to false 4235 setPoster(poster, passive = true) { 4236 // Don't override if call is passive 4237 if (passive && this.poster) { 4238 return Promise.reject(new Error('Poster already set')); 4239 } 4240 4241 // Set property synchronously to respect the call order 4242 this.media.setAttribute('data-poster', poster); 4243 4244 // Show the poster 4245 this.elements.poster.removeAttribute('hidden'); 4246 4247 // Wait until ui is ready 4248 return ready.call(this) 4249 // Load image 4250 .then(() => loadImage(poster)).catch(error => { 4251 // Hide poster on error unless it's been set by another call 4252 if (poster === this.poster) { 4253 ui.togglePoster.call(this, false); 4254 } 4255 // Rethrow 4256 throw error; 4257 }).then(() => { 4258 // Prevent race conditions 4259 if (poster !== this.poster) { 4260 throw new Error('setPoster cancelled by later call to setPoster'); 4261 } 4262 }).then(() => { 4263 Object.assign(this.elements.poster.style, { 4264 backgroundImage: `url('${poster}')`, 4265 // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) 4266 backgroundSize: '' 4267 }); 4268 ui.togglePoster.call(this, true); 4269 return poster; 4270 }); 4271 }, 4272 // Check playing state 4273 checkPlaying(event) { 4274 // Class hooks 4275 toggleClass(this.elements.container, this.config.classNames.playing, this.playing); 4276 toggleClass(this.elements.container, this.config.classNames.paused, this.paused); 4277 toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); 4278 4279 // Set state 4280 Array.from(this.elements.buttons.play || []).forEach(target => { 4281 Object.assign(target, { 4282 pressed: this.playing 4283 }); 4284 target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config)); 4285 }); 4286 4287 // Only update controls on non timeupdate events 4288 if (is.event(event) && event.type === 'timeupdate') { 4289 return; 4290 } 4291 4292 // Toggle controls 4293 ui.toggleControls.call(this); 4294 }, 4295 // Check if media is loading 4296 checkLoading(event) { 4297 this.loading = ['stalled', 'waiting'].includes(event.type); 4298 4299 // Clear timer 4300 clearTimeout(this.timers.loading); 4301 4302 // Timer to prevent flicker when seeking 4303 this.timers.loading = setTimeout(() => { 4304 // Update progress bar loading class state 4305 toggleClass(this.elements.container, this.config.classNames.loading, this.loading); 4306 4307 // Update controls visibility 4308 ui.toggleControls.call(this); 4309 }, this.loading ? 250 : 0); 4310 }, 4311 // Toggle controls based on state and `force` argument 4312 toggleControls(force) { 4313 const { 4314 controls: controlsElement 4315 } = this.elements; 4316 if (controlsElement && this.config.hideControls) { 4317 // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.) 4318 const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); 4319 4320 // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide 4321 this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek)); 4322 } 4323 }, 4324 // Migrate any custom properties from the media to the parent 4325 migrateStyles() { 4326 // Loop through values (as they are the keys when the object is spread ð¤) 4327 Object.values({ 4328 ...this.media.style 4329 }) 4330 // We're only fussed about Plyr specific properties 4331 .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => { 4332 // Set on the container 4333 this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key)); 4334 4335 // Clean up from media element 4336 this.media.style.removeProperty(key); 4337 }); 4338 4339 // Remove attribute if empty 4340 if (is.empty(this.media.style)) { 4341 this.media.removeAttribute('style'); 4342 } 4343 } 4344 }; 4345 4346 class Listeners { 4347 constructor(_player) { 4348 // Device is touch enabled 4349 _defineProperty$1(this, "firstTouch", () => { 4350 const { 4351 player 4352 } = this; 4353 const { 4354 elements 4355 } = player; 4356 player.touch = true; 4357 4358 // Add touch class 4359 toggleClass(elements.container, player.config.classNames.isTouch, true); 4360 }); 4361 // Global window & document listeners 4362 _defineProperty$1(this, "global", (toggle = true) => { 4363 const { 4364 player 4365 } = this; 4366 4367 // Keyboard shortcuts 4368 if (player.config.keyboard.global) { 4369 toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false); 4370 } 4371 4372 // Click anywhere closes menu 4373 toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle); 4374 4375 // Detect touch by events 4376 once.call(player, document.body, 'touchstart', this.firstTouch); 4377 }); 4378 // Container listeners 4379 _defineProperty$1(this, "container", () => { 4380 const { 4381 player 4382 } = this; 4383 const { 4384 config, 4385 elements, 4386 timers 4387 } = player; 4388 4389 // Keyboard shortcuts 4390 if (!config.keyboard.global && config.keyboard.focused) { 4391 on.call(player, elements.container, 'keydown keyup', this.handleKey, false); 4392 } 4393 4394 // Toggle controls on mouse events and entering fullscreen 4395 on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { 4396 const { 4397 controls: controlsElement 4398 } = elements; 4399 4400 // Remove button states for fullscreen 4401 if (controlsElement && event.type === 'enterfullscreen') { 4402 controlsElement.pressed = false; 4403 controlsElement.hover = false; 4404 } 4405 4406 // Show, then hide after a timeout unless another control event occurs 4407 const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); 4408 let delay = 0; 4409 if (show) { 4410 ui.toggleControls.call(player, true); 4411 // Use longer timeout for touch devices 4412 delay = player.touch ? 3000 : 2000; 4413 } 4414 4415 // Clear timer 4416 clearTimeout(timers.controls); 4417 4418 // Set new timer to prevent flicker when seeking 4419 timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay); 4420 }); 4421 4422 // Set a gutter for Vimeo 4423 const setGutter = () => { 4424 if (!player.isVimeo || player.config.vimeo.premium) { 4425 return; 4426 } 4427 const target = elements.wrapper; 4428 const { 4429 active 4430 } = player.fullscreen; 4431 const [videoWidth, videoHeight] = getAspectRatio.call(player); 4432 const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`); 4433 4434 // If not active, remove styles 4435 if (!active) { 4436 if (useNativeAspectRatio) { 4437 target.style.width = null; 4438 target.style.height = null; 4439 } else { 4440 target.style.maxWidth = null; 4441 target.style.margin = null; 4442 } 4443 return; 4444 } 4445 4446 // Determine which dimension will overflow and constrain view 4447 const [viewportWidth, viewportHeight] = getViewportSize(); 4448 const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight; 4449 if (useNativeAspectRatio) { 4450 target.style.width = overflow ? 'auto' : '100%'; 4451 target.style.height = overflow ? '100%' : 'auto'; 4452 } else { 4453 target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null; 4454 target.style.margin = overflow ? '0 auto' : null; 4455 } 4456 }; 4457 4458 // Handle resizing 4459 const resized = () => { 4460 clearTimeout(timers.resized); 4461 timers.resized = setTimeout(setGutter, 50); 4462 }; 4463 on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => { 4464 const { 4465 target 4466 } = player.fullscreen; 4467 4468 // Ignore events not from target 4469 if (target !== elements.container) { 4470 return; 4471 } 4472 4473 // If it's not an embed and no ratio specified 4474 if (!player.isEmbed && is.empty(player.config.ratio)) { 4475 return; 4476 } 4477 4478 // Set Vimeo gutter 4479 setGutter(); 4480 4481 // Watch for resizes 4482 const method = event.type === 'enterfullscreen' ? on : off; 4483 method.call(player, window, 'resize', resized); 4484 }); 4485 }); 4486 // Listen for media events 4487 _defineProperty$1(this, "media", () => { 4488 const { 4489 player 4490 } = this; 4491 const { 4492 elements 4493 } = player; 4494 4495 // Time change on media 4496 on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event)); 4497 4498 // Display duration 4499 on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event)); 4500 4501 // Handle the media finishing 4502 on.call(player, player.media, 'ended', () => { 4503 // Show poster on end 4504 if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) { 4505 // Restart 4506 player.restart(); 4507 4508 // Call pause otherwise IE11 will start playing the video again 4509 player.pause(); 4510 } 4511 }); 4512 4513 // Check for buffer progress 4514 on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event)); 4515 4516 // Handle volume changes 4517 on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event)); 4518 4519 // Handle play/pause 4520 on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event)); 4521 4522 // Loading state 4523 on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event)); 4524 4525 // Click video 4526 if (player.supported.ui && player.config.clickToPlay && !player.isAudio) { 4527 // Re-fetch the wrapper 4528 const wrapper = getElement.call(player, `.${player.config.classNames.video}`); 4529 4530 // Bail if there's no wrapper (this should never happen) 4531 if (!is.element(wrapper)) { 4532 return; 4533 } 4534 4535 // On click play, pause or restart 4536 on.call(player, elements.container, 'click', event => { 4537 const targets = [elements.container, wrapper]; 4538 4539 // Ignore if click if not container or in video wrapper 4540 if (!targets.includes(event.target) && !wrapper.contains(event.target)) { 4541 return; 4542 } 4543 4544 // Touch devices will just show controls (if hidden) 4545 if (player.touch && player.config.hideControls) { 4546 return; 4547 } 4548 if (player.ended) { 4549 this.proxy(event, player.restart, 'restart'); 4550 this.proxy(event, () => { 4551 silencePromise(player.play()); 4552 }, 'play'); 4553 } else { 4554 this.proxy(event, () => { 4555 silencePromise(player.togglePlay()); 4556 }, 'play'); 4557 } 4558 }); 4559 } 4560 4561 // Disable right click 4562 if (player.supported.ui && player.config.disableContextMenu) { 4563 on.call(player, elements.wrapper, 'contextmenu', event => { 4564 event.preventDefault(); 4565 }, false); 4566 } 4567 4568 // Volume change 4569 on.call(player, player.media, 'volumechange', () => { 4570 // Save to storage 4571 player.storage.set({ 4572 volume: player.volume, 4573 muted: player.muted 4574 }); 4575 }); 4576 4577 // Speed change 4578 on.call(player, player.media, 'ratechange', () => { 4579 // Update UI 4580 controls.updateSetting.call(player, 'speed'); 4581 4582 // Save to storage 4583 player.storage.set({ 4584 speed: player.speed 4585 }); 4586 }); 4587 4588 // Quality change 4589 on.call(player, player.media, 'qualitychange', event => { 4590 // Update UI 4591 controls.updateSetting.call(player, 'quality', null, event.detail.quality); 4592 }); 4593 4594 // Update download link when ready and if quality changes 4595 on.call(player, player.media, 'ready qualitychange', () => { 4596 controls.setDownloadUrl.call(player); 4597 }); 4598 4599 // Proxy events to container 4600 // Bubble up key events for Edge 4601 const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' '); 4602 on.call(player, player.media, proxyEvents, event => { 4603 let { 4604 detail = {} 4605 } = event; 4606 4607 // Get error details from media 4608 if (event.type === 'error') { 4609 detail = player.media.error; 4610 } 4611 triggerEvent.call(player, elements.container, event.type, true, detail); 4612 }); 4613 }); 4614 // Run default and custom handlers 4615 _defineProperty$1(this, "proxy", (event, defaultHandler, customHandlerKey) => { 4616 const { 4617 player 4618 } = this; 4619 const customHandler = player.config.listeners[customHandlerKey]; 4620 const hasCustomHandler = is.function(customHandler); 4621 let returned = true; 4622 4623 // Execute custom handler 4624 if (hasCustomHandler) { 4625 returned = customHandler.call(player, event); 4626 } 4627 4628 // Only call default handler if not prevented in custom handler 4629 if (returned !== false && is.function(defaultHandler)) { 4630 defaultHandler.call(player, event); 4631 } 4632 }); 4633 // Trigger custom and default handlers 4634 _defineProperty$1(this, "bind", (element, type, defaultHandler, customHandlerKey, passive = true) => { 4635 const { 4636 player 4637 } = this; 4638 const customHandler = player.config.listeners[customHandlerKey]; 4639 const hasCustomHandler = is.function(customHandler); 4640 on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); 4641 }); 4642 // Listen for control events 4643 _defineProperty$1(this, "controls", () => { 4644 const { 4645 player 4646 } = this; 4647 const { 4648 elements 4649 } = player; 4650 // IE doesn't support input event, so we fallback to change 4651 const inputEvent = browser.isIE ? 'change' : 'input'; 4652 4653 // Play/pause toggle 4654 if (elements.buttons.play) { 4655 Array.from(elements.buttons.play).forEach(button => { 4656 this.bind(button, 'click', () => { 4657 silencePromise(player.togglePlay()); 4658 }, 'play'); 4659 }); 4660 } 4661 4662 // Pause 4663 this.bind(elements.buttons.restart, 'click', player.restart, 'restart'); 4664 4665 // Rewind 4666 this.bind(elements.buttons.rewind, 'click', () => { 4667 // Record seek time so we can prevent hiding controls for a few seconds after rewind 4668 player.lastSeekTime = Date.now(); 4669 player.rewind(); 4670 }, 'rewind'); 4671 4672 // Rewind 4673 this.bind(elements.buttons.fastForward, 'click', () => { 4674 // Record seek time so we can prevent hiding controls for a few seconds after fast forward 4675 player.lastSeekTime = Date.now(); 4676 player.forward(); 4677 }, 'fastForward'); 4678 4679 // Mute toggle 4680 this.bind(elements.buttons.mute, 'click', () => { 4681 player.muted = !player.muted; 4682 }, 'mute'); 4683 4684 // Captions toggle 4685 this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions()); 4686 4687 // Download 4688 this.bind(elements.buttons.download, 'click', () => { 4689 triggerEvent.call(player, player.media, 'download'); 4690 }, 'download'); 4691 4692 // Fullscreen toggle 4693 this.bind(elements.buttons.fullscreen, 'click', () => { 4694 player.fullscreen.toggle(); 4695 }, 'fullscreen'); 4696 4697 // Picture-in-Picture 4698 this.bind(elements.buttons.pip, 'click', () => { 4699 player.pip = 'toggle'; 4700 }, 'pip'); 4701 4702 // Airplay 4703 this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); 4704 4705 // Settings menu - click toggle 4706 this.bind(elements.buttons.settings, 'click', event => { 4707 // Prevent the document click listener closing the menu 4708 event.stopPropagation(); 4709 event.preventDefault(); 4710 controls.toggleMenu.call(player, event); 4711 }, null, false); // Can't be passive as we're preventing default 4712 4713 // Settings menu - keyboard toggle 4714 // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus 4715 // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 4716 this.bind(elements.buttons.settings, 'keyup', event => { 4717 if (![' ', 'Enter'].includes(event.key)) { 4718 return; 4719 } 4720 4721 // Because return triggers a click anyway, all we need to do is set focus 4722 if (event.key === 'Enter') { 4723 controls.focusFirstMenuItem.call(player, null, true); 4724 return; 4725 } 4726 4727 // Prevent scroll 4728 event.preventDefault(); 4729 4730 // Prevent playing video (Firefox) 4731 event.stopPropagation(); 4732 4733 // Toggle menu 4734 controls.toggleMenu.call(player, event); 4735 }, null, false // Can't be passive as we're preventing default 4736 ); 4737 4738 // Escape closes menu 4739 this.bind(elements.settings.menu, 'keydown', event => { 4740 if (event.key === 'Escape') { 4741 controls.toggleMenu.call(player, event); 4742 } 4743 }); 4744 4745 // Set range input alternative "value", which matches the tooltip time (#954) 4746 this.bind(elements.inputs.seek, 'mousedown mousemove', event => { 4747 const rect = elements.progress.getBoundingClientRect(); 4748 const
4748percent = 100 / rect.width * (event.pageX - rect.l
vendor: 10,136 bytes, lines 4748-5023
4748eft); 4749 event.currentTarget.setAttribute('seek-value', percent); 4750 }); 4751 4752 // Pause while seeking 4753 this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { 4754 const seek = event.currentTarget; 4755 const attribute = 'play-on-seeked'; 4756 if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) { 4757 return; 4758 } 4759 4760 // Record seek time so we can prevent hiding controls for a few seconds after seek 4761 player.lastSeekTime = Date.now(); 4762 4763 // Was playing before? 4764 const play = seek.hasAttribute(attribute); 4765 // Done seeking 4766 const done = ['mouseup', 'touchend', 'keyup'].includes(event.type); 4767 4768 // If we're done seeking and it was playing, resume playback 4769 if (play && done) { 4770 seek.removeAttribute(attribute); 4771 silencePromise(player.play()); 4772 } else if (!done && player.playing) { 4773 seek.setAttribute(attribute, ''); 4774 player.pause(); 4775 } 4776 }); 4777 4778 // Fix range inputs on iOS 4779 // Super weird iOS bug where after you interact with an <input type="range">, 4780 // it takes over further interactions on the page. This is a hack 4781 if (browser.isIos) { 4782 const inputs = getElements.call(player, 'input[type="range"]'); 4783 Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target))); 4784 } 4785 4786 // Seek 4787 this.bind(elements.inputs.seek, inputEvent, event => { 4788 const seek = event.currentTarget; 4789 // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) 4790 let seekTo = seek.getAttribute('seek-value'); 4791 if (is.empty(seekTo)) { 4792 seekTo = seek.value; 4793 } 4794 seek.removeAttribute('seek-value'); 4795 player.currentTime = seekTo / seek.max * player.duration; 4796 }, 'seek'); 4797 4798 // Seek tooltip 4799 this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event)); 4800 4801 // Preview thumbnails plugin 4802 // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this 4803 this.bind(elements.progress, 'mousemove touchmove', event => { 4804 const { 4805 previewThumbnails 4806 } = player; 4807 if (previewThumbnails && previewThumbnails.loaded) { 4808 previewThumbnails.startMove(event); 4809 } 4810 }); 4811 4812 // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering 4813 this.bind(elements.progress, 'mouseleave touchend click', () => { 4814 const { 4815 previewThumbnails 4816 } = player; 4817 if (previewThumbnails && previewThumbnails.loaded) { 4818 previewThumbnails.endMove(false, true); 4819 } 4820 }); 4821 4822 // Show scrubbing preview 4823 this.bind(elements.progress, 'mousedown touchstart', event => { 4824 const { 4825 previewThumbnails 4826 } = player; 4827 if (previewThumbnails && previewThumbnails.loaded) { 4828 previewThumbnails.startScrubbing(event); 4829 } 4830 }); 4831 this.bind(elements.progress, 'mouseup touchend', event => { 4832 const { 4833 previewThumbnails 4834 } = player; 4835 if (previewThumbnails && previewThumbnails.loaded) { 4836 previewThumbnails.endScrubbing(event); 4837 } 4838 }); 4839 4840 // Polyfill for lower fill in <input type="range"> for webkit 4841 if (browser.isWebKit) { 4842 Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => { 4843 this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target)); 4844 }); 4845 } 4846 4847 // Current time invert 4848 // Only if one time element is used for both currentTime and duration 4849 if (player.config.toggleInvert && !is.element(elements.display.duration)) { 4850 this.bind(elements.display.currentTime, 'click', () => { 4851 // Do nothing if we're at the start 4852 if (player.currentTime === 0) { 4853 return; 4854 } 4855 player.config.invertTime = !player.config.invertTime; 4856 controls.timeUpdate.call(player); 4857 }); 4858 } 4859 4860 // Volume 4861 this.bind(elements.inputs.volume, inputEvent, event => { 4862 player.volume = event.target.value; 4863 }, 'volume'); 4864 4865 // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) 4866 this.bind(elements.controls, 'mouseenter mouseleave', event => { 4867 elements.controls.hover = !player.touch && event.type === 'mouseenter'; 4868 }); 4869 4870 // Also update controls.hover state for any non-player children of fullscreen element (as above) 4871 if (elements.fullscreen) { 4872 Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => { 4873 this.bind(child, 'mouseenter mouseleave', event => { 4874 if (elements.controls) { 4875 elements.controls.hover = !player.touch && event.type === 'mouseenter'; 4876 } 4877 }); 4878 }); 4879 } 4880 4881 // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) 4882 this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { 4883 elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); 4884 }); 4885 4886 // Show controls when they receive focus (e.g., when using keyboard tab key) 4887 this.bind(elements.controls, 'focusin', () => { 4888 const { 4889 config, 4890 timers 4891 } = player; 4892 4893 // Skip transition to prevent focus from scrolling the parent element 4894 toggleClass(elements.controls, config.classNames.noTransition, true); 4895 4896 // Toggle 4897 ui.toggleControls.call(player, true); 4898 4899 // Restore transition 4900 setTimeout(() => { 4901 toggleClass(elements.controls, config.classNames.noTransition, false); 4902 }, 0); 4903 4904 // Delay a little more for mouse users 4905 const delay = this.touch ? 3000 : 4000; 4906 4907 // Clear timer 4908 clearTimeout(timers.controls); 4909 4910 // Hide again after delay 4911 timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay); 4912 }); 4913 4914 // Mouse wheel for volume 4915 this.bind(elements.inputs.volume, 'wheel', event => { 4916 // Detect "natural" scroll - supported on OS X Safari only 4917 // Other browsers on OS X will be inverted until support improves 4918 const inverted = event.webkitDirectionInvertedFromDevice; 4919 // Get delta from event. Invert if `inverted` is true 4920 const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value); 4921 // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta) 4922 const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); 4923 4924 // Change the volume by 2% 4925 player.increaseVolume(direction / 50); 4926 4927 // Don't break page scrolling at max and min 4928 const { 4929 volume 4930 } = player.media; 4931 if (direction === 1 && volume < 1 || direction === -1 && volume > 0) { 4932 event.preventDefault(); 4933 } 4934 }, 'volume', false); 4935 }); 4936 this.player = _player; 4937 this.lastKey = null; 4938 this.focusTimer = null; 4939 this.lastKeyDown = null; 4940 this.handleKey = this.handleKey.bind(this); 4941 this.toggleMenu = this.toggleMenu.bind(this); 4942 this.firstTouch = this.firstTouch.bind(this); 4943 } 4944 4945 // Handle key presses 4946 handleKey(event) { 4947 const { 4948 player 4949 } = this; 4950 const { 4951 elements 4952 } = player; 4953 const { 4954 key, 4955 type, 4956 altKey, 4957 ctrlKey, 4958 metaKey, 4959 shiftKey 4960 } = event; 4961 const pressed = type === 'keydown'; 4962 const repeat = pressed && key === this.lastKey; 4963 4964 // Bail if a modifier key is set 4965 if (altKey || ctrlKey || metaKey || shiftKey) { 4966 return; 4967 } 4968 4969 // If the event is bubbled from the media element 4970 // Firefox doesn't get the key for whatever reason 4971 if (!key) { 4972 return; 4973 } 4974 4975 // Seek by increment 4976 const seekByIncrement = increment => { 4977 // Divide the max duration into 10th's and times by the number value 4978 player.currentTime = player.duration / 10 * increment; 4979 }; 4980 4981 // Handle the key on keydown 4982 // Reset on keyup 4983 if (pressed) { 4984 // Check focused element 4985 // and if the focused element is not editable (e.g. text input) 4986 // and any that accept key input http://webaim.org/techniques/keyboard/ 4987 const focused = document.activeElement; 4988 if (is.element(focused)) { 4989 const { 4990 editable 4991 } = player.config.selectors; 4992 const { 4993 seek 4994 } = elements.inputs; 4995 if (focused !== seek && matches(focused, editable)) { 4996 return; 4997 } 4998 if (event.key === ' ' && matches(focused, 'button, [role^="menuitem"]')) { 4999 return; 5000 } 5001 } 5002 5003 // Which keys should we prevent default 5004 const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm']; 5005 5006 // If the key is found prevent default (e.g. prevent scrolling for arrows) 5007 if (preventDefault.includes(key)) { 5008 event.preventDefault(); 5009 event.stopPropagation(); 5010 } 5011 switch (key) { 5012 case '0': 5013 case '1': 5014 case '2': 5015 case '3': 5016 case '4': 5017 case '5': 5018 case '6': 5019 case '7': 5020 case '8': 5021 case '9': 5022 if (!repeat) { 5023 seekByIncrement(
vendor: 1,821 bytes, lines 5023-5087
5023parseInt(key, 10)); 5024 } 5025 break; 5026 case ' ': 5027 case 'k': 5028 if (!repeat) { 5029 silencePromise(player.togglePlay()); 5030 } 5031 break; 5032 case 'ArrowUp': 5033 player.increaseVolume(0.1); 5034 break; 5035 case 'ArrowDown': 5036 player.decreaseVolume(0.1); 5037 break; 5038 case 'm': 5039 if (!repeat) { 5040 player.muted = !player.muted; 5041 } 5042 break; 5043 case 'ArrowRight': 5044 player.forward(); 5045 break; 5046 case 'ArrowLeft': 5047 player.rewind(); 5048 break; 5049 case 'f': 5050 player.fullscreen.toggle(); 5051 break; 5052 case 'c': 5053 if (!repeat) { 5054 player.toggleCaptions(); 5055 } 5056 break; 5057 case 'l': 5058 player.loop = !player.loop; 5059 break; 5060 } 5061 5062 // Escape is handle natively when in full screen 5063 // So we only need to worry about non native 5064 if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) { 5065 player.fullscreen.toggle(); 5066 } 5067 5068 // Store last key for next cycle 5069 this.lastKey = key; 5070 } else { 5071 this.lastKey = null; 5072 } 5073 } 5074 5075 // Toggle menu 5076 toggleMenu(event) { 5077 controls.toggleMenu.call(this.player, event); 5078 } 5079 } 5080 5081 var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 5082 5083 function createCommonjsModule(fn, module) { 5084 return module = { exports: {} }, fn(module, module.exports), module.exports; 5085 } 5086 5087 var loadjs_umd = createCommonjsModule(function (module, exports
vendor: 6,402 bytes, lines 5087-5293
5087) { 5088 (function (root, factory) { 5089 { 5090 module.exports = factory(); 5091 } 5092 })(commonjsGlobal, function () { 5093 /** 5094 * Global dependencies. 5095 * @global {Object} document - DOM 5096 */ 5097 5098 var devnull = function () {}, 5099 bundleIdCache = {}, 5100 bundleResultCache = {}, 5101 bundleCallbackQueue = {}; 5102 5103 /** 5104 * Subscribe to bundle load event. 5105 * @param {string[]} bundleIds - Bundle ids 5106 * @param {Function} callbackFn - The callback function 5107 */ 5108 function subscribe(bundleIds, callbackFn) { 5109 // listify 5110 bundleIds = bundleIds.push ? bundleIds : [bundleIds]; 5111 var depsNotFound = [], 5112 i = bundleIds.length, 5113 numWaiting = i, 5114 fn, 5115 bundleId, 5116 r, 5117 q; 5118 5119 // define callback function 5120 fn = function (bundleId, pathsNotFound) { 5121 if (pathsNotFound.length) depsNotFound.push(bundleId); 5122 numWaiting--; 5123 if (!numWaiting) callbackFn(depsNotFound); 5124 }; 5125 5126 // register callback 5127 while (i--) { 5128 bundleId = bundleIds[i]; 5129 5130 // execute callback if in result cache 5131 r = bundleResultCache[bundleId]; 5132 if (r) { 5133 fn(bundleId, r); 5134 continue; 5135 } 5136 5137 // add to callback queue 5138 q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || []; 5139 q.push(fn); 5140 } 5141 } 5142 5143 /** 5144 * Publish bundle load event. 5145 * @param {string} bundleId - Bundle id 5146 * @param {string[]} pathsNotFound - List of files not found 5147 */ 5148 function publish(bundleId, pathsNotFound) { 5149 // exit if id isn't defined 5150 if (!bundleId) return; 5151 var q = bundleCallbackQueue[bundleId]; 5152 5153 // cache result 5154 bundleResultCache[bundleId] = pathsNotFound; 5155 5156 // exit if queue is empty 5157 if (!q) return; 5158 5159 // empty callback queue 5160 while (q.length) { 5161 q[0](bundleId, pathsNotFound); 5162 q.splice(0, 1); 5163 } 5164 } 5165 5166 /** 5167 * Execute callbacks. 5168 * @param {Object or Function} args - The callback args 5169 * @param {string[]} depsNotFound - List of dependencies not found 5170 */ 5171 function executeCallbacks(args, depsNotFound) { 5172 // accept function as argument 5173 if (args.call) args = { 5174 success: args 5175 }; 5176 5177 // success and error callbacks 5178 if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args); 5179 } 5180 5181 /** 5182 * Load individual file. 5183 * @param {string} path - The file path 5184 * @param {Function} callbackFn - The callback function 5185 */ 5186 function loadFile(path, callbackFn, args, numTries) { 5187 var doc = document, 5188 async = args.async, 5189 maxTries = (args.numRetries || 0) + 1, 5190 beforeCallbackFn = args.before || devnull, 5191 pathname = path.replace(/[\?|#].*$/, ''), 5192 pathStripped = path.replace(/^(css|img)!/, ''), 5193 isLegacyIECss, 5194 e; 5195 numTries = numTries || 0; 5196 if (/(^css!|\.css$)/.test(pathname)) { 5197 // css 5198 e = doc.createElement('link'); 5199 e.rel = 'stylesheet'; 5200 e.href = pathStripped; 5201 5202 // tag IE9+ 5203 isLegacyIECss = 'hideFocus' in e; 5204 5205 // use preload in IE Edge (to detect load errors) 5206 if (isLegacyIECss && e.relList) { 5207 isLegacyIECss = 0; 5208 e.rel = 'preload'; 5209 e.as = 'style'; 5210 } 5211 } else if (/(^img!|\.(png|gif|jpg|svg|webp)$)/.test(pathname)) { 5212 // image 5213 e = doc.createElement('img'); 5214 e.src = pathStripped; 5215 } else { 5216 // javascript 5217 e = doc.createElement('script'); 5218 e.src = path; 5219 e.async = async === undefined ? true : async; 5220 } 5221 e.onload = e.onerror = e.onbeforeload = function (ev) { 5222 var result = ev.type[0]; 5223 5224 // treat empty stylesheets as failures to get around lack of onerror 5225 // support in IE9-11 5226 if (isLegacyIECss) { 5227 try { 5228 if (!e.sheet.cssText.length) result = 'e'; 5229 } catch (x) { 5230 // sheets objects created from load errors don't allow access to 5231 // `cssText` (unless error is Code:18 SecurityError) 5232 if (x.code != 18) result = 'e'; 5233 } 5234 } 5235 5236 // handle retries in case of load failure 5237 if (result == 'e') { 5238 // increment counter 5239 numTries += 1; 5240 5241 // exit function and try again 5242 if (numTries < maxTries) { 5243 return loadFile(path, callbackFn, args, numTries); 5244 } 5245 } else if (e.rel == 'preload' && e.as == 'style') { 5246 // activate preloaded stylesheets 5247 return e.rel = 'stylesheet'; // jshint ignore:line 5248 } 5249 5250 // execute callback 5251 callbackFn(path, result, ev.defaultPrevented); 5252 }; 5253 5254 // add to document (unless callback returns `false`) 5255 if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e); 5256 } 5257 5258 /** 5259 * Load multiple files. 5260 * @param {string[]} paths - The file paths 5261 * @param {Function} callbackFn - The callback function 5262 */ 5263 function loadFiles(paths, callbackFn, args) { 5264 // listify paths 5265 paths = paths.push ? paths : [paths]; 5266 var numWaiting = paths.length, 5267 x = numWaiting, 5268 pathsNotFound = [], 5269 fn, 5270 i; 5271 5272 // define callback function 5273 fn = function (path, result, defaultPrevented) { 5274 // handle error 5275 if (result == 'e') pathsNotFound.push(path); 5276 5277 // handle beforeload event. If defaultPrevented then that means the load 5278 // will be blocked (ex. Ghostery/ABP on Safari) 5279 if (result == 'b') { 5280 if (defaultPrevented) pathsNotFound.push(path);else return; 5281 } 5282 numWaiting--; 5283 if (!numWaiting) callbackFn(pathsNotFound); 5284 }; 5285 5286 // load scripts 5287 for (i = 0; i < x; i++) loadFile(paths[i], fn, args); 5288 } 5289 5290 /** 5291 * Initiate script load and register bundle. 5292 * @param {(string|string[])} paths - The file paths 5293 * @param {(string|Function|Object)}
5293 [arg1] - The (1) bundleId or (2) success 5294 * callback or (3) object literal with success/error arguments, numRetries, 5295 * etc. 5296 * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object 5297 * literal with success/error arguments, numRetries, etc. 5298 */ 5299 function loadjs(paths, arg1, arg2) { 5300 var bundleId, args; 5301 5302 // bundleId (if string) 5303 if (arg1 && arg1.trim) bundleId = arg1; 5304 5305 // args (default is {}) 5306 args = (bundleId ? arg2 : arg1) || {}; 5307 5308 // throw error if bundle is already defined 5309 if (bundleId) { 5310 if (bundleId in bundleIdCache) { 5311 throw "LoadJS"; 5312 } else { 5313 bundleIdCache[bundleId] = true; 5314 } 5315 } 5316 function loadFn(resolve, reject) { 5317 loadFiles(paths, function (pathsNotFound) { 5318 // execute callbacks 5319 executeCallbacks(args, pathsNotFound); 5320 5321 // resolve Promise 5322 if (resolve) { 5323 executeCallbacks({ 5324 success: resolve, 5325 error: reject 5326 }, pathsNotFound); 5327 } 5328 5329 // publish bundle load event 5330 publish(bundleId, pathsNotFound); 5331 }, args); 5332 } 5333 if (args.returnPromise) return new Promise(loadFn);else loadFn(); 5334 } 5335 5336 /** 5337 * Execute callbacks when dependencies have been satisfied. 5338 * @param {(string|string[])} deps - List of bundle ids 5339 * @param {Object} args - success/error arguments 5340 */ 5341 loadjs.ready = function ready(deps, args) { 5342 // subscribe to bundle load event 5343 subscribe(deps, function (depsNotFound) { 5344 // execute callbacks 5345 executeCallbacks(args, depsNotFound); 5346 }); 5347 return loadjs; 5348 }; 5349 5350 /** 5351 * Manually satisfy bundle dependencies. 5352 * @param {string} bundleId - The bundle id 5353 */ 5354 loadjs.done = function done(bundleId) { 5355 publish(bundleId, []); 5356 }; 5357 5358 /** 5359 * Reset loadjs dependencies statuses 5360 */ 5361 loadjs.reset = function reset() { 5362 bundleIdCache = {}; 5363 bundleResultCache = {}; 5364 bundleCallbackQueue = {}; 5365 }; 5366 5367 /** 5368 * Determine if bundle has already been defined 5369 * @param String} bundleId - The bundle id 5370 */ 5371 loadjs.isDefined = function isDefined(bundleId) { 5372 return bundleId in bundleIdCache; 5373 }; 5374 5375 // export 5376 return loadjs; 5377 }); 5378 }); 5379 5380 // ========================================================================== 5381 function loadScript(url) { 5382 return new Promise((resolve, reject) => { 5383 loadjs_umd(url, { 5384 success: resolve, 5385 error: reject 5386 }); 5387 }); 5388 } 5389 5390 // ========================================================================== 5391 5392 // Parse Vimeo ID from URL 5393 function parseId$1(url) { 5394 if (is.empty(url)) { 5395 return null; 5396 } 5397 if (is.number(Number(url))) { 5398 return url; 5399 } 5400 const regex = /^.*(vimeo.com\/|video\/)(\d+).*/; 5401 return url.match(regex) ? RegExp.$2 : url; 5402 } 5403 5404 // Try to extract a hash for private videos from the URL 5405 function parseHash(url) { 5406 /* This regex matches a hexadecimal hash if given in any of these forms: 5407 * - [https://player.]vimeo.com/video/{id}/{hash}[?params] 5408 * - [https://player.]vimeo.com/video/{id}?h={hash}[¶ms] 5409 * - [https://player.]vimeo.com/video/{id}?[params]&h={hash} 5410 * - video/{id}/{hash} 5411 * If matched, the hash is available in capture group 4 5412 */ 5413 const regex = /^.*(vimeo.com\/|video\/)(\d+)(\?.*&*h=|\/)+([\d,a-f]+)/; 5414 const found = url.match(regex); 5415 return found && found.length === 5 ? found[4] : null; 5416 } 5417 5418 // Set playback state and trigger change (only on actual change) 5419 function assurePlaybackState$1(play) { 5420 if (play && !this.embed.hasPlayed) { 5421 this.embed.hasPlayed = true; 5422 } 5423 if (this.media.paused === play) { 5424 this.media.paused = !play; 5425 triggerEvent.call(this, this.media, play ? 'play' : 'pause'); 5426 } 5427 } 5428 const vimeo = { 5429 setup() { 5430 const player = this; 5431 5432 // Add embed class for responsive 5433 toggleClass(player.elements.wrapper, player.config.classNames.embed, true); 5434 5435 // Set speed options from config 5436 player.options.speed = player.config.speed.options; 5437 5438 // Set intial ratio 5439 setAspectRatio.call(player); 5440 5441 // Load the SDK if not already 5442 if (!is.object(window.Vimeo)) { 5443 loadScript(player.config.urls.vimeo.sdk).then(() => { 5444 vimeo.ready.call(player); 5445 }).catch(error => { 5446 player.debug.warn('Vimeo SDK (player.js) failed to load', error); 5447 }); 5448 } else { 5449 vimeo.ready.call(player); 5450 } 5451 }, 5452 // API Ready 5453 ready() { 5454 const player = this; 5455 const config = player.config.vimeo; 5456 const { 5457 premium, 5458 referrerPolicy,
vendor: 1,964 bytes, lines 5459-5514
5459 ...frameParams 5460 } = config; 5461 // Get the source URL or ID 5462 let source = player.media.getAttribute('src'); 5463 let hash = ''; 5464 // Get from <div> if needed 5465 if (is.empty(source)) { 5466 source = player.media.getAttribute(player.config.attributes.embed.id); 5467 // hash can also be set as attribute on the <div> 5468 hash = player.media.getAttribute(player.config.attributes.embed.hash); 5469 } else { 5470 hash = parseHash(source); 5471 } 5472 const hashParam = hash ? { 5473 h: hash 5474 } : {}; 5475 5476 // If the owner has a pro or premium account then we can hide controls etc 5477 if (premium) { 5478 Object.assign(frameParams, { 5479 controls: false, 5480 sidedock: false 5481 }); 5482 } 5483 5484 // Get Vimeo params for the iframe 5485 const params = buildUrlParams({ 5486 loop: player.config.loop.active, 5487 autoplay: player.autoplay, 5488 muted: player.muted, 5489 gesture: 'media', 5490 playsinline: player.config.playsinline, 5491 // hash has to be added to iframe-URL 5492 ...hashParam, 5493 ...frameParams 5494 }); 5495 const id = parseId$1(source); 5496 // Build an iframe 5497 const iframe = createElement('iframe'); 5498 const src = format(player.config.urls.vimeo.iframe, id, params); 5499 iframe.setAttribute('src', src); 5500 iframe.setAttribute('allowfullscreen', ''); 5501 iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; ')); 5502 5503 // Set the referrer policy if required 5504 if (!is.empty(referrerPolicy)) { 5505 iframe.setAttribute('referrerPolicy', referrerPolicy); 5506 } 5507 5508 // Inject the package 5509 if (premium || !config.customControls) { 5510 iframe.setAttribute('data-poster', player.poster); 5511 player.media = replaceElement(iframe, player.media); 5512 } else { 5513 const wrapper = createElement('div', { 5514 class
vendor: 7,560 bytes, lines 5514-5765
5514: player.config.classNames.embedContainer, 5515 'data-poster': player.poster 5516 }); 5517 wrapper.appendChild(iframe); 5518 player.media = replaceElement(wrapper, player.media); 5519 } 5520 5521 // Get poster image 5522 if (!config.customControls) { 5523 fetch(format(player.config.urls.vimeo.api, src)).then(response => { 5524 if (is.empty(response) || !response.thumbnail_url) { 5525 return; 5526 } 5527 5528 // Set and show poster 5529 ui.setPoster.call(player, response.thumbnail_url).catch(() => {}); 5530 }); 5531 } 5532 5533 // Setup instance 5534 // https://github.com/vimeo/player.js 5535 player.embed = new window.Vimeo.Player(iframe, { 5536 autopause: player.config.autopause, 5537 muted: player.muted 5538 }); 5539 player.media.paused = true; 5540 player.media.currentTime = 0; 5541 5542 // Disable native text track rendering 5543 if (player.supported.ui) { 5544 player.embed.disableTextTrack(); 5545 } 5546 5547 // Create a faux HTML5 API using the Vimeo API 5548 player.media.play = () => { 5549 assurePlaybackState$1.call(player, true); 5550 return player.embed.play(); 5551 }; 5552 player.media.pause = () => { 5553 assurePlaybackState$1.call(player, false); 5554 return player.embed.pause(); 5555 }; 5556 player.media.stop = () => { 5557 player.pause(); 5558 player.currentTime = 0; 5559 }; 5560 5561 // Seeking 5562 let { 5563 currentTime 5564 } = player.media; 5565 Object.defineProperty(player.media, 'currentTime', { 5566 get() { 5567 return currentTime; 5568 }, 5569 set(time) { 5570 // Vimeo will automatically play on seek if the video hasn't been played before 5571 5572 // Get current paused state and volume etc 5573 const { 5574 embed, 5575 media, 5576 paused, 5577 volume 5578 } = player; 5579 const restorePause = paused && !embed.hasPlayed; 5580 5581 // Set seeking state and trigger event 5582 media.seeking = true; 5583 triggerEvent.call(player, media, 'seeking'); 5584 5585 // If paused, mute until seek is complete 5586 Promise.resolve(restorePause && embed.setVolume(0)) 5587 // Seek 5588 .then(() => embed.setCurrentTime(time)) 5589 // Restore paused 5590 .then(() => restorePause && embed.pause()) 5591 // Restore volume 5592 .then(() => restorePause && embed.setVolume(volume)).catch(() => { 5593 // Do nothing 5594 }); 5595 } 5596 }); 5597 5598 // Playback speed 5599 let speed = player.config.speed.selected; 5600 Object.defineProperty(player.media, 'playbackRate', { 5601 get() { 5602 return speed; 5603 }, 5604 set(input) { 5605 player.embed.setPlaybackRate(input).then(() => { 5606 speed = input; 5607 triggerEvent.call(player, player.media, 'ratechange'); 5608 }).catch(() => { 5609 // Cannot set Playback Rate, Video is probably not on Pro account 5610 player.options.speed = [1]; 5611 }); 5612 } 5613 }); 5614 5615 // Volume 5616 let { 5617 volume 5618 } = player.config; 5619 Object.defineProperty(player.media, 'volume', { 5620 get() { 5621 return volume; 5622 }, 5623 set(input) { 5624 player.embed.setVolume(input).then(() => { 5625 volume = input; 5626 triggerEvent.call(player, player.media, 'volumechange'); 5627 }); 5628 } 5629 }); 5630 5631 // Muted 5632 let { 5633 muted 5634 } = player.config; 5635 Object.defineProperty(player.media, 'muted', { 5636 get() { 5637 return muted; 5638 }, 5639 set(input) { 5640 const toggle = is.boolean(input) ? input : false; 5641 player.embed.setMuted(toggle ? true : player.config.muted).then(() => { 5642 muted = toggle; 5643 triggerEvent.call(player, player.media, 'volumechange'); 5644 }); 5645 } 5646 }); 5647 5648 // Loop 5649 let { 5650 loop 5651 } = player.config; 5652 Object.defineProperty(player.media, 'loop', { 5653 get() { 5654 return loop; 5655 }, 5656 set(input) { 5657 const toggle = is.boolean(input) ? input : player.config.loop.active; 5658 player.embed.setLoop(toggle).then(() => { 5659 loop = toggle; 5660 }); 5661 } 5662 }); 5663 5664 // Source 5665 let currentSrc; 5666 player.embed.getVideoUrl().then(value => { 5667 currentSrc = value; 5668 controls.setDownloadUrl.call(player); 5669 }).catch(error => { 5670 this.debug.warn(error); 5671 }); 5672 Object.defineProperty(player.media, 'currentSrc', { 5673 get() { 5674 return currentSrc; 5675 } 5676 }); 5677 5678 // Ended 5679 Object.defineProperty(player.media, 'ended', { 5680 get() { 5681 return player.currentTime === player.duration; 5682 } 5683 }); 5684 5685 // Set aspect ratio based on video size 5686 Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { 5687 const [width, height] = dimensions; 5688 player.embed.ratio = roundAspectRatio(width, height); 5689 setAspectRatio.call(this); 5690 }); 5691 5692 // Set autopause 5693 player.embed.setAutopause(player.config.autopause).then(state => { 5694 player.config.autopause = state; 5695 }); 5696 5697 // Get title 5698 player.embed.getVideoTitle().then(title => { 5699 player.config.title = title; 5700 ui.setTitle.call(this); 5701 }); 5702 5703 // Get current time 5704 player.embed.getCurrentTime().then(value => { 5705 currentTime = value; 5706 triggerEvent.call(player, player.media, 'timeupdate'); 5707 }); 5708 5709 // Get duration 5710 player.embed.getDuration().then(value => { 5711 player.media.duration = value; 5712 triggerEvent.call(player, player.media, 'durationchange'); 5713 }); 5714 5715 // Get captions 5716 player.embed.getTextTracks().then(tracks => { 5717 player.media.textTracks = tracks; 5718 captions.setup.call(player); 5719 }); 5720 player.embed.on('cuechange', ({ 5721 cues = [] 5722 }) => { 5723 const strippedCues = cues.map(cue => stripHTML(cue.text)); 5724 captions.updateCues.call(player, strippedCues); 5725 }); 5726 player.embed.on('loaded', () => { 5727 // Assure state and events are updated on autoplay 5728 player.embed.getPaused().then(paused => { 5729 assurePlaybackState$1.call(player, !paused); 5730 if (!paused) { 5731 triggerEvent.call(player, player.media, 'playing'); 5732 } 5733 }); 5734 if (is.element(player.embed.element) && player.supported.ui) { 5735 const frame = player.embed.element; 5736 5737 // Fix keyboard focus issues 5738 // https://github.com/sampotts/plyr/issues/317 5739 frame.setAttribute('tabindex', -1); 5740 } 5741 }); 5742 player.embed.on('bufferstart', () => { 5743 triggerEvent.call(player, player.media, 'waiting'); 5744 }); 5745 player.embed.on('bufferend', () => { 5746 triggerEvent.call(player, player.media, 'playing'); 5747 }); 5748 player.embed.on('play', () => { 5749 assurePlaybackState$1.call(player, true); 5750 triggerEvent.call(player, player.media, 'playing'); 5751 }); 5752 player.embed.on('pause', () => { 5753 assurePlaybackState$1.call(player, false); 5754 }); 5755 player.embed.on('timeupdate', data => { 5756 player.media.seeking = false; 5757 currentTime = data.seconds; 5758 triggerEvent.call(player, player.media, 'timeupdate'); 5759 }); 5760 player.embed.on('progress', data => { 5761 player.media.buffered = data.percent; 5762 triggerEvent.call(player, player.media, 'progress'); 5763 5764 // Check all loaded 5765 if (
vendor: 1,374 bytes, lines 5765-5806
5765parseInt(data.percent, 10) === 1) { 5766 triggerEvent.call(player, player.media, 'canplaythrough'); 5767 } 5768 5769 // Get duration as if we do it before load, it gives an incorrect value 5770 // https://github.com/sampotts/plyr/issues/891 5771 player.embed.getDuration().then(value => { 5772 if (value !== player.media.duration) { 5773 player.media.duration = value; 5774 triggerEvent.call(player, player.media, 'durationchange'); 5775 } 5776 }); 5777 }); 5778 player.embed.on('seeked', () => { 5779 player.media.seeking = false; 5780 triggerEvent.call(player, player.media, 'seeked'); 5781 }); 5782 player.embed.on('ended', () => { 5783 player.media.paused = true; 5784 triggerEvent.call(player, player.media, 'ended'); 5785 }); 5786 player.embed.on('error', detail => { 5787 player.media.error = detail; 5788 triggerEvent.call(player, player.media, 'error'); 5789 }); 5790 5791 // Rebuild UI 5792 if (config.customControls) { 5793 setTimeout(() => ui.build.call(player), 0); 5794 } 5795 } 5796 }; 5797 5798 // ========================================================================== 5799 5800 // Parse YouTube ID from URL 5801 function parseId(url) { 5802 if (is.empty(url)) { 5803 return null; 5804 } 5805 const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; 5806 return url.match(regex) ? RegExp.$2
vendor: 14,998 bytes, lines 5806-6205
5806 : url; 5807 } 5808 5809 // Set playback state and trigger change (only on actual change) 5810 function assurePlaybackState(play) { 5811 if (play && !this.embed.hasPlayed) { 5812 this.embed.hasPlayed = true; 5813 } 5814 if (this.media.paused === play) { 5815 this.media.paused = !play; 5816 triggerEvent.call(this, this.media, play ? 'play' : 'pause'); 5817 } 5818 } 5819 function getHost(config) { 5820 if (config.noCookie) { 5821 return 'https://www.youtube-nocookie.com'; 5822 } 5823 if (window.location.protocol === 'http:') { 5824 return 'http://www.youtube.com'; 5825 } 5826 5827 // Use YouTube's default 5828 return undefined; 5829 } 5830 const youtube = { 5831 setup() { 5832 // Add embed class for responsive 5833 toggleClass(this.elements.wrapper, this.config.classNames.embed, true); 5834 5835 // Setup API 5836 if (is.object(window.YT) && is.function(window.YT.Player)) { 5837 youtube.ready.call(this); 5838 } else { 5839 // Reference current global callback 5840 const callback = window.onYouTubeIframeAPIReady; 5841 5842 // Set callback to process queue 5843 window.onYouTubeIframeAPIReady = () => { 5844 // Call global callback if set 5845 if (is.function(callback)) { 5846 callback(); 5847 } 5848 youtube.ready.call(this); 5849 }; 5850 5851 // Load the SDK 5852 loadScript(this.config.urls.youtube.sdk).catch(error => { 5853 this.debug.warn('YouTube API failed to load', error); 5854 }); 5855 } 5856 }, 5857 // Get the media title 5858 getTitle(videoId) { 5859 const url = format(this.config.urls.youtube.api, videoId); 5860 fetch(url).then(data => { 5861 if (is.object(data)) { 5862 const { 5863 title, 5864 height, 5865 width 5866 } = data; 5867 5868 // Set title 5869 this.config.title = title; 5870 ui.setTitle.call(this); 5871 5872 // Set aspect ratio 5873 this.embed.ratio = roundAspectRatio(width, height); 5874 } 5875 setAspectRatio.call(this); 5876 }).catch(() => { 5877 // Set aspect ratio 5878 setAspectRatio.call(this); 5879 }); 5880 }, 5881 // API ready 5882 ready() { 5883 const player = this; 5884 const config = player.config.youtube; 5885 // Ignore already setup (race condition) 5886 const currentId = player.media && player.media.getAttribute('id'); 5887 if (!is.empty(currentId) && currentId.startsWith('youtube-')) { 5888 return; 5889 } 5890 5891 // Get the source URL or ID 5892 let source = player.media.getAttribute('src'); 5893 5894 // Get from <div> if needed 5895 if (is.empty(source)) { 5896 source = player.media.getAttribute(this.config.attributes.embed.id); 5897 } 5898 5899 // Replace the <iframe> with a <div> due to YouTube API issues 5900 const videoId = parseId(source); 5901 const id = generateId(player.provider); 5902 // Replace media element 5903 const container = createElement('div', { 5904 id, 5905 'data-poster': config.customControls ? player.poster : undefined 5906 }); 5907 player.media = replaceElement(container, player.media); 5908 5909 // Only load the poster when using custom controls 5910 if (config.customControls) { 5911 const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`; 5912 5913 // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) 5914 loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded 5915 .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 5916 .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists 5917 .then(image => ui.setPoster.call(player, image.src)).then(src => { 5918 // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) 5919 if (!src.includes('maxres')) { 5920 player.elements.poster.style.backgroundSize = 'cover'; 5921 } 5922 }).catch(() => {}); 5923 } 5924 5925 // Setup instance 5926 // https://developers.google.com/youtube/iframe_api_reference 5927 player.embed = new window.YT.Player(player.media, { 5928 videoId, 5929 host: getHost(config), 5930 playerVars: extend({}, { 5931 // Autoplay 5932 autoplay: player.config.autoplay ? 1 : 0, 5933 // iframe interface language 5934 hl: player.config.hl, 5935 // Only show controls if not fully supported or opted out 5936 controls: player.supported.ui && config.customControls ? 0 : 1, 5937 // Disable keyboard as we handle it 5938 disablekb: 1, 5939 // Allow iOS inline playback 5940 playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0, 5941 // Captions are flaky on YouTube 5942 cc_load_policy: player.captions.active ? 1 : 0, 5943 cc_lang_pref: player.config.captions.language, 5944 // Tracking for stats 5945 widget_referrer: window ? window.location.href : null 5946 }, config), 5947 events: { 5948 onError(event) { 5949 // YouTube may fire onError twice, so only handle it once 5950 if (!player.media.error) { 5951 const code = event.data; 5952 // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError 5953 const message = { 5954 2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.', 5955 5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.', 5956 100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.', 5957 101: 'The owner of the requested video does not allow it to be played in embedded players.', 5958 150: 'The owner of the requested video does not allow it to be played in embedded players.' 5959 }[code] || 'An unknown error occurred'; 5960 player.media.error = { 5961 code, 5962 message 5963 }; 5964 triggerEvent.call(player, player.media, 'error'); 5965 } 5966 }, 5967 onPlaybackRateChange(event) { 5968 // Get the instance 5969 const instance = event.target; 5970 5971 // Get current speed 5972 player.media.playbackRate = instance.getPlaybackRate(); 5973 triggerEvent.call(player, player.media, 'ratechange'); 5974 }, 5975 onReady(event) { 5976 // Bail if onReady has already been called. See issue #1108 5977 if (is.function(player.media.play)) { 5978 return; 5979 } 5980 // Get the instance 5981 const instance = event.target; 5982 5983 // Get the title 5984 youtube.getTitle.call(player, videoId); 5985 5986 // Create a faux HTML5 API using the YouTube API 5987 player.media.play = () => { 5988 assurePlaybackState.call(player, true); 5989 instance.playVideo(); 5990 }; 5991 player.media.pause = () => { 5992 assurePlaybackState.call(player, false); 5993 instance.pauseVideo(); 5994 }; 5995 player.media.stop = () => { 5996 instance.stopVideo(); 5997 }; 5998 player.media.duration = instance.getDuration(); 5999 player.media.paused = true; 6000 6001 // Seeking 6002 player.media.currentTime = 0; 6003 Object.defineProperty(player.media, 'currentTime', { 6004 get() { 6005 return Number(instance.getCurrentTime()); 6006 }, 6007 set(time) { 6008 // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet). 6009 if (player.paused && !player.embed.hasPlayed) { 6010 player.embed.mute(); 6011 } 6012 6013 // Set seeking state and trigger event 6014 player.media.seeking = true; 6015 triggerEvent.call(player, player.media, 'seeking'); 6016 6017 // Seek after events sent 6018 instance.seekTo(time); 6019 } 6020 }); 6021 6022 // Playback speed 6023 Object.defineProperty(player.media, 'playbackRate', { 6024 get() { 6025 return instance.getPlaybackRate(); 6026 }, 6027 set(input) { 6028 instance.setPlaybackRate(input); 6029 } 6030 }); 6031 6032 // Volume 6033 let { 6034 volume 6035 } = player.config; 6036 Object.defineProperty(player.media, 'volume', { 6037 get() { 6038 return volume; 6039 }, 6040 set(input) { 6041 volume = input; 6042 instance.setVolume(volume * 100); 6043 triggerEvent.call(player, player.media, 'volumechange'); 6044 } 6045 }); 6046 6047 // Muted 6048 let { 6049 muted 6050 } = player.config; 6051 Object.defineProperty(player.media, 'muted', { 6052 get() { 6053 return muted; 6054 }, 6055 set(input) { 6056 const toggle = is.boolean(input) ? input : muted; 6057 muted = toggle; 6058 instance[toggle ? 'mute' : 'unMute'](); 6059 instance.setVolume(volume * 100); 6060 triggerEvent.call(player, player.media, 'volumechange'); 6061 } 6062 }); 6063 6064 // Source 6065 Object.defineProperty(player.media, 'currentSrc', { 6066 get() { 6067 return instance.getVideoUrl(); 6068 } 6069 }); 6070 6071 // Ended 6072 Object.defineProperty(player.media, 'ended', { 6073 get() { 6074 return player.currentTime === player.duration; 6075 } 6076 }); 6077 6078 // Get available speeds 6079 const speeds = instance.getAvailablePlaybackRates(); 6080 // Filter based on config 6081 player.options.speed = speeds.filter(s => player.config.speed.options.includes(s)); 6082 6083 // Set the tabindex to avoid focus entering iframe 6084 if (player.supported.ui && config.customControls) { 6085 player.media.setAttribute('tabindex', -1); 6086 } 6087 triggerEvent.call(player, player.media, 'timeupdate'); 6088 triggerEvent.call(player, player.media, 'durationchange'); 6089 6090 // Reset timer 6091 clearInterval(player.timers.buffering); 6092 6093 // Setup buffering 6094 player.timers.buffering = setInterval(() => { 6095 // Get loaded % from YouTube 6096 player.media.buffered = instance.getVideoLoadedFraction(); 6097 6098 // Trigger progress only when we actually buffer something 6099 if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { 6100 triggerEvent.call(player, player.media, 'progress'); 6101 } 6102 6103 // Set last buffer point 6104 player.media.lastBuffered = player.media.buffered; 6105 6106 // Bail if we're at 100% 6107 if (player.media.buffered === 1) { 6108 clearInterval(player.timers.buffering); 6109 6110 // Trigger event 6111 triggerEvent.call(player, player.media, 'canplaythrough'); 6112 } 6113 }, 200); 6114 6115 // Rebuild UI 6116 if (config.customControls) { 6117 setTimeout(() => ui.build.call(player), 50); 6118 } 6119 }, 6120 onStateChange(event) { 6121 // Get the instance 6122 const instance = event.target; 6123 6124 // Reset timer 6125 clearInterval(player.timers.playing); 6126 const seeked = player.media.seeking && [1, 2].includes(event.data); 6127 if (seeked) { 6128 // Unset seeking and fire seeked event 6129 player.media.seeking = false; 6130 triggerEvent.call(player, player.media, 'seeked'); 6131 } 6132 6133 // Handle events 6134 // -1 Unstarted 6135 // 0 Ended 6136 // 1 Playing 6137 // 2 Paused 6138 // 3 Buffering 6139 // 5 Video cued 6140 switch (event.data) { 6141 case -1: 6142 // Update scrubber 6143 triggerEvent.call(player, player.media, 'timeupdate'); 6144 6145 // Get loaded % from YouTube 6146 player.media.buffered = instance.getVideoLoadedFraction(); 6147 triggerEvent.call(player, player.media, 'progress'); 6148 break; 6149 case 0: 6150 assurePlaybackState.call(player, false); 6151 6152 // YouTube doesn't support loop for a single video, so mimick it. 6153 if (player.media.loop) { 6154 // YouTube needs a call to `stopVideo` before playing again 6155 instance.stopVideo(); 6156 instance.playVideo(); 6157 } else { 6158 triggerEvent.call(player, player.media, 'ended'); 6159 } 6160 break; 6161 case 1: 6162 // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) 6163 if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) { 6164 player.media.pause(); 6165 } else { 6166 assurePlaybackState.call(player, true); 6167 triggerEvent.call(player, player.media, 'playing'); 6168 6169 // Poll to get playback progress 6170 player.timers.playing = setInterval(() => { 6171 triggerEvent.call(player, player.media, 'timeupdate'); 6172 }, 50); 6173 6174 // Check duration again due to YouTube bug 6175 // https://github.com/sampotts/plyr/issues/374 6176 // https://code.google.com/p/gdata-issues/issues/detail?id=8690 6177 if (player.media.duration !== instance.getDuration()) { 6178 player.media.duration = instance.getDuration(); 6179 triggerEvent.call(player, player.media, 'durationchange'); 6180 } 6181 } 6182 break; 6183 case 2: 6184 // Restore audio (YouTube starts playing on seek if the video hasn't been played yet) 6185 if (!player.muted) { 6186 player.embed.unMute(); 6187 } 6188 assurePlaybackState.call(player, false); 6189 break; 6190 case 3: 6191 // Trigger waiting event to add loading classes to container as the video buffers. 6192 triggerEvent.call(player, player.media, 'waiting'); 6193 break; 6194 } 6195 triggerEvent.call(player, player.elements.container, 'statechange', false, { 6196 code: event.data 6197 }); 6198 } 6199 } 6200 }); 6201 } 6202 }; 6203 6204 // ========================================================================== 6205
vendor: 24,988 bytes, lines 6205-6866
6205const media = { 6206 // Setup media 6207 setup() { 6208 // If there's no media, bail 6209 if (!this.media) { 6210 this.debug.warn('No media element found!'); 6211 return; 6212 } 6213 6214 // Add type class 6215 toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); 6216 6217 // Add provider class 6218 toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); 6219 6220 // Add video class for embeds 6221 // This will require changes if audio embeds are added 6222 if (this.isEmbed) { 6223 toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); 6224 } 6225 6226 // Inject the player wrapper 6227 if (this.isVideo) { 6228 // Create the wrapper div 6229 this.elements.wrapper = createElement('div', { 6230 class: this.config.classNames.video 6231 }); 6232 6233 // Wrap the video in a container 6234 wrap(this.media, this.elements.wrapper); 6235 6236 // Poster image container 6237 this.elements.poster = createElement('div', { 6238 class: this.config.classNames.poster 6239 }); 6240 this.elements.wrapper.appendChild(this.elements.poster); 6241 } 6242 if (this.isHTML5) { 6243 html5.setup.call(this); 6244 } else if (this.isYouTube) { 6245 youtube.setup.call(this); 6246 } else if (this.isVimeo) { 6247 vimeo.setup.call(this); 6248 } 6249 } 6250 }; 6251 6252 const destroy = instance => { 6253 // Destroy our adsManager 6254 if (instance.manager) { 6255 instance.manager.destroy(); 6256 } 6257 6258 // Destroy our adsManager 6259 if (instance.elements.displayContainer) { 6260 instance.elements.displayContainer.destroy(); 6261 } 6262 instance.elements.container.remove(); 6263 }; 6264 class Ads { 6265 /** 6266 * Ads constructor. 6267 * @param {Object} player 6268 * @return {Ads} 6269 */ 6270 constructor(player) { 6271 /** 6272 * Load the IMA SDK 6273 */ 6274 _defineProperty$1(this, "load", () => { 6275 if (!this.enabled) { 6276 return; 6277 } 6278 6279 // Check if the Google IMA3 SDK is loaded or load it ourselves 6280 if (!is.object(window.google) || !is.object(window.google.ima)) { 6281 loadScript(this.player.config.urls.googleIMA.sdk).then(() => { 6282 this.ready(); 6283 }).catch(() => { 6284 // Script failed to load or is blocked 6285 this.trigger('error', new Error('Google IMA SDK failed to load')); 6286 }); 6287 } else { 6288 this.ready(); 6289 } 6290 }); 6291 /** 6292 * Get the ads instance ready 6293 */ 6294 _defineProperty$1(this, "ready", () => { 6295 // Double check we're enabled 6296 if (!this.enabled) { 6297 destroy(this); 6298 } 6299 6300 // Start ticking our safety timer. If the whole advertisement 6301 // thing doesn't resolve within our set time; we bail 6302 this.startSafetyTimer(12000, 'ready()'); 6303 6304 // Clear the safety timer 6305 this.managerPromise.then(() => { 6306 this.clearSafetyTimer('onAdsManagerLoaded()'); 6307 }); 6308 6309 // Set listeners on the Plyr instance 6310 this.listeners(); 6311 6312 // Setup the IMA SDK 6313 this.setupIMA(); 6314 }); 6315 /** 6316 * In order for the SDK to display ads for our video, we need to tell it where to put them, 6317 * so here we define our ad container. This div is set up to render on top of the video player. 6318 * Using the code below, we tell the SDK to render ads within that div. We also provide a 6319 * handle to the content video player - the SDK will poll the current time of our player to 6320 * properly place mid-rolls. After we create the ad display container, we initialize it. On 6321 * mobile devices, this initialization is done as the result of a user action. 6322 */ 6323 _defineProperty$1(this, "setupIMA", () => { 6324 // Create the container for our advertisements 6325 this.elements.container = createElement('div', { 6326 class: this.player.config.classNames.ads 6327 }); 6328 this.player.elements.container.appendChild(this.elements.container); 6329 6330 // So we can run VPAID2 6331 google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); 6332 6333 // Set language 6334 google.ima.settings.setLocale(this.player.config.ads.language); 6335 6336 // Set playback for iOS10+ 6337 google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); 6338 6339 // We assume the adContainer is the video container of the plyr element that will house the ads 6340 this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); 6341 6342 // Create ads loader 6343 this.loader = new google.ima.AdsLoader(this.elements.displayContainer); 6344 6345 // Listen and respond to ads loaded and error events 6346 this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false); 6347 this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); 6348 6349 // Request video ads to be pre-loaded 6350 this.requestAds(); 6351 }); 6352 /** 6353 * Request advertisements 6354 */ 6355 _defineProperty$1(this, "requestAds", () => { 6356 const { 6357 container 6358 } = this.player.elements; 6359 try { 6360 // Request video ads 6361 const request = new google.ima.AdsRequest(); 6362 request.adTagUrl = this.tagUrl; 6363 6364 // Specify the linear and nonlinear slot sizes. This helps the SDK 6365 // to select the correct creative if multiple are returned 6366 request.linearAdSlotWidth = container.offsetWidth; 6367 request.linearAdSlotHeight = container.offsetHeight; 6368 request.nonLinearAdSlotWidth = container.offsetWidth; 6369 request.nonLinearAdSlotHeight = container.offsetHeight; 6370 6371 // We only overlay ads as we only support video. 6372 request.forceNonLinearFullSlot = false; 6373 6374 // Mute based on current state 6375 request.setAdWillPlayMuted(!this.player.muted); 6376 this.loader.requestAds(request); 6377 } catch (error) { 6378 this.onAdError(error); 6379 } 6380 }); 6381 /** 6382 * Update the ad countdown 6383 * @param {Boolean} start 6384 */ 6385 _defineProperty$1(this, "pollCountdown", (start = false) => { 6386 if (!start) { 6387 clearInterval(this.countdownTimer); 6388 this.elements.container.removeAttribute('data-badge-text'); 6389 return; 6390 } 6391 const update = () => { 6392 const time = formatTime(Math.max(this.manager.getRemainingTime(), 0)); 6393 const label = `${i18n.get('advertisement', this.player.config)} - ${time}`; 6394 this.elements.container.setAttribute('data-badge-text', label); 6395 }; 6396 this.countdownTimer = setInterval(update, 100); 6397 }); 6398 /** 6399 * This method is called whenever the ads are ready inside the AdDisplayContainer 6400 * @param {Event} event - adsManagerLoadedEvent 6401 */ 6402 _defineProperty$1(this, "onAdsManagerLoaded", event => { 6403 // Load could occur after a source change (race condition) 6404 if (!this.enabled) { 6405 return; 6406 } 6407 6408 // Get the ads manager 6409 const settings = new google.ima.AdsRenderingSettings(); 6410 6411 // Tell the SDK to save and restore content video state on our behalf 6412 settings.restoreCustomPlaybackStateOnAdBreakComplete = true; 6413 settings.enablePreloading = true; 6414 6415 // The SDK is polling currentTime on the contentPlayback. And needs a duration 6416 // so it can determine when to start the mid- and post-roll 6417 this.manager = event.getAdsManager(this.player, settings); 6418 6419 // Get the cue points for any mid-rolls by filtering out the pre- and post-roll 6420 this.cuePoints = this.manager.getCuePoints(); 6421 6422 // Add listeners to the required events 6423 // Advertisement error events 6424 this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); 6425 6426 // Advertisement regular events 6427 Object.keys(google.ima.AdEvent.Type).forEach(type => { 6428 this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e)); 6429 }); 6430 6431 // Resolve our adsManager 6432 this.trigger('loaded'); 6433 }); 6434 _defineProperty$1(this, "addCuePoints", () => { 6435 // Add advertisement cue's within the time line if available 6436 if (!is.empty(this.cuePoints)) { 6437 this.cuePoints.forEach(cuePoint => { 6438 if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) { 6439 const seekElement = this.player.elements.progress; 6440 if (is.element(seekElement)) { 6441 const cuePercentage = 100 / this.player.duration * cuePoint; 6442 const cue = createElement('span', { 6443 class: this.player.config.classNames.cues 6444 }); 6445 cue.style.left = `${cuePercentage.toString()}%`; 6446 seekElement.appendChild(cue); 6447 } 6448 } 6449 }); 6450 } 6451 }); 6452 /** 6453 * This is where all the event handling takes place. Retrieve the ad from the event. Some 6454 * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated 6455 * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type 6456 * @param {Event} event 6457 */ 6458 _defineProperty$1(this, "onAdEvent", event => { 6459 const { 6460 container 6461 } = this.player.elements; 6462 // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) 6463 // don't have ad object associated 6464 const ad = event.getAd(); 6465 const adData = event.getAdData(); 6466 6467 // Proxy event 6468 const dispatchEvent = type => { 6469 triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`); 6470 }; 6471 6472 // Bubble the event 6473 dispatchEvent(event.type); 6474 switch (event.type) { 6475 case google.ima.AdEvent.Type.LOADED: 6476 // This is the first event sent for an ad - it is possible to determine whether the 6477 // ad is a video ad or an overlay 6478 this.trigger('loaded'); 6479 6480 // Start countdown 6481 this.pollCountdown(true); 6482 if (!ad.isLinear()) { 6483 // Position AdDisplayContainer correctly for overlay 6484 ad.width = container.offsetWidth; 6485 ad.height = container.offsetHeight; 6486 } 6487 6488 // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); 6489 // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); 6490 6491 break; 6492 case google.ima.AdEvent.Type.STARTED: 6493 // Set volume to match player 6494 this.manager.setVolume(this.player.volume); 6495 break; 6496 case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: 6497 // All ads for the current videos are done. We can now request new advertisements 6498 // in case the video is re-played 6499 6500 // TODO: Example for what happens when a next video in a playlist would be loaded. 6501 // So here we load a new video when all ads are done. 6502 // Then we load new ads within a new adsManager. When the video 6503 // Is started - after - the ads are loaded, then we get ads. 6504 // You can also easily test cancelling and reloading by running 6505 // player.ads.cancel() and player.ads.play from the console I guess. 6506 // this.player.source = { 6507 // type: 'video', 6508 // title: 'View From A Blue Moon', 6509 // sources: [{ 6510 // src: 6511 // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type: 6512 // 'video/mp4', }], poster: 6513 // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks: 6514 // [ { kind: 'captions', label: 'English', srclang: 'en', src: 6515 // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', 6516 // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src: 6517 // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ], 6518 // }; 6519 6520 // TODO: So there is still this thing where a video should only be allowed to start 6521 // playing when the IMA SDK is ready or has failed 6522 6523 if (this.player.ended) { 6524 this.loadAds(); 6525 } else { 6526 // The SDK won't allow new ads to be called without receiving a contentComplete() 6527 this.loader.contentComplete(); 6528 } 6529 break; 6530 case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: 6531 // This event indicates the ad has started - the video player can adjust the UI, 6532 // for example display a pause button and remaining time. Fired when content should 6533 // be paused. This usually happens right before an ad is about to cover the content 6534 6535 this.pauseContent(); 6536 break; 6537 case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: 6538 // This event indicates the ad has finished - the video player can perform 6539 // appropriate UI actions, such as removing the timer for remaining time detection. 6540 // Fired when content should be resumed. This usually happens when an ad finishes 6541 // or collapses 6542 6543 this.pollCountdown(); 6544 this.resumeContent(); 6545 break; 6546 case google.ima.AdEvent.Type.LOG: 6547 if (adData.adError) { 6548 this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`); 6549 } 6550 break; 6551 } 6552 }); 6553 /** 6554 * Any ad error handling comes through here 6555 * @param {Event} event 6556 */ 6557 _defineProperty$1(this, "onAdError", event => { 6558 this.cancel(); 6559 this.player.debug.warn('Ads error', event); 6560 }); 6561 /** 6562 * Setup hooks for Plyr and window events. This ensures 6563 * the mid- and post-roll launch at the correct time. And 6564 * resize the advertisement when the player resizes 6565 */ 6566 _defineProperty$1(this, "listeners", () => { 6567 const { 6568 container 6569 } = this.player.elements; 6570 let time; 6571 this.player.on('canplay', () => { 6572 this.addCuePoints(); 6573 }); 6574 this.player.on('ended', () => { 6575 this.loader.contentComplete(); 6576 }); 6577 this.player.on('timeupdate', () => { 6578 time = this.player.currentTime; 6579 }); 6580 this.player.on('seeked', () => { 6581 const seekedTime = this.player.currentTime; 6582 if (is.empty(this.cuePoints)) { 6583 return; 6584 } 6585 this.cuePoints.forEach((cuePoint, index) => { 6586 if (time < cuePoint && cuePoint < seekedTime) { 6587 this.manager.discardAdBreak(); 6588 this.cuePoints.splice(index, 1); 6589 } 6590 }); 6591 }); 6592 6593 // Listen to the resizing of the window. And resize ad accordingly 6594 // TODO: eventually implement ResizeObserver 6595 window.addEventListener('resize', () => { 6596 if (this.manager) { 6597 this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); 6598 } 6599 }); 6600 }); 6601 /** 6602 * Initialize the adsManager and start playing advertisements 6603 */ 6604 _defineProperty$1(this, "play", () => { 6605 const { 6606 container 6607 } = this.player.elements; 6608 if (!this.managerPromise) { 6609 this.resumeContent(); 6610 } 6611 6612 // Play the requested advertisement whenever the adsManager is ready 6613 this.managerPromise.then(() => { 6614 // Set volume to match player 6615 this.manager.setVolume(this.player.volume); 6616 6617 // Initialize the container. Must be done via a user action on mobile devices 6618 this.elements.displayContainer.initialize(); 6619 try { 6620 if (!this.initialized) { 6621 // Initialize the ads manager. Ad rules playlist will start at this time 6622 this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); 6623 6624 // Call play to start showing the ad. Single video and overlay ads will 6625 // start at this time; the call will be ignored for ad rules 6626 this.manager.start(); 6627 } 6628 this.initialized = true; 6629 } catch (adError) { 6630 // An error may be thrown if there was a problem with the 6631 // VAST response 6632 this.onAdError(adError); 6633 } 6634 }).catch(() => {}); 6635 }); 6636 /** 6637 * Resume our video 6638 */ 6639 _defineProperty$1(this, "resumeContent", () => { 6640 // Hide the advertisement container 6641 this.elements.container.style.zIndex = ''; 6642 6643 // Ad is stopped 6644 this.playing = false; 6645 6646 // Play video 6647 silencePromise(this.player.media.play()); 6648 }); 6649 /** 6650 * Pause our video 6651 */ 6652 _defineProperty$1(this, "pauseContent", () => { 6653 // Show the advertisement container 6654 this.elements.container.style.zIndex = 3; 6655 6656 // Ad is playing 6657 this.playing = true; 6658 6659 // Pause our video. 6660 this.player.media.pause(); 6661 }); 6662 /** 6663 * Destroy the adsManager so we can grab new ads after this. If we don't then we're not 6664 * allowed to call new ads based on google policies, as they interpret this as an accidental 6665 * video requests. https://developers.google.com/interactive- 6666 * media-ads/docs/sdks/android/faq#8 6667 */ 6668 _defineProperty$1(this, "cancel", () => { 6669 // Pause our video 6670 if (this.initialized) { 6671 this.resumeContent(); 6672 } 6673 6674 // Tell our instance that we're done for now 6675 this.trigger('error'); 6676 6677 // Re-create our adsManager 6678 this.loadAds(); 6679 }); 6680 /** 6681 * Re-create our adsManager 6682 */ 6683 _defineProperty$1(this, "loadAds", () => { 6684 // Tell our adsManager to go bye bye 6685 this.managerPromise.then(() => { 6686 // Destroy our adsManager 6687 if (this.manager) { 6688 this.manager.destroy(); 6689 } 6690 6691 // Re-set our adsManager promises 6692 this.managerPromise = new Promise(resolve => { 6693 this.on('loaded', resolve); 6694 this.player.debug.log(this.manager); 6695 }); 6696 // Now that the manager has been destroyed set it to also be un-initialized 6697 this.initialized = false; 6698 6699 // Now request some new advertisements 6700 this.requestAds(); 6701 }).catch(() => {}); 6702 }); 6703 /** 6704 * Handles callbacks after an ad event was invoked 6705 * @param {String} event - Event type 6706 * @param args 6707 */ 6708 _defineProperty$1(this, "trigger", (event, ...args) => { 6709 const handlers = this.events[event]; 6710 if (is.array(handlers)) { 6711 handlers.forEach(handler => { 6712 if (is.function(handler)) { 6713 handler.apply(this, args); 6714 } 6715 }); 6716 } 6717 }); 6718 /** 6719 * Add event listeners 6720 * @param {String} event - Event type 6721 * @param {Function} callback - Callback for when event occurs 6722 * @return {Ads} 6723 */ 6724 _defineProperty$1(this, "on", (event, callback) => { 6725 if (!is.array(this.events[event])) { 6726 this.events[event] = []; 6727 } 6728 this.events[event].push(callback); 6729 return this; 6730 }); 6731 /** 6732 * Setup a safety timer for when the ad network doesn't respond for whatever reason. 6733 * The advertisement has 12 seconds to get its things together. We stop this timer when the 6734 * advertisement is playing, or when a user action is required to start, then we clear the 6735 * timer on ad ready 6736 * @param {Number} time 6737 * @param {String} from 6738 */ 6739 _defineProperty$1(this, "startSafetyTimer", (time, from) => { 6740 this.player.debug.log(`Safety timer invoked from: ${from}`); 6741 this.safetyTimer = setTimeout(() => { 6742 this.cancel(); 6743 this.clearSafetyTimer('startSafetyTimer()'); 6744 }, time); 6745 }); 6746 /** 6747 * Clear our safety timer(s) 6748 * @param {String} from 6749 */ 6750 _defineProperty$1(this, "clearSafetyTimer", from => { 6751 if (!is.nullOrUndefined(this.safetyTimer)) { 6752 this.player.debug.log(`Safety timer cleared from: ${from}`); 6753 clearTimeout(this.safetyTimer); 6754 this.safetyTimer = null; 6755 } 6756 }); 6757 this.player = player; 6758 this.config = player.config.ads; 6759 this.playing = false; 6760 this.initialized = false; 6761 this.elements = { 6762 container: null, 6763 displayContainer: null 6764 }; 6765 this.manager = null; 6766 this.loader = null; 6767 this.cuePoints = null; 6768 this.events = {}; 6769 this.safetyTimer = null; 6770 this.countdownTimer = null; 6771 6772 // Setup a promise to resolve when the IMA manager is ready 6773 this.managerPromise = new Promise((resolve, reject) => { 6774 // The ad is loaded and ready 6775 this.on('loaded', resolve); 6776 6777 // Ads failed 6778 this.on('error', reject); 6779 }); 6780 this.load(); 6781 } 6782 get enabled() { 6783 const { 6784 config 6785 } = this; 6786 return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl)); 6787 } 6788 // Build the tag URL 6789 get tagUrl() { 6790 const { 6791 config 6792 } = this; 6793 if (is.url(config.tagUrl)) { 6794 return config.tagUrl; 6795 } 6796 const params = { 6797 AV_PUBLISHERID: '58c25bb0073ef448b1087ad6', 6798 AV_CHANNELID: '5a0458dc28a06145e4519d21', 6799 AV_URL: window.location.hostname, 6800 cb: Date.now(), 6801 AV_WIDTH: 640, 6802 AV_HEIGHT: 480, 6803 AV_CDIM2: config.publisherId 6804 }; 6805 const base = 'https://go.aniview.com/api/adserver6/vast/'; 6806 return `${base}?${buildUrlParams(params)}`; 6807 } 6808 } 6809 6810 /** 6811 * Returns a number whose value is limited to the given range. 6812 * 6813 * Example: limit the output of this computation to between 0 and 255 6814 * (x * 255).clamp(0, 255) 6815 * 6816 * @param {Number} input 6817 * @param {Number} min The lower boundary of the output range 6818 * @param {Number} max The upper boundary of the output range 6819 * @returns A number within the bounds of min and max 6820 * @type Number 6821 */ 6822 function clamp(input = 0, min = 0, max = 255) { 6823 return Math.min(Math.max(input, min), max); 6824 } 6825 6826 // Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg" 6827 const parseVtt = vttDataString => { 6828 const processedList = []; 6829 const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/); 6830 frames.forEach(frame => { 6831 const result = {}; 6832 const lines = frame.split(/\r\n|\n|\r/); 6833 lines.forEach(line => { 6834 if (!is.number(result.startTime)) { 6835 // The line with start and end times on it is the first line of interest 6836 const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT 6837 6838 if (matchTimes) { 6839 result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`); 6840 result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`); 6841 } 6842 } else if (!is.empty(line.trim()) && is.empty(result.text)) { 6843 // If we already have the startTime, then we're definitely up to the text line(s) 6844 const lineSplit = line.trim().split('#xywh='); 6845 [result.text] = lineSplit; 6846 6847 // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image 6848 if (lineSplit[1]) { 6849 [result.x, result.y, result.w, result.h] = lineSplit[1].split(','); 6850 } 6851 } 6852 }); 6853 if (result.text) { 6854 processedList.push(result); 6855 } 6856 }); 6857 return processedList; 6858 }; 6859 6860 /** 6861 * Preview thumbnails for seek hover and scrubbing 6862 * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar 6863 * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed 6864 * 6865 * Notes: 6866 * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property w
6866ould be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole 6867 * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails 6868 * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered 6869 */ 6870 6871 const fitRatio = (ratio, outer) => { 6872 const targetRatio = outer.width / outer.height; 6873 const result = {}; 6874 if (ratio > targetRatio) { 6875 result.width = outer.width; 6876 result.height = 1 / ratio * outer.width; 6877 } else { 6878 result.height = outer.height; 6879 result.width = ratio * outer.height; 6880 } 6881 return result; 6882 }; 6883 class PreviewThumbnails { 6884 /** 6885 * PreviewThumbnails constructor. 6886 * @param {Plyr} player 6887 * @return {PreviewThumbnails} 6888 */ 6889 constructor(player) { 6890 _defineProperty$1(this, "load", () => { 6891 // Toggle the regular seek tooltip 6892 if (this.player.elements.display.seekTooltip) { 6893 this.player.elements.display.seekTooltip.hidden = this.enabled; 6894 } 6895 if (!this.enabled) return; 6896 this.getThumbnails().then(() => { 6897 if (!this.enabled) { 6898 return; 6899 } 6900 6901 // Render DOM elements 6902 this.render(); 6903 6904 // Check to see if thumb container size was specified manually in CSS 6905 this.determineContainerAutoSizing(); 6906 6907 // Set up listeners 6908 this.listeners(); 6909 this.loaded = true; 6910 }); 6911 }); 6912 // Download VTT files and parse them 6913 _defineProperty$1(this, "getThumbnails", () => { 6914 return new Promise(resolve => { 6915 const { 6916 src 6917 } = this.player.config.previewThumbnails; 6918 if (is.empty(src)) { 6919 throw new Error('Missing previewThumbnails.src config attribute'); 6920 } 6921 6922 // Resolve promise 6923 const sortAndResolve = () => { 6924 // Sort smallest to biggest (e.g., [120p, 480p, 1080p]) 6925 this.thumbnails.sort((x, y) => x.height - y.height); 6926 this.player.debug.log('Preview thumbnails', this.thumbnails); 6927 resolve(); 6928 }; 6929 6930 // Via callback() 6931 if (is.function(src)) { 6932 src(thumbnails => { 6933 this.thumbnails = thumbnails; 6934 sortAndResolve(); 6935 }); 6936 } 6937 // VTT urls 6938 else { 6939 // If string, convert into single-element list 6940 const urls = is.string(src) ? [src] : src; 6941 // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails 6942 const promises = urls.map(u => this.getThumbnail(u)); 6943 // Resolve 6944 Promise.all(promises).then(sortAndResolve); 6945 } 6946 }); 6947 }); 6948 // Process individual VTT file 6949 _defineProperty$1(this, "getThumbnail", url => { 6950 return new Promise(resolve => { 6951 fetch(url).then(response => { 6952 const thumbnail = { 6953 frames: parseVtt(response), 6954 height: null, 6955 urlPrefix: '' 6956 }; 6957 6958 // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file 6959 // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
vendor: 2,335 bytes, lines 6960-7003
6960 // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file 6961 if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) { 6962 thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1); 6963 } 6964 6965 // Download the first frame, so that we can determine/set the height of this thumbnailsDef 6966 const tempImage = new Image(); 6967 tempImage.onload = () => { 6968 thumbnail.height = tempImage.naturalHeight; 6969 thumbnail.width = tempImage.naturalWidth; 6970 this.thumbnails.push(thumbnail); 6971 resolve(); 6972 }; 6973 tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text; 6974 }); 6975 }); 6976 }); 6977 _defineProperty$1(this, "startMove", event => { 6978 if (!this.loaded) return; 6979 if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return; 6980 6981 // Wait until media has a duration 6982 if (!this.player.media.duration) return; 6983 if (event.type === 'touchmove') { 6984 // Calculate seek hover position as approx video seconds 6985 this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100); 6986 } else { 6987 var _this$player$config$m, _this$player$config$m2; 6988 // Calculate seek hover position as approx video seconds 6989 const clientRect = this.player.elements.progress.getBoundingClientRect(); 6990 const percentage = 100 / clientRect.width * (event.pageX - clientRect.left); 6991 this.seekTime = this.player.media.duration * (percentage / 100); 6992 if (this.seekTime < 0) { 6993 // The mousemove fires for 10+px out to the left 6994 this.seekTime = 0; 6995 } 6996 if (this.seekTime > this.player.media.duration - 1) { 6997 // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video 6998 this.seekTime = this.player.media.duration - 1; 6999 } 7000 this.mousePosX = event.pageX; 7001 7002 // Set time text inside image container 7003 this.elements.thumb.time.innerTex
vendor: 10,103 bytes, lines 7003-7212
7003t = formatTime(this.seekTime); 7004 7005 // Get marker point for time 7006 const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({ 7007 time: t 7008 }) => t === Math.round(this.seekTime)); 7009 7010 // Append the point label to the tooltip 7011 if (point) { 7012 // this.elements.thumb.time.innerText.concat('\n'); 7013 this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`); 7014 } 7015 } 7016 7017 // Download and show image 7018 this.showImageAtCurrentTime(); 7019 }); 7020 _defineProperty$1(this, "endMove", () => { 7021 this.toggleThumbContainer(false, true); 7022 }); 7023 _defineProperty$1(this, "startScrubbing", event => { 7024 // Only act on left mouse button (0), or touch device (event.button does not exist or is false) 7025 if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) { 7026 this.mouseDown = true; 7027 7028 // Wait until media has a duration 7029 if (this.player.media.duration) { 7030 this.toggleScrubbingContainer(true); 7031 this.toggleThumbContainer(false, true); 7032 7033 // Download and show image 7034 this.showImageAtCurrentTime(); 7035 } 7036 } 7037 }); 7038 _defineProperty$1(this, "endScrubbing", () => { 7039 this.mouseDown = false; 7040 7041 // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview 7042 if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) { 7043 // The video was already seeked/loaded at the chosen time - hide immediately 7044 this.toggleScrubbingContainer(false); 7045 } else { 7046 // The video hasn't seeked yet. Wait for that 7047 once.call(this.player, this.player.media, 'timeupdate', () => { 7048 // Re-check mousedown - we might have already started scrubbing again 7049 if (!this.mouseDown) { 7050 this.toggleScrubbingContainer(false); 7051 } 7052 }); 7053 } 7054 }); 7055 /** 7056 * Setup hooks for Plyr and window events 7057 */ 7058 _defineProperty$1(this, "listeners", () => { 7059 // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering 7060 this.player.on('play', () => { 7061 this.toggleThumbContainer(false, true); 7062 }); 7063 this.player.on('seeked', () => { 7064 this.toggleThumbContainer(false); 7065 }); 7066 this.player.on('timeupdate', () => { 7067 this.lastTime = this.player.media.currentTime; 7068 }); 7069 }); 7070 /** 7071 * Create HTML elements for image containers 7072 */ 7073 _defineProperty$1(this, "render", () => { 7074 // Create HTML element: plyr__preview-thumbnail-container 7075 this.elements.thumb.container = createElement('div', { 7076 class: this.player.config.classNames.previewThumbnails.thumbContainer 7077 }); 7078 7079 // Wrapper for the image for styling 7080 this.elements.thumb.imageContainer = createElement('div', { 7081 class: this.player.config.classNames.previewThumbnails.imageContainer 7082 }); 7083 this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer); 7084 7085 // Create HTML element, parent+span: time text (e.g., 01:32:00) 7086 const timeContainer = createElement('div', { 7087 class: this.player.config.classNames.previewThumbnails.timeContainer 7088 }); 7089 this.elements.thumb.time = createElement('span', {}, '00:00'); 7090 timeContainer.appendChild(this.elements.thumb.time); 7091 this.elements.thumb.imageContainer.appendChild(timeContainer); 7092 7093 // Inject the whole thumb 7094 if (is.element(this.player.elements.progress)) { 7095 this.player.elements.progress.appendChild(this.elements.thumb.container); 7096 } 7097 7098 // Create HTML element: plyr__preview-scrubbing-container 7099 this.elements.scrubbing.container = createElement('div', { 7100 class: this.player.config.classNames.previewThumbnails.scrubbingContainer 7101 }); 7102 this.player.elements.wrapper.appendChild(this.elements.scrubbing.container); 7103 }); 7104 _defineProperty$1(this, "destroy", () => { 7105 if (this.elements.thumb.container) { 7106 this.elements.thumb.container.remove(); 7107 } 7108 if (this.elements.scrubbing.container) { 7109 this.elements.scrubbing.container.remove(); 7110 } 7111 }); 7112 _defineProperty$1(this, "showImageAtCurrentTime", () => { 7113 if (this.mouseDown) { 7114 this.setScrubbingContainerSize(); 7115 } else { 7116 this.setThumbContainerSizeAndPos(); 7117 } 7118 7119 // Find the desired thumbnail index 7120 // TODO: Handle a video longer than the thumbs where thumbNum is null 7121 const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime); 7122 const hasThumb = thumbNum >= 0; 7123 let qualityIndex = 0; 7124 7125 // Show the thumb container if we're not scrubbing 7126 if (!this.mouseDown) { 7127 this.toggleThumbContainer(hasThumb); 7128 } 7129 7130 // No matching thumb found 7131 if (!hasThumb) { 7132 return; 7133 } 7134 7135 // Check to see if we've already downloaded higher quality versions of this image 7136 this.thumbnails.forEach((thumbnail, index) => { 7137 if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) { 7138 qualityIndex = index; 7139 } 7140 }); 7141 7142 // Only proceed if either thumb num or thumbfilename has changed 7143 if (thumbNum !== this.showingThumb) { 7144 this.showingThumb = thumbNum; 7145 this.loadImage(qualityIndex); 7146 } 7147 }); 7148 // Show the image that's currently specified in this.showingThumb 7149 _defineProperty$1(this, "loadImage", (qualityIndex = 0) => { 7150 const thumbNum = this.showingThumb; 7151 const thumbnail = this.thumbnails[qualityIndex]; 7152 const { 7153 urlPrefix 7154 } = thumbnail; 7155 const frame = thumbnail.frames[thumbNum]; 7156 const thumbFilename = thumbnail.frames[thumbNum].text; 7157 const thumbUrl = urlPrefix + thumbFilename; 7158 if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) { 7159 // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one 7160 // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort 7161 if (this.loadingImage && this.usingSprites) { 7162 this.loadingImage.onload = null; 7163 } 7164 7165 // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image 7166 // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background 7167 // images causes a flicker. Putting a new image over the top does not 7168 const previewImage = new Image(); 7169 previewImage.src = thumbUrl; 7170 previewImage.dataset.index = thumbNum; 7171 previewImage.dataset.filename = thumbFilename; 7172 this.showingThumbFilename = thumbFilename; 7173 this.player.debug.log(`Loading image: ${thumbUrl}`); 7174 7175 // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function... 7176 previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true); 7177 this.loadingImage = previewImage; 7178 this.removeOldImages(previewImage); 7179 } else { 7180 // Update the existing image 7181 this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false); 7182 this.currentImageElement.dataset.index = thumbNum; 7183 this.removeOldImages(this.currentImageElement); 7184 } 7185 }); 7186 _defineProperty$1(this, "showImage", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => { 7187 this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`); 7188 this.setImageSizeAndOffset(previewImage, frame); 7189 if (newImage) { 7190 this.currentImageContainer.appendChild(previewImage); 7191 this.currentImageElement = previewImage; 7192 if (!this.loadedImages.includes(thumbFilename)) { 7193 this.loadedImages.push(thumbFilename); 7194 } 7195 } 7196 7197 // Preload images before and after the current one 7198 // Show higher quality of the same frame 7199 // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading 7200 this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename)); 7201 }); 7202 // Remove all preview images that aren't the designated current image 7203 _defineProperty$1(this, "removeOldImages", currentImage => { 7204 // Get a list of all images, convert it from a DOM list to an array 7205 Array.from(this.currentImageContainer.children).forEach(image => { 7206 if (image.tagName.toLowerCase() !== 'img') { 7207 return; 7208 } 7209 const removeDelay = this.usingSprites ? 500 : 1000; 7210 if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) { 7211 // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients 7212 // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
7213 // eslint-disable-next-line no-param-reassign
vendor: 8,107 bytes, lines 7213-7370
7213 7214 image.dataset.deleting = true; 7215 7216 // This has to be set before the timeout - to prevent issues switching between hover and scrub 7217 const { 7218 currentImageContainer 7219 } = this; 7220 setTimeout(() => { 7221 currentImageContainer.removeChild(image); 7222 this.player.debug.log(`Removing thumb: ${image.dataset.filename}`); 7223 }, removeDelay); 7224 } 7225 }); 7226 }); 7227 // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame 7228 // This will only preload the lowest quality 7229 _defineProperty$1(this, "preloadNearby", (thumbNum, forward = true) => { 7230 return new Promise(resolve => { 7231 setTimeout(() => { 7232 const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text; 7233 if (this.showingThumbFilename === oldThumbFilename) { 7234 // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away 7235 let thumbnailsClone; 7236 if (forward) { 7237 thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum); 7238 } else { 7239 thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse(); 7240 } 7241 let foundOne = false; 7242 thumbnailsClone.forEach(frame => { 7243 const newThumbFilename = frame.text; 7244 if (newThumbFilename !== oldThumbFilename) { 7245 // Found one with a different filename. Make sure it hasn't already been loaded on this page visit 7246 if (!this.loadedImages.includes(newThumbFilename)) { 7247 foundOne = true; 7248 this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`); 7249 const { 7250 urlPrefix 7251 } = this.thumbnails[0]; 7252 const thumbURL = urlPrefix + newThumbFilename; 7253 const previewImage = new Image(); 7254 previewImage.src = thumbURL; 7255 previewImage.onload = () => { 7256 this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`); 7257 if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename); 7258 7259 // We don't resolve until the thumb is loaded 7260 resolve(); 7261 }; 7262 } 7263 } 7264 }); 7265 7266 // If there are none to preload then we want to resolve immediately 7267 if (!foundOne) { 7268 resolve(); 7269 } 7270 } 7271 }, 300); 7272 }); 7273 }); 7274 // If user has been hovering current image for half a second, look for a higher quality one 7275 _defineProperty$1(this, "getHigherQuality", (currentQualityIndex, previewImage, frame, thumbFilename) => { 7276 if (currentQualityIndex < this.thumbnails.length - 1) { 7277 // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container 7278 let previewImageHeight = previewImage.naturalHeight; 7279 if (this.usingSprites) { 7280 previewImageHeight = frame.h; 7281 } 7282 if (previewImageHeight < this.thumbContainerHeight) { 7283 // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while 7284 setTimeout(() => { 7285 // Make sure the mouse hasn't already moved on and started hovering at another image 7286 if (this.showingThumbFilename === thumbFilename) { 7287 this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`); 7288 this.loadImage(currentQualityIndex + 1); 7289 } 7290 }, 300); 7291 } 7292 } 7293 }); 7294 _defineProperty$1(this, "toggleThumbContainer", (toggle = false, clearShowing = false) => { 7295 const className = this.player.config.classNames.previewThumbnails.thumbContainerShown; 7296 this.elements.thumb.container.classList.toggle(className, toggle); 7297 if (!toggle && clearShowing) { 7298 this.showingThumb = null; 7299 this.showingThumbFilename = null; 7300 } 7301 }); 7302 _defineProperty$1(this, "toggleScrubbingContainer", (toggle = false) => { 7303 const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown; 7304 this.elements.scrubbing.container.classList.toggle(className, toggle); 7305 if (!toggle) { 7306 this.showingThumb = null; 7307 this.showingThumbFilename = null; 7308 } 7309 }); 7310 _defineProperty$1(this, "determineContainerAutoSizing", () => { 7311 if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) { 7312 // This will prevent auto sizing in this.setThumbContainerSizeAndPos() 7313 this.sizeSpecifiedInCSS = true; 7314 } 7315 }); 7316 // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS 7317 _defineProperty$1(this, "setThumbContainerSizeAndPos", () => { 7318 const { 7319 imageContainer 7320 } = this.elements.thumb; 7321 if (!this.sizeSpecifiedInCSS) { 7322 const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio); 7323 imageContainer.style.height = `${this.thumbContainerHeight}px`; 7324 imageContainer.style.width = `${thumbWidth}px`; 7325 } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) { 7326 const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio); 7327 imageContainer.style.width = `${thumbWidth}px`; 7328 } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) { 7329 const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio); 7330 imageContainer.style.height = `${thumbHeight}px`; 7331 } 7332 this.setThumbContainerPos(); 7333 }); 7334 _defineProperty$1(this, "setThumbContainerPos", () => { 7335 const scrubberRect = this.player.elements.progress.getBoundingClientRect(); 7336 const containerRect = this.player.elements.container.getBoundingClientRect(); 7337 const { 7338 container 7339 } = this.elements.thumb; 7340 // Find the lowest and highest desired left-position, so we don't slide out the side of the video container 7341 const min = containerRect.left - scrubberRect.left + 10; 7342 const max = containerRect.right - scrubberRect.left - container.clientWidth - 10; 7343 // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth 7344 const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2; 7345 const clamped = clamp(position, min, max); 7346 7347 // Move the popover position 7348 container.style.left = `${clamped}px`; 7349 7350 // The arrow can follow the cursor 7351 container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`); 7352 }); 7353 // Can't use 100% width, in case the video is a different aspect ratio to the video container 7354 _defineProperty$1(this, "setScrubbingContainerSize", () => { 7355 const { 7356 width, 7357 height 7358 } = fitRatio(this.thumbAspectRatio, { 7359 width: this.player.media.clientWidth, 7360 height: this.player.media.clientHeight 7361 }); 7362 this.elements.scrubbing.container.style.width = `${width}px`; 7363 this.elements.scrubbing.container.style.height = `${height}px`; 7364 }); 7365 // Sprites need to be offset to the correct location 7366 _defineProperty$1(this, "setImageSizeAndOffset", (previewImage, frame) => { 7367 if (!this.usingSprites) return; 7368 7369 // Find difference between height and preview container height 7370 const multiplier = this.thumbContainerHeight / frame.h;
vendor: 16,285 bytes, lines 7371-7873
7371 7372 // eslint-disable-next-line no-param-reassign 7373 previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`; 7374 // eslint-disable-next-line no-param-reassign 7375 previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`; 7376 // eslint-disable-next-line no-param-reassign 7377 previewImage.style.left = `-${frame.x * multiplier}px`; 7378 // eslint-disable-next-line no-param-reassign 7379 previewImage.style.top = `-${frame.y * multiplier}px`; 7380 }); 7381 this.player = player; 7382 this.thumbnails = []; 7383 this.loaded = false; 7384 this.lastMouseMoveTime = Date.now(); 7385 this.mouseDown = false; 7386 this.loadedImages = []; 7387 this.elements = { 7388 thumb: {}, 7389 scrubbing: {} 7390 }; 7391 this.load(); 7392 } 7393 get enabled() { 7394 return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled; 7395 } 7396 get currentImageContainer() { 7397 return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer; 7398 } 7399 get usingSprites() { 7400 return Object.keys(this.thumbnails[0].frames[0]).includes('w'); 7401 } 7402 get thumbAspectRatio() { 7403 if (this.usingSprites) { 7404 return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h; 7405 } 7406 return this.thumbnails[0].width / this.thumbnails[0].height; 7407 } 7408 get thumbContainerHeight() { 7409 if (this.mouseDown) { 7410 const { 7411 height 7412 } = fitRatio(this.thumbAspectRatio, { 7413 width: this.player.media.clientWidth, 7414 height: this.player.media.clientHeight 7415 }); 7416 return height; 7417 } 7418 7419 // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset) 7420 if (this.sizeSpecifiedInCSS) { 7421 return this.elements.thumb.imageContainer.clientHeight; 7422 } 7423 return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4); 7424 } 7425 get currentImageElement() { 7426 return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement; 7427 } 7428 set currentImageElement(element) { 7429 if (this.mouseDown) { 7430 this.currentScrubbingImageElement = element; 7431 } else { 7432 this.currentThumbnailImageElement = element; 7433 } 7434 } 7435 } 7436 7437 // ========================================================================== 7438 const source = { 7439 // Add elements to HTML5 media (source, tracks, etc) 7440 insertElements(type, attributes) { 7441 if (is.string(attributes)) { 7442 insertElement(type, this.media, { 7443 src: attributes 7444 }); 7445 } else if (is.array(attributes)) { 7446 attributes.forEach(attribute => { 7447 insertElement(type, this.media, attribute); 7448 }); 7449 } 7450 }, 7451 // Update source 7452 // Sources are not checked for support so be careful 7453 change(input) { 7454 if (!getDeep(input, 'sources.length')) { 7455 this.debug.warn('Invalid source format'); 7456 return; 7457 } 7458 7459 // Cancel current network requests 7460 html5.cancelRequests.call(this); 7461 7462 // Destroy instance and re-setup 7463 this.destroy.call(this, () => { 7464 // Reset quality options 7465 this.options.quality = []; 7466 7467 // Remove elements 7468 removeElement(this.media); 7469 this.media = null; 7470 7471 // Reset class name 7472 if (is.element(this.elements.container)) { 7473 this.elements.container.removeAttribute('class'); 7474 } 7475 7476 // Set the type and provider 7477 const { 7478 sources, 7479 type 7480 } = input; 7481 const [{ 7482 provider = providers.html5, 7483 src 7484 }] = sources; 7485 const tagName = provider === 'html5' ? type : 'div'; 7486 const attributes = provider === 'html5' ? {} : { 7487 src 7488 }; 7489 Object.assign(this, { 7490 provider, 7491 type, 7492 // Check for support 7493 supported: support.check(type, provider, this.config.playsinline), 7494 // Create new element 7495 media: createElement(tagName, attributes) 7496 }); 7497 7498 // Inject the new element 7499 this.elements.container.appendChild(this.media); 7500 7501 // Autoplay the new source? 7502 if (is.boolean(input.autoplay)) { 7503 this.config.autoplay = input.autoplay; 7504 } 7505 7506 // Set attributes for audio and video 7507 if (this.isHTML5) { 7508 if (this.config.crossorigin) { 7509 this.media.setAttribute('crossorigin', ''); 7510 } 7511 if (this.config.autoplay) { 7512 this.media.setAttribute('autoplay', ''); 7513 } 7514 if (!is.empty(input.poster)) { 7515 this.poster = input.poster; 7516 } 7517 if (this.config.loop.active) { 7518 this.media.setAttribute('loop', ''); 7519 } 7520 if (this.config.muted) { 7521 this.media.setAttribute('muted', ''); 7522 } 7523 if (this.config.playsinline) { 7524 this.media.setAttribute('playsinline', ''); 7525 } 7526 } 7527 7528 // Restore class hook 7529 ui.addStyleHook.call(this); 7530 7531 // Set new sources for html5 7532 if (this.isHTML5) { 7533 source.insertElements.call(this, 'source', sources); 7534 } 7535 7536 // Set video title 7537 this.config.title = input.title; 7538 7539 // Set up from scratch 7540 media.setup.call(this); 7541 7542 // HTML5 stuff 7543 if (this.isHTML5) { 7544 // Setup captions 7545 if (Object.keys(input).includes('tracks')) { 7546 source.insertElements.call(this, 'track', input.tracks); 7547 } 7548 } 7549 7550 // If HTML5 or embed but not fully supported, setupInterface and call ready now 7551 if (this.isHTML5 || this.isEmbed && !this.supported.ui) { 7552 // Setup interface 7553 ui.build.call(this); 7554 } 7555 7556 // Load HTML5 sources 7557 if (this.isHTML5) { 7558 this.media.load(); 7559 } 7560 7561 // Update previewThumbnails config & reload plugin 7562 if (!is.empty(input.previewThumbnails)) { 7563 Object.assign(this.config.previewThumbnails, input.previewThumbnails); 7564 7565 // Cleanup previewThumbnails plugin if it was loaded 7566 if (this.previewThumbnails && this.previewThumbnails.loaded) { 7567 this.previewThumbnails.destroy(); 7568 this.previewThumbnails = null; 7569 } 7570 7571 // Create new instance if it is still enabled 7572 if (this.config.previewThumbnails.enabled) { 7573 this.previewThumbnails = new PreviewThumbnails(this); 7574 } 7575 } 7576 7577 // Update the fullscreen support 7578 this.fullscreen.update(); 7579 }, true); 7580 } 7581 }; 7582 7583 // Private properties 7584 // TODO: Use a WeakMap for private globals 7585 // const globals = new WeakMap(); 7586 7587 // Plyr instance 7588 class Plyr { 7589 constructor(target, options) { 7590 /** 7591 * Play the media, or play the advertisement (if they are not blocked) 7592 */ 7593 _defineProperty$1(this, "play", () => { 7594 if (!is.function(this.media.play)) { 7595 return null; 7596 } 7597 7598 // Intecept play with ads 7599 if (this.ads && this.ads.enabled) { 7600 this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play())); 7601 } 7602 7603 // Return the promise (for HTML5) 7604 return this.media.play(); 7605 }); 7606 /** 7607 * Pause the media 7608 */ 7609 _defineProperty$1(this, "pause", () => { 7610 if (!this.playing || !is.function(this.media.pause)) { 7611 return null; 7612 } 7613 return this.media.pause(); 7614 }); 7615 /** 7616 * Toggle playback based on current status 7617 * @param {Boolean} input 7618 */ 7619 _defineProperty$1(this, "togglePlay", input => { 7620 // Toggle based on current state if nothing passed 7621 const toggle = is.boolean(input) ? input : !this.playing; 7622 if (toggle) { 7623 return this.play(); 7624 } 7625 return this.pause(); 7626 }); 7627 /** 7628 * Stop playback 7629 */ 7630 _defineProperty$1(this, "stop", () => { 7631 if (this.isHTML5) { 7632 this.pause(); 7633 this.restart(); 7634 } else if (is.function(this.media.stop)) { 7635 this.media.stop(); 7636 } 7637 }); 7638 /** 7639 * Restart playback 7640 */ 7641 _defineProperty$1(this, "restart", () => { 7642 this.currentTime = 0; 7643 }); 7644 /** 7645 * Rewind 7646 * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime 7647 */ 7648 _defineProperty$1(this, "rewind", seekTime => { 7649 this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime; 7650 }); 7651 /** 7652 * Fast forward 7653 * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime 7654 */ 7655 _defineProperty$1(this, "forward", seekTime => { 7656 this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime; 7657 }); 7658 /** 7659 * Increase volume 7660 * @param {Boolean} step - How much to decrease by (between 0 and 1) 7661 */ 7662 _defineProperty$1(this, "increaseVolume", step => { 7663 const volume = this.media.muted ? 0 : this.volume; 7664 this.volume = volume + (is.number(step) ? step : 0); 7665 }); 7666 /** 7667 * Decrease volume 7668 * @param {Boolean} step - How much to decrease by (between 0 and 1) 7669 */ 7670 _defineProperty$1(this, "decreaseVolume", step => { 7671 this.increaseVolume(-step); 7672 }); 7673 /** 7674 * Trigger the airplay dialog 7675 * TODO: update player with state, support, enabled 7676 */ 7677 _defineProperty$1(this, "airplay", () => { 7678 // Show dialog if supported 7679 if (support.airplay) { 7680 this.media.webkitShowPlaybackTargetPicker(); 7681 } 7682 }); 7683 /** 7684 * Toggle the player controls 7685 * @param {Boolean} [toggle] - Whether to show the controls 7686 */ 7687 _defineProperty$1(this, "toggleControls", toggle => { 7688 // Don't toggle if missing UI support or if it's audio 7689 if (this.supported.ui && !this.isAudio) { 7690 // Get state before change 7691 const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); 7692 // Negate the argument if not undefined since adding the class to hides the controls 7693 const force = typeof toggle === 'undefined' ? undefined : !toggle; 7694 // Apply and get updated state 7695 const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); 7696 7697 // Close menu 7698 if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { 7699 controls.toggleMenu.call(this, false); 7700 } 7701 7702 // Trigger event on change 7703 if (hiding !== isHidden) { 7704 const eventName = hiding ? 'controlshidden' : 'controlsshown'; 7705 triggerEvent.call(this, this.media, eventName); 7706 } 7707 return !hiding; 7708 } 7709 return false; 7710 }); 7711 /** 7712 * Add event listeners 7713 * @param {String} event - Event type 7714 * @param {Function} callback - Callback for when event occurs 7715 */ 7716 _defineProperty$1(this, "on", (event, callback) => { 7717 on.call(this, this.elements.container, event, callback); 7718 }); 7719 /** 7720 * Add event listeners once 7721 * @param {String} event - Event type 7722 * @param {Function} callback - Callback for when event occurs 7723 */ 7724 _defineProperty$1(this, "once", (event, callback) => { 7725 once.call(this, this.elements.container, event, callback); 7726 }); 7727 /** 7728 * Remove event listeners 7729 * @param {String} event - Event type 7730 * @param {Function} callback - Callback for when event occurs 7731 */ 7732 _defineProperty$1(this, "off", (event, callback) => { 7733 off(this.elements.container, event, callback); 7734 }); 7735 /** 7736 * Destroy an instance 7737 * Event listeners are removed when elements are removed 7738 * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory 7739 * @param {Function} callback - Callback for when destroy is complete 7740 * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc) 7741 */ 7742 _defineProperty$1(this, "destroy", (callback, soft = false) => { 7743 if (!this.ready) { 7744 return; 7745 } 7746 const done = () => { 7747 // Reset overflow (incase destroyed while in fullscreen) 7748 document.body.style.overflow = ''; 7749 7750 // GC for embed 7751 this.embed = null; 7752 7753 // If it's a soft destroy, make minimal changes 7754 if (soft) { 7755 if (Object.keys(this.elements).length) { 7756 // Remove elements 7757 removeElement(this.elements.buttons.play); 7758 removeElement(this.elements.captions); 7759 removeElement(this.elements.controls); 7760 removeElement(this.elements.wrapper); 7761 7762 // Clear for GC 7763 this.elements.buttons.play = null; 7764 this.elements.captions = null; 7765 this.elements.controls = null; 7766 this.elements.wrapper = null; 7767 } 7768 7769 // Callback 7770 if (is.function(callback)) { 7771 callback(); 7772 } 7773 } else { 7774 // Unbind listeners 7775 unbindListeners.call(this); 7776 7777 // Cancel current network requests 7778 html5.cancelRequests.call(this); 7779 7780 // Replace the container with the original element provided 7781 replaceElement(this.elements.original, this.elements.container); 7782 7783 // Event 7784 triggerEvent.call(this, this.elements.original, 'destroyed', true); 7785 7786 // Callback 7787 if (is.function(callback)) { 7788 callback.call(this.elements.original); 7789 } 7790 7791 // Reset state 7792 this.ready = false; 7793 7794 // Clear for garbage collection 7795 setTimeout(() => { 7796 this.elements = null; 7797 this.media = null; 7798 }, 200); 7799 } 7800 }; 7801 7802 // Stop playback 7803 this.stop(); 7804 7805 // Clear timeouts 7806 clearTimeout(this.timers.loading); 7807 clearTimeout(this.timers.controls); 7808 clearTimeout(this.timers.resized); 7809 7810 // Provider specific stuff 7811 if (this.isHTML5) { 7812 // Restore native video controls 7813 ui.toggleNativeControls.call(this, true); 7814 7815 // Clean up 7816 done(); 7817 } else if (this.isYouTube) { 7818 // Clear timers 7819 clearInterval(this.timers.buffering); 7820 clearInterval(this.timers.playing); 7821 7822 // Destroy YouTube API 7823 if (this.embed !== null && is.function(this.embed.destroy)) { 7824 this.embed.destroy(); 7825 } 7826 7827 // Clean up 7828 done(); 7829 } else if (this.isVimeo) { 7830 // Destroy Vimeo API 7831 // then clean up (wait, to prevent postmessage errors) 7832 if (this.embed !== null) { 7833 this.embed.unload().then(done); 7834 } 7835 7836 // Vimeo does not always return 7837 setTimeout(done, 200); 7838 } 7839 }); 7840 /** 7841 * Check for support for a mime type (HTML5 only) 7842 * @param {String} type - Mime type 7843 */ 7844 _defineProperty$1(this, "supports", type => support.mime.call(this, type)); 7845 this.timers = {}; 7846 7847 // State 7848 this.ready = false; 7849 this.loading = false; 7850 this.failed = false; 7851 7852 // Touch device 7853 this.touch = support.touch; 7854 7855 // Set the media element 7856 this.media = target; 7857 7858 // String selector passed 7859 if (is.string(this.media)) { 7860 this.media = document.querySelectorAll(this.media); 7861 } 7862 7863 // jQuery, NodeList or Array passed, use first element 7864 if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) { 7865 // eslint-disable-next-line 7866 this.media = this.media[0]; 7867 } 7868 7869 // Set config 7870 this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => { 7871 try { 7872 return JSON.parse(this.media.getAttribute('data-plyr-config')); 7873 } catch (_)
vendor: 8,226 bytes, lines 7873-8169
7873 { 7874 return {}; 7875 } 7876 })()); 7877 7878 // Elements cache 7879 this.elements = { 7880 container: null, 7881 fullscreen: null, 7882 captions: null, 7883 buttons: {}, 7884 display: {}, 7885 progress: {}, 7886 inputs: {}, 7887 settings: { 7888 popup: null, 7889 menu: null, 7890 panels: {}, 7891 buttons: {} 7892 } 7893 }; 7894 7895 // Captions 7896 this.captions = { 7897 active: null, 7898 currentTrack: -1, 7899 meta: new WeakMap() 7900 }; 7901 7902 // Fullscreen 7903 this.fullscreen = { 7904 active: false 7905 }; 7906 7907 // Options 7908 this.options = { 7909 speed: [], 7910 quality: [] 7911 }; 7912 7913 // Debugging 7914 // TODO: move to globals 7915 this.debug = new Console(this.config.debug); 7916 7917 // Log config options and support 7918 this.debug.log('Config', this.config); 7919 this.debug.log('Support', support); 7920 7921 // We need an element to setup 7922 if (is.nullOrUndefined(this.media) || !is.element(this.media)) { 7923 this.debug.error('Setup failed: no suitable element passed'); 7924 return; 7925 } 7926 7927 // Bail if the element is initialized 7928 if (this.media.plyr) { 7929 this.debug.warn('Target already setup'); 7930 return; 7931 } 7932 7933 // Bail if not enabled 7934 if (!this.config.enabled) { 7935 this.debug.error('Setup failed: disabled by config'); 7936 return; 7937 } 7938 7939 // Bail if disabled or no basic support 7940 // You may want to disable certain UAs etc 7941 if (!support.check().api) { 7942 this.debug.error('Setup failed: no support'); 7943 return; 7944 } 7945 7946 // Cache original element state for .destroy() 7947 const clone = this.media.cloneNode(true); 7948 clone.autoplay = false; 7949 this.elements.original = clone; 7950 7951 // Set media type based on tag or data attribute 7952 // Supported: video, audio, vimeo, youtube 7953 const _type = this.media.tagName.toLowerCase(); 7954 // Embed properties 7955 let iframe = null; 7956 let url = null; 7957 7958 // Different setup based on type 7959 switch (_type) { 7960 case 'div': 7961 // Find the frame 7962 iframe = this.media.querySelector('iframe'); 7963 7964 // <iframe> type 7965 if (is.element(iframe)) { 7966 // Detect provider 7967 url = parseUrl(iframe.getAttribute('src')); 7968 this.provider = getProviderByUrl(url.toString()); 7969 7970 // Rework elements 7971 this.elements.container = this.media; 7972 this.media = iframe; 7973 7974 // Reset classname 7975 this.elements.container.className = ''; 7976 7977 // Get attributes from URL and set config 7978 if (url.search.length) { 7979 const truthy = ['1', 'true']; 7980 if (truthy.includes(url.searchParams.get('autoplay'))) { 7981 this.config.autoplay = true; 7982 } 7983 if (truthy.includes(url.searchParams.get('loop'))) { 7984 this.config.loop.active = true; 7985 } 7986 7987 // TODO: replace fullscreen.iosNative with this playsinline config option 7988 // YouTube requires the playsinline in the URL 7989 if (this.isYouTube) { 7990 this.config.playsinline = truthy.includes(url.searchParams.get('playsinline')); 7991 this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language? 7992 } else { 7993 this.config.playsinline = true; 7994 } 7995 } 7996 } else { 7997 // <div> with attributes 7998 this.provider = this.media.getAttribute(this.config.attributes.embed.provider); 7999 8000 // Remove attribute 8001 this.media.removeAttribute(this.config.attributes.embed.provider); 8002 } 8003 8004 // Unsupported or missing provider 8005 if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) { 8006 this.debug.error('Setup failed: Invalid provider'); 8007 return; 8008 } 8009 8010 // Audio will come later for external providers 8011 this.type = types.video; 8012 break; 8013 case 'video': 8014 case 'audio': 8015 this.type = _type; 8016 this.provider = providers.html5; 8017 8018 // Get config from attributes 8019 if (this.media.hasAttribute('crossorigin')) { 8020 this.config.crossorigin = true; 8021 } 8022 if (this.media.hasAttribute('autoplay')) { 8023 this.config.autoplay = true; 8024 } 8025 if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) { 8026 this.config.playsinline = true; 8027 } 8028 if (this.media.hasAttribute('muted')) { 8029 this.config.muted = true; 8030 } 8031 if (this.media.hasAttribute('loop')) { 8032 this.config.loop.active = true; 8033 } 8034 break; 8035 default: 8036 this.debug.error('Setup failed: unsupported type'); 8037 return; 8038 } 8039 8040 // Check for support again but with type 8041 this.supported = support.check(this.type, this.provider); 8042 8043 // If no support for even API, bail 8044 if (!this.supported.api) { 8045 this.debug.error('Setup failed: no support'); 8046 return; 8047 } 8048 this.eventListeners = []; 8049 8050 // Create listeners 8051 this.listeners = new Listeners(this); 8052 8053 // Setup local storage for user settings 8054 this.storage = new Storage(this); 8055 8056 // Store reference 8057 this.media.plyr = this; 8058 8059 // Wrap media 8060 if (!is.element(this.elements.container)) { 8061 this.elements.container = createElement('div'); 8062 wrap(this.media, this.elements.container); 8063 } 8064 8065 // Migrate custom properties from media to container (so they work ð) 8066 ui.migrateStyles.call(this); 8067 8068 // Add style hook 8069 ui.addStyleHook.call(this); 8070 8071 // Setup media 8072 media.setup.call(this); 8073 8074 // Listen for events if debugging 8075 if (this.config.debug) { 8076 on.call(this, this.elements.container, this.config.events.join(' '), event => { 8077 this.debug.log(`event: ${event.type}`); 8078 }); 8079 } 8080 8081 // Setup fullscreen 8082 this.fullscreen = new Fullscreen(this); 8083 8084 // Setup interface 8085 // If embed but not fully supported, build interface now to avoid flash of controls 8086 if (this.isHTML5 || this.isEmbed && !this.supported.ui) { 8087 ui.build.call(this); 8088 } 8089 8090 // Container listeners 8091 this.listeners.container(); 8092 8093 // Global listeners 8094 this.listeners.global(); 8095 8096 // Setup ads if provided 8097 if (this.config.ads.enabled) { 8098 this.ads = new Ads(this); 8099 } 8100 8101 // Autoplay if required 8102 if (this.isHTML5 && this.config.autoplay) { 8103 this.once('canplay', () => silencePromise(this.play())); 8104 } 8105 8106 // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek 8107 this.lastSeekTime = 0; 8108 8109 // Setup preview thumbnails if enabled 8110 if (this.config.previewThumbnails.enabled) { 8111 this.previewThumbnails = new PreviewThumbnails(this); 8112 } 8113 } 8114 8115 // --------------------------------------- 8116 // API 8117 // --------------------------------------- 8118 8119 /** 8120 * Types and provider helpers 8121 */ 8122 get isHTML5() { 8123 return this.provider === providers.html5; 8124 } 8125 get isEmbed() { 8126 return this.isYouTube || this.isVimeo; 8127 } 8128 get isYouTube() { 8129 return this.provider === providers.youtube; 8130 } 8131 get isVimeo() { 8132 return this.provider === providers.vimeo; 8133 } 8134 get isVideo() { 8135 return this.type === types.video; 8136 } 8137 get isAudio() { 8138 return this.type === types.audio; 8139 } 8140 /** 8141 * Get playing state 8142 */ 8143 get playing() { 8144 return Boolean(this.ready && !this.paused && !this.ended); 8145 } 8146 8147 /** 8148 * Get paused state 8149 */ 8150 get paused() { 8151 return Boolean(this.media.paused); 8152 } 8153 8154 /** 8155 * Get stopped state 8156 */ 8157 get stopped() { 8158 return Boolean(this.paused && this.currentTime === 0); 8159 } 8160 8161 /** 8162 * Get ended state 8163 */ 8164 get ended() { 8165 return Boolean(this.media.ended); 8166 } 8167 /** 8168 * Seek to a time 8169 * @param {
vendor: 15,340 bytes, lines 8169-8758
8169Number} input - where to seek to in seconds. Defaults to 0 (the start) 8170 */ 8171 set currentTime(input) { 8172 // Bail if media duration isn't available yet 8173 if (!this.duration) { 8174 return; 8175 } 8176 8177 // Validate input 8178 const inputIsValid = is.number(input) && input > 0; 8179 8180 // Set 8181 this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; 8182 8183 // Logging 8184 this.debug.log(`Seeking to ${this.currentTime} seconds`); 8185 } 8186 8187 /** 8188 * Get current time 8189 */ 8190 get currentTime() { 8191 return Number(this.media.currentTime); 8192 } 8193 8194 /** 8195 * Get buffered 8196 */ 8197 get buffered() { 8198 const { 8199 buffered 8200 } = this.media; 8201 8202 // YouTube / Vimeo return a float between 0-1 8203 if (is.number(buffered)) { 8204 return buffered; 8205 } 8206 8207 // HTML5 8208 // TODO: Handle buffered chunks of the media 8209 // (i.e. seek to another section buffers only that section) 8210 if (buffered && buffered.length && this.duration > 0) { 8211 return buffered.end(0) / this.duration; 8212 } 8213 return 0; 8214 } 8215 8216 /** 8217 * Get seeking status 8218 */ 8219 get seeking() { 8220 return Boolean(this.media.seeking); 8221 } 8222 8223 /** 8224 * Get the duration of the current media 8225 */ 8226 get duration() { 8227 // Faux duration set via config 8228 const fauxDuration = parseFloat(this.config.duration); 8229 // Media duration can be NaN or Infinity before the media has loaded 8230 const realDuration = (this.media || {}).duration; 8231 const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; 8232 8233 // If config duration is funky, use regular duration 8234 return fauxDuration || duration; 8235 } 8236 8237 /** 8238 * Set the player volume 8239 * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage 8240 */ 8241 set volume(value) { 8242 let volume = value; 8243 const max = 1; 8244 const min = 0; 8245 if (is.string(volume)) { 8246 volume = Number(volume); 8247 } 8248 8249 // Load volume from storage if no value specified 8250 if (!is.number(volume)) { 8251 volume = this.storage.get('volume'); 8252 } 8253 8254 // Use config if all else fails 8255 if (!is.number(volume)) { 8256 ({ 8257 volume 8258 } = this.config); 8259 } 8260 8261 // Maximum is volumeMax 8262 if (volume > max) { 8263 volume = max; 8264 } 8265 // Minimum is volumeMin 8266 if (volume < min) { 8267 volume = min; 8268 } 8269 8270 // Update config 8271 this.config.volume = volume; 8272 8273 // Set the player volume 8274 this.media.volume = volume; 8275 8276 // If muted, and we're increasing volume manually, reset muted state 8277 if (!is.empty(value) && this.muted && volume > 0) { 8278 this.muted = false; 8279 } 8280 } 8281 8282 /** 8283 * Get the current player volume 8284 */ 8285 get volume() { 8286 return Number(this.media.volume); 8287 } 8288 /** 8289 * Set muted state 8290 * @param {Boolean} mute 8291 */ 8292 set muted(mute) { 8293 let toggle = mute; 8294 8295 // Load muted state from storage 8296 if (!is.boolean(toggle)) { 8297 toggle = this.storage.get('muted'); 8298 } 8299 8300 // Use config if all else fails 8301 if (!is.boolean(toggle)) { 8302 toggle = this.config.muted; 8303 } 8304 8305 // Update config 8306 this.config.muted = toggle; 8307 8308 // Set mute on the player 8309 this.media.muted = toggle; 8310 } 8311 8312 /** 8313 * Get current muted state 8314 */ 8315 get muted() { 8316 return Boolean(this.media.muted); 8317 } 8318 8319 /** 8320 * Check if the media has audio 8321 */ 8322 get hasAudio() { 8323 // Assume yes for all non HTML5 (as we can't tell...) 8324 if (!this.isHTML5) { 8325 return true; 8326 } 8327 if (this.isAudio) { 8328 return true; 8329 } 8330 8331 // Get audio tracks 8332 return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length); 8333 } 8334 8335 /** 8336 * Set playback speed 8337 * @param {Number} input - the speed of playback (0.5-2.0) 8338 */ 8339 set speed(input) { 8340 let speed = null; 8341 if (is.number(input)) { 8342 speed = input; 8343 } 8344 if (!is.number(speed)) { 8345 speed = this.storage.get('speed'); 8346 } 8347 if (!is.number(speed)) { 8348 speed = this.config.speed.selected; 8349 } 8350 8351 // Clamp to min/max 8352 const { 8353 minimumSpeed: min, 8354 maximumSpeed: max 8355 } = this; 8356 speed = clamp(speed, min, max); 8357 8358 // Update config 8359 this.config.speed.selected = speed; 8360 8361 // Set media speed 8362 setTimeout(() => { 8363 if (this.media) { 8364 this.media.playbackRate = speed; 8365 } 8366 }, 0); 8367 } 8368 8369 /** 8370 * Get current playback speed 8371 */ 8372 get speed() { 8373 return Number(this.media.playbackRate); 8374 } 8375 8376 /** 8377 * Get the minimum allowed speed 8378 */ 8379 get minimumSpeed() { 8380 if (this.isYouTube) { 8381 // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate 8382 return Math.min(...this.options.speed); 8383 } 8384 if (this.isVimeo) { 8385 // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror 8386 return 0.5; 8387 } 8388 8389 // https://stackoverflow.com/a/32320020/1191319 8390 return 0.0625; 8391 } 8392 8393 /** 8394 * Get the maximum allowed speed 8395 */ 8396 get maximumSpeed() { 8397 if (this.isYouTube) { 8398 // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate 8399 return Math.max(...this.options.speed); 8400 } 8401 if (this.isVimeo) { 8402 // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror 8403 return 2; 8404 } 8405 8406 // https://stackoverflow.com/a/32320020/1191319 8407 return 16; 8408 } 8409 8410 /** 8411 * Set playback quality 8412 * Currently HTML5 & YouTube only 8413 * @param {Number} input - Quality level 8414 */ 8415 set quality(input) { 8416 const config = this.config.quality; 8417 const options = this.options.quality; 8418 if (!options.length) { 8419 return; 8420 } 8421 let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number); 8422 let updateStorage = true; 8423 if (!options.includes(quality)) { 8424 const value = closest(options, quality); 8425 this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`); 8426 quality = value; 8427 8428 // Don't update storage if quality is not supported 8429 updateStorage = false; 8430 } 8431 8432 // Update config 8433 config.selected = quality; 8434 8435 // Set quality 8436 this.media.quality = quality; 8437 8438 // Save to storage 8439 if (updateStorage) { 8440 this.storage.set({ 8441 quality 8442 }); 8443 } 8444 } 8445 8446 /** 8447 * Get current quality level 8448 */ 8449 get quality() { 8450 return this.media.quality; 8451 } 8452 8453 /** 8454 * Toggle loop 8455 * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config 8456 * @param {Boolean} input - Whether to loop or not 8457 */ 8458 set loop(input) { 8459 const toggle = is.boolean(input) ? input : this.config.loop.active; 8460 this.config.loop.active = toggle; 8461 this.media.loop = toggle; 8462 8463 // Set default to be a true toggle 8464 /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; 8465 switch (type) { 8466 case 'start': 8467 if (this.config.loop.end && this.config.loop.end <= this.currentTime) { 8468 this.config.loop.end = null; 8469 } 8470 this.config.loop.start = this.currentTime; 8471 // this.config.loop.indicator.start = this.elements.display.played.value; 8472 break; 8473 case 'end': 8474 if (this.config.loop.start >= this.currentTime) { 8475 return this; 8476 } 8477 this.config.loop.end = this.currentTime; 8478 // this.config.loop.indicator.end = this.elements.display.played.value; 8479 break; 8480 case 'all': 8481 this.config.loop.start = 0; 8482 this.config.loop.end = this.duration - 2; 8483 this.config.loop.indicator.start = 0; 8484 this.config.loop.indicator.end = 100; 8485 break; 8486 case 'toggle': 8487 if (this.config.loop.active) { 8488 this.config.loop.start = 0; 8489 this.config.loop.end = null; 8490 } else { 8491 this.config.loop.start = 0; 8492 this.config.loop.end = this.duration - 2; 8493 } 8494 break; 8495 default: 8496 this.config.loop.start = 0; 8497 this.config.loop.end = null; 8498 break; 8499 } */ 8500 } 8501 8502 /** 8503 * Get current loop state 8504 */ 8505 get loop() { 8506 return Boolean(this.media.loop); 8507 } 8508 8509 /** 8510 * Set new media source 8511 * @param {Object} input - The new source object (see docs) 8512 */ 8513 set source(input) { 8514 source.change.call(this, input); 8515 } 8516 8517 /** 8518 * Get current source 8519 */ 8520 get source() { 8521 return this.media.currentSrc; 8522 } 8523 8524 /** 8525 * Get a download URL (either source or custom) 8526 */ 8527 get download() { 8528 const { 8529 download 8530 } = this.config.urls; 8531 return is.url(download) ? download : this.source; 8532 } 8533 8534 /** 8535 * Set the download URL 8536 */ 8537 set download(input) { 8538 if (!is.url(input)) { 8539 return; 8540 } 8541 this.config.urls.download = input; 8542 controls.setDownloadUrl.call(this); 8543 } 8544 8545 /** 8546 * Set the poster image for a video 8547 * @param {String} input - the URL for the new poster image 8548 */ 8549 set poster(input) { 8550 if (!this.isVideo) { 8551 this.debug.warn('Poster can only be set for video'); 8552 return; 8553 } 8554 ui.setPoster.call(this, input, false).catch(() => {}); 8555 } 8556 8557 /** 8558 * Get the current poster image 8559 */ 8560 get poster() { 8561 if (!this.isVideo) { 8562 return null; 8563 } 8564 return this.media.getAttribute('poster') || this.media.getAttribute('data-poster'); 8565 } 8566 8567 /** 8568 * Get the current aspect ratio in use 8569 */ 8570 get ratio() { 8571 if (!this.isVideo) { 8572 return null; 8573 } 8574 const ratio = reduceAspectRatio(getAspectRatio.call(this)); 8575 return is.array(ratio) ? ratio.join(':') : ratio; 8576 } 8577 8578 /** 8579 * Set video aspect ratio 8580 */ 8581 set ratio(input) { 8582 if (!this.isVideo) { 8583 this.debug.warn('Aspect ratio can only be set for video'); 8584 return; 8585 } 8586 if (!is.string(input) || !validateAspectRatio(input)) { 8587 this.debug.error(`Invalid aspect ratio specified (${input})`); 8588 return; 8589 } 8590 this.config.ratio = reduceAspectRatio(input); 8591 setAspectRatio.call(this); 8592 } 8593 8594 /** 8595 * Set the autoplay state 8596 * @param {Boolean} input - Whether to autoplay or not 8597 */ 8598 set autoplay(input) { 8599 this.config.autoplay = is.boolean(input) ? input : this.config.autoplay; 8600 } 8601 8602 /** 8603 * Get the current autoplay state 8604 */ 8605 get autoplay() { 8606 return Boolean(this.config.autoplay); 8607 } 8608 8609 /** 8610 * Toggle captions 8611 * @param {Boolean} input - Whether to enable captions 8612 */ 8613 toggleCaptions(input) { 8614 captions.toggle.call(this, input, false); 8615 } 8616 8617 /** 8618 * Set the caption track by index 8619 * @param {Number} input - Caption index 8620 */ 8621 set currentTrack(input) { 8622 captions.set.call(this, input, false); 8623 captions.setup.call(this); 8624 } 8625 8626 /** 8627 * Get the current caption track index (-1 if disabled) 8628 */ 8629 get currentTrack() { 8630 const { 8631 toggled, 8632 currentTrack 8633 } = this.captions; 8634 return toggled ? currentTrack : -1; 8635 } 8636 8637 /** 8638 * Set the wanted language for captions 8639 * Since tracks can be added later it won't update the actual caption track until there is a matching track 8640 * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc) 8641 */ 8642 set language(input) { 8643 captions.setLanguage.call(this, input, false); 8644 } 8645 8646 /** 8647 * Get the current track's language 8648 */ 8649 get language() { 8650 return (captions.getCurrentTrack.call(this) || {}).language; 8651 } 8652 8653 /** 8654 * Toggle picture-in-picture playback on WebKit/MacOS 8655 * TODO: update player with state, support, enabled 8656 * TODO: detect outside changes 8657 */ 8658 set pip(input) { 8659 // Bail if no support 8660 if (!support.pip) { 8661 return; 8662 } 8663 8664 // Toggle based on current state if not passed 8665 const toggle = is.boolean(input) ? input : !this.pip; 8666 8667 // Toggle based on current state 8668 // Safari 8669 if (is.function(this.media.webkitSetPresentationMode)) { 8670 this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive); 8671 } 8672 8673 // Chrome 8674 if (is.function(this.media.requestPictureInPicture)) { 8675 if (!this.pip && toggle) { 8676 this.media.requestPictureInPicture(); 8677 } else if (this.pip && !toggle) { 8678 document.exitPictureInPicture(); 8679 } 8680 } 8681 } 8682 8683 /** 8684 * Get the current picture-in-picture state 8685 */ 8686 get pip() { 8687 if (!support.pip) { 8688 return null; 8689 } 8690 8691 // Safari 8692 if (!is.empty(this.media.webkitPresentationMode)) { 8693 return this.media.webkitPresentationMode === pip.active; 8694 } 8695 8696 // Chrome 8697 return this.media === document.pictureInPictureElement; 8698 } 8699 8700 /** 8701 * Sets the preview thumbnails for the current source 8702 */ 8703 setPreviewThumbnails(thumbnailSource) { 8704 if (this.previewThumbnails && this.previewThumbnails.loaded) { 8705 this.previewThumbnails.destroy(); 8706 this.previewThumbnails = null; 8707 } 8708 Object.assign(this.config.previewThumbnails, thumbnailSource); 8709 8710 // Create new instance if it is still enabled 8711 if (this.config.previewThumbnails.enabled) { 8712 this.previewThumbnails = new PreviewThumbnails(this); 8713 } 8714 } 8715 /** 8716 * Check for support 8717 * @param {String} type - Player type (audio/video) 8718 * @param {String} provider - Provider (html5/youtube/vimeo) 8719 */ 8720 static supported(type, provider) { 8721 return support.check(type, provider); 8722 } 8723 8724 /** 8725 * Load an SVG sprite into the page 8726 * @param {String} url - URL for the SVG sprite 8727 * @param {String} [id] - Unique ID 8728 */ 8729 static loadSprite(url, id) { 8730 return loadSprite(url, id); 8731 } 8732 8733 /** 8734 * Setup multiple instances 8735 * @param {*} selector 8736 * @param {Object} options 8737 */ 8738 static setup(selector, options = {}) { 8739 let targets = null; 8740 if (is.string(selector)) { 8741 targets = Array.from(document.querySelectorAll(selector)); 8742 } else if (is.nodeList(selector)) { 8743 targets = Array.from(selector); 8744 } else if (is.array(selector)) { 8745 targets = selector.filter(is.element); 8746 } 8747 if (is.empty(targets)) { 8748 return null; 8749 } 8750 return targets.map(t => new Plyr(t, options)); 8751 } 8752 } 8753 Plyr.defaults = cloneDeep(defaults); 8754 8755 return Plyr; 8756 8757})); 8758
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.