PageSourceSearch

https://www.siegenia.com/siauv4/bootstrap/dist/lib/bower/plyr/plyr.js

js siegenia.com collected 2026-10-02 10:30:50 UTC 114,852 bytes, 3,861 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.