This repository has been archived on 2026-01-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
plyr/src/js/plyr.js
T
Guru Prasad Srinivasa b5092c338c Added new configuration option 'handlers'
This option allows developers to attach their custom handlers
before internal plyr's handlers. This should enable developers
to have full control over all the buttons including the ability
to stop event propagation if desired.
2016-01-14 03:01:51 -05:00

2560 lines
91 KiB
JavaScript

// ==========================================================================
// Plyr
// plyr.js v1.5.0
// https://github.com/selz/plyr
// License: The MIT License (MIT)
// ==========================================================================
// Credits: http://paypal.github.io/accessible-html5-video-player/
// ==========================================================================
(function(root, factory) {
'use strict';
/*global define,module*/
if (typeof define === 'function' && define.amd) {
// AMD
define(null, function() { factory(root, document) });
} else if (typeof module === 'object') {
// Node, CommonJS-like
module.exports = factory(root, document);
} else {
// Browser globals (root is window)
root.plyr = factory(root, document);
}
}(this, function(window, document) {
'use strict';
/*global YT,$f*/
// Globals
var fullscreen, config, api = {};
// Default config
var defaults = {
enabled: true,
debug: false,
autoplay: false,
loop: false,
seekTime: 10,
volume: 5,
click: true,
tooltips: false,
displayDuration: true,
iconPrefix: 'icon',
selectors: {
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls'
},
labels: '[data-plyr] .sr-only, label .sr-only',
buttons: {
seek: '[data-plyr="seek"]',
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
forward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
volume: '[data-plyr="volume"]',
captions: '[data-plyr="captions"]',
fullscreen: '[data-plyr="fullscreen"]'
},
progress: {
container: '.plyr__progress',
buffer: '.plyr__progress--buffer',
played: '.plyr__progress--played'
},
captions: '.plyr__captions',
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration'
},
handlers: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
forward: null,
mute: null,
volume: null,
captions: null,
fullscreen: null
},
classes: {
videoWrapper: 'plyr__video-wrapper',
embedWrapper: 'plyr__video-embed',
type: 'plyr--{0}',
stopped: 'plyr--stopped',
playing: 'plyr--playing',
muted: 'plyr--muted',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
hidden: 'plyr__sr-only',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
captions: {
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active'
},
fullscreen: {
enabled: 'plyr--fullscreen-enabled',
active: 'plyr--fullscreen-active',
hideControls: 'plyr--fullscreen--hide-controls'
},
tabFocus: 'tab-focus'
},
captions: {
defaultActive: false
},
fullscreen: {
enabled: true,
fallback: true,
hideControls: true,
allowAudio: false
},
storage: {
enabled: true,
key: 'plyr_volume'
},
controls: ['restart', 'rewind', 'play', 'fast-forward', 'current-time', 'duration', 'mute', 'volume', 'captions', 'fullscreen'],
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
play: 'Play',
pause: 'Pause',
forward: 'Forward {seektime} secs',
played: 'played',
buffered: 'buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
toggleMute: 'Toggle Mute',
toggleCaptions: 'Toggle Captions',
toggleFullscreen: 'Toggle Fullscreen',
frameTitle: 'Player for {title}'
},
types: {
embed: ['youtube', 'vimeo'],
html5: ['video', 'audio']
},
urls: {
vimeo: {
api: 'http://cdn.plyr.io/froogaloop/1.0.0/plyr.froogaloop.js',
},
youtube: {
api: 'https://www.youtube.com/iframe_api'
}
}
};
// Build the default HTML
function _buildControls() {
// Open and add the progress and seek elements
var html = [
'<div class="plyr__controls">',
'<div class="plyr__progress">',
'<label for="seek{id}" class="plyr__sr-only">Seek</label>',
'<input id="seek{id}" class="plyr__progress--seek" type="range" min="0" max="100" step="0.5" value="0" data-plyr="seek">',
'<progress class="plyr__progress--played" max="100" value="0">',
'<span>0</span>% ' + config.i18n.played,
'</progress>',
'<progress class="plyr__progress--buffer" max="100" value="0">',
'<span>0</span>% ' + config.i18n.buffered,
'</progress>',
'</div>',
'<span class="plyr__controls--left">'];
// Restart button
if (_inArray(config.controls, 'restart')) {
html.push(
'<button type="button" data-plyr="restart">',
'<svg><use xlink:href="#' + config.iconPrefix + '-restart" /></svg>',
'<span class="plyr__sr-only">' + config.i18n.restart + '</span>',
'</button>'
);
}
// Rewind button
if (_inArray(config.controls, 'rewind')) {
html.push(
'<button type="button" data-plyr="rewind">',
'<svg><use xlink:href="#' + config.iconPrefix + '-rewind" /></svg>',
'<span class="plyr__sr-only">' + config.i18n.rewind + '</span>',
'</button>'
);
}
// Play/pause button
if (_inArray(config.controls, 'play')) {
html.push(
'<button type="button" data-plyr="play">',
'<svg><use xlink:href="#' + config.iconPrefix + '-play" /></svg>',
'<span class="plyr__sr-only">' + config.i18n.play + '</span>',
'</button>',
'<button type="button" data-plyr="pause">',
'<svg><use xlink:href="#' + config.iconPrefix + '-pause" /></svg>',
'<span class="plyr__sr-only">' + config.i18n.pause + '</span>',
'</button>'
);
}
// Fast forward button
if (_inArray(config.controls, 'fast-forward')) {
html.push(
'<button type="button" data-plyr="fast-forward">',
'<svg><use xlink:href="#' + config.iconPrefix + '-fast-forward" /></svg>',
'<span class="plyr__sr-only">' + config.i18n.forward + '</span>',
'</button>'
);
}
// Media current time display
if (_inArray(config.controls, 'current-time')) {
html.push(
'<span class="plyr__time">',
'<span class="plyr__sr-only">' + config.i18n.currentTime + '</span>',
'<span class="plyr__time--current">00:00</span>',
'</span>'
);
}
// Media duration display
if (_inArray(config.controls, 'duration')) {
html.push(
'<span class="plyr__time">',
'<span class="plyr__sr-only">' + config.i18n.duration + '</span>',
'<span class="plyr__time--duration">00:00</span>',
'</span>'
);
}
// Close left controls
html.push(
'</span>',
'<span class="plyr__controls--right">'
);
// Toggle mute button
if (_inArray(config.controls, 'mute')) {
html.push(
'<button type="button" data-plyr="mute">',
'<svg class="icon--muted"><use xlink:href="#' + config.iconPrefix + '-muted" /></svg>',
'<svg><use xlink:href="#' + config.iconPrefix + '-volume" /></svg>',
'<span class="plyr__sr-only">' + config.i18n.toggleMute + '</span>',
'</button>'
);
}
// Volume range control
if (_inArray(config.controls, 'volume')) {
html.push(
'<label for="volume{id}" class="plyr__sr-only">' + config.i18n.volume + '</label>',
'<input id="volume{id}" class="plyr__volume" type="range" min="0" max="10" value="5" data-plyr="volume">'
);
}
// Toggle captions button
if (_inArray(config.controls, 'captions')) {
html.push(
'<button type="button" data-plyr="captions">',
'<svg class="icon--captions-on"><use xlink:href="#' + config.iconPrefix + '-captions-on" /></svg>',
'<svg><use xlink:href="#' + config.iconPrefix + '-captions-off" /></svg>',
'<span class="plyr__sr-only">' + config.i18n.toggleCaptions + '</span>',
'</button>'
);
}
// Toggle fullscreen button
if (_inArray(config.controls, 'fullscreen')) {
html.push(
'<button type="button" data-plyr="fullscreen">',
'<svg class="icon--exit-fullscreen"><use xlink:href="#' + config.iconPrefix + '-exit-fullscreen" /></svg>',
'<svg><use xlink:href="#' + config.iconPrefix + '-enter-fullscreen" /></svg>',
'<span class="plyr__sr-only">' + config.i18n.toggleFullscreen + '</span>',
'</button>'
);
}
// Close everything
html.push(
'</span>',
'</div>'
);
return html.join('');
}
// Debugging
function _log(text, error) {
if (config.debug && window.console) {
console[(error ? 'error' : 'log')](text);
}
}
// Credits: http://paypal.github.io/accessible-html5-video-player/
// Unfortunately, due to mixed support, UA sniffing is required
function _browserSniff() {
var nAgt = navigator.userAgent,
name = navigator.appName,
fullVersion = '' + parseFloat(navigator.appVersion),
majorVersion = parseInt(navigator.appVersion, 10),
nameOffset,
verOffset,
ix;
// MSIE 11
if ((navigator.appVersion.indexOf('Windows NT') !== -1) && (navigator.appVersion.indexOf('rv:11') !== -1)) {
name = 'IE';
fullVersion = '11;';
}
// MSIE
else if ((verOffset=nAgt.indexOf('MSIE')) !== -1) {
name = 'IE';
fullVersion = nAgt.substring(verOffset + 5);
}
// Chrome
else if ((verOffset=nAgt.indexOf('Chrome')) !== -1) {
name = 'Chrome';
fullVersion = nAgt.substring(verOffset + 7);
}
// Safari
else if ((verOffset=nAgt.indexOf('Safari')) !== -1) {
name = 'Safari';
fullVersion = nAgt.substring(verOffset + 7);
if ((verOffset=nAgt.indexOf('Version')) !== -1) {
fullVersion = nAgt.substring(verOffset + 8);
}
}
// Firefox
else if ((verOffset=nAgt.indexOf('Firefox')) !== -1) {
name = 'Firefox';
fullVersion = nAgt.substring(verOffset + 8);
}
// In most other browsers, 'name/version' is at the end of userAgent
else if ((nameOffset=nAgt.lastIndexOf(' ') + 1) < (verOffset=nAgt.lastIndexOf('/'))) {
name = nAgt.substring(nameOffset,verOffset);
fullVersion = nAgt.substring(verOffset + 1);
if (name.toLowerCase() == name.toUpperCase()) {
name = navigator.appName;
}
}
// Trim the fullVersion string at semicolon/space if present
if ((ix = fullVersion.indexOf(';')) !== -1) {
fullVersion = fullVersion.substring(0, ix);
}
if ((ix = fullVersion.indexOf(' ')) !== -1) {
fullVersion = fullVersion.substring(0, ix);
}
// Get major version
majorVersion = parseInt('' + fullVersion, 10);
if (isNaN(majorVersion)) {
fullVersion = '' + parseFloat(navigator.appVersion);
majorVersion = parseInt(navigator.appVersion, 10);
}
// Return data
return {
name: name,
version: majorVersion,
ios: /(iPad|iPhone|iPod)/g.test(navigator.platform),
touch: 'ontouchstart' in document.documentElement
};
}
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplyr.com/test/h5mt.html
function _supportMime(plyr, mimeType) {
var media = plyr.media;
// Only check video types for video players
if (plyr.type == 'video') {
// Check type
switch (mimeType) {
case 'video/webm': return !!(media.canPlayType && media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''));
case 'video/mp4': return !!(media.canPlayType && media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''));
case 'video/ogg': return !!(media.canPlayType && media.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''));
}
}
// Only check audio types for audio players
else if (plyr.type == 'audio') {
// Check type
switch (mimeType) {
case 'audio/mpeg': return !!(media.canPlayType && media.canPlayType('audio/mpeg;').replace(/no/, ''));
case 'audio/ogg': return !!(media.canPlayType && media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, ''));
case 'audio/wav': return !!(media.canPlayType && media.canPlayType('audio/wav; codecs="1"').replace(/no/, ''));
}
}
// If we got this far, we're stuffed
return false;
}
// Inject a script
function _injectScript(source) {
if (document.querySelectorAll('script[src="' + source + '"]').length) {
return;
}
var tag = document.createElement('script');
tag.src = source;
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
}
// Element exists in an array
function _inArray(haystack, needle) {
return Array.prototype.indexOf && (haystack.indexOf(needle) != -1);
}
// Replace all
function _replaceAll(string, find, replace) {
return string.replace(new RegExp(find.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g, '\\$1'), 'g'), replace);
}
// Wrap an element
function _wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
if (!elements.length) {
elements = [elements];
}
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
for (var i = elements.length - 1; i >= 0; i--) {
var child = (i > 0) ? wrapper.cloneNode(true) : wrapper;
var element = elements[i];
// Cache the current parent and sibling.
var parent = element.parentNode;
var sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
}
else {
parent.appendChild(child);
}
}
}
// Unwrap an element
// http://plainjs.com/javascript/manipulation/unwrap-a-dom-element-35/
function _unwrap(wrapper) {
// Get the element's parent node
var parent = wrapper.parentNode;
// Move all children out of the element
while (wrapper.firstChild) {
parent.insertBefore(wrapper.firstChild, wrapper);
}
// Remove the empty element
parent.removeChild(wrapper);
}
// Remove an element
function _remove(element) {
element.parentNode.removeChild(element);
}
// Prepend child
function _prependChild(parent, element) {
parent.insertBefore(element, parent.firstChild);
}
// Set attributes
function _setAttributes(element, attributes) {
for (var key in attributes) {
element.setAttribute(key, (typeof attributes[key] === 'boolean' && attributes[key]) ? '' : attributes[key]);
}
}
// Insert a HTML element
function _insertElement(type, parent, attributes) {
// Create a new <element>
var element = document.createElement(type);
// Set all passed attributes
_setAttributes(element, attributes);
// Inject the new element
_prependChild(parent, element);
}
// Get a classname from selector
function _getClassname(selector) {
return selector.replace('.', '');
}
// Toggle class on an element
function _toggleClass(element, className, state) {
if (element) {
element.classList[state ? 'add' : 'remove'](className);
}
}
// Has class name
function _hasClass(element, className) {
if (element) {
return element.classList.contains(className);
}
return false;
}
// Toggle event
function _toggleHandler(element, events, callback, toggle) {
var eventList = events.split(' ');
// If a nodelist is passed, call itself on each node
if (element instanceof NodeList) {
for (var x = 0; x < element.length; x++) {
if (element[x] instanceof Node) {
_toggleHandler(element[x], arguments[1], arguments[2], arguments[3]);
}
}
return;
}
// If a single node is passed, bind the event listener
for (var i = 0; i < eventList.length; i++) {
element[toggle ? 'addEventListener' : 'removeEventListener'](eventList[i], callback, false);
}
}
// Bind event
function _on(element, events, callback) {
if (element) {
_toggleHandler(element, events, callback, true);
}
}
// Unbind event
function _off(element, events, callback) {
if (element) {
_toggleHandler(element, events, callback, false);
}
}
// Trigger event
function _triggerEvent(element, event) {
// Bail if no element
if(!element || !event) {
return;
}
// Create faux event
var fauxEvent = document.createEvent('MouseEvents');
// Set the event type
fauxEvent.initEvent(event, true, true);
// Dispatch the event
element.dispatchEvent(fauxEvent);
}
// Toggle aria-pressed state on a toggle button
function _toggleState(target, state) {
// Bail if no target
if(!target) {
return;
}
// Get state
state = (typeof state === 'boolean' ? state : !target.getAttribute('aria-pressed'));
// Set the attribute on target
target.setAttribute('aria-pressed', state);
return state;
}
// Get percentage
function _getPercentage(current, max) {
if (current === 0 || max === 0 || isNaN(current) || isNaN(max)) {
return 0;
}
return ((current / max) * 100).toFixed(2);
}
// Deep extend/merge two Objects
// http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/
// Removed call to arguments.callee (used explicit function name instead)
function _extend(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor && source[property].constructor === Object) {
destination[property] = destination[property] || {};
_extend(destination[property], source[property]);
}
else {
destination[property] = source[property];
}
}
return destination;
}
// Fullscreen API
function _fullscreen() {
var fullscreen = {
supportsFullScreen: false,
isFullScreen: function() { return false; },
requestFullScreen: function() {},
cancelFullScreen: function() {},
fullScreenEventName: '',
element: null,
prefix: ''
},
browserPrefixes = 'webkit moz o ms khtml'.split(' ');
// Check for native support
if (typeof document.cancelFullScreen !== 'undefined') {
fullscreen.supportsFullScreen = true;
}
else {
// Check for fullscreen support by vendor prefix
for (var i = 0, il = browserPrefixes.length; i < il; i++ ) {
fullscreen.prefix = browserPrefixes[i];
if (typeof document[fullscreen.prefix + 'CancelFullScreen'] !== 'undefined') {
fullscreen.supportsFullScreen = true;
break;
}
// Special case for MS (when isn't it?)
else if (typeof document.msExitFullscreen !== 'undefined' && document.msFullscreenEnabled) {
fullscreen.prefix = 'ms';
fullscreen.supportsFullScreen = true;
break;
}
}
}
// Update methods to do something useful
if (fullscreen.supportsFullScreen) {
// Yet again Microsoft awesomeness,
// Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes
fullscreen.fullScreenEventName = (fullscreen.prefix == 'ms' ? 'MSFullscreenChange' : fullscreen.prefix + 'fullscreenchange');
fullscreen.isFullScreen = function(element) {
if (typeof element === 'undefined') {
element = document.body;
}
switch (this.prefix) {
case '':
return document.fullscreenElement == element;
case 'moz':
return document.mozFullScreenElement == element;
default:
return document[this.prefix + 'FullscreenElement'] == element;
}
};
fullscreen.requestFullScreen = function(element) {
if (typeof element === 'undefined') {
element = document.body;
}
return (this.prefix === '') ? element.requestFullScreen() : element[this.prefix + (this.prefix == 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')]();
};
fullscreen.cancelFullScreen = function() {
return (this.prefix === '') ? document.cancelFullScreen() : document[this.prefix + (this.prefix == 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')]();
};
fullscreen.element = function() {
return (this.prefix === '') ? document.fullscreenElement : document[this.prefix + 'FullscreenElement'];
};
}
return fullscreen;
}
// Local storage
function _storage() {
var storage = {
supported: (function() {
if(!('localStorage' in window)) {
return false;
}
// Try to use it (it might be disabled, e.g. user is in private/porn mode)
// see: https://github.com/Selz/plyr/issues/131
try {
// Add test item
window.localStorage.setItem('___test', 'OK');
// Get the test item
var result = window.localStorage.getItem('___test');
// Clean up
window.localStorage.removeItem('___test');
// Check if value matches
return (result === 'OK');
}
catch (e) {
return false;
}
return false;
})()
};
return storage;
}
// Player instance
function Plyr(container) {
var plyr = this;
plyr.container = container;
// Captions functions
// Seek the manual caption time and update UI
function _seekManualCaptions(time) {
// If it's not video, or we're using textTracks, bail.
if (plyr.usingTextTracks || plyr.type !== 'video' || !plyr.supported.full) {
return;
}
// Reset subcount
plyr.subcount = 0;
// Check time is a number, if not use currentTime
// IE has a bug where currentTime doesn't go to 0
// https://twitter.com/Sam_Potts/status/573715746506731521
time = typeof time === 'number' ? time : plyr.media.currentTime;
// If there's no subs available, bail
if (!plyr.captions[plyr.subcount]) {
return;
}
while (_timecodeMax(plyr.captions[plyr.subcount][0]) < time.toFixed(1)) {
plyr.subcount++;
if (plyr.subcount > plyr.captions.length-1) {
plyr.subcount = plyr.captions.length-1;
break;
}
}
// Check if the next caption is in the current time range
if (plyr.media.currentTime.toFixed(1) >= _timecodeMin(plyr.captions[plyr.subcount][0]) &&
plyr.media.currentTime.toFixed(1) <= _timecodeMax(plyr.captions[plyr.subcount][0])) {
plyr.currentCaption = plyr.captions[plyr.subcount][1];
// Trim caption text
var content = plyr.currentCaption.trim();
// Render the caption (only if changed)
if (plyr.captionsContainer.innerHTML != content) {
// Empty caption
// Otherwise NVDA reads it twice
plyr.captionsContainer.innerHTML = '';
// Set new caption text
plyr.captionsContainer.innerHTML = content;
}
}
else {
plyr.captionsContainer.innerHTML = '';
}
}
// Display captions container and button (for initialization)
function _showCaptions() {
// If there's no caption toggle, bail
if (!plyr.buttons.captions) {
return;
}
_toggleClass(plyr.container, config.classes.captions.enabled, true);
if (config.captions.defaultActive) {
_toggleClass(plyr.container, config.classes.captions.active, true);
_toggleState(plyr.buttons.captions, true);
}
}
// Utilities for caption time codes
function _timecodeMin(tc) {
var tcpair = [];
tcpair = tc.split(' --> ');
return _subTcSecs(tcpair[0]);
}
function _timecodeMax(tc) {
var tcpair = [];
tcpair = tc.split(' --> ');
return _subTcSecs(tcpair[1]);
}
function _subTcSecs(tc) {
if (tc === null || tc === undefined) {
return 0;
}
else {
var tc1 = [],
tc2 = [],
seconds;
tc1 = tc.split(',');
tc2 = tc1[0].split(':');
seconds = Math.floor(tc2[0]*60*60) + Math.floor(tc2[1]*60) + Math.floor(tc2[2]);
return seconds;
}
}
// Find all elements
function _getElements(selector) {
return plyr.container.querySelectorAll(selector);
}
// Find a single element
function _getElement(selector) {
return _getElements(selector)[0];
}
// Determine if we're in an iframe
function _inFrame() {
try {
return window.self !== window.top;
}
catch (e) {
return true;
}
}
// Insert controls
function _injectControls() {
// Make a copy of the html
var html = config.html;
// Insert custom video controls
_log('Injecting custom controls.');
// If no controls are specified, create default
if (!html) {
html = _buildControls();
}
// Replace seek time instances
html = _replaceAll(html, '{seektime}', config.seekTime);
// Replace all id references with random numbers
html = _replaceAll(html, '{id}', Math.floor(Math.random() * (10000)));
// Controls container
var container;
// Inject to custom location
if (config.selectors.controls.container !== null) {
container = config.selectors.controls.container;
if(typeof selector === 'string') {
container = document.querySelector(container);
}
}
// Inject into the container by default
if (!(container instanceof HTMLElement)) {
container = plyr.container
}
// Inject controls HTML
container.insertAdjacentHTML('beforeend', html);
// Setup tooltips
if (config.tooltips) {
var labels = _getElements(config.selectors.labels);
for (var i = labels.length - 1; i >= 0; i--) {
var label = labels[i];
_toggleClass(label, config.classes.hidden, false);
_toggleClass(label, config.classes.tooltip, true);
}
}
}
// Find the UI controls and store references
function _findElements() {
try {
plyr.controls = _getElement(config.selectors.controls.wrapper);
// Buttons
plyr.buttons = {};
plyr.buttons.seek = _getElement(config.selectors.buttons.seek);
plyr.buttons.play = _getElement(config.selectors.buttons.play);
plyr.buttons.pause = _getElement(config.selectors.buttons.pause);
plyr.buttons.restart = _getElement(config.selectors.buttons.restart);
plyr.buttons.rewind = _getElement(config.selectors.buttons.rewind);
plyr.buttons.forward = _getElement(config.selectors.buttons.forward);
plyr.buttons.fullscreen = _getElement(config.selectors.buttons.fullscreen);
// Inputs
plyr.buttons.volume = _getElement(config.selectors.buttons.volume);
plyr.buttons.mute = _getElement(config.selectors.buttons.mute);
plyr.buttons.captions = _getElement(config.selectors.buttons.captions);
plyr.checkboxes = _getElements('[type="checkbox"]');
// Progress
plyr.progress = {};
plyr.progress.container = _getElement(config.selectors.progress.container);
// Progress - Buffering
plyr.progress.buffer = {};
plyr.progress.buffer.bar = _getElement(config.selectors.progress.buffer);
plyr.progress.buffer.text = plyr.progress.buffer.bar && plyr.progress.buffer.bar.getElementsByTagName('span')[0];
// Progress - Played
plyr.progress.played = {};
plyr.progress.played.bar = _getElement(config.selectors.progress.played);
plyr.progress.played.text = plyr.progress.played.bar && plyr.progress.played.bar.getElementsByTagName('span')[0];
// Volume
plyr.volume = _getElement(config.selectors.buttons.volume);
// Timing
plyr.duration = _getElement(config.selectors.duration);
plyr.currentTime = _getElement(config.selectors.currentTime);
plyr.seekTime = _getElements(config.selectors.seekTime);
return true;
}
catch(e) {
_log('It looks like there\'s a problem with your controls html. Bailing.', true);
// Restore native video controls
plyr.media.setAttribute('controls', '');
return false;
}
}
// Setup aria attribute for play and iframe title
function _setTitle(iframe) {
// Find the current text
var label = config.i18n.play;
// If there's a media title set, use that for the label
if (typeof(config.title) !== 'undefined' && config.title.length) {
label += ', ' + config.title;
}
// If there's a play button, set label
if (plyr.buttons.play) {
plyr.buttons.play.setAttribute('aria-label', label);
}
// Set iframe title
// https://github.com/Selz/plyr/issues/124
if (iframe instanceof HTMLElement) {
iframe.setAttribute('title', config.i18n.frameTitle.replace('{title}', config.title));
}
}
// Setup media
function _setupMedia() {
// If there's no media, bail
if (!plyr.media) {
_log('No audio or video element found!', true);
return false;
}
if (plyr.supported.full) {
// Remove native video controls
plyr.media.removeAttribute('controls');
// Add type class
_toggleClass(plyr.container, config.classes.type.replace('{0}', plyr.type), true);
// If there's no autoplay attribute, assume the video is stopped and add state class
_toggleClass(plyr.container, config.classes.stopped, config.autoplay);
// Add iOS class
_toggleClass(plyr.container, config.classes.isIos, plyr.browser.ios);
// Add touch class
_toggleClass(plyr.container, config.classes.isTouch, plyr.browser.touch);
// Inject the player wrapper
if (plyr.type === 'video') {
// Create the wrapper div
var wrapper = document.createElement('div');
wrapper.setAttribute('class', config.classes.videoWrapper);
// Wrap the video in a container
_wrap(plyr.media, wrapper);
// Cache the container
plyr.videoContainer = wrapper;
}
}
// Embeds
if (_inArray(config.types.embed, plyr.type)) {
_setupEmbed();
// Clean up
plyr.embedId = null;
}
else {
// Autoplay
if (config.autoplay) {
_play();
}
}
}
// Setup YouTube/Vimeo
function _setupEmbed() {
var container = document.createElement('div'),
videoId = plyr.embedId,
id = plyr.type + '-' + Math.floor(Math.random() * (10000));
// Remove old containers
var containers = _getElements('[id^="' + plyr.type + '-"]');
for (var i = containers.length - 1; i >= 0; i--) {
_remove(containers[i]);
}
// Add embed class for responsive
_toggleClass(plyr.media, config.classes.videoWrapper, true);
_toggleClass(plyr.media, config.classes.embedWrapper, true);
// YouTube
if (plyr.type === 'youtube') {
// Create the YouTube container
plyr.media.appendChild(container);
// Set ID
container.setAttribute('id', id);
// Setup API
if (typeof YT === 'object') {
_youTubeReady(videoId, container);
}
else {
// Load the API
_injectScript(config.urls.youtube.api);
// Setup callback for the API
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
// Add to queue
window.onYouTubeReadyCallbacks.push(function() { _youTubeReady(videoId, container) });
// Set callback to process queue
window.onYouTubeIframeAPIReady = function () {
window.onYouTubeReadyCallbacks.forEach(function(callback) { callback(); });
};
}
}
// Vimeo
else if (plyr.type === 'vimeo') {
// Inject the iframe
var iframe = document.createElement('iframe');
// Watch for iframe load
iframe.loaded = false;
_on(iframe, 'load', function() { iframe.loaded = true; });
_setAttributes(iframe, {
'src': 'https://player.vimeo.com/video/' + videoId + '?player_id=' + id + '&api=1&badge=0&byline=0&portrait=0&title=0',
'id': id,
'webkitallowfullscreen': '',
'mozallowfullscreen': '',
'allowfullscreen': '',
'frameborder': 0
});
container.appendChild(iframe);
plyr.media.appendChild(container);
// Load the API
if (!('$f' in window)) {
_injectScript(config.urls.vimeo.api);
}
// Wait for fragaloop load
var timer = window.setInterval(function() {
if ('$f' in window && iframe.loaded) {
window.clearInterval(timer);
_vimeoReady.call(iframe);
}
}, 50);
}
}
// When embeds are ready
function _embedReady() {
// Inject and update UI
if (plyr.supported.full) {
// Only setup controls once
if (!plyr.container.querySelectorAll(config.selectors.controls.wrapper).length) {
_setupInterface();
}
}
// Set title
_setTitle(_getElement('iframe'));
// Set the volume
_setVolume();
_updateVolume();
}
// Handle YouTube API ready
function _youTubeReady(videoId, container) {
// Setup timers object
// We have to poll YouTube for updates
if (!('timer' in plyr)) {
plyr.timer = {};
}
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
plyr.embed = new YT.Player(container.id, {
videoId: videoId,
playerVars: {
autoplay: (config.autoplay ? 1 : 0),
controls: (plyr.supported.full ? 0 : 1),
rel: 0,
showinfo: 0,
iv_load_policy: 3,
cc_load_policy: (config.captions.defaultActive ? 1 : 0),
cc_lang_pref: 'en',
wmode: 'transparent',
modestbranding: 1,
disablekb: 1,
origin: '*' // https://code.google.com/p/gdata-issues/issues/detail?id=5788#c45
},
events: {
'onReady': function(event) {
// Get the instance
var instance = event.target;
// Create a faux HTML5 API using the YouTube API
plyr.media.play = function() {
instance.playVideo();
plyr.media.paused = false;
};
plyr.media.pause = function() {
instance.pauseVideo();
plyr.media.paused = true;
};
plyr.media.stop = function() {
instance.stopVideo();
plyr.media.paused = true;
};
plyr.media.duration = instance.getDuration();
plyr.media.paused = !config.autoplay;
plyr.media.currentTime = instance.getCurrentTime();
plyr.media.muted = instance.isMuted();
// Trigger timeupdate
_triggerEvent(plyr.media, 'timeupdate');
// Reset timer
window.clearInterval(plyr.timer.buffering);
// Setup buffering
plyr.timer.buffering = window.setInterval(function() {
// Get loaded % from YouTube
plyr.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress
_triggerEvent(plyr.media, 'progress');
// Bail if we're at 100%
if (plyr.media.buffered === 1) {
window.clearInterval(plyr.timer.buffering);
}
}, 200);
// Update UI
_embedReady();
// Display duration if available
if (config.displayDuration) {
_displayDuration();
}
},
'onStateChange': function(event) {
// Get the instance
var instance = event.target;
// Reset timer
window.clearInterval(plyr.timer.playing);
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case 0:
plyr.media.paused = true;
_triggerEvent(plyr.media, 'ended');
break;
case 1:
plyr.media.paused = false;
_triggerEvent(plyr.media, 'play');
// Poll to get playback progress
plyr.timer.playing = window.setInterval(function() {
// Set the current time
plyr.media.currentTime = instance.getCurrentTime();
// Trigger timeupdate
_triggerEvent(plyr.media, 'timeupdate');
}, 200);
break;
case 2:
plyr.media.paused = true;
_triggerEvent(plyr.media, 'pause');
}
}
}
});
}
// Vimeo ready
function _vimeoReady() {
/* jshint validthis: true */
plyr.embed = $f(this);
// Setup on ready
plyr.embed.addEvent('ready', function() {
// Create a faux HTML5 API using the Vimeo API
plyr.media.play = function() {
plyr.embed.api('play');
plyr.media.paused = false;
};
plyr.media.pause = function() {
plyr.embed.api('pause');
plyr.media.paused = true;
};
plyr.media.stop = function() {
plyr.embed.api('stop');
plyr.media.paused = true;
};
plyr.media.paused = !config.autoplay;
plyr.media.currentTime = 0;
// Update UI
_embedReady();
plyr.embed.api('getCurrentTime', function (value) {
plyr.media.currentTime = value;
// Trigger timeupdate
_triggerEvent(plyr.media, 'timeupdate');
});
plyr.embed.api('getDuration', function(value) {
plyr.media.duration = value;
// Display duration if available
if (plyr.supported.full && config.displayDuration) {
_displayDuration();
}
});
plyr.embed.addEvent('play', function() {
plyr.media.paused = false;
_triggerEvent(plyr.media, 'play');
});
plyr.embed.addEvent('pause', function() {
plyr.media.paused = true;
_triggerEvent(plyr.media, 'pause');
});
plyr.embed.addEvent('playProgress', function(data) {
plyr.media.currentTime = data.seconds;
_triggerEvent(plyr.media, 'timeupdate');
});
plyr.embed.addEvent('loadProgress', function(data) {
plyr.media.buffered = data.percent;
_triggerEvent(plyr.media, 'progress');
});
plyr.embed.addEvent('finish', function() {
plyr.media.paused = true;
_triggerEvent(plyr.media, 'ended');
});
// Always seek to 0
//plyr.embed.api('seekTo', 0);
// Prevent autoplay if needed (seek will play)
//if (!config.autoplay) {
// plyr.embed.api('pause');
//}
});
}
// Setup captions
function _setupCaptions() {
if (plyr.type === 'video') {
// Inject the container
if (!_getElement(config.selectors.captions)) {
plyr.videoContainer.insertAdjacentHTML('afterbegin', '<div class="' + _getClassname(config.selectors.captions) + '"><span></span></div>');
}
// Cache selector
plyr.captionsContainer = _getElement(config.selectors.captions).querySelector('span');
// Determine if HTML5 textTracks is supported
plyr.usingTextTracks = false;
if (plyr.media.textTracks) {
plyr.usingTextTracks = true;
}
// Get URL of caption file if exists
var captionSrc = '',
kind,
children = plyr.media.childNodes;
for (var i = 0; i < children.length; i++) {
if (children[i].nodeName.toLowerCase() === 'track') {
kind = children[i].kind;
if (kind === 'captions' || kind === 'subtitles') {
captionSrc = children[i].getAttribute('src');
}
}
}
// Record if caption file exists or not
plyr.captionExists = true;
if (captionSrc === '') {
plyr.captionExists = false;
_log('No caption track found.');
}
else {
_log('Caption track found; URI: ' + captionSrc);
}
// If no caption file exists, hide container for caption text
if (!plyr.captionExists) {
_toggleClass(plyr.container, config.classes.captions.enabled);
}
// If caption file exists, process captions
else {
// Turn off native caption rendering to avoid double captions
// This doesn't seem to work in Safari 7+, so the <track> elements are removed from the dom below
var tracks = plyr.media.textTracks;
for (var x = 0; x < tracks.length; x++) {
tracks[x].mode = 'hidden';
}
// Enable UI
_showCaptions(plyr);
// Disable unsupported browsers than report false positive
if ((plyr.browser.name === 'IE' && plyr.browser.version >= 10) ||
(plyr.browser.name === 'Firefox' && plyr.browser.version >= 31) ||
(plyr.browser.name === 'Chrome' && plyr.browser.version >= 43) ||
(plyr.browser.name === 'Safari' && plyr.browser.version >= 7)) {
// Debugging
_log('Detected unsupported browser for HTML5 captions. Using fallback.');
// Set to false so skips to 'manual' captioning
plyr.usingTextTracks = false;
}
// Rendering caption tracks
// Native support required - http://caniuse.com/webvtt
if (plyr.usingTextTracks) {
_log('TextTracks supported.');
for (var y = 0; y < tracks.length; y++) {
var track = tracks[y];
if (track.kind === 'captions' || track.kind === 'subtitles') {
_on(track, 'cuechange', function() {
// Clear container
plyr.captionsContainer.innerHTML = '';
// Display a cue, if there is one
if (this.activeCues[0] && this.activeCues[0].hasOwnProperty('text')) {
plyr.captionsContainer.appendChild(this.activeCues[0].getCueAsHTML().trim());
}
});
}
}
}
// Caption tracks not natively supported
else {
_log('TextTracks not supported so rendering captions manually.');
// Render captions from array at appropriate time
plyr.currentCaption = '';
plyr.captions = [];
if (captionSrc !== '') {
// Create XMLHttpRequest Object
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var records = [],
record,
req = xhr.responseText;
records = req.split('\n\n');
for (var r = 0; r < records.length; r++) {
record = records[r];
plyr.captions[r] = [];
plyr.captions[r] = record.split('\n');
}
// Remove first element ('VTT')
plyr.captions.shift();
_log('Successfully loaded the caption file via AJAX.');
}
else {
_log('There was a problem loading the caption file via AJAX.', true);
}
}
};
xhr.open('get', captionSrc, true);
xhr.send();
}
}
// If Safari 7+, removing track from DOM [see 'turn off native caption rendering' above]
if (plyr.browser.name === 'Safari' && plyr.browser.version >= 7) {
_log('Safari 7+ detected; removing track from DOM.');
// Find all <track> elements
tracks = plyr.media.getElementsByTagName('track');
// Loop through and remove one by one
for (var t = 0; t < tracks.length; t++) {
plyr.media.removeChild(tracks[t]);
}
}
}
}
}
// Setup fullscreen
function _setupFullscreen() {
if ((plyr.type != 'audio' || config.fullscreen.allowAudio) && config.fullscreen.enabled) {
// Check for native support
var nativeSupport = fullscreen.supportsFullScreen;
if (nativeSupport || (config.fullscreen.fallback && !_inFrame())) {
_log((nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled.');
// Add styling hook
_toggleClass(plyr.container, config.classes.fullscreen.enabled, true);
}
else {
_log('Fullscreen not supported and fallback disabled.');
}
// Toggle state
_toggleState(plyr.buttons.fullscreen, false);
// Set control hide class hook
if (config.fullscreen.hideControls) {
_toggleClass(plyr.container, config.classes.fullscreen.hideControls, true);
}
}
}
// Play media
function _play() {
if('play' in plyr.media) {
plyr.media.play();
}
}
// Pause media
function _pause() {
if('pause' in plyr.media) {
plyr.media.pause();
}
}
// Toggle playback
function _togglePlay(toggle) {
// Play
if (toggle === true) {
_play();
}
// Pause
else if (toggle === false) {
_pause();
}
// True toggle
else {
plyr.media[plyr.media.paused ? 'play' : 'pause']();
}
}
// Rewind
function _rewind(seekTime) {
// Use default if needed
if (typeof seekTime !== 'number') {
seekTime = config.seekTime;
}
_seek(plyr.media.currentTime - seekTime);
}
// Fast forward
function _forward(seekTime) {
// Use default if needed
if (typeof seekTime !== 'number') {
seekTime = config.seekTime;
}
_seek(plyr.media.currentTime + seekTime);
}
// Seek to time
// The input parameter can be an event or a number
function _seek(input) {
var targetTime = 0,
paused = plyr.media.paused;
// Explicit position
if (typeof input === 'number') {
targetTime = input;
}
// Event
else if (typeof input === 'object' && (input.type === 'input' || input.type === 'change')) {
// It's the seek slider
// Seek to the selected time
targetTime = ((input.target.value / input.target.max) * plyr.media.duration);
}
// Normalise targetTime
if (targetTime < 0) {
targetTime = 0;
}
else if (targetTime > plyr.media.duration) {
targetTime = plyr.media.duration;
}
// Set the current time
// Try/catch incase the media isn't set and we're calling seek() from source() and IE moans
try {
plyr.media.currentTime = targetTime.toFixed(1);
}
catch(e) {}
// Trigger timeupdate for embed and restore pause state
if ('embed' in plyr) {
// YouTube
if (plyr.type === 'youtube') {
plyr.embed.seekTo(targetTime);
}
// Vimeo
if (plyr.type === 'vimeo') {
plyr.embed.api('seekTo', targetTime);
}
// Trigger timeupdate
_triggerEvent(plyr.media, 'timeupdate');
if (paused) {
_pause();
}
}
// Logging
_log('Seeking to ' + plyr.media.currentTime + ' seconds');
// Special handling for 'manual' captions
_seekManualCaptions(targetTime);
}
// Check playing state
function _checkPlaying() {
_toggleClass(plyr.container, config.classes.playing, !plyr.media.paused);
_toggleClass(plyr.container, config.classes.stopped, plyr.media.paused);
}
// Toggle fullscreen
function _toggleFullscreen(event) {
// Check for native support
var nativeSupport = fullscreen.supportsFullScreen;
// If it's a fullscreen change event, it's probably a native close
if (event && event.type === fullscreen.fullScreenEventName) {
plyr.isFullscreen = fullscreen.isFullScreen(plyr.container);
}
// If there's native support, use it
else if (nativeSupport) {
// Request fullscreen
if (!fullscreen.isFullScreen(plyr.container)) {
fullscreen.requestFullScreen(plyr.container);
}
// Bail from fullscreen
else {
fullscreen.cancelFullScreen();
}
// Check if we're actually full screen (it could fail)
plyr.isFullscreen = fullscreen.isFullScreen(plyr.container);
}
else {
// Otherwise, it's a simple toggle
plyr.isFullscreen = !plyr.isFullscreen;
// Bind/unbind escape key
if (plyr.isFullscreen) {
_on(document, 'keyup', _handleEscapeFullscreen);
document.body.style.overflow = 'hidden';
}
else {
_off(document, 'keyup', _handleEscapeFullscreen);
document.body.style.overflow = '';
}
}
// Set class hook
_toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen);
// Set button state
_toggleState(plyr.buttons.fullscreen, plyr.isFullscreen);
// Toggle controls visibility based on mouse movement and location
var hoverTimer, isMouseOver = false;
// Show the player controls
function _showControls() {
// Set shown class
_toggleClass(plyr.container, config.classes.hover, true);
// Clear timer every movement
window.clearTimeout(hoverTimer);
// If the mouse is not over the controls, set a timeout to hide them
if (!isMouseOver) {
hoverTimer = window.setTimeout(function() {
_toggleClass(plyr.container, config.classes.hover, false);
}, 2000);
}
}
// Check mouse is over the controls
function _setMouseOver (event) {
isMouseOver = (event.type === 'mouseenter');
}
if (config.fullscreen.hideControls) {
// Hide on entering full screen
_toggleClass(plyr.controls, config.classes.hover, false);
// Keep an eye on the mouse location in relation to controls
_toggleHandler(plyr.controls, 'mouseenter mouseleave', _setMouseOver, plyr.isFullscreen);
// Show the controls on mouse move
_toggleHandler(plyr.container, 'mousemove', _showControls, plyr.isFullscreen);
}
}
// Bail from faux-fullscreen
function _handleEscapeFullscreen(event) {
// If it's a keypress and not escape, bail
if ((event.which || event.charCode || event.keyCode) === 27 && plyr.isFullscreen) {
_toggleFullscreen();
}
}
// Mute
function _toggleMute(muted) {
// If the method is called without parameter, toggle based on current value
if (typeof muted !== 'boolean') {
muted = !plyr.media.muted;
}
// Set button state
_toggleState(plyr.buttons.mute, muted);
// Set mute on the player
plyr.media.muted = muted;
// YouTube
if (plyr.type === 'youtube') {
plyr.embed[plyr.media.muted ? 'mute' : 'unMute']();
// Trigger timeupdate
_triggerEvent(plyr.media, 'volumechange');
}
// Vimeo
if (plyr.type === 'vimeo') {
if (plyr.media.muted) {
plyr.embed.api('setVolume', 0);
}
else {
plyr.embed.api('setVolume', parseFloat(config.volume / 10));
}
// Trigger timeupdate
_triggerEvent(plyr.media, 'volumechange');
}
}
// Set volume
function _setVolume(volume) {
// Use default if no value specified
if (typeof volume === 'undefined') {
if (config.storage.enabled && _storage().supported) {
volume = window.localStorage[config.storage.key] || config.volume;
}
else {
volume = config.volume;
}
}
// Maximum is 10
if (volume > 10) {
volume = 10;
}
// Minimum is 0
if (volume < 0) {
volume = 0;
}
// Set the player volume
plyr.media.volume = parseFloat(volume / 10);
// Store in config
config.volume = volume;
// YouTube
if (plyr.type === 'youtube') {
plyr.embed.setVolume(plyr.media.volume * 100);
}
// Vimeo
if (plyr.type === 'vimeo') {
plyr.embed.api('setVolume', plyr.media.volume);
}
// Trigger volumechange for embeds
if ('embed' in plyr) {
_triggerEvent(plyr.media, 'volumechange');
}
// Toggle muted state
if (plyr.media.muted && volume > 0) {
_toggleMute();
}
}
// Update volume UI and storage
function _updateVolume() {
// Get the current volume
var volume = plyr.media.muted ? 0 : (plyr.media.volume * 10);
// Update the <input type="range"> if present
if (plyr.supported.full && plyr.volume) {
plyr.volume.value = volume;
}
// Store the volume in storage
if (config.storage.enabled && _storage().supported) {
window.localStorage.setItem(config.storage.key, volume);
}
// Toggle class if muted
_toggleClass(plyr.container, config.classes.muted, (volume === 0));
// Update checkbox for mute state
if (plyr.supported.full && plyr.buttons.mute) {
_toggleState(plyr.buttons.mute, (volume === 0));
}
}
// Toggle captions
function _toggleCaptions(show) {
// If there's no full support, or there's no caption toggle
if (!plyr.supported.full || !plyr.buttons.captions) {
return;
}
// If the method is called without parameter, toggle based on current value
if (typeof show !== 'boolean') {
show = (plyr.container.className.indexOf(config.classes.captions.active) === -1);
}
// Set global
plyr.captionsEnabled = show;
// Toggle state
_toggleState(plyr.buttons.captions, plyr.captionsEnabled);
// Add class hook
_toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled);
}
// Check if media is loading
function _checkLoading(event) {
var loading = (event.type === 'waiting');
// Clear timer
clearTimeout(plyr.loadingTimer);
// Timer to prevent flicker when seeking
plyr.loadingTimer = setTimeout(function() {
_toggleClass(plyr.container, config.classes.loading, loading);
}, (loading ? 250 : 0));
}
// Update <progress> elements
function _updateProgress(event) {
var progress = plyr.progress.played.bar,
text = plyr.progress.played.text,
value = 0;
if (event) {
switch (event.type) {
// Video playing
case 'timeupdate':
case 'seeking':
value = _getPercentage(plyr.media.currentTime, plyr.media.duration);
// Set seek range value only if it's a 'natural' time event
if (event.type == 'timeupdate' && plyr.buttons.seek) {
plyr.buttons.seek.value = value;
}
break;
// Events from seek range
case 'change':
case 'input':
value = event.target.value;
break;
// Check buffer status
case 'playing':
case 'progress':
progress = plyr.progress.buffer.bar;
text = plyr.progress.buffer.text;
value = (function() {
var buffered = plyr.media.buffered;
// HTML5
if (buffered && buffered.length) {
return _getPercentage(buffered.end(0), plyr.media.duration);
}
// YouTube returns between 0 and 1
else if (typeof buffered === 'number') {
return (buffered * 100);
}
return 0;
})();
}
}
// Set values
if (progress) {
progress.value = value;
}
if (text) {
text.innerHTML = value;
}
}
// Update the displayed time
function _updateTimeDisplay(time, element) {
// Bail if there's no duration display
if (!element) {
return;
}
// Fallback to 0
if (isNaN(time)) {
time = 0;
}
plyr.secs = parseInt(time % 60);
plyr.mins = parseInt((time / 60) % 60);
plyr.hours = parseInt(((time / 60) / 60) % 60);
// Do we need to display hours?
var displayHours = (parseInt(((plyr.media.duration / 60) / 60) % 60) > 0);
// Ensure it's two digits. For example, 03 rather than 3.
plyr.secs = ('0' + plyr.secs).slice(-2);
plyr.mins = ('0' + plyr.mins).slice(-2);
// Render
element.innerHTML = (displayHours ? plyr.hours + ':' : '') + plyr.mins + ':' + plyr.secs;
}
// Show the duration on metadataloaded
function _displayDuration() {
var duration = plyr.media.duration || 0;
// If there's only one time display, display duration there
if (!plyr.duration && config.displayDuration && plyr.media.paused) {
_updateTimeDisplay(duration, plyr.currentTime);
}
// If there's a duration element, update content
if (plyr.duration) {
_updateTimeDisplay(duration, plyr.duration);
}
}
// Handle time change event
function _timeUpdate(event) {
// Duration
_updateTimeDisplay(plyr.media.currentTime, plyr.currentTime);
// Playing progress
_updateProgress(event);
}
// Add elements to HTML5 media (source, tracks, etc)
function _insertChildElements(type, attributes) {
if (typeof attributes === 'string') {
_insertElement(type, plyr.media, { src: attributes });
}
else if (attributes.constructor === Array) {
for (var i = attributes.length - 1; i >= 0; i--) {
_insertElement(type, plyr.media, attributes[i]);
}
}
}
// Add common function to retrieve media source
function _source(source) {
// If not null or undefined, parse it
if(typeof source !== 'undefined') {
_updateSource(source);
return;
}
// Return the current source
var url;
switch(plyr.type) {
case 'youtube':
url = plyr.embed.getVideoUrl();
break;
case 'vimeo':
plyr.embed.api('getVideoUrl', function (value) {
url = value;
});
break;
default:
url = plyr.media.currentSrc;
break;
}
return url || '';
}
// Update source
// Sources are not checked for support so be careful
function _updateSource(source) {
if (typeof source === 'undefined' || !('sources' in source) || !source.sources.length) {
_log('Invalid source format', true);
return;
}
// Pause playback
_pause();
// Clean up YouTube stuff
if (plyr.type === 'youtube') {
// Destroy the embed instance
plyr.embed.destroy();
// Clear timer
window.clearInterval(plyr.timer.buffering);
window.clearInterval(plyr.timer.playing);
}
else if (plyr.type === 'video') {
// Remove video wrapper
_remove(plyr.videoContainer);
}
// Remove the old media
_remove(plyr.media);
// Set the type
if ('type' in source) {
plyr.type = source.type;
// Get child type for video (it might be an embed)
if(plyr.type === 'video') {
var firstSource = source.sources[0];
if('type' in firstSource && _inArray(config.types.embed, firstSource.type)) {
plyr.type = firstSource.type;
}
}
}
// Create new markup
switch(plyr.type) {
case 'video':
plyr.media = document.createElement('video');
break;
case 'audio':
plyr.media = document.createElement('audio');
break;
case 'youtube':
case 'vimeo':
plyr.media = document.createElement('div');
plyr.embedId = source.sources[0].src;
break;
}
// Inject the new element
_prependChild(plyr.container, plyr.media);
// Set attributes for audio video
if (_inArray(config.types.html5, plyr.type)) {
if (config.crossorigin) {
plyr.media.setAttribute('crossorigin', '');
}
if (config.autoplay) {
plyr.media.setAttribute('autoplay', '');
}
if ('poster' in source) {
plyr.media.setAttribute('poster', source.poster);
}
if (config.loop) {
plyr.media.setAttribute('loop', '');
}
}
// Classname reset
plyr.container.className = plyr.originalClassName;
// Restore class hooks
_toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen);
_toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled);
// Autoplay the new source?
config.autoplay = (source.autoplay || config.autoplay);
// Set new sources for html5
if (_inArray(config.types.html5, plyr.type)) {
_insertChildElements('source', source.sources);
}
// Set up from scratch
_setupMedia();
// Trigger media updated
_mediaUpdated();
// HTML5 stuff
if (_inArray(config.types.html5, plyr.type)) {
// Set volume
_setVolume();
_updateVolume();
// UI updates
if (plyr.supported.full) {
// Reset time display
_timeUpdate();
// Update the UI
_checkPlaying();
}
// Setup captions
if ('tracks' in source) {
_insertChildElements('track', source.tracks);
// Captions
_setupCaptions();
}
// Load HTML5 sources
plyr.media.load();
// Play if autoplay attribute is present
if (config.autoplay) {
_play();
}
}
if ('title' in source) {
config.title = source.title;
_setTitle();
}
}
// Update poster
function _updatePoster(source) {
if (plyr.type === 'video') {
plyr.media.setAttribute('poster', source);
}
}
// Listen for events
function _listeners() {
// IE doesn't support input event, so we fallback to change
var inputEvent = (plyr.browser.name == 'IE' ? 'change' : 'input');
// Click play/pause helper
function _togglePlay(play) {
// Toggle playback
if (play) {
_play();
}
else {
_pause();
}
// Determine which buttons
var trigger = plyr.buttons[play ? "play" : "pause"],
target = plyr.buttons[play ? "pause" : "play"];
// Setup focus and tab focus
if(target) {
var hadTabFocus = _hasClass(trigger, config.classes.tabFocus);
setTimeout(function() {
target.focus();
if(hadTabFocus) {
_toggleClass(trigger, config.classes.tabFocus, false);
_toggleClass(target, config.classes.tabFocus, true);
}
}, 100);
}
}
// Detect tab focus
function checkFocus() {
var focused = document.activeElement;
if (!focused || focused == document.body) {
focused = null;
}
else if (document.querySelector) {
focused = document.querySelector(':focus');
}
for (var button in plyr.buttons) {
var element = plyr.buttons[button];
_toggleClass(element, config.classes.tabFocus, (element === focused));
}
}
_on(window, 'keyup', function(event) {
var code = (event.keyCode ? event.keyCode : event.which);
if (code == 9) {
checkFocus();
}
});
_on(document.body, 'click', function() {
_toggleClass(_getElement('.' + config.classes.tabFocus), config.classes.tabFocus, false);
});
for (var button in plyr.buttons) {
var element = plyr.buttons[button];
_on(element, 'blur', function() {
_toggleClass(element, 'tab-focus', false);
});
}
function _registerHandler(element, event, userHandler, defaultHandler) {
_on(element, event, function(e) {
if(userHandler) {
userHandler(e);
}
defaultHandler(e);
});
}
// Play
_registerHandler(plyr.buttons.play, 'click', config.handlers.play, function() { _togglePlay(true); });
// Pause
_registerHandler(plyr.buttons.pause, 'click', config.handlers.pause, function() { _togglePlay(); });
// Restart
_registerHandler(plyr.buttons.restart, 'click', config.handlers.restart, _seek);
// Rewind
_registerHandler(plyr.buttons.rewind, 'click', config.handlers.rewind, _rewind);
// Fast forward
_registerHandler(plyr.buttons.forward, 'click', config.handlers.forward, _forward);
// Seek
_registerHandler(plyr.buttons.seek, inputEvent, config.handlers.seek, _seek);
// Set volume
_registerHandler(plyr.volume, inputEvent, config.handlers.volume, function() {
_setVolume(plyr.volume.value);
});
// Mute
_registerHandler(plyr.buttons.mute, 'click', config.handlers.mute, _toggleMute);
// Fullscreen
_registerHandler(plyr.buttons.fullscreen, 'click', config.handlers.fullscreen, _toggleFullscreen);
// Handle user exiting fullscreen by escaping etc
if (fullscreen.supportsFullScreen) {
_on(document, fullscreen.fullScreenEventName, _toggleFullscreen);
}
// Time change on media
_on(plyr.media, 'timeupdate seeking', _timeUpdate);
// Update manual captions
_on(plyr.media, 'timeupdate', _seekManualCaptions);
// Display duration
_on(plyr.media, 'loadedmetadata', _displayDuration);
// Captions
_on(plyr.buttons.captions, 'click', _toggleCaptions);
// Handle the media finishing
_on(plyr.media, 'ended', function() {
// Clear
if (plyr.type === 'video') {
plyr.captionsContainer.innerHTML = '';
}
// Reset UI
_checkPlaying();
});
// Check for buffer progress
_on(plyr.media, 'progress playing', _updateProgress);
// Handle native mute
_on(plyr.media, 'volumechange', _updateVolume);
// Handle native play/pause
_on(plyr.media, 'play pause', _checkPlaying);
// Loading
_on(plyr.media, 'waiting canplay seeked', _checkLoading);
// Click video
if (plyr.type === 'video' && config.click) {
_on(plyr.videoContainer, 'click', function() {
if (plyr.media.paused) {
_play();
}
else if (plyr.media.ended) {
_seek();
_play();
}
else {
_pause();
}
});
}
}
// Destroy an instance
// Event listeners are removed when elements are removed
// http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
function _destroy() {
// Bail if the element is not initialized
if (!plyr.init) {
return null;
}
// Reset container classname
plyr.container.setAttribute('class', _getClassname(config.selectors.container));
// Remove init flag
plyr.init = false;
// Remove controls
_remove(_getElement(config.selectors.controls.wrapper));
// YouTube
if (plyr.type === 'youtube') {
plyr.embed.destroy();
return;
}
// If video, we need to remove some more
if (plyr.type === 'video') {
// Remove captions
_remove(_getElement(config.selectors.captions));
// Remove video wrapper
_unwrap(plyr.videoContainer);
}
// Restore native video controls
plyr.media.setAttribute('controls', '');
// Clone the media element to remove listeners
// http://stackoverflow.com/questions/19469881/javascript-remove-all-event-listeners-of-specific-type
var clone = plyr.media.cloneNode(true);
plyr.media.parentNode.replaceChild(clone, plyr.media);
}
// Setup a player
function _init() {
// Bail if the element is initialized
if (plyr.init) {
return null;
}
// Setup the fullscreen api
fullscreen = _fullscreen();
// Sniff out the browser
plyr.browser = _browserSniff();
// Get the media element
plyr.media = plyr.container.querySelectorAll('audio, video, div')[0];
// Add style hook
_toggleClass(plyr.container, defaults.selectors.container.replace('.', ''), true);
// Get original classname
plyr.originalClassName = plyr.container.className;
// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
var tagName = plyr.media.tagName.toLowerCase();
if (tagName === 'div') {
plyr.type = plyr.media.getAttribute('data-type');
plyr.embedId = plyr.media.getAttribute('data-video-id');
// Clean up
plyr.media.removeAttribute('data-type');
plyr.media.removeAttribute('data-video-id');
}
else {
plyr.type = tagName;
config.crossorigin = (plyr.media.getAttribute('crossorigin') !== null);
config.autoplay = (config.autoplay || (plyr.media.getAttribute('autoplay') !== null));
config.loop = (config.loop || (plyr.media.getAttribute('loop') !== null));
}
// Check for full support
plyr.supported = api.supported(plyr.type);
// If no native support, bail
if (!plyr.supported.basic) {
return false;
}
// Debug info
_log(plyr.browser.name + ' ' + plyr.browser.version);
// Setup media
_setupMedia();
// Setup interface
if (plyr.type == 'video' || plyr.type == 'audio') {
// Bail if no support
if (!plyr.supported.full) {
// Successful setup
plyr.init = true;
// Don't inject controls if no full support
return;
}
// Setup UI
_setupInterface();
// Display duration if available
if (config.displayDuration) {
_displayDuration();
}
// Set title on button and frame
_setTitle();
}
// Successful setup
plyr.init = true;
}
function _setupInterface() {
// Inject custom controls
_injectControls();
// Find the elements
if (!_findElements()) {
return false;
}
// Captions
_setupCaptions();
// Media updated
_mediaUpdated();
// Set volume
_setVolume();
_updateVolume();
}
function _mediaUpdated() {
// Setup fullscreen
_setupFullscreen();
// Listeners
_listeners();
}
// Initialize instance
_init();
// If init failed, return an empty object
if (!plyr.init) {
return {};
}
return {
media: plyr.media,
play: _play,
pause: _pause,
restart: _seek,
rewind: _rewind,
forward: _forward,
seek: _seek,
source: _source,
poster: _updatePoster,
setVolume: _setVolume,
togglePlay: _togglePlay,
toggleMute: _toggleMute,
toggleCaptions: _toggleCaptions,
toggleFullscreen: _toggleFullscreen,
isFullscreen: function() { return plyr.isFullscreen || false; },
support: function(mimeType) { return _supportMime(plyr, mimeType); },
destroy: _destroy,
restore: _init
};
}
// Check for support
api.supported = function(type) {
var browser = _browserSniff(),
oldIE = (browser.name === 'IE' && browser.version <= 9),
iPhone = /iPhone|iPod/i.test(navigator.userAgent),
audio = !!document.createElement('audio').canPlayType,
video = !!document.createElement('video').canPlayType,
basic, full;
switch (type) {
case 'video':
basic = video;
full = (basic && (!oldIE && !iPhone));
break;
case 'audio':
basic = audio;
full = (basic && !oldIE);
break;
case 'vimeo':
case 'youtube':
basic = true;
full = (!oldIE && !iPhone);
break;
default:
basic = (audio && video);
full = (basic && !oldIE);
}
return {
basic: basic,
full: full
};
};
// Expose setup function
api.setup = function(elements, options) {
// Get the players
var instances = [];
// Select the elements
// Assume elements is a NodeList by default
if (typeof elements === 'string') {
elements = document.querySelectorAll(elements);
}
// Single HTMLElement passed
else if (elements instanceof HTMLElement) {
elements = [elements];
}
// No selector passed, possibly options as first argument
else if (!(elements instanceof NodeList) && typeof elements !== 'string') {
// If options are the first argument
if (typeof options === 'undefined' && typeof elements === 'object') {
options = elements;
}
// Use default selector
elements = document.querySelectorAll(defaults.selectors.container);
}
// Extend the default options with user specified
config = _extend(defaults, options);
// Bail if disabled or no basic support
// You may want to disable certain UAs etc
if (!config.enabled || !api.supported().basic || !elements.length) {
return false;
}
// Create a player instance for each element
for (var i = elements.length - 1; i >= 0; i--) {
// Get the current element
var element = elements[i];
// Setup a player instance and add to the element
if (typeof element.plyr === 'undefined') {
// Create new instance
var instance = new Plyr(element);
// Set plyr to false if setup failed
element.plyr = (Object.keys(instance).length ? instance : false);
// Callback
if (typeof config.onSetup === 'function') {
config.onSetup.apply(element.plyr);
}
}
// Add to return array even if it's already setup
instances.push(element.plyr);
}
return instances;
};
return api;
}));