// ========================================================================== // plyr // plyr.js v1.3.6 // https://github.com/selz/plyr // license: the mit license (mit) // ========================================================================== // credits: http://paypal.github.io/accessible-html5-video-player/ // ========================================================================== (function (api) { 'use strict'; /*global yt*/ // globals var fullscreen, config, callbacks = { youtube: [] }; // default config var defaults = { enabled: true, debug: false, seektime: 10, volume: 5, click: true, tooltips: true, displayduration: true, iconprefix: 'icon', selectors: { container: '.player', controls: '.player-controls', labels: '[data-player] .sr-only, label .sr-only', buttons: { seek: '[data-player="seek"]', play: '[data-player="play"]', pause: '[data-player="pause"]', restart: '[data-player="restart"]', rewind: '[data-player="rewind"]', forward: '[data-player="fast-forward"]', mute: '[data-player="mute"]', volume: '[data-player="volume"]', captions: '[data-player="captions"]', fullscreen: '[data-player="fullscreen"]' }, progress: { container: '.player-progress', buffer: '.player-progress-buffer', played: '.player-progress-played' }, captions: '.player-captions', currenttime: '.player-current-time', duration: '.player-duration' }, classes: { videowrapper: 'player-video-wrapper', embedwrapper: 'player-video-embed', type: 'player-{0}', stopped: 'stopped', playing: 'playing', muted: 'muted', loading: 'loading', tooltip: 'player-tooltip', hidden: 'sr-only', hover: 'player-hover', captions: { enabled: 'captions-enabled', active: 'captions-active' }, fullscreen: { enabled: 'fullscreen-enabled', active: 'fullscreen-active', hidecontrols: 'fullscreen-hide-controls' } }, captions: { defaultactive: false }, fullscreen: { enabled: true, fallback: true, hidecontrols: true }, storage: { enabled: true, key: 'plyr_volume' }, controls: ['restart', 'rewind', 'play', 'fast-forward', 'current-time', 'duration', 'mute', 'volume', /*'captions',*/ 'fullscreen'], i18n: { restart: '重新播放', rewind: '后退{seektime}秒', play: '播放', pause: '暂停', forward: '快进{seektime}秒', played: '播放中', buffered: '缓冲中', currenttime: '当前时间', duration: '持续时间', volume: '音量', togglemute: '静音', togglecaptions: '字幕', togglefullscreen: '全屏' } }; // build the default html function _buildcontrols() { // open and add the progress and seek elements var html = [ '
', '
', '', '', '', '0% ' + config.i18n.played, '', '', '0% ' + config.i18n.buffered, '', '
', '']; // 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 elements are removed from the dom below var tracks = player.media.texttracks; for (var x = 0; x < tracks.length; x++) { tracks[x].mode = 'hidden'; } // enable ui _showcaptions(player); // disable unsupported browsers than report false positive if ((player.browser.name === 'ie' && player.browser.version >= 10) || (player.browser.name === 'firefox' && player.browser.version >= 31) || (player.browser.name === 'chrome' && player.browser.version >= 43) || (player.browser.name === 'safari' && player.browser.version >= 7)) { // debugging _log('detected unsupported browser for html5 captions. using fallback.'); // set to false so skips to 'manual' captioning player.usingtexttracks = false; } // rendering caption tracks // native support required - http://caniuse.com/webvtt if (player.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 player.captionscontainer.innerhtml = ''; // display a cue, if there is one if (this.activecues[0] && this.activecues[0].hasownproperty('text')) { player.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 player.currentcaption = ''; player.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]; player.captions[r] = []; player.captions[r] = record.split('\n'); } // remove first element ('vtt') player.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 (player.browser.name === 'safari' && player.browser.version >= 7) { _log('safari 7+ detected; removing track from dom.'); // find all elements tracks = player.media.getelementsbytagname('track'); // loop through and remove one by one for (var t = 0; t < tracks.length; t++) { player.media.removechild(tracks[t]); } } } } } // setup fullscreen function _setupfullscreen() { if (player.type != 'audio' && 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(player.container, config.classes.fullscreen.enabled, true); } else { _log('fullscreen not supported and fallback disabled.'); } // toggle state _togglestate(player.buttons.fullscreen, false); // set control hide class hook if (config.fullscreen.hidecontrols) { _toggleclass(player.container, config.classes.fullscreen.hidecontrols, true); } } } // play media function _play() { player.media.play(); } // pause media function _pause() { player.media.pause(); } // toggle playback function _toggleplay(toggle) { // play if (toggle === true) { _play(); } // pause else if (toggle === false) { _pause(); } // true toggle else { player.media[player.media.paused ? 'play' : 'pause'](); } } // rewind function _rewind(seektime) { // use default if needed if (typeof seektime !== 'number') { seektime = config.seektime; } _seek(player.media.currenttime - seektime); } // fast forward function _forward(seektime) { // use default if needed if (typeof seektime !== 'number') { seektime = config.seektime; } _seek(player.media.currenttime + seektime); } // seek to time // the input parameter can be an event or a number function _seek(input) { var targettime = 0, paused = player.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) * player.media.duration); } // normalise targettime if (targettime < 0) { targettime = 0; } else if (targettime > player.media.duration) { targettime = player.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 { player.media.currenttime = targettime.tofixed(1); } catch (e) {} // youtube if (player.type == 'youtube') { player.embed.seekto(targettime); if (paused) { _pause(); } // trigger timeupdate _triggerevent(player.media, 'timeupdate'); } // logging _log('seeking to ' + player.media.currenttime + ' seconds'); // special handling for 'manual' captions _seekmanualcaptions(targettime); } // check playing state function _checkplaying() { _toggleclass(player.container, config.classes.playing, !player.media.paused); _toggleclass(player.container, config.classes.stopped, player.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) { player.isfullscreen = fullscreen.isfullscreen(player.container); } // if there's native support, use it else if (nativesupport) { // request fullscreen if (!fullscreen.isfullscreen(player.container)) { fullscreen.requestfullscreen(player.container); } // bail from fullscreen else { fullscreen.cancelfullscreen(); } // check if we're actually full screen (it could fail) player.isfullscreen = fullscreen.isfullscreen(player.container); } else { // otherwise, it's a simple toggle player.isfullscreen = !player.isfullscreen; // bind/unbind escape key if (player.isfullscreen) { _on(document, 'keyup', _handleescapefullscreen); document.body.style.overflow = 'hidden'; } else { _off(document, 'keyup', _handleescapefullscreen); document.body.style.overflow = ''; } } // set class hook _toggleclass(player.container, config.classes.fullscreen.active, player.isfullscreen); // set button state _togglestate(player.buttons.fullscreen, player.isfullscreen); // toggle controls visibility based on mouse movement and location var hovertimer, ismouseover = false; // show the player controls function _showcontrols() { // set shown class _toggleclass(player.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(player.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(player.controls, config.classes.hover, false); // keep an eye on the mouse location in relation to controls _togglehandler(player.controls, 'mouseenter mouseleave', _setmouseover, player.isfullscreen); // show the controls on mouse move _togglehandler(player.container, 'mousemove', _showcontrols, player.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 && player.isfullscreen) { _togglefullscreen(); } } // 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 player.media.volume = parsefloat(volume / 10); // youtube if (player.type == 'youtube') { player.embed.setvolume(player.media.volume * 100); // trigger timeupdate _triggerevent(player.media, 'volumechange'); } // toggle muted state if (player.media.muted && volume > 0) { _togglemute(); } } // mute function _togglemute(muted) { // if the method is called without parameter, toggle based on current value if (typeof muted !== 'boolean') { muted = !player.media.muted; } // set button state _togglestate(player.buttons.mute, muted); // set mute on the player player.media.muted = muted; // youtube if (player.type === 'youtube') { player.embed[player.media.muted ? 'mute' : 'unmute'](); // trigger timeupdate _triggerevent(player.media, 'volumechange'); } } // update volume ui and storage function _updatevolume() { // get the current volume var volume = player.media.muted ? 0 : (player.media.volume * 10); // update the if present if (player.supported.full && player.volume) { player.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(player.container, config.classes.muted, (volume === 0)); // update checkbox for mute state if (player.supported.full && player.buttons.mute) { _togglestate(player.buttons.mute, (volume === 0)); } } // toggle captions function _togglecaptions(show) { // if there's no full support, or there's no caption toggle if (!player.supported.full || !player.buttons.captions) { return; } // if the method is called without parameter, toggle based on current value if (typeof show !== 'boolean') { show = (player.container.classname.indexof(config.classes.captions.active) === -1); } // toggle state _togglestate(player.buttons.captions, show); // add class hook _toggleclass(player.container, config.classes.captions.active, show); } // check if media is loading function _checkloading(event) { var loading = (event.type === 'waiting'); // clear timer cleartimeout(player.loadingtimer); // timer to prevent flicker when seeking player.loadingtimer = settimeout(function () { _toggleclass(player.container, config.classes.loading, loading); }, (loading ? 250 : 0)); } // update elements function _updateprogress(event) { var progress = player.progress.played.bar, text = player.progress.played.text, value = 0; if (event) { switch (event.type) { // video playing case 'timeupdate': case 'seeking': value = _getpercentage(player.media.currenttime, player.media.duration); // set seek range value only if it's a 'natural' time event if (event.type == 'timeupdate' && player.buttons.seek) { player.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 = player.progress.buffer.bar; text = player.progress.buffer.text; value = (function () { var buffered = player.media.buffered; // html5 if (buffered && buffered.length) { return _getpercentage(buffered.end(0), player.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; } player.secs = parseint(time % 60); player.mins = parseint((time / 60) % 60); player.hours = parseint(((time / 60) / 60) % 60); // do we need to display hours? var displayhours = (parseint(((player.media.duration / 60) / 60) % 60) > 0); // ensure it's two digits. for example, 03 rather than 3. player.secs = ('0' + player.secs).slice(-2); player.mins = ('0' + player.mins).slice(-2); // render element.innerhtml = (displayhours ? player.hours + ':' : '') + player.mins + ':' + player.secs; } // show the duration on metadataloaded function _displayduration() { var duration = player.media.duration || 0; // if there's only one time display, display duration there if (!player.duration && config.displayduration && player.media.paused) { _updatetimedisplay(duration, player.currenttime); } // if there's a duration element, update content if (player.duration) { _updatetimedisplay(duration, player.duration); } } // handle time change event function _timeupdate(event) { // duration _updatetimedisplay(player.media.currenttime, player.currenttime); // playing progress _updateprogress(event); } // remove children and src attribute function _removesources() { // find child elements var sources = player.media.queryselectorall('source'); // remove each for (var i = sources.length - 1; i >= 0; i--) { _remove(sources[i]); } // remove src attribute player.media.removeattribute('src'); } // inject a source function _addsource(attributes) { if (attributes.src) { // create a new var element = document.createelement('source'); // set all passed attributes _setattributes(element, attributes); // inject the new source _prependchild(player.media, element); } } // update source // sources are not checked for support so be careful function _parsesource(sources) { // youtube if (player.type === 'youtube' && typeof sources === 'string') { // destroy youtube instance player.embed.destroy(); // re-setup youtube // we don't use loadvideoby[x] here since it has issues _setupyoutube(sources); // update times _timeupdate(); // bail return; } // pause playback (webkit freaks out) _pause(); // restart _seek(); // remove current sources _removesources(); // if a single source is passed // .source('path/to/video.mp4') if (typeof sources === 'string') { _addsource({ src: sources }); } // an array of source objects // check if a source exists, use that or set the 'src' attribute? // .source([{ src: 'path/to/video.mp4', type: 'video/mp4' },{ src: 'path/to/video.webm', type: 'video/webm' }]) else if (sources.constructor === array) { for (var index in sources) { _addsource(sources[index]); } } if (player.supported.full) { // reset time display _timeupdate(); // update the ui _checkplaying(); } // re-load sources player.media.load(); // play if autoplay attribute is present if (player.media.getattribute('autoplay') !== null) { _play(); } } // update poster function _updateposter(source) { if (player.type === 'video') { player.media.setattribute('poster', source); } } // listen for events function _listeners() { // ie doesn't support input event, so we fallback to change var inputevent = (player.browser.name == 'ie' ? 'change' : 'input'); // 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 player.buttons) { var element = player.buttons[button]; _toggleclass(element, 'tab-focus', (element === focused)); } } _on(window, 'keyup', function (event) { var code = (event.keycode ? event.keycode : event.which); if (code == 9) { checkfocus(); } }); for (var button in player.buttons) { var element = player.buttons[button]; _on(element, 'blur', function () { _toggleclass(element, 'tab-focus', false); }); } // play _on(player.buttons.play, 'click', function () { _play(); settimeout(function () { player.buttons.pause.focus(); }, 100); }); // pause _on(player.buttons.pause, 'click', function () { _pause(); settimeout(function () { player.buttons.play.focus(); }, 100); }); // restart _on(player.buttons.restart, 'click', _seek); // rewind _on(player.buttons.rewind, 'click', _rewind); // fast forward _on(player.buttons.forward, 'click', _forward); // seek _on(player.buttons.seek, inputevent, _seek); // set volume _on(player.volume, inputevent, function () { _setvolume(this.value); }); // mute _on(player.buttons.mute, 'click', _togglemute); // fullscreen _on(player.buttons.fullscreen, 'click', _togglefullscreen); // handle user exiting fullscreen by escaping etc if (fullscreen.supportsfullscreen) { _on(document, fullscreen.fullscreeneventname, _togglefullscreen); } // time change on media _on(player.media, 'timeupdate seeking', _timeupdate); // update manual captions _on(player.media, 'timeupdate', _seekmanualcaptions); // display duration _on(player.media, 'loadedmetadata', _displayduration); // captions _on(player.buttons.captions, 'click', _togglecaptions); // handle the media finishing _on(player.media, 'ended', function () { // clear if (player.type === 'video') { player.captionscontainer.innerhtml = ''; } // reset ui _checkplaying(); }); // check for buffer progress _on(player.media, 'progress playing', _updateprogress); // handle native mute _on(player.media, 'volumechange', _updatevolume); // handle native play/pause _on(player.media, 'play pause', _checkplaying); // loading _on(player.media, 'waiting canplay seeked', _checkloading); // click video if (player.type === 'video' && config.click) { _on(player.videocontainer, 'click', function () { if (player.media.paused) { _triggerevent(player.buttons.play, 'click'); } else if (player.media.ended) { _seek(); _triggerevent(player.buttons.play, 'click'); } else { _triggerevent(player.buttons.pause, 'click'); } }); } } // 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 (!player.init) { return null; } // reset container classname player.container.setattribute('class', config.selectors.container.replace('.', '')); // remove init flag player.init = false; // remove controls _remove(_getelement(config.selectors.controls)); // youtube if (player.type === 'youtube') { player.embed.destroy(); return; } // if video, we need to remove some more if (player.type === 'video') { // remove captions _remove(_getelement(config.selectors.captions)); // remove video wrapper _unwrap(player.videocontainer); } // restore native video controls player.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 = player.media.clonenode(true); player.media.parentnode.replacechild(clone, player.media); } // setup a player function _init() { // bail if the element is initialized if (player.init) { return null; } // setup the fullscreen api fullscreen = _fullscreen(); // sniff out the browser player.browser = _browsersniff(); // get the media element player.media = player.container.queryselectorall('audio, video, div')[0]; // set media type var tagname = player.media.tagname.tolowercase(); if (tagname === 'div') { player.type = player.media.getattribute('data-type'); } else { player.type = tagname; } // check for full support player.supported = api.supported(player.type); // if no native support, bail if (!player.supported.basic) { return false; } // debug info _log(player.browser.name + ' ' + player.browser.version); // setup media _setupmedia(); // setup interface if (player.type == 'video' || player.type == 'audio') { // bail if no support if (!player.supported.full) { // successful setup player.init = true; // don't inject controls if no full support return; } // setup ui _setupinterface(); // display duration if available if (config.displayduration) { _displayduration(); } // set up aria-label for play button with the title option _setupplayaria(); } // successful setup player.init = true; } function _setupinterface() { // inject custom controls _injectcontrols(); // find the elements if (!_findelements()) { return false; } // captions _setupcaptions(); // set volume _setvolume(); _updatevolume(); // setup fullscreen _setupfullscreen(); // listeners _listeners(); } // initialize instance _init(); // if init failed, return an empty object if (!player.init) { return {}; } return { media: player.media, play: _play, pause: _pause, restart: _seek, rewind: _rewind, forward: _forward, seek: _seek, source: _parsesource, poster: _updateposter, setvolume: _setvolume, toggleplay: _toggleplay, togglemute: _togglemute, togglecaptions: _togglecaptions, togglefullscreen: _togglefullscreen, isfullscreen: function () { return player.isfullscreen || false; }, support: function (mimetype) { return _supportmime(player, 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 '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 (options) { // 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) { return false; } // get the players var elements = document.queryselectorall(config.selectors.container), players = []; // 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 players.push(element.plyr); } return players; }; }(this.plyr = this.plyr || {}));