PageSourceSearch

https://www.jbl.com.sg/on/demandware.static/Sites-JB-APAC-NCOM-Site/-/en_SG/v1790551988158/js/plyr.js

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

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