/*jslint node: true, browser: true, white: true, indent: 2, unparam: true, plusplus: true */ /*global $, jQuery */ "use strict"; // maintain an array of Able Player instances for use globally (e.g., for keeping prefs in sync) var AblePlayerInstances = []; (function ($) { $(function () { $('video, audio').each(function (index, element) { if ($(element).data('able-player') !== undefined) { AblePlayerInstances.push(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).on('keydown',function(e) { if (AblePlayer.nextIndex === 1) { AblePlayer.lastCreated.onPlayerKeyPress(e); } }); /** * Construct the AblePlayer object. * * @param object media jQuery selector or element identifying the media. */ window.AblePlayer = function(media) { var thisObj = this; // Keep track of the last player created for use with global events. AblePlayer.lastCreated = this; this.media = media; if ($(media).length === 0) { this.provideFallback(); return; } // Default variables assignment // The following variables CAN be overridden with HTML attributes // autoplay (Boolean; if present always resolves to true, regardless of value) if ($(media).attr('autoplay') !== undefined) { this.autoplay = true; // this value remains constant this.okToPlay = true; // this value can change dynamically } else { this.autoplay = false; this.okToPlay = false; } // loop (Boolean; if present always resolves to true, regardless of value) this.loop = ($(media).attr('loop') !== undefined) ? true : false; // playsinline (Boolean; if present always resolves to true, regardless of value) this.playsInline = ($(media).attr('playsinline') !== undefined) ? '1' : '0'; // poster (Boolean, indicating whether media element has a poster attribute) this.hasPoster = ( $(media).attr('poster') || $(media).data('poster') ) ? true : false; this.audioPoster = $(media).data('poster'); this.audioPosterAlt = $(media).data('poster-alt' ); // get height and width attributes, if present // and add them to variables // Not currently used, but might be useful for resizing player this.width = $(media).attr('width') ?? 0; this.height = $(media).attr('height') ?? 0; // start-time var startTime = $(media).data('start-time'); var isNumeric = ( typeof startTime === 'number' || ( typeof startTime === 'string' && value.trim() !== '' && ! isNaN(value) && isFinite( Number(value) ) ) ) ? true : false; this.startTime = ( startTime !== undefined && isNumeric ) ? startTime : 0; // debug this.debug = ($(media).data('debug') !== undefined && $(media).data('debug') !== false) ? true : 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; } // Control whether text descriptions are read aloud // set to "false" if the sole purpose of the WebVTT descriptions file // is to integrate text description into the transcript // set to "true" to write description text to a div // This variable does *not* control the method by which description is read. // For that, see below (this.descMethod) if ($(media).data('descriptions-audible') !== undefined && $(media).data('descriptions-audible') === false) { this.readDescriptionsAloud = false; } else if ($(media).data('description-audible') !== undefined && $(media).data('description-audible') === false) { // support both singular and plural spelling of attribute this.readDescriptionsAloud = false; } else { this.readDescriptionsAloud = true; } // setting initial this.descVoices to an empty array // to be populated later by getBrowserVoices this.descVoices = []; // Method by which text descriptions are read // valid values of data-desc-reader are: // 'brower' (default) - text-based audio description is handled by the browser, if supported // 'screenreader' - text-based audio description is always handled by screen readers // The latter may be preferable by owners of websites in languages that are not well supported // by the Web Speech API this.descReader = ($(media).data('desc-reader') == 'screenreader') ? 'screenreader' : 'browser'; // Default state of captions and descriptions // This setting is overridden by user preferences, if they exist // values for data-state-captions and data-state-descriptions are 'on' or 'off' this.defaultStateCaptions = ($(media).data('state-captions') == 'off') ? 0 : 1; this.defaultStateDescriptions = ($(media).data('state-descriptions') == 'on') ? 1 : 0; // Default setting for prefDescPause // Extended description (i.e., pausing during description) is on by default // but this settings give website owners control over that // since they know the nature of their videos, and whether pausing is necessary // This setting is overridden by user preferences, if they exist this.defaultDescPause = ($(media).data('desc-pause-default') == 'off') ? 0 : 1; // Headings // By default, an off-screen heading is automatically added to the top of the media player // It is intelligently assigned a heading level based on context, via misc.js > getNextHeadingLevel() // Authors can override this behavior by manually assigning a heading level using data-heading-level // Accepted values are 1-6, or 0 which indicates "no heading" // (i.e., author has already hard-coded a heading before the media player; Able Player doesn't need to do this) if ($(media).data('heading-level') !== undefined && $(media).data('heading-level') !== "") { var headingLevel = $(media).data('heading-level'); if (/^[0-6]*$/.test(headingLevel)) { // must be a valid HTML heading level 1-6; or 0 this.playerHeadingLevel = headingLevel; } } // 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 & a valid target element) // 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 var transcriptDivLocation = $(media).data('transcript-div'); if ( transcriptDivLocation !== undefined && transcriptDivLocation !== "" && null !== document.getElementById( transcriptDivLocation ) ) { this.transcriptDivLocation = transcriptDivLocation; } else { this.transcriptDivLocation = null; } var includeTranscript = $(media).data('include-transcript'); this.hideTranscriptButton = ( includeTranscript !== undefined && includeTranscript === false) ? true : false; this.transcriptType = null; if ($(media).data('transcript-src') !== undefined) { this.transcriptSrc = $(media).data('transcript-src'); if (this.transcriptSrcHasRequiredParts()) { this.transcriptType = 'manual'; } else { console.log('ERROR: Able Player transcript is missing required parts'); } } else if ($(media).find('track[kind="captions"],track[kind="subtitles"],track:not([kind])').length > 0) { // required tracks are present. COULD automatically generate a transcript this.transcriptType = (this.transcriptDivLocation) ? 'external' : '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 for a more seamless reading experience // If true, line breaks are preserved, so content can be presented karaoke-style, or as lines in a poem this.lyricsMode = ($(media).data('lyrics-mode') !== undefined && $(media).data('lyrics-mode') !== false) ? true : false; // Set Transcript Title if defined explicitly. See transcript.js. if ($(media).data('transcript-title') !== undefined && $(media).data('transcript-title') !== "") { this.transcriptTitle = $(media).data('transcript-title'); } // Sign Language // sign language can be a modal (default) or assigned to a div on the page. var signDivLocation = $(media).data('sign-div'); if ( signDivLocation !== undefined && signDivLocation !== "" && null !== document.getElementById( signDivLocation ) ) { this.$signDivLocation = $( '#' + signDivLocation ); } else { this.$signDivLocation = null; } // 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' this.defaultCaptionsPosition = ($(media).data('captions-position') === 'overlay') ? 'overlay' : 'below'; // Chapters var chaptersDiv = $(media).data('chapters-div'); if ( chaptersDiv !== undefined && chaptersDiv !== "") { this.chaptersDivLocation = chaptersDiv; } if ($(media).data('chapters-title') !== undefined) { // NOTE: empty string is valid; results in no title being displayed this.chaptersTitle = $(media).data('chapters-title'); } var defaultChapter = $(media).data('chapters-default'); this.defaultChapter = ( defaultChapter !== undefined && defaultChapter !== "") ? defaultChapter : null; // Slower/Faster buttons // valid values of data-speed-icons are 'animals' (default) and 'arrows' // 'animals' uses turtle and rabbit; 'arrows' uses up/down arrows this.speedIcons = ($(media).data('speed-icons') === 'arrows') ? 'arrows' : 'animals'; // Seekbar // valid values of data-seekbar-scope are 'chapter' and 'video'; will also accept 'chapters' var seekbarScope = $(media).data('seekbar-scope'); this.seekbarScope = ( seekbarScope === 'chapter' || seekbarScope === 'chapters') ? 'chapter' : 'video'; // YouTube var youTubeId = $(media).data('youtube-id'); if ( youTubeId !== undefined && youTubeId !== "") { this.youTubeId = this.getYouTubeId(youTubeId); if ( ! this.hasPoster ) { let poster = this.getYouTubePosterUrl(this.youTubeId,'640'); $(media).attr( 'poster', poster ); } } var youTubeDescId = $(media).data('youtube-desc-id'); if ( youTubeDescId !== undefined && youTubeDescId !== "") { this.youTubeDescId = this.getYouTubeId(youTubeDescId); } var youTubeSignId = $(media).data('youtube-sign-src'); if ( youTubeSignId !== undefined && youTubeSignId !== "") { this.youTubeSignId = this.getYouTubeId(youTubeSignId); } var youTubeNoCookie = $(media).data('youtube-nocookie'); this.youTubeNoCookie = (youTubeNoCookie !== undefined && youTubeNoCookie) ? true : false; // Vimeo var vimeoId = $(media).data('vimeo-id'); if ( vimeoId !== undefined && vimeoId !== "") { this.vimeoId = this.getVimeoId(vimeoId); if ( ! this.hasPoster ) { let poster = thisObj.getVimeoPosterUrl(this.vimeoId,'1200'); $(media).attr( 'poster', poster ); } } var vimeoDescId = $(media).data('vimeo-desc-id'); if ( vimeoDescId !== undefined && vimeoDescId !== "") { this.vimeoDescId = this.getVimeoId(vimeoDescId); } // Skin // valid values of data-skin are: // '2020' (default as of 4.6), all buttons in one row beneath a full-width seekbar // 'legacy', two rows of controls; seekbar positioned in available space within top row this.skin = ($(media).data('skin') == 'legacy') ? 'legacy' : '2020'; // Size // width of Able Player is determined using the following order of precedence: // 1. data-width attribute // 2. width attribute (for video or audio, although it is not valid HTML for audio) // 3. Intrinsic size from video (video only, determined later) if ($(media).data('width') !== undefined) { this.playerWidth = parseInt($(media).data('width')); } else if ($(media)[0].getAttribute('width')) { // NOTE: jQuery attr() returns null for all invalid HTML attributes // (e.g., width on