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