1// ========================================================================== 2// Plyr 3// plyr.js v2.0.13 4// https://github.com/sampotts/plyr 5// License: The MIT License (MIT) 6// ========================================================================== 7// Credits: http://paypal.github.io/accessible-html5-video-player/ 8// ========================================================================== 9 10;(function (root, factory) { 11 'use strict'; 12 /*global define,module*/ 13 14 if (typeof module === 'object' && typeof module.exports === 'object') { 15 // Node, CommonJS-like 16 module.exports = factory(root, document); 17 } else if (typeof define === 'function' && define.amd) { 18 // AMD 19 define([], function () { 20 return factory(root, document); 21 }); 22 } else { 23 // Browser globals (root is window) 24 root.plyr = factory(root, document); 25 } 26}(typeof window !== 'undefined' ? window : this, function (window, document) { 27 'use strict'; 28 29 // Globals 30 var fullscreen, 31 scroll = {x: 0, y: 0}, 32 33 // Default config 34 defaults = { 35 enabled: true, 36 debug: false, 37 autoplay: false, 38 loop: false, 39 seekTime: 10, 40 volume: 10, 41 volumeMin: 0, 42 volumeMax: 10, 43 volumeStep: 1, 44 duration: null, 45 displayDuration: true, 46 loadSprite: true, 47 iconPrefix: 'plyr', 48 iconUrl: 'https://cdn.plyr.io/2.0.13/plyr.svg', 49 blankUrl: 'https://cdn.selz.com/plyr/blank.mp4', 50 clickToPlay: true, 51 hideControls: true, 52 showPosterOnEnd: false, 53 disableContextMenu: true, 54 keyboardShorcuts: { 55 focused: true, 56 global: false 57 }, 58 tooltips: { 59 controls: false, 60 seek: true 61 }, 62 selectors: { 63 html5: 'video, audio', 64 embed: '[data-type]', 65 editable: 'input, textarea, select, [contenteditable]', 66 container: '.plyr', 67 controls: { 68 container: null, 69 wrapper: '.plyr__controls' 70 }, 71 labels: '[data-plyr]', 72 buttons: { 73 seek: '[data-plyr="seek"]', 74 play: '[data-plyr="play"]', 75 pause: '[data-plyr="pause"]', 76 restart: '[data-plyr="restart"]', 77 rewind: '[data-plyr="rewind"]', 78 forward: '[data-plyr="fast-forward"]', 79 mute: '[data-plyr="mute"]', 80 captions: '[data-plyr="captions"]', 81 fullscreen: '[data-plyr="fullscreen"]' 82 }, 83 volume: { 84 input: '[data-plyr="volume"]', 85 display: '.plyr__volume--display' 86 }, 87 progress: { 88 container: '.plyr__progress', 89 buffer: '.plyr__progress--buffer', 90 played: '.plyr__progress--played' 91 }, 92 captions: '.plyr__captions', 93 currentTime: '.plyr__time--current', 94 duration: '.plyr__time--duration' 95 }, 96 classes: { 97 setup: 'plyr--setup', 98 ready: 'plyr--ready', 99 videoWrapper: 'plyr__video-wrapper', 100 embedWrapper: 'plyr__video-embed', 101 type: 'plyr--{0}', 102 stopped: 'plyr--stopped', 103 playing: 'plyr--playing', 104 muted: 'plyr--muted', 105 loading: 'plyr--loading', 106 hover: 'plyr--hover', 107 tooltip: 'plyr__tooltip', 108 hidden: 'plyr__sr-only', 109 hideControls: 'plyr--hide-controls', 110 isIos: 'plyr--is-ios', 111 isTouch: 'plyr--is-touch', 112 captions: { 113 enabled: 'plyr--captions-enabled', 114 active: 'plyr--captions-active' 115 }, 116 fullscreen: { 117 enabled: 'plyr--fullscreen-enabled', 118 active: 'plyr--fullscreen-active' 119 }, 120 tabFocus: 'tab-focus' 121 }, 122 captions: { 123 defaultActive: false 124 }, 125 fullscreen: { 126 enabled: true, 127 fallback: true, 128 allowAudio: false 129 }, 130 storage: { 131 enabled: true, 132 key: 'plyr' 133 }, 134 controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'fullscreen'], 135 i18n: { 136 restart: 'Restart', 137 rewind: 'Rewind {seektime} secs', 138 play: 'Play', 139 pause: 'Pause', 140 forward: 'Forward {seektime} secs', 141 played: 'played', 142 buffered: 'buffered', 143 currentTime: 'Current time', 144 duration: 'Duration', 145 volume: 'Volume', 146 toggleMute: 'Toggle Mute', 147 toggleCaptions: 'Toggle Captions', 148 toggleFullscreen: 'Toggle Fullscreen', 149 frameTitle: 'Player for {title}' 150 }, 151 types: { 152 embed: ['youtube', 'vimeo', 'soundcloud'], 153 html5: ['video', 'audio'] 154 }, 155 // URLs 156 urls: { 157 vimeo: { 158 api: 'https://player.vimeo.com/api/player.js', 159 }, 160 youtube: { 161 api: 'https://www.youtube.com/iframe_api' 162 }, 163 soundcloud: { 164 api: 'https://w.soundcloud.com/player/api.js' 165 } 166 }, 167 // Custom control listeners 168 listeners: { 169 seek: null, 170 play: null, 171 pause: null, 172 restart: null, 173 rewind: null, 174 forward: null, 175 mute: null, 176 volume: null, 177 captions: null, 178 fullscreen: null 179 },
180 // Events to watch on HTML5 media elements 181 events: ['ready', 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied'], 182 // Logging 183 logPrefix: '[Plyr]' 184 }; 185 186 // Credits: http://paypal.github.io/accessible-html5-video-player/ 187 // Unfortunately, due to mixed support, UA sniffing is required 188 function _browserSniff() { 189 var ua = navigator.userAgent, 190 name = navigator.appName, 191 fullVersion = '' + parseFloat(navigator.appVersion), 192 majorVersion = parseInt(navigator.appVersion, 10), 193 nameOffset, 194 verOffset, 195 ix, 196 isIE = false, 197 isFirefox = false, 198 isChrome = false, 199 isSafari = false; 200 201 if ((navigator.appVersion.indexOf('Windows NT') !== -1) && (navigator.appVersion.indexOf('rv:11') !== -1)) { 202 // MSIE 11 203 isIE = true; 204 name = 'IE'; 205 fullVersion = '11'; 206 } else if ((verOffset = ua.indexOf('MSIE')) !== -1) { 207 // MSIE 208 isIE = true; 209 name = 'IE'; 210 fullVersion = ua.substring(verOffset + 5); 211 } else if ((verOffset = ua.indexOf('Chrome')) !== -1) { 212 // Chrome 213 isChrome = true; 214 name = 'Chrome'; 215 fullVersion = ua.substring(verOffset + 7); 216 } else if ((verOffset = ua.indexOf('Safari')) !== -1) { 217 // Safari 218 isSafari = true; 219 name = 'Safari'; 220 fullVersion = ua.substring(verOffset + 7); 221 if ((verOffset = ua.indexOf('Version')) !== -1) { 222 fullVersion = ua.substring(verOffset + 8); 223 } 224 } else if ((verOffset = ua.indexOf('Firefox')) !== -1) { 225 // Firefox 226 isFirefox = true; 227 name = 'Firefox'; 228 fullVersion = ua.substring(verOffset + 8); 229 } else if ((nameOffset = ua.lastIndexOf(' ') + 1) < (verOffset = ua.lastIndexOf('/'))) { 230 // In most other browsers, 'name/version' is at the end of userAgent 231 name = ua.substring(nameOffset, verOffset); 232 fullVersion = ua.substring(verOffset + 1); 233 234 if (name.toLowerCase() === name.toUpperCase()) { 235 name = navigator.appName; 236 } 237 } 238 239 // Trim the fullVersion string at semicolon/space if present 240 if ((ix = fullVersion.indexOf(';')) !== -1) { 241 fullVersion = fullVersion.substring(0, ix); 242 } 243 if ((ix = fullVersion.indexOf(' ')) !== -1) { 244 fullVersion = fullVersion.substring(0, ix); 245 } 246 247 // Get major version 248 majorVersion = parseInt('' + fullVersion, 10); 249 if (isNaN(majorVersion)) { 250 fullVersion = '' + parseFloat(navigator.appVersion); 251 majorVersion = parseInt(navigator.appVersion, 10); 252 } 253 254 // Return data 255 return { 256 name: name, 257 version: majorVersion, 258 isIE: isIE, 259 isFirefox: isFirefox, 260 isChrome: isChrome, 261 isSafari: isSafari, 262 isIos: /(iPad|iPhone|iPod)/g.test(navigator.platform), 263 isIphone: /(iPhone|iPod)/g.test(navigator.userAgent), 264 isTouch: 'ontouchstart' in document.documentElement 265 }; 266 } 267 268 // Check for mime type support against a player instance 269 // Credits: http://diveintohtml5.info/everything.html 270 // Related: http://www.leanbackplyr.com/test/h5mt.html 271 function _supportMime(plyr, mimeType) { 272 var media = plyr.media; 273 274 if (plyr.type === 'video') { 275 // Check type 276 switch (mimeType) { 277 case 'video/webm': 278 return !!(media.canPlayType && media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '')); 279 case 'video/mp4': 280 return !!(media.canPlayType && media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '')); 281 case 'video/ogg': 282 return !!(media.canPlayType && media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '')); 283 } 284 } else if (plyr.type === 'audio') { 285 // Check type 286 switch (mimeType) { 287 case 'audio/mpeg': 288 return !!(media.canPlayType && media.canPlayType('audio/mpeg;').replace(/no/, '')); 289 case 'audio/ogg': 290 return !!(media.canPlayType && media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '')); 291 case 'audio/wav': 292 return !!(media.canPlayType && media.canPlayType('audio/wav; codecs="1"').replace(/no/, '')); 293 } 294 } 295 296 // If we got this far, we're stuffed 297 return false;
298 } 299 300 // Inject a script 301 function _injectScript(source) { 302 if (document.querySelectorAll('script[src="' + source + '"]').length) { 303 return; 304 } 305 306 var tag = document.createElement('script'); 307 tag.src = source; 308 var firstScriptTag = document.getElementsByTagName('script')[0]; 309 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 310 } 311 312 // Element exists in an array 313 function _inArray(haystack, needle) { 314 return Array.prototype.indexOf && (haystack.indexOf(needle) !== -1); 315 } 316 317 // Replace all 318 function _replaceAll(string, find, replace) { 319 return string.replace(new RegExp(find.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g, '\\$1'), 'g'), replace); 320 } 321 322 // Wrap an element 323 function _wrap(elements, wrapper) { 324 // Convert `elements` to an array, if necessary. 325 if (!elements.length) { 326 elements = [elements]; 327 } 328 329 // Loops backwards to prevent having to clone the wrapper on the 330 // first element (see `child` below). 331 for (var i = elements.length - 1; i >= 0; i--) { 332 var child = (i > 0) ? wrapper.cloneNode(true) : wrapper; 333 var element = elements[i]; 334 335 // Cache the current parent and sibling. 336 var parent = element.parentNode; 337 var sibling = element.nextSibling; 338 339 // Wrap the element (is automatically removed from its current 340 // parent). 341 child.appendChild(element); 342 343 // If the element had a sibling, insert the wrapper before 344 // the sibling to maintain the HTML structure; otherwise, just 345 // append it to the parent. 346 if (sibling) { 347 parent.insertBefore(child, sibling); 348 } else { 349 parent.appendChild(child); 350 } 351 352 return child; 353 } 354 } 355 356 // Unwrap an element 357 // http://plainjs.com/javascript/manipulation/unwrap-a-dom-element-35/ 358 /*function _unwrap(wrapper) { 359 // Get the element's parent node 360 var parent = wrapper.parentNode; 361 362 // Move all children out of the element 363 while (wrapper.firstChild) { 364 parent.insertBefore(wrapper.firstChild, wrapper); 365 } 366 367 // Remove the empty element 368 parent.removeChild(wrapper); 369 }*/ 370 371 // Remove an element 372 function _remove(element) { 373 if (!element) { 374 return; 375 } 376 element.parentNode.removeChild(element); 377 } 378 379 // Prepend child 380 function _prependChild(parent, element) { 381 parent.insertBefore(element, parent.firstChild); 382 } 383 384 // Set attributes 385 function _setAttributes(element, attributes) { 386 for (var key in attributes) { 387 element.setAttribute(key, (_is.boolean(attributes[key]) && attributes[key]) ? '' : attributes[key]); 388 } 389 } 390 391 // Insert a HTML element 392 function _insertElement(type, parent, attributes) { 393 // Create a new <element> 394 var element = document.createElement(type); 395 396 // Set all passed attributes 397 _setAttributes(element, attributes); 398 399 // Inject the new element 400 _prependChild(parent, element); 401 } 402 403 // Get a classname from selector 404 function _getClassname(selector) { 405 return selector.replace('.', ''); 406 } 407 408 // Toggle class on an element 409 function _toggleClass(element, className, state) { 410 if (element) { 411 if (element.classList) { 412 element.classList[state ? 'add' : 'remove'](className); 413 } else { 414 var name = (' ' + element.className + ' ').replace(/\s+/g, ' ').replace(' ' + className + ' ', ''); 415 element.className = name + (state ? ' ' + className : ''); 416 } 417 } 418 } 419 420 // Has class name 421 function _hasClass(element, className) { 422 if (element) { 423 if (element.classList) { 424 return element.classList.contains(className); 425 } else { 426 return new RegExp('(\\s|^)' + className + '(\\s|$)').test(element.className); 427 } 428 } 429 return false; 430 } 431 432 // Element matches selector 433 function _matches(element, selector) { 434 var p = Element.prototype; 435 436 var f = p.matches || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || function (s) { 437 return [].indexOf.call(document.querySelectorAll(s), this) !== -1; 438 }; 439 440 return f.call(element, selector); 441 } 442 443 // Bind along with custom handler 444 function _proxyListener(element, eventName, userListener, defaultListener, useCapture) { 445 _on(element, eventName, function (event) { 446 if (userListener) { 447 userListener.apply(element, [event]); 448 } 449 defaultListener.apply(element, [event]); 450 }, useCapture); 451 } 452 453 // Toggle event listener 454 function _toggleListener(element, events, callback, toggle, useCapture) { 455 var eventList = events.split(' '); 456 457 // Whether the listener is a capturing listener or not 458 // Default to false 459 if (!_is.boolean(useCapture)) { 460 useCapture = false;
461 } 462 463 // If a nodelist is passed, call itself on each node 464 if (element instanceof NodeList) { 465 for (var x = 0; x < element.length; x++) { 466 if (element[x] instanceof Node) { 467 _toggleListener(element[x], arguments[1], arguments[2], arguments[3]); 468 } 469 } 470 return; 471 } 472 473 // If a single node is passed, bind the event listener 474 for (var i = 0; i < eventList.length; i++) { 475 element[toggle ? 'addEventListener' : 'removeEventListener'](eventList[i], callback, useCapture); 476 } 477 } 478 479 // Bind event 480 function _on(element, events, callback, useCapture) { 481 if (element) { 482 _toggleListener(element, events, callback, true, useCapture); 483 } 484 } 485 486 // Unbind event 487 /*function _off(element, events, callback, useCapture) { 488 if (element) { 489 _toggleListener(element, events, callback, false, useCapture); 490 } 491 }*/ 492 493 // Trigger event 494 function _event(element, type, bubbles, properties) { 495 // Bail if no element 496 if (!element || !type) { 497 return; 498 } 499 500 // Default bubbles to false 501 if (!_is.boolean(bubbles)) { 502 bubbles = false; 503 } 504 505 // Create and dispatch the event 506 var event = new CustomEvent(type, { 507 bubbles: bubbles, 508 detail: properties 509 }); 510 511 // Dispatch the event 512 element.dispatchEvent(event); 513 } 514 515 // Toggle aria-pressed state on a toggle button 516 // http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles 517 function _toggleState(target, state) { 518 // Bail if no target 519 if (!target) { 520 return; 521 } 522 523 // Get state 524 state = (_is.boolean(state) ? state : !target.getAttribute('aria-pressed')); 525 526 // Set the attribute on target 527 target.setAttribute('aria-pressed', state); 528 529 return state; 530 } 531 532 // Get percentage 533 function _getPercentage(current, max) { 534 if (current === 0 || max === 0 || isNaN(current) || isNaN(max)) { 535 return 0; 536 } 537 return ((current / max) * 100).toFixed(2); 538 } 539 540 // Deep extend/merge destination object with N more objects 541 // http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/ 542 // Removed call to arguments.callee (used explicit function name instead) 543 function _extend() { 544 // Get arguments 545 var objects = arguments; 546 547 // Bail if nothing to merge 548 if (!objects.length) { 549 return; 550 } 551 552 // Return first if specified but nothing to merge 553 if (objects.length === 1) { 554 return objects[0]; 555 } 556 557 // First object is the destination 558 var destination = Array.prototype.shift.call(objects), 559 length = objects.length; 560 561 // Loop through all objects to merge 562 for (var i = 0; i < length; i++) { 563 var source = objects[i]; 564 565 for (var property in source) { 566 if (source[property] && source[property].constructor && source[property].constructor === Object) { 567 destination[property] = destination[property] || {}; 568 _extend(destination[property], source[property]); 569 } else { 570 destination[property] = source[property]; 571 } 572 } 573 } 574 575 return destination; 576 } 577 578 // Check variable types 579 var _is = { 580 object: function (input) { 581 return input !== null && typeof (input) === 'object'; 582 }, 583 array: function (input) { 584 return input !== null && (typeof (input) === 'object' && input.constructor === Array); 585 }, 586 number: function (input) { 587 return input !== null && (typeof (input) === 'number' && !isNaN(input - 0) || (typeof input === 'object' && input.constructor === Number)); 588 }, 589 string: function (input) { 590 return input !== null && (typeof input === 'string' || (typeof input === 'object' && input.constructor === String)); 591 }, 592 boolean: function (input) { 593 return input !== null && typeof input === 'boolean'; 594 }, 595 nodeList: function (input) { 596 return input !== null && input instanceof NodeList; 597 }, 598 htmlElement: function (input) { 599 return input !== null && input instanceof HTMLElement; 600 }, 601 function: function (input) { 602 return input !== null && typeof input === 'function'; 603 }, 604 undefined: function (input) { 605 return input !== null && typeof input === 'undefined'; 606 } 607 }; 608 609 // Parse YouTube ID from url 610 function _parseYouTubeId(url) { 611 var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; 612 return (url.match(regex)) ? RegExp.$2 : url; 613 } 614 615 // Parse Vimeo ID from url 616 function _parseVimeoId(url) { 617 var regex = /^.*(vimeo.com\/|video\/)(\d+).*/; 618 return (url.match(regex)) ? RegExp.$2 : url; 619 } 620 621 // Fullscreen API 622 function _fullscreen() { 623 var fullscreen = { 624 supportsFullScreen: false, 625 isFullScreen: function () { 626 return false;
627 }, 628 requestFullScreen: function () { 629 }, 630 cancelFullScreen: function () { 631 }, 632 fullScreenEventName: '', 633 element: null, 634 prefix: '' 635 }, 636 browserPrefixes = 'webkit o moz ms khtml'.split(' '); 637 638 // Check for native support 639 if (!_is.undefined(document.cancelFullScreen)) { 640 fullscreen.supportsFullScreen = true; 641 } else { 642 // Check for fullscreen support by vendor prefix 643 for (var i = 0, il = browserPrefixes.length; i < il; i++) { 644 fullscreen.prefix = browserPrefixes[i]; 645 646 if (!_is.undefined(document[fullscreen.prefix + 'CancelFullScreen'])) { 647 fullscreen.supportsFullScreen = true; 648 break; 649 } else if (!_is.undefined(document.msExitFullscreen) && document.msFullscreenEnabled) { 650 // Special case for MS (when isn't it?) 651 fullscreen.prefix = 'ms'; 652 fullscreen.supportsFullScreen = true; 653 break; 654 } 655 } 656 } 657 658 // Update methods to do something useful 659 if (fullscreen.supportsFullScreen) { 660 // Yet again Microsoft awesomeness, 661 // Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes 662 fullscreen.fullScreenEventName = (fullscreen.prefix === 'ms' ? 'MSFullscreenChange' : fullscreen.prefix + 'fullscreenchange'); 663 664 fullscreen.isFullScreen = function (element) { 665 if (_is.undefined(element)) { 666 element = document.body; 667 } 668 switch (this.prefix) { 669 case '': 670 return document.fullscreenElement === element; 671 case 'moz': 672 return document.mozFullScreenElement === element; 673 default: 674 return document[this.prefix + 'FullscreenElement'] === element; 675 } 676 }; 677 fullscreen.requestFullScreen = function (element) { 678 if (_is.undefined(element)) { 679 element = document.body; 680 } 681 return (this.prefix === '') ? element.requestFullScreen() : element[this.prefix + (this.prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')](); 682 }; 683 fullscreen.cancelFullScreen = function () { 684 return (this.prefix === '') ? document.cancelFullScreen() : document[this.prefix + (this.prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')](); 685 }; 686 fullscreen.element = function () { 687 return (this.prefix === '') ? document.fullscreenElement : document[this.prefix + 'FullscreenElement']; 688 }; 689 } 690 691 return fullscreen; 692 } 693 694 // Local storage 695 var _storage = { 696 supported: (function () { 697 if (!('localStorage' in window)) { 698 return false; 699 } 700 701 // Try to use it (it might be disabled, e.g. user is in private/porn mode) 702 // see: https://github.com/sampotts/plyr/issues/131 703 try { 704 // Add test item 705 window.localStorage.setItem('___test', 'OK'); 706 707 // Get the test item 708 var result = window.localStorage.getItem('___test'); 709 710 // Clean up 711 window.localStorage.removeItem('___test'); 712 713 // Check if value matches 714 return (result === 'OK'); 715 } catch (e) { 716 return false; 717 } 718 719 return false; 720 })() 721 }; 722 723 // Player instance 724 function Plyr(media, config) { 725 var plyr = this, 726 timers = {}, 727 api; 728 729 // Set media 730 plyr.media = media; 731 var original = media.cloneNode(true); 732 733 // Trigger events, with plyr instance passed 734 function _triggerEvent(element, type, bubbles, properties) { 735 _event(element, type, bubbles, _extend({}, properties, { 736 plyr: api 737 })); 738 } 739 740 // Debugging 741 function _console(type, args) { 742 if (config.debug && window.console) { 743 args = Array.prototype.slice.call(args); 744 745 if (_is.string(config.logPrefix) && config.logPrefix.length) { 746 args.unshift(config.logPrefix); 747 } 748 749 console[type].apply(console, args); 750 } 751 } 752 753 var _log = function () { 754 _console('log', arguments) 755 }, 756 _warn = function () { 757 _console('warn', arguments) 758 }; 759 760 // Log config options 761 _log('Config', config); 762 763 // Get icon URL 764 function _getIconUrl() { 765 return { 766 url: config.iconUrl, 767 absolute: (config.iconUrl.indexOf("http") === 0) || plyr.browser.isIE 768 }; 769 } 770 771 // Build the default HTML 772 function _buildControls() { 773 // Create html array 774 var html = [], 775 iconUrl = _getIconUrl(), 776 iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + config.iconPrefix; 777 778 // Larger overlaid play button 779 if (_inArray(config.controls, 'play-large')) { 780 html.push( 781 '<button type="button" data-plyr="play" class="plyr__play-large">', 782 '<svg><use xlink:href="' + iconPath + '-play" /></svg>', 783 '<span class="plyr__sr-only">' + config.i18n.play + '</span>', 784 '</button>' 785 ); 786 } 787 788 html.push('<div class="plyr__controls">'); 789 790 // Restart button 791 if (_inArray(config.controls, 'restart')) { 792 html.push( 793 '<button type="button" data-plyr="restart">', 794 '<svg><use xlink:href="' + iconPath + '-restart" /></svg>', 795 '<span class="plyr__sr-only">' + config.i18n.restart + '</span>', 796 '</button>' 797 ); 798 } 799 800 // Rewind button 801 if (_inArray(config.controls, 'rewind')) { 802 html.push( 803 '<button type="button" data-plyr="rewind">', 804 '<svg><use xlink:href="' + iconPath + '-rewind" /></svg>', 805 '<span class="plyr__sr-only">' + config.i18n.rewind + '</span>', 806 '</button>' 807 ); 808 } 809 810 // Play Pause button 811 // TODO: This should be a toggle button really? 812 if (_inArray(config.controls, 'play')) { 813 html.push( 814 '<button type="button" data-plyr="play">', 815 '<svg><use xlink:href="' + iconPath + '-play" /></svg>', 816 '<span class="plyr__sr-only">' + config.i18n.play + '</span>', 817 '</button>', 818 '<button type="button" data-plyr="pause">', 819 '<svg><use xlink:href="' + iconPath + '-pause" /></svg>', 820 '<span class="plyr__sr-only">' + config.i18n.pause + '</span>', 821 '</button>' 822 ); 823 } 824 825 // Fast forward button 826 if (_inArray(config.controls, 'fast-forward')) { 827 html.push( 828 '<button type="button" data-plyr="fast-forward">', 829 '<svg><use xlink:href="' + iconPath + '-fast-forward" /></svg>', 830 '<span class="plyr__sr-only">' + config.i18n.forward + '</span>', 831 '</button>' 832 ); 833 } 834 835 // Progress 836 if (_inArray(config.controls, 'progress')) { 837 // Create progress 838 html.push('<span class="plyr__progress">', 839 '<label for="seek{id}" class="plyr__sr-only">Seek</label>', 840 '<input id="seek{id}" class="plyr__progress--seek" type="range" min="0" max="100" step="0.1" value="
8400" data-plyr="seek">', 841 '<progress class="plyr__progress--played" max="100" value="0" role="presentation"></progress>', 842 '<progress class="plyr__progress--buffer" max="100" value="0">', 843 '<span>0</span>% ' + config.i18n.buffered, 844 '</progress>'); 845 846 // Seek tooltip 847 if (config.tooltips.seek) { 848 html.push('<span class="plyr__tooltip">00:00</span>'); 849 } 850 851 // Close 852 html.push('</span>'); 853 } 854 855 // Media current time display 856 if (_inArray(config.controls, 'current-time')) { 857 html.push( 858 '<span class="plyr__time">', 859 '<span class="plyr__sr-only">' + config.i18n.currentTime + '</span>', 860 '<span class="plyr__time--current">00:00</span>', 861 '</span>' 862 ); 863 } 864 865 // Media duration display 866 if (_inArray(config.controls, 'duration')) { 867 html.push( 868 '<span class="plyr__time">', 869 '<span class="plyr__sr-only">' + config.i18n.duration + '</span>', 870 '<span class="plyr__time--duration">00:00</span>', 871 '</span>' 872 ); 873 } 874 875 // Toggle mute button 876 if (_inArray(config.controls, 'mute')) { 877 html.push( 878 '<button type="button" data-plyr="mute">', 879 '<svg class="icon--muted"><use xlink:href="' + iconPath + '-muted" /></svg>', 880 '<svg><use xlink:href="' + iconPath + '-volume" /></svg>', 881 '<span class="plyr__sr-only">' + config.i18n.toggleMute + '</span>', 882 '</button>' 883 ); 884 } 885 886 // Volume range control 887 if (_inArray(config.controls, 'volume')) { 888 html.push( 889 '<span class="plyr__volume">', 890 '<label for="volume{id}" class="plyr__sr-only">' + config.i18n.volume + '</label>', 891 '<input id="volume{id}" class="plyr__volume--input" type="range" min="' + config.volumeMin + '" max="' + config.volumeMax + '" value="' + config.volume + '" data-plyr="volume">', 892 '<progress class="plyr__volume--display" max="' + config.volumeMax + '" value="' + config.volumeMin + '" role="presentation"></progress>', 893 '</span>' 894 ); 895 } 896 897 // Toggle captions button 898 if (_inArray(config.controls, 'captions')) { 899 html.push( 900 '<button type="button" data-plyr="captions">', 901 '<svg class="icon--captions-on"><use xlink:href="' + iconPath + '-captions-on" /></svg>', 902 '<svg><use xlink:href="' + iconPath + '-captions-off" /></svg>', 903 '<span class="plyr__sr-only">' + config.i18n.toggleCaptions + '</span>', 904 '</button>' 905 ); 906 } 907 908 // Toggle fullscreen button 909 if (_inArray(config.controls, 'fullscreen')) { 910 html.push( 911 '<button type="button" data-plyr="fullscreen">', 912 '<svg class="icon--exit-fullscreen"><use xlink:href="' + iconPath + '-exit-fullscreen" /></svg>', 913 '<svg><use xlink:href="' + iconPath + '-enter-fullscreen" /></svg>', 914 '<span class="plyr__sr-only">' + config.i18n.toggleFullscreen + '</span>', 915 '</button>' 916 ); 917 } 918 919 // Close everything 920 html.push('</div>'); 921 922 return html.join(''); 923 } 924 925 // Setup fullscreen 926 function _setupFullscreen() { 927 if (!plyr.supported.full) { 928 return; 929 } 930 931 if ((plyr.type !== 'audio' || config.fullscreen.allowAudio) && config.fullscreen.enabled) { 932 // Check for native support 933 var nativeSupport = fullscreen.supportsFullScreen; 934 935 if (nativeSupport || (config.fullscreen.fallback && !_inFrame())) { 936 _log((nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled'); 937 938 // Add styling hook 939 _toggleClass(plyr.container, config.classes.fullscreen.enabled, true); 940 } else { 941 _log('Fullscreen not supported and fallback disabled'); 942 } 943 944 // Toggle state 945 if (plyr.buttons && plyr.buttons.fullscreen) { 946 _toggleState(plyr.buttons.fullscreen, false); 947 } 948 949 // Setup focus trap 950 _focusTrap(); 951 } 952 } 953 954 // Setup captions 955 function _setupCaptions() { 956 // Bail if not HTML5 video 957 if (plyr.type !== 'video') { 958 return; 959 } 960 961 // Inject the container 962 if (!_getElement(config.selectors.captions)) { 963 plyr.videoContainer.insertAdjacentHTML('afterbegin', '<div class="' + _getClassname(config.selectors.captions) + '"></div>'); 964 } 965 966 // Determine if HTML5 textTracks is supported 967 plyr.usingTextTracks = false;
968 if (plyr.media.textTracks) { 969 plyr.usingTextTracks = true; 970 } 971 972 // Get URL of caption file if exists 973 var captionSrc = '', 974 kind, 975 children = plyr.media.childNodes; 976 977 for (var i = 0; i < children.length; i++) { 978 if (children[i].nodeName.toLowerCase() === 'track') { 979 kind = children[i].kind; 980 if (kind === 'captions' || kind === 'subtitles') { 981 captionSrc = children[i].getAttribute('src'); 982 } 983 } 984 } 985 986 // Record if caption file exists or not 987 plyr.captionExists = true; 988 if (captionSrc === '') { 989 plyr.captionExists = false; 990 _log('No caption track found'); 991 } else { 992 _log('Caption track found; URI: ' + captionSrc); 993 } 994 995 // If no caption file exists, hide container for caption text 996 if (!plyr.captionExists) { 997 _toggleClass(plyr.container, config.classes.captions.enabled); 998 } else { 999 // Turn off native caption rendering to avoid double captions 1000 // This doesn't seem to work in Safari 7+, so the <track> elements are removed from the dom below 1001 var tracks = plyr.media.textTracks; 1002 for (var x = 0; x < tracks.length; x++) { 1003 tracks[x].mode = 'hidden'; 1004 } 1005 1006 // Enable UI 1007 _showCaptions(plyr); 1008 1009 // Disable unsupported browsers than report false positive 1010 // Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1033144 1011 if ((plyr.browser.isIE && plyr.browser.version >= 10) || 1012 (plyr.browser.isFirefox && plyr.browser.version >= 31)) { 1013 1014 // Debugging 1015 _log('Detected browser with known TextTrack issues - using manual fallback'); 1016 1017 // Set to false so skips to 'manual' captioning 1018 plyr.usingTextTracks = false; 1019 } 1020 1021 // Rendering caption tracks 1022 // Native support required - http://caniuse.com/webvtt 1023 if (plyr.usingTextTracks) {
1024 _log('TextTracks supported'); 1025 1026 for (var y = 0; y < tracks.length; y++) { 1027 var track = tracks[y]; 1028 1029 if (track.kind === 'captions' || track.kind === 'subtitles') { 1030 _on(track, 'cuechange', function () { 1031 // Display a cue, if there is one 1032 if (this.activeCues[0] && 'text' in this.activeCues[0]) { 1033 _setCaption(this.activeCues[0].getCueAsHTML()); 1034 } else { 1035 _setCaption(); 1036 } 1037 }); 1038 } 1039 } 1040 } else { 1041 // Caption tracks not natively supported 1042 _log('TextTracks not supported so rendering captions manually'); 1043 1044 // Render captions from array at appropriate time 1045 plyr.currentCaption = ''; 1046 plyr.captions = []; 1047 1048 if (captionSrc !== '') { 1049 // Create XMLHttpRequest Object 1050 var xhr = new XMLHttpRequest(); 1051 1052 xhr.onreadystatechange = function () { 1053 if (xhr.readyState === 4) { 1054 if (xhr.status === 200) { 1055 var captions = [], 1056 caption, 1057 req = xhr.responseText; 1058 1059 //According to webvtt spec, line terminator consists of one of the following 1060 // CRLF (U+000D U+000A), LF (U+000A) or CR (U+000D) 1061 var lineSeparator = '\r\n'; 1062 if (req.indexOf(lineSeparator + lineSeparator) === -1) { 1063 if (req.indexOf('\r\r') !== -1) { 1064 lineSeparator = '\r'; 1065 } else { 1066 lineSeparator = '\n'; 1067 } 1068 } 1069 1070 captions = req.split(lineSeparator + lineSeparator); 1071 1072 for (var r = 0; r < captions.length; r++) { 1073 caption = captions[r]; 1074 plyr.captions[r] = []; 1075 1076 // Get the parts of the captions 1077 var parts = caption.split(lineSeparator), 1078 index = 0; 1079 1080 // Incase caption numbers are added 1081 if (parts[index].indexOf(":") === -1) { 1082 index = 1; 1083 } 1084 1085 plyr.captions[r] = [parts[index], parts[index + 1]]; 1086 } 1087 1088 // Remove first element ('VTT') 1089 plyr.captions.shift(); 1090 1091 _log('Successfully loaded the caption file via AJAX'); 1092 } else { 1093 _warn(config.logPrefix + 'There was a problem loading the caption file via AJAX'); 1094 } 1095 } 1096 }; 1097 1098 xhr.open('get', captionSrc, true); 1099 1100 xhr.send(); 1101 } 1102 } 1103 } 1104 } 1105 1106 // Set the current caption 1107 function _setCaption(caption) { 1108 /* jshint unused:false */ 1109 var container = _getElement(config.selectors.captions), 1110 content = document.createElement('span'); 1111 1112 // Empty the container 1113 container.innerHTML = ''; 1114 1115 // Default to empty 1116 if (_is.undefined(caption)) { 1117 caption = ''; 1118 } 1119 1120 // Set the span content 1121 if (_is.string(caption)) { 1122 content.innerHTML = caption.trim(); 1123 } else { 1124 content.appendChild(caption); 1125 } 1126 1127 // Set new caption text 1128 container.appendChild(content); 1129 1130 // Force redraw (for Safari) 1131 var redraw = container.offsetHeight; 1132 } 1133 1134 // Captions functions 1135 // Seek the manual caption time and update UI 1136 function _seekManualCaptions(time) { 1137 // Utilities for caption time codes 1138 function _timecodeCommon(tc, pos) { 1139 var tcpair = []; 1140 tcpair = tc.split(' --> '); 1141 for (var i = 0; i < tcpair.length; i++) { 1142 // WebVTT allows for extra meta data after the timestamp line 1143 // So get rid of this if it exists 1144 tcpair[i] = tcpair[i].replace(/(\d+:\d+:\d+\.\d+).*/, "$1"); 1145 } 1146 return _subTcSecs(tcpair[pos]); 1147 } 1148 1149 function _timecodeMin(tc) { 1150 return _timecodeCommon(tc, 0); 1151 } 1152 1153 function _timecodeMax(tc) { 1154 return _timecodeCommon(tc, 1); 1155 } 1156 1157 function _subTcSecs(tc) { 1158 if (tc === null || tc === undefined) { 1159 return 0; 1160 } else { 1161 var tc1 = [], 1162 tc2 = [], 1163 seconds; 1164 tc1 = tc.split(','); 1165 tc2 = tc1[0].split(':'); 1166 seconds = Math.floor(tc2[0] * 60 * 60) + Math.floor(tc2[1] * 60) + Math.floor(tc2[2]); 1167 return seconds; 1168 } 1169 } 1170 1171 // If it's not video, or we're using textTracks, bail. 1172 if (plyr.usingTextTracks || plyr.type !== 'video' || !plyr.supported.full) { 1173 return; 1174 } 1175 1176 // Reset subcount 1177 plyr.subcount = 0; 1178 1179 // Check time is a number, if not use currentTime 1180 // IE has a bug where currentTime doesn't go to 0 1181 // https://twitter.com/Sam_Potts/status/573715746506731521 1182 time = _is.number(time) ? time : plyr.media.currentTime; 1183 1184 // If there's no subs available, bail 1185 if (!plyr.captions[plyr.subcount]) { 1186 return; 1187 } 1188
1189 while (_timecodeMax(plyr.captions[plyr.subcount][0]) < time.toFixed(1)) { 1190 plyr.subcount++; 1191 if (plyr.subcount > plyr.captions.length - 1) { 1192 plyr.subcount = plyr.captions.length - 1; 1193 break; 1194 } 1195 } 1196 1197 // Check if the next caption is in the current time range 1198 if (plyr.media.currentTime.toFixed(1) >= _timecodeMin(plyr.captions[plyr.subcount][0]) && 1199 plyr.media.currentTime.toFixed(1) <= _timecodeMax(plyr.captions[plyr.subcount][0])) { 1200 plyr.currentCaption = plyr.captions[plyr.subcount][1]; 1201 1202 // Render the caption 1203 _setCaption(plyr.currentCaption); 1204 } else { 1205 _setCaption(); 1206 } 1207 } 1208 1209 // Display captions container and button (for initialization) 1210 function _showCaptions() { 1211 // If there's no caption toggle, bail 1212 if (!plyr.buttons.captions) { 1213 return; 1214 } 1215 1216 _toggleClass(plyr.container, config.classes.captions.enabled, true); 1217 1218 // Try to load the value from storage 1219 var active = plyr.storage.captionsEnabled; 1220 1221 // Otherwise fall back to the default config 1222 if (!_is.boolean(active)) { 1223 active = config.captions.defaultActive; 1224 } 1225 1226 if (active) { 1227 _toggleClass(plyr.container, config.classes.captions.active, true); 1228 _toggleState(plyr.buttons.captions, true); 1229 } 1230 } 1231 1232 // Find all elements 1233 function _getElements(selector) { 1234 return plyr.container.querySelectorAll(selector); 1235 } 1236 1237 // Find a single element 1238 function _getElement(selector) { 1239 return _getElements(selector)[0]; 1240 } 1241 1242 // Determine if we're in an iframe 1243 function _inFrame() { 1244 try { 1245 return window.self !== window.top; 1246 } catch (e) { 1247 return true; 1248 } 1249 } 1250 1251 // Trap focus inside container 1252 function _focusTrap() { 1253 var tabbables = _getElements('input:not([disabled]), button:not([disabled])'), 1254 first = tabbables[0], 1255 last = tabbables[tabbables.length - 1]; 1256 1257 function _checkFocus(event) { 1258 // If it is TAB 1259 if (event.which === 9 && plyr.isFullscreen) { 1260 if (event.target === last && !event.shiftKey) { 1261 // Move focus to first element that can be tabbed if Shift isn't used 1262 event.preventDefault(); 1263 first.focus(); 1264 } else if (event.target === first && event.shiftKey) { 1265 // Move focus to last element that can be tabbed if Shift is used 1266 event.preventDefault(); 1267 last.focus(); 1268 } 1269 } 1270 } 1271 1272 // Bind the handler 1273 _on(plyr.container, 'keydown', _checkFocus); 1274 } 1275 1276 // Add elements to HTML5 media (source, tracks, etc) 1277 function _insertChildElements(type, attributes) { 1278 if (_is.string(attributes)) { 1279 _insertElement(type, plyr.media, {src: attributes}); 1280 } else if (attributes.constructor === Array) { 1281 for (var i = attributes.length - 1; i >= 0; i--) { 1282 _insertElement(type, plyr.media, attributes[i]); 1283 } 1284 } 1285 } 1286 1287 // Insert controls 1288 function _injectControls() { 1289 // Sprite 1290 if (config.loadSprite) { 1291 var iconUrl = _getIconUrl(); 1292 1293 // Only load external sprite using AJAX 1294 if (iconUrl.absolute) { 1295 _log('AJAX loading absolute SVG sprite' + (plyr.browser.isIE ? ' (due to IE)' : '')); 1296 loadSprite(iconUrl.url, "sprite-plyr"); 1297 } else { 1298 _log('Sprite will be used as external resource directly'); 1299 } 1300 } 1301 1302 // Make a copy of the html 1303 var html = config.html; 1304 1305 // Insert custom video controls 1306 _log('Injecting custom controls'); 1307 1308 // If no controls are specified, create default 1309 if (!html) { 1310 html = _buildControls(); 1311 } 1312 1313 // Replace seek time instances 1314 html = _replaceAll(html, '{seektime}', config.seekTime); 1315 1316 // Replace all id references with random numbers 1317 html = _replaceAll(html, '{id}', Math.floor(Math.random() * (10000))); 1318 1319 // Controls container 1320 var target; 1321 1322 // Inject to custom location 1323 if (_is.string(config.selectors.controls.container)) { 1324 target = document.querySelector(config.selectors.controls.container); 1325 } 1326 1327 // Inject into the container by default 1328 if (!_is.htmlElement(target)) { 1329 target = plyr.container 1330 } 1331 1332 // Inject controls HTML 1333 target.insertAdjacentHTML('beforeend', html); 1334 1335 // Setup tooltips 1336 if (config.tooltips.controls) { 1337 var labels = _getElements([config.selectors.controls.wrapper, ' ', config.selectors.labels, ' .', config.classes.hidden].join('')); 1338 1339 for (var i = labels.length - 1; i >= 0; i--) { 1340 var label = labels[i]; 1341 1342 _toggleClass(label, config.classes.hidden, false); 1343 _toggleClass(label, config.classes.tooltip, true); 1344 } 1345 } 1346 } 1347 1348 // Find the UI controls and store references 1349 function _findElements() {
1350 try { 1351 plyr.controls = _getElement(config.selectors.controls.wrapper); 1352 1353 // Buttons 1354 plyr.buttons = {}; 1355 plyr.buttons.seek = _getElement(config.selectors.buttons.seek); 1356 plyr.buttons.play = _getElements(config.selectors.buttons.play); 1357 plyr.buttons.pause = _getElement(config.selectors.buttons.pause); 1358 plyr.buttons.restart = _getElement(config.selectors.buttons.restart); 1359 plyr.buttons.rewind = _getElement(config.selectors.buttons.rewind); 1360 plyr.buttons.forward = _getElement(config.selectors.buttons.forward); 1361 plyr.buttons.fullscreen = _getElement(config.selectors.buttons.fullscreen); 1362 1363 // Inputs 1364 plyr.buttons.mute = _getElement(config.selectors.buttons.mute); 1365 plyr.buttons.captions = _getElement(config.selectors.buttons.captions); 1366 1367 // Progress 1368 plyr.progress = {}; 1369 plyr.progress.container = _getElement(config.selectors.progress.container); 1370 1371 // Progress - Buffering 1372 plyr.progress.buffer = {}; 1373 plyr.progress.buffer.bar = _getElement(config.selectors.progress.buffer); 1374 plyr.progress.buffer.text = plyr.progress.buffer.bar && plyr.progress.buffer.bar.getElementsByTagName('span')[0]; 1375 1376 // Progress - Played 1377 plyr.progress.played = _getElement(config.selectors.progress.played); 1378 1379 // Seek tooltip 1380 plyr.progress.tooltip = plyr.progress.container && plyr.progress.container.querySelector('.' + config.classes.tooltip); 1381 1382 // Volume 1383 plyr.volume = {}; 1384 plyr.volume.input = _getElement(config.selectors.volume.input); 1385 plyr.volume.display = _getElement(config.selectors.volume.display); 1386 1387 // Timing 1388 plyr.duration = _getElement(config.selectors.duration); 1389 plyr.currentTime = _getElement(config.selectors.currentTime); 1390 plyr.seekTime = _getElements(config.selectors.seekTime); 1391 1392 return true; 1393 } catch (e) { 1394 _warn('It looks like there is a problem with your controls HTML'); 1395 1396 // Restore native video controls 1397 _toggleNativeControls(true); 1398 1399 return false; 1400 } 1401 } 1402 1403 // Toggle style hook 1404 function _toggleStyleHook() { 1405 _toggleClass(plyr.container, config.selectors.container.replace('.', ''), plyr.supported.full); 1406 } 1407 1408 // Toggle native controls 1409 function _toggleNativeControls(toggle) { 1410 if (toggle && _inArray(config.types.html5, plyr.type)) { 1411 plyr.media.setAttribute('controls', ''); 1412 } else { 1413 plyr.media.removeAttribute('controls'); 1414 } 1415 } 1416 1417 // Setup aria attribute for play and iframe title 1418 function _setTitle(iframe) { 1419 // Find the current text 1420 var label = config.i18n.play; 1421 1422 // If there's a media title set, use that for the label 1423 if (_is.string(config.title) && config.title.length) { 1424 label += ', ' + config.title; 1425 1426 // Set container label 1427 plyr.container.setAttribute('aria-label', config.title); 1428 } 1429 1430 // If there's a play button, set label 1431 if (plyr.supported.full && plyr.buttons.play) { 1432 for (var i = plyr.buttons.play.length - 1; i >= 0; i--) { 1433 plyr.buttons.play[i].setAttribute('aria-label', label); 1434 } 1435 } 1436 1437 // Set iframe title 1438 // https://github.com/sampotts/plyr/issues/124 1439 if (_is.htmlElement(iframe)) { 1440 iframe.setAttribute('title', config.i18n.frameTitle.replace('{title}', config.title)); 1441 } 1442 } 1443 1444 // Setup localStorage 1445 function _setupStorage() { 1446 var value = null; 1447 plyr.storage = {}; 1448 1449 // Bail if we don't have localStorage support or it's disabled 1450 if (!_storage.supported || !config.storage.enabled) { 1451 return; 1452 } 1453 1454 // Clean up old volume 1455 // https://github.com/sampotts/plyr/issues/171 1456 window.localStorage.removeItem('plyr-volume'); 1457 1458 // load value from the current key 1459 value = window.localStorage.getItem(config.storage.key); 1460 1461 if (!value) { 1462 // Key wasn't set (or had been cleared), move along 1463 return; 1464 } else if (/^\d+(\.\d+)?$/.test(value)) { 1465 // If value is a number, it's probably volume from an older 1466 // version of plyr. See: https://github.com/sampotts/plyr/pull/313 1467 // Update the key to be JSON 1468 _updateStorage({volume: parseFloat(value)}); 1469 } else { 1470 // Assume it's JSON from this or a later version of plyr 1471 plyr.storage = JSON.parse(value); 1472 } 1473 } 1474 1475 // Save a value back to local storage 1476 function _updateStorage(value) { 1477 // Bail if we don't have localStorage support or it's disabled 1478 if (!_storage.supported || !config.storage.enabled) { 1479 return; 1480 } 1481 1482 // Update the working copy of the values 1483 _extend(plyr.storage, value); 1484 1485 // Update storage 1486 window.localStorage.setItem(config.storage.key, JSON.stringify(plyr.storage)); 1487 } 1488 1489 // Setup media 1490 function _setupMedia() { 1491 // If there's no media, bail 1492 if (!plyr.media) { 1493 _warn('No media element found!'); 1494 return; 1495 } 1496 1497 if (plyr.supported.full) { 1498 // Add type class 1499 _toggleClass(plyr.container, config.classes.type.replace('{0}', plyr.type), true); 1500 1501 // Add video class for embeds 1502 // This will require changes if audio embeds are added 1503 if (_inArray(config.types.embed, plyr.type)) { 1504 _toggleClass(plyr.container, config.classes.type.replace('{0}', 'video'), true); 1505 } 1506 1507 // If there's no autoplay attribute, assume the video is stopped and add state class 1508 _toggleClass(plyr.container, config.classes.stopped, config.auto
1508play); 1509 1510 // Add iOS class 1511 _toggleClass(plyr.container, config.classes.isIos, plyr.browser.isIos); 1512 1513 // Add touch class 1514 _toggleClass(plyr.container, config.classes.isTouch, plyr.browser.isTouch); 1515 1516 // Inject the player wrapper 1517 if (plyr.type === 'video') { 1518 // Create the wrapper div 1519 var wrapper = document.createElement('div'); 1520 wrapper.setAttribute('class', config.classes.videoWrapper); 1521 1522 // Wrap the video in a container 1523 _wrap(plyr.media, wrapper); 1524 1525 // Cache the container 1526 plyr.videoContainer = wrapper; 1527 } 1528 } 1529 1530 // Embeds 1531 if (_inArray(config.types.embed, plyr.type)) { 1532 _setupEmbed(); 1533 } 1534 } 1535 1536 // Setup YouTube/Vimeo 1537 function _setupEmbed() { 1538 var container = document.createElement('div'), 1539 mediaId, 1540 id = plyr.type + '-' + Math.floor(Math.random() * (10000)); 1541 1542 // Parse IDs from URLs if supplied 1543 switch (plyr.type) { 1544 case 'youtube': 1545 mediaId = _parseYouTubeId(plyr.embedId); 1546 break; 1547 1548 case 'vimeo': 1549 mediaId = _parseVimeoId(plyr.embedId); 1550 break; 1551 1552 default: 1553 mediaId = plyr.embedId; 1554 } 1555 1556 // Remove old containers 1557 var containers = _getElements('[id^="' + plyr.type + '-"]'); 1558 for (var i = containers.length - 1; i >= 0; i--) { 1559 _remove(containers[i]); 1560 } 1561 1562 // Add embed class for responsive 1563 _toggleClass(plyr.media, config.classes.videoWrapper, true); 1564 _toggleClass(plyr.media, config.classes.embedWrapper, true); 1565 1566 if (plyr.type === 'youtube') { 1567 // Create the YouTube container 1568 plyr.media.appendChild(container); 1569 1570 // Set ID 1571 container.setAttribute('id', id); 1572 1573 // Setup API 1574 if (_is.object(window.YT)) { 1575 _youTubeReady(mediaId, container); 1576 } else { 1577 // Load the API 1578 _injectScript(config.urls.youtube.api); 1579 1580 // Setup callback for the API 1581 window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; 1582 1583 // Add to queue 1584 window.onYouTubeReadyCallbacks.push(function () { 1585 _youTubeReady(mediaId, container); 1586 }); 1587 1588 // Set callback to process queue 1589 window.onYouTubeIframeAPIReady = function () { 1590 window.onYouTubeReadyCallbacks.forEach(function (callback) { 1591 callback(); 1592 }); 1593 }; 1594 } 1595 } else if (plyr.type === 'vimeo') { 1596 // Vimeo needs an extra div to hide controls on desktop (which has full support) 1597 if (plyr.supported.full) { 1598 plyr.media.appendChild(container); 1599 } else { 1600 container = plyr.media; 1601 } 1602 1603 // Set ID 1604 container.setAttribute('id', id); 1605 1606 // Load the API if not already 1607 if (!_is.object(window.Vimeo)) { 1608 _injectScript(config.urls.vimeo.api); 1609 1610 // Wait for fragaloop load 1611 var vimeoTimer = window.setInterval(function () { 1612 if (_is.object(window.Vimeo)) { 1613 window.clearInterval(vimeoTimer); 1614 _vimeoReady(mediaId, container); 1615 } 1616 }, 50); 1617 } else { 1618 _vimeoReady(mediaId, container); 1619 } 1620 } else if (plyr.type === 'soundcloud') { 1621 // TODO: Currently unsupported and undocumented 1622 // Inject the iframe 1623 var soundCloud = document.createElement('iframe'); 1624 1625 // Watch for iframe load 1626 soundCloud.loaded = false; 1627 _on(soundCloud, 'load', function () { 1628 soundCloud.loaded = true; 1629 }); 1630 1631 _setAttributes(soundCloud, { 1632 'src': 'https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/' + mediaId, 1633 'id': id 1634 }); 1635 1636 container.appendChild(soundCloud); 1637 plyr.media.appendChild(container); 1638 1639 // Load the API if not already 1640 if (!window.SC) { 1641 _injectScript(config.urls.soundcloud.api); 1642 } 1643 1644 // Wait for SC load 1645 var soundCloudTimer = window.setInterval(function () { 1646 if (window.SC && soundCloud.loaded) { 1647 window.clearInterval(soundCloudTimer); 1648 _soundcloudReady.call(soundCloud); 1649 } 1650 }, 50); 1651 } 1652 } 1653 1654 // When embeds are ready 1655 function _embedReady() { 1656 // Setup the UI and call ready if full support 1657 if (plyr.supported.full) { 1658 _setupInterface(); 1659 _ready(); 1660 } 1661 1662 // Set title 1663 _setTitle(_getElement('iframe')); 1664 } 1665 1666 // Handle YouTube API ready 1667 function _youTubeReady(videoId, container) { 1668 // Setup instance 1669 // https://developers.google.com/youtube/iframe_api_reference 1670 plyr.embed = new window.YT.Player(container.id, { 1671 videoId: videoId, 1672 playerVars: {
1673 autoplay: (config.autoplay ? 1 : 0), 1674 controls: (plyr.supported.full ? 0 : 1), 1675 rel: 0, 1676 showinfo: 0, 1677 iv_load_policy: 3, 1678 cc_load_policy: (config.captions.defaultActive ? 1 : 0), 1679 cc_lang_pref: 'en', 1680 wmode: 'transparent', 1681 modestbranding: 1, 1682 disablekb: 1, 1683 origin: '*' // https://code.google.com/p/gdata-issues/issues/detail?id=5788#c45 1684 }, 1685 events: { 1686 'onError': function (event) { 1687 _triggerEvent(plyr.container, 'error', true, { 1688 code: event.data, 1689 embed: event.target 1690 }); 1691 }, 1692 'onReady': function (event) { 1693 // Get the instance 1694 var instance = event.target; 1695 1696 // Create a faux HTML5 API using the YouTube API 1697 plyr.media.play = function () { 1698 instance.playVideo(); 1699 plyr.media.paused = false; 1700 }; 1701 plyr.media.pause = function () { 1702 instance.pauseVideo(); 1703 plyr.media.paused = true; 1704 }; 1705 plyr.media.stop = function () { 1706 instance.stopVideo(); 1707 plyr.media.paused = true; 1708 }; 1709 plyr.media.duration = instance.getDuration(); 1710 plyr.media.paused = true; 1711 plyr.media.currentTime = 0; 1712 plyr.media.muted = instance.isMuted(); 1713 1714 // Set title 1715 config.title = instance.getVideoData().title; 1716 1717 // Set the tabindex 1718 if (plyr.supported.full) { 1719 plyr.media.querySelector('iframe').setAttribute('tabindex', '-1'); 1720 } 1721 1722 // Update UI 1723 _embedReady(); 1724 1725 // Trigger timeupdate 1726 _triggerEvent(plyr.media, 'timeupdate'); 1727 1728 // Trigger timeupdate 1729 _triggerEvent(plyr.media, 'durationchange'); 1730 1731 // Reset timer 1732 window.clearInterval(timers.buffering); 1733 1734 // Setup buffering 1735 timers.buffering = window.setInterval(function () { 1736 // Get loaded % from YouTube 1737 plyr.media.buffered = instance.getVideoLoadedFraction(); 1738 1739 // Trigger progress only when we actually buffer something 1740 if (plyr.media.lastBuffered === null || plyr.media.lastBuffered < plyr.media.buffered) { 1741 _triggerEvent(plyr.media, 'progress'); 1742 } 1743 1744 // Set last buffer point 1745 plyr.media.lastBuffered = plyr.media.buffered; 1746 1747 // Bail if we're at 100% 1748 if (plyr.media.buffered === 1) { 1749 window.clearInterval(timers.buffering); 1750 1751 // Trigger event 1752 _triggerEvent(plyr.media, 'canplaythrough'); 1753 } 1754 }, 200); 1755 }, 1756 'onStateChange': function (event) { 1757 // Get the instance 1758 var instance = event.target; 1759 1760 // Reset timer 1761 window.clearInterval(timers.playing); 1762 1763 // Handle events 1764 // -1 Unstarted 1765 // 0 Ended 1766 // 1 Playing 1767 // 2 Paused 1768 // 3 Buffering 1769 // 5 Video cued 1770 switch (event.data) { 1771 case 0: 1772 plyr.media.paused = true; 1773 _triggerEvent(plyr.media, 'ended'); 1774 break; 1775 1776 case 1: 1777 plyr.media.paused = false; 1778 1779 // If we were seeking, fire seeked event 1780 if (plyr.media.seeking) { 1781 _triggerEvent(plyr.media, 'seeked'); 1782 } 1783 1784 plyr.media.seeking = false; 1785 _triggerEvent(plyr.media, 'play'); 1786 _triggerEvent(plyr.media, 'playing'); 1787 1788 // Poll to get playback progress 1789 timers.playing = window.setInterval(function () { 1790 // Set the current time 1791 plyr.media.currentTime = instance.getCurrentTime(); 1792 1793 // Trigger timeupdate 1794 _triggerEvent(plyr.media, 'timeupdate'); 1795 }, 100); 1796 1797 // Check duration again due to YouTube bug 1798 // https://github.com/sampotts/plyr/issues/374 1799 // https://code.google.com/p/gdata-issues/issues/detail?id=8690 1800 if (plyr.media.duration !== instance.getDuration()) { 1801 plyr.media.duration = instance.getDuration(); 1802 _triggerEvent(plyr.media, 'durationchange'); 1803 } 1804 1805 break; 1806 1807 case 2: 1808 plyr.media.paused = true; 1809 _triggerEvent(plyr.media, 'pause'); 1810 break; 1811 } 1812 1813 _triggerEvent(plyr.container, 'statechange', false, { 1814 code: event.data 1815 }); 1816 } 1817 } 1818 }); 1819 } 1820 1821 // Vimeo ready 1822 function _vimeoReady(mediaId, container) { 1823 // Setup instance 1824 // https://github.com/vimeo/player.js 1825 plyr.embed = new window.Vimeo.Player(container, { 1826 id: parseInt(mediaId), 1827 loop: config.loop,
1828 autoplay: config.autoplay, 1829 byline: false, 1830 portrait: false, 1831 title: false 1832 }); 1833 1834 // Create a faux HTML5 API using the Vimeo API 1835 plyr.media.play = function () { 1836 plyr.embed.play(); 1837 plyr.media.paused = false; 1838 }; 1839 plyr.media.pause = function () { 1840 plyr.embed.pause(); 1841 plyr.media.paused = true; 1842 }; 1843 plyr.media.stop = function () { 1844 plyr.embed.stop(); 1845 plyr.media.paused = true; 1846 }; 1847 1848 plyr.media.paused = true; 1849 plyr.media.currentTime = 0; 1850 1851 // Update UI 1852 _embedReady(); 1853 1854 plyr.embed.getCurrentTime().then(function (value) { 1855 plyr.media.currentTime = value; 1856 1857 // Trigger timeupdate 1858 _triggerEvent(plyr.media, 'timeupdate'); 1859 }); 1860 1861 plyr.embed.getDuration().then(function (value) { 1862 plyr.media.duration = value; 1863 1864 // Trigger timeupdate 1865 _triggerEvent(plyr.media, 'durationchange'); 1866 }); 1867 1868 // TODO: Captions 1869 /*if (config.captions.defaultActive) { 1870 plyr.embed.enableTextTrack('en'); 1871 }*/ 1872 1873 plyr.embed.on('loaded', function () { 1874 // Fix keyboard focus issues 1875 // https://github.com/sampotts/plyr/issues/317 1876 if (_is.htmlElement(plyr.embed.element) && plyr.supported.full) { 1877 plyr.embed.element.setAttribute('tabindex', '-1'); 1878 } 1879 }); 1880 1881 plyr.embed.on('play', function () { 1882 plyr.media.paused = false; 1883 _triggerEvent(plyr.media, 'play'); 1884 _triggerEvent(plyr.media, 'playing'); 1885 }); 1886 1887 plyr.embed.on('pause', function () { 1888 plyr.media.paused = true; 1889 _triggerEvent(plyr.media, 'pause'); 1890 }); 1891 1892 plyr.embed.on('timeupdate', function (data) { 1893 plyr.media.seeking = false; 1894 plyr.media.currentTime = data.seconds; 1895 _triggerEvent(plyr.media, 'timeupdate'); 1896 }); 1897 1898 plyr.embed.on('progress', function (data) { 1899 plyr.media.buffered = data.percent; 1900 _triggerEvent(plyr.media, 'progress'); 1901 1902 if (parseInt(data.percent) === 1) { 1903 // Trigger event 1904 _triggerEvent(plyr.media, 'canplaythrough'); 1905 } 1906 }); 1907 1908 plyr.embed.on('seeked', function () { 1909 plyr.media.seeking = false; 1910 _triggerEvent(plyr.media, 'seeked'); 1911 _triggerEvent(plyr.media, 'play'); 1912 }); 1913 1914 plyr.embed.on('ended', function () { 1915 plyr.media.paused = true; 1916 _triggerEvent(plyr.media, 'ended'); 1917 }); 1918 } 1919 1920 // Soundcloud ready 1921 function _soundcloudReady() { 1922 /* jshint validthis: true */ 1923 plyr.embed = window.SC.Widget(this); 1924 1925 // Setup on ready 1926 plyr.embed.bind(window.SC.Widget.Events.READY, function () { 1927 // Create a faux HTML5 API using the Soundcloud API 1928 plyr.media.play = function () { 1929 plyr.embed.play(); 1930 plyr.media.paused = false; 1931 }; 1932 plyr.media.pause = function () { 1933 plyr.embed.pause(); 1934 plyr.media.paused = true; 1935 }; 1936 plyr.media.stop = function () { 1937 plyr.embed.seekTo(0); 1938 plyr.embed.pause(); 1939 plyr.media.paused = true; 1940 }; 1941 1942 plyr.media.paused = true; 1943 plyr.media.currentTime = 0; 1944 1945 plyr.embed.getDuration(function (value) { 1946 plyr.media.duration = value / 1000; 1947 1948 // Update UI 1949 _embedReady(); 1950 }); 1951 1952 plyr.embed.getPosition(function (value) { 1953 plyr.media.currentTime = value; 1954 1955 // Trigger timeupdate 1956 _triggerEvent(plyr.media, 'timeupdate'); 1957 }); 1958 1959 plyr.embed.bind(window.SC.Widget.Events.PLAY, function () { 1960 plyr.media.paused = false; 1961 _triggerEvent(plyr.media, 'play'); 1962 _triggerEvent(plyr.media, 'playing'); 1963 }); 1964 1965 plyr.embed.bind(window.SC.Widget.Events.PAUSE, function () { 1966 plyr.media.paused = true; 1967 _triggerEvent(plyr.media, 'pause'); 1968 }); 1969 1970 plyr.embed.bind(window.SC.Widget.Events.PLAY_PROGRESS, function (data) { 1971 plyr.media.seeking = false;
1972 plyr.media.currentTime = data.currentPosition / 1000; 1973 _triggerEvent(plyr.media, 'timeupdate'); 1974 }); 1975 1976 plyr.embed.bind(window.SC.Widget.Events.LOAD_PROGRESS, function (data) { 1977 plyr.media.buffered = data.loadProgress; 1978 _triggerEvent(plyr.media, 'progress'); 1979 1980 if (parseInt(data.loadProgress) === 1) { 1981 // Trigger event 1982 _triggerEvent(plyr.media, 'canplaythrough'); 1983 } 1984 }); 1985 1986 plyr.embed.bind(window.SC.Widget.Events.FINISH, function () { 1987 plyr.media.paused = true; 1988 _triggerEvent(plyr.media, 'ended'); 1989 }); 1990 }); 1991 } 1992 1993 // Play media 1994 function _play() { 1995 if ('play' in plyr.media) { 1996 plyr.media.play(); 1997 } 1998 } 1999 2000 // Pause media 2001 function _pause() { 2002 if ('pause' in plyr.media) { 2003 plyr.media.pause(); 2004 } 2005 } 2006 2007 // Toggle playback 2008 function _togglePlay(toggle) { 2009 // True toggle 2010 if (!_is.boolean(toggle)) { 2011 toggle = plyr.media.paused; 2012 } 2013 2014 if (toggle) { 2015 _play(); 2016 } else { 2017 _pause(); 2018 } 2019 2020 return toggle; 2021 } 2022 2023 // Rewind 2024 function _rewind(seekTime) { 2025 // Use default if needed 2026 if (!_is.number(seekTime)) { 2027 seekTime = config.seekTime; 2028 } 2029 _seek(plyr.media.currentTime - seekTime); 2030 } 2031 2032 // Fast forward 2033 function _forward(seekTime) { 2034 // Use default if needed 2035 if (!_is.number(seekTime)) { 2036 seekTime = config.seekTime; 2037 } 2038 _seek(plyr.media.currentTime + seekTime); 2039 } 2040 2041 // Seek to time 2042 // The input parameter can be an event or a number 2043 function _seek(input) { 2044 var targetTime = 0, 2045 paused = plyr.media.paused, 2046 duration = _getDuration(); 2047 2048 if (_is.number(input)) { 2049 targetTime = input; 2050 } else if (_is.object(input) && _inArray(['input', 'change'], input.type)) { 2051 // It's the seek slider 2052 // Seek to the selected time 2053 targetTime = ((input.target.value / input.target.max) * duration); 2054 } 2055 2056 // Normalise targetTime 2057 if (targetTime < 0) { 2058 targetTime = 0; 2059 } else if (targetTime > duration) { 2060 targetTime = duration; 2061 } 2062 2063 // Update seek range and progress 2064 _updateSeekDisplay(targetTime); 2065 2066 // Set the current time 2067 // Try/catch incase the media isn't set and we're calling seek() from source() and IE moans 2068 try { 2069 plyr.media.currentTime = targetTime.toFixed(4); 2070 } catch (e) { 2071 } 2072 2073 // Embeds 2074 if (_inArray(config.types.embed, plyr.type)) { 2075 switch (plyr.type) { 2076 case 'youtube': 2077 plyr.embed.seekTo(targetTime); 2078 break; 2079 2080 case 'vimeo': 2081 // Round to nearest second for vimeo 2082 plyr.embed.setCurrentTime(targetTime.toFixed(0)); 2083 break; 2084 2085 case 'soundcloud': 2086 plyr.embed.seekTo(targetTime * 1000); 2087 break; 2088 } 2089 2090 if (paused) { 2091 _pause(); 2092 } 2093 2094 // Trigger timeupdate 2095 _triggerEvent(plyr.media, 'timeupdate'); 2096 2097 // Set seeking flag 2098 plyr.media.seeking = true; 2099 2100 // Trigger seeking 2101 _triggerEvent(plyr.media, 'seeking'); 2102 } 2103 2104 // Logging 2105 _log('Seeking to ' + plyr.media.currentTime + ' seconds'); 2106 2107 // Special handling for 'manual' captions 2108 _seekManualCaptions(targetTime); 2109 } 2110 2111 // Get the duration (or custom if set) 2112 function _getDuration() { 2113 // It should be a number, but parse it just incase 2114 var duration = parseInt(config.duration), 2115 2116 // True duration 2117 mediaDuration = 0; 2118 2119 // Only if duration available 2120 if (plyr.media.duration !== null && !isNaN(plyr.media.duration)) { 2121 mediaDuration = plyr.media.duration; 2122 } 2123 2124 // If custom duration is funky, use regular duration 2125 return (isNaN(duration) ? mediaDuration : duration); 2126 } 2127 2128 // Check playing state 2129 function _checkPlaying() { 2130 _toggleClass(plyr.container, config.classes.playing, !plyr.media.paused); 2131 2132 _toggleClass(plyr.container, config.classes.stopped, plyr.media.paused); 2133 2134 _toggleControls(plyr.media.paused); 2135 } 2136 2137 // Save scroll position 2138 function _saveScrollPosition() { 2139 scroll = { 2140 x: window.pageXOffset || 0, 2141 y: window.pageYOffset || 0 2142 }; 2143 } 2144 2145 // Restore scroll position 2146 function _restoreScrollPosition() { 2147 window.scrollTo(scroll.x, scroll.y); 2148 } 2149 2150 // Toggle fullscreen 2151 function _toggleFullscreen(event) { 2152 // Check for native support 2153 var nativeSupport = fullscreen.supportsFullScreen; 2154 2155 if (nativeSupport) { 2156 // If it's a fullscreen change event, update the UI 2157 if (event && event.type === fullscreen.fullScreenEventName) { 2158 plyr.isFullscreen = fullscreen.isFullScreen(plyr.container); 2159 } else { 2160 // Else it's a user request to enter or exit 2161 if (!fullscreen.isFullScreen(plyr.container)) { 2162 // Save scroll position 2163 _saveScrollPosition(); 2164 2165 // Request full screen 2166 fullscreen.requestFullScreen(plyr.container); 2167 } else { 2168 // Bail from fullscreen 2169 fullscreen.cancelFullScreen(); 2170 } 2171 2172 // Check if we're actually full screen (it could fail) 2173 plyr.isFullscreen = fullscreen.isFullScreen(plyr.container); 2174 2175 return; 2176 } 2177 } else { 2178 // Otherwise, it's a simple toggle
2179 plyr.isFullscreen = !plyr.isFullscreen; 2180 2181 // Bind/unbind escape key 2182 document.body.style.overflow = plyr.isFullscreen ? 'hidden' : ''; 2183 } 2184 2185 // Set class hook 2186 _toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen); 2187 2188 // Trap focus 2189 _focusTrap(plyr.isFullscreen); 2190 2191 // Set button state 2192 if (plyr.buttons && plyr.buttons.fullscreen) { 2193 _toggleState(plyr.buttons.fullscreen, plyr.isFullscreen); 2194 } 2195 2196 // Trigger an event 2197 _triggerEvent(plyr.container, plyr.isFullscreen ? 'enterfullscreen' : 'exitfullscreen', true); 2198 2199 // Restore scroll position 2200 if (!plyr.isFullscreen && nativeSupport) { 2201 _restoreScrollPosition(); 2202 } 2203 } 2204 2205 // Mute 2206 function _toggleMute(muted) { 2207 // If the method is called without parameter, toggle based on current value 2208 if (!_is.boolean(muted)) { 2209 muted = !plyr.media.muted; 2210 } 2211 2212 // Set button state 2213 _toggleState(plyr.buttons.mute, muted); 2214 2215 // Set mute on the player 2216 plyr.media.muted = muted; 2217 2218 // If volume is 0 after unmuting, set to default 2219 if (plyr.media.volume === 0) { 2220 _setVolume(config.volume); 2221 } 2222 2223 // Embeds 2224 if (_inArray(config.types.embed, plyr.type)) { 2225 // YouTube 2226 switch (plyr.type) { 2227 case 'youtube': 2228 plyr.embed[plyr.media.muted ? 'mute' : 'unMute'](); 2229 break; 2230 2231 case 'vimeo': 2232 case 'soundcloud': 2233 plyr.embed.setVolume(plyr.media.muted ? 0 : parseFloat(config.volume / config.volumeMax)); 2234 break; 2235 } 2236 2237 // Trigger volumechange for embeds 2238 _triggerEvent(plyr.media, 'volumechange'); 2239 } 2240 } 2241 2242 // Set volume 2243 function _setVolume(volume) { 2244 var max = config.volumeMax, 2245 min = config.volumeMin; 2246 2247 // Load volume from storage if no value specified 2248 if (_is.undefined(volume)) { 2249 volume = plyr.storage.volume; 2250 } 2251 2252 // Use config if all else fails 2253 if (volume === null || isNaN(volume)) { 2254 volume = config.volume; 2255 } 2256 2257 // Maximum is volumeMax 2258 if (volume > max) { 2259 volume = max; 2260 } 2261 // Minimum is volumeMin 2262 if (volume < min) { 2263 volume = min; 2264 } 2265 2266 // Set the player volume 2267 plyr.media.volume = parseFloat(volume / max); 2268 2269 // Set the display 2270 if (plyr.volume.display) { 2271 plyr.volume.display.value = volume; 2272 } 2273 2274 // Embeds 2275 if (_inArray(config.types.embed, plyr.type)) { 2276 switch (plyr.type) { 2277 case 'youtube': 2278 plyr.embed.setVolume(plyr.media.volume * 100); 2279 break; 2280 2281 case 'vimeo': 2282 case 'soundcloud': 2283 plyr.embed.setVolume(plyr.media.volume); 2284 break; 2285 } 2286 2287 // Trigger volumechange for embeds 2288 _triggerEvent(plyr.media, 'volumechange'); 2289 } 2290 2291 // Toggle muted state 2292 if (volume === 0) { 2293 plyr.media.muted = true; 2294 } else if (plyr.media.muted && volume > 0) { 2295 _toggleMute(); 2296 } 2297 } 2298 2299 // Increase volume 2300 function _increaseVolume(step) { 2301 var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax); 2302 2303 if (!_is.number(step)) { 2304 step = config.volumeStep; 2305 } 2306 2307 _setVolume(volume + step); 2308 } 2309 2310 // Decrease volume 2311 function _decreaseVolume(step) { 2312 var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax); 2313 2314 if (!_is.number(step)) { 2315 step = config.volumeStep; 2316 } 2317 2318 _setVolume(volume - step); 2319 } 2320 2321 // Update volume UI and storage 2322 function _updateVolume() { 2323 // Get the current volume 2324 var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax); 2325 2326 // Update the <input type="range"> if present 2327 if (plyr.supported.full) { 2328 if (plyr.volume.input) { 2329 plyr.volume.input.value = volume; 2330 } 2331 if (plyr.volume.display) { 2332 plyr.volume.display.value = volume; 2333 } 2334 } 2335 2336 // Update the volume in storage 2337 _updateStorage({volume: volume}); 2338 2339 // Toggle class if muted 2340 _toggleClass(plyr.container, config.classes.muted, (volume === 0)); 2341 2342 // Update checkbox for mute state 2343 if (plyr.supported.full && plyr.buttons.mute) { 2344 _toggleState(plyr.buttons.mute, (volume === 0)); 2345 } 2346 } 2347 2348 // Toggle captions 2349 function _toggleCaptions(show) { 2350 // If there's no full support, or there's no caption toggle 2351 if (!plyr.supported.full || !plyr.buttons.captions) { 2352 return; 2353 } 2354 2355 // If the method is called without parameter, toggle based on current value 2356 if (!_is.boolean(show)) {
2357 show = (plyr.container.className.indexOf(config.classes.captions.active) === -1); 2358 } 2359 2360 // Set global 2361 plyr.captionsEnabled = show; 2362 2363 // Toggle state 2364 _toggleState(plyr.buttons.captions, plyr.captionsEnabled); 2365 2366 // Add class hook 2367 _toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled); 2368 2369 // Trigger an event 2370 _triggerEvent(plyr.container, plyr.captionsEnabled ? 'captionsenabled' : 'captionsdisabled', true); 2371 2372 // Save captions state to localStorage 2373 _updateStorage({captionsEnabled: plyr.captionsEnabled}); 2374 } 2375 2376 // Check if media is loading 2377 function _checkLoading(event) { 2378 var loading = (event.type === 'waiting'); 2379 2380 // Clear timer 2381 clearTimeout(timers.loading); 2382 2383 // Timer to prevent flicker when seeking 2384 timers.loading = setTimeout(function () { 2385 // Toggle container class hook 2386 _toggleClass(plyr.container, config.classes.loading, loading); 2387 2388 // Show controls if loading, hide if done 2389 _toggleControls(loading); 2390 }, (loading ? 250 : 0)); 2391 } 2392 2393 // Update <progress> elements 2394 function _updateProgress(event) { 2395 if (!plyr.supported.full) { 2396 return; 2397 } 2398 2399 var progress = plyr.progress.played, 2400 value = 0, 2401 duration = _getDuration(); 2402 2403 if (event) { 2404 switch (event.type) { 2405 // Video playing 2406 case 'timeupdate': 2407 case 'seeking': 2408 if (plyr.controls.pressed) { 2409 return; 2410 } 2411 2412 value = _getPercentage(plyr.media.currentTime, duration); 2413 2414 // Set seek range value only if it's a 'natural' time event 2415 if (event.type === 'timeupdate' && plyr.buttons.seek) { 2416 plyr.buttons.seek.value = value; 2417 } 2418 2419 break; 2420 2421 // Check buffer status 2422 case 'playing': 2423 case 'progress': 2424 progress = plyr.progress.buffer; 2425 value = (function () { 2426 var buffered = plyr.media.buffered; 2427 2428 if (buffered && buffered.length) { 2429 // HTML5 2430 return _getPercentage(buffered.end(0), duration); 2431 } else if (_is.number(buffered)) { 2432 // YouTube returns between 0 and 1 2433 return (buffered * 100); 2434 } 2435 2436 return 0; 2437 })(); 2438 2439 break; 2440 } 2441 } 2442 2443 // Set values 2444 _setProgress(progress, value); 2445 } 2446 2447 // Set <progress> value 2448 function _setProgress(progress, value) { 2449 if (!plyr.supported.full) { 2450 return; 2451 } 2452 2453 // Default to 0 2454 if (_is.undefined(value)) { 2455 value = 0; 2456 } 2457 // Default to buffer or bail 2458 if (_is.undefined(progress)) { 2459 if (plyr.progress && plyr.progress.buffer) { 2460 progress = plyr.progress.buffer; 2461 } else { 2462 return; 2463 } 2464 } 2465 2466 // One progress element passed 2467 if (_is.htmlElement(progress)) { 2468 progress.value = value; 2469 } else if (progress) { 2470 // Object of progress + text element 2471 if (progress.bar) { 2472 progress.bar.value = value; 2473 } 2474 if (progress.text) { 2475 progress.text.innerHTML = value; 2476 } 2477 } 2478 } 2479 2480 // Update the displayed time 2481 function _updateTimeDisplay(time, element) { 2482 // Bail if there's no duration display 2483 if (!element) { 2484 return; 2485 } 2486 2487 // Fallback to 0 2488 if (isNaN(time)) { 2489 time = 0; 2490 } 2491 2492 plyr.secs = parseInt(time % 60); 2493 plyr.mins = parseInt((time / 60) % 60); 2494 plyr.hours = parseInt(((time / 60) / 60) % 60); 2495 2496 // Do we need to display hours? 2497 var displayHours = (parseInt(((_getDuration() / 60) / 60) % 60) > 0); 2498 2499 // Ensure it's two digits. For example, 03 rather than 3. 2500 plyr.secs = ('0' + plyr.secs).slice(-2); 2501 plyr.mins = ('0' + plyr.mins).slice(-2); 2502 2503 // Render 2504 element.innerHTML = (displayHours ? plyr.hours + ':' : '') + plyr.mins + ':' + plyr.secs; 2505 } 2506 2507 // Show the duration on metadataloaded 2508 function _displayDuration() { 2509 if (!plyr.supported.full) { 2510 return; 2511 } 2512 2513 // Determine duration 2514 var duration = _getDuration() || 0; 2515 2516 // If there's only one time display, display duration there 2517 if (!plyr.duration && config.displayDuration && plyr.media.paused) { 2518 _updateTimeDisplay(duration, plyr.currentTime); 2519 } 2520 2521 // If there's a duration element, update content 2522 if (plyr.duration) { 2523 _updateTimeDisplay(duration, plyr.duration); 2524 } 2525 2526 // Update the tooltip (if visible) 2527 _updateSeekTooltip(); 2528 } 2529 2530 // Handle time change event 2531 function _timeUpdate(event) { 2532 // Duration 2533 _updateTimeDisplay(plyr.media.currentTime, plyr.currentTime); 2534 2535 // Ignore updates while seeking 2536 if (event && event.type === 'timeupdate' && plyr.media.seeking) { 2537 return; 2538 } 2539 2540 // Playing progress 2541 _updateProgress(event); 2542 } 2543 2544 // Update seek range and progress 2545 function _updateSeekDisplay(time) { 2546 // Default to 0 2547 if (!_is.number(time)) { 2548 time = 0; 2549 } 2550 2551 var duration = _getDuration(), 2552 value = _getPercentage(time, duration); 2553 2554 // Update progress 2555 if (plyr.progress && plyr.progress.played) { 2556 plyr.progress.played.value = value; 2557 } 2558 2559 // Update seek range input 2560 if (plyr.buttons && plyr.buttons.seek) { 2561 plyr.buttons.seek.value = value; 2562 } 2563 } 2564 2565 // Update hover tooltip for seeking 2566 function _updateSeekTooltip(event) { 2567 var duration = _getDuration(); 2568 2569 // Bail if setting not true 2570 if (!config.tooltips.seek || !plyr.progress.container || duration === 0) { 2571 return; 2572 } 2573 2574 // Calculate percentage 2575 var clientRect = plyr.progress.container.getBoundingClientRect(), 2576 percent = 0, 2577 visible = config.classes.tooltip + '--visible'; 2578 2579 // Determine percentage, if already visible 2580 if (!event) { 2581 if (_hasClass(plyr.progress.tooltip, visible)) { 2582 percent = plyr.progress.tooltip.style.left.replace('%', ''); 2583 } else { 2584 return; 2585 } 2586 } else { 2587 percent = ((100 / clientRect.width) * (event.pageX - clientRect.left)); 2588 } 2589 2590 // Set bounds 2591 if (percent < 0) { 2592 percent = 0; 2593 } else if (percent > 100) { 2594 percent = 100; 2595 } 2596 2597 // Display the time a click would seek to 2598 _updateTimeDisplay(((duration / 100) * percent), plyr.progress.tooltip); 2599 2600 // Set position 2601 plyr.progress.tooltip.style.left = percent + "%"; 2602 2603 // Show/hide the tooltip 2604 // If the event is a moues in/out and percentage is inside bounds 2605 if (event && _inArray(['mouseenter', 'mouseleave'], event.type)) { 2606 _toggleClass(plyr.progress.tooltip, visible, (event.type === 'mouseenter')); 2607 } 2608 } 2609 2610 // Show the player controls in fullscreen mode 2611 function _toggleControls(toggle) { 2612 // Don't hide if config says not to, it's audio, or not ready or loading 2613 if (!config.hideControls || plyr.type === 'audio') { 2614 return; 2615 } 2616
2617 var delay = 0, 2618 isEnterFullscreen = false, 2619 show = toggle, 2620 loading = _hasClass(plyr.container, config.classes.loading); 2621 2622 // Default to false if no boolean 2623 if (!_is.boolean(toggle)) { 2624 if (toggle && toggle.type) { 2625 // Is the enter fullscreen event 2626 isEnterFullscreen = (toggle.type === 'enterfullscreen'); 2627 2628 // Whether to show controls 2629 show = _inArray(['mousemove', 'touchstart', 'mouseenter', 'focus'], toggle.type); 2630 2631 // Delay hiding on move events 2632 if (_inArray(['mousemove', 'touchmove'], toggle.type)) { 2633 delay = 2000; 2634 } 2635 2636 // Delay a little more for keyboard users 2637 if (toggle.type === 'focus') { 2638 delay = 3000; 2639 } 2640 } else { 2641 show = _hasClass(plyr.container, config.classes.hideControls); 2642 } 2643 } 2644 2645 // Clear timer every movement 2646 window.clearTimeout(timers.hover); 2647 2648 // If the mouse is not over the controls, set a timeout to hide them 2649 if (show || plyr.media.paused || loading) { 2650 _toggleClass(plyr.container, config.classes.hideControls, false); 2651 2652 // Always show controls when paused or if touch 2653 if (plyr.media.paused || loading) { 2654 return; 2655 } 2656 2657 // Delay for hiding on touch 2658 if (plyr.browser.isTouch) { 2659 delay = 3000; 2660 } 2661 } 2662 2663 // If toggle is false or if we're playing (regardless of toggle), 2664 // then set the timer to hide the controls 2665 if (!show || !plyr.media.paused) { 2666 timers.hover = window.setTimeout(function () { 2667 // If the mouse is over the controls (and not entering fullscreen), bail 2668 if ((plyr.controls.pressed || plyr.controls.hover) && !isEnterFullscreen) { 2669 return; 2670 } 2671 2672 _toggleClass(plyr.container, config.classes.hideControls, true); 2673 }, delay); 2674 } 2675 } 2676 2677 // Add common function to retrieve media source 2678 function _source(source) { 2679 // If not null or undefined, parse it 2680 if (!_is.undefined(source)) { 2681 _updateSource(source); 2682 return; 2683 } 2684 2685 // Return the current source 2686 var url; 2687 switch (plyr.type) { 2688 case 'youtube': 2689 url = plyr.embed.getVideoUrl(); 2690 break; 2691 2692 case 'vimeo': 2693 plyr.embed.getVideoUrl.then(function (value) { 2694 url = value; 2695 }); 2696 break; 2697 2698 case 'soundcloud': 2699 plyr.embed.getCurrentSound(function (object) { 2700 url = object.permalink_url; 2701 }); 2702 break; 2703 2704 default: 2705 url = plyr.media.currentSrc; 2706 break; 2707 } 2708 2709 return url || ''; 2710 } 2711 2712 // Update source 2713 // Sources are not checked for support so be careful 2714 function _updateSource(source) { 2715 if (!_is.object(source) || !('sources' in source) || !source.sources.length) { 2716 _warn('Invalid source format'); 2717 return; 2718 } 2719 2720 // Remove ready class hook 2721 _toggleClass(plyr.container, config.classes.ready, false); 2722 2723 // Pause playback 2724 _pause(); 2725 2726 // Update seek range and progress 2727 _updateSeekDisplay(); 2728 2729 // Reset buffer progress 2730 _setProgress(); 2731 2732 // Cancel current network requests 2733 _cancelRequests(); 2734 2735 // Setup new source 2736 function setup() { 2737 // Remove embed object 2738 plyr.embed = null; 2739 2740 // Remove the old media 2741 _remove(plyr.media); 2742 2743 // Remove video container 2744 if (plyr.type === 'video' && plyr.videoContainer) { 2745 _remove(plyr.videoContainer); 2746 } 2747 2748 // Reset class name 2749 if (plyr.container) { 2750 plyr.container.removeAttribute('class'); 2751 } 2752 2753 // Set the type 2754 if ('type' in source) { 2755 plyr.type = source.type; 2756 2757 // Get child type for video (it might be an embed) 2758 if (plyr.type === 'video') { 2759 var firstSource = source.sources[0]; 2760 2761 if ('type' in firstSource && _inArray(config.types.embed, firstSource.type)) { 2762 plyr.type = firstSource.type; 2763 } 2764 } 2765 } 2766 2767 // Check for support 2768 plyr.supported = supported(plyr.type); 2769 2770 // Create new markup 2771 switch (plyr.type) { 2772 case 'video': 2773 plyr.media = document.createElement('video'); 2774 break; 2775 2776 case 'audio': 2777 plyr.media = document.createElement('audio'); 2778 break; 2779 2780 case 'youtube': 2781 case 'vimeo': 2782 case 'soundcloud': 2783 plyr.media = document.createElement('div'); 2784 plyr.embedId = source.sources[0].src; 2785 break; 2786 } 2787 2788 // Inject the new element 2789 _prependChild(plyr.container, plyr.media); 2790 2791 // Autoplay the new source? 2792 if (_is.boolean(source.autoplay)) {
2793 config.autoplay = source.autoplay; 2794 } 2795 2796 // Set attributes for audio and video 2797 if (_inArray(config.types.html5, plyr.type)) { 2798 if (config.crossorigin) { 2799 plyr.media.setAttribute('crossorigin', ''); 2800 } 2801 if (config.autoplay) { 2802 plyr.media.setAttribute('autoplay', ''); 2803 } 2804 if ('poster' in source) { 2805 plyr.media.setAttribute('poster', source.poster); 2806 } 2807 if (config.loop) { 2808 plyr.media.setAttribute('loop', ''); 2809 } 2810 } 2811 2812 // Restore class hooks 2813 _toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen); 2814 _toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled); 2815 _toggleStyleHook(); 2816 2817 // Set new sources for html5 2818 if (_inArray(config.types.html5, plyr.type)) { 2819 _insertChildElements('source', source.sources); 2820 } 2821 2822 // Set up from scratch 2823 _setupMedia(); 2824 2825 // HTML5 stuff 2826 if (_inArray(config.types.html5, plyr.type)) { 2827 // Setup captions 2828 if ('tracks' in source) { 2829 _insertChildElements('track', source.tracks); 2830 } 2831 2832 // Load HTML5 sources 2833 plyr.media.load(); 2834 } 2835 2836 // If HTML5 or embed but not fully supported, setupInterface and call ready now 2837 if (_inArray(config.types.html5, plyr.type) || (_inArray(config.types.embed, plyr.type) && !plyr.supported.full)) { 2838 // Setup interface 2839 _setupInterface(); 2840 2841 // Call ready 2842 _ready(); 2843 } 2844 2845 // Set aria title and iframe title
2846 config.title = source.title; 2847 _setTitle(); 2848 } 2849 2850 // Destroy instance adn wait for callback 2851 // Vimeo throws a wobbly if you don't wait 2852 _destroy(setup, false); 2853 } 2854 2855 // Update poster 2856 function _updatePoster(source) { 2857 if (plyr.type === 'video') { 2858 plyr.media.setAttribute('poster', source); 2859 } 2860 } 2861 2862 // Listen for control events 2863 function _controlListeners() { 2864 // IE doesn't support input event, so we fallback to change 2865 var inputEvent = (plyr.browser.isIE ? 'change' : 'input'); 2866 2867 // Click play/pause helper 2868 function togglePlay() { 2869 var play = _togglePlay(); 2870 2871 // Determine which buttons 2872 var trigger = plyr.buttons[play ? 'play' : 'pause'], 2873 target = plyr.buttons[play ? 'pause' : 'play']; 2874 2875 // Get the last play button to account for the large play button 2876 if (target && target.length > 1) { 2877 target = target[target.length - 1]; 2878 } else { 2879 target = target[0]; 2880 } 2881 2882 // Setup focus and tab focus 2883 if (target) { 2884 var hadTabFocus = _hasClass(trigger, config.classes.tabFocus); 2885 2886 setTimeout(function () { 2887 target.focus(); 2888 2889 if (hadTabFocus) { 2890 _toggleClass(trigger, config.classes.tabFocus, false); 2891 _toggleClass(target, config.classes.tabFocus, true); 2892 } 2893 }, 100); 2894 } 2895 } 2896 2897 // Get the focused element 2898 function getFocusElement() { 2899 var focused = document.activeElement; 2900 2901 if (!focused || focused === document.body) { 2902 focused = null; 2903 } else { 2904 focused = document.querySelector(':focus'); 2905 } 2906 2907 return focused; 2908 } 2909 2910 // Get the key code for an event 2911 function getKeyCode(event) { 2912 return event.keyCode ? event.keyCode : event.which; 2913 } 2914 2915 // Detect tab focus 2916 function checkTabFocus(focused) { 2917 for (var button in plyr.buttons) { 2918 var element = plyr.buttons[button]; 2919 2920 if (_is.nodeList(element)) { 2921 for (var i = 0; i < element.length; i++) { 2922 _toggleClass(element[i], config.classes.tabFocus, (element[i] === focused)); 2923 } 2924 } else { 2925 _toggleClass(element, config.classes.tabFocus, (element === focused)); 2926 } 2927 } 2928 } 2929 2930 // Keyboard shortcuts 2931 if (config.keyboardShorcuts.focused) { 2932 var last = null; 2933 2934 // Handle global presses 2935 if (config.keyboardShorcuts.global) { 2936 _on(window, 'keydown keyup', function (event) { 2937 var code = getKeyCode(event), 2938 focused = getFocusElement(), 2939 allowed = [48, 49, 50, 51, 52, 53, 54, 56, 57, 75, 77, 70, 67], 2940 count = get().length; 2941 2942 // Only handle global key press if there's only one player 2943 // and the key is in the allowed keys 2944 // and if the focused element is not editable (e.g. text input) 2945 // and any that accept key input http://webaim.org/techniques/keyboard/ 2946 if (count === 1 && _inArray(allowed, code) && (!_is.htmlElement(focused) || !_matches(focused, config.selectors.editable))) { 2947 handleKey(event); 2948 } 2949 }); 2950 } 2951 2952 // Handle presses on focused 2953 _on(plyr.container, 'keydown keyup', handleKey); 2954 } 2955 2956 function handleKey(event) { 2957 var code = getKeyCode(event), 2958 pressed = event.type === 'keydown', 2959 held = pressed && code === last; 2960 2961 // If the event is bubbled from the media element 2962 // Firefox doesn't get the keycode for whatever reason 2963 if (!_is.number(code)) { 2964 return; 2965 } 2966 2967 // Seek by the number keys 2968 function seekByKey() { 2969 // Get current duration 2970 var duration = plyr.media.duration; 2971 2972 // Bail if we have no duration set 2973 if (!_is.number(duration)) { 2974 return; 2975 } 2976 2977 // Divide the max duration into 10th's and times by the number value 2978 _seek((duration / 10) * (code - 48)); 2979 } 2980 2981 // Handle the key on keydown 2982 // Reset on keyup 2983 if (pressed) { 2984 // Which keycodes should we prevent default 2985 var preventDefault = [48, 49, 50, 51, 52, 53, 54, 56, 57, 32, 75, 38, 40, 77, 39, 37, 70, 67]; 2986 2987 // If the code is found prevent default (e.g. prevent scrolling for arrows) 2988 if (_inArray(preventDefault, code)) { 2989 event.preventDefault(); 2990 event.stopPropagation(); 2991 } 2992 2993 switch (code) { 2994 // 0-9 2995 case 48: 2996 case 49: 2997 case 50: 2998 case 51: 2999 case 52: 3000 case 53: 3001 case 54: 3002 case 55: 3003 case 56: 3004 case 57: 3005 if (!held) { 3006 seekByKey(); 3007 } 3008 break; 3009 // Space and K key 3010 case 32: 3011 case 75: 3012 if (!held) { 3013 _togglePlay(); 3014 } 3015 break; 3016 // Arrow up 3017 case 38: 3018 _increaseVolume(); 3019 break; 3020 // Arrow down 3021 case 40: 3022 _decreaseVolume(); 3023 break; 3024 // M key 3025 case 77: 3026 if (!held) { 3027 _toggleMute() 3028 } 3029 break; 3030 // Arrow forward 3031 case 39: 3032 _forward(); 3033 break; 3034 // Arrow back 3035 case 37: 3036 _rewind(); 3037 break; 3038 // F key 3039 case 70: 3040 _toggleFullscreen(); 3041 break; 3042 // C key 3043 case 67: 3044 if (!held) { 3045 _toggleCaptions(); 3046 } 3047 break; 3048 } 3049 3050 // Escape is handle natively when in full screen 3051 // So we only need to worry about non native 3052 if (!fullscreen.supportsFullScreen && plyr.isFullscreen && code === 27) { 3053 _toggleFullscreen(); 3054 } 3055 3056 // Store last code for next cycle 3057 last = code; 3058 } else { 3059 last = null; 3060 } 3061 } 3062 3063 // Focus/tab management 3064 _on(window, 'keyup', function (event) { 3065 var code = getKeyCode(event), 3066 focused = getFocusElement(); 3067 3068 if (code === 9) { 3069 checkTabFocus(focused); 3070 } 3071 }); 3072 _on(document.body, 'click', function () { 3073 _toggleClass(_getElement('.' + config.classes.tabFocus), config.classes.tabFocus, false); 3074 }); 3075 for (var button in plyr.buttons) { 3076 var element = plyr.buttons[button]; 3077 3078 _on(element, 'blur', function () { 3079 _toggleClass(element, 'tab-focus', false); 3080 }); 3081 } 3082 3083 // Play 3084 _proxyListener(plyr.buttons.play, 'click', config.listeners.play, togglePlay); 3085 3086 // Pause 3087 _proxyListener(plyr.buttons.pause, 'click', config.listeners.pause, togglePlay); 3088 3089 // Restart 3090 _proxyListener(plyr.buttons.restart, 'click', config.listeners.restart, _seek); 3091 3092 // Rewind 3093 _proxyListener(plyr.buttons.rewind, 'click', config.listeners.rewind, _rewind); 3094 3095 // Fast forward 3096 _proxyListener(plyr.buttons.forward, 'click', config.listeners.forward, _forward); 3097 3098 // Seek 3099 _proxyListener(plyr.buttons.seek, inputEvent, config.listeners.seek, _seek); 3100 3101 // Set volume 3102 _proxyListener(plyr.volume.input, inputEvent, config.listeners.volume, function () { 3103 _setVolume(plyr.volume.input.value); 3104 }); 3105 3106 // Mute 3107 _proxyListener(plyr.buttons.mute, 'click', config.listeners.mute, _toggleMute); 3108 3109 // Fullscreen 3110 _proxyListener(plyr.buttons.fullscreen, 'click', config.listeners.fullscreen, _toggleFullscreen); 3111 3112 // Handle user exiting fullscreen by escaping etc 3113 if (fullscreen.supportsFullScreen) { 3114 _on(document, fullscreen.fullScreenEventName, _toggleFullscreen); 3115 } 3116 3117 // Captions 3118 _proxyListener(plyr.buttons.captions, 'click', config.listeners.captions, _toggleCaptions); 3119 3120 // Seek tooltip 3121 _on(plyr.progress.container, 'mouseenter mouseleave mousemove', _updateSeekTooltip); 3122 3123 // Toggle controls visibility based on mouse movement 3124 if (config.hideControls) { 3125 // Toggle controls on mouse events and entering fullscreen 3126 _on(plyr.container, 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', _toggleControls); 3127 3128 // Watch for cursor over controls so they don't hide when trying to interact 3129 _on(plyr.controls, 'mouseenter mouseleave', function (event) { 3130 plyr.controls.hover = event.type === 'mouseenter'; 3131 }); 3132 3133 // Watch for cursor over controls so they don't hide when trying to interact 3134 _on(plyr.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { 3135 plyr.controls.pressed = _inArray(['mousedown', 'touchstart'], event.type); 3136 }); 3137 3138 // Focus in/out on controls
3139 _on(plyr.controls, 'focus blur', _toggleControls, true); 3140 } 3141 3142 // Adjust volume on scroll 3143 _on(plyr.volume.input, 'wheel', function (event) { 3144 event.preventDefault(); 3145 3146 // Detect "natural" scroll - suppored on OS X Safari only 3147 // Other browsers on OS X will be inverted until support improves 3148 var inverted = event.webkitDirectionInvertedFromDevice, 3149 step = (config.volumeStep / 5); 3150 3151 // Scroll down (or up on natural) to decrease 3152 if (event.deltaY < 0 || event.deltaX > 0) { 3153 if (inverted) { 3154 _decreaseVolume(step); 3155 } else { 3156 _increaseVolume(step); 3157 } 3158 } 3159 3160 // Scroll up (or down on natural) to increase 3161 if (event.deltaY > 0 || event.deltaX < 0) { 3162 if (inverted) { 3163 _increaseVolume(step); 3164 } else { 3165 _decreaseVolume(step); 3166 } 3167 } 3168 }); 3169 } 3170 3171 // Listen for media events 3172 function _mediaListeners() { 3173 // Time change on media 3174 _on(plyr.media, 'timeupdate seeking', _timeUpdate); 3175 3176 // Update manual captions 3177 _on(plyr.media, 'timeupdate', _seekManualCaptions); 3178 3179 // Display duration 3180 _on(plyr.media, 'durationchange loadedmetadata', _displayDuration); 3181 3182 // Handle the media finishing 3183 _on(plyr.media, 'ended', function () { 3184 // Show poster on end 3185 if (plyr.type === 'video' && config.showPosterOnEnd) { 3186 // Clear 3187 if (plyr.type === 'video') { 3188 _setCaption(); 3189 } 3190 3191 // Restart 3192 _seek(); 3193 3194 // Re-load media 3195 plyr.media.load(); 3196 } 3197 }); 3198 3199 // Check for buffer progress 3200 _on(plyr.media, 'progress playing', _updateProgress); 3201 3202 // Handle native mute 3203 _on(plyr.media, 'volumechange', _updateVolume); 3204 3205 // Handle native play/pause 3206 _on(plyr.media, 'play pause ended', _checkPlaying); 3207 3208 // Loading 3209 _on(plyr.media, 'waiting canplay seeked', _checkLoading); 3210 3211 // Click video 3212 if (config.clickToPlay && plyr.type !== 'audio') { 3213 // Re-fetch the wrapper 3214 var wrapper = _getElement('.' + config.classes.videoWrapper); 3215 3216 // Bail if there's no wrapper (this should never happen) 3217 if (!wrapper) { 3218 return; 3219 } 3220 3221 // Set cursor 3222 wrapper.style.cursor = "pointer"; 3223 3224 // On click play, pause ore restart 3225 _on(wrapper, 'click', function () { 3226 // Touch devices will just show controls (if we're hiding controls) 3227 if (config.hideControls && plyr.browser.isTouch && !plyr.media.paused) { 3228 return; 3229 } 3230 3231 if (plyr.media.paused) { 3232 _play(); 3233 } else if (plyr.media.ended) { 3234 _seek(); 3235 _play(); 3236 } else { 3237 _pause(); 3238 } 3239 }); 3240 } 3241 3242 // Disable right click 3243 if (config.disableContextMenu) { 3244 _on(plyr.media, 'contextmenu', function (event) { 3245 event.preventDefault(); 3246 }); 3247 } 3248 3249 // Proxy events to container 3250 // Bubble up key events for Edge 3251 _on(plyr.media, config.events.concat(['keyup', 'keydown']).join(' '), function (event) { 3252 _triggerEvent(plyr.container, event.type, true); 3253 }); 3254 } 3255 3256 // Cancel current network requests 3257 // See https://github.com/sampotts/plyr/issues/174 3258 function _cancelRequests() { 3259 if (!_inArray(config.types.html5, plyr.type)) { 3260 return; 3261 } 3262 3263 // Remove child sources 3264 var sources = plyr.media.querySelectorAll('source'); 3265 for (var i = 0; i < sources.length; i++) { 3266 _remove(sources[i]); 3267 } 3268 3269 // Set blank video src attribute 3270 // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error 3271 // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection 3272 plyr.media.setAttribute('src', config.blankUrl); 3273 3274 // Load the new empty source 3275 // This will cancel existing requests 3276 // See https://github.com/sampotts/plyr/issues/174 3277 plyr.media.load(); 3278 3279 // Debugging
3280 _log('Cancelled network requests'); 3281 } 3282 3283 // Destroy an instance 3284 // Event listeners are removed when elements are removed 3285 // http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory 3286 function _destroy(callback, restore) { 3287 // Bail if the element is not initialized 3288 if (!plyr.init) { 3289 return null; 3290 } 3291 3292 // Type specific stuff 3293 switch (plyr.type) { 3294 case 'youtube': 3295 // Clear timers 3296 window.clearInterval(timers.buffering); 3297 window.clearInterval(timers.playing); 3298 3299 // Destroy YouTube API 3300 plyr.embed.destroy(); 3301 3302 // Clean up 3303 cleanUp(); 3304 3305 break; 3306 3307 case 'vimeo': 3308 // Destroy Vimeo API 3309 // then clean up (wait, to prevent postmessage errors) 3310 plyr.embed.unload().then(cleanUp); 3311 3312 // Vimeo does not always return 3313 timers.cleanUp = window.setTimeout(cleanUp, 200); 3314 3315 break; 3316 3317 case 'video': 3318 case 'audio': 3319 // Restore native video controls 3320 _toggleNativeControls(true); 3321 3322 // Clean up 3323 cleanUp(); 3324 3325 break; 3326 } 3327 3328 function cleanUp() { 3329 clearTimeout(timers.cleanUp); 3330 3331 // Default to restore original element 3332 if (!_is.boolean(restore)) { 3333 restore = true; 3334 } 3335 3336 // Callback 3337 if (_is.function(callback)) { 3338 callback.call(original); 3339 } 3340 3341 // Bail if we don't need to restore the original element 3342 if (!restore) { 3343 return; 3344 } 3345 3346 // Remove init flag 3347 plyr.init = false; 3348 3349 // Replace the container with the original element provided 3350 plyr.container.parentNode.replaceChild(original, plyr.container); 3351 3352 // Allow overflow (set on fullscreen) 3353 document.body.style.overflow = ''; 3354 3355 // Event 3356 _triggerEvent(original, 'destroyed', true); 3357 } 3358 } 3359 3360 // Setup a player 3361 function _init() { 3362 // Bail if the element is initialized 3363 if (plyr.init) { 3364 return null; 3365 } 3366 3367 // Setup the fullscreen api 3368 fullscreen = _fullscreen(); 3369 3370 // Sniff out the browser 3371 plyr.browser = _browserSniff(); 3372 3373 // Bail if nothing to setup 3374 if (!_is.htmlElement(plyr.media)) { 3375 return; 3376 } 3377 3378 // Load saved settings from localStorage 3379 _setupStorage(); 3380 3381 // Set media type based on tag or data attribute 3382 // Supported: video, audio, vimeo, youtube 3383 var tagName = media.tagName.toLowerCase(); 3384 if (tagName === 'div') { 3385 plyr.type = media.getAttribute('data-type'); 3386 plyr.embedId = media.getAttribute('data-video-id'); 3387 3388 // Clean up 3389 media.removeAttribute('data-type'); 3390 media.removeAttribute('data-video-id'); 3391 } else { 3392 plyr.type = tagName; 3393 config.crossorigin = (media.getAttribute('crossorigin') !== null); 3394 config.autoplay = (config.autoplay || (media.getAttribute('autoplay') !== null)); 3395 config.loop = (config.loop || (media.getAttribute('loop') !== null)); 3396 } 3397 3398 // Check for support 3399 plyr.supported = supported(plyr.type); 3400 3401 // If no native support, bail 3402 if (!plyr.supported.basic) { 3403 return; 3404 } 3405 3406 // Wrap media 3407 plyr.container = _wrap(media, document.createElement('div')); 3408 3409 // Allow focus to be captured 3410 plyr.container.setAttribute('tabindex', 0); 3411 3412 // Add style hook 3413 _toggleStyleHook(); 3414 3415 // Debug info 3416 _log('' + plyr.browser.name + ' ' + plyr.browser.version); 3417 3418 // Setup media 3419 _setupMedia(); 3420 3421 // Setup interface 3422 // If embed but not fully supported, setupInterface (to avoid flash of controls) and call ready now 3423 if (_inArray(config.types.html5, plyr.type) || (_inArray(config.types.embed, plyr.type) && !plyr.supported.full)) { 3424 // Setup UI 3425 _setupInterface(); 3426 3427 // Call ready 3428 _ready(); 3429 3430 // Set title on button and frame 3431 _setTitle(); 3432 } 3433 3434 // Successful setup 3435 plyr.init = true; 3436 } 3437 3438 // Setup the UI 3439 function _setupInterface() { 3440 // Don't setup interface if no support 3441 if (!plyr.supported.full) { 3442 _warn('Basic support only', plyr.type); 3443
3444 // Remove controls 3445 _remove(_getElement(config.selectors.controls.wrapper)); 3446 3447 // Remove large play 3448 _remove(_getElement(config.selectors.buttons.play)); 3449 3450 // Restore native controls 3451 _toggleNativeControls(true); 3452 3453 // Bail 3454 return; 3455 } 3456 3457 // Inject custom controls if not present 3458 var controlsMissing = !_getElements(config.selectors.controls.wrapper).length; 3459 if (controlsMissing) { 3460 // Inject custom controls 3461 _injectControls(); 3462 } 3463 3464 // Find the elements 3465 if (!_findElements()) { 3466 return; 3467 } 3468 3469 // If the controls are injected, re-bind listeners for controls 3470 if (controlsMissing) { 3471 _controlListeners(); 3472 } 3473 3474 // Media element listeners 3475 _mediaListeners(); 3476 3477 // Remove native controls 3478 _toggleNativeControls(); 3479 3480 // Setup fullscreen 3481 _setupFullscreen(); 3482 3483 // Captions 3484 _setupCaptions(); 3485 3486 // Set volume 3487 _setVolume(); 3488 _updateVolume(); 3489 3490 // Reset time display 3491 _timeUpdate(); 3492 3493 // Update the UI 3494 _checkPlaying(); 3495 } 3496 3497 api = { 3498 getOriginal: function () { 3499 return original; 3500 }, 3501 getContainer: function () { 3502 return plyr.container 3503 }, 3504 getEmbed: function () { 3505 return plyr.embed; 3506 }, 3507 getMedia: function () { 3508 return plyr.media; 3509 }, 3510 getType: function () { 3511 return plyr.type; 3512 }, 3513 getDuration: _getDuration, 3514 getCurrentTime: function () { 3515 return plyr.media.currentTime; 3516 }, 3517 getVolume: function () { 3518 return plyr.media.volume; 3519 }, 3520 isMuted: function () { 3521 return plyr.media.muted; 3522 }, 3523 isReady: function () { 3524 return _hasClass(plyr.container, config.classes.ready); 3525 }, 3526 isLoading: function () { 3527 return _hasClass(plyr.container, config.classes.loading); 3528 }, 3529 isPaused: function () { 3530 return plyr.media.paused; 3531 }, 3532 on: function (event, callback) { 3533 _on(plyr.container, event, callback); 3534 return this; 3535 }, 3536 play: _play, 3537 pause: _pause, 3538 stop: function () { 3539 _pause(); 3540 _seek(); 3541 }, 3542 restart: _seek, 3543 rewind: _rewind, 3544 forward: _forward, 3545 seek: _seek, 3546 source: _source, 3547 poster: _updatePoster, 3548 setVolume: _setVolume, 3549 togglePlay: _togglePlay, 3550 toggleMute: _toggleMute, 3551 toggleCaptions: _toggleCaptions, 3552 toggleFullscreen: _toggleFullscreen, 3553 toggleControls: _toggleControls, 3554 isFullscreen: function () { 3555 return plyr.isFullscreen || false; 3556 }, 3557 support: function (mimeType) { 3558 return _supportMime(plyr, mimeType); 3559 }, 3560 destroy: _destroy 3561 }; 3562 3563 // Everything done 3564 function _ready() { 3565 // Ready event at end of execution stack 3566 window.setTimeout(function () { 3567 _triggerEvent(plyr.media, 'ready'); 3568 }, 0); 3569 3570 // Set class hook on media element 3571 _toggleClass(plyr.media, defaults.classes.setup, true); 3572 3573 // Set container class for ready 3574 _toggleClass(plyr.container, config.classes.ready, true); 3575 3576 // Store a refernce to instance 3577 plyr.media.plyr = api; 3578 3579 // Autoplay 3580 if (config.autoplay) { 3581 _play(); 3582 } 3583 } 3584 3585 // Initialize instance 3586 _init(); 3587 3588 // If init failed, return null 3589 if (!plyr.init) { 3590 return null; 3591 } 3592 3593 return api; 3594 } 3595 3596 // Load a sprite 3597 function loadSprite(url, id) { 3598 var x = new XMLHttpRequest(); 3599 3600 // If the id is set and sprite exists, bail 3601 if (_is.string(id) && _is.htmlElement(document.querySelector('#' + id))) { 3602 return; 3603 } 3604 3605 // Create placeholder (to prevent loading twice) 3606 var container = document.createElement('div'); 3607 container.setAttribute('hidden', ''); 3608 if (_is.string(id)) { 3609 container.setAttribute('id', id); 3610 } 3611 document.body.insertBefore(container, document.body.childNodes[0]); 3612 3613 // Check for CORS support 3614 if ('withCredentials' in x) { 3615 x.open('GET', url, true); 3616 } else { 3617 return; 3618 } 3619 3620 // Inject hidden div with sprite on load 3621 x.onload = function () { 3622 container.innerHTML = x.responseText; 3623 } 3624 3625 x.send(); 3626 } 3627 3628 // Check for support 3629 function supported(type) { 3630 var browser = _browserSniff(), 3631 isOldIE = (browser.isIE && browser.version <= 9), 3632 isIos = browser.isIos, 3633 isIphone = browser.isIphone, 3634 audioSupport = !!document.createElement('audio').canPlayType, 3635 videoSupport = !!document.createElement('video').canPlayType, 3636 basic = false, 3637 full = false;
3638 3639 switch (type) { 3640 case 'video': 3641 basic = videoSupport; 3642 full = (basic && (!isOldIE && !isIphone)); 3643 break; 3644 3645 case 'audio': 3646 basic = audioSupport; 3647 full = (basic && !isOldIE); 3648 break; 3649 3650 // Vimeo does not seem to be supported on iOS via API 3651 // Issue raised https://github.com/vimeo/player.js/issues/87 3652 case 'vimeo': 3653 basic = true; 3654 full = (!isOldIE && !isIos); 3655 break; 3656 3657 case 'youtube': 3658 basic = true; 3659 full = (!isOldIE && !isIos); 3660 3661 // YouTube seems to work on iOS 10+ on iPad 3662 if (isIos && !isIphone && browser.version >= 10) { 3663 full = true; 3664 } 3665 3666 break; 3667 3668 case 'soundcloud': 3669 basic = true; 3670 full = (!isOldIE && !isIphone); 3671 break; 3672 3673 default: 3674 basic = (audioSupport && videoSupport); 3675 full = (basic && !isOldIE); 3676 } 3677 3678 return { 3679 basic: basic, 3680 full: full 3681 }; 3682 } 3683 3684 // Setup function 3685 function setup(targets, options) { 3686 // Get the players 3687 var players = [], 3688 instances = [], 3689 selector = [defaults.selectors.html5, defaults.selectors.embed].join(','); 3690 3691 // Select the elements 3692 if (_is.string(targets)) { 3693 // String selector passed 3694 targets = document.querySelectorAll(targets); 3695 } else if (_is.htmlElement(targets)) { 3696 // Single HTMLElement passed 3697 targets = [targets]; 3698 } else if (!_is.nodeList(targets) && !_is.array(targets) && !_is.string(targets)) { 3699 // No selector passed, possibly options as first argument 3700 // If options are the first argument 3701 if (_is.undefined(options) && _is.object(targets)) { 3702 options = targets; 3703 } 3704 3705 // Use default selector 3706 targets = document.querySelectorAll(selector); 3707 } 3708 3709 // Convert NodeList to array 3710 if (_is.nodeList(targets)) { 3711 targets = Array.prototype.slice.call(targets); 3712 } 3713 3714 // Bail if disabled or no basic support 3715 // You may want to disable certain UAs etc 3716 if (!supported().basic || !targets.length) { 3717 return false; 3718 } 3719 3720 // Add to container list 3721 function add(target, media) { 3722 if (!_hasClass(media, defaults.classes.hook)) { 3723 players.push({ 3724 // Always wrap in a <div> for styling 3725 //container: _wrap(media, document.createElement('div')), 3726 // Could be a container or the media itself 3727 target: target, 3728 // This should be the <video>, <audio> or <div> (YouTube/Vimeo) 3729 media: media 3730 }); 3731 } 3732 } 3733 3734 // Check if the targets have multiple media elements 3735 for (var i = 0; i < targets.length; i++) { 3736 var target = targets[i]; 3737 3738 // Get children 3739 var children = target.querySelectorAll(selector); 3740 3741 // If there's more than one media element child, wrap them 3742 if (children.length) { 3743 for (var x = 0; x < children.length; x++) { 3744 add(target, children[x]); 3745 } 3746 } else if (_matches(target, selector)) { 3747 // Target is media element 3748 add(target, target); 3749 } 3750 } 3751 3752 // Create a player instance for each element 3753 players.forEach(function (player) { 3754 var element = player.target, 3755 media = player.media, 3756 match = false; 3757 3758 // The target element can also be the media element 3759 if (media === element) { 3760 match = true; 3761 } 3762 3763 // Setup a player instance and add to the element 3764 // Create instance-specific config 3765 var data = {}; 3766 3767 // Try parsing data attribute config 3768 try { 3769 data = JSON.parse(element.getAttribute('data-plyr')); 3770 } catch (e) { 3771 } 3772 3773 var config = _extend({}, defaults, options, data); 3774 3775 // Bail if not enabled 3776 if (!config.enabled) { 3777 return null; 3778 } 3779 3780 // Create new instance 3781 var instance = new Plyr(media, config); 3782 3783 // Go to next if setup failed 3784 if (!_is.object(instance)) { 3785 return; 3786 } 3787 3788 // Listen for events if debugging 3789 if (config.debug) { 3790 var events = config.events.concat(['setup', 'statechange', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled']); 3791 3792 _on(instance.getContainer(), events.join(' '), function (event) { 3793 console.log([config.logPrefix, 'event:', event.type].join(' '), event.detail.plyr); 3794 }); 3795 } 3796 3797 // Callback 3798 _event(instance.getContainer(), 'setup', true, { 3799 plyr: instance 3800 }); 3801 3802 // Add to return array even if it's already setup 3803 instances.push(instance); 3804 }); 3805 3806 return instances; 3807 } 3808 3809 // Get all instances within a provided container 3810 function get(container) { 3811 if (_is.string(container)) { 3812 // Get selector if string passed 3813 container = document.querySelector(container); 3814 } else if (_is.undefined(container)) { 3815 // Use body by default to get all on page 3816 container = document.body; 3817 } 3818 3819 // If we have a HTML element 3820 if (_is.htmlElement(container)) { 3821 var elements = container.querySelectorAll('.' + defaults.classes.setup), 3822 instances = []; 3823
3824 Array.prototype.slice.call(elements).forEach(function (element) { 3825 if (_is.object(element.plyr)) { 3826 instances.push(element.plyr); 3827 } 3828 }); 3829 3830 return instances; 3831 } 3832 3833 return []; 3834 } 3835 3836 return { 3837 setup: setup, 3838 supported: supported, 3839 loadSprite: loadSprite, 3840 get: get 3841 }; 3842})); 3843 3844// Custom event polyfill 3845// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent 3846(function () { 3847 if (typeof window.CustomEvent === 'function') { 3848 return; 3849 } 3850 3851 function CustomEvent(event, params) { 3852 params = params || {bubbles: false, cancelable: false, detail: undefined}; 3853 var evt = document.createEvent('CustomEvent'); 3854 evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); 3855 return evt; 3856 } 3857 3858 CustomEvent.prototype = window.Event.prototype; 3859 3860 window.CustomEvent = CustomEvent; 3861})();
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.