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