/* // JavaScript for Able Player // HTML5 Media API: // http://www.w3.org/TR/html5/embedded-content-0.html#htmlmediaelement // http://dev.w3.org/html5/spec-author-view/video.html // W3C API Test Page: // http://www.w3.org/2010/05/video/mediaevents.html // Uses JW Player as fallback // JW Player configuration options: // http://support.jwplayer.com/customer/portal/articles/1413113-configuration-options-reference // (NOTE: some options are not documented, e.g., volume) // JW Player 6 API reference: // http://support.jwplayer.com/customer/portal/articles/1413089-javascript-api-reference // YouTube Player API for iframe Embeds https://developers.google.com/youtube/iframe_api_reference // YouTube Player Parameters https://developers.google.com/youtube/player_parameters?playerVersion=HTML5 // YouTube Data API https://developers.google.com/youtube/v3 // Google API Client Library for JavaScript https://developers.google.com/api-client-library/javascript/dev/dev_jscript // Google API Explorer: YouTube services and methods https://developers.google.com/apis-explorer/#s/youtube/v3/ */ /*jslint node: true, browser: true, white: true, indent: 2, unparam: true, plusplus: true */ /*global $, jQuery */ "use strict"; (function ($) { $(document).ready(function () { $('video, audio').each(function (index, element) { if ($(element).data('able-player') !== undefined) { new AblePlayer($(this),$(element)); } }); }); // YouTube player support; pass ready event to jQuery so we can catch in player. window.onYouTubeIframeAPIReady = function() { AblePlayer.youtubeIframeAPIReady = true; $('body').trigger('youtubeIframeAPIReady', []); }; // If there is only one player on the page, dispatch global keydown events to it // Otherwise, keydowwn events are handled locally (see event.js > handleEventListeners()) $(window).keydown(function(e) { if (AblePlayer.nextIndex === 1) { AblePlayer.lastCreated.onPlayerKeyPress(e); } }); // Construct an AblePlayer object // Parameters are: // media - jQuery selector or element identifying the media. window.AblePlayer = function(media) { // Keep track of the last player created for use with global events. AblePlayer.lastCreated = this; this.media = media; if ($(media).length === 0) { this.provideFallback('ERROR: No media specified.'); return; } /////////////////////////////// // // Default variables assignment // /////////////////////////////// // The following variables CAN be overridden with HTML attributes // autoplay if ($(media).attr('autoplay') !== undefined && $(media).attr('autoplay') !== "false") { this.autoplay = true; } else { this.autoplay = false; } // loop (NOT FULLY SUPPORTED) if ($(media).attr('loop') !== undefined && $(media).attr('loop') !== "false") { this.loop = true; } else { this.loop = false; } // start-time if ($(media).data('start-time') !== undefined && $(media).data('start-time') !== "") { this.startTime = $(media).data('start-time'); } else { this.startTime = 0; } // debug if ($(media).data('debug') !== undefined && $(media).data('debug') !== "false") { this.debug = true; } else { this.debug = false; } // Path to root directory of Able Player code if ($(media).data('root-path') !== undefined) { // add a trailing slash if there is none this.rootPath = $(media).data('root-path').replace(/\/?$/, '/'); } else { this.rootPath = this.getRootPath(); } // Volume // Range is 0 to 10. Best not to crank it to avoid overpowering screen readers this.defaultVolume = 7; if ($(media).data('volume') !== undefined && $(media).data('volume') !== "") { var volume = $(media).data('volume'); if (volume >= 0 && volume <= 10) { this.defaultVolume = volume; } } this.volume = this.defaultVolume; // Optional Buttons // Buttons are added to the player controller if relevant media is present // However, in some applications it might be undesirable to show buttons // (e.g., if chapters or transcripts are provided in an external container) if ($(media).data('use-chapters-button') !== undefined && $(media).data('use-chapters-button') === false) { this.useChaptersButton = false; } else { this.useChaptersButton = true; } if ($(media).data('use-descriptions-button') !== undefined && $(media).data('use-descriptions-button') === false) { this.useDescriptionsButton = false; } else { this.useDescriptionsButton = true; } // Transcripts // There are three types of interactive transcripts. // In descending of order of precedence (in case there are conflicting tags), they are: // 1. "manual" - A manually coded external transcript (requires data-transcript-src) // 2. "external" - Automatically generated, written to an external div (requires data-transcript-div) // 3. "popup" - Automatically generated, written to a draggable, resizable popup window that can be toggled on/off with a button // If data-include-transcript="false", there is no "popup" transcript this.transcriptType = null; if ($(media).data('transcript-src') !== undefined) { this.transcriptSrc = $(media).data('transcript-src'); if (this.transcriptSrcHasRequiredParts()) { this.transcriptType = 'manual'; } else { this.transcriptType = null; } } else if (media.find('track[kind="captions"], track[kind="subtitles"]').length > 0) { // required tracks are present. COULD automatically generate a transcript if ($(media).data('transcript-div') !== undefined && $(media).data('transcript-div') !== "") { this.transcriptDivLocation = $(media).data('transcript-div'); this.transcriptType = 'external'; } else if ($(media).data('include-transcript') !== undefined) { if ($(media).data('include-transcript') !== false) { this.transcriptType = 'popup'; } } else { this.transcriptType = 'popup'; } } // In "Lyrics Mode", line breaks in WebVTT caption files are supported in the transcript // If false (default), line breaks are are removed from transcripts in order to provide a more seamless reading experience // If true, line breaks are preserved, so content can be presented karaoke-style, or as lines in a poem if ($(media).data('lyrics-mode') !== undefined && $(media).data('lyrics-mode') !== "false") { this.lyricsMode = true; } else { this.lyricsMode = false; } // Transcript Title if ($(media).data('transcript-title') !== undefined && $(media).data('transcript-title') !== "") { this.transcriptTitle = $(media).data('transcript-title'); } else { // do nothing. The default title will be defined later (see transcript.js) } // Captions // data-captions-position can be used to set the default captions position // this is only the default, and can be overridden by user preferences // valid values of data-captions-position are 'below' and 'overlay' if ($(media).data('captions-position') === 'overlay') { this.defaultCaptionsPosition = 'overlay'; } else { // the default, even if not specified this.defaultCaptionsPosition = 'below'; } // Chapters if ($(media).data('chapters-div') !== undefined && $(media).data('chapters-div') !== "") { this.chaptersDivLocation = $(media).data('chapters-div'); } if ($(media).data('chapters-title') !== undefined) { // NOTE: empty string is valid; results in no title being displayed this.chaptersTitle = $(media).data('chapters-title'); } if ($(media).data('chapters-default') !== undefined && $(media).data('chapters-default') !== "") { this.defaultChapter = $(media).data('chapters-default'); } else { this.defaultChapter = null; } // Previous/Next buttons // valid values of data-prevnext-unit are 'playlist' and 'chapter'; will also accept 'chapters' if ($(media).data('prevnext-unit') === 'chapter' || $(media).data('prevnext-unit') === 'chapters') { this.prevNextUnit = 'chapter'; } else if ($(media).data('prevnext-unit') === 'playlist') { this.prevNextUnit = 'playlist'; } else { this.prevNextUnit = false; } // Slower/Faster buttons // valid values of data-speed-icons are 'arrows' (default) and 'animals' // use 'animals' to use turtle and rabbit if ($(media).data('speed-icons') === 'animals') { this.speedIcons = 'animals'; } else { this.speedIcons = 'arrows'; } // Seekbar // valid values of data-seekbar-scope are 'chapter' and 'video'; will also accept 'chapters' if ($(media).data('seekbar-scope') === 'chapter' || $(media).data('seekbar-scope') === 'chapters') { this.seekbarScope = 'chapter'; } else { this.seekbarScope = 'video'; } // YouTube if ($(media).data('youtube-id') !== undefined && $(media).data('youtube-id') !== "") { this.youTubeId = $(media).data('youtube-id'); } if ($(media).data('youtube-desc-id') !== undefined && $(media).data('youtube-desc-id') !== "") { this.youTubeDescId = $(media).data('youtube-desc-id'); } // Icon type // By default, AblePlayer uses scalable icomoon fonts for the player controls // and falls back to images if the user has a custom style sheet that overrides font-family // use data-icon-type to force controls to use either 'font', 'images' or 'svg' this.iconType = 'font'; this.forceIconType = false; if ($(media).data('icon-type') !== undefined && $(media).data('icon-type') !== "") { var iconType = $(media).data('icon-type'); if (iconType === 'font' || iconType == 'image' || iconType == 'svg') { this.iconType = iconType; this.forceIconType = true; } } if ($(media).data('allow-fullscreen') !== undefined && $(media).data('allow-fullscreen') === false) { this.allowFullScreen = false; } else { this.allowFullScreen = true; } // Seek interval // Number of seconds to seek forward or back with Rewind & Forward buttons // Unless specified with data-seek-interval, the default value is re-calculated in initialize.js > setSeekInterval(); // Calculation attempts to intelligently assign a reasonable interval based on media length this.defaultSeekInterval = 10; this.useFixedSeekInterval = false; if ($(media).data('seek-interval') !== undefined && $(media).data('seek-interval') !== "") { var seekInterval = $(media).data('seek-interval'); if (/^[1-9][0-9]*$/.test(seekInterval)) { // must be a whole number greater than 0 this.seekInterval = seekInterval; this.useFixedSeekInterval = true; // do not override with calculuation } } // Now Playing // Shows "Now Playing:" plus the title of the current track above player // Only used if there is a playlist if ($(media).data('show-now-playing') !== undefined && $(media).data('show-now-playing') === "false") { this.showNowPlaying = false; } else { this.showNowPlaying = true; } // Fallback Player // The only supported fallback is JW Player, licensed separately // JW Player files must be included in folder specified in this.fallbackPath // JW Player will be loaded as needed in browsers that don't support HTML5 media // NOTE: As of 2.3.44, NO FALLBACK is used unless data-fallback='jw' this.fallback = null; this.fallbackPath = null; this.testFallback = false; if ($(media).data('fallback') !== undefined && $(media).data('fallback') !== "") { var fallback = $(media).data('fallback'); if (fallback === 'jw') { this.fallback = fallback; } } if (this.fallback === 'jw') { if ($(media).data('fallback-path') !== undefined && $(media).data('fallback-path') !== "false") { this.fallbackPath = $(media).data('fallback-path'); } else { this.fallbackPath = this.rootPath + 'thirdparty/'; } if ($(media).data('test-fallback') !== undefined && $(media).data('test-fallback') !== "false") { this.testFallback = true; } } // Language this.lang = 'en'; if ($(media).data('lang') !== undefined && $(media).data('lang') !== "") { var lang = $(media).data('lang'); if (lang.length == 2) { this.lang = lang; } } // Player language is determined as follows: // 1. Lang attributes on or , if a matching translation file is available // 2. The value of this.lang, if a matching translation file is available // 3. English // To override this formula and force #2 to take precedence over #1, set data-force-lang="true" if ($(media).data('force-lang') !== undefined && $(media).data('force-lang') !== "false") { this.forceLang = true; } else { this.forceLang = false; } // Metadata Tracks if ($(media).data('meta-type') !== undefined && $(media).data('meta-type') !== "") { this.metaType = $(media).data('meta-type'); } if ($(media).data('meta-div') !== undefined && $(media).data('meta-div') !== "") { this.metaDiv = $(media).data('meta-div'); } // Search if ($(media).data('search') !== undefined && $(media).data('search') !== "") { // conducting a search currently requires an external div in which to write the results if ($(media).data('search-div') !== undefined && $(media).data('search-div') !== "") { this.searchString = $(media).data('search'); this.searchDiv = $(media).data('search-div'); } } // Define built-in variables that CANNOT be overridden with HTML attributes this.setDefaults(); //////////////////////////////////////// // // End assignment of default variables // //////////////////////////////////////// this.ableIndex = AblePlayer.nextIndex; AblePlayer.nextIndex += 1; this.title = $(media).attr('title'); // populate translation object with localized versions of all labels and prompts // use defer method to defer additional processing until text is retrieved this.tt = {}; var thisObj = this; $.when(this.getTranslationText()).then( function () { if (thisObj.countProperties(thisObj.tt) > 50) { // close enough to ensure that most text variables are populated thisObj.setup(); } else { // can't continue loading player with no text thisObj.provideFallback('ERROR: Failed to load translation table'); } } ); }; // Index to increment every time new player is created. AblePlayer.nextIndex = 0; AblePlayer.prototype.setup = function() { var thisObj = this; this.reinitialize().then(function () { if (!thisObj.player) { // No player for this media, show last-line fallback. thisObj.provideFallback('Unable to play media'); } else { thisObj.setupInstance().then(function () { thisObj.recreatePlayer(); }); } }); }; AblePlayer.youtubeIframeAPIReady = false; AblePlayer.loadingYoutubeIframeAPI = false; })(jQuery); (function ($) { // Set default variable values. AblePlayer.prototype.setDefaults = function () { // this.playing will change to true after 'playing' event is triggered this.playing = false; this.getUserAgent(); this.setIconColor(); this.setButtonImages(); }; AblePlayer.prototype.getRootPath = function() { // returns Able Player root path (assumes ableplayer.js is in /build, one directory removed from root) var scripts, i, scriptSrc, scriptFile, fullPath, ablePath, parentFolderIndex, rootPath; scripts= document.getElementsByTagName('script'); for (i=0; i < scripts.length; i++) { scriptSrc = scripts[i].src; scriptFile = scriptSrc.substr(scriptSrc.lastIndexOf('/')); if (scriptFile.indexOf('ableplayer') !== -1) { // this is the ableplayerscript fullPath = scriptSrc.split('?')[0]; // remove any ? params break; } } ablePath= fullPath.split('/').slice(0, -1).join('/'); // remove last filename part of path parentFolderIndex = ablePath.lastIndexOf('/'); rootPath = ablePath.substring(0, parentFolderIndex) + '/'; return rootPath; } AblePlayer.prototype.setIconColor = function() { // determine the best color choice (white or black) for icons, // given the background-color of their container elements // Source for relative luminance formula: // https://en.wikipedia.org/wiki/Relative_luminance // We need to know the color *before* creating the element // so the element doesn't exist yet when this function is called // therefore, need to create a temporary element then remove it after color is determined // Temp element must be added to the DOM or WebKit can't retrieve its CSS properties var $elements, i, $el, bgColor, rgb, red, green, blue, luminance, iconColor; $elements = ['controller', 'toolbar']; for (i=0; i<$elements.length; i++) { if ($elements[i] == 'controller') { $el = $('
', { 'class': 'able-controller' }).hide(); } else if ($elements[i] === 'toolbar') { $el = $('
', { 'class': 'able-window-toolbar' }).hide(); } $('body').append($el); bgColor = $el.css('background-color'); // bgColor is a string in the form 'rgb(R, G, B)', perhaps with a 4th item for alpha; // split the 3 or 4 channels into an array rgb = bgColor.replace(/[^\d,]/g, '').split(','); red = rgb[0]; green = rgb[1]; blue = rgb[2]; luminance = (0.2126 * red) + (0.7152 * green) + (0.0722 * blue); // range is 1 - 255; therefore 125 is the tipping point if (luminance < 125) { // background is dark iconColor = 'white'; } else { // background is light iconColor = 'black'; } if ($elements[i] === 'controller') { this.iconColor = iconColor; } else if ($elements[i] === 'toolbar') { this.toolbarIconColor = iconColor; } $el.remove(); } }; AblePlayer.prototype.setButtonImages = function() { // NOTE: volume button images are now set dynamically within volume.js this.imgPath = this.rootPath + 'button-icons/' + this.iconColor + '/'; this.playButtonImg = this.imgPath + 'play.png'; this.pauseButtonImg = this.imgPath + 'pause.png'; this.restartButtonImg = this.imgPath + 'restart.png'; this.rewindButtonImg = this.imgPath + 'rewind.png'; this.forwardButtonImg = this.imgPath + 'forward.png'; this.previousButtonImg = this.imgPath + 'previous.png'; this.nextButtonImg = this.imgPath + 'next.png'; if (this.speedIcons === 'arrows') { this.fasterButtonImg = this.imgPath + 'slower.png'; this.slowerButtonImg = this.imgPath + 'faster.png'; } else if (this.speedIcons === 'animals') { this.fasterButtonImg = this.imgPath + 'rabbit.png'; this.slowerButtonImg = this.imgPath + 'turtle.png'; } this.captionsButtonImg = this.imgPath + 'captions.png'; this.chaptersButtonImg = this.imgPath + 'chapters.png'; this.signButtonImg = this.imgPath + 'sign.png'; this.transcriptButtonImg = this.imgPath + 'transcript.png'; this.descriptionsButtonImg = this.imgPath + 'descriptions.png'; this.fullscreenExpandButtonImg = this.imgPath + 'fullscreen-expand.png'; this.fullscreenCollapseButtonImg = this.imgPath + 'fullscreen-collapse.png'; this.prefsButtonImg = this.imgPath + 'preferences.png'; this.helpButtonImg = this.imgPath + 'help.png'; }; // Initialize player based on data on page. // This sets some variables, but does not modify anything. Safe to call multiple times. // Can call again after updating this.media so long as new media element has the same ID. AblePlayer.prototype.reinitialize = function () { var deferred, promise, thisObj, errorMsg, srcFile; deferred = new $.Deferred(); promise = deferred.promise(); thisObj = this; // if F12 Developer Tools aren't open in IE (through 9, no longer a problen in IE10) // console.log causes an error - can't use debug without a console to log messages to if (! window.console) { this.debug = false; } this.startedPlaying = false; // TODO: Move this setting to cookie. this.autoScrollTranscript = true; //this.autoScrollTranscript = this.getCookie(autoScrollTranscript); // (doesn't work) // Bootstrap from this.media possibly being an ID or other selector. this.$media = $(this.media).first(); this.media = this.$media[0]; // Set media type to 'audio' or 'video'; this determines some of the behavior of player creation. if (this.$media.is('audio')) { this.mediaType = 'audio'; } else if (this.$media.is('video')) { this.mediaType = 'video'; } else { this.mediaType = this.$media.get(0).tagName; errorMsg = 'Media player initialized with ' + this.mediaType + '#' + this.mediaId + '. '; errorMsg += 'Expecting an HTML5 audio or video element.'; this.provideFallback(errorMsg); deferred.fail(); return promise; } this.$sources = this.$media.find('source'); this.player = this.getPlayer(); if (!this.player) { // an error was generated in getPlayer() this.provideFallback(this.error); } this.setIconType(); this.setDimensions(); deferred.resolve(); return promise; }; AblePlayer.prototype.setDimensions = function() { // if