',
''];
// restart button
if (_inarray(config.controls, 'restart')) {
html.push(
''
);
}
// rewind button
if (_inarray(config.controls, 'rewind')) {
html.push(
''
);
}
// play/pause button
if (_inarray(config.controls, 'play')) {
html.push(
'',
''
);
}
// fast forward button
if (_inarray(config.controls, 'fast-forward')) {
html.push(
''
);
}
// media current time display
if (_inarray(config.controls, 'current-time')) {
html.push(
'',
'' + config.i18n.currenttime + '',
'00:00',
''
);
}
// media duration display
if (_inarray(config.controls, 'duration')) {
html.push(
'',
'' + config.i18n.duration + '',
'00:00',
''
);
}
// close left controls
html.push(
'',
''
);
// toggle mute button
// if (_inarray(config.controls, 'mute')) {
// html.push(
// ''
// );
// }
// volume range control
// if (_inarray(config.controls, 'volume')) {
// html.push(
// '',
// ''
// );
// }
// toggle captions button
if (_inarray(config.controls, 'captions')) {
html.push(
''
);
}
// toggle fullscreen button
if (_inarray(config.controls, 'fullscreen')) {
html.push(
''
);
}
// close everything
html.push(
'',
'
'
);
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)
};
}
function _supportmime(player, mimetype) {
var media = player.media;
// only check video types for video players
if (player.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 (player.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, attributes[key]);
}
}
// toggle class on an element
function _toggleclass(element, name, state) {
if (element) {
if (element.classlist) {
element.classlist[state ? 'add' : 'remove'](name);
} else {
var classname = (' ' + element.classname + ' ').replace(/\s+/g, ' ').replace(' ' + name + ' ', '');
element.classname = classname + (state ? ' ' + name : '');
}
}
}
// 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) {
// 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) {
// 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 () {
try {
return 'localstorage' in window && window.localstorage !== null;
} catch (e) {
return false;
}
})()
};
return storage;
}
// player instance
function plyr(container) {
var player = this;
player.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 (player.usingtexttracks || player.type !== 'video' || !player.supported.full) {
return;
}
// reset subcount
player.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 : player.media.currenttime;
while (_timecodemax(player.captions[player.subcount][0]) < time.tofixed(1)) {
player.subcount++;
if (player.subcount > player.captions.length - 1) {
player.subcount = player.captions.length - 1;
break;
}
}
// check if the next caption is in the current time range
if (player.media.currenttime.tofixed(1) >= _timecodemin(player.captions[player.subcount][0]) &&
player.media.currenttime.tofixed(1) <= _timecodemax(player.captions[player.subcount][0])) {
player.currentcaption = player.captions[player.subcount][1];
// trim caption text
var content = player.currentcaption.trim();
// render the caption (only if changed)
if (player.captionscontainer.innerhtml != content) {
// empty caption
// otherwise nvda reads it twice
player.captionscontainer.innerhtml = '';
// set new caption text
player.captionscontainer.innerhtml = content;
}
} else {
player.captionscontainer.innerhtml = '';
}
}
// display captions container and button (for initialization)
function _showcaptions() {
// if there's no caption toggle, bail
if (!player.buttons.captions) {
return;
}
_toggleclass(player.container, config.classes.captions.enabled, true);
if (config.captions.defaultactive) {
_toggleclass(player.container, config.classes.captions.active, true);
_togglestate(player.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 player.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)));
// inject into the container
player.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 {
player.controls = _getelement(config.selectors.controls);
// buttons
player.buttons = {};
player.buttons.seek = _getelement(config.selectors.buttons.seek);
player.buttons.play = _getelement(config.selectors.buttons.play);
player.buttons.pause = _getelement(config.selectors.buttons.pause);
player.buttons.restart = _getelement(config.selectors.buttons.restart);
player.buttons.rewind = _getelement(config.selectors.buttons.rewind);
player.buttons.forward = _getelement(config.selectors.buttons.forward);
player.buttons.fullscreen = _getelement(config.selectors.buttons.fullscreen);
// inputs
player.buttons.mute = _getelement(config.selectors.buttons.mute);
player.buttons.captions = _getelement(config.selectors.buttons.captions);
player.checkboxes = _getelements('[type="checkbox"]');
// progress
player.progress = {};
player.progress.container = _getelement(config.selectors.progress.container);
// progress - buffering
player.progress.buffer = {};
player.progress.buffer.bar = _getelement(config.selectors.progress.buffer);
player.progress.buffer.text = player.progress.buffer.bar && player.progress.buffer.bar.getelementsbytagname('span')[0];
// progress - played
player.progress.played = {};
player.progress.played.bar = _getelement(config.selectors.progress.played);
player.progress.played.text = player.progress.played.bar && player.progress.played.bar.getelementsbytagname('span')[0];
// volume
player.volume = _getelement(config.selectors.buttons.volume);
// timing
player.duration = _getelement(config.selectors.duration);
player.currenttime = _getelement(config.selectors.currenttime);
player.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
player.media.setattribute('controls', '');
return false;
}
}
// setup aria attribute for play
function _setupplayaria() {
// if there's no play button, bail
if (!player.buttons.play) {
return;
}
// find the current text
var label = player.buttons.play.innertext || 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;
}
player.buttons.play.setattribute('aria-label', label);
}
// setup media
function _setupmedia() {
// if there's no media, bail
if (!player.media) {
_log('no audio or video element found!', true);
return false;
}
if (player.supported.full) {
// remove native video controls
player.media.removeattribute('controls');
// add type class
_toggleclass(player.container, config.classes.type.replace('{0}', player.type), true);
// if there's no autoplay attribute, assume the video is stopped and add state class
_toggleclass(player.container, config.classes.stopped, (player.media.getattribute('autoplay') === null));
// add ios class
if (player.browser.ios) {
_toggleclass(player.container, 'ios', true);
}
// inject the player wrapper
if (player.type === 'video') {
// create the wrapper div
var wrapper = document.createelement('div');
wrapper.setattribute('class', config.classes.videowrapper);
// wrap the video in a container
_wrap(player.media, wrapper);
// cache the container
player.videocontainer = wrapper;
}
}
// youtube
if (player.type == 'youtube') {
_setupyoutube(player.media.getattribute('data-video-id'));
}
// autoplay
if (player.media.getattribute('autoplay') !== null) {
_play();
}
}
// setup youtube
function _setupyoutube(id) {
// remove old containers
var containers = _getelements('[id^="youtube"]');
for (var i = containers.length - 1; i >= 0; i--) {
_remove(containers[i]);
}
// create the youtube container
var container = document.createelement('div');
container.setattribute('id', 'youtube-' + math.floor(math.random() * (10000)));
player.media.appendchild(container);
// add embed class for responsive
_toggleclass(player.media, config.classes.videowrapper, true);
_toggleclass(player.media, config.classes.embedwrapper, true);
if (typeof yt === 'object') {
_ytready(id, container);
} else {
// load the api
_injectscript('https://www.youtube.com/iframe_api');
// add callback to queue
callbacks.youtube.push(function () {
_ytready(id, container);
});
// setup callback for the api
window.onyoutubeiframeapiready = function () {
for (var i = callbacks.youtube.length - 1; i >= 0; i--) {
// fire callback
callbacks.youtube[i]();
// remove from queue
callbacks.youtube.splice(i, 1);
}
};
}
}
// handle api ready
function _ytready(id, container) {
_log('youtube api ready');
// setup timers object
// we have to poll youtube for updates
if (!('timer' in player)) {
player.timer = {};
}
// setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new yt.player(container.id, {
videoid: id,
playervars: {
autoplay: 0,
controls: (player.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
},
events: {
'onready': function (event) {
// get the instance
var instance = event.target;
// create a faux html5 api using the youtube api
player.media.play = function () {
instance.playvideo();
};
player.media.pause = function () {
instance.pausevideo();
};
player.media.stop = function () {
instance.stopvideo();
};
player.media.duration = instance.getduration();
player.media.paused = true;
player.media.currenttime = instance.getcurrenttime();
player.media.muted = instance.ismuted();
// trigger timeupdate
_triggerevent(player.media, 'timeupdate');
// reset timer
window.clearinterval(player.timer.buffering);
// setup buffering
player.timer.buffering = window.setinterval(function () {
// get loaded % from youtube
player.media.buffered = instance.getvideoloadedfraction();
// trigger progress
_triggerevent(player.media, 'progress');
// bail if we're at 100%
if (player.media.buffered === 1) {
window.clearinterval(player.timer.buffering);
}
}, 200);
if (player.supported.full) {
// only setup controls once
if (!player.container.queryselectorall(config.selectors.controls).length) {
_setupinterface();
}
// display duration if available
if (config.displayduration) {
_displayduration();
}
}
},
'onstatechange': function (event) {
// get the instance
var instance = event.target;
// reset timer
window.clearinterval(player.timer.playing);
// handle events
// -1 unstarted
// 0 ended
// 1 playing
// 2 paused
// 3 buffering
// 5 video cued
switch (event.data) {
case 0:
player.media.paused = true;
_triggerevent(player.media, 'ended');
break;
case 1:
player.media.paused = false;
_triggerevent(player.media, 'play');
// poll to get playback progress
player.timer.playing = window.setinterval(function () {
// set the current time
player.media.currenttime = instance.getcurrenttime();
// trigger timeupdate
_triggerevent(player.media, 'timeupdate');
}, 200);
break;
case 2:
player.media.paused = true;
_triggerevent(player.media, 'pause');
}
}
}
});
}
// setup captions
function _setupcaptions() {
if (player.type === 'video') {
// inject the container
player.videocontainer.insertadjacenthtml('afterbegin', '
');
// cache selector
player.captionscontainer = _getelement(config.selectors.captions).queryselector('span');
// determine if html5 texttracks is supported
player.usingtexttracks = false;
if (player.media.texttracks) {
player.usingtexttracks = true;
}
// get url of caption file if exists
var captionsrc = '',
kind,
children = player.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
player.captionexists = true;
if (captionsrc === '') {
player.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 (!player.captionexists) {
_toggleclass(player.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