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