PageSourceSearch

https://clockshop.org/wp-content/plugins/ableplayer/build/ableplayer.min.js?ver=99.2.0

js clockshop.org collected 2026-10-02 03:59:06 UTC 764,900 bytes, 16,949 lines download raw bytes

1/*
2	// JavaScript for Able Player
3
4	// HTML5 Media API:
5	// http://www.w3.org/TR/html5/embedded-content-0.html#htmlmediaelement
6	// http://dev.w3.org/html5/spec-author-view/video.html
7
8	// W3C API Test Page:
9	// http://www.w3.org/2010/05/video/mediaevents.html
10
11	// YouTube Player API for iframe Embeds
12	https://developers.google.com/youtube/iframe_api_reference
13
14	// YouTube Player Parameters
15	https://developers.google.com/youtube/player_parameters?playerVersion=HTML5
16
17	// YouTube Data API
18	https://developers.google.com/youtube/v3
19
20	// Vimeo Player API
21	https://github.com/vimeo/player.js
22
23	// Google API Client Library for JavaScript
24	https://developers.google.com/api-client-library/javascript/dev/dev_jscript
25
26	// Google API Explorer: YouTube services and methods
27	https://developers.google.com/apis-explorer/#s/youtube/v3/
28
29	// Web Speech API (Speech Synthesis)
30	// https://w3c.github.io/speech-api/#tts-section
31	// https://developer.mozilla.org/en-US/docs/Web/API/Window/speechSynthesis
32*/
33
34/*jslint node: true, browser: true, white: true, indent: 2, unparam: true, plusplus: true */
35/*global $, jQuery */
36"use strict";
37
38// maintain an array of Able Player instances for use globally (e.g., for keeping prefs in sync)
39var AblePlayerInstances = [];
40
41(function ($) {
42	$(function () {
43
44		$('video, audio').each(function (index, element) {
45			if ($(element).data('able-player') !== undefined) {
46				AblePlayerInstances.push(new AblePlayer($(this),$(element)));
47			}
48		});
49	});
50
51	// YouTube player support; pass ready event to jQuery so we can catch in player.
52	window.onYouTubeIframeAPIReady = function() {
53		AblePlayer.youTubeIframeAPIReady = true;
54		$('body').trigger('youTubeIframeAPIReady', []);
55	};
56	// If there is only one player on the page, dispatch global keydown events to it
57	// Otherwise, keydowwn events are handled locally (see event.js > handleEventListeners())
58	$(window).on('keydown',function(e) {
59		if (AblePlayer.nextIndex === 1) {
60			AblePlayer.lastCreated.onPlayerKeyPress(e);
61		}
62	});
63
64	// Construct an AblePlayer object
65	// Parameters are:
66	// media - jQuery selector or element identifying the media.
67	window.AblePlayer = function(media) {
68
69
70		var thisObj = this;
71
72		// Keep track of the last player created for use with global events.
73		AblePlayer.lastCreated = this;
74		this.media = media;
75
76		if ($(media).length === 0) {
77			this.provideFallback();
78			return;
79		}
80
81		///////////////////////////////
82		//
83		// Default variables assignment
84		//
85		///////////////////////////////
86
87		// The following variables CAN be overridden with HTML attributes
88
89		// autoplay (Boolean; if present always resolves to true, regardless of value)
90		if ($(media).attr('autoplay') !== undefined) {
91			this.autoplay = true; // this value remains constant 
92			this.okToPlay = true; // this value can change dynamically
93		}
94		else {
95			this.autoplay = false;
96			this.okToPlay = false;
97		}
98		
99		// loop (Boolean; if present always resolves to true, regardless of value)
100		if ($(media).attr('loop') !== undefined) {
101			this.loop = true;
102		}
103		else {
104			this.loop = false;
105		}
106
107		// playsinline (Boolean; if present always resolves to true, regardless of value)
108		if ($(media).attr('playsinline') !== undefined) {
109			this.playsInline = '1'; // this value gets passed to YT.Player contructor in youtube.js
110		}
111		else {
112			this.playsInline = '0';
113		}
114
115		// poster (Boolean, indicating whether media element has a poster attribute)
116		if ($(media).attr('poster')) {
117			this.hasPoster = true;
118		}
119		else {
120			this.hasPoster = false;
121		}
122
123		// get height and width attributes, if present 
124		// and add them to variables 
125		// Not currently used, but might be useful for resizing player  
126		if ($(media).attr('width')) { 
127			this.width = $(media).attr('width'); 
128		}
129		if ($(media).attr('height')) { 
130			this.height = $(media).attr('height');
131		}
132
133		// start-time
134		if ($(media).data('start-time') !== undefined && $.isNumeric($(media).data('start-time'))) {
135			this.startTime = $(media).data('start-time');
136		}
137		else {
138			this.startTime = 0;
139		}
140
141		// debug
142		if ($(media).data('debug') !== undefined && $(media).data('debug') !== false) {
143			this.debug = true;
144		}
145		else {
146			this.debug = false;
147		}
148
149		// Path to root directory of Able Player code
150		if ($(media).data('root-path') !== undefined) {
151			// add a trailing slash if there is none
152			this.rootPath = $(media).data('root-path').replace(/\/?$/, '/');
153		}
154		else {
155			this.rootPath = this.getRootPath();
156		}
157
158		// Volume
159		// Range is 0 to 10. Best not to crank it to avoid overpowering screen readers
160		this.defaultVolume = 7;
161		if ($(media).data('volume') !== undefined && $(media).data('volume') !== "") {
162			var volume = $(media).data('volume');
163			if (volume >= 0 && volume <= 10) {
164				this.defaultVolume = volume;
165			}
166		}
167		this.volume = this.defaultVolume;
168
169		// Optional Buttons
170		// Buttons are added to the player controller if relevant media is present
171		// However, in some applications it might be undesirable to show buttons
172		// (e.g., if chapters or transcripts are provided in an external container)
173
174		if ($(media).data('use-chapters-button') !== undefined && $(media).data('use-chapters-button') === false) {
175			this.useChaptersButton = false;
176		}
177		else {
178			this.useChaptersButton = true;
179		}
180		
181		// Control whether text descriptions are read aloud 
182		// set to "false" if the sole purpose of the WebVTT descriptions file
183		// is to integrate text description into the transcript
184		// set to "true" to write description text to a div 
185		// This variable does *not* control the method by which description is read. 
186		// For that, see below (this.descMethod) 
187		if ($(media).data('descriptions-audible') !== undefined && $(media).data('descriptions-audible') === false) {
188			this.readDescriptionsAloud = false;
189		}
190		else if ($(media).data('description-audible') !== undefined && $(media).data('description-audible') === false) {
191			// support both singular and plural spelling of attribute
192			this.readDescriptionsAloud = false;
193		}
194		else {
195			this.readDescriptionsAloud = true;
196		}
197
198		// Method by which text descriptions are read  
199		// valid values of data-desc-reader are:
200		// 'brower' (default) - text-based audio description is handled by the browser, if supported  
201		// 'screenreader' - text-based audio description is always handled by screen readers 
202		// The latter may be preferable by owners of websites in languages that are not well supported 
203		// by the Web Speech API  
204		if ($(media).data('desc-reader') == 'screenreader') {
205			this.descReader = 'screenreader';
206		}
207		else {
208			this.descReader = 'browser';
209		}
210
211		// Default state of captions and descriptions 
212		// This setting is overridden by user preferences, if they exist 
213		// values for data-state-captions and data-state-descriptions are 'on' or 'off' 
214		if ($(media).data('state-captions') == 'off') {
215			this.defaultStateCaptions = 0; // off 
216		}
217		else {
218			this.defaultStateCaptions = 1; // on by default
219		}
220		if ($(media).data('state-descriptions') == 'on') {
221			this.defaultStateDescriptions = 1; // on
222		}
223		else {
224			this.defaultStateDescriptions = 0; // off by default
225		}
226
227		// Default setting for prefDescPause  
228		// Extended description (i.e., pausing during description) is on by default 
229		// but this settings give website owners control over that 
230		// since they know the nature of their videos, and whether pausing is necessary 
231		// This setting is overridden by user preferences, if they exist 
232		if ($(media).data('desc-pause-default') == 'off') {
233			this.defaultDescPause = 0; // off 
234		}
235		else {
236			this.defaultDescPause = 1; // on by default
237		}
238
239		// Headings
240		// By default, an off-screen heading is automatically added to the top of the media player
241		// It is intelligently assigned a heading level based on context, via misc.js > getNextHeadingLevel()
242		// Authors can override this behavior by manually assigning a heading level using data-heading-level
243		// Accepted values are 1-6, or 0 which indicates "no heading"
244		// (i.e., author has already hard-coded a heading before the media player; Able Player doesn't need to do this)
245		if ($(media).data('heading-level') !== undefined && $(media).data('heading-level') !== "") {
246			var headingLevel = $(media).data('heading-level');
247			if (/^[0-6]*$/.test(headingLevel)) { // must be a valid HTML heading level 1-6; or 0
248				this.playerHeadingLevel = headingLevel;
249			}
250		}
251
252		// Transcripts
253		// There are three types of interactive transcripts.
254		// In descending of order of precedence (in case there are conflicting tags), they are:
255		// 1. "manual" - A manually coded external transcript (requires data-transcript-src)
256		// 2. "external" - Automatically generated, written to an external div (requires data-transcript-div)
257		// 3. "popup" - Automatically generated, written to a draggable, resizable popup window that can be toggled on/off with a button
258		// If data-include-transcript="false", there is no "popup" transcript
259
260		if ($(media).data('transcript-div') !== undefined && $(media).data('transcript-div') !== "") {
261			this.transcriptDivLocation = $(media).data('transcript-div');
262		}
263		else {
264			this.transcriptDivLocation = null;
265		}
266		if ($(media).data('include-transcript') !== undefined && $(media).data('include-transcript') === false) {
267			this.hideTranscriptButton = true;
268		}
269		else {
270			this.hideTranscriptButton = null;
271		}
272
273		this.transcriptType = null;
274		if ($(media).data('transcript-src') !== undefined) {
275			this.transcriptSrc = $(media).data('transcript-src');
276			if (this.transcriptSrcHasRequiredParts()) {
277				this.transcriptType = 'manual';
278			}
279			else { 
280				console.log('ERROR: Able Player transcript is missing required parts');
281			}
282		}
283		else if ($(media).find('track[kind="captions"], track[kind="subtitles"]').length > 0) {
284			// required tracks are present. COULD automatically generate a transcript
285			if (this.transcriptDivLocation) {
286				this.transcriptType = 'external';
287			}
288			else {
289				this.transcriptType = 'popup';
290			}
291		}
292
293		// In "Lyrics Mode", line breaks in WebVTT caption files are supported in the transcript
294		// If false (default), line breaks are are removed from transcripts in order to provide a more seamless reading experience
295		// If true, line breaks are preserved, so content can be presented karaoke-style, or as lines in a poem
296		if ($(media).data('lyrics-mode') !== undefined && $(media).data('lyrics-mode') !== false) {
297			this.lyricsMode = true;
298		}
299		else {
300			this.lyricsMode = false;
301		}
302
303		// Transcript Title
304		if ($(media).data('transcript-title') !== undefined && $(media).data('transcript-title') !== "") {
305			this.transcriptTitle = $(media).data('transcript-title');
306		}
307		else {
308			// do nothing. The default title will be defined later (see transcript.js)
309		}
310
311		// Captions
312		// data-captions-position can be used to set the default captions position
313		// this is only the default, and can be overridden by user preferences
314		// valid values of data-captions-position are 'below' and 'overlay'
315		if ($(media).data('captions-position') === 'overlay') {
316			this.defaultCaptionsPosition = 'overlay';
317		}
318		else { // the default, even if not specified
319			this.defaultCaptionsPosition = 'below';
320		}
321
322		// Chapters
323		if ($(media).data('chapters-div') !== undefined && $(media).data('chapters-div') !== "") {
324			this.chaptersDivLocation = $(media).data('chapters-div');
325		}
326
327		if ($(media).data('chapters-title') !== undefined) {
328			// NOTE: empty string is valid; results in no title being displayed
329			this.chaptersTitle = $(media).data('chapters-title');
330		}
331
332		if ($(media).data('chapters-default') !== undefined && $(media).data('chapters-default') !== "") {
333			this.defaultChapter = $(media).data('chapters-default');
334		}
335		else {
336			this.defaultChapter = null;
337		}
338
339		// Slower/Faster buttons
340		// valid values of data-speed-icons are 'animals' (default) and 'arrows'
341		// 'animals' uses turtle and rabbit; 'arrows' uses up/down arrows
342		if ($(media).data('speed-icons') === 'arrows') {
343			this.speedIcons = 'arrows';
344		}
345		else {
346			this.speedIcons = 'animals';
347		}
348
349		// Seekbar
350		// valid values of data-seekbar-scope are 'chapter' and 'video'; will also accept 'chapters'
351		if ($(media).data('seekbar-scope') === 'chapter' || $(media).data('seekbar-scope') === 'chapters') {
352			this.seekbarScope = 'chapter';
353		}
354		else {
355			this.seekbarScope = 'video';
356		}
357
358		// YouTube
359		if ($(media).data('youtube-id') !== undefined && $(media).data('youtube-id') !== "") {
360			this.youTubeId = this.getYouTubeId($(media).data('youtube-id'));
361		}
362
363		if ($(media).data('youtube-desc-id') !== undefined && $(media).data('youtube-desc-id') !== "") {
364			this.youTubeDescId = this.getYouTubeId($(media).data('youtube-desc-id'));
365		}
366
367		if ($(media).data('youtube-nocookie') !== undefined && $(media).data('youtube-nocookie')) {
368			this.youTubeNoCookie = true;
369		}
370		else {
371			this.youTubeNoCookie = false;
372		}
373
374		// Vimeo
375		if ($(media).data('vimeo-id') !== undefined && $(media).data('vimeo-id') !== "") {
376			this.vimeoId = this.getVimeoId($(media).data('vimeo-id'));
377		}
378		if ($(media).data('vimeo-desc-id') !== undefined && $(media).data('vimeo-desc-id') !== "") {
379			this.vimeoDescId = this.getVimeoId($(media).data('vimeo-desc-id'));
380		}
381
382		// Skin
383		// valid values of data-skin are:
384		// 'legacy' (default), two rows of controls; seekbar positioned in available space within top row
385		// '2020', all buttons in one row beneath a full-width seekbar
386		if ($(media).data('skin') == '2020') {
387			this.skin = '2020';
388		}
389		else {
390			this.skin = 'legacy';
391		}
392
393		// Size 
394		// width of Able Player is determined using the following order of precedence: 
395		// 1. data-width attribute 
396		// 2. width attribute (for video or audio, although it is not valid HTML for audio)
397		// 3. Intrinsic size from video (video only, determined later)
398		if ($(media).data('width') !== undefined) {
399			this.playerWidth = parseInt($(media).data('width'));
400		}
401		else if ($(media)[0].getAttribute('width')) {
402			// NOTE: jQuery attr() returns null for all invalid HTML attributes 
403			// (e.g., width on <audio>)
404			// but it can be acessed via JavaScript getAttribute() 
405			this.playerWidth = parseInt($(media)[0].getAttribute('width'));
406		}
407		else { 
408			this.playerWidth = null; 
409		}
410
411		// Icon type
412		// By default, AblePlayer 3.0.33 and higher uses SVG icons for the player controls
413		// Fallback for browsers that don't support SVG is scalable icomoon fonts
414		// Ultimate fallback is images, if the user has a custom style sheet that overrides font-family
415		// Use data-icon-type to force controls to use either 'svg', 'font', or 'images'
416		this.iconType = 'font';
417		this.forceIconType = false;
418		if ($(media).data('icon-type') !== undefined && $(media).data('icon-type') !== "") {
419			var iconType = $(media).data('icon-type');
420			if (iconType === 'font' || iconType == 'image' || iconType == 'svg') {
421				this.iconType = iconType;
422				this.forceIconType = true;
423			}
424		}
425
426		if ($(media).data('allow-fullscreen') !== undefined && $(media).data('allow-fullscreen') === false) {
427			this.allowFullscreen = false;
428		}
429		else {
430			this.allowFullscreen = true;
431		}
432		// Define other variables that are used in fullscreen program flow 
433		this.clickedFullscreenButton = false; 
434		this.restoringAfterFullscreen = false;			
435
436		// Seek interval
437		// Number of seconds to seek forward or back with Rewind & Forward buttons
438		// Unless specified with data-seek-interval, the default value is re-calculated in initialize.js > setSeekInterval();
439		// Calculation attempts to intelligently assign a reasonable interval based on media length
440		this.defaultSeekInterval = 10;
441		this.useFixedSeekInterval = false; // will change to true if media has valid data-seek-interval attribute
442		if ($(media).data('seek-interval') !== undefined && $(media).data('seek-interval') !== "") {
443			var seekInterval = $(media).data('seek-interval');
444			if (/^[1-9][0-9]*$/.test(seekInterval)) { // must be a whole number greater than 0
445				this.seekInterval = seekInterval;
446				this.useFixedSeekInterval = true; // do not override with calculuation
447			}
448		}
449
450		// Now Playing
451		// Shows "Now Playing:" plus the title of the current track above player
452		// Only used if there is a playlist
453		if ($(media).data('show-now-playing') !== undefined && $(media).data('show-now-playing') === false) {
454			this.showNowPlaying = false;
455		}
456		else {
457			this.showNowPlaying = true;
458		}
459
460		// TTML support (experimental); enabled for testing with data-use-ttml (Boolean)
461		if ($(media).data('use-ttml') !== undefined) {
462			this.useTtml = true;
463			// The following may result in a console error.
464			this.convert = require('xml-js');
465		}
466		else {
467			this.useTtml = false;
468		}
469
470		// Fallback
471		// The data-test-fallback attribute can be used to test the fallback solution in any browser
472		if ($(media).data('test-fallback') !== undefined && $(media).data('test-fallback') !== false) {
473			if ($(media).data('test-fallback') == '2') { 
474				this.testFallback = 2; // emulate browser that doesn't support HTML5 media 
475			}
476			else { 
477				this.testFallback = 1; // emulate failure to load Able Player 
478			}
479		}
480		else { 
481			this.testFallback = false; 
482		}
483
484		// Language
485		// Player language is determined given the following precedence:
486		// 1. The value of data-lang on the media element, if provided and a matching translation file is available
487		// 2. Lang attribute on <html> or <body>, if a matching translation file is available
488		// 3. English
489		// Final calculation occurs in translation.js > getTranslationText()
490		if ($(media).data('lang') !== undefined && $(media).data('lang') !== "") {
491			this.lang = $(media).data('lang').toLowerCase();
492		}
493		else {
494			this.lang = null;
495		}
496
497		// Metadata Tracks
498		if ($(media).data('meta-type') !== undefined && $(media).data('meta-type') !== "") {
499			this.metaType = $(media).data('meta-type');
500		}
501
502		if ($(media).data('meta-div') !== undefined && $(media).data('meta-div') !== "") {
503			this.metaDiv = $(media).data('meta-div');
504		}
505
506		// Search
507		// conducting a search requires an external div in which to write the results
508		if ($(media).data('search-div') !== undefined && $(media).data('search-div') !== "") {
509
510			this.searchDiv = $(media).data('search-div');
511
512			// Search term (optional; could be assigned later in a JavaScript application)
513			if ($(media).data('search') !== undefined && $(media).data('search') !== "") {
514				this.searchString = $(media).data('search');
515			}
516
517			// Search Language
518			if ($(media).data('search-lang') !== undefined && $(media).data('search-lang') !== "") {
519				this.searchLang = $(media).data('search-lang');
520			}
521			else {
522				this.searchLang = null; // will change to final value of this.lang in translation.js > getTranslationText()
523			}
524
525			// Search option: Ignore capitalization in search terms
526			if ($(media).data('search-ignore-caps') !== undefined && $(media).data('search-ignore-caps') !== false) {
527				this.searchIgnoreCaps = true;
528			}
529			else {
530				this.searchIgnoreCaps = false;
531			}
532
533			// conducting a search currently requires an external div in which to write the results
534			if ($(media).data('search-div') !== undefined && $(media).data('search-div') !== "") {
535				this.searchString = $(media).data('search');
536				this.searchDiv = $(media).data('search-div');
537			}
538		}
539
540		// Hide controls when video starts playing
541		// They will reappear again when user presses a key or moves the mouse
542		// As of v4.0, controls are hidden automatically on playback in fullscreen mode
543		if ($(media).data('hide-controls') !== undefined && $(media).data('hide-controls') !== false) {
544			this.hideControls = true;
545			this.hideControlsOriginal = true; // a copy of hideControls, since the former may change if user enters full screen mode
546		}
547		else {
548			this.hideControls = false;
549			this.hideControlsOriginal = false;
550		}
551
552		// Steno mode
553		// Enable support for Able Player keyboard shortcuts in textaarea fields
554		// so users can control the player while transcribing
555		if ($(media).data('steno-mode') !== undefined && $(media).data('steno-mode') !== false) {
556			this.stenoMode = true;
557			// Add support for stenography in an iframe via data-steno-iframe-id
558			if ($(media).data('steno-iframe-id') !== undefined && $(media).data('steno-iframe-id') !== "") {
559				this.stenoFrameId = $(media).data('steno-iframe-id');
560				this.$stenoFrame = $('#' + this.stenoFrameId);
561				if (!(this.$stenoFrame.length)) {
562					// iframe not found
563					this.stenoFrameId = null;
564					this.$stenoFrame = null;
565				}
566			}
567			else {
568				this.stenoFrameId = null;
569				this.$stenoFrame = null;
570			}
571		}
572		else {
573			this.stenoMode = false;
574			this.stenoFrameId = null;
575			this.$stenoFrame = null;
576		}
577
578		// Define built-in variables that CANNOT be overridden with HTML attributes
579		this.setDefaults();
580
581		////////////////////////////////////////
582		//
583		// End assignment of default variables
584		//
585		////////////////////////////////////////
586
587		this.ableIndex = AblePlayer.nextIndex;
588		AblePlayer.nextIndex += 1;
589
590		this.title = $(media).attr('title');
591
592		// populate translation object with localized versions of all labels and prompts
593		// use defer method to defer additional processing until text is retrieved
594		this.tt = {};
595		var thisObj = this;
596		$.when(this.getTranslationText()).then(
597			function () {
598				if (thisObj.countProperties(thisObj.tt) > 50) {
599					// close enough to ensure that most text variables are populated
600					thisObj.setup();
601				}
602				else {
603					// can't continue loading player with no text
604					thisObj.provideFallback();
605				}
606			}
607		).
608		fail(function() { 
609			thisObj.provideFallback(); 
610		});
611	};
612
613	// Index to increment every time new player is created.
614	AblePlayer.nextIndex = 0;
615
616	AblePlayer.prototype.setup = function() {
617
618		var thisObj = this;
619		this.initializing = true; // will remain true until entire sequence of function calls is complete
620
621		this.reinitialize().then(function () {
622			if (!thisObj.player) {
623				// No player for this media, show last-line fallback.
624				thisObj.provideFallback();
625			}
626			else {
627				thisObj.setupInstance().then(function () {
628					thisObj.setupInstancePlaylist();
629					if (thisObj.hasPlaylist) {
630						// for playlists, recreatePlayer() is called from within cuePlaylistItem()
631					}
632					else {
633						thisObj.recreatePlayer().then(function() { 
634							thisObj.initializing = false;
635							thisObj.playerCreated = true; // remains true until browser is refreshed		
636						});
637					}
638				});
639			}
640		});
641	};
642
643	AblePlayer.getActiveDOMElement = function () {
644		var activeElement = document.activeElement;
645
646		// For shadow DOMs we need to keep digging down through the DOMs
647		while (activeElement.shadowRoot && activeElement.shadowRoot.activeElement) {
648			activeElement = activeElement.shadowRoot.activeElement;
649		}
650
651		return activeElement;
652	};
653
654	AblePlayer.localGetElementById = function(element, id) {
655		if (element.getRootNode)
656		{
657			// Use getRootNode() and querySelector() where supported (for shadow DOM support)
658			return $(element.getRootNode().querySelector('#' + id));
659		}
660		else
661		{
662			// If getRootNode is not supported it should be safe to use document.getElementById (since there is no shadow DOM support)
663			return $(document.getElementById(id));
664		}
665	};
666
667	AblePlayer.youTubeIframeAPIReady = false;
668	AblePlayer.loadingYouTubeIframeAPI = false;
669})(jQuery);
670
671(function ($) {
672	// Set default variable values.
673	AblePlayer.prototype.setDefaults = function () {
674
675		this.playerCreated = false; // will set to true after recreatePlayer() is complete the first time
676		this.playing = false; // will change to true after 'playing' event is triggered
677		this.paused = true; // will always be the opposite of this.playing (available for convenience)
678		this.clickedPlay = false; // will change to true temporarily if user clicks 'play' (or pause)
679		this.fullscreen = false; // will change to true if player is in full screen mode
680		this.swappingSrc = false; // will change to true temporarily while media source is being swapped
681		this.initializing = false; // will change to true temporarily while initPlayer() is processing
682		this.cueingPlaylistItems = false; // will change to true temporarily while cueing next playlist item
683		this.buttonWithFocus = null; // will change to 'previous' or 'next' if user clicks either of those buttons
684		this.speechEnabled = null; // will change either to 'true' in initSpeech(), or false if not supported
685
686		this.setIconColor();
687		this.setButtonImages();
688	};
689
690	AblePlayer.prototype.getRootPath = function() {
691
692		// returns Able Player root path (assumes ableplayer.js is in /build, one directory removed from root)
693		var scripts, i, scriptSrc, scriptFile, fullPath, ablePath, parentFolderIndex, rootPath;
694		scripts= document.getElementsByTagName('script');
695		for (i=0; i < scripts.length; i++) {
696			scriptSrc = scripts[i].src;
697			scriptFile = scriptSrc.substring(scriptSrc.lastIndexOf('/'));
698			if (scriptFile.indexOf('ableplayer') !== -1) {
699				// this is the ableplayerscript
700				fullPath = scriptSrc.split('?')[0]; // remove any ? params
701				break;
702			}
703		}
704		ablePath= fullPath.split('/').slice(0, -1).join('/'); // remove last filename part of path
705		parentFolderIndex = ablePath.lastIndexOf('/');
706		rootPath = ablePath.substring(0, parentFolderIndex) + '/';
707		return rootPath;
708	}
709
710	AblePlayer.prototype.setIconColor = function() {
711
712		// determine the best color choice (white or black) for icons,
713		// given the background-color of their container elements
714		// Source for relative luminance formula:
715		// https://en.wikipedia.org/wiki/Relative_luminance
716
717		// We need to know the color *before* creating the element
718		// so the element doesn't exist yet when this function is called
719		// therefore, need to create a temporary element then remove it after color is determined
720		// Temp element must be added to the DOM or WebKit can't retrieve its CSS properties
721
722		var $elements, i, $el, bgColor, rgb, red, green, blue, luminance, iconColor;
723
724		$elements = ['controller', 'toolbar'];
725		for (i=0; i<$elements.length; i++) {
726			if ($elements[i] == 'controller') {
727				$el =	 $('<div>', {
728					'class': 'able-controller'
729				}).hide();
730			}
731			else if ($elements[i] === 'toolbar') {
732				$el =	 $('<div>', {
733					'class': 'able-window-toolbar'
734				}).hide();
735			}
736			$('body').append($el);
737			bgColor = $el.css('background-color');
738			// bgColor is a string in the form 'rgb(R, G, B)', perhaps with a 4th item for alpha;
739			// split the 3 or 4 channels into an array
740			rgb = bgColor.replace(/[^\d,]/g, '').split(',');
741			red = rgb[0];
742			green = rgb[1];
743			blue = rgb[2];
744			luminance = (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
745			// range is 1 - 255; therefore 125 is the tipping point
746			if (luminance < 125) { // background is dark
747				iconColor = 'white';
748			}
749			else { // background is light
750				iconColor = 'black';
751			}
752			if ($elements[i] === 'controller') {
753				this.iconColor = iconColor;
754			}
755			else if ($elements[i] === 'toolbar') {
756				this.toolbarIconColor = iconColor;
757			}
758			$el.remove();
759		}
760	};
761
762	AblePlayer.prototype.setButtonImages = function() {
763
764		// NOTE: volume button images are now set dynamically within volume.js
765		this.imgPath = this.rootPath + 'button-icons/' + this.iconColor + '/';
766		this.playButtonImg = this.imgPath + 'play.png';
767		this.pauseButtonImg = this.imgPath + 'pause.png';
768
769		this.restartButtonImg = this.imgPath + 'restart.png';
770
771		this.rewindButtonImg = this.imgPath + 'rewind.png';
772		this.forwardButtonImg = this.imgPath + 'forward.png';
773
774		this.previousButtonImg = this.imgPath + 'previous.png';
775		this.nextButtonImg = this.imgPath + 'next.png';
776
777		if (this.speedIcons === 'arrows') {
778			this.fasterButtonImg = this.imgPath + 'slower.png';
779			this.slowerButtonImg = this.imgPath + 'faster.png';
780		}
781		else if (this.speedIcons === 'animals') {
782			this.fasterButtonImg = this.imgPath + 'rabbit.png';
783			this.slowerButtonImg = this.imgPath + 'turtle.png';
784		}
785
786		this.captionsButtonImg = this.imgPath + 'captions.png';
787		this.chaptersButtonImg = this.imgPath + 'chapters.png';
788		this.signButtonImg = this.imgPath + 'sign.png';
789		this.transcriptButtonImg = this.imgPath + 'transcript.png';
790		this.descriptionsButtonImg = this.imgPath + 'descriptions.png';
791
792		this.fullscreenExpandButtonImg = this.imgPath + 'fullscreen-expand.png';
793		this.fullscreenCollapseButtonImg = this.imgPath + 'fullscreen-collapse.png';
794
795		this.prefsButtonImg = this.imgPath + 'preferences.png';
796		this.helpButtonImg = this.imgPath + 'help.png';
797	};
798
799	AblePlayer.prototype.getSvgData = function(button) {
800
801		// returns array of values for creating <svg> tag for specified button
802		// 0 = <svg> viewBox attribute
803		// 1 = <path> d (description) attribute
804		var svg = Array();
805
806		switch (button) {
807
808			case 'play':
809				svg[0] = '0 0 16 20';
810				svg[1] = 'M0 18.393v-16.429q0-0.29 0.184-0.402t0.441 0.033l14.821 8.237q0.257 0.145 0.257 0.346t-0.257 0.346l-14.821 8.237q-0.257 0.145-0.441 0.033t-0.184-0.402z';
811				break;
812
813			case 'pause':
814				svg[0] = '0 0 20 20';
815				svg[1] = 'M0 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h5.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-5.714q-0.29 0-0.502-0.212t-0.212-0.502zM10 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h5.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-5.714q-0.29 0-0.502-0.212t-0.212-0.502z';
816				break;
817
818			case 'stop':
819				svg[0] = '0 0 20 20';
820				svg[1] = 'M0 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h15.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-15.714q-0.29 0-0.502-0.212t-0.212-0.502z';
821				break;
822
823			case 'restart':
824				svg[0] = '0 0 20 20';
825				svg[1] = 'M18 8h-6l2.243-2.243c-1.133-1.133-2.64-1.757-4.243-1.757s-3.109 0.624-4.243 1.757c-1.133 1.133-1.757 2.64-1.757 4.243s0.624 3.109 1.757 4.243c1.133 1.133 2.64 1.757 4.243 1.757s3.109-0.624 4.243-1.757c0.095-0.095 0.185-0.192 0.273-0.292l1.505 1.317c-1.466 1.674-3.62 2.732-6.020 2.732-4.418 0-8-3.582-8-8s3.582-8 8-8c2.209 0 4.209 0.896 5.656 2.344l2.344-2.344v6z';
826				break;
827
828			case 'rewind':
829				svg[0] = '0 0 24 24';
830				svg[1] = 'M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z';
831				break;
832
833			case 'forward':
834				svg[0] = '0 0 24 24';
835				svg[1] = 'M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z';
836				break;
837
838			case 'previous':
839				svg[0] = '0 0 20 20';
840				svg[1] = 'M5 17.5v-15h2.5v6.875l6.25-6.25v13.75l-6.25-6.25v6.875z';
841				break;
842
843			case 'next':
844				svg[0] = '0 0 20 20';
845				svg[1] = 'M15 2.5v15h-2.5v-6.875l-6.25 6.25v-13.75l6.25 6.25v-6.875z';
846				break;
847
848			case 'slower':
849				svg[0] = '0 0 20
vendor: 11,454 bytes, lines 849-921
849 20';
850				svg[1] = 'M0 7.321q0-0.29 0.212-0.502t0.502-0.212h10q0.29 0 0.502 0.212t0.212 0.502-0.212 0.502l-5 5q-0.212 0.212-0.502 0.212t-0.502-0.212l-5-5q-0.212-0.212-0.212-0.502z';
851				break;
852
853			case 'faster':
854				svg[0] = '0 0 11 20';
855				svg[1] = 'M0 12.411q0-0.29 0.212-0.502l5-5q0.212-0.212 0.502-0.212t0.502 0.212l5 5q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-10q-0.29 0-0.502-0.212t-0.212-0.502z';
856				break;
857
858			case 'turtle':
859				svg[0] = '0 0 20 20';
860				svg[1] = 'M17.212 3.846c-0.281-0.014-0.549 0.025-0.817 0.144-1.218 0.542-1.662 2.708-2.163 3.942-1.207 2.972-7.090 4.619-11.755 5.216-0.887 0.114-1.749 0.74-2.428 1.466 0.82-0.284 2.126-0.297 2.74 0.144 0.007 0.488-0.376 1.062-0.625 1.37-0.404 0.5-0.398 0.793 0.12 0.793 0.473 0 0.752 0.007 1.635 0 0.393-0.003 0.618-0.16 1.49-1.49 3.592 0.718 5.986-0.264 5.986-0.264s0.407 1.755 1.418 1.755h1.49c0.633 0 0.667-0.331 0.625-0.433-0.448-1.082-0.68-1.873-0.769-2.5-0.263-1.857 0.657-3.836 2.524-5.457 0.585 0.986 2.253 0.845 2.909-0.096s0.446-2.268-0.192-3.221c-0.49-0.732-1.345-1.327-2.188-1.37zM8.221 4.663c-0.722-0.016-1.536 0.111-2.5 0.409-4.211 1.302-4.177 4.951-3.51 5.745 0 0-0.955 0.479-0.409 1.274 0.448 0.652 3.139 0.191 5.409-0.529s4.226-1.793 5.312-2.692c0.948-0.785 0.551-2.106-0.505-1.947-0.494-0.98-1.632-2.212-3.798-2.26zM18.846 5.962c0.325 0 0.577 0.252 0.577 0.577s-0.252 0.577-0.577 0.577c-0.325 0-0.577-0.252-0.577-0.577s0.252-0.577 0.577-0.577z';
861				break;
862
863			case 'rabbit':
864				svg[0] = '0 0 20 20';
865				svg[1] = 'M10.817 0c-2.248 0-1.586 0.525-1.154 0.505 1.551-0.072 5.199 0.044 6.851 2.428 0 0-1.022-2.933-5.697-2.933zM10.529 0.769c-2.572 0-2.837 0.51-2.837 1.106 0 0.545 1.526 0.836 2.524 0.697 2.778-0.386 4.231-0.12 5.264 0.865-1.010 0.779-0.75 1.401-1.274 1.851-1.093 0.941-2.643-0.673-4.976-0.673-2.496 0-4.712 1.92-4.712 4.76-0.157-0.537-0.769-0.913-1.442-0.913-0.974 0-1.514 0.637-1.514 1.49 0 0.769 1.13 1.791 2.861 0.938 0.499 1.208 2.265 1.364 2.452 1.418 0.538 0.154 1.875 0.098 1.875 0.865 0 0.794-1.034 1.094-1.034 1.707 0 1.070 1.758 0.873 2.284 1.034 1.683 0.517 2.103 1.214 2.788 2.212 0.771 1.122 2.572 1.408 2.572 0.625 0-3.185-4.413-4.126-4.399-4.135 0.608-0.382 2.139-1.397 2.139-3.534 0-1.295-0.703-2.256-1.755-2.861 1.256 0.094 2.572 1.205 2.572 2.74 0 1.877-0.653 2.823-0.769 2.957 1.975-1.158 3.193-3.91 3.029-6.37 0.61 0.401 1.27 0.577 1.971 0.625 0.751 0.052 1.475-0.225 1.635-0.529 0.38-0.723 0.162-2.321-0.12-2.837-0.763-1.392-2.236-1.73-3.606-1.683-1.202-1.671-3.812-2.356-5.529-2.356zM1.37 3.077l-0.553 1.538h3.726c0.521-0.576 1.541-1.207 2.284-1.538h-5.457zM18.846 5.192c0.325 0 0.577 0.252 0.577 0.577s-0.252 0.577-0.577 0.577c-0.325 0-0.577-0.252-0.577-0.577s0.252-0.577 0.577-0.577zM0.553 5.385l-0.553 1.538h3.197c0.26-0.824 0.586-1.328 0.769-1.538h-3.413z';
866				break;
867
868			case 'ellipsis':
869				svg[0] = '0 0 20 20';
870				svg[1] = 'M10.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.985 2.199-2.2s-0.984-2.2-2.199-2.2zM3.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.986 2.199-2.2s-0.984-2.2-2.199-2.2zM17.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.985 2.199-2.2s-0.984-2.2-2.199-2.2z';
871				break;
872
873			case 'pipe':
874				svg[0] = '0 0 20 20';
875				svg[1] = 'M10.15 0.179h0.623c0.069 0 0.127 0.114 0.127 0.253v19.494c0 0.139-0.057 0.253-0.127 0.253h-1.247c-0.069 0-0.126-0.114-0.126-0.253v-19.494c0-0.139 0.057-0.253 0.126-0.253h0.623z';
876				break;
877
878			case 'captions':
879				svg[0] = '0 0 20 20';
880				svg[1] = 'M0.033 3.624h19.933v12.956h-19.933v-12.956zM18.098 10.045c-0.025-2.264-0.124-3.251-0.743-3.948-0.112-0.151-0.322-0.236-0.496-0.344-0.606-0.386-3.465-0.526-6.782-0.526s-6.313 0.14-6.907 0.526c-0.185 0.108-0.396 0.193-0.519 0.344-0.607 0.697-0.693 1.684-0.731 3.948 0.037 2.265 0.124 3.252 0.731 3.949 0.124 0.161 0.335 0.236 0.519 0.344 0.594 0.396 3.59 0.526 6.907 0.547 3.317-0.022 6.176-0.151 6.782-0.547 0.174-0.108 0.384-0.183 0.496-0.344 0.619-0.697 0.717-1.684 0.743-3.949v0 0zM9.689 9.281c-0.168-1.77-1.253-2.813-3.196-2.813-1.773 0-3.168 1.387-3.168 3.617 0 2.239 1.271 3.636 3.372 3.636 1.676 0 2.851-1.071 3.035-2.852h-2.003c-0.079 0.661-0.397 1.168-1.068 1.168-1.059 0-1.253-0.91-1.253-1.876 0-1.33 0.442-2.010 1.174-2.010 0.653 0 1.068 0.412 1.13 1.129h1.977zM16.607 9.281c-0.167-1.77-1.252-2.813-3.194-2.813-1.773 0-3.168 1.387-3.168 3.617 0 2.239 1.271 3.636 3.372 3.636 1.676 0 2.851-1.071 3.035-2.852h-2.003c-0.079 0.661-0.397 1.168-1.068 1.168-1.059 0-1.253-0.91-1.253-1.876 0-1.33 0.441-2.010 1.174-2.010 0.653 0 1.068 0.412 1.13 1.129h1.976z';
881				break;
882
883			case 'descriptions':
884				svg[0] = '0 0 20 20';
885				svg[1] = 'M17.623 3.57h-1.555c1.754 1.736 2.763 4.106 2.763 6.572 0 2.191-0.788 4.286-2.189 5.943h1.484c1.247-1.704 1.945-3.792 1.945-5.943-0-2.418-0.886-4.754-2.447-6.572v0zM14.449 3.57h-1.55c1.749 1.736 2.757 4.106 2.757 6.572 0 2.191-0.788 4.286-2.187 5.943h1.476c1.258-1.704 1.951-3.792 1.951-5.943-0-2.418-0.884-4.754-2.447-6.572v0zM11.269 3.57h-1.542c1.752 1.736 2.752 4.106 2.752 6.572 0 2.191-0.791 4.286-2.181 5.943h1.473c1.258-1.704 1.945-3.792 1.945-5.943 0-2.418-0.876-4.754-2.447-6.572v0zM10.24 9.857c0 3.459-2.826 6.265-6.303 6.265v0.011h-3.867v-12.555h3.896c3.477 0 6.274 2.806 6.274 6.279v0zM6.944 9.857c0-1.842-1.492-3.338-3.349-3.338h-0.876v6.686h0.876c1.858 0 3.349-1.498 3.349-3.348v0z';
886				break;
887
888			case 'sign':
889				svg[0] = '0 0 20 20';
890				svg[1] = 'M10.954 10.307c0.378 0.302 0.569 1.202 0.564 1.193 0.697 0.221 1.136 0.682 1.136 0.682 1.070-0.596 1.094-0.326 1.558-0.682 0.383-0.263 0.366-0.344 0.567-1.048 0.187-0.572-0.476-0.518-1.021-1.558-0.95 0.358-1.463 0.196-1.784 0.167-0.145-0.020-0.12 0.562-1.021 1.247zM14.409 17.196c-0.133 0.182-0.196 0.218-0.363 0.454-0.28 0.361 0.076 0.906 0.253 0.82 0.206-0.076 0.341-0.488 0.567-0.623 0.115-0.061 0.422-0.513 0.709-0.82 0.211-0.238 0.363-0.344 0.564-0.594 0.341-0.422 0.412-0.744 0.709-1.193 0.184-0.236 0.312-0.307 0.481-0.594 0.886-1.679 0.628-2.432 1.475-3.629 0.26-0.353 0.552-0.442 0.964-0.653 0.383-2.793-0.888-4.356-0.879-4.361-1.067 0.623-1.644 0.879-2.751 0.82-0.417-0.005-0.636-0.182-1.048-0.145-0.385 0.015-0.582 0.159-0.964 0.29-0.589 0.182-0.91 0.344-1.529 0.535-0.393 0.11-0.643 0.115-1.050 0.255-0.348 0.147-0.182 0.029-0.427 0.312-0.317 0.348-0.238 0.623-0.535 1.222-0.371 0.785-0.326 0.891-0.115 0.987-0.14 0.402-0.174 0.672-0.14 1.107 0.039 0.331-0.101 0.562 0.255 0.825 0.483 0.361 1.499 1.205 1.757 1.217 0.39-0.012 1.521 0.029 2.096-0.368 0.13-0.081 0.167-0.162 0.056 0.145-0.022 0.037-1.433 1.136-1.585 1.131-1.794 0.056-1.193 0.157-1.303 0.115-0.091 0-0.955-1.055-1.477-0.682-0.196 0.12-0.287 0.236-0.363 0.452 0.066 0.137 0.383 0.358 0.675 0.54 0.422 0.27 0.461 0.552 0.881 0.653 0.513 0.115 1.060 0.039 1.387 0.081 0.125 0.034 1.256-0.297 1.961-0.675 0.65-0.336-0.898 0.648-1.276 1.131-1.141 0.358-0.82 0.373-1.362 0.483-0.503 0.115-0.479 0.086-0.822 0.196-0.356 0.086-0.648 0.572-0.312 0.825 0.201 0.167 0.827-0.066 1.445-0.086 0.275-0.005 1.391-0.518 1.644-0.653 0.633-0.339 1.099-0.81 1.472-1.077 0.518-0.361-0.584 0.991-1.050 1.558zM8.855 9.799c-0.378-0.312-0.569-1.212-0.564-1.217-0.697-0.206-1.136-0.667-1.136-0.653-1.070 0.582-1.099 0.312-1.558 0.653-0.388 0.277-0.366 0.363-0.567 1.045-0.187 0.594 0.471 0.535 1.021 1.561 0.95-0.344 1.463-0.182 1.784-0.142 0.145 0.010 0.12-0.572 1.021-1.247zM5.4 2.911c0.133-0.191 0.196-0.228 0.368-0.454 0.27-0.371-0.081-0.915-0.253-0.849-0.211 0.096-0.346 0.508-0.599 0.653-0.093 0.052-0.4 0.503-0.682 0.82-0.211 0.228-0.363 0.334-0.564 0.599-0.346 0.407-0.412 0.729-0.709 1.161-0.184 0.258-0.317 0.324-0.481 0.621-0.886 1.669-0.631 2.422-1.475 3.6-0.26 0.38-0.552 0.461-0.964 0.682-0.383 2.788 0.883 4.346 0.879 4.336 1.068-0.609 1.639-0.861 2.751-0.825 0.417 0.025 0.636 0.201 1.048 0.174 0.385-0.025 0.582-0.169 0.964-0.285 0.589-0.196 0.91-0.358 1.499-0.54 0.422-0.12 0.672-0.125 1.080-0.285 0.348-0.128 0.182-0.010 0.427-0.282 0.312-0.358 0.238-0.633 0.508-1.217 0.398-0.8 0.353-0.906 0.142-0.991 0.135-0.412 0.174-0.677 0.14-1.107-0.044-0.336 0.101-0.572-0.255-0.82-0.483-0.375-1.499-1.22-1.752-1.222-0.395 0.002-1.526-0.039-2.101 0.339-0.13 0.101-0.167 0.182-0.056-0.11 0.022-0.052 1.433-1.148 1.585-1.163 1.794-0.039 1.193-0.14 1.303-0.088 0.091-0.007 0.955 1.045 1.477 0.682 0.191-0.13 0.287-0.245 0.368-0.452-0.071-0.147-0.388-0.368-0.68-0.537-0.422-0.282-0.464-0.564-0.881-0.655-0.513-0.125-1.065-0.049-1.387-0.11-0.125-0.015-1.256 0.317-1.956 0.68-0.66 0.351 0.893-0.631 1.276-1.136 1.136-0.339 0.81-0.353 1.36-0.479 0.501-0.101 0.476-0.071 0.82-0.172 0.351-0.096 0.648-0.577 0.312-0.849-0.206-0.152-0.827 0.081-1.44 0.086-0.28 0.020-1.396 0.533-1.649 0.677-0.633 0.329-1.099 0.8-1.472 1.048-0.523 0.38 0.584-0.967 1.050-1.529z';
891				break;
892
893			case 'mute':
894			case 'volume-mute':
895				svg[0] = '0 0 20 20';
896				svg[1] = 'M7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714zM18.75 12.093v1.657h-1.657l-2.093-2.093-2.093 2.093h-1.657v-1.657l2.093-2.093-2.093-2.093v-1.657h1.657l2.093 2.093 2.093-2.093h1.657v1.657l-2.093 2.093z';
897				break;
898
899			case 'volume-soft':
900				svg[0] = '0 0 20 20';
901				svg[1] = 'M10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z';
902				break;
903
904			case 'volume-medium':
905				svg[0] = '0 0 20 20';
906				svg[1] = 'M14.053 16.241c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 2.559-2.559 2.559-6.722 0-9.281-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c1.594 1.594 2.471 3.712 2.471 5.966s-0.878 4.373-2.471 5.966c-0.183 0.183-0.423 0.275-0.663 0.275zM10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z';
907				break;
908
909			case 'volume-loud':
910				svg[0] = '0 0 21 20';
911				svg[1] = 'M17.384 18.009c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.712-1.712 2.654-3.988 2.654-6.408s-0.943-4.696-2.654-6.408c-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.066 2.066 3.204 4.813 3.204 7.734s-1.138 5.668-3.204 7.734c-0.183 0.183-0.423 0.275-0.663 0.275zM14.053 16.241c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 2.559-2.559 2.559-6.722 0-9.281-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c1.594 1.594 2.471 3.712 2.471 5.966s-0.878 4.373-2.471 5.966c-0.183 0.183-0.423 0.275-0.663 0.275zM10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z';
912				break;
913
914			case 'chapters':
915				svg[0] = '0 0 20 20';
916				svg[1] = 'M5 2.5v17.5l6.25-6.25 6.25 6.25v-17.5zM15 0h-12.5v17.5l1.25-1.25v-15h11.25z';
917				break;
918
919			case 'transcript':
920				svg[0] = '0 0 20 20';
921				svg[1] = 'M0
921.033 3.624h19.933v12.956h-19.933v-12.956zM18.098 10.045c-0.025-2.264-0.124-3.251-0.743-3.948-0.112-0.151-0.322-0.236-0.496-0.344-0.606-0.386-3.465-0.526-6.782-0.526s-6.313 0.14-6.907 0.526c-0.185 0.108-0.396 0.193-0.519 0.344-0.607 0.697-0.693 1.684-0.731 3.948 0.037 2.265 0.124 3.252 0.731 3.949 0.124 0.161 0.335 0.236 0.519 0.344 0.594 0.396 3.59 0.526 6.907 0.547 3.317-0.022 6.176-0.151 6.782-0.547 0.174-0.108 0.384-0.183 0.496-0.344 0.619-0.697 0.717-1.684 0.743-3.949v0 0zM9.689 9.281c-0.168-1.77-1.253-2.813-3.196-2.813-1.773 0-3.168 1.387-3.168 3.617 0 2.239 1.271 3.636 3.372 3.636 1.676 0 2.851-1.071 3.035-2.852h-2.003c-0.079 0.661-0.397 1.168-1.068 1.168-1.059 0-1.253-0.91-1.253-1.876 0-1.33 0.442-2.010 1.174-2.010 0.653 0 1.068 0.412 1.13 1.129h1.977zM16.607 9.281c-0.167-1.77-1.252-2.813-3.194-2.813-1.773 0-3.168 1.387-3.168 3.617 0 2.239 1.271 3.636 3.372 3.636 1.676 0 2.851-1.071 3.035-2.852h-2.003c-0.079 0.661-0.397 1.168-1.068 1.168-1.059 0-1.253-0.91-1.253-1.876 0-1.33 0.441-2.010 1.174-2.010 0.653 0 1.068 0.412 1.13 1.129h1.976z';
922				break;
923
924			case 'preferences':
925				svg[0] = '0 0 20 20';
926				svg[1] = 'M18.238 11.919c-1.049-1.817-0.418-4.147 1.409-5.205l-1.965-3.404c-0.562 0.329-1.214 0.518-1.911 0.518-2.1 0-3.803-1.714-3.803-3.828h-3.931c0.005 0.653-0.158 1.314-0.507 1.919-1.049 1.818-3.382 2.436-5.212 1.382l-1.965 3.404c0.566 0.322 1.056 0.793 1.404 1.396 1.048 1.815 0.42 4.139-1.401 5.2l1.965 3.404c0.56-0.326 1.209-0.513 1.902-0.513 2.094 0 3.792 1.703 3.803 3.808h3.931c-0.002-0.646 0.162-1.3 0.507-1.899 1.048-1.815 3.375-2.433 5.203-1.387l1.965-3.404c-0.562-0.322-1.049-0.791-1.395-1.391zM10 14.049c-2.236 0-4.050-1.813-4.050-4.049s1.813-4.049 4.050-4.049 4.049 1.813 4.049 4.049c-0 2.237-1.813 4.049-4.049 4.049z';
927				break;
928
929			case 'close':
930				svg[0] = '0 0 16 20';
931				svg[1] = 'M1.228 14.933q0-0.446 0.312-0.759l3.281-3.281-3.281-3.281q-0.313-0.313-0.313-0.759t0.313-0.759l1.518-1.518q0.313-0.313 0.759-0.313t0.759 0.313l3.281 3.281 3.281-3.281q0.313-0.313 0.759-0.313t0.759 0.313l1.518 1.518q0.313 0.313 0.313 0.759t-0.313 0.759l-3.281 3.281 3.281 3.281q0.313 0.313 0.313 0.759t-0.313 0.759l-1.518 1.518q-0.313 0.313-0.759 0.313t-0.759-0.313l-3.281-3.281-3.281 3.281q-0.313 0.313-0.759 0.313t-0.759-0.313l-1.518-1.518q-0.313-0.313-0.313-0.759z';
932				break;
933
934			case 'fullscreen-expand':
935				svg[0] = '0 0 20 20';
936				svg[1] = 'M0 18.036v-5q0-0.29 0.212-0.502t0.502-0.212 0.502 0.212l1.607 1.607 3.705-3.705q0.112-0.112 0.257-0.112t0.257 0.112l1.272 1.272q0.112 0.112 0.112 0.257t-0.112 0.257l-3.705 3.705 1.607 1.607q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-5q-0.29 0-0.502-0.212t-0.212-0.502zM8.717 8.393q0-0.145 0.112-0.257l3.705-3.705-1.607-1.607q-0.212-0.212-0.212-0.502t0.212-0.502 0.502-0.212h5q0.29 0 0.502 0.212t0.212 0.502v5q0 0.29-0.212 0.502t-0.502 0.212-0.502-0.212l-1.607-1.607-3.705 3.705q-0.112 0.112-0.257 0.112t-0.257-0.112l-1.272-1.272q-0.112-0.112-0.112-0.257z';
937				break;
938
939			case 'fullscreen-collapse':
940				svg[0] = '0 0 20 20';
941				svg[1] = 'M0.145 16.964q0-0.145 0.112-0.257l3.705-3.705-1.607-1.607q-0.212-0.212-0.212-0.502t0.212-0.502 0.502-0.212h5q0.29 0 0.502 0.212t0.212 0.502v5q0 0.29-0.212 0.502t-0.502 0.212-0.502-0.212l-1.607-1.607-3.705 3.705q-0.112 0.112-0.257 0.112t-0.257-0.112l-1.272-1.272q-0.112-0.112-0.112-0.257zM8.571 9.464v-5q0-0.29 0.212-0.502t0.502-0.212 0.502 0.212l1.607 1.607 3.705-3.705q0.112-0.112 0.257-0.112t0.257 0.112l1.272 1.272q0.112 0.112 0.112 0.257t-0.112 0.257l-3.705 3.705 1.607 1.607q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-5q-0.29 0-0.502-0.212t-0.212-0.502z';
942				break;
943
944			case 'help':
945				svg[0] = '0 0 11 20';
946				svg[1] = 'M0.577 6.317q-0.028-0.167 0.061-0.313 1.786-2.969 5.179-2.969 0.893 0 1.797 0.346t1.629 0.926 1.183 1.423 0.458 1.769q0 0.603-0.173 1.127t-0.391 0.854-0.614 0.664-0.642 0.485-0.681 0.396q-0.458 0.257-0.765 0.725t-0.307 0.748q0 0.19-0.134 0.363t-0.313 0.173h-2.679q-0.167 0-0.285-0.206t-0.117-0.419v-0.502q0-0.926 0.725-1.747t1.596-1.211q0.658-0.301 0.938-0.625t0.279-0.848q0-0.469-0.519-0.826t-1.2-0.357q-0.725 0-1.205 0.324-0.391 0.279-1.194 1.283-0.145 0.179-0.346 0.179-0.134 0-0.279-0.089l-1.83-1.395q-0.145-0.112-0.173-0.279zM3.786 16.875v-2.679q0-0.179 0.134-0.313t0.313-0.134h2.679q0.179 0 0.313 0.134t0.134 0.313v2.679q0 0.179-0.134 0.313t-0.313 0.134h-2.679q-0.179 0-0.313-0.134t-0.134-0.313z';
947				break;
948		}
949
950		return svg;
951	};
952
953	// Initialize player based on data on page.
954	// This sets some variables, but does not modify anything.	Safe to call multiple times.
955	// Can call again after updating this.media so long as new media element has the same ID.
956	AblePlayer.prototype.reinitialize = function () {
957
958		var deferred, promise, thisObj, errorMsg, srcFile;
959
960		deferred = new $.Deferred();
961		promise = deferred.promise();
962		thisObj = this;
963
964		// if F12 Developer Tools aren't open in IE (through 9, no longer a problen in IE10)
965		// console.log causes an error - can't use debug without a console to log messages to
966		if (! window.console) {
967			this.debug = false;
968		}
969
970		this.startedPlaying = false;
971		// TODO: Move this setting to cookie.
972		this.autoScrollTranscript = true;
973		//this.autoScrollTranscript = this.getCookie(autoScrollTranscript); // (doesn't work)
974
975		// Bootstrap from this.media possibly being an ID or other selector.
976		this.$media = $(this.media).first();
977		this.media = this.$media[0];
978
979		// Set media type to 'audio' or 'video'; this determines some of the behavior of player creation.
980		if (this.$media.is('audio')) {
981			this.mediaType = 'audio';
982		}
983		else if (this.$media.is('video')) {
984			this.mediaType = 'video';
985		}
986		else {
987			// Able Player was initialized with some element other than <video> or <audio>
988			this.provideFallback();
989			deferred.fail();
990			return promise;
991		}
992
993		this.$sources = this.$media.find('source');
994
995		this.player = this.getPlayer();
996		if (!this.player) {
997			// an error was generated in getPlayer()
998			this.provideFallback();
999		}
1000		this.setIconType();
1001
1002		deferred.resolve();
1003		return promise;
1004	};
1005
1006	AblePlayer.prototype.setPlayerSize = function(width, height) {
1007
1008		var mediaId = this.$media.attr('id');
1009
1010		// Called again after width and height are known 
1011
1012		if (this.mediaType === 'audio') { 			
1013			if (this.playerWidth) { 
1014				this.$ableWrapper.css('width',this.playerWidth + 'px'); 
1015			}
1016		}
1017		else if (width > 0 && height > 0) { 
1018			this.playerWidth = width; 
1019			this.playerHeight = height; 
1020			this.aspectRatio = height / width; 
1021		}
1022	};
1023
1024	AblePlayer.prototype.setIconType = function() {
1025
1026		// returns either "svg", "font" or "image" (in descending order of preference)
1027		// Test for support of each type. If not supported, test the next type.
1028		// last resort is image icons
1029
1030		var $tempButton, $testButton, controllerFont;
1031
1032		if (this.forceIconType) {
1033			// use value specified in data-icon-type
1034			return false;
1035		}
1036
1037		// test for SVG support
1038		// Test this method widely; failed as expected on IE8 and below
1039		// https://stackoverflow.com/a/27568129/744281
1040		if (!!(document.createElementNS && document.createElementNS('http://www.w3.org/2000/svg','svg').createSVGRect)) {
1041			// browser supports SVG
1042			this.iconType = 'svg';
1043		}
1044		else {
1045			// browser does NOT support SVG
1046			// test whether browser can support icon fonts, and whether user has overriding the default style sheet
1047			// which could cause problems with proper display of the icon fonts
1048			if (window.getComputedStyle) {
1049
1050				// webkit doesn't return calculated styles unless element has been added to the DOM
1051				// and is visible (note: visibly clipped is considered "visible")
1052				// use playpauseButton for font-family test if it exists; otherwise must create a new temp button
1053				if ($('span.icon-play').length) {
1054					$testButton = $('span.icon-play');
1055				}
1056				else {
1057					$tempButton = $('<span>',{
1058						'class': 'icon-play able-clipped'
1059					});
1060					$('body').append($tempButton);
1061					$testButton = $tempButton;
1062				}
1063
1064				// the following retrieves the computed value of font-family
1065				// tested in Firefox 45.x with "Allow pages to choose their own fonts" unchecked - works!
1066				// tested in Chrome 49.x with Font Changer plugin - works!
1067				// tested in IE with user-defined style sheet enables - works!
1068				// It does NOT account for users who have "ignore font styles on web pages" checked in IE
1069				// There is no known way to check for that ???
1070				controllerFont = window.getComputedStyle($testButton.get(0), null).getPropertyValue('font-family');
1071				if (typeof controllerFont !== 'undefined') {
1072					if (controllerFont.indexOf('able') !== -1) {
1073						this.iconType = 'font';
1074					}
1075					else {
1076						this.iconType = 'image';
1077					}
1078				}
1079				else {
1080					// couldn't get computed font-family; use images to be safe
1081					this.iconType = 'image';
1082				}
1083			}
1084			else { 
1085				// window.getComputedStyle is not supported (IE 8 and earlier)
1086				// No known way to detect computed font
1087				// The following retrieves the value from the style sheet, not the computed font
1088				// controllerFont = $tempButton.get(0).currentStyle.fontFamily;
1089				// It will therefore return "able", even if the user is overriding that with a custom style sheet
1090				// To be safe, use images
1091				this.iconType = 'image';
1092			}
1093			if (typeof $tempButton !== 'undefined') {
1094				$tempButton.remove();
1095			}
1096		}
1097	};
1098
1099	// Perform one-time setup for this instance of player; called after player is first initialized.
1100	AblePlayer.prototype.setupInstance = function () {
1101
1102		var deferred = new $.Deferred();
1103		var promise = deferred.promise();
1104
1105		if (this.$media.attr('id')) {
1106			this.mediaId = this.$media.attr('id');
1107		}
1108		else {
vendor: 4,087 bytes, lines 1109-1225
1109			// Ensure the base media element always has an ID.
1110			this.mediaId = "ableMediaId_" + this.ableIndex;
1111			this.$media.attr('id', this.mediaId);
1112		}
1113		deferred.resolve();
1114		return promise;
1115	};
1116
1117	AblePlayer.prototype.setupInstancePlaylist = function() {
1118
1119		// find a matching playlist and set this.hasPlaylist
1120		// if there is one, also set this.$playlist, this.playlistIndex, & this.playlistEmbed
1121		var thisObj = this;
1122
1123		this.hasPlaylist = false; // will change to true if a matching playlist is found
1124
1125		$('.able-playlist').each(function() {
1126			if ($(this).data('player') === thisObj.mediaId) {
1127				// this is the playlist for the current player
1128				thisObj.hasPlaylist = true;
1129				// If using an embedded player, we'll replace $playlist with the clone later.
1130				thisObj.$playlist = $(this).find('li');
1131
1132				// check to see if list item has YouTube as its source
1133				// if it does, inject a thumbnail from YouTube
1134				var $youTubeVideos = $(this).find('li[data-youtube-id]');
1135				$youTubeVideos.each(function() {
1136					var youTubeId = $(this).attr('data-youtube-id');
1137					var youTubePoster = thisObj.getYouTubePosterUrl(youTubeId,'120');
1138					var $youTubeImg = $('<img>',{
1139						'src': youTubePoster,
1140						'alt': ''
1141					});
1142					$(this).find('button').prepend($youTubeImg);
1143				});
1144
1145				// check to see if list item has Vimeo as its source
1146				// if it does, inject a thumbnail from Vimeo
1147				var $vimeoVideos = $(this).find('li[data-vimeo-id]');
1148				$vimeoVideos.each(function() {
1149					var vimeoId = $(this).attr('data-youtube-id');
1150					var vimeoPoster = thisObj.getVimeoPosterUrl(vimeoId,'120');
1151					var $vimeoImg = $('<img>',{
1152						'src': vimeoPoster,
1153						'alt': ''
1154					});
1155					$(this).find('button').prepend($vimeoImg);
1156				});
1157
1158				// add accessibility to the list markup
1159				$(this).find('li span').attr('aria-hidden','true');
1160				thisObj.playlistIndex = 0;
1161				var dataEmbedded = $(this).data('embedded');
1162				if (typeof dataEmbedded !== 'undefined' && dataEmbedded !== false) {
1163					// embed playlist within player
1164					thisObj.playlistEmbed = true;
1165				}
1166				else {
1167					thisObj.playlistEmbed = false;
1168				}
1169			}
1170		});
1171
1172		if (this.hasPlaylist && this.loop) {
1173			// browser will loop the current track in the playlist, rather than the playlist
1174			// therefore, need to remove loop attribute from media element
1175			// but keep this.loop as true and handle the playlist looping ourselves
1176			this.media.removeAttribute('loop');
1177		}
1178		if (this.hasPlaylist && this.playlistEmbed) {
1179			// Copy the playlist out of the dom, so we can reinject when we build the player.
1180			var parent = this.$playlist.parent();
1181			this.$playlistDom = parent.clone();
1182			parent.remove();
1183		}
1184		if (this.hasPlaylist && this.$sources.length === 0) {
1185			// no source elements were provided. Construct them from the first playlist item
1186			this.cuePlaylistItem(0);
1187			// redefine this.$sources now that media contains one or more <source> elements
1188			this.$sources = this.$media.find('source');
1189		}
1190	};
1191
1192	AblePlayer.prototype.recreatePlayer = function () {
1193
1194		// Creates the appropriate player for the current source.
1195		// This function is called each time a new media instance is loaded 
1196		// e.g., 
1197		// User clicks on an item in a playlist 
1198		// User swaps to/from described version of video 
1199		// Blocks of code that only need to be executed once are controlled 
1200		// by this.playerCreated 
1201
1202		// TODO: Ensure when recreating player that we carry over the mediaId
1203		if (!this.player) {
1204			console.log("Can't create player; no appropriate player type detected.");
1205			return;
1206		}
1207
1208		var deferred, promise, thisObj, prefsGroups, i;
1209
1210		deferred = new $.Deferred();
1211		promise = deferred.promise();
1212		thisObj = this;
1213
1214		this.playerDeleted = false; // reset after deletePlayer() 
1215
1216		// set temp stopgap to prevent this function from executing again before finished
1217		this.recreatingPlayer = true; 
1218
1219		if (!this.playerCreated) {
1220			// only call these functions once
1221			this.loadCurrentPreferences();
1222			this.injectPlayerCode();
1223			this.resizePlayer(this.media.videoWidth,this.media.videoHeight); 
1224		}
1225
1226		this.getSampleDescriptionText(); 
1227
1228		this.initSignLanguage();
1229
1230		this.initPlayer().then(function() {
1231
1232			thisObj.getTracks().then(function() { 
1233
1234				thisObj.initDescription().then(function() {
1235
1236					thisObj.setupTracks().then(function() {
1237						if (thisObj.hasClosedDesc) { 
1238							if (!thisObj.$descDiv || 
1239								(thisObj.$descDiv && !($.contains(thisObj.$ableDiv[0], thisObj.$descDiv[0])))) {
1240								// descDiv either doesn't exist, or exists in an orphaned state 
1241								// Either way, it needs to be rebuilt...  
1242								thisObj.injectTextDescriptionArea();
1243							}
1244						}
1245						thisObj.initSpeech('init');
1246
1247						thisObj.setupTranscript().then(function() {
1248
1249							thisObj.initStenoFrame().then(function() {
1250
1251								if (thisObj.stenoMode && thisObj.$stenoFrame) {
1252									thisObj.stenoFrameContents = thisObj.$stenoFrame.contents();
1253								}
1254								thisObj.getMediaTimes().then(function(mediaTimes) {
1255
1256									thisObj.duration = mediaTimes['duration'];
1257									thisObj.elapsed = mediaTimes['elapsed'];
1258									thisObj.setFullscreen(false);
1259
1260									if (typeof thisObj.volume === 'undefined') {
1261										thisObj.volume = thisObj.defaultVolume;
1262									}
1263									if (thisObj.volume) {
1264										thisObj.setVolume(thisObj.volume);
1265									}
1266									if (thisObj.transcriptType) {
1267										thisObj.addTranscriptAreaEvents();
1268										thisObj.updateTranscript();
1269									}
1270									if (thisObj.captions.length) {
1271										thisObj.initDefaultCaption();
1272									}
1273
1274									// setMediaAttributes() sets textTrack.mode to 'disabled' for all tracks
1275									// This tells browsers to ignore the text tracks so Able Player can handle them
1276									// However, timing is critical as browsers - especially Safari - tend to ignore this request
1277									// unless it's sent late in the intialization process.
1278									// If browsers ignore the request, the result is redundant captions
1279									thisObj.setMediaAttributes();
1280									thisObj.addControls();
1281									thisObj.addEventListeners();
1282
1283									// inject each of the hidden forms that will be accessed from the Preferences popup menu
1284									prefsGroups = thisObj.getPreferencesGroups();
1285									for (i = 0; i < prefsGroups.length; i++) {
1286										thisObj.injectPrefsForm(prefsGroups[i]);
1287									}
1288									thisObj.setupPopups();
1289									thisObj.updateCaption();
1290									thisObj.injectVTS();
1291									if (thisObj.chaptersDivLocation) {
1292										thisObj.populateChaptersDiv();
1293									}
1294									thisObj.showSearchResults();
1295
1296									// Go ahead and load media, without user requesting it
1297									// Ideally, we would wait until user clicks play, rather than unnecessarily consume their bandwidth
1298									// However, the media needs to load for us to get the media's duration
1299									if (thisObj.player === 'html5') {
1300										if (!thisObj.loadingMedia) { 
1301											thisObj.$media[0].load();
1302											thisObj.loadingMedia = true; 
1303										}
1304									}
1305									// refreshControls is called twice building/initializing the player
1306									// this is the second. Best to pause a bit before executing, to be sure all prior steps are complete
1307									setTimeout(function() {
1308										thisObj.refreshControls('init'); 
1309										deferred.resolve(); 
1310									},100);								
1311								}); 
1312							}); 
1313						});
1314					});
1315				});			 
1316			});
1317		},
1318		function() {	 // initPlayer fail
1319			thisObj.provideFallback();
1320		});
1321		return promise; 
1322	};
1323
1324	AblePlayer.prototype.initPlayer = function () {
1325
1326		var thisObj = this;
1327		var playerPromise;
1328		// First run player specific initialization.
1329		if (this.player === 'html5') {
1330			playerPromise = this.initHtml5Player();
1331		}
1332		else if (this.player === 'youtube') {
1333			playerPromise = this.initYouTubePlayer();
1334		}
1335		else if (this.player === 'vimeo') {
1336			playerPromise = this.initVimeoPlayer();
1337		}
1338		// After player specific initialization is done, run remaining general initialization.
1339		var deferred = new $.Deferred();
1340		var promise = deferred.promise();
1341		playerPromise.done(
1342			function () { // done/resolved
1343				if (thisObj.useFixedSeekInterval) {
1344					if (!thisObj.seekInterval) {
1345						thisObj.seekInterval = thisObj.defaultSeekInterval;
1346					}
1347					else {
1348						// fixed seekInterval was already assigned, using value of data-seek-interval attribute
1349					}
1350					thisObj.seekIntervalCalculated = true;
1351				}
1352				else {
1353					thisObj.setSeekInterval();
1354				}
1355				deferred.resolve();
1356			}
1357		).fail(function () { // failed
1358			deferred.reject();
1359			}
1360		);
1361
1362		return promise;
1363	};
1364
1365	AblePlayer.prototype.initStenoFrame = function() {
1366
1367		var thisObj, deferred, promise, $iframe;
1368		thisObj = this;
1369
1370		deferred = new $.Deferred();
1371		promise = deferred.promise();
1372
1373		if (this.stenoMode && this.$stenoFrame) {
1374
1375			if (this.$stenoFrame[0].contentWindow,document.readyState == 'complete') {
1376				// iframe has already loaded
1377				deferred.resolve();
1378			}
1379			else {
1380				// iframe has not loaded. Wait for it.
1381				this.$stenoFrame.on('load',function() {
1382					deferred.resolve();
1383				});
1384			}
1385		}
1386		else {
1387			// there is no stenoFrame to initialize
1388			deferred.resolve();
1389		}
1390		return promise;
1391	};
1392
1393	AblePlayer.prototype.setSeekInterval = function () {
1394
1395		// this function is only called if this.useFixedSeekInterval is false
1396		// if this.useChapterTimes, this is called as each new chapter is loaded
1397		// otherwise, it's called once, as the player is initialized
1398		var thisObj, duration;
1399		thisObj = this;
1400		this.seekInterval = this.defaultSeekInterval;
1401		if (this.useChapterTimes) {
1402			duration = this.chapterDuration;
1403		}
1404		else {
1405			duration = this.duration;
1406		}
1407		if (typeof duration === 'undefined' || duration < 1) {
1408			// no duration; just use default for now but keep trying until duration is available
1409			this.seekIntervalCalculated = false;
1410			return;
1411		}
1412		else {
1413			if (duration <= 20) {
1414				this.seekInterval = 5;	 // 4 steps max
1415			}
1416			else if (duration <= 30) {
1417				this.seekInterval = 6; // 5 steps max
1418			}
1419			else if (duration <= 40) {
1420				this.seekInterval = 8; // 5 steps max
1421			}
1422			else if (duration <= 100) {
1423				this.seekInterval = 10; // 10 steps max
1424			}
1425			else {
1426				// never more than 10 steps from start to end
1427				this.seekInterval = (duration / 10);
1428			}
1429			this.seekIntervalCalculated = true;
1430		}
1431	};
1432
1433	AblePlayer.prototype.initDefaultCaption = function () {
1434
1435		var captions, i;
1436
1437		captions = this.captions;
1438		if (captions.length > 0) {
1439			for (i=0; i<captions.length; i++) {
1440				if (captions[i].def === true) {
1441					this.captionLang = captions[i].language;
1442					this.selectedCaptions = captions[i];
1443				}
1444			}
1445			if (typeof this.captionLang === 'undefined') {
1446				// No caption track was flagged as default
1447				// find and use a caption language that matches the player language
1448				for (i=0; i<captions.length; i++) {
1449					if (captions[i].language === this.lang) {
1450						this.captionLang = captions[i].language;
1451						this.selectedCaptions = captions[i];
1452					}
1453				}
1454			}
1455			if (typeof this.captionLang === 'undefined') {
1456				// Still no matching caption track
1457				// just use the first track
1458				this.captionLang = captions[0].language;
1459				this.selectedCaptions = captions[0];
1460			}
1461			if (typeof this.captionLang !== 'undefined') {
1462				// reset transcript selected <option> to this.captionLang
1463				if (this.$transcriptLanguageSelect) {
1464					this.$transcriptLanguageSelect.find('option[lang=' + this.captionLang + ']').prop('selected',true);
1465				}
1466				// sync all other tracks to this same languge
1467				this.syncTrackLanguages('init',this.captionLang);
1468			}
1469			if (this.player === 'vimeo') {
1470				if (this.usingVimeoCaptions && this.prefCaptions == 1) {
1471						// initialize Vimeo captions to the default language
1472						this.vimeoPlayer.enableTextTrack(this.captionLang).then(function(track) {
1473							// track.language = the iso code for the language
1474							// track.kind = 'captions' or 'subtitles'
1475							// track.label = the human-readable label
1476						}).catch(function(error) {
1477							switch (error.name) {
1478								case 'InvalidTrackLanguageError':
1479									// no track was available with the specified language
1480									console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
1481									break;
1482								case 'InvalidTrackError':
1483									// no track was available with the specified language and kind
1484									console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
1485									break;
1486								default:
1487									// some other error occurred
1488									console.log('Error loading ' + track.label + ' ' + track.kind + ' track');
1489									break;
1490							}
1491						});
1492					}
1493					else {
1494						// disable Vimeo captions.
1495						this.vimeoPlayer.disableTextTrack().then(function() {
1496							// Vimeo captions disabled
1497						}).catch(function(error) {
1498							console.log('Error disabling Vimeo text track: ',error);
1499						});
1500					}
1501				}
1502		}
1503	};
1504
1505	AblePlayer.prototype.initHtml5Player = function () {
1506		// Nothing special to do!
1507		var deferred = new $.Deferred();
1508		var promise = deferred.promise();
1509		deferred.resolve();
1510		return promise;
1511	};
1512
1513	// Sets media/track/source attributes; is called whenever player is recreated since $media may have changed.
1514	AblePlayer.prototype.setMediaAttributes = function () {
1515		// Firefox puts videos in tab order; remove.
1516		this.$media.attr('tabindex', -1);
1517
1518		// Keep native player from displaying captions/subtitles by setting textTrack.mode='disabled'
1519		// https://dev.w3.org/html5/spec-author-view/video.html#text-track-mode
1520		// This *should* work but historically hasn't been supported in all browsers
1521		// Workaround for non-supporting browsers is to remove default attribute
1522		// We're doing that too in track.js > setupCaptions()
1523		var textTracks = this.$media.get(0).textTracks;
1524		if (textTracks) {
1525			var i = 0;
1526			while (i < textTracks.length) {
1527				textTracks[i].mode = 'disabled';
1528				i += 1;
1529			}
1530		}
1531	};
1532
1533	AblePlayer.prototype.getPlayer = function() {
1534
1535		// Determine which player to use, if any
1536		// return 'html5', 'youtube', 'vimeo', or null
1537
1538		var i, sourceType, $newItem;
1539		if (this.testFallback) { 
1540			return null; 
1541		}
1542		else if (this.youTubeId) {
1543			if (this.mediaType !== 'video') {
1544				// attempting to play a YouTube video using an element other than <video>
1545				return null;
1546			}
1547			else {
1548				return 'youtube';
1549			}
1550		}
1551		else if (this.vimeoId) {
1552			if (this.mediaType !== 'video') {
1553				// attempting to play a Vimeo video using an element other than <video>
1554				return null;
1555			}
1556			else {
1557				return 'vimeo';
1558			}
1559
1560		}
1561		else if (this.media.canPlayType) {
1562			return 'html5';
1563		}
1564		else {
1565			// Browser does not support the available media file
1566			return null;
1567		}
1568	};
1569
1570})(jQuery);
1571
1572(function ($) {
1573	AblePlayer.prototype.setCookie = function(cookieValue) {
1574
1575		Cookies.set('Able-Player', JSON.stringify(cookieValue), {
1576			expires: 90,
1577			sameSite: 'strict'
1578		});
1579	};
1580
1581	AblePlayer.prototype.getCookie = function() {
1582
1583		var defaultCookie = {
1584			preferences: {},
1585			sign: {},
1586			transcript: {},
1587			voices: []
1588		};
1589
1590		var cookie;
1591		try {
vendor: 10,388 bytes, lines 1592-1926
1592			cookie = JSON.parse(Cookies.get('Able-Player'));
1593		}
1594		catch (err) {
1595			// Original cookie can't be parsed; update to default
1596			this.setCookie(defaultCookie);
1597			cookie = defaultCookie;
1598		}
1599		if (cookie) {
1600			return cookie;
1601		}
1602		else {
1603			return defaultCookie;
1604		}
1605	};
1606
1607	AblePlayer.prototype.updateCookie = function( setting ) {
1608
1609		// called when a particular setting had been updated
1610		// useful for settings updated indepedently of Preferences dialog
1611		// e.g., prefAutoScrollTranscript, which is updated in control.js > handleTranscriptLockToggle()
1612		// setting is any supported preference name (e.g., "prefCaptions")
1613		// OR 'transcript' or 'sign' (not user-defined preferences, used to save position of draggable windows)
1614		var cookie, $window, windowPos, available, i, prefName, voiceLangFound, newVoice;
1615		cookie = this.getCookie();
1616		if (setting === 'transcript' || setting === 'sign') {
1617			if (setting === 'transcript') {
1618				$window = this.$transcriptArea;
1619				windowPos = $window.position();
1620				if (typeof cookie.transcript === 'undefined') {
1621					cookie.transcript = {};
1622				}
1623				cookie.transcript['position'] = $window.css('position'); // either 'relative' or 'absolute'
1624				cookie.transcript['zindex'] = $window.css('z-index');
1625				cookie.transcript['top'] = windowPos.top;
1626				cookie.transcript['left'] = windowPos.left;
1627				cookie.transcript['width'] = $window.width();
1628				cookie.transcript['height'] = $window.height();
1629			}
1630			else if (setting === 'sign') {
1631				$window = this.$signWindow;
1632				windowPos = $window.position();
1633				if (typeof cookie.sign === 'undefined') {
1634					cookie.sign = {};
1635				}
1636				cookie.sign['position'] = $window.css('position'); // either 'relative' or 'absolute'
1637				cookie.sign['zindex'] = $window.css('z-index');
1638				cookie.sign['top'] = windowPos.top;
1639				cookie.sign['left'] = windowPos.left;
1640				cookie.sign['width'] = $window.width();
1641				cookie.sign['height'] = $window.height();
1642			}
1643		}
1644		else if (setting === 'voice') { 
1645			if (typeof cookie.voices === 'undefined') {
1646				cookie.voices = [];
1647			}
1648			// replace preferred voice for this lang in cookie.voices array, if one exists 
1649			// otherwise, add it to the array 
1650			voiceLangFound = false; 
1651			for (var v=0; v < cookie.voices.length; v++) { 						
1652				if (cookie.voices[v].lang === this.prefDescVoiceLang) { 
1653					voiceLangFound = true; 
1654					cookie.voices[v].name = this.prefDescVoice; 
1655				}
1656			}
1657			if (!voiceLangFound) { 
1658				// no voice has been saved yet for this language. Add it to array. 
1659				newVoice = {'name':this.prefDescVoice, 'lang':this.prefDescVoiceLang};
1660				cookie.voices.push(newVoice); 
1661			}								
1662		}
1663		else {
1664			available = this.getAvailablePreferences();
1665			// Rebuild cookie with current cookie values,
1666			// replacing the one value that's been changed
1667			for (i = 0; i < available.length; i++) {
1668				prefName = available[i]['name'];
1669				if (prefName == setting) {
1670					// this is the one that requires an update
1671					cookie.preferences[prefName] = this[prefName];
1672				}
1673			}
1674		}
1675		// Save updated cookie
1676		this.setCookie(cookie);
1677	};
1678
1679	AblePlayer.prototype.getPreferencesGroups = function() {
1680
1681		// return array of groups in the order in which they will appear
1682		// in the Preferences popup menu
1683		// Human-readable label for each group is defined in translation table
1684		if (this.usingYouTubeCaptions) {
1685			// no transcript is possible 
1686			return ['captions','descriptions','keyboard']; 
1687		}
1688		else if (this.usingVimeoCaptions) { 
1689			// users cannot control caption appearance
1690			// and no transcript is possible
1691			return ['descriptions','keyboard']; 
1692		}
1693		else { 
1694			return ['captions','descriptions','keyboard','transcript']; 
1695		} 
1696	}
1697
1698	AblePlayer.prototype.getAvailablePreferences = function() {
1699
1700		// Return the list of currently available preferences.
1701		// Preferences with no 'label' are set within player, not shown in Prefs dialog
1702		var prefs = [];
1703
1704		// Modifier keys preferences
1705		prefs.push({
1706			'name': 'prefAltKey', // use alt key with shortcuts
1707			'label': this.tt.prefAltKey,
1708			'group': 'keyboard',
1709			'default': 1
1710		});
1711		prefs.push({
1712			'name': 'prefCtrlKey', // use ctrl key with shortcuts
1713			'label': this.tt.prefCtrlKey,
1714			'group': 'keyboard',
1715			'default': 1
1716		});
1717		prefs.push({
1718			'name': 'prefShiftKey',
1719			'label': this.tt.prefShiftKey,
1720			'group': 'keyboard',
1721			'default': 0
1722		});
1723
1724		// Transcript preferences
1725		prefs.push({
1726			'name': 'prefTranscript', // transcript default state
1727			'label': null,
1728			'group': 'transcript',
1729			'default': 0 // off because turning it on has a certain WOW factor
1730		});
1731		prefs.push({
1732			'name': 'prefHighlight', // highlight transcript as media plays
1733			'label': this.tt.prefHighlight,
1734			'group': 'transcript',
1735			'default': 1 // on because many users can benefit
1736		});
1737		prefs.push({
1738			'name': 'prefAutoScrollTranscript',
1739			'label': null,
1740			'group': 'transcript',
1741			'default': 1
1742		});
1743		prefs.push({
1744			'name': 'prefTabbable', // tab-enable transcript
1745			'label': this.tt.prefTabbable,
1746			'group': 'transcript',
1747			'default': 0 // off because if users don't need it, it impedes tabbing elsewhere on the page
1748		});
1749
1750		// Caption preferences
1751
1752		prefs.push({
1753			'name': 'prefCaptions', // closed captions default state
1754			'label': null,
1755			'group': 'captions',
1756			'default': this.defaultStateCaptions
1757		});
1758
1759		if (!this.usingYouTubeCaptions) {
1760
1761			/* // not supported yet
1762			prefs.push({
1763				'name': 'prefCaptionsStyle',
1764				'label': this.tt.prefCaptionsStyle,
1765				'group': 'captions',
1766				'default': this.tt.captionsStylePopOn
1767			});
1768			*/
1769			// captions are always positioned above the player for audio 
1770			if (this.mediaType === 'video') {
1771				prefs.push({
1772					'name': 'prefCaptionsPosition',
1773					'label': this.tt.prefCaptionsPosition,
1774					'group': 'captions',
1775					'default': this.defaultCaptionsPosition
1776				});
1777			}	
1778			prefs.push({
1779				'name': 'prefCaptionsFont',
1780				'label': this.tt.prefCaptionsFont,
1781				'group': 'captions',
1782				'default': 'sans-serif'
1783			});
1784		}
1785		// This is the one option that is supported by YouTube IFrame API
1786		prefs.push({
1787			'name': 'prefCaptionsSize',
1788			'label': this.tt.prefCaptionsSize,
1789			'group': 'captions',
1790			'default': '100%'
1791		});
1792
1793		if (!this.usingYouTubeCaptions) {
1794
1795			prefs.push({
1796				'name': 'prefCaptionsColor',
1797				'label': this.tt.prefCaptionsColor,
1798				'group': 'captions',
1799				'default': 'white'
1800			});
1801			prefs.push({
1802				'name': 'prefCaptionsBGColor',
1803				'label': this.tt.prefCaptionsBGColor,
1804				'group': 'captions',
1805				'default': 'black'
1806			});
1807			prefs.push({
1808				'name': 'prefCaptionsOpacity',
1809				'label': this.tt.prefCaptionsOpacity,
1810				'group': 'captions',
1811				'default': '100%'
1812			});
1813		}
1814
1815		if (this.mediaType === 'video') { 
1816			// Description preferences
1817			prefs.push({
1818				'name': 'prefDesc', // audio description default state
1819				'label': null,
1820				'group': 'descriptions',
1821				'default': this.defaultStateDescriptions
1822			});
1823			prefs.push({
1824				'name': 'prefDescMethod', // audio description default format (if both 'video' and 'text' are available)
1825				'label': null,
1826				'group': 'descriptions',
1827				'default': 'video' // video (an alternative described version) always wins
1828			});
1829			prefs.push({
1830				'name': 'prefDescVoice',
1831				'label': this.tt.prefDescVoice,
1832				'group': 'descriptions',
1833				'default': null // will be set later, in injectPrefsForm()
1834			});
1835			prefs.push({
1836				'name': 'prefDescPitch',
1837				'label': this.tt.prefDescPitch,
1838				'group': 'descriptions',
1839				'default': 1 // 0 to 2
1840			});
1841			prefs.push({
1842				'name': 'prefDescRate',
1843				'label': this.tt.prefDescRate,
1844				'group': 'descriptions',
1845				'default': 1 // 0.1 to 10 (1 is normal speech; 2 is fast but decipherable; >2 is super fast)
1846			});
1847			prefs.push({
1848				'name': 'prefDescVolume',
1849				'label': this.tt.volume,
1850				'group': 'descriptions',
1851				'default': 1 // 0 to 1
1852			});
1853			prefs.push({
1854				'name': 'prefDescPause', // automatically pause when closed description starts
1855				'label': this.tt.prefDescPause,
1856				'group': 'descriptions',
1857				'default': this.defaultDescPause
1858			});
1859			prefs.push({
1860				'name': 'prefDescVisible', // visibly show closed description (if avilable and used)
1861				'label': this.tt.prefDescVisible,
1862				'group': 'descriptions',
1863				'default': 0 // off as of 4.3.16, to avoid overloading the player with visible features
1864			});
1865		}
1866		// Preferences without a category (not shown in Preferences dialogs)
1867		prefs.push({
1868			'name': 'prefSign', // open sign language window by default if avilable
1869			'label': null,
1870			'group': null,
1871			'default': 0 // off because clicking an icon to see the sign window has a powerful impact
1872		});
1873
1874		return prefs;
1875	};
1876
1877	AblePlayer.prototype.loadCurrentPreferences = function () {
1878
1879		// Load current/default preferences from cookie into the AblePlayer object.
1880
1881		var available = this.getAvailablePreferences();
1882		var cookie = this.getCookie();
1883		// Copy current cookie values into this object, and fill in any default values.
1884		for (var ii = 0; ii < available.length; ii++) {
1885			var prefName = available[ii]['name'];
1886			var defaultValue = available[ii]['default'];
1887			if (cookie.preferences[prefName] !== undefined) {
1888				this[prefName] = cookie.preferences[prefName];
1889			}
1890			else {
1891				cookie.preferences[prefName] = defaultValue;
1892				this[prefName] = defaultValue;
1893			}
1894		}
1895
1896		// Also load array of preferred voices from cookie 
1897		if (typeof cookie.voices !== 'undefined') { 
1898			this.prefVoices = cookie.voices;  
1899		}			
1900
1901		this.setCookie(cookie);
1902	};
1903
1904	AblePlayer.prototype.injectPrefsForm = function (form) {
1905
1906		// Creates a preferences form and injects it.
1907		// form is one of the supported forms (groups) defined in getPreferencesGroups()
1908
1909		var thisObj, available, descLangs,
1910			$prefsDiv, formTitle, introText,
1911			$prefsIntro,$prefsIntroP2,p3Text,$prefsIntroP3,i, j,
1912			$fieldset, fieldsetClass, fieldsetId,
1913			$descFieldset, $descLegend, $legend, legendId,
1914			thisPref, $thisDiv, thisClass, thisId, $thisLabel, $thisField,
1915			$div1,id1,$radio1,$label1,
1916			$div2,id2,$radio2,$label2,
1917			options,$thisOption,optionValue,optionLang,optionText,sampleCapsDiv,
1918			changedPref,changedSpan,changedText,
1919			currentDescState, prefDescVoice, 
1920			$kbHeading,$kbList,kbLabels,keys,kbListText,$kbListItem,
1921			dialog,saveButton,cancelButton;
1922
1923		thisObj = this;
1924		available = this.getAvailablePreferences();
1925
1926		// outer container, will be assigned role="dialog"
1927		$prefsDiv = $('<div>',{
1928			'class': 'able-prefs-form '
1929		});
1930		var customClass = 'able-prefs-form-' + form;
1931		$prefsDiv.addClass(customClass);
1932
1933		// add titles and intros 
1934		if (form == 'captions') {			
1935			formTitle = this.tt.prefTitleCaptions;
1936			// Intro text removed in 4.4.32 to cut down on unnecessary verbiage 
1937		}
1938		else if (form == 'descriptions') {
1939			formTitle = this.tt.prefTitleDescriptions;
1940			var $prefsIntro = $('<p>',{
1941				text: this.tt.prefIntroDescription1
1942			});
1943			var $prefsIntroUL = $('<ul>');
1944			var $prefsIntroLI1 = $('<li>',{
1945				text: this.tt.prefDescFormatOption1
1946			});
1947			var $prefsIntroLI2 = $('<li>',{
1948				text: this.tt.prefDescFormatOption2
1949			});
1950
1951			$prefsIntroUL.append($prefsIntroLI1,$prefsIntroLI2);
1952			if (this.hasOpenDesc && this.hasClosedDesc) {
1953				currentDescState = this.tt.prefIntroDescription2 + ' ';
1954				currentDescState += '<strong>' + this.tt.prefDescFormatOption1b + '</strong>';
1955				currentDescState += ' <em>' + this.tt.and + '</em> <strong>' + this.tt.prefDescFormatOption2b + '</strong>.';
1956			}
1957			else if (this.hasOpenDesc) {
1958				currentDescState = this.tt.prefIntroDescription2;
1959				currentDescState += ' <strong>' + this.tt.prefDescFormatOption1b + '</strong>.';
1960			}
1961			else if (this.hasClosedDesc) {
1962				currentDescState = this.tt.prefIntroDescription2;
1963				currentDescState += ' <strong>' + this.tt.prefDescFormatOption2b + '</strong>.';
1964			}
1965			else {
1966				currentDescState = this.tt.prefIntroDescriptionNone;
1967			}
1968			$prefsIntroP2 = $('<p>',{
1969				html: currentDescState
1970			});
1971
1972			p3Text = this.tt.prefIntroDescription3;
1973			if (this.hasOpenDesc || this.hasClosedDesc) {
1974				p3Text += ' ' + this.tt.prefIntroDescription4;
1975			}
1976			$prefsIntroP3 = $('<p>',{
1977				text: p3Text
1978			});
1979
1980			$prefsDiv.append($prefsIntro,$prefsIntroUL,$prefsIntroP2,$prefsIntroP3);
1981		}
1982		else if (form == 'keyboard') {
1983			formTitle = this.tt.prefTitleKeyboard;
1984			introText = this.tt.prefIntroKeyboard1;
1985			introText += ' ' + this.tt.prefIntroKeyboard2;
1986			introText += ' ' + this.tt.prefIntroKeyboard3;
1987			$prefsIntro = $('<p>',{
1988				text: introText
1989			});
1990			$prefsDiv.append($prefsIntro);
1991		}
1992		else if (form == 'transcript') {
1993			formTitle = this.tt.prefTitleTranscript;
1994			// Intro text removed in 4.4.32 to cut down on unnecessary verbiage 
1995		}
1996
1997		$fieldset = $('<div>').attr('role','group');	
1998		fieldsetClass = 'able-prefs-' + form;
1999		fieldsetId = this.mediaId + '-prefs-' + form;
2000		legendId = fieldsetId + '-legend';
2001		$fieldset.addClass(fieldsetClass).attr('id',fieldsetId);
2002		if (form === 'keyboard') {
2003			$legend = $('<h2>' + this.tt.prefHeadingKeyboard1 + '</h2>');
2004			$legend.attr('id',legendId);
2005			$fieldset.attr('aria-labelledby',legendId);
2006			$fieldset.append($legend);
2007		}
2008		else if (form === 'descriptions') {
2009			$legend = $('<h2>' + this.tt.prefHeadingTextDescription + '</h2>');
2010			$legend.attr('id',legendId);
2011			$fieldset.attr('aria-labelledby',legendId);
2012			$fieldset.append($legend);
2013		}
2014		for (i=0; i<available.length; i++) {
2015
2016			// only include prefs on the current form if they have a label
2017			if ((available[i]['group'] == form) && available[i]['label']) {
2018
2019				thisPref = available[i]['name'];
2020				thisClass = 'able-' + thisPref;
2021				thisId = this.mediaId + '_' + thisPref;
2022				$thisDiv = $('<div>').addClass(thisClass);
2023
2024				if (form === 'captions') {
2025					$thisLabel = $('<label for="' + thisId + '"> ' + available[i]['label'] + '</label>');
2026					$thisField = $('<select>',{
2027						name: thisPref,
2028						id: thisId,
2029					});
2030					if (thisPref !== 'prefCaptions' && thisPref !== 'prefCaptionsStyle') {
2031						// add a change handler that updates the style of the sample caption text
2032						$thisField.change(function() {
2033							changedPref = $(this).attr('name');
2034							thisObj.stylizeCaptions(thisObj.$sampleCapsDiv,changedPref);
2035						});
2036					}
2037					options = this.getCaptionsOptions(thisPref);
2038					for (j=0; j < options.length; j++) {
2039						if (thisPref === 'prefCaptionsPosition') {
2040							optionValue = options[j];
2041							if (optionValue === 'overlay') {
2042								optionText = this.tt.captionsPositionOverlay;
2043							}
2044							else if (optionValue === 'below') {
2045								optionValue = options[j];
2046								optionText = this.tt.captionsPositionBelow;
2047							}
2048						}
2049						else if (thisPref === 'prefCaptionsFont' || thisPref === 'prefCaptionsColor' || thisPref === 'prefCaptionsBGColor') {
2050							optionValue = options[j][0];
2051							optionText = options[j][1];
2052						}
2053						else if (thisPref === 'prefCaptionsOpacity') {
2054							optionValue = options[j];
2055							optionText = options[j];
2056							if (optionValue === '0%') {
vendor: 5,370 bytes, lines 2057-2215
2057								optionText += ' (' + this.tt.transparent + ')';
2058							}
2059							else if (optionValue === '100%') {
2060								optionText += ' (' + this.tt.solid + ')';
2061							}
2062						}
2063						else {
2064							optionValue = options[j];
2065							optionText = options[j];
2066						}
2067						$thisOption = $('<option>',{
2068							value: optionValue,
2069							text: optionText
2070						});
2071						if (this[thisPref] === optionValue) {
2072							$thisOption.prop('selected',true);
2073						}
2074						$thisField.append($thisOption);
2075					}
2076					$thisDiv.append($thisLabel,$thisField);
2077				}
2078				else if (form === 'descriptions') {
2079					$thisLabel = $('<label for="' + thisId + '"> ' + available[i]['label'] + '</label>');
2080					if (thisPref === 'prefDescPause' || thisPref === 'prefDescVisible') {
2081						// these preferences are checkboxes
2082						$thisDiv.addClass('able-prefs-checkbox');
2083						$thisField = $('<input>',{
2084							type: 'checkbox',
2085							name: thisPref,
2086							id: thisId,
2087							value: 'true'
2088						});
2089						// check current active value for this preference
2090						if (this[thisPref] === 1) {
2091							$thisField.prop('checked',true);
2092						}
2093						$thisDiv.append($thisField,$thisLabel);
2094					}
2095					else if (this.synth) {
2096						// Only show these options if browser supports speech synthesis
2097						$thisDiv.addClass('able-prefs-select');
2098						$thisField = $('<select>',{
2099							name: thisPref,
2100							id: thisId,
2101						});
2102						if (thisPref === 'prefDescVoice' && this.descVoices) {
2103							prefDescVoice = this.getPrefDescVoice(); 				
2104							for (j=0; j < this.descVoices.length; j++) {
2105								optionValue = this.descVoices[j].name;
2106								optionLang = this.descVoices[j].lang.substring(0,2).toLowerCase(); 
2107								optionText = optionValue + ' (' + this.descVoices[j].lang + ')';
2108								$thisOption = $('<option>',{
2109									'value': optionValue,
2110									'data-lang': optionLang,
2111									text: optionText
2112								});
2113								if (prefDescVoice === optionValue) {
2114									$thisOption.prop('selected',true);
2115								}
2116								$thisField.append($thisOption);
2117							}
2118							this.$voiceSelectField = $thisField;
2119						}
2120						else {
2121							if (thisPref == 'prefDescPitch') { // 0 to 2
2122								options = [0,0.5,1,1.5,2];
2123							}
2124							else if (thisPref == 'prefDescRate') { // 0.1 to 10
2125								// Tests with a variety of voices on MacOS and Windows
2126								// yielded the following choices that seem reasonable for audio description:
2127								// 0.5 - too slow (exclude this)
2128								// 0.7 - casual
2129								// 0.8 - add this
2130								// 0.9 - add this
2131								// 1 - normal
2132								// 1.1 - add this
2133								// 1.2 - add this
2134								// 1.5 - quick
2135								// 2 - speedy
2136								// 2.5 - fleet
2137								// 3 - fast! (some voices don't get any faster than this
2138
2139								// Note: if these values are modified, must also modfiy them
2140								// in makePrefsValueReadable()
2141								options = [0.7,0.8,0.9,1,1.1,1.2,1.5,2,2.5,3];
2142							}
2143							else if (thisPref == 'prefDescVolume') { // 0 (mute) to 1
2144								options = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1];
2145							}
2146							if (typeof options !== 'undefined') {
2147								for (j=0; j < options.length; j++) {
2148									optionValue = options[j];
2149									optionText = this.makePrefsValueReadable(thisPref,optionValue);
2150									$thisOption = $('<option>',{
2151										value: optionValue,
2152										text: optionText
2153									});
2154									if (this[thisPref] == optionValue) {
2155										$thisOption.prop('selected',true);
2156									}
2157									$thisField.append($thisOption);
2158									$thisDiv.append($thisLabel,$thisField);
2159								}
2160							}
2161						}
2162						// add a change handler that announces the sample description text
2163						$thisField.on('change',function() {
2164							thisObj.announceDescriptionText('sample',thisObj.currentSampleText);
2165						});
2166						$thisDiv.append($thisLabel,$thisField);
2167					}
2168				}
2169				else { // all other fields are checkboxes
2170					$thisLabel = $('<label for="' + thisId + '"> ' + available[i]['label'] + '</label>');
2171					$thisField = $('<input>',{
2172						type: 'checkbox',
2173						name: thisPref,
2174						id: thisId,
2175						value: 'true'
2176					});
2177					// check current active value for this preference
2178					if (this[thisPref] === 1) {
2179						$thisField.prop('checked',true);
2180					}
2181					if (form === 'keyboard') {
2182						// add a change handler that updates the list of current keyboard shortcuts
2183						$thisField.on('change',function() {
2184							changedPref = $(this).attr('name');
2185							if (changedPref === 'prefAltKey') {
2186								changedSpan = '.able-modkey-alt';
2187								changedText = thisObj.tt.prefAltKey + ' + ';
2188							}
2189							else if (changedPref === 'prefCtrlKey') {
2190								changedSpan = '.able-modkey-ctrl';
2191								changedText = thisObj.tt.prefCtrlKey + ' + ';
2192							}
2193							else if (changedPref === 'prefShiftKey') {
2194								changedSpan = '.able-modkey-shift';
2195								changedText = thisObj.tt.prefShiftKey + ' + ';
2196							}
2197							if ($(this).is(':checked')) {
2198								$(changedSpan).text(changedText);
2199							}
2200							else {
2201								$(changedSpan).text('');
2202							}
2203						});
2204					}
2205					$thisDiv.append($thisField,$thisLabel);
2206				}
2207				$fieldset.append($thisDiv);
2208			}
2209		}
2210		$prefsDiv.append($fieldset);
2211
2212		if (form === 'captions') {
2213			// add a sample closed caption div to prefs dialog
2214			// do not show this for YouTube captions, since it's not an accurate reflection
2215			if (!this.usingYouTubeCaptions) {
2216				this.$sampleCapsDiv = $('<div>',{
2217					'class': 'able-captions-sample'
2218				}).text(this.tt.sampleCaptionText);
2219				$prefsDiv.append(this.$sampleCapsDiv);
2220				this.stylizeCaptions(this.$sampleCapsDiv);
2221			}
2222		}
2223		else if (form === 'descriptions') {
2224			if (this.synth) {
2225				// add a div with sample audio description text
2226				this.$sampleDescDiv = $('<div>',{
2227					'class': 'able-desc-sample'
2228				}).text(this.tt.sampleDescriptionText);
2229				$prefsDiv.append(this.$sampleDescDiv);
2230				this.currentSampleText = this.tt.sampleDescriptionText; 
2231			}
2232		}
2233		else if (form === 'keyboard') {
2234			// add a current list of keyboard shortcuts
2235			$kbHeading = $('<h2>',{
2236				text: this.tt.prefHeadingKeyboard2
2237			});
2238			$kbList = $('<ul>');
2239			// create arrays of kbLabels and keys
2240			kbLabels = [];
2241			keys = [];
2242			for (i=0; i<this.controls.length; i++) {
2243				if (this.controls[i] === 'play') {
2244					kbLabels.push(this.tt.play + '/' + this.tt.pause);
2245					keys.push('p</span> <em>' + this.tt.or + '</em> <span class="able-help-modifiers"> ' + this.tt.spacebar);
2246				}
2247				else if (this.controls[i] === 'restart') {
2248					kbLabels.push(this.tt.restart);
2249					keys.push('s');
2250				}
2251				else if (this.controls[i] === 'previous') {
2252					kbLabels.push(this.tt.prevTrack);
2253					keys.push('b'); // b = back
2254				}
2255				else if (this.controls[i] === 'next') {
2256					kbLabels.push(this.tt.nextTrack);
2257					keys.push('n');
2258				}
2259				else if (this.controls[i] === 'rewind') {
2260					kbLabels.push(this.tt.rewind);
2261					keys.push('r');
2262				}
2263				else if (this.controls[i] === 'forward') {
2264					kbLabels.push(this.tt.forward);
2265					keys.push('f');
2266				}
2267				else if (this.controls[i] === 'volume') {
2268					kbLabels.push(this.tt.volume);
2269					keys.push('v</span> <em>' + this.tt.or + '</em> <span class="able-modkey">1-9');
2270					// mute toggle
2271					kbLabels.push(this.tt.mute + '/' + this.tt.unmute);
2272					keys.push('m');
2273				}
2274				else if (this.controls[i] === 'captions') {
2275					if (this.captions.length > 1) {
2276						// caption button launches a Captions popup menu
2277						kbLabels.push(this.tt.captions);
2278					}
2279					else {
2280						// there is only one caption track
2281						// therefore caption button is a toggle
2282						if (this.captionsOn) {
2283							kbLabels.push(this.tt.hideCaptions);
2284						}
2285						else {
2286							kbLabels.push(this.tt.showCaptions);
2287						}
2288					}
2289					keys.push('c');
2290				}
2291				else if (this.controls[i] === 'descriptions') {
2292					if (this.descOn) {
2293						kbLabels.push(this.tt.turnOffDescriptions);
2294					}
2295					else {
2296						kbLabels.push(this.tt.turnOnDescriptions);
2297					}
2298					keys.push('d');
2299				}
2300				else if (this.controls[i] === 'prefs') {
2301					kbLabels.push(this.tt.preferences);
2302					keys.push('e');
2303				}
2304				else if (this.controls[i] === 'help') {
2305					kbLabels.push(this.tt.help);
2306					keys.push('h');
2307				}
2308			}
2309			for (i=0; i<keys.length; i++) {
2310				// alt
2311				kbListText = '<span class="able-modkey-alt">';
2312				if (this.prefAltKey === 1) {
2313					kbListText += this.tt.prefAltKey + ' + ';
2314				}
2315				kbListText += '</span>';
2316				// ctrl
2317				kbListText += '<span class="able-modkey-ctrl">';
2318				if (this.prefCtrlKey === 1) {
2319					kbListText += this.tt.prefCtrlKey + ' + ';
2320				}
2321				kbListText += '</span>';
2322				// shift
2323				kbListText += '<span class="able-modkey-shift">';
2324				if (this.prefShiftKey === 1) {
2325					kbListText += this.tt.prefShiftKey + ' + ';
2326				}
2327				kbListText += '</span>';
2328				kbListText += '<span class="able-modkey">' + keys[i] + '</span>';
2329				kbListText += ' = ' + kbLabels[i];
2330				$kbListItem = $('<li>',{
2331					html: kbListText
2332				});
2333				$kbList.append($kbListItem);
2334			}
2335			// add Escape key
2336			kbListText = '<span class="able-modkey">' + this.tt.escapeKey + '</span>';
2337			kbListText += ' = ' + this.tt.escapeKeyFunction;
2338			$kbListItem = $('<li>',{
2339				html: kbListText
2340			});
2341			$kbList.append($kbListItem);
2342			// put it all together
2343			$prefsDiv.append($kbHeading,$kbList);
2344		}
2345
2346		// $prefsDiv (dialog) must be appended to the BODY!
2347		// otherwise when aria-hidden="true" is applied to all background content
2348		// that will include an ancestor of the dialog,
2349		// which will render the dialog unreadable by screen readers
2350		$('body').append($prefsDiv);
2351		dialog = new AccessibleDialog($prefsDiv, this.$prefsButton, 'dialog', true, formTitle, $prefsIntro, thisObj.tt.closeButtonLabel, '32em');
2352
2353		// Add save and cancel buttons.
2354		$prefsDiv.append('<hr>');
2355		saveButton = $('<button class="modal-button">' + this.tt.save + '</button>');
2356		cancelButton = $('<button class="modal-button">' + this.tt.cancel + '</button>');
2357		saveButton.click(function () {
2358			dialog.hide();
2359			thisObj.savePrefsFromForm();
2360		});
2361		cancelButton.click(function () {
2362			dialog.hide();
2363			thisObj.resetPrefsForm();
2364		});
2365
2366		$prefsDiv.append(saveButton);
2367		$prefsDiv.append(cancelButton);
2368
2369		// Associate the dialog's H1 as aria-labelledby for groups of fields
2370		// (alternative to fieldset and legend) 
2371		if (form === 'captions' || form === 'transcript') { 
2372			$fieldset.attr('aria-labelledby',dialog.titleH1.attr('id')); 
2373		}
2374
2375		// add global reference for future control
2376		if (form === 'captions') {
2377			this.captionPrefsDialog = dialog;
2378		}
2379		else if (form === 'descriptions') {
2380			this.descPrefsDialog = dialog;
2381		}
2382		else if (form === 'keyboard') {
2383			this.keyboardPrefsDialog = dialog;
2384		}
2385		else if (form === 'transcript') {
2386			this.transcriptPrefsDialog = dialog;
2387		}
2388
2389		// Add click handler for dialog close button
2390		// (button is added in dialog.js)
2391		$('div.able-prefs-form button.modalCloseButton').click(function() {
2392			thisObj.resetPrefsForm();
2393		})
2394		// Add handler for escape key
2395		$('div.able-prefs-form').keydown(function(e) {
2396			if (e.which === 27) { // escape
2397				thisObj.resetPrefsForm();
2398			}
2399		});
2400	};
2401
2402	AblePlayer.prototype.getPrefDescVoice = function () {
2403
2404		// return user's preferred voice for the current language from cookie.voices 
2405		var lang, cookie, i; 
2406
2407		if (this.selectedDescriptions) { 
2408			lang = this.selectedDescriptions.language; 
2409		}
2410		else if (this.captionLang) { 
2411			lang = this.captionLang; 
2412		}
2413		else { 
2414			lang = this.lang; 
2415		}
2416		cookie = this.getCookie(); 
2417		if (cookie.voices) { 
2418			for (i=0; i < cookie.voices.length; i++) { 
2419				if (cookie.voices[i].lang === lang) { 					
2420					return cookie.voices[i].name; 
2421				}
2422			}
2423		}
2424		return null; // user has no saved preference
2425	}
2426
2427	AblePlayer.prototype.rebuildDescPrefsForm = function () {
2428
vendor: 7,890 bytes, lines 2429-2664
2429		// Called if this.descVoices changes, which may happen if: 
2430		//  getBrowserVoices() succeeds after an earlier failure 
2431		//  user changes language of captions/subtitles and descVoices changes to match the new language 
2432
2433		var i, optionValue, optionText, $thisOption;
2434
2435		this.$voiceSelectField = $('#' + this.mediaId + '_prefDescVoice');
2436		this.$voiceSelectField.empty();
2437		for (i=0; i < this.descVoices.length; i++) {
2438			optionValue = this.descVoices[i].name;
2439			optionText = optionValue + ' (' + this.descVoices[i].lang + ')';
2440			$thisOption = $('<option>',{
2441				'value': optionValue,
2442				'data-lang': this.descVoices[i].lang.substring(0,2).toLowerCase(),
2443				text: optionText
2444			});
2445			if (this.prefDescVoice == optionValue) {
2446				$thisOption.prop('selected',true);
2447			}
2448			this.$voiceSelectField.append($thisOption);
2449		}
2450	};
2451
2452	 AblePlayer.prototype.makePrefsValueReadable = function(pref,value) {
2453
2454		 // The values for pitch, rate, and volume (web speech API)
2455		 // are strange and inconsistent between variables
2456		 // this function returns text that is more readable than the values themselves
2457
2458		 if (pref === 'prefDescPitch') {
2459			 if (value === 0) {
2460				 return this.tt.prefDescPitch1;
2461			 }
2462			 else if (value === 0.5) {
2463				 return this.tt.prefDescPitch2;
2464			 }
2465			 else if (value === 1) {
2466				 return this.tt.prefDescPitch3;
2467			 }
2468			 else if (value === 1.5) {
2469				 return this.tt.prefDescPitch4;
2470			 }
2471			 else if (value === 2) {
2472				 return this.tt.prefDescPitch5;
2473			 }
2474		 }
2475		 else if (pref === 'prefDescRate') {
2476			 // default in the API is 0.1 to 10, where 1 is normal speaking voice
2477			 // our custom range offers several rates close to 1
2478			 // plus a couple of crazy fast ones for sport
2479			 // Our more readable options (1-10) or mapped here to API values 
2480			 if (value === 0.7) {
2481				 return 1;
2482			 }
2483			 else if (value === 0.8) {
2484				 return 2;
2485			 }
2486			 else if (value === 0.9) {
2487				 return 3;
2488			 }
2489			 else if (value === 1) {
2490				 return 4;
2491			 }
2492			 else if (value === 1.1) {
2493				 return 5;
2494			 }
2495			 else if (value === 1.2) {
2496				 return 6;
2497			 }
2498			 else if (value === 1.5) {
2499				 return 7;
2500			 }
2501			 else if (value === 2) {
2502				 return 8;
2503			 }
2504			 else if (value === 2.5) {
2505				 return 9;
2506			 }
2507			 else if (value === 3) {
2508				 return 10;
2509			 }
2510		 }
2511		 else if (pref === 'prefDescVolume') {
2512			 // values range from 0.1 to 1.0
2513			 return value * 10;
2514		 }
2515		 return value;
2516	 };
2517
2518	 AblePlayer.prototype.resetPrefsForm = function () {
2519
2520		 // Reset preferences form with default values from cookie
2521		 // Called when:
2522		 // User clicks cancel or close button in Prefs Dialog
2523		 // User presses Escape to close Prefs dialog
2524		 // User clicks Save in Prefs dialog, & there's more than one player on page
2525
2526		 var thisObj, cookie, available, i, prefName, prefId, thisDiv, thisId;
2527
2528		 thisObj = this;
2529		 cookie = this.getCookie();
2530		 available = this.getAvailablePreferences();
2531		 for (i=0; i<available.length; i++) {
2532			 prefName = available[i]['name'];
2533			 prefId = this.mediaId + '_' + prefName;
2534			 if ((prefName.indexOf('Captions') !== -1) && (prefName !== 'prefCaptions')) {
2535				 // this is a caption-related select box
2536				 $('select[name="' + prefName + '"]').val(cookie.preferences[prefName]);
2537			 }
2538			 else { // all others are checkboxes
2539				 if (this[prefName] === 1) {
2540					 $('input[name="' + prefName + '"]').prop('checked',true);
2541					}
2542					else {
2543						$('input[name="' + prefName + '"]').prop('checked',false);
2544					}
2545				}
2546			}
2547			// also restore style of sample caption div
2548			this.stylizeCaptions(this.$sampleCapsDiv);
2549	 };
2550
2551	AblePlayer.prototype.savePrefsFromForm = function () {
2552
2553		// Return a prefs object constructed from the form.
2554		// called when user saves the Preferences form
2555		// update cookie with new value
2556		var cookie, available, prefName, prefId, 
2557			voiceSelectId, newVoice, newVoiceLang, numChanges, voiceLangFound, 
2558			numCapChanges, capSizeChanged, capSizeValue, newValue;
2559
2560		numChanges = 0;
2561		numCapChanges = 0; // changes to caption-style-related preferences
2562		capSizeChanged = false;
2563		cookie = this.getCookie();
2564		available = this.getAvailablePreferences();
2565		for (var i=0; i < available.length; i++) {
2566			// only prefs with labels are used in the Prefs form
2567			if (available[i]['label']) {
2568				prefName = available[i]['name'];
2569				prefId = this.mediaId + '_' + prefName;
2570				if (prefName === 'prefDescVoice') { 
2571					if (typeof cookie.voices === 'undefined') {
2572						cookie.voices = [];
2573					}
2574					voiceSelectId = this.mediaId + '_prefDescVoice';
2575					this.prefDescVoice = $('select#' + voiceSelectId).find(':selected').val();
2576					this.prefDescVoiceLang = $('select#' + voiceSelectId).find(':selected').attr('data-lang');
2577					// replace preferred voice for this lang in cookie.voices array, if one exists 
2578					// otherwise, add it to the array 
2579					voiceLangFound = false; 
2580					for (var v=0; v < cookie.voices.length; v++) { 						
2581						if (cookie.voices[v].lang === this.prefDescVoiceLang) { 
2582							voiceLangFound = true; 
2583							cookie.voices[v].name = this.prefDescVoice; 
2584						}
2585					}
2586					if (!voiceLangFound) { 
2587						// no voice has been saved yet for this language. Add it to array. 
2588						newVoice = {'name':this.prefDescVoice, 'lang':this.prefDescVoiceLang};
2589						cookie.voices.push(newVoice); 
2590					}					
2591					numChanges++; 
2592				}
2593				else if (prefName == 'prefDescMethod') {
2594					// As of v4.0.10, prefDescMethod is no longer a choice
2595					// this.prefDescMethod = $('input[name="' + prefName + '"]:checked').val();
2596					this.prefDescMethod = 'video';
2597					if (this.prefDescMethod !== cookie.preferences['prefDescMethod']) { // user's preference has changed
2598						cookie.preferences['prefDescMethod'] = this.prefDescMethod;
2599						numChanges++;
2600					}
2601				}
2602				else if ((prefName.indexOf('Captions') !== -1) && (prefName !== 'prefCaptions')) {
2603					// this is one of the caption-related select fields
2604					newValue = $('select[id="' + prefId + '"]').val();
2605					if (cookie.preferences[prefName] !== newValue) { // user changed setting
2606						cookie.preferences[prefName] = newValue;
2607						// also update global var for this pref (for caption fields, not done elsewhere)
2608						this[prefName] = newValue;
2609						numChanges++;
2610						numCapChanges++;
2611					}
2612					if (prefName === 'prefCaptionsSize') {
2613						capSizeChanged = true;
2614						capSizeValue = newValue;
2615					}
2616				}
2617				else if ((prefName.indexOf('Desc') !== -1) && (prefName !== 'prefDescPause') && prefName !== 'prefDescVisible') {
2618					// this is one of the description-related select fields
2619					newValue = $('select[id="' + prefId + '"]').val();
2620					if (cookie.preferences[prefName] !== newValue) { // user changed setting
2621						cookie.preferences[prefName] = newValue;
2622						// also update global var for this pref
2623						this[prefName] = newValue;
2624						numChanges++;
2625					}
2626				}
2627				else { // all other fields are checkboxes
2628					if ($('input[id="' + prefId + '"]').is(':checked')) {
2629						cookie.preferences[prefName] = 1;
2630						if (this[prefName] === 1) {
2631							// nothing has changed
2632						}
2633						else {
2634							// user has just turned this pref on
2635							this[prefName] = 1;
2636							numChanges++;
2637						}
2638					}
2639					else { // thisPref is not checked
2640						cookie.preferences[prefName] = 0;
2641						if (this[prefName] === 1) {
2642							// user has just turned this pref off
2643							this[prefName] = 0;
2644							numChanges++;
2645						}
2646						else {
2647							// nothing has chaged
2648						}
2649					}
2650				}
2651			}
2652		}
2653		if (numChanges > 0) {
2654			this.setCookie(cookie);
2655			this.showAlert(this.tt.prefSuccess);
2656		}
2657		else {
2658			this.showAlert(this.tt.prefNoChange);
2659		}
2660		if (this.player === 'youtube' &&
2661			(typeof this.usingYouTubeCaptions !== 'undefined' && this.usingYouTubeCaptions) &&
2662			capSizeChanged) {
2663				// update font size of YouTube captions
2664				this.youTubePlayer.setOption('captions','fontSize',this.translatePrefs('size',capSizeValue,'y
2664outube'));
2665		}
2666		if (AblePlayerInstances.length > 1) {
2667			// there are multiple players on this page.
2668			// update prefs for ALL of them
2669			for (var i=0; i<AblePlayerInstances.length; i++) {
2670				AblePlayerInstances[i].updatePrefs();
2671				AblePlayerInstances[i].geteferences();
2672				AblePlayerInstances[i].resetPrefsForm();
2673				if (numCapChanges > 0) {
2674					AblePlayerInstances[i].stylizeCaptions(AblePlayerInstances[i].$captionsDiv);
2675					// also apply same changes to descriptions, if present
2676					if (typeof AblePlayerInstances[i].$descDiv !== 'undefined') {
2677						AblePlayerInstances[i].stylizeCaptions(AblePlayerInstances[i].$descDiv);
2678					}
2679				}
2680			}
2681		}
2682		else {
2683			// there is only one player
2684			this.updatePrefs();
2685			if (numCapChanges > 0) {
2686				this.stylizeCaptions(this.$captionsDiv);
2687				// also apply same changes to descriptions, if present
2688				if (typeof this.$descDiv !== 'undefined') {
2689					this.stylizeCaptions(this.$descDiv);
2690				}
2691			}
2692		}
2693	}
2694
2695	AblePlayer.prototype.updatePrefs = function () {
2696
2697		// Update player based on current prefs. Safe to call multiple times.
2698
2699		if (this.$transcriptDiv) { 
2700			// tabbable transcript
2701			if (this.prefTabbable === 1) {
2702				this.$transcriptDiv.find('span.able-transcript-seekpoint').attr('tabindex','0');
2703			}
2704			else {
2705				this.$transcriptDiv.find('span.able-transcript-seekpoint').removeAttr('tabindex');
2706			}
2707
2708			// transcript highlights
2709			if (this.prefHighlight === 0) {
2710				// user doesn't want highlights; remove any existing highlights
2711				this.$transcriptDiv.find('span').removeClass('able-highlight');
2712			}
2713		}
2714
2715		// Re-initialize caption and description in case relevant settings have changed
2716		this.updateCaption();
2717		this.initDescription();
2718	};
2719
2720	AblePlayer.prototype.usingModifierKeys = function(e) {
2721
2722		// return true if user is holding down required modifier keys
2723		if ((this.prefAltKey === 1) && !e.altKey) {
2724			return false;
2725		}
2726		if ((this.prefCtrlKey === 1) && !e.ctrlKey) {
2727			return false;
2728		}
2729		if ((this.prefShiftKey === 1) && !e.shiftKey) {
2730			return false;
2731		}
2732		return true;
2733	};
2734
2735})(jQuery);
2736
2737(function ($) {
2738	// See section 4.1 of dev.w3.org/html5/webvtt for format details.
2739	AblePlayer.prototype.parseWebVTT = function(srcFile,text) {
2740
2741//		var deferred = new $.Deferred();
2742//		var promise = deferred.promise();
2743
2744		// Normalize line ends to \n.
2745		text = text.replace(/(\r\n|\n|\r)/g,'\n');
2746
2747		var parserState = {
2748			src: srcFile,
2749			text: text,
2750			error: null,
2751			metadata: {},
2752			cues: [],
2753			line: 1,
2754			column: 1
2755		};
2756
2757		try {
2758			act(parserState, parseFileBody);
2759		}
2760		catch (err) {
2761			var errString = 'Invalid WebVTT file: ' + parserState.src + '\n';
2762			errString += 'Line: ' + parserState.line + ', ';
2763			errString += 'Column: ' + parserState.column + '\n';
2764			errString += err;
2765			if (console.warn) {
vendor: 8,711 bytes, lines 2766-3092
2766				console.warn(errString);
2767			}
2768			else if (console.log) {
2769				console.log(errString);
2770			}
2771		}
2772		return parserState;
2773	}
2774
2775	function actList(state, list) {
2776		var results = [];
2777		for (var ii = 0; ii < list.length; ii++) {
2778			results.push(act(state, list[ii]));
2779		}
2780		return results;
2781	}
2782
2783	// Applies the action and checks for errors.
2784	function act(state, action) {
2785		var val = action(state);
2786		if (state.error !== null) {
2787			throw state.error;
2788		}
2789		return val;
2790	}
2791
2792	function updatePosition(state, cutText) {
2793		for (var ii = 0; ii < cutText.length; ii++) {
2794			if (cutText[ii] === '\n') {
2795				state.column = 1;
2796				state.line += 1;
2797			}
2798			else {
2799				state.column += 1;
2800			}
2801		}
2802	}
2803
2804	function cut(state, length) {
2805		var returnText = state.text.substring(0, length);
2806		updatePosition(state, returnText);
2807		state.text = state.text.substring(length);
2808		return returnText;
2809	}
2810
2811	function cutLine(state, length) {
2812		var nextEOL = state.text.indexOf('\n');
2813		var returnText;
2814		if (nextEOL === -1) {
2815			returnText = state.text;
2816			updatePosition(state, returnText);
2817			state.text = '';
2818		}
2819		else {
2820			returnText = state.text.substring(0, nextEOL);
2821			updatePosition(state, returnText + '\n');
2822			state.text = state.text.substring(nextEOL + 1);
2823		}
2824		return returnText;
2825	}
2826
2827	function peekLine(state) {
2828		var nextEOL = state.text.indexOf('\n');
2829		if (nextEOL === -1) {
2830			return state.text;
2831		}
2832		else {
2833			return state.text.substring(0, nextEOL);
2834		}
2835	}
2836
2837	function parseFileBody(state) {
2838		actList(state, [
2839			eatOptionalBOM,
2840			eatSignature]);
2841		var c = state.text[0];
2842		if (c === ' ' || c === '\t' || c === '\n') {
2843			actList(state, [
2844				eatUntilEOLInclusive,
2845				parseMetadataHeaders,
2846				eatAtLeast1EmptyLines,
2847				parseCuesAndComments]);
2848		}
2849		else {
2850			state.error = "WEBVTT signature not followed by whitespace.";
2851		}
2852	}
2853
2854	// Parses all metadata headers until a cue is discovered.
2855	function parseMetadataHeaders(state) {
2856		while (true) {
2857			var nextLine = peekLine(state);
2858			if (nextLine.indexOf('-->') !== -1) {
2859				return;
2860			}
2861			else if (nextLine.length === 0) {
2862				return;
2863			}
2864			else {
2865				var keyValue = act(state, getMetadataKeyValue);
2866				state.metadata[keyValue[0]] = keyValue[1];
2867				act(state, eatUntilEOLInclusive);
2868			}
2869		}
2870	}
2871
2872	function nextSpaceOrNewline(s) {
2873		var possible = [];
2874		var spaceIndex = s.indexOf(' ');
2875		if (spaceIndex >= 0) {
2876			possible.push(spaceIndex);
2877		}
2878		var tabIndex = s.indexOf('\t');
2879		if (tabIndex >= 0) {
2880			possible.push(tabIndex);
2881		}
2882		var lineIndex = s.indexOf('\n');
2883		if (lineIndex >= 0) {
2884			possible.push(lineIndex);
2885		}
2886
2887		return Math.min.apply(null, possible);
2888	}
2889
2890	function getMetadataKeyValue(state) {
2891		var next = state.text.indexOf('\n');
2892		var pair = cut(state, next);
2893		var colon = pair.indexOf(':');
2894		if (colon === -1) {
2895			state.error = 'Missing colon.';
2896			return;
2897		}
2898		else {
2899			var pairName = pair.substring(0, colon);
2900			var pairValue = pair.substring(colon + 1);
2901			return [pairName, pairValue];
2902		}
2903	}
2904
2905	function getSettingsKeyValue(state) {
2906		var next = nextSpaceOrNewline(state.text);
2907		var pair = cut(state, next);
2908		var colon = pair.indexOf(':');
2909		if (colon === -1) {
2910			state.error = 'Missing colon.';
2911			return;
2912		}
2913		else {
2914			var pairName = pair.substring(0, colon);
2915			var pairValue = pair.substring(colon + 1);
2916			return [pairName, pairValue];
2917		}
2918	}
2919
2920	function parseCuesAndComments(state) {
2921		while (true) {
2922			var nextLine = peekLine(state);
2923			// If NOTE is not on a line all its own, it must be followed by a space or tab.
2924			if (nextLine.indexOf('NOTE') === 0 && ((nextLine.length === 4) || (nextLine[4] === ' ') || (nextLine[4] === '\t'))) {
2925				actList(state, [eatComment, eatEmptyLines]);
2926			}
2927			else if ($.trim(nextLine).length === 0 && state.text.length > 0) {
2928				act(state, eatEmptyLines);
2929			}
2930			else if ($.trim(nextLine).length > 0) {
2931				act(state, parseCue);
2932			}
2933			else {
2934				// Everythings parsed!
2935				return;
2936			}
2937		}
2938	}
2939
2940	function parseCue(state) {
2941
2942		var nextLine = peekLine(state);
2943		var cueId;
2944		var errString;
2945
2946		if(nextLine.indexOf('-->') === -1) {
2947			cueId = cutLine(state);
2948			nextLine = peekLine(state);
2949			if(nextLine.indexOf('-->') === -1) {
2950				errString = 'Invalid WebVTT file: ' + state.src + '\n';
2951				errString += 'Line: ' + state.line + ', ';
2952				errString += 'Column: ' + state.column + '\n';
2953				errString += 'Expected cue timing for cueId \''+cueId+'\' but found: ' + nextLine + '\n';
2954				if (console.warn) {
2955					console.warn(errString);
2956				}
2957				else if (console.log) {
2958					console.log(errString);
2959				}
2960				return; // Return leaving line for parseCuesAndComments to handle
2961			}
2962		}
2963
2964		var cueTimings = actList(state, [getTiming,
2965																		 eatAtLeast1SpacesOrTabs,
2966																		 eatArrow,
2967																		 eatAtLeast1SpacesOrTabs,
2968																		 getTiming]);
2969
2970		var startTime = cueTimings[0];
2971		var endTime = cueTimings[4];
2972		if (startTime >= endTime) {
2973			state.error = 'Start time is not sooner than end time.';
2974			return;
2975		}
2976
2977		act(state, eatSpacesOrTabs);
2978		var cueSettings = act(state, getCueSettings);
2979		// Cut the newline.
2980		cut(state, 1);
2981		var components = act(state, getCuePayload);
2982
2983		if (typeof cueId === 'undefined') {
2984			cueId = state.cues.length + 1;
2985		}
2986		state.cues.push({
2987			id: cueId,
2988			start: startTime,
2989			end: endTime,
2990			settings: cueSettings,
2991			components: components
2992		});
2993	}
2994
2995	function getCueSettings(state) {
2996		var cueSettings = {};
2997		while (state.text.length > 0 && state.text[0] !== '\n') {
2998			var keyValue = act(state, getSettingsKeyValue);
2999			cueSettings[keyValue[0]] = keyValue[1];
3000			act(state, eatSpacesOrTabs);
3001		}
3002		return cueSettings;
3003	}
3004
3005	function getCuePayload(state) {
3006		// Parser based on instructions in draft.
3007		var result = {type: 'internal', tagName: '', value: '', classes: [], annotation: '', parent: null, children: [], language: ''};
3008		var current = result;
3009		var languageStack = [];
3010		while (state.text.length > 0) {
3011			var nextLine = peekLine(state);
3012			if (nextLine.indexOf('-->') !== -1 || /^\s+$/.test(nextLine)) {
3013				break; // Handle empty cues
3014			}
3015			// Have to separately detect double-lines ending cue due to our non-standard parsing.
3016			// TODO: Redo outer algorithm to conform to W3 spec?
3017			if (state.text.length >= 2 && state.text[0] === '\n' && state.text[1] === '\n') {
3018				cut(state, 2);
3019				break;
3020			}
3021
3022			var token = getCueToken(state);
3023			// We'll use the tokens themselves as objects where possible.
3024			if (token.type === 'string') {
3025				current.children.push(token);
3026			}
3027			else if (token.type === 'startTag') {
3028				token.type = token.tagName;
3029				// Define token.parent; added by Terrill to fix bug end 'endTag' loop
3030				token.parent = current;
3031				if ($.inArray(token.tagName, ['i', 'b', 'u', 'ruby']) !== -1) {
3032					if (languageStack.length > 0) {
3033						current.language = languageStack[languageStack.length - 1];
3034					}
3035					current.children.push(token);
3036					current = token;
3037				}
3038				else if (token.tagName === 'rt' && current.tagName === 'ruby') {
3039					if (languageStack.length > 0) {
3040						current.language = languageStack[languageStack.length - 1];
3041					}
3042					current.children.push(token);
3043					current = token;
3044				}
3045				else if (token.tagName === 'c') {
3046					token.value = token.annotation;
3047					if (languageStack.length > 0) {
3048						current.language = languageStack[languageStack.length - 1];
3049					}
3050					current.children.push(token);
3051					current = token;
3052				}
3053				else if (token.tagName === 'v') {
3054					token.value = token.annotation;
3055					if (languageStack.length > 0) {
3056						current.language = languageStack[languageStack.length - 1];
3057					}
3058					current.children.push(token);
3059					current = token;
3060				}
3061				else if (token.tagName === 'lang') {
3062					languageStack.push(token.annotation);
3063					if (languageStack.length > 0) {
3064						current.language = languageStack[languageStack.length - 1];
3065					}
3066					current.children.push(token);
3067					current = token;
3068				}
3069			}
3070			else if (token.type === 'endTag') {
3071				if (token.tagName === current.type && $.inArray(token.tagName, ['c', 'i', 'b', 'u', 'ruby', 'rt', 'v']) !== -1) {
3072					// NOTE from Terrill: This was resulting in an error because current.parent was undefined
3073					// Fixed (I think) by assigning current token to token.parent in 'startTag' loop
3074					current = current.parent;
3075				}
3076				else if (token.tagName === 'lang' && current.type === 'lang') {
3077					current = current.parent;
3078					languageStack.pop();
3079				}
3080				else if (token.tagName === 'ruby' && current.type === 'rt') {
3081					current = current.parent.parent;
3082				}
3083			}
3084			else if (token.type === 'timestampTag') {
3085				var tempState = {
3086					text: token.value,
3087					error: null,
3088					metadata: {},
3089					cues: [],
3090					line: 1,
3091					column: 1
3092				};
vendor: 7,277 bytes, lines 3093-3424
3093				try {
3094					var timing = act(tempState, getTiming);
3095					if (tempState.text.length === 0) {
3096						token.value = timing;
3097						current.push(token);
3098					}
3099				}
3100				catch (err) {
3101				}
3102			}
3103		}
3104		return result;
3105	}
3106
3107	// Gets a single cue token; uses the method in the w3 specification.
3108	function getCueToken(state) {
3109		var tokenState = 'data';
3110		var result = [];
3111		var buffer = '';
3112		var token = {type: '', tagName: '', value: '', classes: [], annotation: '', children: []}
3113
3114		while (true) {
3115			var c;
3116			// Double newlines indicate end of token.
3117			if (state.text.length >= 2 && state.text[0] === '\n' && state.text[1] === '\n') {
3118				c = '\u0004';
3119			}
3120			else if (state.text.length > 0) {
3121				c = state.text[0];
3122			}
3123			else {
3124				// End of file.
3125				c = '\u0004';
3126			}
3127			if (tokenState === 'data') {
3128				if (c === '&') {
3129					buffer = '&';
3130					tokenState = 'escape';
3131				}
3132				else if (c === '<') {
3133					if (result.length === 0) {
3134						tokenState = 'tag';
3135					}
3136					else {
3137						token.type = 'string';
3138						token.value = result.join('');
3139						return token;
3140					}
3141				}
3142				else if (c === '\u0004') {
3143					return {type: 'string', value: result.join('')};
3144				}
3145				else {
3146					result.push(c);
3147				}
3148			}
3149			else if (tokenState === 'escape') {
3150				if (c === '&') {
3151					result.push(buffer);
3152					buffer = '&';
3153				}
3154				else if (c.match(/[0-9a-z]/)) {
3155					buffer += c;
3156				}
3157				else if (c === ';') {
3158					if (buffer === '&amp') {
3159						result.push('&');
3160					}
3161					else if (buffer === '&lt') {
3162						result.push('<');
3163					}
3164					else if (buffer === '&gt') {
3165						result.push('>');
3166					}
3167					else if (buffer === '&lrm') {
3168						result.push('\u200e');
3169					}
3170					else if (buffer === '&rlm') {
3171						result.push('\u200f');
3172					}
3173					else if (buffer === '&nbsp') {
3174						result.push('\u00a0');
3175					}
3176					else {
3177						result.push(buffer);
3178						result.push(';');
3179					}
3180					tokenState = 'data';
3181				}
3182				else if (c === '<' || c === '\u0004') {
3183					result.push(buffer);
3184					token.type = 'string';
3185					token.value = result.join('');
3186					return token;
3187				}
3188				else if (c === '\t' || c === '\n' || c === '\u000c' || c === ' ') { // Handle unescaped & chars as strings
3189					result.push(buffer);
3190					token.type = 'string';
3191					token.value = result.join('');
3192					return token;
3193				}
3194				else {
3195					result.push(buffer);
3196					tokenState = 'data';
3197				}
3198			}
3199			else if (tokenState === 'tag') {
3200				if (c === '\t' || c === '\n' || c === '\u000c' || c === ' ') {
3201					tokenState = 'startTagAnnotation';
3202				}
3203				else if (c === '.') {
3204					tokenState = 'startTagClass';
3205				}
3206				else if (c === '/') {
3207					tokenState = 'endTag';
3208				}
3209				else if (c.match('[0-9]')) {
3210					tokenState = 'timestampTag';
3211					result.push(c);
3212				}
3213				else if (c === '>') {
3214					cut(state, 1);
3215					break;
3216				}
3217				else if (c === '\u0004') {
3218					token.tagName = '';
3219					token.type = 'startTag';
3220					return token;
3221				}
3222				else {
3223					result.push(c);
3224					tokenState = 'startTag';
3225				}
3226			}
3227			else if (tokenState === 'startTag') {
3228				if (c === '\t' || c === '\u000c' || c === ' ') {
3229					tokenState = 'startTagAnnotation';
3230				}
3231				else if (c === '\n') {
3232					buffer = c;
3233					tokenState = 'startTagAnnotation';
3234				}
3235				else if (c === '.') {
3236					tokenState = 'startTagClass';
3237				}
3238				else if (c === '>') {
3239					cut(state, 1);
3240					token.tagName = result.join('');
3241					token.type = 'startTag';
3242					return token;
3243				}
3244				else if (c === '\u0004') {
3245					token.tagName = result.join('');
3246					token.type = 'startTag';
3247					return token;
3248				}
3249				else {
3250					result.push(c);
3251				}
3252			}
3253			else if (tokenState === 'startTagClass') {
3254				if (c === '\t' || c === '\u000c' || c === ' ') {
3255					token.classes.push(buffer);
3256					buffer = '';
3257					tokenState = 'startTagAnnotation';
3258				}
3259				else if (c === '\n') {
3260					token.classes.push(buffer);
3261					buffer = c;
3262					tokenState = 'startTagAnnotation';
3263				}
3264				else if (c === '.') {
3265					token.classes.push(buffer);
3266					buffer = "";
3267				}
3268				else if (c === '>') {
3269					cut(state, 1);
3270					token.classes.push(buffer);
3271					token.type = 'startTag';
3272					token.tagName = result.join('');
3273					return token;
3274				}
3275				else if (c === '\u0004') {
3276					token.classes.push(buffer);
3277					token.type = 'startTag';
3278					token.tagName = result.join('');
3279					return token;
3280				}
3281				else {
3282					buffer += 'c';
3283				}
3284			}
3285			else if (tokenState === 'startTagAnnotation') {
3286				if (c === '>') {
3287					cut(state, 1);
3288					buffer = $.trim(buffer).replace(/ +/, ' ');
3289					token.type = 'startTag';
3290					token.tagName = result.join('');
3291					token.annotation = buffer;
3292					return token;
3293				}
3294				else if (c === '\u0004') {
3295					buffer = $.trim(buffer).replace(/ +/, ' ');
3296					token.type = 'startTag';
3297					token.tagName = result.join('');
3298					token.annotation = buffer;
3299					return token;
3300				}
3301				else {
3302					buffer += c;
3303				}
3304			}
3305			else if (tokenState === 'endTag') {
3306				if (c === '>') {
3307					cut(state, 1);
3308					token.type = 'endTag';
3309					token.tagName = result.join('');
3310					return token;
3311				}
3312				else if (c === '\u0004') {
3313					token.type = 'endTag';
3314					token.tagName = result.join('');
3315					return token;
3316				}
3317				else {
3318					result.push(c);
3319				}
3320			}
3321			else if (tokenState === 'timestampTag') {
3322				if (c === '>') {
3323					cut(state, 1);
3324					token.type = 'timestampTag';
3325					token.name = result.join('');
3326					return token;
3327				}
3328				else if (c === '\u0004') {
3329					token.type = 'timestampTag';
3330					token.name = result.join('');
3331					return token;
3332				}
3333				else {
3334					result.push(c);
3335				}
3336			}
3337			else {
3338				throw 'Unknown tokenState ' + tokenState;
3339			}
3340
3341			cut(state, 1);
3342		}
3343	}
3344
3345	function eatComment(state) {
3346		// Cut the NOTE line.
3347		var noteLine = cutLine(state);
3348		if (noteLine.indexOf('-->') !== -1) {
3349			state.error = 'Invalid syntax: --> in NOTE line.';
3350			return;
3351		}
3352		while (true) {
3353			var nextLine = peekLine(state);
3354			if ($.trim(nextLine).length === 0) {
3355				// End of comment.
3356				return;
3357			}
3358			else if (nextLine.indexOf('-->') !== -1) {
3359				state.error = 'Invalid syntax: --> in comment.';
3360				return;
3361			}
3362			else {
3363				cutLine(state);
3364			}
3365		}
3366	}
3367
3368	// Initial byte order mark.
3369	function eatOptionalBOM(state) {
3370		if (state.text[0] === '\ufeff') {
3371			cut(state, 1);
3372		}
3373
3374	}
3375
3376	// "WEBVTT" string.
3377	function eatSignature(state) {
3378		if (state.text.substring(0,6) === 'WEBVTT') {
3379			cut(state, 6);
3380		}
3381		else {
3382			state.error = 'Invalid signature.';
3383		}
3384	}
3385
3386	function eatArrow(state) {
3387		if (state.text.length < 3 || state.text.substring(0,3) !== '-->') {
3388			state.error = 'Missing -->';
3389		}
3390		else {
3391			cut(state, 3);
3392		}
3393	}
3394
3395	function eatSingleSpaceOrTab(state) {
3396		if (state.text[0] === '\t' || state.text[0] === ' ') {
3397			cut(state, 1);
3398		}
3399		else {
3400			state.error = 'Missing space.';
3401		}
3402	}
3403
3404	function eatSpacesOrTabs(state) {
3405		while (state.text[0] === '\t' || state.text[0] === ' ') {
3406			cut(state, 1);
3407		}
3408	}
3409
3410	function eatAtLeast1SpacesOrTabs(state) {
3411		var numEaten = 0;
3412		while (state.text[0] === '\t' || state.text[0] === ' ') {
3413			cut(state, 1);
3414			numEaten += 1;
3415		}
3416		if (numEaten === 0) {
3417			state.error = 'Missing space.';
3418		}
3419	}
3420
3421	function eatUntilEOLInclusive(state) {
3422		var nextEOL = state.text.indexOf('\n');
3423		if (nextEOL === -1) {
3424			state.error = 'Missing EOL.';
3425		}
3426		else {
3427			cut(state, nextEOL + 1);
3428		}
3429	}
3430
3431	function eatEmptyLines(state) {
3432		while (state.text.length > 0) {
3433			var nextLine = peekLine(state);
3434			if ($.trim(nextLine).length === 0) {
3435				cutLine(state);
3436			}
3437			else {
3438				break;
3439			}
3440		}
3441	}
3442
3443	// Eats empty lines, but throws an error if there's not at least one.
3444	function eatAtLeast1EmptyLines(state) {
3445		var linesEaten = 0;
3446		while (state.text.length > 0) {
3447			var nextLine = peekLine(state);
3448			if ($.trim(nextLine).length === 0) {
3449				cutLine(state);
3450				linesEaten += 1;
3451			}
3452			else {
3453				break;
3454			}
3455		}
3456		if (linesEaten === 0) {
3457			state.error = 'Missing empty line.';
3458		}
3459	}
3460
3461	function getTiming(state) {
3462		var nextSpace = nextSpaceOrNewline(state.text);
3463		if (nextSpace === -1) {
3464			state.error('Missing timing.');
3465			return;
3466		}
3467		var timestamp = cut(state, nextSpace);
3468
3469		var results = /((\d\d):)?((\d\d):)(\d\d).(\d\d\d)|(\d+).(\d\d\d)/.exec(timestamp);
3470
3471		if (!results) {
3472			state.error = 'Unable to parse timestamp';
3473			return;
3474		}
3475		var time = 0;
3476		var hours = results[2];
3477		var minutes = results[4];
3478
3479		if (minutes) {
3480			if (parseInt(minutes, 10) > 59) {
3481				state.error = 'Invalid minute range';
3482				return;
3483			}
3484			if (hours) {
3485				time += 3600 * parseInt(hours, 10);
3486			}
3487			time += 60 * parseInt(minutes, 10);
3488			var seconds = results[5];
3489			if (parseInt(seconds, 10) > 59) {
3490				state.error = 'Invalid second range';
3491				return;
3492			}
3493
3494			time += parseInt(seconds, 10);
3495			time += parseInt(results[6], 10) / 1000;
3496		}
3497		else {
3498			time += parseInt(results[7], 10);
3499			time += parseInt(results[8], 10) / 1000;
3500		}
3501
3502		return time;
3503	}
3504})(jQuery);
3505
3506(function ($) {
3507
3508	AblePlayer.prototype.injectPlayerCode = function() {
3509
3510		// create and inject surrounding HTML structure
3511		// If IOS:
3512		//	If video:
3513		//	 IOS does not support any of the player's functionality
3514		//	 - everything plays in its own player
3515		//	 Therefore, AblePlayer is not loaded & all functionality is disabled
3516		//	 (this all determined. If this is IOS && video, this function is never called)
3517		//	If audio:
3518		//	 HTML cannot be injected as a *parent* of the <audio> element
3519		//	 It is therefore injected *after* the <audio> element
3520		//	 This is only a problem in IOS 6 and earlier,
3521		//	 & is a known bug, fixed in IOS 7
3522
3523		var thisObj, captionsContainer, prefsGroups, i;
3524		thisObj = this;
3525
3526		// create three wrappers and wrap them around the media element. 
3527		// From inner to outer:
3528		// $mediaContainer - contains the original media element
3529		// $ableDiv - contains the media player and all its objects (e.g., captions, controls, descriptions)
3530		// $ableWrapper - contains additional widgets (e.g., transcript window, sign window)
3531		this.$mediaContainer = this.$media.wrap('<div class="able-media-container"></div>').parent();
3532		this.$ableDiv = this.$mediaContainer.wrap('<div class="able"></div>').parent();
3533		this.$ableWrapper = this.$ableDiv.wrap('<div class="able-wrapper"></div>').parent();
3534		this.$ableWrapper.addClass('able-skin-' + this.skin);
3535				
3536		this.$ableWrapper.css({
3537			'width': this.playerWidth + 'px' 
3538		});
3539
3540		if (this.mediaType === 'video') {
3541			// youtube adds its own big play button
3542			// don't show ours *unless* video has a poster attribute
3543			// (which obstructs the YouTube poster & big play button)
3544			if (this.iconType != 'image' && (this.player !== 'youtube' || this.hasPoster)) {
3545				this.injectBigPlayButton();
3546			}
3547		}
3548
3549		// add container that captions or description will be appended to
3550		// Note: new Jquery object must be assigned _after_ wrap, hence the temp captionsContainer variable
3551		captionsContainer = $('<div>'); 
3552		if (this.mediaType === 'video') { 
3553			captionsContainer.addClass('able-vidcap-container'); 
3554		}
3555		else if (this.mediaType === 'audio') { 
3556			captionsContainer.addClass('able-audcap-container'); 
3557			// hide this by default. It will be shown if captions are available 
3558			captionsContainer.addClass('captions-off');
3559		}
3560
3561		this.injectPlayerControlArea(); // this may need to be injected after captions??? 
3562		this.$captionsContainer = this.$mediaContainer.wrap(captionsContainer).parent();
3563		this.injectAlert();
3564		this.injectPlaylist();
3565
3566		// Do this last, as it should be prepended to the top of this.$ableDiv
3567		// after everything else has prepended
3568		this.injectOffscreenHeading();
3569	};
3570
3571	AblePlayer.prototype.injectOffscreenHeading = function () {
3572
3573		// Inject an offscreen heading to the media container.
3574		// If heading hasn't already been manually defined via data-heading-level,
3575		// automatically assign a level that is one level deeper than the closest parent heading
3576		// as determined by getNextHeadingLevel()
3577		var headingType;
3578		if (this.playerHeadingLevel == '0') {
3579			// do NOT inject a heading (at author's request)
3580		}
3581		else {
3582			if (typeof this.playerHeadingLevel === 'undefined') {
3583				this.playerHeadingLevel = this.getNextHeadingLevel(this.$ableDiv); // returns in integer 1-6
3584			}
3585			headingType = 'h' + this.playerHeadingLevel.toString();
3586			this.$headingDiv = $('<' + headingType + '>');
3587			this.$ableDiv.prepend(this.$headingDiv);
3588			this.$headingDiv.addClass('able-offscreen');
3589			this.$headingDiv.text(this.tt.playerHeading);
3590		}
3591	};
3592
3593	AblePlayer.prototype.injectBigPlayButton = function () {
3594
3595		var thisObj, svgData, buttonIcon, svgPath;
3596
3597		thisObj = this; 
3598
3599		this.$bigPlayButton = $('<button>', {
3600			'class': 'able-big-play-button',
3601			'aria-hidden': false,
3602			'aria-label': this.tt.play,
3603			'tabindex': 0
3604		});
3605
3606		if (this.iconType == 'svg') { 
3607			svgData = this.getSvgData('play');
3608			buttonIcon = $('<svg>',{
3609				'focusable': 'false',
3610				'aria-hidden': 'true',
3611				'viewBox': svgData[0]
3612			});
3613			svgPath = $('<path>',{
3614				'd': svgData[1]
3615			});
3616			buttonIcon.append(svgPath);
3617			this.$bigPlayButton.html(buttonIcon);
3618
3619			// Final step: Need to refresh the DOM in order for browser to process & display the SVG
3620			this.$bigPlayButton.html(this.$bigPlayButton.html());
3621		}
3622		else { // use icon font 
3623			this.$bigPlayIcon = $('<span>', {
3624				'class': 'icon-play',
3625			});
3626			this.$bigPlayButton.append(this.$bigPlayIcon);
3627		}
3628
3629		this.$bigPlayButton.click(function (event) {
3630			event.preventDefault();
3631			thisObj.handlePlay();
3632		});
3633		
3634		this.$mediaContainer.append(this.$bigPlayButton);
3635	};
3636
3637	AblePlayer.prototype.injectPlayerControlArea = function () {
3638
3639		this.$playerDiv = $('<div>', {
3640			'class' : 'able-player',
3641			'role' : 'region',
3642			'aria-label' : this.mediaType + ' player'
3643		});
3644		this.$playerDiv.addClass('able-'+this.mediaType);
3645		if (this.hasPlaylist && this.showNowPlaying) { 
3646			this.$nowPlayingDiv = $('<div>',{
3647				'class' : 'able-now-playing',
3648				'aria-live' : 'assertive',
3649				'aria-atomic': 'true'
3650			});
3651		}
3652		this.$controllerDiv = $('<div>',{
3653			'class' : 'able-controller'
3654		});
3655		this.$controllerDiv.addClass('able-' + this.iconColor + '-controls');
3656
3657		this.$statusBarDiv = $('<div>',{
3658			'class' : 'able-status-bar'
3659		});
3660		this.$timer = $('<span>',{
3661			'class' : 'able-timer'
3662		});
3663		this.$elapsedTimeContainer = $('<span>',{
3664			'class': 'able-elapsedTime',
3665			text: '0:00'
3666		});
3667		this.$durationContainer = $('<span>',{
3668			'class': 'able-duration'
3669		});
3670		this.$timer.append(this.$elapsedTimeContainer).append(this.$durationContainer);
3671
3672		this.$speed = $('<span>',{
3673			'class' : 'able-speed',
3674			'aria-live' : 'assertive'
3675		}).text(this.tt.speed + ': 1x');
3676
3677		this.$status = $('<span>',{
3678			'class' : 'able-status',
3679			'aria-live' : 'polite'
3680		});
3681
3682		// Put everything together.
3683		this.$statusBarDiv.append(this.$timer, this.$speed, this.$status);
3684		if (this.showNowPlaying) {
3685			this.$playerDiv.append(this.$nowPlayingDiv, this.$controllerDiv, this.$statusBarDiv);
3686		}
3687		else { 
3688			this.$playerDiv.append(this.$controllerDiv, this.$statusBarDiv);
3689		}
3690		if (this.mediaType === 'video') { 
3691			// the player controls go after the media & captions 
3692			this.$ableDiv.append(this.$playerDiv);
3693		}
3694		else { 
3695			// the player controls go before the media & captions 
3696			this.$ableDiv.prepend(this.$playerDiv);
3697		}
3698	};
3699
3700	AblePlayer.prototype.injectTextDescriptionArea = function () {
3701
3702		// create a div for writing description text
3703		this.$descDiv = $('<div>',{
3704			'class': 'able-descriptions'
3705		});
3706		// Add ARIA so description will be announced by screen readers
3707		// Later (in description.js > showDescription()), 
3708		// if browser supports Web Speech API and this.descMethod === 'browser'
3709		// these attributes will be removed 
3710		this.$descDiv.attr({
3711			'aria-live': 'assertive',
3712			'aria-atomic': 'true'
3713		});
3714		// Start off with description hidden.
3715		// It will be exposed conditionally within description.js > initDescription()
3716		this.$descDiv.hide();
3717		this.$ableDiv.append(this.$descDiv);
3718	};
3719
3720	AblePlayer.prototype.getDefaultWidth = function(which) {
3721
3722		// return default width of resizable elements
3723		// these values are somewhat arbitrary, but seem to result in good usability
3724		// if users disagree, they can resize (and resposition) them
3725		if (which === 'transcript') {
3726			return 450;
3727		}
3728		else if (which === 'sign') {
3729			return 400;
3730		}
3731	};
3732
3733	AblePlayer.prototype.positionDraggableWindow = function (which, width) {
3734
3735		// which is either 'transcript' or 'sign'
3736
3737		var cookie, cookiePos, $window, dragged, windowPos, currentWindowPos, firstTime, zIndex;
3738
3739		cookie = this.getCookie();
3740		if (which === 'transcript') {
3741			$window = this.$transcriptArea;
3742			if (typeof cookie.transcript !== 'undefined') {
3743				cookiePos = cookie.transcript;
3744			}
3745		}
3746		else if (which === 'sign') {
3747			$window = this.$signWindow;
3748			if (typeof cookie.transcript !== 'undefined') {
3749				cookiePos = cookie.sign;
3750			}
3751		}
3752		if (typeof cookiePos !== 'undefined' && !($.isEmptyObject(cookiePos))) {
3753			// position window using stored values from cookie
3754			$window.css({
3755				'position': cookiePos['position'],
3756				'width': cookiePos['width'],
3757				'z-index': cookiePos['zindex']
3758			});
3759			if (cookiePos['position'] === 'absolute') {
3760				$window.css({
3761					'top': cookiePos['top'],
3762					'left': cookiePos['left']
3763				});
3764			}
3765			// since cookie is not page-specific, z-index needs may vary across different pages
3766			this.updateZIndex(which);
3767		}
3768		else {
3769			// position window using default values
3770			windowPos = this.getOptimumPosition(which, width);
3771			if (typeof width === 'undefined') {
3772				width = this.getDefaultWidth(which);
3773			}
3774			$window.css({
3775				'position': windowPos[0],
3776				'width': width,
3777				'z-index': windowPos[3]
3778			});
3779			if (windowPos[0] === 'absolute') {
3780				$window.css({
3781					'top': windowPos[1] + 'px',
3782					'left': windowPos[2] + 'px',
3783				});
3784			}
3785		}
3786	};
3787
3788	AblePlayer.prototype.getOptimumPosition = function (targetWindow, targetWidth) {
3789
3790		// returns optimum position for targetWindow, as an array with the following structure:
3791		// 0 - CSS position ('absolute' or 'relative')
3792		// 1 - top
3793		// 2 - left
3794		// 3 - zindex (if not default)
3795		// targetWindow is either 'transcript' or 'sign'
3796		// if there is room to the right of the player, position element there
3797		// else if there is room the left of the player, position element there
3798		// else position element beneath player
3799
3800		var gap, position, ableWidth, ableHeight, ableOffset, ableTop, ableLeft,
3801			 windowWidth, otherWindowWidth, zIndex;
3802
3803		if (typeof targetWidth === 'undefined') {
3804			targetWidth = this.getDefaultWidth(targetWindow);
3805		}
3806
3807		gap = 5; // number of pixels to preserve between Able Player objects
3808
3809		position = []; // position, top, left
3810
3811		ableWidth = this.$ableDiv.width();
3812		ableHeight = this.$ableDiv.height();
3813		ableOffset = this.$ableDiv.offset();
3814		ableTop = ableOffset.top;
3815		ableLeft = ableOffset.left;
3816		windowWidth = $(window).width();
3817		otherWindowWidth = 0; // width of other visiable draggable windows will be added to this
3818
3819		if (targetWindow === 'transcript') {
3820			if (typeof this.$signWindow !== 'undefined') {
3821				if (this.$signWindow.is(':visible')) {
3822					otherWindowWidth = this.$signWindow.width() + gap;
3823				}
3824			}
3825		}
3826		else if (targetWindow === 'sign') {
3827			if (typeof this.$transcriptArea !== 'undefined') {
3828				if (this.$transcriptArea.is(':visible')) {
3829					otherWindowWidth = this.$transcriptArea.width() + gap;
3830				}
3831			}
3832		}
3833		if (targetWidth < (windowWidth - (ableLeft + ableWidth + gap + otherWindowWidth))) {
3834			// there's room to the left of $ableDiv
3835			position[0] = 'absolute';
3836			position[1] = 0;
3837			position[2] = ableWidth + otherWindowWidth + gap;
3838		}
3839		else if (targetWidth + gap < ableLeft) {
3840			// there's room to the right of $ableDiv
3841			position[0] = 'absolute';
3842			position[1] = 0;
3843			position[2] = ableLeft - targetWidth - gap;
3844		}
3845		else {
3846			// position element below $ableDiv
3847			position[0] = 'relative';
3848			// no need to define top, left, or z-index
3849		}
3850		return position;
3851	};
3852
3853	AblePlayer.prototype.injectAlert = function () {
3854
3855		// inject two alerts, one visible for all users and one for screen reader users only
3856
3857		var top;
3858
3859		this.$alertBox = $('<div role="alert"></div>');
3860		this.$alertBox.addClass('able-alert');
3861		this.$alertBox.hide();
3862		this.$alertBox.appendTo(this.$ableDiv);
3863		if (this.mediaType == 'audio') {
3864			top = '-10';
3865		}
3866		else {
3867			// position just below top of video by default 
3868			// but this will change after video player is fully sized 
3869			// see control.js > resizePlayer() 
3870			top = '10'; 
3871		}
3872		this.$alertBox.css({
3873			top: top + 'px'
3874		});
3875
3876		this.$srAlertBox = $('<div role="alert"></div>');
3877		this.$srAlertBox.addClass('able-screenreader-alert');
3878		this.$srAlertBox.appendTo(this.$ableDiv);
3879	};
3880
3881	AblePlayer.prototype.injectPlaylist = function () {
3882
3883		if (this.playlistEmbed === true) {
3884			// move playlist into player, immediately before statusBarDiv
3885			var playlistClone = this.$playlistDom.clone();
3886			playlistClone.insertBefore(this.$statusBarDiv);
3887			// Update to the new playlist copy.
3888			this.$playlist = playlistClone.find('li');
3889		}
3890	};
3891
3892	AblePlayer.prototype.createPopup = function (which, tracks) {
3893
3894		// Create popup menu and append to player
3895		// 'which' parameter is either 'captions', 'chapters', 'prefs', 'transcript-window' or 'sign-window'
3896		// 'tracks', if provided, is a list of tracks to be used as menu items
3897
3898		var thisObj, $menu, includeMenuItem, prefCats, i, $menuItem, prefCat, whichPref,
3899			hasDefault, track, windowOptions, whichPref, whichMenu,
3900			$thisItem, $prevItem, $nextItem;
3901
3902		thisObj = this;
3903
3904		$menu = $('<ul>',{
3905			'id': this.mediaId + '-' + which + '-menu',
3906			'class': 'able-popup',
3907			'role': 'menu'
3908		}).hide();
3909
3910		if (which === 'captions') {
3911			$menu.addClass('able-popup-captions');
3912		}
3913
3914		// Populate menu with menu items
3915		if (which === 'prefs') {
3916			if (this.prefCats.length > 1) {
3917				for (i = 0; i < this.prefCats.length; i++) {
3918					$menuItem = $('<li></li>',{
3919						'role': 'menuitem',
3920						'tabindex': '-1'
3921					});
3922					prefCat = this.prefCats[i];
3923					if (prefCat === 'captions') {
3924						$menuItem.text(this.tt.prefMenuCaptions);
3925					}
3926					else if (prefCat === 'descriptions') {
3927						$menuItem.text(this.tt.prefMenuDescriptions);
3928					}
3929					else if (prefCat === 'keyboard') {
3930						$menuItem.text(this.tt.prefMenuKeyboard);
3931					}
3932					else if (prefCat === 'transcript') {
3933						$menuItem.text(this.tt.prefMenuTranscript);
3934					}
3935					$menuItem.on('click',function() {
3936						whichPref = $(this).text();
3937						thisObj.showingPrefsDialog = true;
3938						thisObj.setFullscreen(false);
3939						if (whichPref === thisObj.tt.prefMenuCaptions) {
3940							thisObj.captionPrefsDialog.show();
3941						}
3942						else if (whichPref === thisObj.tt.prefMenuDescriptions) {
3943							thisObj.descPrefsDialog.show();
3944						}
3945						else if (whichPref === thisObj.tt.prefMenuKeyboard) {
3946							thisObj.keyboardPrefsDialog.show();
3947						}
3948						else if (whichPref === thisObj.tt.prefMenuTranscript) {
3949							thisObj.transcriptPrefsDialog.show();
3950						}
3951						thisObj.closePopups();
3952						thisObj.showingPrefsDialog = false;
vendor: 14,879 bytes, lines 3953-4417
3953					});
3954					$menu.append($menuItem);
3955				}
3956				this.$prefsButton.attr('data-prefs-popup','menu');
3957			}
3958			else if (this.prefCats.length == 1) {
3959				// only 1 category, so don't create a popup menu.
3960				// Instead, open dialog directly when user clicks Prefs button
3961				this.$prefsButton.attr('data-prefs-popup',this.prefCats[0]);
3962			}
3963		}
3964		else if (which === 'captions' || which === 'chapters') {
3965			hasDefault = false;
3966			for (i = 0; i < tracks.length; i++) {
3967				track = tracks[i];
3968				if (which === 'captions' && this.player === 'html5' && typeof track.cues === 'undefined') {						
3969					includeMenuItem = false; 
3970				}
3971				else { 
3972					includeMenuItem = true; 
3973				}
3974				if (includeMenuItem) {
3975					$menuItem = $('<li></li>',{
3976						'role': 'menuitemradio',
3977						'tabindex': '-1',
3978						'lang': track.language
3979					});
3980					if (track.def && this.prefCaptions == 1) {
3981						$menuItem.attr('aria-checked','true');
3982						hasDefault = true;
3983					}
3984					else {
3985						$menuItem.attr('aria-checked','false');
3986					}			
3987					// Get a label using track data
3988					if (which == 'captions') {
3989						$menuItem.text(track.label);
3990						$menuItem.on('click',this.getCaptionClickFunction(track));
3991					}
3992					else if (which == 'chapters') {
3993						$menuItem.text(this.flattenCueForCaption(track) + ' - ' + this.formatSecondsAsColonTime(track.start));
3994						$menuItem.on('click',this.getChapterClickFunction(track.start));
3995					}
3996					$menu.append($menuItem);
3997				}
3998			}
3999			if (which === 'captions') {
4000				// add a 'captions off' menu item
4001				$menuItem = $('<li></li>',{
4002					'role': 'menuitemradio',
4003					'tabindex': '-1',
4004				}).text(this.tt.captionsOff);
4005				if (this.prefCaptions === 0) {
4006					$menuItem.attr('aria-checked','true');
4007					hasDefault = true;
4008				}
4009				else { 
4010					$menuItem.attr('aria-checked','false');
4011				}
4012				$menuItem.on('click',this.getCaptionOffFunction());
4013				$menu.append($menuItem);
4014			}
4015		}
4016		else if (which === 'transcript-window' || which === 'sign-window') {
4017			windowOptions = [];
4018			windowOptions.push({
4019				'name': 'move',
4020				'label': this.tt.windowMove
4021			});
4022			windowOptions.push({
4023				'name': 'resize',
4024				'label': this.tt.windowResize
4025			});
4026			windowOptions.push({
4027				'name': 'close',
4028				'label': this.tt.windowClose
4029			});
4030			for (i = 0; i < windowOptions.length; i++) {
4031				$menuItem = $('<li></li>',{
4032					'role': 'menuitem',
4033					'tabindex': '-1',
4034					'data-choice': windowOptions[i].name
4035				});
4036				$menuItem.text(windowOptions[i].label);
4037				$menuItem.on('click mousedown',function(e) {
4038					e.stopPropagation();
4039					if (typeof e.button !== 'undefined' && e.button !== 0) {
4040						// this was a mouse click (if click is triggered by keyboard, e.button is undefined)
4041						// and the button was not a left click (left click = 0)
4042						// therefore, ignore this click
4043						return false;
4044					}
4045					if (!thisObj.windowMenuClickRegistered && !thisObj.finishingDrag) {
4046						thisObj.windowMenuClickRegistered = true;
4047						thisObj.handleMenuChoice(which.substring(0, which.indexOf('-')), $(this).attr('data-choice'), e);
4048					}
4049				});
4050				$menu.append($menuItem);
4051			}
4052		}
4053		// assign default item, if there isn't one already
4054		if (which === 'captions' && !hasDefault) {
4055			// check the menu item associated with the default language
4056			// as determined in control.js > syncTrackLanguages()
4057			if ($menu.find('li[lang=' + this.captionLang + ']')) {
4058				// a track exists for the default language. Check that item in the menu
4059				$menu.find('li[lang=' + this.captionLang + ']').attr('aria-checked','true');
4060			}
4061			else {
4062				// check the last item (captions off)
4063				$menu.find('li').last().attr('aria-checked','true');
4064			}
4065		}
4066		else if (which === 'chapters') {
4067			if ($menu.find('li:contains("' + this.defaultChapter + '")')) {
4068				$menu.find('li:contains("' + this.defaultChapter + '")').attr('aria-checked','true').addClass('able-focus');
4069			}
4070			else {
4071				$menu.find('li').first().attr('aria-checked','true').addClass('able-focus');
4072			}
4073		}
4074		// add keyboard handlers for navigating within popups
4075		$menu.on('keydown',function (e) {
4076			
4077			whichMenu = $(this).attr('id').split('-')[1];
4078			$thisItem = $(this).find('li:focus');
4079			if ($thisItem.is(':first-child')) {
4080				// this is the first item in the menu
4081				$prevItem = $(this).find('li').last(); // wrap to bottom
4082				$nextItem = $thisItem.next();
4083			}
4084			else if ($thisItem.is(':last-child')) {
4085				// this is the last Item
4086				$prevItem = $thisItem.prev();
4087				$nextItem = $(this).find('li').first(); // wrap to top
4088			}
4089			else {
4090				$prevItem = $thisItem.prev();
4091				$nextItem = $thisItem.next();
4092			}
4093			if (e.which === 9) { // Tab
4094				if (e.shiftKey) {
4095					$thisItem.removeClass('able-focus');
4096					$prevItem.focus().addClass('able-focus');
4097				}
4098				else {
4099					$thisItem.removeClass('able-focus');
4100					$nextItem.focus().addClass('able-focus');
4101				}
4102			}
4103			else if (e.which === 40 || e.which === 39) { // down or right arrow
4104				$thisItem.removeClass('able-focus');
4105				$nextItem.focus().addClass('able-focus');
4106			}
4107			else if (e.which == 38 || e.which === 37) { // up or left arrow
4108				$thisItem.removeClass('able-focus');
4109				$prevItem.focus().addClass('able-focus');
4110			}
4111			else if (e.which === 32 || e.which === 13) { // space or enter
4112				$thisItem.click();
4113			}
4114			else if (e.which === 27) {	// Escape
4115				$thisItem.removeClass('able-focus');
4116				thisObj.closePopups();
4117				e.stopPropagation;
4118			}
4119			e.preventDefault();
4120		});
4121		this.$controllerDiv.append($menu);
4122		return $menu;
4123	};
4124
4125	AblePlayer.prototype.closePopups = function () {
4126
4127		var thisObj = this;
4128
4129		if (this.chaptersPopup && this.chaptersPopup.is(':visible')) {
4130			this.chaptersPopup.hide();
4131			this.$chaptersButton.attr('aria-expanded','false').focus();
4132		}
4133		if (this.captionsPopup && this.captionsPopup.is(':visible')) {
4134			this.captionsPopup.hide();
4135			this.$ccButton.attr('aria-expanded', 'false');
4136			this.waitThenFocus(this.$ccButton);
4137		}
4138		if (this.prefsPopup && this.prefsPopup.is(':visible') && !this.hidingPopup) {
4139			this.hidingPopup = true; // stopgap to prevent popup from re-opening again on keypress
4140			this.prefsPopup.hide();
4141			// restore menu items to their original state
4142			this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
4143			this.$prefsButton.attr('aria-expanded', 'false');
4144			if (!this.showingPrefsDialog) {
4145				this.waitThenFocus(thisObj.$prefsButton);
4146			}
4147			// wait briefly, then reset hidingPopup
4148			setTimeout(function() {
4149				thisObj.hidingPopup = false;
4150			},100);
4151		}
4152		if (this.$volumeSlider && this.$volumeSlider.is(':visible')) {
4153			this.$volumeSlider.hide().attr('aria-hidden','true');			
4154			this.$volumeButton.attr('aria-expanded', 'false').focus();
4155		}
4156		if (this.$transcriptPopup && this.$transcriptPopup.is(':visible')) {
4157			this.hidingPopup = true; 
4158			this.$transcriptPopup.hide();
4159			// restore menu items to their original state
4160			this.$transcriptPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
4161			this.$transcriptPopupButton.attr('aria-expanded','false').focus();			
4162			// wait briefly, then reset hidingPopup
4163			setTimeout(function() {
4164				thisObj.hidingPopup = false;
4165			},100);
4166		}
4167		if (this.$signPopup && this.$signPopup.is(':visible')) {
4168			this.$signPopup.hide();
4169			// restore menu items to their original state
4170			this.$signPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
4171			this.$signPopupButton.attr('aria-expanded','false').focus();
4172		}
4173	};
4174
4175	AblePlayer.prototype.setupPopups = function (which) {
4176
4177		// Create and fill in the popup menu forms for various controls.
4178		// parameter 'which' is passed if refreshing content of an existing popup ('captions' or 'chapters')
4179		// If which is undefined, automatically setup 'captions', 'chapters', and 'prefs' popups
4180		// However, only setup 'transcript-window' and 'sign-window' popups if passed as value of which
4181		var popups, thisObj, hasDefault, i, j,
4182				tracks, track, $trackButton, $trackLabel,
4183				radioName, radioId, $menu, $menuItem,
4184				prefCats, prefCat, prefLabel;
4185
4186		popups = [];
4187		if (typeof which === 'undefined') {
4188			popups.push('prefs');
4189		}
4190
4191		if (which === 'captions' || (typeof which === 'undefined')) {
4192			if (this.captions.length > 0) {
4193				popups.push('captions');
4194			}
4195		}
4196		if (which === 'chapters' || (typeof which === 'undefined')) {
4197			if (this.chapters.length > 0 && this.useChaptersButton) {
4198				popups.push('chapters');
4199			}
4200		}
4201		if (which === 'transcript-window' && this.transcriptType === 'popup') {
4202			popups.push('transcript-window');
4203		}
4204		if (which === 'sign-window' && this.hasSignLanguage) {
4205			popups.push('sign-window');
4206		}
4207		if (popups.length > 0) {
4208			thisObj = this;
4209			for (var i=0; i<popups.length; i++) {
4210				var popup = popups[i];
4211				hasDefault = false;
4212				if (popup == 'prefs') {
4213					this.prefsPopup = this.createPopup('prefs');
4214				}
4215				else if (popup == 'captions') {
4216					if (typeof this.captionsPopup === 'undefined' || !this.captionsPopup) {
4217						this.captionsPopup = this.createPopup('captions',this.captions);
4218					}
4219				}
4220				else if (popup == 'chapters') {
4221					if (this.selectedChapters) {
4222						tracks = this.selectedChapters.cues;
4223					}
4224					else if (this.chapters.length >= 1) {
4225						tracks = this.chapters[0].cues;
4226					}
4227					else {
4228						tracks = [];
4229					}
4230					if (typeof this.chaptersPopup === 'undefined' || !this.chaptersPopup) {
4231						this.chaptersPopup = this.createPopup('chapters',tracks);
4232					}
4233				}
4234				else if (popup == 'transcript-window') {
4235					return this.createPopup('transcript-window');
4236				}
4237				else if (popup == 'sign-window') {
4238					return this.createPopup('sign-window');
4239				}
4240			}
4241		}
4242	};
4243
4244	AblePlayer.prototype.provideFallback = function() {
4245
4246		// provide fallback in case of a critical error building the player 
4247		// to test, set data-test-fallback to either of the following values: 
4248		// 1 = emulate failure to build Able Player 
4249		// 2 = emulate browser that doesn't support HTML5 media 
4250
4251		var i, $fallback;
4252
4253		if (this.usingFallback) { 
4254			// fallback has already been implemented. 
4255			// stopgap to prevent this function from executing twice on the same media element
4256			return; 
4257		}
4258		else { 
4259			this.usingFallback = true; 
4260		}
4261
4262		if (!this.testFallback) { 
4263			// this is not a test. 
4264			// an actual error has resulted in this function being called. 
4265			// use scenario 1 
4266			this.testFallback = 1; 
4267		}
4268
4269		if (typeof this.$media === 'undefined') { 
4270			// this function has been called prior to initialize.js > reinitialize() 
4271			// before doing anything, need to create the jQuery media object
4272			this.$media = $(this.media); 
4273		} 
4274
4275		// get/assign an id for the media element 
4276		if (this.$media.attr('id')) { 
4277			this.mediaId = this.$media.attr('id'); 
4278		}
4279		else { 
4280			this.mediaId = 'media' + Math.floor(Math.random() * 1000000000).toString();
4281		} 
4282
4283		// check whether element has nested fallback content 
4284		this.hasFallback = false; 
4285		if (this.$media.children().length) { 
4286			i = 0; 
4287			while (i < this.$media.children().length && !this.hasFallback) { 
4288				if (!(this.$media.children()[i].tagName === 'SOURCE' || 
4289					this.$media.children()[i].tagName === 'TRACK')) { 
4290					// this element is something other than <source> or <track> 
4291					this.hasFallback = true; 
4292				}
4293				i++; 
4294			}
4295		}
4296		if (!this.hasFallback) { 
4297			// the HTML code does not include any nested fallback content 
4298			// inject our own 
4299			// NOTE: this message is not translated, since fallback may be needed 
4300			// due to an error loading the translation file 
4301			// This will only be needed on very rare occasions, so English is ok. 
4302			$fallback = $('<p>').text('Media player unavailable.'); 
4303			this.$media.append($fallback); 
4304		}
4305
4306		// get height and width attributes, if present 
4307		// and add them to a style attribute
4308		if (this.$media.attr('width')) { 
4309			this.$media.css('width',this.$media.attr('width') + 'px'); 
4310		}
4311		if (this.$media.attr('height')) { 
4312			this.$media.css('height',this.$media.attr('height') + 'px'); 
4313		}
4314		// Remove data-able-player attribute 
4315		this.$media.removeAttr('data-able-player'); 
4316
4317		// Add controls attribute (so browser will add its own controls)
4318		this.$media.prop('controls',true); 
4319
4320		if (this.testFallback == 2) { 
4321
4322			// emulate browser failure to support HTML5 media by changing the media tag name 
4323			// browsers should display the supported content that's nested inside 
4324			$(this.$media).replaceWith($('<foobar id="foobar-' + this.mediaId + '">'));
4325			this.$newFallbackElement = $('#foobar-' + this.mediaId); 			
4326
4327			// append all children from the original media 
4328			if (this.$media.children().length) { 
4329				i = this.$media.children().length - 1; 
4330				while (i >= 0) { 
4331					this.$newFallbackElement.prepend($(this.$media.children()[i])); 
4332					i--; 
4333				}
4334			}
4335			if (!this.hasFallback) { 
4336				// inject our own fallback content, defined above
4337				this.$newFallbackElement.append($fallback);
4338			}
4339		}
4340		return; 
4341	};
4342
4343	AblePlayer.prototype.calculateControlLayout = function () {
4344
4345		// Calculates the layout for controls based on media and options.
4346		// Returns an array with 4 keys (for legacy skin) or 2 keys (for 2020 skin)
4347		// Keys are the following order:
4348		// 0 = Top left
4349		// 1 = Top right
4350		// 2 = Bottom left (legacy skin only)
4351		// 3 = Bottom right (legacy skin only)
4352		// Each key contains an array of control names to put in that section.
4353
4354		var controlLayout, volumeSupported, playbackSupported, totalButtonWidth, numA11yButtons;
4355
4356		controlLayout = [];
4357		controlLayout[0] = [];
4358		controlLayout[1] = [];
4359		if (this.skin === 'legacy') {
4360			controlLayout[2] = [];
4361			controlLayout[3] = [];
4362		}
4363
4364		controlLayout[0].push('play');
4365		controlLayout[0].push('restart');
4366		controlLayout[0].push('rewind');
4367		controlLayout[0].push('forward');
4368
4369		if (this.skin === 'legacy') {
4370			controlLayout[1].push('seek');
4371		}
4372
4373		if (this.hasPlaylist) {
4374			if (this.skin === 'legacy') {
4375				controlLayout[0].push('previous');
4376				controlLayout[0].push('next');
4377			}
4378			else if (this.skin == '2020') {
4379				controlLayout[0].push('previous');
4380				controlLayout[0].push('next');
4381			}
4382		}
4383
4384		if (this.isPlaybackRateSupported()) {
4385			playbackSupported = true;
4386			if (this.skin === 'legacy') {
4387				controlLayout[2].push('slower');
4388				controlLayout[2].push('faster');
4389			}
4390		}
4391		else {
4392			playbackSupported = false;
4393		}
4394
4395		numA11yButtons = 0;
4396		if (this.hasCaptions) {
4397			numA11yButtons++;
4398			if (this.skin === 'legacy') {
4399				controlLayout[2].push('captions');
4400			}
4401			else if (this.skin == '2020') {
4402				controlLayout[1].push('captions');
4403			}
4404		}
4405		if (this.hasSignLanguage) {
4406			numA11yButtons++;
4407			if (this.skin === 'legacy') {
4408				controlLayout[2].push('sign');
4409			}
4410			else if (this.skin == '2020') {
4411				controlLayout[1].push('sign');
4412			}
4413		}
4414		if (this.mediaType === 'video') { 
4415			if (this.hasOpenDesc || this.hasClosedDesc) {
4416				numA11yButtons++;
4417				if (this.skin === 'legacy') {
4418					controlLayout[2].push('descriptions');
4419				}
4420				else if (this.skin == '2020') {
4421					controlLayout[1].push('descriptions');
4422				}
4423			}
4424		}
4425		if (this.transcriptType === 'popup' && !(this.hideTranscriptButton)) {
4426			numA11yButtons++;
4427			if (this.skin === 'legacy') {
4428				controlLayout[2].push('transcript');
4429			}
4430			else if (this.skin == '2020') {
4431				controlLayout[1].push('transcript');
4432			}
4433		}
4434		if (this.hasChapters && this.useChaptersButton) {
4435			numA11yButtons++;
4436			if (this.skin === 'legacy') {
4437				controlLayout[2].push('chapters');
4438			}
4439			else if (this.skin == '2020') {
4440				controlLayout[1].push('chapters');
4441			}
4442		}
4443
4444		if (this.skin == '2020' && numA11yButtons > 0) {
4445			controlLayout[1].push('pipe');
4446		}
4447
4448		if (playbackSupported && this.skin === '2020') {
4449			controlLayout[1].push('faster');
4450			controlLayout[1].push('slower');
4451			controlLayout[1].push('pipe');
4452		}
4453
4454		if (this.skin === 'legacy') {
4455			controlLayout[3].push('preferences');
4456		}
4457		else if (this.skin == '2020') {
4458			controlLayout[1].push('preferences');
4459		}
4460
4461		if (this.mediaType === 'video' && this.allowFullscreen) {
4462			if (this.skin === 'legacy') {
4463				controlLayout[3].push('fullscreen');
4464			}
4465			else {
4466				controlLayout[1].push('fullscreen');
4467			}
4468		}
4469
4470		if (this.browserSupportsVolume()) {
4471			volumeSupported = true; // defined in case we decide to move volume button elsewhere
4472			this.volumeButton = 'volume-' + this.getVolumeName(this.volume);
4473			if (this.skin === 'legacy') {
4474				controlLayout[1].push('volume');
4475			}
4476			else if (this.skin == '2020') {
4477				controlLayout[1].push('volume');
4478			}
4479		}
4480		else {
4481			volumeSupported = false;
4482			this.volume = false;
4483		}
4484		return controlLayout;
4485	};
4486
4487	AblePlayer.prototype.addControls = function() {
4488
4489		// determine which controls to show based on several factors:
4490		// mediaType (audio vs video)
4491		// availability of tracks (e.g., for closed captions & audio description)
4492		// browser support (e.g., for sliders and speedButtons)
4493		// user preferences (???)
4494		// some controls are aligned on the left, and others on the right
4495
4496		var thisObj, baseSliderWidth, controlLayout, numSections,
4497		i, j, k, controls, $controllerSpan, $sliderDiv, sliderLabel, $pipe, $pipeImg,
4498		svgData, svgPath, control,
4499		$buttonLabel, $buttonImg, buttonImgSrc, buttonTitle, $newButton, iconClass, buttonIcon,
4500		buttonUse, buttonText, position, buttonHeight, buttonWidth, buttonSide, controllerWidth,
4501		tooltipId, tooltipY, tooltipX, tooltipWidth, tooltipStyle, tooltip, tooltipTimerId,
4502		captionLabel, popupMenuId;
4503
4504		thisObj = this;
4505
4506		baseSliderWidth = 100; // arbitrary value, will be recalculated in refreshControls()
4507
4508		// Initialize the layout into the this.controlLayout variable.
4509		controlLayout = this.calculateControlLayout();
4510		numSections = controlLayout.length;
4511
4512		// add an empty div to serve as a tooltip
4513		tooltipId = this.mediaId + '-tooltip';
4514		this.$tooltipDiv = $('<div>',{
4515			'id': tooltipId,
4516			'class': 'able-tooltip'
4517		}).hide();
4518		this.$controllerDiv.append(this.$tooltipDiv);
4519
4520		if (this.skin == '2020') {			
4521			// add a full-width seek bar
4522			$sliderDiv = $('<div class="able-seekbar"></div>');
4523			sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel;
4524			this.$controllerDiv.append($sliderDiv);
4525			this.seekBar = new AccessibleSlider(this.mediaType, $sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
4526		}
4527
4528		for (i = 0; i < numSections; i++) {
4529			controls = controlLayout[i];
4530			if ((i % 2) === 0) { // even keys on the left
4531				$controllerSpan = $('<div>',{
4532					'class': 'able-left-controls'
4533				});
4534			}
4535			else { // odd keys on the right
4536				$controllerSpan = $('<div>',{
4537					'class': 'able-right-controls'
4538				});
4539			}
4540			this.$controllerDiv.append($controllerSpan);
4541
4542			for (j=0; j<controls.length; j++) {
4543				control = controls[j];
4544				if (control === 'seek') {
4545					$sliderDiv = $('<div class="able-seekbar"></div>');
4546					sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel;
4547					$controllerSpan.append($sliderDiv);
4548					if (typeof this.duration === 'undefined' || this.duration === 0) {
4549						// set arbitrary starting duration, and change it when duration is known
4550						this.duration = 60;
4551						// also set elapsed to 0
4552						this.elapsed = 0;
4553					}
4554					this.seekBar = new AccessibleSlider(this.mediaType, $sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
4555				}
4556				else if (control === 'pipe') {
4557					$pipe = $('<span>', {
4558						'tabindex': '-1',
4559						'aria-hidden': 'true'
4560					});
4561					if (this.iconType === 'font') {
4562						$pipe.addClass('icon-pipe');
4563					}
4564					else {
4565						$pipeImg = $('<img>', {
4566							src: this.rootPath + 'button-icons/' + this.iconColor + '/pipe.png',
4567							alt: '',
4568							role: 'presentation'
4569						});
4570						$pipe.append($pipeImg);
4571					}
4572					$controllerSpan.append($pipe);
4573				}
4574				else {
4575					// this control is a button
4576					if (control === 'volume') {
4577						buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + this.volumeButton + '.png';
4578					}
4579					else if (control === 'fullscreen') {
4580						buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/fullscreen-expand.png';
4581					}
4582					else if (control === 'slower') {
4583						if (this.speedIcons === 'animals') {
4584							buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/turtle.png';
4585						}
4586						else {
4587							buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/slower.png';
4588						}
4589					}
4590					else if (control === 'faster') {
4591						if (this.speedIcons === 'animals') {
4592							buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/rabbit.png';
4593						}
4594						else {
4595							buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/faster.png';
4596						}
4597					}
4598					else {
4599						buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + control + '.png';
4600					}
4601					buttonTitle = this.getButtonTitle(control);
4602
4603					// icomoon documentation recommends the following markup for screen readers:
4604					// 1. link element (or in our case, button). Nested inside this element:
4605					// 2. span that contains the icon font (in our case, buttonIcon)
4606					// 3. span that contains a visually hidden label for screen readers (buttonLabel)
4607					// In addition, we are adding aria-label to the button (but not title)
4608					// And if iconType === 'image', we are replacing #2 with an image (with alt="" and role="presentation")
4609					// This has been thoroughly tested and works well in all screen reader/browser combinations
4610					// See https://github.com/ableplayer/ableplayer/issues/81
4611
4612					// NOTE: Changed from <button> to <div role="button" as of 4.2.18
4613					// because <button> elements are rendered poorly in high contrast mode
4614					// in some OS/browser/plugin combinations
4615					$newButton = $('<div>',{
4616						'role': 'button',
4617						'tabindex': '0',
4618						'aria-label': buttonTitle,
4619						'class': 'able-button-handler-' + control
4620					});
4621
4622					if (control === 'volume' || control === 'preferences' || control === 'captions') {
4623						if (control == 'preferences') {
4624							this.prefCats = this.getPreferencesGroups();
4625							if (this.prefCats.length > 1) {
4626								// Prefs button will trigger a menu
4627								popupMenuId = this.mediaId + '-prefs-menu';
4628								$newButton.attr({
4629									'aria-controls': popupMenuId,
4630									'aria-haspopup': 'menu',
4631									'aria-expanded': 'false'
4632							});
4633							}
4634							else if (this.prefCats.length === 1) {
4635								// Prefs button will trigger a dialog
4636								$newButton.attr({
4637									'aria-haspopup': 'dialog'
4638								});
4639							}
4640						}
4641						else if (control === 'volume') {
4642							popupMenuId = this.mediaId + '-volume-slider';
4643							// volume slider popup is not a menu or a dialog
4644							// therefore, using aria-expanded rather than aria-haspopup to communicate properties/state
4645							$newButton.attr({
4646								'aria-controls': popupMenuId,
4647								'aria-expanded': 'false'
4648							});
4649						} else if (control === 'captions' && this.captions) {
4650							if (this.captions.length > 1) {
4651								$newButton.attr('aria-expanded', 'false')
4652							} else {
4653								$newButton.attr('aria-pressed', 'false')
4654							}
4655						}
4656					}
4657					if (this.iconType === 'font') {
4658						if (control === 'volume') {
4659							iconClass = 'icon-' + this.volumeButton;
4660						}
4661						else if (control === 'slower') {
4662							if (this.speedIcons === 'animals') {
4663								iconClass = 'icon-turtle';
4664							}
4665							else {
4666								iconClass = 'icon-slower';
4667							}
4668						}
4669						else if (control === 'faster') {
4670							if (this.speedIcons === 'animals') {
4671								iconClass = 'icon-rabbit';
4672							}
4673							else {
4674								iconClass = 'icon-faster';
4675							}
4676						}
4677						else {
4678							iconClass = 'icon-' + control;
4679						}
4680						buttonIcon = $('<span>',{
4681							'class': iconClass,
4682							'aria-hidden': 'true'
4683						});
4684						$newButton.append(buttonIcon);
4685					}
4686					else if (this.iconType === 'svg') {
4687
4688					/*
4689						// Unused option for adding SVG:
4690						// Use <use> element to link to button-icons/able-icons.svg
4691						// Advantage: SVG file can be cached
4692						// Disadvantage: Not supported by Safari 6, IE 6-11, or Edge 12
4693						// Instead, adding <svg> element within each <button>
4694						if (control === 'volume') {
4695							iconClass = 'svg-' + this.volumeButton;
4696						}
4697						else if (control === 'fullscreen') {
4698							iconClass = 'svg-fullscreen-expand';
4699						}
4700						else if (control === 'slower') {
4701							if (this.speedIcons === 'animals') {
4702								iconClass = 'svg-turtle';
4703							}
4704							else {
4705								iconClass = 'svg-slower';
4706							}
4707						}
4708						else if (control === 'faster') {
4709							if (this.speedIcons === 'animals') {
4710								iconClass = 'svg-rabbit';
4711							}
4712							else {
4713								iconClass = 'svg-faster';
4714							}
4715						}
4716						else {
4717							iconClass = 'svg-' + control;
4718						}
4719						buttonIcon = $('<svg>',{
4720							'class': iconClass
4721						});
4722						buttonUse = $('<use>',{
4723							'xlink:href': this.rootPath + 'button-icons/able-icons.svg#' + iconClass
4724						});
4725						buttonIcon.append(buttonUse);
4726						*/
4727						var svgData;
4728						if (control === 'volume') {
4729							svgData = this.getSvgData(this.volumeButton);
4730						}
4731						else if (control === 'fullscreen') {
4732							svgData = this.getSvgData('fullscreen-expand');
4733						}
4734						else if (control === 'slower') {
4735							if (this.speedIcons === 'animals') {
4736								svgData = this.getSvgData('turtle');
4737							}
4738							else {
4739								svgData = this.getSvgData('slower');
4740							}
4741						}
4742						else if (control === 'faster') {
4743							if (this.speedIcons === 'animals') {
4744								svgData = this.getSvgData('rabbit');
4745							}
4746							else {
4747								svgData = this.getSvgData('faster');
4748							}
4749						}
4750						else {
4751							svgData = this.getSvgData(control);
4752						}
4753						buttonIcon = $('<svg>',{
4754							'focusable': 'false',
4755							'aria-hidden': 'true',
4756							'viewBox': svgData[0]
4757						});
4758						svgPath = $('<path>',{
4759							'd': svgData[1]
4760						});
4761						buttonIcon.append(svgPath);
4762						$newButton.html(buttonIcon);
4763
4764						// Final step: Need to refresh the DOM in order for browser to process & display the SVG
4765						$newButton.html($newButton.html());
4766					}
4767					else {
4768						// use images
4769						$buttonImg = $('<img>',{
4770							'src': buttonImgSrc,
4771							'alt': '',
4772							'role': 'presentation'
4773						});
4774						$newButton.append($buttonImg);
4775					}
4776					// add the visibly-hidden label for screen readers that don't support aria-label on the button
4777					var $buttonLabel = $('<span>',{
4778						'class': 'able-clipped'
4779					}).text(buttonTitle);
4780					$newButton.append($buttonLabel);
4781					// add an event listener that displays a tooltip on mouseenter or focus
4782					$newButton.on('mouseenter focus',function(e) {
4783
4784						// when entering a new tooltip, we can forget about hiding the previous tooltip.
4785						// since the same tooltip div is used, it's location just changes.
4786						clearTimeout(tooltipTimerId);
4787
4788						var buttonText = $(this).attr('aria-label');
4789						// get position of this button
4790						var position = $(this).position();
4791						var buttonHeight = $(this).height();
4792						var buttonWidth = $(this).width();
4793						// position() is expressed using top and left (of button);
4794						// add right (of button) too, for convenience
4795						var controllerWidth = thisObj.$controllerDiv.width();
4796						position.right = controllerWidth - position.left - buttonWidth;
4797
4798						// The following formula positions tooltip above the button  
4799						// var tooltipY = position.top - buttonHeight - 15;
4800
4801						// The following formula positions tooltip below the button 
4802						// which allows the tooltip to be hoverable as per WCAG 2.x SC 1.4.13
4803						// without obstructing the seekbar 
4804						var tooltipY = position.top + buttonHeight + 5; 
4805
4806						if ($(this).parent().hasClass('able-right-controls')) {
4807							// this control is on the right side
4808							var buttonSide = 'right';
4809						}
4810						else {
4811							// this control is on the left side
4812							var buttonSide = 'left';
4813						}
4814						// populate tooltip, then calculate its width before showing it
4815						var tooltipWidth = AblePlayer.localGetElementById($newButton[0], tooltipId).text(buttonText).width();
4816						// center the tooltip horizontally over the button
4817						if (buttonSide == 'left') {
4818							var tooltipX = position.left - tooltipWidth/2;
4819							if (tooltipX < 0) {
4820								// tooltip would exceed the bounds of the player. Adjust.
4821								tooltipX = 2;
4822							}
4823							var tooltipStyle = {
4824								left: tooltipX + 'px',
4825								right: '',
4826								top: tooltipY + 'px'
4827							};
4828						}
4829						else {
4830							var tooltipX = position.right - tooltipWidth/2;
4831							if (tooltipX < 0) {
4832								// tooltip would exceed the bounds of the player. Adjust.
4833								tooltipX = 2;
4834							}
4835							var tooltipStyle = {
4836								left: '',
4837								right: tooltipX + 'px',
4838								top: tooltipY + 'px'
4839							};
4840						}
4841						var tooltip = AblePlayer.localGetElementById($newButton[0], tooltipId).text(buttonText).css(tooltipStyle);
4842						thisObj.showTooltip(tooltip);
4843						$(this).on('mouseleave blur',function() {
4844
4845							// hide tooltip  (original line of code)
4846							// AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
4847
4848							// The above line was replaced with the following block 
4849							// in order to meet WCAG 2.x SC 1.4.13 
4850							// (keep the tooltip visible if user hovers over it)
4851							// This causes unwanted side effects if tooltips are positioned above the buttons 
4852							// as the persistent tooltip obstructs the seekbar, 
4853							// blocking users from being able to move a pointer from a button to the seekbar 
4854							// This limitation was addressed in 4.4.49 by moving the tooltip below the buttons 
4855
4856							// clear existing timeout before reassigning variable
4857							clearTimeout(tooltipTimerId);
4858							tooltipTimerId = setTimeout(function() {
4859								// give the user a half second to move cursor to tooltip before removing
4860								// see https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus#hoverable
4861								AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
4862							}, 500);
4863
4864							thisObj.$tooltipDiv.on('mouseenter focus', function() {
4865								clearTimeout(tooltipTimerId);
4866							});
4867
4868							thisObj.$tooltipDiv.on('mouseleave blur', function() {
4869								AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
4870							});
4871							
4872						}); 
4873					});
4874
4875					if (control === 'captions') {
4876						if (!this.prefCaptions || this.prefCaptions !== 1) {
4877							// captions are available, but user has them turned off
4878							if (this.captions.length > 1) {
4879								captionLabel = this.tt.captions;
4880							}
4881							else {
4882								captionLabel = this.tt.showCaptions;
4883							}
4884							$newButton.addClass('buttonOff').attr('title',captionLabel);
4885							$newButton.attr('aria-pressed', 'false');
4886							
4887
4888						}
4889					}
4890					else if (control === 'descriptions') {
4891						if (!this.prefDesc || this.prefDesc !== 1) {
4892							// user prefer non-audio described version
4893							// Therefore, load media without description
4894							// Description can be toggled on later with this button
4895							$newButton.addClass('buttonOff').attr('title',this.tt.turnOnDescriptions);
4896						}
4897					}
4898
4899					$controllerSpan.append($newButton);
4900
4901					// create variables of buttons that are referenced throughout the AblePlayer object
4902					if (control === 'play') {
4903						this.$playpauseButton = $newButton;
4904					}
4905					else if (control == 'previous') {
4906						this.$prevButton = $newButton;
4907						// if player is being rebuilt because user clicked the Prev button
4908						// return focus to that (newly built) button
4909						if (this.buttonWithFocus == 'previous') {
4910							this.$prevButton.focus();
4911							this.buttonWithFocus = null;
4912						}
4913					}
4914					else if (control == 'next') {
4915						this.$nextButton = $newButton;
4916						// if player is being rebuilt because user clicked the Next button
4917						// return focus to that (newly built) button
4918						if (this.buttonWithFocus == 'next') {
4919							this.$nextButton.focus();
4920							this.buttonWithFocus = null;
4921						}
4922					}
4923					else if (control === 'captions') {
4924						this.$ccButton = $newButton;
4925					}
4926					else if (control === 'sign') {
4927						this.$signButton = $newButton;
4928						// gray out sign button if sign language window is not active
4929						if (!(this.$signWindow.is(':visible'))) {
4930							this.$signButton.addClass('buttonOff');
4931						}
4932					}
4933					else if (control === 'descriptions') {
4934						this.$descButton = $newButton;
4935						// button will be enabled or disabled in description.js > initDescription()
4936					}
4937					else if (control === 'mute') {
4938						this.$muteButton = $newButton;
4939					}
4940					else if (control === 'transcript') {
4941						this.$transcriptButton = $newButton;
4942						// gray out transcript button if transcript is not active
4943						if (!(this.$transcriptDiv.is(':visible'))) {
4944							this.$transcriptButton.addClass('buttonOff').attr('title',this.tt.showTranscript);
4945						}
4946					}
4947					else if (control === 'fullscreen') {
4948						this.$fullscreenButton = $newButton;
4949					}
4950					else if (control === 'chapters') {
4951						this.$chaptersButton = $newButton;
4952					}
4953					else if (control === 'preferences') {
4954						this.$prefsButton = $newButton;
4955					}
4956					else if (control === 'volume') {
4957						this.$volumeButton = $newButton;
4958					}
4959				}
4960				if (control === 'volume') {
4961					// in addition to the volume button, add a hidden slider
4962					this.addVolumeSlider($controllerSpan);
4963				}
4964			}
4965			if ((i % 2) == 1) {
4966				this.$controllerDiv.append('<div style="clear:both;"></div>');
4967			}
4968		}
4969
4970		if (typeof this.$captionsDiv !== 'undefined') {
4971			// stylize captions based on user prefs
4972			this.stylizeCaptions(this.$captionsDiv);
4973		}
4974		if (typeof this.$descDiv !== 'undefined') {
4975			// stylize descriptions based on user's caption prefs
4976			this.stylizeCaptions(this.$descDiv);
4977		}
4978
4979		// combine left and right controls arrays for future reference
4980		this.controls = [];
4981		for (var sec in controlLayout) if (controlLayout.hasOwnProperty(sec)) {
4982			this.controls = this.controls.concat(controlLayout[sec]);
4983		}
4984
4985		// Update state-based display of controls.
4986		this.refreshControls('init');
4987	};
4988
4989	AblePlayer.prototype.useSvg = function () {
4990
4991		// Modified from IcoMoon.io svgxuse
4992		// @copyright Copyright (c) 2016 IcoMoon.io
4993		// @license		Licensed under MIT license
4994		// See https://github.com/Keyamoon/svgxuse
4995		// @version		1.1.16
4996
4997		var cache = Object.create(null); // holds xhr objects to prevent multiple requests
4998		var checkUseElems,
4999				tid; // timeout id
5000		var debouncedCheck = function () {
5001			clearTimeout(tid);
5002			tid = setTimeout(checkUseElems, 100);
5003		};
5004		var unobserveChanges = function () {
5005			return;
5006		};
5007		var observeChanges = function () {
5008			var observer;
5009			window.addEventListener('resize', debouncedCheck, false);
5010			window.addEventListener('orientationchange', debouncedCheck, false);
5011			if (window.MutationObserver) {
5012				observer = new MutationObserver(debouncedCheck);
5013				observer.observe(document.documentElement, {
5014					childList: true,
5015					subtree: true,
5016					attributes: true
5017				});
5018				unobserveChanges = function () {
5019					try {
5020						observer.disconnect();
5021						window.removeEventListener('resize', debouncedCheck, false);
5022						window.removeEventListener('orientationchange', debouncedCheck, false);
5023					} catch (ignore) {}
5024				};
5025			}
5026			else {
5027				document.documentElement.addEventListener('DOMSubtreeModified', debouncedCheck, false);
5028				unobserveChanges = function () {
5029					document.documentElement.removeEventListener('DOMSubtreeModified', debouncedCheck, false);
5030					window.removeEventListener('resize', debouncedCheck, false);
5031					window.removeEventListener('orientationchange', debouncedCheck, false);
5032				};
5033			}
5034		};
5035		var xlinkNS = 'http://www.w3.org/1999/xlink';
5036		checkUseElems = function () {
5037			var base,
5038					bcr,
5039					fallback = '', // optional fallback URL in case no base path to SVG file was given and no symbol definition was found.
5040					hash,
5041					i,
5042					Request,
5043					inProgressCount = 0,
5044					isHidden,
5045					url,
5046					uses,
5047					xhr;
5048			if (window.XMLHttpRequest) {
5049				Request = new XMLHttpRequest();
5050				if (Request.withCredentials !== undefined) {
5051					Request = XMLHttpRequest;
5052				}
5053				else {
5054					Request = XDomainRequest || undefined;
5055				}
5056			}
5057			if (Request === undefined) {
5058				return;
5059			}
5060			function observeIfDone() {
5061				// If done with making changes, start watching for chagnes in DOM again
5062				inProgressCount -= 1;
5063				if (inProgressCount === 0) { // if all xhrs were resolved
5064					observeChanges(); // watch for changes to DOM
5065				}
5066			}
5067			function attrUpdateFunc(spec) {
5068				return function () {
5069					if (cache[spec.base] !== true) {
5070						spec.useEl.setAttributeNS(xlinkNS, 'xlink:href', '#' + spec.hash);
5071					}
5072				};
5073			}
5074			function onloadFunc(xhr) {
5075				return function () {
5076					var body = document.body;
5077					var x = document.createElement('x');
5078					var svg;
5079					xhr.onload = null;
5080					x.innerHTML = xhr.responseText;
5081					svg = x.getElementsByTagName('svg')[0];
5082					if (svg) {
5083						svg.setAttribute('aria-hidden', 'true');
5084						svg.style.position = 'absolute';
5085						svg.style.width = 0;
5086						svg.style.height = 0;
5087						svg.style.overflow = 'hidden';
5088						body.insertBefore(svg, body.firstChild);
5089					}
5090					observeIfDone();
5091				};
5092			}
5093			function onErrorTimeout(xhr) {
5094				return function () {
5095					xhr.onerror = null;
5096					xhr.ontimeout = null;
5097					observeIfDone();
5098				};
5099			}
5100			unobserveChanges(); // stop watching for changes to DOM
5101			// find all use elements
5102			uses = document.getElementsByTagName('use');
5103			for (i = 0; i < uses.length; i += 1) {
5104				try {
5105					bcr = uses[i].getBoundingClientRect();
5106				} catch (ignore) {
5107					// failed to get bounding rectangle of the use element
5108					bcr = false;
5109				}
5110				url = uses[i].getAttributeNS(xlinkNS, 'href').split('#');
5111				base = url[0];
5112				hash = url[1];
5113				isHidden = bcr && bcr.left === 0 && bcr.right === 0 && bcr.top === 0 && bcr.bottom === 0;
5114				if (bcr && bcr.width === 0 && bcr.height === 0 && !isHidden) {
5115					// the use element is empty
5116					// if there is a reference to an external SVG, try to fetch it
5117					// use the optional fallback URL if there is no reference to an external SVG
5118					if (fallback && !base.length && hash && !document.getElementById(hash)) {
5119						base = fallback;
5120					}
5121					if (base.length) {
5122						// schedule updating xlink:href
5123						xhr = cache[base];
5124						if (xhr !== true) {
5125							// true signifies that prepending the SVG was not required
5126							setTimeout(attrUpdateFunc({
5127								useEl: uses[i],
5128								base: base,
5129								hash: hash
5130							}), 0);
5131						}
5132						if (xhr === undefined) {
5133							xhr = new Request();
5134							cache[base] = xhr;
5135							xhr.onload = onloadFunc(xhr);
5136							xhr.onerror = onErrorTimeout(xhr);
5137							xhr.ontimeout = onErrorTimeout(xhr);
5138							xhr.open('GET', base);
5139							xhr.send();
5140							inProgressCount += 1;
5141						}
5142					}
5143				}
5144				else {
5145					if (!isHidden) {
5146						if (cache[base] === undefined) {
5147							// remember this URL if the use element was not empty and no request was sent
5148							cache[base] = true;
5149						}
5150						else if (cache[base].onload) {
5151							// if it turns out that prepending the SVG is not necessary,
5152							// abort the in-progress xhr.
5153							cache[base].abort();
5154							cache[base].onload = undefined;
5155							cache[base] = true;
5156						}
5157					}
5158				}
5159			}
5160			uses = '';
5161			inProgressCount += 1;
5162			observeIfDone();
5163		};
5164/*
5165		// The load event fires when all resources have finished loading, which allows detecting whether SVG use elements are empty.
5166		window.addEventListener('load', function winLoad() {
5167			window.removeEventListener('load', winLoad, false); // to prevent memory leaks
5168			tid = setTimeout(checkUseElems, 0);
5169		}, false);
5170*/
5171	};
5172
5173	AblePlayer.prototype.cuePlaylistItem = function(sourceIndex) {
5174
5175		// Move to a new item in a playlist.
5176		// NOTE: Swapping source for audio description is handled elsewhere;
5177		// see description.js > swapDescription()
5178
5179		/*
5180			// Decided against preventing a reload of the current item in the playlist.
5181			// If it's clickable, users should be able to click on it and expect something to happen.
5182			// Leaving here though in case it's determined to be desirable.
5183		if (sourceIndex === this.playlistItemIndex) {
5184			// user has requested the item that's currently playing
5185			// just ignore the request
5186			return;
5187		}
5188		this.playlistItemIndex = sourceIndex;
5189		*/
5190
5191		var $newItem, prevPlayer, newPlayer, itemTitle, itemLang, sources, s, i, $newSource, nowPlayingSpan;
5192
5193		var thisObj = this;
5194
5195		prevPlayer = this.player;
5196
5197		if (this.initializing) { // this is the first track - user hasn't pressed play yet
5198			// do nothing.
5199		}
5200		else {
5201				if (this.playerCreated) {
5202				// remove the old
5203				this.deletePlayer('playlist');
5204			}
5205		}
5206
5207		// set swappingSrc; needs to be true within recreatePlayer(), called below
5208		this.swappingSrc = true;
5209
5210		// if a new playlist item is being requested, and playback has already started,  
5211		// it should be ok to play automatically, regardless of how it was requested 
5212		if (this.startedPlaying) { 
5213			this.okToPlay = true; 
5214		}
5215		else { 
5216			this.okToPlay = false; 
5217		}
5218
5219		// We are no longer loading the previous media source 
5220		// Only now, as a new source is requested, is it safe to reset this var 
5221		// It will be reset to true when media.load() is called 
5222		this.loadingMedia = false; 
5223
5224		// Determine appropriate player to play this media
5225		$newItem = this.$playlist.eq(sourceIndex);
5226		if (this.hasAttr($newItem,'data-youtube-id')) {
5227			this.youTubeId = this.getYouTubeId($newItem.attr('data-youtube-id'));
5228			if (this.hasAttr($newItem,'data-youtube-desc-id')) {
5229				this.youTubeDescId = this.getYouTubeId($newItem.attr('data-youtube-desc-id'));
5230			}
5231			newPlayer = 'youtube';
5232		}
5233		else if (this.hasAttr($newItem,'data-vimeo-id')) {
5234			this.vimeoId = this.getVimeoId($newItem.attr('data-vimeo-id'));
5235			if (this.hasAttr($newItem,'data-vimeo-desc-id')) {
5236				this.vimeoDescId = this.getVimeoId($newItem.attr('data-vimeo-desc-id'));
5237			}
5238			newPlayer = 'vimeo';
5239		}
5240		else {
5241				newPlayer = 'html5';
5242		}
5243		if (newPlayer === 'youtube') {
5244			if (prevPlayer === 'html5') {
5245				// pause and hide the previous media
5246				if (this.playing) {
5247					this.pauseMedia();
5248				}
5249				this.$media.hide();
5250			}
5251		}
5252		else {
5253				// the new player is not youtube
5254			this.youTubeId = false;
5255			if (prevPlayer === 'youtube') {
5256				// unhide the media element
5257				this.$media.show();
5258			}
5259		}
5260		this.player = newPlayer;
5261
5262		// remove source and track elements from previous playlist item
5263		this.$media.empty();
5264
5265		// transfer media attributes from playlist to media element
5266		if (this.hasAttr($newItem,'data-poster')) {
5267			this.$media.attr('poster',$newItem.attr('data-poster'));
5268		}
5269		if (this.hasAttr($newItem,'data-youtube-desc-id')) {
5270			this.$media.attr('data-youtube-desc-id',$newItem.attr('data-youtube-desc-id'));
5271		}
5272		if (this.youTubeId) {
5273			this.$media.attr('data-youtube-id',$newItem.attr('data-youtube-id'));
5274		}
5275
5276		// add new <source> elements from playlist data
5277		var $sourceSpans = $newItem.children('span.able-source');
5278		if ($sourceSpans.length) {
5279			$sourceSpans.each(function() {
5280				if (thisObj.hasAttr($(this),'data-src')) {
5281					// this is the only required attribute
5282					var $newSource = $('<source>',{
5283						'src': $(this).attr('data-src')
5284					});
5285					if (thisObj.hasAttr($(this),'data-type')) {
5286						$newSource.attr('type',$(this).attr('data-type'));
5287					}
5288					if (thisObj.hasAttr($(this),'data-desc-src')) {
5289						$newSource.attr('data-desc-src',$(this).attr('data-desc-src'));
5290					}
5291					if (thisObj.hasAttr($(this),'data-sign-src')) {
5292						$newSource.attr('data-sign-src',$(this).attr('data-sign-src'));
5293					}
5294					thisObj.$media.append($newSource);
5295				}
5296			});
5297		}
5298
5299		// add new <track> elements from playlist data
5300		var $trackSpans = $newItem.children('span.able-track');
5301		if ($trackSpans.length) {
5302			 // for each element in $trackSpans, create a new <track> element
5303			$trackSpans.each(function() {
5304				if (thisObj.hasAttr($(this),'data-src') &&
5305					thisObj.hasAttr($(this),'data-kind') &&
5306					thisObj.hasAttr($(this),'data-srclang')) {
5307					// all required attributes are present
5308					var $newTrack = $('<track>',{
5309						'src': $(this).attr('data-src'),
5310						'kind': $(this).attr('data-kind'),
5311						'srclang': $(this).attr('data-srclang')
5312					});
5313					if (thisObj.hasAttr($(this),'data-label')) {
5314						$newTrack.attr('label',$(this).attr('data-label'));
5315					}
5316					if (thisObj.hasAttr($(this),'data-desc')) {
5317						$newTrack.attr('data-desc',$(this).attr('data-desc'));
5318					}
5319					thisObj.$media.append($newTrack);
5320				}
5321			});
5322		}
5323
5324		itemTitle = $newItem.text();
5325		if (this.hasAttr($newItem,'lang')) {
5326			itemLang = $newItem.attr('lang');
5327		}
5328		// Update relevant arrays
5329		this.$sources = this.$media.find('source');
5330
5331		// recreate player, informed by new attributes and track elements
5332		if (this.recreatingPlayer) { 
5333			// stopgap to prevent multiple firings of recreatePlayer()
5334			return; 
5335		}
5336		this.recreatePlayer().then(function() { 
5337
5338			// update playlist to indicate which item is playing
5339			thisObj.$playlist.removeClass('able-current')
5340				.children('button').removeAttr('aria-current');
5341			thisObj.$playlist.eq(sourceIndex).addClass('able-current')
5342				.children('button').attr('aria-current','true'); 
5343			
5344			// update Now Playing div
5345			if (thisObj.showNowPlaying === true) {
5346				if (typeof thisObj.$nowPlayingDiv !== 'undefined') {
5347					nowPlayingSpan = $('<span>');
5348					if (typeof itemLang !== 'undefined') {
5349						nowPlayingSpan.attr('lang',itemLang);
5350					}
5351					nowPlayingSpan.html('<span>' + thisObj.tt.selectedTrack + ':</span>' + itemTitle);
5352					thisObj.$nowPlayingDiv.html(nowPlayingSpan);
5353				}
5354			}
5355
5356			// if thisObj.swappingSrc is true, media will autoplay when ready
5357			if (thisObj.initializing) { // this is the first track - user hasn't pressed play yet
5358				thisObj.swappingSrc = false;
5359			}
5360			else {
5361				if (thisObj.player === 'html5') {
5362					if (!thisObj.loadingMedia) { 
5363						thisObj.media.load();
5364						thisObj.loadingMedia = true; 
5365					}
5366				}
5367				else if (thisObj.player === 'youtube') {
5368					thisObj.okToPlay = true; 
5369				}
5370			}
5371			thisObj.initializing = false;
5372			thisObj.playerCreated = true; // remains true until browser is refreshed		
5373		});	
5374	};
5375
5376	AblePlayer.prototype.deletePlayer = function(context) {
5377
5378		// remove player components that need to be rebuilt 
5379		// after swapping media sources that have different durations 
5380		// or explicitly declared data-desc attributes  
5381
5382		// Context is one of the following: 
5383		// playlist - called from cuePlaylistItem() 
5384		// swap-desc-html - called from swapDescription with this.player == 'html'
5385		// swap-desc-youtube - called from swapDescription with this.player == 'youtube'  
5386		// swap-desc-vimeo -  called from swapDescription with this.player == 'vimeo'  
5387
5388		if (this.player === 'youtube' && this.youTubePlayer) { 
5389			this.youTubePlayer.destroy(); 
5390		}
5391
5392		if (this.player === 'vimeo' && this.vimeoPlayer) { 
5393			this.vimeoPlayer.destroy(); 
5394		}
5395
5396
5397/*	TODO - Investigate: when is this needed? 
5398		// remove previous video's attributes and child elements from media element
5399		this.$media.removeAttr('poster width height');
5400		this.$media.empty();
5401*/
5402		// Empty elements that will be rebuilt
5403		this.$controllerDiv.empty();
5404		// this.$statusBarDiv.empty();
5405		// this.$timer.empty();
5406		this.$elapsedTimeContainer.empty().text('0:00'); // span.able-elapsedTime
5407		this.$durationContainer.empty(); // span.able-duration
5408		
5409		// Remove popup windows and modal dialogs; these too will be rebuilt
5410		if (this.$signWindow) {
5411				this.$signWindow.remove();
5412		}
5413		if (this.$transcriptArea) {
5414				this.$transcriptArea.remove();
5415		}
5416		$('.able-modal-dialog').remove();
5417		
5418		// Remove caption and description wrappers 
5419		if (this.$captionsWrapper) {
5420			this.$captionsWrapper.remove();
5421		}
5422		if (this.$descDiv) { 
5423			this.$descDiv.remove(); 
5424		}
5425		
5426		// reset key variables
5427		this.hasCaptions = false;
5428		this.hasChapters = false;
5429		this.hasDescTracks = false; 
5430		this.hasOpenDesc = false;
5431		this.hasClosedDesc = false; 
5432
5433		this.captionsPopup = null;
5434		this.chaptersPopup = null;
5435		this.transcriptType = null;
5436
5437		this.playerDeleted = true; // will reset to false in recreatePlayer() 
5438	};
5439
5440	AblePlayer.prototype.getButtonTitle = function(control) {
5441
5442		if (control === 'playpause') {
5443			return this.tt.play;
5444		}
5445		else if (control === 'play') {
5446			return this.tt.play;
5447		}
5448		else if (control === 'pause') {
5449			return this.tt.pause;
5450		}
5451		else if (control === 'restart') {
5452			return this.tt.restart;
5453		}
5454		else if (control === 'previous') {
5455			return this.tt.prevTrack;
5456		}
5457		else if (control === 'next') {
5458			return this.tt.nextTrack;
5459		}
5460		else if (control === 'rewind') {
5461			return this.tt.rewind;
5462		}
5463		else if (control === 'forward') {
5464			return this.tt.forward;
5465		}
5466		else if (control === 'captions') {
5467			if (this.captions.length > 1) {
5468				return this.tt.captions;
5469			}
5470			else {
5471				if (this.captionsOn) {
5472					return this.tt.hideCaptions;
5473				}
5474				else {
5475					return this.tt.showCaptions;
5476				}
5477			}
5478		}
5479		else if (control === 'descriptions') {
5480			if (this.descOn) {
5481				return this.tt.turnOffDescriptions;
5482			}
5483			else {
5484				return this.tt.turnOnDescriptions;
5485			}
5486		}
5487		else if (control === 'transcript') {
5488			if (this.$transcriptDiv.is(':visible')) {
5489				return this.tt.hideTranscript;
5490			}
5491			else {
5492				return this.tt.showTranscript;
5493			}
5494		}
5495		else if (control === 'chapters') {
5496			return this.tt.chapters;
5497		}
5498		else if (control === 'sign') {
5499			return this.tt.sign;
5500		}
5501		else if (control === 'volume') {
5502			return this.tt.volume;
5503		}
5504		else if (control === 'faster') {
5505			return this.tt.faster;
5506		}
5507		else if (control === 'slower') {
5508			return this.tt.slower;
5509		}
5510		else if (control === 'preferences') {
5511			return this.tt.preferences;
5512		}
5513		else if (control === 'help') {
5514			// return this.tt.help;
5515		}
5516		else if (control === 'fullscreen') { 
5517			if (!this.fullscreen) {
5518				return this.tt.enterFullscreen; 
5519			}
5520			else { 
5521				return this.tt.exitFullscreen; 
5522			}
5523		}
5524		else {
5525			// there should be no other controls, but just in case:
5526			// return the name of the control with first letter in upper case
5527			// ultimately will need to get a translated label from this.tt
5528			if (this.debug) {
5529				console.log('Found an untranslated label: ' + control);
5530			}
5531			return control.charAt(0).toUpperCase() + control.slice(1);
5532		}
5533	};
5534
5535
5536})(jQuery);
5537
5538(function ($) {
5539	// Loads files referenced in track elements, and performs appropriate setup.
5540	// For example, captions and text descriptions.
5541	// This will be called whenever the player is recreated.
5542	// Added in v2.2.23: Also handles YouTube caption tracks
5543
5544	AblePlayer.prototype.setupTracks = function () {
5545
5546		var thisObj, deferred, promise, loadingPromises, loadingPromise,
5547			i, tracks, track, kind;
5548
5549		thisObj = this;
5550
5551		deferred = new $.Deferred();
5552		promise = deferred.promise();
5553
5554		loadingPromises = [];
5555
5556		if ($('#able-vts').length) {
5557			// Page includes a container for a VTS instance
5558			this.vtsTracks = [];
5559			this.hasVts = true;
5560		} else {
5561			this.hasVts = false;
5562		}
5563
5564		// Source array for populating the above arrays 
5565		// varies, depending on whether there are dedicated description tracks 
5566		if (this.hasDescTracks && this.descOn) {
5567			tracks = this.altTracks;
5568		} else {
5569			tracks = this.tracks;
5570		}
5571		for (i = 0; i < tracks.length; i++) {
5572
5573			track = tracks[i];
5574			kind = track.kind;
5575
5576			if (!track.src) {
5577				if (thisObj.usingYouTubeCaptions || thisObj.usingVimeoCaptions) {
5578					// skip all the hullabaloo and go straight to setupCaptions
5579					thisObj.setupCaptions(track);
5580				} else {
5581					// Nothing to load!
5582					// Skip this track; move on to next i
5583				}				
5584				continue;
5585			}
5586			var trackSrc = track.src;
5587			loadingPromise = this.loadTextObject(track.src); // resolves with src, trackText
5588			loadingPromises.push(loadingPromise.catch(function (src) {
5589				console.warn('Failed to load captions track from ' + src);
5590			}));
5591			loadingPromise.then((function (track, kind) {
5592				var trackSrc = track.src;
5593				var trackLang = track.language;
5594				var trackLabel = track.label;
5595				var trackDesc = track.desc;
5596
5597				return function (trackSrc, trackText) {
5598					// these are the two vars returned from loadTextObject
5599
5600					var trackContents = trackText;
5601					var cues = thisObj.parseWebVTT(trackSrc, trackContents).cues;
5602					if (thisObj.hasVts) {
5603
5604						// setupVtsTracks() is in vts.js
5605						thisObj.setupVtsTracks(kind, trackLang, trackDesc, trackLabel, trackSrc, trackContents);
5606					}
5607					if (kind === 'captions' || kind === 'subtitles') {
5608						thisObj.setupCaptions(track, cues);
5609					} else if (kind === 'descriptions') {
5610						thisObj.setupDescriptions(track, cues);
5611					} else if (kind === 'chapters') {
5612						thisObj.setupChapters(track, cues);
5613					} else if (kind === 'metadata') {
5614						thisObj.setupMetadata(track, cues);
5615					}
5616				}
5617			})(track, kind));
5618		}
5619		if (thisObj.usingYouTubeCaptions || thisObj.usingVimeoCaptions) {
5620			deferred.resolve(); 
5621		}
5622		else { 			
5623			$.when.apply($, loadingPromises).then(function () {
5624				deferred.resolve();
5625			});
5626		}
5627		return promise;
5628	};
5629
5630	AblePlayer.prototype.getTracks = function () {
5631		
5632		// define an array tracks with the following structure:
5633		// kind - string, e.g. "captions", "descriptions"
5634		// src - string, URL of WebVTT source file
vendor: 2,390 bytes, lines 5635-5705
5635		// language - string, lang code
5636		// label - string to display, e.g., in CC menu
5637		// def - Boolean, true if this is the default track
5638		// cues - array with startTime, endTime, and payload
5639		// desc - Boolean, true if track includes a data-desc attribute
5640
5641		var thisObj, deferred, promise, captionTracks, altCaptionTracks,
5642			trackLang, trackLabel, isDefault, forDesc, hasDefault, hasTrackInDefLang,
5643			trackFound, i, j, capLabel, inserted;
5644
5645		thisObj = this;
5646		hasDefault = false; 
5647
5648		deferred = new $.Deferred();
5649		promise = deferred.promise();
5650
5651		this.$tracks = this.$media.find('track');
5652		this.tracks = []; // only includes tracks that do NOT have data-desc
5653		this.altTracks = []; // only includes tracks that DO have data-desc 
5654
5655		// Arrays for each kind, to be populated later  
5656		this.captions = [];
5657		this.descriptions = [];
5658		this.chapters = [];
5659		this.meta = [];
5660
5661		this.hasCaptionsTrack = false; // will change to true if one or more tracks has kind="captions"
5662		this.hasDescTracks = false; // will change to true if one or more tracks has data-desc
5663
5664		if (this.$tracks.length) {			
5665
5666			this.usingYouTubeCaptions = false; 
5667			// create object from HTML5 tracks
5668			this.$tracks.each(function (index, element) {
5669
5670				if ($(this).attr('kind') === 'captions') {
5671					thisObj.hasCaptionsTrack = true; 
5672				}
5673				else if ($(this).attr('kind') === 'descriptions') {
5674					thisObj.hasClosedDesc = true; 
5675				}
5676
5677				// srcLang should always be included with <track>, but HTML5 spec doesn't require it
5678				// if not provided, assume track is the same language as the default player language
5679				if ($(this).attr('srclang')) {
5680					trackLang = $(this).attr('srclang');
5681				} 
5682				else {
5683					trackLang = thisObj.lang;
5684				}
5685				if ($(this).attr('label')) {
5686					trackLabel = $(this).attr('label');
5687				} 
5688				else {
5689					trackLabel = thisObj.getLanguageName(trackLang);
5690				}
5691
5692				if (typeof $(this).attr('default') !== 'undefined' && !hasDefault) {	
5693					isDefault = true;
5694					hasDefault = true; 
5695				} 
5696				else if (trackLang === thisObj.lang) {
5697					// this track is in the default lang of the player 
5698					// save this for later 
5699					// if there is no other default track specified 
5700					// this will be the default 
5701					hasTrackInDefLang = true; 
5702					isDefault = false; // for now; this could change if there's no default attribute					
5703				}
5704				else {
5705					isDefault = false;
5706				}
5707				if (isDefault) {
5708					// this.captionLang will also be the default language for non-caption tracks
5709					thisObj.captionLang = trackLang;
5710				}
5711
5712				if ($(this).data('desc') !== undefined) {
5713					forDesc = true;
5714					thisObj.hasDescTracks = true;
5715				} 
5716				else {
5717					forDesc = false;
5718				}
5719				if (forDesc) {
5720					thisObj.altTracks.push({
5721						'kind': $(this).attr('kind'),
5722						'src': $(this).attr('src'),
5723						'language': trackLang,
5724						'label': trackLabel,
5725						'def': isDefault,
5726						'desc': forDesc
5727					});
5728				} else {
5729					thisObj.tracks.push({
5730						'kind': $(this).attr('kind'),
5731						'src': $(this).attr('src'),
5732						'language': trackLang,
5733						'label': trackLabel,
5734						'def': isDefault,
5735						'desc': forDesc
5736					});
5737				}
5738
5739				if (index == thisObj.$tracks.length - 1) {
5740					// This is the last track. 
5741					if (!hasDefault) { 
5742						if (hasTrackInDefLang) { 
5743							thisObj.captionLang = thisObj.lang; 
5744							trackFound = false; 									
5745							i = 0; 
5746							while (i < thisObj.tracks.length && !trackFound) { 
5747								if (thisObj.tracks[i]['language'] === thisObj.lang) { 
5748									thisObj.tracks[i]['def'] = true; 
5749									trackFound = true; 
5750								}
5751								i++; 
5752							}
5753						}
5754						else { 
5755							// use the first track 
5756							thisObj.tracks[0]['def'] = true; 
5757							thisObj.captionLang = thisObj.tracks[0]['language']; 
5758						}
5759					}
5760					// Remove 'default' attribute from all <track> elements
5761					// This data has already been saved to this.tracks
5762					// and some browsers will display the default captions, 
5763					// despite all standard efforts to suppress them
5764					thisObj.$media.find('track').removeAttr('default'); 
5765				}
5766			});
5767		}
5768		if (!this.$tracks.length || !this.hasCaptionsTrack) { 
5769			// this media has no track elements 
5770			// if this is a youtube or vimeo player, check there for captions/subtitles
5771			if (this.player === 'youtube') {
5772				this.getYouTubeCaptionTracks(this.youTubeId).then(function() {
5773					if (thisObj.hasCaptions) { 
5774						thisObj.usingYouTubeCaptions = true; 
5775						if (thisObj.$captionsWrapper) {
5776							thisObj.$captionsWrapper.remove();
5777						}
5778					}					
5779					deferred.resolve();
5780				});
5781			}
5782			else if (this.player === 'vimeo') {
5783				this.getVimeoCaptionTracks().then(function() {
5784					if (thisObj.hasCaptions) { 
5785						thisObj.usingVimeoCaptions = true; 
5786						if (thisObj.$captionsWrapper) {
5787							thisObj.$captionsWrapper.remove();
5788						}
5789					}
5790					deferred.resolve();
5791				});
5792			}
5793			else {
5794				// this is neither YouTube nor Vimeo
5795				// there just ain't no tracks (captions or otherwise)
5796				this.hasCaptions = false; 
5797				if (thisObj.$captionsWrapper) {
5798					thisObj.$captionsWrapper.remove();
5799				}
5800				deferred.resolve();
5801			}
5802		}
5803		else { 
5804			// there is at least one track with kind="captions" 
5805			deferred.resolve(); 
5806
5807		}
5808		return promise;
5809
5810	};
5811
5812	AblePlayer.prototype.setupCaptions = function (track, cues) {
5813
5814		// Setup player for display of captions (one track at a time)
5815		var thisObj, captions, inserted, i, capLabel;
5816
5817		// Insert track into captions array 
5818		// in its proper alphabetical sequence by label  
5819		if (typeof cues === 'undefined') {
5820			cues = null;
5821		}
5822
5823		if (this.usingYouTubeCaptions || this.usingVimeoCaptions) { 
5824			// this.captions has already been populated 
5825			// For YouTube, this happens in youtube.js > getYouTubeCaptionTracks()
5826			// For VImeo, this happens in vimeo.js > getVimeoCaptionTracks() 
5827			// So, nothing to do here... 
5828		}
5829		else { 
5830
5831			if (this.captions.length === 0) { // this is the first	
5832				this.captions.push({
5833					'language': track.language,
5834					'label': track.label,
5835					'def': track.def,
5836					'cues': cues
5837				});
5838			} 
5839			else { // there are already captions in the array			
5840				inserted = false;
5841				for (i = 0; i < this.captions.length; i++) {
5842					capLabel = track.label;
5843					if (capLabel.toLowerCase() < this.captions[i].label.toLowerCase()) {
5844						// insert before track i
5845						this.captions.splice(i, 0, {
5846							'language': track.language,
5847							'label': track.label,
5848							'def': track.def,
5849							'cues': cues
5850						});
5851						inserted = true;
5852						break;
5853					}
5854				}
5855				if (!inserted) {
5856					// just add track to the end
5857					this.captions.push({
5858						'language': track.language,
5859						'label': track.label,
5860						'def': track.def,
5861						'cues': cues
5862					});
5863				}	
5864			}
5865		}
5866
5867		// there are captions available 
5868		this.hasCaptions = true;
5869		this.currentCaption = -1; 
5870		if (this.prefCaptions === 1) {
5871			this.captionsOn = true;
5872		} else if (this.prefCaptions === 0) {
5873			this.captionsOn = false;
5874		} else {
5875			// user has no prefs. Use default state.
5876			if (this.defaultStateCaptions === 1) {
5877				this.captionsOn = true;
5878			} else {
5879				this.captionsOn = false;
5880			}
5881		}
5882		if (this.mediaType === 'audio' && this.captionsOn) {
5883			this.$captionsContainer.removeClass('captions-off');
5884		}
5885
5886		if (!this.$captionsWrapper || 
5887			(this.$captionsWrapper && !($.contains(this.$ableDiv[0], this.$captionsWrapper[0])))) {
5888			// captionsWrapper either doesn't exist, or exists in an orphaned state 
5889			// Either way, it needs to be rebuilt...  
5890			this.$captionsDiv = $('<div>', {
5891				'class': 'able-captions',
5892			});
5893			this.$captionsWrapper = $('<div>', {
5894				'class': 'able-captions-wrapper',
5895				'aria-hidden': 'true'
5896			}).hide();
5897			if (this.prefCaptionsPosition === 'below') {
5898				this.$captionsWrapper.addClass('able-captions-below');
5899			} else {
5900				this.$captionsWrapper.addClass('able-captions-overlay');
5901			}
5902			this.$captionsWrapper.append(this.$captionsDiv);
5903			this.$captionsContainer.append(this.$captionsWrapper);
5904		}
5905	};
5906
5907	AblePlayer.prototype.setupDescriptions = function (track, cues) {
5908
5909		// called via setupTracks() only if there is track with kind="descriptions"
5910		// prepares for delivery of text description , in case it's needed
5911		// whether and how it's delivered is controlled within description.js > initDescription()
5912
5913		this.hasClosedDesc = true;
5914		this.currentDescription = -1;
5915		this.descriptions.push({
5916			cues: cues,
5917			language: track.language
5918		});
5919	};
5920
5921	AblePlayer.prototype.setupChapters = function (track, cues) {
5922
5923		// NOTE: WebVTT supports nested timestamps (to form an outline)
5924		// This is not currently supported.
5925
5926		this.hasChapters = true;
5927		this.chapters.push({
5928			cues: cues,
5929			language: track.language
5930		});
5931	};
5932
5933	AblePlayer.prototype.setupMetadata = function (track, cues, trackDesc) {
5934
5935		if (this.metaType === 'text') {
5936			// Metadata is only supported if data-meta-div is provided
5937			// The player does not display metadata internally
5938			if (this.metaDiv) {
5939				if ($('#' + this.metaDiv)) {
5940					// container exists
5941					this.$metaDiv = $('#' + this.metaDiv);
5942					this.hasMeta = true;
5943					this.meta = cues;
5944				}
5945			}
5946		} else if (this.metaType === 'selector') {
5947			this.hasMeta = true;
5948			this.visibleSelectors = [];
5949			this.meta = cues;
5950		}
5951	};
5952
5953	AblePlayer.prototype.loadTextObject = function (src) {
5954
5955		// TODO: Incorporate the following function, moved from setupTracks()
5956		// convert XMl/TTML captions file
5957		/*
5958		if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('<?xml'))) {
5959			trackContents = thisObj.ttml2webvtt(trackText);
5960		}
5961		*/
5962		var deferred, promise, thisObj, $tempDiv;
5963
5964		deferred = new $.Deferred();
5965		promise = deferred.promise();
5966		thisObj = this;
5967
5968		// create a temp div for holding data
5969		$tempDiv = $('<div>', {
5970			style: 'display:none'
5971		});
5972		$tempDiv.load(src, function (trackText, status, req) {
5973			if (status === 'error') {
5974				if (thisObj.debug) {
5975					console.log('error reading file ' + src + ': ' + status);
5976				}
5977				deferred.reject(src);
5978			} else {
5979				deferred.resolve(src, trackText);
5980			}
5981			$tempDiv.remove();
5982		});
5983		return promise;
5984	};
5985
5986})(jQuery);
5987
5988(function ($) {
5989	AblePlayer.prototype.initYouTubePlayer = function () {
5990
5991		var thisObj, deferred, promise, youTubeId, googleApiPromise, json;
5992		thisObj = this;
5993
5994		deferred = new $.Deferred();
5995		promise = deferred.promise();
5996
5997		this.youTubePlayerReady = false; 
5998
5999		// if a described version is available && user prefers desription
6000		// init player using the described version
6001		if (this.youTubeDescId && this.prefDesc) {
6002			youTubeId = this.youTubeDescId;
6003		}
6004		else {
6005			youTubeId = this.youTubeId;
6006		}
6007		this.activeYouTubeId = youTubeId;
6008		if (AblePlayer.youTubeIframeAPIReady) {
6009			// Script already loaded and ready.
6010			thisObj.finalizeYoutubeInit().then(function() {
6011				deferred.resolve();
6012			});
6013		}
6014		else {
6015			// Has another player already started loading the script? If so, abort...
6016			if (!AblePlayer.loadingYouTubeIframeAPI) {
6017				$.getScript('https://www.youtube.com/iframe_api').fail(function () {
6018					deferred.fail();
6019				});
6020			}
6021
6022			// Otherwise, keeping waiting for script load event...
6023			$('body').on('youTubeIframeAPIReady', function () {
6024				thisObj.finalizeYoutubeInit().then(function() {
6025					deferred.resolve();
6026				});
6027			});
6028		}
6029		return promise;
6030	};
6031
6032	AblePlayer.prototype.finalizeYoutubeInit = function () {
6033
6034		// This is called once we're sure the Youtube iFrame API is loaded -- see above
6035
6036		var deferred, promise, thisObj, containerId, ccLoadPolicy, videoDimensions, autoplay;
6037
6038		deferred = new $.Deferred();
6039		promise = deferred.promise();
6040
6041		thisObj = this;
6042
6043		containerId = this.mediaId + '_youtube';
6044
6045		this.$mediaContainer.prepend($('<div>').attr('id', containerId));
6046		// NOTE: Tried the following in place of the above in January 2016
6047		// because in some cases two videos were being added to the DOM
6048		// However, once v2.2.23 was fairly stable, unable to reproduce that problem
6049		// so maybe it's not an issue. This is preserved here temporarily, just in case it's needed...
6050		// thisObj.$mediaContainer.html($('<div>').attr('id', containerId));
6051
6052		// cc_load_policy:
6053		// 0 - show captions depending on user's preference on YouTube
6054		// 1 - show captions by default, even if the user has turned them off
6055		// IMPORTANT: This *must* be set to 1 or some browsers 
6056		// fail to load any texttracks (observed in Chrome, not in Firefox) 
6057		ccLoadPolicy = 1;
6058
6059		if (this.okToPlay) {
6060			autoplay = 1;
6061		}
6062		else {
6063			autoplay = 0;
6064		}
6065
6066		// Documentation https://developers.google.com/youtube/player_parameters
6067
6068		if (typeof this.captionLang == 'undefined') { 
6069			// init using the default player lang
6070			this.captionLang = this.lang; 
6071		}
6072		this.youTubePlayer = new YT.Player(containerId, {
6073			videoId: this.activeYouTubeId,
6074			host: this.youTubeNoCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com',
6075			playerVars: {
6076				autoplay: autoplay,
6077				enablejsapi: 1,
6078				disableKb: 1, // disable keyboard shortcuts, using our own
6079				playsinline: this.playsInline,
6080				start: this.startTime,
6081				controls: 0, // no controls, using our own
6082				cc_load_policy: ccLoadPolicy,
6083				cc_lang_pref: this.captionLang, // set the caption language 
6084				hl: this.lang, // set the UI language to match Able Player 
6085				modestbranding: 1, // no YouTube logo in controller
6086				rel: 0, // when video ends, show only related videos from same channel (1 shows any)
6087				iv_load_policy: 3 // do not show video annotations
6088			},
6089			events: {
6090				onReady: function () {
6091					thisObj.youTubePlayerReady = true; 
6092					if (!thisObj.playerWidth || !thisObj.playerHeight) { 
6093						thisObj.getYouTubeDimensions();
6094					}
6095					if (thisObj.playerWidth && thisObj.playerHeight) { 
6096						thisObj.youTubePlayer.setSize(thisObj.playerWidth,thisObj.playerHeight);
6097						thisObj.$ableWrapper.css({
6098							'width': thisObj.playerWidth + 'px'
6099						});
6100					}
6101					if (thisObj.swappingSrc) {
6102						// swap is now complete
6103						thisObj.swappingSrc = false;
6104						thisObj.restoreFocus();
6105						thisObj.cueingPlaylistItem = false;
6106						if (thisObj.playing || thisObj.okToPlay) {
6107							// resume playing
6108							thisObj.playMedia();
6109						}
6110					}
6111					if (thisObj.userClickedPlaylist) {
6112						thisObj.userClickedPlaylist = false; // reset
6113					}
6114					if (thisObj.recreatingPlayer) { 
6115						thisObj.recreatingPlayer = false; // reset
6116					}
6117					deferred.resolve();
6118				},
6119				onError: function (x) {
6120					deferred.fail();
6121				},
6122				onStateChange: function (x) {				
6123					thisObj.getPlayerState().then(function(playerState) {
6124						// values of playerState: 'playing','paused','buffering','ended'
6125						if (playerState === 'playing') {
6126							thisObj.playing = true;
6127							thisObj.startedPlaying = true;
6128							thisObj.paused = false;
6129						}
6130						else if (playerState == 'ended') {
6131							thisObj.onMediaComplete();
6132						}
6133						else {
6134							thisObj.playing = false;
6135							thisObj.paused = true;
6136						}
6137						if (thisObj.stoppingYouTube && playerState === 'paused') {
6138							if (typeof thisObj.$posterImg !== 'undefined') {
6139								thisObj.$posterImg.show();
6140							}
6141							thisObj.stoppingYouTube = false;
6142							thisObj.seeking = false;
6143							thisObj.playing = false;
6144							thisObj.paused = true;
6145						}
6146					});
6147					// If caption tracks are hosted locally, but are also available on YouTube,
6148					// we need to turn them off on YouTube or there will be redundant captions 
6149					// This is the most reliable event on which to unload the caption module 
6150					if (thisObj.player === 'youtube' && !thisObj.usingYouTubeCaptions) { 						
6151						if (thisObj.youTubePlayer.getOptions('captions')) { 							
6152							thisObj.youTubePlayer.unloadModule('captions');
6153						}
6154					}			 			
6155				},
6156				onPlaybackQualityChange: function () {
6157					// do something
6158				},
6159			}
6160		});
6161		if (!this.hasPlaylist) {
6162			// remove the media element, since YouTube replaces that with its own element in an iframe
6163			// this is handled differently for playlists. See buildplayer.js > cuePlaylistItem()
6164			this.$media.remove();
6165		}		
6166		return promise;
6167	};
6168
6169	AblePlayer.prototype.getYouTubeDimensions = function (youTubeContainerId) {
6170
6171		// The YouTube iframe API does not have a getSize() of equivalent method 
6172		// so, need to get dimensions from YouTube's iframe 
6173
6174		var $iframe, width, height; 
6175
6176		$iframe = this.$ableWrapper.find('iframe'); 
6177		if (typeof $iframe !== 'undefined') {
6178			if ($iframe.prop('width')) { 
6179				width = $iframe.prop('width');			
6180				if ($iframe.prop('height')) { 
6181					height = $iframe.prop('height');
6182					this.resizePlayer(width,height); 
6183				}
6184			}
6185		}
6186	};
6187
6188	AblePlayer.prototype.getYouTubeCaptionTracks = function (youTubeId) {
6189
6190		// get data via YouTube IFrame Player API, and push data to this.tracks & this.captions
6191		// NOTE: Caption tracks are not available through the IFrame Player API 
6192		// until AFTER the video has started playing. 
6193		// Therefore, this function plays the video briefly in order to load the captions module
6194		// then stops the video and collects the data needed to build the cc menu 
6195		// This is stupid, but seemingly unavoidable. 
6196		// Caption tracks could be obtained through the YouTube Data API 
6197		// but this required authors to have a Google API key, 
6198		// which would complicate Able Player installation 
6199
6200		var deferred = new $.Deferred();
6201		var promise = deferred.promise();
6202
6203		var thisObj, ytTracks, i, trackLang, trackLabel, isDefaultTrack;
6204
6205		thisObj = this;
6206		
vendor: 4,768 bytes, lines 6207-6331
6207		if (!this.youTubePlayer.getOption('captions','tracklist')) { 
6208
6209			// no tracks were found, probably because the captions module hasn't loaded  
6210			// play video briefly (required in order to load the captions module) 
6211			// and after the apiChange event is triggered, try again to retreive tracks
6212			this.youTubePlayer.addEventListener('onApiChange',function(x) { 
6213
6214				// getDuration() also requires video to play briefly 
6215				// so, let's set that while we're here 				
6216				thisObj.duration = thisObj.youTubePlayer.getDuration();				
6217
6218				if (thisObj.loadingYouTubeCaptions) { 				
6219					// loadingYouTubeCaptions is a stopgap in case onApiChange is called more than once 
6220					ytTracks = thisObj.youTubePlayer.getOption('captions','tracklist');					
6221					if (!thisObj.okToPlay) { 
6222						// Don't stopVideo() - that cancels loading 
6223						// Just pause 
6224						// No need to seekTo(0) - so little time has passed it isn't noticeable to the user 
6225						thisObj.youTubePlayer.pauseVideo(); 
6226					}
6227					if (ytTracks && ytTracks.length) { 
6228						// Step through ytTracks and add them to global tracks array
6229						// Note: Unlike YouTube Data API, the IFrame Player API only returns 
6230						// tracks that are published, and does NOT include ASR captions 
6231						// So, no additional filtering is required 
6232						for (i=0; i < ytTracks.length; i++) {
6233							trackLang = ytTracks[i].languageCode; 
6234							trackLabel = ytTracks[i].languageName; // displayName and languageName seem to always have the same value
6235							isDefaultTrack = false; 
6236							if (typeof thisObj.captionLang !== 'undefined') { 
6237								if (trackLang === thisObj.captionLang) {
6238									isDefaultTrack = true;						
6239								}
6240							}
6241							else if (typeof thisObj.lang !== 'undefined') { 
6242								if (trackLang === thisObj.lang) {
6243									isDefaultTrack = true;						
6244								}
6245							}
6246							thisObj.tracks.push({
6247								'kind': 'captions',
6248								'language': trackLang,
6249								'label': trackLabel,
6250								'def': isDefaultTrack
6251							});
6252							thisObj.captions.push({
6253								'language': trackLang,
6254								'label': trackLabel,
6255								'def': isDefaultTrack,
6256								'cues': null
6257							});							
6258						}
6259						thisObj.hasCaptions = true;
6260						// setupPopups again with new captions array, replacing original
6261						thisObj.setupPopups('captions');				
6262					}
6263					else { 
6264						// there are no YouTube captions 
6265						thisObj.usingYouTubeCaptions = false; 
6266						thisObj.hasCaptions = false;
6267					}
6268					thisObj.loadingYouTubeCaptions = false; 
6269					if (thisObj.okToPlay) { 
6270						thisObj.youTubePlayer.playVideo();
6271					}
6272				}
6273				if (thisObj.captionLangPending) { 
6274					// user selected a new caption language prior to playback starting 
6275					// set it now 
6276					thisObj.youTubePlayer.setOption('captions', 'track', {'languageCode': thisObj.captionLangPending});
6277					thisObj.captionLangPending = null; 
6278				}
6279				if (typeof thisObj.prefCaptionsSize !== 'undefined') { 
6280					// set the default caption size 
6281					// this doesn't work until the captions module is loaded 
6282					thisObj.youTubePlayer.setOption('captions','fontSize',thisObj.translatePrefs('size',thisObj.prefCaptionsSize,'youtube'));
6283				}
6284				deferred.resolve();
6285			});
6286			// Trigger the above event listener by briefly playing the video 		
6287			this.loadingYouTubeCaptions = true; 	
6288			this.youTubePlayer.playVideo();		
6289		}
6290		return promise;
6291	};
6292
6293	AblePlayer.prototype.getYouTubeTimedTextUrl = function (youTubeId, trackName, trackLang) {
6294
6295		// return URL for retrieving WebVTT source via YouTube's timedtext API
6296		// Note: This API seems to be undocumented, and could break anytime
6297		// UPDATE: Google removed this API on November 10, 2021 
6298		// This function is no longer called, but is preserved here for reference 
6299		var url = 'https://www.youtube.com/api/timedtext?fmt=vtt';
6300		url += '&v=' + youTubeId;
6301		url += '&lang=' + trackLang;
6302		// if track has a value in the name field, it's *required* in the URL
6303		if (trackName !== '') {
6304			url += '&name=' + trackName;
6305		}
6306		return url;
6307	};
6308
6309	AblePlayer.prototype.getYouTubePosterUrl = function (youTubeId, width) {
6310
6311			 // return a URL for retrieving a YouTube poster image
6312			 // supported values of width: 120, 320, 480, 640
6313
6314			 var url = 'https://img.youtube.com/vi/' + youTubeId;
6315			 if (width == '120') {
6316				 // default (small) thumbnail, 120 x 90
6317				 return url + '/default.jpg';
6318			 }
6319			 else if (width == '320') {
6320				 // medium quality thumbnail, 320 x 180
6321				 return url + '/hqdefault.jpg';
6322			 }
6323			 else if (width == '480') {
6324				 // high quality thumbnail, 480 x 360
6325				 return url + '/hqdefault.jpg';
6326			 }
6327			 else if (width == '640') {
6328				 // standard definition poster image, 640 x 480
6329				 return url + '/sddefault.jpg';
6330			 }
6331			 return false;
6332	};
6333
6334	AblePlayer.prototype.getYouTubeId = function (url) {
6335
6336		// return a YouTube ID, extracted from a full YouTube URL
6337		// Supported URL patterns (with http or https): 
6338		// https://youtu.be/xxx
6339		// https://www.youtube.com/watch?v=xxx
6340		// https://www.youtube.com/embed/xxx
6341
6342		// in all supported patterns, the id is the last 11 characters 
6343		var idStartPos, id; 
6344
6345		if (url.indexOf('youtu') !== -1) { 
6346			// this is a full Youtube URL 
6347			url = url.trim(); 
6348			idStartPos = url.length - 11; 
6349			id = url.substring(idStartPos); 
6350			return id; 
6351		}
6352		else { 
6353			return url; 
6354		}
6355};
6356
6357})(jQuery);
6358
6359(function ($) {
6360
6361
6362	// Events:
6363	//	 startTracking(event, position)
6364	//	 tracking(event, position)
6365	//	 stopTracking(event, position)
6366
6367	window. AccessibleSlider = function(mediaType, div, orientation, length, min, max, bigInterval, label, className, trackingMedia, initialState) {
6368
6369		// mediaType is either 'audio' or 'video'
6370		// div is the host element around which the slider will be built
6371		// orientation is either 'horizontal' or 'vertical'
6372		// length is the width or height of the slider, depending on orientation
6373		// min is the low end of the slider scale
6374		// max is the high end of the slider scale
6375		// bigInterval is the number of steps supported by page up/page down (set to 0 if not supported)
6376		// (smallInterval, defined as nextStep below, is always set to 1) - this is the interval supported by arrow keys
6377		// label is used within an aria-label attribute to identify the slider to screen reader users
6378		// className is used as the root within class names (e.g., 'able-' + classname + '-head')
6379		// trackingMedia is true if this is a media timeline; otherwise false
6380		// initialState is either 'visible' or 'hidden'
6381
6382		var thisObj, coords;
6383
6384		thisObj = this;
6385
6386		// Initialize some variables.
6387		this.position = 0; // Note: position does not change while tracking.
6388		this.tracking = false;
6389		this.trackDevice = null; // 'mouse' or 'keyboard'
6390		this.keyTrackPosition = 0;
6391		this.lastTrackPosition = 0;
6392		this.nextStep = 1;
6393		this.inertiaCount = 0;
6394
6395		this.bodyDiv = $(div);
6396
6397		// Add divs for tracking amount of media loaded and played
6398		if (trackingMedia) {
6399			this.loadedDiv = $('<div></div>');
6400			this.playedDiv = $('<div></div>');
6401		}
6402
6403		// Add a seekhead
6404		this.seekHead = $('<div>',{
6405			'aria-orientation': orientation,
6406			'class': 'able-' + className + '-head'
6407		});
6408
6409		if (initialState === 'visible') {
6410			this.seekHead.attr('tabindex', '0');
6411		}
6412		else {
6413			this.seekHead.attr('tabindex', '-1');
6414		}
6415		// Since head is focusable, it gets the aria roles/titles.
6416		this.seekHead.attr({
6417			'role': 'slider',
6418			'aria-label': label,
6419			'aria-valuemin': min,
6420			'aria-valuemax': max
6421		});
6422
6423		this.timeTooltipTimeoutId = null;
6424		this.overTooltip = false;
6425		this.timeTooltip = $('<div>');
6426		this.bodyDiv.append(this.timeTooltip);
6427
6428		this.timeTooltip.attr('role', 'tooltip');
6429		this.timeTooltip.addClass('able-tooltip');
6430		this.timeTooltip.on('mouseenter focus', function(){
6431			thisObj.overTooltip = true;
6432			clearInterval(thisObj.timeTooltipTimeoutId);
6433		});
6434		this.timeTooltip.on('mouseleave blur', function(){
6435			thisObj.overTooltip = false;
6436			$(this).hide();
6437		});
6438		this.timeTooltip.hide();
6439
6440		this.bodyDiv.append(this.loadedDiv);
6441		this.bodyDiv.append(this.playedDiv);
6442		this.bodyDiv.append(this.seekHead);
6443
6444		this.bodyDiv.wrap('<div></div>');
6445		this.wrapperDiv = this.bodyDiv.parent();
6446
6447		if (this.skin === 'legacy') {
6448			if (orientation === 'horizontal') {
6449				this.wrapperDiv.width(length);
6450				this.loadedDiv.width(0);
6451			}
6452			else {
6453				this.wrapperDiv.height(length);
6454				this.loadedDiv.height(0);
6455			}
6456		}
6457		this.wrapperDiv.addClass('able-' + className + '-wrapper');
6458
6459		if (trackingMedia) {
6460			this.loadedDiv.addClass('able-' + className + '-loaded');
6461
6462			this.playedDiv.width(0);
6463			this.playedDiv.addClass('able-' + className + '-played');
6464
6465			// Set a default duration. User can call this dynamically if duration changes.
6466			this.setDuration(max);
6467		}
6468
6469		// handle seekHead events
6470		this.seekHead.on('mouseenter mouseleave mousemove mousedown mouseup focus blur touchstart touchmove touchend', function (e) {
6471
6472			coords = thisObj.pointerEventToXY(e);
6473
6474			if (e.type === 'mouseenter' || e.type === 'focus') {
6475				thisObj.overHead = true;
6476			}
6477			else if (e.type === 'mouseleave' || e.type === 'blur') {
6478				thisObj.overHead = false;
vendor: 1,931 bytes, lines 6479-6534
6479				if (!thisObj.overBody && thisObj.tracking && thisObj.trackDevice === 'mouse') {
6480					thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
6481				}
6482			}
6483			else if (e.type === 'mousemove' || e.type === 'touchmove') {
6484				if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
6485					thisObj.trackHeadAtPageX(coords.x);
6486				}
6487			}
6488			else if (e.type === 'mousedown' || e.type === 'touchstart') {
6489				thisObj.startTracking('mouse', thisObj.pageXToPosition(thisObj.seekHead.offset() + (thisObj.seekHead.width() / 2)));
6490				if (!thisObj.bodyDiv.is(':focus')) {
6491					thisObj.bodyDiv.focus();
6492				}
6493				e.preventDefault();
6494			}
6495			else if (e.type === 'mouseup' || e.type === 'touchend') {
6496				if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
6497					thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
6498				}
6499			}
6500			if (e.type !== 'mousemove' && e.type !== 'mousedown' && e.type !== 'mouseup' && e.type !== 'touchstart' && e.type !== 'touchend') {
6501				thisObj.refreshTooltip();
6502			}
6503		});
6504
6505		// handle bodyDiv events
6506		this.bodyDiv.on(
6507			'mouseenter mouseleave mousemove mousedown mouseup keydown keyup touchstart touchmove touchend', function (e) {
6508
6509			coords = thisObj.pointerEventToXY(e);
6510
6511			if (e.type === 'mouseenter') {
6512				thisObj.overBody = true;
6513				thisObj.overBodyMousePos = {
6514					x: coords.x,
6515					y: coords.y
6516				};
6517			}
6518			else if (e.type === 'mouseleave') {
6519				thisObj.overBody = false;
6520				thisObj.overBodyMousePos = null;
6521				if (!thisObj.overHead && thisObj.tracking && thisObj.trackDevice === 'mouse') {
6522					thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
6523				}
6524			}
6525			else if (e.type === 'mousemove' || e.type === 'touchmove') {
6526				thisObj.overBodyMousePos = {
6527					x: coords.x,
6528					y: coords.y
6529				};
6530				if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
6531					thisObj.trackHeadAtPageX(coords.x);
6532				}
6533			}
6534			else if (e.type === 'mousedown' || e.type === 'touchstart') {
6535				thisObj.startTracking('mouse', thisObj.pageXToPosition(coords.x));
6536				thisObj.trackHeadAtPageX(coords.x);
6537				if (!thisObj.seekHead.is(':focus')) {
6538					thisObj.seekHead.focus();
6539				}
6540				e.preventDefault();
6541			}
6542			else if (e.type === 'mouseup' || e.type === 'touchend') {
6543				if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
6544					thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
6545				}
6546			}
6547			else if (e.type === 'keydown') {
6548				// Home
6549				if (e.which === 36) {
6550					thisObj.trackImmediatelyTo(0);
6551				}
6552				// End
6553				else if (e.which === 35) {
6554					thisObj.trackImmediatelyTo(thisObj.duration);
6555				}
6556				// Left arrow or down arrow
6557				else if (e.which === 37 || e.which === 40) {
6558					thisObj.arrowKeyDown(-1);
6559				}
6560				// Right arrow or up arrow
6561				else if (e.which === 39 || e.which === 38) {
6562					thisObj.arrowKeyDown(1);
6563				}
6564				// Page up
6565				else if (e.which === 33 && bigInterval > 0) {
6566					thisObj.arrowKeyDown(bigInterval);
6567				}
6568				// Page down
6569				else if (e.which === 34 && bigInterval > 0) {
6570					thisObj.arrowKeyDown(-bigInterval);
6571				}
6572				else {
6573					return;
6574				}
6575				e.preventDefault();
6576			}
6577			else if (e.type === 'keyup') {
6578				if (e.which >= 33 && e.which <= 40) {
6579					if (thisObj.tracking && thisObj.trackDevice === 'keyboard') {
6580						thisObj.stopTracking(thisObj.keyTrackPosition);
6581					}
6582					e.preventDefault();
6583				}
6584			}
6585			if (!thisObj.overTooltip && e.type !== 'mouseup' && e.type !== 'keydown' && e.type !== 'keydown') {
6586				thisObj.refreshTooltip();
6587			}
6588		});
6589	}
6590
6591	AccessibleSlider.prototype.arrowKeyDown = function (multiplier) {
6592		if (this.tracking && this.trackDevice === 'keyboard') {
6593			this.keyTrackPosition = this.boundPos(this.keyTrackPosition + (this.nextStep * multiplier));
6594			this.inertiaCount += 1;
6595			if (this.inertiaCount === 20) {
6596				this.inertiaCount = 0;
6597				this.nextStep *= 2;
6598			}
6599			this.trackHeadAtPosition(this.keyTrackPosition);
6600		}
6601		else {
6602			this.nextStep = 1;
6603			this.inertiaCount = 0;
6604			this.keyTrackPosition = this.boundPos(this.position + (this.nextStep * multiplier));
6605			this.startTracking('keyboard', this.keyTrackPosition);
6606			this.trackHeadAtPosition(this.keyTrackPosition);
6607		}
6608	};
6609/*
6610	AccessibleSlider.prototype.pageUp = function (multiplier) {
6611		if (this.tracking && this.trackDevice === 'keyboard') {
6612			this.keyTrackPosition = this.boundPos(this.keyTrackPosition + (this.nextStep * multiplier));
6613			this.inertiaCount += 1;
6614			if (this.inertiaCount === 20) {
6615				this.inertiaCount = 0;
6616				this.nextStep *= 2;
6617			}
6618			this.trackHeadAtPosition(this.keyTrackPosition);
6619		}
6620		else {
6621			this.nextStep = 1;
6622			this.inertiaCount = 0;
6623			this.keyTrackPosition = this.boundPos(this.position + (this.nextStep * multiplier));
6624			this.startTracking('keyboard', this.keyTrackPosition);
6625			this.trackHeadAtPosition(this.keyTrackPosition);
6626		}
6627	};
6628*/
6629	AccessibleSlider.prototype.pageXToPosition = function (pageX) {
6630		var offset = pageX - this.bodyDiv.offset().left;
6631		var position = this.duration * (offset / this.bodyDiv.width());
6632		return this.boundPos(position);
6633	};
6634
6635	AccessibleSlider.prototype.boundPos = function (position) {
6636		return Math.max(0, Math.min(position, this.duration));
6637	}
6638
6639	AccessibleSlider.prototype.setDuration = function (duration) {
6640		if (duration !== this.duration) {
6641			this.duration = duration;
6642			this.resetHeadLocation();
6643			this.seekHead.attr('aria-valuemax', duration);
6644		}
6645	};
6646
6647	AccessibleSlider.prototype.setWidth = function (width) {
6648		this.wrapperDiv.width(width);
6649		this.resizeDivs();
6650		this.resetHeadLocation();
6651	};
6652
6653	AccessibleSlider.prototype.getWidth = function () {
6654		return this.wrapperDiv.width();
6655	};
6656
6657	AccessibleSlider.prototype.resizeDivs = function () {
6658		this.playedDiv.width(this.bodyDiv.width() * (this.position / this.duration));
6659		this.loadedDiv.width(this.bodyDiv.width() * this.buffered);
6660	};
6661
6662	// Stops tracking, sets the head location to the current position.
6663	AccessibleSlider.prototype.resetHeadLocation = function () {
6664		var ratio = this.position / this.duration;
6665		var center = this.bodyDiv.width() * ratio;
6666		this.seekHead.css('left', center - (this.seekHead.width() / 2));
6667
6668		if (this.tracking) {
6669			this.stopTracking(this.position);
6670		}
6671	};
6672
6673	AccessibleSlider.prototype.setPosition = function (position, updateLive) {
6674		this.position = position;
6675		this.resetHeadLocation();
6676		if (this.overHead) {
6677			this.refreshTooltip();
6678		}
6679		this.resizeDivs();
6680		this.updateAriaValues(position, updateLive);
6681	}
6682
6683	// TODO: Native HTML5 can have several buffered segments, and this actually happens quite often.	Change this to display them all.
6684	AccessibleSlider.prototype.setBuffered = function (ratio) {
vendor: 5,641 bytes, lines 6685-6864
6685		if (!isNaN(ratio)) {
6686			this.buffered = ratio;
6687			this.redrawDivs;
6688		}
6689	}
6690
6691	AccessibleSlider.prototype.startTracking = function (device, position) {
6692		if (!this.tracking) {
6693			this.trackDevice = device;
6694			this.tracking = true;
6695			this.bodyDiv.trigger('startTracking', [position]);
6696		}
6697	};
6698
6699	AccessibleSlider.prototype.stopTracking = function (position) {
6700		this.trackDevice = null;
6701		this.tracking = false;
6702		this.bodyDiv.trigger('stopTracking', [position]);
6703		this.setPosition(position, true);
6704	};
6705
6706	AccessibleSlider.prototype.trackHeadAtPageX = function (pageX) {
6707		var position = this.pageXToPosition(pageX);
6708		var newLeft = pageX - this.bodyDiv.offset().left - (this.seekHead.width() / 2);
6709		newLeft = Math.max(0, Math.min(newLeft, this.bodyDiv.width() - this.seekHead.width()));
6710		this.lastTrackPosition = position;
6711		this.seekHead.css('left', newLeft);
6712		this.reportTrackAtPosition(position);
6713	};
6714
6715	AccessibleSlider.prototype.trackHeadAtPosition = function (position) {
6716		var ratio = position / this.duration;
6717		var center = this.bodyDiv.width() * ratio;
6718		this.lastTrackPosition = position;
6719		this.seekHead.css('left', center - (this.seekHead.width() / 2));
6720		this.reportTrackAtPosition(position);
6721	};
6722
6723	AccessibleSlider.prototype.reportTrackAtPosition = function (position) {
6724		this.bodyDiv.trigger('tracking', [position]);
6725		this.updateAriaValues(position, true);
6726	};
6727
6728	AccessibleSlider.prototype.updateAriaValues = function (position, updateLive) {
6729		// TODO: Localize, move to another function.
6730		var pHours = Math.floor(position / 3600);
6731		var pMinutes = Math.floor((position % 3600) / 60);
6732		var pSeconds = Math.floor(position % 60);
6733
6734		var pHourWord = pHours === 1 ? 'hour' : 'hours';
6735		var pMinuteWord = pMinutes === 1 ? 'minute' : 'minutes';
6736		var pSecondWord = pSeconds === 1 ? 'second' : 'seconds';
6737
6738		var descriptionText;
6739		if (pHours > 0) {
6740			descriptionText = pHours +
6741				' ' + pHourWord +
6742				', ' + pMinutes +
6743				' ' + pMinuteWord +
6744				', ' + pSeconds +
6745				' ' + pSecondWord;
6746		}
6747		else if (pMinutes > 0) {
6748			descriptionText	 = pMinutes +
6749				' ' + pMinuteWord +
6750				', ' + pSeconds +
6751				' ' + pSecondWord;
6752		}
6753		else {
6754			descriptionText = pSeconds + ' ' + pSecondWord;
6755		}
6756
6757		/* Comment to stop live region from generating or being used. */
6758		if (!this.liveAriaRegion) {
6759			this.liveAriaRegion = $('<span>', {
6760				'class': 'able-offscreen',
6761				'aria-live': 'polite'
6762			});
6763			this.wrapperDiv.append(this.liveAriaRegion);
6764		}
6765		if (updateLive && (this.liveAriaRegion.text() !== descriptionText)) {
6766			this.liveAriaRegion.text(descriptionText);
6767		}
6768
6769		// Uncomment the following lines to use aria values instead of separate live region.
6770		this.seekHead.attr('aria-valuetext', descriptionText);
6771		this.seekHead.attr('aria-valuenow', Math.floor(position).toString());
6772	};
6773
6774	AccessibleSlider.prototype.trackImmediatelyTo = function (position) {
6775		this.startTracking('keyboard', position);
6776		this.trackHeadAtPosition(position);
6777		this.keyTrackPosition = position;
6778	};
6779
6780	AccessibleSlider.prototype.refreshTooltip = function () {
6781		if (this.overHead) {
6782			this.timeTooltip.show();
6783			if (this.tracking) {
6784				this.timeTooltip.text(this.positionToStr(this.lastTrackPosition));
6785			}
6786			else {
6787				this.timeTooltip.text(this.positionToStr(this.position));
6788			}
6789			this.setTooltipPosition(this.seekHead.position().left + (this.seekHead.width() / 2));
6790		}
6791		else if (this.overBody && this.overBodyMousePos) {
6792			this.timeTooltip.show();
6793			this.timeTooltip.text(this.positionToStr(this.pageXToPosition(this.overBodyMousePos.x)));
6794			this.setTooltipPosition(this.overBodyMousePos.x - this.bodyDiv.offset().left);
6795		}
6796		else {
6797
6798			clearTimeout(this.timeTooltipTimeoutId);
6799			var _this = this;
6800			this.timeTooltipTimeoutId = setTimeout(function() {
6801				// give user a half second move cursor over tooltip
6802				_this.timeTooltip.hide();
6803			}, 500);
6804		}
6805	};
6806
6807	AccessibleSlider.prototype.hideSliderTooltips = function () {
6808		this.overHead = false;
6809		this.overBody = false;
6810		this.timeTooltip.hide();
6811	};
6812
6813	AccessibleSlider.prototype.setTooltipPosition = function (x) {
6814		this.timeTooltip.css({
6815			left: x - (this.timeTooltip.width() / 2) - 10,
6816			bottom: this.seekHead.height() + 10
6817		});
6818	};
6819
6820	AccessibleSlider.prototype.positionToStr = function (seconds) {
6821
6822		// same logic as misc.js > formatSecondsAsColonTime()
6823		var dHours = Math.floor(seconds / 3600);
6824		var dMinutes = Math.floor(seconds / 60) % 60;
6825		var dSeconds = Math.floor(seconds % 60);
6826		if (dSeconds < 10) {
6827			dSeconds = '0' + dSeconds;
6828		}
6829		if (dHours > 0) {
6830			if (dMinutes < 10) {
6831				dMinutes = '0' + dMinutes;
6832			}
6833			return dHours + ':' + dMinutes + ':' + dSeconds;
6834		}
6835		else {
6836			return dMinutes + ':' + dSeconds;
6837		}
6838	};
6839
6840	AccessibleSlider.prototype.pointerEventToXY = function(e) {
6841
6842		// returns array of coordinates x and y in response to both mouse and touch events
6843		// for mouse events, this comes from e.pageX and e.pageY
6844		// for touch events, it's a bit more complicated
6845		var out = {x:0, y:0};
6846		if (e.type == 'touchstart' || e.type == 'touchmove' || e.type == 'touchend' || e.type == 'touchcancel') {
6847			var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
6848			out.x = touch.pageX;
6849			out.y = touch.pageY;
6850		}
6851		else if (e.type == 'mousedown' || e.type == 'mouseup' || e.type == 'mousemove' || e.type == 'mouseover'|| e.type=='mouseout' || e.type=='mouseenter' || e.type=='mouseleave') {
6852			out.x = e.pageX;
6853			out.y = e.pageY;
6854		}
6855		return out;
6856	};
6857
6858})(jQuery);
6859
6860(function ($) {
6861
6862	AblePlayer.prototype.addVolumeSlider = function($div) {
6863
6864		// Prior to v4.4.64, we were using a custom-build vertical volunme slider 
6865		// Changed to input type="range" because it's standard and gaining more widespread support 
6866		// including screen reader support  
6867		// TODO: Improve presentation of vertical slider. That requires some CSS finesse.  
6868
6869		var thisObj, volumeSliderId, volumeHelpId, volumePct, tickLabelsId, $tickLabels, i, $tickOption, tickLabel;
6870
6871		thisObj = this;
6872
6873		// define a few variables
6874		volumeSliderId = this.mediaId + '-volume-slider';
6875		volumeHelpId = this.mediaId + '-volume-help';
6876		tickLabelsId = this.mediaId + '-volume-tick-labels';
6877
6878		this.$volumeSlider = $('<div>',{
6879			'id': volumeSliderId,
6880			'class': 'able-volume-slider',
6881			'aria-hidden': 'true'
6882		}).hide();
6883		this.$volumeSliderTooltip = $('<div>',{
6884			'class': 'able-tooltip',
6885			'role': 'tooltip'
6886		}).hide();
6887		this.$volumeRange = $('<input>',{ 
6888			'type': 'range',
6889			'min': '0',
6890			'max': '10',
6891			'step': '1',
6892			'orient': 'vertical', // non-standard, but required for Firefox 
6893			'aria-label': this.tt.volumeUpDown,
6894			'value': this.volume
6895			// 'list': tickLabelsId // Uncomment this to use tickLabels (see note below)
6896		});
6897		volumePct = parseInt(thisObj.volume) / 10 * 100;
6898		this.$volumeHelp = $('<div>',{
6899			'id': volumeHelpId,
6900			'class': 'able-volume-help',
6901			'aria-live': 'polite'
6902		}).text(volumePct + '%');
6903		this.$volumeButton.attr({
6904			'aria-describedby': volumeHelpId
6905		});
6906		$tickLabels = $('<datalist>',{
6907			'id': tickLabelsId
6908		}); 
6909		for (i = 0; i <= 10; i++) { 
6910			if (i === 0) { 
6911				tickLabel = this.tt.mute; 
6912			}
6913			else { 
6914				tickLabel = (i * 10) + '%'; 
6915			}
6916			$tickOption = $('<option>',{ 
6917				'value': i, 
6918				'label': tickLabel
6919			})
6920			$tickLabels.append($tickOption); 
6921		}		
6922		this.$volumeSlider.append(this.$volumeSliderTooltip,this.$volumeRange,this.$volumeHelp);
6923		// To add $tickLabels, use the following line of code to replace the one above 
6924		// and uncommnet the 'list' property in the definition of this.$volumeRange above 
6925		// As of Nov 2022, this feature is not supported by any screen reader 
6926		// this.$volumeSlider.append(this.$volumeSliderTooltip,this.$volumeRange,this.$volumeHelp,$tickLabels);
6927
6928		$div.append(this.$volumeSlider);
6929
6930		// add event listeners
6931		this.$volumeRange.on('change',function (e) {
6932			thisObj.handleVolumeChange($(this).val());
6933		});
6934
6935		this.$volumeRange.on('input',function (e) {
6936			thisObj.handleVolumeChange($(this).val());
6937		});
6938			
6939		this.$volumeRange.on('keydown',function (e) {
6940
6941			// Escape key or Enter key or Tab key
6942			if (e.which === 27 || e.which === 13 || e.which === 9) {
6943				// close popup
6944				if (thisObj.$volumeSlider.is(':visible')) {
6945					thisObj.closingVolume = true; // stopgap
6946					thisObj.hideVolumePopup();
6947				}
6948				else {
6949					if (!thisObj.closingVolume) {
6950						thisObj.showVolumePopup();
6951					}
6952				}
6953			}
6954			else {
6955				return;
6956			}
6957		});
6958	};
6959
6960	AblePlayer.prototype.refreshVolumeHelp = function(volume) {
6961
6962		// make adjustments based on current volume
6963		var volumePct;
6964		volumePct = (volume/10) * 100;
6965
6966		// Update help text 
6967		if (this.$volumeHelp) { 
6968			this.$volumeHelp.text(volumePct + '%'); 
6969		}
6970
6971		// Update the default value of the volume slider input field 
6972		// This doesn't seem to be necessary; browsers remember the previous setting during a session 
6973		// but this is a fallback in case they don't 
6974		this.$volumeRange.attr('value',volume);   
6975	};
6976
6977	AblePlayer.prototype.refreshVolumeButton = function(volume) {
6978
6979		var volumeName, volumePct, volumeLabel, volumeIconClass, volumeImg, newSvgData;
6980
6981		volumeName = this.getVolumeName(volume);
6982		volumePct = (volume/10) * 100;
6983		volumeLabel = this.tt.volume + ' ' + volumePct + '%';
6984
6985		if (this.iconType === 'font') {
6986			volumeIconClass = 'icon-volume-' + volumeName;
6987			this.$volumeButton.find('span').first().removeClass().addClass(volumeIconClass);
6988			this.$volumeButton.find('span.able-clipped').text(volumeLabel);
6989		}
6990		else if (this.iconType === 'image') {
6991			volumeImg = this.imgPath + 'volume-' + volumeName + '.png';
6992			this.$volumeButton.find('img').attr('src',volumeImg);
6993		}
6994		else if (this.iconType === 'svg') {
6995			if (volumeName !== 'mute') {
6996				volumeName = 'volume-' + volumeName;
6997			}
vendor: 4,996 bytes, lines 6998-7206
6998			newSvgData = this.getSvgData(volumeName);
6999			this.$volumeButton.find('svg').attr('viewBox',newSvgData[0]);
7000			this.$volumeButton.find('path').attr('d',newSvgData[1]);
7001		}
7002	};
7003
7004	AblePlayer.prototype.handleVolumeButtonClick = function() {
7005
7006		if (this.$volumeSlider.is(':visible')) {
7007			this.hideVolumePopup();			
7008		}
7009		else {
7010			this.showVolumePopup();
7011		}
7012	}; 
7013
7014	AblePlayer.prototype.handleVolumeKeystroke = function(keycode) {
7015
7016		// keycode is an ASCII key code 49-57 (numeric keys 1-9), 
7017		// keyboard shortcuts for changing volume 
7018
7019		var volume; 
7020
7021		if (keycode >= 49 && keycode <= 57) {
7022			volume = keycode - 48;
7023		}
7024		else { 
7025			return false; 
7026		}
7027
7028		if (this.isMuted() && volume > 0) {
7029			this.setMute(false);
7030		}
7031		else if (volume === 0) {
7032			this.setMute(true);
7033		}
7034		else {
7035			this.setVolume(volume); // this.volume will be updated after volumechange event fires (event.js)
7036			this.refreshVolumeHelp(volume);
7037			this.refreshVolumeButton(volume);
7038		}
7039	}; 
7040
7041
7042	AblePlayer.prototype.handleVolumeChange = function(volume) {
7043
7044		// handle volume change using the volume input slider  
7045
7046		if (this.isMuted() && volume > 0) {
7047			this.setMute(false);
7048		}
7049		else if (volume === 0) {
7050			this.setMute(true);
7051		}
7052		else {
7053			this.setVolume(volume); // this.volume will be updated after volumechange event fires (event.js)
7054			this.refreshVolumeHelp(volume);
7055			this.refreshVolumeButton(volume);
7056		}
7057	};
7058
7059	AblePlayer.prototype.handleMute = function() {
7060
7061		if (this.isMuted()) {
7062			this.setMute(false);
7063		}
7064		else {
7065			this.setMute(true);
7066		}
7067	};
7068
7069	AblePlayer.prototype.showVolumePopup = function() {
7070
7071		this.closePopups();
7072		this.$tooltipDiv.hide();
7073		this.$volumeSlider.show().attr('aria-hidden','false');
7074		this.$volumeButton.attr('aria-expanded','true');
7075		this.$volumeButton.focus(); // for screen reader expanded state to be read
7076		this.waitThenFocus(this.$volumeRange);
7077	};
7078
7079	AblePlayer.prototype.hideVolumePopup = function() {
7080
7081		var thisObj = this;
7082
7083		this.$volumeSlider.hide().attr('aria-hidden','true');
7084		this.$volumeButton.attr('aria-expanded','false').focus();
7085		// wait a second before resetting stopgap var
7086		// otherwise the keypress used to close volume popup will trigger the volume button
7087		setTimeout(function() {
7088			thisObj.closingVolume = false;
7089		}, 1000);
7090	};
7091
7092	AblePlayer.prototype.isMuted = function () {
7093
7094		if (this.player === 'html5') {
7095			return this.media.muted;
7096		}
7097		else if (this.player === 'youtube') {
7098			return this.youTubePlayer.isMuted();
7099		}
7100	};
7101
7102	AblePlayer.prototype.setMute = function(mute) {
7103
7104		// mute is either true (muting) or false (unmuting)
7105		if (mute) {
7106			// save current volume so it can be restored after unmute
7107			this.lastVolume = this.volume;
7108			this.volume = 0;
7109		}
7110		else { // restore to previous volume
7111			if (typeof this.lastVolume !== 'undefined') {
7112				this.volume = this.lastVolume;
7113			}
7114		}
7115
7116		if (this.player === 'html5') {
7117			this.media.muted = mute;
7118		}
7119		else if (this.player === 'youtube') {
7120			if (mute) {
7121				this.youTubePlayer.mute();
7122			}
7123			else {
7124				this.youTubePlayer.unMute();
7125			}
7126		}
7127		this.setVolume(this.volume); 
7128		this.refreshVolumeHelp(this.volume);
7129		this.refreshVolumeButton(this.volume);
7130	};
7131
7132	AblePlayer.prototype.setVolume = function (volume) {
7133
7134		// volume is 1 to 10
7135		// convert as needed depending on player
7136
7137		var newVolume; 
7138
7139		if (this.player === 'html5') {
7140			// volume is 0 to 1
7141			newVolume = volume / 10; 
7142			this.media.volume = newVolume;
7143			
7144			if (this.hasSignLanguage && this.signVideo) {
7145				this.signVideo.volume = 0; // always mute
7146			}
7147		}
7148		else if (this.player === 'youtube') {
7149			// volume is 0 to 100
7150			newVolume = volume * 10; 
7151			this.youTubePlayer.setVolume(newVolume);
7152			this.volume = volume;
7153		}
7154		else if (this.player === 'vimeo') {
7155			// volume is 0 to 1
7156			newVolume = volume / 10; 
7157			this.vimeoPlayer.setVolume(newVolume).then(function() {
7158				// setVolume finished.
7159				// could do something here
7160				// successful completion also fires a 'volumechange' event (see event.js)
7161			});
7162		}
7163		this.lastVolume = volume;
7164	};
7165
7166	AblePlayer.prototype.getVolume = function (volume) {
7167
7168		// return volume using common audio control scale 1 to 10
7169		if (this.player === 'html5') {
7170			// uses 0 to 1 scale
7171			return this.media.volume * 10;
7172		}
7173		else if (this.player === 'youtube') {
7174			// uses 0 to 100 scale
7175			if (this.youTubePlayerReady) { 
7176				return this.youTubePlayer.getVolume() / 10;
7177			}
7178		}
7179		if (this.player === 'vimeo') {
7180			// uses 0 to 1 scale
7181			// this.vimeoPlayer.getVolume() takes too long to resolve with a value
7182			// Just use variable that's already been defined (should be the same value anyway)
7183			return this.volume;
7184		}
7185	};
7186
7187	AblePlayer.prototype.getVolumeName = function (volume) {
7188
7189		// returns 'mute','soft','medium', or 'loud' depending on volume level
7190		if (volume == 0) {
7191			return 'mute';
7192		}
7193		else if (volume == 10) {
7194			return 'loud';
7195		}
7196		else if (volume < 5) {
7197			return 'soft';
7198		}
7199		else {
7200			return 'medium';
7201		}
7202	};
7203
7204})(jQuery);
7205
7206(function ($) {
7207	var focusableElementsSelector = "a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]";
7208
7209	// Based on the incredible accessible modal dialog.
7210	window.AccessibleDialog = function(modalDiv, $returnElement, dialogRole, isModal, title, $descDiv, closeButtonLabel, width, fullscreen, escapeHook) {
7211
7212		this.title = title;
7213		this.closeButtonLabel = closeButtonLabel;
7214		this.focusedElementBeforeModal = $returnElement;
7215		this.escapeHook = escapeHook;
7216		this.baseId = $(modalDiv).attr('id') || Math.floor(Math.random() * 1000000000).toString();
7217		var thisObj = this;
7218		var modal = modalDiv;
7219		this.modal = modal;
7220		modal.css({
7221			'width': width || '50%'
7222		});
7223		modal.addClass('able-modal-dialog');
7224
7225		if (!fullscreen) {
7226			var closeButton = $('<button>',{
7227				 'class': 'modalCloseButton',
7228				 'title': thisObj.closeButtonLabel,
7229				 'aria-label': thisObj.closeButtonLabel
7230			}).text('X');
7231			closeButton.keydown(function (e) {
7232				// Space key down
7233				if (e.which === 32) {
7234					thisObj.hide();
7235				}
7236			}).click(function () {
7237				thisObj.hide();
7238			});
7239
7240			var titleH1 = $('<h1></h1>');
7241			titleH1.attr('id', 'modalTitle-' + this.baseId);
7242			titleH1.css('text-align', 'center');
7243			titleH1.text(title);
7244			this.titleH1 = titleH1; 
7245
7246			modal.attr({
7247				'aria-labelledby': 'modalTitle-' + this.baseId,
7248			});
7249			modal.prepend(titleH1);
7250			modal.prepend(closeButton);
7251		}
7252
7253		modal.attr({
7254			'aria-hidden': 'true',
7255			'role': dialogRole,
7256		});
7257		if (isModal) { 
7258			modal.attr('aria-modal','true');
7259		}
7260
7261		modal.keydown(function (e) {
7262			// Escape
7263			if (e.which === 27) {
7264				if (thisObj.escapeHook) {
7265					thisObj.escapeHook(e, this);
7266				}
7267				else {
7268					thisObj.hide();
7269					e.preventDefault();
7270				}
7271			}
7272			// Tab
7273			else if (e.which === 9) {
7274				// Manually loop tab navigation inside the modal.
7275				var parts = modal.find('*');
7276				var focusable = parts.filter(focusableElementsSelector).filter(':visible');
7277
7278				if (focusable.length === 0) {
7279					return;
7280				}
7281
7282				var focused = $(':focus');
7283				var currentIndex = focusable.index(focused);
7284				if (e.shiftKey) {
7285					// If backwards from first element, go to last.
7286					if (currentIndex === 0) {
7287						focusable.get(focusable.length - 1).focus();
7288						e.preventDefault();
7289					}
7290				}
7291				else {
7292					if (currentIndex === focusable.length - 1) {
7293						focusable.get(0).focus();
7294						e.preventDefault();
7295					}
7296				}
7297			}
7298			e.stopPropagation();
7299		});
7300
7301		$('body > *').not('.able-modal-overlay').not('.able-modal-dialog').removeAttr('aria-hidden');
7302	};
7303
7304	AccessibleDialog.prototype.show = function () {
7305		if (!this.overlay) {
7306			// Generate overlay.
7307			var overlay = $('<div></div>').attr({
7308				 'class': 'able-modal-overlay',
7309				 'tabindex': '-1'
7310			});
7311			this.overlay = overlay;
7312			$('body').append(overlay);
7313
7314			// Keep from moving focus out of dialog when clicking outside of it.
7315			overlay.on('mousedown.accessibleModal', function (e) {
7316				e.preventDefault();
7317			});
7318		}
7319
7320		$('body > *').not('.able-modal-overlay').not('.able-modal-dialog').attr('aria-hidden', 'true');
7321
7322		this.overlay.css('display', 'block');
7323		this.modal.css('display', 'block');
7324		this.modal.attr({
7325			'aria-hidden': 'false',
7326			'tabindex': '-1'
7327		});
7328
7329		var focusable = this.modal.find("*").filter(focusableElementsSelector).filter(':visible');
7330		if (focusable.length === 0) {
7331			this.focusedElementBeforeModal.blur();
7332		}
7333		var thisObj = this;
7334		setTimeout(function () {
7335			// set focus on the first focusable element
7336			thisObj.modal.find('button.modalCloseButton').first().focus();
7337		}, 300);
7338	};
7339
7340	AccessibleDialog.prototype.hide = function () {
7341		if (this.overlay) {
7342			this.overlay.css('display', 'none');
7343		}
7344		this.modal.css('display', 'none');
7345		this.modal.attr('aria-hidden', 'true');
7346		$('body > *').not('.able-modal-overlay').not('.able-modal-dialog').removeAttr('aria-hidden');
7347
7348		this.focusedElementBeforeModal.focus();
7349	};
7350
7351	AccessibleDialog.prototype.getInputs = function () {
7352
7353		// return an array of input elements within this dialog
7354		if (this.modal) {
7355			var inputs = this.modal.find('input');
7356			return inputs;
7357		}
7358		return false;
7359	};
7360
7361})(jQuery);
7362
7363(function ($) {
7364
7365	AblePlayer.prototype.getNextHeadingLevel = function($element) {
7366
7367		// Finds the nearest heading in the ancestor tree
7368		// Loops over each parent of the current element until a heading is found
7369		// If multiple headings are found beneath a given parent, get the closest
7370		// Returns an integer (1-6) representing the next available heading level
7371
7372		var $parents, $foundHeadings, numHeadings, headingType, headingNumber;
7373
7374		$parents = $element.parents();
7375		$parents.each(function(){
7376			$foundHeadings = $(this).children(':header');
7377			numHeadings = $foundHeadings.length;
7378			if (numHeadings) {
7379				headingType = $foundHeadings.eq(numHeadings-1).prop('tagName');
7380				return false;
7381			}
7382		});
7383		if (typeof headingType === 'undefined') {
7384			// page has no headings
7385			headingNumber = 1;
7386		}
7387		else {
7388			// Increment closest heading by one if less than 6.
7389			headingNumber = parseInt(headingType[1]);
7390			headingNumber += 1;
7391			if (headingNumber > 6) {
7392				headingNumber = 6;
7393			}
7394		}
7395		return headingNumber;
7396	};
7397
7398	AblePlayer.prototype.countProperties = function(obj) {
7399
7400		// returns the number of properties in an object
7401		var count, prop;
7402		count = 0;
7403		for (prop in obj) {
7404			if (obj.hasOwnProperty(prop)) {
7405				++count;
7406			}
7407		}
7408		return count;
7409	};
7410
7411	AblePlayer.prototype.formatSecondsAsColonTime = function (seconds, showFullTime) {
7412
7413		// Takes seconds and converts to string of form hh:mm:ss
7414		// If showFullTime is true, shows 00 for hours if time is less than an hour
7415		//	 and show milliseconds	(e.g., 00:00:04.123 as in Video Track Sorter)
7416		// Otherwise, omits empty hours and milliseconds (e.g., 00:04 as in timer on controller)
7417
7418		var dHours, dMinutes, dSeconds,
7419				parts, milliSeconds, numShort, i;
7420
7421		if (showFullTime) {
7422			// preserve milliseconds, if included in seconds
7423			parts = seconds.toString().split('.');
7424			if (parts.length === 2) {
7425				milliSeconds = parts[1];
7426				if (milliSeconds.length < 3) {
7427					numShort = 3 - milliSeconds.length;
7428					for (i=1; i <= numShort; i++) {
7429						milliSeconds += '0';
7430					}
7431				}
7432			}
7433			else {
7434				milliSeconds = '000';
7435			}
7436		}
7437		dHours = Math.floor(seconds / 3600);
7438		dMinutes = Math.floor(seconds / 60) % 60;
7439		dSeconds = Math.floor(seconds % 60);
7440		if (dSeconds < 10) {
7441			dSeconds = '0' + dSeconds;
7442		}
7443		if (dHours > 0) {
7444			if (dMinutes < 10) {
7445				dMinutes = '0' + dMinutes;
7446			}
7447			if (showFullTime) {
7448				return dHours + ':' + dMinutes + ':' + dSeconds + '.' + milliSeconds;
7449			}
7450			else {
7451				return dHours + ':' + dMinutes + ':' + dSeconds;
7452			}
7453		}
7454		else {
7455			if (showFullTime) {
7456				if (dHours < 1) {
7457					dHours = '00';
7458				}
7459				else if (dHours < 10) {
7460					dHours = '0' + dHours;
7461				}
7462				if (dMinutes < 1) {
7463					dMinutes = '00';
7464				}
7465				else if (dMinutes < 10) {
7466					dMinutes = '0' + dMinutes;
7467				}
7468				return dHours + ':' + dMinutes + ':' + dSeconds + '.' + milliSeconds;
7469			}
7470			else {
7471				return dMinutes + ':' + dSeconds;
7472			}
7473		}
7474	};
7475
7476	AblePlayer.prototype.getSecondsFromColonTime = function (timeStr) {
7477
7478		// Converts string of form hh:mm:ss to seconds
7479		var timeParts, hours, minutes, seconds, newTime;
7480
7481		timeParts = timeStr.split(':');
7482		if (timeParts.length === 3) {
7483			hours = parseInt(timeParts[0]);
7484			minutes = parseInt(timeParts[1]);
7485			seconds = parseFloat(timeParts[2]);
7486			return ((hours * 3600) + (minutes * 60) + (seconds));
7487		}
7488		else if (timeParts.length === 2) {
7489			minutes = parseInt(timeParts[0]);
7490			seconds = parseFloat(timeParts[1]);
7491			return ((minutes * 60) + (seconds));
7492		}
7493		else if (timeParts.length === 1) {
7494			seconds = parseFloat(timeParts[0]);
7495			return seconds;
7496		}
7497	};
7498
7499	AblePlayer.prototype.capitalizeFirstLetter = function (string) {
7500		return string.charAt(0).toUpperCase() + string.slice(1);
7501	};
7502
7503	AblePlayer.prototype.roundDown = function (value, decimals) {
7504
7505		// round value down to the nearest X decimal points
7506		// where X is the value of the decimals parameter
7507		return Number(Math.floor(value+'e'+decimals)+'e-'+decimals);
7508	};
7509
7510	AblePlayer.prototype.hasAttr = function (object, attribute) {
7511
7512		 // surprisingly, there is no hasAttr() function in Jquery as of 3.2.1
7513		 // return true if object has attribute; otherwise false
7514		 // selector is a Jquery object
7515		 // attribute is a string
7516
7517		var attr = object.attr(attribute);
7518
7519		// For some browsers, `attr` is undefined; for others,
7520		// `attr` is false.	 Check for both.
7521		if (typeof attr !== typeof undefined && attr !== false) {
7522			return true;
7523		}
7524		else {
7525			 return false;
7526		}
7527	};
7528
7529	Number.isInteger = Number.isInteger || function(value) {
7530
7531		// polyfill for IE11, which doesn't otherwise support Number.isInteger
7532		// https://stackoverflow.com/a/31720368/744281
7533		return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
7534	};
7535
7536})(jQuery);
7537
7538(function ($) {
7539	AblePlayer.prototype.initDescription = function() {
7540
7541		// set default mode for delivering description (open vs closed)
7542		// based on availability and user preference
7543
7544		// called when player is being built, or when a user
7545		// toggles the Description button or changes a description-related preference
7546
7547		// The following variables are applicable to delivery of description:
7548		// defaultStateDescriptions == 'on' or 'off', defined by website owner (overridden by prefDesc) 
7549		// prefDesc == 1 if user wants description (i.e., Description button is on); else 0
7550		// prefDescPause == 1 to pause video when description starts; else 0
7551		// prefDescVisible == 1 to visibly show text-based description area; else 0
7552		// prefDescMethod == either 'video' or 'text' (as of v4.0.10, prefDescMethod is always 'video')
7553		// descMethod is the format actually used ('video' or 'text'), regardless of user preference 
7554		// hasOpenDesc == true if a described version of video is available via data-desc-src attribute
7555		// hasClosedDesc == true if a description text track is available
7556		// descOn == true if description of either type is on
7557		// readDescriptionsAloud == true if text description is to be announced audibly; otherwise false
7558		// descReader == either 'browser' or 'screenreader'
7559
7560		var deferred, promise, thisObj;
7561
7562		deferred = new $.Deferred();
7563		promise = deferred.promise();
7564		thisObj = this;
7565
7566		if (this.mediaType === 'audio') { 
7567			deferred.resolve(); 
7568		}
7569
7570		// check to see if there's an open-described version of this video
7571		// checks only the first source since if a described version is provided,
7572		// it must be provided for all sources
7573		this.descFile = this.$sources.first().attr('data-desc-src');
7574		if (typeof this.descFile !== 'undefined') {
7575			this.hasOpenDesc = true;
7576		}
7577		else {
7578			// there's no open-described version via data-desc-src,
7579			// but what about data-youtube-desc-src or data-vimeo-desc-src?
7580			// if these exist, they would have been defined earlier 
7581			if (this.youTubeDescId || this.vimeoDescId) {
7582				this.hasOpenDesc = true;
7583			}
7584			else { // there are no open-described versions from any source
7585				this.hasOpenDesc = false;
7586			}
7587		}
7588
7589		// Set this.descMethod based on media availability & user preferences
7590		if (this.hasOpenDesc && this.hasClosedDesc) {
7591			// both formats are available. User gets their preference. 
7592			if (this.prefDescMethod) { 
7593				this.descMethod = this.prefDescMethod;
7594			}
7595			else { 
7596				// user has no preference. Video is default. 
7597				this.descMethod = 'video'; 
7598			}
7599		}
7600		else if (this.hasOpenDesc) {
7601			this.descMethod = 'video';
7602		}
7603		else if (this.hasClosedDesc) {
7604			this.descMethod = 'text';
7605		}
7606		else { 
7607			// no description is available for this video 
7608			this.descMethod = null; 
7609		}
7610
7611		// Set the default state of descriptions
7612		if (this.descMethod) { 
7613			if (this.prefDesc === 1) { 
7614				this.descOn = true; 
7615			}
7616			else if (this.prefDesc === 0) { 
7617				this.descOn = false; 
7618			}
7619			else { 				
7620				// user has no prefs. Use default state. 
7621				if (this.defaultStateDescriptions === 1)	{ 			
7622					this.descOn = true; 
7623				}
7624				else { 
7625					this.descOn = false; 
7626				}
7627			}
7628		}
7629		else { 			
7630			this.descOn = false;
7631		}
7632		if (typeof this.$descDiv === 'undefined' && this.hasClosedDesc && this.descMethod === 'text') {		
7633			this.injectTextDescriptionArea();
7634		}
7635
7636		if (this.descOn) {
7637			if (this.descMethod === 'video') {
7638				if (!this.usingDescribedVersion()) {
7639					// switched from non-described to described version
7640					this.swapDescription();
7641				}
7642			}
7643			if (this.hasClosedDesc) {
7644				if (this.prefDescVisible) {
7645					// make description text visible
7646					if (typeof this.$descDiv !== 'undefined') {
7647						this.$descDiv.show();
7648						this.$descDiv.removeClass('able-clipped');
7649					}
7650				}
7651				else {
7652					// keep it visible to screen readers, but hide it visibly
7653					if (typeof this.$descDiv !== 'undefined') {
7654						this.$descDiv.addClass('able-clipped');
7655					}
7656				}
7657			}
7658		}
7659		else { // description is off.
7660			if (this.descMethod === 'video') { // user has turned off described version of video
7661				if (this.usingDescribedVersion()) {
7662					// user was using the described verion. Swap for non-described version
7663					this.swapDescription();
7664				}
7665			}
7666			else if (this.descMethod === 'text') { // user has turned off text description
7667				// hide description div from everyone, including screen reader users
7668				if (typeof this.$descDiv !== 'undefined') {
7669					this.$descDiv.hide();
7670					this.$descDiv.removeClass('able-clipped');
7671				}
7672			}
7673		}
7674		deferred.resolve();
7675		return promise; 
7676	};
7677
7678	AblePlayer.prototype.usingDescribedVersion = function () {
7679
7680		// Returns true if currently using audio description, false otherwise.
7681
7682		if (this.player === 'youtube') {
7683			return (this.activeYouTubeId === this.youTubeDescId);
7684		}
7685		else if (this.player === 'vimeo') {
7686			return (this.activeVimeoId === this.vimeoDescId);
7687		}
7688		else {
7689			return (this.$sources.first().attr('data-desc-src') === this.$sources.first().attr('src'));
7690		}
7691	};
7692
7693	AblePlayer.prototype.initSpeech = function (context) { 
7694
7695		// Some browsers &/or operating systems require a user-initiated click 
7696		// before this.synth.getVoices() will work. As of Nov 2022: 
7697		// Chrome requires a click before synth.getVoices() will work
7698		// iOS requires a click before synth.speak() will work 
7699		// A hack to address this: Listen for ANY click, then play an inaudible utterance 
7700		// to intitiate speech synthesis 		
7701		// https://stackoverflow.com/questions/32193704/js-speech-synthesis-issue-on-ios
7702		// This function does that, and sets this.speechEnabled
7703		// It's called with either of these contexts: 
7704		// 'init' - player is being initialized 
7705		// 'play' - user has clicked play 
7706		// 'prefs' - user has clicked prefs button
7707		// 'desc' - it's time to announce a description!  
7708
7709		var thisObj = this; 
7710		
7711		if (this.speechEnabled === null) {  
7712
7713			if (typeof this.synth !== 'undefined') { 
7714				// cancel any previous synth instance and reinitialize  
7715				this.synth.cancel(); 
7716			}	
7717
7718			if (window.speechSynthesis) {
7719
7720				// browser supports speech synthesis 
7721
7722				this.synth = window.speechSynthesis;
7723
7724				if (context === 'init') { 
7725					// handle a click on anything, in case the user 
7726					// clicks something before they click 'play' or 'prefs' buttons
7727					// that would allow us to init speech before it's needed 
7728					$(document).on('click',function() { 			
7729						var greeting = new SpeechSynthesisUtterance('Hi!');
7730						greeting.volume = 0; // silent 
7731						greeting.rate = 10; // fastest speed supported by the API  
7732						thisObj.synth.speak(greeting);
7733						greeting.onstart = function(e) { 						
7734							// utterance has started 
7735							$(document).off('click'); // unbind the click event listener 		
7736						}
7737						greeting.onend = function(e) {
7738							// should now be able to get browser voices 
7739							// in browsers that require a click 
7740							thisObj.getBrowserVoices(); 
7741							if (thisObj.descVoices.length) { 
7742								thisObj.speechEnabled = true; 
7743							}
7744						};
7745					}); 
7746									
7747					// go ahead and call get browser voices in case it might work, 
7748					// for browsers that don't require a click 
7749					this.getBrowserVoices(); 
7750					if (this.descVoices.length) { 
7751						this.speechEnabled = true; 
7752					}
7753				}
7754				else {  // context is either 'play' or 'prefs' or 'desc'
7755					var greeting = new SpeechSynthesisUtterance('Hi!');
7756					greeting.volume = 0; // silent 
7757					greeting.rate = 10; // fastest speed supported by the API  
7758					thisObj.synth.speak(greeting);
7759					greeting.onstart = function(e) { 						
7760						// utterance has started 
7761						$(document).off('click'); // unbind the click event listener 			
7762					};
7763					greeting.onend = function(e) {
7764						// should now be able to get browser voices 
7765						// in browsers that require a click 
7766						thisObj.getBrowserVoices(); 
7767						if (thisObj.descVoices.length) { 
7768							thisObj.speechEnabled = true; 
7769						}
7770					};							
7771				}
7772			}
7773			else { 
7774				// browser does not support speech synthesis
7775				this.speechEnabled = false; 
7776			}
7777		}
7778	}; 
7779
7780	AblePlayer.prototype.getBrowserVoices = function () {
7781		
7782		// define this.descVoices array 
7783		// includes only languages that match the language of the captions or player 
7784
7785		var voices, descLangs, voiceLang, preferredLang;
7786
7787		if (this.captionLang) { 
7788			preferredLang = this.captionLang.substring(0,2).toLowerCase();
7789		}
7790		else { 
7791			preferredLang = this.lang.substring(0,2).toLowerCase();
7792		}
7793		this.descVoices = []; 
7794		voices = this.synth.getVoices();
7795		descLangs = this.getDescriptionLangs();
7796		if (voices.length > 0) {
7797			this.descVoices = [];
7798			// available languages are identified with local suffixes (e.g., en-US)
7799			for (var i=0; i<voices.length; i++) {
7800				// match only the first 2 characters of the lang code
7801				voiceLang = voices[i].lang.substring(0,2).toLowerCase();
7802				if (voiceLang === preferredLang && (descLangs.indexOf(voiceLang) !== -1)) {
7803					// this voice matches preferredLang 
7804					// AND there's a matching description track in this language
7805					// Add this voice to final array 
7806					this.descVoices.push(voices[i]);
7807				}
7808			}
7809			if (!this.descVoices.length) {
7810				// no voices available in the default language(s)
7811				// just use all voices, regardless of language
7812				this.descVoices = voices;
7813			}
7814		}
7815		return false;
vendor: 12,063 bytes, lines 7816-8188
7816	};
7817
7818	AblePlayer.prototype.getDescriptionLangs = function () {
7819
7820		// returns an array of languages (from srclang atttributes)
7821		// in which there are description tracks
7822		// use only first two characters of the lang code
7823		var descLangs = [];
7824		if (this.tracks) {
7825			for (var i=0; i < this.tracks.length; i++) {
7826				if (this.tracks[i].kind === 'descriptions') {
7827					descLangs.push(this.tracks[i].language.substring(0,2).toLowerCase());
7828				}
7829			}
7830		}
7831		return descLangs;
7832	};
7833
7834	AblePlayer.prototype.setDescriptionVoice = function () {
7835
7836		// set description voice on player init, or when user changes caption language 
7837		// Voice is determined in the following order of precedence: 
7838		// 1. User's preferred voice for this language, saved in a cookie
7839		// 2. The first available voice in the array of available voices for this browser in this language
7840
7841		var cookie, voices, prefDescVoice, descVoice, descLang, prefVoiceFound;
7842		cookie = this.getCookie(); 
7843		if (typeof cookie.voices !== 'undefined') { 			
7844			prefDescVoice = this.getPrefDescVoice(); 
7845		}
7846		else { 
7847			prefDescVoice = null; 
7848		}
7849	
7850		this.getBrowserVoices();
7851		this.rebuildDescPrefsForm();
7852
7853		if (this.selectedDescriptions) { 
7854			descLang = this.selectedDescriptions.language;
7855		}
7856		else if (this.captionLang) { 
7857			descLang = this.captionLang; 
7858		}
7859		else { 
7860			descLang = this.lang; 
7861		}
7862
7863		if (this.synth) {
7864			voices = this.synth.getVoices();
7865			if (voices.length > 0) {
7866				if (prefDescVoice) { 
7867					// select the language that matches prefDescVoice, if it's available 
7868					prefVoiceFound = false; 
7869					for (var i=0; i<voices.length; i++) {
7870						// first, be sure voice is the correct language
7871						if (voices[i].lang.substring(0,2).toLowerCase() === descLang.substring(0,2).toLowerCase()) {
7872							if (voices[i].name === prefDescVoice) { 
7873								descVoice = voices[i].name; 
7874								prefVoiceFound = true; 
7875								break;
7876							}
7877						}
7878					}
7879				}
7880				if (!prefVoiceFound) { 
7881					// select the first language that matches the first 2 characters of the lang code
7882					for (var i=0; i<voices.length; i++) {
7883						if (voices[i].lang.substring(0,2).toLowerCase() === descLang.substring(0,2).toLowerCase()) {
7884							descVoice = voices[i].name;
7885							break;
7886						}
7887					}
7888				}
7889				// make this the user's current preferred voice
7890				this.prefDescVoice = descVoice;
7891				this.prefDescVoiceLang = descLang;
7892				// select this voice in the Description Prefs dialog
7893				if (this.$voiceSelectField) {
7894					var selectedOption = this.$voiceSelectField.find('option[value="' + this.prefDescVoice + '"]');
7895					this.$voiceSelectField.val(this.prefDescVoice);
7896				}
7897				this.updateCookie('voice'); 
7898			}
7899		}
7900	};
7901
7902	AblePlayer.prototype.swapDescription = function() {
7903
7904		// swap described and non-described source media, depending on which is playing
7905		// this function is only called in two circumstances:
7906		// 1. Swapping to described version when initializing player (based on user prefs & availability)
7907		// (playerCreated == false)
7908		// 2. User is toggling description
7909		// (playerCreated == true)
7910
7911		var thisObj, i, origSrc, descSrc, srcType, newSource;
7912
7913		thisObj = this;
7914
7915		// We are no longer loading the previous media source 
7916		// Only now, as a new source is requested, is it safe to reset this var 
7917		// It will be reset to true when media.load() is called 
7918		this.loadingMedia = false; 
7919
7920		// get element that has focus at the time swap is initiated 
7921		// after player is rebuilt, focus will return to that same element 
7922		// (if it exists)
7923		this.$focusedElement = $(':focus'); 
7924
7925		// get current time of current source, and attempt to start new video at the same time
7926		// whether this is possible will be determined after the new media source has loaded 
7927		// see onMediaNewSourceLoad() 
7928		if (this.elapsed > 0) { 
7929			this.swapTime = this.elapsed; 
7930		}
7931		else { 
7932			this.swapTime = 0; 
7933		}
7934		if (this.duration > 0) { 
7935			this.prevDuration = this.duration; 										
7936		}
7937
7938		// Capture current playback state, so media can resume after source is swapped 
7939		if (!this.okToPlay) { 
7940			this.okToPlay = this.playing; 
7941		}
7942
7943		if (this.descOn) {
7944			// user has requested the described version
7945			this.showAlert(this.tt.alertDescribedVersion);
7946		}
7947		else {
7948			// user has requested the non-described version
7949			this.showAlert(this.tt.alertNonDescribedVersion);
7950		}
7951
7952		if (this.player === 'html5') {
7953
7954			this.swappingSrc = true;
7955			this.paused = true; 
7956
7957			if (this.usingDescribedVersion()) {
7958				// the described version is currently playing. Swap to non-described
7959				for (i=0; i < this.$sources.length; i++) {
7960					// for all <source> elements, replace src with data-orig-src
7961					origSrc = this.$sources[i].getAttribute('data-orig-src');
7962					srcType = this.$sources[i].getAttribute('type');
7963					if (origSrc) {
7964						this.$sources[i].setAttribute('src',origSrc);
7965					}
7966				}						
7967			}
7968			else {
7969				// the non-described version is currently playing. Swap to described.
7970				for (i=0; i < this.$sources.length; i++) {
7971					// for all <source> elements, replace src with data-desc-src (if one exists)
7972					// then store original source in a new data-orig-src attribute
7973					origSrc = this.$sources[i].getAttribute('src');
7974					descSrc = this.$sources[i].getAttribute('data-desc-src');
7975					srcType = this.$sources[i].getAttribute('type');
7976					if (descSrc) {
7977						this.$sources[i].setAttribute('src',descSrc);
7978						this.$sources[i].setAttribute('data-orig-src',origSrc);
7979					}
7980				}
7981			}
7982
7983			if (this.recreatingPlayer) { 
7984				// stopgap to prevent multiple firings of recreatePlayer()
7985				return; 
7986			}	
7987			if (this.playerCreated) { 
7988				// delete old player, then recreate it with new source & tracks 
7989				this.deletePlayer('swap-desc-html'); 			
7990				this.recreatePlayer().then(function() { 
7991					if (!thisObj.loadingMedia) { 
7992						thisObj.media.load();
7993						thisObj.loadingMedia = true; 
7994					}
7995				});
7996			}
7997			else { 
7998				// player is in the process of being created
7999				// no need to recreate it 	
8000			} 
8001		}
8002		else if (this.player === 'youtube') {
8003
8004			if (this.usingDescribedVersion()) {
8005				// the described version is currently playing. Swap to non-described
8006				this.activeYouTubeId = this.youTubeId;
8007			}
8008			else {
8009				// the non-described version is currently playing. Swap to described.
8010				this.activeYouTubeId = this.youTubeDescId;
8011			}
8012			if (typeof this.youTubePlayer !== 'undefined') {
8013				thisObj.swappingSrc = true; 
8014				if (thisObj.playing) {
8015					// loadVideoById() loads and immediately plays the new video at swapTime
8016					thisObj.youTubePlayer.loadVideoById(thisObj.activeYouTubeId,thisObj.swapTime);
8017				}
8018				else {
8019					// cueVideoById() loads the new video and seeks to swapTime, but does not play
8020					thisObj.youTubePlayer.cueVideoById(thisObj.activeYouTubeId,thisObj.swapTime);
8021				}
8022			}
8023			if (this.playerCreated) { 
8024				this.deletePlayer('swap-desc-youtube'); 				
8025			}
8026			// player needs to be recreated with new source 
8027			if (this.recreatingPlayer) { 
8028				// stopgap to prevent multiple firings of recreatePlayer()
8029				return; 
8030			}	
8031			this.recreatePlayer().then(function() { 
8032				// nothing to do here 
8033				// next steps occur when youtube onReady event fires 
8034				// see youtube.js > finalizeYoutubeInit() 
8035			});				
8036		}
8037		else if (this.player === 'vimeo') {
8038			if (this.usingDescribedVersion()) {
8039				// the described version is currently playing. Swap to non-described
8040				this.activeVimeoId = this.vimeoId;
8041				this.showAlert(this.tt.alertNonDescribedVersion);
8042			}
8043			else {
8044				// the non-described version is currently playing. Swap to described.
8045				this.activeVimeoId = this.vimeoDescId;
8046				this.showAlert(this.tt.alertDescribedVersion);
8047			}
8048			if (this.playerCreated) { 
8049				this.deletePlayer('swap-desc-vimeo'); 				
8050			}
8051			// player needs to be recreated with new source 
8052			if (this.recreatingPlayer) { 
8053				// stopgap to prevent multiple firings of recreatePlayer()
8054				return; 
8055			}	
8056			this.recreatePlayer().then(function() { 
8057				// load the new video source
8058				thisObj.vimeoPlayer.loadVideo(thisObj.activeVimeoId).then(function() {
8059					if (thisObj.playing) {
8060						// video was playing when user requested an alternative version
8061						// seek to swapTime and continue playback (playback happens automatically)
8062						thisObj.vimeoPlayer.setCurrentTime(thisObj.swapTime);
8063					}
8064					else {
8065						// Vimeo autostarts immediately after video loads
8066						// The "Described" button should not trigger playback, so stop this before the user notices.
8067						thisObj.vimeoPlayer.pause();
8068					}
8069				});
8070			});
8071		}
8072	};
8073
8074	AblePlayer.prototype.showDescription = function(now) {
8075
8076		if (!this.hasClosedDesc || this.swappingSrc || !this.descOn || this.descMethod === 'video') {			
8077			return;
8078		}
8079
8080		var thisObj, i, cues, d, thisDescription, descText, msg;
8081		thisObj = this;
8082
8083		var flattenComponentForDescription = function (component) {
8084			var result = [];
8085			if (component.type === 'string') {
8086				result.push(component.value);
8087			}
8088			else {
8089				for (var i = 0; i < component.children.length; i++) {
8090					result.push(flattenComponentForDescription(component.children[i]));
8091				}
8092			}
8093			return result.join('');
8094		};
8095
8096		if (this.selectedDescriptions) {
8097			cues = this.selectedDescriptions.cues;
8098		}
8099		else if (this.descriptions.length >= 1) {
8100			cues = this.descriptions[0].cues;
8101		}
8102		else {
8103			cues = [];
8104		}
8105		for (d = 0; d < cues.length; d++) {
8106			if ((cues[d].start <= now) && (cues[d].end > now)) {
8107				thisDescription = d;
8108				break;
8109			}
8110		}
8111		if (typeof thisDescription !== 'undefined') {
8112			if (this.currentDescription !== thisDescription) {
8113				// temporarily remove aria-live from $status in order to prevent description from being interrupted
8114				this.$status.removeAttr('aria-live');
8115				descText = flattenComponentForDescription(cues[thisDescription].components);
8116				if (this.descReader === 'screenreader') { 					
8117					// load the new description into the container div for screen readers to read
8118					this.$descDiv.html(descText);
8119				}
8120				else if (this.speechEnabled) { 
8121					// use browser's built-in speech synthesis
8122					this.announceDescriptionText('description',descText);
8123					if (this.prefDescVisible) {
8124						// write description to the screen for sighted users
8125						// but remove ARIA attributes since it isn't intended to be read by screen readers
8126						this.$descDiv.html(descText).removeAttr('aria-live aria-atomic');
8127					}
8128				}
8129				else {
8130					// browser does not support speech synthesis
8131					// load the new description into the container div for screen readers to read
8132					this.$descDiv.html(descText);
8133				}
8134				if (this.prefDescPause && this.descMethod === 'text') {
8135					this.pauseMedia();
8136					this.pausedForDescription = true;
8137				}
8138				this.currentDescription = thisDescription;
8139			}
8140		}
8141		else {
8142			this.$descDiv.html('');
8143			this.currentDescription = -1;
8144			// restore aria-live to $status
8145			this.$status.attr('aria-live','polite');
8146		}
8147	};
8148
8149	AblePlayer.prototype.syncSpeechToPlaybackRate = function(rate) { 
8150
8151		// called when user changed playback rate 
8152		// adjust rate of audio description to match 
8153
8154		var speechRate; 
8155
8156		if (rate === 0.5) { 
8157			speechRate = 0.7; // option 1 in prefs menu 
8158		}
8159		else if (rate === 0.75) { 
8160			speechRate =  0.8; // option 2 in prefs menu 
8161		}
8162		else if (rate === 1.0) { 		
8163			speechRate =  1; // option 4 in prefs menu (normal speech, default)
8164		}
8165		else if (rate === 1.25) { 
8166			speechRate =  1.1; // option 5 in prefs menu
8167		}
8168		else if (rate === 1.5) { 
8169			speechRate =  1.2; // option 6 in prefs menu 
8170		}
8171		else if (rate === 1.75) { 
8172			speechRate =  1.5; // option 7 in prefs menu 
8173		}
8174		else if (rate === 2.0) { 
8175			speechRate =  2; // option 8 in prefs menu (fast)
8176		}
8177		else if (rate === 2.25) { 
8178			speechRate =  2.5; // option 9 in prefs menu (very fast)
8179		}
8180		else if (rate >= 2.5) { 
8181			speechRate =  3; // option 10 in prefs menu (super fast) 
8182		}
8183		this.prefDescRate = speechRate;
8184	}; 
8185
8186	AblePlayer.prototype.announceDescriptionText = function(context, text) {
8187
8188		// this function announces description text using speech synthesis
8189		// it's only called if already determined that browser supports speech synthesis
8190		// context is either:
8191		// 'description' - actual description text extracted from WebVTT file
8192		// 'sample' - called when user changes a setting in Description Prefs dialog
8193
8194		var thisObj, voiceName, i, voice, pitch, rate, volume, utterance,
8195			timeElapsed, secondsElapsed;
8196
8197		thisObj = this;
8198
8199		// As of Feb 2021,
8200		// 1. In some browsers (e.g., Chrome) window.speechSynthesis.getVoices()
8201		//  returns 0 voices unless the request is triggered with a user click
8202		//  Therefore, description may have failed to initialize when the page loaded
8203		//  This function cannot have been called without a mouse click.
8204		//  Therefore, this is a good time to check that, and try again if needed
8205		// 2. In some browsers, the window.speechSynthesis.speaking property fails to reset,
8206		//  and onend event is never fired. This prevents new speech from being spoken.
8207		//  window.speechSynthesis.cancel() also fails, so it's impossible to recover.
8208		//  This only seems to happen with some voices.
8209		//  Typically the first voice in the getVoices() array (index 0) is realiable
8210		//  When speech synthesis gets wonky, this is a deep problem that impacts all browsers
8211		//  and typically requires a computer reboot to make right again.
8212		//  This has been observed frequently in macOS Big Sur, but also in Windows 10
8213		//  To ignore user's voice preferences and always use the first voice, set the following var to true
8214		//	This is for testing only; not recommended for production
8215		// 	unless the voice select field is also removed from the Prefs dialog
8216		var useFirstVoice = false;
8217	
8218		if (!this.speechEnabled) {
8219			// voices array failed to load the first time. Try again
8220			this.initSpeech('desc');
8221		}
8222
8223		if (context === 'sample') {
8224			// get settings from form
8225			voiceName = $('#' + this.mediaId + '_prefDescVoice').val();
8226			pitch = $('#' + this.mediaId + '_prefDescPitch').val();
8227			rate = $('#' + this.mediaId + '_prefDescRate').val();
8228			volume = $('#' + this.mediaId + '_prefDescVolume').val();
8229		}
8230		else {
8231			// get settings from global prefs
8232			voiceName = this.prefDescVoice;
8233			pitch = this.prefDescPitch;
8234			rate = this.prefDescRate;
8235			volume = this.prefDescVolume;
8236		}
8237
8238		// get the voice associated with the user's chosen voice name
8239		if (this.descVoices) {
8240			if (this.descVoices.length > 0) {
8241				if (useFirstVoice) {
8242					voice = this.descVoices[0];
8243				}
8244				else if (voiceName) {
8245					// get the voice that matches user's preferred voiceName
8246					for (i = 0; i < this.descVoices.length; i++) {
8247						if (this.descVoices[i].name == voiceName) {
8248							voice = this.descVoices[i];
8249							break;
8250						}
8251					}
8252				}
8253				if (typeof voice === 'undefined') {
8254					// no matching voice was found
8255					// use the first voice in the array
8256					voice = this.descVoices[0];
8257				}
8258				utterance = new SpeechSynthesisUtterance();
8259				utterance.voice = voice;
8260				utterance.voiceURI = 'native';
8261				utterance.volume = volume;
8262				utterance.rate = rate;
8263				utterance.pitch = pitch;
8264				utterance.text = text;
8265				// TODO: Consider the best language for the utterance:
8266				// language of the web page? (this.lang)
8267				// language of the WebVTT description track?
8268				// language of the user's chosen voice?
8269				// If there's a mismatch between any of these, the description will likely be unintelligible
8270				utterance.lang = this.lang;
8271				utterance.onstart = function(e) { 
8272					// utterance has started 
8273				};
8274				utterance.onpause = function(e) { 
8275					// utterance has paused 
8276				};
8277				utterance.onend = function(e) {
8278					// utterance has ended 
8279					this.speakingDescription = false; 
8280					timeElapsed = e.elapsedTime; 
8281					// As of Firefox 95, e.elapsedTime is expressed in seconds 
8282					// Other browsers (tested in Chrome & Edge) express this in milliseconds 
8283					// Assume no utterance will require over 100 seconds to express... 
8284					if (timeElapsed > 100) { 
8285						// time is likely expressed in milliseconds 
8286						secondsElapsed = (e.elapsedTime/1000).toFixed(2); 
8287					}
8288					else { 
8289						// time is likely already expressed in seconds; just need to round it
8290						secondsElapsed = (e.elapsedTime).toFixed(2); 
8291					}
8292					if (this.debug) { 
8293						console.log('Finished speaking. That took ' + secondsElapsed + ' seconds.');
8294					}
8295					if (context === 'description') {
8296						if (thisObj.prefDescPause) {
8297							if (thisObj.pausedForDescription) {
8298								thisObj.playMedia();
8299								this.pausedForDescription = false;
8300							}
8301						}
8302					}
8303				};
8304				utterance.onerror = function(e) {
8305					// handle error
8306					console.log('Web Speech API error',e);
8307				};
8308				if (this.synth.paused) { 
8309					this.synth.resume();					
8310				}
8311				this.synth.speak(utterance);
8312				this.speakingDescription = true; 
8313			}
8314		}
8315	};
8316
8317})(jQuery);
8318
8319(function ($) {
8320
8321	AblePlayer.prototype.isIOS = function(version) {
8322
8323		// return true if this is IOS
8324		// if version is provided check for a particular version
8325
8326		var userAgent, iOS;
8327
8328		userAgent = navigator.userAgent.toLowerCase();
8329		iOS = /ipad|iphone|ipod/.exec(userAgent);
8330		if (iOS) {
8331			if (typeof version !== 'undefined') {
8332				if (userAgent.indexOf('os ' + version) !== -1) {
8333					// this is the target version of iOS
8334					return true;
8335				}
8336				else {
8337					return false;
8338				}
8339			}
8340			else {
8341				// no version was specified
8342				return true;
8343			}
8344		}
8345		else {
8346			// this is not IOS
8347			return false;
8348		}
8349	};
8350
8351	AblePlayer.prototype.browserSupportsVolume = function() {
8352
8353		// To test whether the browser supports changing the volume, 
8354			// create a new audio element and try setting the volume to something other than 1. 
8355			// Then, retrieve the current setting to see if it preserved it. 
8356
8357			// Unfortunately, this doesn't work in iOS. In 2022, our tests yield the same results as reported here:  
8358			// https://stackoverflow.com/questions/72861253/how-do-i-detect-if-a-browser-does-not-support-changing-html-audio-volume
8359
8360			// So, unfortunately we have to resort to sniffing for iOS  
8361			// before testing for support in other browsers 
8362			var audio, testVolume; 
8363 
8364			if (this.isIOS()) { 
8365				return false; 
8366			}
8367
8368			testVolume = 0.9;  // any value between 0.1 and 0.9 
8369			audio = new Audio();
8370      audio.volume = testVolume;
8371			if (audio.volume === testVolume) { 
8372				return true; 
8373			} 
8374			else { 
8375				return false; 
8376			}
8377	};
8378
8379	AblePlayer.prototype.nativeFullscreenSupported = function () {
8380
8381		return document.fullscreenEnabled ||
8382			document.webkitFullscreenEnabled ||
8383			document.mozFullscreenEnabled ||
8384			document.msFullscreenEnabled;
8385	};
8386
8387})(jQuery);
8388
8389(function ($) {
8390	AblePlayer.prototype.seekTo = function (newTime) {
8391
8392		var thisObj = this;
8393
8394		// define variables to be used for analytics
8395		// e.g., to measure the extent to which users seek back and forward
8396		this.seekFromTime = this.media.currentTime;
8397		this.seekToTime = newTime;
8398
8399		this.seeking = true;
8400		this.liveUpdatePending = true;
8401
8402		if (this.speakingDescription) { 			
8403			this.synth.cancel(); 
8404		}
8405
8406		if (this.player === 'html5') {
8407			var seekable;
8408
8409			this.startTime = newTime;
8410			// Check HTML5 media "seekable" property to be sure media is seekable to startTime
8411			seekable = this.media.seekable;
8412			if (seekable.length > 0 && this.startTime >= seekable.start(0) && this.startTime <= seekable.end(0)) {
8413				// ok to seek to startTime
8414				// canplaythrough will be triggered when seeking is complete
8415				// this.seeking will be set to false at that point
8416				this.media.currentTime = this.startTime; 
8417				this.seekStatus = 'complete'; 
8418				if (this.hasSignLanguage && this.signVideo) {
8419					// keep sign languge video in sync
8420					this.signVideo.currentTime = this.startTime;
8421				}
8422			}
8423		}
8424		else if (this.player === 'youtube') {
8425			this.youTubePlayer.seekTo(newTime,true);
8426			if (newTime > 0) {
8427				if (typeof this.$posterImg !== 'undefined') {
8428					this.$posterImg.hide();
8429				}
8430			}
8431		}
8432		else if (this.player === 'vimeo') {
8433			this.vimeoPlayer.setCurrentTime(newTime).then(function() {
8434				// seek finished.
8435				// successful completion also fires a 'seeked' event (see event.js)
8436				thisObj.elapsed = newTime;
8437				thisObj.refreshControls('timeline');
8438			})
8439		}
8440		this.refreshControls('timeline');
8441	};
8442
8443	AblePlayer.prototype.getMediaTimes = function (duration, elapsed) {
8444
8445		 // Returns an array with keys 'duration' and 'elapsed'
8446		 // Vars passed to this function come courtesy of select Vimeo events
8447		 // Use those if they're available.
8448		 // Otherwise, will need to call the relevant media API
8449		 // This function should only be called from onMediaUpdateTime()
8450		 // If duration and elapsed are needed other times, use this.duration and this.elapsed
8451
8452		// both values are expressed in seconds, and all player APIs are similar:
8453		// they return a value that is rounded to the nearest second before playback begins,
8454		// then to the nearest thousandth of a second after playback begins
8455		// With HTML5 media API, some browsers are more precise (e.g., Firefox rounds to 6 decimal points)
8456		// but inconsistent (values with 9 decimal points have been sporadically observed in Safari)
8457		// For standardization, values are rounded to 6 decimal points before they're returned
8458
8459		var deferred, promise, thisObj, mediaTimes;
8460		mediaTimes = {};
8461
8462		deferred = new $.Deferred();
8463		promise = deferred.promise();
8464		thisObj = this;
8465		if (typeof duration !== 'undefined' && typeof elapsed !== 'undefined') {
8466			mediaTimes['duration'] = duration;
8467			mediaTimes['elapsed'] = elapsed;
8468			deferred.resolve(mediaTimes);
8469		}
8470		else {
8471			this.getDuration().then(function(duration) {
8472				mediaTimes['duration'] = thisObj.roundDown(duration,6);
8473				thisObj.getElapsed().then(function(elapsed) {
vendor: 4,316 bytes, lines 8474-8640
8474					mediaTimes['elapsed'] = thisObj.roundDown(elapsed,6);
8475					deferred.resolve(mediaTimes);
8476				});
8477			});
8478		}
8479		return promise;
8480	};
8481
8482	AblePlayer.prototype.getDuration = function () {
8483
8484		// returns duration of the current media, expressed in seconds
8485		// function is called by getMediaTimes, and return value is sanitized there
8486		var deferred, promise, thisObj;
8487
8488		deferred = new $.Deferred();
8489		promise = deferred.promise();
8490		thisObj = this;
8491
8492		if (this.player === 'vimeo') {
8493			if (this.vimeoPlayer) {
8494				 this.vimeoPlayer.getDuration().then(function(duration) {
8495					if (duration === undefined || isNaN(duration) || duration === -1) {
8496						deferred.resolve(0);
8497					}
8498					else {
8499						deferred.resolve(duration);
8500					}
8501				});
8502			}
8503			else { // vimeoPlayer hasn't been initialized yet.
8504				deferred.resolve(0);
8505			}
8506		}
8507		else {
8508			var duration;
8509			if (this.player === 'html5') {
8510				duration = this.media.duration;			
8511			}
8512			else if (this.player === 'youtube') {
8513				if (this.youTubePlayerReady) {
8514					if (this.duration > 0) { 
8515						// duration was already retrieved while checking for captions
8516						duration = this.duration; 
8517					}
8518					else { 
8519						duration = this.youTubePlayer.getDuration();
8520					}
8521				}
8522				else { // the YouTube player hasn't initialized yet
8523					duration = 0;
8524				}
8525			}
8526			if (duration === undefined || isNaN(duration) || duration === -1) {
8527				deferred.resolve(0);
8528			}
8529			else {
8530				deferred.resolve(duration);
8531			}
8532		}
8533		return promise;
8534	};
8535
8536	AblePlayer.prototype.getElapsed = function () {
8537
8538		// returns elapsed time of the current media, expressed in seconds
8539		// function is called by getMediaTimes, and return value is sanitized there
8540
8541		var deferred, promise, thisObj;
8542
8543		deferred = new $.Deferred();
8544		promise = deferred.promise();
8545		thisObj = this;
8546
8547		if (this.player === 'vimeo') {
8548			if (this.vimeoPlayer) {
8549				this.vimeoPlayer.getCurrentTime().then(function(elapsed) {
8550					if (elapsed === undefined || isNaN(elapsed) || elapsed === -1) {
8551						deferred.resolve(0);
8552					}
8553					else {
8554						deferred.resolve(elapsed);
8555					}
8556				});
8557			}
8558			else { // vimeoPlayer hasn't been initialized yet.
8559				deferred.resolve(0);
8560			}
8561		}
8562		else {
8563			var elapsed;
8564			if (this.player === 'html5') {
8565				elapsed = this.media.currentTime;
8566			}
8567			else if (this.player === 'youtube') {
8568				if (this.youTubePlayerReady) {
8569					elapsed = this.youTubePlayer.getCurrentTime();
8570				}
8571				else { // the YouTube player hasn't initialized yet
8572					elapsed = 0;
8573				}
8574			}
8575			if (elapsed === undefined || isNaN(elapsed) || elapsed === -1) {
8576				deferred.resolve(0);
8577			}
8578			else {
8579				deferred.resolve(elapsed);
8580			}
8581		}
8582		return promise;
8583	};
8584
8585	AblePlayer.prototype.getPlayerState = function () {
8586
8587		// Returns one of the following states:
8588		//	'stopped' - Not yet played for the first time, or otherwise reset to unplayed.
8589		//	'ended' - Finished playing.
8590		//	'paused' - Not playing, but not stopped or ended.
8591		//	'buffering' - Momentarily paused to load, but will resume once data is loaded.
8592		//	'playing' - Currently playing.
8593
8594		// Commented out the following in 3.2.1 - not sure of its intended purpose
8595		// It can be useful to know player state even when swapping src
8596		// and the overhead is seemingly minimal
8597		// TODO - Investigate this further. Delete if it's not needed
8598		/*
8599		if (this.swappingSrc) {
8600			return;
8601		}
8602		*/
8603		var deferred, promise, thisObj, duration, elapsed;
8604		deferred = new $.Deferred();
8605		promise = deferred.promise();
8606		thisObj = this;
8607
8608		if (this.player === 'html5') {
8609			if (this.media.ended) {
8610				deferred.resolve('ended');
8611			}
8612			else if (this.media.paused) {
8613				deferred.resolve('paused');
8614			}
8615			else if (this.media.readyState !== 4) {
8616				deferred.resolve('buffering');
8617			}
8618			else {
8619				deferred.resolve('playing');
8620			}
8621		}
8622		else if (this.player === 'youtube' && this.youTubePlayerReady) {
8623			var state = this.youTubePlayer.getPlayerState();
8624			if (state === -1 || state === 5) {
8625				deferred.resolve('stopped');
8626			}
8627			else if (state === 0) {
8628				deferred.resolve('ended');
8629			}
8630			else if (state === 1) {
8631				deferred.resolve('playing');
8632			}
8633			else if (state === 2) {
8634				deferred.resolve('paused');
8635			}
8636			else if (state === 3) {
8637				deferred.resolve('buffering');
8638			}
8639		}
8640		else if (this.player === 'vimeo' && this.vimeoPlayer) {
8641				// curiously, Vimeo's API has no getPlaying(), getBuffering(), or getState() methods
8642			// so maybe if it's neither paused nor ended, it must be playing???
8643			this.vimeoPlayer.getPaused().then(function(paused) {
8644				if (paused) {
8645					deferred.resolve('paused');
8646				}
8647				else {
8648					thisObj.vimeoPlayer.getEnded().then(function(ended) {
8649						if (ended) {
8650							deferred.resolve('ended');
8651						}
8652						else {
8653							deferred.resolve('playing');
8654						}
8655					});
8656				}
8657			});
8658		}
8659		return promise;
8660	};
8661
8662	AblePlayer.prototype.isPlaybackRateSupported = function () {
8663
8664		if (this.player === 'html5') {
8665			if (this.media.playbackRate) {
8666				return true;
8667			}
8668			else {
8669				return false;
8670			}
8671		}
8672		else if (this.player === 'youtube') {
8673			// Youtube supports varying playback rates per video.	 
8674			// Only expose controls if more than one playback rate is available.
8675			if (this.youTubePlayerReady) { 
8676				if (this.youTubePlayer.getAvailablePlaybackRates().length > 1) {
8677					return true;
8678				}
8679				else { 
8680					return false;
8681				}
8682			}
8683			else {
8684				return false;
8685			}
8686		}
8687		else if (this.player === 'vimeo') {
8688			// since this takes longer to determine, it was set previously in initVimeoPlayer()
8689			return this.vimeoSupportsPlaybackRateChange;
8690		}
8691	};
8692
8693	AblePlayer.prototype.setPlaybackRate = function (rate) {
8694		
8695		rate = Math.max(0.5, rate);
8696
8697		if (this.hasClosedDesc && this.descMethod === 'text') { 
8698			// keep speech rate in sync with playback rate even if descOn is false 
8699			this.syncSpeechToPlaybackRate(rate); 
8700		}
8701
8702		if (this.player === 'html5') {
8703			this.media.playbackRate = rate;
8704		}
8705		else if (this.player === 'youtube') {
8706			this.youTubePlayer.setPlaybackRate(rate);
8707		}
8708		else if (this.player === 'vimeo') {
8709			this.vimeoPlayer.setPlaybackRate(rate);
8710		}
8711		if (this.hasSignLanguage && this.signVideo) {
8712			this.signVideo.playbackRate = rate;
8713		}
8714		this.playbackRate = rate;
8715		this.$speed.text(this.tt.speed + ': ' + rate.toFixed(2).toString() + 'x');
8716	};	
8717
8718	AblePlayer.prototype.getPlaybackRate = function () {
8719
8720		if (this.player === 'html5') {
8721			return this.media.playbackRate;
8722		}
8723		else if (this.player === 'youtube') {
8724			if (this.youTubePlayerReady) {
8725				return this.youTubePlayer.getPlaybackRate();
8726			}
8727		}
8728	};
8729
8730	AblePlayer.prototype.isPaused = function () {
8731
8732		 // Note there are three player states that count as paused in this sense,
8733		// and one of them is named 'paused'.
8734		// A better name would be 'isCurrentlyNotPlayingOrBuffering'
8735
8736		var state;
8737
8738		if (this.player === 'vimeo') {
8739			// just rely on value of this.playing
8740			if (this.playing) {
8741				return false;
8742			}
8743			else {
8744				return true;
8745			}
8746		}
8747		else {
8748			this.getPlayerState().then(function(state) {
8749				// if any of the following is true, consider the media 'paused'
8750				return state === 'paused' || state === 'stopped' || state === 'ended';
8751			});
8752		}
8753	};
8754
8755	AblePlayer.prototype.pauseMedia = function () {
8756
8757		var thisObj = this;
8758
8759		if (this.player === 'html5') {
8760			this.media.pause(true);
8761			if (this.hasSignLanguage && this.signVideo) {
8762				this.signVideo.pause(true);
8763			}
8764		}
8765		else if (this.player === 'youtube') {
8766			this.youTubePlayer.pauseVideo();
8767		}
8768		else if (this.player === 'vimeo') {
8769			this.vimeoPlayer.pause();
8770		}
8771	};
8772
8773	AblePlayer.prototype.playMedia = function () {
8774
8775		var thisObj = this;
8776
8777		if (this.player === 'html5') {
8778			this.media.play(true);
8779			if (this.hasSignLanguage && this.signVideo) {
8780				this.signVideo.play(true);
8781			}
8782		}
8783		else if (this.player === 'youtube') {
8784
8785			this.youTubePlayer.playVideo();
8786			if (typeof this.$posterImg !== 'undefined') {
8787				this.$posterImg.hide();
8788			}
8789			this.stoppingYouTube = false;
8790		}
8791		else if (this.player === 'vimeo') {
8792			 this.vimeoPlayer.play();
8793		}
8794		this.startedPlaying = true;
8795		if (this.hideControls) {
8796			// wait briefly after playback begins, then hide controls
8797			this.hidingControls = true;
8798			this.invokeHideControlsTimeout();
8799		}
8800	};
8801
8802	AblePlayer.prototype.fadeControls = function(direction) {
8803
8804		// Visibly fade controls without hiding them from screen reader users
8805
8806		// direction is either 'out' or 'in'
8807
8808		// TODO: This still needs work.
8809		// After the player fades, it's replaced by an empty space
8810		// Would be better if the video and captions expanded to fill the void
8811		// Attempted to fade out to 0 opacity, then move the playerDiv offscreen
8812		// and expand the mediaContainer to fill the vacated space
8813		// However, my attempts to do this have been choppy and buggy
8814		// Code is preserved below and commented out
8815
8816		var thisObj, mediaHeight, playerHeight, newMediaHeight;
8817		var thisObj = this;
8818
8819		if (direction == 'out') {
8820			// get the original height of two key components:
8821			mediaHeight = this.$mediaContainer.height();
8822			playerHeight = this.$playerDiv.height();
8823			newMediaHeight = mediaHeight + playerHeight;
8824
8825			// fade slowly to transparency
8826			this.$playerDiv.fadeTo(2000,0,function() {
8827				/*
8828				// when finished, position playerDiv offscreen
8829				// thisObj.$playerDiv.addClass('able-offscreen');
8830				// Expand the height of mediaContainer to fill the void (needs work)
8831				thisObj.$mediaContainer.animate({
8832					height: newMediaHeight
8833				},500);
8834				*/
8835			});
8836		}
8837		else if (direction == 'in') {
8838			// restore captionsContainer to its original height (needs work)
8839			// this.$mediaContainer.removeAttr('style');
8840			// fade relatively quickly back to its original position with full opacity
8841			// this.$playerDiv.removeClass('able-offscreen').fadeTo(100,1);
8842			this.$playerDiv.fadeTo(100,1);
8843		}
8844	};
8845
8846	AblePlayer.prototype.invokeHideControlsTimeout = function () {
8847
8848		// invoke timeout for waiting a few seconds after a mouse move or key down
8849		// before hiding controls again
8850		var thisObj = this;
8851		this.hideControlsTimeout = window.setTimeout(function() {
8852			if (typeof thisObj.playing !== 'undefined' && thisObj.playing === true && thisObj.hideControls) {
8853				thisObj.fadeControls('out');
8854				thisObj.controlsHidden = true;
8855			}
8856		},5000);
8857		this.hideControlsTimeoutStatus = 'active';
8858	};
8859
8860	AblePlayer.prototype.refreshControls = function(context, duration, elapsed) {
8861
8862		// context is one of the following:
8863		// 'init' - initial build (or subsequent change that requires full rebuild)
8864		// 'timeline' - a change may effect time-related controls
8865		// 'captions' - a change may effect caption-related controls
8866		// 'descriptions' - a change may effect description-related controls
8867		// 'transcript' - a change may effect the transcript window or button
8868		// 'fullscreen' - a change has been triggered by full screen toggle
8869		// 'playpause' - a change triggered by either a 'play' or 'pause' event
8870
8871		// NOTE: context is not currently supported.
8872		// The steps in this function have too many complex interdependencies
8873		// The gains in efficiency are offset by the possibility of introducing bugs
8874		// For now, executing everything
8875		context = 'init';
8876
8877		// duration and elapsed are passed from callback functions of Vimeo API events
8878		// duration is expressed as sss.xxx
8879		// elapsed is expressed as sss.xxx
8880
8881		var thisObj, duration, elapsed, lastChapterIndex, displayElapsed,
8882			updateLive, textByState, timestamp, widthUsed,
8883			leftControls, rightControls, seekbarWidth, seekbarSpacer, captionsCount,
8884			buffered, newTop, statusBarHeight, speedHeight, statusBarWidthBreakpoint,
8885			newSvgData;
8886
8887		thisObj = this;
8888		if (this.swappingSrc) {
8889			if (this.playing) {
8890				// wait until new source has loaded before refreshing controls
8891				// can't wait if player is NOT playing because some critical events
8892				// won't fire until playback of new media starts
8893				return;
8894			}
8895		}
8896
8897		if (context === 'timeline' || context === 'init') {
8898			// all timeline-related functionality requires both duration and elapsed
8899			if (typeof this.duration === 'undefined') {
8900			 	// wait until duration is known before proceeding with refresh
8901			 	return;
8902			}
8903			if (this.useChapterTimes) {
8904				this.chapterDuration = this.getChapterDuration();
8905				this.chapterElapsed = this.getChapterElapsed();
8906			}
8907
8908			if (this.useFixedSeekInterval === false && this.seekIntervalCalculated === false && this.duration > 0) {
8909				// couldn't calculate seekInterval previously; try again.
8910				this.setSeekInterval();
8911			}
8912
8913			if (this.seekBar) {
8914				if (this.useChapterTimes) {
8915					lastChapterIndex = this.selectedChapters.cues.length-1;
8916					if (this.selectedChapters.cues[lastChapterIndex] == this.currentChapter) {
8917						// this is the last chapter
8918						if (this.currentChapter.end !== this.duration) {
8919							// chapter ends before or after video ends
8920							// need to adjust seekbar duration to match video end
8921							this.seekBar.setDuration(this.duration - this.currentChapter.start);
8922						}
8923						else {
8924							this.seekBar.setDuration(this.chapterDuration);
8925						}
8926					}
8927					else {
8928						// this is not the last chapter
8929						this.seekBar.setDuration(this.chapterDuration);
8930					}
8931				}
8932				else {
8933					if (!(this.duration === undefined || isNaN(this.duration) || this.duration === -1)) {
8934						this.seekBar.setDuration(this.duration);
8935					}
8936				}
8937				if (!(this.seekBar.tracking)) {
8938					// Only update the aria live region if we have an update pending
8939					// (from a seek button control) or if the seekBar has focus.
8940					// We use document.activeElement instead of $(':focus') due to a strange bug:
8941					// 	When the seekHead element is focused, .is(':focus') is failing and $(':focus') is returning an undefined element.
8942					updateLive = this.liveUpdatePending || this.seekBar.seekHead.is($(document.activeElement));
8943					this.liveUpdatePending = false;
vendor: 1,290 bytes, lines 8944-8983
8944					if (this.useChapterTimes) {
8945						this.seekBar.setPosition(this.chapterElapsed, updateLive);
8946					}
8947					else {
8948						this.seekBar.setPosition(this.elapsed, updateLive);
8949					}
8950				}
8951
8952				// When seeking, display the seek bar time instead of the actual elapsed time.
8953				if (this.seekBar.tracking) {
8954					displayElapsed = this.seekBar.lastTrackPosition;
8955				}
8956				else {
8957					if (this.useChapterTimes) {
8958						displayElapsed = this.chapterElapsed;
8959					}
8960					else {
8961						displayElapsed = this.elapsed;
8962					}
8963				}
8964			}
8965			// update elapsed & duration
8966			if (typeof this.$durationContainer !== 'undefined') {
8967				if (this.useChapterTimes) {
8968					this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.chapterDuration));
8969				}
8970				else {
8971					this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.duration));
8972				}
8973			}
8974			if (typeof this.$elapsedTimeContainer !== 'undefined') {
8975				this.$elapsedTimeContainer.text(this.formatSecondsAsColonTime(displayElapsed));
8976			}
8977
8978			if (this.skin === 'legacy') {
8979				// Update seekbar width. // steve
8980				// To do this, we need to calculate the width of all buttons surrounding it.
8981				/* if (this.seekBar) {
8982					widthUsed = 0;
8983					leftControls = this.seekBar.wrapperDiv.parent().prev('div.able-left-control
8983s');
8984					rightControls = leftControls.next('div.able-right-controls');
8985					leftControls.children().each(function () {
8986						if ($(this).attr('role')=='button') {
8987							widthUsed += $(this).outerWidth(true); // true = include margin
8988						}
8989					});
8990					rightControls.children().each(function () {
8991						if ($(this).attr('role')=='button') {
8992							widthUsed += $(this).outerWidth(true);
8993						}
8994					});
8995					if (this.fullscreen) {
8996						seekbarWidth = $(window).width() - widthUsed;
8997					}
8998					else {
8999						// seekbar is wide enough to fill the remaining space
9000						// include a 5px buffer to account for minor browser differences  
9001					}					
9002					// Sometimes some minor fluctuations based on browser weirdness, so set a threshold.
9003					if (Math.abs(seekbarWidth - this.seekBar.getWidth()) > 5) {
9004						this.seekBar.setWidth(seekbarWidth);
9005					}
9006				} */
9007			}
9008
9009			// Update buffering progress.
9010			// TODO: Currently only using the first HTML5 buffered interval,
9011			// but this fails sometimes when buffering is split into two or more intervals.
9012			if (this.player === 'html5') {
9013				if (this.media.buffered.length > 0) {
9014					buffered = this.media.buffered.end(0);
9015					if (this.useChapterTimes) {
9016						if (buffered > this.chapterDuration) {
9017							buffered = this.chapterDuration;
9018						}
9019						if (this.seekBar) {
9020							this.seekBar.setBuffered(buffered / this.chapterDuration);
9021						}
9022					}
9023					else {
9024						if (this.seekBar) {
9025							if (!isNaN(buffered)) {
9026								this.seekBar.setBuffered(buffered / duration);
9027							}
9028						}
9029					}
9030				}
9031			}
9032			else if (this.player === 'youtube') {
9033				if (this.seekBar) {
9034					if (this.youTubePlayerReady) {
9035						this.seekBar.setBuffered(this.youTubePlayer.getVideoLoadedFraction());
9036					}
9037				}
9038			}
9039			else if (this.player === 'vimeo') {
9040				// TODO: Add support for Vimeo buffering update
9041			}
9042		} // end if context == 'timeline' or 'init'
9043
9044		if (context === 'descriptions' || context == 'init'){
9045
9046			if (this.$descButton) {
9047				if (this.descOn) {
9048					this.$descButton.removeClass('buttonOff').attr('aria-label',this.tt.turnOffDescriptions);
9049					this.$descButton.find('span.able-clipped').text(this.tt.turnOffDescriptions);
9050				}
9051				else {
9052					this.$descButton.addClass('buttonOff').attr('aria-label',this.tt.turnOnDescriptions);
9053					this.$descButton.find('span.able-clipped').text(this.tt.turnOnDescriptions);
9054				}
9055			}
9056		}
9057
9058		if (context === 'captions' || context == 'init'){
9059
9060			if (this.$ccButton) {
9061
9062				captionsCount = this.captions.length;
9063
9064				// Button has a different title depending on the number of captions.
9065				// If only one caption track, this is "Show captions" and "Hide captions"
9066				// Otherwise, it is just always "Captions"
9067				if (!this.captionsOn) {
9068					this.$ccButton.addClass('buttonOff');
9069					this.$ccButton.attr('aria-pressed', 'false')
9070					if (captionsCount === 1) {
9071						this.$ccButton.attr('aria-label',this.tt.showCaptions);
9072						this.$ccButton.find('span.able-clipped').text(this.tt.showCaptions);
9073					}
9074				}
9075				else {
9076					this.$ccButton.removeClass('buttonOff');
9077					this.$ccButton.attr('aria-pressed', 'true')
9078					if (captionsCount === 1) {
9079						this.$ccButton.attr('aria-label',this.tt.hideCaptions);
9080						this.$ccButton.find('span.able-clipped').text(this.tt.hideCaptions);
9081					}
9082				}
9083
9084				if (captionsCount > 1) {
9085					this.$ccButton.attr({
9086						'aria-label': this.tt.captions,
9087						'aria-haspopup': 'true',
9088						'aria-controls': this.mediaId + '-captions-menu'
9089					});
9090					this.$ccButton.find('span.able-clipped').text(this.tt.captions);
9091				}
9092			}
9093		}
9094
9095		if (context === 'fullscreen' || context == 'init'){
9096			if (this.$fullscreenButton) {
9097				if (!this.fullscreen) {
9098					this.$fullscreenButton.attr('aria-label', this.tt.enterFullscreen);
9099					if (this.iconType === 'font') {
9100						this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-collapse').addClass('icon-fullscreen-expand');
9101						this.$fullscreenButton.find('span.able-clipped').text(this.tt.enterFullscreen);
9102					}
9103					else if (this.iconType === 'svg') {
9104						newSvgData = this.getSvgData('fullscreen-expand');
9105						this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
9106						this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
9107						this.$fullscreenButton.find('span.able-clipped').text(this.tt.enterFullscreen);
9108					}
9109					else {
9110						this.$fullscreenButton.find('img').attr('src',this.fullscreenExpandButtonImg);
9111					}
9112				}
9113				else {
9114					this.$fullscreenButton.attr('aria-label',this.tt.exitFullscreen);
9115					if (this.iconType === 'font') {
9116						this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-expand').addClass('icon-fullscreen-collapse');
9117						this.$fullscreenButton.find('span.able-clipped').text(this.tt.exitFullscreen);
9118					}
9119					else if (this.iconType === 'svg') {
9120						newSvgData = this.getSvgData('fullscreen-collapse');
9121						this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
9122						this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
9123						this.$fullscreenButton.find('span.able-clipped').text(this.tt.exitFullscreen);
9124					}
9125					else {
9126						this.$fullscreenButton.find('img').attr('src',this.fullscreenCollapseButtonImg);
9127					}
9128				}
9129			}
9130		}
9131		if (context === 'playpause' || context == 'init'){
9132			if (typeof this.$bigPlayButton !== 'undefined' && typeof this.seekBar !== 'undefined') {
9133				// Choose show/hide for big play button and adjust position.
9134				if (this.paused && !this.seekBar.tracking) {
9135					if (!this.hideBigPlayButton) {
9136						this.$bigPlayButton.show();
9137						this.$bigPlayButton.attr('aria-hidden', 'false');
9138
9139					}
9140					if (this.fullscreen) {
9141						this.$bigPlayButton.width($(window).width());
9142						this.$bigPlayButton.height($(window).height());
9143					}
9144					else {
9145						this.$bigPlayButton.width(this.$mediaContainer.width());
9146						this.$bigPlayButton.height(this.$mediaContainer.height());
9147					}
9148				}
9149				else {
9150					this.$bigPlayButton.hide();
9151					this.$bigPlayButton.attr('aria-hidden', 'true');
9152				}
9153			}
9154		}
9155
9156		if (context === 'transcript' || context == 'init'){
9157
9158			if (this.transcriptType) {
9159				// Sync checkbox and autoScrollTranscript with user preference
9160				if (this.prefAutoScrollTranscript === 1) {
9161					this.autoScrollTranscript = true;
9162					this.$autoScrollTranscriptCheckbox.prop('checked',true);
9163				}
9164				else {
9165					this.autoScrollTranscript = false;
9166					this.$autoScrollTranscriptCheckbox.prop('checked',false);
9167				}
9168
9169				// If transcript locked, scroll transcript to current highlight location.
9170				if (this.autoScrollTranscript && this.currentHighlight) {
9171					newTop = Math.floor(this.$transcriptDiv.scrollTop() +
9172						$(this.currentHighlight).position().top -
9173						(this.$transcriptDiv.height() / 2) +
9174						($(this.currentHighlight).height() / 2));
9175					if (newTop !== Math.floor(this.$transcriptDiv.scrollTop())) {
9176						// Set a flag to ignore the coming scroll event.
9177						// there's no other way I know of to differentiate programmatic and user-initiated scroll events.
9178						this.scrollingTranscript = true;
9179						// only scroll once after moving a highlight
9180						if (this.movingHighlight) {
9181							this.$transcriptDiv.scrollTop(newTop);
9182											this.movingHighlight = false;
9183									}
9184					}
9185				}
9186			}
9187		}
9188
9189		if (context === 'init') {
9190
9191			if (this.$chaptersButton) {
9192				this.$chaptersButton.attr({
9193					'aria-label': this.tt.chapters,
9194					'aria-haspopup': 'true',
9195					'aria-controls': this.mediaId + '-chapters-menu'
9196				});
9197			}
9198		}
9199
9200		if (context === 'timeline' || context === 'playpause' || context === 'init') {
9201
9202			// update status
9203			textByState = {
9204				'stopped': this.tt.statusStopped,
9205				'paused': this.tt.statusPaused,
9206				'playing': this.tt.statusPlaying,
9207				'buffering': this.tt.statusBuffering,
9208				'ended': this.tt.statusEnd
9209			};
9210
9211			if (this.stoppingYouTube) {
9212				// stoppingYouTube is true temporarily while video is paused and seeking to 0
9213				// See notes in handleRestart()
9214				// this.stoppingYouTube will be reset when seek to 0 is finished (in event.js > onMediaUpdateTime())
9215				if (this.$status.text() !== this.tt.statusStopped) {
9216					this.$status.text(this.tt.statusStopped);
9217				}
9218				if (this.$playpauseButton.find('span').first().hasClass('icon-pause')) {
9219					if (this.iconType === 'font') {
9220						this.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
9221						this.$playpauseButton.find('span.able-clipped').text(this.tt.play);
9222					}
9223					else if (this.iconType === 'svg') {
9224						newSvgData = this.getSvgData('play');
9225						this.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
9226						this.$playpauseButton.find('path').attr('d',newSvgData[1]);
9227						this.$playpauseButton.find('span.able-clipped').text(this.tt.play);
9228					}
9229					else {
9230						this.$playpauseButton.find('img').attr('src',this.playButtonImg);
9231					}
9232				}
9233			}
9234			else {
9235				if (typeof this.$status !== 'undefined' && typeof this.seekBar !== 'undefined') {
9236					// Update the text only if it's changed since it has role="alert";
9237					// also don't update while tracking, since this may Pause/Play the player but we don't want to send a Pause/Play update.
9238					this.getPlayerState().then(function(currentState) {
9239						if (thisObj.$status.text() !== textByState[currentState] && !thisObj.seekBar.tracking) {
9240							// Debounce updates; only update after status has stayed steadily different for a while
9241							// "A while" is defined differently depending on context
9242							if (thisObj.swappingSrc) {
9243								// this is where most of the chatter occurs (e.g., playing, paused, buffering, playing),
9244								// so set a longer wait time before writing a status message
9245								if (!thisObj.debouncingStatus) {
9246									thisObj.statusMessageThreshold = 2000; // in ms (2 seconds)
9247								}
9248							}
9249							else {
9250								// for all other contexts (e.g., users clicks Play/Pause)
9251								// user should receive more rapid feedback
9252								if (!thisObj.debouncingStatus) {
9253									thisObj.statusMessageThreshold = 250; // in ms
9254								}
9255							}
9256							timestamp = (new Date()).getTime();
9257							if (!thisObj.statusDebounceStart) {
9258								thisObj.statusDebounceStart = timestamp;
9259								// Call refreshControls() again after allotted time has passed
9260								thisObj.debouncingStatus = true;
9261								thisObj.statusTimeout = setTimeout(function () {
9262									thisObj.debouncingStatus = false;
9263									thisObj.refreshControls(context);
9264								}, thisObj.statusMessageThreshold);
9265							}
9266							else if ((timestamp - thisObj.statusDebounceStart) > thisObj.statusMessageThreshold) {
9267								thisObj.$status.text(textByState[currentState]);
9268								thisObj.statusDebounceStart = null;
9269								clearTimeout(thisObj.statusTimeout);
9270								thisObj.statusTimeout = null;
9271							}
9272						}
9273						else {
9274							thisObj.statusDebounceStart = null;
9275							thisObj.debouncingStatus = false;
9276							clearTimeout(thisObj.statusTimeout);
9277							thisObj.statusTimeout = null;
9278						}
9279						// Don't change play/pause button display while using the seek bar (or if YouTube stopped)
9280						if (!thisObj.seekBar.tracking && !thisObj.stoppingYouTube) {
9281							if (currentState === 'paused' || currentState === 'stopped' || currentState === 'ended') {
9282								thisObj.$playpauseButton.attr('aria-label',thisObj.tt.play);
9283
9284								if (thisObj.iconType === 'font') {
9285									thisObj.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
9286									thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.play);
9287								}
9288								else if (thisObj.iconType === 'svg') {
9289									newSvgData = thisObj.getSvgData('play');
9290									thisObj.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
9291									thisObj.$playpauseButton.find('path').attr('d',newSvgData[1]);
9292									thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.play);
9293								}
9294								else {
9295									thisObj.$playpauseButton.find('img').attr('src',thisObj.playButtonImg);
9296								}
9297							}
9298							else {
9299								thisObj.$playpauseButton.attr('aria-label',thisObj.tt.pause);
9300
9301								if (thisObj.iconType === 'font') {
9302									thisObj.$playpauseButton.find('span').first().removeClass('icon-play').addClass('icon-pause');
9303									thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.pause);
9304								}
9305								else if (thisObj.iconType === 'svg') {
9306									newSvgData = thisObj.getSvgData('pause');
9307									thisObj.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
9308									thisObj.$playpauseButton.find('path').attr('d',newSvgData[1]);
9309									thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.pause);
9310								}
9311								else {
9312									thisObj.$playpauseButton.find('img').attr('src',thisObj.pauseButtonImg);
9313								}
9314							}
9315						}
9316					});
9317				}
9318			}
9319		}
9320
9321		// Show/hide status bar content conditionally
9322		if (!this.fullscreen) {
9323			statusBarWidthBreakpoint = 300;
9324			statusBarHeight = this.$statusBarDiv.height();
9325			speedHeight = this.$statusBarDiv.find('span.able-speed').height();
9326			if (speedHeight > (statusBarHeight + 5)) {
9327				// speed bar is wrapping (happens often in German player)
9328				this.$statusBarDiv.find('span.able-speed').hide();
9329				this.hidingSpeed = true;
9330			}
9331			else {
9332				if (this.hidingSpeed) {
9333					this.$statusBarDiv.find('span.able-speed').show();
9334					this.hidingSpeed = false;
9335				}
9336				if (this.$statusBarDiv.width() < statusBarWidthBreakpoint) {
9337					// Player is too small for a speed span
9338					this.$statusBarDiv.find('span.able-speed').hide();
9339					this.hidingSpeed = true;
9340				}
9341				else {
9342					if (this.hidingSpeed) {
9343						this.$statusBarDiv.find('span.able-speed').show();
9344						this.hidingSpeed = false;
9345					}
9346				}
9347			}
9348		}
9349
9350	};
9351
9352	AblePlayer.prototype.getHiddenWidth = function($el) {
9353
9354		// jQuery returns for width() if element is hidden
9355		// this function is a workaround
9356
9357		// save a reference to a cloned element that can be measured
9358		var $hiddenElement = $el.clone().appendTo('body');
9359
9360		// calculate the width of the clone
9361		var width = $hiddenElement.outerWidth();
9362
9363		// remove the clone from the DOM
9364		$hiddenElement.remove();
9365
9366		return width;
9367	};
9368
9369	AblePlayer.prototype.handlePlay = function(e) {
9370
9371		if (this.paused) {
9372			// user clicked play 
9373			this.okToPlay = true; 
9374			this.playMedia();
9375			if (this.synth.paused) { 
9376				// media was paused while description was speaking 
9377				// resume utterance 
9378				this.synth.resume(); 
9379			}
9380		}
9381		else {
9382			// user clicked pause
9383			this.okToPlay = false; 
9384			this.pauseMedia();
9385			if (this.speakingDescription) { 
9386				// pause the current utterance 
9387				// it will resume when the user presses play 
9388				this.synth.pause();				
9389			}
9390		}
9391		if (this.speechEnabled === null) { 			
9392			this.initSpeech('play'); 
9393		}
9394	};
9395
9396	AblePlayer.prototype.handleRestart = function() {
9397
9398		if (this.speakingDescription) { 
9399			// cancel audio description 
9400			this.synth.cancel();				
9401		}			
9402		this.seekTo(0);
9403	};
9404
9405	AblePlayer.prototype.handlePrevTrack = function() {
9406
9407		if (this.playlistIndex === 0) {
9408			// currently on the first track
9409			// wrap to bottom and play the last track
9410			this.playlistIndex = this.$playlist.length - 1;
9411		}
9412		else {
9413			this.playlistIndex--;
9414		}
9415		this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
9416		this.cuePlaylistItem(this.playlistIndex);
9417	};
9418
9419	AblePlayer.prototype.handleNextTrack = function() {
9420
9421		if (this.playlistIndex === this.$playlist.length - 1) {
9422			// currently on the last track
9423			// wrap to top and play the forst track
9424			this.playlistIndex = 0;
9425		}
9426		else {
9427			this.playlistIndex++;
9428		}
9429		this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
9430		this.cuePlaylistItem(this.playlistIndex);
9431	};
9432
9433	AblePlayer.prototype.handleRewind = function() {
9434
9435		var targetTime;
9436
9437		targetTime = this.elapsed - this.seekInterval;
9438		if (this.useChapterTimes) {
9439			if (targetTime < this.currentChapter.start) {
9440				targetTime = this.currentChapter.start;
9441			}
9442		}
9443		else {
9444			if (targetTime < 0) {
9445				targetTime = 0;
9446			}
9447		}
9448		this.seekTo(targetTime);
9449	};
9450
9451	AblePlayer.prototype.handleFastForward = function() {
9452
9453		var targetTime, lastChapterIndex;
9454
9455		lastChapterIndex = this.chapters.length-1;
9456		targetTime = this.elapsed + this.seekInterval;
9457		if (this.useChapterTimes) {
9458			if (this.chapters[lastChapterIndex] == this.currentChapter) {
9459				// this is the last chapter
9460				if (targetTime > this.duration || targetTime > this.currentChapter.end) {
9461					// targetTime would exceed the end of the video (or chapter)
9462					// scrub to end of whichever is earliest
9463					targetTime = Math.min(this.duration, this.currentChapter.end);
9464				}
9465				else if (this.duration % targetTime < this.seekInterval) {
9466					// nothing left but pocket change after seeking to targetTime
9467					// go ahead and seek to end of video (or chapter), whichever is earliest
9468					targetTime = Math.min(this.duration, this.currentChapter.end);
9469				}
9470			}
9471			else {
9472				// this is not the last chapter
9473				if (targetTime > this.currentChapter.end) {
9474					// targetTime would exceed the end of the chapter
9475					// scrub exactly to end of chapter
9476					targetTime = this.currentChapter.end;
9477				}
9478			}
9479		}
9480		else {
9481			// not using chapter times
9482			if (targetTime > this.duration) {
9483				targetTime = this.duration;
9484			}
9485		}
9486		this.seekTo(targetTime);
9487	};
9488
9489	AblePlayer.prototype.handleRateIncrease = function() {
9490		this.changeRate(1);
9491	};
9492
9493	AblePlayer.prototype.handleRateDecrease = function() {
9494		this.changeRate(-1);
9495	};
9496
9497	// Increases or decreases playback rate, where dir is 1 or -1 indication direction.
9498	AblePlayer.prototype.changeRate = function (dir) {
9499
9500		var rates, currentRate, index, newRate, vimeoMin, vimeoMax;
9501
9502		if (this.player === 'html5') {
9503			this.setPlaybackRate(this.getPlaybackRate() + (0.25 * dir));
9504		}
9505		else if (this.player === 'youtube') {
9506			if (this.youTubePlayerReady) {
9507				rates = this.youTubePlayer.getAvailablePlaybackRates();
9508				currentRate = this.getPlaybackRate();
9509				index = rates.indexOf(currentRate);
9510				if (index === -1) {
9511					console.log('ERROR: Youtube returning unknown playback rate ' + currentRate.toString());
9512				}
9513				else {
9514					index += dir;
9515					// Can only increase or decrease rate if there's another rate available.
9516					if (index < rates.length && index >= 0) {
9517						this.setPlaybackRate(rates[index]);
9518					}
9519				}
9520			}
9521		}
9522		else if (this.player === 'vimeo') {
9523			// range is 0.5 to 2
9524			// increase/decrease in inrements of 0.5
9525			vimeoMin = 0.5;
9526			vimeoMax = 2;
9527			if (dir === 1) {
9528				if (this.vimeoPlaybackRate + 0.5 <= vimeoMax) {
9529					newRate = this.vimeoPlaybackRate + 0.5;
9530				}
9531				else {
9532					newRate = vimeoMax;
9533				}
9534			}
9535			else if (dir === -1) {
9536				if (this.vimeoPlaybackRate - 0.5 >= vimeoMin) {
9537					newRate = this.vimeoPlaybackRate - 0.5;
9538				}
9539				else {
9540					newRate = vimeoMin;
9541				}
9542			}
9543			this.setPlaybackRate(newRate);
9544		}
9545	};
9546
9547	AblePlayer.prototype.handleCaptionToggle = function() {
9548
9549		var thisObj = this;
9550		var captions;
9551		if (this.hidingPopup) {
9552			// stopgap to prevent spacebar in Firefox from reopening popup
9553			// immediately after closing it
9554			this.hidingPopup = false;
9555			return false;
9556		}
9557		if (this.captions.length) {
9558			captions = this.captions;
9559		}
9560		else {
9561			captions = [];
9562		}
9563		if (captions.length === 1) {
9564			// When there's only one set of captions, just do an on/off toggle.
9565			if (this.captionsOn === true) {
9566				// turn them off
9567				this.captionsOn = false;
9568				this.prefCaptions = 0;
9569				this.$ccButton.attr('aria-pressed', 'false');
9570				this.updateCookie('prefCaptions');
9571				if (this.usingYouTubeCaptions) {
9572					this.youTubePlayer.unloadModule('captions');
9573				}
9574				else if (this.usingVimeoCaptions) { 
9575					this.vimeoPlayer.disableTextTrack(); 
9576				}
9577				else {
9578					this.$captionsWrapper.hide();
9579				}
9580			}
9581			else {
9582				// captions are off. Turn them on.
9583				this.captionsOn = true;
9584				this.prefCaptions = 1;
9585				this.$ccButton.attr('aria-pressed', 'true');
9586				this.updateCookie('prefCaptions');
9587				if (this.usingYouTubeCaptions) {
9588					this.youTubePlayer.loadModule('captions');
9589				}
9590				else if (this.usingVimeoCaptions) { 
9591					this.vimeoPlayer.enableTextTrack(this.captionLang).then(function(track) {
9592						// track.language = the iso code for the language
9593						// track.kind = 'captions' or 'subtitles'
9594						// track.label = the human-readable label
9595					}).catch(function(error) {
9596						switch (error.name) {
9597							case 'InvalidTrackLanguageError':
9598								// no track was available with the specified language
9599								console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
9600								break;
9601							case 'InvalidTrackError':
9602								// no track was available with the specified language and kind
9603								console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
9604								break;
9605							default:
9606								// some other error occurred
9607								console.log('Error loading ' + track.label + ' ' + track.kind + ' track');
9608								break;
9609							}
9610					});	
9611				}
9612				else {
9613					this.$captionsWrapper.show();
9614				}
9615				for (var i=0; i<captions.length; i++) {
9616					if (captions[i].def === true) { // this is the default language
9617						this.selectedCaptions = captions[i];
9618					}
9619				}
9620				this.selectedCaptions = this.captions[0];
9621				if (this.descriptions.length >= 0) {
9622					this.selectedDescriptions = this.descriptions[0];
9623				}
9624			}
9625			this.refreshControls('captions');
9626		}
9627		else {
9628			// there is more than one caption track.
9629			// clicking on a track is handled via caption.js > getCaptionClickFunction()
9630			if (this.captionsPopup && this.captionsPopup.is(':visible')) {
9631				this.captionsPopup.hide();
9632				this.hidingPopup = false;
9633				this.$ccButton.attr('aria-expanded', 'false')
9634				this.waitThenFocus(this.$ccButton);
9635			}
9636			else {
9637				this.closePopups();
9638				if (this.captionsPopup) {
9639					this.captionsPopup.show();
9640					this.$ccButton.attr('aria-expanded','true');
9641
9642					// Gives time to "register" expanded ccButton
9643					setTimeout(function() {
9644						thisObj.captionsPopup.css('top', thisObj.$ccButton.position().top - thisObj.captionsPopup.outerHeight());
9645						thisObj.captionsPopup.css('left', thisObj.$ccButton.position().left)
9646						// Place focus on the first button (even if another button is checked)
9647						thisObj.captionsPopup.find('li').removeClass('able-focus');
9648						thisObj.captionsPopup.find('li').first().focus().addClass('able-focus');
9649					}, 50);
9650				}
9651			}
9652		}
9653	};
9654
9655	/**
9656	 * Gives enough time for DOM changes to take effect before adjusting focus.
9657	 * Helpful for allowing screen reading of elements whose state is intermittently changed.
9658	 * 
9659	 * @param {*} $el element to focus on
9660	 * @param {*} timeout optional wait time in milliseconds before focus
9661	 */
9662	AblePlayer.prototype.waitThenFocus = function($el, timeout) {
9663
9664		// Default wait time of 50 ms
9665		var _timeout = (timeout === undefined || timeout === null) ? 50 : timeout;
9666		
9667		setTimeout(function() {
9668			$el.focus();
9669		}, _timeout);
9670	}
9671
9672	AblePlayer.prototype.handleChapters = function () {
9673		if (this.hidingPopup) {
9674			// stopgap to prevent spacebar in Firefox from reopening popup
9675			// immediately after closing it
9676			this.hidingPopup = false;
9677			return false;
9678		}
9679		if (this.chaptersPopup.is(':visible')) {
9680			this.chaptersPopup.hide();
9681			this.hidingPopup = false;
9682			this.$chaptersButton.attr('aria-expanded','false').focus();
9683		}
9684		else {
9685			this.closePopups();
9686			this.chaptersPopup.show();
9687			this.$chaptersButton.attr('aria-expanded','true');
9688			this.chaptersPopup.css('top', this.$chaptersButton.position().top - this.chaptersPopup.outerHeight());
9689			this.chaptersPopup.css('left', this.$chaptersButton.position().left)
9690
9691			// Highlight the current chapter, if any chapters are checked
9692			// Otherwise, place focus on the first chapter
9693			this.chaptersPopup.find('li').removeClass('able-focus');
9694			if (this.chaptersPopup.find('li[aria-checked="true"]').length) {
9695				this.chaptersPopup.find('li[aria-checked="true"]').focus().addClass('able-focus');
9696			}
9697			else {
9698				this.chaptersPopup.find('li').first().addClass('able-focus').attr('aria-checked','true').focus();
9699			}
9700		}
9701	};
9702
9703	AblePlayer.prototype.handleDescriptionToggle = function() {
9704
9705		this.descOn = !this.descOn;
9706		this.prefDesc = + this.descOn; // convert boolean to integer
9707		this.updateCookie('prefDesc');
9708		if (typeof this.$descDiv !== 'undefined') {
9709			if (!this.$descDiv.is(':hidden')) {
9710				this.$descDiv.hide();
9711			}
9712			// NOTE: now showing $descDiv here if previously hidden 
9713			// that's handled elsewhere, dependent on whether there's text to show
9714		}
9715		this.initDescription();
9716		this.refreshControls('descriptions');
9717	};
9718
9719	AblePlayer.prototype.handlePrefsClick = function(pref) {
9720
9721		// NOTE: the prefs menu is positioned near the right edge of the player
9722		// This assumes the Prefs button is also positioned in that vicinity
9723		// (last or second-last button the right)
9724
9725		// NOTE: If previously unable to fully populate the Description dialog
9726		// because the Web Speech API failed to getVoices()
9727		// now is a good time to try again
9728		// so the Description dialog can be rebuilt before the user requests it
9729
9730		var thisObj, prefsButtonPosition, prefsMenuRight, prefsMenuLeft;
9731
9732		thisObj = this;
9733
9734		if (this.speechEnabled === null) { 			
9735			this.initSpeech('prefs'); 
9736		}
9737		if (this.hidingPopup) {
9738			// stopgap to prevent spacebar in Firefox from reopening popup
9739			// immediately after closing it
9740			this.hidingPopup = false;
9741			return false;
9742		}
9743		if (this.prefsPopup.is(':visible')) {
9744			this.prefsPopup.hide();
9745			this.$prefsButton.attr('aria-expanded','false');
9746			// restore each menu item to original hidden state
9747			this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
9748			if (!this.showingPrefsDialog) {
9749				this.$prefsButton.focus();
9750			}
9751			// wait briefly, then reset hidingPopup
9752			setTimeout(function() {
9753				thisObj.hidingPopup = false;
9754			},100);
9755		}
9756		else {
9757			this.closePopups();
9758			this.prefsPopup.show();
9759			this.$prefsButton.attr('aria-expanded','true');
9760			this.$prefsButton.focus(); // focus first on prefs button to announce expanded state
9761			// give time for focus on button then adjust popup settings and focus
9762			setTimeout(function() {
9763				prefsButtonPosition = thisObj.$prefsButton.position();
9764				prefsMenuRight = thisObj.$ableDiv.width() - 5;
9765				prefsMenuLeft = prefsMenuRight - thisObj.prefsPopup.width();
9766				thisObj.prefsPopup.css('top', prefsButtonPosition.top - thisObj.prefsPopup.outerHeight());
9767				thisObj.prefsPopup.css('left', prefsMenuLeft);
9768				// remove prior focus and set focus on first item; also change tabindex from -1 to 0
9769				thisObj.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','0');
9770				thisObj.prefsPopup.find('li').first().focus().addClass('able-focus');
9771			}, 50);
9772		}
9773	};
9774
9775	AblePlayer.prototype.handleHelpClick = function() {
9776		this.setFullscreen(false);
9777		this.helpDialog.show();
9778	};
9779
9780	AblePlayer.prototype.handleTranscriptToggle = function () {
9781
9782		var thisObj = this;
9783
9784		if (this.$transcriptDiv.is(':visible')) {
9785			this.$transcriptArea.hide();
9786			this.$transcriptButton.addClass('buttonOff').attr('aria-label',this.tt.showTranscript);
9787			
9788			this.$transcriptButton.find('span.able-clipped').text(this.tt.showTranscript);
9789			this.prefTranscript = 0;
9790			this.$transcriptButton.focus().addClass('able-focus');
9791			// wait briefly before resetting stopgap var
9792			// otherwise the keypress used to select 'Close' will trigger the transcript button
9793			// Benchmark tests: If this is gonna happen, it typically happens in around 3ms; max 12ms
9794			// Setting timeout to 100ms is a virtual guarantee of proper functionality
9795			setTimeout(function() {
9796				thisObj.closingTranscript = false;
9797			}, 100);
9798		}
9799		else {
9800			
9801			this.positionDraggableWindow('transcript');
9802			this.$transcriptArea.show();
9803			// showing transcriptArea has a cascading effect of showing all content *within* transcriptArea
9804			// need to re-hide the popup menu
9805			this.$transcriptPopup.hide();
9806			this.$transcriptButton.removeClass('buttonOff').attr('aria-label',this.tt.hideTranscript);
9807			this.$transcriptButton.find('span.able-clipped').text(this.tt.hideTranscript);
9808			this.prefTranscript = 1;
9809			// move focus to first focusable element (window options button)
9810			this.focusNotClick = true;
9811			this.$transcriptArea.find('button').first().focus();
9812			// wait briefly before resetting stopgap var
9813			setTimeout(function() {
9814				thisObj.focusNotClick = false;
9815			}, 100);
9816		}
9817		this.updateCookie('prefTranscript');
9818	};
9819
9820	AblePlayer.prototype.handleSignToggle = function () {
9821
9822		var thisObj = this;
9823
9824		if (this.$signWindow.is(':visible')) {
9825			this.$signWindow.hide();
9826			this.$signButton.addClass('buttonOff').attr('aria-label',this.tt.showSign);
9827			this.$signButton.find('span.able-clipped').text(this.tt.showSign);
9828			this.prefSign = 0;
9829			this.$signButton.focus().addClass('able-focus');
9830			// wait briefly before resetting stopgap var
9831			// otherwise the keypress used to select 'Close' will trigger the transcript button
9832			setTimeout(function() {
9833				thisObj.closingSign = false;
9834			}, 100);
9835		}
9836		else {
9837			this.positionDraggableWindow('sign');
9838			this.$signWindow.show();
9839			// showing signWindow has a cascading effect of showing all content *within* signWindow
9840			// need to re-hide the popup menu
9841			this.$signPopup.hide();
9842			this.$signButton.removeClass('buttonOff').attr('aria-label',this.tt.hideSign);
9843			this.$signButton.find('span.able-clipped').text(this.tt.hideSign);
9844			this.prefSign = 1;
9845			this.focusNotClick = true;
9846			this.$signWindow.find('button').first().focus();
9847			// wait briefly before resetting stopgap var
9848			// otherwise the keypress used to select 'Close' will trigger the transcript button
9849			setTimeout(function() {
9850				thisObj.focusNotClick = false;
9851			}, 100);
9852		}
9853		this.updateCookie('prefSign');
9854	};
9855
9856	AblePlayer.prototype.isFullscreen = function () {
9857
9858		// NOTE: This has been largely replaced as of 3.2.5 with a Boolean this.fullscreen,
9859		// which is defined in setFullscreen()
9860		// This function returns true if *any* element is fullscreen
9861		// but doesn't tell us whether a particular element is in fullscreen
9862		// (e.g., if there are multiple players on the page)
9863		// The Boolean this.fullscreen is defined separately for each player instance
9864
9865		if (this.nativeFullscreenSupported()) {
9866			return (document.fullscreenElement ||
9867							document.webkitFullscreenElement ||
9868							document.webkitCurrentFullscreenElement ||
9869							document.mozFullscreenElement ||
9870							document.msFullscreenElement) ? true : false;
9871		}
9872		else {
9873			return this.modalFullscreenActive ? true : false;
9874		}
9875	}
9876
9877	AblePlayer.prototype.setFullscreen = function (fullscreen) {
9878
9879		if (this.fullscreen == fullscreen) {
9880			// replace isFullscreen() with a Boolean. see function for explanation
9881			return;
9882		}
9883		var thisObj = this;
9884		var $el = this.$ableWrapper;
9885		var el = $el[0];
9886
9887		if (this.nativeFullscreenSupported()) {
9888			// Note: many varying names for options for browser compatibility.
9889			if (fullscreen) {
9890				// Initialize fullscreen
9891
9892				if (el.requestFullscreen) {
9893					el.requestFullscreen();
9894				}
9895				else if (el.webkitRequestFullscreen) {
9896					el.webkitRequestFullscreen();
9897				}
9898				else if (el.mozRequestFullscreen) {
9899					el.mozRequestFullscreen();
9900				}
9901				else if (el.msRequestFullscreen) {
9902					el.msRequestFullscreen();
9903				}
9904				this.fullscreen = true;
9905			}
9906			else {
9907				// Exit fullscreen
9908				this.restoringAfterFullscreen = true; 
9909				if (document.exitFullscreen) {
9910					document.exitFullscreen();
9911				}
9912				else if (document.webkitExitFullscreen) {
9913					document.webkitExitFullscreen();
9914				}
9915				else if (document.webkitCancelFullscreen) {
9916					document.webkitCancelFullscreen();
9917				}
9918				else if (document.mozCancelFullscreen) {
9919					document.mozCancelFullscreen();
9920				}
9921				else if (document.msExitFullscreen) {
9922					document.msExitFullscreen();
9923				}
9924				this.fullscreen = false;
9925			}
9926		}
9927		else {
9928			// Non-native fullscreen support through modal dialog.
9929			// Create dialog on first run through.
9930			if (!this.fullscreenDialog) {
9931				var $dialogDiv = $('<div>');
9932				// create a hidden alert, communicated to screen readers via aria-describedby
9933				var $fsDialogAlert = $('<p>',{
9934					'class': 'able-screenreader-alert'
9935				}).text(this.tt.fullscreen); // In English: "Full screen"; TODO: Add alert text that is more descriptive
9936				$dialogDiv.append($fsDialogAlert);
9937				// now render this as a dialog
9938				this.fullscreenDialog = new AccessibleDialog($dialogDiv, this.$fullscreenButton, 'dialog', true, 'Fullscreen video player', $fsDialogAlert, this.tt.exitFullscreen, '100%', true, function () { thisObj.handleFullscreenToggle() });
9939				$('body').append($dialogDiv);
9940			}
9941
9942			// Track whether paused/playing before moving element; moving the element can stop playback.
9943			var wasPaused = this.paused;
9944
9945			if (fullscreen) {
9946				this.modalFullscreenActive = true;
9947				this.fullscreenDialog.show();
9948
9949				// Move player element into fullscreen dialog, then show.
9950				// Put a placeholder element where player was.
9951				this.$modalFullscreenPlaceholder = $('<div class="placeholder">');
9952				this.$modalFullscreenPlaceholder.insertAfter($el);
9953				$el.appendTo(this.fullscreenDialog.modal);
9954
9955				// Column left css is 50% by default; set to 100% for full screen.
9956				if ($el === this.$ableColumnLeft) {
9957					$el.width('100%');
9958				}
9959				var newHeight = $(window).height() - this.$playerDiv.height();
9960				if (typeof this.$descDiv !== 'undefined') {				
9961					if (!this.$descDiv.is(':hidden')) {
9962						newHeight -= this.$descDiv.height();
9963					}
9964				}
9965			}
9966			else {
9967				this.modalFullscreenActive = false;
9968				if ($el === this.$ableColumnLeft) {
9969					$el.width('50%');
9970				}
9971				$el.insertAfter(this.$modalFullscreenPlaceholder);
9972				this.$modalFullscreenPlaceholder.remove();
9973				this.fullscreenDialog.hide();
9974			}
9975
9976			// Resume playback if moving stopped it.
9977			if (!wasPaused && this.paused) {
9978				this.playMedia();
9979			}
9980		}
9981		// add event handlers for changes in fullscreen mode. 
9982		// Browsers natively trigger this event with the Escape key,  
9983		// in addition to clicking the exit fullscreen button 
9984		$(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange', function(e) {
9985			// NOTE: e.type = the specific event that fired (in case needing to control for browser-specific idiosyncrasies)
9986			if (!thisObj.fullscreen) {
9987				// user has just exited full screen
9988				thisObj.restoringAfterFullscreen = true;
9989			}
9990			else if (!thisObj.clickedFullscreenButton) {
9991				// user triggered fullscreenchange without clicking fullscreen button
9992				thisObj.fullscreen = false;
9993				thisObj.restoringAfterFullscreen = true;
9994			}
9995			thisObj.resizePlayer();
9996			thisObj.refreshControls('fullscreen');
9997
9998			// NOTE: The fullscreenchange (or browser-equivalent) event is triggered twice
9999			// when exiting fullscreen via the "Exit fullscreen" button (only once if using Escape)
10000			// Not sure why, but consequently we need to be sure thisObj.clickedFullscreenButton
10001			// continues to be true through both events
10002			// Could use a counter variable to control that (reset to false after the 2nd trigger)
10003			// However, since I don't know why it's happening, and whether it's 100% reliable
10004			// resetting clickedFullscreenButton after a timeout seems to be better approach
10005			setTimeout(function() {
10006				thisObj.clickedFullscreenButton = false;
10007				thisObj.restoringAfterFullscreen = false; 
10008			},1000);
10009		});		
10010	};
10011
10012	AblePlayer.prototype.handleFullscreenToggle = function () {
10013
10014		var stillPaused = this.paused;
10015		this.setFullscreen(!this.fullscreen);
10016		if (stillPaused) {
10017			this.pauseMedia(); // when toggling fullscreen and media is just paused, keep media paused.
10018		}
10019		else if (!stillPaused) {
10020			this.playMedia(); // when toggling fullscreen and media is playing, continue playing.
10021		}
10022		// automatically hide controller in fullscreen mode
10023		// then reset back to original setting after exiting fullscreen mode
10024		if (this.fullscreen) {
10025			this.hideControls = true;
10026			if (this.playing) {
10027				// go ahead and hide the controls
10028				this.fadeControls('out');
10029				this.controlsHidden = true;
10030			}
10031		}
10032		else {
10033			// exit fullscreen mode
10034			this.hideControls = this.hideControlsOriginal;
10035			if (!this.hideControls) { // do not hide controls
10036				if (this.controlsHidden) {
10037					this.fadeControls('in');
10038					this.controlsHidden = false;
10039				}
10040				// if there's an active timeout to fade controls out again, clear it
10041				if (this.hideControlsTimeoutStatus === 'active') {
10042					window.clearTimeout(this.hideControlsTimeout);
10043					this.hideControlsTimeoutStatus = 'clear';
10044				}
10045			}
10046		}
10047		// don't resizePlayer yet; that will be called in response to the window resize event 
10048		// this.resizePlayer();
10049	};
10050
10051	AblePlayer.prototype.handleTranscriptLockToggle = function (val) {
10052
10053		this.autoScrollTranscript = val; // val is boolean
10054		this.prefAutoScrollTranscript = +val; // convert boolean to numeric 1 or 0 for cookie
10055		this.updateCookie('prefAutoScrollTranscript');
10056		this.refreshControls('transcript');
10057	};
10058
10059
10060	AblePlayer.prototype.showTooltip = function($tooltip) {
10061
10062		if (($tooltip).is(':animated')) {
10063			$tooltip.stop(true,true).show();
10064		}
10065		else {
10066			$tooltip.stop().show();
10067		}
10068	};
10069
10070	AblePlayer.prototype.showAlert = function( msg, location ) {
10071
10072		// location is either of the following:
10073		// 'main' (default)
10074		// 'screenreader (visibly hidden)
10075		// 'sign' (sign language window)
10076		// 'transcript' (trasncript window)
10077		var thisObj, $alertBox, $parentWindow, alertLeft, alertTop;
10078
10079		thisObj = this;
10080
10081		if (location === 'transcript') {
10082			$alertBox = this.$transcriptAlert;
10083			$parentWindow = this.$transcriptArea;
10084		}
10085		else if (location === 'sign') {
10086			$alertBox = this.$signAlert;
10087			$parentWindow = this.$signWindow;
10088		}
10089		else if (location === 'screenreader') {
10090			$alertBox = this.$srAlertBox;
10091		}
10092		else {
10093			$alertBox = this.$alertBox;
10094		}
10095		$alertBox.text(msg).show();
10096		if (location == 'transcript' || location === 'sign') {
10097			if ($parentWindow.width() > $alertBox.width()) {
10098				alertLeft = $parentWindow.width() / 2 - $alertBox.width() / 2;
10099			}
10100			else {
10101				// alert box is wider than its container. Position it far left and let it wrap
10102				alertLeft = 10;
10103			}
10104			if (location === 'sign') {
10105				// position alert in the lower third of the sign window (to avoid covering the signer)
10106				alertTop = ($parentWindow.height() / 3) * 2;
10107			}
10108			else if (location === 'transcript') {
10109				// position alert just beneath the toolbar to avoid getting lost among transcript text
10110				alertTop = this.$transcriptToolbar.height() + 30;
10111			}
10112			$alertBox.css({
10113				top: alertTop + 'px',
10114				left: alertLeft + 'px'
10115			});
10116		}
10117		else if (location !== 'screenreader') {
10118			// The original formula incorporated offset() into the calculation
10119			// but at some point this began resulting in an alert that's off-centered
10120			// Changed in v2.2.17, but here's the original for reference in case needed:
10121			// left: this.$playerDiv.offset().left + (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
10122			$alertBox.css({
10123				left: (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
10124			});
10125		}
10126		if (location !== 'screenreader') {
10127			setTimeout(function () {
10128				$alertBox.fadeOut(300);
10129			}, 3000);
10130		}
10131	};
10132
10133	AblePlayer.prototype.showedAlert = function (which) {
10134
10135		// returns true if the target alert has already been shown
10136		// useful for throttling alerts that only need to be shown once
10137		// e.g., move alerts with instructions for dragging a window
10138
10139		if (which === 'transcript') {
10140			if (this.showedTranscriptAlert) {
10141				return true;
10142			}
10143			else {
10144				return false;
10145			}
10146		}
10147		else if (which === 'sign') {
10148			if (this.showedSignAlert) {
10149				return true;
10150			}
10151			else {
10152				return false;
10153			}
10154		}
10155		return false;
10156	}
10157
10158	// Resizes all relevant player attributes.
10159	AblePlayer.prototype.resizePlayer = function (width, height) {
10160
10161		var captionSizeOkMin, captionSizeOkMax, captionSize, newCaptionSize, newLineHeight;
10162
10163		var newWidth, newHeight, $iframe, alertTop; 
10164
10165		if (this.mediaType === 'audio') { 
10166			return; 
10167		}
10168
10169		if (typeof width !== 'undefined' && typeof height !== 'undefined') { 
10170			// this is being called the first time a player is initialized 
10171			// width and height were collected from the HTML, YouTube, or Vimeo media API
10172			// so are reflective of the actual size of the media 
10173			// use these values to calculate aspectRatio 
10174			this.aspectRatio = height / width;  
10175			if (this.playerWidth) { 
10176				// default width is already defined via a width or data-width attribute. Use that. 				
10177				newWidth = this.playerWidth; 
vendor: 2,991 bytes, lines 10178-10278
10178				if (this.playerHeight) { 
10179					newHeight = this.playerHeight; 
10180				}
10181				else { 
10182					newHeight = Math.round(newWidth * this.aspectRatio); 
10183					this.playerHeight = newHeight; 
10184				}
10185			}
10186			else { 
10187				// playerWidth was not defined via HTML attributes 
10188				if (this.player === 'html5') { 
10189					newWidth = $(window).width();
10190				}
10191				else { 
10192					newWidth = this.$ableWrapper.width(); 
10193				}
10194				newHeight = Math.round(newWidth * this.aspectRatio); 
10195			}				
10196		}			
10197		else if (this.fullscreen) { 
10198			this.$ableWrapper.addClass('fullscreen');  
10199			newWidth = $(window).width();			
10200			// the 5 pixel buffer is arbitrary, but results in a better fit for all browsers
10201			newHeight = $(window).height() - this.$playerDiv.outerHeight() - 5; 
10202			this.positionCaptions('overlay');
10203		}
10204		else { // not fullscreen, and not first time initializing player 			
10205			this.$ableWrapper.removeClass('fullscreen');
10206			if (this.player === 'html5') { 
10207				if (this.playerWidth) { 
10208					newWidth = this.playerWidth; 
10209				}
10210				else {
10211					// use full size of window 
10212					// player will be downsized to fit container if CSS requires it 
10213					newWidth = $(window).width();
10214				}
10215			}
10216			else { 
10217				newWidth = this.$ableWrapper.width(); 
10218			}
10219			newHeight = Math.round(newWidth * this.aspectRatio); 
10220			this.positionCaptions(this.prefCaptionsPosition); 
10221		}
10222		if (this.debug) {
10223			console.log('resizePlayer to ' + newWidth + 'x' + newHeight); 		
10224		}
10225		// Now size the player with newWidth and newHeight
10226		if (this.player === 'youtube' || this.player === 'vimeo') { 
10227			$iframe = this.$ableWrapper.find('iframe'); 
10228			if (this.player === 'youtube' && this.youTubePlayer) { 
10229				// alternatively, YouTube API offers a method for setting the video size 
10230				// this adds width and height attributes to the iframe 
10231				// but might have other effects, so best to do it this way 
10232				this.youTubePlayer.setSize(newWidth,newHeight); 
10233			}
10234			else { 
10235				// Vimeo API does not have a method for changing size of player 
10236				// Therefore, need to change iframe attributes directly 
10237				$iframe.attr({
10238					'width': newWidth,
10239					'height': newHeight
10240				}); 
10241			}
10242			if (this.playerWidth && this.playerHeight) { 
10243				if (this.fullscreen) { 
10244					// remove constraints 
10245					$iframe.css({ 
10246						'max-width': '',
10247						'max-height': ''
10248					});	
10249				}
10250				else {
10251					// use CSS on iframe to enforce explicitly defined size constraints
10252					$iframe.css({ 
10253						'max-width': this.playerWidth + 'px',
10254						'max-height': this.playerHeight + 'px'
10255					});
10256				}
10257			}
10258		}
10259		else if (this.player === 'html5') { 
10260			if (this.fullscreen) {
10261				this.$media.attr({
10262					'width': newWidth,
10263					'height': newHeight
10264				});
10265				this.$ableWrapper.css({
10266					'width': newWidth,
10267					'height': newHeight
10268				});
10269			}
10270			else { 
10271				// No constraints. Let CSS handle the positioning. 
10272				this.$media.removeAttr('width height');
10273				this.$ableWrapper.css({
10274					'width': newWidth + 'px',
10275					'height': 'auto'
10276				});
10277			}				
10278		}
10279		// Resize captions
10280		if (typeof this.$captionsDiv !== 'undefined') {
10281
10282			// Font-size is too small in full screen view 
10283			// use viewport units (vw) instead
10284			// % units work fine if not fullscreen  
10285			// prefCaptionSize is expressed as a percentage 
10286			captionSize = parseInt(this.prefCaptionsSize,10);
10287			if (this.fullscreen) { 
10288				captionSize = (captionSize / 100) + 'vw'; 
10289			}
10290			else { 
10291				captionSize = captionSize + '%'; 
10292			}
10293			this.$captionsDiv.css({
10294				'font-size': captionSize
10295			});
10296		}
10297
10298		// Reposition alert message (video player only)
10299		// just below the vertical center of the mediaContainer
10300		// hopefully above captions, but not too far from the controller bar		
10301		if (this.mediaType === 'video') { 
10302			alertTop = Math.round(this.$mediaContainer.height() / 3) * 2;			
10303			this.$alertBox.css({
10304				top: alertTop + 'px'
10305			});
10306		}
10307
10308		this.refreshControls();			
10309	};
10310
10311	AblePlayer.prototype.retrieveOffscreenWindow = function( which, width, height ) {
10312
10313		// check to be sure popup windows ('transcript' or 'sign') are positioned on-screen
10314		// (they sometimes disappear off-screen when entering fullscreen mode)
10315		// if off-screen, recalculate so they are back on screen
10316
10317		var window, windowPos, windowTop, windowLeft, windowRight, windowWidth, windowBottom, windowHeight;
10318
10319		if (which == 'transcript') {
10320			window = this.$transcriptArea;
10321		}
10322		else if (which == 'sign') {
10323			window = this.$signWindow;
10324		}
10325		windowWidth = window.width();
10326		windowHeight = window.height();
10327		windowPos = window.position();
10328		windowTop = windowPos.top;
10329		windowLeft = windowPos.left;
10330		windowRight = windowLeft + windowWidth;
10331		windowBottom = windowTop + windowHeight;
10332
10333		if (windowTop < 0) { // off-screen to the top
10334			windowTop = 10;
10335			window.css('top',windowTop);
10336		}
10337		if (windowLeft < 0) { // off-screen to the left
10338			windowLeft = 10;
10339			window.css('left',windowLeft);
10340		}
10341		if (windowRight > width) { // off-screen to the right
10342			windowLeft = (width - 20) - windowWidth;
10343			window.css('left',windowLeft);
10344		}
10345		if (windowBottom > height) { // off-screen to the bottom
10346			windowTop = (height - 10) - windowHeight;
10347			window.css('top',windowTop);
10348		}
10349	};
10350
10351	AblePlayer.prototype.getHighestZIndex = function() {
10352
10353		// returns the highest z-index on page
10354		// used to ensure dialogs (or potentially other windows) are on top
10355
10356		var max, $elements, z;
10357		max = 0;
10358
10359		// exclude the Able Player dialogs and windows
10360		$elements = $('body *').not('.able-modal-dialog,.able-modal-dialog *,.able-modal-overlay,.able-modal-overlay *,.able-sign-window,.able-transcript-area');
10361
10362		$elements.each(function(){
10363			z = $(this).css('z-index');
10364			if (Number.isInteger(+z)) { // work only with integer values, not 'auto'
10365				if (parseInt(z) > max) {
10366					max = parseInt(z);
10367				}
10368			}
10369		});
10370		return max;
10371	};
10372
10373	AblePlayer.prototype.updateZIndex = function(which) {
10374
10375		// update z-index of 'transcript' or 'sign', relative to each other
10376		// direction is always 'up' (i.e., move window to top)
10377		// windows come to the top when the user clicks on them
10378		var defHighZ, defLowZ, highestZ, transcriptZ, signZ, newHighZ, newLowZ;
10379
10380		// set the default z-indexes, as defined in ableplayer.css
10381		defHighZ = 8000; // by default, assigned to the sign window
10382		defLowZ = 7000; // by default, assigned to the transcript area
10383		highestZ = this.getHighestZIndex(); // highest z-index on the page, excluding Able Player windows & modals
10384
10385		// NOTE: Although highestZ is collected here, it currently isn't used.
10386		// If something on the page has a higher z-index than the transcript or sign window, do we care?
10387		// Excluding it here assumes "No". Our immediate concern is with the relationship between our own components.
10388		// If we elevate our z-indexes so our content is on top, we run the risk of starting a z-index war.
10389
10390		if (typeof this.$transcriptArea === 'undefined' || typeof this.$signWindow === 'undefined' ) {
10391			// at least one of the windows doesn't exist, so there's no conflict
10392			// since z-index may have been stored to a cookie on another page, need to restore default
10393			if (typeof this.$transcriptArea !== 'undefined') {
10394				transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
10395				if (transcriptZ > defLowZ) {
10396					// restore to the default
10397					this.$transcriptArea.css('z-index',defLowZ);
10398				}
10399			}
10400			else if (typeof this.$signWindow !== 'undefined') {
10401				signZ = parseInt(this.$signWindow.css('z-index'));
10402				if (signZ > defHighZ) {
10403					// restore to the default
10404					this.$signWindow.css('z-index',defHighZ);
10405				}
10406			}
10407			return false;
vendor: 7,469 bytes, lines 10408-10630
10408		}
10409
10410		// both windows exist
10411
10412		// get current values
10413		transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
10414		signZ = parseInt(this.$signWindow.css('z-index'));
10415
10416		if (transcriptZ === signZ) {
10417			// the two windows are equal; restore defaults (the target window will be on top)
10418			newHighZ = defHighZ;
10419			newLowZ = defLowZ;
10420		}
10421		else if (transcriptZ > signZ) {
10422			if (which === 'transcript') {
10423				// transcript is already on top; nothing to do
10424				return false;
10425			}
10426			else {
10427				// swap z's
10428				newHighZ = transcriptZ;
10429				newLowZ = signZ;
10430			}
10431		}
10432		else { // signZ is greater
10433			if (which === 'sign') {
10434				// sign is already on top; nothing to do
10435				return false;
10436			}
10437			else {
10438				newHighZ = signZ;
10439				newLowZ = transcriptZ;
10440			}
10441		}
10442		// now assign the new values
10443		if (which === 'transcript') {
10444			this.$transcriptArea.css('z-index',newHighZ);
10445			this.$signWindow.css('z-index',newLowZ);
10446		}
10447		else if (which === 'sign') {
10448			this.$signWindow.css('z-index',newHighZ);
10449			this.$transcriptArea.css('z-index',newLowZ);
10450		}
10451	};
10452
10453	AblePlayer.prototype.syncTrackLanguages = function (source, language) {
10454
10455		// this function is called when the player is built (source == 'init')
10456		// and again when user changes the language of either 'captions' or 'transcript'
10457		// It syncs the languages of chapters, descriptions, and metadata tracks
10458		// NOTE: Caption and transcript languages are somewhat independent from one another
10459		// If a user changes the caption language, the transcript follows
10460		// However, if a user changes the transcript language, this only affects the transcript
10461		// This was a group decision based on the belief that users may want a transcript
10462		// that is in a different language than the captions
10463
10464		var i, captions, descriptions, chapters, meta, langHasChanged;
10465
10466		// Captions
10467		for (i = 0; i < this.captions.length; i++) {
10468			if (this.captions[i].language === language) {
10469				captions = this.captions[i];
10470			}
10471		}
10472		// Chapters
10473		for (i = 0; i < this.chapters.length; i++) {
10474			if (this.chapters[i].language === language) {
10475				chapters = this.chapters[i];
10476			}
10477		}
10478		// Descriptions
10479		for (i = 0; i < this.descriptions.length; i++) {
10480			if (this.descriptions[i].language === language) {
10481				descriptions = this.descriptions[i];
10482			}
10483		}
10484		// Metadata
10485		for (i = 0; i < this.meta.length; i++) {
10486			if (this.meta[i].language === language) {
10487				meta = this.meta[i];
10488			}
10489		}
10490		// regardless of source...
10491		this.transcriptLang = language;
10492		if (source === 'init' || source === 'captions') {
10493			this.captionLang = language;
10494			this.selectedCaptions = captions;
10495			this.selectedChapters = chapters;
10496			this.selectedDescriptions = descriptions;
10497			this.selectedMeta = meta;
10498			this.transcriptCaptions = captions;
10499			this.transcriptChapters = chapters;
10500			this.transcriptDescriptions = descriptions;
10501			this.updateChaptersList();
10502			// the following was commented out in Oct/Nov 2018.
10503			// chapters popup is setup automatically when setupPopups() is called later with no param
10504			// not sure why it was included here.
10505			// this.setupPopups('chapters');
10506		}
10507		else if (source === 'transcript') {
10508			this.transcriptCaptions = captions;
10509			this.transcriptChapters = chapters;
10510			this.transcriptDescriptions = descriptions;
10511		}
10512		if (this.selectedDescriptions) {
10513			// updating description voice to match new description language			
10514			this.setDescriptionVoice();			
10515			if (this.$sampleDescDiv) { 
10516				if (this.sampleText) { 
10517					for (i = 0; i < this.sampleText.length; i++) { 
10518						if (this.sampleText[i].lang === this.selectedDescriptions.language) { 
10519							this.currentSampleText = this.sampleText[i]['text']; 
10520							this.$sampleDescDiv.html(this.currentSampleText); 
10521						}
10522					}
10523				}
10524			}
10525		}
10526		this.updateTranscript();
10527	};
10528
10529})(jQuery);
10530
10531(function ($) {
10532	AblePlayer.prototype.updateCaption = function (time) {
10533
10534		if (!this.usingYouTubeCaptions && !this.usingVimeoCaptions && 
10535			(typeof this.$captionsWrapper !== 'undefined')) {
10536			if (this.captionsOn) {
10537				this.$captionsWrapper.show();
10538				if (typeof time !== 'undefined') {
10539					this.showCaptions(time);
10540				}
10541			}
10542			else if (this.$captionsWrapper) {
10543				this.$captionsWrapper.hide();
10544				this.prefCaptions = 0;
10545			}
10546		}
10547	};
10548
10549	AblePlayer.prototype.updateCaptionsMenu = function (lang) {
10550		
10551		// uncheck all previous menu items
10552		this.captionsPopup.find('li').attr('aria-checked','false');
10553		if (typeof lang === 'undefined') {
10554			// check the last menu item (captions off)
10555			this.captionsPopup.find('li').last().attr('aria-checked','true');
10556		}
10557		else {
10558			// check the newly selected lang
10559			this.captionsPopup.find('li[lang=' + lang + ']').attr('aria-checked','true');
10560		}
10561	};
10562
10563	AblePlayer.prototype.getCaptionClickFunction = function (track) {
10564
10565		// Returns the function used when a caption is clicked in the captions menu.
10566		// Not called if user clicks "Captions off". Instead, that triggers getCaptionOffFunction()
10567
10568		var thisObj = this;
10569		return function () {
10570
10571			thisObj.selectedCaptions = track;
10572			thisObj.captionLang = track.language;
10573			thisObj.currentCaption = -1;
10574			if (thisObj.usingYouTubeCaptions) {
10575				if (thisObj.captionsOn) {
10576					// Two things must be true in order for setOption() to work: 
10577					// The YouTube caption module must be loaded 
10578					// and the video must have started playing 
10579					if (thisObj.youTubePlayer.getOptions('captions') && thisObj.startedPlaying) {						
10580						thisObj.youTubePlayer.setOption('captions', 'track', {'languageCode': thisObj.captionLang});
10581					}
10582					else {
10583						// the two conditions were not met 
10584						// try again to set the language after onApiChange event is triggered 
10585						// meanwhile, the following variable will hold the value
10586						thisObj.captionLangPending = thisObj.captionLang;
10587					}
10588				}
10589				else {
10590					if (thisObj.youTubePlayer.getOptions('captions')) { 
10591						thisObj.youTubePlayer.setOption('captions', 'track', {'languageCode': thisObj.captionLang});
10592					}					
10593					else { 
10594						thisObj.youTubePlayer.loadModule('captions');
10595						thisObj.captionLangPending = thisObj.captionLang; 
10596					}
10597				}
10598			}
10599			else if (thisObj.usingVimeoCaptions) {
10600				thisObj.vimeoPlayer.enableTextTrack(thisObj.captionLang).then(function(track) {
10601					// track.language = the iso code for the language
10602					// track.kind = 'captions' or 'subtitles'
10603					// track.label = the human-readable label
10604				}).catch(function(error) {
10605					switch (error.name) {
10606						case 'InvalidTrackLanguageError':
10607							// no track was available with the specified language
10608							console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
10609							break;
10610						case 'InvalidTrackError':
10611							// no track was available with the specified language and kind
10612							console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
10613							break;
10614						default:
10615							// some other error occurred
10616							console.log('Error loading ' + track.label + ' ' + track.kind + ' track');
10617							break;
10618						}
10619				});
10620			}
10621			else { // using local track elements for captions/subtitles
10622				thisObj.syncTrackLanguages('captions',thisObj.captionLang);
10623				if (!thisObj.swappingSrc) {
10624					thisObj.updateCaption(thisObj.elapsed);
10625					thisObj.showDescription(thisObj.elapsed);
10626				}
10627			}
10628			thisObj.captionsOn = true;
10629			// stopgap to prevent spacebar in Firefox from reopening popup
10630			// immediately after closing it (used in handleCaptionToggle(
vendor: 5,595 bytes, lines 10630-10814
10630))
10631			thisObj.hidingPopup = true;
10632			thisObj.captionsPopup.hide();
10633			thisObj.$ccButton.attr('aria-expanded', 'false');
10634			if (thisObj.mediaType === 'audio') {
10635				thisObj.$captionsContainer.removeClass('captions-off');
10636			}
10637			// Ensure stopgap gets cancelled if handleCaptionToggle() isn't called
10638			// e.g., if user triggered button with Enter or mouse click, not spacebar
10639			setTimeout(function() {
10640				thisObj.hidingPopup = false;
10641			}, 100);
10642			thisObj.updateCaptionsMenu(thisObj.captionLang);
10643			thisObj.waitThenFocus(thisObj.$ccButton);
10644
10645			// save preference to cookie
10646			thisObj.prefCaptions = 1;
10647			thisObj.updateCookie('prefCaptions');
10648			thisObj.refreshControls('captions');
10649		}
10650	};
10651
10652	// Returns the function used when the "Captions Off" button is clicked in the captions tooltip.
10653	AblePlayer.prototype.getCaptionOffFunction = function () {
10654
10655		var thisObj = this;
10656		return function () {
10657
10658			if (thisObj.player == 'youtube') {
10659				thisObj.youTubePlayer.unloadModule('captions');
10660			}
10661			else if (thisObj.usingVimeoCaptions) {
10662				thisObj.vimeoPlayer.disableTextTrack();
10663			}
10664			thisObj.captionsOn = false;
10665			thisObj.currentCaption = -1;
10666
10667			if (thisObj.mediaType === 'audio') {
10668				thisObj.$captionsContainer.addClass('captions-off');
10669			}
10670
10671			// stopgap to prevent spacebar in Firefox from reopening popup
10672			// immediately after closing it (used in handleCaptionToggle())
10673			thisObj.hidingPopup = true;
10674			thisObj.captionsPopup.hide();
10675			thisObj.$ccButton.attr('aria-expanded', 'false');
10676			// Ensure stopgap gets cancelled if handleCaptionToggle() isn't called
10677			// e.g., if user triggered button with Enter or mouse click, not spacebar
10678			setTimeout(function() {
10679				thisObj.hidingPopup = false;
10680			}, 100);
10681			thisObj.updateCaptionsMenu();
10682			thisObj.waitThenFocus(thisObj.$ccButton);
10683
10684			// save preference to cookie
10685			thisObj.prefCaptions = 0;
10686			thisObj.updateCookie('prefCaptions');
10687			if (!this.swappingSrc) {
10688				thisObj.refreshControls('captions');
10689				thisObj.updateCaption();
10690			}
10691		}
10692	};
10693
10694	AblePlayer.prototype.showCaptions = function(now) {
10695
10696		var c, thisCaption, captionText;
10697		var cues;
10698		if (this.selectedCaptions.cues.length) {
10699			cues = this.selectedCaptions.cues;
10700		}
10701		else if (this.captions.length >= 1) {
10702			cues = this.captions[0].cues;
10703		}
10704		else {
10705			cues = [];
10706		}
10707		for (c = 0; c < cues.length; c++) {
10708			if ((cues[c].start <= now) && (cues[c].end > now)) {
10709				thisCaption = c;
10710				break;
10711			}
10712		}
10713		if (typeof thisCaption !== 'undefined') {
10714			if (this.currentCaption !== thisCaption) {
10715				// it's time to load the new caption into the container div
10716				captionText = this.flattenCueForCaption(cues[thisCaption]).replace('\n', '<br>');
10717				this.$captionsDiv.html(captionText);
10718				this.currentCaption = thisCaption;
10719				if (captionText.length === 0) {
10720					// hide captionsDiv; otherwise background-color is visible due to padding
10721					this.$captionsDiv.css('display','none');
10722				}
10723				else {
10724					this.$captionsDiv.css('display','inline-block');
10725				}
10726			}
10727		}
10728		else {			
10729			this.$captionsDiv.html('').css('display','none');
10730			this.currentCaption = -1;
10731		}
10732	};
10733
10734	AblePlayer.prototype.flattenCueForCaption = function (cue) {
10735
10736		// Takes a cue and returns the caption text to display
10737		// Also used for chapters
10738
10739		// Support for 'i' and 'b' tags added in 2.3.66
10740		// TODO: Add support for 'c' (class) and 'ruby'
10741
10742		// c (class): <c.myClass1.myClass2>Some text</c>
10743		// Classes can be used to modify other tags too (e.g., <v.loud>)
10744		// If <c> tag, should be rendered as a <span>
10745
10746		// ruby: http://www.w3schools.com/tags/tag_ruby.asp
10747
10748		// WebVTT also supports 'u' (underline)
10749		// I see no reason to support that in Able Player.
10750		// If it's available authors are likely to use it incorrectly
10751		// where <i> or <b> should be used instead
10752		// Here are the rare use cases where an underline is appropriate on the web:
10753		// http://html5doctor.com/u-element/
10754
10755		var result = [];
10756
10757		var flattenComponent = function (component) {
10758			var result = [], ii;
10759			if (component.type === 'string') {
10760				result.push(component.value);
10761			}
10762			else if (component.type === 'v') {
10763				result.push('(' + component.value + ')');
10764				for (ii = 0; ii < component.children.length; ii++) {
10765					result.push(flattenComponent(component.children[ii]));
10766				}
10767			}
10768			else if (component.type === 'i') {
10769				result.push('<em>');
10770				for (ii = 0; ii < component.children.length; ii++) {
10771					result.push(flattenComponent(component.children[ii]));
10772				}
10773				result.push('</em>');
10774			}
10775			else if (component.type === 'b') {
10776				result.push('<strong>');
10777				for (ii = 0; ii < component.children.length; ii++) {
10778					result.push(flattenComponent(component.children[ii]));
10779				}
10780				result.push('</strong>');
10781			}
10782			else {
10783				for (ii = 0; ii < component.children.length; ii++) {
10784					result.push(flattenComponent(component.children[ii]));
10785				}
10786			}
10787			return result.join('');
10788		};
10789
10790		if (typeof cue.components !== 'undefined') {
10791			for (var ii = 0; ii < cue.components.children.length; ii++) {
10792				result.push(flattenComponent(cue.components.children[ii]));
10793			}
10794		}
10795		return result.join('');
10796	};
10797
10798	AblePlayer.prototype.getCaptionsOptions = function(pref) {
10799
10800		var options = [];
10801
10802		switch (pref) {
10803
10804			case 'prefCaptionsFont':
10805				options[0] = ['serif',this.tt.serif];
10806				options[1] = ['sans-serif',this.tt.sans];
10807				options[2] = ['cursive',this.tt.cursive];
10808				options[3] = ['fantasy',this.tt.fantasy];
10809				options[4] = ['monospace',this.tt.monospace];
10810				break;
10811
10812			case 'prefCaptionsColor':
10813			case 'prefCaptionsBGColor':
10814				// HTML color values must be in English
10815				options[0] = ['white',this.tt.white];
10816				options[1] = ['yellow',this.tt.yellow];
10817				options[2] = ['green',this.tt.green];
10818				options[3] = ['cyan',this.tt.cyan];
10819				options[4] = ['blue',this.tt.blue];
10820				options[5] = ['magenta',this.tt.magenta];
10821				options[6] = ['red',this.tt.red];
10822				options[7] = ['black',this.tt.black];
10823				break;
10824
10825			case 'prefCaptionsSize':
10826				options[0] = '75%';
10827				options[1] = '100%';
10828				options[2] = '125%';
10829				options[3] = '150%';
10830				options[4] = '200%';
10831				break;
10832
10833			case 'prefCaptionsOpacity':
10834				options[0] = '0%';
10835				options[1] = '25%';
10836				options[2] = '50%';
10837				options[3] = '75%';
10838				options[4] = '100%';
10839				break;
10840
10841			case 'prefCaptionsStyle':
10842				options[0] = this.tt.captionsStylePopOn;
10843				options[1] = this.tt.captionsStyleRollUp;
10844				break;
10845
10846			case 'prefCaptionsPosition':
10847				options[0] = 'overlay';
10848				options[1] = 'below';
10849				break;
10850
10851		}
10852		return options;
10853	};
10854
10855	AblePlayer.prototype.translatePrefs = function(pref, value, outputFormat) {
10856
10857		// translate current value of pref to a value supported by outputformat
10858		if (outputFormat == 'youtube') {
10859			if (pref === 'size') {
10860				// YouTube font sizes are a range from -1 to 3 (0 = default)
10861				switch (value) {
10862					case '75%':
10863						return -1;
10864					case '100%':
10865						return 0;
10866					case '125%':
10867						return 1;
10868					case '150%':
10869						return 2;
10870					case '200%':
10871						return 3;
10872				}
10873			}
10874		}
10875		return false;
10876	}
10877
10878	AblePlayer.prototype.stylizeCaptions = function($element, pref) {
10879
10880		// $element is the jQuery element containing the captions
10881		// this function handles stylizing of the sample caption text in the Prefs dialog
10882		// plus the actual production captions
10883		// TODO: consider applying the same user prefs to visible text-based description
10884		var property, newValue, opacity, lineHeight;
10885
10886		if (typeof $element !== 'undefined') {
10887			if (pref == 'prefCaptionsPosition') {
10888				this.positionCaptions();
10889			}
10890			else if (typeof pref !== 'undefined') {
10891				// just change the one property that user just changed
10892				if (pref === 'prefCaptionsFont') {
10893					property = 'font-family';
10894				}
10895				else if (pref === 'prefCaptionsSize') {
10896					property = 'font-size';
10897				}
10898				else if (pref === 'prefCaptionsColor') {
10899					property = 'color';
10900				}
10901				else if (pref === 'prefCaptionsBGColor') {
10902					property = 'background-color';
10903				}
10904				else if (pref === 'prefCaptionsOpacity') {
10905					property = 'opacity';
10906				}
10907				if (pref === 'prefCaptionsOpacity') {
10908					newValue = parseFloat($('#' + this.mediaId + '_' + pref).val()) / 100.0;
10909				}
10910				else {
10911					newValue = $('#' + this.mediaId + '_' + pref).val();
10912				}
10913				$element.css(property, newValue);
10914			}
10915			else { // no property was specified, update all styles with current saved prefs
10916				opacity = parseFloat(this.prefCaptionsOpacity) / 100.0;
10917				$element.css({
10918					'font-family': this.prefCaptionsFont,
10919					'color': this.prefCaptionsColor,
10920					'background-color': this.prefCaptionsBGColor,
10921					'opacity': opacity
10922				});
10923				if ($element === this.$captionsDiv) {
10924					if (typeof this.$captionsDiv !== 'undefined') {
10925						this.$captionsDiv.css({
10926							'font-size': this.prefCaptionsSize
10927						});
10928					}
10929				}
10930				if (this.prefCaptionsPosition === 'below') {
10931					// also need to add the background color to the wrapper div
10932					if (typeof this.$captionsWrapper !== 'undefined') {
10933						this.$captionsWrapper.css({
10934							'background-color': this.prefCaptionsBGColor,
10935							'opacity': '1'
10936						});
10937					}
10938				}
10939				else if (this.prefCaptionsPosition === 'overlay') {
10940					// no background color for overlay wrapper, captions are displayed in-line
10941					if (typeof this.$captionsWrapper !== 'undefined') {
10942						this.$captionsWrapper.css({
10943							'background-color': 'transparent',
10944							'opacity': ''
10945						});
10946					}
10947				}
10948				this.positionCaptions();
10949			}
10950		}
10951	};
10952	AblePlayer.prototype.positionCaptions = function(position) {
10953
10954		// set caption position to either 'overlay' or 'below'
10955		// if position parameter was passed to this function, use that
10956		// otherwise use user preference
10957		if (typeof position === 'undefined') {
10958			position = this.prefCaptionsPosition;
10959		}
10960		if (typeof this.$captionsWrapper !== 'undefined') {
10961
10962			if (position == 'below') {
10963				this.$captionsWrapper.removeClass('able-captions-overlay').addClass('able-captions-below');
10964				// also need to update in-line styles
10965				this.$captionsWrapper.css({
10966					'background-color': this.prefCaptionsBGColor,
10967					'opacity': '1'
10968				});
10969			}
10970			else {
10971				this.$captionsWrapper.removeClass('able-captions-below').addClass('able-captions-overlay');
10972				this.$captionsWrapper.css({
10973					'background-color': 'transparent',
10974					'opacity': ''
10975				});
10976			}
10977		}
10978	};
10979
10980})(jQuery);
10981
10982(function ($) {
10983
10984	AblePlayer.prototype.populateChaptersDiv = function() {
10985
10986		var headingLevel, headingType, headingId, $chaptersHeading,
10987			$chaptersList;
10988
10989		if ($('#' + this.chaptersDivLocation)) {
10990
10991			this.$chaptersDiv = $('#' + this.chaptersDivLocation);
10992			this.$chaptersDiv.addClass('able-chapters-div');
10993
10994			// empty content from previous build before starting fresh
10995			this.$chaptersDiv.empty(); 
10996
10997			// add optional header
10998			if (this.chaptersTitle) {
10999				headingLevel = this.getNextHeadingLevel(this.$chaptersDiv);
11000				headingType = 'h' + headingLevel.toString();
vendor: 10,563 bytes, lines 11001-11344
11001				headingId = this.mediaId + '-chapters-heading';
11002				$chaptersHeading = $('<' + headingType + '>', {
11003					'class': 'able-chapters-heading',
11004					'id': headingId
11005				}).text(this.chaptersTitle);
11006				this.$chaptersDiv.append($chaptersHeading);
11007			}
11008
11009			this.$chaptersNav = $('<nav>');
11010			if (this.chaptersTitle) {
11011				this.$chaptersNav.attr('aria-labelledby',headingId);
11012			}
11013			else {
11014				this.$chaptersNav.attr('aria-label',this.tt.chapters);
11015			}
11016			this.$chaptersDiv.append(this.$chaptersNav);
11017
11018			// populate this.$chaptersNav with a list of chapters
11019			this.updateChaptersList();
11020		}
11021	};
11022
11023	AblePlayer.prototype.updateChaptersList = function() {
11024
11025		var thisObj, cues, $chaptersList, c, thisChapter,
11026			$chapterItem, $chapterButton, buttonId, hasDefault,
11027			getClickFunction, $clickedItem, $chaptersList, thisChapterIndex;
11028
11029		thisObj = this;
11030
11031		// TODO: Update this so it can change the chapters popup menu 
11032		// currently it only works if chapters are in an external container
11033		if (!this.$chaptersNav) {
11034			return false;
11035		}
11036
11037		if (typeof this.useChapterTimes === 'undefined') {
11038			if (this.seekbarScope === 'chapter' && this.selectedChapters.cues.length) {
11039				this.useChapterTimes = true;
11040			}
11041			else {
11042				this.useChapterTimes = false;
11043			}
11044		}
11045		if (this.useChapterTimes) {
11046			cues = this.selectedChapters.cues;
11047		}
11048		else if (this.chapters.length >= 1) {
11049			cues = this.chapters[0].cues;
11050		}
11051		else {
11052			cues = [];
11053		}
11054		if (cues.length > 0) {
11055			$chaptersList = $('<ul>');
11056			for (c = 0; c < cues.length; c++) {
11057				thisChapter = c;
11058				$chapterItem = $('<li></li>');
11059				$chapterButton = $('<button>',{
11060					'type': 'button',
11061					'val': thisChapter
11062				}).text(this.flattenCueForCaption(cues[thisChapter]));
11063
11064				// add event listeners
11065				getClickFunction = function (time) {
11066					return function () {
11067						thisObj.seekTrigger = 'chapter';
11068						$clickedItem = $(this).closest('li');
11069						$chaptersList = $(this).closest('ul').find('li');
11070						thisChapterIndex = $chaptersList.index($clickedItem);
11071						$chaptersList.removeClass('able-current-chapter')
11072							.children('button').removeAttr('aria-current');
11073						$clickedItem.addClass('able-current-chapter')
11074							.children('button').attr('aria-current','true');
11075						// Need to updateChapter before seeking to it
11076						// Otherwise seekBar is redrawn with wrong chapterDuration and/or chapterTime
11077						thisObj.updateChapter(time);
11078						thisObj.seekTo(time);
11079					}
11080				};
11081				$chapterButton.on('click',getClickFunction(cues[thisChapter].start)); // works with Enter too
11082				$chapterButton.on('focus',function() {
11083					$(this).closest('ul').find('li').removeClass('able-focus');
11084					$(this).closest('li').addClass('able-focus');
11085				});
11086				$chapterItem.on('hover',function() {
11087					$(this).closest('ul').find('li').removeClass('able-focus');
11088					$(this).addClass('able-focus');
11089				});
11090				$chapterItem.on('mouseleave',function() {
11091					$(this).removeClass('able-focus');
11092				});
11093				$chapterButton.on('blur',function() {
11094					$(this).closest('li').removeClass('able-focus');
11095				});
11096
11097				// put it all together
11098				$chapterItem.append($chapterButton);
11099				$chaptersList.append($chapterItem);
11100				if (this.defaultChapter === cues[thisChapter].id) {
11101					$chapterButton.attr('aria-current','true').parent('li').addClass('able-current-chapter');
11102					this.currentChapter = cues[thisChapter];
11103					hasDefault = true;
11104				}
11105			}
11106			if (!hasDefault) {
11107				// select the first chapter
11108				this.currentChapter = cues[0];
11109				$chaptersList.find('button').first().attr('aria-current','true')
11110					.parent('li').addClass('able-current-chapter');
11111			}
11112			this.$chaptersNav.html($chaptersList);
11113		}
11114		return false;
11115	};
11116
11117	AblePlayer.prototype.seekToChapter = function(chapterId) {
11118
11119		// step through chapters looking for matching ID
11120		var i=0;
11121		while (i < this.selectedChapters.cues.length) {
11122			if (this.selectedChapters.cues[i].id == chapterId) {
11123				// found the target chapter! Seek to it
11124				this.seekTo(this.selectedChapters.cues[i].start);
11125				this.updateChapter(this.selectedChapters.cues[i].start);
11126				break;
11127			}
11128			i++;
11129		}
11130	};
11131
11132	AblePlayer.prototype.updateChapter = function (now) {
11133
11134		// as time-synced chapters change during playback, track changes in current chapter
11135		if (typeof this.selectedChapters === 'undefined') {
11136			return;
11137		}
11138
11139		var chapters, i, thisChapterIndex, chapterLabel;
11140
11141		chapters = this.selectedChapters.cues;
11142		for (i = 0; i < chapters.length; i++) {
11143			if ((chapters[i].start <= now) && (chapters[i].end > now)) {
11144				thisChapterIndex = i;
11145				break;
11146			}
11147		}
11148		if (typeof thisChapterIndex !== 'undefined') {
11149			if (this.currentChapter !== chapters[thisChapterIndex]) {
11150				// this is a new chapter
11151				this.currentChapter = chapters[thisChapterIndex];
11152				if (this.useChapterTimes) {
11153					this.chapterDuration = this.getChapterDuration();
11154					this.seekIntervalCalculated = false; // will be recalculated in setSeekInterval()
11155				}
11156				if (typeof this.$chaptersDiv !== 'undefined') {
11157					// chapters are listed in an external container
11158					this.$chaptersDiv.find('ul').find('li')
11159						.removeClass('able-current-chapter')
11160						.children('button').removeAttr('aria-current');
11161					this.$chaptersDiv.find('ul').find('li').eq(thisChapterIndex)
11162						.addClass('able-current-chapter')
11163						.children('button').attr('aria-current','true');
11164				}
11165			}
11166		}
11167	};
11168
11169	AblePlayer.prototype.getChapterDuration = function () {
11170
11171		// called if this.seekbarScope === 'chapter'
11172		// get duration of the current chapter
11173
11174		var lastChapterIndex, chapterEnd;
11175
11176		if (typeof this.currentChapter === 'undefined') {
11177			return 0;
11178		}
11179		if (typeof this.duration === 'undefined') {
11180			return 0;
11181		}
11182		lastChapterIndex = this.selectedChapters.cues.length-1;
11183		if (this.selectedChapters.cues[lastChapterIndex] == this.currentChapter) {
11184			// this is the last chapter
11185			if (this.currentChapter.end !== this.duration) {
11186				// chapter ends before or after video ends, adjust chapter end to match video end
11187				chapterEnd = this.duration;
11188				this.currentChapter.end = this.duration;
11189			}
11190			else {
11191				chapterEnd = this.currentChapter.end;
11192			}
11193		}
11194		else { // this is not the last chapter
11195			chapterEnd = this.currentChapter.end;
11196		}
11197		return chapterEnd - this.currentChapter.start;
11198	};
11199
11200	AblePlayer.prototype.getChapterElapsed = function () {
11201		// called if this.seekbarScope === 'chapter'
11202		// get current elapsed time, relative to the current chapter duration
11203
11204		if (typeof this.currentChapter === 'undefined') {
11205			return 0;
11206		}
11207
11208		if (this.elapsed > this.currentChapter.start) {
11209			return this.elapsed - this.currentChapter.start;
11210		}
11211		else {
11212			return 0;
11213		}
11214	};
11215
11216	AblePlayer.prototype.convertChapterTimeToVideoTime = function (chapterTime) {
11217
11218		// chapterTime is the time within the current chapter
11219		// return the same time, relative to the entire video
11220		if (typeof this.currentChapter !== 'undefined') {
11221			var newTime = this.currentChapter.start + chapterTime;
11222			if (newTime > this.currentChapter.end) {
11223				return this.currentChapter.end;
11224			}
11225			else {
11226				return newTime;
11227			}
11228		}
11229		else {
11230			return chapterTime;
11231		}
11232	};
11233
11234	AblePlayer.prototype.getChapterClickFunction = function (time) {
11235
11236		// Returns the function used when a chapter is clicked in the chapters menu.
11237		var thisObj = this;
11238		return function () {
11239			thisObj.seekTrigger = 'chapter';
11240			thisObj.seekTo(time);
11241			// stopgap to prevent spacebar in Firefox from reopening popup
11242			// immediately after closing it (used in handleChapters())
11243			thisObj.hidingPopup = true;
11244			thisObj.chaptersPopup.hide();
11245			// Ensure stopgap gets cancelled if handleChapters() isn't called
11246			// e.g., if user triggered button with Enter or mouse click, not spacebar
11247			setTimeout(function() {
11248				thisObj.hidingPopup = false;
11249			}, 100);
11250			thisObj.$chaptersButton.focus();
11251		}
11252	};
11253
11254})(jQuery);
11255
11256(function ($) {
11257	AblePlayer.prototype.updateMeta = function (time) {
11258		if (this.hasMeta) {
11259			if (this.metaType === 'text') {
11260				this.$metaDiv.show();
11261				this.showMeta(time || this.elapsed);
11262			}
11263			else {
11264				this.showMeta(time || this.elapsed);
11265			}
11266		}
11267	};
11268
11269	AblePlayer.prototype.showMeta = function(now) {
11270		var tempSelectors, m, thisMeta,
11271			cues, cueText, cueLines, i, line,
11272			showDuration, focusTarget;
11273
11274		tempSelectors = [];
11275		if (this.meta.length >= 1) {
11276			cues = this.meta;
11277		}
11278		else {
11279			cues = [];
11280		}
11281		for (m = 0; m < cues.length; m++) {
11282			if ((cues[m].start <= now) && (cues[m].end > now)) {
11283				thisMeta = m;
11284				break;
11285			}
11286		}
11287		if (typeof thisMeta !== 'undefined') {
11288			if (this.currentMeta !== thisMeta) {
11289				if (this.metaType === 'text') {
11290					// it's time to load the new metadata cue into the container div
11291					this.$metaDiv.html(this.flattenCueForMeta(cues[thisMeta]).replace('\n', '<br>'));
11292				}
11293				else if (this.metaType === 'selector') {
11294					// it's time to show content referenced by the designated selector(s)
11295					cueText = this.flattenCueForMeta(cues[thisMeta]);
11296					cueLines = cueText.split('\n');
11297					for (i=0; i<cueLines.length; i++) {
11298						line = $.trim(cueLines[i]);
11299						if (line.toLowerCase().trim() === 'pause') {
11300							// don't show big play button when pausing via metadata
11301							this.hideBigPlayButton = true;
11302							this.pauseMedia();
11303						}
11304						else if (line.toLowerCase().substring(0,6) == 'focus:') {
11305							focusTarget = line.substring(6).trim();
11306							if ($(focusTarget).length) {
11307								$(focusTarget).focus();
11308							}
11309						}
11310						else {
11311							if ($(line).length) {
11312								// selector exists
11313								this.currentMeta = thisMeta;
11314								showDuration = parseInt($(line).attr('data-duration'));
11315								if (typeof showDuration !== 'undefined' && !isNaN(showDuration)) {
11316									$(line).show().delay(showDuration).fadeOut();
11317								}
11318								else {
11319									// no duration specified. Just show the element until end time specified in VTT file
11320									$(line).show();
11321								}
11322								// add to array of visible selectors so it can be hidden at end time
11323								this.visibleSelectors.push(line);
11324								tempSelectors.push(line);
11325
11326							}
11327						}
11328					}
11329					// now step through this.visibleSelectors and remove anything that's stale
11330					if (this.visibleSelectors && this.visibleSelectors.length) {
11331						if (this.visibleSelectors.length !== tempSelectors.length) {
11332							for (i=this.visibleSelectors.length-1; i>=0; i--) {
11333								if ($.inArray(this.visibleSelectors[i],tempSelectors) == -1) {
11334									$(this.visibleSelectors[i]).hide();
11335									this.visibleSelectors.splice(i,1);
11336								}
11337							}
11338						}
11339					}
11340
11341				}
11342			}
11343		}
11344		else {
11345			// there is currently no metadata. Empty stale content
11346			if (typeof this.$metaDiv !== 'undefined') {
11347				this.$metaDiv.html('');
11348			}
11349			if (this.visibleSelectors && this.visibleSelectors.length) {
11350				for (i=0; i<this.visibleSelectors.length; i++) {
11351					$(this.visibleSelectors[i]).hide();
11352				}
11353				// reset array
11354				this.visibleSelectors = [];
11355			}
11356			this.currentMeta = -1;
11357		}
11358	};
11359
11360	// Takes a cue and returns the metadata text to display for it.
11361	AblePlayer.prototype.flattenCueForMeta = function (cue) {
11362		var result = [];
11363
11364		var flattenComponent = function (component) {
11365			var result = [], ii;
11366			if (component.type === 'string') {
11367				result.push(component.value);
11368			}
11369			else if (component.type === 'v') {
11370				result.push('[' + component.value + ']');
11371				for (ii = 0; ii < component.children.length; ii++) {
11372					result.push(flattenComponent(component.children[ii]));
11373				}
11374			}
11375			else {
11376				for (ii = 0; ii < component.children.length; ii++) {
11377					result.push(flattenComponent(component.children[ii]));
11378				}
11379			}
11380			return result.join('');
11381		}
11382
11383		for (var ii = 0; ii < cue.components.children.length; ii++) {
11384			result.push(flattenComponent(cue.components.children[ii]));
11385		}
11386
11387		return result.join('');
11388	};
11389
11390})(jQuery);
11391
11392(function ($) {
11393
11394	AblePlayer.prototype.setupTranscript = function() {
11395
11396		var deferred = new $.Deferred();
11397		var promise = deferred.promise();
11398
11399		if (this.usingYouTubeCaptions || this.usingVimeoCaptions) { 
11400			// a transcript is not possible 
11401			this.transcriptType = null; 
11402			deferred.resolve();
11403		}
11404		else { 
11405			if (!this.transcriptType) {
11406				// previously set transcriptType to null since there are no <track> elements
11407				// check again to see if captions have been collected from other sources (e.g., YouTube)
11408
11409				if (this.captions.length) {
11410					// captions are possible! Use the default type (popup)
11411					// if other types ('external' and 'manual') were desired, transcriptType would not be null here
11412					this.transcriptType = 'popup';
11413				}
11414			}
11415			if (this.transcriptType) {
11416				if (this.transcriptType === 'popup' || this.transcriptType === 'external') {
11417					this.injectTranscriptArea();
11418					deferred.resolve();
11419				}
11420				else if (this.transcriptType === 'manual') {
11421					this.setupManualTranscript();
11422					deferred.resolve();
11423				}
11424			}
11425			else {
11426				// there is no transcript
11427				deferred.resolve();
11428			}
11429		}
11430		return promise;
11431	};
11432
11433	AblePlayer.prototype.injectTranscriptArea = function() {
11434
11435		var thisObj, $autoScrollLabel, $languageSelectWrapper, $languageSelectLabel, i, $option;
11436
11437		thisObj = this;
11438		this.$transcriptArea = $('<div>', {
11439			'class': 'able-transcript-area',
11440			'role': 'dialog',
11441			'aria-label': this.tt.transcriptTitle
11442		});
11443
11444		this.$transcriptToolbar = $('<div>', {
11445			'class': 'able-window-toolbar able-' + this.toolbarIconColor + '-controls'
11446		});
11447
11448		this.$transcriptDiv = $('<div>', {
11449			'class' : 'able-transcript'
11450		});
11451
11452		// Transcript toolbar content
11453
11454		// Add auto Scroll checkbox
11455		this.$autoScrollTranscriptCheckbox = $('<input>', {
11456			'id': 'autoscroll-transcript-checkbox-' + this.mediaId,
11457			'type': 'checkbox'
11458		});
11459		$autoScrollLabel = $('<label>', {
11460				'for': 'autoscroll-transcript-checkbox-' + this.mediaId
11461		}).text(this.tt.autoScroll);
11462		this.$transcriptToolbar.append($autoScrollLabel,this.$autoScrollTranscriptCheckbox);
11463
11464		// Add field for selecting a transcript language
11465		// Only necessary if there is more than one language
11466		if (this.captions.length > 1) {
11467			$languageSelectWrapper = $('<div>',{
11468				'class': 'transcript-language-select-wrapper'
11469			});
11470			$languageSelectLabel = $('<label>',{
11471				'for': 'transcript-language-select-' + this.mediaId
11472			}).text(this.tt.language);
11473			this.$transcriptLanguageSelect = $('<select>',{
11474				'id': 'transcript-language-select-' + this.mediaId
11475			});
11476			for (i=0; i < this.captions.length; i++) {
11477				$option = $('<option></option>',{
11478					value: this.captions[i]['language'],
11479					lang: this.captions[i]['language']
11480				}).text(this.captions[i]['label']);
11481				if (this.captions[i]['def']) {
11482					$option.prop('selected',true);
11483				 }
11484				this.$transcriptLanguageSelect.append($option);
11485			 }
11486		}
11487		if ($languageSelectWrapper) {
11488			$languageSelectWrapper.append($languageSelectLabel,this.$transcriptLanguageSelect);
11489			this.$transcriptToolbar.append($languageSelectWrapper);
11490		}
11491		this.$transcriptArea.append(this.$transcriptToolbar, this.$transcriptDiv);
11492
11493		// If client has provided separate transcript location, put it there.
11494		// Otherwise append it to the body
11495		if (this.transcriptDivLocation) {
11496			$('#' + this.transcriptDivLocation).append(this.$transcriptArea);
11497		}
11498		else {
11499			this.$ableWrapper.append(this.$transcriptArea);
11500		}
11501
11502		// make it draggable (popup only; NOT external transcript)
11503		if (!this.transcriptDivLocation) {
11504			this.initDragDrop('transcript');
11505			if (this.prefTranscript === 1) {
11506				// transcript is on. Go ahead and position it
11507				this.positionDraggableWindow('transcript',this.getDefaultWidth('transcript'));
11508			}
11509		}
11510
11511		// If client has provided separate transcript location, override user's preference for hiding transcript
11512		if (!this.prefTranscript && !this.transcriptDivLocation) {
11513			this.$transcriptArea.hide();
11514		}
11515	};
11516
11517	AblePlayer.prototype.addTranscriptAreaEvents = function() {
11518
11519		var thisObj = this;
11520
11521		this.$autoScrollTranscriptCheckbox.click(function () {
11522			thisObj.handleTranscriptLockToggle(thisObj.$autoScrollTranscriptCheckbox.prop('checked'));
11523		});
11524
11525		this.$transcriptDiv.on('mousewheel DOMMouseScroll click scroll', function (e) {
11526			// Propagation is stopped in transcript click handler, so clicks are on the scrollbar
11527			// or outside of a clickable span.
11528			if (!thisObj.scrollingTranscript) {
11529				thisObj.autoScrollTranscript = false;
vendor: 5,404 bytes, lines 11530-11690
11530				thisObj.refreshControls('transcript');
11531			}
11532			thisObj.scrollingTranscript = false;
11533		});
11534
11535		if (typeof this.$transcriptLanguageSelect !== 'undefined') {
11536
11537			this.$transcriptLanguageSelect.on('click mousedown',function (e) {
11538				// execute default behavior
11539				// prevent propagation of mouse event to toolbar or window
11540				e.stopPropagation();
11541			});
11542
11543			this.$transcriptLanguageSelect.on('change',function () {
11544
11545				var language = thisObj.$transcriptLanguageSelect.val();
11546
11547				thisObj.syncTrackLanguages('transcript',language);
11548			});
11549		}
11550	};
11551
11552	AblePlayer.prototype.transcriptSrcHasRequiredParts = function() {
11553
11554		// check the external transcript to be sure it has all required components
11555		// return true or false
11556		// in the process, define all the needed variables and properties
11557
11558		if ($('#' + this.transcriptSrc).length) {
11559			this.$transcriptArea = $('#' + this.transcriptSrc);
11560			if (this.$transcriptArea.find('.able-window-toolbar').length) {
11561				this.$transcriptToolbar = this.$transcriptArea.find('.able-window-toolbar').eq(0);
11562				if (this.$transcriptArea.find('.able-transcript').length) {
11563					this.$transcriptDiv = this.$transcriptArea.find('.able-transcript').eq(0);
11564					if (this.$transcriptArea.find('.able-transcript-seekpoint').length) {
11565						this.$transcriptSeekpoints = this.$transcriptArea.find('.able-transcript-seekpoint');
11566						return true;
11567					}
11568				}
11569			}
11570		}
11571		return false;
11572	}
11573
11574	AblePlayer.prototype.setupManualTranscript = function() {
11575
11576		var $autoScrollInput, $autoScrollLabel;
11577
11578		$autoScrollInput = $('<input>', {
11579			'id': 'autoscroll-transcript-checkbox-' + this.mediaId,
11580			'type': 'checkbox'
11581		});
11582		$autoScrollLabel = $('<label>', {
11583				'for': 'autoscroll-transcript-checkbox-' + this.mediaId
11584		}).text(this.tt.autoScroll);
11585
11586		// Add an auto-scroll checkbox to the toolbar.
11587		this.$autoScrollTranscriptCheckbox = $autoScrollInput;
11588		this.$transcriptToolbar.append($autoScrollLabel, this.$autoScrollTranscriptCheckbox);
11589
11590	};
11591
11592	AblePlayer.prototype.updateTranscript = function() {
11593
11594		if (!this.transcriptType) {
11595			return;
11596		}
11597		if (this.playerCreated && !this.$transcriptArea) { 
11598			return; 
11599		}
11600		if (this.transcriptType === 'external' || this.transcriptType === 'popup') {
11601
11602			var chapters, captions, descriptions;
11603
11604			// Language of transcript might be different than language of captions
11605			// But both are in sync by default
11606			if (this.transcriptLang) {
11607				captions = this.transcriptCaptions.cues;
11608			}
11609			else {
11610				if (this.transcriptCaptions) {
11611					this.transcriptLang = this.transcriptCaptions.language;
11612					captions = this.transcriptCaptions.cues;
11613				}
11614				else if (this.selectedCaptions) {
11615					this.transcriptLang = this.captionLang;
11616					captions = this.selectedCaptions.cues;
11617				}
11618			}
11619
11620			// setup chapters
11621			if (this.transcriptChapters) {
11622				chapters = this.transcriptChapters.cues;
11623			}
11624			else if (this.chapters.length > 0) {
11625				// Try and match the caption language.
11626				if (this.transcriptLang) {
11627					for (var i = 0; i < this.chapters.length; i++) {
11628						if (this.chapters[i].language === this.transcriptLang) {
11629							chapters = this.chapters[i].cues;
11630						}
11631					}
11632				}
11633				if (typeof chapters === 'undefined') {
11634					chapters = this.chapters[0].cues || [];
11635				}
11636			}
11637
11638			// setup descriptions
11639			if (this.transcriptDescriptions) {
11640				descriptions = this.transcriptDescriptions.cues;
11641			}
11642			else if (this.descriptions.length > 0) {
11643				// Try and match the caption language.
11644				if (this.transcriptLang) {
11645					for (var i = 0; i < this.descriptions.length; i++) {
11646						if (this.descriptions[i].language === this.transcriptLang) {
11647							descriptions = this.descriptions[i].cues;
11648						}
11649					}
11650				}
11651				if (!descriptions) {
11652					descriptions = this.descriptions[0].cues || [];
11653				}
11654			}
11655
11656			var div = this.generateTranscript(chapters || [], captions || [], descriptions || []);
11657			this.$transcriptDiv.html(div);
11658			// reset transcript selected <option> to this.transcriptLang
11659			if (this.$transcriptLanguageSelect) {
11660				this.$transcriptLanguageSelect.find('option:selected').prop('selected',false);
11661				this.$transcriptLanguageSelect.find('option[lang=' + this.transcriptLang + ']').prop('selected',true);
11662			}
11663		}
11664
11665		var thisObj = this;
11666
11667		// Make transcript tabbable if preference is turned on.
11668		if (this.prefTabbable === 1) {
11669			this.$transcriptDiv.find('span.able-transcript-seekpoint').attr('tabindex','0');
11670		}
11671
11672		// handle clicks on text within transcript
11673		// Note: This event listeners handles clicks only, not keydown events
11674		// Pressing Enter on an element that is not natively clickable does NOT trigger click()
11675		// Keydown events are handled elsehwere, both globally (ableplayer-base.js) and locally (event.js)
11676		if (this.$transcriptArea.length > 0) {
11677			this.$transcriptArea.find('span.able-transcript-seekpoint').click(function(e) {
11678				thisObj.seekTrigger = 'transcript';
11679				var spanStart = parseFloat($(this).attr('data-start'));
11680				// Add a tiny amount so that we're inside the span.
11681				spanStart += .01;
11682				// Each click within the transcript triggers two click events (not sure why)
11683				// this.seekingFromTranscript is a stopgab to prevent two calls to SeekTo()
11684				if (!thisObj.seekingFromTranscript) {
11685					thisObj.seekingFromTranscript = true;
11686					thisObj.seekTo(spanStart);
11687				}
11688				else {
11689					// don't seek a second time, but do reset var
11690					thisObj.seekingFromTranscript = false;
11691				}
11692			});
11693		}
11694	};
11695
11696	AblePlayer.prototype.highlightTranscript = function (currentTime) {
11697
11698		// Show highlight in transcript marking current caption.
11699
11700		if (!this.transcriptType) {
11701			return;
11702		}
11703
11704		var start, end, isChapterHeading;
11705		var thisObj = this;
11706
11707		currentTime = parseFloat(currentTime);
11708
11709		// Highlight the current transcript item.
11710		this.$transcriptArea.find('span.able-transcript-seekpoint').each(function() {
11711			start = parseFloat($(this).attr('data-start'));
11712			end = parseFloat($(this).attr('data-end'));
11713			// be sure this isn't a chapter (don't highlight chapter headings)
11714			if ($(this).parent().hasClass('able-transcript-chapter-heading')) {
11715				isChapterHeading = true;
11716			}
11717			else {
11718				isChapterHeading = false;
11719			}
11720
11721			if (currentTime >= start && currentTime <= end && !isChapterHeading) {
11722
11723				// If this item isn't already highlighted, it should be
11724				if (!($(this).hasClass('able-highlight'))) {
11725					// remove all previous highlights before adding one to current span
11726					thisObj.$transcriptArea.find('.able-highlight').removeClass('able-highlight');
11727					$(this).addClass('able-highlight');
11728					thisObj.movingHighlight = true;
11729				}
11730				return false;
11731			}
11732		});
11733		thisObj.currentHighlight = thisObj.$transcriptArea.find('.able-highlight');
11734		if (thisObj.currentHighlight.length === 0) {
11735			// Nothing highlighted.
11736			thisObj.currentHighlight = null;
11737		}
11738	};
11739
11740	AblePlayer.prototype.generateTranscript = function(chapters, captions, descriptions) {
11741
11742		var thisObj = this;
11743
11744		var $main = $('<div class="able-transcript-container"></div>');
11745		var transcriptTitle;
11746
11747		// set language for transcript container
11748		$main.attr('lang', this.transcriptLang);
11749
11750		if (typeof this.transcriptTitle !== 'undefined') {
11751			transcriptTitle = this.transcriptTitle;
11752		}
11753		else if (this.lyricsMode) {
11754			transcriptTitle = this.tt.lyricsTitle;
11755		}
11756		else {
11757			transcriptTitle = this.tt.transcriptTitle;
11758		}
11759
11760		if (!this.transcriptDivLocation) {
11761			// only add an HTML heading to internal transcript
11762			// external transcript is expected to have its own heading
11763			var headingNumber = this.playerHeadingLevel;
11764			headingNumber += 1;
11765			var chapterHeadingNumber = headingNumber + 1;
11766
11767			if (headingNumber <= 6) {
11768				var transcriptHeading = 'h' + headingNumber.toString();
11769			}
11770			else {
11771				var transcriptHeading = 'div';
11772			}
11773			var $transcriptHeadingTag = $('<' + transcriptHeading + '>');
11774			$transcriptHeadingTag.addClass('able-transcript-heading');
11775			if (headingNumber > 6) {
11776				$transcriptHeadingTag.attr({
11777					'role': 'heading',
11778					'aria-level': headingNumber
11779				});
11780			}
11781			$transcriptHeadingTag.text(transcriptTitle);
11782
11783			// set language of transcript heading to language of player
11784			// this is independent of language of transcript
11785			$transcriptHeadingTag.attr('lang', this.lang);
11786
11787			$main.append($transcriptHeadingTag);
11788		}
11789
11790		var nextChapter = 0;
11791		var nextCap = 0;
11792		var nextDesc = 0;
11793
11794		var addChapter = function(div, chap) {
11795
11796			if (chapterHeadingNumber <= 6) {
11797				var chapterHeading = 'h' + chapterHeadingNumber.toString();
11798			}
11799			else {
11800				var chapterHeading = 'div';
11801			}
11802
11803			var $chapterHeadingTag = $('<' + chapterHeading + '>',{
11804				'class': 'able-transcript-chapter-heading'
11805			});
11806			if (chapterHeadingNumber > 6) {
11807				$chapterHeadingTag.attr({
11808					'role': 'heading',
11809					'aria-level': chapterHeadingNumber
11810				});
11811			}
11812
11813			var flattenComponentForChapter = function(comp) {
11814
11815				var result = [];
11816				if (comp.type === 'string') {
11817					result.push(comp.value);
11818				}
11819				else {
11820					for (var i = 0; i < comp.children.length; i++) {
11821						result = result.concat(flattenComponentForChapter(comp.children[i]));
11822					}
11823				}
11824				return result;
11825			}
11826
11827			var $chapSpan = $('<span>',{
11828				'class': 'able-transcript-seekpoint'
11829			});
11830			for (var i = 0; i < chap.components.children.length; i++) {
11831				var results = flattenComponentForChapter(chap.components.children[i]);
11832				for (var jj = 0; jj < results.length; jj++) {
11833					$chapSpan.append(results[jj]);
11834				}
11835			}
11836			$chapSpan.attr('data-start', chap.start.toString());
11837			$chapSpan.attr('data-end', chap.end.toString());
11838			$chapterHeadingTag.append($chapSpan);
11839
11840			div.append($chapterHeadingTag);
11841		};
11842
11843		var addDescription = function(div, desc) {
11844			var $descDiv = $('<div>', {
11845				'class': 'able-transcript-desc'
11846			});
11847			var $descHiddenSpan = $('<span>',{
11848				'class': 'able-hidden'
11849			});
11850			$descHiddenSpan.attr('lang', thisObj.lang);
11851			$descHiddenSpan.text(thisObj.tt.prefHeadingDescription + ': ');
11852			$descDiv.append($descHiddenSpan);
11853
11854			var flattenComponentForDescription = function(comp) {
11855
11856				var result = [];
11857				if (comp.type === 'string') {
11858					result.push(comp.value);
11859				}
11860				else {
11861					for (var i = 0; i < comp.children.length; i++) {
11862						result = result.concat(flattenComponentForDescription(comp.children[i]));
11863					}
11864				}
11865				return result;
11866			}
11867
11868			var $descSpan = $('<span>',{
11869				'class': 'able-transcript-seekpoint'
11870			});
11871			for (var i = 0; i < desc.components.children.length; i++) {
11872				var results = flattenComponentForDescription(desc.components.children[i]);
11873				for (var jj = 0; jj < results.length; jj++) {
11874					$descSpan.append(results[jj]);
11875				}
11876			}
11877			$descSpan.attr('data-start', desc.start.toString());
11878			$descSpan.attr('data-end', desc.end.toString());
11879			$descDiv.append($descSpan);
11880
11881			div.append($descDiv);
11882		};
11883
11884		var addCaption = function(div, cap) {
11885
11886			var $capSpan = $('<span>',{
11887				'class': 'able-transcript-seekpoint able-transcript-caption'
11888			});
11889
11890			var flattenComponentForCaption = function(comp) {
11891
11892				var result = [];
11893
11894				var parts = 0;
11895
11896				var flattenString = function (str) {
11897
11898					parts++;
11899
11900					var flatStr;
11901					var result = [];
11902					if (str === '') {
11903						return result;
11904					}
11905
11906					var openBracket = str.indexOf('[');
11907					var closeBracket = str.indexOf(']');
11908					var openParen = str.indexOf('(');
11909					var closeParen = str.indexOf(')');
11910
11911					var hasBrackets = openBracket !== -1 && closeBracket !== -1;
11912					var hasParens = openParen !== -1 && closeParen !== -1;
11913
11914					if (hasParens || hasBrackets) {
11915						if (parts > 1) {
11916							// force a line break between sections that contain parens or brackets
11917							var silentSpanBreak = '<br/>';
11918						}
11919						else {
11920							var silentSpanBreak = '';
11921						}
11922						var silentSpanOpen = silentSpanBreak + '<span class="able-unspoken">';
11923						var silentSpanClose = '</span>';
11924						if (hasParens && hasBrackets) {
11925							// string has both!
11926							if (openBracket < openParen) {
11927								// brackets come first. Parse parens separately
11928								hasParens = false;
11929							}
11930							else {
11931								// parens come first. Parse brackets separately
11932								hasBrackets = false;
11933							}
11934						}
11935					}
11936					if (hasParens) {
11937						flatStr = str.substring(0, openParen);
11938						flatStr += silentSpanOpen;
11939						flatStr += str.substring(openParen, closeParen + 1);
11940						flatStr += silentSpanClose;
11941						flatStr += flattenString(str.substring(closeParen + 1));
11942						result.push(flatStr);
11943					}
11944					else if (hasBrackets) {
11945						flatStr = str.substring(0, openBracket);
11946						flatStr += silentSpanOpen;
11947						flatStr += str.substring(openBracket, closeBracket + 1);
11948						flatStr += silentSpanClose;
11949						flatStr += flattenString(str.substring(closeBracket + 1));
11950						result.push(flatStr);
11951					}
11952					else {
11953						result.push(str);
11954					}
11955					return result;
11956				};
11957
11958				if (comp.type === 'string') {
11959					result = result.concat(flattenString(comp.value));
11960				}
11961				else if (comp.type === 'v') {
11962					var $vSpan = $('<span>',{
11963						'class': 'able-unspoken'
11964					});
11965					$vSpan.text('(' + comp.value + ')');
11966					result.push($vSpan);
11967					for (var i = 0; i < comp.children.length; i++) {
11968						var subResults = flattenComponentForCaption(comp.children[i]);
11969						for (var jj = 0; jj < subResults.length; jj++) {
11970							result.push(subResults[jj]);
11971						}
11972					}
11973				}
11974				else if (comp.type === 'b' || comp.type === 'i') {
11975					if (comp.type === 'b') {
11976						var $tag = $('<strong>');
11977					}
11978					else if (comp.type === 'i') {
11979						var $tag = $('<em>');
11980					}
11981					for (var i = 0; i < comp.children.length; i++) {
11982						var subResults = flattenComponentForCaption(comp.children[i]);
11983						for (var jj = 0; jj < subResults.length; jj++) {
11984							$tag.append(subResults[jj]);
11985						}
11986					}
11987					if (comp.type === 'b' || comp.type == 'i') {
11988						result.push($tag,' ');
11989					}
11990				}
11991				else {
11992					for (var i = 0; i < comp.children.length; i++) {
11993						result = result.concat(flattenComponentForCaption(comp.children[i]));
11994					}
11995				}
11996				return result;
11997			};
11998
11999			for (var i = 0; i < cap.components.children.length; i++) {
12000				var results = flattenComponentForCaption(cap.components.children[i]);
12001				for (var jj = 0; jj < results.length; jj++) {
12002					var result = results[jj];
12003					if (typeof result === 'string') {
12004						if (thisObj.lyricsMode) {
12005							// add <br> BETWEEN each caption and WITHIN each caption (if payload includes "\n")
12006							result = result.replace('\n','<br>') + '<br>';
12007						}
12008						else {
12009							// just add a space between captions
12010							result += ' ';
12011						}
12012					}
12013					$capSpan.append(result);
12014				}
12015			}
12016			$capSpan.attr('data-start', cap.start.toString());
12017			$capSpan.attr('data-end', cap.end.toString());
12018			div.append($capSpan);
12019			div.append(' \n');
12020		};
12021
12022		// keep looping as long as any one of the three arrays has content
12023		while ((nextChapter < chapters.length) || (nextDesc < descriptions.length) || (nextCap < captions.length)) {
12024
12025			if ((nextChapter < chapters.length) && (nextDesc < descriptions.length) && (nextCap < captions.length)) {
12026				// they all three have content
12027				var firstStart = Math.min(chapters[nextChapter].start,descriptions[nextDesc].start,captions[nextCap].start);
12028			}
12029			else if ((nextChapter < chapters.length) && (nextDesc < descriptions.length)) {
12030				// chapters & descriptions have content
12031				var firstStart = Math.min(chapters[nextChapter].start,descriptions[nextDesc].start);
12032			}
12033			else if ((nextChapter < chapters.length) && (nextCap < captions.length)) {
12034				// chapters & captions have content
12035				var firstStart = Math.min(chapters[nextChapter].start,captions[nextCap].start);
12036			}
12037			else if ((nextDesc < descriptions.length) && (nextCap < captions.length)) {
12038				// descriptions & captions have content
12039				var firstStart = Math.min(descriptions[nextDesc].start,captions[nextCap].start);
12040			}
12041			else {
12042				var firstStart = null;
12043			}
12044			if (firstStart !== null) {
12045				if (typeof chapters[nextChapter] !== 'undefined' && chapters[nextChapter].start === firstStart) {
12046					addChapter($main, chapters[nextChapter]);
12047					nextChapter += 1;
12048				}
12049				else if (typeof descriptions[nextDesc] !== 'undefined' && descriptions[nextDesc].start === firstStart) {
12050					addDescription($main, descriptions[nextDesc]);
12051					nextDesc += 1;
12052				}
12053				else {
12054					addCaption($main, captions[nextCap]);
12055					nextCap += 1;
12056				}
12057			}
12058			else {
12059				if (nextChapter < chapters.length) {
12060					addChapter($main, chapters[nextChapter]);
12061					nextChapter += 1;
12062				}
12063				else if (nextDesc < descriptions.length) {
12064					addDescription($main, descriptions[nextDesc]);
12065					nextDesc += 1;
12066				}
12067				else if (nextCap < captions.length) {
12068					addCaption($main, captions[nextCap]);
12069					nextCap += 1;
12070				}
12071			}
12072		}
12073		// organize transcript into blocks using [] and () as starting points
12074		var $components = $main.children();
12075		var spanCount = 0;
12076		var openBlock = true;
12077		$components.each(function() {
12078			if ($(this).hasClass('able-transcript-caption')) {
12079				if ($(this).text().indexOf('[') !== -1 || $(this).text().indexOf('(') !== -1) {
12080					// this caption includes a bracket or parenth. Start a new block
12081					// close the previous block first
12082					if (spanCount > 0) {
12083						$main.find('.able-block-temp').removeClass('able-block-temp').wrapAll('<div class="able-transcript-block"></div>');
12084						spanCount = 0;
12085					}
12086				}
12087				$(this).addClass('able-block-temp');
12088				spanCount++;
12089			}
12090			else {
12091				// this is not a caption. Close the caption block
12092				if (spanCount > 0) {
12093					$main.find('.able-block-temp').removeClass('able-block-temp').wrapAll('<div class="able-transcript-block"></div>');
12094					spanCount = 0;
12095				}
12096			}
12097		});
12098		return $main;
12099	};
12100
12101})(jQuery);
12102
12103(function ($) {
12104	AblePlayer.prototype.showSearchResults = function() {
12105
12106		// search VTT file for all instances of searchTerms
12107		// Currently just supports search terms separated with one or more spaces
12108
12109		// TODO: Add support for more robust search syntax:
12110		// Search terms wrapped in quotation marks ("") must occur exactly as they appear in the quotes
12111		// Search terms with an attached minus sign (e.g., -term) are to be excluded from results
12112		// Boolean AND/OR operators
12113		// ALSO: Add localization support
12114
12115		var thisObj = this;
12116		if (this.searchDiv && this.searchString) {
12117			if ($('#' + this.SearchDiv)) {
12118				var searchStringHtml = '<p>' + this.tt.resultsSummary1 + ' ';
12119					searchStringHtml += '<span id="able-search-term-echo">' + this.searchString + '</span>';
12120					searchStringHtml += '</p>';
12121				var resultsArray = this.searchFor(this.searchString, this.searchIgnoreCaps);
12122				if (resultsArray.length > 0) {
12123					var $resultsSummary = $('<p>',{
12124						'class': 'able-search-results-summary'
12125					});
12126					var resultsSummaryText = this.tt.resultsSummary2;
12127					resultsSummaryText += ' <strong>' + resultsArray.length + '</strong> ';
12128					resultsSummaryText += this.tt.resultsSummary3 + ' ';
12129					resultsSummaryText += this.tt.resultsSummary4;
12130					$resultsSummary.html(resultsSummaryText);
12131					var $resultsList = $('<ul>');
12132					for (var i = 0; i < resultsArray.length; i++) {
12133						var resultId = 'aria-search-result-' + i;
12134						var $resultsItem = $('<li>',{});
12135						var itemStartTime = this.secondsToTime(resultsArray[i]['start']);
12136						var itemLabel = this.tt.searchButtonLabel + ' ' + itemStartTime['title'];
12137						var itemStartSpan = $('<button>',{
12138							'class': 'able-search-results-time',
12139							'data-start': resultsArray[i]['start'],
12140							'title': itemLabel,
12141							'aria-label': itemLabel,
12142							'aria-describedby': resultId
12143						});
12144						itemStartSpan.text(itemStartTime['value']);
12145						// add a listener for clisk on itemStart
12146						itemStartSpan.on('click',function(e) {
12147							thisObj.seekTrigger = 'search';
12148							var spanStart = parseFloat($(this).attr('data-start'));
12149							// Add a tiny amount so that we're inside the span.
12150							spanStart += .01;
12151							thisObj.seeking = true;
12152							thisObj.seekTo(spanStart);
12153						});
12154
12155						var itemText = $('<span>',{
12156							'class': 'able-search-result-text',
12157							'id': resultId
12158						})
12159						itemText.html('...' + resultsArray[i]['caption'] + '...');
12160						$resultsItem.append(itemStartSpan, itemText);
12161						$resultsList.append($resultsItem);
12162					}
12163					$('#' + this.searchDiv).html(searchStringHtml).append($resultsSummary,$resultsList);
12164				}
12165				else {
12166					var noResults = $('<p>').text(this.tt.noResultsFound);
12167					$('#' + this.searchDiv).html(searchStringHtml).append(noResults);
12168				}
12169			}
12170		}
12171	};
12172
12173	AblePlayer.prototype.searchFor = function(searchString, ignoreCaps) {
12174
12175		// return chronological array of caption cues that match searchTerms
12176		var captionLang, captions, results, caption, c, i, j;
12177		results = [];
12178		// split searchTerms into an array
12179		var searchTerms = searchString.split(' ');
12180		if (this.captions.length > 0) {
12181			// Get caption track that matches this.searchLang
12182			for (i=0; i < this.captions.length; i++) {
12183				if (this.captions[i].language === this.searchLang) {
12184					captionLang = this.searchLang;
12185					captions = this.captions[i].cues;
12186				}
12187			}
12188			if (captions.length > 0) {
12189				c = 0;
12190				for (i = 0; i < captions.length; i++) {
12191					if ($.inArray(captions[i].components.children[0]['type'], ['string','i','b','u','v','c']) !== -1) {
12192						caption = this.flattenCueForCaption(captions[i]);
12193						var captionNormalized = ignoreCaps ? caption.toLowerCase() : caption;
12194						for (j = 0; j < searchTerms.length; j++) {
12195							var searchTermNormalized = ignoreCaps ? searchTerms[j].toLowerCase() : searchTerms[j];
12196							if (captionNormalized.indexOf(searchTermNormalized) !== -1) {
12197								results[c] = [];
12198								results[c]['start'] = captions[i].start;
12199								results[c]['lang'] = captionLang;
12200								results[c]['caption'] = this.highlightSearchTerm(searchTerms,caption);
12201								c++;
12202								break;
12203							}
12204						}
12205					}
12206				}
12207			}
12208		}
12209		return results;
12210	};
12211
12212	AblePlayer.prototype.highlightSearchTerm = function(searchTerms, resultString) {
12213		// highlight ALL found searchTerms in the current resultString
12214		// Need to step through the remaining terms to see if they're present as well
12215		searchTerms.forEach(function(searchTerm) {
12216			var reg = new RegExp(searchTerm, 'gi');
12217			resultString = resultString.replace(reg, '<span class="able-search-term">$&</span>');
12218		});
12219		return resultString;
12220	};
12221
12222	AblePlayer.prototype.secondsToTime = function(totalSeconds) {
12223
12224		// return an array of totalSeconds converted into two formats
12225		// time['value'] = HH:MM:SS with hours dropped if there are none
12226		// time['title'] = a speakable rendering, so speech rec users can easily speak the link
12227
12228		// first, round down to nearest second
12229		var totalSeconds = Math.floor(totalSeconds);
12230
12231		var hours = parseInt( totalSeconds / 3600 , 10) % 24;
12232		var minutes = parseInt( totalSeconds / 60 , 10) % 60;
12233		var seconds = totalSeconds % 60;
12234		var value = '';
12235		var title = '';
12236		if (hours > 0) {
12237			value += hours + ':';
12238			if (hours == 1) {
12239				title += '1 ' + this.tt.hour + ' ';
12240			}
12241			else {
12242				title += hours + ' ' + this.tt.hours + ' ';
12243			}
12244		}
12245		if (minutes < 10) {
12246			value += '0' + minutes + ':';
12247			if (minutes > 0) {
12248				if (minutes == 1) {
12249					title += '1 ' + this.tt.minute + ' ';
12250				}
12251				else {
12252					title += minutes + ' ' + this.tt.minutes + ' ';
12253				}
12254			}
12255		}
12256		else {
12257			value += minutes + ':';
12258			title += minutes + ' ' + this.tt.minutes + ' ';
12259		}
12260		if (seconds < 10) {
12261			value += '0' + seconds;
12262			if (seconds > 0) {
12263				if (seconds == 1) {
12264					title += '1 ' + this.tt.second + ' ';
12265				}
12266				else {
12267					title += seconds + ' ' + this.tt.seconds + ' ';
12268				}
12269			}
12270		}
12271		else {
12272			value += seconds;
12273			title += seconds + ' ' + this.tt.seconds + ' ';
12274		}
12275		var time = [];
12276		time['value'] = value;
12277		time['title'] = title;
12278		return time;
12279	};
12280})(jQuery);
12281
12282(function ($) {
12283	// Media events
12284	AblePlayer.prototype.onMediaUpdateTime = function (duration, elapsed) {
12285
12286
12287		// duration and elapsed are passed from callback functions of Vimeo API events
12288		// duration is expressed as sss.xxx
12289		// elapsed is expressed as sss.xxx
12290		var thisObj = this;
12291		this.getMediaTimes(duration,elapsed).then(function(mediaTimes) {
12292			thisObj.duration = mediaTimes['duration'];
12293			thisObj.elapsed = mediaTimes['elapsed'];
12294			if (thisObj.duration > 0) { 
12295				// do all the usual time-sync stuff during playback
12296				if (thisObj.prefHighlight === 1) {
12297					thisObj.highlightTranscript(thisObj.elapsed);
12298				}				
12299				thisObj.updateCaption(thisObj.elapsed);
12300				thisObj.showDescription(thisObj.elapsed);
12301				thisObj.updateChapter(thisObj.elapsed);
12302				thisObj.updateMeta(thisObj.elapsed);
12303				thisObj.refreshControls('timeline', thisObj.duration, thisObj.elapsed); 
12304			}	
12305		});
12306	};
12307
12308	AblePlayer.prototype.onMediaPause = function () {
12309
12310		if (this.controlsHidden) {
12311			this.fadeControls('in');
12312			this.controlsHidden = false;
12313		}
12314		if (this.hideControlsTimeoutStatus === 'active') {
12315			window.clearTimeout(this.hideControlsTimeout);
12316			this.hideControlsTimeoutStatus = 'clear';
12317
12318		}
12319		this.refreshControls('playpause');
12320	};
12321
12322	AblePlayer.prototype.onMediaComplete = function () {
12323		// if there's a playlist, advance to next item and start playing
12324		if (this.hasPlaylist && !this.cueingPlaylistItem) {
12325			if (this.playlistIndex === (this.$playlist.length - 1)) {
12326				// this is the last track in the playlist
12327				if (this.loop) {
12328					this.playlistIndex = 0;
12329					this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
12330					this.cuePlaylistItem(0);
12331				}
12332				else {
12333					this.playing = false;
12334					this.paused = true;
12335				}
12336			}
12337			else {
12338				// this is not the last track. Play the next one.
12339				this.playlistIndex++;
12340				this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
12341				this.cuePlaylistItem(this.playlistIndex)
12342			}
12343		}
12344		this.refreshControls('init');
12345	};
12346
12347	AblePlayer.prototype.onMediaNewSourceLoad = function () {
12348
12349		var loadIsComplete = false; 
12350
12351		if (this.cueingPlaylistItem) {
12352			// this variable was set in order to address bugs caused by multiple firings of media 'end' event
12353			// safe to reset now
12354			this.cueingPlaylistItem = false;
12355		}
12356		if (this.recreatingPlayer) { 
12357			// same as above; different bugs 
12358			this.recreatingPlayer = false; 
12359		}
12360		if (this.playbackRate) {
12361			// user has set playbackRate on a previous src or track
12362			// use that setting on the new src or track too
12363			this.setPlaybackRate(this.playbackRate);
12364		}
12365		if (this.userClickedPlaylist) {
12366			if (!this.startedPlaying || this.okToPlay) {
12367				// start playing; no further user action is required
12368				this.playMedia();
12369				loadIsComplete = true; 
12370			 }
12371		}
12372		else if (this.seekTrigger == 'restart' ||
12373				this.seekTrigger == 'chapter' ||
12374				this.seekTrigger == 'transcript' ||
12375				this.seekTrigger == 'search'
12376				) {
12377			// by clicking on any of these elements, user is likely intending to play
12378			// Not included: elements where user might click multiple times in succession
12379			// (i.e., 'rewind', 'forward', or seekbar); for these, video remains paused until user initiates play
12380			this.playMedia();
12381			loadIsComplete = true; 
12382		}
12383		else if (this.swappingSrc) {
12384			// new source file has just been loaded
12385			if (this.hasPlaylist) {
12386				// a new source file from the playlist has just been loaded
12387				if ((this.playlistIndex !== this.$playlist.length) || this.loop) {
12388					// this is not the last track in the playlist (OR playlist is looping so it doesn't matter)
12389					this.playMedia();
12390					loadIsComplete = true; 
12391				}
12392			}
12393			else if (this.swapTime > 0) {
12394				if (this.seekStatus === 'complete') { 
12395					if (this.okToPlay) {
12396						// should be able to resume playback
12397						this.playMedia();					
12398					}
12399					loadIsComplete = true; 
12400				}
12401				else if (this.seekStatus === 'seeking') { 
12402				}
12403				else { 
12404					if (this.swapTime === this.elapsed) { 
12405						// seek is finished! 
12406						this.seekStatus = 'complete'; 
12407						if (this.okToPlay) {
12408							// should be able to resume playback
12409							this.playMedia();					
12410						}
12411						loadIsComplete = true; 
12412					}
12413					else { 
12414						// seeking hasn't started yet 
12415						// first, determine whether it's possible 
12416						if (this.hasDescTracks) { 
12417							// do nothing. Unable to seek ahead if there are descTracks
12418							loadIsComplete = true; 
12419						}
12420						else if (this.durationsAreCloseEnough(this.duration,this.prevDuration)) {
12421							// durations of two sources are close enough to making seek ahead in new source ok
12422							this.seekStatus = 'seeking'; 
12423							this.seekTo(this.swapTime);
12424						}
12425						else { 							
12426							// durations of two sources are too dissimilar to support seeking ahead to swapTime.  						
12427							loadIsComplete = true; 
12428						}
12429					}
12430				}
12431			}
12432			else {				
12433				// swapTime is 0. No seeking required. 
12434				if (this.playing) { 
12435					this.playMedia(); 
12436					// swap is complete. Reset vars. 
12437					loadIsComplete = true; 					
12438				}
12439			}
12440		}
12441		else if (!this.startedPlaying) {
12442			if (this.startTime > 0) {
12443				if (this.seeking) {
12444					// a seek has already been initiated
12445					// since canplaythrough has been triggered, the seek is complete
12446					this.seeking = false;
12447					if (this.okToPlay) {
12448						this.playMedia();
12449					}
12450					loadIsComplete = true; 
12451				}
12452				else {
12453					// haven't started seeking yet
12454					this.seekTo(this.startTime);
12455				}
12456			}
12457			else if (this.defaultChapter && typeof this.selectedChapters !== 'undefined') {
12458				this.seekToChapter(this.defaultChapter);
12459			}
12460			else {
12461				// there is no startTime, therefore no seeking required
12462				if (this.okToPlay) {
12463					this.playMedia();
12464				}
12465				loadIsComplete = true; 				
12466			}
12467		}
12468		else if (this.hasPlaylist) { 
12469			// new source media is part of a playlist, but user didn't click on it 
12470			// (and somehow, swappingSrc is false)
12471			// this may happen when the previous track ends and next track loads 
12472			// this same code is called above when swappingSrc is true 
12473			if ((this.playlistIndex !== this.$playlist.length) || this.loop) {
12474				// this is not the last track in the playlist (OR playlist is looping so it doesn't matter)
12475				this.playMedia();
12476				loadIsComplete = true; 
12477			}
12478		}
12479		else { 
12480			// None of the above. 
12481			// User is likely seeking to a new time, but not loading a new media source
12482			// need to reset vars 
12483			loadIsComplete = true; 
12484		}
12485		if (loadIsComplete) { 
12486			// reset vars 
12487			this.swappingSrc = false; 			
12488			this.seekStatus = null; 
12489			this.swapTime = 0; 
12490			this.seekTrigger = null;
12491			this.seekingFromTranscript = false;		
12492			this.userClickedPlaylist = false;
12493			this.okToPlay = false; 	
12494		}
12495		this.refreshControls('init');
12496		if (this.$focusedElement) { 		
12497			this.restoreFocus(); 
12498			this.$focusedElement = null; 
12499		}
12500	};
12501
12502	AblePlayer.prototype.durationsAreCloseEnough = function(d1,d2) { 
12503
12504		// Compare the durations of two media sources to determine whether it's ok to seek ahead after swapping src 
12505		// The durations may not be exact, but they might be "close enough" 
12506		// returns true if "close enough", otherwise false 
12507
12508		var tolerance, diff; 
12509		
12510		tolerance = 1;  // number of seconds between rounded durations that is considered "close enough" 
12511		
12512		diff = Math.abs(Math.round(d1) - Math.round(d2)); 
12513		
12514		if (diff <= tolerance) {
12515			return true;  
12516		}
12517		else { 
12518			return false; 
12519		}
12520	};
12521
12522	AblePlayer.prototype.restoreFocus = function() { 
12523
12524		// function called after player has been rebuilt (during media swap)
12525		// the original focusedElement no longer exists, 
12526		// but this function finds a match in the new player 
12527		// and places focus there 
12528
12529		var classList; 
12530
12531		if (this.$focusedElement) { 
12532			
12533			if ((this.$focusedElement).attr('role') === 'button') { 
12534				classList = this.$focusedElement.attr("class").split(/\s+/);
12535				$.each(classList, function(index, item) {
12536					if (item.substring(0,20) === 'able-button-handler-') {
12537						$('div.able-controller div.' + item).focus();  
12538					}
12539				});
12540			}
12541		}
12542
12543	};
12544
12545	AblePlayer.prototype.addSeekbarListeners = function () {
12546
12547		var thisObj = this;
12548
12549		// Handle seek bar events.
12550		this.seekBar.bodyDiv.on('startTracking', function (e) {
12551			thisObj.pausedBeforeTracking = thisObj.paused;
12552			thisObj.pauseMedia();
12553		}).on('tracking', function (e, position) {
12554			// Scrub transcript, captions, and metadata.
12555			thisObj.highlightTranscript(position);
12556			thisObj.updateCaption(position);
12557			thisObj.showDescription(position);
12558			thisObj.updateChapter(thisObj.convertChapterTimeToVideoTime(position));
12559			thisObj.updateMeta(position);
12560			thisObj.refreshControls('init');
12561		}).on('stopTracking', function (e, position) {
12562			if (thisObj.useChapterTimes) {
12563				thisObj.seekTo(thisObj.convertChapterTimeToVideoTime(position));
12564			}
12565			else {
12566				thisObj.seekTo(position);
12567			}
12568			if (!thisObj.pausedBeforeTracking) {
12569				setTimeout(function () {
12570					thisObj.playMedia();
12571				}, 200);
12572			}
12573		});
12574	};
12575
12576	AblePlayer.prototype.onClickPlayerButton = function (el) {
12577
12578		var whichButton, prefsPopup;
12579
12580		whichButton = this.getButtonNameFromClass($(el).attr('class')); 
12581
12582		if (whichButton === 'play') {
12583			this.clickedPlay = true;
12584			this.handlePlay();
12585		}
12586		else if (whichButton === 'restart') {
12587			this.seekTrigger = 'restart';
12588			this.handleRestart();
12589		}
12590		else if (whichButton === 'previous') {
12591			this.userClickedPlaylist = true;
12592			this.okToPlay = true; 
12593			this.seekTrigger = 'previous';
12594			this.buttonWithFocus = 'previous';
12595			this.handlePrevTrack();
12596		}
12597		else if (whichButton === 'next') {
12598			this.userClickedPlaylist = true;
12599			this.okToPlay = true; 
12600			this.seekTrigger = 'next';
12601			this.buttonWithFocus = 'next';
12602			this.handleNextTrack();
12603		}
12604		else if (whichButton === 'rewind') {
12605			this.seekTrigger = 'rewind';
12606			this.handleRewind();
12607		}
12608		else if (whichButton === 'forward') {
12609			this.seekTrigger = 'forward';
12610			this.handleFastForward();
12611		}
12612		else if (whichButton === 'mute') {
12613			this.handleMute();
12614		}
12615		else if (whichButton === 'volume') {
12616			this.handleVolumeButtonClick();
12617		}
12618		else if (whichButton === 'faster') {
12619			this.handleRateIncrease();
12620		}
12621		else if (whichButton === 'slower') {
12622			this.handleRateDecrease();
12623		}
12624		else if (whichButton === 'captions') {
12625			this.handleCaptionToggle();
12626		}
12627		else if (whichButton === 'chapters') {
12628			this.handleChapters();
12629		}
12630		else if (whichButton === 'descriptions') {
12631			this.handleDescriptionToggle();
12632		}
12633		else if (whichButton === 'sign') {
12634			if (!this.closingSign) {
12635				this.handleSignToggle();
12636			}
12637		}
12638		else if (whichButton === 'preferences') {
12639			if ($(el).attr('data-prefs-popup') === 'menu') {
12640				this.handlePrefsClick();
12641			}
12642			else {
12643				this.showingPrefsDialog = true; // stopgap
12644				this.closePopups();
12645				prefsPopup = $(el).attr('data-prefs-popup');
12646				if (prefsPopup === 'keyboard') {
12647					this.keyboardPrefsDialog.show();
12648				}
12649				else if (prefsPopup === 'captions') {
12650					this.captionPrefsDialog.show();
12651				}
12652				else if (prefsPopup === 'descriptions') {
12653					this.descPrefsDialog.show();
12654				}
12655				else if (prefsPopup === 'transcript') {
12656					this.transcriptPrefsDialog.show();
12657				}
12658				this.showingPrefsDialog = false;
12659			}
12660		}
12661		else if (whichButton === 'help') {
12662			this.handleHelpClick();
12663		}
12664		else if (whichButton === 'transcript') {
12665			if (!this.closingTranscript) {
12666				this.handleTranscriptToggle();
12667			}
12668		}
12669		else if (whichButton === 'fullscreen') {
12670			this.clickedFullscreenButton = true;
12671			this.handleFullscreenToggle();
12672		}
12673	};
12674
12675	AblePlayer.prototype.getButtonNameFromClass = function (classString) { 
12676
12677		// player control buttons all have class="able-button-handler-x"  where x is the identifier 
12678		// buttons might also have other classes assigned though
12679
12680		var classes, i; 
12681
12682		classes = classString.split(' '); 
12683		for (i = 0; i < classes.length; i++) { 
12684			if (classes[i].substring(0,20) === 'able-button-handler-') { 
12685				return classes[i].substring(20); 
12686			}
12687		}		
12688		return classString; 
12689	}
12690
12691	AblePlayer.prototype.okToHandleKeyPress = function () {
12692
12693		// returns true unless user's focus is on a UI element
12694		// that is likely to need supported keystrokes, including space
12695
12696		var activeElement = AblePlayer.getActiveDOMElement();
12697
12698		if ($(activeElement).prop('tagName') === 'INPUT') {
12699			return false;
12700		}
12701		else {
12702			return true;
12703		}
12704	};
12705
12706	AblePlayer.prototype.onPlayerKeyPress = function (e) {
12707
12708		// handle keystrokes (using DHTML Style Guide recommended key combinations)
12709		// https://web.archive.org/web/20130127004544/http://dev.aol.com/dhtml_style_guide/#mediaplayer
12710		// Modifier keys Alt + Ctrl are on by default, but can be changed within Preferences
12711		// NOTE #1: Style guide only supports Play/Pause, Stop, Mute, Captions, & Volume Up & Down
12712		// The rest are reasonable best choices
12713		// NOTE #2: If there are multiple players on a single page, keystroke handlers
12714		// are only bound to the FIRST player
12715		// NOTE #3: The DHTML Style Guide is now the W3C WAI-ARIA Authoring Guide and has undergone many revisions
12716		// including removal of the "media player" design pattern. There's an issue about that:
12717		// https://github.com/w3c/aria-practices/issues/27
12718
12719		var which, $thisElement;
12720
12721		// Convert to lower case.
12722		which = e.which;
12723		if (which >= 65 && which <= 90) {
12724			which += 32;
12725		}
12726		$thisElement = $(document.activeElement);
12727
12728		if (which === 27) { // escape
12729			if (this.$transcriptArea && $.contains(this.$transcriptArea[0],$thisElement[0]) && !this.hidingPopup) {
12730				// This element is part of transcript area.
12731				this.handleTranscriptToggle();
12732				return false;
12733			}
12734		}
12735		if (!this.okToHandleKeyPress()) {
12736			return false;
12737		}
12738
12739		// Only use keypress to control player if focus is NOT on a form field or contenteditable element
12740		// (or a textarea element with player in stenoMode)
12741		if (!(
12742			$(':focus').is('[contenteditable]') ||
12743			$(':focus').is('input') ||
12744			($(':focus').is('textarea') && !this.stenoMode) ||
12745			$(':focus').is('select') ||
12746			e.target.hasAttribute('contenteditable') ||
12747			e.target.tagName === 'INPUT' ||
12748			(e.target.tagName === 'TEXTAREA' && !this.stenoMode) ||
12749			e.target.tagName === 'SELECT'
12750		)){
12751			if (which === 27) { // escape
12752				this.closePopups();
12753				this.$tooltipDiv.hide();
12754				this.seekBar.hideSliderTooltips();
12755			}
12756			else if (which === 32) { // spacebar = play/pause
12757				// disable spacebar support for play/pause toggle as of 4.2.10
12758				// spacebar should not be handled everywhere on the page, since users use that to scroll the page
12759				// when the player has focus, most controls are buttons so spacebar should be used to trigger the buttons
12760				if ($thisElement.attr('role') === 'button') {
12761					// register a click on this element
12762					e.preventDefault();
12763					$thisElement.click();
12764				}
12765			}
12766			else if (which === 112) { // p = play/pause
12767				if (this.usingModifierKeys(e)) {
12768					e.preventDefault();
12769					this.handlePlay();
12770				}
12771			}
12772			else if (which === 115) { // s = stop (now restart)
12773				if (this.usingModifierKeys(e)) {
12774					e.preventDefault();
12775					this.handleRestart();
12776				}
12777			}
12778			else if (which === 109) { // m = mute
12779				if (this.usingModifierKeys(e)) {
12780					e.preventDefault();
12781					this.handleMute();
12782				}
12783			}
12784			else if (which === 118) { // v = volume
12785				if (this.usingModifierKeys(e)) {
12786					e.preventDefault();
12787					this.handleVolumeButtonClick();
12788				}
12789			}
12790			else if (which >= 49 && which <= 57) { // set volume 1-9
12791				if (this.usingModifierKeys(e)) {
12792					e.preventDefault();
12793					this.handleVolumeKeystroke(which);
12794				}
12795			}
12796			else if (which === 99) { // c = caption toggle
12797				if (this.usingModifierKeys(e)) {
12798					e.preventDefault();
12799					this.handleCaptionToggle();
12800				}
12801			}
12802			else if (which === 100) { // d = description
12803				if (this.usingModifierKeys(e)) {
12804					e.preventDefault();
12805					this.handleDescriptionToggle();
12806				}
12807			}
12808			else if (which === 102) { // f = forward
12809				if (this.usingModifierKeys(e)) {
12810					e.preventDefault();
12811					this.handleFastForward();
12812				}
12813			}
12814			else if (which === 114) { // r = rewind
12815				if (this.usingModifierKeys(e)) {
12816					e.preventDefault();
12817					this.handleRewind();
12818				}
12819			}
12820			else if (which === 98) { // b = back (previous track)
12821				if (this.usingModifierKeys(e)) {
12822					e.preventDefault();
12823					this.handlePrevTrack();
12824				}
12825			}
12826			else if (which === 110) { // n = next track
12827				if (this.usingModifierKeys(e)) {
12828					e.preventDefault();
12829					this.handleNextTrack();
12830				}
12831			}
12832			else if (which === 101) { // e = preferences
12833				if (this.usingModifierKeys(e)) {
12834					e.preventDefault();
12835					this.handlePrefsClick();
12836				}
12837			}
12838			else if (which === 13) { // Enter
12839				if ($thisElement.attr('role') === 'button' || $thisElement.prop('tagName') === 'SPAN') {
12840					// register a click on this element
12841					// if it's a transcript span the transcript span click handler will take over
12842					$thisElement.click();
12843				}
12844				else if ($thisElement.prop('tagName') === 'LI') {
12845					$thisElement.click();
12846				}
12847			}
12848		}
12849	};
12850
12851	AblePlayer.prototype.addHtml5MediaListeners = function () {
12852
12853		var thisObj = this;
12854
12855		// NOTE: iOS and some browsers do not support autoplay
12856		// and no events are triggered until media begins to play
12857		// Able Player gets around this by automatically loading media in some circumstances
12858		// (see initialize.js > initPlayer() for details)
12859
12860		this.$media
12861			.on('emptied',function() {
12862				// do something
12863			})
12864			.on('loadedmetadata',function() {
12865				// should be able to get duration now
12866				thisObj.duration = thisObj.media.duration;				
12867				var x = 50.5; 
12868				var y = 51.9; 
12869				var diff = Math.abs(Math.round(x)-Math.round(y)); 
12870			})
12871			.on('canplay',function() {
12872				// previously handled seeking to startTime here
12873				// but it's probably safer to wait for canplaythrough
12874				// so we know player can seek ahead to anything
12875			})
12876			.on('canplaythrough',function() {
12877				// previously onMediaNewSourceLoad() was called on 'loadedmetadata' 
12878				// but that proved to be too soon for some of this functionality. 
12879				// TODO: Monitor this. If moving it here causes performance issues, 
12880				// consider moving some or all of this functionality to 'canplay' 
12881					thisObj.onMediaNewSourceLoad(); 								
12882			})
12883			.on('play',function() {
12884				// both 'play' and 'playing' seem to be fired in all browsers (including IE11)
12885				// therefore, doing nothing here & doing everything when 'playing' is triggered
12886				 thisObj.refreshControls('playpause');
12887			})
12888			.on('playing',function() {
12889				thisObj.playing = true;
12890				thisObj.paused = false;
12891				thisObj.swappingSrc = false; 
12892				thisObj.refreshControls('playpause');
12893			})
12894			.on('ended',function() {
12895				thisObj.playing = false;
12896				thisObj.paused = true;
12897				thisObj.onMediaComplete();
12898			})
12899			.on('progress', function() {
12900				thisObj.refreshControls('timeline');
12901			})
12902			.on('waiting',function() {
12903				 // do something
12904				 // previously called refreshControls() here but this event probably doesn't warrant a refresh
12905			})
12906			.on('durationchange',function() {
12907				// Display new duration.
12908				thisObj.refreshControls('timeline');
12909			})
12910			.on('timeupdate',function() {
12911				thisObj.onMediaUpdateTime(); // includes a call to refreshControls()
12912			})
12913			.on('pause',function() {				
12914				if (!thisObj.clickedPlay) {					
12915					// 'pause' was triggered automatically, not initiated by user
12916					// this happens in some browsers when swapping source
12917					// (e.g., between tracks in a playlist or swapping description)
12918					if (thisObj.hasPlaylist || thisObj.swappingSrc) {						
12919						// do NOT set playing to false.
12920						// doing so prevents continual playback after new track is loaded
12921					}
12922					else {
12923						thisObj.playing = false;
12924						thisObj.paused = true;
12925					}
12926				}
12927				else {
12928					thisObj.playing = false;
vendor: 4,697 bytes, lines 12929-13054
12929					thisObj.paused = true;
12930				}
12931				thisObj.clickedPlay = false; // done with this variable
12932				thisObj.onMediaPause(); // includes a call to refreshControls()
12933			})
12934			.on('ratechange',function() {
12935				// do something
12936			})
12937			.on('volumechange',function() {
12938				thisObj.volume = thisObj.getVolume();
12939			})
12940			.on('error',function() {
12941				if (thisObj.debug) {
12942					switch (thisObj.media.error.code) {
12943						case 1:
12944							console.log('HTML5 Media Error: MEDIA_ERR_ABORTED');
12945							break;
12946						case 2:
12947							console.log('HTML5 Media Error: MEDIA_ERR_NETWORK ');
12948							break;
12949						case 3:
12950							console.log('HTML5 Media Error: MEDIA_ERR_DECODE ');
12951							break;
12952						case 4:
12953							console.log('HTML5 Media Error: MEDIA_ERR_SRC_NOT_SUPPORTED ');
12954							break;
12955					}
12956				}
12957			});
12958	};
12959
12960	AblePlayer.prototype.addVimeoListeners = function () {
12961
12962		var thisObj = this;
12963
12964		// Vimeo doesn't seem to support chaining of on() functions
12965		// so each event listener must be attached separately
12966		this.vimeoPlayer.on('loaded', function(vimeoId) {
12967			 // Triggered when a new video is loaded in the player
12968			thisObj.onMediaNewSourceLoad();
12969		 });
12970		this.vimeoPlayer.on('play', function(data) {
12971			// Triggered when the video plays
12972			thisObj.playing = true;
12973			thisObj.startedPlaying = true;
12974			thisObj.paused = false;
12975			thisObj.refreshControls('playpause');
12976		});
12977		this.vimeoPlayer.on('ended', function(data) {
12978			// Triggered any time the video playback reaches the end.
12979			// Note: when loop is turned on, the ended event will not fire.
12980			thisObj.playing = false;
12981			thisObj.paused = true;
12982			thisObj.onMediaComplete();
12983		});
12984		this.vimeoPlayer.on('bufferstart', function() {
12985			// Triggered when buffering starts in the player.
12986			// This is also triggered during preload and while seeking.
12987			// There is no associated data with this event.
12988		});
12989		this.vimeoPlayer.on('bufferend', function() {
12990			// Triggered when buffering ends in the player.
12991			// This is also triggered at the end of preload and seeking.
12992			// There is no associated data with this event.
12993		});
12994		this.vimeoPlayer.on('progress', function(data) {
12995			// Triggered as the video is loaded.
12996			 // Reports back the amount of the video that has been buffered (NOT the amount played)
12997			 // Data has keys duration, percent, and seconds
12998	 	});
12999		this.vimeoPlayer.on('seeking', function(data) {
13000		 	// Triggered when the player starts seeking to a specific time.
13001			 // A timeupdate event will also be fired at the same time.
13002	 	});
13003		this.vimeoPlayer.on('seeked', function(data) {
13004			// Triggered when the player seeks to a specific time.
13005			// A timeupdate event will also be fired at the same time.
13006		});
13007		this.vimeoPlayer.on('timeupdate',function(data) {
13008			// Triggered as the currentTime of the video updates.
13009			 // It generally fires every 250ms, but it may vary depending on the browser.
13010			thisObj.onMediaUpdateTime(data['duration'], data['seconds']);
13011		});
13012		this.vimeoPlayer.on('pause',function(data) {
13013			// Triggered when the video pauses
13014			if (!thisObj.clickedPlay) {
13015					// 'pause' was triggered automatically, not initiated by user
13016				// this happens in some browsers (not Chrome, as of 70.x)
13017				// when swapping source (e.g., between tracks in a playlist, or swapping description)
13018				if (thisObj.hasPlaylist || thisObj.swappingSrc) {
13019						// do NOT set playing to false.
13020					// doing so prevents continual playback after new track is loaded
13021				}
13022				else {
13023					thisObj.playing = false;
13024					thisObj.paused = true;
13025				}
13026			}
13027			else {
13028				thisObj.playing = false;
13029				thisObj.paused = true;
13030			}
13031			thisObj.clickedPlay = false; // done with this variable
13032			thisObj.onMediaPause();
13033			thisObj.refreshControls('playpause');
13034		});
13035		this.vimeoPlayer.on('playbackratechange',function(data) {
13036			// Triggered when the playback rate of the video in the player changes.
13037			// The ability to change rate can be disabled by the creator
13038			// and the event will not fire for those videos.
13039			// data contains one key: 'playbackRate'
13040			thisObj.vimeoPlaybackRate = data['playbackRate'];
13041		});
13042		this.vimeoPlayer.on('texttrackchange', function(data) {
13043			// Triggered when the active text track (captions/subtitles) changes.
13044			// The values will be null if text tracks are turned off.
13045			// data contains three keys: kind, label, language
13046		});
13047		this.vimeoPlayer.on('volumechange',function(data) {
13048			// Triggered when the volume in the player changes.
13049			// Some devices do not support setting the volume of the video
13050			// independently from the system volume,
13051			// so this event will never fire on those devices.
13052			thisObj.volume = data['volume'] * 10;
13053		});
13054		this.vimeoPlayer.on('error',function(data) {
13055			// do something with the available data
13056			// data contains three keys: message, method, name
13057			// message: A user-friendly error message
13058			// method: The Vimeo API method call that triggered the error
13059			// name: Name of the error (not necesssarily user-friendly)
13060		});
13061	};
13062
13063	AblePlayer.prototype.addEventListeners = function () {
13064
13065		var thisObj, whichButton, thisElement;
13066
13067		// Save the current object context in thisObj for use with inner functions.
13068		thisObj = this;
13069
13070		// Appropriately resize media player for full screen.
13071		$(window).on('resize',function () {
13072			thisObj.resizePlayer();
13073		});
13074
13075		// Refresh player if it changes from hidden to visible
13076		// There is no event triggered by a change in visibility
13077		// but MutationObserver works in most browsers (but NOT in IE 10 or earlier)
13078		// http://caniuse.com/#feat=mutationobserver
13079		if (window.MutationObserver) {
13080			var target = this.$ableDiv[0];
13081			var observer = new MutationObserver(function(mutations) {
13082				mutations.forEach(function(mutation) {
13083					if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
13084						// the player's style attribute has changed. Check to see if it's visible
13085						if (thisObj.$ableDiv.is(':visible')) {
13086							thisObj.refreshControls('init');
13087						}
13088					}
13089				});
13090			});
13091			var config = { attributes: true, childList: true, characterData: true };
13092			observer.observe(target, config);
13093		}
13094		else {
13095			// browser doesn't support MutationObserver
13096			// TODO: Figure out an alternative solution for this rare use case in older browsers
13097			// See example in buildplayer.js > useSvg()
13098		}
13099		if (typeof this.seekBar !== 'undefined') {
13100			this.addSeekbarListeners();
13101		}
13102		else {
13103			// wait a bit and try again
13104			// TODO: Should set this up to keep trying repeatedly.
13105			// Seekbar listeners are critical.
13106			setTimeout(function() {
13107				if (typeof thisObj.seekBar !== 'undefined') {
13108					thisObj.addSeekbarListeners();
13109				}
13110			},2000);
13111		}
13112
13113		// handle clicks on player buttons
13114		this.$controllerDiv.find('div[role="button"]').on('click',function(e){
13115			e.stopPropagation();
13116			thisObj.onClickPlayerButton(this);
13117		});
13118
13119		// handle clicks (left only) anywhere on the page. If any popups are open, close them.
13120		$(document).on('click',function(e) {
13121
13122			if (e.button !== 0) { // not a left click
13123				return false;
13124			}			
13125			if ($('.able-popup:visible').length || $('.able-volume-popup:visible')) {
13126				// at least one popup is visible
13127				thisObj.closePopups();
13128			}
13129			if (e.target.tagName === 'VIDEO') { 
13130				// user clicked the video (not an element that sits on top of the video)
13131				// handle this as a play/pause toggle click 
13132				thisObj.clickedPlay = true; 
13133			}			
13134		});
13135
13136		// handle mouse movement over player; make controls visible again if hidden
13137		this.$ableDiv.on('mousemove',function() {
13138			if (thisObj.controlsHidden) {
13139				thisObj.fadeControls('in');
13140				thisObj.controlsHidden = false;
13141				// if there's already an active timeout, clear it and start timer again
13142				if (thisObj.hideControlsTimeoutStatus === 'active') {
13143					window.clearTimeout(thisObj.hideControlsTimeout);
13144					thisObj.hideControlsTimeoutStatus = 'clear';
13145				}
13146				if (thisObj.hideControls) {
13147					// after showing controls, hide them again after a brief timeout
13148					thisObj.invokeHideControlsTimeout();
13149				}
13150			}
13151			else {
13152				// if there's already an active timeout, clear it and start timer again
13153				if (thisObj.hideControlsTimeoutStatus === 'active') {
13154					window.clearTimeout(thisObj.hideControlsTimeout);
13155					thisObj.hideControlsTimeoutStatus = 'clear';
13156					if (thisObj.hideControls) {
13157						thisObj.invokeHideControlsTimeout();
13158					}
13159				}
13160			}
13161		});
13162
13163		// if user presses a key from anywhere on the page, show player controls
13164		$(document).keydown(function(e) {
13165			if (thisObj.controlsHidden) {
13166				thisObj.fadeControls('in');
13167				thisObj.controlsHidden = false;
13168				if (thisObj.hideControlsTimeoutStatus === 'active') {
13169					window.clearTimeout(thisObj.hideControlsTimeout);
13170					thisObj.hideControlsTimeoutStatus = 'clear';
13171				}
13172				if (thisObj.hideControls) {
13173					// after showing controls, hide them again after a brief timeout
13174					thisObj.invokeHideControlsTimeout();
13175				}
13176			}
13177			else {
13178				// controls are visible
13179				// if there's already an active timeout, clear it and start timer again
13180				if (thisObj.hideControlsTimeoutStatus === 'active') {
13181					window.clearTimeout(thisObj.hideControlsTimeout);
13182					thisObj.hideControlsTimeoutStatus = 'clear';
13183
13184					if (thisObj.hideControls) {
13185						thisObj.invokeHideControlsTimeout();
13186					}
13187				}
13188			}
13189		});
13190
13191		// handle local keydown events if this isn't the only player on the page;
13192		// otherwise these are dispatched by global handler (see ableplayer-base,js)
13193		this.$ableDiv.keydown(function (e) {
13194			if (AblePlayer.nextIndex > 1) {
13195				thisObj.onPlayerKeyPress(e);
13196			}
13197		});
13198
13199		// If stenoMode is enabled in an iframe, handle keydown events from the iframe
13200		if (this.stenoMode && (typeof this.stenoFrameContents !== 'undefined')) {
13201			this.stenoFrameContents.on('keydown',function(e) {
13202				thisObj.onPlayerKeyPress(e);
13203			});
13204		};
13205
13206		// transcript is not a child of this.$ableDiv
13207		// therefore, must be added separately
13208		if (this.$transcriptArea) {
13209			this.$transcriptArea.on('keydown',function (e) {
13210				if (AblePlayer.nextIndex > 1) {
13211					thisObj.onPlayerKeyPress(e);
13212				}
13213			});
13214		}
13215
13216		// handle clicks on playlist items
13217		if (this.$playlist) {
13218			this.$playlist.click(function(e) {
13219				if (!thisObj.userClickedPlaylist) {
13220					// stopgap in case multiple clicks are fired on the same playlist item
13221					thisObj.userClickedPlaylist = true; // will be set to false after new src is loaded & canplaythrough is triggered
13222					thisObj.playlistIndex = $(this).index();
13223					thisObj.cuePlaylistItem(thisObj.playlistIndex);
13224				}
13225			});
13226		}
13227
13228		// Also play/pause when clicking on the media.
13229		this.$media.click(function () {
13230			thisObj.handlePlay();
13231		});
13232
13233		// add listeners for media events
13234		if (this.player === 'html5') {
13235			this.addHtml5MediaListeners();
13236		}
13237		else if (this.player === 'vimeo') {
13238			 this.addVimeoListeners();
13239		}
13240		else if (this.player === 'youtube') {
13241			// Youtube doesn't give us time update events, so we just periodically generate them ourselves
13242			setInterval(function () {
13243				thisObj.onMediaUpdateTime();
13244			}, 300);
13245		}
13246	};
13247})(jQuery);
13248
13249(function ($) {
13250
13251	AblePlayer.prototype.initDragDrop = function ( which ) {
13252
13253		// supported values of which: 'sign', 'transcript'
13254
13255		// NOTE: "Drag and Drop" for Able Player is a metaphor only!!!
13256		// HTML5 Drag & Drop API enables moving elements to new locations in the DOM
13257		// Thats not our purpose; we're simply changing the visible position on-screen
vendor: 4,298 bytes, lines 13258-13397
13258		// Therefore, the drag & drop interface was overhauled in v2.3.41 to simple
13259		// use mouse (and keyboard) events to change CSS positioning properties
13260
13261		// There are nevertheless lessons to be learned from Drag & Drop about accessibility:
13262		// http://dev.opera.com/articles/accessible-drag-and-drop/
13263
13264		var thisObj, $window, $toolbar, windowName, $resizeHandle, $resizeSvg, 
13265			i, x1, y1, x2, y2, $resizeLine, resizeZIndex;
13266
13267		thisObj = this;
13268
13269		if (which === 'transcript') {
13270			$window = this.$transcriptArea;
13271			windowName = 'transcript-window';
13272			$toolbar = this.$transcriptToolbar;
13273		}
13274		else if (which === 'sign') {
13275			$window = this.$signWindow;
13276			windowName = 'sign-window';
13277			$toolbar = this.$signToolbar;
13278		}
13279
13280		// add class to trigger change in cursor on hover
13281		$toolbar.addClass('able-draggable');
13282
13283		// add resize handle selector to bottom right corner
13284		$resizeHandle = $('<div>',{
13285			'class': 'able-resizable'
13286		});
13287
13288		// fill it with three parallel diagonal lines 
13289		$resizeSvg = $('<svg>').attr({
13290			'width': '100%',
13291			'height': '100%',
13292			'viewBox': '0 0 100 100',
13293			'preserveAspectRatio': 'none'
13294		});
13295		for (i=1; i<=3; i++) { 
13296			if (i === 1) { 
13297				x1 = '100'; 
13298				y1 = '0'; 
13299				x2 = '0'; 
13300				y2 = '100'; 
13301			}
13302			else if (i === 2) { 
13303				x1 = '33'; 
13304				y1 = '100'; 
13305				x2 = '100'; 
13306				y2 = '33'; 
13307			}
13308			else if (i === 3) { 
13309				x1 = '67'; 
13310				y1 = '100'; 
13311				x2 = '100'; 
13312				y2 = '67'; 
13313			}
13314			$resizeLine = $('<line>').attr({ 
13315				'x1': x1,
13316				'y1': y1,
13317				'x2': x2,
13318				'y2': y2,
13319				'vector-effect': 'non-scaling-stroke'				
13320			})
13321			$resizeSvg.append($resizeLine); 
13322		}
13323		$resizeHandle.html($resizeSvg); 
13324
13325		// assign z-index that's slightly higher than parent window
13326		resizeZIndex = parseInt($window.css('z-index')) + 100;
13327		$resizeHandle.css('z-index',resizeZIndex);
13328		$window.append($resizeHandle);
13329
13330		// Final step: Need to refresh the DOM in order for browser to process & display the SVG
13331		$resizeHandle.html($resizeHandle.html());
13332
13333		// add event listener to toolbar to start and end drag
13334		// other event listeners will be added when drag starts
13335		$toolbar.on('mousedown mouseup touchstart touchend', function(e) {
13336			e.stopPropagation();
13337			if (e.type === 'mousedown' || e.type === 'touchstart') {
13338				if (!thisObj.windowMenuClickRegistered) {
13339					thisObj.windowMenuClickRegistered = true;
13340					thisObj.startMouseX = e.pageX;
13341					thisObj.startMouseY = e.pageY;
13342					thisObj.dragDevice = 'mouse'; // ok to use this even if device is a touchpad
13343					thisObj.startDrag(which, $window);
13344				}
13345			}
13346			else if (e.type === 'mouseup' || e.type === 'touchend') {
13347				if (thisObj.dragging && thisObj.dragDevice === 'mouse') {
13348					thisObj.endDrag(which);
13349				}
13350			}
13351			return false;
13352		});
13353
13354		// add event listeners for resizing
13355		$resizeHandle.on('mousedown mouseup touchstart touchend', function(e) {
13356			e.stopPropagation();
13357			if (e.type === 'mousedown' || e.type === 'touchstart') {
13358				if (!thisObj.windowMenuClickRegistered) {
13359					thisObj.windowMenuClickRegistered = true;
13360					thisObj.startMouseX = e.pageX;
13361					thisObj.startMouseY = e.pageY;
13362					thisObj.startResize(which, $window);
13363				}
13364			}
13365			else if (e.type === 'mouseup' || e.type === 'touchend') {
13366				if (thisObj.resizing) {
13367					thisObj.endResize(which);
13368				}
13369			}
13370			return false;
13371		});
13372
13373		// whenever a window is clicked, bring it to the foreground
13374		$window.on('click', function() {
13375
13376			if (!thisObj.windowMenuClickRegistered && !thisObj.finishingDrag) {
13377				thisObj.updateZIndex(which);
13378			}
13379			thisObj.finishingDrag = false;
13380		});
13381
13382		this.addWindowMenu(which,$window,windowName);
13383	};
13384
13385	AblePlayer.prototype.addWindowMenu = function(which, $window, windowName) {
13386
13387		var thisObj, $windowAlert, menuId, $newButton, $buttonIcon, buttonImgSrc, $buttonImg,
13388			$buttonLabel, tooltipId, $tooltip, $popup,
13389			label, position, buttonHeight, buttonWidth, tooltipY, tooltipX, tooltipStyle, tooltip,
13390			$optionList, menuBaseId, options, i, $optionItem, option, menuId;
13391
13392		thisObj = this;
13393
13394		// Add a Boolean that will be set to true temporarily if window button or a menu item is clicked
13395		// This will prevent the click event from also triggering a mousedown event on the toolbar
13396		// (which would unexpectedly send the window into drag mode)
13397		this.windowMenuClickRegistered = false;
13398
13399		// Add another Boolean that will be set to true temporarily when mouseup fires at the end of a drag
13400		// this will prevent the click event from being triggered
13401		this.finishingDrag = false;
13402
13403		// create an alert div and add it to window
13404		$windowAlert = $('<div role="alert"></div>');
13405		$windowAlert.addClass('able-alert');
13406		$windowAlert.hide();
13407		$windowAlert.appendTo(this.$activeWindow);
13408		$windowAlert.css({
13409			top: $window.offset().top
13410		});
13411
13412		// add button to draggable window which triggers a popup menu
13413		// for now, re-use preferences icon for this purpose
13414		menuId = this.mediaId + '-' + windowName + '-menu';
13415		$newButton = $('<button>',{
13416			'type': 'button',
13417			'tabindex': '0',
13418			'aria-label': this.tt.windowButtonLabel,
13419			'aria-haspopup': 'true',
13420			'aria-controls': menuId,
13421			'aria-expanded': 'false',
13422			'class': 'able-button-handler-preferences'
13423		});
13424		if (this.iconType === 'font') {
13425			$buttonIcon = $('<span>',{
13426				'class': 'icon-preferences',
13427				'aria-hidden': 'true'
13428			});
13429			$newButton.append($buttonIcon);
13430		}
13431		else {
13432			// use image
13433			buttonImgSrc = this.rootPath + 'button-icons/' + this.toolbarIconColor + '/preferences.png';
13434			$buttonImg = $('<img>',{
13435				'src': buttonImgSrc,
13436				'alt': '',
13437				'role': 'presentation'
13438			});
13439			$newButton.append($buttonImg);
13440		}
13441
13442		// add the visibly-hidden label for screen readers that don't support aria-label on the button
13443		$buttonLabel = $('<span>',{
13444			'class': 'able-clipped'
13445		}).text(this.tt.windowButtonLabel);
13446		$newButton.append($buttonLabel);
13447
13448		// add a tooltip that displays aria-label on mouseenter or focus
13449		tooltipId = this.mediaId + '-' + windowName + '-tooltip';
13450		$tooltip = $('<div>',{
13451			'class' : 'able-tooltip',
13452			'id' : tooltipId
13453		}).hide();
13454		$newButton.on('mouseenter focus',function(e) {
13455			var label = $(this).attr('aria-label');
13456			// get position of this button
13457			var position = $(this).position();
13458			var buttonHeight = $(this).height();
13459			var buttonWidth = $(this).width();
13460			var tooltipY = position.top - buttonHeight - 5;
13461			var tooltipX = 0;
13462			var tooltipStyle = {
13463				left: '',
13464				right: tooltipX + 'px',
13465				top: tooltipY + 'px'
13466			};
13467			var tooltip = AblePlayer.localGetElementById($newButton[0], tooltipId).text(label).css(tooltipStyle);
13468			thisObj.showTooltip(tooltip);
13469			$(this).on('mouseleave blur',function() {
13470				AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
13471			});
13472		});
13473
13474		// setup popup menu
13475		$popup = this.setupPopups(windowName); // 'transcript-window' or 'sign-window'
13476		// define vars and assemble all the parts
13477		if (which === 'transcript') {
13478			this.$transcriptAlert = $windowAlert;
13479			this.$transcriptPopupButton = $newButton;
13480			this.$transcriptPopup = $popup;
13481			this.$transcriptToolbar.prepend($windowAlert,$newButton,$tooltip,$popup);
13482		}
13483		else if (which === 'sign') {
13484			this.$signAlert = $windowAlert;
13485			this.$signPopupButton = $newButton;
13486			this.$signPopup = $popup;
13487			this.$signToolbar.append($windowAlert,$newButton,$tooltip,$popup);
13488		}
13489
13490		// handle button click
13491		$newButton.on('click mousedown keydown',function(e) {
13492
13493			if (thisObj.focusNotClick) {
13494				return false;
13495			}
13496			if (thisObj.dragging) {
13497				thisObj.dragKeys(which, e);
13498				return false;
13499			}
13500			e.stopPropagation();
13501			if (!thisObj.windowMenuClickRegistered && !thisObj.finishingDrag) {
13502				// don't set windowMenuClickRegistered yet; that happens in handler function
13503				thisObj.handleWindowButtonClick(which, e);
13504			}
13505			thisObj.finishingDrag = false;
13506		});
13507
13508		this.addResizeDialog(which, $window);
13509	};
13510
13511	AblePlayer.prototype.addResizeDialog = function (which, $window) {
13512
13513		var thisObj, $windowPopup, $windowButton,
13514			widthId, heightId, startingWidth, startingHeight, aspectRatio,
13515			$resizeForm, $resizeWrapper,
13516			$resizeWidthDiv, $resizeWidthInput, $resizeWidthLabel,
13517			$resizeHeightDiv, $resizeHeightInput, $resizeHeightLabel,
13518			tempWidth, tempHeight,
13519			$saveButton, $cancelButton, newWidth, newHeight, resizeDialog;
13520
13521		thisObj = this;
13522
13523		if (which === 'transcript') {
13524			$windowPopup = this.$transcriptPopup;
13525			$windowButton = this.$transcriptPopupButton;
13526		}
13527		else if (which === 'sign') {
13528			$windowPopup = this.$signPopup;
13529			$windowButton = this.$signPopupButton;
13530		}
13531
13532		widthId = this.mediaId + '-resize-' + which + '-width';
13533		heightId = this.mediaId + '-resize-' + which + '-height';
13534		startingWidth = $window.width();
13535		startingHeight = $window.height();
13536		aspectRatio = startingWidth / startingHeight;
13537
13538		$resizeForm = $('<div></div>',{
13539			'class' : 'able-resize-form'
13540		});
13541
13542		// inner container for all content, will be assigned to modal div's aria-describedby
13543		$resizeWrapper = $('<div></div>');
13544
13545		// width field
13546		$resizeWidthDiv = $('<div></div>');
13547		$resizeWidthInput = $('<input>',{
13548			'type': 'text',
13549			'id': widthId,
13550			'value': startingWidth
13551		});
13552		$resizeWidthLabel = $('<label>',{
13553			'for': widthId
13554		}).text(this.tt.width);
13555
13556		// height field
13557		$resizeHeightDiv = $('<div></div>');
13558		$resizeHeightInput = $('<input>',{
13559			'type': 'text',
13560			'id': heightId,
13561			'value': startingHeight
13562		});
13563		$resizeHeightLabel = $('<label>',{
13564			'for': heightId
13565		}).text(this.tt.height);
13566
13567		if (which === 'sign') {
13568			// make height a read-only field
13569			// and calculate its value based on width to preserve aspect ratio
13570			$resizeHeightInput.prop('readonly',true);
13571			$resizeWidthInput.on('input',function() {
13572				tempWidth = $(this).val();
13573				tempHeight = Math.round(tempWidth/aspectRatio, 0);
13574				$resizeHeightInput.val(tempHeight);
13575			})
13576		}
13577
13578		// Add save and cancel buttons.
13579		$saveButton = $('<button class="modal-button">' + this.tt.save + '</button>');
13580		$cancelButton = $('<button class="modal-button">' + this.tt.cancel + '</button>');
13581		$saveButton.on('click',function () {
13582			newWidth = $('#' + widthId).val();
13583			newHeight = $('#' + heightId).val();
13584			if (newWidth !== startingWidth || newHeight !== startingHeight) {
13585				thisObj.resizeObject(which,newWidth,newHeight);
13586				thisObj.updateCookie(which);
13587			}
13588			resizeDialog.hide();
13589			$windowPopup.hide();
13590			$windowButton.focus();
13591		});
13592		$cancelButton.on('click',function () {
13593			resizeDialog.hide();
13594			$windowPopup.hide();
13595			$windowButton.focus();
13596		});
13597
13598		// Now assemble all the parts
13599		$resizeWidthDiv.append($resizeWidthLabel,$resizeWidthInput);
13600		$resizeHeightDiv.append($resizeHeightLabel,$resizeHeightInput);
13601		$resizeWrapper.append($resizeWidthDiv,$resizeHeightDiv);
13602		$resizeForm.append($resizeWrapper,'<hr>',$saveButton,$cancelButton);
13603
13604		// must be appended to the BODY!
13605		// otherwise when aria-hidden="true" is applied to all background content
13606		// that will include an ancestor of the dialog,
13607		// which will render the dialog unreadable by screen readers
13608		$('body').append($resizeForm);
13609		resizeDialog = new AccessibleDialog($resizeForm, $windowButton, 'dialog', true, this.tt.windowResizeHeading, $resizeWrapper, this.tt.closeButtonLabel, '20em');
13610		if (which === 'transcript') {
13611			this.transcriptResizeDialog = resizeDialog;
13612		}
13613		else if (which === 'sign') {
13614			this.signResizeDialog = resizeDialog;
13615		}
13616	};
13617
13618	AblePlayer.prototype.handleWindowButtonClick = function (which, e) {
13619
13620		var thisObj, $windowPopup, $windowButton, $toolbar, popupTop;
13621
13622		thisObj = this;
13623
13624		if (this.focusNotClick) {
13625			// transcript or sign window has just opened,
13626			// and focus moved to the window button
13627			// ignore the keystroke that triggered the popup
13628			return false;
13629		}
13630
13631		if (which === 'transcript') {
13632			$windowPopup = this.$transcriptPopup;
13633			$windowButton = this.$transcriptPopupButton;
13634			$toolbar = this.$transcriptToolbar;
13635		}
13636		else if (which === 'sign') {
13637			$windowPopup = this.$signPopup;
13638			$windowButton = this.$signPopupButton;
13639			$toolbar = this.$signToolbar;
13640		}
13641
13642		if (e.type === 'keydown') {
13643			// user pressed a key
13644			if (e.which === 32 || e.which === 13) {
13645				// this was Enter or space
13646				this.windowMenuClickRegistered = true;
13647			}
13648			else if (e.which === 27) { // escape
13649				if ($windowPopup.is(':visible')) {
13650					// close the popup menu
13651					$windowPopup.hide('fast', function() {
13652						// also reset the Boolean
13653						thisObj.windowMenuClickRegistered = false;
13654						// also restore menu items to their original state
13655						$windowPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
13656						// also return focus to window options button
13657						$windowButton.focus();
13658					});
13659				}
13660				else {
13661					// popup isn't open. Close the window
13662					if (which === 'sign') {
13663						this.handleSignToggle();
13664					}
13665					else if (which === 'transcript') {
13666						this.handleTranscriptToggle();
13667					}
13668				}
13669			}
13670			else {
13671				return false;
13672			}
13673		}
13674		else {
13675			// this was a mouse event
13676			this.windowMenuClickRegistered = true;
13677		}
13678
13679		if ($windowPopup.is(':visible')) {
13680			$windowPopup.hide(200,'',function() {
13681				thisObj.windowMenuClickRegistered = false; // reset
13682			});
13683			$windowPopup.find('li').removeClass('able-focus');
13684			$windowButton.attr('aria-expanded','false').focus();
13685		}
13686		else {
13687			// first, be sure window is on top
13688			this.updateZIndex(which);
13689			popupTop = $windowButton.position().top + $windowButton.outerHeight();
13690			$windowPopup.css('top', popupTop);
13691			$windowPopup.show(200,'',function() {
13692				$windowButton.attr('aria-expanded','true');
13693				$(this).find('li').first().focus().addClass('able-focus');
13694				thisObj.windowMenuClickRegistered = false; // reset
13695			});
13696		}
13697	};
13698
13699	AblePlayer.prototype.handleMenuChoice = function (which, choice, e) {
13700
13701		var thisObj, $window, $windowPopup, $windowButton, resizeDialog, width, height, $thisRadio;
13702
13703		thisObj = this;
13704		if (which === 'transcript') {
13705			$window = this.$transcriptArea;
13706			$windowPopup = this.$transcriptPopup;
13707			$windowButton = this.$transcriptPopupButton;
13708			resizeDialog = this.transcriptResizeDialog;
13709		}
13710		else if (which === 'sign') {
13711			$window = this.$signWindow;
13712			$windowPopup = this.$signPopup;
13713			$windowButton = this.$signPopupButton;
13714			resizeDialog = this.signResizeDialog;
13715		}
13716		this.$activeWindow = $window;
13717
13718		if (e.type === 'keydown') {
13719			if (e.which === 27) { // escape
13720				// hide the popup menu
13721				$windowPopup.hide('fast', function() {
13722					// also reset the Boolean
13723					thisObj.windowMenuClickRegistered = false;
13724					// also restore menu items to their original state
13725					$windowPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
13726					$windowButton.attr('aria-expanded','false');
13727					// also return focus to window options button
13728					$windowButton.focus();
13729				});
13730				return false;
13731			}
13732			else {
13733				// all other keys will be handled by upstream functions
13734				if (choice !== 'close') {
13735					this.$activeWindow = $window;
13736				}
13737				return false;
13738			}
13739		}
13740
13741		// hide the popup menu
13742		$windowPopup.hide('fast', function() {
13743			// also reset the boolean
13744			thisObj.windowMenuClickRegistered = false;
13745			// also restore menu items to their original state
13746			$windowPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
13747			$windowButton.attr('aria-expanded','false');
13748		});
13749		if (choice !== 'close') {
13750			$windowButton.focus();
13751		}
13752		if (choice === 'move') {
13753
13754			// temporarily add role="application" to activeWindow
13755			// otherwise, screen readers incercept arrow keys and moving window will not work
13756			this.$activeWindow.attr('role','application');
13757
13758			if (!this.showedAlert(which)) {
13759				this.showAlert(this.tt.windowMoveAlert,which);
13760				if (which === 'transcript') {
13761					this.showedTranscriptAlert = true;
13762				}
13763				else if (which === 'sign') {
13764					this.showedSignAlert = true;
13765				}
13766			}
13767			if (e.type === 'keydown') {
13768				this.dragDevice = 'keyboard';
13769			}
13770			else {
13771				this.dragDevice = 'mouse';
13772			}
13773			this.startDrag(which, $window);
13774			$windowPopup.hide().parent().focus();
13775		}
13776		else if (choice == 'resize') {
13777			// resize through the menu uses a form, not drag
13778			var resizeFields = resizeDialog.getInputs();
13779			if (resizeFields) {
13780				// reset width and height values in form
13781				resizeFields[0].value = $window.width();
13782				resizeFields[1].value = $window.height();
13783			}
13784			resizeDialog.show();
13785		}
13786		else if (choice == 'close') {
13787			// close window, place focus on corresponding button on controller bar
13788			if (which === 'transcript') {
13789				this.closingTranscript = true; // stopgrap to prevent double-firing of keypress
13790				this.handleTranscriptToggle();
13791			}
13792			else if (which === 'sign') {
13793				this.closingSign = true; // stopgrap to prevent double-firing of keypress
13794				this.handleSignToggle();
13795			}
13796		}
13797	};
13798
13799	AblePlayer.prototype.startDrag = function(which, $element) {
13800
13801		var thisObj, $windowPopup, zIndex, startPos, newX, newY;
13802
13803		thisObj = this;
13804
13805		if (!this.$activeWindow) {
13806			this.$activeWindow = $element;
13807		}
13808		this.dragging = true;
13809
13810		if (which === 'transcript') {
13811			$windowPopup = this.$transcriptPopup;
13812		}
13813		else if (which === 'sign') {
13814			$windowPopup = this.$signPopup;
13815		}
13816
13817		if (!this.showedAlert(which)) {
13818			this.showAlert(this.tt.windowMoveAlert,which);
13819			if (which === 'transcript') {
13820				this.showedTranscriptAlert = true;
13821			}
13822			else if (which === 'sign') {
13823				this.showedSignAlert = true;
13824			}
13825		}
13826
13827		// if window's popup menu is open, close it
13828		if ($windowPopup.is(':visible')) {
13829			$windowPopup.hide();
13830		}
13831
13832		// be sure this window is on top
13833		this.updateZIndex(which);
13834
13835		// get starting position of element
13836		startPos = this.$activeWindow.position();
13837		this.dragStartX = startPos.left;
13838		this.dragStartY = startPos.top;
13839
13840		if (typeof this.startMouseX === 'undefined') {
13841			this.dragDevice = 'keyboard';
13842			this.dragKeyX = this.dragStartX;
13843			this.dragKeyY = this.dragStartY;
13844			// add stopgap to prevent the Enter that triggered startDrag() from also triggering dragEnd()
13845			this.startingDrag = true;
13846		}
13847		else {
13848			this.dragDevice = 'mouse';
13849			// get offset between mouse position and top left corner of draggable element
13850			this.dragOffsetX = this.startMouseX - this.dragStartX;
13851			this.dragOffsetY = this.startMouseY - this.dragStartY;
13852		}
13853
13854		// prepare element for dragging
13855		this.$activeWindow.addClass('able-drag').css({
13856			'position': 'absolute',
13857			'top': this.dragStartY + 'px',
13858			'left': this.dragStartX + 'px'
13859		}).focus();
13860
13861		// add device-specific event listeners
13862		if (this.dragDevice === 'mouse') { // might also be a touchpad
13863			$(document).on('mousemove touchmove',function(e) {
13864				if (thisObj.dragging) {
13865					// calculate new top left based on current mouse position - offset
13866					newX = e.pageX - thisObj.dragOffsetX;
13867					newY = e.pageY - thisObj.dragOffsetY;
13868					thisObj.resetDraggedObject( newX, newY );
13869				}
13870			});
13871		}
13872		else if (this.dragDevice === 'keyboard') {
13873			this.$activeWindow.on('keydown',function(e) {
13874				if (thisObj.dragging) {
13875					thisObj.dragKeys(which, e);
13876				}
13877			});
13878		}
13879		return false;
13880	};
13881
13882	AblePlayer.prototype.dragKeys = function(which, e) {
13883
13884		var key, keySpeed;
13885
13886		var thisObj = this;
13887
13888		// stopgap to prevent firing on initial Enter or space
13889		// that selected "Move" from menu
13890		if (this.startingDrag) {
13891			this.startingDrag = false;
13892			return false;
13893		}
13894		key = e.which;
13895		keySpeed = 10; // pixels per keypress event
13896
13897		switch (key) {
13898			case 37:	// left
13899			case 63234:
13900				 this.dragKeyX -= keySpeed;
13901				break;
13902			case 38:	// up
13903			case 63232:
13904				this.dragKeyY -= keySpeed;
13905				break;
13906			case 39:	// right
13907			case 63235:
13908				this.dragKeyX += keySpeed;
13909				break;
13910			case 40:	// down
13911			case 63233:
13912				this.dragKeyY += keySpeed;
13913				break;
13914			case 13: 	// enter
13915			case 27: 	// escape
13916				this.endDrag(which);
13917				return false;
13918			default:
13919				return false;
13920		}
13921		this.resetDraggedObject(this.dragKeyX,this.dragKeyY);
13922		if (e.preventDefault) {
13923			e.preventDefault();
13924		}
13925		return false;
13926	};
13927
13928	AblePlayer.prototype.resetDraggedObject = function ( x, y) {
13929
13930		this.$activeWindow.css({
13931			'left': x + 'px',
13932			'top': y + 'px'
13933		});
13934	},
13935
13936	AblePlayer.prototype.resizeObject = function ( which, width, height ) {
13937
13938		var innerHeight;
13939
13940		// which is either 'transcript' or 'sign'
13941		this.$activeWindow.css({
13942			'width': width + 'px',
13943			'height': height + 'px'
13944		});
13945
13946		if (which === 'transcript') {
13947			// $activeWindow is the outer $transcriptArea
13948			// but the inner able-transcript also needs to be resized proporitionally
13949			// (it's 50px less than its outer container)
13950			innerHeight = height - 50;
13951			this.$transcriptDiv.css('height', innerHeight + 'px');
13952		}
13953	};
13954
13955	AblePlayer.prototype.endDrag = function(which) {
13956
13957		var thisObj, $window, $windowPopup, $windowButton;
13958		thisObj = this;
13959
13960		if (which === 'transcript') {
13961			$windowPopup = this.$transcriptPopup;
13962			$windowButton = this.$transcriptPopupButton;
13963		}
13964		else if (which === 'sign') {
13965			$windowPopup = this.$signPopup;
13966			$windowButton = this.$signPopupButton;
13967		}
13968
13969		$(document).off('mousemove mouseup touchmove touchup');
13970		this.$activeWindow.off('keydown').removeClass('able-drag');
13971		// restore activeWindow role from 'application' to 'dialog'
13972		this.$activeWindow.attr('role','dialog');
13973		this.$activeWindow = null;
13974
13975		if (this.dragDevice === 'keyboard') {
13976			$windowButton.focus();
13977		}
13978		this.dragging = false;
13979
13980		// save final position of dragged element
13981		this.updateCookie(which);
13982
13983		// reset starting mouse positions
13984		this.startMouseX = undefined;
13985		this.startMouseY = undefined;
13986
13987		// Boolean to stop stray events from firing
13988		this.windowMenuClickRegistered = false;
13989		this.finishingDrag = true; // will be reset after window click event
13990		// finishingDrag should e reset after window click event,
13991		// which is triggered automatically after mouseup
13992		// However, in case that's not reliable in some browsers
13993		// need to ensure this gets cancelled
13994		setTimeout(function() {
13995			thisObj.finishingDrag = false;
13996		}, 100);
13997	};
13998
13999	AblePlayer.prototype.isCloseToCorner = function($window, mouseX, mouseY) {
14000
14001		// return true if mouse is close to bottom right corner (resize target)
14002		var tolerance, position, top, left, width, height, bottom, right;
14003
14004		tolerance = 10; // number of pixels in both directions considered "close enough"
14005
14006		// first, get position of element
14007		position = $window.offset();
14008		top = position.top;
14009		left = position.left;
14010		width = $window.width();
14011		height = $window.height();
14012		bottom = top + height;
14013		right = left + width;
14014		if ((Math.abs(bottom-mouseY) <= tolerance) && (Math.abs(right-mouseX) <= tolerance)) {
14015			return true;
14016		}
14017		return false;
14018	};
14019
14020	AblePlayer.prototype.startResize = function(which, $element) {
14021
14022		var thisObj, $windowPopup, zIndex, startPos, newWidth, newHeight;
14023
14024		thisObj = this;
14025		this.$activeWindow = $element;
14026		this.resizing = true;
14027
14028		if (which === 'transcript') {
14029			$windowPopup = this.$transcriptPopup;
14030		}
14031		else if (which === 'sign') {
14032			$windowPopup = this.$signPopup;
14033		}
14034
14035		// if window's popup menu is open, close it & place focus on button (???)
14036		if ($windowPopup.is(':visible')) {
14037			$windowPopup.hide().parent().focus();
14038		}
14039
14040		// get starting width and height
14041		startPos = this.$activeWindow.position();
14042		this.dragKeyX = this.dragStartX;
14043		this.dragKeyY = this.dragStartY;
14044		this.dragStartWidth = this.$activeWindow.width();
14045		this.dragStartHeight = this.$activeWindow.height();
14046
14047		// add event listeners
14048		$(document).on('mousemove touchmove',function(e) {
14049			if (thisObj.resizing) {
14050				// calculate new width and height based on changes to mouse position
14051				newWidth = thisObj.dragStartWidth + (e.pageX - thisObj.startMouseX);
14052				newHeight = thisObj.dragStartHeight + (e.pageY - thisObj.startMouseY);
14053				thisObj.resizeObject( which, newWidth, newHeight );
14054			}
14055		});
14056		return false;
14057	};
14058
14059	AblePlayer.prototype.endResize = function(which) {
14060
14061		var $window, $windowPopup, $windowButton;
14062
14063		if (which === 'transcript') {
14064			$windowPopup = this.$transcriptPopup;
14065			$windowButton = this.$transcriptPopupButton;
14066		}
14067		else if (which === 'sign') {
14068			$windowPopup = this.$signPopup;
14069			$windowButton = this.$signPopupButton;
14070		}
14071
14072		$(document).off('mousemove mouseup touchmove touchup');
14073		this.$activeWindow.off('keydown');
14074		$windowButton.show().focus();
14075		this.resizing = false;
14076		this.$activeWindow.removeClass('able-resize');
14077
14078		// save final width and height of dragged element
14079		this.updateCookie(which);
14080
14081		// Booleans for preventing stray events
14082		this.windowMenuClickRegistered = false;
14083		this.finishingDrag = true;
14084
14085		// finishingDrag should e reset after window click event,
14086		// which is triggered automatically after mouseup
14087		// However, in case that's not reliable in some browsers
14088		// need to ensure this gets cancelled
14089		setTimeout(function() {
14090			this.finishingDrag = false;
14091		}, 100);
14092	};
14093
14094})(jQuery);
14095
14096(function ($) {
14097	AblePlayer.prototype.initSignLanguage = function() {
14098
14099		// Sign language is only currently supported in HTML5 player, not YouTube or Vimeo
14100		if (this.player === 'html5') {
14101			// check to see if there's a sign language video accompanying this video
14102			// check only the first source
14103			// If sign language is provided, it must be provided for all sources
14104			this.signFile = this.$sources.first().attr('data-sign-src');
14105			if (this.signFile) {
14106				if (this.isIOS()) {
14107					// IOS does not allow multiple videos to play simultaneously
14108					// Therefore, sign language as rendered by Able Player unfortunately won't work
14109					this.hasSignLanguage = false;
14110					if (this.debug) {
14111						console.log('Sign language has been disabled due to IOS restrictions');
14112					}
14113				}
14114				else {
14115					if (this.debug) {
14116						console.log('This video has an accompanying sign language video: ' + this.signFile);
14117					}
14118					this.hasSignLanguage = true;
14119					this.injectSignPlayerCode();
14120				}
14121			}
14122			else {
14123				this.hasSignLanguage = false;
14124			}
14125		}
14126		else {
14127			this.hasSignLanguage = false;
14128		}
14129	};
14130
14131	AblePlayer.prototype.injectSignPlayerCode = function() {
14132
14133		// create and inject surrounding HTML structure
14134
14135		var thisObj, signVideoId, signVideoWidth, i, signSrc, srcType, $signSource;
14136
14137		thisObj = this;
14138
14139		signVideoWidth = this.getDefaultWidth('sign');
14140
14141		signVideoId = this.mediaId + '-sign';
14142		this.$signVideo = $('<video>',{
14143			'id' : signVideoId,
14144			'tabindex' : '-1'
14145		});
14146		this.signVideo = this.$signVideo[0];
14147		// for each original <source>, add a <source> to the sign <video>
14148		for (i=0; i < this.$sources.length; i++) {
14149			signSrc = this.$sources[i].getAttribute('data-sign-src');
14150			srcType = this.$sources[i].getAttribute('type');
14151			if (signSrc) {
14152				$signSource = $('<source>',{
14153					'src' : signSrc,
14154					'type' : srcType
14155				});
14156				this.$signVideo.append($signSource);
14157			}
14158			else {
14159				// source is missing a sign language version
14160				// can't include sign language
14161				this.hasSignLanguage = false;
14162				break;
14163			}
14164		}
14165
14166		this.$signWindow = $('<div>',{
14167			'class' : 'able-sign-window',
14168			'role': 'dialog',
14169			'aria-label': this.tt.sign
14170		});
14171		this.$signToolbar = $('<div>',{
14172			'class': 'able-window-toolbar able-' + this.toolbarIconColor + '-controls'
14173		});
14174
14175		this.$signWindow.append(this.$signToolbar, this.$signVideo);
14176
14177		this.$ableWrapper.append(this.$signWindow);
14178
14179		// make it draggable
14180		this.initDragDrop('sign');
14181
14182		if (this.prefSign === 1) {
14183			// sign window is on. Go ahead and position it and show it
14184			this.positionDraggableWindow('sign',this.getDefaultWidth('sign'));
14185		}
14186		else {
14187			this.$signWindow.hide();
14188		}
14189	};
14190
14191})(jQuery);
14192
14193(function ($
vendor: 18,947 bytes, lines 14193-15169
14193) {
14194	// Look up ISO 639-1 language codes to be used as subtitle labels
14195	// In some instances "name" has been trunctation for readability
14196	// Sources:
14197	// http://stackoverflow.com/questions/3217492/list-of-language-codes-in-yaml-or-json/4900304#4900304
14198	// https://www.venea.net/web/culture_code
14199
14200	var isoLangs = {
14201		"ab":{
14202				"name":"Abkhaz",
14203				"nativeName":"аҧсуа"
14204		},
14205		"aa":{
14206				"name":"Afar",
14207				"nativeName":"Afaraf"
14208		},
14209		"af":{
14210				"name":"Afrikaans",
14211				"nativeName":"Afrikaans"
14212		},
14213		"ak":{
14214				"name":"Akan",
14215				"nativeName":"Akan"
14216		},
14217		"sq":{
14218				"name":"Albanian",
14219				"nativeName":"Shqip"
14220		},
14221		"am":{
14222				"name":"Amharic",
14223				"nativeName":"አማርኛ"
14224		},
14225		"ar":{
14226				"name":"Arabic",
14227				"nativeName":"العربية"
14228		},
14229		"an":{
14230				"name":"Aragonese",
14231				"nativeName":"Aragonés"
14232		},
14233		"hy":{
14234				"name":"Armenian",
14235				"nativeName":"Հայերեն"
14236		},
14237		"as":{
14238				"name":"Assamese",
14239				"nativeName":"অসমীয়া"
14240		},
14241		"av":{
14242				"name":"Avaric",
14243				"nativeName":"авар мацӀ, магӀарул мацӀ"
14244		},
14245		"ae":{
14246				"name":"Avestan",
14247				"nativeName":"avesta"
14248		},
14249		"ay":{
14250				"name":"Aymara",
14251				"nativeName":"aymar aru"
14252		},
14253		"az":{
14254				"name":"Azerbaijani",
14255				"nativeName":"azərbaycan dili"
14256		},
14257		"bm":{
14258				"name":"Bambara",
14259				"nativeName":"bamanankan"
14260		},
14261		"ba":{
14262				"name":"Bashkir",
14263				"nativeName":"башҡорт теле"
14264		},
14265		"eu":{
14266				"name":"Basque",
14267				"nativeName":"euskara, euskera"
14268		},
14269		"be":{
14270				"name":"Belarusian",
14271				"nativeName":"Беларуская"
14272		},
14273		"bn":{
14274				"name":"Bengali",
14275				"nativeName":"বাংলা"
14276		},
14277		"bh":{
14278				"name":"Bihari",
14279				"nativeName":"भोजपुरी"
14280		},
14281		"bi":{
14282				"name":"Bislama",
14283				"nativeName":"Bislama"
14284		},
14285		"bs":{
14286				"name":"Bosnian",
14287				"nativeName":"bosanski jezik"
14288		},
14289		"br":{
14290				"name":"Breton",
14291				"nativeName":"brezhoneg"
14292		},
14293		"bg":{
14294				"name":"Bulgarian",
14295				"nativeName":"български език"
14296		},
14297		"my":{
14298				"name":"Burmese",
14299				"nativeName":"ဗမာစာ"
14300		},
14301		"ca":{
14302				"name":"Catalan",
14303				"nativeName":"Català"
14304		},
14305		"ch":{
14306				"name":"Chamorro",
14307				"nativeName":"Chamoru"
14308		},
14309		"ce":{
14310				"name":"Chechen",
14311				"nativeName":"нохчийн мотт"
14312		},
14313		"ny":{
14314				"name":"Chichewa",
14315				"nativeName":"chiCheŵa, chinyanja"
14316		},
14317		"zh":{
14318				"name":"Chinese",
14319				"nativeName":"中文 (Zhōngwén), 汉语, 漢語"
14320		},
14321		"cv":{
14322				"name":"Chuvash",
14323				"nativeName":"чӑваш чӗлхи"
14324		},
14325		"kw":{
14326				"name":"Cornish",
14327				"nativeName":"Kernewek"
14328		},
14329		"co":{
14330				"name":"Corsican",
14331				"nativeName":"corsu, lingua corsa"
14332		},
14333		"cr":{
14334				"name":"Cree",
14335				"nativeName":"ᓀᐦᐃᔭᐍᐏᐣ"
14336		},
14337		"hr":{
14338				"name":"Croatian",
14339				"nativeName":"hrvatski"
14340		},
14341		"cs":{
14342				"name":"Czech",
14343				"nativeName":"česky, čeština"
14344		},
14345		"da":{
14346				"name":"Danish",
14347				"nativeName":"dansk"
14348		},
14349		"dv":{
14350				"name":"Divehi",
14351				"nativeName":"ދިވެހި"
14352		},
14353		"nl":{
14354				"name":"Dutch",
14355				"nativeName":"Nederlands, Vlaams"
14356		},
14357		"en":{
14358				"name":"English",
14359				"nativeName":"English"
14360		},
14361		"eo":{
14362				"name":"Esperanto",
14363				"nativeName":"Esperanto"
14364		},
14365		"et":{
14366				"name":"Estonian",
14367				"nativeName":"eesti, eesti keel"
14368		},
14369		"ee":{
14370				"name":"Ewe",
14371				"nativeName":"Eʋegbe"
14372		},
14373		"fo":{
14374				"name":"Faroese",
14375				"nativeName":"føroyskt"
14376		},
14377		"fj":{
14378				"name":"Fijian",
14379				"nativeName":"vosa Vakaviti"
14380		},
14381		"fi":{
14382				"name":"Finnish",
14383				"nativeName":"suomi, suomen kieli"
14384		},
14385		"fr":{
14386				"name":"French",
14387				"nativeName":"français, langue française"
14388		},
14389		"ff":{
14390				"name":"Fula",
14391				"nativeName":"Fulfulde, Pulaar, Pular"
14392		},
14393		"gl":{
14394				"name":"Galician",
14395				"nativeName":"Galego"
14396		},
14397		"ka":{
14398				"name":"Georgian",
14399				"nativeName":"ქართული"
14400		},
14401		"de":{
14402				"name":"German",
14403				"nativeName":"Deutsch"
14404		},
14405		"el":{
14406				"name":"Greek",
14407				"nativeName":"Ελληνικά"
14408		},
14409		"gn":{
14410				"name":"Guaraní",
14411				"nativeName":"Avañeẽ"
14412		},
14413		"gu":{
14414				"name":"Gujarati",
14415				"nativeName":"ગુજરાતી"
14416		},
14417		"ht":{
14418				"name":"Haitian",
14419				"nativeName":"Kreyòl ayisyen"
14420		},
14421		"ha":{
14422				"name":"Hausa",
14423				"nativeName":"Hausa, هَوُسَ"
14424		},
14425		"he":{
14426				"name":"Hebrew",
14427				"nativeName":"עברית"
14428		},
14429		"hz":{
14430				"name":"Herero",
14431				"nativeName":"Otjiherero"
14432		},
14433		"hi":{
14434				"name":"Hindi",
14435				"nativeName":"हिन्दी, हिंदी"
14436		},
14437		"ho":{
14438				"name":"Hiri Motu",
14439				"nativeName":"Hiri Motu"
14440		},
14441		"hu":{
14442				"name":"Hungarian",
14443				"nativeName":"Magyar"
14444		},
14445		"ia":{
14446				"name":"Interlingua",
14447				"nativeName":"Interlingua"
14448		},
14449		"id":{
14450				"name":"Indonesian",
14451				"nativeName":"Bahasa Indonesia"
14452		},
14453		"ie":{
14454				"name":"Interlingue",
14455				"nativeName":"Originally called Occidental; then Interlingue after WWII"
14456		},
14457		"ga":{
14458				"name":"Irish",
14459				"nativeName":"Gaeilge"
14460		},
14461		"ig":{
14462				"name":"Igbo",
14463				"nativeName":"Asụsụ Igbo"
14464		},
14465		"ik":{
14466				"name":"Inupiaq",
14467				"nativeName":"Iñupiaq, Iñupiatun"
14468		},
14469		"io":{
14470				"name":"Ido",
14471				"nativeName":"Ido"
14472		},
14473		"is":{
14474				"name":"Icelandic",
14475				"nativeName":"Íslenska"
14476		},
14477		"it":{
14478				"name":"Italian",
14479				"nativeName":"Italiano"
14480		},
14481		"iu":{
14482				"name":"Inuktitut",
14483				"nativeName":"ᐃᓄᒃᑎᑐᑦ"
14484		},
14485		"ja":{
14486				"name":"Japanese",
14487				"nativeName":"日本語 (にほんご/にっぽんご)"
14488		},
14489		"jv":{
14490				"name":"Javanese",
14491				"nativeName":"basa Jawa"
14492		},
14493		"kl":{
14494				"name":"Kalaallisut",
14495				"nativeName":"kalaallisut, kalaallit oqaasii"
14496		},
14497		"kn":{
14498				"name":"Kannada",
14499				"nativeName":"ಕನ್ನಡ"
14500		},
14501		"kr":{
14502				"name":"Kanuri",
14503				"nativeName":"Kanuri"
14504		},
14505		"ks":{
14506				"name":"Kashmiri",
14507				"nativeName":"कश्मीरी, كشميري‎"
14508		},
14509		"kk":{
14510				"name":"Kazakh",
14511				"nativeName":"Қазақ тілі"
14512		},
14513		"km":{
14514				"name":"Khmer",
14515				"nativeName":"ភាសាខ្មែរ"
14516		},
14517		"ki":{
14518				"name":"Kikuyu",
14519				"nativeName":"Gĩkũyũ"
14520		},
14521		"rw":{
14522				"name":"Kinyarwanda",
14523				"nativeName":"Ikinyarwanda"
14524		},
14525		"ky":{
14526				"name":"Kyrgyz",
14527				"nativeName":"кыргыз тили"
14528		},
14529		"kv":{
14530				"name":"Komi",
14531				"nativeName":"коми кыв"
14532		},
14533		"kg":{
14534				"name":"Kongo",
14535				"nativeName":"KiKongo"
14536		},
14537		"ko":{
14538				"name":"Korean",
14539				"nativeName":"한국어 (韓國語), 조선말 (朝鮮語)"
14540		},
14541		"ku":{
14542				"name":"Kurdish",
14543				"nativeName":"Kurdî, كوردی‎"
14544		},
14545		"kj":{
14546				"name":"Kuanyama",
14547				"nativeName":"Kuanyama"
14548		},
14549		"la":{
14550				"name":"Latin",
14551				"nativeName":"latine, lingua latina"
14552		},
14553		"lb":{
14554				"name":"Luxembourgish",
14555				"nativeName":"Lëtzebuergesch"
14556		},
14557		"lg":{
14558				"name":"Luganda",
14559				"nativeName":"Luganda"
14560		},
14561		"li":{
14562				"name":"Limburgish",
14563				"nativeName":"Limburgs"
14564		},
14565		"ln":{
14566				"name":"Lingala",
14567				"nativeName":"Lingála"
14568		},
14569		"lo":{
14570				"name":"Lao",
14571				"nativeName":"ພາສາລາວ"
14572		},
14573		"lt":{
14574				"name":"Lithuanian",
14575				"nativeName":"lietuvių kalba"
14576		},
14577		"lu":{
14578				"name":"Luba-Katanga",
14579				"nativeName":""
14580		},
14581		"lv":{
14582				"name":"Latvian",
14583				"nativeName":"latviešu valoda"
14584		},
14585		"gv":{
14586				"name":"Manx",
14587				"nativeName":"Gaelg, Gailck"
14588		},
14589		"mk":{
14590				"name":"Macedonian",
14591				"nativeName":"македонски јазик"
14592		},
14593		"mg":{
14594				"name":"Malagasy",
14595				"nativeName":"Malagasy fiteny"
14596		},
14597		"ms":{
14598				"name":"Malay",
14599				"nativeName":"bahasa Melayu, بهاس ملايو‎"
14600		},
14601		"ml":{
14602				"name":"Malayalam",
14603				"nativeName":"മലയാളം"
14604		},
14605		"mt":{
14606				"name":"Maltese",
14607				"nativeName":"Malti"
14608		},
14609		"mi":{
14610				"name":"Māori",
14611				"nativeName":"te reo Māori"
14612		},
14613		"mr":{
14614				"name":"Marathi",
14615				"nativeName":"मराठी"
14616		},
14617		"mh":{
14618				"name":"Marshallese",
14619				"nativeName":"Kajin M̧ajeļ"
14620		},
14621		"mn":{
14622				"name":"Mongolian",
14623				"nativeName":"монгол"
14624		},
14625		"na":{
14626				"name":"Nauru",
14627				"nativeName":"Ekakairũ Naoero"
14628		},
14629		"nv":{
14630				"name":"Navajo",
14631				"nativeName":"Diné bizaad, Dinékʼehǰí"
14632		},
14633		"nb":{
14634				"name":"Norwegian Bokmål",
14635				"nativeName":"Norsk bokmål"
14636		},
14637		"nd":{
14638				"name":"North Ndebele",
14639				"nativeName":"isiNdebele"
14640		},
14641		"ne":{
14642				"name":"Nepali",
14643				"nativeName":"नेपाली"
14644		},
14645		"ng":{
14646				"name":"Ndonga",
14647				"nativeName":"Owambo"
14648		},
14649		"nn":{
14650				"name":"Norwegian Nynorsk",
14651				"nativeName":"Norsk nynorsk"
14652		},
14653		"no":{
14654				"name":"Norwegian",
14655				"nativeName":"Norsk"
14656		},
14657		"ii":{
14658				"name":"Nuosu",
14659				"nativeName":"ꆈꌠ꒿ Nuosuhxop"
14660		},
14661		"nr":{
14662				"name":"South Ndebele",
14663				"nativeName":"isiNdebele"
14664		},
14665		"oc":{
14666				"name":"Occitan",
14667				"nativeName":"Occitan"
14668		},
14669		"oj":{
14670				"name":"Ojibwe",
14671				"nativeName":"ᐊᓂᔑᓈᐯᒧᐎᓐ"
14672		},
14673		"cu":{
14674				"name":"Church Slavonic",
14675				"nativeName":"ѩзыкъ словѣньскъ"
14676		},
14677		"om":{
14678				"name":"Oromo",
14679				"nativeName":"Afaan Oromoo"
14680		},
14681		"or":{
14682				"name":"Oriya",
14683				"nativeName":"ଓଡ଼ିଆ"
14684		},
14685		"os":{
14686				"name":"Ossetian",
14687				"nativeName":"ирон æвзаг"
14688		},
14689		"pa":{
14690				"name":"Punjabi",
14691				"nativeName":"ਪੰਜਾਬੀ, پنجابی‎"
14692		},
14693		"pi":{
14694				"name":"Pāli",
14695				"nativeName":"पाऴि"
14696		},
14697		"fa":{
14698				"name":"Persian",
14699				"nativeName":"فارسی"
14700		},
14701		"pl":{
14702				"name":"Polish",
14703				"nativeName":"polski"
14704		},
14705		"ps":{
14706				"name":"Pashto",
14707				"nativeName":"پښتو"
14708		},
14709		"pt":{
14710				"name":"Portuguese",
14711				"nativeName":"Português"
14712		},
14713		"qu":{
14714				"name":"Quechua",
14715				"nativeName":"Runa Simi, Kichwa"
14716		},
14717		"rm":{
14718				"name":"Romansh",
14719				"nativeName":"rumantsch grischun"
14720		},
14721		"rn":{
14722				"name":"Kirundi",
14723				"nativeName":"kiRundi"
14724		},
14725		"ro":{
14726				"name":"Romanian",
14727				"nativeName":"română"
14728		},
14729		"ru":{
14730				"name":"Russian",
14731				"nativeName":"русский язык"
14732		},
14733		"sa":{
14734				"name":"Sanskrit",
14735				"nativeName":"संस्कृतम्"
14736		},
14737		"sc":{
14738				"name":"Sardinian",
14739				"nativeName":"sardu"
14740		},
14741		"sd":{
14742				"name":"Sindhi",
14743				"nativeName":"सिन्धी, سنڌي، سندھی‎"
14744		},
14745		"se":{
14746				"name":"Northern Sami",
14747				"nativeName":"Davvisámegiella"
14748		},
14749		"sm":{
14750				"name":"Samoan",
14751				"nativeName":"gagana faa Samoa"
14752		},
14753		"sg":{
14754				"name":"Sango",
14755				"nativeName":"yângâ tî sängö"
14756		},
14757		"sr":{
14758				"name":"Serbian",
14759				"nativeName":"српски језик"
14760		},
14761		"gd":{
14762				"name":"Gaelic",
14763				"nativeName":"Gàidhlig"
14764		},
14765		"sn":{
14766				"name":"Shona",
14767				"nativeName":"chiShona"
14768		},
14769		"si":{
14770				"name":"Sinhalese",
14771				"nativeName":"සිංහල"
14772		},
14773		"sk":{
14774				"name":"Slovak",
14775				"nativeName":"slovenčina"
14776		},
14777		"sl":{
14778				"name":"Slovene",
14779				"nativeName":"slovenščina"
14780		},
14781		"so":{
14782				"name":"Somali",
14783				"nativeName":"Soomaaliga, af Soomaali"
14784		},
14785		"st":{
14786				"name":"Southern Sotho",
14787				"nativeName":"Sesotho"
14788		},
14789		"es":{
14790				"name":"Spanish",
14791				"nativeName":"español, castellano"
14792		},
14793		"su":{
14794				"name":"Sundanese",
14795				"nativeName":"Basa Sunda"
14796		},
14797		"sw":{
14798				"name":"Swahili",
14799				"nativeName":"Kiswahili"
14800		},
14801		"ss":{
14802				"name":"Swati",
14803				"nativeName":"SiSwati"
14804		},
14805		"sv":{
14806				"name":"Swedish",
14807				"nativeName":"svenska"
14808		},
14809		"ta":{
14810				"name":"Tamil",
14811				"nativeName":"தமிழ்"
14812		},
14813		"te":{
14814				"name":"Telugu",
14815				"nativeName":"తెలుగు"
14816		},
14817		"tg":{
14818				"name":"Tajik",
14819				"nativeName":"тоҷикӣ, toğikī, تاجیکی‎"
14820		},
14821		"th":{
14822				"name":"Thai",
14823				"nativeName":"ไทย"
14824		},
14825		"ti":{
14826				"name":"Tigrinya",
14827				"nativeName":"ትግርኛ"
14828		},
14829		"bo":{
14830				"name":"Tibetan",
14831				"nativeName":"བོད་ཡིག"
14832		},
14833		"tk":{
14834				"name":"Turkmen",
14835				"nativeName":"Türkmen, Түркмен"
14836		},
14837		"tl":{
14838				"name":"Tagalog",
14839				"nativeName":"Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔"
14840		},
14841		"tn":{
14842				"name":"Tswana",
14843				"nativeName":"Setswana"
14844		},
14845		"to":{
14846				"name":"Tonga",
14847				"nativeName":"faka Tonga"
14848		},
14849		"tr":{
14850				"name":"Turkish",
14851				"nativeName":"Türkçe"
14852		},
14853		"ts":{
14854				"name":"Tsonga",
14855				"nativeName":"Xitsonga"
14856		},
14857		"tt":{
14858				"name":"Tatar",
14859				"nativeName":"татарча, tatarça, تاتارچا‎"
14860		},
14861		"tw":{
14862				"name":"Twi",
14863				"nativeName":"Twi"
14864		},
14865		"ty":{
14866				"name":"Tahitian",
14867				"nativeName":"Reo Tahiti"
14868		},
14869		"ug":{
14870				"name":"Uyghur",
14871				"nativeName":"Uyƣurqə, ئۇيغۇرچە‎"
14872		},
14873		"uk":{
14874				"name":"Ukrainian",
14875				"nativeName":"українська"
14876		},
14877		"ur":{
14878				"name":"Urdu",
14879				"nativeName":"اردو"
14880		},
14881		"uz":{
14882				"name":"Uzbek",
14883				"nativeName":"zbek, Ўзбек, أۇزبېك‎"
14884		},
14885		"ve":{
14886				"name":"Venda",
14887				"nativeName":"Tshivenḓa"
14888		},
14889		"vi":{
14890				"name":"Vietnamese",
14891				"nativeName":"Tiếng Việt"
14892		},
14893		"vo":{
14894				"name":"Volapük",
14895				"nativeName":"Volapük"
14896		},
14897		"wa":{
14898				"name":"Walloon",
14899				"nativeName":"Walon"
14900		},
14901		"cy":{
14902				"name":"Welsh",
14903				"nativeName":"Cymraeg"
14904		},
14905		"wo":{
14906				"name":"Wolof",
14907				"nativeName":"Wollof"
14908		},
14909		"fy":{
14910				"name":"Western Frisian",
14911				"nativeName":"Frysk"
14912		},
14913		"xh":{
14914				"name":"Xhosa",
14915				"nativeName":"isiXhosa"
14916		},
14917		"yi":{
14918				"name":"Yiddish",
14919				"nativeName":"ייִדיש"
14920		},
14921		"yo":{
14922				"name":"Yoruba",
14923				"nativeName":"Yorùbá"
14924		},
14925		"za":{
14926				"name":"Zhuang",
14927				"nativeName":"Saɯ cueŋƅ, Saw cuengh"
14928		},
14929		"ar-dz":{
14930				"name":"Arabic (Algeria)",
14931				"nativeName":"العربية (الجزائر)"
14932		},
14933		"ar-bh":{
14934				"name":"Arabic (Bahrain)",
14935				"nativeName":"العربية (البحرين)"
14936		},
14937		"ar-eg":{
14938				"name":"Arabic (Egypt)",
14939				"nativeName":"العربية (مصر)"
14940		},
14941		"ar-iq":{
14942				"name":"Arabic (Iraq)",
14943				"nativeName":"العربية (العراق)"
14944		},
14945		"ar-jo":{
14946				"name":"Arabic (Jordan)",
14947				"nativeName":"العربية (الأردن)"
14948		},
14949		"ar-kw":{
14950				"name":"Arabic (Kuwait)",
14951				"nativeName":"العربية (الكويت)"
14952		},
14953		"ar-lb":{
14954				"name":"Arabic (Lebanon)",
14955				"nativeName":"العربية (لبنان)"
14956		},
14957		"ar-ly":{
14958				"name":"Arabic (Libya)",
14959				"nativeName":"العربية (ليبيا)"
14960		},
14961		"ar-ma":{
14962				"name":"Arabic (Morocco)",
14963				"nativeName":"العربية (المملكة المغربية)"
14964		},
14965		"ar-om":{
14966				"name":"Arabic (Oman)",
14967				"nativeName":"العربية (عمان)"
14968		},
14969		"ar-qa":{
14970				"name":"Arabic (Qatar)",
14971				"nativeName":"العربية (قطر)"
14972		},
14973		"ar-sa":{
14974				"name":"Arabic (Saudi Arabia)",
14975				"nativeName":"العربية (المملكة العربية السعودية)"
14976		},
14977		"ar-sy":{
14978				"name":"Arabic (Syria)",
14979				"nativeName":"العربية (سوريا)"
14980		},
14981		"ar-tn":{
14982				"name":"Arabic (Tunisia)",
14983				"nativeName":"العربية (تونس)"
14984		},
14985		"ar-ae":{
14986				"name":"Arabic (U.A.E.)",
14987				"nativeName":"العربية (الإمارات العربية المتحدة)"
14988		},
14989		"ar-ye":{
14990				"name":"Arabic (Yemen)",
14991				"nativeName":"العربية (اليمن)"
14992		},
14993		"de-at":{
14994				"name":"German (Austria)",
14995				"nativeName":"Deutsch (Österreich)"
14996		},
14997		"de-li":{
14998				"name":"German (Liechtenstein)",
14999				"nativeName":"Deutsch (Liechtenstein)"
15000		},
15001		"de-lu":{
15002				"name":"German (Luxembourg)",
15003				"nativeName":"Deutsch (Luxemburg)"
15004		},
15005		"de-ch":{
15006				"name":"German (Switzerland)",
15007				"nativeName":"Deutsch (Schweiz)"
15008		},
15009		"en-au":{
15010				"name":"English (Australia)",
15011				"nativeName":"English (Australia)"
15012		},
15013		"en-bz":{
15014				"name":"English (Belize)",
15015				"nativeName":"English (Belize)"
15016		},
15017		"en-ca":{
15018				"name":"English (Canada)",
15019				"nativeName":"English (Canada)"
15020		},
15021		"en-ie":{
15022				"name":"English (Ireland)",
15023				"nativeName":"English (Ireland)"
15024		},
15025		"en-jm":{
15026				"name":"English (Jamaica)",
15027				"nativeName":"English (Jamaica)"
15028		},
15029		"en-nz":{
15030				"name":"English (New Zealand)",
15031				"nativeName":""
15032		},
15033		"en-za":{
15034				"name":"English (South Africa)",
15035				"nativeName":"English (South Africa)"
15036		},
15037		"en-tt":{
15038				"name":"English (Trinidad)",
15039				"nativeName":"English (Trinidad y Tobago)"
15040		},
15041		"en-gb":{
15042				"name":"English (United Kingdom)",
15043				"nativeName":"English (United Kingdom)"
15044		},
15045		"en-us":{
15046				"name":"English (United States)",
15047				"nativeName":"English (United States)"
15048		},
15049		"es-ar":{
15050				"name":"Spanish (Argentina)",
15051				"nativeName":"Español (Argentina)"
15052		},
15053		"es-bo":{
15054				"name":"Spanish (Bolivia)",
15055				"nativeName":"Español (Bolivia)"
15056		},
15057		"es-cl":{
15058				"name":"Spanish (Chile)",
15059				"nativeName":"Español (Chile)"
15060		},
15061		"es-co":{
15062				"name":"Spanish (Colombia)",
15063				"nativeName":"Español (Colombia)"
15064		},
15065		"es-cr":{
15066				"name":"Spanish (Costa Rica)",
15067				"nativeName":"Español (Costa Rica)"
15068		},
15069		"es-do":{
15070				"name":"Spanish (Dominican Republic)",
15071				"nativeName":"Español (República Dominicana)"
15072		},
15073		"es-ec":{
15074				"name":"Spanish (Ecuador)",
15075				"nativeName":"Español (Ecuador)"
15076		},
15077		"es-sv":{
15078				"name":"Spanish (El Salvador)",
15079				"nativeName":"Español (El Salvador)"
15080		},
15081		"es-gt":{
15082				"name":"Spanish (Guatemala)",
15083				"nativeName":"Español (Guatemala)"
15084		},
15085		"es-hn":{
15086				"name":"Spanish (Honduras)",
15087				"nativeName":"Español (Honduras)"
15088		},
15089		"es-mx":{
15090				"name":"Spanish (Mexico)",
15091				"nativeName":"Español (México)"
15092		},
15093		"es-ni":{
15094				"name":"Spanish (Nicaragua)",
15095				"nativeName":"Español (Nicaragua)"
15096		},
15097		"es-pa":{
15098				"name":"Spanish (Panama)",
15099				"nativeName":"Español (Panamá)"
15100		},
15101		"es-py":{
15102				"name":"Spanish (Paraguay)",
15103				"nativeName":"Español (Paraguay)"
15104		},
15105		"es-pe":{
15106				"name":"Spanish (Peru)",
15107				"nativeName":"Español (Perú)"
15108		},
15109		"es-pr":{
15110				"name":"Spanish (Puerto Rico)",
15111				"nativeName":"Español (Puerto Rico)"
15112		},
15113		"es-uy":{
15114				"name":"Spanish (Uruguay)",
15115				"nativeName":"Español (Uruguay)"
15116		},
15117		"es-ve":{
15118				"name":"Spanish (Venezuela)",
15119				"nativeName":"Español (Venezuela)"
15120		},
15121		"fr-be":{
15122				"name":"French (Belgium)",
15123				"nativeName":"français (Belgique)"
15124		},
15125		"fr-ca":{
15126				"name":"French (Canada)",
15127				"nativeName":"français (Canada)"
15128		},
15129		"fr-lu":{
15130				"name":"French (Luxembourg)",
15131				"nativeName":"français (Luxembourg)"
15132		},
15133		"fr-ch":{
15134				"name":"French (Switzerland)",
15135				"nativeName":"français (Suisse)"
15136		},
15137		"it-ch":{
15138				"name":"Italian (Switzerland)",
15139				"nativeName":"italiano (Svizzera)"
15140		},
15141		"nl-be":{
15142				"name":"Dutch (Belgium)",
15143				"nativeName":"Nederlands (België)"
15144		},
15145		"pt-br":{
15146				"name":"Portuguese (Brazil)",
15147				"nativeName":"Português (Brasil)"
15148		},
15149		"sv-fi":{
15150				"name":"Swedish (Finland)",
15151				"nativeName":"svenska (Finland)"
15152		},
15153		"zh-hk":{
15154				"name":"Chinese (Hong Kong)",
15155				"nativeName":"中文(香港特别行政區)"
15156		},
15157		"zh-cn":{
15158				"name":"Chinese (PRC)",
15159				"nativeName":"中文(中华人民共和国)"
15160		},
15161		"zh-sg":{
15162				"name":"Chinese (Singapore)",
15163				"nativeName":"中文(新加坡)"
15164		},
15165		"zh-tw":{
15166				"name":"Chinese Traditional (Taiwan)",
15167				"nativeName":"中文(台灣)"
15168		}
15169	}
15169
15170
15171	AblePlayer.prototype.getLanguageName = function (key,whichName) {
15172
15173		// return language name associated with lang code "key"
15174		// whichName is either "English" or "local" (i.e., native name)
15175
15176		var lang, code, subTag; 
15177		lang = isoLangs[key.toLowerCase()];
15178		if (lang) { 
15179			if (whichName === 'local') { 
15180				return lang.nativeName; 
15181			}
15182			else { 
15183				return lang.name; 
15184			}
15185		}
15186		else if (key.includes('-')) {
15187			code = key.substring(0,2);
15188			subTag = key.substring(3);  
15189			lang = isoLangs[code.toLowerCase()]; 
15190			if (lang) { 
15191				if (whichName === 'local') { 
15192					return lang.nativeName + ' (' + subTag + ')'; 
15193				}
15194				else { 
15195					return lang.name + ' (' + subTag + ')'; 
15196				}
15197			}
15198		}
15199		// if all else has failed, use the key as the label 
15200		return key;
15201	};
15202
15203})(jQuery);
15204(function ($) {
15205	AblePlayer.prototype.getSupportedLangs = function() {
15206		// returns an array of languages for which AblePlayer has translation tables
15207		var langs = ['ca','cs','da','de','en','es','fr','he','id','it','ja','nb','nl','pt','pt-br','sv','tr','zh-tw'];
15208		return langs;
15209	};
15210
15211	AblePlayer.prototype.getTranslationText = function() {
15212
15213		// determine language, then get labels and prompts from corresponding translation var
15214
15215		var deferred, thisObj, supportedLangs, docLang, msg, translationFile, collapsedLang, i, 
15216			similarLangFound;
15217		deferred = $.Deferred();
15218		thisObj = this;
15219
15220		supportedLangs = this.getSupportedLangs(); // returns an array
15221
15222		if (this.lang) { // a data-lang attribute is included on the media element
15223			if ($.inArray(this.lang,supportedLangs) === -1) {
15224				// the specified language is not supported
15225				if (this.lang.indexOf('-') == 2) {
15226					// this is a localized lang attribute (e.g., fr-CA)
15227					// try the parent language, given the first two characters
15228					if ($.inArray(this.lang.substring(0,2),supportedLangs) !== -1) {
15229						// parent lang is supported. Use that.
15230						this.lang = this.lang.substring(0,2);
15231					}
15232					else {
15233						// the parent language is not supported either
15234						// unable to use the specified language
15235						this.lang = null;
15236					}
15237				}
15238				else {
15239					// this is not a localized language.
15240					// but maybe there's a similar localized language supported  
15241					// that has the same parent?  
15242					similarLangFound = false; 
15243					i = 0; 
15244					while (i < supportedLangs.length) { 
15245						if (supportedLangs[i].substring(0,2) == this.lang) { 
15246							this.lang = supportedLangs[i]; 				
15247							similarLangFound = true; 
15248						}
15249						i++; 
15250					}
15251					if (!similarLangFound) { 
15252						// language requested via data-lang is not supported
15253						this.lang = null;
15254					}
15255				}
15256			}
15257		}
15258
15259		if (!this.lang) {
15260			// try the language of the web page, if specified
15261			if ($('body').attr('lang')) {
15262				docLang = $('body').attr('lang').toLowerCase();
15263			}
15264			else if ($('html').attr('lang')) {
15265				docLang = $('html').attr('lang').toLowerCase();
15266			}
15267			else {
15268				docLang = null;
15269			}
15270			if (docLang) {
15271				if ($.inArray(docLang,supportedLangs) !== -1) {
15272					// the document language is supported
15273					this.lang = docLang;
15274				}
15275				else {
15276					// the document language is not supported
15277					if (docLang.indexOf('-') == 2) {
15278						// this is a localized lang attribute (e.g., fr-CA)
15279						// try the parent language, given the first two characters
15280						if ($.inArray(docLang.substring(0,2),supportedLangs) !== -1) {
15281							// the parent language is supported. use that.
15282							this.lang = docLang.substring(0,2);
15283						}
15284					}
15285				}
15286			}
15287		}
15288
15289		if (!this.lang) {
15290			// No supported language has been specified by any means
15291			// Fallback to English
15292			this.lang = 'en';
15293		}
15294
15295		if (!this.searchLang) {
15296			this.searchLang = this.lang;
15297		}
15298		translationFile = this.rootPath + 'translations/' + this.lang + '.js';
15299		$.getJSON(translationFile, function(data) {
15300			// success!
15301			thisObj.tt = data; 
15302			deferred.resolve(); 
15303		})
15304		.fail(function() {
15305			console.log( "Critical Error: Unable to load translation file:",translationFile);			
15306			thisObj.provideFallback();
15307			deferred.fail();
15308		})
15309		return deferred.promise();
15310	};
15311
15312	AblePlayer.prototype.getSampleDescriptionText = function() {
15313
15314		// Create an array of sample description text in all languages 
15315		// This needs to be readily available for testing different voices 
15316		// in the Description Preferences dialog 
15317		var thisObj, supportedLangs, i, thisLang, translationFile, thisText, translation; 
15318		
15319		supportedLangs = this.getSupportedLangs(); 
15320
15321		thisObj = this; 
15322
15323		this.sampleText = []; 
15324		for (i=0; i < supportedLangs.length; i++) { 
15325			translationFile = this.rootPath + 'translations/' + supportedLangs[i] + '.js';
15326			$.getJSON(translationFile, thisLang, (function(thisLang) {
15327					return function(data) { 
15328						thisText = data.sampleDescriptionText; 
15329						translation = {'lang':thisLang, 'text': thisText}; 
15330						thisObj.sampleText.push(translation); 						
15331					};
15332			}(supportedLangs[i])) // pass lang to callback function 
15333			); 				 
15334		}
15335	};
15336
15337})(jQuery);
15338
15339(function($) {
15340	AblePlayer.prototype.computeEndTime = function(startTime, durationTime) {
15341		var SECONDS = 0;
15342		var MINUTES = 1;
15343		var HOURS = 2;
15344
15345		var startParts = startTime
15346			.split(':')
15347			.reverse()
15348			.map(function(value) {
15349				return parseFloat(value);
15350			});
15351
15352		var durationParts = durationTime
15353			.split(':')
15354			.reverse()
15355			.map(function(value) {
15356				return parseFloat(value);
15357			});
15358
15359		var endTime = startParts
15360			.reduce(function(acc, val, index) {
15361				var sum = val + durationParts[index];
15362
15363				if (index === SECONDS) {
15364					if (sum > 60) {
15365						durationParts[index + 1] += 1;
15366						sum -= 60;
15367					}
15368
15369					sum = sum.toFixed(3);
15370				}
15371
15372				if (index === MINUTES) {
15373					if (sum > 60) {
15374						durationParts[index + 1] += 1;
15375						sum -= 60;
15376					}
15377				}
15378
15379				if (sum < 10) {
15380					sum = '0' + sum;
15381				}
15382
15383				acc.push(sum);
15384
15385				return acc;
15386			}, [])
15387			.reverse()
15388			.join(':');
15389
15390		return endTime;
15391	};
15392
15393	AblePlayer.prototype.ttml2webvtt = function(contents) {
15394		var thisObj = this;
15395
15396		var xml = thisObj.convert.xml2json(contents, {
15397			ignoreComment: true,
15398			alwaysChildren: true,
15399			compact: true,
15400			spaces: 2
15401		});
15402
15403		var vttHeader = 'WEBVTT\n\n\n';
15404		var captions = JSON.parse(xml).tt.body.div.p;
15405
15406		var vttCaptions = captions.reduce(function(acc, value, index) {
15407			var text = value._text;
15408			var isArray = Array.isArray(text);
15409			var attributes = value._attributes;
15410			var endTime = thisObj.computeEndTime(attributes.begin, attributes.dur);
15411
15412			var caption =
15413				thisObj.computeEndTime(attributes.begin, '00:00:0') +
15414				' --> ' +
15415				thisObj.computeEndTime(attributes.begin, attributes.dur) +
15416				'\n' +
15417				(isArray ? text.join('\n') : text) +
15418				'\n\n';
15419
15420			return acc + caption;
15421		}, vttHeader);
15422
15423		return vttCaptions;
15424	};
15425})(jQuery);
15426
15427/*! Copyright (c) 2014 - Paul Tavares - purtuga - @paul_tavares - MIT License */
15428;(function($){
15429
15430		/**
15431		 * Delays the execution of a function until an expression returns true.
15432		 * The expression is checked every 100 milliseconds for as many tries
15433		 * as defined in in the attempts option
15434		 *
15435		 * @param {Object} options
15436		 * @param {Function} options.when
15437		 *                      Function to execute on every interval.
15438		 *                      Must return true (boolean) in order for
15439		 *                      options.do to be executed.
15440		 * @param {Function} [options.exec]
15441		 *                      Function to be executed once options.when()
15442		 *                      returns true.
15443		 * @param {Interger} [options.interval=100]
15444		 *                      How long to wait in-between tries.
15445		 * @param {Interger} [options.attempts=100]
15446		 *                      How many tries to use before its considered
15447		 *                      a failure.
15448		 * @param {Interger} [options.delayed=0]
15449		 *                      Number of miliseconds to wait before execution
15450														is started. Default is imediately.
15451		 *
15452		 * @return {jQuery.Promise}
15453		 *
15454		 * @example
15455		 *
15456		 *      $.doWhen({
15457		 *          when: function(){
15458		 *              return false;
15459		 *          },
15460		 *          exec: function(){
15461		 *              alert("never called given false response on when param!");
15462		 *          }
15463		 *      })
15464		 *      .fail(function(){
15465		 *          alert('ALERT: FAILED CONDITION');
15466		 *      })
15467		 *      .then(function(){
15468		 *          alert("resolved.");
15469		 *      });
15470		 *
15471		 */
15472		$.doWhen = function(options) {
15473
15474				return $.Deferred(function(dfd){
15475
15476						var opt = $.extend({}, {
15477										when:       null,
15478										exec:       function(){},
15479										interval:   100,
15480										attempts:   100,
15481										delayed:    0
15482								},
15483								options,
15484								{
15485										checkId: null
15486								}),
15487								startChecking = function(){
15488
15489										// Check condition now and if true, then resolve object
15490										if (opt.when() === true) {
15491
15492												opt.exec.call(dfd.promise());
15493												dfd.resolve();
15494												return;
15495
15496										}
15497
15498										// apply minimal UI and hide the overlay
15499										opt.checkId = setInterval(function(){
15500
15501														if (opt.attempts === 0) {
15502
15503																clearInterval(opt.checkId);
15504																dfd.reject();
15505
15506														} else {
15507
15508																--opt.attempts;
15509
15510																if (opt.when() === true) {
15511
15512																		opt.attempts = 0;
15513																		clearInterval(opt.checkId);
15514																		opt.exec.call(dfd.promise());
15515																		dfd.resolve();
15516
15517																}
15518
15519														}
15520
15521												}, opt.interval);
15522
15523								};
15524
15525						if (opt.delayed > 0) {
15526
15527								setTimeout(startChecking, Number(opt.delayed));
15528
15529						} else {
15530
15531								startChecking();
15532
15533						}
15534
15535				}).promise();
15536
15537		};
15538
15539})(jQuery);
15540/* Video Transcript Sorter (VTS)
15541 * Used to synchronize time stamps from WebVTT resources
15542 * so they appear in the proper sequence within an auto-generated interactive transcript
15543*/
15544
15545(function ($) {
15546	AblePlayer.prototype.injectVTS = function() {
15547
15548		// To add a transcript sorter to a web page:
15549		// Add <div id="able-vts"></div> to the web page
15550
15551		// Define all variables
15552		var thisObj, tracks, $heading;
15553		var $instructions, $p1, $p2, $ul, $li1, $li2, $li3;
15554		var $fieldset, $legend, i, $radioDiv, radioId, $label, $radio;
15555		var $saveButton, $savedTable;
15556
15557		thisObj = this;
15558
15559		if ($('#able-vts').length) {
15560			// Page includes a container for a VTS instance
15561
15562			// Are they qualifying tracks?
15563			if (this.vtsTracks.length) {
15564				// Yes - there are!
15565
15566				// Build an array of unique languages
15567				this.langs = [];
15568				this.getAllLangs(this.vtsTracks);
15569
15570				// Set the default VTS language
15571				this.vtsLang = this.lang;
15572
15573				// Inject a heading
15574				$heading = $('<h2>').text('Video Transcript Sorter'); // TODO: Localize; intelligently assign proper heading level
15575				$('#able-vts').append($heading);
15576
15577				// Inject an empty div for writing messages
vendor: 34,209 bytes, lines 15578-16599
15578				this.$vtsAlert = $('<div>',{
15579					'id': 'able-vts-alert',
15580					'aria-live': 'polite',
15581					'aria-atomic': 'true'
15582				})
15583				$('#able-vts').append(this.$vtsAlert);
15584
15585				// Inject instructions (TODO: Localize)
15586				$instructions = $('<div>',{
15587					'id': 'able-vts-instructions'
15588				});
15589				$p1 = $('<p>').text('Use the Video Transcript Sorter to perform any of the following tasks:');
15590				$ul = $('<ul>');
15591				$li1 = $('<li>').text('Reorder chapters, descriptions, captions, and/or subtitles so they appear in the proper sequence in Able Player\'s auto-generated transcript.');
15592				$li2 = $('<li>').text('Modify content or start/end times (all are directly editable within the table).');
15593				$li3 = $('<li>').text('Insert new content, such as chapters or descriptions.');
15594				$p2 = $('<p>').text('When finished editing, click the "Save Changes" button. This will auto-generate new content for all relevant timed text files (chapters, descriptions, captions, and/or subtitles), which can be copied and pasted into separate WebVTT files for use by Able Player.');
15595				$ul.append($li1,$li2,$li3);
15596				$instructions.append($p1,$ul,$p2);
15597				$('#able-vts').append($instructions);
15598
15599				// Inject a fieldset with radio buttons for each language
15600				$fieldset = $('<fieldset>');
15601				$legend = $('<legend>').text('Select a language'); // TODO: Localize this
15602				$fieldset.append($legend)
15603				for (i in this.langs) {
15604					radioId = 'vts-lang-radio-' + this.langs[i];
15605					$radioDiv = $('<div>',{
15606						// uncomment the following if label is native name
15607						// 'lang': this.langs[i]
15608					});
15609					$radio = $('<input>', {
15610						'type': 'radio',
15611						'name': 'vts-lang',
15612						'id': radioId,
15613						'value': this.langs[i]
15614					}).on('click',function() {
15615						thisObj.vtsLang = $(this).val();
15616						thisObj.showVtsAlert('Loading ' + thisObj.getLanguageName(thisObj.vtsLang) + ' tracks');
15617						thisObj.injectVtsTable('update',thisObj.vtsLang);
15618					});
15619					if (this.langs[i] == this.lang) {
15620						// this is the default language.
15621						$radio.prop('checked',true);
15622					}
15623					$label = $('<label>', {
15624						'for': radioId
15625						// Two options for label:
15626						// getLanguageName() - with second parameter "local" would return native name, otherwise returns English; 
15627						// TODO: if using this be sure to add lang attr to <div> (see above)
15628					}).text(this.getLanguageName(this.langs[i]));
15629					$radioDiv.append($radio,$label);
15630					$fieldset.append($radioDiv);
15631				}
15632				$('#able-vts').append($fieldset);
15633
15634				// Inject a 'Save Changes' button
15635				$saveButton = $('<button>',{
15636					'type': 'button',
15637					'id': 'able-vts-save',
15638					'value': 'save'
15639				}).text('Save Changes'); // TODO: Localize this
15640				$('#able-vts').append($saveButton);
15641
15642				// Inject a table with one row for each cue in the default language
15643				this.injectVtsTable('add',this.vtsLang);
15644
15645				// TODO: Add drag/drop functionality for mousers
15646
15647				// Add event listeners for contenteditable cells
15648				var kindOptions, beforeEditing, editedCell, editedContent, i, closestKind;
15649				kindOptions = ['captions','chapters','descriptions','subtitles'];
15650				$('td[contenteditable="true"]').on('focus',function() {
15651					beforeEditing = $(this).text();
15652				}).on('blur',function() {
15653					if (beforeEditing != $(this).text()) {
15654						editedCell = $(this).index();
15655						editedContent = $(this).text();
15656						if (editedCell === 1) {
15657							// do some simple spelling auto-correct
15658							if ($.inArray(editedContent,kindOptions) === -1) {
15659								// whatever user typed is not a valid kind
15660								// assume they correctly typed the first character
15661								if (editedContent.substring(0,1) === 's') {
15662									$(this).text('subtitles');
15663								}
15664								else if (editedContent.substring(0,1) === 'd') {
15665									$(this).text('descriptions');
15666								}
15667								else if (editedContent.substring(0,2) === 'ch') {
15668									$(this).text('chapters');
15669								}
15670								else {
15671									// whatever else they types, assume 'captions'
15672									$(this).text('captions');
15673								}
15674							}
15675						}
15676						else if (editedCell === 2 || editedCell === 3) {
15677							// start or end time
15678							// ensure proper formatting (with 3 decimal places)
15679							$(this).text(thisObj.formatTimestamp(editedContent));
15680						}
15681					}
15682				}).on('keydown',function(e) {
15683					// don't allow keystrokes to trigger Able Player (or other) functions
15684					// while user is editing
15685					e.stopPropagation();
15686				});
15687
15688				// handle click on the Save button
15689
15690				// handle click on the Save button
15691				$('#able-vts-save').on('click',function(e) {
15692					e.stopPropagation();
15693					if ($(this).attr('value') == 'save') {
15694						// replace table with WebVTT output in textarea fields (for copying/pasting)
15695						$(this).attr('value','cancel').text('Return to Editor'); // TODO: Localize this
15696						$savedTable = $('#able-vts table');
15697						$('#able-vts-instructions').hide();
15698						$('#able-vts > fieldset').hide();
15699						$('#able-vts table').remove();
15700						$('#able-vts-icon-credit').remove();
15701						thisObj.parseVtsOutput($savedTable);
15702					}
15703					else {
15704						// cancel saving, and restore the table using edited content
15705						$(this).attr('value','save').text('Save Changes'); // TODO: Localize this
15706						$('#able-vts-output').remove();
15707						$('#able-vts-instructions').show();
15708						$('#able-vts > fieldset').show();
15709						$('#able-vts').append($savedTable);
15710						$('#able-vts').append(thisObj.getIconCredit());
15711						thisObj.showVtsAlert('Cancelling saving. Any edits you made have been restored in the VTS table.'); // TODO: Localize this
15712					}
15713				});
15714			}
15715		}
15716	};
15717
15718	AblePlayer.prototype.setupVtsTracks = function(kind, lang, trackDesc, label, src, contents) {
15719
15720		// TODO: Add support for trackDesc 
15721		// (to destinguish between tracks for the decribed vs non-described versions)		
15722
15723		// Called from tracks.js
15724		var srcFile, vtsCues;
15725
15726		srcFile = this.getFilenameFromPath(src);
15727		vtsCues = this.parseVtsTracks(contents);
15728
15729		this.vtsTracks.push({
15730			'kind': kind,
15731			'language': lang,
15732			'label': label,
15733			'srcFile': srcFile,
15734			'cues': vtsCues
15735		});
15736	};
15737
15738	AblePlayer.prototype.getFilenameFromPath = function(path) {
15739
15740		var lastSlash;
15741
15742		lastSlash = path.lastIndexOf('/');
15743		if (lastSlash === -1) {
15744			// there are no slashes in path.
15745			return path;
15746		}
15747		else {
15748			return path.substring(lastSlash+1);
15749		}
15750	};
15751
15752	AblePlayer.prototype.getFilenameFromTracks = function(kind,lang) {
15753
15754		for (var i=0; i<this.vtsTracks.length; i++) {
15755			if (this.vtsTracks[i].kind === kind && this.vtsTracks[i].language === lang) {
15756				// this is a matching track
15757				// srcFile has already been converted to filename from path before saving to vtsTracks
15758				return this.vtsTracks[i].srcFile;
15759			}
15760		}
15761		// no matching track found
15762		return false;
15763	};
15764
15765	AblePlayer.prototype.parseVtsTracks = function(contents) {
15766
15767		var rows, timeParts, cues, i, j, thisRow, nextRow, content, blankRow;
15768		rows = contents.split("\n");
15769		cues = [];
15770		i = 0;
15771		while (i < rows.length) {
15772			thisRow = rows[i];
15773			if (thisRow.indexOf(' --> ') !== -1) {
15774				// this is probably a time row
15775				timeParts = thisRow.trim().split(' ');
15776				if (this.isValidTimestamp(timeParts[0]) && this.isValidTimestamp(timeParts[2])) {
15777					// both timestamps are valid. This is definitely a time row
15778					content = '';
15779					j = i+1;
15780					blankRow = false;
15781					while (j < rows.length && !blankRow) {
15782						nextRow = rows[j].trim();
15783						if (nextRow.length > 0) {
15784							if (content.length > 0) {
15785								// add back the EOL between rows of content
15786								content += "\n" + nextRow;
15787							}
15788							else {
15789								// this is the first row of content. No need for an EOL
15790								content += nextRow;
15791							}
15792						}
15793						else {
15794							blankRow = true;
15795						}
15796						j++;
15797					}
15798					cues.push({
15799						'start': timeParts[0],
15800						'end': timeParts[2],
15801						'content': content
15802					});
15803					i = j; //skip ahead
15804				}
15805			}
15806			else {
15807				i++;
15808			}
15809		}
15810		return cues;
15811	};
15812
15813	AblePlayer.prototype.isValidTimestamp = function(timestamp) {
15814
15815		// return true if timestamp contains only numbers or expected punctuation
15816		if (/^[0-9:,.]*$/.test(timestamp)) {
15817			return true;
15818		}
15819		else {
15820			return false;
15821		}
15822	};
15823
15824	AblePlayer.prototype.formatTimestamp = function(timestamp) {
15825
15826		// timestamp is a string in the form "HH:MM:SS.xxx"
15827		// Take some simple steps to ensure edited timestamp values still adhere to expected format
15828
15829		var firstPart, lastPart;
15830
15831		firstPart = timestamp.substring(0,timestamp.lastIndexOf('.')+1);
15832		lastPart = timestamp.substring(timestamp.lastIndexOf('.')+1);
15833
15834		// TODO: Be sure each component within firstPart has only exactly two digits
15835		// Probably can't justify doing this automatically
15836		// If users enters '5' for minutes, that could be either '05' or '50'
15837		// This should trigger an error and prompt the user to correct the value before proceeding
15838
15839		// Be sure lastPart has exactly three digits
15840		if (lastPart.length > 3) {
15841			// chop off any extra digits
15842			lastPart = lastPart.substring(0,3);
15843		}
15844		else if (lastPart.length < 3) {
15845			// add trailing zeros
15846			while (lastPart.length < 3) {
15847				lastPart += '0';
15848			}
15849		}
15850		return firstPart + lastPart;
15851	};
15852
15853
15854	AblePlayer.prototype.injectVtsTable = function(action,lang) {
15855
15856		// action is either 'add' (for a new table) or 'update' (if user has selected a new lang)
15857
15858		var $table, headers, i, $tr, $th, $td, rows, rowNum, rowId;
15859
15860		if (action === 'update') {
15861			// remove existing table
15862			$('#able-vts table').remove();
15863			$('#able-vts-icon-credit').remove();
15864		}
15865
15866		$table = $('<table>',{
15867			'lang': lang
15868		});
15869		$tr = $('<tr>',{
15870			'lang': 'en' // TEMP, until header row is localized
15871		});
15872		headers = ['Row #','Kind','Start','End','Content','Actions']; // TODO: Localize this
15873		for (i=0; i < headers.length; i++) {
15874			$th = $('<th>', {
15875				'scope': 'col'
15876			}).text(headers[i]);
15877			if (headers[i] === 'Actions') {
15878				$th.addClass('actions');
15879			}
15880			$tr.append($th);
15881		}
15882		$table.append($tr);
15883
15884		// Get all rows (sorted by start time), and inject them into table
15885		rows = this.getAllRows(lang);
15886		for (i=0; i < rows.length; i++) {
15887			rowNum = i + 1;
15888			rowId = 'able-vts-row-' + rowNum;
15889			$tr = $('<tr>',{
15890				'id': rowId,
15891				'class': 'kind-' + rows[i].kind
15892			});
15893			// Row #
15894			$td = $('<td>').text(rowNum);
15895			$tr.append($td);
15896
15897			// Kind
15898			$td = $('<td>',{
15899				'contenteditable': 'true'
15900			}).text(rows[i].kind);
15901			$tr.append($td);
15902
15903			// Start
15904			$td = $('<td>',{
15905				'contenteditable': 'true'
15906			}).text(rows[i].start);
15907			$tr.append($td);
15908
15909			// End
15910			$td = $('<td>',{
15911				'contenteditable': 'true'
15912			}).text(rows[i].end);
15913			$tr.append($td);
15914
15915			// Content
15916			$td = $('<td>',{
15917				'contenteditable': 'true'
15918			}).text(rows[i].content); // TODO: Preserve tags
15919			$tr.append($td);
15920
15921					// Actions
15922			$td = this.addVtsActionButtons(rowNum,rows.length);
15923			$tr.append($td);
15924
15925			$table.append($tr);
15926		}
15927		$('#able-vts').append($table);
15928
15929		// Add credit for action button SVG icons
15930		$('#able-vts').append(this.getIconCredit());
15931
15932	};
15933
15934	AblePlayer.prototype.addVtsActionButtons = function(rowNum,numRows) {
15935
15936		// rowNum is the number of the current table row (starting with 1)
15937		// numRows is the total number of rows (excluding the header row)
15938		// TODO: Position buttons so they're vertically aligned, even if missing an Up or Down button
15939		var thisObj, $td, buttons, i, button, $button, $svg, $g, pathString, pathString2, $path, $path2;
15940		thisObj = this;
15941		$td = $('<td>');
15942		buttons = ['up','down','insert','delete'];
15943
15944		for (i=0; i < buttons.length; i++) {
15945			button = buttons[i];
15946			if (button === 'up') {
15947				if (rowNum > 1) {
15948					$button = $('<button>',{
15949						'id': 'able-vts-button-up-' + rowNum,
15950						'title': 'Move up',
15951						'aria-label': 'Move Row ' + rowNum + ' up'
15952					}).on('click', function(el) {
15953						thisObj.onClickVtsActionButton(el.currentTarget);
15954					});
15955					$svg = $('<svg>',{
15956						'focusable': 'false',
15957						'aria-hidden': 'true',
15958						'x': '0px',
15959						'y': '0px',
15960						'width': '254.296px',
15961						'height': '254.296px',
15962						'viewBox': '0 0 254.296 254.296',
15963						'style': 'enable-background:new 0 0 254.296 254.296'
15964					});
15965					pathString = 'M249.628,176.101L138.421,52.88c-6.198-6.929-16.241-6.929-22.407,0l-0.381,0.636L4.648,176.101'
15966						+ 'c-6.198,6.897-6.198,18.052,0,24.981l0.191,0.159c2.892,3.305,6.865,5.371,11.346,5.371h221.937c4.577,0,8.613-2.161,11.41-5.594'
15967						+ 'l0.064,0.064C255.857,194.153,255.857,182.998,249.628,176.101z';
15968					$path = $('<path>',{
15969						'd': pathString
15970					});
15971					$g = $('<g>').append($path);
15972					$svg.append($g);
15973					$button.append($svg);
15974					// Refresh button in the DOM in order for browser to process & display the SVG
15975					$button.html($button.html());
15976					$td.append($button);
15977				}
15978			}
15979			else if (button === 'down') {
15980				if (rowNum < numRows) {
15981					$button = $('<button>',{
15982						'id': 'able-vts-button-down-' + rowNum,
15983						'title': 'Move down',
15984						'aria-label': 'Move Row ' + rowNum + ' down'
15985					}).on('click', function(el) {
15986						thisObj.onClickVtsActionButton(el.currentTarget);
15987					});
15988					$svg = $('<svg>',{
15989						'focusable': 'false',
15990						'aria-hidden': 'true',
15991						'x': '0px',
15992						'y': '0px',
15993						'width': '292.362px',
15994						'height': '292.362px',
15995						'viewBox': '0 0 292.362 292.362',
15996						'style': 'enable-background:new 0 0 292.362 292.362'
15997					});
15998					pathString = 'M286.935,69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952,0-9.233,1.807-12.85,5.424'
15999						+ 'C1.807,72.998,0,77.279,0,82.228c0,4.948,1.807,9.229,5.424,12.847l127.907,127.907c3.621,3.617,7.902,5.428,12.85,5.428'
16000						+ 's9.233-1.811,12.847-5.428L286.935,95.074c3.613-3.617,5.427-7.898,5.427-12.847C292.362,77.279,290.548,72.998,286.935,69.377z';
16001					$path = $('<path>',{
16002						'd': pathString
16003					});
16004					$g = $('<g>').append($path);
16005					$svg.append($g);
16006					$button.append($svg);
16007					// Refresh button in the DOM in order for browser to process & display the SVG
16008					$button.html($button.html());
16009					$td.append($button);
16010				}
16011			}
16012			else if (button === 'insert') {
16013				// Add Insert button to all rows
16014				$button = $('<button>',{
16015					'id': 'able-vts-button-insert-' + rowNum,
16016					'title': 'Insert row below',
16017					'aria-label': 'Insert row before Row ' + rowNum
16018				}).on('click', function(el) {
16019					thisObj.onClickVtsActionButton(el.currentTarget);
16020				});
16021				$svg = $('<svg>',{
16022					'focusable': 'false',
16023					'aria-hidden': 'true',
16024					'x': '0px',
16025					'y': '0px',
16026					'width': '401.994px',
16027					'height': '401.994px',
16028					'viewBox': '0 0 401.994 401.994',
16029					'style': 'enable-background:new 0 0 401.994 401.994'
16030				});
16031				pathString = 'M394,154.175c-5.331-5.33-11.806-7.994-19.417-7.994H255.811V27.406c0-7.611-2.666-14.084-7.994-19.414'
16032					+ 'C242.488,2.666,236.02,0,228.398,0h-54.812c-7.612,0-14.084,2.663-19.414,7.993c-5.33,5.33-7.994,11.803-7.994,19.414v118.775'
16033					+ 'H27.407c-7.611,0-14.084,2.664-19.414,7.994S0,165.973,0,173.589v54.819c0,7.618,2.662,14.086,7.992,19.411'
16034					+ 'c5.33,5.332,11.803,7.994,19.414,7.994h118.771V374.59c0,7.611,2.664,14.089,7.994,19.417c5.33,5.325,11.802,7.987,19.414,7.987'
16035					+ 'h54.816c7.617,0,14.086-2.662,19.417-7.987c5.332-5.331,7.994-11.806,7.994-19.417V255.813h118.77'
16036					+ 'c7.618,0,14.089-2.662,19.417-7.994c5.329-5.325,7.994-11.793,7.994-19.411v-54.819C401.991,165.973,399.332,159.502,394,154.175z';
16037				$path = $('<path>',{
16038					'd': pathString
16039				});
16040				$g = $('<g>').append($path);
16041				$svg.append($g);
16042				$button.append($svg);
16043				// Refresh button in the DOM in order for browser to process & display the SVG
16044				$button.html($button.html());
16045				$td.append($button);
16046			}
16047			else if (button === 'delete') {
16048				// Add Delete button to all rows
16049				$button = $('<button>',{
16050					'id': 'able-vts-button-delete-' + rowNum,
16051					'title': 'Delete row ',
16052					'aria-label': 'Delete Row ' + rowNum
16053				}).on('click', function(el) {
16054					thisObj.onClickVtsActionButton(el.currentTarget);
16055				});
16056				$svg = $('<svg>',{
16057					'focusable': 'false',
16058					'aria-hidden': 'true',
16059					'x': '0px',
16060					'y': '0px',
16061					'width': '508.52px',
16062					'height': '508.52px',
16063					'viewBox': '0 0 508.52 508.52',
16064					'style': 'enable-background:new 0 0 508.52 508.52'
16065				});
16066				pathString = 'M397.281,31.782h-63.565C333.716,14.239,319.478,0,301.934,0h-95.347'
16067					+ 'c-17.544,0-31.782,14.239-31.782,31.782h-63.565c-17.544,0-31.782,14.239-31.782,31.782h349.607'
16068					+ 'C429.063,46.021,414.825,31.782,397.281,31.782z';
16069				$path = $('<path>',{
16070					'd': pathString
16071				});
16072				pathString2 = 'M79.456,476.737c0,17.544,14.239,31.782,31.782,31.782h286.042'
16073					+ 'c17.544,0,31.782-14.239,31.782-31.782V95.347H79.456V476.737z M333.716,174.804c0-8.772,7.151-15.891,15.891-15.891'
16074					+ 'c8.74,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.151,15.891-15.891,15.891c-8.74,0-15.891-7.151-15.891-15.891V174.804z'
16075					+ 'M238.369,174.804c0-8.772,7.119-15.891,15.891-15.891c8.74,0,15.891,7.119,15.891,15.891v254.26'
16076					+ 'c0,8.74-7.151,15.891-15.891,15.891c-8.772,0-15.891-7.151-15.891-15.891V174.804z M143.021,174.804'
16077					+ 'c0-8.772,7.119-15.891,15.891-15.891c8.772,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.119,15.891-15.891,15.891'
16078					+ 'c-8.772,0-15.891-7.151-15.891-15.891V174.804z';
16079				$path2 = $('<path>',{
16080					'd': pathString2
16081				});
16082
16083				$g = $('<g>').append($path,$path2);
16084				$svg.append($g);
16085				$button.append($svg);
16086				// Refresh button in the DOM in order for browser to process & display the SVG
16087				$button.html($button.html());
16088				$td.append($button);
16089			}
16090		}
16091		return $td;
16092	};
16093
16094	AblePlayer.prototype.updateVtsActionButtons = function($buttons,nextRowNum) {
16095
16096		// TODO: Add some filters to this function to add or delete 'Up' and 'Down' buttons
16097		// if row is moved to/from the first/last rows
16098		var i, $thisButton, id, label, newId, newLabel;
16099		for (i=0; i < $buttons.length; i++) {
16100			$thisButton = $buttons.eq(i);
16101			id = $thisButton.attr('id');
16102			label = $thisButton.attr('aria-label');
16103			// replace the integer (id) within each of the above strings
16104			newId = id.replace(/[0-9]+/g, nextRowNum);
16105			newLabel = label.replace(/[0-9]+/g, nextRowNum);
16106			$thisButton.attr('id',newId);
16107			$thisButton.attr('aria-label',newLabel);
16108		}
16109	}
16110
16111	AblePlayer.prototype.getIconCredit = function() {
16112
16113		var credit;
16114		credit = '<div id="able-vts-icon-credit">'
16115			+ 'Action buttons made by <a href="https://www.flaticon.com/authors/elegant-themes">Elegant Themes</a> '
16116			+ 'from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> '
16117			+ 'are licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" '
16118			+ 'target="_blank">CC 3.0 BY</a>'
16119			+ '</div>';
16120			return credit;
16121	};
16122
16123	AblePlayer.prototype.getAllLangs = function(tracks) {
16124
16125		// update this.langs with any unique languages found in tracks
16126		var i;
16127		for (i in tracks) {
16128			if (tracks[i].hasOwnProperty('language')) {
16129				if ($.inArray(tracks[i].language,this.langs) === -1) {
16130					// this language is not already in the langs array. Add it.
16131					this.langs[this.langs.length] = tracks[i].language;
16132				}
16133			}
16134		}
16135	};
16136
16137	AblePlayer.prototype.getAllRows = function(lang) {
16138
16139		// returns an array of data to be displayed in VTS table
16140		// includes all cues for tracks of any type with matching lang
16141		// cues are sorted by start time
16142		var i, track, c, cues;
16143		cues = [];
16144		for (i=0; i < this.vtsTracks.length; i++) {
16145			track = this.vtsTracks[i];
16146			if (track.language == lang) {
16147				// this track matches the language. Add its cues to array
16148				for (c in track.cues) {
16149					cues.push({
16150						'kind': track.kind,
16151						'lang': lang,
16152						'id': track.cues[c].id,
16153						'start': track.cues[c].start,
16154						'end': track.cues[c].end,
16155						'content': track.cues[c].content
16156					});
16157				}
16158			}
16159		}
16160		// Now sort cues by start time
16161		cues.sort(function(a,b) {
16162			return a.start > b.start ? 1 : -1;
16163		});
16164		return cues;
16165	};
16166
16167
16168	AblePlayer.prototype.onClickVtsActionButton = function(el) {
16169
16170		// handle click on up, down, insert, or delete button
16171		var idParts, action, rowNum;
16172		idParts = $(el).attr('id').split('-');
16173		action = idParts[3];
16174		rowNum = idParts[4];
16175		if (action == 'up') {
16176			// move the row up
16177			this.moveRow(rowNum,'up');
16178		}
16179		else if (action == 'down') {
16180			// move the row down
16181			this.moveRow(rowNum,'down');
16182		}
16183		else if (action == 'insert') {
16184			// insert a row below
16185			this.insertRow(rowNum);
16186		}
16187		else if (action == 'delete') {
16188			// delete the row
16189			this.deleteRow(rowNum);
16190		}
16191	};
16192
16193	AblePlayer.prototype.insertRow = function(rowNum) {
16194
16195		// Insert empty row below rowNum
16196		var $table, $rows, numRows, newRowNum, newRowId, newTimes, $tr, $td;
16197		var $select, options, i, $option, newKind, newClass, $parentRow;
16198		var i, nextRowNum, $buttons;
16199
16200		$table = $('#able-vts table');
16201		$rows = $table.find('tr');
16202
16203		numRows = $rows.length - 1; // exclude header row
16204
16205		newRowNum = parseInt(rowNum) + 1;
16206		newRowId = 'able-vts-row-' + newRowNum;
16207
16208		// Create an empty row
16209		$tr = $('<tr>',{
16210			'id': newRowId
16211		});
16212
16213		// Row #
16214		$td = $('<td>').text(newRowNum);
16215		$tr.append($td);
16216
16217		// Kind (add a select field for chosing a kind)
16218		newKind = null;
16219		$select = $('<select>',{
16220			'id': 'able-vts-kind-' + newRowNum,
16221			'aria-label': 'What kind of track is this?',
16222			'placeholder': 'Select a kind'
16223		}).on('change',function() {
16224			newKind = $(this).val();
16225			newClass = 'kind-' + newKind;
16226			$parentRow = $(this).closest('tr');
16227			// replace the select field with the chosen value as text
16228			$(this).parent().text(newKind);
16229			// add a class to the parent row
16230			$parentRow.addClass(newClass);
16231		});
16232		options = ['','captions','chapters','descriptions','subtitles'];
16233		for (i=0; i<options.length; i++) {
16234			$option = $('<option>',{
16235				'value': options[i]
16236			}).text(options[i]);
16237			$select.append($option);
16238		}
16239		$td = $('<td>').append($select);
16240		$tr.append($td);
16241
16242		// Start
16243		$td = $('<td>',{
16244			'contenteditable': 'true'
16245		}); // TODO; Intelligently assign a new start time (see getAdjustedTimes())
16246		$tr.append($td);
16247
16248		// End
16249		$td = $('<td>',{
16250			'contenteditable': 'true'
16251		}); // TODO; Intelligently assign a new end time (see getAdjustedTimes())
16252		$tr.append($td);
16253
16254		// Content
16255		$td = $('<td>',{
16256			'contenteditable': 'true'
16257		});
16258		$tr.append($td);
16259
16260		// Actions
16261		$td = this.addVtsActionButtons(newRowNum,numRows);
16262		$tr.append($td);
16263
16264		// Now insert the new row
16265		$table.find('tr').eq(rowNum).after($tr);
16266
16267		// Update row.id, Row # cell, & action items for all rows after the inserted one
16268		for (i=newRowNum; i <= numRows; i++) {
16269			nextRowNum = i + 1;
16270			$rows.eq(i).attr('id','able-vts-row-' + nextRowNum); // increment tr id
16271			$rows.eq(i).find('td').eq(0).text(nextRowNum); // increment Row # as expressed in first td
16272			$buttons = $rows.eq(i).find('button');
16273			this.updateVtsActionButtons($buttons,nextRowNum);
16274		}
16275
16276		// Auto-adjust times
16277		this.adjustTimes(newRowNum);
16278
16279		// Announce the insertion
16280		this.showVtsAlert('A new row ' + newRowNum + ' has been inserted'); // TODO: Localize this
16281
16282		// Place focus in new select field
16283		$select.focus();
16284
16285	};
16286
16287	AblePlayer.prototype.deleteRow = function(rowNum) {
16288
16289		var $table, $rows, numRows, i, nextRowNum, $buttons;
16290
16291		$table = $('#able-vts table');
16292		$table[0].deleteRow(rowNum);
16293		$rows = $table.find('tr'); // this does not include the deleted row
16294		numRows = $rows.length - 1; // exclude header row
16295
16296		// Update row.id, Row # cell, & action buttons for all rows after the deleted one
16297		for (i=rowNum; i <= numRows; i++) {
16298			nextRowNum = i;
16299			$rows.eq(i).attr('id','able-vts-row-' + nextRowNum); // increment tr id
16300			$rows.eq(i).find('td').eq(0).text(nextRowNum); // increment Row # as expressed in first td
16301			$buttons = $rows.eq(i).find('button');
16302			this.updateVtsActionButtons($buttons,nextRowNum);
16303		}
16304
16305		// Announce the deletion
16306		this.showVtsAlert('Row ' + rowNum + ' has been deleted'); // TODO: Localize this
16307
16308	};
16309
16310	AblePlayer.prototype.moveRow = function(rowNum,direction) {
16311
16312		// swap two rows
16313		var $rows, $thisRow, otherRowNum, $otherRow, newTimes, msg;
16314
16315		$rows = $('#able-vts table').find('tr');
16316		$thisRow = $('#able-vts table').find('tr').eq(rowNum);
16317		if (direction == 'up') {
16318			otherRowNum = parseInt(rowNum) - 1;
16319			$otherRow = $('#able-vts table').find('tr').eq(otherRowNum);
16320			$otherRow.before($thisRow);
16321		}
16322		else if (direction == 'down') {
16323			otherRowNum = parseInt(rowNum) + 1;
16324			$otherRow = $('#able-vts table').find('tr').eq(otherRowNum);
16325			$otherRow.after($thisRow);
16326		}
16327		// Update row.id, Row # cell, & action buttons for the two swapped rows
16328		$thisRow.attr('id','able-vts-row-' + otherRowNum);
16329		$thisRow.find('td').eq(0).text(otherRowNum);
16330		this.updateVtsActionButtons($thisRow.find('button'),otherRowNum);
16331		$otherRow.attr('id','able-vts-row-' + rowNum);
16332		$otherRow.find('td').eq(0).text(rowNum);
16333		this.updateVtsActionButtons($otherRow.find('button'),rowNum);
16334
16335		// auto-adjust times
16336		this.adjustTimes(otherRowNum);
16337
16338		// Announce the move (TODO: Localize this)
16339		msg = 'Row ' + rowNum + ' has been moved ' + direction;
16340		msg += ' and is now Row ' + otherRowNum;
16341		this.showVtsAlert(msg);
16342	};
16343
16344	AblePlayer.prototype.adjustTimes = function(rowNum) {
16345
16346		// Adjusts start and end times of the current, previous, and next rows in VTS table
16347		// after a move or insert
16348		// NOTE: Fully automating this process would be extraordinarily complicated
16349		// The goal here is simply to make subtle tweaks to ensure rows appear
16350		// in the new order within the Able Player transcript
16351		// Additional tweaking will likely be required by the user
16352
16353		// HISTORY: Originally set minDuration to 2 seconds for captions and .500 for descriptions
16354		// However, this can results in significant changes to existing caption timing,
16355		// with not-so-positive results.
16356		// As of 3.1.15, setting minDuration to .001 for all track kinds
16357		// Users will have to make further adjustments manually if needed
16358
16359		// TODO: Add WebVTT validation on save, since tweaking times is risky
16360
16361		var	 minDuration, $rows, prevRowNum, nextRowNum, $row, $prevRow, $nextRow,
16362				kind, prevKind, nextKind,
16363				start, prevStart, nextStart,
16364				end, prevEnd, nextEnd;
16365
16366		// Define minimum duration (in seconds) for each kind of track
16367		minDuration = [];
16368		minDuration['captions'] = .001;
16369		minDuration['descriptions'] = .001;
16370		minDuration['chapters'] = .001;
16371
16372		// refresh rows object
16373		$rows = $('#able-vts table').find('tr');
16374
16375		// Get kind, start, and end from current row
16376		$row = $rows.eq(rowNum);
16377		if ($row.is('[class^="kind-"]')) {
16378			// row has a class that starts with "kind-"
16379			// Extract kind from the class name
16380			kind = this.getKindFromClass($row.attr('class'));
16381		}
16382		else {
16383			// Kind has not been assigned (e.g., newly inserted row)
16384			// Set as captions row by default
16385			kind = 'captions';
16386		}
16387		start = this.getSecondsFromColonTime($row.find('td').eq(2).text());
16388		end = this.getSecondsFromColonTime($row.find('td').eq(3).text());
16389
16390		// Get kind, start, and end from previous row
16391		if (rowNum > 1) {
16392			// this is not the first row. Include the previous row
16393			prevRowNum = rowNum - 1;
16394			$prevRow = $rows.eq(prevRowNum);
16395			if ($prevRow.is('[class^="kind-"]')) {
16396				// row has a class that starts with "kind-"
16397				// Extract kind from the class name
16398			 prevKind = this.getKindFromClass($prevRow.attr('class'));
16399			}
16400			else {
16401				// Kind has not been assigned (e.g., newly inserted row)
16402				prevKind = null;
16403			}
16404			prevStart = this.getSecondsFromColonTime($prevRow.find('td').eq(2).text());
16405			prevEnd = this.getSecondsFromColonTime($prevRow.find('td').eq(3).text());
16406		}
16407		else {
16408			// this is the first row
16409			prevRowNum = null;
16410			$prevRow = null;
16411			prevKind = null;
16412			prevStart = null;
16413			prevEnd = null;
16414		}
16415
16416		// Get kind, start, and end from next row
16417		if (rowNum < ($rows.length - 1)) {
16418			// this is not the last row. Include the next row
16419			nextRowNum = rowNum + 1;
16420			$nextRow = $rows.eq(nextRowNum);
16421			if ($nextRow.is('[class^="kind-"]')) {
16422				// row has a class that starts with "kind-"
16423				// Extract kind from the class name
16424			 nextKind = this.getKindFromClass($nextRow.attr('class'));
16425			}
16426			else {
16427				// Kind has not been assigned (e.g., newly inserted row)
16428				nextKind = null;
16429			}
16430			nextStart = this.getSecondsFromColonTime($nextRow.find('td').eq(2).text());
16431			nextEnd = this.getSecondsFromColonTime($nextRow.find('td').eq(3).text());
16432		}
16433		else {
16434			// this is the last row
16435			nextRowNum = null;
16436			$nextRow = null;
16437			nextKind = null;
16438			nextStart = null;
16439			nextEnd = null;
16440		}
16441
16442		if (isNaN(start)) {
16443			if (prevKind == null) {
16444				// The previous row was probably inserted, and user has not yet selected a kind
16445				// automatically set it to captions
16446				prevKind = 'captions';
16447				$prevRow.attr('class','kind-captions');
16448				$prevRow.find('td').eq(1).html('captions');
16449			}
16450			// Current row has no start time (i.e., it's an inserted row)
16451			if (prevKind === 'captions') {
16452				// start the new row immediately after the captions end
16453				start = (parseFloat(prevEnd) + .001).toFixed(3);
16454				if (nextStart) {
16455					// end the new row immediately before the next row starts
16456					end = (parseFloat(nextStart) - .001).toFixed(3);
16457				}
16458				else {
16459					// this is the last row. Use minDuration to calculate end time.
16460					end = (parseFloat(start) + minDuration[kind]).toFixed(3);
16461				}
16462			}
16463			else if (prevKind === 'chapters') {
16464				// start the new row immediately after the chapter start (not end)
16465				start = (parseFloat(prevStart) + .001).toFixed(3);
16466				if (nextStart) {
16467					// end the new row immediately before the next row starts
16468					end = (parseFloat(nextStart) - .001).toFixed(3);
16469				}
16470				else {
16471					// this is the last row. Use minDuration to calculate end time.
16472					end = (parseFloat(start) + minDurartion[kind]).toFixed(3);
16473				}
16474			}
16475			else if (prevKind === 'descriptions') {
16476				// start the new row minDuration['descriptions'] after the description starts
16477				// this will theoretically allow at least a small cushion for the description to be read
16478				start = (parseFloat(prevStart) + minDuration['descriptions']).toFixed(3);
16479				end = (parseFloat(start) + minDuration['descriptions']).toFixed(3);
16480			}
16481		}
16482		else {
16483			// current row has a start time (i.e., an existing row has been moved))
16484			if (prevStart) {
16485				// this is not the first row.
16486				if (prevStart < start) {
16487					if (start < nextStart) {
16488						// No change is necessary
16489					}
16490					else {
16491						// nextStart needs to be incremented
16492						nextStart = (parseFloat(start) + minDuration[kind]).toFixed(3);
16493						nextEnd = (parseFloat(nextStart) + minDuration[nextKind]).toFixed(3);
16494						// TODO: Ensure nextEnd does not exceed the following start (nextNextStart)
16495						// Or... maybe this is getting too complicated and should be left up to the user
16496					}
16497				}
16498				else {
16499					// start needs to be incremented
16500					start = (parseFloat(prevStart) + minDuration[prevKind]).toFixed(3);
16501					end = (parseFloat(start) + minDuration[kind]).toFixed(3);
16502				}
16503			}
16504			else {
16505				// this is the first row
16506				if (start < nextStart) {
16507					// No change is necessary
16508				}
16509				else {
16510					// nextStart needs to be incremented
16511					nextStart = (parseFloat(start) + minDuration[kind]).toFixed(3);
16512					nextEnd = (parseFloat(nextStart) + minDuration[nextKind]).toFixed(3);
16513				}
16514			}
16515		}
16516
16517		// check to be sure there is sufficient duration between new start & end times
16518		if (end - start < minDuration[kind]) {
16519			// duration is too short. Change end time
16520			end = (parseFloat(start) + minDuration[kind]).toFixed(3);
16521			if (nextStart) {
16522				// this is not the last row
16523				// increase start time of next row
16524				nextStart = (parseFloat(end) + .001).toFixed(3);
16525			}
16526		}
16527
16528		// Update all affected start/end times
16529		$row.find('td').eq(2).text(this.formatSecondsAsColonTime(start,true));
16530		$row.find('td').eq(3).text(this.formatSecondsAsColonTime(end,true));
16531		if ($prevRow) {
16532			$prevRow.find('td').eq(2).text(this.formatSecondsAsColonTime(prevStart,true));
16533			$prevRow.find('td').eq(3).text(this.formatSecondsAsColonTime(prevEnd,true));
16534		}
16535		if ($nextRow) {
16536			$nextRow.find('td').eq(2).text(this.formatSecondsAsColonTime(nextStart,true));
16537			$nextRow.find('td').eq(3).text(this.formatSecondsAsColonTime(nextEnd,true));
16538		}
16539	};
16540
16541	AblePlayer.prototype.getKindFromClass = function(myclass) {
16542
16543		// This function is called when a class with prefix "kind-" is found in the class attribute
16544
16545		var kindStart, kindEnd;
16546
16547		kindStart = myclass.indexOf('kind-')+5;
16548		kindEnd = myclass.indexOf(' ',kindStart);
16549		if (kindEnd == -1) {
16550			// no spaces found, "kind-" must be the only myclass
16551			return myclass.substring(kindStart);
16552		}
16553		else {
16554			// kind-* is one of multiple classes 
16555			// the following will find it regardless of position of "kind-*" within the class string
16556			return myclass.substring(kindStart,kindEnd);
16557		}
16558	};
16559
16560	AblePlayer.prototype.showVtsAlert = function(message) {
16561
16562		// this is distinct from greater Able Player showAlert()
16563		// because it's positioning needs are unique
16564		// For now, alertDiv is fixed at top left of screen
16565		// but could ultimately be modified to appear near the point of action in the VTS table
16566		this.$vtsAlert.text(message).show().delay(3000).fadeOut('slow');
16567	};
16568
16569	AblePlayer.prototype.parseVtsOutput = function($table) {
16570
16571		// parse table into arrays, then into WebVTT content, for each kind
16572		// Display the WebVTT content in textarea fields for users to copy and paste
16573		var lang, i, kinds, kind, vtt, $rows, start, end, content, $output;
16574
16575		lang = $table.attr('lang');
16576		kinds = ['captions','chapters','descriptions','subtitles'];
16577		vtt = {};
16578		for (i=0; i < kinds.length; i++) {
16579			kind = kinds[i];
16580			vtt[kind] = 'WEBVTT' + "\n\n";
16581		}
16582		$rows = $table.find('tr');
16583		if ($rows.length > 0) {
16584			for (i=0; i < $rows.length; i++) {
16585				kind = $rows.eq(i).find('td').eq(1).text();
16586				if ($.inArray(kind,kinds) !== -1) {
16587					start = $rows.eq(i).find('td').eq(2).text();
16588					end = $rows.eq(i).find('td').eq(3).text();
16589					content = $rows.eq(i).find('td').eq(4).text();
16590					if (start !== undefined && end !== undefined) {
16591						vtt[kind] += start + ' --> ' + end + "\n";
16592						if (content !== 'undefined') {
16593							vtt[kind] += content;
16594						}
16595						vtt[kind] += "\n\n";
16596					}
16597				}
16598			}
16599		}
16600		$output = $('<div>',{
16601			'id': 'able-vts-output'
16602		})
16603		$('#able-vts').append($output);
16604		for (i=0; i < kinds.length; i++) {
16605			kind = kinds[i];
16606			if (vtt[kind].length > 8) {
16607				// some content has been added
16608				this.showWebVttOutput(kind,vtt[kind],lang)
16609			}
16610		}
16611	};
16612
16613	AblePlayer.prototype.showWebVttOutput = function(kind,vttString,lang) {
16614
16615		var $heading, filename, $p, pText, $textarea;
16616
16617		$heading = $('<h3>').text(kind.charAt(0).toUpperCase() + kind.slice(1));
16618		filename = this.getFilenameFromTracks(kind,lang);
16619		pText = 'If you made changes, copy/paste the following content ';
16620		if (filename) {
16621			pText += 'to replace the original content of your ' + this.getLanguageName(lang) + ' ';
16622			pText += '<em>' + kind + '</em> WebVTT file (<strong>' + filename + '</strong>).';
16623		}
16624		else {
16625			pText += 'into a new ' + this.getLanguageName(lang) + ' <em>' + kind + '</em> WebVTT file.';
16626		}
16627		$p = $('<p>',{
16628			'class': 'able-vts-output-instructions'
16629		}).html(pText);
16630		$textarea = $('<textarea>').text(vttString);
16631		$('#able-vts-output').append($heading,$p,$textarea);
16632	};
16633
16634})(jQuery);
16635
16636
16637(function ($) {
16638	AblePlayer.prototype.initVimeoPlayer = function () {
16639
16640		var thisObj, deferred, promise, containerId, vimeoId, autoplay, videoDimensions, options;
16641		thisObj = this;
16642
16643		deferred = new $.Deferred();
16644		promise = deferred.promise();
16645
16646		containerId = this.mediaId + '_vimeo';
16647
16648		// add container to which Vimeo player iframe will be appended
16649		this.$mediaContainer.prepend($('<div>').attr('id', containerId));
16650
16651		// if a described version is available && user prefers desription
16652		// init player using the described version
16653		if (this.vimeoDescId && this.prefDesc) {
16654			vimeoId = this.vimeoDescId;
16655		}
16656		else {
16657			vimeoId = this.vimeoId;
16658		}
16659		this.activeVimeoId = vimeoId;
16660
16661		// Notes re. Vimeo Embed Options:
16662		// If a video is owned by a user with a paid Plus, PRO, or Business account,
16663		// setting the "controls" option to "false" will hide the default controls, without hiding captions.
16664		// This is a new option from Vimeo; previously used "background:true" to hide the controller,
16665		// but that had unwanted side effects:
16666		//  - In addition to hiding the controls, it also hides captions
16667		//  - It automatically autoplays (initializing the player with autoplay:false does not override this)
16668		//  - It automatically loops (but this can be overridden by initializing the player with loop:false)
16669		//  - It automatically sets volume to 0 (not sure if this can be overridden, since no longer using the background option)
16670
16671		if (this.okToPlay) {
16672			autoplay = 'true';
16673		}
16674		else {
16675			autoplay = 'false';
16676		}
16677
16678		if (this.playerWidth) {			
16679			if (this.vimeoUrlHasParams) { 
16680				// use url param, not id 
16681				options = {
16682					url: vimeoId,
16683					width: this.playerWidth,
16684					controls: false
16685				}
16686			}
16687			else { 
16688				options = {
16689					id: vimeoId,
16690					width: this.playerWidth,
16691					controls: false
16692				}
16693			}
16694		}
16695		else { 
16696			// initialize without width & set width later 
16697			if (this.vimeoUrlHasParams) { 
16698				options = {
16699					url: vimeoId,
16700					controls: false
16701				}
16702			}
16703			else { 
16704				options = {
16705					id: vimeoId,
16706					controls: false
16707				}
16708			}
16709		}
16710
16711		this.vimeoPlayer = new Vimeo.Player(containerId, options);
16712
16713		this.vimeoPlayer.ready().then(function() {
16714			// add tabindex -1 on iframe so vimeo frame cannot be focused on
16715			$('#'+containerId).children('iframe').attr({
16716				'tabindex': '-1',
16717				'aria-hidden': true
16718			});
16719
16720			// get video's intrinsic size and initiate player dimensions
16721			thisObj.vimeoPlayer.getVideoWidth().then(function(width) {						
16722				if (width) { 
16723					// also get height 
16724					thisObj.vimeoPlayer.getVideoHeight().then(function(height) {	
16725						if (height) { 								
16726							thisObj.resizePlayer(width,height); 								
16727						}
16728					});														
16729				}
16730			}).catch(function(error) {
16731				// an error occurred getting height or width 
16732				// TODO: Test this to see how gracefully it organically recovers 
16733			});
16734
16735			if (!thisObj.hasPlaylist) {
16736				// remove the media element, since Vimeo replaces that with its own element in an iframe
16737				// this is handled differently for playlists. See buildplayer.js > cuePlaylistItem()
16738				thisObj.$media.remove();
16739
16740				// define variables that will impact player setup
16741
16742				// vimeoSupportsPlaybackRateChange
16743				// changing playbackRate is only supported if the video is hosted on a Pro or Business account
16744				// unfortunately there is no direct way to query for that information.
16745				// this.vimeoPlayer.getPlaybackRate() returns a value, regardless of account type
16746				// This is a hack:
16747				// Attempt to change the playbackRate. If it results in an error, assume changing playbackRate is not supported.
vendor: 4,365 bytes, lines 16748-16908
16748				// Supported playbackRate values are 0.5 to 2.
16749				thisObj.vimeoPlaybackRate = 1;
16750				thisObj.vimeoPlayer.setPlaybackRate(thisObj.vimeoPlaybackRate).then(function(playbackRate) {
16751				// playback rate was set
16752					thisObj.vimeoSupportsPlaybackRateChange = true;
16753				}).catch(function(error) {
16754					thisObj.vimeoSupportsPlaybackRateChange = false;
16755				});
16756				deferred.resolve();
16757			}
16758		});
16759		return promise;
16760	};
16761
16762	AblePlayer.prototype.getVimeoPaused = function () {
16763
16764		var deferred, promise;
16765		deferred = new $.Deferred();
16766		promise = deferred.promise();
16767
16768		this.vimeoPlayer.getPaused().then(function (paused) {
16769			// paused is Boolean
16770			deferred.resolve(paused);
16771		});
16772
16773		return promise;
16774	}
16775
16776	AblePlayer.prototype.getVimeoEnded = function () {
16777
16778		var deferred, promise;
16779		deferred = new $.Deferred();
16780		promise = deferred.promise();
16781
16782		this.vimeoPlayer.getEnded().then(function (ended) {
16783			// ended is Boolean
16784			deferred.resolve(ended);
16785		});
16786
16787		return promise;
16788	}
16789
16790	AblePlayer.prototype.getVimeoState = function () {
16791
16792		var thisObj, deferred, promise, promises, gettingPausedPromise, gettingEndedPromise;
16793
16794		thisObj = this;
16795
16796		deferred = new $.Deferred();
16797		promise = deferred.promise();
16798		promises = [];
16799
16800		gettingPausedPromise = this.vimeoPlayer.getPaused();
16801		gettingEndedPromise = this.vimeoPlayer.getEnded();
16802
16803		promises.push(gettingPausedPromise);
16804		promises.push(gettingEndedPromise);
16805
16806		gettingPausedPromise.then(function (paused) {
16807			deferred.resolve(paused);
16808		});
16809		gettingEndedPromise.then(function (ended) {
16810			deferred.resolve(ended);
16811		});
16812		$.when.apply($, promises).then(function () {
16813			deferred.resolve();
16814		});
16815		return promise;
16816	}
16817
16818	AblePlayer.prototype.getVimeoCaptionTracks = function () {
16819
16820		// get data via Vimeo Player API, and push data to this.captions
16821		// Note: Vimeo doesn't expose the caption cues themselves
16822		// so this.captions will only include metadata about caption tracks; not cues
16823		var deferred = new $.Deferred();
16824		var promise = deferred.promise();
16825
16826		var thisObj, i, trackId, isDefaultTrack;
16827
16828		thisObj = this;
16829
16830		this.vimeoPlayer.getTextTracks().then(function(tracks) {
16831
16832				// each Vimeo track includes the following:
16833				// label (local name of the language)
16834				// language (2-character code)
16835				// kind (captions or subtitles, as declared by video owner)
16836				// mode ('disabled' or 'showing')
16837
16838				if (tracks.length) {
16839
16840					// create a new button for each caption track
16841					for (i=0; i<tracks.length; i++) {
16842
16843						thisObj.hasCaptions = true;
16844						if (thisObj.prefCaptions === 1) {
16845								thisObj.captionsOn = true;
16846						}
16847						else {
16848							thisObj.captionsOn = false;
16849						}
16850						// assign the default track based on language of the player
16851						if (tracks[i]['language'] === thisObj.lang) {
16852							isDefaultTrack = true;
16853						}
16854						else {
16855								isDefaultTrack = false;
16856						}
16857						thisObj.tracks.push({
16858							'kind': tracks[i]['kind'],
16859							'language': tracks[i]['language'],
16860							'label': tracks[i]['label'],
16861							'def': isDefaultTrack
16862						});						
16863					}
16864					thisObj.captions = thisObj.tracks; 
16865					thisObj.hasCaptions = true;
16866
16867					// setupPopups again with new captions array, replacing original
16868					thisObj.setupPopups('captions');
16869					deferred.resolve();
16870			 	}
16871			 	else {
16872					thisObj.hasCaptions = false;
16873					thisObj.usingVimeoCaptions = false; 
16874					deferred.resolve();
16875				}
16876			});
16877
16878		return promise;
16879	};
16880
16881	AblePlayer.prototype.getVimeoPosterUrl = function (vimeoId, width) {
16882
16883		// this is a placeholder, copied from getYouTubePosterUrl()
16884		// Vimeo doesn't seem to have anything similar, 
16885		// nor does it seem to be possible to get the poster via the Vimeo API
16886		// Vimeo playlist support (with thumbnail images) may require use of data-poster 
16887
16888		// return a URL for retrieving a YouTube poster image
16889		// supported values of width: 120, 320, 480, 640
16890
16891		var url = 'https://img.youtube.com/vi/' + youTubeId;
16892		if (width == '120') {
16893			// default (small) thumbnail, 120 x 90
16894			return url + '/default.jpg';
16895		}
16896		else if (width == '320') {
16897			// medium quality thumbnail, 320 x 180
16898			return url + '/hqdefault.jpg';
16899		}
16900		else if (width == '480') {
16901			// high quality thumbnail, 480 x 360
16902			return url + '/hqdefault.jpg';
16903		}
16904		else if (width == '640') {
16905			// standard definition poster image, 640 x 480
16906			return url + '/sddefault.jpg';
16907		}
16908		return false;
16909	};	
16910
16911	AblePlayer.prototype.getVimeoId = function (url) {
16912		
16913		// return a Vimeo ID, extracted from a full Vimeo URL
16914		// Supported URL patterns are anything containing 'vimeo.com'
16915		//  and ending with a '/' followed by the ID. 
16916		// (Vimeo IDs do not have predicatable lengths)
16917		
16918		// Update: If URL contains parameters, return the full url 
16919		// This will need to be passed to the Vimeo Player API 
16920		// as a url parameter, not as an id parameter		 
16921		this.vimeoUrlHasParams = false; 
16922	
16923		var idStartPos, id; 
16924
16925		if (typeof url === 'number') { 
16926			// this is likely already a vimeo ID 
16927			return url; 
16928		}
16929		else if (url.indexOf('vimeo.com') !== -1) { 
16930			// this is a full Vimeo URL 
16931			if (url.indexOf('?') !== -1) { 
16932				// URL contains parameters 
16933				this.vimeoUrlHasParams = true; 
16934				return url; "use strict";var $,$,$,AblePlayerInstances=[];!function(t){t(function(){t("video, audio").each(function(e,i){void 0!==t(i).data("able-player")&&AblePlayerInstances.push(new AblePlayer(t(this),t(i)))})}),window.onYouTubeIframeAPIReady=function(){AblePlayer.youTubeIframeAPIReady=!0,t("body").trigger("youTubeIframeAPIReady",[])},t(window).on("keydown",function(t){1===AblePlayer.nextIndex&&AblePlayer.lastCreated.onPlayerKeyPress(t)}),window.AblePlayer=function(e){var i=this;if(AblePlayer.lastCreated=this,this.media=e,0===t(e).length){this.provideFallback();return}if(void 0!==t(e).attr("autoplay")?(this.autoplay=!0,this.okToPlay=!0):(this.autoplay=!1,this.okToPlay=!1),void 0!==t(e).attr("loop")?this.loop=!0:this.loop=!1,void 0!==t(e).attr("playsinline")?this.playsInline="1":this.playsInline="0",t(e).attr("poster")?this.hasPoster=!0:this.hasPoster=!1,t(e).attr("width")&&(this.width=t(e).attr("width")),t(e).attr("height")&&(this.height=t(e).attr("height")),void 0!==t(e).data("start-time")&&t.isNumeric(t(e).data("start-time"))?this.startTime=t(e).data("start-time"):this.startTime=0,void 0!==t(e).data("debug")&&!1!==t(e).data("debug")?this.debug=!0:this.debug=!1,void 0!==t(e).data("root-path")?this.rootPath=t(e).data("root-path").replace(/\/?$/,"/"):this.rootPath=this.getRootPath(),this.defaultVolume=7,void 0!==t(e).data("volume")&&""!==t(e).data("volume")){var s=t(e).data("volume");s>=0&&s<=10&&(this.defaultVolume=s)}if(this.volume=this.defaultVolume,void 0!==t(e).data("use-chapters-button")&&!1===t(e).data("use-chapters-button")?this.useChaptersButton=!1:this.useChaptersButton=!0,void 0!==t(e).data("descriptions-audible")&&!1===t(e).data("descriptions-audible")?this.readDescriptionsAloud=!1:void 0!==t(e).data("description-audible")&&!1===t(e).data("description-audible")?this.readDescriptionsAloud=!1:this.readDescriptionsAloud=!0,"screenreader"==t(e).data("desc-reader")?this.descReader="screenreader":this.descReader="browser","off"==t(e).data("state-captions")?this.defaultStateCaptions=0:this.defaultStateCaptions=1,"on"==t(e).data("state-descriptions")?this.defaultStateDescriptions=1:this.defaultStateDescriptions=0,"off"==t(e).data("desc-pause-default")?this.defaultDescPause=0:this.defaultDescPause=1,void 0!==t(e).data("heading-level")&&""!==t(e).data("heading-level")){var a=t(e).data("heading-level");/^[0-6]*$/.test(a)&&(this.playerHeadingLevel=a)}if(void 0!==t(e).data("transcript-div")&&""!==t(e).data("transcript-div")?this.transcriptDivLocation=t(e).data("transcript-div"):this.transcriptDivLocation=null,void 0!==t(e).data("include-transcript")&&!1===t(e).data("include-transcript")?this.hideTranscriptButton=!0:this.hideTranscriptButton=null,this.transcriptType=null,void 0!==t(e).data("transcript-src")?(this.transcriptSrc=t(e).data("transcript-src"),this.transcriptSrcHasRequiredParts()?this.transcriptType="manual":console.log("ERROR: Able Player transcript is missing required parts")):t(e).find('track[kind="captions"], track[kind="subtitles"]').length>0&&(this.transcriptDivLocation?this.transcriptType="external":this.transcriptType="popup"),void 0!==t(e).data("lyrics-mode")&&!1!==t(e).data("lyrics-mode")?this.lyricsMode=!0:this.lyricsMode=!1,void 0!==t(e).data("transcript-title")&&""!==t(e).data("transcript-title")&&(this.transcriptTitle=t
16934(e).data("transcript-title")),"overlay"===t(e).data("captions-position")?this.defaultCaptionsPosition="overlay":this.defaultCaptionsPosition="below",void 0!==t(e).data("chapters-div")&&""!==t(e).data("chapters-div")&&(this.chaptersDivLocation=t(e).data("chapters-div")),void 0!==t(e).data("chapters-title")&&(this.chaptersTitle=t(e).data("chapters-title")),void 0!==t(e).data("chapters-default")&&""!==t(e).data("chapters-default")?this.defaultChapter=t(e).data("chapters-default"):this.defaultChapter=null,"arrows"===t(e).data("speed-icons")?this.speedIcons="arrows":this.speedIcons="animals","chapter"===t(e).data("seekbar-scope")||"chapters"===t(e).data("seekbar-scope")?this.seekbarScope="chapter":this.seekbarScope="video",void 0!==t(e).data("youtube-id")&&""!==t(e).data("youtube-id")&&(this.youTubeId=this.getYouTubeId(t(e).data("youtube-id"))),void 0!==t(e).data("youtube-desc-id")&&""!==t(e).data("youtube-desc-id")&&(this.youTubeDescId=this.getYouTubeId(t(e).data("youtube-desc-id"))),void 0!==t(e).data("youtube-nocookie")&&t(e).data("youtube-nocookie")?this.youTubeNoCookie=!0:this.youTubeNoCookie=!1,void 0!==t(e).data("vimeo-id")&&""!==t(e).data("vimeo-id")&&(this.vimeoId=this.getVimeoId(t(e).data("vimeo-id"))),void 0!==t(e).data("vimeo-desc-id")&&""!==t(e).data("vimeo-desc-id")&&(this.vimeoDescId=this.getVimeoId(t(e).data("vimeo-desc-id"))),"2020"==t(e).data("skin")?this.skin="2020":this.skin="legacy",void 0!==t(e).data("width")?this.playerWidth=parseInt(t(e).data("width")):t(e)[0].getAttribute("width")?this.playerWidth=parseInt(t(e)[0].getAttribute("width")):this.playerWidth=null,this.iconType="font",this.forceIconType=!1,void 0!==t(e).data("icon-type")&&""!==t(e).data("icon-type")){var n=t(e).data("icon-type");("font"===n||"image"==n||"svg"==n)&&(this.iconType=n,this.forceIconType=!0)}if(void 0!==t(e).data("allow-fullscreen")&&!1===t(e).data("allow-fullscreen")?this.allowFullscreen=!1:this.allowFullscreen=!0,this.clickedFullscreenButton=!1,this.restoringAfterFullscreen=!1,this.defaultSeekInterval=10,this.useFixedSeekInterval=!1,void 0!==t(e).data("seek-interval")&&""!==t(e).data("seek-interval")){var r=t(e).data("seek-interval");/^[1-9][0-9]*$/.test(r)&&(this.seekInterval=r,this.useFixedSeekInterval=!0)}void 0!==t(e).data("show-now-playing")&&!1===t(e).data("show-now-playing")?this.showNowPlaying=!1:this.showNowPlaying=!0,void 0!==t(e).data("use-ttml")?(this.useTtml=!0,this.convert=require("xml-js")):this.useTtml=!1,void 0!==t(e).data("test-fallback")&&!1!==t(e).data("test-fallback")?"2"==t(e).data("test-fallback")?this.testFallback=2:this.testFallback=1:this.testFallback=!1,void 0!==t(e).data("lang")&&""!==t(e).data("lang")?this.lang=t(e).data("lang").toLowerCase():this.lang=null,void 0!==t(e).data("meta-type")&&""!==t(e).data("meta-type")&&(this.metaType=t(e).data("meta-type")),void 0!==t(e).data("meta-div")&&""!==t(e).data("meta-div")&&(this.metaDiv=t(e).data("meta-div")),void 0!==t(e).data("search-div")&&""!==t(e).data("search-div")&&(this.searchDiv=t(e).data("search-div"),void 0!==t(e).data("search")&&""!==t(e).data("search")&&(this.searchString=t(e).data("search")),void 0!==t(e).data("search-lang")&&""!==t(e).data("search-lang")?this.searchLang=t(e).data("search-lang"):this.searchLang=null,void 0!==t(e).data("search-ignore-caps")&&!1!==t(e).data("search-ignore-caps")?this.searchIgnoreCaps=!0:this.searchIgnoreCaps=!1,void 0!==t(e).data("search-div")&&""!==t(e).data("search-div")&&(this.searchString=t(e).data("search"),this.searchDiv=t(e).data("search-div"))),void 0!==t(e).data("hide-controls")&&!1!==t(e).data("hide-controls")?(this.hideControls=!0,this.hideControlsOriginal=!0):(this.hideControls=!1,this.hideControlsOriginal=!1),void 0!==t(e).data("steno-mode")&&!1!==t(e).data("steno-mode")?(this.stenoMode=!0,void 0!==t(e).data("steno-iframe-id")&&""!==t(e).data("steno-iframe-id")?(this.stenoFrameId=t(e).data("steno-iframe-id"),this.$stenoFrame=t("#"+this.stenoFrameId),this.$stenoFrame.length||(this.stenoFrameId=null,this.$stenoFrame=null)):(this.stenoFrameId=null,this.$stenoFrame=null)):(this.stenoMode=!1,this.stenoFrameId=null,this.$stenoFrame=null),this.setDefaults(),this.ableIndex=AblePlayer.nextIndex,AblePlayer.nextIndex+=1,this.title=t(e).attr("title"),this.tt
16934={};var i=this;t.when(this.getTranslationText()).then(function(){i.countProperties(i.tt)>50?i.setup():i.provideFallback()}).fail(function(){i.provideFallback()})},AblePlayer.nextIndex=0,AblePlayer.prototype.setup=function(){var t=this;this.initializing=!0,this.reinitialize().then(function(){t.player?t.setupInstance().then(function(){t.setupInstancePlaylist(),t.hasPlaylist||t.recreatePlayer().then(function(){t.initializing=!1,t.playerCreated=!0})}):t.provideFallback()})},AblePlayer.getActiveDOMElement=function(){for(var t=document.activeElement;t.shadowRoot&&t.shadowRoot.activeElement;)t=t.shadowRoot.activeElement;return t},AblePlayer.localGetElementById=function(e,i){return e.getRootNode?t(e.getRootNode().querySelector("#"+i)):t(document.getElementById(i))},AblePlayer.youTubeIframeAPIReady=!1,AblePlayer.loadingYouTubeIframeAPI=!1}(jQuery),function(t){AblePlayer.prototype.setDefaults=function(){this.playerCreated=!1,this.playing=!1,this.paused=!0,this.clickedPlay=!1,this.fullscreen=!1,this.swappingSrc=!1,this.initializing=!1,this.cueingPlaylistItems=!1,this.buttonWithFocus=null,this.speechEnabled=null,this.setIconColor(),this.setButtonImages()},AblePlayer.prototype.getRootPath=function(){var t,e,i,s,a,n,r,o;for(e=0,t=document.getElementsByTagName("script");e<t.length;e++)if(-1!==(s=(i=t[e].src).substring(i.lastIndexOf("/"))).indexOf("ableplayer")){a=i.split("?")[0];break}return r=(n=a.split("/").slice(0,-1).join("/")).lastIndexOf("/"),o=n.substring(0,r)+"/"},AblePlayer.prototype.setIconColor=function(){var e,i,s,a,n,r,o,h,l,p;for(i=0,e=["controller","toolbar"];i<e.length;i++)"controller"==e[i]?s=t("<div>",{class:"able-controller"}).hide():"toolbar"===e[i]&&(s=t("<div>",{class:"able-window-toolbar"}).hide()),t("body").append(s),r=(n=(a=s.css("background-color")).replace(/[^\d,]/g,"").split(","))[0],p=(l=.2126*r+.7152*(o=n[1])+.0722*(h=n[2]))<125?"white":"black","controller"===e[i]?this.iconColor=p:"toolbar"===e[i]&&(this.toolbarIconColor=p),s.remove()},AblePlayer.prototype.setButtonImages=function(){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","arrows"===this.speedIcons?(this.fasterButtonImg=this.imgPath+"slower.png",this.slowerButtonImg=this.imgPath+"faster.png"):"animals"===this.speedIcons&&(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"},AblePlayer.prototype.getSvgData=function(t){var e=[];switch(t){case"play":e[0]="0 0 16 20",e[1]="M0 18.393v-16.429q0-0.29 0.184-0.402t0.441 0.033l14.821 8.237q0.257 0.145 0.257 0.346t-0.257 0.346l-14.821 8.237q-0.257 0.145-0.441 0.033t-0.184-0.402z";break;case"pause":e[0]="0 0 20 20",e[1]="M0 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h5.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-5.714q-0.29 0-0.502-0.212t-0.212-0.502zM10 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h5.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-5.714q-0.29 0-0.502-0.212t-0.212-0.502z";break;case"stop":e[0]="0 0 20 20",e[1]="M0 18.036v-15.714q0-0.29 0.212-0.502t0.502-0.212h15.714q0.29 0 0.502 0.212t0.212 0.502v15.714q0 0.29-0.212 0.502t-0.502 0.212h-15.714q-0.29 0-0.502-0.212t-0.212-0.502z";break;case"restart":e[0]="0 0 20 20",e[1]="M18 8h-6l2.243-2.243c-1.133-1.133-2.64-1.757-4.243-1.757s-3.109 0.624-4.243 1.757c-1.133 1.133-1.757 2.64-1.757 4.243s0.624 3.109 1.757 4.243c1.133 1.133 2.64 1.757 4.243 1.757s3.109-0.624 4.243-1.757c0.095-0.095 0.185-0.192 0.273-0.292l1.505 1.317c-1.466 1.674-3.62 2.732-6.020 2.732-4.418 0-8-3.582-8-8s3.582-8 8-8c2.209 0 4.209 0.896 5.656 2.344l2.344-2.344v6z";break;case"rewind":e[0]="0 0 24 24",e[1]="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z";break;case"forward":e[0]="0 0 24 24",e[1]="M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z";break;case"previous":e[0]="0 0 20 20",e[1]="M5 17.5v-15h2.5v6.875l6.25-6.25v13.75l-6.25-6.25v6.875z";break;case"next":e[0]="0 0 20 20",e[1]="M15 2.5v15h-2.5v-6.875l-6.25 6.25v-13.75l6.25 6.25v-6.875z";break;case"slower":e[0]="0 0 20 20",e[1]="M0 7.321q0-0.29 0.212-0.502t0.502-0.212h10q0.29 0 0.502 0.212t0.212 0.502-0.212 0.502l-5 5q-0.212 0.212-0.502 0.212t-0.502-0.212l-5-5q-0.212-0.212-0.212-0.502z";break;case"faster":e[0]="0 0 11 20",e[1]="M0 12.411q0-0.29 0.212-0.502l5-5q0.212-0.212 0.502-0.212t0.502 0.212l5 5q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-10q-0.29 0-0.502-0.212t-0.212-0.502z";break;case"turtle":e[0]="0 0 20 20",e[1]="M17.212 3.846c-0.281-0.014-0.549 0.025-0.817 0.144-1.218 0.542-1.662 2.708-2.163 3.942-1.207 2.972-7.090 4.619-11.755 5.216-0.887 0.114-1.749 0.74-2.428 1.466 0.82-0.284 2.126-0.297 2.74 0.144 0.007 0.488-0.376 1.062-0.625 1.37-0.404 0.5-0.398 0.793 0.12 0.793 0.473 0 0.752 0.007 1.635 0 0.393-0.003 0.618-0.16 1.49-1.49 3.592 0.718 5.986-0.264 5.986-0.264s0.407 1.755 1.418 1.755h1.49c0.633 0 0.667-0.331 0.625-0.433-0.448-1.082-0.68-1.873-0.769-2.5-0.263-1.857 0.657-3.836 2.524-5.457 0.585 0.986 2.253 0.845 2.909-0.096s0.446-2.268-0.192-3.221c-0.49-0.732-1.345-1.327-2.188-1.37zM8.221 4.663c-0.722-0.016-1.536 0.111-2.5 0.409-4.211 1.302-4.177 4.951-3.51 5.745 0 0-0.955 0.479-0.409 1.274 0.448 0.652 3.139 0.191 5.409-0.529s4.226-1.793 5.312-2.692c0.948-0.785 0.551-2.106-0.505-1.947-0.494-0.98-1.632-2.212-3.798-2.26zM18.846 5.962c0.325 0 0.577 0.252 0.577 0.577s-0.252 0.577-0.577 0.577c-0.325 0-0.577-0.252-0.577-0.577s0.252-0.577 0.577-0.577z";break;case"rabbit":e[0]="0 0 20 20",e[1]="M10.817 0c-2.248 0-1.586 0.525-1.154 0.505 1.551-0.072 5.199 0.044 6.851 2.428 0 0-1.022-2.933-5.697-2.933zM10.529 0.769c-2.572 0-2.837 0.51-2.837 1.106 0 0.545 1.526 0.836 2.524 0.697 2.778-0.386 4.231-0.12 5.264 0.865-1.010 0.779-0.75 1.401-1.274 1.851-1.093 0.941-2.643-0.673-4.976-0.673-2.496 0-4.712 1.92-4.712 4.76-0.157-0.537-0.769-0.913-1.442-0.913-0.974 0-1.514 0.637-1.514 1.49 0 0.769 1.13 1.791 2.861 0.938 0.499 1.208 2.265 1.364 2.452 1.418 0.538 0.154 1.875 0.098 1.875 0.865 0 0.794-1.034 1.094-1.034 1.707 0 1.070 1.758 0.873 2.284 1.034 1.683 0.517 2.103 1.214 2.788 2.212 0.771 1.122 2.572 1.408 2.572 0.625 0-3.185-4.413-4.126-4.399-4.135 0.608-0.382 2.139-1.397 2.139-3.534 0-1.295-0.703-2.256-1.755-2.861 1.256 0.094 2.572 1.205 2.572 2.74 0 1.877-0.653 2.823-0.769 2.957 1.975-1.158 3.193-3.91 3.029-6.37 0.61 0.401 1.27 0.577 1.971 0.625 0.751 0.052 1.475-0.225 1.635-0.529 0.38-0.723 0.162-2.321-0.12-2.837-0.763-1.392-2.236-1.73-3.606-1.683-1.202-1.671-3.812-2.356-5.529-2.356zM1.37 3.077l-0.553 1.538h3.726c0.521-0.576 1.541-1.207 2.284-1.538h-5.457zM18.846 5.192c0.325 0 0.577 0.252 0.577 0.577s-0.252 0.577-0.577 0.577c-0.325 0-0.577-0.252-0.577-0.577s0.252-0.577 0.577-0.577zM0.553 5.385l-0.553 1.538h3.197c0.26-0.824 0.586-1.328 0.769-1.538h-3.413z";break;case"ellipsis":e[0]="0 0 20 20",e[1]="M10.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.985 2.199-2.2s-0.984-2.2-2.199-2.2zM3.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.986 2.199-2.2s-0.984-2.2-2.199-2.2zM17.001 7.8c-1.215 0-2.201 0.985-2.201 2.2s0.986 2.2 2.201 2.2c1.215 0 2.199-0.985 2.199-2.2s-0.984-2.2-2.199-2.2z";break;case"pipe":e[0]="0 0 20 20",e[1]="M10.15 0.179h0.623c0.069 0 0.127 0.114 0.127 0.253v19.494c0 0.139-0.057 0.253-0.127 0.253h-1.247c-0.069 0-0.126-0.114-0.126-0.253v-19.494c0-0.139 0.057-0.253 0.126-0.253h0.623z";break;case"captions":case"transcript":e[0]="0 0 20 20",e[1]="M0.033 3.624h19.933v12.956h-19.933v-12.956zM18.098 10.045c-0.025-2.264-0.124-3.251-0.743-3.948-0.112-0.151-0.322-0.236-0.496-0.344-0.606-0.386-3.465-0.526-6.782-0.526s-6.313 0.14-6.907 0.526c-0.185 0.108-0.396 0.193-0.519 0.344-0.607 0.697-0.693 1.684-0.731 3.948 0.037 2.265 0.124 3.252 0.731 3.949 0.124 0.161 0.335 0.236 0.519 0.344 0.594 0.396 3.59 
169340.526 6.907 0.547 3.317-0.022 6.176-0.151 6.782-0.547 0.174-0.108 0.384-0.183 0.496-0.344 0.619-0.697 0.717-1.684 0.743-3.949v0 0zM9.689 9.281c-0.168-1.77-1.253-2.813-3.196-2.813-1.773 0-3.168 1.387-3.168 3.617 0 2.239 1.271 3.636 3.372 3.636 1.676 0 2.851-1.071 3.035-2.852h-2.003c-0.079 0.661-0.397 1.168-1.068 1.168-1.059 0-1.253-0.91-1.253-1.876 0-1.33 0.442-2.010 1.174-2.010 0.653 0 1.068 0.412 1.13 1.129h1.977zM16.607 9.281c-0.167-1.77-1.252-2.813-3.194-2.813-1.773 0-3.168 1.387-3.168 3.617 0 2.239 1.271 3.636 3.372 3.636 1.676 0 2.851-1.071 3.035-2.852h-2.003c-0.079 0.661-0.397 1.168-1.068 1.168-1.059 0-1.253-0.91-1.253-1.876 0-1.33 0.441-2.010 1.174-2.010 0.653 0 1.068 0.412 1.13 1.129h1.976z";break;case"descriptions":e[0]="0 0 20 20",e[1]="M17.623 3.57h-1.555c1.754 1.736 2.763 4.106 2.763 6.572 0 2.191-0.788 4.286-2.189 5.943h1.484c1.247-1.704 1.945-3.792 1.945-5.943-0-2.418-0.886-4.754-2.447-6.572v0zM14.449 3.57h-1.55c1.749 1.736 2.757 4.106 2.757 6.572 0 2.191-0.788 4.286-2.187 5.943h1.476c1.258-1.704 1.951-3.792 1.951-5.943-0-2.418-0.884-4.754-2.447-6.572v0zM11.269 3.57h-1.542c1.752 1.736 2.752 4.106 2.752 6.572 0 2.191-0.791 4.286-2.181 5.943h1.473c1.258-1.704 1.945-3.792 1.945-5.943 0-2.418-0.876-4.754-2.447-6.572v0zM10.24 9.857c0 3.459-2.826 6.265-6.303 6.265v0.011h-3.867v-12.555h3.896c3.477 0 6.274 2.806 6.274 6.279v0zM6.944 9.857c0-1.842-1.492-3.338-3.349-3.338h-0.876v6.686h0.876c1.858 0 3.349-1.498 3.349-3.348v0z";break;case"sign":e[0]="0 0 20 20",e[1]="M10.954 10.307c0.378 0.302 0.569 1.202 0.564 1.193 0.697 0.221 1.136 0.682 1.136 0.682 1.070-0.596 1.094-0.326 1.558-0.682 0.383-0.263 0.366-0.344 0.567-1.048 0.187-0.572-0.476-0.518-1.021-1.558-0.95 0.358-1.463 0.196-1.784 0.167-0.145-0.020-0.12 0.562-1.021 1.247zM14.409 17.196c-0.133 0.182-0.196 0.218-0.363 0.454-0.28 0.361 0.076 0.906 0.253 0.82 0.206-0.076 0.341-0.488 0.567-0.623 0.115-0.061 0.422-0.513 0.709-0.82 0.211-0.238 0.363-0.344 0.564-0.594 0.341-0.422 0.412-0.744 0.709-1.193 0.184-0.236 0.312-0.307 0.481-0.594 0.886-1.679 0.628-2.432 1.475-3.629 0.26-0.353 0.552-0.442 0.964-0.653 0.383-2.793-0.888-4.356-0.879-4.361-1.067 0.623-1.644 0.879-2.751 0.82-0.417-0.005-0.636-0.182-1.048-0.145-0.385 0.015-0.582 0.159-0.964 0.29-0.589 0.182-0.91 0.344-1.529 0.535-0.393 0.11-0.643 0.115-1.050 0.255-0.348 0.147-0.182 0.029-0.427 0.312-0.317 0.348-0.238 0.623-0.535 1.222-0.371 0.785-0.326 0.891-0.115 0.987-0.14 0.402-0.174 0.672-0.14 1.107 0.039 0.331-0.101 0.562 0.255 0.825 0.483 0.361 1.499 1.205 1.757 1.217 0.39-0.012 1.521 0.029 2.096-0.368 0.13-0.081 0.167-0.162 0.056 0.145-0.022 0.037-1.433 1.136-1.585 1.131-1.794 0.056-1.193 0.157-1.303 0.115-0.091 0-0.955-1.055-1.477-0.682-0.196 0.12-0.287 0.236-0.363 0.452 0.066 0.137 0.383 0.358 0.675 0.54 0.422 0.27 0.461 0.552 0.881 0.653 0.513 0.115 1.060 0.039 1.387 0.081 0.125 0.034 1.256-0.297 1.961-0.675 0.65-0.336-0.898 0.648-1.276 1.131-1.141 0.358-0.82 0.373-1.362 0.483-0.503 0.115-0.479 0.086-0.822 0.196-0.356 0.086-0.648 0.572-0.312 0.825 0.201 0.167 0.827-0.066 1.445-0.086 0.275-0.005 1.391-0.518 1.644-0.653 0.633-0.339 1.099-0.81 1.472-1.077 0.518-0.361-0.584 0.991-1.050 1.558zM8.855 9.799c-0.378-0.312-0.569-1.212-0.564-1.217-0.697-0.206-1.136-0.667-1.136-0.653-1.070 0.582-1.099 0.312-1.558 0.653-0.388 0.277-0.366 0.363-0.567 1.045-0.187 0.594 0.471 0.535 1.021 1.561 0.95-0.344 1.463-0.182 1.784-0.142 0.145 0.010 0.12-0.572 1.021-1.247zM5.4 2.911c0.133-0.191 0.196-0.228 0.368-0.454 0.27-0.371-0.081-0.915-0.253-0.849-0.211 0.096-0.346 0.508-0.599 0.653-0.093 0.052-0.4 0.503-0.682 0.82-0.211 0.228-0.363 0.334-0.564 0.599-0.346 0.407-0.412 0.729-0.709 1.161-0.184 0.258-0.317 0.324-0.481 0.621-0.886 1.669-0.631 2.422-1.475 3.6-0.26 0.38-0.552 0.461-0.964 0.682-0.383 2.788 0.883 4.346 0.879 4.336 1.068-0.609 1.639-0.861 2.751-0.825 0.417 0.025 0.636 0.201 1.048 0.174 0.385-0.025 0.582-0.169 0.964-0.285 0.589-0.196 0.91-0.358 1.499-0.54 0.422-0.12 0.672-0.125 1.080-0.285 0.348-0.128 0.182-0.010 0.427-0.282 0.312-0.358 0.238-0.633 0.508-1.217 0.398-0.8 0.353-0.906 0.142-0.991 0.135-0.412 0.174-0.677 0.14-1.107-0.044-0.336 0.101-0.572-0.255-0.82-0.483-0.375-1.499-1.22-1.752-1.222-0.395 0.002-1.526-0.039-2.101 0.339-0.13 0.101-0.167 0.182-0.056-0.11 0.022-0.052 1.433-1.148 1.585-1.163 1.794-0.039 1.193-0.14 1.303-0.088 0.091-0.007 0.955 1.045 1.477 0.682 0.191-0.13 0.287-0.245 0.368-0.452-0.071-0.147-0.388-0.368-0.68-0.537-0.422-0.282-0.464-0.564-0.881-0.655-0.513-0.125-1.065-0.049-1.387-0.11-0.125-0.015-1.256 0.317-1.956 0.68-0.66 0.351 0.893-0.631 1.276-1.136 1.136-0.339 0.81-0.353 1.36-0.479 0.501-0.101 0.476-0.071 0.82-0.172 0.351-0.096 0.648-0.577 0.312-0.849-0.206-0.152-0.827 0.081-1.44 0.086-0.28 0.020-1.396 0.533-1.649 0.677-0.633 0.329-1.099 0.8-1.472 1.048-0.523 0.38 0.584-0.967 1.050-1.529z";break;case"mute":case"volume-mute":e[0]="0 0 20 20",e[1]="M7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714zM18.7
169345 12.093v1.657h-1.657l-2.093-2.093-2.093 2.093h-1.657v-1.657l2.093-2.093-2.093-2.093v-1.657h1.657l2.093 2.093 2.093-2.093h1.657v1.657l-2.093 2.093z";break;case"volume-soft":e[0]="0 0 20 20",e[1]="M10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z";break;case"volume-medium":e[0]="0 0 20 20",e[1]="M14.053 16.241c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 2.559-2.559 2.559-6.722 0-9.281-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c1.594 1.594 2.471 3.712 2.471 5.966s-0.878 4.373-2.471 5.966c-0.183 0.183-0.423 0.275-0.663 0.275zM10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z";break;case"volume-loud":e[0]="0 0 21 20",e[1]="M17.384 18.009c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.712-1.712 2.654-3.988 2.654-6.408s-0.943-4.696-2.654-6.408c-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.066 2.066 3.204 4.813 3.204 7.734s-1.138 5.668-3.204 7.734c-0.183 0.183-0.423 0.275-0.663 0.275zM14.053 16.241c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 2.559-2.559 2.559-6.722 0-9.281-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c1.594 1.594 2.471 3.712 2.471 5.966s-0.878 4.373-2.471 5.966c-0.183 0.183-0.423 0.275-0.663 0.275zM10.723 14.473c-0.24 0-0.48-0.092-0.663-0.275-0.366-0.366-0.366-0.96 0-1.326 1.584-1.584 1.584-4.161 0-5.745-0.366-0.366-0.366-0.96 0-1.326s0.96-0.366 1.326 0c2.315 2.315 2.315 6.082 0 8.397-0.183 0.183-0.423 0.275-0.663 0.275zM7.839 1.536c0.501-0.501 0.911-0.331 0.911 0.378v16.172c0 0.709-0.41 0.879-0.911 0.378l-4.714-4.713h-3.125v-7.5h3.125l4.714-4.714z";break;case"chapters":e[0]="0 0 20 20",e[1]="M5 2.5v17.5l6.25-6.25 6.25 6.25v-1
169347.5zM15 0h-12.5v17.5l1.25-1.25v-15h11.25z";break;case"preferences":e[0]="0 0 20 20",e[1]="M18.238 11.919c-1.049-1.817-0.418-4.147 1.409-5.205l-1.965-3.404c-0.562 0.329-1.214 0.518-1.911 0.518-2.1 0-3.803-1.714-3.803-3.828h-3.931c0.005 0.653-0.158 1.314-0.507 1.919-1.049 1.818-3.382 2.436-5.212 1.382l-1.965 3.404c0.566 0.322 1.056 0.793 1.404 1.396 1.048 1.815 0.42 4.139-1.401 5.2l1.965 3.404c0.56-0.326 1.209-0.513 1.902-0.513 2.094 0 3.792 1.703 3.803 3.808h3.931c-0.002-0.646 0.162-1.3 0.507-1.899 1.048-1.815 3.375-2.433 5.203-1.387l1.965-3.404c-0.562-0.322-1.049-0.791-1.395-1.391zM10 14.049c-2.236 0-4.050-1.813-4.050-4.049s1.813-4.049 4.050-4.049 4.049 1.813 4.049 4.049c-0 2.237-1.813 4.049-4.049 4.049z";break;case"close":e[0]="0 0 16 20",e[1]="M1.228 14.933q0-0.446 0.312-0.759l3.281-3.281-3.281-3.281q-0.313-0.313-0.313-0.759t0.313-0.759l1.518-1.518q0.313-0.313 0.759-0.313t0.759 0.313l3.281 3.281 3.281-3.281q0.313-0.313 0.759-0.313t0.759 0.313l1.518 1.518q0.313 0.313 0.313 0.759t-0.313 0.759l-3.281 3.281 3.281 3.281q0.313 0.313 0.313 0.759t-0.313 0.759l-1.518 1.518q-0.313 0.313-0.759 0.313t-0.759-0.313l-3.281-3.281-3.281 3.281q-0.313 0.313-0.759 0.313t-0.759-0.313l-1.518-1.518q-0.313-0.313-0.313-0.759z";break;case"fullscreen-expand":e[0]="0 0 20 20",e[1]="M0 18.036v-5q0-0.29 0.212-0.502t0.502-0.212 0.502 0.212l1.607 1.607 3.705-3.705q0.112-0.112 0.257-0.112t0.257 0.112l1.272 1.272q0.112 0.112 0.112 0.257t-0.112 0.257l-3.705 3.705 1.607 1.607q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-5q-0.29 0-0.502-0.212t-0.212-0.502zM8.717 8.393q0-0.145 0.112-0.257l3.705-3.705-1.607-1.607q-0.212-0.212-0.212-0.502t0.212-0.502 0.502-0.212h5q0.29 0 0.502 0.212t0.212 0.502v5q0 0.29-0.212 0.502t-0.502 0.212-0.502-0.212l-1.607-1.607-3.705 3.705q-0.112 0.112-0.257 0.112t-0.257-0.112l-1.272-1.272q-0.112-0.112-0.112-0.257z";break;case"fullscreen-collapse":e[0]="0 0 20 20",e[1]="M0.145 16.964q0-0.145 0.112-0.257l3.705-3.705-1.607-1.607q-0.212-0.212-0.212-0.502t0.212-0.502 0.502-0.212h5q0.29 0 0.502 0.212t0.212 0.502v5q0 0.29-0.212 0.502t-0.502 0.212-0.502-0.212l-1.607-1.607-3.705 3.705q-0.112 0.112-0.257 0.112t-0.257-0.112l-1.272-1.272q-0.112-0.112-0.112-0.257zM8.571 9.464v-5q0-0.29 0.212-0.502t0.502-0.212 0.502 0.212l1.607 1.607 3.705-3.705q0.112-0.112 0.257-0.112t0.257 0.112l1.272 1.272q0.112 0.112 0.112 0.257t-0.112 0.257l-3.705 3.705 1.607 1.607q0.212 0.212 0.212 0.502t-0.212 0.502-0.502 0.212h-5q-0.29 0-0.502-0.212t-0.212-0.502z";break;case"help":e[0]="0 0 11 20",e[1]="M0.577 6.317q-0.028-0.167 0.061-0.313 1.786-2.969 5.179-2.969 0.893 0 1.797 0.346t1.629 0.926 1.183 1.423 0.458 1.769q0 0.603-0.173 1.127t-0.391 0.854-0.614 0.664-0.642 0.485-0.681 0.396q-0.458 0.257-0.765 0.725t-0.307 0.748q0 0.19-0.134 0.363t-0.313 0.173h-2.679q-0.167 0-0.285-0.206t-0.117-0.419v-0.502q0-0.926 0.725-1.747t1.596-1.211q0.658-0.301 0.938-0.625t0.279-0.848q0-0.469-0.519-0.826t-1.2-0.357q-0.725 0-1.205 0.324-0.391 0.279-1.194 1.283-0.145 0.179-0.346 0.179-0.134 0-0.279-0.089l-1.83-1.395q-0.145-0.112-0.173-0.279zM3.786 16.875v-2.679q0-0.179 0.134-0.313t0.313-0.134h2.679q0.179 0 0.313 0.134t0.134 0.313v2.679q0 0.179-0.134 0.313t-0.313 0.134h-2.679q-0.179 0-0.313-0.134t-0.134-0.313z"}return e},AblePlayer.prototype.reinitialize=function(){var e,i,s;if(i=(e=new t.Deferred).promise(),s=this,window.console||(this.debug=!1),this.startedPlaying=!1,this.autoScrollTranscript=!0,this.$media=t(this.media).first(),this.media=this.$media[0],this.$media.is("audio"))this.mediaType="audio";else{if(!this.$media.is("video"))return this.provideFallback(),e.fail(),i;this.mediaType="video"}return this.$sources=this.$media.find("source"),this.player=this.getPlayer(),this.player||this.provideFallback(),this.setIconType(),e.resolve(),i},AblePlayer.prototype.setPlayerSize=function(t,e){this.$media.attr("id"),"audio"===this.mediaType?this.playerWidth&&this.$ableWrapper.css("width",this.playerWidth+"px"):t>0&&e>0&&(this.playerWidth=t,this.playerHeight=e,this.aspectRatio=e/t)},AblePlayer.prototype.setIconType=function(){var e,i,s;if(this.forceIconType)return!1;document.createElementNS&&document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect?this.iconType="svg":(window.getComputedStyle?(t("span.icon-play").length?i=t("span.icon-play"):(e=t("<span>",{class:"icon-play able-clipped"}),t("body").append(e),i=e),void 0!==(s=window.getComputedStyle(i.get(0),null).getPropertyValue("font-family"))&&-1!==s.indexOf("able")?this.iconType="font":this.iconType="image"):this.iconType="image",void 0!==e&&e.remove())},AblePlayer.prototype.setupInstance=function(){var e=new t.Deferred,i=e.promise();return this.$media.attr("id")?this.mediaId=this.$media.attr("id"):(this.mediaId="ableMediaId_"+this.ableIndex,this.$media.attr("id",this.mediaId)),e.resolve(),i},AblePlayer.prototype.setupInstancePlaylist=function(){var e=this;if(this.hasPlaylist=!1,t(".able-playlist").each(function(){if(t(this).data("player")===e.mediaId){e.hasPlaylist=!0,e.$playlist=t
16934(this).find("li"),t(this).find("li[data-youtube-id]").each(function(){var i=t(this).attr("data-youtube-id"),s=e.getYouTubePosterUrl(i,"120"),a=t("<img>",{src:s,alt:""});t(this).find("button").prepend(a)}),t(this).find("li[data-vimeo-id]").each(function(){var i=t(this).attr("data-youtube-id"),s=e.getVimeoPosterUrl(i,"120"),a=t("<img>",{src:s,alt:""});t(this).find("button").prepend(a)}),t(this).find("li span").attr("aria-hidden","true"),e.playlistIndex=0;var i=t(this).data("embedded");void 0!==i&&!1!==i?e.playlistEmbed=!0:e.playlistEmbed=!1}}),this.hasPlaylist&&this.loop&&this.media.removeAttribute("loop"),this.hasPlaylist&&this.playlistEmbed){var i=this.$playlist.parent();this.$playlistDom=i.clone(),i.remove()}this.hasPlaylist&&0===this.$sources.length&&(this.cuePlaylistItem(0),this.$sources=this.$media.find("source"))},AblePlayer.prototype.recreatePlayer=function(){var e,i,s,a,n;if(!this.player){console.log("Can't create player; no appropriate player type detected.");return}return i=(e=new t.Deferred).promise(),s=this,this.playerDeleted=!1,this.recreatingPlayer=!0,this.playerCreated||(this.loadCurrentPreferences(),this.injectPlayerCode(),this.resizePlayer(this.media.videoWidth,this.media.videoHeight)),this.get
16934SampleDescriptionText(),this.initSignLanguage(),this.initPlayer().then(function(){s.getTracks().then(function(){s.initDescription().then(function(){s.setupTracks().then(function(){s.hasClosedDesc&&(!s.$descDiv||s.$descDiv&&!t.contains(s.$ableDiv[0],s.$descDiv[0]))&&s.injectTextDescriptionArea(),s.initSpeech("init"),s.setupTranscript().then(function(){s.initStenoFrame().then(function(){s.stenoMode&&s.$stenoFrame&&(s.stenoFrameContents=s.$stenoFrame.contents()),s.getMediaTimes().then(function(t){for(s.duration=t.duration,s.elapsed=t.elapsed,s.setFullscreen(!1),void 0===s.volume&&(s.volume=s.defaultVolume),s.volume&&s.setVolume(s.volume),s.transcriptType&&(s.addTranscriptAreaEvents(),s.updateTranscript()),s.captions.length&&s.initDefaultCaption(),s.setMediaAttributes(),s.addControls(),s.addEventListeners(),a=s.getPreferencesGroups(),n=0;n<a.length;n++)s.injectPrefsForm(a[n]);s.setupPopups(),s.updateCaption(),s.injectVTS(),s.chaptersDivLocation&&s.populateChaptersDiv(),s.showSearchResults(),"html5"!==s.player||s.loadingMedia||(s.$media[0].load(),s.loadingMedia=!0),setTimeout(function(){s.refreshControls("init"),e.resolve()},100)})})})})})})},function(){s.provideFallback()}),i},AblePlayer.prototype.initPlayer=function(){var e,i=this;"html5"===this.player?e=this.initHtml5Player():"youtube"===this.player?e=this.initYouTubePlayer():"vimeo"===this.player&&(e=this.initVimeoPlayer());var s=new t.Deferred,a=s.promise();return e.done(function(){i.useFixedSeekInterval?(i.seekInterval||(i.seekInterval=i.defaultSeekInterval),i.seekIntervalCalculated=!0):i.setSeekInterval(),s.resolve()}).fail(function(){s.reject()}),a},AblePlayer.prototype.initStenoFrame=function(){var e,i,s;return e=this,s=(i=new t.Deferred).promise(),this.stenoMode&&this.$stenoFrame?(this.$stenoFrame[0].contentWindow,"complete"==document.readyState)?i.resolve():this.$stenoFrame.on("load",function(){i.resolve()}):i.resolve(),s},AblePlayer.prototype.setSeekInterval=function(){var t,e;if(t=this,this.seekInterval=this.defaultSeekInterval,void 0===(e=this.useChapterTimes?this.chapterDuration:this.duration)||e<1){this.seekIntervalCalculated=!1;return}e<=20?this.seekInterval=5:e<=30?this.seekInterval=6:e<=40?this.seekInterval=8:e<=100?this.seekInterval=10:this.seekInterval=e/10,this.seekIntervalCalculated=!0},AblePlayer.prototype.initDefaultCaption=function(){var t,e;if((t=this.captions).length>0){for(e=0;e<t.length;e++)!0===t[e].def&&(this.captionLang=t[e].language,this.selectedCaptions=t[e]);if(void 0===this.captionLang)for(e=0;e<t.length;e++)t[e].language===this.lang&&(this.captionLang=t[e].language,this.selectedCaptions=t[e]);void 0===this.captionLang&&(this.captionLang=t[0].language,this.selectedCaptions=t[0]),void 0!==this.captionLang&&(this.$transcriptLanguageSelect&&this.$transcriptLanguageSelect.find("option[lang="+this.captionLang+"]").prop("selected",!0),this.syncTrackLanguages("init",this.captionLang)),"vimeo"===this.player&&(this.usingVimeoCaptions&&1==this.prefCaptions?this.vimeoPlayer.enableTextTrack(this.captionLang).then(function(t){}).catch(function(t){switch(t.name){case"InvalidTrackLanguageError":case"InvalidTrackError":console.log("No "+track.kind+" track is available in the specified language ("+track.label+")");break;default:console.log("Error loading "+track.label+" "+track.kind+" track")}}):this.vimeoPlayer.disableTextTrack().then(function(){}).catch(function(t){console.log("Error disabling Vimeo text track: ",t)}))}},AblePlayer.prototype.initHtml5Player=function(){var e=new t.Deferred,i=e.promise();return e.resolve(),i},AblePlayer.prototype.setMediaAttributes=function(){this.$media.attr("tabindex",-1);var t=this.$media.get(0).textTracks;if(t)for(var e=0;e<t.length;)t[e].mode="disabled",e+=1},AblePlayer.prototype.getPlayer=function(){return this.testFallback?null:this.youTubeId?"video"!==this.mediaType?null:"youtube":this.vimeoId?"video"!==this.mediaType?null:"vimeo":this.media.canPlayType?"html5":null}}(jQuery),function(t){AblePlayer.prototype.setCookie=function(t){Cookies.set("Able-Player",JSON.stringify(t),{expires:90,sameSite:"strict"})},AblePlayer.prototype.getCookie=function(){var t,e={preferences:{},sign:{},transcript:{},voices:[]};
16934try{t=JSON.parse(Cookies.get("Able-Player"))}catch(i){this.setCookie(e),t=e}return t||e},AblePlayer.prototype.updateCookie=function(t){var e,i,s,a,n,r,o,h;if(e=this.getCookie(),"transcript"===t||"sign"===t)"transcript"===t?(s=(i=this.$transcriptArea).position(),void 0===e.transcript&&(e.transcript={}),e.transcript.position=i.css("position"),e.transcript.zindex=i.css("z-index"),e.transcript.top=s.top,e.transcript.left=s.left,e.transcript.width=i.width(),e.transcript.height=i.height()):"sign"===t&&(s=(i=this.$signWindow).position(),void 0===e.sign&&(e.sign={}),e.sign.position=i.css("position"),e.sign.zindex=i.css("z-index"),e.sign.top=s.top,e.sign.left=s.left,e.sign.width=i.width(),e.sign.height=i.height());else if("voice"===t){void 0===e.voices&&(e.voices=[]),o=!1;for(var l=0;l<e.voices.length;l++)e.voices[l].lang===this.prefDescVoiceLang&&(o=!0,e.voices[l].name=this.prefDescVoice);o||(h={name:this.prefDescVoice,lang:this.prefDescVoiceLang},e.voices.push(h))}else for(n=0,a=this.getAvailablePreferences();n<a.length;n++)(r=a[n].name)==t&&(e.preferences[r]=this[r]);this.setCookie(e)},AblePlayer.prototype.getPreferencesGroups=function(){return this.usingYouTubeCaptions?["captions","descriptions","keyboard"]:this.usingVimeoCaptions?["descriptions","keyboard"]:["captions","descriptions","keyboard","transcript"]},AblePlayer.prototype.getAvailablePreferences=function(){var t=[];return t.push({name:"prefAltKey",label:this.tt.prefAltKey,group:"keyboard",default:1}),t.push({name:"prefCtrlKey",label:this.tt.prefCtrlKey,group:"keyboard",default:1}),t.push({name:"prefShiftKey",label:this.tt.prefShiftKey,group:"keyboard",default:0}),t.push({name:"prefTranscript",label:null,group:"transcript",default:0}),t.push({name:"prefHighlight",label:this.tt.prefHighlight,group:"transcript",default:1}),t.push({name:"prefAutoScrollTranscript",label:null,group:"transcript",default:1}),t.push({name:"prefTabbable",label:this.tt.prefTabbable,group:"transcript",default:0}),t.push({name:"prefCaptions",label:null,group:"captions",default:this.defaultStateCaptions}),this.usingYouTubeCaptions||("video"===this.mediaType&&t.push({name:"prefCaptionsPosition",label:this.tt.prefCaptionsPosition,group:"captions",default:this.defaultCaptionsPosition}),t.push({name:"prefCaptionsFont",label:this.tt.prefCaptionsFont,group:"captions",default:"sans-serif"})),t.push({name:"prefCaptionsSize",label:this.tt.prefCaptionsSize,group:"captions",default:"100%"}),this.usingYouTubeCaptions||(t.push({name:"prefCaptionsColor",label:this.tt.prefCaptionsColor,group:"captions",default:"white"}),t.push({name:"prefCaptionsBGColor",label:this.tt.prefCaptionsBGColor,group:"captions",default:"black"}),t.push({name:"prefCaptionsOpacity",label:this.tt.prefCaptionsOpacity,group:"captions",default:"100%"})),"video"===this.mediaType&&(t.push({name:"prefDesc",label:null,group:"descriptions",default:this.defaultStateDescriptions}),t.push({name:"prefDescMethod",label:null,group:"descriptions",default:"video"}),t.push({name:"prefDescVoice",label:this.tt.prefDescVoice,group:"descriptions",default:null}),t.push({name:"prefDescPitch",label:this.tt.prefDescPitch,group:"descriptions",default:1}),t.push({name:"prefDescRate",label:this.tt.prefDescRate,group:"descriptions",default:1}),t.push({name:"prefDescVolume",label:this.tt.volume,group:"descriptions",default:1}),t.push({name:"prefDescPause",label:this.tt.prefDescPause,group:"descriptions",default:this.defaultDescPause}),t.push({name:"prefDescVisible",label:this.tt.prefDescVisible,group:"descriptions",default:0})),t.push({name:"prefSign",label:null,group:null,default:0}),t},AblePlayer.prototype.loadCurrentPreferences=function(){for(var t=this.getAvailablePreferences(),e=this.getCookie(),i=0;i<t.length;i++){var s=t[i].name,a=t[i].default;void 0!==e.preferences[s]?this[s]=e.preferences[s]:(e.preferences[s]=a,this[s]=a)}void 0!==e.voices&&(this.prefVoices=e.voices),this.setCookie(e)},AblePlayer.prototype.injectPrefsForm=function(e){var i,s,a,n,r,o,h,l,p,c,u,d,f,g,_,v,m,y,b,k,C,T,w,P,D,x,S,B,I,N,A,M,V,F,L,z,O,E,R,W,H;if(i=this,s=this.getAvailablePreferences(),(a=t("<div>",{class:"able-prefs-form "})).addClass("able-prefs-form-"+e),"captions"==e)n=this.tt.prefTitleCaptions;else if("descriptions"==e){n=this.tt.prefTitleDescriptions;var o=t("<p>",{text:this.tt.prefIntroDescription1}),q=t("<ul>"),K=t("<li>",{text:this.tt.prefDescFormatOption1}),j=t("<li>",{text:this.tt.prefDescFormatOption2});q.append(K,j),this.hasOpenDesc&&this.hasClosedDesc?(A=this.tt.prefIntroDescription2+" ",A+="<strong>"+this.tt.prefDescFormatOption1b+"</strong>",A+=" <em>"+this.tt.and+"</em> <strong>"+this.tt.prefDescFormatOption2b+"</strong>."):this.hasOpenDesc?(A=this.tt.prefIntroDescription2,A+=" <strong>"+this.tt.prefDescFormatOption1b+"</strong>."):this.hasClosedDesc?(A=this.tt.prefIntroDescription2,A+=" <strong>"+this.tt.prefDescFormatOption2b+"</strong>."):A=this.tt.prefIntroDescriptionNone,h=t("<p>",{html:A}),l=this.tt.prefIntroDescription3,(this.hasOpenDesc||this.hasClosedDesc)&&(l+=" "+this.tt.prefIntroDescription4),p=t("<p>",{text:l}),a.append(o,q,h,p)}else"keyboard"==e?(n=this.tt.prefTitleKeyboard,r=this.tt.prefIntroKeyboard1,r+=" "+this.tt.prefIntroKeyboard2,r+=" "+this.tt.prefIntroKeyboard3,o=t("<p>",{text:r}),a.append(o)):"transcript"==e&&(n=this.tt.prefTitleTranscript);for(d=t("<div>").attr("role","group"),f="able-prefs-"+e,v=(g=this.mediaId+"-prefs-"+e)+"-legend",d.addClass(f).attr("id",g),"keyboard"===e?((_=t("<h2>"+this.tt.prefHeadingKeyboard1+"</h2>")).attr("id",v),d.attr("aria-labelledby",v),d.append(_)):"descriptions"===e&&((_=t("<h2>"+this.tt.prefHeadingTextDescription+"</h2>")).attr("id",v),d.attr("aria-labelledby",v),d.append(_)),c=0;c<s.length;c++)if(s[c].group==e&&s[c].label){if(b="able-"+(m=s[c].name),k=this.mediaId+"_"+m,y=t("<div>").addClass(b),"captions"===e){for(C=t('<label for="'+k+'"> '+s[c].label+"</label>"),T=t("<select>",{name:m,id:k}),"prefCaptions"!==m&&"prefCaptionsStyle"!==m&&T.change(function(){B=t(this).attr("name"),i.stylizeCaptions(i.$sampleCapsDiv,B)}),w=this.getCaptionsOptions(m),u=0;u<w.length;u++)"prefCaptionsPosition"===m?"overlay"===(D=w[u])?S=this.tt.captionsPositionOverlay:"below"===D&&(D=w[u],S=this.tt.captionsPositionBelow):"prefCaptionsFont"===m||"prefCaptionsColor"===m||"prefCaptionsBGColor"===m?(D=w[u][0],S=w[u][1]):"prefCaptionsOpacity"===m?(D=w[u],S=w[u],"0%"===D?S+=" ("+this.tt.transparent+")":"100%"===D&&(S+=" ("+this.tt.s
16934olid+")")):(D=w[u],S=w[u]),P=t("<option>",{value:D,text:S}),this[m]===D&&P.prop("selected",!0),T.append(P);y.append(C,T)}else if("descriptions"===e){if(C=t('<label for="'+k+'"> '+s[c].label+"</label>"),"prefDescPause"===m||"prefDescVisible"===m)y.addClass("able-prefs-checkbox"),T=t("<input>",{type:"checkbox",name:m,id:k,value:"true"}),1===this[m]&&T.prop("checked",!0),y.append(T,C);else if(this.synth){if(y.addClass("able-prefs-select"),T=t("<select>",{name:m,id:k}),"prefDescVoice"===m&&this.descVoices){for(u=0,M=this.getPrefDescVoice();u<this.descVoices.length;u++)D=this.descVoices[u].name,x=this.descVoices[u].lang.substring(0,2).toLowerCase(),S=D+" ("+this.descVoices[u].lang+")",P=t("<option>",{value:D,"data-lang":x,text:S}),M===D&&P.prop("selected",!0),T.append(P);this.$voiceSelectField=T}else if("prefDescPitch"==m?w=[0,.5,1,1.5,2]:"prefDescRate"==m?w=[.7,.8,.9,1,1.1,1.2,1.5,2,2.5,3]:"prefDescVolume"==m&&(w=[.1,.2,.3,.4,.5,.6,.7,.8,.9,1]),void 0!==w)for(u=0;u<w.length;u++)D=w[u],S=this.makePrefsValueReadable(m,D),P=t("<option>",{value:D,text:S}),this[m]==D&&P.prop("selected",!0),T.append(P),y.append(C,T);T.on("change",function(){i.announceDescriptionText("sample",i.currentSampleText)}),y.append(C,T)}}else C=t('<label for="'+k+'"> '+s[c].label+"</label>"),T=t("<input>",{type:"checkbox",name:m,id:k,value:"true"}),1===this[m]&&T.prop("checked",!0),"keyboard"===e&&T.on("change",function(){"prefAltKey"===(B=t(this).attr("name"))?(I=".able-modkey-alt",N=i.tt.prefAltKey+" + "):"prefCtrlKey"===B?(I=".able-modkey-ctrl",N=i.tt.prefCtrlKey+" + "):"prefShiftKey"===B&&(I=".able-modkey-shift",N=i.tt.prefShiftKey+" + "),t(this).is(":checked")?t(I).text(N):t(I).text("")}),y.append(T,C);d.append(y)}if(a.append(d),"captions"===e)this.usingYouTubeCaptions||(this.$sampleCapsDiv=t("<div>",{class:"able-captions-sample"}).text(this.tt.sampleCaptionText),a.append(this.$sampleCapsDiv),this.stylizeCaptions(this.$sampleCapsDiv));else if("descriptions"===e)this.synth&&(this.$sampleDescDiv=t("<div>",{class:"able-desc-sample"}).text(this.tt.sampleDescriptionText),a.append(this.$sampleDescDiv),this.currentSampleText=this.tt.sampleDescriptionText);else if("keyboard"===e){for(c=0,V=t("<h2>",{text:this.tt.prefHeadingKeyboard2}),F=t("<ul>"),L=[],z=[];c<this.controls.length;c++)"play"===this.controls[c]?(L.push(this.tt.play+"/"+this.tt.pause),z.push("p</span> <em>"+this.tt.or+'</em> <span class="able-help-modifiers"> '+this.tt.spacebar)):"restart"===this.controls[c]?(L.push(this.tt.restart),z.push("s")):"previous"===this.controls[c]?(L.push(this.tt.prevTrack),z.push("b")):"next"===this.controls[c]?(L.push(this.tt.nextTrack),z.push("n")):"rewind"===this.controls[c]?(L.push(this.tt.rewind),z.push("r")):"forward"===this.controls[c]?(L.push(this.tt.forward),z.push("f")):"volume"===this.controls[c]?(L.push(this.tt.volume),z.push("v</span> <em>"+this.tt.or+'</em> <span class="able-modkey">1-9'),L.push(this.tt.mute+"/"+this.tt.unmute),z.push("m")):"captions"===this.controls[c]?(this.captions.length>1?L.push(this.tt.captions):this.captionsOn?L.push(this.tt.hideCaptions):L.push(this.tt.showCaptions),z.push("c")):"descriptions"===this.controls[c]?(this.descOn?L.push(this.tt.turnOffDescriptions):L.push(this.tt.turnOnDescriptions),z.push("d")):"prefs"===this.controls[c]?(L.push(this.tt.preferences),z.push("e")):"help"===this.controls[c]&&(L.push(this.tt.help),z.push("h"));for(c=0;c<z.length;c++)O='<span class="able-modkey-alt">',1===this.prefAltKey&&(O+=this.tt.prefAltKey+" + "),O+="</span>",O+='<span class="able-modkey-ctrl">',1===this.prefCtrlKey&&(O+=this.tt.prefCtrlKey+" + "),O+="</span>",O+='<span class="able-modkey-shift">',1===this.prefShiftKey&&(O+=this.tt.prefShiftKey+" + "),O+="</span>",O+='<span class="able-modkey">'+z[c]+"</span>",O+=" = "+L[c],E=t("<li>",{html:O}),F.append(E);O='<span class="able-modkey">'+this.tt.escapeKey+"</span>",O+=" = "+this.tt.escapeKeyFunction,E=t("<li>",{html:O}),F.append(E),a.append(V,F)}t("body").append(a),R=new AccessibleDialog(a,this.$prefsButton,"dialog",!0,n,o,i.tt.closeButtonLabel,"32em"),a.append("<hr>"),W=t('<button class="modal-button">'+this.tt.save+"</button>"),H=t('<button class="modal-button">'+this.tt.cancel+"</button>"),W.click(function(){R.hide(),i.savePrefsFromForm()}),H.click(function(){R.hide(),i.resetPrefsForm()}),a.append(W),a.append(H),("captions"===e||"transcript"===e)&&d.attr("aria-labelledby",R.titleH1.attr("id")),"captions"===e?this.captionPrefsDialog=R:"descriptions"===e?this.descPrefsDialog=R:"keyboard"===e?this.keyboardPrefsDialog=R:"transcript"===e&&(this.transcriptPrefsDialog=R),t("div.able-prefs-form button.modalCloseButton").click(function(){i.resetPrefsForm()}),t("div.able-prefs-form").keydown(function(t){27===t.which&&i.resetPrefsForm()})},AblePlayer.prototype.getPrefDescVoice=function(){var t,e,i;if(t=this.selectedDescriptions?this.selectedDescriptions.language:this.captionLang?this.captionLang:this.lang,(e=this.getCookie()).voices){for(i=0;i<e.voices.length;i++)if(e.voices[i].lang===t)return e.voices[i].name}return null},AblePlayer.prototype.rebuildDescPrefsForm=function(){var e,i,s,a;for(this.$voiceSelectField=t("#"+this.mediaId+"_prefDescVoice"),this.$voiceSelectField.empty(),e=0;e<this.descVoices.length;e++)s=(i=this.descVoices[e].name)+" ("+this.descVoices[e].lang+")",a=t("<option>",{value:i,"data-lang":this.descVoices[e].lang.substring(0,2).toLowerCase(),text:s}),this.prefDescVoice==i&&a.prop("selected",!0),this.$voiceSelectField.append(a)},AblePlayer.prototype.makePrefsValueReadable=function(t,e){if("prefDescPitch"===t){if(0===e)return this.tt.prefDescPitch1;
16934if(.5===e)return this.tt.prefDescPitch2;if(1===e)return this.tt.prefDescPitch3;else if(1.5===e)return this.tt.prefDescPitch4;else if(2===e)return this.tt.prefDescPitch5}else if("prefDescRate"===t){if(.7===e)return 1;if(.8===e)return 2;if(.9===e)return 3;else if(1===e)return 4;else if(1.1===e)return 5;else if(1.2===e)return 6;else if(1.5===e)return 7;else if(2===e)return 8;else if(2.5===e)return 9;else if(3===e)return 10}else if("prefDescVolume"===t)return 10*e;return e},AblePlayer.prototype.resetPrefsForm=function(){var e,i,s,a,n,r;for(a=0,e=this,i=this.getCookie(),s=this.getAvailablePreferences();a<s.length;a++)n=s[a].name,r=this.mediaId+"_"+n,-1!==n.indexOf("Captions")&&"prefCaptions"!==n?t('select[name="'+n+'"]').val(i.preferences[n]):1===this[n]?t('input[name="'+n+'"]').prop("checked",!0):t('input[name="'+n+'"]').prop("checked",!1);this.stylizeCaptions(this.$sampleCapsDiv)},AblePlayer.prototype.savePrefsFromForm=function(){var e,i,s,a,n,r,o,h,l,p,c,u;o=0,l=0,p=!1,e=this.getCookie(),i=this.getAvailablePreferences();for(var d=0;d<i.length;d++)if(i[d].label){if(s=i[d].name,a=this.mediaId+"_"+s,"prefDescVoice"===s){void 0===e.voices&&(e.voices=[]),n=this.mediaId+"_prefDescVoice",this.prefDescVoice=t("select#"+n).find(":selected").val(),this.prefDescVoiceLang=t("select#"+n).find(":selected").attr("data-lang"),h=!1;for(var f=0;f<e.voices.length;f++)e.voices[f].lang===this.prefDescVoiceLang&&(h=!0,e.voices[f].name=this.prefDescVoice);h||(r={name:this.prefDescVoice,lang:this.prefDescVoiceLang},e.voices.push(r)),o++}else"prefDescMethod"==s?(this.prefDescMethod="video",this.prefDescMethod!==e.preferences.prefDescMethod&&(e.preferences.prefDescMethod=this.prefDescMethod,o++)):-1!==s.indexOf("Captions")&&"prefCaptions"!==s?(u=t('select[id="'+a+'"]').val(),e.preferences[s]!==u&&(e.preferences[s]=u,this[s]=u,o++,l++),"prefCaptionsSize"===s&&(p=!0,c=u)):-1!==s.indexOf("Desc")&&"prefDescPause"!==s&&"prefDescVisible"!==s?(u=t('select[id="'+a+'"]').val(),e.preferences[s]!==u&&(e.preferences[s]=u,this[s]=u,o++)):t('input[id="'+a+'"]').is(":checked")?(e.preferences[s]=1,1===this[s]||(this[s]=1,o++)):(e.preferences[s]=0,1===this[s]&&(this[s]=0,o++))}if(o>0?(this.setCookie(e),this.showAlert(this.tt.prefSuccess)):this.showAlert(this.tt.prefNoChange),"youtube"===this.player&&void 0!==this.usingYouTubeCaptions&&this.usingYouTubeCaptions&&p&&this.youTubePlayer.setOption("captions","fontSize",this.translatePrefs("size",c,"youtube")),AblePlayerInstances.length>1)for(var d=0;d<AblePlayerInstances.length;d++)AblePlayerInstances[d].updatePrefs(),AblePlayerInstances[d].geteferences(),AblePlayerInstances[d].resetPrefsForm(),l>0&&(AblePlayerInstances[d].stylizeCaptions(AblePlayerInstances[d].$captionsDiv),void 0!==AblePlayerInstances[d].$descDiv&&AblePlayerInstances[d].stylizeCaptions(AblePlayerInstances[d].$descDiv));else this.updatePrefs(),l>0&&(this.stylizeCaptions(this.$captionsDiv),void 0!==this.$descDiv&&this.stylizeCaptions(this.$descDiv))},AblePlayer.prototype.updatePrefs=function(){this.$transcriptDiv&&(1===this.prefTabbable?this.$transcriptDiv.find("span.able-transcript-seekpoint").attr("tabindex","0"):this.$transcriptDiv.find("span.able-transcript-seekpoint").removeAttr("tabindex"),0===this.prefHighlight&&this.$transcriptDiv.find("span").removeClass("able-highlight")),this.updateCaption(),this.initDescription()},AblePlayer.prototype.usingModifierKeys=function(t){return(1!==this.prefAltKey||!!t.altKey)&&(1!==this.prefCtrlKey||!!t.ctrlKey)&&(1!==this.prefShiftKey||!!t.shiftKey)}}(jQuery),function(t){function e(t,e){for(var s=[],a=0;a<e.length;a++)s.push(i(t,e[a]));return s}function i(t,e){var i=e(t);if(null!==t.error)throw t.error;return i}function s(t,e){for(var i=0;i<e.length;i++)"\n"===e[i]?(t.column=1,t.line+=1):t.column+=1}function a(t,e){var i=t.text.substring(0,e);return s(t,i),t.text=t.text.substring(e),i}function n(t,e){var i,a=t.text.indexOf("\n");return -1===a?(i=t.text,s(t,i),t.text=""):(i=t.text.substring(0,a),s(t,i+"\n"),t.text=t.text.substring(a+1)),i}function r(t){var e=t.text.indexOf("\n");return -1===e?t.text:t.text.substring(0,e)}function o(t){e(t,[m,y]);var i=t.text[0];" "===i||"	"===i||"\n"===i?e(t,[w,h,D,u]):t.error="WEBVTT signature not followed by whitespace."}function h(t){for(;;){var e=r(t);if(-1!==e.indexOf("-->"))return;if(0===e.length)return;var s=i(t,p);t.metadata[s[0]]=s[1],i(t,w)}}function l(t){var e=[],i=t.indexOf(" ");i>=0&&e.push(i);var s=t.indexOf("	");s>=0&&e.push(s);var a=t.indexOf("\n");return a>=0&&e.push(a),Math.min.apply(null,e)}function p(t){var e,i=t.text.indexOf("\n"),s=a(t,i),n=s.indexOf(":");if(-1===n){t.error="Missing colon.";return}return[s.substring(0,n),s.substring(n+1)]}function c(t){var e,i=l(t.text),s=a(t,i),n=s.indexOf(":");if(-1===n){t.error="Missing colon.";return}return[s.substring(0,n),s.substring(n+1)]}function u(s){for(;;){var a=r(s);if(0===a.indexOf("NOTE")&&(4===a.length||" "===a[4]||"	"===a[4]))e(s,[v,P]);else if(0===t.trim(a).length&&s.text.length>0)i(s,P);else{if(!(t.trim(a).length>0))return;i(s,d)}}}function d(t){var s,o,h=r(t);if(-1===h.indexOf("-->")&&(s=n(t),-1===(h=r(t)).indexOf("-->"))){o="Invalid WebVTT file: "+t.src+"\n",o+="Line: "+t.line+", ",o+="Column: "+t.column+"\n",o+="Expected cue timing for cueId '"+s+"' but found: "+h+"\n",console.warn?console.warn(o):console.log&&console.log(o);return}var l=e(t,[x,T,b,T,x]),p=l[0],c=l[4];if(p>=c){t.error="Start time is not sooner than end time.";return}i(t,C);var u=i(t,f);a(t,1);var d=i(t,g);void 0===s&&(s=t.cues.length+1),t.cues.push({id:s,start:p,end:c,settings:u,components:d})}function f(t){for(var e={};t.text.length>0&&"\n"!==t.text[0];){var s=i(t,c);e[s[0]]=s[1],i(t,C)}return e}function g(e){for(var s={type:"internal",tagName:"",value:"",classes:[],annotation:"",parent:null,children:[],language:""},n=s,o=[];e.text.length>0;){var h=r(e);
16934if(-1!==h.indexOf("-->")||/^\s+$/.test(h))break;if(e.text.length>=2&&"\n"===e.text[0]&&"\n"===e.text[1]){a(e,2);break}var l=_(e);if("string"===l.type)n.children.push(l);else if("startTag"===l.type)l.type=l.tagName,l.parent=n,-1!==t.inArray(l.tagName,["i","b","u","ruby"])?(o.length>0&&(n.language=o[o.length-1]),n.children.push(l),n=l):"rt"===l.tagName&&"ruby"===n.tagName?(o.length>0&&(n.language=o[o.length-1]),n.children.push(l),n=l):"c"===l.tagName?(l.value=l.annotation,o.length>0&&(n.language=o[o.length-1]),n.children.push(l),n=l):"v"===l.tagName?(l.value=l.annotation,o.length>0&&(n.language=o[o.length-1]),n.children.push(l),n=l):"lang"===l.tagName&&(o.push(l.annotation),o.length>0&&(n.language=o[o.length-1]),n.children.push(l),n=l);else if("endTag"===l.type)l.tagName===n.type&&-1!==t.inArray(l.tagName,["c","i","b","u","ruby","rt","v"])?n=n.parent:"lang"===l.tagName&&"lang"===n.type?(n=n.parent,o.pop()):"ruby"===l.tagName&&"rt"===n.type&&(n=n.parent.parent);else if("timestampTag"===l.type){var p={text:l.value,error:null,metadata:{},cues:[],line:1,column:1};try{var c=i(p,x);0===p.text.length&&(l.value=c,n.push(l))}catch(u){}}}return s}function _(e){for(var i,s="data",n=[],r="",o={type:"",tagName:"",value:"",classes:[],annotation:"",children:[]};;){if(i=e.text.length>=2&&"\n"===e.text[0]&&"\n"===e.text[1]?"\x04":e.text.length>0?e.text[0]:"\x04","data"===s){if("&"===i)r="&",s="escape";else if("<"===i){if(0!==n.length)return o.type="string",o.value=n.join(""),o;s="tag"}else{if("\x04"===i)return{type:"string",value:n.join("")};n.push(i)}}else if("escape"===s){if("&"===i)n.push(r),r="&";else if(i.match(/[0-9a-z]/))r+=i;else if(";"===i)"&amp"===r?n.push("&"):"&lt"===r?n.push("<"):"&gt"===r?n.push(">"):"&lrm"===r?n.push("‎"):"&rlm"===r?n.push("‏"):"&nbsp"===r?n.push("\xa0"):(n.push(r),n.push(";")),s="data";else{if("<"===i||"\x04"===i||"	"===i||"\n"===i||"\f"===i||" "===i)return n.push(r),o.type="string",o.value=n.join(""),o;n.push(r),s="data"}}else if("tag"===s){if("	"===i||"\n"===i||"\f"===i||" "===i)s="startTagAnnotation";else if("."===i)s="startTagClass";else if("/"===i)s="endTag";else if(i.match("[0-9]"))s="timestampTag",n.push(i);else if(">"===i){a(e,1);break}else{if("\x04"===i)return o.tagName="",o.type="startTag",o;n.push(i),s="startTag"}}else if("startTag"===s){if("	"===i||"\f"===i||" "===i)s="startTagAnnotation";else if("\n"===i)r=i,s="startTagAnnotation";else if("."===i)s="startTagClass";else{if(">"===i)return a(e,1),o.tagName=n.join(""),o.type="startTag",o;if("\x04"===i)return o.tagName=n.join(""),o.type="startTag",o;n.push(i)}}else if("startTagClass"===s){if("	"===i||"\f"===i||" "===i)o.classes.push(r),r="",s="startTagAnnotation";else if("\n"===i)o.classes.push(r),r=i,s="startTagAnnotation";else if("."===i)o.classes.push(r),r="";else{if(">"===i)return a(e,1),o.classes.push(r),o.type="startTag",o.tagName=n.join(""),o;if("\x04"===i)return o.classes.push(r),o.type="startTag",o.tagName=n.join(""),o;r+="c"}}else if("startTagAnnotation"===s){if(">"===i)return a(e,1),r=t.trim(r).replace(/ +/," "),o.type="startTag",o.tagName=n.join(""),o.annotation=r,o;if("\x04"===i)return r=t.trim(r).replace(/ +/," "),o.type="startTag",o.tagName=n.join(""),o.annotation=r,o;r+=i}else if("endTag"===s){if(">"===i)return a(e,1),o.type="endTag",o.tagName=n.join(""),o;if("\x04"===i)return o.type="endTag",o.tagName=n.join(""),o;n.push(i)}else if("timestampTag"===s){if(">"===i)return a(e,1),o.type="timestampTag",o.name=n.join(""),o;if("\x04"===i)return o.type="timestampTag",o.name=n.join(""),o;n.push(i)}else throw"Unknown tokenState "+s;a(e,1)}}function v(e){if(-1!==n(e).indexOf("-->")){e.error="Invalid syntax: --> in NOTE line.";return}for(;;){var i=r(e);if(0===t.trim(i).length)return;if(-1!==i.indexOf("-->")){e.error="Invalid syntax: --> in comment.";return}n(e)}}function m(t){"\uFEFF"===t.text[0]&&a(t,1)}function y(t){"WEBVTT"===t.text.substring(0,6)?a(t,6):t.error="Invalid signature."}function b(t){t.text.length<3||"-->"!==t.text.substring(0,3)?t.error="Missing -->":a(t,3)}function k(t){"	"===t.text[0]||" "===t.text[0]?a(t,1):t.error="Missing space."}function C(t){for(;"	"===t.text[0]||" "===t.text[0];)a(t,1)}function T(t){for(var e=0;"	"===t.text[0]||" "===t.text[0];)a(t,1),e+=1;0===e&&(t.error="Missing space.")}function w(t){var e=t.text.indexOf("\n");-1===e?t.error="Missing EOL.":a(t,e+1)}function P(e){for(;e.text.length>0;){var i=r(e);
16934if(0===t.trim(i).length)n(e);else break}}function D(e){for(var i=0;e.text.length>0;){var s=r(e);if(0===t.trim(s).length)n(e),i+=1;else break}0===i&&(e.error="Missing empty line.")}function x(t){var e=l(t.text);if(-1===e){t.error("Missing timing.");return}var i=a(t,e),s=/((\d\d):)?((\d\d):)(\d\d).(\d\d\d)|(\d+).(\d\d\d)/.exec(i);if(!s){t.error="Unable to parse timestamp";return}var n=0,r=s[2],o=s[4];if(o){if(parseInt(o,10)>59){t.error="Invalid minute range";return}r&&(n+=3600*parseInt(r,10)),n+=60*parseInt(o,10);var h=s[5];if(parseInt(h,10)>59){t.error="Invalid second range";return}n+=parseInt(h,10),n+=parseInt(s[6],10)/1e3}else n+=parseInt(s[7],10),n+=parseInt(s[8],10)/1e3;return n}AblePlayer.prototype.parseWebVTT=function(t,e){var s={src:t,text:e=e.replace(/(\r\n|\n|\r)/g,"\n"),error:null,metadata:{},cues:[],line:1,column:1};try{i(s,o)}catch(a){var n="Invalid WebVTT file: "+s.src+"\n";n+="Line: "+s.line+", ",n+="Column: "+s.column+"\n",n+=a,console.warn?console.warn(n):console.log&&console.log(n)}return s}}(jQuery),function(t){AblePlayer.prototype.injectPlayerCode=function(){var e,i;e=this,this.$mediaContainer=this.$media.wrap('<div class="able-media-container"></div>').parent(),this.$ableDiv=this.$mediaContainer.wrap('<div class="able"></div>').parent(),this.$ableWrapper=this.$ableDiv.wrap('<div class="able-wrapper"></div>').parent(),this.$ableWrapper.addClass("able-skin-"+this.skin),this.$ableWrapper.css({width:this.playerWidth+"px"}),"video"===this.mediaType&&"image"!=this.iconType&&("youtube"!==this.player||this.hasPoster)&&this.injectBigPlayButton(),i=t("<div>"),"video"===this.mediaType?i.addClass("able-vidcap-container"):"audio"===this.mediaType&&(i.addClass("able-audcap-container"),i.addClass("captions-off")),this.injectPlayerControlArea(),this.$captionsContainer=this.$mediaContainer.wrap(i).parent(),this.injectAlert(),this.injectPlaylist(),this.injectOffscreenHeading()},AblePlayer.prototype.injectOffscreenHeading=function(){var e;"0"==this.playerHeadingLevel||(void 0===this.playerHeadingLevel&&(this.playerHeadingLevel=this.getNextHeadingLevel(this.$ableDiv)),e="h"+this.playerHeadingLevel.toString(),this.$headingDiv=t("<"+e+">"),this.$ableDiv.prepend(this.$headingDiv),this.$headingDiv.addClass("able-offscreen"),this.$headingDiv.text(this.tt.playerHeading))},AblePlayer.prototype.injectBigPlayButton=function(){var e,i,s,a;e=this,this.$bigPlayButton=t("<button>",{class:"able-big-play-button","aria-hidden":!1,"aria-label":this.tt.play,tabindex:0}),"svg"==this.iconType?(s=t("<svg>",{focusable:"false","aria-hidden":"true",viewBox:(i=this.getSvgData("play"))[0]}),a=t("<path>",{d:i[1]}),s.append(a),this.$bigPlayButton.html(s),this.$bigPlayButton.html(this.$bigPlayButton.html())):(this.$bigPlayIcon=t("<span>",{class:"icon-play"}),this.$bigPlayButton.append(this.$bigPlayIcon)),this.$bigPlayButton.click(function(t){t.preventDefault(),e.handlePlay()}),this.$mediaContainer.append(this.$bigPlayButton)},AblePlayer.prototype.injectPlayerControlArea=function(){this.$playerDiv=t("<div>",{class:"able-player",role:"region","aria-label":this.mediaType+" player"}),this.$playerDiv.addClass("able-"+this.mediaType),this.hasPlaylist&&this.showNowPlaying&&(this.$nowPlayingDiv=t("<div>",{class:"able-now-playing","aria-live":"assertive","aria-atomic":"true"})),this.$controllerDiv=t("<div>",{class:"able-controller"}),this.$controllerDiv.addClass("able-"+this.iconColor+"-controls"),this.$statusBarDiv=t("<div>",{class:"able-status-bar"}),this.$timer=t("<span>",{class:"able-timer"}),this.$elapsedTimeContainer=t("<span>",{class:"able-elapsedTime",text:"0:00"}),this.$durationContainer=t("<span>",{class:"able-duration"}),this.$timer.append(this.$elapsedTimeContainer).append(this.$durationContainer),this.$speed=t("<span>",{class:"able-speed","aria-live":"assertive"}).text(this.tt.speed+": 1x"),this.$status=t("<span>",{class:"able-status","aria-live":"polite"}),this.$statusBarDiv.append(this.$timer,this.$speed,this.$status),this.showNowPlaying?this.$playerDiv.append(this.$nowPlayingDiv,this.$controllerDiv,this.$statusBarDiv):this.$playerDiv.append(this.$controllerDiv,this.$statusBarDiv),"video"===this.mediaType?this.$ableDiv.append(this.$playerDiv):this.$ableDiv.prepend(this.$playerDiv)},AblePlayer.prototype.injectTextDescriptionArea=function(){this.$descDiv=t("<div>",{class:"able-descriptions"}),this.$descDiv.attr({"aria-live":"assertive","aria-atomic":"true"}),this.$descDiv.hide(),this.$ableDiv.append(this.$descDiv)},AblePlayer.prototype.getDefaultWidth=function(t){return"transcript"===t?450:"sign"===t?400:void 0},AblePlayer.prototype.positionDraggableWindow=function(e,i){var s,a,n,r;s=this.getCookie(),"transcript"===e?(n=this.$transcriptArea,void 0!==s.transcript&&(a=s.transcript)):"sign"===e&&(n=this.$signWindow,void 0!==s.transcript&&(a=s.sign)),void 0===a||t.isEmptyObject(a)?(r=this.getOptimumPosition(e,i),void 0===i&&(i=this.getDefaultWidth(e)),n.css({position:r[0],width:i,"z-index":r[3]}),"absolute"===r[0]&&n.css({top:r[1]+"px",left:r[2]+"px"})):(n.css({position:a.position,width:a.width,"z-index":a.zindex}
16934),"absolute"===a.position&&n.css({top:a.top,left:a.left}),this.updateZIndex(e))},AblePlayer.prototype.getOptimumPosition=function(e,i){var s,a,n,r,o,h,l,p,c;return void 0===i&&(i=this.getDefaultWidth(e)),s=5,a=[],n=this.$ableDiv.width(),r=this.$ableDiv.height(),h=(o=this.$ableDiv.offset()).top,l=o.left,p=t(window).width(),c=0,"transcript"===e?void 0!==this.$signWindow&&this.$signWindow.is(":visible")&&(c=this.$signWindow.width()+s):"sign"===e&&void 0!==this.$transcriptArea&&this.$transcriptArea.is(":visible")&&(c=this.$transcriptArea.width()+s),i<p-(l+n+s+c)?(a[0]="absolute",a[1]=0,a[2]=n+c+s):i+s<l?(a[0]="absolute",a[1]=0,a[2]=l-i-s):a[0]="relative",a},AblePlayer.prototype.injectAlert=function(){var e;this.$alertBox=t('<div role="alert"></div>'),this.$alertBox.addClass("able-alert"),this.$alertBox.hide(),this.$alertBox.appendTo(this.$ableDiv),e="audio"==this.mediaType?"-10":"10",this.$alertBox.css({top:e+"px"}),this.$srAlertBox=t('<div role="alert"></div>'),this.$srAlertBox.addClass("able-screenreader-alert"),this.$srAlertBox.appendTo(this.$ableDiv)},AblePlayer.prototype.injectPlaylist=function(){if(!0===this.playlistEmbed){var t=this.$playlistDom.clone();t.insertBefore(this.$statusBarDiv),this.$playlist=t.find("li")}},AblePlayer.prototype.createPopup=function(e,i){var s,a,n,r,o,h,l,p,c,u,d,f,g,_;if(s=this,a=t("<ul>",{id:this.mediaId+"-"+e+"-menu",class:"able-popup",role:"menu"}).hide(),"captions"===e&&a.addClass("able-popup-captions"),"prefs"===e){if(this.prefCats.length>1){for(r=0;r<this.prefCats.length;r++)o=t("<li></li>",{role:"menuitem",tabindex:"-1"}),"captions"===(h=this.prefCats[r])?o.text(this.tt.prefMenuCaptions):"descriptions"===h?o.text(this.tt.prefMenuDescriptions):"keyboard"===h?o.text(this.tt.prefMenuKeyboard):"transcript"===h&&o.text(this.tt.prefMenuTranscript),o.on("click",function(){l=t(this).text(),s.showingPrefsDialog=!0,s.setFullscreen(!1),l===s.tt.prefMenuCaptions?s.captionPrefsDialog.show():l===s.tt.prefMenuDescriptions?s.descPrefsDialog.show():l===s.tt.prefMenuKeyboard?s.keyboardPrefsDialog.show():l===s.tt.prefMenuTranscript&&s.transcriptPrefsDialog.show(),s.closePopups(),s.showingPrefsDialog=!1}),a.append(o);this.$prefsButton.attr("data-prefs-popup","menu")}else 1==this.prefCats.length&&this.$prefsButton.attr("data-prefs-popup",this.prefCats[0])}else if("captions"===e||"chapters"===e){for(r=0,p=!1;r<i.length;r++)c=i[r],(n="captions"!==e||"html5"!==this.player||void 0!==c.cues)&&(o=t("<li></li>",{role:"menuitemradio",tabindex:"-1",lang:c.language}),c.def&&1==this.prefCaptions?(o.attr("aria-checked","true"),p=!0):o.attr("aria-checked","false"),"captions"==e?(o.text(c.label),o.on("click",this.getCaptionClickFunction(c))):"chapters"==e&&(o.text(this.flattenCueForCaption(c)+" - "+this.formatSecondsAsColonTime(c.start)),o.on("click",this.getChapterClickFunction(c.start))),a.append(o));"captions"===e&&(o=t("<li></li>",{role:"menuitemradio",tabindex:"-1"}).text(this.tt.captionsOff),0===this.prefCaptions?(o.attr("aria-checked","true"),p=!0):o.attr("aria-checked","false"),o.on("click",this.getCaptionOffFunction()),a.append(o))}else if("transcript-window"===e||"sign-window"===e)for((u=[]).push({name:"move",label:this.tt.windowMove}),u.push({name:"resize",label:this.tt.windowResize}),u.push({name:"close",label:this.tt.windowClose}),r=0;r<u.length;r++)(o=t("<li></li>",{role:"menuitem",tabindex:"-1","data-choice":u[r].name})).text(u[r].label),o.on("click mousedown",function(i){if(i.stopPropagation(),void 0!==i.button&&0!==i.button)return!1;s.windowMenuClickRegistered||s.finishingDrag||(s.windowMenuClickRegistered=!0,s.handleMenuChoice(e.substring(0,e.indexOf("-")),t(this).attr("data-choice"),i))}),a.append(o);return"captions"!==e||p?"chapters"===e&&(a.find('li:contains("'+this.defaultChapter+'")')?a.find('li:contains("'+this.defaultChapter+'")').attr("aria-checked","true").addClass("able-focus"):a.find("li").first().attr("aria-checked","true").addClass("able-focus")):a.find("li[lang="+this.captionLang+"]")?a.find("li[lang="+this.captionLang+"]").attr("aria-checked","true"):a.find("li").last().attr("aria-checked","true"),a.on("keydown",function(e){d=t(this).attr("id").split("-")[1],(f=t(this).find("li:focus")).is(":first-child")?(g=t(this).find("li").last(),_=f.next()):f.is(":last-child")?(g=f.prev(),_=t(this).find("li").first()):(g=f.prev(),_=f.next()),9===e.which?e.shiftKey?(f.removeClass("able-focus"),g.focus().addClass("able-focus")):(f.removeClass("able-focus"),_.focus().addClass("able-focus")):40===e.which||39===e.which?(f.removeClass("able-focus"),_.focus().addClass("able-focus")):38==e.which||37===e.which?(f.removeClass("able-focus"),g.focus().addClass("able-focus")):32===e.which||13===e.which?f.click():27===e.which&&(f.removeClass("able-focus"),s.closePopups(),e.stopPropagation),e.preventDefault()}),this.$controllerDiv.append(a),a},AblePlayer.prototype.closePopups=function(){var t=this;this.chaptersPopup&&this.chaptersPopup.is(":visible")&&(this.chaptersPopup.hide(),this.$chaptersButton.attr("aria-expanded","fal
16934se").focus()),this.captionsPopup&&this.captionsPopup.is(":visible")&&(this.captionsPopup.hide(),this.$ccButton.attr("aria-expanded","false"),this.waitThenFocus(this.$ccButton)),this.prefsPopup&&this.prefsPopup.is(":visible")&&!this.hidingPopup&&(this.hidingPopup=!0,this.prefsPopup.hide(),this.prefsPopup.find("li").removeClass("able-focus").attr("tabindex","-1"),this.$prefsButton.attr("aria-expanded","false"),this.showingPrefsDialog||this.waitThenFocus(t.$prefsButton),setTimeout(function(){t.hidingPopup=!1},100)),this.$volumeSlider&&this.$volumeSlider.is(":visible")&&(this.$volumeSlider.hide().attr("aria-hidden","true"),this.$volumeButton.attr("aria-expanded","false").focus()),this.$transcriptPopup&&this.$transcriptPopup.is(":visible")&&(this.hidingPopup=!0,this.$transcriptPopup.hide(),this.$transcriptPopup.find("li").removeClass("able-focus").attr("tabindex","-1"),this.$transcriptPopupButton.attr("aria-expanded","false").focus(),setTimeout(function(){t.hidingPopup=!1},100)),this.$signPopup&&this.$signPopup.is(":visible")&&(this.$signPopup.hide(),this.$signPopup.find("li").removeClass("able-focus").attr("tabindex","-1"),this.$signPopupButton.attr("aria-expanded","false").focus())},AblePlayer.prototype.setupPopups=function(t){var e,i,s,a,n;if(e=[],void 0===t&&e.push("prefs"),("captions"===t||void 0===t)&&this.captions.length>0&&e.push("captions"),("chapters"===t||void 0===t)&&this.chapters.length>0&&this.useChaptersButton&&e.push("chapters"),"transcript-window"===t&&"popup"===this.transcriptType&&e.push("transcript-window"),"sign-window"===t&&this.hasSignLanguage&&e.push("sign-window"),e.length>0){i=this;for(var a=0;a<e.length;a++){var r=e[a];if(s=!1,"prefs"==r)this.prefsPopup=this.createPopup("prefs");else if("captions"==r)void 0!==this.captionsPopup&&this.captionsPopup||(this.captionsPopup=this.createPopup("captions",this.captions));else if("chapters"==r)n=this.selectedChapters?this.selectedChapters.cues:this.chapters.length>=1?this.chapters[0].cues:[],void 0!==this.chaptersPopup&&this.chaptersPopup||(this.chaptersPopup=this.createPopup("chapters",n));else if("transcript-window"==r)return this.createPopup("transcript-window");else if("sign-window"==r)return this.createPopup("sign-window")}}},AblePlayer.prototype.provideFallback=function(){var e,i;if(!this.usingFallback){if(this.usingFallback=!0,this.testFallback||(this.testFallback=1),void 0===this.$media&&(this.$media=t(this.media)),this.$media.attr("id")?this.mediaId=this.$media.attr("id"):this.mediaId="media"+Math.floor(1e9*Math.random()).toString(),this.hasFallback=!1,this.$media.children().length)for(e=0;e<this.$media.children().length&&!this.hasFallback;)"SOURCE"===this.$media.children()[e].tagName||"TRACK"===this.$media.children()[e].tagName||(this.hasFallback=!0),e++;if(this.hasFallback||(i=t("<p>").text("Media player unavailable."),this.$media.append(i)),this.$media.attr("width")&&this.$media.css("width",this.$media.attr("width")+"px"),this.$media.attr("height")&&this.$media.css("height",this.$media.attr("height")+"px"),this.$media.removeAttr("data-able-player"),this.$media.prop("controls",!0),2==this.testFallback){if(t(this.$media).replaceWith(t('<foobar id="foobar-'+this.mediaId+'">')),this.$newFallbackElement=t("#foobar-"+this.mediaId),this.$media.children().length)for(e=this.$media.children().length-1;e>=0;)this.$newFallbackElement.prepend(t(this.$media.children()[e])),e--;this.hasFallback||this.$newFallbackElement.append(i)}}},AblePlayer.prototype.calculateControlLayout=function(){var t,e,i,s;return(t=[])[0]=[],t[1]=[],"legacy"===this.skin&&(t[2]=[],t[3]=[]),t[0].push("play"),t[0].push("restart"),t[0].push("rewind"),t[0].push("forward"),"legacy"===this.skin&&t[1].push("seek"),this.hasPlaylist&&("legacy"===this.skin?(t[0].push("previous"),t[0].push("next")):"2020"==this.skin&&(t[0].push("previous"),t[0].push("next"))),this.isPlaybackRateSupported()?(i=!0,"legacy"===this.skin&&(t[2].push("slower"),t[2].push("faster"))):i=!1,s=0,this.hasCaptions&&(s++,"legacy"===this.skin?t[2].push("captions"):"2020"==this.skin&&t[1].push("captions")),this.hasSignLanguage&&(s++,"legacy"===this.skin?t[2].push("sign"):"2020"==this.skin&&t[1].push("sign")),"video"===this.mediaType&&(this.hasOpenDesc||this.hasClosedDesc)&&(s++,"legacy"===this.skin?t[2].push("descriptions"):"2020"==this.skin&&t[1].push("descriptions")),"popup"!==this.transcriptType||this.hideTranscriptButton||(s++,"legacy"===this.skin?t[2].push("transcript"):"2020"==this.skin&&t[1].push("transcript")),this.hasChapters&&this.useChaptersButton&&(s++,"legacy"===this.skin?t[2].push("chapters"):"2020"==this.skin&&t[1].push("chapters")),"2020"==this.skin&&s>0&&t[1].push("pipe"),i&&"2020"===this.skin&&(t[1].push("faster"),t[1].push("slower"),t[1].push("pipe")),"legacy"===this.skin?t[3].push("preferences"):"2020"==this.skin&&t[1].push("preferences"),"video"===this.mediaType&&this.allowFullscreen&&("legacy"===this.skin?t[3].push("fullscreen"):t[1].push("fullscreen")),this.browserSupportsVolume()?(e=!0,this.volumeButton="volume-"+this.getVolumeName(this.volume),"legacy"===this.skin?t[1].push("volume"):"2020"==this.skin&&t[1].push("volume")):(e=!1,this.volume=!1),t}
16934,AblePlayer.prototype.addControls=function(){for(e=this,i=100,a=(s=this.calculateControlLayout()).length,T=this.mediaId+"-tooltip",this.$tooltipDiv=t("<div>",{id:T,class:"able-tooltip"}).hide(),this.$controllerDiv.append(this.$tooltipDiv),"2020"==this.skin&&(l=t('<div class="able-seekbar"></div>'),p=this.mediaType+" "+this.tt.seekbarLabel,this.$controllerDiv.append(l),this.seekBar=new AccessibleSlider(this.mediaType,l,"horizontal",i,0,this.duration,this.seekInterval,p,"seekbar",!0,"visible")),n=0;n<a;n++){for(o=s[n],h=n%2==0?t("<div>",{class:"able-left-controls"}):t("<div>",{class:"able-right-controls"}),this.$controllerDiv.append(h),r=0;r<o.length;r++){if("seek"===(g=o[r]))l=t('<div class="able-seekbar"></div>'),p=this.mediaType+" "+this.tt.seekbarLabel,h.append(l),(void 0===this.duration||0===this.duration)&&(this.duration=60,this.elapsed=0),this.seekBar=new AccessibleSlider(this.mediaType,l,"horizontal",i,0,this.duration,this.seekInterval,p,"seekbar",!0,"visible");else if("pipe"===g)c=t("<span>",{tabindex:"-1","aria-hidden":"true"}),"font"===this.iconType?c.addClass("icon-pipe"):(u=t("<img>",{src:this.rootPath+"button-icons/"+this.iconColor+"/pipe.png",alt:"",role:"presentation"}),c.append(u)),h.append(c);else{m="volume"===g?this.rootPath+"button-icons/"+this.iconColor+"/"+this.volumeButton+".png":"fullscreen"===g?this.rootPath+"button-icons/"+this.iconColor+"/fullscreen-expand.png":"slower"===g?"animals"===this.speedIcons?this.rootPath+"button-icons/"+this.iconColor+"/turtle.png":this.rootPath+"button-icons/"+this.iconColor+"/slower.png":"faster"===g?"animals"===this.speedIcons?this.rootPath+"button-icons/"+this.iconColor+"/rabbit.png":this.rootPath+"button-icons/"+this.iconColor+"/faster.png":this.rootPath+"button-icons/"+this.iconColor+"/"+g+".png",b=t("<div>",{role:"button",tabindex:"0","aria-label":y=this.getButtonTitle(g),class:"able-button-handler-"+g}),("volume"===g||"preferences"===g||"captions"===g)&&("preferences"==g?(this.prefCats=this.getPreferencesGroups(),this.prefCats.length>1?(D=this.mediaId+"-prefs-menu",b.attr({"aria-controls":D,"aria-haspopup":"menu","aria-expanded":"false"})):1===this.prefCats.length&&b.attr({"aria-haspopup":"dialog"})):"volume"===g?(D=this.mediaId+"-volume-slider",b.attr({"aria-controls":D,"aria-expanded":"false"})):"captions"===g&&this.captions&&(this.captions.length>1?b.attr("aria-expanded","false"):b.attr("aria-pressed","false"))),"font"===this.iconType?(C=t("<span>",{class:k="volume"===g?"icon-"+this.volumeButton:"slower"===g?"animals"===this.speedIcons?"icon-turtle":"icon-slower":"faster"===g?"animals"===this.speedIcons?"icon-rabbit":"icon-faster":"icon-"+g,"aria-hidden":"true"}),b.append(C)):"svg"===this.iconType?(C=t("<svg>",{focusable:"false","aria-hidden":"true",viewBox:(d="volume"===g?this.get
16934SvgData(this.volumeButton):"fullscreen"===g?this.getSvgData("fullscreen-expand"):"slower"===g?"animals"===this.speedIcons?this.getSvgData("turtle"):this.getSvgData("slower"):"faster"===g?"animals"===this.speedIcons?this.getSvgData("rabbit"):this.getSvgData("faster"):this.getSvgData(g))[0]}),f=t("<path>",{d:d[1]}),C.append(f),b.html(C),b.html(b.html())):(v=t("<img>",{src:m,alt:"",role:"presentation"}),b.append(v));var e,i,s,a,n,r,o,h,l,p,c,u,d,f,g,_,v,m,y,b,k,C,T,w,P,D,_=t("<span>",{class:"able-clipped"}).text(y);b.append(_),b.on("mouseenter focus",function(i){clearTimeout(w);var s=t(this).attr("aria-label"),a=t(this).position(),n=t(this).height(),r=t(this).width(),o=e.$controllerDiv.width();a.right=o-a.left-r;var h=a.top+n+5;if(t(this).parent().hasClass("able-right-controls"))var l="right";else var l="left";var p=AblePlayer.localGetElementById(b[0],T).text(s).width();if("left"==l){var c=a.left-p/2;c<0&&(c=2);var u={left:c+"px",right:"",top:h+"px"}}else{var c=a.right-p/2;c<0&&(c=2);var u={left:"",right:c+"px",top:h+"px"}}var d=AblePlayer.localGetElementById(b[0],T).text(s).css(u);e.showTooltip(d),t(this).on("mouseleave blur",function(){clearTimeout(w),w=setTimeout(function(){AblePlayer.localGetElementById(b[0],T).text("").hide()},500),e.$tooltipDiv.on("mouseenter focus",function(){clearTimeout(w)}),e.$tooltipDiv.on("mouseleave blur",function(){AblePlayer.localGetElementById(b[0],T).text("").hide()})})}),"captions"===g?this.prefCaptions&&1===this.prefCaptions||(P=this.captions.length>1?this.tt.captions:this.tt.showCaptions,b.addClass("buttonOff").attr("title",P),b.attr("aria-pressed","false")):"descriptions"!==g||this.prefDesc&&1===this.prefDesc||b.addClass("buttonOff").attr("title",this.tt.turnOnDescriptions),h.append(b),"play"===g?this.$playpauseButton=b:"previous"==g?(this.$prevButton=b,"previous"==this.buttonWithFocus&&(this.$prevButton.focus(),this.buttonWithFocus=null)):"next"==g?(this.$nextButton=b,"next"==this.buttonWithFocus&&(this.$nextButton.focus(),this.buttonWithFocus=null)):"captions"===g?this.$ccButton=b:"sign"===g?(this.$signButton=b,this.$signWindow.is(":visible")||this.$signButton.addClass("buttonOff")):"descriptions"===g?this.$descButton=b:"mute"===g?this.$muteButton=b:"transcript"===g?(this.$transcriptButton=b,this.$transcriptDiv.is(":visible")||this.$transcriptButton.addClass("buttonOff").attr("title",this.tt.showTranscript)):"fullscreen"===g?this.$fullscreenButton=b:"chapters"===g?this.$chaptersButton=b:"preferences"===g?this.$prefsButton=b:"volume"===g&&(this.$volumeButton=b)}"volume"===g&&this.addVolumeSlider(h)}n%2==1&&this.$controllerDiv.append('<div style="clear:both;"></div>')}for(var x in void 0!==this.$captionsDiv&&this.stylizeCaptions(this.$captionsDiv),void 0!==this.$descDiv&&this.stylizeCaptions(this.$descDiv),this.controls=[],s)s.hasOwnProperty(x)&&(this.controls=this.controls.concat(s[x]));this.refreshControls("init")},AblePlayer.prototype.useSvg=function(){// @license		Licensed under MIT license
16935var t,e,i=Object.create(null),s=function(){clearTimeout(e),e=setTimeout(t,100)},a=function(){},n=function(){var t;window.addEventListener("resize",s,!1),window.addEventListener("orientationchange",s,!1),window.MutationObserver?((t=new MutationObserver(s)).observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0}),a=function(){try{t.disconnect(),window.removeEventListener("resize",s,!1),window.removeEventListener("orientationchange",s,!1)}catch(e){}}):(document.documentElement.addEventListener("DOMSubtreeModified",s,!1),a=function(){document.documentElement.removeEventListener("DOMSubtreeModified",s,!1),window.removeEventListener("resize",s,!1),window.removeEventListener("orientationchange",s,!1)})},r="http://www.w3.org/1999/xlink";t=function(){var t,e,s,o,h,l,p,c,u,d=0;if(window.XMLHttpRequest&&(h=void 0!==(h=new XMLHttpRequest).withCredentials?XMLHttpRequest:XDomainRequest||void 0),void 0!==h){for(a(),c=document.getElementsByTagName("use"),o=0;o<c.length;o+=1){try{e=c[o].getBoundingClientRect()}catch(f){e=!1}t=(p=c[o].getAttributeNS(r,"href").split("#"))[0],s=p[1],l=e&&0===e.left&&0===e.right&&0===e.top&&0===e.bottom,e&&0===e.width&&0===e.height&&!l?t.length&&(!0!==(u=i[t])&&setTimeout(_({useEl:c[o],base:t,hash:s}),0),void 0===u&&(u=new h,i[t]=u,u.onload=v(u),u.onerror=m(u),u.ontimeout=m(u),u.open("GET",t),u.send(),d+=1)):!l&&(void 0===i[t]?i[t]=!0:i[t].onload&&(i[t].abort(),i[t].onload=void 0,i[t]=!0))}c="",d+=1,g()}function g(){0==(d-=1)&&n()}function _(t){return function(){!0!==i[t.base]&&t.useEl.setAttributeNS(r,"xlink:href","#"+t.hash)}}function v(t){return function(){var e,i=document.body,s=document.createElement("x");
16935t.onload=null,s.innerHTML=t.responseText,(e=s.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",i.insertBefore(e,i.firstChild)),g()}}function m(t){return function(){t.onerror=null,t.ontimeout=null,g()}}}},AblePlayer.prototype.cuePlaylistItem=function(e){var i,s,a,n,r,o,h=this;s=this.player,this.initializing||this.playerCreated&&this.deletePlayer("playlist"),this.swappingSrc=!0,this.startedPlaying?this.okToPlay=!0:this.okToPlay=!1,this.loadingMedia=!1,i=this.$playlist.eq(e),this.hasAttr(i,"data-youtube-id")?(this.youTubeId=this.getYouTubeId(i.attr("data-youtube-id")),this.hasAttr(i,"data-youtube-desc-id")&&(this.youTubeDescId=this.getYouTubeId(i.attr("data-youtube-desc-id"))),a="youtube"):this.hasAttr(i,"data-vimeo-id")?(this.vimeoId=this.getVimeoId(i.attr("data-vimeo-id")),this.hasAttr(i,"data-vimeo-desc-id")&&(this.vimeoDescId=this.getVimeoId(i.attr("data-vimeo-desc-id"))),a="vimeo"):a="html5","youtube"===a?"html5"===s&&(this.playing&&this.pauseMedia(),this.$media.hide()):(this.youTubeId=!1,"youtube"===s&&this.$media.show()),this.player=a,this.$media.empty(),this.hasAttr(i,"data-poster")&&this.$media.attr("poster",i.attr("data-poster")),this.hasAttr(i,"data-youtube-desc-id")&&this.$media.attr("data-youtube-desc-id",i.attr("data-youtube-desc-id")),this.youTubeId&&this.$media.attr("data-youtube-id",i.attr("data-youtube-id"));var l=i.children("span.able-source");l.length&&l.each(function(){if(h.hasAttr(t(this),"data-src")){var e=t("<source>",{src:t(this).attr("data-src")});h.hasAttr(t(this),"data-type")&&e.attr("type",t(this).attr("data-type")),h.hasAttr(t(this),"data-desc-src")&&e.attr("data-desc-src",t(this).attr("data-desc-src")),h.hasAttr(t(this),"data-sign-src")&&e.attr("data-sign-src",t(this).attr("data-sign-src")),h.$media.append(e)}});var p=i.children("span.able-track");p.length&&p.each(function(){if(h.hasAttr(t(this),"data-src")&&h.hasAttr(t(this),"data-kind")&&h.hasAttr(t(this),"data-srclang")){var e=t("<track>",{src:t(this).attr("data-src"),kind:t(this).attr("data-kind"),srclang:t(this).attr("data-srclang")});h.hasAttr(t(this),"data-label")&&e.attr("label",t(this).attr("data-label")),h.hasAttr(t(this),"data-desc")&&e.attr("data-desc",t(this).attr("data-desc")),h.$media.append(e)}}),n=i.text(),this.hasAttr(i,"lang")&&(r=i.attr("lang")),this.$sources=this.$media.find("source"),!this.recreatingPlayer&&this.recreatePlayer().then(function(){h.$playlist.removeClass("able-current").children("button").removeAttr("aria-current"),h.$playlist.eq(e).addClass("able-current").children("button").attr("aria-current","true"),!0===h.showNowPlaying&&void 0!==h.$nowPlayingDiv&&(o=t("<span>"),void 0!==r&&o.attr("lang",r),o.html("<span>"+h.tt.selectedTrack+":</span>"+n),h.$nowPlayingDiv.html(o)),h.initializing?h.swappingSrc=!1:"html5"===h.player?h.loadingMedia||(h.media.load(),h.loadingMedia=!0):"youtube"===h.player&&(h.okToPlay=!0),h.initializing=!1,h.playerCreated=!0})},AblePlayer.prototype.deletePlayer=function(e){"youtube"===this.player&&this.youTubePlayer&&this.youTubePlayer.destroy(),"vimeo"===this.player&&this.vimeoPlayer&&this.vimeoPlayer.destroy(),this.$controllerDiv.empty(),this.$elapsedTimeContainer.empty().text("0:00"),this.$durationContainer.empty(),this.$signWindow&&this.$signWindow.remove(),this.$transcriptArea&&this.$transcriptArea.remove(),t(".able-modal-dialog").remove(),this.$captionsWrapper&&this.$captionsWrapper.remove(),this.$descDiv&&this.$descDiv.remove(),this.hasCaptions=!1,this.hasChapters=!1,this.hasDescTracks=!1,this.hasOpenDesc=!1,this.hasClosedDesc=!1,this.captionsPopup=null,this.chaptersPopup=null,this.transcriptType=null,this.playerDeleted=!0},AblePlayer.prototype.getButtonTitle=function(t){if("playpause"===t||"play"===t)return this.tt.play;if("pause"===t)return this.tt.pause;if("restart"===t)return this.tt.restart;if("previous"===t)return this.tt.prevTrack;else if("next"===t)return this.tt.nextTrack;else if("rewind"===t)return this.tt.rewind;else if("forward"===t)return this.tt.forward;else if("captions"===t)return this.captions.length>1?this.tt.captions:this.captionsOn?this.tt.hideCaptions:this.tt.showCaptions;else if("descriptions"===t)return this.descOn?this.tt.turnOffDescriptions:this.tt.turnOnDescriptions;else if("transcript"===t)return this.$transcriptDiv.is(":visible")?this.tt.hideTranscript:this.tt.showTranscript;else if("chapters"===t)return this.tt.chapters;else if("sign"===t)return this.tt.sign;else if("volume"===t)return this.tt.volume;else if("faster"===t)return this.tt.faster;else if("slower"===t)return this.tt.slower;else if("preferences"===t)return this.tt.preferences;else if("help"===t);else if("fullscreen"!==t)return this.debug&&console.log("Found an untranslated label: "+t),t.charAt(0).toUpperCase()+t.slice(1);else return this.fullscreen?this.tt.exitFullscreen:this.tt.enterFullscreen}}(jQuery),function(t){AblePlayer.prototype.setupTracks=function(){var e,i,s,a,n,r,o,h,l;for(e=this,s=(i=new t.Deferred).promise(),a=[],t("#able-vts").length?(this.vtsTracks=[],this.hasVts=!0):this.hasVts=!1,o=this.hasDescTracks&&this.descOn?this.altTracks:this.tracks,r=0;r<o.length;r++){if(l=(h=o[r]).kind,!h.src){(e.usingYouTubeCaptions||e.usingVimeoCaptions)&&e.setupCaptions(h);continue}h.src,n=this.loadTextObject(h.src),a.push(n.catch(function(t){console.warn("Failed to load captions track from "+t)})),n.then(function(t,i){t.src;var s=t.language,a=t.label,n=t.desc;return function(r,o){var h=o,l=e.parseWebVTT(r,h).cues;e.hasVts&&e.setupVtsTracks(i,s,n,a,r,h),"captions"===i||"subtitles"===i?e.setupCaptions(t,l):"descriptions"===i?e.setupDescriptions(t,l):"chapters"===i?e.setupChapters(t,l):"metadata"===i&&e.setupMetadata(t,l)}}(h,l))}return e.usingYouTubeCaptions||e.usingVimeoCaptions?i.resolve():t.when.apply(t,a).then(function(){i.resolve()}),s},AblePlayer.prototype.getTracks=function(){var e,i,s,a,n,r,o,h,l,p,c;return e=this,h=!1,s=(i=new t.Deferred).promise(),this.$tracks=this.$media.find("track"),this.tracks=[],this.altTracks=[],this.captions=[],this.descriptions=[],this.chapters=[],this.meta=[],this.hasCaptionsTrack=!1,this.hasDescTracks=!1,this.$tracks.length&&(this.usingYouTubeCaptions=!1,this.$tracks.each(function(i,s){if("captions"===t(this).attr("kind")?e.hasCaptionsTrack=!0:"descriptions"===t(this).attr("kind")&&(e.hasClosedDesc=!0),a=t(this).attr("srclang")?t(this).attr("srclang"):e.lang,n=t(this).attr("label")?t(this).attr("label"):e.getLanguageName(a),void 0===t(this).attr("default")||h?(a===e.lang&&(l=!0),r=!1):(r=!0,h=!0),r&&(e.captionLang=a),void 0!==t(this).data("desc")?(o=!0,e.hasDescTracks=!0):o=!1,o?e.altTracks.push({kind:t(this).attr("kind"),src:t(this).attr("src"),language:a,label:n,def:r,desc:o}):e.tracks.push({kind:t(this).attr("kind"),src:t(this).attr("src"),language:a,label:n,def:r,desc:o}),i==e.$tracks.length-1){if(!h){if(l)for(e.captionLang=e.lang,p=!1,c=0;c<e.tracks.length&&!p;)e.tracks[c].language===e.lang&&(e.tracks[c].def=!0,p=!0),c++;else e.tracks[0].def=!0,e.captionLang=e.tracks[0].language}e.$media.find("track").removeAttr("default")}})),this.$tracks.length&&this.hasCaptionsTrack?i.resolve():"youtube"===this.player?this.getYouTubeCaptionTracks(this.youTubeId).then(function(){e.hasCaptions&&(e.usingYouTubeCaptions=!0,e.$captionsWrapper&&e.$captionsWrapper.remove()),i.resolve()}):"vimeo"===this.player?this.getVimeoCaptionTracks().then(function(){e.hasCaptions&&(e.usingVimeoCaptions=!0,e.$captionsWrapper&&e.$captionsWrapper.remove()),i.resolve()}):(this.hasCaptions=!1,e.$captionsWrapper&&e.$captionsWrapper.remove(),i.resolve()),s},AblePlayer.prototype.setupCaptions=function(e,i){var s,a,n;if(void 0===i&&(i=null),this.usingYouTubeCaptions||this.usingVimeoCaptions);else if(0===this.captions.length)this.captions.push({language:e.language,label:e.label,def:e.def,cues:i});else{for(a=0,s=!1;a<this.captions.length;a++)if((n=e.label).toLowerCase()<this.captions[a].label.toLowerCase()){this.captions.splice(a,0,{language:e.language,label:e.label,def:e.def,cues:i}),s=!0;break}s||this.captions.push({language:e.language,label:e.label,def:e.def,cues:i})}this.hasCaptions=!0,this.currentCaption=-1,1===this.prefCaptions?this.captionsOn=!0:0===this.prefCaptions?this.captionsOn=!1:1===this.defaultStateCaptions?this.captionsOn=!0:this.captionsOn=!1,"audio"===this.mediaType&&this.captionsOn&&this.$captionsContainer.removeClass("captions-off"),this.$captionsWrapper&&(!this.$captionsWrapper||t.contains(this.$ableDiv[0],this.$captionsWrapper[0]))||(this.$captionsDiv=t("<div>",{class:"able-captions"}),this.$captionsWrapper=t("<div>",{class:"able-captions-wrapper","aria-hidden":"true"}).hide(),"below"===this.prefCaptionsPosition?this.$captionsWrapper.addClass("able-captions-below"):this.$captionsWrapper.addClass("able-captions-overlay"),this.$captionsWrapper.append(this.$captionsDiv),this.$captionsContainer.append(this.$captionsWrapper))},AblePlayer.prototype.setupDescriptions=function(t,e){this.hasClosedDesc=!0,this.currentDescription=-1,this.descriptions.push({cues:e,language:t.language})},AblePlayer.prototype.setupChapters=function(t,e){this.hasChapters=!0,this.chapters.push({cues:e,language:t.language})},AblePlayer.prototype.setupMetadata=function(e,i,s){"text"===this.metaType?this.metaDiv&&t("#"+this.metaDiv)&&(this.$metaDiv=t("#"+this.metaDiv),this.hasMeta=!0,this.meta=i):"selector"===this.metaType&&(this.hasMeta=!0,this.visibleSelectors=[],this.meta=i)},AblePlayer.prototype.loadTextObject=function(e){var i,s,a,n;return s=(i=new t.Deferred).promise(),a=this,(n=t("<div>",{style:"display:none"})).load(e,function(t,s,r){"error"===s?(a.debug&&console.log("error reading file "+e+": "+s),i.reject(e)):i.resolve(e,t),n.remove()}),s}}(jQuery),function(t){AblePlayer.prototype.initYouTubePlayer=function(){var e,i,s,a;return e=this,s=(i=new t.Deferred).promise(),this.youTubePlayerReady=!1,a=this.youTubeDescId&&this.prefDesc?this.youTubeDescId:this.youTubeId,this.activeYouTubeId=a,AblePlayer.youTubeIframeAPIReady?e.finalizeYoutubeInit().then(function(){i.resolve()}):(AblePlayer.loadingYouTubeIframeAPI||t.getScript("https://www.youtube.com/iframe_api").fail(function(){i.fail()}),t("body").on("youTubeIframeAPIReady",function(){e.finalizeYoutubeInit().then(function(){i.resolve()})})),s},AblePlayer.prototype.finalizeYoutubeInit=function(){var e,i,s,a,n,r;return i=(e=new t.Deferred).promise(),s=this,a=this.mediaId+"_youtube",this.$mediaContainer.prepend(t("<div>").attr("id",a)),n=1,r=this.okToPlay?1:0,void 0===this.captionLang&&(this.captionLang=this.lang),this.youTubePlayer=new YT.Player(a,{videoId:this.activeYouTubeId,host:this.youTubeNoCookie?"https://www.youtube-nocookie.com":"https://www.youtube.com",playerVars:{autoplay:r,enablejsapi:1,disableKb:1,playsinline:this.playsInl
16935ine,start:this.startTime,controls:0,cc_load_policy:n,cc_lang_pref:this.captionLang,hl:this.lang,modestbranding:1,rel:0,iv_load_policy:3},events:{onReady:function(){s.youTubePlayerReady=!0,s.playerWidth&&s.playerHeight||s.getYouTubeDimensions(),s.playerWidth&&s.playerHeight&&(s.youTubePlayer.setSize(s.playerWidth,s.playerHeight),s.$ableWrapper.css({width:s.playerWidth+"px"})),s.swappingSrc&&(s.swappingSrc=!1,s.restoreFocus(),s.cueingPlaylistItem=!1,(s.playing||s.okToPlay)&&s.playMedia()),s.userClickedPlaylist&&(s.userClickedPlaylist=!1),s.recreatingPlayer&&(s.recreatingPlayer=!1),e.resolve()},onError:function(t){e.fail()},onStateChange:function(t){s.getPlayerState().then(function(t){"playing"===t?(s.playing=!0,s.startedPlaying=!0,s.paused=!1):"ended"==t?s.onMediaComplete():(s.playing=!1,s.paused=!0),s.stoppingYouTube&&"paused"===t&&(void 0!==s.$posterImg&&s.$posterImg.show(),s.stoppingYouTube=!1,s.seeking=!1,s.playing=!1,s.paused=!0)}),"youtube"===s.player&&!s.usingYouTubeCaptions&&s.youTubePlayer.getOptions("captions")&&s.youTubePlayer.unloadModule("captions")},onPlaybackQualityChange:function(){}}}),this.hasPlaylist||this.$media.remove(),i},AblePlayer.prototype.getYouTubeDimensions=function(t){var e,i,s;void 0!==(e=this.$ableWrapper.find("iframe"))&&e.prop("width")&&(i=e.prop("width"),e.prop("height")&&(s=e.prop("height"),this.resizePlayer(i,s)))},AblePlayer.prototype.getYouTubeCaptionTracks=function(e){var i,s,a,n,r,o,h=new t.Deferred,l=h.promise();return i=this,this.youTubePlayer.getOption("captions","tracklist")||(this.youTubePlayer.addEventListener("onApiChange",function(t){if(i.duration=i.youTubePlayer.getDuration(),i.loadingYouTubeCaptions){if(s=i.youTubePlayer.getOption("captions","tracklist"),i.okToPlay||i.youTubePlayer.pauseVideo(),s&&s.length){for(a=0;a<s.length;a++)n=s[a].languageCode,r=s[a].languageName,o=!1,void 0!==i.captionLang?n===i.captionLang&&(o=!0):void 0!==i.lang&&n===i.lang&&(o=!0),i.tracks.push({kind:"captions",language:n,label:r,def:o}),i.captions.push({language:n,label:r,def:o,cues:null});i.hasCaptions=!0,i.setupPopups("captions")}else i.usingYouTubeCaptions=!1,i.hasCaptions=!1;i.loadingYouTubeCaptions=!1,i.okToPlay&&i.youTubePlayer.playVideo()}i.captionLangPending&&(i.youTubePlayer.setOption("captions","track",{languageCode:i.captionLangPending}),i.captionLangPending=null),void 0!==i.prefCaptionsSize&&i.youTubePlayer.setOption("captions","fontSize",i.translatePrefs("size",i.prefCaptionsSize,"youtube")),h.resolve()}),this.loadingYouTubeCaptions=!0,this.youTubePlayer.playVideo()),l},AblePlayer.prototype.getYouTubeTimedTextUrl=function(t,e,i){var s="https://www.youtube.com/api/timedtext?fmt=vtt";return s+="&v="+t,s+="&lang="+i,""!==e&&(s+="&name="+e),s},AblePlayer.prototype.getYouTubePosterUrl=function(t,e){var i="https://img.youtube.com/vi/"+t;return"120"==e?i+"/default.jpg":"320"==e?i+"/hqdefault.jpg":"480"==e?i+"/hqdefault.jpg":"640"==e&&i+"/sddefault.jpg"},AblePlayer.prototype.getYouTubeId=function(t){var e,i;return -1!==t.indexOf("youtu")?(e=(t=t.trim()).length-11,i=t.substring(e)):t}}(jQuery),function(t){window.AccessibleSlider=function(e,i,s,a,n,r,o,h,l,p,c){var u,d;u=this,this.position=0,this.tracking=!1,this.trackDevice=null,this.keyTrackPosition=0,this.lastTrackPosition=0,this.nextStep=1,this.inertiaCount=0,this.bodyDiv=t(i),p&&(this.loadedDiv=t("<div></div>"),this.playedDiv=t("<div></div>")),this.seekHead=t("<div>",{"aria-orientation":s,class:"able-"+l+"-head"}),"visible"===c?this.seekHead.attr("tabindex","0"):this.seekHead.attr("tabindex","-1"),this.seekHead.attr({role:"slider","aria-label":h,"aria-valuemin":n,"aria-valuemax":r}),this.timeTooltipTimeoutId=null,this.overTooltip=!1,this.timeTooltip=t("<div>"),this.bodyDiv.append(this.timeTooltip),this.timeTooltip.attr("role","tooltip"),this.timeTooltip.addClass("able-tooltip"),this.timeTooltip.on("mouseenter focus",function(){u.overTooltip=!0,clearInterval(u.timeTooltipTimeoutId)}),this.timeTooltip.on("mouseleave blur",function(){u.overTooltip=!1,t(this).hide()}),this.timeTooltip.hide(),this.bodyDiv.append(this.loadedDiv),this.bodyDiv.append(this.playedDiv),this.bodyDiv.append(this.seekHead),this.bodyDiv.wrap("<div></div>"),this.wrapperDiv=this.bodyDiv.parent(),"legacy"===this.skin&&("horizontal"===s?(this.wrapperDiv.width(a),this.loadedDiv.width(0)):(this.wrapperDiv.height(a),this.loadedDiv.he
16935ight(0))),this.wrapperDiv.addClass("able-"+l+"-wrapper"),p&&(this.loadedDiv.addClass("able-"+l+"-loaded"),this.playedDiv.width(0),this.playedDiv.addClass("able-"+l+"-played"),this.setDuration(r)),this.seekHead.on("mouseenter mouseleave mousemove mousedown mouseup focus blur touchstart touchmove touchend",function(t){d=u.pointerEventToXY(t),"mouseenter"===t.type||"focus"===t.type?u.overHead=!0:"mouseleave"===t.type||"blur"===t.type?(u.overHead=!1,!u.overBody&&u.tracking&&"mouse"===u.trackDevice&&u.stopTracking(u.pageXToPosition(d.x))):"mousemove"===t.type||"touchmove"===t.type?u.tracking&&"mouse"===u.trackDevice&&u.trackHeadAtPageX(d.x):"mousedown"===t.type||"touchstart"===t.type?(u.startTracking("mouse",u.pageXToPosition(u.seekHead.offset()+u.seekHead.width()/2)),u.bodyDiv.is(":focus")||u.bodyDiv.focus(),t.preventDefault()):("mouseup"===t.type||"touchend"===t.type)&&u.tracking&&"mouse"===u.trackDevice&&u.stopTracking(u.pageXToPosition(d.x)),"mousemove"!==t.type&&"mousedown"!==t.type&&"mouseup"!==t.type&&"touchstart"!==t.type&&"touchend"!==t.type&&u.refreshTooltip()}),this.bodyDiv.on("mouseenter mouseleave mousemove mousedown mouseup keydown keyup touchstart touchmove touchend",function(t){if(d=u.pointerEventToXY(t),"mouseenter"===t.type)u.overBody=!0,u.overBodyMousePos={x:d.x,y:d.y};else if("mouseleave"===t.type)u.overBody=!1,u.overBodyMousePos=null,!u.overHead&&u.tracking&&"mouse"===u.trackDevice&&u.stopTracking(u.pageXToPosition(d.x));else if("mousemove"===t.type||"touchmove"===t.type)u.overBodyMousePos={x:d.x,y:d.y},u.tracking&&"mouse"===u.trackDevice&&u.trackHeadAtPageX(d.x);else if("mousedown"===t.type||"touchstart"===t.type)u.startTracking("mouse",u.pageXToPosition(d.x)),u.trackHeadAtPageX(d.x),u.seekHead.is(":focus")||u.seekHead.focus(),t.preventDefault();else if("mouseup"===t.type||"touchend"===t.type)u.tracking&&"mouse"===u.trackDevice&&u.stopTracking(u.pageXToPosition(d.x));else if("keydown"===t.type){if(36===t.which)u.trackImmediatelyTo(0);else if(35===t.which)u.trackImmediatelyTo(u.duration);else if(37===t.which||40===t.which)u.arrowKeyDown(-1);else if(39===t.which||38===t.which)u.arrowKeyDown(1);else if(33===t.which&&o>0)u.arrowKeyDown(o);else{if(34!==t.which||!(o>0))return;u.arrowKeyDown(-o)}t.preventDefault()}else"keyup"===t.type&&t.which>=33&&t.which<=40&&(u.tracking&&"keyboard"===u.trackDevice&&u.stopTracking(u.keyTrackPosition),t.preventDefault());u.overTooltip||"mouseup"===t.type||"keydown"===t.type||"keydown"===t.type||u.refreshTooltip()})},AccessibleSlider.prototype.arrowKeyDown=function(t){this.tracking&&"keyboard"===this.trackDevice?(this.keyTrackPosition=this.boundPos(this.keyTrackPosition+this.nextStep*t),this.inertiaCount+=1,20===this.inertiaCount&&(this.inertiaCount=0,this.nextStep*=2),this.trackHeadAtPosition(this.keyTrackPosition)):(this.nextStep=1,this.inertiaCount=0,this.keyTrackPosition=this.boundPos(this.position+this.nextStep*t),this.startTracking("keyboard",this.keyTrackPosition),this.trackHeadAtPosition(this.keyTrackPosition))},AccessibleSlider.prototype.pageXToPosition=function(t){var e=t-this.bodyDiv.offset().left,i=this.duration*(e/this.bodyDiv.width());return this.boundPos(i)},AccessibleSlider.prototype.boundPos=function(t){return Math.max(0,Math.min(t,this.duration))},AccessibleSlider.prototype.setDuration=function(t){t!==this.duration&&(this.duration=t,this.resetHeadLocation(),this.seekHead.attr("aria-valuemax",t))},AccessibleSlider.prototype.setWidth=function(t){this.wrapperDiv.width(t),this.resizeDivs(),this.resetHeadLocation()},AccessibleSlider.prototype.getWidth=function(){return this.wrapperDiv.width()},AccessibleSlider.prototype.resizeDivs=function(){this.playedDiv.width(this.bodyDiv.width()*(this.position/this.duration)),this.loadedDiv.width(this.bodyDiv.width()*this.buffered)},AccessibleSlider.prototype.resetHeadLocation=function(){var t=this.position/this.duration,e=this.bodyDiv.width()*t;this.seekHead.css("left",e-this.seekHead.width()/2),this.tracking&&this.stopTracking(this.position)},AccessibleSlider.prototype.setPosition=function(t,e){this.position=t,this.resetHeadLocation(),this.overHead&&this.refreshTooltip(),this.resizeDivs(),this.updateAriaValues(t,e)},AccessibleSlider.prototype.setBuffered=function(t){isNaN(t)||(this.buffered=t,this.redrawDivs)},AccessibleSlider.prototype.startTracking=function(t,e){this.tracking||(this.trackDevice=t,this.tracking=!0,this.bodyDiv.trigger("startTracking",[e]))},AccessibleSlider.prototype.stopTracking=function(t){this.trackDevice=null,this.tracking=!1,this.bodyDiv.trigger("stopTracking",[t]),this.setPosition(t,!0)},AccessibleSlider.prototype.trackHeadAtPageX=function(t){var e=this.pageXToPosition(t),i=t-this.bodyDiv.offset().left-this.seekHead.width()/2;i=Math.max(0,Math.min(i,this.bodyDiv.width()-this.seekHead.width())),this.lastTrackPosition=e,this.seekHead.css("left",i),this.reportTrackAtPosition(e)},AccessibleSlider.prototype.trackHeadAtPosition=function(t){var e=t/this.duration,i=this.bodyDiv.width()*e;this.lastTrackPosition=t,this.seekHead.css("left",i-this.seekHead.width()/2),this.reportTrackAtPosition(t)},AccessibleSlider.prototype.reportTrackAtPosition=function(t){this.bodyDiv.trigger("tracking",[t]),this.updateAriaValues(t,!0)},AccessibleSlider.prototype.updateAriaValues=function(e,i){var s,a=Math.floor(e/3600),n=Math.floor(e%3600/60),r=M
16935ath.floor(e%60),o=1===n?"minute":"minutes",h=1===r?"second":"seconds";s=a>0?a+" "+(1===a?"hour":"hours")+", "+n+" "+o+", "+r+" "+h:n>0?n+" "+o+", "+r+" "+h:r+" "+h,this.liveAriaRegion||(this.liveAriaRegion=t("<span>",{class:"able-offscreen","aria-live":"polite"}),this.wrapperDiv.append(this.liveAriaRegion)),i&&this.liveAriaRegion.text()!==s&&this.liveAriaRegion.text(s),this.seekHead.attr("aria-valuetext",s),this.seekHead.attr("aria-valuenow",Math.floor(e).toString())},AccessibleSlider.prototype.trackImmediatelyTo=function(t){this.startTracking("keyboard",t),this.trackHeadAtPosition(t),this.keyTrackPosition=t},AccessibleSlider.prototype.refreshTooltip=function(){if(this.overHead)this.timeTooltip.show(),this.tracking?this.timeTooltip.text(this.positionToStr(this.lastTrackPosition)):this.timeTooltip.text(this.positionToStr(this.position)),this.setTooltipPosition(this.seekHead.position().left+this.seekHead.width()/2);else if(this.overBody&&this.overBodyMousePos)this.timeTooltip.show(),this.timeTooltip.text(this.positionToStr(this.pageXToPosition(this.overBodyMousePos.x))),this.setTooltipPosition(this.overBodyMousePos.x-this.bodyDiv.offset().left);else{clearTimeout(this.timeTooltipTimeoutId);var t=this;this.timeTooltipTimeoutId=setTimeout(function(){t.timeTooltip.hide()},500)}},AccessibleSlider.prototype.hideSliderTooltips=function(){this.overHead=!1,this.overBody=!1,this.timeTooltip.hide()},AccessibleSlider.prototype.setTooltipPosition=function(t){this.timeTooltip.css({left:t-this.timeTooltip.width()/2-10,bottom:this.seekHead.height()+10})},AccessibleSlider.prototype.positionToStr=function(t){var e=Math.floor(t/3600),i=Math.floor(t/60)%60,s=Math.floor(t%60);return(s<10&&(s="0"+s),e>0)?(i<10&&(i="0"+i),e+":"+i+":"+s):i+":"+s},AccessibleSlider.prototype.pointerEventToXY=function(t){var e={x:0,y:0};if("touchstart"==t.type||"touchmove"==t.type||"touchend"==t.type||"touchcancel"==t.type){var i=t.originalEvent.touches[0]||t.originalEvent.changedTouches[0];e.x=i.pageX,e.y=i.pageY}else("mousedown"==t.type||"mouseup"==t.type||"mousemove"==t.type||"mouseover"==t.type||"mouseout"==t.type||"mouseenter"==t.type||"mouseleave"==t.type)&&(e.x=t.pageX,e.y=t.pageY);return e}}(jQuery),function(t){AblePlayer.prototype.addVolumeSlider=function(e){var i,s,a,n,r,o,h,l,p;for(i=this,s=this.mediaId+"-volume-slider",a=this.mediaId+"-volume-help",r=this.mediaId+"-volume-tick-labels",this.$volumeSlider=t("<div>",{id:s,class:"able-volume-slider","aria-hidden":"true"}).hide(),this.$volumeSliderTooltip=t("<div>",{class:"able-tooltip",role:"tooltip"}).hide(),this.$volumeRange=t("<input>",{type:"range",min:"0",max:"10",step:"1",orient:"vertical","aria-label":this.tt.volumeUpDown,value:this.volume}),n=parseInt(i.volume)/10*100,this.$volumeHelp=t("<div>",{id:a,class:"able-volume-help","aria-live":"polite"}).text(n+"%"),this.$volumeButton.attr({"aria-describedby":a}),o=t("<datalist>",{id:r}),h=0;h<=10;h++)p=0===h?this.tt.mute:10*h+"%",l=t("<option>",{value:h,label:p}),o.append(l);this.$volumeSlider.append(this.$volumeSliderTooltip,this.$volumeRange,this.$volumeHelp),e.append(this.$volumeSlider),this.$volumeRange.on("change",function(e){i.handleVolumeChange(t(this).val())}),this.$volumeRange.on("input",function(e){i.handleVolumeChange(t(this).val())}),this.$volumeRange.on("keydown",function(t){(27===t.which||13===t.which||9===t.which)&&(i.$volumeSlider.is(":visible")?(i.closingVolume=!0,i.hideVolumePopup()):i.closingVolume||i.showVolumePopup())})},AblePlayer.prototype.refreshVolumeHelp=function(t){var e;e=t/10*100,this.$volumeHelp&&this.$volumeHelp.text(e+"%"),this.$volumeRange.attr("value",t)},AblePlayer.prototype.refreshVolumeButton=function(t){var e,i,s,a,n,r;e=this.getVolumeName(t),i=t/10*100,s=this.tt.volume+" "+i+"%","font"===this.iconType?(a="icon-volume-"+e,this.$volumeButton.find("span").first().removeClass().addClass(a),this.$volumeButton.find("span.able-clipped").text(s)):"image"===this.iconType?(n=this.imgPath+"volume-"+e+".png",this.$volumeButton.find("img").attr("src",n)):"svg"===this.iconType&&("mute"!==e&&(e="volume-"+e),r=this.getSvgData(e),this.$volumeButton.find("svg").attr("viewBox",r[0]),this.$volumeButton.find("path").attr("d",r[1]))},AblePlayer.prototype.handleVolumeButtonClick=function(){this.$volumeSlider.is(":visible")?this.hideVolumePopup():this.showVolumePopup()},AblePlayer.prototype.handleVolumeKeystroke=function(t){var e;if(!(t>=49)||!(t<=57))return!1;e=t-48,this.isMuted()&&e>0?this.setMute(!1):0===e?this.setMute(!0):(this.setVolume(e),this.refreshVolumeHelp(e),this.refreshVolumeButton(e))},AblePlayer.prototype.handleVolumeChange=function(t){this.isMuted()&&t>0?this.setMute(!1):0===t?this.setMute(!0):(this.setVolume(t),this.refreshVolumeHelp(t),this.refreshVolumeButton(t))},AblePlayer.prototype.handleMute=function(){this.isMuted()?this.setMute(!1):this.setMute(!0)},AblePlayer.prototype.showVolumePopup=function(){this.closePopups(),this.$tooltipDiv.hide(),this.$volumeSlider.show().attr("aria-hidden","false"),this.$volumeButton.attr("aria-expanded","true"),this.$volumeButton.focus(),this.waitThenFocus(this.$volumeRange)},AblePlayer.prototype.hideVolumePopup=function(){var t=this;this.$volumeSlider.hide().attr("aria-hidden","true"),this.$volumeButton.attr("aria-expanded","fal
16935se").focus(),setTimeout(function(){t.closingVolume=!1},1e3)},AblePlayer.prototype.isMuted=function(){return"html5"===this.player?this.media.muted:"youtube"===this.player?this.youTubePlayer.isMuted():void 0},AblePlayer.prototype.setMute=function(t){t?(this.lastVolume=this.volume,this.volume=0):void 0!==this.lastVolume&&(this.volume=this.lastVolume),"html5"===this.player?this.media.muted=t:"youtube"===this.player&&(t?this.youTubePlayer.mute():this.youTubePlayer.unMute()),this.setVolume(this.volume),this.refreshVolumeHelp(this.volume),this.refreshVolumeButton(this.volume)},AblePlayer.prototype.setVolume=function(t){var e;"html5"===this.player?(e=t/10,this.media.volume=e,this.hasSignLanguage&&this.signVideo&&(this.signVideo.volume=0)):"youtube"===this.player?(e=10*t,this.youTubePlayer.setVolume(e),this.volume=t):"vimeo"===this.player&&(e=t/10,this.vimeoPlayer.setVolume(e).then(function(){})),this.lastVolume=t},AblePlayer.prototype.getVolume=function(t){return"html5"===this.player?10*this.media.volume:"youtube"===this.player&&this.youTubePlayerReady?this.youTubePlayer.getVolume()/10:"vimeo"===this.player?this.volume:void 0},AblePlayer.prototype.getVolumeName=function(t){return 0==t?"mute":10==t?"loud":t<5?"soft":"medium"}}(jQuery),function(t){var e="a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]";window.AccessibleDialog=function(i,s,a,n,r,o,h,l,p,c){this.title=r,this.closeButtonLabel=h,this.focusedElementBeforeModal=s,this.escapeHook=c,this.baseId=t(i).attr("id")||Math.floor(1e9*Math.random()).toString();var u=this,d=i;if(this.modal=d,d.css({width:l||"50%"}),d.addClass("able-modal-dialog"),!p){var f=t("<button>",{class:"modalCloseButton",title:u.closeButtonLabel,"aria-label":u.closeButtonLabel}).text("X");f.keydown(function(t){32===t.which&&u.hide()}).click(function(){u.hide()});var g=t("<h1></h1>");g.attr("id","modalTitle-"+this.baseId),g.css("text-align","center"),g.text(r),this.titleH1=g,d.attr({"aria-labelledby":"modalTitle-"+this.baseId}),d.prepend(g),d.prepend(f)}d.attr({"aria-hidden":"true",role:a}),n&&d.attr("aria-modal","true"),d.keydown(function(i){if(27===i.which)u.escapeHook?u.escapeHook(i,this):(u.hide(),i.preventDefault());else if(9===i.which){var s=d.find("*").filter(e).filter(":visible");if(0===s.length)return;var a=t(":focus"),n=s.index(a);
16935i.shiftKey?0===n&&(s.get(s.length-1).focus(),i.preventDefault()):n===s.length-1&&(s.get(0).focus(),i.preventDefault())}i.stopPropagation()}),t("body > *").not(".able-modal-overlay").not(".able-modal-dialog").removeAttr("aria-hidden")},AccessibleDialog.prototype.show=function(){if(!this.overlay){var i=t("<div></div>").attr({class:"able-modal-overlay",tabindex:"-1"});this.overlay=i,t("body").append(i),i.on("mousedown.accessibleModal",function(t){t.preventDefault()})}t("body > *").not(".able-modal-overlay").not(".able-modal-dialog").attr("aria-hidden","true"),this.overlay.css("display","block"),this.modal.css("display","block"),this.modal.attr({"aria-hidden":"false",tabindex:"-1"}),0===this.modal.find("*").filter(e).filter(":visible").length&&this.focusedElementBeforeModal.blur();var s=this;setTimeout(function(){s.modal.find("button.modalCloseButton").first().focus()},300)},AccessibleDialog.prototype.hide=function(){this.overlay&&this.overlay.css("display","none"),this.modal.css("display","none"),this.modal.attr("aria-hidden","true"),t("body > *").not(".able-modal-overlay").not(".able-modal-dialog").removeAttr("aria-hidden"),this.focusedElementBeforeModal.focus()},AccessibleDialog.prototype.getInputs=function(){return!!this.modal&&this.modal.find("input")}}(jQuery),$=jQuery,AblePlayer.prototype.getNextHeadingLevel=function(t){var e,i,s,a,n;return(e=t.parents()).each(function(){if(s=(i=$(this).children(":header")).length)return a=i.eq(s-1).prop("tagName"),!1}),void 0===a?n=1:(n=parseInt(a[1]),(n+=1)>6&&(n=6)),n},AblePlayer.prototype.countProperties=function(t){var e,i;for(i in e=0,t)t.hasOwnProperty(i)&&++e;return e},AblePlayer.prototype.formatSecondsAsColonTime=function(t,e){var i,s,a,n,r,o,h;if(e){if(2===(n=t.toString().split(".")).length){if((r=n[1]).length<3)for(h=1,o=3-r.length;h<=o;h++)r+="0"}else r="000"}return(i=Math.floor(t/3600),s=Math.floor(t/60)%60,(a=Math.floor(t%60))<10&&(a="0"+a),i>0)?(s<10&&(s="0"+s),e)?i+":"+s+":"+a+"."+r:i+":"+s+":"+a:e?(i<1?i="00":i<10&&(i="0"+i),s<1?s="00":s<10&&(s="0"+s),i+":"+s+":"+a+"."+r):s+":"+a},AblePlayer.prototype.getSecondsFromColonTime=function(t){var e,i,s,a;return 3===(e=t.split(":")).length?(i=parseInt(e[0]),3600*i+60*(s=parseInt(e[1]))+(a=parseFloat(e[2]))):2===e.length?60*(s=parseInt(e[0]))+(a=parseFloat(e[1])):1===e.length?parseFloat(e[0]):void 0},AblePlayer.prototype.capitalizeFirstLetter=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},AblePlayer.prototype.roundDown=function(t,e){return Number(Math.floor(t+"e"+e)+"e-"+e)},AblePlayer.prototype.hasAttr=function(t,e){var i=t.attr(e);return void 0!==i&&!1!==i},Number.isInteger=Number.isInteger||function(t){return"number"==typeof t&&isFinite(t)&&Math.floor(t)===t},function(t){AblePlayer.prototype.initDescription=function(){var e,i,s;return i=(e=new t.Deferred).promise(),s=this,"audio"===this.mediaType&&e.resolve(),this.descFile=this.$sources.first().attr("data-desc-src"),void 0!==this.descFile?this.hasOpenDesc=!0:this.youTubeDescId||this.vimeoDescId?this.hasOpenDesc=!0:this.hasOpenDesc=!1,this.hasOpenDesc&&this.hasClosedDesc?this.prefDescMethod?this.descMethod=this.prefDescMethod:this.descMethod="video":this.hasOpenDesc?this.descMethod="video":this.hasClosedDesc?this.descMethod="text":this.descMethod=null,this.descMethod?1===this.prefDesc?this.descOn=!0:0===this.prefDesc?this.descOn=!1:1===this.defaultStateDescriptions?this.descOn=!0:this.descOn=!1:this.descOn=!1,void 0===this.$descDiv&&this.hasClosedDesc&&"text"===this.descMethod&&this.injectTextDescriptionArea(),this.descOn?("video"!==this.descMethod||this.usingDescribedVersion()||this.swapDescription(),this.hasClosedDesc&&(this.prefDescVisible?void 0!==this.$descDiv&&(this.$descDiv.show(),this.$descDiv.removeClass("able-clipped")):void 0!==this.$descDiv&&this.$descDiv.addClass("able-clipped"))):"video"===this.descMethod?this.usingDescribedVersion()&&this.swapDescription():"text"===this.descMethod&&void 0!==this.$descDiv&&(this.$descDiv.hide(),this.$descDiv.removeClass("able-clipped")),e.resolve(),i},AblePlayer.prototype.usingDescribedVersion=function(){return"youtube"===this.player?this.activeYouTubeId===this.youTubeDescId:"vimeo"===this.player?this.activeVimeoId===this.vimeoDescId:this.$sources.first().attr("data-desc-src")===this.$sources.first().attr("src")},AblePlayer.prototype.initSpeech=function(e){var i=this;if(null===this.speechEnabled){if(void 0!==this.synth&&this.synth.cancel(),window.speechSynthesis){if(this.synth=window.speechSynthesis,"init"===e)t(document).on("click",function(){var e=new SpeechSynthesisUtterance("Hi!");e.volume=0,e.rate=10,i.synth.speak(e),e.onstart=function(e){t(document).off("
16935click")},e.onend=function(t){i.getBrowserVoices(),i.descVoices.length&&(i.speechEnabled=!0)}}),this.getBrowserVoices(),this.descVoices.length&&(this.speechEnabled=!0);else{var s=new SpeechSynthesisUtterance("Hi!");s.volume=0,s.rate=10,i.synth.speak(s),s.onstart=function(e){t(document).off("click")},s.onend=function(t){i.getBrowserVoices(),i.descVoices.length&&(i.speechEnabled=!0)}}}else this.speechEnabled=!1}},AblePlayer.prototype.getBrowserVoices=function(){var t,e,i,s;if(s=this.captionLang?this.captionLang.substring(0,2).toLowerCase():this.lang.substring(0,2).toLowerCase(),this.descVoices=[],t=this.synth.getVoices(),e=this.getDescriptionLangs(),t.length>0){this.descVoices=[];for(var a=0;a<t.length;a++)(i=t[a].lang.substring(0,2).toLowerCase())===s&&-1!==e.indexOf(i)&&this.descVoices.push(t[a]);this.descVoices.length||(this.descVoices=t)}return!1},AblePlayer.prototype.getDescriptionLangs=function(){var t=[];if(this.tracks)for(var e=0;e<this.tracks.length;e++)"descriptions"===this.tracks[e].kind&&t.push(this.tracks[e].language.substring(0,2).toLowerCase());return t},AblePlayer.prototype.setDescriptionVoice=function(){var t,e,i,s,a,n;if(i=void 0!==(t=this.getCookie()).voices?this.getPrefDescVoice():null,this.getBrowserVoices(),this.rebuildDescPrefsForm(),a=this.selectedDescriptions?this.selectedDescriptions.language:this.captionLang?this.captionLang:this.lang,this.synth&&(e=this.synth.getVoices()).length>0){if(i){n=!1;for(var r=0;r<e.length;r++)if(e[r].lang.substring(0,2).toLowerCase()===a.substring(0,2).toLowerCase()&&e[r].name===i){s=e[r].name,n=!0;break}}if(!n){for(var r=0;r<e.length;r++)if(e[r].lang.substring(0,2).toLowerCase()===a.substring(0,2).toLowerCase()){s=e[r].name;break}}this.prefDescVoice=s,this.prefDescVoiceLang=a,this.$voiceSelectField&&(this.$voiceSelectField.find('option[value="'+this.prefDescVoice+'"]'),this.$voiceSelectField.val(this.prefDescVoice)),this.updateCookie("voice")}},AblePlayer.prototype.swapDescription=function(){var e,i,s,a,n;if(e=this,this.loadingMedia=!1,this.$focusedElement=t(":focus"),this.elapsed>0?this.swapTime=this.elapsed:this.swapTime=0,this.duration>0&&(this.prevDuration=this.duration),this.okToPlay||(this.okToPlay=this.playing),this.descOn?this.showAlert(this.tt.alertDescribedVersion):this.showAlert(this.tt.alertNonDescribedVersion),"html5"===this.player){if(this.swappingSrc=!0,this.paused=!0,this.usingDescribedVersion())for(i=0;i<this.$sources.length;i++)s=this.$sources[i].getAttribute("data-orig-src"),n=this.$sources[i].getAttribute("type"),s&&this.$sources[i].setAttribute("src",s);else for(i=0;i<this.$sources.length;i++)s=this.$sources[i].getAttribute("src"),a=this.$sources[i].getAttribute("data-desc-src"),n=this.$sources[i].getAttribute("type"),a&&(this.$sources[i].setAttribute("src",a),this.$sources[i].setAttribute("data-orig-src",s));if(this.recreatingPlayer)return;this.playerCreated&&(this.deletePlayer("swap-desc-html"),this.recreatePlayer().then(function(){e.loadingMedia||(e.media.load(),e.loadingMedia=!0)}))}else if("youtube"===this.player){if(this.usingDescribedVersion()?this.activeYouTubeId=this.youTubeId:this.activeYouTubeId=this.youTubeDescId,void 0!==this.youTubePlayer&&(e.swappingSrc=!0,e.playing?e.youTubePlayer.loadVideoById(e.activeYouTubeId,e.swapTime):e.youTubePlayer.cueVideoById(e.activeYouTubeId,e.swapTime)),this.playerCreated&&this.deletePlayer("swap-desc-youtube"),this.recreatingPlayer)return;this.recreatePlayer().then(function(){})}else if("vimeo"===this.player){if(this.usingDescribedVersion()?(this.activeVimeoId=this.vimeoId,this.showAlert(this.tt.alertNonDescribedVersion)):(this.activeVimeoId=this.vimeoDescId,this.showAlert(this.tt.alertDescribedVersion)),this.playerCreated&&this.deletePlayer("swap-desc-vimeo"),this.recreatingPlayer)return;this.recreatePlayer().then(function(){e.vimeoPlayer.loadVideo(e.activeVimeoId).then(function(){e.playing?e.vimeoPlayer.setCurrentTime(e.swapTime):e.vimeoPlayer.pause()})})}},AblePlayer.prototype.showDescription=function(t){if(this.hasClosedDesc&&!this.swappingSrc&&this.descOn&&"video"!==this.descMethod){e=this;var e,i,s,a,n,r=function(t){var e=[];
16935if("string"===t.type)e.push(t.value);else for(var i=0;i<t.children.length;i++)e.push(r(t.children[i]));return e.join("")};for(s=0,i=this.selectedDescriptions?this.selectedDescriptions.cues:this.descriptions.length>=1?this.descriptions[0].cues:[];s<i.length;s++)if(i[s].start<=t&&i[s].end>t){a=s;break}void 0!==a?this.currentDescription!==a&&(this.$status.removeAttr("aria-live"),n=r(i[a].components),"screenreader"===this.descReader?this.$descDiv.html(n):this.speechEnabled?(this.announceDescriptionText("description",n),this.prefDescVisible&&this.$descDiv.html(n).removeAttr("aria-live aria-atomic")):this.$descDiv.html(n),this.prefDescPause&&"text"===this.descMethod&&(this.pauseMedia(),this.pausedForDescription=!0),this.currentDescription=a):(this.$descDiv.html(""),this.currentDescription=-1,this.$status.attr("aria-live","polite"))}},AblePlayer.prototype.syncSpeechToPlaybackRate=function(t){var e;.5===t?e=.7:.75===t?e=.8:1===t?e=1:1.25===t?e=1.1:1.5===t?e=1.2:1.75===t?e=1.5:2===t?e=2:2.25===t?e=2.5:t>=2.5&&(e=3),this.prefDescRate=e},AblePlayer.prototype.announceDescriptionText=function(e,i){var s,a,n,r,o,h,l,p,c,u;if(s=this,this.speechEnabled||this.initSpeech("desc"),"sample"===e?(a=t("#"+this.mediaId+"_prefDescVoice").val(),o=t("#"+this.mediaId+"_prefDescPitch").val(),h=t("#"+this.mediaId+"_prefDescRate").val(),l=t("#"+this.mediaId+"_prefDescVolume").val()):(a=this.prefDescVoice,o=this.prefDescPitch,h=this.prefDescRate,l=this.prefDescVolume),this.descVoices&&this.descVoices.length>0){if(a){for(n=0;n<this.descVoices.length;n++)if(this.descVoices[n].name==a){r=this.descVoices[n];break}}void 0===r&&(r=this.descVoices[0]),(p=new SpeechSynthesisUtterance).voice=r,p.voiceURI="native",p.volume=l,p.rate=h,p.pitch=o,p.text=i,p.lang=this.lang,p.onstart=function(t){},p.onpause=function(t){},p.onend=function(t){this.speakingDescription=!1,u=(c=t.elapsedTime)>100?(t.elapsedTime/1e3).toFixed(2):t.elapsedTime.toFixed(2),this.debug&&console.log("Finished speaking. That took "+u+" seconds."),"description"===e&&s.prefDescPause&&s.pausedForDescription&&(s.playMedia(),this.pausedForDescription=!1)},p.onerror=function(t){console.log("Web Speech API error",t)},this.synth.paused&&this.synth.resume(),this.synth.speak(p),this.speakingDescription=!0}}}(jQuery),jQuery,AblePlayer.prototype.isIOS=function(t){var e,i;return e=navigator.userAgent.toLowerCase(),!!(i=/ipad|iphone|ipod/.exec(e))&&(void 0===t||-1!==e.indexOf("os "+t))},AblePlayer.prototype.browserSupportsVolume=function(){var t,e;return!this.isIOS()&&(e=.9,(t=new Audio).volume=e,t.volume===e)},AblePlayer.prototype.nativeFullscreenSupported=function(){return document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullscreenEnabled||document.msFullscreenEnabled},function(t){AblePlayer.prototype.seekTo=function(t){var e,i=this;this.seekFromTime=this.media.currentTime,this.seekToTime=t,this.seeking=!0,this.liveUpdatePending=!0,this.speakingDescription&&this.synth.cancel(),"html5"===this.player?(this.startTime=t,(e=this.media.seekable).length>0&&this.startTime>=e.start(0)&&this.startTime<=e.end(0)&&(this.media.currentTime=this.startTime,this.seekStatus="complete",this.hasSignLanguage&&this.signVideo&&(this.signVideo.currentTime=this.startTime))):"youtube"===this.player?(this.youTubePlayer.seekTo(t,!0),t>0&&void 0!==this.$posterImg&&this.$posterImg.hide()):"vimeo"===this.player&&this.vimeoPlayer.setCurrentTime(t).then(function(){i.elapsed=t,i.refreshControls("timeline")}),this.refreshControls("timeline")},AblePlayer.prototype.getMediaTimes=function(e,i){var s,a,n,r;return r={},a=(s=new t.Deferred).promise(),n=this,void 0!==e&&void 0!==i?(r.duration=e,r.elapsed=i,s.resolve(r)):this.getDuration().then(function(t){r.duration=n.roundDown(t,6),n.getElapsed().then(function(t){r.elapsed=n.roundDown(t,6),s.resolve(r)})}),a},AblePlayer.prototype.getDuration=function(){var e,i,s,a;return i=(e=new t.Deferred).promise(),s=this,"vimeo"===this.player?this.vimeoPlayer?this.vimeoPlayer.getDuration().then(function(t){void 0===t||isNaN(t)||-1===t?e.resolve(0):e.resolve(t)}):e.resolve(0):("html5"===this.player?a=this.media.duration:"youtube"===this.player&&(a=this.youTubePlayerReady?this.duration>0?this.duration:this.youTubePlayer.getDuration():0),void 0===a||isNaN(a)||-1===a?e.resolve(0):e.resolve(a)),i},AblePlayer.prototype.getElapsed=function(){var e,i,s,a;return i=(e=new t.Deferred).promise(),s=this,"vimeo"===this.player?this.vimeoPlayer?this.vimeoPlayer.getCurrentTime().then(function(t){void 0===t||isNaN(t)||-1===t?e.resolve(0):e.resolve(t)}):e.resolve(0):("html5"===this.player?a=this.media.currentTime:"youtube"===this.player&&(a=this.youTubePlayerReady?this.youTubePlayer.getCurrentTime():0),void 0===a||isNaN(a)||-1===a?e.resolve(0):e.resolve(a)),i},AblePlayer.prototype.getPlayerState=function(){var e,i,s;if(i=(e=new t.Deferred).promise(),s=this,"html5"===this.player)this.media.ended?e.resolve("ended"):this.media.paused?e.resolve("paused"):4!==this.media.readyState?e.resolve("buffering"):e.resolve("playing");else if("youtube"===this.player&&this.youTubePlayerReady){var a=this.youTubePlayer.getPlayerState();-1===a||5===a?e.resolve("stopped"):0===a?e.resolve("ended"):1===a?e.resolve("playing"):2===a?e.resolve("paused"):3===a&&e.resolve("buffering")}else"vimeo"===this.player&&this.vimeoPlayer&&this.vimeoPlayer.getPaused().then(function(t){t?e.resolve("paused"):s.vimeoPlayer.getEnded().then(function(t){t?e.resolve("ended"):e.resolve("playing")})});return i},AblePlayer.prototype.isPlaybackRateSupported=function(){return"html5"===this.player?!!this.media.playbackRate:"youtube"===this.player?!!this.youTubePlayerReady&&this.youTubePlayer.getAvailablePlaybackRates().length>1:"vimeo"===this.player?this.vimeoSupportsPlaybackRateChange:void 0},AblePlayer.prototype.setPlaybackRate=function(t){t=Math.max(.5,t),this.hasClosedDesc&&"text"===this.descMethod&&this.syncSpeechToPlaybackRate(t),"html5"===this.player?this.media.playbackRate=t:"youtube"===this.player?this.youTubePlayer.setPlaybackRate(t):"vimeo"===this.player&&this.vimeoPlayer.setPlaybackRate(t),this.hasSignLanguage&&this.signVideo&&(this.signVideo.playbackRate=t),this.playbackRate=t,this.$speed.text(this.tt.speed+": "+t.toFixed(2).toString()+"x")},AblePlayer.prototype.getPlaybackRate=function(){return"html5"===this.player?this.media.playbackRate:"youtube"===this.player&&this.youTubePlayerReady?this.youTubePlayer.getPlaybackRate():void 0},AblePlayer.prototype.isPaused=function(){if("vimeo"===this.player)return!this.playing;this.getPlayerState().then(function(t){return"paused"===t||"stopped"===t||"ended"===t})},AblePlayer.prototype.pauseMedia=function(){"html5"===this.player?(this.media.pause(!0),this.hasSignLanguage&&this.signVideo&&this.signVideo.pause(!0)):"youtube"===this.player?this.youTubePlayer.pauseVideo():"vimeo"===this.player&&this.vimeoPlayer.pause()},AblePlayer.prototype.playMedia=function(){"html5"===this.player?(this.media.play(!0),this.hasSignLanguage&&this.signVideo&&this.signVideo.play(!0)):"youtube"===this.player?(this.youTubePlayer.playVideo(),void 0!==this.$posterImg&&this.$posterImg.hide(),this.stoppingYouTube=!1):"vimeo"===this.player&&this.vimeoPlayer.play(),this.startedPlaying=!0,this.hideControls&&(this.hidingControls=!0,this.invokeHideControlsTimeout())},AblePlayer.prototype.fadeControls=function(t){var e,i,s;"out"==t?(s=(e=this.$mediaContainer.height())+(i=this.$playerDiv.height()),this.$playerDiv.fadeTo(2e3,0,function(){})):"in"==t&&this.$playerDiv.fadeTo(100,1)},AblePlayer.prototype.invokeHideControlsTimeout=function(){var t=this;this.hideControlsTimeout=window.setTimeout(function(){void 0!==t.playing&&!0===t.playing&&t.hideControls&&(t.fadeControls("out"),t.controlsHidden=!0)},5e3),this.hideControlsTimeoutStatus="active"}
16935,AblePlayer.prototype.refreshControls=function(e,i,s){var a,i,n,r,o,h,l,p,c,u,d,f,g,_;if(e="init",a=this,!this.swappingSrc||!this.playing){if("timeline"===e||"init"===e){if(void 0===this.duration)return;this.useChapterTimes&&(this.chapterDuration=this.getChapterDuration(),this.chapterElapsed=this.getChapterElapsed()),!1===this.useFixedSeekInterval&&!1===this.seekIntervalCalculated&&this.duration>0&&this.setSeekInterval(),this.seekBar&&(this.useChapterTimes?(n=this.selectedChapters.cues.length-1,this.selectedChapters.cues[n]==this.currentChapter&&this.currentChapter.end!==this.duration?this.seekBar.setDuration(this.duration-this.currentChapter.start):this.seekBar.setDuration(this.chapterDuration)):void 0===this.duration||isNaN(this.duration)||-1===this.duration||this.seekBar.setDuration(this.duration),this.seekBar.tracking||(o=this.liveUpdatePending||this.seekBar.seekHead.is(t(document.activeElement)),this.liveUpdatePending=!1,this.useChapterTimes?this.seekBar.setPosition(this.chapterElapsed,o):this.seekBar.setPosition(this.elapsed,o)),r=this.seekBar.tracking?this.seekBar.lastTrackPosition:this.useChapterTimes?this.chapterElapsed:this.elapsed),void 0!==this.$durationContainer&&(this.useChapterTimes?this.$durationContainer.text(" / "+this.formatSecondsAsColonTime(this.chapterDuration)):this.$durationContainer.text(" / "+this.formatSecondsAsColonTime(this.duration))),void 0!==this.$elapsedTimeContainer&&this.$elapsedTimeContainer.text(this.formatSecondsAsColonTime(r)),this.skin,"html5"===this.player?this.media.buffered.length>0&&(c=this.media.buffered.end(0),this.useChapterTimes?(c>this.chapterDuration&&(c=this.chapterDuration),this.seekBar&&this.seekBar.setBuffered(c/this.chapterDuration)):this.seekBar&&!isNaN(c)&&this.seekBar.setBuffered(c/i)):"youtube"===this.player?this.seekBar&&this.youTubePlayerReady&&this.seekBar.setBuffered(this.youTubePlayer.getVideoLoadedFraction()):this.player}("descriptions"===e||"init"==e)&&this.$descButton&&(this.descOn?(this.$descButton.removeClass("buttonOff").attr("aria-label",this.tt.turnOffDescriptions),this.$descButton.find("span.able-clipped").text(this.tt.turnOffDescriptions)):(this.$descButton.addClass("buttonOff").attr("aria-label",this.tt.turnOnDescriptions),this.$descButton.find("span.able-clipped").text(this.tt.turnOnDescriptions))),("captions"===e||"init"==e)&&this.$ccButton&&(p=this.captions.length,this.captionsOn?(this.$ccButton.removeClass("buttonOff"),this.$ccButton.attr("aria-pressed","true"),1===p&&(this.$ccButton.attr("aria-label",this.tt.hideCaptions),this.$ccButton.find("span.able-clipped").text(this.tt.hideCaptions))):(this.$ccButton.addClass("buttonOff"),this.$ccButton.attr("aria-pressed","false"),1===p&&(this.$ccButton.attr("aria-label",this.tt.showCaptions),this.$ccButton.find("span.able-clipped").text(this.tt.showCaptions))),p>1&&(this.$ccButton.attr({"aria-label":this.tt.captions,"aria-haspopup":"true","aria-controls":this.mediaId+"-captions-menu"}),this.$ccButton.find("span.able-clipped").text(this.tt.captions))),("fullscreen"===e||"init"==e)&&this.$fullscreenButton&&(this.fullscreen?(this.$fullscreenButton.attr("aria-label",this.tt.exitFullscreen),"font"===this.iconType?(this.$fullscreenButton.find("span").first().removeClass("icon-fullscreen-expand").addClass("icon-fullscreen-collapse"),this.$fullscreenButton.find("span.able-clipped").text(this.tt.exitFullscreen)):"svg"===this.iconType?(_=this.getSvgData("fullscreen-collapse"),this.$fullscreenButton.find("svg").attr("viewBox",_[0]),this.$fullscreenButton.find("path").attr("d",_[1]),this.$fullscreenButton.find("span.able-clipped").text(this.tt.exitFullscreen)):this.$fullscreenButton.find("img").attr("src",this.fullscreenCollapseButtonImg)):(this.$fullscreenButton.attr("aria-label",this.tt.enterFullscreen),"font"===this.iconType?(this.$fullscreenButton.find("span").first().removeClass("icon-fullscreen-collapse").addClass("icon-fullscreen-expand"),this.$fullscreenButton.find("span.able-clipped").text(this.tt.enterFullscreen)):"svg"===this.iconType?(_=this.getSvgData("fullscreen-expand"),this.$fullscreenButton.find("svg").attr("viewBox",_[0]),this.$fullscreenButton.find("path").attr("d",_[1]),this.$fullscreenButton.find("span.able-clipped").text(this.tt.enterFullscreen)):this.$fullscreenButton.find("img").attr("src",this.fullscreenExpandButtonImg))),("playpause"===e||"init"==e)&&void 0!==this.$bigPlayButton&&void 0!==this.seekBar&&(this.paused&&!this.seekBar.tracking?(this.hideBigPlayButton||(this.$bigPlayButton.show(),this.$bigPlayButton.attr("aria-hidden","false")),this.fullscreen?(this.$bigPlayButton.width(t(window).width()),this.$bigPlayButton.height(t(window).height())):(this.$bigPlayButton.width(this.$mediaContainer.width()),this.$bigPlayButton.height(this.$mediaContainer.height()))):(this.$bigPlayButton.hide(),this.$bigPlayButton.attr("aria-hidden","true"))),("transcript"===e||"init"==e)&&this.transcriptType&&(1===this.prefAutoScrollTranscript?(this.autoScrollTranscript=!0,this.$autoScrollTranscriptCheckbox.prop("checked",!0)):(this.autoScrollTranscript=!1,this.$autoScrollTranscriptCheckbox.prop("checked",!1)),this.autoScrollTranscript&&this.currentHighlight&&(u=Math.floor(this.$transcriptDiv.scrollTop()+t(this.currentHighlight).position().top-this.$transcriptDiv.height()/2+t(this.currentHighlight).height()/2))!==Math.floor(this.$transcriptDiv.scrollTop())&&(this.scrollingTranscript=!0,this.movingHighlight&&(this.$transcriptDiv.scrollTop(u),this.movingHighlight=!1))),"init"===e&&this.$chaptersButton&&this.$chaptersButton.attr({"aria-label":this.tt.chapters,"aria-haspopup":"true","aria-controls":this.mediaId+"-chapter
16935s-menu"}),("timeline"===e||"playpause"===e||"init"===e)&&(h={stopped:this.tt.statusStopped,paused:this.tt.statusPaused,playing:this.tt.statusPlaying,buffering:this.tt.statusBuffering,ended:this.tt.statusEnd},this.stoppingYouTube?(this.$status.text()!==this.tt.statusStopped&&this.$status.text(this.tt.statusStopped),this.$playpauseButton.find("span").first().hasClass("icon-pause")&&("font"===this.iconType?(this.$playpauseButton.find("span").first().removeClass("icon-pause").addClass("icon-play"),this.$playpauseButton.find("span.able-clipped").text(this.tt.play)):"svg"===this.iconType?(_=this.getSvgData("play"),this.$playpauseButton.find("svg").attr("viewBox",_[0]),this.$playpauseButton.find("path").attr("d",_[1]),this.$playpauseButton.find("span.able-clipped").text(this.tt.play)):this.$playpauseButton.find("img").attr("src",this.playButtonImg))):void 0!==this.$status&&void 0!==this.seekBar&&this.getPlayerState().then(function(t){a.$status.text()===h[t]||a.seekBar.tracking?(a.statusDebounceStart=null,a.debouncingStatus=!1,clearTimeout(a.statusTimeout),a.statusTimeout=null):(a.swappingSrc?a.debouncingStatus||(a.statusMessageThreshold=2e3):a.debouncingStatus||(a.statusMessageThreshold=250),l=new Date().getTime(),a.statusDebounceStart?l-a.statusDebounceStart>a.statusMessageThreshold&&(a.$status.text(h[t]),a.statusDebounceStart=null,clearTimeout(a.statusTimeout),a.statusTimeout=null):(a.statusDebounceStart=l,a.debouncingStatus=!0,a.statusTimeout=setTimeout(function(){a.debouncingStatus=!1,a.refreshControls(e)},a.statusMessageThreshold))),a.seekBar.tracking||a.stoppingYouTube||("paused"===t||"stopped"===t||"ended"===t?(a.$playpauseButton.attr("aria-label",a.tt.play),"font"===a.iconType?(a.$playpauseButton.find("span").first().removeClass("icon-pause").addClass("icon-play"),a.$playpauseButton.find("span.able-clipped").text(a.tt.play)):"svg"===a.iconType?(_=a.getSvgData("play"),a.$playpauseButton.find("svg").attr("viewBox",_[0]),a.$playpauseButton.find("path").attr("d",_[1]),a.$playpauseButton.find("span.able-clipped").text(a.tt.play)):a.$playpauseButton.find("img").attr("src",a.playButtonImg)):(a.$playpauseButton.attr("aria-label",a.tt.pause),"font"===a.iconType?(a.$playpauseButton.find("span").first().removeClass("icon-play").addClass("icon-pause"),a.$playpauseButton.find("span.able-clipped").text(a.tt.pause)):"svg"===a.iconType?(_=a.getSvgData("pause"),a.$playpauseButton.find("svg").attr("viewBox",_[0]),a.$playpauseButton.find("path").attr("d",_[1]),a.$playpauseButton.find("span.able-clipped").text(a.tt.pause)):a.$playpauseButton.find("img").attr("src",a.pauseButtonImg)))})),!this.fullscreen&&(g=300,d=this.$statusBarDiv.height(),(f=this.$statusBarDiv.find("span.able-speed").height())>d+5?(this.$statusBarDiv.find("span.able-speed").hide(),this.hidingSpeed=!0):(this.hidingSpeed&&(this.$statusBarDiv.find("span.able-speed").show(),this.hidingSpeed=!1),this.$statusBarDiv.width()<g?(this.$statusBarDiv.find("span.able-speed").hide(),this.hidingSpeed=!0):this.hidingSpeed&&(this.$statusBarDiv.find("span.able-speed").show(),this.hidingSpeed=!1)))}},AblePlayer.prototype.getHiddenWidth=function(t){var e=t.clone().appendTo("body"),i=e.outerWidth();return e.remove(),i},AblePlayer.prototype.handlePlay=function(t){this.paused?(this.okToPlay=!0,this.playMedia(),this.synth.paused&&this.synth.resume()):(this.okToPlay=!1,this.pauseMedia(),this.speakingDescription&&this.synth.pause()),null===this.speechEnabled&&this.initSpeech("play")},AblePlayer.prototype.handleRestart=function(){this.speakingDescription&&this.synth.cancel(),this.seekTo(0)},AblePlayer.prototype.handlePrevTrack=function(){0===this.playlistIndex?this.playlistIndex=this.$playlist.length-1:this.playlistIndex--,this.cueingPlaylistItem=!0,this.cuePlaylistItem(this.playlistIndex)},AblePlayer.prototype.handleNextTrack=function(){this.playlistIndex===this.$playlist.length-1?this.playlistIndex=0:this.playlistIndex++,this.cueingPlaylistItem=!0,this.cuePlaylistItem(this.playlistIndex)},AblePlayer.prototype.handleRewind=function(){var t;t=this.elapsed-this.seekInterval,this.useChapterTimes?t<this.currentChapter.start&&(t=this.currentChapter.start):t<0&&(t=0),this.seekTo(t)},AblePlayer.prototype.handleFastForward=function(){var t,e;e=this.chapters.length-1,t=this.elapsed+this.seekInterval,this.useChapterTimes?this.chapters[e]==this.currentChapter?t>this.duration||t>
16935this.currentChapter.end?t=Math.min(this.duration,this.currentChapter.end):this.duration%t<this.seekInterval&&(t=Math.min(this.duration,this.currentChapter.end)):t>this.currentChapter.end&&(t=this.currentChapter.end):t>this.duration&&(t=this.duration),this.seekTo(t)},AblePlayer.prototype.handleRateIncrease=function(){this.changeRate(1)},AblePlayer.prototype.handleRateDecrease=function(){this.changeRate(-1)},AblePlayer.prototype.changeRate=function(t){var e,i,s,a,n,r;"html5"===this.player?this.setPlaybackRate(this.getPlaybackRate()+.25*t):"youtube"===this.player?this.youTubePlayerReady&&(e=this.youTubePlayer.getAvailablePlaybackRates(),i=this.getPlaybackRate(),-1===(s=e.indexOf(i))?console.log("ERROR: Youtube returning unknown playback rate "+i.toString()):(s+=t)<e.length&&s>=0&&this.setPlaybackRate(e[s])):"vimeo"===this.player&&(n=.5,r=2,1===t?a=this.vimeoPlaybackRate+.5<=r?this.vimeoPlaybackRate+.5:r:-1===t&&(a=this.vimeoPlaybackRate-.5>=n?this.vimeoPlaybackRate-.5:n),this.setPlaybackRate(a))},AblePlayer.prototype.handleCaptionToggle=function(){var t,e=this;if(this.hidingPopup)return this.hidingPopup=!1,!1;if(1===(t=this.captions.length?this.captions:[]).length){if(!0===this.captionsOn)this.captionsOn=!1,this.prefCaptions=0,this.$ccButton.attr("aria-pressed","false"),this.updateCookie("prefCaptions"),this.usingYouTubeCaptions?this.youTubePlayer.unloadModule("captions"):this.usingVimeoCaptions?this.vimeoPlayer.disableTextTrack():this.$captionsWrapper.hide();else{this.captionsOn=!0,this.prefCaptions=1,this.$ccButton.attr("aria-pressed","true"),this.updateCookie("prefCaptions"),this.usingYouTubeCaptions?this.youTubePlayer.loadModule("captions"):this.usingVimeoCaptions?this.vimeoPlayer.enableTextTrack(this.captionLang).then(function(t){}).catch(function(t){switch(t.name){case"InvalidTrackLanguageError":case"InvalidTrackError":console.log("No "+track.kind+" track is available in the specified language ("+track.label+")");break;default:console.log("Error loading "+track.label+" "+track.kind+" track")}}):this.$captionsWrapper.show();for(var i=0;i<t.length;i++)!0===t[i].def&&(this.selectedCaptions=t[i]);this.selectedCaptions=this.captions[0],this.descriptions.length>=0&&(this.selectedDescriptions=this.descriptions[0])}this.refreshControls("captions")}else this.captionsPopup&&this.captionsPopup.is(":visible")?(this.captionsPopup.hide(),this.hidingPopup=!1,this.$ccButton.attr("aria-expanded","false"),this.waitThenFocus(this.$ccButton)):(this.closePopups(),this.captionsPopup&&(this.captionsPopup.show(),this.$ccButton.attr("aria-expanded","true"),setTimeout(function(){e.captionsPopup.css("top",e.$ccButton.position().top-e.captionsPopup.outerHeight()),e.captionsPopup.css("left",e.$ccButton.position().left),e.captionsPopup.find("li").removeClass("able-focus"),e.captionsPopup.find("li").first().focus().addClass("able-focus")},50)))},AblePlayer.prototype.waitThenFocus=function(t,e){setTimeout(function(){t.focus()},null==e?50:e)},AblePlayer.prototype.handleChapters=function(){if(this.hidingPopup)return this.hidingPopup=!1,!1;this.chaptersPopup.is(":visible")?(this.chaptersPopup.hide(),this.hidingPopup=!1,this.$chaptersButton.attr("aria-expanded","false").focus()):(this.closePopups(),this.chaptersPopup.show(),this.$chaptersButton.attr("aria-expanded","true"),this.chaptersPopup.css("top",this.$chaptersButton.position().top-this.chaptersPopup.outerHeight()),this.chaptersPopup.css("left",this.$chaptersButton.position().left),this.chaptersPopup.find("li").removeClass("able-focus"),this.chaptersPopup.find('li[aria-checked="true"]').length?this.chaptersPopup.find('li[aria-checked="true"]').focus().addClass("able-focus"):this.chaptersPopup.find("li").first().addClass("able-focus").attr("aria-checked","true").focus())},AblePlayer.prototype.handleDescriptionToggle=function(){this.descOn=!this.descOn,this.prefDesc=+this.descOn,this.updateCookie("prefDesc"),void 0===this.$descDiv||this.$descDiv.is(":hidden")||this.$descDiv.hide(),this.initDescription(),this.refreshControls("descriptions")},AblePlayer.prototype.handlePrefsClick=function(t){var e,i,s,a;if(e=this,null===this.speechEnabled&&this.initSpeech("prefs"),this.hidingPopup)return this.hidingPopup=!1,!1;this.prefsPopup.is(":visible")?(this.prefsPopup.hide(),this.$prefsButton.attr("aria-expanded","fal
16935se"),this.prefsPopup.find("li").removeClass("able-focus").attr("tabindex","-1"),this.showingPrefsDialog||this.$prefsButton.focus(),setTimeout(function(){e.hidingPopup=!1},100)):(this.closePopups(),this.prefsPopup.show(),this.$prefsButton.attr("aria-expanded","true"),this.$prefsButton.focus(),setTimeout(function(){i=e.$prefsButton.position(),a=(s=e.$ableDiv.width()-5)-e.prefsPopup.width(),e.prefsPopup.css("top",i.top-e.prefsPopup.outerHeight()),e.prefsPopup.css("left",a),e.prefsPopup.find("li").removeClass("able-focus").attr("tabindex","0"),e.prefsPopup.find("li").first().focus().addClass("able-focus")},50))},AblePlayer.prototype.handleHelpClick=function(){this.setFullscreen(!1),this.helpDialog.show()},AblePlayer.prototype.handleTranscriptToggle=function(){var t=this;this.$transcriptDiv.is(":visible")?(this.$transcriptArea.hide(),this.$transcriptButton.addClass("buttonOff").attr("aria-label",this.tt.showTranscript),this.$transcriptButton.find("span.able-clipped").text(this.tt.showTranscript),this.prefTranscript=0,this.$transcriptButton.focus().addClass("able-focus"),setTimeout(function(){t.closingTranscript=!1},100)):(this.positionDraggableWindow("transcript"),this.$transcriptArea.show(),this.$transcriptPopup.hide(),this.$transcriptButton.removeClass("buttonOff").attr("aria-label",this.tt.hideTranscript),this.$transcriptButton.find("span.able-clipped").text(this.tt.hideTranscript),this.prefTranscript=1,this.focusNotClick=!0,this.$transcriptArea.find("button").first().focus(),setTimeout(function(){t.focusNotClick=!1},100)),this.updateCookie("prefTranscript")},AblePlayer.prototype.handleSignToggle=function(){var t=this;this.$signWindow.is(":visible")?(this.$signWindow.hide(),this.$signButton.addClass("buttonOff").attr("aria-label",this.tt.showSign),this.$signButton.find("span.able-clipped").text(this.tt.showSign),this.prefSign=0,this.$signButton.focus().addClass("able-focus"),setTimeout(function(){t.closingSign=!1},100)):(this.positionDraggableWindow("sign"),this.$signWindow.show(),this.$signPopup.hide(),this.$signButton.removeClass("buttonOff").attr("aria-label",this.tt.hideSign),this.$signButton.find("span.able-clipped").text(this.tt.hideSign),this.prefSign=1,this.focusNotClick=!0,this.$signWindow.find("button").first().focus(),setTimeout(function(){t.focusNotClick=!1},100)),this.updateCookie("prefSign")},AblePlayer.prototype.isFullscreen=function(){return this.nativeFullscreenSupported()?!!document.fullscreenElement||!!document.webkitFullscreenElement||!!document.webkitCurrentFullscreenElement||!!document.mozFullscreenElement||!!document.msFullscreenElement:!!this.modalFullscreenActive},AblePlayer.prototype.setFullscreen=function(e){if(this.fullscreen!=e){var i=this,s=this.$ableWrapper,a=s[0];if(this.nativeFullscreenSupported())e?(a.requestFullscreen?a.requestFullscreen():a.webkitRequestFullscreen?a.webkitRequestFullscreen():a.mozRequestFullscreen?a.mozRequestFullscreen():a.msRequestFullscreen&&a.msRequestFullscreen(),this.fullscreen=!0):(this.restoringAfterFullscreen=!0,document.exitFullscreen?document.exitFullscreen():document.webkitExitFullscreen?document.webkitExitFullscreen():document.webkitCancelFullscreen?document.webkitCancelFullscreen():document.mozCancelFullscreen?document.mozCancelFullscreen():document.msExitFullscreen&&document.msExitFullscreen(),this.fullscreen=!1);else{if(!this.fullscreenDialog){var n=t("<div>"),r=t("<p>",{class:"able-screenreader-alert"}).text(this.tt.fullscreen);n.append(r),this.fullscreenDialog=new AccessibleDialog(n,this.$fullscreenButton,"dialog",!0,"Fullscreen video player",r,this.tt.exitFullscreen,"100%",!0,function(){i.handleFullscreenToggle()}),t("body").append(n)}var o=this.paused;if(e){this.modalFullscreenActive=!0,this.fullscreenDialog.show(),this.$modalFullscreenPlaceholder=t('<div class="placeholder">'),this.$modalFullscreenPlaceholder.insertAfter(s),s.appendTo(this.fullscreenDialog.modal),s===this.$ableColumnLeft&&s.width("100%");var h=t(window).height()-this.$playerDiv.height();void 0===this.$descDiv||this.$descDiv.is(":hidden")||(h-=this.$descDiv.height())}else this.modalFullscreenActive=!1,s===this.$ableColumnLeft&&s.width("50%"),s.insertAfter(this.$modalFullscreenPlaceholder),this.$modalFullscreenPlaceholder.remove(),this.fullscreenDialog.hide();!o&&this.paused&&this.playMedia()}t(document).on("webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange",function(t){i.fullscreen?i.clickedFullscreenButton||(i.fullscreen=!1,i.restoringAfterFullscreen=!0):i.restoringAfterFullscreen=!0,i.resizePlayer(),i.refreshControl
16935s("fullscreen"),setTimeout(function(){i.clickedFullscreenButton=!1,i.restoringAfterFullscreen=!1},1e3)})}},AblePlayer.prototype.handleFullscreenToggle=function(){var t=this.paused;this.setFullscreen(!this.fullscreen),t?this.pauseMedia():t||this.playMedia(),this.fullscreen?(this.hideControls=!0,this.playing&&(this.fadeControls("out"),this.controlsHidden=!0)):(this.hideControls=this.hideControlsOriginal,this.hideControls||(this.controlsHidden&&(this.fadeControls("in"),this.controlsHidden=!1),"active"!==this.hideControlsTimeoutStatus||(window.clearTimeout(this.hideControlsTimeout),this.hideControlsTimeoutStatus="clear")))},AblePlayer.prototype.handleTranscriptLockToggle=function(t){this.autoScrollTranscript=t,this.prefAutoScrollTranscript=+t,this.updateCookie("prefAutoScrollTranscript"),this.refreshControls("transcript")},AblePlayer.prototype.showTooltip=function(t){t.is(":animated")?t.stop(!0,!0).show():t.stop().show()},AblePlayer.prototype.showAlert=function(t,e){var i,s,a,n,r;i=this,"transcript"===e?(s=this.$transcriptAlert,a=this.$transcriptArea):"sign"===e?(s=this.$signAlert,a=this.$signWindow):s="screenreader"===e?this.$srAlertBox:this.$alertBox,s.text(t).show(),"transcript"==e||"sign"===e?(n=a.width()>s.width()?a.width()/2-s.width()/2:10,"sign"===e?r=a.height()/3*2:"transcript"===e&&(r=this.$transcriptToolbar.height()+30),s.css({top:r+"px",left:n+"px"})):"screenreader"!==e&&s.css({left:this.$playerDiv.width()/2-s.width()/2}),"screenreader"!==e&&setTimeout(function(){s.fadeOut(300)},3e3)},AblePlayer.prototype.showedAlert=function(t){if("transcript"===t){if(this.showedTranscriptAlert)return!0}else if("sign"===t&&this.showedSignAlert)return!0;return!1},AblePlayer.prototype.resizePlayer=function(e,i){var s,a,n,r,o;"audio"!==this.mediaType&&(void 0!==e&&void 0!==i?(this.aspectRatio=i/e,this.playerWidth?(a=this.playerWidth,this.playerHeight?n=this.playerHeight:(n=Math.round(a*this.aspectRatio),this.playerHeight=n)):n=Math.round((a="html5"===this.player?t(window).width():this.$ableWrapper.width())*this.aspectRatio)):this.fullscreen?(this.$ableWrapper.addClass("fullscreen"),a=t(window).width(),n=t(window).height()-this.$playerDiv.outerHeight()-5,this.positionCaptions("overlay")):(this.$ableWrapper.removeClass("fullscreen"),n=Math.round((a="html5"===this.player?this.playerWidth?this.playerWidth:t(window).width():this.$ableWrapper.width())*this.aspectRatio),this.positionCaptions(this.prefCaptionsPosition)),this.debug&&console.log("resizePlayer to "+a+"x"+n),"youtube"===this.player||"vimeo"===this.player?(r=this.$ableWrapper.find("iframe"),"youtube"===this.player&&this.youTubePlayer?this.youTubePlayer.setSize(a,n):r.attr({width:a,height:n}),this.playerWidth&&this.playerHeight&&(this.fullscreen?r.css({"max-width":"","max-height":""}):r.css({"max-width":this.playerWidth+"px","max-height":this.playerHeight+"px"}))):"html5"===this.player&&(this.fullscreen?(this.$media.attr({width:a,height:n}),this.$ableWrapper.css({width:a,height:n})):(this.$media.removeAttr("width height"),this.$ableWrapper.css({width:a+"px",height:"auto"}))),void 0!==this.$captionsDiv&&(s=parseInt(this.prefCaptionsSize,10),this.fullscreen?s=s/100+"vw":s+="%",this.$captionsDiv.css({"font-size":s})),"video"===this.mediaType&&(o=2*Math.round(this.$mediaContainer.height()/3),this.$alertBox.css({top:o+"px"})),this.refreshControls())},AblePlayer.prototype.retrieveOffscreenWindow=function(t,e,i){var s,a,n,r,o,h,l,p;"transcript"==t?s=this.$transcriptArea:"sign"==t&&(s=this.$signWindow),h=s.width(),p=s.height(),n=(a=s.position()).top,o=(r=a.left)+h,l=n+p,n<0&&(n=10,s.css("top",n)),r<0&&(r=10,s.css("left",r)),o>e&&(r=e-20-h,s.css("left",r)),l>i&&(n=i-10-p,s.css("top",n))},AblePlayer.prototype.getHighestZIndex=function(){var e,i,s;return e=0,(i=t("body *").not(".able-modal-dialog,.able-modal-dialog *,.able-modal-overlay,.able-modal-overlay *,.able-sign-window,.able-transcript-area")).each(function(){Number.isInteger(+(s=t(this).css("z-index")))&&parseInt(s)>e&&(e=parseInt(s))}),e},AblePlayer.prototype.updateZIndex=function(t){var e,i,s,a,n,r,o;if(e=8e3,i=7e3,s=this.getHighestZIndex(),void 0===this.$transcriptArea||void 0===this.$signWindow)return void 0!==this.$transcriptArea?(a=parseInt(this.$transcriptArea.css("z-index")))>i&&this.$transcriptArea.css("z-index",i):void 0!==this.$signWindow&&(n=parseInt(this.$signWindow.css("z-index")))>e&&this.$signWindow.css("z-index",e),!1;if((a=parseInt(this.$transcriptArea.css("z-index")))===(n=parseInt(this.$signWindow.css("z-index"))))r=e,o=i;else if(a>n){if("transcript"===t)return!1;r=a,o=n}else{if("sign"===t)return!1;r=n,o=a}"transcript"===t?(this.$transcriptArea.css("z-index",r),this.$signWindow.css("z-index",o)):"sign"===t&&(this.$signWindow.css("z-index",r),this.$transcriptArea.css("z-index",o))},AblePlayer.prototype.syncTrackLanguages=function(t,e){var i,s,a,n,r;for(i=0;i<this.captions.length;i++)this.captions[i].language===e&&(s=this.captions[i]);for(i=0;i<this.chapters.length;i++)this.chapters[i].language===e&&(n=this.chapters[i]);for(i=0;i<this.descriptions.length;i++)this.descriptions[i].language===e&&(a=this.descriptions[i]);for(i=0;i<this.meta.length;i++)this.meta[i].language===e&&(r=this.meta[i]);if(this.transcriptLang=e,"init"===t||"captions"===t?(this.captionLang=e,this.selectedCaptions=s,this.selectedChapters=n,this.selectedDescriptions=a,this.selectedMeta=r,this.transcriptCaptions=s,this.transcriptChapters=n,this.transcriptDescriptions=a,this.updateChaptersList()):"transcript"===t&&(this.transcriptCaptions=s,this.transcriptChapters=n,this.transcriptDescriptions=a),this.selectedDescriptions&&(this.setDescriptionVoice(),this.$sampleDescDiv&&this.sampleText))for(i=0;i<this.sampleText.length;i++)this.sampleText[i].lang===this.selectedDescriptions.language&&(this.currentSampleText=this.sampleText[i].text,this.$sampleDescDiv.html(this.currentSampleText));this.updateTranscript()}}(jQuery),function(t){AblePlayer.prototype.updateCaption=function(t){this.usingYouTubeCaptions||this.usingVimeoCaptions||void 0===this.$captionsWrapper||(this.captionsOn?(this.$captionsWrapper.show(),void 0!==t&&this.showCaptions(t)):this.$captionsWrapper&&(this.$captionsWrapper.hide(),this.prefCaptions=0))},AblePlayer.prototype.updateCaptionsMenu=function(t){this.captionsPopup.find("li").attr("aria-checked","fal
16935se"),void 0===t?this.captionsPopup.find("li").last().attr("aria-checked","true"):this.captionsPopup.find("li[lang="+t+"]").attr("aria-checked","true")},AblePlayer.prototype.getCaptionClickFunction=function(t){var e=this;return function(){e.selectedCaptions=t,e.captionLang=t.language,e.currentCaption=-1,e.usingYouTubeCaptions?e.captionsOn?e.youTubePlayer.getOptions("captions")&&e.startedPlaying?e.youTubePlayer.setOption("captions","track",{languageCode:e.captionLang}):e.captionLangPending=e.captionLang:e.youTubePlayer.getOptions("captions")?e.youTubePlayer.setOption("captions","track",{languageCode:e.captionLang}):(e.youTubePlayer.loadModule("captions"),e.captionLangPending=e.captionLang):e.usingVimeoCaptions?e.vimeoPlayer.enableTextTrack(e.captionLang).then(function(t){}).catch(function(e){switch(e.name){case"InvalidTrackLanguageError":case"InvalidTrackError":console.log("No "+t.kind+" track is available in the specified language ("+t.label+")");break;default:console.log("Error loading "+t.label+" "+t.kind+" track")}}):(e.syncTrackLanguages("captions",e.captionLang),e.swappingSrc||(e.updateCaption(e.elapsed),e.showDescription(e.elapsed))),e.captionsOn=!0,e.hidingPopup=!0,e.captionsPopup.hide(),e.$ccButton.attr("aria-expanded","false"),"audio"===e.mediaType&&e.$captionsContainer.removeClass("captions-off"),setTimeout(function(){e.hidingPopup=!1},100),e.updateCaptionsMenu(e.captionLang),e.waitThenFocus(e.$ccButton),e.prefCaptions=1,e.updateCookie("prefCaptions"),e.refreshControls("captions")}},AblePlayer.prototype.getCaptionOffFunction=function(){var t=this;return function(){"youtube"==t.player?t.youTubePlayer.unloadModule("captions"):t.usingVimeoCaptions&&t.vimeoPlayer.disableTextTrack(),t.captionsOn=!1,t.currentCaption=-1,"audio"===t.mediaType&&t.$captionsContainer.addClass("captions-off"),t.hidingPopup=!0,t.captionsPopup.hide(),t.$ccButton.attr("aria-expanded","false"),setTimeout(function(){t.hidingPopup=!1},100),t.updateCaptionsMenu(),t.waitThenFocus(t.$ccButton),t.prefCaptions=0,t.updateCookie("prefCaptions"),this.swappingSrc||(t.refreshControls("captions"),t.updateCaption())}},AblePlayer.prototype.showCaptions=function(t){var e,i,s,a;for(e=0,a=this.selectedCaptions.cues.length?this.selectedCaptions.cues:this.captions.length>=1?this.captions[0].cues:[];e<a.length;e++)if(a[e].start<=t&&a[e].end>t){i=e;break}void 0!==i?this.currentCaption!==i&&(s=this.flattenCueForCaption(a[i]).replace("\n","<br>"),this.$captionsDiv.html(s),this.currentCaption=i,0===s.length?this.$captionsDiv.css("display","none"):this.$captionsDiv.css("display","inline-block")):(this.$captionsDiv.html("").css("display","none"),this.currentCaption=-1)},AblePlayer.prototype.flattenCueForCaption=function(t){var e=[],i=function(t){var e,s=[];if("string"===t.type)s.push(t.value);else if("v"===t.type)for(s.push("("+t.value+")"),e=0;e<t.children.length;e++)s.push(i(t.children[e]));else if("i"===t.type){for(s.push("<em>"),e=0;e<t.children.length;e++)s.push(i(t.children[e]));s.push("</em>")}else if("b"===t.type){for(s.push("<strong>"),e=0;e<t.children.length;e++)s.push(i(t.children[e]));s.push("</strong>")}else for(e=0;e<t.children.length;e++)s.push(i(t.children[e]));return s.join("")};if(void 0!==t.components)for(var s=0;s<t.components.children.length;s++)e.push(i(t.components.children[s]));return e.join("")},AblePlayer.prototype.getCaptionsOptions=function(t){var e=[];switch(t){case"prefCaptionsFont":e[0]=["serif",this.tt.serif],e[1]=["sans-serif",this.tt.sans],e[2]=["cursive",this.tt.cursive],e[3]=["fantasy",this.tt.fantasy],e[4]=["monospace",this.tt.monospace];break;case"prefCaptionsColor":case"prefCaptionsBGColor":e[0]=["white",this.tt.white],e[1]=["yellow",this.tt.yellow],e[2]=["green",this.tt.green],e[3]=["cyan",this.tt.cyan],e[4]=["blue",this.tt.blue],e[5]=["magenta",this.tt.magenta],e[6]=["red",this.tt.red],e[7]=["black",this.tt.black];break;case"prefCaptionsSize":e[0]="75%",e[1]="100%",e[2]="125%",e[3]="150%",e[4]="200%";break;case"prefCaptionsOpacity":e[0]="0%",e[1]="25%",e[2]="50%",e[3]="75%",e[4]="100%";break;case"prefCaptionsStyle":e[0]=this.tt.captionsStylePopOn,e[1]=this.tt.captionsStyleRollUp;break;case"prefCaptionsPosition":e[0]="overlay",e[1]="below"}return e}
vendor: 4,657 bytes, line 16935
16935,AblePlayer.prototype.translatePrefs=function(t,e,i){if("youtube"==i&&"size"===t)switch(e){case"75%":return -1;case"100%":return 0;case"125%":return 1;case"150%":return 2;case"200%":return 3}return!1},AblePlayer.prototype.stylizeCaptions=function(e,i){var s,a,n;void 0!==e&&("prefCaptionsPosition"==i?this.positionCaptions():void 0!==i?("prefCaptionsFont"===i?s="font-family":"prefCaptionsSize"===i?s="font-size":"prefCaptionsColor"===i?s="color":"prefCaptionsBGColor"===i?s="background-color":"prefCaptionsOpacity"===i&&(s="opacity"),a="prefCaptionsOpacity"===i?parseFloat(t("#"+this.mediaId+"_"+i).val())/100:t("#"+this.mediaId+"_"+i).val(),e.css(s,a)):(n=parseFloat(this.prefCaptionsOpacity)/100,e.css({"font-family":this.prefCaptionsFont,color:this.prefCaptionsColor,"background-color":this.prefCaptionsBGColor,opacity:n}),e===this.$captionsDiv&&void 0!==this.$captionsDiv&&this.$captionsDiv.css({"font-size":this.prefCaptionsSize}),"below"===this.prefCaptionsPosition?void 0!==this.$captionsWrapper&&this.$captionsWrapper.css({"background-color":this.prefCaptionsBGColor,opacity:"1"}):"overlay"===this.prefCaptionsPosition&&void 0!==this.$captionsWrapper&&this.$captionsWrapper.css({"background-color":"transparent",opacity:""}),this.positionCaptions()))},AblePlayer.prototype.positionCaptions=function(t){void 0===t&&(t=this.prefCaptionsPosition),void 0!==this.$captionsWrapper&&("below"==t?(this.$captionsWrapper.removeClass("able-captions-overlay").addClass("able-captions-below"),this.$captionsWrapper.css({"background-color":this.prefCaptionsBGColor,opacity:"1"})):(this.$captionsWrapper.removeClass("able-captions-below").addClass("able-captions-overlay"),this.$captionsWrapper.css({"background-color":"transparent",opacity:""})))}}(jQuery),function(t){AblePlayer.prototype.populateChaptersDiv=function(){var e,i,s,a;t("#"+this.chaptersDivLocation)&&(this.$chaptersDiv=t("#"+this.chaptersDivLocation),this.$chaptersDiv.addClass("able-chapters-div"),this.$chaptersDiv.empty(),this.chaptersTitle&&(a=t("<"+(i="h"+(e=this.getNextHeadingLevel(this.$chaptersDiv)).toString())+">",{class:"able-chapters-heading",id:s=this.mediaId+"-chapters-heading"}).text(this.chaptersTitle),this.$chaptersDiv.append(a)),this.$chaptersNav=t("<nav>"),this.chaptersTitle?this.$chaptersNav.attr("aria-labelledby",s):this.$chaptersNav.attr("aria-label",this.tt.chapters),this.$chaptersDiv.append(this.$chaptersNav),this.updateChaptersList())},AblePlayer.prototype.updateChaptersList=function(){var e,i,s,a,n,r,o,h,l,p,c;if(e=this,!this.$chaptersNav)return!1;if(void 0===this.useChapterTimes&&("chapter"===this.seekbarScope&&this.selectedChapters.cues.length?this.useChapterTimes=!0:this.useChapterTimes=!1),(i=this.useChapterTimes?this.selectedChapters.cues:this.chapters.length>=1?this.chapters[0].cues:[]).length>0){for(a=0,s=t("<ul>");a<i.length;a++)n=a,r=t("<li></li>"),o=t("<button>",{type:"button",val:n}).text(this.flattenCueForCaption(i[n])),l=function(i){return function(){e.seekTrigger="chapter",p=t(this).closest("li"),c=(s=t(this).closest("ul").find("li")).index(p),s.removeClass("able-current-chapter").children("button").removeAttr("aria-current"),p.addClass("able-current-chapter").children("button").attr("aria-current","true"),e.updateChapter(i),e.seekTo(i)}},o.on("click",l(i[n].start)),o.on("focus",function(){t(this).closest("ul").find("li").removeClass("able-focus"),t(this).closest("li").addClass("able-focus")}),r.on("hover",function(){t(this).closest("ul").find("li").removeClass("able-focus"),t(this).addClass("able-focus")}),r.on("mouseleave",function(){t(this).removeClass("able-focus")}),o.on("blur",function(){t(this).closest("li").removeClass("able-focus")}),r.append(o),s.append(r),this.defaultChapter===i[n].id&&(o.attr("aria-current","true").parent("li").addClass("able-current-chapter"),this.currentChapter=i[n],h=!0);h||(this.currentChapter=i[0],s.find("button").first().attr("aria-current","true").parent("li").addClass("able-current-chapter")),this.$chaptersNav.html(s)}return!1},AblePlayer.prototype.seekToChapter=function(t){for(var e=0;e<this.selectedChapters.cues.length;){if(this.selectedChapters.cues[e].id==t){this.seekTo(this.selectedChapters.cues[e].start),this.updateChapter(this.selectedChapters.cues[e].start);break}e++}},AblePlayer.prototype.updateChapter=function(t){var e,i,s;if(void 0!==this.selectedChapters){for(i=0,e=this.selectedChapters.cues;i<e.length;i++)if(e[i].start<=t&&e[i].end>t){s=i;break}void 0!==s&&this.currentChapter!==e[s]&&(this.currentChapter=e[s],this.useChapterTimes&&(this.chapterDuration=this.getChapterDuration(),this.seekIntervalCalculated=!1),void 0!==this.$chaptersDiv&&(this.$chaptersDiv.f
16935ind("ul").find("li").removeClass("able-current-chapter").children("button").removeAttr("aria-current"),this.$chaptersDiv.find("ul").find("li").eq(s).addClass("able-current-chapter").children("button").attr("aria-current","true")))}},AblePlayer.prototype.getChapterDuration=function(){var t,e;return void 0===this.currentChapter||void 0===this.duration?0:(t=this.selectedChapters.cues.length-1,this.selectedChapters.cues[t]==this.currentChapter&&this.currentChapter.end!==this.duration?(e=this.duration,this.currentChapter.end=this.duration):e=this.currentChapter.end,e-this.currentChapter.start)},AblePlayer.prototype.getChapterElapsed=function(){return void 0===this.currentChapter?0:this.elapsed>this.currentChapter.start?this.elapsed-this.currentChapter.start:0},AblePlayer.prototype.convertChapterTimeToVideoTime=function(t){if(void 0===this.currentChapter)return t;var e=this.currentChapter.start+t;return e>this.currentChapter.end?this.currentChapter.end:e},AblePlayer.prototype.getChapterClickFunction=function(t){var e=this;return function(){e.seekTrigger="chapter",e.seekTo(t),e.hidingPopup=!0,e.chaptersPopup.hide(),setTimeout(function(){e.hidingPopup=!1},100),e.$chaptersButton.focus()}}}(jQuery),function(t){AblePlayer.prototype.updateMeta=function(t){this.hasMeta&&("text"===this.metaType&&this.$metaDiv.show(),this.showMeta(t||this.elapsed))},AblePlayer.prototype.showMeta=function(e){var i,s,a,n,r,o,h,l,p,c;for(s=0,i=[],n=this.meta.length>=1?this.meta:[];s<n.length;s++)if(n[s].start<=e&&n[s].end>e){a=s;break}if(void 0!==a){if(this.currentMeta!==a){if("text"===this.metaType)this.$metaDiv.html(this.flattenCueForMeta(n[a]).replace("\n","<br>"));else if("selector"===this.metaType){for(h=0,o=(r=this.flattenCueForMeta(n[a])).split("\n");h<o.length;h++)"pause"===(l=t.trim(o[h])).toLowerCase().trim()?(this.hideBigPlayButton=!0,this.pauseMedia()):"focus:"==l.toLowerCase().substring(0,6)?t(c=l.substring(6).trim()).length&&t(c).focus():t(l).length&&(this.currentMeta=a,void 0===(p=parseInt(t(l).attr("data-duration")))||isNaN(p)?t(l).show():t(l).show().delay(p).fadeOut(),this.visibleSelectors.push(l),i.push(l));if(this.visibleSelectors&&this.visibleSelectors.length&&this.visibleSelectors.length!==i.length)for(h=this.visibleSelectors.length-1;h>=0;h--)-1==t.inArray(this.visibleSelectors[h],i)&&(t(this.visibleSelectors[h]).hide(),this.visibleSelectors.splice(h,1))}}}else{if(void 0!==this.$metaDiv&&this.$metaDiv.html(""),this.visibleSelectors&&this.visibleSelectors.length){for(h=0;h<this.visibleSelectors.length;h++)t(this.visibleSelectors[h]).hide();this.visibleSelectors=[]}this.currentMeta=-1}},AblePlayer.prototype.flattenCueForMeta=function(t){for(var e=[],i=function(t){var e,s=[];if("string"===t.type)s.push(t.value);else if("v"===t.type)for(s.push("["+t.value+"]"),e=0;e<t.children.length;e++)s.push(i(t.children[e]));else for(e=0;e<t.children.length;e++)s.push(i(t.children[e]));return s.join("")},s=0;s<t.components.children.length;s++)e.push(i(t.components.children[s]));return e.join("")}}(jQuery),function(t){AblePlayer.prototype.setupTranscript=function(){var e=new t.Deferred,i=e.promise();return this.usingYouTubeCaptions||this.usingVimeoCaptions?(this.transcriptType=null,e.resolve()):(!this.transcriptType&&this.captions.length&&(this.transcriptType="popup"),this.transcriptType?"popup"===this.transcriptType||"external"===this.transcriptType?(this.injectTranscriptArea(),e.resolve()):"manual"===this.transcriptType&&(this.setupManualTranscript(),e.resolve()):e.resolve()),i},AblePlayer.prototype.injectTranscriptArea=function(){var e,i,s,a,n,r;if(e=this,this.$transcriptArea=t("<div>",{class:"able-transcript-area",role:"dialog","aria-label":this.tt.transcriptTitle}),this.$transcriptToolbar=t("<div>",{class:"able-window-toolbar able-"+this.toolbarIconColor+"-controls"}),this.$transcriptDiv=t("<div>",{class:"able-transcript"}),this.$autoScrollTranscriptCheckbox=t("<input>",{id:"autoscroll-transcript-checkbox-"+this.mediaId,type:"checkbox"}),i=t("<label>",{for:"autoscroll-transcript-checkbox-"+this.mediaId}).text(this.tt.autoScroll),this.$transcriptToolbar.append(i,this.$autoScrollTranscriptCheckbox),this.captions.length>1)for(n=0,s=t("<div>",{class:"transcript-language-select-wrapper"}),a=t("<label>",{for:"transcript-language-select-"+this.mediaId}).text(this.tt.language),this.$transcriptLanguageSelect=t("<select>",{id:"transcript-language-select-"+this.mediaId});n<this.captions.length;n++)r=t("<option></option>",{value:this.captions[n].language,lang:this.captions[n].language}).text(this.captions[n].label),this.captions[n].def&&r.prop("selected",!0),this.$transcriptLanguageSelect.append(r);s&&(s.append(a,this.$transcriptLanguageSelect),this.$transcriptToolbar.append(s)),this.$transcriptArea.append(this.$transcriptToolbar,this.$transcriptDiv),this.transcriptDivLocation?t("#"+this.transcriptDivLocation).append(this.$transcriptArea):this.$ableWrapper.append(this.$transcriptArea),this.transcriptDivLocation||(this.initDragDrop("transcript"),1===this.prefTranscript&&this.positionDraggableWindow("transcript",this.getDefaultWidth("transcript"))),this.prefTranscript||this.transcriptDivLocation||this.$transcriptArea.hide()},AblePlayer.prototype.addTranscriptAreaEvents=function(){var t=this;this.$autoScrollTranscriptCheckbox.click(function(){t.handleTranscriptLockToggle(t.$autoScrollTranscriptCheckbox.prop("checked"))}),this.$transcriptDiv.on("mousewheel DOMMouseScroll click scroll",function(e){t.scrollingTranscript||(t.autoScrollTranscript=!1,t.refreshControl
vendor: 4,321 bytes, line 16935
16935s("transcript")),t.scrollingTranscript=!1}),void 0!==this.$transcriptLanguageSelect&&(this.$transcriptLanguageSelect.on("click mousedown",function(t){t.stopPropagation()}),this.$transcriptLanguageSelect.on("change",function(){var e=t.$transcriptLanguageSelect.val();t.syncTrackLanguages("transcript",e)}))},AblePlayer.prototype.transcriptSrcHasRequiredParts=function(){return!!t("#"+this.transcriptSrc).length&&(this.$transcriptArea=t("#"+this.transcriptSrc),!!this.$transcriptArea.find(".able-window-toolbar").length&&(this.$transcriptToolbar=this.$transcriptArea.find(".able-window-toolbar").eq(0),!!this.$transcriptArea.find(".able-transcript").length&&(this.$transcriptDiv=this.$transcriptArea.find(".able-transcript").eq(0),!!this.$transcriptArea.find(".able-transcript-seekpoint").length)))&&(this.$transcriptSeekpoints=this.$transcriptArea.find(".able-transcript-seekpoint"),!0)},AblePlayer.prototype.setupManualTranscript=function(){var e,i;e=t("<input>",{id:"autoscroll-transcript-checkbox-"+this.mediaId,type:"checkbox"}),i=t("<label>",{for:"autoscroll-transcript-checkbox-"+this.mediaId}).text(this.tt.autoScroll),this.$autoScrollTranscriptCheckbox=e,this.$transcriptToolbar.append(i,this.$autoScrollTranscriptCheckbox)},AblePlayer.prototype.updateTranscript=function(){if(this.transcriptType&&(!this.playerCreated||this.$transcriptArea)){if("external"===this.transcriptType||"popup"===this.transcriptType){if(this.transcriptLang?i=this.transcriptCaptions.cues:this.transcriptCaptions?(this.transcriptLang=this.transcriptCaptions.language,i=this.transcriptCaptions.cues):this.selectedCaptions&&(this.transcriptLang=this.captionLang,i=this.selectedCaptions.cues),this.transcriptChapters)e=this.transcriptChapters.cues;else if(this.chapters.length>0){if(this.transcriptLang)for(var e,i,s,a=0;a<this.chapters.length;a++)this.chapters[a].language===this.transcriptLang&&(e=this.chapters[a].cues);void 0===e&&(e=this.chapters[0].cues||[])}if(this.transcriptDescriptions)s=this.transcriptDescriptions.cues;else if(this.descriptions.length>0){if(this.transcriptLang)for(var a=0;a<this.descriptions.length;a++)this.descriptions[a].language===this.transcriptLang&&(s=this.descriptions[a].cues);s||(s=this.descriptions[0].cues||[])}var n=this.generateTranscript(e||[],i||[],s||[]);this.$transcriptDiv.html(n),this.$transcriptLanguageSelect&&(this.$transcriptLanguageSelect.find("option:selected").prop("selected",!1),this.$transcriptLanguageSelect.find("option[lang="+this.transcriptLang+"]").prop("selected",!0))}var r=this;1===this.prefTabbable&&this.$transcriptDiv.find("span.able-transcript-seekpoint").attr("tabindex","0"),this.$transcriptArea.length>0&&this.$transcriptArea.find("span.able-transcript-seekpoint").click(function(e){r.seekTrigger="transcript";var i=parseFloat(t(this).attr("data-start"));i+=.01,r.seekingFromTranscript?r.seekingFromTranscript=!1:(r.seekingFromTranscript=!0,r.seekTo(i))})}},AblePlayer.prototype.highlightTranscript=function(e){if(this.transcriptType){var i,s,a,n=this;e=parseFloat(e),this.$transcriptArea.find("span.able-transcript-seekpoint").each(function(){if(i=parseFloat(t(this).attr("data-start")),s=parseFloat(t(this).attr("data-end")),a=!!t(this).parent().hasClass("able-transcript-chapter-heading"),e>=i&&e<=s&&!a)return t(this).hasClass("able-highlight")||(n.$transcriptArea.find(".able-highlight").removeClass("able-highlight"),t(this).addClass("able-highlight"),n.movingHighlight=!0),!1}),n.currentHighlight=n.$transcriptArea.find(".able-highlight"),0===n.currentHighlight.length&&(n.currentHighlight=null)}},AblePlayer.prototype.generateTranscript=function(e,i,s){var a,n=this,r=t('<div class="able-transcript-container"></div>');if(r.attr("lang",this.transcriptLang),a=void 0!==this.transcriptTitle?this.transcriptTitle:this.lyricsMode?this.tt.lyricsTitle:this.tt.transcriptTitle,!this.transcriptDivLocation){var o=this.playerHeadingLevel,h=(o+=1)+1;if(o<=6)var l="h"+o.toString();else var l="div";var p=t("<"+l+">");p.addClass("able-transcript-heading"),o>6&&p.attr({role:"heading","aria-level":o}),p.text(a),p.attr("lang",this.lang),r.append(p)}for(var c=0,u=0,d=0,f=function(e,i){if(h<=6)var s="h"+h.toString();else var s="div";var a=t("<"+s+">",{class:"able-transcript-chapter-heading"});h>6&&a.attr({role:"heading","aria-level":h});for(var n=function(t){var e=[];
16935if("string"===t.type)e.push(t.value);else for(var i=0;i<t.children.length;i++)e=e.concat(n(t.children[i]));return e},r=t("<span>",{class:"able-transcript-seekpoint"}),o=0;o<i.components.children.length;o++)for(var l=n(i.components.children[o]),p=0;p<l.length;p++)r.append(l[p]);r.attr("data-start",i.start.toString()),r.attr("data-end",i.end.toString()),a.append(r),e.append(a)},g=function(e,i){var s=t("<div>",{class:"able-transcript-desc"}),a=t("<span>",{class:"able-hidden"});a.attr("lang",n.lang),a.text(n.tt.prefHeadingDescription+": "),s.append(a);for(var r=function(t){var e=[];if("string"===t.type)e.push(t.value);else for(var i=0;i<t.children.length;i++)e=e.concat(r(t.children[i]));return e},o=t("<span>",{class:"able-transcript-seekpoint"}),h=0;h<i.components.children.length;h++)for(var l=r(i.components.children[h]),p=0;p<l.length;p++)o.append(l[p]);o.attr("data-start",i.start.toString()),o.attr("data-end",i.end.toString()),s.append(o),e.append(s)},_=function(e,i){for(var s=t("<span>",{class:"able-transcript-seekpoint able-transcript-caption"}),a=function(e){var i=[],s=0,n=function(t){s++;var e,i=[];if(""===t)return i;var a=t.indexOf("["),r=t.indexOf("]"),o=t.indexOf("("),h=t.indexOf(")"),l=-1!==a&&-1!==r,p=-1!==o&&-1!==h;if(p||l){if(s>1)var c="<br/>";else var c="";var u=c+'<span class="able-unspoken">',d="</span>";p&&l&&(a<o?p=!1:l=!1)}return p?(e=t.substring(0,o),e+=u,e+=t.substring(o,h+1),e+=d,i.push(e+=n(t.substring(h+1)))):l?(e=t.substring(0,a),e+=u,e+=t.substring(a,r+1),e+=d,i.push(e+=n(t.substring(r+1)))):i.push(t),i};if("string"===e.type)i=i.concat(n(e.value));else if("v"===e.type){var r=t("<span>",{class:"able-unspoken"});r.text("("+e.value+")"),i.push(r);for(var o=0;o<e.children.length;o++)for(var h=a(e.children[o]),l=0;l<h.length;l++)i.push(h[l])}else if("b"===e.type||"i"===e.type){if("b"===e.type)var p=t("<strong>");else if("i"===e.type)var p=t("<em>");for(var o=0;o<e.children.length;o++)for(var h=a(e.children[o]),l=0;l<h.length;l++)p.append(h[l]);("b"===e.type||"i"==e.type)&&i.push(p," ")}else for(var o=0;o<e.children.length;o++)i=i.concat(a(e.children[o]));return i},r=0;r<i.components.children.length;r++)for(var o=a(i.components.children[r]),h=0;h<o.length;h++){var l=o[h];"string"==typeof l&&(n.lyricsMode?l=l.replace("\n","<br>")+"<br>":l+=" "),s.append(l)}s.attr("data-start",i.start.toString()),s.attr("data-end",i.end.toString()),e.append(s),e.append(" \n")};c<e.length||d<s.length||u<i.length;){if(c<e.length&&d<s.length&&u<i.length)var v=Math.min(e[c].start,s[d].start,i[u].start);else if(c<e.length&&d<s.length)var v=Math.min(e[c].start,s[d].start);else if(c<e.length&&u<i.length)var v=Math.min(e[c].start,i[u].start);else if(d<s.length&&u<i.length)var v=Math.min(s[d].start,i[u].start);else var v=null;null!==v?void 0!==e[c]&&e[c].start===v?(f(r,e[c]),c+=1):void 0!==s[d]&&s[d].start===v?(g(r,s[d]),d+=1):(_(r,i[u]),u+=1):c<e.length?(f(r,e[c]),c+=1):d<s.length?(g(r,s[d]),d+=1):u<i.length&&(_(r,i[u]),u+=1)}var m=r.children(),y=0;return m.each(function(){t(this).hasClass("able-transcript-caption")?((-1!==t(this).text().indexOf("[")||-1!==t(this).text().indexOf("("))&&y>0&&(r.find(".able-block-temp").removeClass("able-block-temp").wrapAll('<div class="able-transcript-block"></div>'),y=0),t(this).addClass("able-block-temp"),y++):y>0&&(r.find(".able-block-temp").removeClass("able-block-temp").wrapAll('<div class="able-transcript-block"></div>'),y=0)}),r}}(jQuery),function(t){AblePlayer.prototype.showSearchResults=function(){var e=this;if(this.searchDiv&&this.searchString&&t("#"+this.SearchDiv)){var i="<p>"+this.tt.resultsSummary1+" ";i+='<span id="able-search-term-echo">'+this.searchString+"</span>",i+="</p>";var s=this.searchFor(this.searchString,this.searchIgnoreCaps);if(s.length>0){var a=t("<p>",{class:"able-search-results-summary"}),n=this.tt.resultsSummary2;n+=" <strong>"+s.length+"</strong> ",n+=this.tt.resultsSummary3+" ",n+=this.tt.resultsSummary4,a.html(n);for(var r=t("<ul>"),o=0;o<s.length;o++){var h="aria-search-result-"+o,l=t("<li>",{}),p=this.secondsToTime(s[o].start),c=this.tt.searchButtonLabel+" "+p.title,u=t("<button>",{class:"able-search-results-time","data-start":s[o].start,title:c,"aria-label":c,"aria-describedby":h});u.text(p.value),u.on("click",function(i){e.seekTrigger="search";var s=parseFloat(t(this).attr("data-start"));s+=.01,e.seeking=!0,e.seekTo(s)});var d=t("<span>",{class:"able-search-result-text",id:h});d.html("..."+s[o].caption+"..."),l.append(u,d),r.append(l)}t("#"+this.searchDiv).html(i).append(a,r)}else{var f=t("<p>").text(this.tt.noResultsFound);t("#"+this.searchDiv).html(i).append(f)}}},AblePlayer.prototype.searchFor=function(e,i){n=[];var s,a,n,r,o,h,l,p=e.split(" ");if(this.captions.length>0){for(h=0;h<this.captions.length;h++)this.captions[h].language===this.searchLang&&(s=this.searchLang,a=this.captions[h].cues);if(a.length>0){for(h=0,o=0;h<a.length;h++)if(-1!==t.inArray(a[h].components.children[0].type,["string","i","b","u","v","c"])){r=this.flattenCueForCaption(a[h]);var c=i?r.toLowerCase():r;for(l=0;l<p.length;l++){var u=i?p[l].toLowerCase():p[l];if(-1!==c.indexOf(u)){n[o]=[],n[o].start=a[h].start,n[o].lang=s,n[o].caption=this.highlightSearchTerm(p,r),o++;break}}}}}return n},AblePlayer.prototype.highlightSearchTerm=function(t,e){return t.forEach(function(t){var i=RegExp(t,"gi");e=e.replace(i,'<span class="able-search-term">$&</span>')}),e},AblePlayer.prototype.secondsToTime=function(t){var t=Math.floor(t),e=parseInt(t/3600,10)%24,i=parseInt(t/60,10)%60,s=t%60,a="",n="";e>0&&(a+=e+":",1==e?n+="1 "+this.tt.hour+" ":n+=e+" "+this.tt.hours+" "),i<10?(a+="0"+i+":",i>0&&(1==i?n+="1 "+this.tt.minute+" ":n+=i+" "+this.tt.minutes+" ")):(a+=i+":",n+=i+" "+this.tt.minutes+" "),s<10?(a+="0"+s,s>0&&(1==s?n+="1 "+this.tt.second+" ":n+=s+" "+this.tt.seconds+" ")):(a+=s,n+=s+" "+this.tt.seconds+" ");var r=[];return r.value=a,r.title=n,r}}(jQuery),function(t){AblePlayer.prototype.onMediaUpdateTime=function(t,e){var i=this;this.getMediaTimes(t,e).then(function(t){i.duration=t.duration,i.elapsed=t.elapsed,i.duration>0&&(1===i.prefHighlight&&i.highlightTranscript(i.elapsed),i.updateCaption(i.elapsed),i.showDescription(i.elapsed),i.updateChapter(i.elapsed),i.updateMeta(i.elapsed),i.refreshControl
16935s("timeline",i.duration,i.elapsed))})},AblePlayer.prototype.onMediaPause=function(){this.controlsHidden&&(this.fadeControls("in"),this.controlsHidden=!1),"active"===this.hideControlsTimeoutStatus&&(window.clearTimeout(this.hideControlsTimeout),this.hideControlsTimeoutStatus="clear"),this.refreshControls("playpause")},AblePlayer.prototype.onMediaComplete=function(){this.hasPlaylist&&!this.cueingPlaylistItem&&(this.playlistIndex===this.$playlist.length-1?this.loop?(this.playlistIndex=0,this.cueingPlaylistItem=!0,this.cuePlaylistItem(0)):(this.playing=!1,this.paused=!0):(this.playlistIndex++,this.cueingPlaylistItem=!0,this.cuePlaylistItem(this.playlistIndex))),this.refreshControls("init")},AblePlayer.prototype.onMediaNewSourceLoad=function(){var t=!1;this.cueingPlaylistItem&&(this.cueingPlaylistItem=!1),this.recreatingPlayer&&(this.recreatingPlayer=!1),this.playbackRate&&this.setPlaybackRate(this.playbackRate),this.userClickedPlaylist?(!this.startedPlaying||this.okToPlay)&&(this.playMedia(),t=!0):"restart"==this.seekTrigger||"chapter"==this.seekTrigger||"transcript"==this.seekTrigger||"search"==this.seekTrigger?(this.playMedia(),t=!0):this.swappingSrc?this.hasPlaylist?(this.playlistIndex!==this.$playlist.length||this.loop)&&(this.playMedia(),t=!0):this.swapTime>0?"complete"===this.seekStatus?(this.okToPlay&&this.playMedia(),t=!0):"seeking"===this.seekStatus||(this.swapTime===this.elapsed?(this.seekStatus="complete",this.okToPlay&&this.playMedia(),t=!0):this.hasDescTracks?t=!0:this.durationsAreCloseEnough(this.duration,this.prevDuration)?(this.seekStatus="seeking",this.seekTo(this.swapTime)):t=!0):this.playing&&(this.playMedia(),t=!0):this.startedPlaying?this.hasPlaylist?(this.playlistIndex!==this.$playlist.length||this.loop)&&(this.playMedia(),t=!0):t=!0:this.startTime>0?this.seeking?(this.seeking=!1,this.okToPlay&&this.playMedia(),t=!0):this.seekTo(this.startTime):this.defaultChapter&&void 0!==this.selectedChapters?this.seekToChapter(this.defaultChapter):(this.okToPlay&&this.playMedia(),t=!0),t&&(this.swappingSrc=!1,this.seekStatus=null,this.swapTime=0,this.seekTrigger=null,this.seekingFromTranscript=!1,this.userClickedPlaylist=!1,this.okToPlay=!1),this.refreshControls("init"),this.$focusedElement&&(this.restoreFocus(),this.$focusedElement=null)},AblePlayer.prototype.durationsAreCloseEnough=function(t,e){var i,s;return i=1,(s=Math.abs(Math.round(t)-Math.round(e)))<=i},AblePlayer.prototype.restoreFocus=function(){var e;this.$focusedElement&&"button"===this.$focusedElement.attr("role")&&(e=this.$focusedElement.attr("class").split(/\s+/),t.each(e,function(e,i){"able-button-handler-"===i.substring(0,20)&&t("div.able-controller div."+i).focus()}))},AblePlayer.prototype.addSeekbarListeners=function(){var t=this;this.seekBar.bodyDiv.on("startTracking",function(e){t.pausedBeforeTracking=t.paused,t.pauseMedia()}).on("tracking",function(e,i){t.highlightTranscript(i),t.updateCaption(i),t.showDescription(i),t.updateChapter(t.convertChapterTimeToVideoTime(i)),t.updateMeta(i),t.refreshControls("init")}).on("stopTracking",function(e,i){t.useChapterTimes?t.seekTo(t.convertChapterTimeToVideoTime(i)):t.seekTo(i),t.pausedBeforeTracking||setTimeout(function(){t.playMedia()},200)})},AblePlayer.prototype.onClickPlayerButton=function(e){var i,s;"play"===(i=this.getButtonNameFromClass(t(e).attr("class")))?(this.clickedPlay=!0,this.handlePlay()):"restart"===i?(this.seekTrigger="restart",this.handleRestart()):"previous"===i?(this.userClickedPlaylist=!0,this.okToPlay=!0,this.seekTrigger="previous",this.buttonWithFocus="previous",this.handlePrevTrack()):"next"===i?(this.userClickedPlaylist=!0,this.okToPlay=!0,this.seekTrigger="next",this.buttonWithFocus="next",this.handleNextTrack()):"rewind"===i?(this.seekTrigger="rewind",this.handleRewind()):"forward"===i?(this.seekTrigger="forward",this.handleFastForward()):"mute"===i?this.handleMute():"volume"===i?this.handleVolumeButtonClick():"faster"===i?this.handleRateIncrease():"slower"===i?this.handleRateDecrease():"captions"===i?this.handleCaptionToggle():"chapters"===i?this.handleChapters():"descriptions"===i?this.handleDescriptionToggle(
16935):"sign"===i?this.closingSign||this.handleSignToggle():"preferences"===i?"menu"===t(e).attr("data-prefs-popup")?this.handlePrefsClick():(this.showingPrefsDialog=!0,this.closePopups(),"keyboard"===(s=t(e).attr("data-prefs-popup"))?this.keyboardPrefsDialog.show():"captions"===s?this.captionPrefsDialog.show():"descriptions"===s?this.descPrefsDialog.show():"transcript"===s&&this.transcriptPrefsDialog.show(),this.showingPrefsDialog=!1):"help"===i?this.handleHelpClick():"transcript"===i?this.closingTranscript||this.handleTranscriptToggle():"fullscreen"===i&&(this.clickedFullscreenButton=!0,this.handleFullscreenToggle())},AblePlayer.prototype.getButtonNameFromClass=function(t){var e,i;for(i=0,e=t.split(" ");i<e.length;i++)if("able-button-handler-"===e[i].substring(0,20))return e[i].substring(20);return t},AblePlayer.prototype.okToHandleKeyPress=function(){return"INPUT"!==t(AblePlayer.getActiveDOMElement()).prop("tagName")},AblePlayer.prototype.onPlayerKeyPress=function(e){var i,s;return((i=e.which)>=65&&i<=90&&(i+=32),s=t(document.activeElement),27===i&&this.$transcriptArea&&t.contains(this.$transcriptArea[0],s[0])&&!this.hidingPopup)?(this.handleTranscriptToggle(),!1):!!this.okToHandleKeyPress()&&void(!(t(":focus").is("[contenteditable]")||t(":focus").is("input")||t(":focus").is("textarea")&&!this.stenoMode||t(":focus").is("select")||e.target.hasAttribute("contenteditable")||"INPUT"===e.target.tagName||"TEXTAREA"===e.target.tagName&&!this.stenoMode||"SELECT"===e.target.tagName)&&(27===i?(this.closePopups(),this.$tooltipDiv.hide(),this.seekBar.hideSliderTooltips()):32===i?"button"===s.attr("role")&&(e.preventDefault(),s.click()):112===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handlePlay()):115===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleRestart()):109===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleMute()):118===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleVolumeButtonClick()):i>=49&&i<=57?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleVolumeKeystroke(i)):99===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleCaptionToggle()):100===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleDescriptionToggle()):102===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleFastForward()):114===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleRewind()):98===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handlePrevTrack()):110===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handleNextTrack()):101===i?this.usingModifierKeys(e)&&(e.preventDefault(),this.handlePrefsClick()):13===i&&("button"===s.attr("role")||"SPAN"===s.prop("tagName")?s.click():"LI"===s.prop("tagName")&&s.click())))},AblePlayer.prototype.addHtml5MediaListeners=function(){var t=this;this.$media.on("emptied",function(){}).on("loadedmetadata",function(){t.duration=t.media.duration}).on("canplay",function(){}).on("canplaythrough",function(){t.onMediaNewSourceLoad()}).on("play",function(){t.refreshControls("playpause")}).on("playing",function(){t.playing=!0,t.paused=!1,t.swappingSrc=!1,t.refreshControls("playpause")}).on("ended",function(){t.playing=!1,t.paused=!0,t.onMediaComplete()}).on("progress",function(){t.refreshControls("timeline")}).on("waiting",function(){}).on("durationchange",function(){t.refreshControls("timeline")}).on("timeupdate",function(){t.onMediaUpdateTime()}).on("pause",function(){t.clickedPlay?(t.playing=!1,t.paused=!0):t.hasPlaylist||t.swappingSrc||(t.playing=!1,t.paused=!0),t.clickedPlay=!1,t.onMediaPause()}).on("ratechange",function(){}).on("volumechange",function(){t.volume=t.getVolume()}).on("error",function(){if(t.debug)switch(t.media.error.code){case 1:console.log("HTML5 Media Error: MEDIA_ERR_ABORTED");break;case 2:console.log("HTML5 Media Error: MEDIA_ERR_NETWORK ");break;case 3:console.log("HTML5 Media Error: MEDIA_ERR_DECODE ");break;case 4:console.log("HTML5 Media Error: MEDIA_ERR_SRC_NOT_SUPPORTED ")}})},AblePlayer.prototype.addVimeoListeners=function(){var t=this;this.vimeoPlayer.on("loaded",function(e){t.onMediaNewSourceLoad()}),this.vimeoPlayer.on("play",function(e){t.playing=!0,t.startedPlaying=!0,t.paused=!1,t.refreshControl
16935s("playpause")}),this.vimeoPlayer.on("ended",function(e){t.playing=!1,t.paused=!0,t.onMediaComplete()}),this.vimeoPlayer.on("bufferstart",function(){}),this.vimeoPlayer.on("bufferend",function(){}),this.vimeoPlayer.on("progress",function(t){}),this.vimeoPlayer.on("seeking",function(t){}),this.vimeoPlayer.on("seeked",function(t){}),this.vimeoPlayer.on("timeupdate",function(e){t.onMediaUpdateTime(e.duration,e.seconds)}),this.vimeoPlayer.on("pause",function(e){t.clickedPlay?(t.playing=!1,t.paused=!0):t.hasPlaylist||t.swappingSrc||(t.playing=!1,t.paused=!0),t.clickedPlay=!1,t.onMediaPause(),t.refreshControls("playpause")}),this.vimeoPlayer.on("playbackratechange",function(e){t.vimeoPlaybackRate=e.playbackRate}),this.vimeoPlayer.on("texttrackchange",function(t){}),this.vimeoPlayer.on("volumechange",function(e){t.volume=10*e.volume}),this.vimeoPlayer.on("error",function(t){})},AblePlayer.prototype.addEventListeners=function(){var e;if(e=this,t(window).on("resize",function(){e.resizePlayer()}),window.MutationObserver){var i,s=this.$ableDiv[0];new MutationObserver(function(t){t.forEach(function(t){"attributes"===t.type&&"style"===t.attributeName&&e.$ableDiv.is(":visible")&&e.refreshControls("init")})}).observe(s,{attributes:!0,childList:!0,characterData:!0})}void 0!==this.seekBar?this.addSeekbarListeners():setTimeout(function(){void 0!==e.seekBar&&e.addSeekbarListeners()},2e3),this.$controllerDiv.find('div[role="button"]').on("click",function(t){t.stopPropagation(),e.onClickPlayerButton(this)}),t(document).on("click",function(i){if(0!==i.button)return!1;(t(".able-popup:visible").length||t(".able-volume-popup:visible"))&&e.closePopups(),"VIDEO"===i.target.tagName&&(e.clickedPlay=!0)}),this.$ableDiv.on("mousemove",function(){e.controlsHidden?(e.fadeControls("in"),e.controlsHidden=!1,"active"===e.hideControlsTimeoutStatus&&(window.clearTimeout(e.hideControlsTimeout),e.hideControlsTimeoutStatus="clear"),e.hideControls&&e.invokeHideControlsTimeout()):"active"===e.hideControlsTimeoutStatus&&(window.clearTimeout(e.hideControlsTimeout),e.hideControlsTimeoutStatus="clear",e.hideControls&&e.invokeHideControlsTimeout())}),t(document).keydown(function(t){e.controlsHidden?(e.fadeControls("in"),e.controlsHidden=!1,"active"===e.hideControlsTimeoutStatus&&(window.clearTimeout(e.hideControlsTimeout),e.hideControlsTimeoutStatus="clear"),e.hideControls&&e.invokeHideControlsTimeout()):"active"===e.hideControlsTimeoutStatus&&(window.clearTimeout(e.hideControlsTimeout),e.hideControlsTimeoutStatus="clear",e.hideControls&&e.invokeHideControlsTimeout())}),this.$ableDiv.keydown(function(t){AblePlayer.nextIndex>1&&e.onPlayerKeyPress(t)}),this.stenoMode&&void 0!==this.stenoFrameContents&&this.stenoFrameContents.on("keydown",function(t){e.onPlayerKeyPress(t)}),this.$transcriptArea&&this.$transcriptArea.on("keydown",function(t){AblePlayer.nextIndex>1&&e.onPlayerKeyPress(t)}),this.$playlist&&this.$playlist.click(function(i){e.userClickedPlaylist||(e.userClickedPlaylist=!0,e.playlistIndex=t(this).index(),e.cuePlaylistItem(e.playlistIndex))}),this.$media.click(function(){e.handlePlay()}),"html5"===this.player?this.addHtml5MediaListeners():"vimeo"===this.player?this.addVimeoListeners():"youtube"===this.player&&setInterval(function(){e.onMediaUpdateTime()},300)}}(jQuery),function(t){AblePlayer.prototype.initDragDrop=function(e){var i,s,a,n,r,o,h,l,p,c,u,d,f;for(i=this,"transcript"===e?(s=this.$transcriptArea,n="transcript-window",a=this.$transcriptToolbar):"sign"===e&&(s=this.$signWindow,n="sign-window",a=this.$signToolbar),a.addClass("able-draggable"),r=t("<div>",{class:"able-resizable"}),o=t("<svg>").attr({width:"100%",height:"100%",viewBox:"0 0 100 100",preserveAspectRatio:"none"}),h=1;h<=3;h++)1===h?(l="100",p="0",c="0",u="100"):2===h?(l="33",p="100",c="100",u="33"):3===h&&(l="67",p="100",c="100",u="67"),d=t("<line>").attr({x1:l,y1:p,x2:c,y2:u,"vector-effect":"non-scaling-stroke"}),o.append(d);r.html(o),f=parseInt(s.css("z-index"))+100,r.css("z-index",f),s.append(r),r.html(r.html()),a.on("mousedown mouseup touchstart touchend",function(t){return t.stopPropagation(),"mousedown"===t.type||"touchstart"===t.type?i.windowMenuClickRegistered||(i.windowMenuClickRegistered=!0,i.startMouseX=t.pageX,i.startMouseY=t.pageY,i.dragDevice="mouse",i.startDrag(e,s)):("mouseup"===t.type||"touchend"===t.type)&&i.dragging&&"mouse"===i.dragDevice&&i.endDrag(e),!1}),r.on("mousedown mouseup touchstart touchend",function(t){return t.stopPropagation(),"mousedown"===t.type||"touchstart"===t.type?i.windowMenuClickRegistered||(i.windowMenuClickRegistered=!0,i.startMouseX=t.pageX,i.startMouseY=t.pageY,i.startResize(e,s)):("mouseup"===t.type||"touchend"===t.type)&&i.resizing&&i.endResize(e),!1}),s.on("click",function(){i.windowMenuClickRegistered||i.finishingDrag||i.updateZIndex(e),i.finishingDrag=!1}),this.addWindowMenu(e,s,n)},AblePlayer.prototype.addWindowMenu=function(e,i,s){var a,n,r,o,h,l,p,c,u,d,f;a=this,this.windowMenuClickRegistered=!1,this.finishingDrag=!1,(n=t('<div role="alert"></div>')).addClass("able-alert"),n.hide(),n.appendTo(this.$activeWindow),n.css({top:i.offset().top}),r=this.mediaId+"-"+s+"-menu",o=t("<button>",{type:"button",tabindex:"0","aria-label":this.tt.windowButtonLabel,"aria-haspopup":"true","aria-controls":r,"aria-expanded":"false",class:"able-button-handler-preferences"}),"font"===this.iconType?(h=t("<span>",{class:"icon-preferences","aria-hidden":"true"}),o.append(h)):(p=t("<img>",{src:l=this.rootPath+"button-icons/"+this.toolbarIconColor+"/preferences.png",alt:"",role:"presentation"}),o.append(p)),c=t("<span>",{class:"able-clipped"}
16935).text(this.tt.windowButtonLabel),o.append(c),d=t("<div>",{class:"able-tooltip",id:u=this.mediaId+"-"+s+"-tooltip"}).hide(),o.on("mouseenter focus",function(e){var i=t(this).attr("aria-label"),s=t(this).position(),n=t(this).height();t(this).width();var r=s.top-n-5,h=AblePlayer.localGetElementById(o[0],u).text(i).css({left:"",right:"0px",top:r+"px"});a.showTooltip(h),t(this).on("mouseleave blur",function(){AblePlayer.localGetElementById(o[0],u).text("").hide()})}),f=this.setupPopups(s),"transcript"===e?(this.$transcriptAlert=n,this.$transcriptPopupButton=o,this.$transcriptPopup=f,this.$transcriptToolbar.prepend(n,o,d,f)):"sign"===e&&(this.$signAlert=n,this.$signPopupButton=o,this.$signPopup=f,this.$signToolbar.append(n,o,d,f)),o.on("click mousedown keydown",function(t){return!a.focusNotClick&&(a.dragging?(a.dragKeys(e,t),!1):void(t.stopPropagation(),a.windowMenuClickRegistered||a.finishingDrag||a.handleWindowButtonClick(e,t),a.finishingDrag=!1))}),this.addResizeDialog(e,i)},AblePlayer.prototype.addResizeDialog=function(e,i){var s,a,n,r,o,h,l,p,c,u,d,f,g,_,v,m,y,b,k,C,T,w,P;s=this,"transcript"===e?(a=this.$transcriptPopup,n=this.$transcriptPopupButton):"sign"===e&&(a=this.$signPopup,n=this.$signPopupButton),r=this.mediaId+"-resize-"+e+"-width",o=this.mediaId+"-resize-"+e+"-height",p=(h=i.width())/(l=i.height()),c=t("<div></div>",{class:"able-resize-form"}),u=t("<div></div>"),d=t("<div></div>"),f=t("<input>",{type:"text",id:r,value:h}),g=t("<label>",{for:r}).text(this.tt.width),_=t("<div></div>"),v=t("<input>",{type:"text",id:o,value:l}),m=t("<label>",{for:o}).text(this.tt.height),"sign"===e&&(v.prop("readonly",!0),f.on("input",function(){b=Math.round((y=t(this).val())/p,0),v.val(b)})),k=t('<button class="modal-button">'+this.tt.save+"</button>"),C=t('<button class="modal-button">'+this.tt.cancel+"</button>"),k.on("click",function(){T=t("#"+r).val(),w=t("#"+o).val(),(T!==h||w!==l)&&(s.resizeObject(e,T,w),s.updateCookie(e)),P.hide(),a.hide(),n.focus()}),C.on("click",function(){P.hide(),a.hide(),n.focus()}),d.append(g,f),_.append(m,v),u.append(d,_),c.append(u,"<hr>",k,C),t("body").append(c),P=new AccessibleDialog(c,n,"dialog",!0,this.tt.windowResizeHeading,u,this.tt.closeButtonLabel,"20em"),"transcript"===e?this.transcriptResizeDialog=P:"sign"===e&&(this.signResizeDialog=P)},AblePlayer.prototype.handleWindowButtonClick=function(e,i){var s,a,n,r,o;if(s=this,this.focusNotClick)return!1;if("transcript"===e?(a=this.$transcriptPopup,n=this.$transcriptPopupButton,r=this.$transcriptToolbar):"sign"===e&&(a=this.$signPopup,n=this.$signPopupButton,r=this.$signToolbar),"keydown"===i.type){if(32===i.which||13===i.which)this.windowMenuClickRegistered=!0;else{if(27!==i.which)return!1;a.is(":visible")?a.hide("fast",function(){s.windowMenuClickRegistered=!1,a.find("li").removeClass("able-focus").attr("tabindex","-1"),n.focus()}):"sign"===e?this.handleSignToggle():"transcript"===e&&this.handleTranscriptToggle()}}else this.windowMenuClickRegistered=!0;a.is(":visible")?(a.hide(200,"",function(){s.windowMenuClickRegistered=!1}),a.find("li").removeClass("able-focus"),n.attr("aria-expanded","false").focus()):(this.updateZIndex(e),o=n.position().top+n.outerHeight(),a.css("top",o),a.show(200,"",function(){n.attr("aria-expanded","true"),t(this).find("li").first().focus().addClass("able-focus"),s.windowMenuClickRegistered=!1}))},AblePlayer.prototype.handleMenuChoice=function(t,e,i){var s,a,n,r,o;if(s=this,"transcript"===t?(a=this.$transcriptArea,n=this.$transcriptPopup,r=this.$transcriptPopupButton,o=this.transcriptResizeDialog):"sign"===t&&(a=this.$signWindow,n=this.$signPopup,r=this.$signPopupButton,o=this.signResizeDialog),this.$activeWindow=a,"keydown"===i.type)return 27===i.which?(n.hide("fast",function(){s.windowMenuClickRegistered=!1,n.find("li").removeClass("able-focus").attr("tabindex","-1"),r.attr("aria-expanded","false"),r.focus()}),!1):("close"!==e&&(this.$activeWindow=a),!1);if(n.hide("fast",function(){s.windowMenuClickRegistered=!1,n.find("li").removeClass("able-focus").attr("tabindex","-1"),r.attr("aria-expanded","false")}),"close"!==e&&r.focus(),"move"===e)this.$activeWindow.attr("role","application"),this.showedAlert(t)||(this.showAlert(this.tt.windowMoveAlert,t),"transcript"===t?this.showedTranscriptAlert=!0:"sign"!==t||(this.showedSignAlert=!0)),"keydown"===i.t
16935ype?this.dragDevice="keyboard":this.dragDevice="mouse",this.startDrag(t,a),n.hide().parent().focus();else if("resize"==e){var h=o.getInputs();h&&(h[0].value=a.width(),h[1].value=a.height()),o.show()}else"close"==e&&("transcript"===t?(this.closingTranscript=!0,this.handleTranscriptToggle()):"sign"===t&&(this.closingSign=!0,this.handleSignToggle()))},AblePlayer.prototype.startDrag=function(e,i){var s,a,n,r,o;return s=this,this.$activeWindow||(this.$activeWindow=i),this.dragging=!0,"transcript"===e?a=this.$transcriptPopup:"sign"===e&&(a=this.$signPopup),this.showedAlert(e)||(this.showAlert(this.tt.windowMoveAlert,e),"transcript"===e?this.showedTranscriptAlert=!0:"sign"!==e||(this.showedSignAlert=!0)),a.is(":visible")&&a.hide(),this.updateZIndex(e),n=this.$activeWindow.position(),this.dragStartX=n.left,this.dragStartY=n.top,void 0===this.startMouseX?(this.dragDevice="keyboard",this.dragKeyX=this.dragStartX,this.dragKeyY=this.dragStartY,this.startingDrag=!0):(this.dragDevice="mouse",this.dragOffsetX=this.startMouseX-this.dragStartX,this.dragOffsetY=this.startMouseY-this.dragStartY),this.$activeWindow.addClass("able-drag").css({position:"absolute",top:this.dragStartY+"px",left:this.dragStartX+"px"}).focus(),"mouse"===this.dragDevice?t(document).on("mousemove touchmove",function(t){s.dragging&&(r=t.pageX-s.dragOffsetX,o=t.pageY-s.dragOffsetY,s.resetDraggedObject(r,o))}):"keyboard"===this.dragDevice&&this.$activeWindow.on("keydown",function(t){s.dragging&&s.dragKeys(e,t)}),!1},AblePlayer.prototype.dragKeys=function(t,e){var i,s;if(this.startingDrag)return this.startingDrag=!1,!1;switch(i=e.which,s=10,i){case 37:case 63234:this.dragKeyX-=s;break;case 38:case 63232:this.dragKeyY-=s;break;case 39:case 63235:this.dragKeyX+=s;break;case 40:case 63233:this.dragKeyY+=s;break;case 13:case 27:return this.endDrag(t),!1;default:return!1}return this.resetDraggedObject(this.dragKeyX,this.dragKeyY),e.preventDefault&&e.preventDefault(),!1},AblePlayer.prototype.resetDraggedObject=function(t,e){this.$activeWindow.css({left:t+"px",top:e+"px"})},AblePlayer.prototype.resizeObject=function(t,e,i){var s;this.$activeWindow.css({width:e+"px",height:i+"px"}),"transcript"===t&&(s=i-50,this.$transcriptDiv.css("height",s+"px"))},AblePlayer.prototype.endDrag=function(e){var i,s,a;i=this,"transcript"===e?(s=this.$transcriptPopup,a=this.$transcriptPopupButton):"sign"===e&&(s=this.$signPopup,a=this.$signPopupButton),t(document).off("mousemove mouseup touchmove touchup"),this.$activeWindow.off("keydown").removeClass("able-drag"),this.$activeWindow.attr("role","dialog"),this.$activeWindow=null,"keyboard"===this.dragDevice&&a.focus(),this.dragging=!1,this.updateCookie(e),this.startMouseX=void 0,this.startMouseY=void 0,this.windowMenuClickRegistered=!1,this.finishingDrag=!0,setTimeout(function(){i.finishingDrag=!1},100)},AblePlayer.prototype.isCloseToCorner=function(t,e,i){var s,a,n,r,o,h,l,p;return s=10,n=(a=t.offset()).top,r=a.left,o=t.width(),l=n+(h=t.height()),p=r+o,!!(Math.abs(l-i)<=s&&Math.abs(p-e)<=s)},AblePlayer.prototype.startResize=function(e,i){var s,a,n,r,o;return s=this,this.$activeWindow=i,this.resizing=!0,"transcript"===e?a=this.$transcriptPopup:"sign"===e&&(a=this.$signPopup),a.is(":visible")&&a.hide().parent().focus(),n=this.$activeWindow.position(),this.dragKeyX=this.dragStartX,this.dragKeyY=this.dragStartY,this.dragStartWidth=this.$activeWindow.width(),this.dragStartHeight=this.$activeWindow.height(),t(document).on("mousemove touchmove",function(t){s.resizing&&(r=s.dragStartWidth+(t.pageX-s.startMouseX),o=s.dragStartHeight+(t.pageY-s.startMouseY),s.resizeObject(e,r,o))}),!1},AblePlayer.prototype.endResize=function(e){var i,s;"transcript"===e?(i=this.$transcriptPopup,s=this.$transcriptPopupButton):"sign"===e&&(i=this.$signPopup,s=this.$signPopupButton),t(document).off("mousemove mouseup touchmove touchup"),this.$activeWindow.off("keydown"),s.show().focus(),this.resizing=!1,this.$activeWindow.removeClass("able-resize"),this.updateCookie(e),this.windowMenuClickRegistered=!1,this.finishingDrag=!0,setTimeout(function(){this.finishingDrag=!1},100)}}(jQuery),function(t){AblePlayer.prototype.initSignLanguage=function(){"html5"===this.player?(this.signFile=this.$sources.first().attr("data-sign-src"),this.signFile?this.isIOS()?(this.hasSignLanguage=!1,this.debug&&console.log("Sign language has been disabled due to IOS restrictions")):(this.debug&&console.log("This video has an accompanying sign language video: "+this.signFile),this.hasSignLanguage=!0,this.injectSignPlayerCode()):this.hasSignLanguage=!1):this.hasSignLanguage=!1},AblePlayer.prototype.injectSignPlayerCode=function(){var e,i,s,a,n,r,o;for(a=0,e=this,s=this.getDefaultWidth("sign"),i=this.mediaId+"-sign",this.$signVideo=t("<video>",{id:i,tabindex:"-1"}),this.signVideo=this.$signVideo[0];a<this.$sources.length;a++)if(n=this.$sources[a].getAttribute("data-sign-src"),r=this.$sources[a].getAttribute("type"),n)o=t("<source>",{src:n,type:r}),this.$signVideo.append(o);else{this.hasSignLanguage=!1;break}this.$signWindow=t("<div>",{class:"able-sign-window",role:"dialog","aria-label":this.tt.sign}),this.$signToolbar=t("<div>",{class:"able-window-toolbar able-"+this.toolbarIconColor+"-control
16935s"}),this.$signWindow.append(this.$signToolbar,this.$signVideo),this.$ableWrapper.append(this.$signWindow),this.initDragDrop("sign"),1===this.prefSign?this.positionDraggableWindow("sign",this.getDefaultWidth("sign")):this.$signWindow.hide()}}(jQuery),function(t){var e={ab:{name:"Abkhaz",nativeName:"аҧсуа"},aa:{name:"Afar",nativeName:"Afaraf"},af:{name:"Afrikaans",nativeName:"Afrikaans"},ak:{name:"Akan",nativeName:"Akan"},sq:{name:"Albanian",nativeName:"Shqip"},am:{name:"Amharic",nativeName:"አማርኛ"},ar:{name:"Arabic",nativeName:"العربية"},an:{name:"Aragonese",nativeName:"Aragon\xe9s"},hy:{name:"Armenian",nativeName:"Հայերեն"},as:{name:"Assamese",nativeName:"অসমীয়া"},av:{name:"Avaric",nativeName:"авар мацӀ, магӀарул мацӀ"},ae:{name:"Avestan",nativeName:"avesta"},ay:{name:"Aymara",nativeName:"aymar aru"},az:{name:"Azerbaijani",nativeName:"azərbaycan dili"},bm:{name:"Bambara",nativeName:"bamanankan"},ba:{name:"Bashkir",nativeName:"башҡорт теле"},eu:{name:"Basque",nativeName:"euskara, euskera"},be:{name:"Belarusian",nativeName:"Беларуская"},bn:{name:"Bengali",nativeName:"বাংলা"},bh:{name:"Bihari",nativeName:"भोजपुरी"},bi:{name:"Bislama",nativeName:"Bislama"},bs:{name:"Bosnian",nativeName:"bosanski jezik"},br:{name:"Breton",nativeName:"brezhoneg"},bg:{name:"Bulgarian",nativeName:"български език"},my:{name:"Burmese",nativeName:"ဗမာစာ"},ca:{name:"Catalan",nativeName:"Catal\xe0"},ch:{name:"Chamorro",nativeName:"Chamoru"},ce:{name:"Chechen",nativeName:"нохчийн мотт"},ny:{name:"Chichewa",nativeName:"chiCheŵa, chinyanja"},zh:{name:"Chinese",nativeName:"中文 (Zhōngw\xe9n), 汉语, 漢語"},cv:{name:"Chuvash",nativeName:"чӑваш чӗлхи"},kw:{name:"Cornish",nativeName:"Kernewek"},co:{name:"Corsican",nativeName:"corsu, lingua corsa"},cr:{name:"Cree",nativeName:"ᓀᐦᐃᔭᐍᐏᐣ"},hr:{name:"Croatian",nativeName:"hrvatski"},cs:{name:"Czech",nativeName:"česky, čeština"},da:{name:"Danish",nativeName:"dansk"},dv:{name:"Divehi",nativeName:"ދިވެހި"},nl:{name:"Dutch",nativeName:"Nederlands, Vlaams"},en:{name:"English",nativeName:"English"},eo:{name:"Esperanto",nativeName:"Esperanto"},et:{name:"Estonian",nativeName:"eesti, eesti keel"},ee:{name:"Ewe",nativeName:"Eʋegbe"},fo:{name:"Faroese",nativeName:"f\xf8royskt"},fj:{name:"Fijian",nativeName:"vosa Vakaviti"},fi:{name:"Finnish",nativeName:"suomi, suomen kieli"},fr:{name:"French",nativeName:"fran\xe7ais, langue fran\xe7aise"},ff:{name:"Fula",nativeName:"Fulfulde, Pulaar, Pular"},gl:{name:"Galician",nativeName:"Galego"},ka:{name:"Georgian",nativeName:"ქართული"},de:{name:"German",nativeName:"Deutsch"},el:{name:"Greek",nativeName:"Ελληνικά"},gn:{name:"Guaran\xed",nativeName:"Ava\xf1eẽ"},gu:{name:"Gujarati",nativeName:"ગુજરાતી"},ht:{name:"Haitian",nativeName:"Krey\xf2l ayisyen"},ha:{name:"Hausa",nativeName:"Hausa, هَوُسَ"},he:{name:"Hebrew",nativeName:"עברית"},hz:{name:"Herero",nativeName:"Otjiherero"},hi:{name:"Hindi",nativeName:"हिन्दी, हिंदी"},ho:{name:"Hiri Motu",nativeName:"Hiri Motu"},hu:{name:"Hungarian",nativeName:"Magyar"},ia:{name:"Interlingua",nativeName:"Interlingua"},id:{name:"Indonesian",nativeName:"Bahasa Indonesia"},ie:{name:"Interlingue",nativeName:"Originally called Occidental; then Interlingue after WWII"},ga:{name:"Irish",nativeName:"Gaeilge"},ig:{name:"Igbo",nativeName:"Asụsụ Igbo"},ik:{name:"Inupiaq",nativeName:"I\xf1upiaq, I\xf1upiatun"},io:{name:"Ido",nativeName:"Ido"},is:{name:"Icelandic",nativeName:"\xcdslenska"},it:{name:"Italian",nativeName:"Italiano"},iu:{name:"Inuktitut",nativeName:"ᐃᓄᒃᑎᑐᑦ"},ja:{name:"Japanese",nativeName:"日本語 (にほんご/にっぽんご)"},jv:{name:"Javanese",nativeName:"basa Jawa"},kl:{name:"Kalaallisut",nativeName:"kalaallisut, kalaallit oqaasii"},kn:{name:"Kannada",nativeName:"ಕನ್ನಡ"},kr:{name:"Kanuri",nativeName:"Kanuri"},ks:{name:"Kashmiri",nativeName:"कश्मीरी, كشميري‎"},kk:{name:"Kazakh",nativeName:"Қазақ тілі"},km:{name:"Khmer",nativeName:"ភាសាខ្មែរ"},ki:{name:"Kikuyu",nativeName:"Gĩkũyũ"},rw:{name:"Kinyarwanda",nativeName:"Ikinyarwanda"},ky:{name:"Kyrgyz",nativeName:"кыргыз тили"},kv:{name:"Komi",nativeName:"коми кыв"}
16935,kg:{name:"Kongo",nativeName:"KiKongo"},ko:{name:"Korean",nativeName:"한국어 (韓國語), 조선말 (朝鮮語)"},ku:{name:"Kurdish",nativeName:"Kurd\xee, كوردی‎"},kj:{name:"Kuanyama",nativeName:"Kuanyama"},la:{name:"Latin",nativeName:"latine, lingua latina"},lb:{name:"Luxembourgish",nativeName:"L\xebtzebuergesch"},lg:{name:"Luganda",nativeName:"Luganda"},li:{name:"Limburgish",nativeName:"Limburgs"},ln:{name:"Lingala",nativeName:"Ling\xe1la"},lo:{name:"Lao",nativeName:"ພາສາລາວ"},lt:{name:"Lithuanian",nativeName:"lietuvių kalba"},lu:{name:"Luba-Katanga",nativeName:""},lv:{name:"Latvian",nativeName:"latviešu valoda"},gv:{name:"Manx",nativeName:"Gaelg, Gailck"},mk:{name:"Macedonian",nativeName:"македонски јазик"},mg:{name:"Malagasy",nativeName:"Malagasy fiteny"},ms:{name:"Malay",nativeName:"bahasa Melayu, بهاس ملايو‎"},ml:{name:"Malayalam",nativeName:"മലയാളം"},mt:{name:"Maltese",nativeName:"Malti"},mi:{name:"Māori",nativeName:"te reo Māori"},mr:{name:"Marathi",nativeName:"मराठी"},mh:{name:"Marshallese",nativeName:"Kajin M̧ajeļ"},mn:{name:"Mongolian",nativeName:"монгол"},na:{name:"Nauru",nativeName:"Ekakairũ Naoero"},nv:{name:"Navajo",nativeName:"Din\xe9 bizaad, Din\xe9kʼehǰ\xed"},nb:{name:"Norwegian Bokm\xe5l",nativeName:"Norsk bokm\xe5l"},nd:{name:"North Ndebele",nativeName:"isiNdebele"},ne:{name:"Nepali",nativeName:"नेपाली"},ng:{name:"Ndonga",nativeName:"Owambo"},nn:{name:"Norwegian Nynorsk",nativeName:"Norsk nynorsk"},no:{name:"Norwegian",nativeName:"Norsk"},ii:{name:"Nuosu",nativeName:"ꆈꌠ꒿ Nuosuhxop"},nr:{name:"South Ndebele",nativeName:"isiNdebele"},oc:{name:"Occitan",nativeName:"Occitan"},oj:{name:"Ojibwe",nativeName:"ᐊᓂᔑᓈᐯᒧᐎᓐ"},cu:{name:"Church Slavonic",nativeName:"ѩзыкъ словѣньскъ"},om:{name:"Oromo",nativeName:"Afaan Oromoo"},or:{name:"Oriya",nativeName:"ଓଡ଼ିଆ"},os:{name:"Ossetian",nativeName:"ирон \xe6взаг"},pa:{name:"Punjabi",nativeName:"ਪੰਜਾਬੀ, پنجابی‎"},pi:{name:"Pāli",nativeName:"पाऴि"},fa:{name:"Persian",nativeName:"فارسی"},pl:{name:"Polish",nativeName:"polski"},ps:{name:"Pashto",nativeName:"پښتو"},pt:{name:"Portuguese",nativeName:"Portugu\xeas"},qu:{name:"Quechua",nativeName:"Runa Simi, Kichwa"},rm:{name:"Romansh",nativeName:"rumantsch grischun"},rn:{name:"Kirundi",nativeName:"kiRundi"},ro:{name:"Romanian",nativeName:"rom\xe2nă"},ru:{name:"Russian",nativeName:"русский язык"},sa:{name:"Sanskrit",nativeName:"संस्कृतम्"}
16935,sc:{name:"Sardinian",nativeName:"sardu"},sd:{name:"Sindhi",nativeName:"सिन्धी, سنڌي، سندھی‎"},se:{name:"Northern Sami",nativeName:"Davvis\xe1megiella"},sm:{name:"Samoan",nativeName:"gagana faa Samoa"},sg:{name:"Sango",nativeName:"y\xe2ng\xe2 t\xee s\xe4ng\xf6"},sr:{name:"Serbian",nativeName:"српски језик"},gd:{name:"Gaelic",nativeName:"G\xe0idhlig"},sn:{name:"Shona",nativeName:"chiShona"},si:{name:"Sinhalese",nativeName:"සිංහල"},sk:{name:"Slovak",nativeName:"slovenčina"},sl:{name:"Slovene",nativeName:"slovenščina"},so:{name:"Somali",nativeName:"Soomaaliga, af Soomaali"},st:{name:"Southern Sotho",nativeName:"Sesotho"},es:{name:"Spanish",nativeName:"espa\xf1ol, castellano"},su:{name:"Sundanese",nativeName:"Basa Sunda"},sw:{name:"Swahili",nativeName:"Kiswahili"},ss:{name:"Swati",nativeName:"SiSwati"},sv:{name:"Swedish",nativeName:"svenska"},ta:{name:"Tamil",nativeName:"தமிழ்"},te:{name:"Telugu",nativeName:"తెలుగు"},tg:{name:"Tajik",nativeName:"тоҷикӣ, toğikī, تاجیکی‎"},th:{name:"Thai",nativeName:"ไทย"},ti:{name:"Tigrinya",nativeName:"ትግርኛ"},bo:{name:"Tibetan",nativeName:"བོད་ཡིག"},tk:{name:"Turkmen",nativeName:"T\xfcrkmen, Түркмен"},tl:{name:"Tagalog",nativeName:"Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔"},tn:{name:"Tswana",nativeName:"Setswana"},to:{name:"Tonga",nativeName:"faka Tonga"},tr:{name:"Turkish",nativeName:"T\xfcrk\xe7e"},ts:{name:"Tsonga",nativeName:"Xitsonga"},tt:{name:"Tatar",nativeName:"татарча, tatar\xe7a, تاتارچا‎"},tw:{name:"Twi",nativeName:"Twi"},ty:{name:"Tahitian",nativeName:"Reo Tahiti"},ug:{name:"Uyghur",nativeName:"Uyƣurqə, ئۇيغۇرچە‎"},uk:{name:"Ukrainian",nativeName:"українська"},ur:{name:"Urdu",nativeName:"اردو"},uz:{name:"Uzbek",nativeName:"zbek, Ўзбек, أۇزبېك‎"},ve:{name:"Venda",nativeName:"Tshivenḓa"},vi:{name:"Vietnamese",nativeName:"Tiếng Việt"},vo:{name:"Volap\xfck",nativeName:"Volap\xfck"},wa:{name:"Walloon",nativeName:"Walon"},cy:{name:"Welsh",nativeName:"Cymraeg"},wo:{name:"Wolof",nativeName:"Wollof"},fy:{name:"Western Frisian",nativeName:"Frysk"},xh:{name:"Xhosa",nativeName:"isiXhosa"},yi:{name:"Yiddish",nativeName:"ייִדיש"},yo:{name:"Yoruba",nativeName:"Yor\xf9b\xe1"},za:{name:"Zhuang",nativeName:"Saɯ cueŋƅ, Saw cuengh"},"ar-dz":{name:"Arabic (Algeria)",nativeName:"العربية (الجزائر)"},"ar-bh":{name:"Arabic (Bahrain)",nativeName:"العربية (البحرين)"},"ar-eg":{name:"Arabic (Egypt)",nativeName:"العربية (مصر)"},"ar-iq":{name:"Arabic (Iraq)",nativeName:"العربية (العراق)"},"ar-jo":{name:"Arabic (Jordan)",nativeName:"العربية (الأردن)"},"ar-kw":{name:"Arabic (Kuwait)",nativeName:"العربية (الكويت)"},"ar-lb":{name:"Arabic (Lebanon)",nativeName:"العربية (لبنان)"},"ar-ly":{name:"Arabic (Libya)",nativeName:"العربية (ليبيا)"},"ar-ma":{name:"Arabic (Morocco)",nativeName:"العربية (المملكة المغربية)"},"ar-om":{name:"Arabic (Oman)",nativeName:"العربية (عمان)"},"ar-qa":{name:"Arabic (Qatar)",nativeName:"العربية (قطر)"},"ar-sa":{name:"Arabic (Saudi Arabia)",nativeName:"العربية (المملكة العربية السعودية)"},"ar-sy":{name:"Arabic (Syria)",nativeName:"العربية (سوريا)"},"ar-tn":{name:"Arabic (Tunisia)",nativeName:"العربية (تونس)"},"ar-ae":{name:"Arabic (U.A.E.)",nativeName:"العربية (الإمارات العربية المتحدة)"},"ar-ye":{name:"Arabic (Yemen)",nativeName:"العربية (اليمن)"},"de-at":{name:"German (Austria)",nativeName:"Deutsch (\xd6sterreich)"},"de-li":{name:"German (Liechtenstein)",nativeName:"Deutsch (Liechtenstein)"},"de-lu":{name:"German (Luxembourg)",nativeName:"Deutsch (Luxemburg)"},"de-ch":{name:"German (Switzerland)",nativeName:"Deutsch (Schweiz)"},"en-au":{name:"English (Australia)",nativeName:"English (Australia)"},"en-bz":{name:"English (Belize)",nativeName:"English (Belize)"}
16935,"en-ca":{name:"English (Canada)",nativeName:"English (Canada)"},"en-ie":{name:"English (Ireland)",nativeName:"English (Ireland)"},"en-jm":{name:"English (Jamaica)",nativeName:"English (Jamaica)"},"en-nz":{name:"English (New Zealand)",nativeName:""},"en-za":{name:"English (South Africa)",nativeName:"English (South Africa)"},"en-tt":{name:"English (Trinidad)",nativeName:"English (Trinidad y Tobago)"},"en-gb":{name:"English (United Kingdom)",nativeName:"English (United Kingdom)"},"en-us":{name:"English (United States)",nativeName:"English (United States)"},"es-ar":{name:"Spanish (Argentina)",nativeName:"Espa\xf1ol (Argentina)"},"es-bo":{name:"Spanish (Bolivia)",nativeName:"Espa\xf1ol (Bolivia)"},"es-cl":{name:"Spanish (Chile)",nativeName:"Espa\xf1ol (Chile)"},"es-co":{name:"Spanish (Colombia)",nativeName:"Espa\xf1ol (Colombia)"},"es-cr":{name:"Spanish (Costa Rica)",nativeName:"Espa\xf1ol (Costa Rica)"},"es-do":{name:"Spanish (Dominican Republic)",nativeName:"Espa\xf1ol (Rep\xfablica Dominicana)"},"es-ec":{name:"Spanish (Ecuador)",nativeName:"Espa\xf1ol (Ecuador)"},"es-sv":{name:"Spanish (El Salvador)",nativeName:"Espa\xf1ol (El Salvador)"},"es-gt":{name:"Spanish (Guatemala)",nativeName:"Espa\xf1ol (Guatemala)"},"es-hn":{name:"Spanish (Honduras)",nativeName:"Espa\xf1ol (Honduras)"},"es-mx":{name:"Spanish (Mexico)",nativeName:"Espa\xf1ol (M\xe9xico)"},"es-ni":{name:"Spanish (Nicaragua)",nativeName:"Espa\xf1ol (Nicaragua)"},"es-pa":{name:"Spanish (Panama)",nativeName:"Espa\xf1ol (Panam\xe1)"},"es-py":{name:"Spanish (Paraguay)",nativeName:"Espa\xf1ol (Paraguay)"},"es-pe":{name:"Spanish (Peru)",nativeName:"Espa\xf1ol (Per\xfa)"},"es-pr":{name:"Spanish (Puerto Rico)",nativeName:"Espa\xf1ol (Puerto Rico)"},"es-uy":{name:"Spanish (Uruguay)",nativeName:"Espa\xf1ol (Uruguay)"},"es-ve":{name:"Spanish (Venezuela)",nativeName:"Espa\xf1ol (Venezuela)"},"fr-be":{name:"French (Belgium)",nativeName:"fran\xe7ais (Belgique)"},"fr-ca":{name:"French (Canada)",nativeName:"fran\xe7ais (Canada)"},"fr-lu":{name:"French (Luxembourg)",nativeName:"fran\xe7ais (Luxembourg)"},"fr-ch":{name:"French (Switzerland)",nativeName:"fran\xe7ais (Suisse)"},"it-ch":{name:"Italian (Switzerland)",nativeName:"italiano (Svizzera)"},"nl-be":{name:"Dutch (Belgium)",nativeName:"Nederlands (Belgi\xeb)"},"pt-br":{name:"Portuguese (Brazil)",nativeName:"Portugu\xeas (Brasil)"},"sv-fi":{name:"Swedish (Finland)",nativeName:"svenska (Finland)"},"zh-hk":{name:"Chinese (Hong Kong)",nativeName:"中文(香港特别行政區)"},"zh-cn":{name:"Chinese (PRC)",nativeName:"中文(中华人民共和国)"},"zh-sg":{name:"Chinese (Singapore)",nativeName:"中文(新加坡)"},"zh-tw":{name:"Chinese Traditional (Taiwan)",nativeName:"中文(台灣)"}};AblePlayer.prototype.getLanguageName=function(t,i){var s,a,n;return(s=e[t.toLowerCase()])?"local"===i?s.nativeName:s.name:t.includes("-")&&(a=t.substring(0,2),n=t.substring(3),s=e[a.toLowerCase()])?"local"===i?s.nativeName+" ("+n+")":s.name+" ("+n+")":t}}(jQuery),function(t){AblePlayer.prototype.getSupportedLangs=function(){return["ca","cs","da","de","en","es","fr","he","id","it","ja","nb","nl","pt","pt-br","sv","tr","zh-tw"]},AblePlayer.prototype.getTranslationText=function(){var e,i,s,a,n,r,o;if(e=t.Deferred(),i=this,s=this.getSupportedLangs(),this.lang&&-1===t.inArray(this.lang,s)){if(2==this.lang.indexOf("-"))-1!==t.inArray(this.lang.substring(0,2),s)?this.lang=this.lang.substring(0,2):this.lang=null;else{for(o=!1,r=0;r<s.length;)s[r].substring(0,2)==this.lang&&(this.lang=s[r],o=!0),r++;o||(this.lang=null)}}return!this.lang&&(a=t("body").attr("lang")?t("body").attr("lang").toLowerCase():t("html").attr("lang")?t("html").attr("lang").toLowerCase():null)&&(-1!==t.inArray(a,s)?this.lang=a:2==a.indexOf("-")&&-1!==t.inArray(a.substring(0,2),s)&&(this.lang=a.substring(0,2))),this.lang||(this.lang="en"),this.searchLang||(this.searchLang=this.lang),n=this.rootPath+"translations/"+this.lang+".js",t.getJSON(n,function(t){i.tt=t,e.resolve()}).fail(function(){console.log("Critical Error: Unable to load translation file:",n),i.provideFallback(),e.fail()}),e.promise()},AblePlayer.prototype.getSampleDescriptionText=function(){var e,i,s,a,n,r,o;for(s=0,i=this.getSupportedLangs(),e=this,this.sampleText=[];s<i.length;s++)n=this.rootPath+"translations/"+i[s]+".js",t.getJSON(n,a,function(t){return function(i){o={lang:t,text:r=i.sampleDescriptionText},e.sampleText.push(o)}}(i[s]))}}(jQuery),jQuery,AblePlayer.prototype.computeEndTime=function(t,e){var i=t.split(":").reverse().map(function(t){return parseFloat(t)}),s=e.split(":").reverse().map(function(t){return parseFloat(t)});
16935return i.reduce(function(t,e,i){var a=e+s[i];return 0===i&&(a>60&&(s[i+1]+=1,a-=60),a=a.toFixed(3)),1===i&&a>60&&(s[i+1]+=1,a-=60),a<10&&(a="0"+a),t.push(a),t},[]).reverse().join(":")},AblePlayer.prototype.ttml2webvtt=function(t){var e,i=this;return JSON.parse(i.convert.xml2json(t,{ignoreComment:!0,alwaysChildren:!0,compact:!0,spaces:2})).tt.body.div.p.reduce(function(t,e,s){var a=e._text,n=Array.isArray(a),r=e._attributes;return i.computeEndTime(r.begin,r.dur),t+(i.computeEndTime(r.begin,"00:00:0")+" --> "+i.computeEndTime(r.begin,r.dur)+"\n"+(n?a.join("\n"):a))+"\n\n"},"WEBVTT\n\n\n")},function(t){t.doWhen=function(e){return t.Deferred(function(i){var s=t.extend({},{when:null,exec:function(){},interval:100,attempts:100,delayed:0},e,{checkId:null}),a=function(){if(!0===s.when()){s.exec.call(i.promise()),i.resolve();return}s.checkId=setInterval(function(){0===s.attempts?(clearInterval(s.checkId),i.reject()):(--s.attempts,!0===s.when()&&(s.attempts=0,clearInterval(s.checkId),s.exec.call(i.promise()),i.resolve()))},s.interval)};s.delayed>0?setTimeout(a,Number(s.delayed)):a()}).promise()}}(jQuery),function(t){AblePlayer.prototype.injectVTS=function(){var e,i,s,a,n,r,o,h,l,p,c,u,d,f,g,_,v,m,y,b,k,C;if(e=this,t("#able-vts").length&&this.vtsTracks.length){for(u in this.langs=[],this.getAllLangs(this.vtsTracks),this.vtsLang=this.lang,i=t("<h2>").text("Video Transcript Sorter"),t("#able-vts").append(i),this.$vtsAlert=t("<div>",{id:"able-vts-alert","aria-live":"polite","aria-atomic":"true"}),t("#able-vts").append(this.$vtsAlert),s=t("<div>",{id:"able-vts-instructions"}),a=t("<p>").text("Use the Video Transcript Sorter to perform any of the following tasks:"),r=t("<ul>"),o=t("<li>").text("Reorder chapters, descriptions, captions, and/or subtitles so they appear in the proper sequence in Able Player's auto-generated transcript."),h=t("<li>").text("Modify content or start/end times (all are directly editable within the table)."),l=t("<li>").text("Insert new content, such as chapters or descriptions."),n=t("<p>").text('When finished editing, click the "Save Changes" button. This will auto-generate new content for all relevant timed text files (chapters, descriptions, captions, and/or subtitles), which can be copied and pasted into separate WebVTT files for use by Able Player.'),r.append(o,h,l),s.append(a,r,n),t("#able-vts").append(s),p=t("<fieldset>"),c=t("<legend>").text("Select a language"),p.append(c),this.langs)f="vts-lang-radio-"+this.langs[u],d=t("<div>",{}),_=t("<input>",{type:"radio",name:"vts-lang",id:f,value:this.langs[u]}).on("click",function(){e.vtsLang=t(this).val(),e.showVtsAlert("Loading "+e.getLanguageName(e.vtsLang)+" tracks"),e.injectVtsTable("update",e.vtsLang)}),this.langs[u]==this.lang&&_.prop("checked",!0),g=t("<label>",{for:f}).text(this.getLanguageName(this.langs[u])),d.append(_,g),p.append(d);t("#able-vts").append(p),v=t("<button>",{type:"button",id:"able-vts-save",value:"save"}).text("Save Changes"),t("#able-vts").append(v),this.injectVtsTable("add",this.vtsLang),y=["captions","chapters","descriptions","subtitles"],t('td[contenteditable="true"]').on("focus",function(){b=t(this).text()}).on("blur",function(){b!=t(this).text()&&(k=t(this).index(),C=t(this).text(),1===k?-1===t.inArray(C,y)&&("s"===C.substring(0,1)?t(this).text("subtitles"):"d"===C.substring(0,1)?t(this).text("descriptions"):"ch"===C.substring(0,2)?t(this).text("chapters"):t(this).text("captions")):(2===k||3===k)&&t(this).text(e.formatTimestamp(C)))}).on("keydown",function(t){t.stopPropagation()}),t("#able-vts-save").on("click",function(i){i.stopPropagation(),"save"==t(this).attr("value")?(t(this).attr("value","cancel").text("Return to Editor"),m=t("#able-vts table"),t("#able-vts-instructions").hide(),t("#able-vts > fieldset").hide(),t("#able-vts table").remove(),t("#able-vts-icon-credit").remove(),e.parseVtsOutput(m)):(t(this).attr("value","save").text("Save Changes"),t("#able-vts-output").remove(),t("#able-vts-instructions").show(),t("#able-vts > fieldset").show(),t("#able-vts").append(m),t("#able-vts").append(e.getIconCredit()),e.showVtsAlert("Cancelling saving. Any edits you made have been restored in the VTS table."))})}}
vendor: 5,561 bytes, line 16935
16935,AblePlayer.prototype.setupVtsTracks=function(t,e,i,s,a,n){var r,o;r=this.getFilenameFromPath(a),o=this.parseVtsTracks(n),this.vtsTracks.push({kind:t,language:e,label:s,srcFile:r,cues:o})},AblePlayer.prototype.getFilenameFromPath=function(t){var e;return -1===(e=t.lastIndexOf("/"))?t:t.substring(e+1)},AblePlayer.prototype.getFilenameFromTracks=function(t,e){for(var i=0;i<this.vtsTracks.length;i++)if(this.vtsTracks[i].kind===t&&this.vtsTracks[i].language===e)return this.vtsTracks[i].srcFile;return!1},AblePlayer.prototype.parseVtsTracks=function(t){var e,i,s,a,n,r,o,h,l;for(e=t.split("\n"),s=[],a=0;a<e.length;)if(-1!==(r=e[a]).indexOf(" --> ")){if(i=r.trim().split(" "),this.isValidTimestamp(i[0])&&this.isValidTimestamp(i[2])){for(h="",n=a+1,l=!1;n<e.length&&!l;)(o=e[n].trim()).length>0?h.length>0?h+="\n"+o:h+=o:l=!0,n++;s.push({start:i[0],end:i[2],content:h}),a=n}}else a++;return s},AblePlayer.prototype.isValidTimestamp=function(t){return!!/^[0-9:,.]*$/.test(t)},AblePlayer.prototype.formatTimestamp=function(t){var e,i;if(e=t.substring(0,t.lastIndexOf(".")+1),(i=t.substring(t.lastIndexOf(".")+1)).length>3)i=i.substring(0,3);else if(i.length<3)for(;i.length<3;)i+="0";return e+i},AblePlayer.prototype.injectVtsTable=function(e,i){var s,a,n,r,o,h,l,p,c;for("update"===e&&(t("#able-vts table").remove(),t("#able-vts-icon-credit").remove()),s=t("<table>",{lang:i}),r=t("<tr>",{lang:"en"}),a=["Row #","Kind","Start","End","Content","Actions"],n=0;n<a.length;n++)o=t("<th>",{scope:"col"}).text(a[n]),"Actions"===a[n]&&o.addClass("actions"),r.append(o);for(s.append(r),l=this.getAllRows(i),n=0;n<l.length;n++)r=t("<tr>",{id:c="able-vts-row-"+(p=n+1),class:"kind-"+l[n].kind}),h=t("<td>").text(p),r.append(h),h=t("<td>",{contenteditable:"true"}).text(l[n].kind),r.append(h),h=t("<td>",{contenteditable:"true"}).text(l[n].start),r.append(h),h=t("<td>",{contenteditable:"true"}).text(l[n].end),r.append(h),h=t("<td>",{contenteditable:"true"}).text(l[n].content),r.append(h),h=this.addVtsActionButtons(p,l.length),r.append(h),s.append(r);t("#able-vts").append(s),t("#able-vts").append(this.getIconCredit())},AblePlayer.prototype.addVtsActionButtons=function(e,i){var s,a,n,r,o,h,l,p,c,u,d,f;for(r=0,s=this,a=t("<td>"),n=["up","down","insert","delete"];r<n.length;r++)"up"===(o=n[r])?e>1&&(h=t("<button>",{id:"able-vts-button-up-"+e,title:"Move up","aria-label":"Move Row "+e+" up"}).on("click",function(t){s.onClickVtsActionButton(t.currentTarget)}),l=t("<svg>",{focusable:"false","aria-hidden":"true",x:"0px",y:"0px",width:"254.296px",height:"254.296px",viewBox:"0 0 254.296 254.296",style:"enable-background:new 0 0 254.296 254.296"}),d=t("<path>",{d:c="M249.628,176.101L138.421,52.88c-6.198-6.929-16.241-6.929-22.407,0l-0.381,0.636L4.648,176.101c-6.198,6.897-6.198,18.052,0,24.981l0.191,0.159c2.892,3.305,6.865,5.371,11.346,5.371h221.937c4.577,0,8.613-2.161,11.41-5.594l0.064,0.064C255.857,194.153,255.857,182.998,249.628,176.101z"}),p=t("<g>").append(d),l.append(p),h.append(l),h.html(h.html()),a.append(h)):"down"===o?e<i&&(h=t("<button>",{id:"able-vts-button-down-"+e,title:"Move down","aria-label":"Move Row "+e+" down"}).on("click",function(t){s.onClickVtsActionButton(t.currentTarget)}),l=t("<svg>",{focusable:"false","aria-hidden":"true",x:"0px",y:"0px",width:"292.362px",height:"292.362px",viewBox:"0 0 292.362 292.362",style:"enable-background:new 0 0 292.362 292.362"}),d=t("<path>",{d:c="M286.935,69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952,0-9.233,1.807-12.85,5.424C1.807,72.998,0,77.279,0,82.228c0,4.948,1.807,9.229,5.424,12.847l127.907,127.907c3.621,3.617,7.902,5.428,12.85,5.428s9.233-1.811,12.847-5.428L286.935,95.074c3.613-3.617,5.427-7.898,5.427-12.847C292.362,77.279,290.548,72.998,286.935,69.377z"}),p=t("<g>").append(d),l.append(p),h.append(l),h.html(h.html()),a.append(h)):"insert"===o?(h=t("<button>",{id:"able-vts-button-insert-"+e,title:"Insert row below","aria-label":"Insert row before Row "+e}).on("click",function(t){s.onClickVtsActionButton(t.currentTarget)}),l=t("<svg>",{focusable:"false","aria-hidden":"true",x:"0px",y:"0px",width:"401.994px",height:"401.994px",viewBox:"0 0 401.994 401.994",style:"enable-background:new 0 0 401.994 401.994"}),d=t("<path>",{d:c="M394,154.175c-5.331-5.33-11.806-7.994-19.417-7.994H255.811V27.406c0-7.611-2.666-14.084-7.994-19.414C242.488,2.666,236.02,0,228.398,0h-54.812c-7.612,0-14.084,2.663-19.414,7.993c-5.33,5.33-7.994,11.803-7.994,19.414v118.775H27.407c-7.611,0-14.084,2.664-19.414,7.994S0,165.973,0,173.589v54.819c0,7.618,2.662,14.086,7.992,19.411c5.33,5.332,11.803,7.994,19.414,7.994h118.771V374.59c0,7.611,2.664,14.089,7.994,19.417c5.33,5.325,11.802,7.987,19.414,7.987h54.816c7.617,0,14.086-2.662,19.417-7.987c5.332-5.331,7.994-11.806,7.994-19.417V255.813h118.77c7.618,0,14.089-2.662,19.417-7.994c5.329-5.325,7.994-11.793,7.994-19.411v-54.819C401.991,165.973,399.332,159.502,394,154.175z"}),p=t("<g>").append(d),l.append(p),h.append(l),h.html(h.html()),a.append(h)):"delete"===o&&(h=t("<button>",{id:"able-vts-button-delete-"+e,title:"Delete row ","aria-label":"Delete Row "+e}).on("click",function(t){s.onClickVtsActionButton(t.currentTarget)}),l=t("<svg>",{focusable:"false","aria-hidden":"true",x:"0px",y:"0px",width:"508.52px",height:"508.52px",viewBox:"0 0 508.52 508.52",style:"enable-background:new 0 0 508.52 508.52"}),d=t("<path>",{d:c="M397.281,31.782h-63.565C333.716,14.239,319.478,0,301.934,0h-95.347c-17.544,0-31.782,14.239-31.782,31.782h-63.565c-17.544,0-31.782,14.239-31.782,31.782h349.607C429.063,46.021,414.825,31.782,397.281,31.782z"}),f=t("<path>
16935",{d:u="M79.456,476.737c0,17.544,14.239,31.782,31.782,31.782h286.042c17.544,0,31.782-14.239,31.782-31.782V95.347H79.456V476.737z M333.716,174.804c0-8.772,7.151-15.891,15.891-15.891c8.74,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.151,15.891-15.891,15.891c-8.74,0-15.891-7.151-15.891-15.891V174.804zM238.369,174.804c0-8.772,7.119-15.891,15.891-15.891c8.74,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.151,15.891-15.891,15.891c-8.772,0-15.891-7.151-15.891-15.891V174.804z M143.021,174.804c0-8.772,7.119-15.891,15.891-15.891c8.772,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.119,15.891-15.891,15.891c-8.772,0-15.891-7.151-15.891-15.891V174.804z"}),p=t("<g>").append(d,f),l.append(p),h.append(l),h.html(h.html()),a.append(h));return a},AblePlayer.prototype.updateVtsActionButtons=function(t,e){var i,s,a,n,r,o;for(i=0;i<t.length;i++)a=(s=t.eq(i)).attr("id"),n=s.attr("aria-label"),r=a.replace(/[0-9]+/g,e),o=n.replace(/[0-9]+/g,e),s.attr("id",r),s.attr("aria-label",o)},AblePlayer.prototype.getIconCredit=function(){var t;return'<div id="able-vts-icon-credit">Action buttons made by <a href="https://www.flaticon.com/authors/elegant-themes">Elegant Themes</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> are licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div>'},AblePlayer.prototype.getAllLangs=function(e){var i;for(i in e)e[i].hasOwnProperty("language")&&-1===t.inArray(e[i].language,this.langs)&&(this.langs[this.langs.length]=e[i].language)},AblePlayer.prototype.getAllRows=function(t){var e,i,s,a;for(e=0,a=[];e<this.vtsTracks.length;e++)if((i=this.vtsTracks[e]).language==t)for(s in i.cues)a.push({kind:i.kind,lang:t,id:i.cues[s].id,start:i.cues[s].start,end:i.cues[s].end,content:i.cues[s].content});return a.sort(function(t,e){return t.start>e.start?1:-1}),a},AblePlayer.prototype.onClickVtsActionButton=function(e){var i,s,a;s=(i=t(e).attr("id").split("-"))[3],a=i[4],"up"==s?this.moveRow(a,"up"):"down"==s?this.moveRow(a,"down"):"insert"==s?this.insertRow(a):"delete"==s&&this.deleteRow(a)},AblePlayer.prototype.insertRow=function(e){var i,s,a,n,r,o,h,l,p,c,u,d,f,g,_,v;for(a=(s=(i=t("#able-vts table")).find("tr")).length-1,o=t("<tr>",{id:r="able-vts-row-"+(n=parseInt(e)+1)}),h=t("<td>").text(n),o.append(h),d=null,l=t("<select>",{id:"able-vts-kind-"+n,"aria-label":"What kind of track is this?",placeholder:"Select a kind"}).on("change",function(){f="kind-"+(d=t(this).val()),g=t(this).closest("tr"),t(this).parent().text(d),g.addClass(f)}),p=["","captions","chapters","descriptions","subtitles"],c=0;c<p.length;c++)u=t("<option>",{value:p[c]}).text(p[c]),l.append(u);for(h=t("<td>").append(l),o.append(h),h=t("<td>",{contenteditable:"true"}),o.append(h),h=t("<td>",{contenteditable:"true"}),o.append(h),h=t("<td>",{contenteditable:"true"}),o.append(h),h=this.addVtsActionButtons(n,a),o.append(h),i.find("tr").eq(e).after(o),c=n;c<=a;c++)_=c+1,s.eq(c).attr("id","able-vts-row-"+_),s.eq(c).find("td").eq(0).text(_),v=s.eq(c).find("button"),this.updateVtsActionButtons(v,_);this.adjustTimes(n),this.showVtsAlert("A new row "+n+" has been inserted"),l.focus()},AblePlayer.prototype.deleteRow=function(e){var i,s,a,n,r,o;for((i=t("#able-vts table"))[0].deleteRow(e),a=(s=i.find("tr")).length-1,n=e;n<=a;n++)r=n,s.eq(n).attr("id","able-vts-row-"+r),s.eq(n).find("td").eq(0).text(r),o=s.eq(n).find("button"),this.updateVtsActionButtons(o,r);this.showVtsAlert("Row "+e+" has been deleted")},AblePlayer.prototype.moveRow=function(e,i){var s,a,n,r,o;s=t("#able-vts table").find("tr"),a=t("#able-vts table").find("tr").eq(e),"up"==i?(n=parseInt(e)-1,(r=t("#able-vts table").find("tr").eq(n)).before(a)):"down"==i&&(n=parseInt(e)+1,(r=t("#able-vts table").find("tr").eq(n)).after(a)),a.attr("id","able-vts-row-"+n),a.find("td").eq(0).text(n),this.updateVtsActionButtons(a.find("button"),n),r.attr("id","able-vts-row-"+e),r.find("td").eq(0).text(e),this.updateVtsActionButtons(r.find("button"),e),this.adjustTimes(n),o="Row "+e+" has been moved "+i,o+=" and is now Row "+n,this.showVtsAlert(o)},AblePlayer.prototype.adjustTimes=function(e){var i,s,a,n,r,o,h,l,p,c,u,d,f,g,_,v;(i=[]).captions=.001,i.descriptions=.001,i.chapters=.001,l=(r=(s=t("#able-vts table").find("tr")).eq(e)).is('[class^="kind-"]')?this.getKindFromClass(r.attr("class")):"captions",u=this.getSecondsFromColonTime(r.find("td").eq(2).text()),g=this.getSecondsFromColonTime(r.find("td").eq(3).text()),e>1?(a=e-1,p=(o=s.eq(a)).is('[class^="kind-"]')?this.getKindFromClass(o.attr("class")):null,d=this.getSecondsFromColonTime(o.find("td").eq(2).text()),_=this.getSecondsFromColonTime(o.find("td").eq(3).text())):(a=null,o=null,p=null,d=null,_=null),e<s.length-1?(n=e+1,c=(h=s.eq(n)).is('[class^="kind-"]')?this.getKindFromClass(h.attr("class")):null,f=this.getSecondsFromColonTime(h.find("td").eq(2).text()),v=this.getSecondsFromColonTime(h.find("td").eq(3).text())):(n=null,h=null,c=null,f=null,v=null),isNaN(u)?(null==p&&(p="captions",o.attr("class","kind-captions"),o.find("td").eq(1).html("captions")),"captions"===p?(u=(parseFloat(_)+.001).toFixed(3),g=f?(parseFloat(f)-.001).toFixed(3):(parseFloat(u)+i[l]).toFixed(3)):"chapters"===p?(u=(parseFloat(d)+.001).toFixed(3),g=f?(parseFloat(f)-.001).toFixed(3):(parseFloat(u)+minDurartion[l]).toFixed(3)):"descriptions"===p&&(u=(parseFloat(d)+i.descriptions).toFixed(3),g=(parseFloat(u)+i.descriptions).toFixed(3))):d?d<u?u<f||(f=(parseFloat(u)+i[l]).toFixed(3),v=(parseFloat(f)+i[c]).toFixed(3)):(u=(parseFloat(d)+i[p]).toFixed(3),g=(parseFloat(u)+i[l]).toFixed(3)):u<f||(f=(parseFloat(u)+i[l]).toFixed(3),v=(parseFloat(f)+i[c]).toFixed(3)),g-u<i[l]&&(g=(parseFloat(u)+i[l]).toFixed(3),f&&(f=(parseFloat(g)+.001).toFixed(3))),r.find("td").eq(2).text(this.formatSecondsAsColonTime(u,!0)),r.find("td").eq(3).text(this.formatSecondsAsColonTime(g,!0)),o&&(o.find("td").eq(2).text(this.formatSecondsAsColonTime(d,!0)),o.find("td").eq(3).text(this.formatSecondsAsColonTime(_,!0))),h&&(h.find("td").eq(2).text(this.formatSecondsAsColonTime(f,!0)),h.find("td").eq(3).text(this.formatSecondsAsColonTime(v,!0)))},AblePlayer.prototype.getKindFromClass=function(t){var e,i;return(e=t.indexOf("kind-")+5,-1==(i=t.indexOf(" ",e)))?t.substring(e):t.substring(e,i)},AblePlayer.prototype.showVtsAlert=function(t){this.$vtsAlert.text(t).show().delay(3e3).fadeOut("slow")},AblePlayer.prototype.parseVtsOutput=function(e){var i,s,a,n,r,o,h,l,p,c;for(s=0,i=e.attr("lang"),a=["captions","chapters","descriptions","subtitles"],r={};s<a.length;s++)r[n=a[s]]="WEBVTT\n\n";if((o=e.find("tr")).length>0)for(s=0;s<o.length;s++)n=o.eq(s).find("td").eq(1).text(),-1!==t.inArray(n,a)&&(h=o.eq(s).find("td").eq(2).text(),l=o.eq(s).find("td").eq(3).text(),p=o.eq(s).find("td").eq(4).text(),void 0!==h&&void 0!==l&&(r[n]+=h+" --> "+l+"\n","undefined"!==p&&(r[n]+=p),r[n]+="\n\n"));for(c=t("<div>",{id:"able-vts-output"}),t("#able-vts").append(c),s=0;s<a.length;s++)r[n=a[s]].length>8&&this.showWebVttOutput(n,r[n],i)},AblePlayer.prototype.showWebVttOutput=function(e,i,s){var a,n,r,o,h;a=t("<h3>").text(e.charAt(0).toUpperCase()+e.slice(1)),n=this.getFilenameFromTracks(e,s),o="If you made changes, copy/paste the following content ",n?(o+="to replace the original content of your "+this.getLanguageName(s)+" ",o+="<em>"+e+"</em> WebVTT file (<strong>"+n+"</strong>
16935)."):o+="into a new "+this.getLanguageName(s)+" <em>"+e+"</em> WebVTT file.",r=t("<p>",{class:"able-vts-output-instructions"}).html(o),h=t("<textarea>").text(i),t("#able-vts-output").append(a,r,h)}}(jQuery),function(t){AblePlayer.prototype.initVimeoPlayer=function(){var e,i,s,a,n,r,o;return e=this,s=(i=new t.Deferred).promise(),a=this.mediaId+"_vimeo",this.$mediaContainer.prepend(t("<div>").attr("id",a)),n=this.vimeoDescId&&this.prefDesc?this.vimeoDescId:this.vimeoId,this.activeVimeoId=n,r=this.okToPlay?"true":"false",o=this.playerWidth?this.vimeoUrlHasParams?{url:n,width:this.playerWidth,controls:!1}:{id:n,width:this.playerWidth,controls:!1}:this.vimeoUrlHasParams?{url:n,controls:!1}:{id:n,controls:!1},this.vimeoPlayer=new Vimeo.Player(a,o),this.vimeoPlayer.ready().then(function(){t("#"+a).children("iframe").attr({tabindex:"-1","aria-hidden":!0}),e.vimeoPlayer.getVideoWidth().then(function(t){t&&e.vimeoPlayer.getVideoHeight().then(function(i){i&&e.resizePlayer(t,i)})}).catch(function(t){}),e.hasPlaylist||(e.$media.remove(),e.vimeoPlaybackRate=1,e.vimeoPlayer.setPlaybackRate(e.vimeoPlaybackRate).then(function(t){e.vimeoSupportsPlaybackRateChange=!0}).catch(function(t){e.vimeoSupportsPlaybackRateChange=!1}),i.resolve())}),s},AblePlayer.prototype.getVimeoPaused=function(){var e,i;return i=(e=new t.Deferred).promise(),this.vimeoPlayer.getPaused().then(function(t){e.resolve(t)}),i},AblePlayer.prototype.getVimeoEnded=function(){var e,i;return i=(e=new t.Deferred).promise(),this.vimeoPlayer.getEnded().then(function(t){e.resolve(t)}),i},AblePlayer.prototype.getVimeoState=function(){var e,i,s,a,n,r;return e=this,s=(i=new t.Deferred).promise(),a=[],n=this.vimeoPlayer.getPaused(),r=this.vimeoPlayer.getEnded(),a.push(n),a.push(r),n.then(function(t){i.resolve(t)}),r.then(function(t){i.resolve(t)}),t.when.apply(t,a).then(function(){i.resolve()}),s},AblePlayer.prototype.getVimeoCaptionTracks=function(){var e,i,s,a=new t.Deferred,n=a.promise();return e=this,this.vimeoPlayer.getTextTracks().then(function(t){if(t.length){for(i=0;i<t.length;i++)e.hasCaptions=!0,1===e.prefCaptions?e.captionsOn=!0:e.captionsOn=!1,s=t[i].language===e.lang,e.tracks.push({kind:t[i].kind,language:t[i].language,label:t[i].label,def:s});e.captions=e.tracks,e.hasCaptions=!0,e.setupPopups("captions"),a.resolve()}else e.hasCaptions=!1,e.usingVimeoCaptions=!1,a.resolve()}),n},AblePlayer.prototype.getVimeoPosterUrl=function(t,e){var i="https://img.youtube.com/vi/"+youTubeId;return"120"==e?i+"/default.jpg":"320"==e?i+"/hqdefault.jpg":"480"==e?i+"/hqdefault.jpg":"640"==e&&i+"/sddefault.jpg"},AblePlayer.prototype.getVimeoId=function(t){var e,i;return(this.vimeoUrlHasParams=!1,"number"==typeof t||-1===t.indexOf("vimeo.com"))?t:-1!==t.indexOf("?")?(this.vimeoUrlHasParams=!0,t):(e=(t=t.trim()).lastIndexOf("/")+1,i=t.substring(e))}}(jQuery);
16936			}
16937			else { 			
16938				url = url.trim(); 
16939				idStartPos = url.lastIndexOf('/') + 1; 
16940				id = url.substring(idStartPos); 
16941				return id; 
16942			}
16943		}
16944		else { 
16945			return url; 
16946		}
16947};	
16948
16949})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.