/*
// 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
// 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
// Vimeo Player API
https://github.com/vimeo/player.js
// 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/
// Web Speech API (Speech Synthesis)
https://w3c.github.io/speech-api/#tts-section
https://developer.mozilla.org/en-US/docs/Web/API/Window/speechSynthesis
*/
/*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 an AblePlayer object
// Parameters are:
// 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)
if ($(media).attr('loop') !== undefined) {
this.loop = true;
}
else {
this.loop = false;
}
// playsinline (Boolean; if present always resolves to true, regardless of value)
if ($(media).attr('playsinline') !== undefined) {
this.playsInline = '1'; // this value gets passed to YT.Player contructor in youtube.js
}
else {
this.playsInline = '0';
}
// poster (Boolean, indicating whether media element has a poster attribute)
if ($(media).attr('poster')) {
this.hasPoster = true;
}
else {
this.hasPoster = false;
}
// get height and width attributes, if present
// and add them to variables
// Not currently used, but might be useful for resizing player
if ($(media).attr('width')) {
this.width = $(media).attr('width');
}
if ($(media).attr('height')) {
this.height = $(media).attr('height');
}
// 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;
if ( startTime !== undefined && isNumeric ) {
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;
}
// 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
if ($(media).data('desc-reader') == 'screenreader') {
this.descReader = 'screenreader';
}
else {
this.descReader = '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'
if ($(media).data('state-captions') == 'off') {
this.defaultStateCaptions = 0; // off
}
else {
this.defaultStateCaptions = 1; // on by default
}
if ($(media).data('state-descriptions') == 'on') {
this.defaultStateDescriptions = 1; // on
}
else {
this.defaultStateDescriptions = 0; // off by default
}
// 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
if ($(media).data('desc-pause-default') == 'off') {
this.defaultDescPause = 0; // off
}
else {
this.defaultDescPause = 1; // on by default
}
// 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)
// 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
if ($(media).data('transcript-div') !== undefined && $(media).data('transcript-div') !== "") {
this.transcriptDivLocation = $(media).data('transcript-div');
}
else {
this.transcriptDivLocation = null;
}
if ($(media).data('include-transcript') !== undefined && $(media).data('include-transcript') === false) {
this.hideTranscriptButton = true;
}
else {
this.hideTranscriptButton = null;
}
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"]').length > 0) {
// required tracks are present. COULD automatically generate a transcript
if (this.transcriptDivLocation) {
this.transcriptType = 'external';
}
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;
}
// Slower/Faster buttons
// valid values of data-speed-icons are 'animals' (default) and 'arrows'
// 'animals' uses turtle and rabbit; 'arrows' uses up/down arrows
if ($(media).data('speed-icons') === 'arrows') {
this.speedIcons = 'arrows';
}
else {
this.speedIcons = 'animals';
}
// 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 = this.getYouTubeId($(media).data('youtube-id'));
}
if ($(media).data('youtube-desc-id') !== undefined && $(media).data('youtube-desc-id') !== "") {
this.youTubeDescId = this.getYouTubeId($(media).data('youtube-desc-id'));
}
if ($(media).data('youtube-nocookie') !== undefined && $(media).data('youtube-nocookie')) {
this.youTubeNoCookie = true;
}
else {
this.youTubeNoCookie = false;
}
// Vimeo
if ($(media).data('vimeo-id') !== undefined && $(media).data('vimeo-id') !== "") {
this.vimeoId = this.getVimeoId($(media).data('vimeo-id'));
}
if ($(media).data('vimeo-desc-id') !== undefined && $(media).data('vimeo-desc-id') !== "") {
this.vimeoDescId = this.getVimeoId($(media).data('vimeo-desc-id'));
}
// Skin
// valid values of data-skin are:
// '2020' (default as of 5.0), all buttons in one row beneath a full-width seekbar
// 'legacy', two rows of controls; seekbar positioned in available space within top row
if ($(media).data('skin') == 'legacy') {
this.skin = 'legacy';
}
else {
this.skin = '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