// ========================================================================== // Plyr // plyr.js v1.1.5 // 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; // Default config var defaults = { enabled: true, debug: false, seekTime: 10, volume: 5, click: true, tooltips: false, displayDuration: true, 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: { video: "player-video", videoWrapper: "player-video-wrapper", embedWrapper: "player-video-embed", audio: "player-audio", stopped: "stopped", playing: "playing", muted: "muted", loading: "loading", tooltip: "player-tooltip", hidden: "sr-only", hover: "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"], onSetup: function() {}, youtube: { regex: /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ } }; // Build the default HTML function _buildControls() { // Open and add the progress and seek elements var html = [ "
", "
", "", "", "", "0% played", "", "", "0% 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( "", "Current time", "00:00", "" ); } // Media duration display if(_inArray(config.controls, "duration")) { html.push( "", "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) }; } // Check for mime type support against a player instance // Credits: http://diveintohtml5.info/everything.html // Related: http://www.leanbackplayer.com/test/h5mt.html 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); } } } // 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) { events = 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 < events.length; i++) { element[toggle ? "addEventListener" : "removeEventListener"](events[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 checkbox function _toggleCheckbox(event) { // Only listen for return key if(event.keyCode && event.keyCode != 13) { return true; } // Toggle the checkbox event.target.checked = !event.target.checked; // Trigger change event _triggerEvent(event.target, "change"); } // 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; } } } // Safari doesn't support the ALLOW_KEYBOARD_INPUT flag (for security) so set it to not supported // https://bugs.webkit.org/show_bug.cgi?id=121496 if(fullscreen.prefix === "webkit" && !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/)) { fullscreen.supportsFullScreen = false; } // 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; } 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) { return (this.prefix === "") ? element.requestFullScreen() : element[this.prefix + (this.prefix == "ms" ? "RequestFullscreen" : "RequestFullScreen")](this.prefix === "webkit" ? element.ALLOW_KEYBOARD_INPUT : null); }; 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]; // Render the caption player.captionsContainer.innerHTML = player.currentCaption; } else { // Clear the caption 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); player.buttons.captions.checked = 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 attributes function _setupAria() { // If there's no play button, bail if(!player.buttons.play) { return; } // Find the current text var label = player.buttons.play.innerText || "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[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 var firstSource = player.media.querySelectorAll("source")[0], matches = firstSource.src.match(config.youtube.regex); if(firstSource.type == "video/youtube" && matches && matches[2].length == 11) { _setupYouTube(matches[2]); } } } // Autoplay if(player.media.getAttribute("autoplay") !== null) { _play(); } } // Setup YouTube function _setupYouTube(id) { player.embed = true; // Hide the