PageSourceSearch

https://www.gmina-pionki.pl/wcag_video_player/ableplayer.min.js

js gmina-pionki.pl collected 2026-10-02 21:18:52 UTC 511,039 bytes, 16,159 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	// YouTube Player Parameters
14	https://developers.google.com/youtube/player_parameters?playerVersion=HTML5
15
16	// YouTube Data API
17	https://developers.google.com/youtube/v3
18
19	// Vimeo Player API
20	https://github.com/vimeo/player.js
21
22	// Google API Client Library for JavaScript
23	https://developers.google.com/api-client-library/javascript/dev/dev_jscript
24
25	// Google API Explorer: YouTube services and methods
26	https://developers.google.com/apis-explorer/#s/youtube/v3/
27
28	// Web Speech API (Speech Synthesis)
29	// https://w3c.github.io/speech-api/#tts-section
30	// https://developer.mozilla.org/en-US/docs/Web/API/Window/speechSynthesis
31*/
32
33/*jslint node: true, browser: true, white: true, indent: 2, unparam: true, plusplus: true */
34/*global $, jQuery */
35"use strict";
36
37// maintain an array of Able Player instances for use globally (e.g., for keeping prefs in sync)
38var AblePlayerInstances = [];
39
40(function ($) {
41	$(document).ready(function () {
42
43		$('video, audio').each(function (index, element) {
44			if ($(element).data('able-player') !== undefined) {
45				AblePlayerInstances.push(new AblePlayer($(this),$(element)));
46			}
47		});
48	});
49
50	// YouTube player support; pass ready event to jQuery so we can catch in player.
51	window.onYouTubeIframeAPIReady = function() {
52		AblePlayer.youtubeIframeAPIReady = true;
53		$('body').trigger('youtubeIframeAPIReady', []);
54	};
55	// If there is only one player on the page, dispatch global keydown events to it
56	// Otherwise, keydowwn events are handled locally (see event.js > handleEventListeners())
57	$(window).keydown(function(e) {
58		if (AblePlayer.nextIndex === 1) {
59			AblePlayer.lastCreated.onPlayerKeyPress(e);
60		}
61	});
62
63	// Construct an AblePlayer object
64	// Parameters are:
65	// media - jQuery selector or element identifying the media.
66	window.AblePlayer = function(media) {
67		// Keep track of the last player created for use with global events.
68		AblePlayer.lastCreated = this;
69		this.media = media;
70		if ($(media).length === 0) {
71			this.provideFallback();
72			return;
73		}
74
75		///////////////////////////////
76		//
77		// Default variables assignment
78		//
79		///////////////////////////////
80
81		// The following variables CAN be overridden with HTML attributes
82
83		// autoplay (Boolean; if present always resolves to true, regardless of value)
84		if ($(media).attr('autoplay') !== undefined) {
85			this.autoplay = true; // this value remains constant
86			this.okToPlay = true; // this value can change dynamically
87		}
88		else {
89			this.autoplay = false;
90			this.okToPlay = false;
91		}
92
93		// loop (Boolean; if present always resolves to true, regardless of value)
94		if ($(media).attr('loop') !== undefined) {
95			this.loop = true;
96		}
97		else {
98			this.loop = false;
99		}
100
101		// playsinline (Boolean; if present always resolves to true, regardless of value)
102		if ($(media).attr('playsinline') !== undefined) {
103			this.playsInline = '1'; // this value gets passed to YT.Player contructor in youtube.js
104		}
105		else {
106			this.playsInline = '0';
107		}
108
109		// poster (Boolean, indicating whether media element has a poster attribute)
110		if ($(media).attr('poster')) {
111			this.hasPoster = true;
112		}
113		else {
114			this.hasPoster = false;
115		}
116
117		// start-time
118		if ($(media).data('start-time') !== undefined && $.isNumeric($(media).data('start-time'))) {
119			this.startTime = $(media).data('start-time');
120		}
121		else {
122			this.startTime = 0;
123		}
124
125		// debug
126		if ($(media).data('debug') !== undefined && $(media).data('debug') !== false) {
127			this.debug = true;
128		}
129		else {
130			this.debug = false;
131		}
132
133		// Path to root directory of Able Player code
134		if ($(media).data('root-path') !== undefined) {
135			// add a trailing slash if there is none
136			this.rootPath = $(media).data('root-path').replace(/\/?$/, '/');
137		}
138		else {
139			this.rootPath = this.getRootPath();
140		}
141
142		// Volume
143		// Range is 0 to 10. Best not to crank it to avoid overpowering screen readers
144		this.defaultVolume = 7;
145		if ($(media).data('volume') !== undefined && $(media).data('volume') !== "") {
146			var volume = $(media).data('volume');
147			if (volume >= 0 && volume <= 10) {
148				this.defaultVolume = volume;
149			}
150		}
151		this.volume = this.defaultVolume;
152
153		// Optional Buttons
154		// Buttons are added to the player controller if relevant media is present
155		// However, in some applications it might be undesirable to show buttons
156		// (e.g., if chapters or transcripts are provided in an external 
156container)
157
158		if ($(media).data('use-chapters-button') !== undefined && $(media).data('use-chapters-button') === false) {
159			this.useChaptersButton = false;
160		}
161		else {
162			this.useChaptersButton = true;
163		}
164
165		if ($(media).data('use-descriptions-button') !== undefined && $(media).data('use-descriptions-button') === false) {
166			this.useDescriptionsButton = false;
167		}
168		else {
169			this.useDescriptionsButton = true;
170		}
171
172		// Silence audio description
173		// set to "false" if the sole purposes of the WebVTT descriptions file
174		// is to display description text visibly and to integrate it into the transcript
175		if ($(media).data('descriptions-audible') !== undefined && $(media).data('descriptions-audible') === false) {
176			this.exposeTextDescriptions = false;
177		}
178		else if ($(media).data('description-audible') !== undefined && $(media).data('description-audible') === false) {
179  		// support both singular and plural spelling of attribute
180			this.exposeTextDescriptions = false;
181		}
182		else {
183			this.exposeTextDescriptions = true;
184		}
185
186		// Headings
187		// By default, an off-screen heading is automatically added to the top of the media player
188		// It is intelligently assigned a heading level based on context, via misc.js > getNextHeadingLevel()
189		// Authors can override this behavior by manually assigning a heading level using data-heading-level
190		// Accepted values are 1-6, or 0 which indicates "no heading"
191		// (i.e., author has already hard-coded a heading before the media player; Able Player doesn't need to do this)
192		if ($(media).data('heading-level') !== undefined && $(media).data('heading-level') !== "") {
193			var headingLevel = $(media).data('heading-level');
194			if (/^[0-6]*$/.test(headingLevel)) { // must be a valid HTML heading level 1-6; or 0
195				this.playerHeadingLevel = headingLevel;
196			}
197		}
198
199		// Transcripts
200		// There are three types of interactive transcripts.
201		// In descending of order of precedence (in case there are conflicting tags), they are:
202		// 1. "manual" - A manually coded external transcript (requires data-transcript-src)
203		// 2. "external" - Automatically generated, written to an external div (requires data-transcript-div)
204		// 3. "popup" - Automatically generated, written to a draggable, resizable popup window that can be toggled on/off with a button
205		// If data-include-transcript="false", there is no "popup" transcript
206
207		if ($(media).data('transcript-div') !== undefined && $(media).data('transcript-div') !== "") {
208		  this.transcriptDivLocation = $(media).data('transcript-div');
209		}
210		else {
211  		this.transcriptDivLocation = null;
212    }
213		if ($(media).data('include-transcript') !== undefined && $(media).data('include-transcript') === false) {
214  		this.hideTranscriptButton = true;
215    }
216    else {
217      this.hideTranscriptButton = null;
218    }
219
220		this.transcriptType = null;
221		if ($(media).data('transcript-src') !== undefined) {
222			this.transcriptSrc = $(media).data('transcript-src');
223			if (this.transcriptSrcHasRequiredParts()) {
224				this.transcriptType = 'manual';
225			}
226		}
227		else if ($(media).find('track[kind="captions"], track[kind="subtitles"]').length > 0) {
228			// required tracks are present. COULD automatically generate a transcript
229      if (this.transcriptDivLocation) {
230				this.transcriptType = 'external';
231			}
232			else {
233				this.transcriptType = 'popup';
234			}
235		}
236
237		// In "Lyrics Mode", line breaks in WebVTT caption files are supported in the transcript
238		// If false (default), line breaks are are removed from transcripts in order to provide a more seamless reading experience
239		// If true, line breaks are preserved, so content can be presented karaoke-style, or as lines in a poem
240		if ($(media).data('lyrics-mode') !== undefined && $(media).data('lyrics-mode') !== false) {
241			this.lyricsMode = true;
242		}
243		else {
244			this.lyricsMode = false;
245		}
246
247		// Transcript Title
248		if ($(media).data('transcript-title') !== undefined && $(media).data('transcript-title') !== "") {
249			this.transcriptTitle = $(media).data('transcript-title');
250		}
251		else {
252			// do nothing. The default title will be defined later (see transcript.js)
253		}
254
255		// Captions
256		// data-captions-position can be used to set the default captions position
257		// this is only the default, and can be overridden by user preferences
258		// valid values of data-captions-position are 'below' and 'overlay'
259		if ($(media).data('captions-position') === 'overlay') {
260			this.defaultCaptionsPosition = 'overlay';
261		}
262		else { // the default, even if not specified
263			this.defaultCaptionsPosition = 'below';
264		}
265
266		// Chapters
267		if ($(media).data('chapters-div') !== undefined && $(media).data('chapters-div') !== "") {
268			this.chaptersDivLocation = $(media).data('chapters-div');
269		}
270
271		if ($(media).data('chapters-title') !== undefined) {
272			// NOTE: empty string is valid; results in no title being displayed
273			this.chaptersTitle = $(media).data('chapters-title');
274		}
275
276		if ($(media).data('chapters-default') !== undefined && $(media).data('chapters-default') !== "") {
277			this.defaultChapter = $(media).data('chapters-default');
278		}
279		else {
280			this.defaultChapter = null;
281		}
282
283		// Slower/Faster buttons
284		// valid values of data-speed-icons are 'animals' (default) and 'arrows'
285		// 'animals' uses turtle and rabbit; 'arrows' uses up/down arrows
286		if ($(media).data('speed-icons') === 'arrows') {
287			this.speedIcons = 'arrows';
288		}
289		else {
290			this.speedIcons = 'animals';
291		}
292
293		// Seekbar
294		// valid values of data-seekbar-scope are 'chapter' and 'video'; will also accept 'chapters'
295		if ($(media).data('seekbar-scope') === 'chapter' || $(media).data('seekbar-scope') === 'chapters') {
296			this.seekbarScope = 'chapter';
297		}
298		else {
299			this.seekbarScope = 'video';
300		}
301
302		// YouTube
303		if ($(media).data('youtube-id') !== undefined && $(media).data('youtube-id') !== "") {
304			this.youTubeId = $(media).data('youtube-id');
305		}
306
307		if ($(media).data('youtube-desc-id') !== undefined && $(media).data('youtube-desc-id') !== "") {
308			this.youTubeDescId = $(media).data('youtube-desc-id');
309		}
310
311		if ($(media).data('youtube-nocookie') !== undefined && $(media).data('youtube-nocookie')) {
312			this.youTubeNoCookie = true;
313		}
314		else {
315			this.youTubeNoCookie = false;
316		}
317
318		// Vimeo
319		if ($(media).data('vimeo-id') !== undefined && $(media).data('vimeo-id') !== "") {
320			this.vimeoId = $(media).data('vimeo-id');
321		}
322		if ($(media).data('vimeo-desc-id') !== undefined && $(media).data('vimeo-desc-id') !== "") {
323			this.vimeoDescId = $(media).data('vimeo-desc-id');
324		}
325
326		// Skin
327		// valid values of data-skin are:
328		// 'legacy' (default), two rows of controls; seekbar positioned in available space within top row
329		// '2020', all buttons in one row beneath a full-width seekbar
330		if ($(media).data('skin') == '2020') {
331			this.skin = '2020';
332		}
333		else {
334			this.skin = 'legacy';
335		}
336
337		// Icon type
338		// By default, AblePlayer 3.0.33 and higher uses SVG icons for the player controls
339		// Fallback for browsers that don't support SVG is scalable icomoon fonts
340		// Ultimate fallback is images, if the user has a custom style sheet that overrides font-family
341		// Use data-icon-type to force controls to use either 'svg', 'font', or 'images'
342		this.iconType = 'font';
343		this.forceIconType = false;
344		if ($(media).data('icon-type') !== undefined && $(media).data('icon-type') !== "") {
345			var iconType = $(media).data('icon-type');
346			if (iconType === 'font' || iconType == 'image' || iconType == 'svg') {
347				this.iconType = iconType;
348				this.forceIconType = true;
349			}
350		}
351
352		if ($(media).data('allow-fullscreen') !== undefined && $(media).data('allow-fullscreen') === false) {
353			this.allowFullScreen = false;
354		}
355		else {
356			this.allowFullScreen = true;
357		}
358
359		// Seek interval
360		// Number of seconds to seek forward or back with Rewind & Forward buttons
361		// Unless specified with data-seek-interval, the default value is re-calculated in initialize.js > setSeekInterval();
362		// Calculation attempts to intelligently assign a reasonable interval based on media length
363		this.defaultSeekInterval = 10;
364		this.useFixedSeekInterval = false; // will change to true if media has valid data-seek-interval attribute
365		if ($(media).data('seek-interval') !== undefined && $(media).data('seek-interval') !== "") {
366			var seekInterval = $(media).data('seek-interval');
367			if (/^[1-9][0-9]*$/.test(seekInterval)) { // must be a whole number greater than 0
368				this.seekInterval = seekInterval;
369				this.useFixedSeekInterval = true; // do not override with calculuation
370			}
371		}
372
373		// Now Playing
374		// Shows "Now Playing:" plus the title of the current track above player
375		// Only used if there is a playlist
376		if ($(media).data('show-now-playing') !== undefined && $(media).data('show-now-playing') === false) {
377			this.showNowPlaying = false;
378		}
379		else {
380			this.showNowPlaying = true;
381		}
382
383		// TTML support (experimental); enabled for testing with data-use-ttml (Boolean)
384		if ($(media).data('use-ttml') !== undefined) {
385			this.useTtml = true;
386			// The following may result in a console error.
387			this.convert = require('xml-js');
388		}
389		else {
390			this.useTtml = false;
391		}
392
393		// Fallback
394		// The only supported fallback content as of version 4.0 is:
395		// 1. Content nested within the <audio> or <video> element.
396		// 2. A standard localized message (see buildplayer.js > provideFallback()
397		// The data-test-fallback attribute can be used to test the fallback solution in any browser
398		if ($(media).data('test-fallback') !== undefined && $(media).data('test-fallback') !== false) {
399			this.testFallback = true;
400		}
401
402		// Language
403		this.lang = 'en';
404		if ($(media).data('lang') !== undefined && $(media).data('lang') !== "") {
405			var lang = $(media).data('lang');
406			if (lang.length == 2) {
407				this.lang = lang;
408			}
409		}
410		// Player language is determined as follows (in translation.js > getTranslationText() ):
411		// 1. Lang attributes on <html> or <body>, if a matching translation file is available
412		// 2. The value of this.lang, if a matching translation file is available
413		// 3. English
414		// To override this formula and force #2 to take precedence over #1, set data-force-lang="true"
415		if ($(media).data('force-lang') !== undefined && $(media).data('force-lang') !== false) {
416			this.forceLang = true;
417		}
418		else {
419			this.forceLang = false;
420		}
421
422		// Metadata Tracks
423		if ($(media).data('meta-type') !== undefined && $(media).data('meta-type') !== "") {
424			this.metaType = $(media).data('meta-type');
425		}
426
427		if ($(media).data('meta-div') !== undefined && $(media).data('meta-div') !== "") {
428			this.metaDiv = $(media).data('meta-div');
429		}
430
431		// Search
432		if ($(media).data('search') !== undefined && $(media).data('search') !== "") {
433			// conducting a search currently requires an external div in which to write the results
434			if ($(media).data('search-div') !== undefined && $(media).data('search-div') !== "") {
435				this.searchString = $(media).data('search');
436				this.searchDiv = $(media).data('search-div');
437			}
438
439			// Search Language
440			if ($(media).data('search-lang') !== undefined && $(media).data('search-lang') !== "") {
441				this.searchLang = $(media).data('search-lang');
442			}
443			else {
444				this.searchLang = null; // will change to final value of this.lang in translation.js > getTranslationText()
445			}
446
447			// conducting a search currently requires an external div in which to write the results
448			if ($(media).data('search-div') !== undefined && $(media).data('search-div') !== "") {
449				this.searchString = $(media).data('search');
450				this.searchDiv = $(media).data('search-div');
451			}
452		}
453
454		// Hide controls when video starts playing
455		// They will reappear again when user presses a key or moves the mouse
456		// As of v4.0, controls are hidden automatically on playback in fullscreen mode
457		if ($(media).data('hide-controls') !== undefined && $(media).data('hide-controls') !== false) {
458			this.hideControls = true;
459			this.hideControlsOriginal = true; // a copy of hideControls, since the former may change if user enters full screen mode
460		}
461		else {
462			this.hideControls = false;
463			this.hideControlsOriginal = false;
464		}
465
466		// Steno mode
467		// Enable support for Able Player keyboard shortcuts in textaarea fields
468		// so users can control the player while transcribing
469		if ($(media).data('steno-mode') !== undefined && $(media).data('steno-mode') !== false) {
470			this.stenoMode = true;
471		}
472		else {
473			this.stenoMode = false;
474		}
475
476		// Define built-in variables that CANNOT be overridden with HTML attributes
477		this.setDefaults();
478
479		////////////////////////////////////////
480		//
481		// End assignment of default variables
482		//
483		////////////////////////////////////////
484
485		this.ableIndex = AblePlayer.nextIndex;
486		AblePlayer.nextIndex += 1;
487
488		this.title = $(media).attr('title');
489
490		// populate translation object with localized versions of all labels and prompts
491		// use defer method to defer additional processing until text is retrieved
492		this.tt = {};
493		var thisObj = this;
494		$.when(this.getTranslationText()).then(
495			function () {
496				if (thisObj.countProperties(thisObj.tt) > 50) {
497					// close enough to ensure that most text variables are populated
498					thisObj.setup();
499				}
500				else {
501					// can't continue loading player with no text
502					thisObj.provideFallback();
503				}
504			}
505		);
506	};
507
508	// Index to increment every time new player is created.
509	AblePlayer.nextIndex = 0;
510
511	AblePlayer.prototype.setup = function() {
512
513		var thisObj = this;
514		this.initializing = true; // will remain true until entire sequence of function calls is complete
515		this.reinitialize().then(function () {
516			if (!thisObj.player) {
517				// No player for this media, show last-line fallback.
518				thisObj.provideFallback();
519			}
520			else {
521				thisObj.setupInstance().then(function () {
522					thisObj.setupInstancePlaylist();
523					if (!thisObj.hasPlaylist) {
524						// for playlists, recreatePlayer() is called from within cuePlaylistItem()
525						thisObj.recreatePlayer();
526					}
527					thisObj.initializing = false;
528					thisObj.playerCreated = true; // remains true until browser is refreshed
529				});
530			}
531		});
532	};
533
534	AblePlayer.getActiveDOMElement = function () {
535		var activeElement = document.activeElement;
536
537		// For shadow DOMs we need to keep digging down through the DOMs
538		while (activeElement.shadowRoot && activeElement.shadowRoot.activeElement) {
539			activeElement = activeElement.shadowRoot.activeElement;
540		}
541
542		return activeElement;
543	};
544
545	AblePlayer.localGetElementById = function(element, id) {
546		if (element.getRootNode)
547		{
548			// Use getRootNode() and querySelector() where supported (for shadow DOM support)
549			return $(element.getRootNode().querySelector('#' + id));
550		}
551		else
552		{
553			// If getRootNode is not supported it should be safe to use document.getElementById (since there is no shadow DOM support)
554			return $(document.getElementById(id));
555		}
556	};
557
558
559
560	AblePlayer.youtubeIframeAPIReady = false;
561	AblePlayer.loadingYoutubeIframeAPI = false;
562})(jQuery);
563
564(function ($) {
565	// Set default variable values.
566	AblePlayer.prototype.setDefaults = function () {
567
568		this.playerCreated = false; // will set to true after recreatePlayer() is complete the first time
569		this.playing = false; // will change to true after 'playing' event is triggered
570		this.paused = true; // will always be the opposite of this.playing (available for convenience)
571		this.clickedPlay = false; // will change to true temporarily if user clicks 'play' (or pause)
572		this.fullscreen = false; // will change to true if player is in full screen mode
573		this.swappingSrc = false; // will change to true temporarily while media source is being swapped
574		this.initializing = false; // will change to true temporarily while initPlayer() is processing
575		this.cueingPlaylistItems = false; // will change to true temporarily while cueing next playlist item
576		this.okToPlay = false; // will change to true if conditions are acceptible for automatic playback after media loads
577		this.buttonWithFocus = null; // will change to 'previous' or 'next' if user clicks either of those buttons
578
579		this.getUserAgent();
580		this.setIconColor();
581		this.setButtonImages();
582	};
583
584	AblePlayer.prototype.getRootPath = function() {
585
586		// returns Able Player root path (assumes ableplayer.js is in /build, one directory removed from root)
587		var scripts, i, scriptSrc, scriptFile, fullPath, ablePath, parentFolderIndex, rootPath;
588		scripts= document.getElementsByTagName('script');
589		for (i=0; i < scripts.length; i++) {
590			scriptSrc = scripts[i].src;
591			scriptFile = scriptSrc.substr(scriptSrc.lastIndexOf('/'));
592			if (scriptFile.indexOf('ableplayer') !== -1) {
593				// this is the ableplayerscript
594				fullPath = scriptSrc.split('?')[0]; // remove any ? params
595				break;
596			}
597		}
598		ablePath= fullPath.split('/').slice(0, -1).join('/'); // remove last filename part of path
599		parentFolderIndex = ablePath.lastIndexOf('/');
600		rootPath = ablePath.substring(0, parentFolderIndex) + '/';
601		return rootPath;
602	}
603
604	AblePlayer.prototype.setIconColor = function() {
605
606		// determine the best color choice (white or black) for icons,
607		// given the background-color of their container elements
608		// Source for relative luminance formula:
609		// https://en.wikipedia.org/wiki/Relative_luminance
610
611		// We need to know the color *before* creating the element
612		// so the element doesn't exist yet when this function is called
613		// therefore, need to create a temporary element then remove it after color is determined
614		// Temp element must be added to the DOM or WebKit can't retrieve its CSS properties
615
616		var $elements, i, $el, bgColor, rgb, red, green, blue, luminance, iconColor;
617
618		$elements = ['controller', 'toolbar'];
619		for (i=0; i<$elements.length; i++) {
620			if ($elements[i] == 'controller') {
621				$el =	 $('<div>', {
622					'class': 'able-controller'
623				}).hide();
624			}
625			else if ($elements[i] === 'toolbar') {
626				$el =	 $('<div>', {
627					'class': 'able-window-toolbar'
628				}).hide();
629			}
630			$('body').append($el);
631			bgColor = $el.css('background-color');
632			// bgColor is a string in the form 'rgb(R, G, B)', perhaps with a 4th item for alpha;
633			// split the 3 or 4 channels into an array
634			rgb = bgColor.replace(/[^\d,]/g, '').split(',');
635			red = rgb[0];
636			green = rgb[1];
637			blue = rgb[2];
638			luminance = (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
639			// range is 1 - 255; therefore 125 is the tipping point
640			if (luminance < 125) { // background is dark
641				iconColor = 'white';
642			}
643			else { // background is light
644				iconColor = 'black';
645			}
646			if ($elements[i] === 'controller') {
647				this.iconColor = iconColor;
648			}
649			else if ($elements[i] === 'toolbar') {
650				this.toolbarIconColor = iconColor;
651			}
652			$el.remove();
653		}
654	};
655
656	AblePlayer.prototype.setButtonImages = function() {
657
658		// NOTE: volume button images are now set dynamically within volume.js
659		this.imgPath = this.rootPath + 'button-icons/' + this.iconColor + '/';
660		this.playButtonImg = this.imgPath + 'play.png';
661		this.pauseButtonImg = this.imgPath + 'pause.png';
662
663		this.restartButtonImg = this.imgPath + 'restart.png';
664
665		this.rewindButtonImg = this.imgPath + 'rewind.png';
666		this.forwardButtonImg = this.imgPath + 'forward.png';
667
668		this.previousButtonImg = this.imgPath + 'previous.png';
669		this.nextButtonImg = this.imgPath + 'next.png';
670
671		if (this.speedIcons === 'arrows') {
672			this.fasterButtonImg = this.imgPath + 'slower.png';
673			this.slowerButtonImg = this.imgPath + 'faster.png';
674		}
675		else if (this.speedIcons === 'animals') {
676			this.fasterButtonImg = this.imgPath + 'rabbit.png';
677			this.slowerButtonImg = this.imgPath + 'turtle.png';
678		}
679
680		this.captionsButtonImg = this.imgPath + 'captions.png';
681		this.chaptersButtonImg = this.imgPath + 'chapters.png';
682		this.signButtonImg = this.imgPath + 'sign.png';
683		this.transcriptButtonImg = this.imgPath + 'transcript.png';
684		this.descriptionsButtonImg = this.imgPath + 'descriptions.png';
685
686		this.fullscreenExpandButtonImg = this.imgPath + 'fullscreen-expand.png';
687		this.fullscreenCollapseButtonImg = this.imgPath + 'fullscreen-collapse.png';
688
689		this.prefsButtonImg = this.imgPath + 'preferences.png';
690		this.helpButtonImg = this.imgPath + 'help.png';
691	};
692
693	AblePlayer.prototype.getSvgData = function(button) {
694
695		// returns array of values for creating <svg> tag for specified button
696		// 0 = <svg> viewBox attribute
697		// 1 = <path> d (description) attribute
698		var svg = Array();
699
700		switch (button) {
701
702			case 'play':
703				svg[0] = '0 0 16 20';
704				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';
705				break;
706
707			case 'pause':
708				svg[0] = '0 0 20 20';
709				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';
710				break;
711
712			case 'stop':
713				svg[0] = '0 0 20 20';
714				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';
715				break;
716
717			case 'restart':
718				svg[0] = '0 0 20 20';
719				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';
720				break;
721
722			case 'rewind':
723				svg[0] = '0 0 20 20';
724				svg[1] = 'M11.25 3.125v6.25l6.25-6.25v13.75l-6.25-6.25v6.25l-6.875-6.875z';
725				break;
726
727			case 'forward':
728				svg[0] = '0 0 20 20';
729				svg[1] = 'M10 16.875v-6.25l-6.25 6.25v-13.75l6.25 6.25v-6.25l6.875 6.875z';
730				break;
731
732			case 'previous':
733				svg[0] = '0 0 20 20';
734				svg[1] = 'M5 17.5v-15h2.5v6.875l6.25-6.25v13.75l-6.25-6.25v6.875z';
735				break;
736
737			case 'next':
738				svg[0] = '0 0 20 20';
739				svg[1] = 'M15 2.5v15h-2.5v-6.875l-6.25 6.25v-13.75l6.25 6.25v-6.875z';
740				break;
741
742			case 'slower':
743				svg[0] = '0 0 20
vendor: 14,731 bytes, lines 743-836
743 20';
744				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';
745				break;
746
747			case 'faster':
748				svg[0] = '0 0 11 20';
749				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';
750				break;
751
752			case 'turtle':
753				svg[0] = '0 0 20 20';
754				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';
755				break;
756
757			case 'rabbit':
758				svg[0] = '0 0 20 20';
759				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';
760				break;
761
762			case 'ellipsis':
763				svg[0] = '0 0 20 20';
764				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';
765				break;
766
767			case 'pipe':
768				svg[0] = '0 0 20 20';
769				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';
770				break;
771
772			case 'captions':
773				svg[0] = '0 0 20 20';
774				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';
775				break;
776
777			case 'descriptions':
778				svg[0] = '0 0 20 20';
779				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';
780				break;
781
782			case 'sign':
783				svg[0] = '0 0 20 20';
784				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';
785				break;
786
787			case 'mute':
788			case 'volume-mute':
789				svg[0] = '0 0 20 20';
790				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';
791				break;
792
793			case 'volume-soft':
794				svg[0] = '0 0 20 20';
795				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';
796				break;
797
798			case 'volume-medium':
799				svg[0] = '0 0 20 20';
800				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';
801				break;
802
803			case 'volume-loud':
804				svg[0] = '0 0 21 20';
805				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';
806				break;
807
808			case 'chapters':
809				svg[0] = '0 0 20 20';
810				svg[1] = 'M5 2.5v17.5l6.25-6.25 6.25 6.25v-17.5zM15 0h-12.5v17.5l1.25-1.25v-15h11.25z';
811				break;
812
813			case 'transcript':
814				svg[0] = '0 0 20 20';
815				svg[1] = 'M0 19.107v-17.857q0-0.446 0.313-0.759t0.759-0.313h8.929v6.071q0 0.446 0.313 0.759t0.759 0.313h6.071v11.786q0 0.446-0.313 0.759t-0.759 0.312h-15q-0.446 0-0.759-0.313t-0.313-0.759zM4.286 15.536q0 0.156 0.1 0.257t0.257 0.1h7.857q0.156 0 0.257-0.1t0.1-0.257v-0.714q0-0.156-0.1-0.257t-0.257-0.1h-7.857q-0.156 0-0.257 0.1t-0.1 0.257v0.714zM4.286 12.679q0 0.156 0.1 0.257t0.257 0.1h7.857q0.156 0 0.257-0.1t0.1-0.257v-0.714q0-0.156-0.1-0.257t-0.257-0.1h-7.857q-0.156 0-0.257 0.1t-0.1 0.257v0.714zM4.286 9.821q0 0.156 0.1 0.257t0.257 0.1h7.857q0.156 0 0.257-0.1t0.1-0.257v-0.714q0-0.156-0.1-0.257t-0.257-0.1h-7.857q-0.156 0-0.257 0.1t-0.1 0.257v0.714zM11.429 5.893v-5.268q0.246 0.156 0.402 0.313l4.554 4.554q0.156 0.156 0.313 0.402h-5.268z';
816				break;
817
818			case 'preferences':
819				svg[0] = '0 0 20 20';
820				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';
821				break;
822
823			case 'close':
824				svg[0] = '0 0 16 20';
825				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';
826				break;
827
828			case 'fullscreen-expand':
829				svg[0] = '0 0 20 20';
830				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';
831				break;
832
833			case 'fullscreen-collapse':
834				svg[0] = '0 0 20 20';
835				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';
836				break;
837
838			case 'help':
839				svg[0] = '0 0 11 20';
840				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';
841				break;
842		}
843
844		return svg;
845	};
846
847	// Initialize player based on data on page.
848	// This sets some variables, but does not modify anything.	Safe to call multiple times.
849	// Can call again after updating this.media so long as new media element has the same ID.
850	AblePlayer.prototype.reinitialize = function () {
851
852		var deferred, promise, thisObj, errorMsg, srcFile;
853
854		deferred = new $.Deferred();
855		promise = deferred.promise();
856		thisObj = this;
857
858		// if F12 Developer Tools aren't open in IE (through 9, no longer a problen in IE10)
859		// console.log causes an error - can't use debug without a console to log messages to
860		if (! window.console) {
861			this.debug = false;
862		}
863
864		this.startedPlaying = false;
865		// TODO: Move this setting to cookie.
866		this.autoScrollTranscript = true;
867		//this.autoScrollTranscript = this.getCookie(autoScrollTranscript); // (doesn't work)
868
869		// Bootstrap from this.media possibly being an ID or other selector.
870		this.$media = $(this.media).first();
871		this.media = this.$media[0];
872		// Set media type to 'audio' or 'video'; this determines some of the behavior of player creation.
873		if (this.$media.is('audio')) {
874			this.mediaType = 'audio';
875		}
876		else if (this.$media.is('video')) {
877			this.mediaType = 'video';
878		}
879		else {
880			// Able Player was initialized with some element other than <video> or <audio>
881			this.provideFallback();
882			deferred.fail();
883			return promise;
884		}
885
886		this.$sources = this.$media.find('source');
887
888		this.player = this.getPlayer();
889		if (!this.player) {
890			// an error was generated in getPlayer()
891			this.provideFallback();
892		}
893		this.setIconType();
894		this.setDimensions();
895
896		deferred.resolve();
897		return promise;
898	};
899
900	AblePlayer.prototype.setDimensions = function() {
901		// if media element includes width and height attributes,
902		// use these to set the max-width and max-height of the player
903		if (this.$media.attr('width') && this.$media.attr('height')) {
904			this.playerMaxWidth = parseInt(this.$media.attr('width'), 10);
905			this.playerMaxHeight = parseInt(this.$media.attr('height'), 10);
906		}
907		else if (this.$media.attr('width')) {
908			// media element includes a width attribute, but not height
909			this.playerMaxWidth = parseInt(this.$media.attr('width'), 10);
910		}
911		else {
912			// set width to width of #player
913			// don't set height though; YouTube will automatically set that to match width
914			this.playerMaxWidth = this.$media.parent().width();
915			this.playerMaxHeight = this.getMatchingHeight(this.playerMaxWidth);
916		}
917		// override width and height attributes with in-line CSS to make video responsive
918		this.$media.css({
919			'width': '100%',
920			'height': 'auto'
921		});
922	};
923
924	AblePlayer.prototype.getMatchingHeight = function(width) {
925
926		// returns likely height for a video, given width
927		// These calculations assume 16:9 aspect ratio (the YouTube standard)
928		// Videos recorded in other resolutions will be sized to fit, with black bars on each side
929		// This function is only called if the <video> element does not have width and height attributes
930
931		var widths, heights, closestWidth, closestIndex, closestHeight, height;
932
933		widths = [ 3840, 2560, 1920, 1280, 854, 640, 426 ];
934		heights = [ 2160, 1440, 1080, 720, 480, 360, 240 ];
935		closestWidth = null;
936		closestIndex = null;
937
938		$.each(widths, function(index){
939			if (closestWidth == null || Math.abs(this - width) < Math.abs(closestWidth - width)) {
940				closestWidth = this;
941				closestIndex = index;
942			}
943		});
944		closestHeight = heights[closestIndex];
945		this.aspectRatio = closestWidth / closestHeight;
946		height = Math.round(width / this.aspectRatio);
947		return height;
948	};
949
950	AblePlayer.prototype.setIconType = function() {
951
952		// returns either "svg", "font" or "image" (in descending order of preference)
953		// Test for support of each type. If not supported, test the next type.
954		// last resort is image icons
955
956		var $tempButton, $testButton, controllerFont;
957
958		if (this.forceIconType) {
959			// use value specified in data-icon-type
960			return false;
961		}
962
963		// test for SVG support
964		// Test this method widely; failed as expected on IE8 and below
965		// https://stackoverflow.com/a/27568129/744281
966		if (!!(document.createElementNS && document.createElementNS('http://www.w3.org/2000/svg','svg').createSVGRect)) {
967			// browser supports SVG
968			this.iconType = 'svg';
969		}
970		else {
971			// browser does NOT support SVG
972			// test whether browser can support icon fonts, and whether user has overriding the default style sheet
973			// which could cause problems with proper display of the icon fonts
974			if (window.getComputedStyle) {
975
976				// webkit doesn't return calculated styles unless element has been added to the DOM
977				// and is visible (note: visibly clipped is considered "visible")
978				// use playpauseButton for font-family test if it exists; otherwise must create a new temp button
979				if ($('span.icon-play').length) {
980					$testButton = $('span.icon-play');
981				}
982				else {
983					$tempButton = $('<span>',{
984						'class': 'icon-play able-clipped'
985					});
986					$('body').append($tempButton);
987					$testButton = $tempButton;
988				}
989
990				// the following retrieves the computed value of font-family
991				// tested in Firefox 45.x with "Allow pages to choose their own fonts" unchecked - works!
992				// tested in Chrome 49.x with Font Changer plugin - works!
993				// tested in IE with user-defined style sheet enables - works!
994				// It does NOT account for users who have "ignore font styles on web pages" checked in IE
995				// There is no known way to check for that ???
996				controllerFont = window.getComputedStyle($testButton.get(0), null).getPropertyValue('font-family');
997				if (typeof controllerFont !== 'undefined') {
998					if (controllerFont.indexOf('able') !== -1) {
999						this.iconType = 'font';
1000					}
1001					else {
1002						this.iconType = 'image';
1003					}
1004				}
1005				else {
1006					// couldn't get computed font-family; use images to be safe
1007					this.iconType = 'image';
1008				}
1009			}
1010			else { // window.getComputedStyle is not supported (IE 8 and earlier)
1011				// No known way to detect computed font
1012				// The following retrieves the value from the style sheet, not the computed font
1013				// controllerFont = $tempButton.get(0).currentStyle.fontFamily;
1014				// It will therefore return "able", even if the user is overriding that with a custom style sheet
1015				// To be safe, use images
1016				this.iconType = 'image';
1017			}
1018			if (this.debug) {
1019				console.log('Using ' + this.iconType + 's for player controls');
1020			}
1021			if (typeof $tempButton !== 'undefined') {
1022				$tempButton.remove();
1023			}
1024		}
1025	};
1026
1027	// Perform one-time setup for this instance of player; called after player is first initialized.
1028	AblePlayer.prototype.setupInstance = function () {
1029
1030		var deferred = new $.Deferred();
1031		var promise = deferred.promise();
1032
1033		if (this.$media.attr('id')) {
1034			this.mediaId = this.$media.attr('id');
1035		}
1036		else {
1037			// Ensure the base media element always has an ID.
1038			this.mediaId = "ableMediaId_" + this.ableIndex;
1039			this.$media.attr('id', this.mediaId);
1040		}
1041		deferred.resolve();
1042		return promise;
1043	};
1044
1045	AblePlayer.prototype.setupInstancePlaylist = function() {
1046
1047		// find a matching playlist and set this.hasPlaylist
1048		// if there is one, also set this.$playlist, this.playlistIndex, & this.playlistEmbed
1049		var thisObj = this;
1050
1051		this.hasPlaylist = false; // will change to true if a matching playlist is found
1052
1053		$('.able-playlist').each(function() {
1054			if ($(this).data('player') === thisObj.mediaId) {
1055				// this is the playlist for the current player
1056				thisObj.hasPlaylist = true;
1057				// If using an embedded player, we'll replace $playlist with the clone later.
1058				thisObj.$playlist = $(this).find('li');
1059
1060				// check to see if list item has YouTube as its source
1061				// if it does, inject a thumbnail from YouTube
1062				var $youTubeVideos = $(this).find('li[data-youtube-id]');
1063				$youTubeVideos.each(function() {
1064					var youTubeId = $(this).attr('data-youtube-id');
1065					var youTubePoster = thisObj.getYouTubePosterUrl(youTubeId,'120');
1066					var $youTubeImg = $('<img>',{
1067						'src': youTubePoster,
1068						'alt': ''
1069					});
1070					$(this).find('button').prepend($youTubeImg);
1071				});
1072
1073				// add accessibility to the list markup
1074				$(this).find('li span').attr('aria-hidden','true');
1075				thisObj.playlistIndex = 0;
1076				var dataEmbedded = $(this).data('embedded');
1077				if (typeof dataEmbedded !== 'undefined' && dataEmbedded !== false) {
1078					// embed playlist within player
1079					thisObj.playlistEmbed = true;
1080				}
1081				else {
1082					thisObj.playlistEmbed = false;
1083				}
1084			}
1085		});
1086
1087		if (this.hasPlaylist && this.loop) {
1088			// browser will loop the current track in the playlist, rather than the playlist
1089			// therefore, need to remove loop attribute from media element
1090			// but keep this.loop as true and handle the playlist looping ourselves
1091			this.media.removeAttribute('loop');
1092		}
1093		if (this.hasPlaylist && this.playlistEmbed) {
1094			// Copy the playlist out of the dom, so we can reinject when we build the player.
1095			var parent = this.$playlist.parent();
1096			this.$playlistDom = parent.clone();
1097			parent.remove();
1098		}
1099		if (this.hasPlaylist && this.$sources.length === 0) {
1100			// no source elements were provided. Construct them from the first playlist item
1101			this.cuePlaylistItem(0);
1102			// redefine this.$sources now that media contains one or more <source> elements
1103			this.$sources = this.$media.find('source');
1104		}
1105
1106	};
1107
1108	AblePlayer.prototype.recreatePlayer = function () {
1109
1110		// Creates the appropriate player for the current source.
1111		var thisObj, prefsGroups, i;
1112		thisObj = this;
1113
1114		// TODO: Ensure when recreating player that we carry over the mediaId
1115		if (!this.player) {
1116			console.log("Can't create player; no appropriate player type detected.");
1117			return;
1118		}
1119		if (!this.playerCreated) {
1120			// only call these functions once
1121			 this.loadCurrentPreferences();
1122			this.injectPlayerCode();
1123		}
1124
1125		// call all remaining functions each time a new media instance is loaded
1126
1127		this.initSignLanguage();
1128
1129		this.initPlayer().then(function() {
1130
1131			 thisObj.setupTracks().then(function() {
1132
1133				thisObj.setupAltCaptions().then(function() {
1134
1135					thisObj.setupTranscript().then(function() {
1136
1137            thisObj.getMediaTimes().then(function(mediaTimes) {
1138
1139              thisObj.duration = mediaTimes['duration'];
1140              thisObj.elapsed = mediaTimes['elapsed'];
1141
1142              thisObj.setFullscreen(false);
1143
1144              if (typeof thisObj.volume === 'undefined') {
1145  						  thisObj.volume = thisObj.defaultVolume;
1146						  }
1147						  if (thisObj.volume) {
1148                thisObj.setVolume(thisObj.volume);
1149              }
1150
1151              if (thisObj.transcriptType) {
1152							  thisObj.addTranscriptAreaEvents();
1153                thisObj.updateTranscript();
1154						  }
1155              if (thisObj.mediaType === 'video') {
1156							  thisObj.initDescription();
1157						  }
1158              if (thisObj.captions.length) {
1159							  thisObj.initDefaultCaption();
1160						  }
1161
1162              // setMediaAttributes() sets textTrack.mode to 'disabled' for all tracks
1163              // This tells browsers to ignore the text tracks so Able Player can handle them
1164              // However, timing is critical as browsers - especially Safari - tend to ignore this request
1165              // unless it's sent late in the intialization process.
1166              // If browsers ignore the request, the result is redundant captions
1167              thisObj.setMediaAttributes();
1168              thisObj.addControls();
1169              thisObj.addEventListeners();
1170
1171              // inject each of the hidden forms that will be accessed from the Preferences popup menu
1172              prefsGroups = thisObj.getPreferencesGroups();
1173              for (i = 0; i < prefsGroups.length; i++) {
1174							  thisObj.injectPrefsForm(prefsGroups[i]);
1175				      }
1176              thisObj.setupPopups();
1177              thisObj.updateCaption();
1178              thisObj.injectVTS();
1179              if (thisObj.chaptersDivLocation) {
1180							  thisObj.populateChaptersDiv();
1181				      }
1182              thisObj.showSearchResults();
1183
1184              // Go ahead and load media, without user requesting it
1185              // Ideally, we would wait until user clicks play, rather than unnecessarily consume their bandwidth
1186              // However, the media needs to load before the 'loadedmetadata' event is fired
1187              // and until that happens we can't get the media's duration
1188              if (thisObj.player === 'html5') {
1189							  thisObj.$media[0].load();
1190						  }
1191              // refreshControls is called twice building/initializing the player
1192              // this is the second. Best to pause a bit before executing, to be sure all prior steps are complete
1193              setTimeout(function() {
1194							  thisObj.refreshControls('init');
1195						  },100);
1196            });
1197					},
1198					function() {	 // initPlayer fail
1199						thisObj.provideFallback();
1200					});
1201				});
1202			});
1203		});
1204	};
1205
1206	AblePlayer.prototype.initPlayer = function () {
1207
1208		var thisObj = this;
1209		var playerPromise;
1210		// First run player specific initialization.
1211		if (this.player === 'html5') {
1212			playerPromise = this.initHtml5Player();
1213		}
1214		else if (this.player === 'youtube') {
1215			playerPromise = this.initYouTubePlayer();
1216		}
1217		else if (this.player === 'vimeo') {
1218			playerPromise = this.initVimeoPlayer();
1219		}
1220		// After player specific initialization is done, run remaining general initialization.
1221		var deferred = new $.Deferred();
1222		var promise = deferred.promise();
1223		playerPromise.done(
1224			function () { // done/resolved
1225				if (thisObj.useFixedSeekInterval) {
1226  				if (!thisObj.seekInterval) {
1227            thisObj.seekInterval = thisObj.defaultSeekInterval;
1228          }
1229          else {
1230            // fixed seekInterval was already assigned, using value of data-seek-interval attribute
1231          }
1232          thisObj.seekIntervalCalculated = true;
1233        }
1234        else {
1235					thisObj.setSeekInterval();
1236				}
1237				deferred.resolve();
1238			}
1239		).fail(function () { // failed
1240			deferred.reject();
1241			}
1242		);
1243
1244		return promise;
1245	};
1246
1247	AblePlayer.prototype.setSeekInterval = function () {
1248
1249		// this function is only called if this.useFixedSeekInterval is false
1250		// if this.useChapterTimes, this is called as each new chapter is loaded
1251		// otherwise, it's called once, as the player is initialized
1252		var thisObj, duration;
1253		thisObj = this;
1254		this.seekInterval = this.defaultSeekInterval;
1255		if (this.useChapterTimes) {
1256			duration = this.chapterDuration;
1257		}
1258		else {
1259			duration = this.duration;
1260		}
1261		if (typeof duration === 'undefined' || duration < 1) {
1262			// no duration; just use default for now but keep trying until duration is available
1263			this.seekIntervalCalculated = false;
1264			return;
1265		}
1266		else {
1267			if (duration <= 20) {
1268				this.seekInterval = 5;	 // 4 steps max
1269			}
1270			else if (duration <= 30) {
1271				this.seekInterval = 6; // 5 steps max
1272			}
1273			else if (duration <= 40) {
1274				this.seekInterval = 8; // 5 steps max
1275			}
1276			else if (duration <= 100) {
1277				this.seekInterval = 10; // 10 steps max
1278			}
1279			else {
1280				// never more than 10 steps from start to end
1281				this.seekInterval = (duration / 10);
1282			}
1283			this.seekIntervalCalculated = true;
1284		}
1285	};
1286
1287	AblePlayer.prototype.initDefaultCaption = function () {
1288
1289		var captions, i;
1290
1291		captions = this.captions;
1292		if (captions.length > 0) {
1293			for (i=0; i<captions.length; i++) {
1294				if (captions[i].def === true) {
1295					this.captionLang = captions[i].language;
1296					this.selectedCaptions = captions[i];
1297				}
1298			}
1299			if (typeof this.captionLang === 'undefined') {
1300				// No caption track was flagged as default
1301				// find and use a caption language that matches the player language
1302				for (i=0; i<captions.length; i++) {
1303					if (captions[i].language === this.lang) {
1304						this.captionLang = captions[i].language;
1305						this.selectedCaptions = captions[i];
1306					}
1307				}
1308			}
1309			if (typeof this.captionLang === 'undefined') {
1310				// Still no matching caption track
1311				// just use the first track
1312				this.captionLang = captions[0].language;
1313				this.selectedCaptions = captions[0];
1314			}
1315			if (typeof this.captionLang !== 'undefined') {
1316				// reset transcript selected <option> to this.captionLang
1317				if (this.$transcriptLanguageSelect) {
1318					this.$transcriptLanguageSelect.find('option[lang=' + this.captionLang + ']').prop('selected',true);
1319				}
1320				// sync all other tracks to this same languge
1321				this.syncTrackLanguages('init',this.captionLang);
1322			}
1323      if (this.player === 'vimeo') {
1324  			if (this.usingVimeoCaptions && this.prefCaptions == 1) {
1325      			// initialize Vimeo captions to the default language
1326            this.vimeoPlayer.enableTextTrack(this.captionLang).then(function(track) {
1327	  				  // track.language = the iso code for the language
1328              // track.kind = 'captions' or 'subtitles'
1329              // track.label = the human-readable label
1330				    }).catch(function(error) {
1331					    switch (error.name) {
1332						    case 'InvalidTrackLanguageError':
1333							    // no track was available with the specified language
1334                  console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
1335                  break;
1336                case 'InvalidTrackError':
1337							    // no track was available with the specified language and kind
1338                  console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
1339                  break;
1340                default:
1341							    // some other error occurred
1342                  console.log('Error loading ' + track.label + ' ' + track.kind + ' track');
1343                  break;
1344    		      }
1345				    });
1346			    }
1347          else {
1348  			    // disable Vimeo captions.
1349            this.vimeoPlayer.disableTextTrack().then(function() {
1350    			    // Vimeo captions disabled
1351  			    }).catch(function(error) {
1352              console.log('Error disabling Vimeo text track: ',error);
1353            });
1354			    }
1355        }
1356    }
1357	};
1358
1359	AblePlayer.prototype.initHtml5Player = function () {
1360		// Nothing special to do!
1361		var deferred = new $.Deferred();
1362		var promise = deferred.promise();
1363		deferred.resolve();
1364		return promise;
1365	};
1366
1367	// Sets media/track/source attributes; is called whenever player is recreated since $media may have changed.
1368	AblePlayer.prototype.setMediaAttributes = function () {
1369		// Firefox puts videos in tab order; remove.
1370		this.$media.attr('tabindex', -1);
1371
1372		// Keep native player from displaying captions/subtitles by setting textTrack.mode='disabled'
1373		// https://dev.w3.org/html5/spec-author-view/video.html#text-track-mode
1374		// This *should* work but historically hasn't been supported in all browsers
1375		// Workaround for non-supporting browsers is to remove default attribute
1376		// We're doing that too in track.js > setupCaptions()
1377		var textTracks = this.$media.get(0).textTracks;
1378		if (textTracks) {
1379			var i = 0;
1380			while (i < textTracks.length) {
1381				textTracks[i].mode = 'disabled';
1382				i += 1;
1383			}
1384		}
1385	};
1386
1387	AblePlayer.prototype.getPlayer = function() {
1388
1389		// Determine which player to use, if any
1390		// return 'html5', 'youtube', 'vimeo', or null
1391
1392		var i, sourceType, $newItem;
1393		if (this.youTubeId) {
1394			if (this.mediaType !== 'video') {
1395				// attempting to play a YouTube video using an element other than <video>
1396				return null;
1397			}
1398			else {
1399				return 'youtube';
1400			}
1401		}
1402		else if (this.vimeoId) {
1403			if (this.mediaType !== 'video') {
1404				// attempting to play a Vimeo video using an element other than <video>
1405				return null;
1406			}
1407			else {
1408				return 'vimeo';
1409			}
1410
1411		}
1412		else if (this.testFallback ||
1413						 ((this.isUserAgent('msie 7') || this.isUserAgent('msie 8') || this.isUserAgent('msie 9')) && this.mediaType === 'video') ||
1414						 (this.isIOS() && (this.isIOS(4) || this.isIOS(5) || this.isIOS(6)))
1415						) {
1416			// the user wants to test the fallback player, or
1417			// the user is using an older version of IE or IOS,
1418			// both of which had buggy implementation of HTML5 video
1419			return null;
1420		}
1421		else if (this.media.canPlayType) {
1422			return 'html5';
1423		}
1424		else {
1425			// Browser does not support the available media file
1426			return null;
1427		}
1428	};
1429
1430})(jQuery);
1431
1432(function ($) {
1433	AblePlayer.prototype.setCookie = function(cookieValue) {
1434		Cookies.set('Able-Player', cookieValue, { expires:90 });
1435		// set the cookie lifetime for 90 days
1436	};
1437
1438	AblePlayer.prototype.getCookie = function() {
1439
1440		var defaultCookie = {
1441			preferences: {},
1442			sign: {},
1443			transcript: {}
1444		};
1445
1446		var cookie;
1447		try {
1448			cookie = Cookies.getJSON('Able-Player');
1449		}
1450		catch (err) {
1451			// Original cookie can't be parsed; update to default
1452			Cookies.getJSON(defaultCookie);
1453			cookie = defaultCookie;
1454		}
1455		if (cookie) {
1456			return cookie;
1457		}
1458		else {
1459			return defaultCookie;
1460		}
1461	};
1462
1463	AblePlayer.prototype.updateCookie = function( setting ) {
1464
1465		// called when a particular setting had been updated
1466		// useful for settings updated indepedently of Preferences dialog
1467		// e.g., prefAutoScrollTranscript, which is updated in control.js >
1467 handleTranscriptLockToggle()
1468		// setting is any supported preference name (e.g., "prefCaptions")
1469		// OR 'transcript' or 'sign' (not user-defined preferences, used to save position of draggable windows)
1470		var cookie, $window, windowPos, available, i, prefName;
1471		cookie = this.getCookie();
1472
1473		if (setting === 'transcript' || setting === 'sign') {
1474			if (setting === 'transcript') {
1475				$window = this.$transcriptArea;
1476				windowPos = $window.position();
1477				if (typeof cookie.transcript === 'undefined') {
1478					cookie.transcript = {};
1479				}
1480				cookie.transcript['position'] = $window.css('position'); // either 'relative' or 'absolute'
1481				cookie.transcript['zindex'] = $window.css('z-index');
1482				cookie.transcript['top'] = windowPos.top;
1483				cookie.transcript['left'] = windowPos.left;
1484				cookie.transcript['width'] = $window.width();
1485				cookie.transcript['height'] = $window.height();
1486			}
1487			else if (setting === 'sign') {
1488				$window = this.$signWindow;
1489				windowPos = $window.position();
1490				if (typeof cookie.sign === 'undefined') {
1491					cookie.sign = {};
1492				}
1493				cookie.sign['position'] = $window.css('position'); // either 'relative' or 'absolute'
1494				cookie.sign['zindex'] = $window.css('z-index');
1495				cookie.sign['top'] = windowPos.top;
1496				cookie.sign['left'] = windowPos.left;
1497				cookie.sign['width'] = $window.width();
1498				cookie.sign['height'] = $window.height();
1499			}
1500		}
1501		else {
1502			available = this.getAvailablePreferences();
1503			// Rebuild cookie with current cookie values,
1504			// replacing the one value that's been changed
1505			for (i = 0; i < available.length; i++) {
1506				prefName = available[i]['name'];
1507				if (prefName == setting) {
1508					// this is the one that requires an update
1509					cookie.preferences[prefName] = this[prefName];
1510				}
1511			}
1512		}
1513		// Save updated cookie
1514		this.setCookie(cookie);
1515	};
1516
1517	AblePlayer.prototype.getPreferencesGroups = function() {
1518
1519		// return array of groups in the order in which they will appear
1520		// in the Preferences popup menu
1521		// Human-readable label for each group is defined in translation table
1522		if (this.mediaType === 'video') {
1523			return ['captions','descriptions','keyboard','transcript'];
1524		}
1525		else if (this.mediaType === 'audio') {
1526			var groups = [];
1527			groups.push('keyboard');
1528			if (this.lyricsMode) {
1529				groups.push('transcript');
1530			}
1531			return groups;
1532		}
1533	}
1534
1535	AblePlayer.prototype.getAvailablePreferences = function() {
1536
1537		// Return the list of currently available preferences.
1538		// Preferences with no 'label' are set within player, not shown in Prefs dialog
1539		var prefs = [];
1540
1541		// Modifier keys preferences
1542		prefs.push({
1543			'name': 'prefAltKey', // use alt key with shortcuts
1544			'label': this.tt.prefAltKey,
1545			'group': 'keyboard',
1546			'default': 1
1547		});
1548		prefs.push({
1549			'name': 'prefCtrlKey', // use ctrl key with shortcuts
1550			'label': this.tt.prefCtrlKey,
1551			'group': 'keyboard',
1552			'default': 1
1553		});
1554		prefs.push({
1555			'name': 'prefShiftKey',
1556			'label': this.tt.prefShiftKey,
1557			'group': 'keyboard',
1558			'default': 0
1559		});
1560
1561		// Transcript preferences
1562		prefs.push({
1563			'name': 'prefTranscript', // transcript default state
1564			'label': null,
1565			'group': 'transcript',
1566			'default': 0 // off because turning it on has a certain WOW factor
1567		});
1568		prefs.push({
1569			'name': 'prefHighlight', // highlight transcript as media plays
1570			'label': this.tt.prefHighlight,
1571			'group': 'transcript',
1572			'default': 1 // on because many users can benefit
1573		});
1574		prefs.push({
1575			'name': 'prefAutoScrollTranscript',
1576			'label': null,
1577			'group': 'transcript',
1578			'default': 1
1579		});
1580		prefs.push({
1581			'name': 'prefTabbable', // tab-enable transcript
1582			'label': this.tt.prefTabbable,
1583			'group': 'transcript',
1584			'default': 0 // off because if users don't need it, it impedes tabbing elsewhere on the page
1585		});
1586
1587		if (this.mediaType === 'video') {
1588
1589			// Caption preferences
1590			prefs.push({
1591				'name': 'prefCaptions', // closed captions default state
1592				'label': null,
1593				'group': 'captions',
1594				'default': 1
1595			});
1596/* // not supported yet
1597			prefs.push({
1598				'name': 'prefCaptionsStyle',
1599				'label': this.tt.prefCaptionsStyle,
1600				'group': 'captions',
1601				'default': this.tt.captionsStylePopOn
1602			});
1603*/
1604			prefs.push({
1605				'name': 'prefCaptionsPosition',
1606				'label': this.tt.prefCaptionsPosition,
1607				'group': 'captions',
1608				'default': this.defaultCaptionsPosition
1609			});
1610			prefs.push({
1611				'name': 'prefCaptionsFont',
1612				'label': this.tt.prefCaptionsFont,
1613				'group': 'captions',
1614				'default': 'sans'
1615			});
1616			prefs.push({
1617				'name': 'prefCaptionsSize',
1618				'label': this.tt.prefCaptionsSize,
1619				'group': 'captions',
1620				'default': '100%'
1621			});
1622			prefs.push({
1623				'name': 'prefCaptionsColor',
1624				'label': this.tt.prefCaptionsColor,
1625				'group': 'captions',
1626				'default': 'white'
1627			});
1628			prefs.push({
1629				'name': 'prefCaptionsBGColor',
1630				'label': this.tt.prefCaptionsBGColor,
1631				'group': 'captions',
1632				'default': 'black'
1633			});
1634			prefs.push({
1635				'name': 'prefCaptionsOpacity',
1636				'label': this.tt.prefCaptionsOpacity,
1637				'group': 'captions',
1638				'default': '100%'
1639			});
1640
1641			// Description preferences
1642			prefs.push({
1643				'name': 'prefDesc', // audio description default state
1644				'label': null,
1645				'group': 'descriptions',
1646				'default': 0 // off because users who don't need it might find it distracting
1647			});
1648			prefs.push({
1649				'name': 'prefDescFormat', // audio description default state
1650				'label': null,
1651				'group': 'descriptions',
1652				'default': 'video'
1653			});
1654			prefs.push({
1655				'name': 'prefDescPause', // automatically pause when closed description starts
1656				'label': this.tt.prefDescPause,
1657				'group': 'descriptions',
1658				'default': 0 // off because it burdens user with restarting after every pause
1659			});
1660			prefs.push({
1661				'name': 'prefVisibleDesc', // visibly show closed description (if avilable and used)
1662				'label': this.tt.prefVisibleDesc,
1663				'group': 'descriptions',
1664				'default': 1 // on because sighted users probably want to see this cool feature in action
1665			});
1666
1667			// Video preferences without a category (not shown in Preferences dialogs)
1668			prefs.push({
1669				'name': 'prefSign', // open sign language window by default if avilable
1670				'label': null,
1671				'group': null,
1672				'default': 0 // off because clicking an icon to see the sign window has a powerful impact
1673			});
1674
1675		}
1676		return prefs;
1677	};
1678
1679	AblePlayer.prototype.loadCurrentPreferences = function () {
1680
1681  	// Load current/default preferences from cookie into the AblePlayer object.
1682
1683		var available = this.getAvailablePreferences();
1684		var cookie = this.getCookie();
1685
1686		// Copy current cookie values into this object, and fill in any default values.
1687		for (var ii = 0; ii < available.length; ii++) {
1688			var prefName = available[ii]['name'];
1689			var defaultValue = available[ii]['default'];
1690			if (cookie.preferences[prefName] !== undefined) {
1691				this[prefName] = cookie.preferences[prefName];
1692			}
1693			else {
1694				cookie.preferences[prefName] = defaultValue;
1695				this[prefName] = defaultValue;
1696			}
1697		}
1698
1699		// Save since we may have added default values.
1700		this.setCookie(cookie);
1701	};
1702
1703	AblePlayer.prototype.injectPrefsForm = function (form) {
1704
1705		// Creates a preferences form and injects it.
1706		// form is one of the supported forms (groups) defined in getPreferencesGroups()
1707
1708		var available, thisObj, $prefsDiv, formTitle, introText,
1709			$prefsIntro,$prefsIntroP2,p3Text,$prefsIntroP3,i, j,
1710			$fieldset, fieldsetClass, fieldsetId,
1711			$descFieldset, $descLegend, $legend,
1712			thisPref, $thisDiv, thisClass, thisId, $thisLabel, $thisField,
1713			$div1,id1,$radio1,$label1,
1714			$div2,id2,$radio2,$label2,
1715			options,$thisOption,optionValue,optionText,sampleCapsDiv,
1716			changedPref,changedSpan,changedText,
1717			currentDescState,
1718			$kbHeading,$kbList,kbLabels,keys,kbListText,$kbListItem,
1719			dialog,saveButton,cancelButton;
1720
1721		thisObj = this;
1722		available = this.getAvailablePreferences();
1723
1724		// outer container, will be assigned role="dialog"
1725		$prefsDiv = $('<div>',{
1726			'class': 'able-prefs-form '
1727		});
1728		var customClass = 'able-prefs-form-' + form;
1729		$prefsDiv.addClass(customClass);
1730
1731		// add intro
1732		if (form == 'captions') {
1733			formTitle = this.tt.prefTitleCaptions;
1734			introText = this.tt.prefIntroCaptions;
1735			// Uncomment the following line to include a cookie warning
1736			// Not included for now in order to cut down on unnecessary verbiage
1737			// introText += ' ' + this.tt.prefCookieWarning;
1738			$prefsIntro = $('<p>',{
1739				text: introText
1740			});
1741			$prefsDiv.append($prefsIntro);
1742		}
1743		else if (form == 'descriptions') {
1744			formTitle = this.tt.prefTitleDescriptions;
1745			var $prefsIntro = $('<p>',{
1746				text: this.tt.prefIntroDescription1
1747			});
1748			var $prefsIntroUL = $('<ul>');
1749			var $prefsIntroLI1 = $('<li>',{
1750				text: this.tt.prefDescFormatOption1
1751			});
1752			var $prefsIntroLI2 = $('<li>',{
1753				text: this.tt.prefDescFormatOption2
1754			});
1755
1756			$prefsIntroUL.append($prefsIntroLI1,$prefsIntroLI2);
1757			if (this.hasOpenDesc && this.hasClosedDesc) {
1758				currentDescState = this.tt.prefIntroDescription2 + ' ';
1759				currentDescState += '<strong>' + this.tt.prefDescFormatOption1b + '</strong>';
1760				currentDescState += ' <em>' + this.tt.and + '</em> <strong>' + this.tt.prefDescFormatOption2b + '</strong>.';
1761			}
1762			else if (this.hasOpenDesc) {
1763				currentDescState = this.tt.prefIntroDescription2;
1764				currentDescState += ' <strong>' + this.tt.prefDescFormatOption1b + '</strong>.';
1765			}
1766			else if (this.hasClosedDesc) {
1767				currentDescState = this.tt.prefIntroDescription2;
1768				currentDescState += ' <strong>' + this.tt.prefDescFormatOption2b + '</strong>.';
1769			}
1770			else {
1771				currentDescState = this.tt.prefIntroDescriptionNone;
1772			}
1773			$prefsIntroP2 = $('<p>',{
1774				html: currentDescState
1775			});
1776
1777			p3Text = this.tt.prefIntroDescription3;
1778			if (this.hasOpenDesc || this.hasClosedDesc) {
1779				p3Text += ' ' + this.tt.prefIntroDescription4;
1780			}
1781			$prefsIntroP3 = $('<p>',{
1782				text: p3Text
1783			});
1784
1785			$prefsDiv.append($prefsIntro,$prefsIntroUL,$prefsIntroP2,$prefsIntroP3);
1786		}
1787		else if (form == 'keyboard') {
1788			formTitle = this.tt.prefTitleKeyboard;
1789			introText = this.tt.prefIntroKeyboard1;
1790			introText += ' ' + this.tt.prefIntroKeyboard2;
1791			introText += ' ' + this.tt.prefIntroKeyboard3;
1792			$prefsIntro = $('<p>',{
1793				text: introText
1794			});
1795			$prefsDiv.append($prefsIntro);
1796		}
1797		else if (form == 'transcript') {
1798			formTitle = this.tt.prefTitleTranscript;
1799			introText = this.tt.prefIntroTranscript;
1800			// Uncomment the following line to include a cookie warning
1801			// Not included for now in order to cut down on unnecessary verbiage
1802			// introText += ' ' + this.tt.prefCookieWarning;
1803			$prefsIntro = $('<p>',{
1804				text: introText
1805			});
1806			$prefsDiv.append($prefsIntro);
1807		}
1808
1809		$fieldset = $('<fieldset>');
1810		fieldsetClass = 'able-prefs-' + form;
1811		fieldsetId = this.mediaId + '-prefs-' + form;
1812		$fieldset.addClass(fieldsetClass).attr('id',fieldsetId);
1813		if (form === 'keyboard') {
1814		  $legend = $('<legend>' + this.tt.prefHeadingKeyboard1 + '</legend>');
1815			$fieldset.append($legend);
1816		}
1817		else if (form === 'descriptions') {
1818  		$legend = $('<legend>' + this.tt.prefHeadingTextDescription + '</legend>');
1819  		$fieldset.append($legend);
1820		}
1821		for (i=0; i<available.length; i++) {
1822
1823			// only include prefs on the current form if they have a label
1824			if ((available[i]['group'] == form) && available[i]['label']) {
1825
1826				thisPref = available[i]['name'];
1827				thisClass = 'able-' + thisPref;
1828				thisId = this.mediaId + '_' + thisPref;
1829				$thisDiv = $('<div>').addClass(thisClass);
1830
1831				if (form === 'captions') {
1832					$thisLabel = $('<label for="' + thisId + '"> ' + available[i]['label'] + '</label>');
1833					$thisField = $('<select>',{
1834						name: thisPref,
1835						id: thisId,
1836					});
1837					if (thisPref !== 'prefCaptions' && thisPref !== 'prefCaptionsStyle') {
1838						// add a change handler that updates the style of the sample caption text
1839						$thisField.change(function() {
1840							changedPref = $(this).attr('name');
1841							thisObj.stylizeCaptions(thisObj.$sampleCapsDiv,changedPref);
1842						});
1843					}
1844					options = this.getCaptionsOptions(thisPref);
1845					for (j=0; j < options.length; j++) {
1846						if (thisPref === 'prefCaptionsPosition') {
1847							optionValue = options[j];
1848							if (optionValue === 'overlay') {
1849								optionText = this.tt.captionsPositionOverlay;
1850							}
1851							else if (optionValue === 'below') {
1852								optionValue = options[j];
1853								optionText = this.tt.captionsPositionBelow;
1854							}
1855						}
1856						else if (thisPref === 'prefCaptionsFont' || thisPref === 'prefCaptionsColor' || thisPref === 'prefCaptionsBGColor') {
1857							optionValue = options[j][0];
1858							optionText = options[j][1];
1859						}
1860						else if (thisPref === 'prefCaptionsOpacity') {
1861							optionValue = options[j];
1862							optionText = options[j];
1863							if (optionValue === '0%') {
1864								optionText += ' (' + this.tt.transparent + ')';
1865							}
1866							else if (optionValue === '100%') {
1867								optionText += ' (' + this.tt.solid + ')';
1868							}
1869						}
1870						else {
1871							optionValue = options[j];
1872							optionText = options[j];
1873						}
1874						$thisOption = $('<option>',{
1875							value: optionValue,
1876							text: optionText
1877						});
1878						if (this[thisPref] === optionValue) {
1879							$thisOption.prop('selected',true);
1880						}
1881						$thisField.append($thisOption);
1882					}
1883					$thisDiv.append($thisLabel,$thisField);
1884				}
1885				else { // all other fields are checkboxes
1886					$thisLabel = $('<label for="' + thisId + '"> ' + available[i]['label'] + '</label>');
1887					$thisField = $('<input>',{
1888						type: 'checkbox',
1889						name: thisPref,
1890						id: thisId,
1891						value: 'true'
1892					});
1893					// check current active value for this preference
1894					if (this[thisPref] === 1) {
1895						$thisField.prop('checked',true);
1896					}
1897					if (form === 'keyboard') {
1898						// add a change handler that updates the list of current keyboard shortcuts
1899						$thisField.change(function() {
1900							changedPref = $(this).attr('name');
1901							if (changedPref === 'prefAltKey') {
1902								changedSpan = '.able-modkey-alt';
1903								changedText = thisObj.tt.prefAltKey + ' + ';
1904							}
1905							else if (changedPref === 'prefCtrlKey') {
1906								changedSpan = '.able-modkey-ctrl';
1907								changedText = thisObj.tt.prefCtrlKey + ' + ';
1908							}
1909							else if (changedPref === 'prefShiftKey') {
1910								changedSpan = '.able-modkey-shift';
1911								changedText = thisObj.tt.prefShiftKey + ' + ';
1912							}
1913							if ($(this).is(':checked')) {
1914								$(changedSpan).text(changedText);
1915							}
1916							else {
1917								$(changedSpan).text('');
1918							}
1919						});
1920					}
1921					$thisDiv.append($thisField,$thisLabel);
1922				}
1923				$fieldset.append($thisDiv);
1924			}
1925		}
1926		$prefsDiv.append($fieldset);
1927
1928		if (form === 'captions') {
1929			// add a sample closed caption div to prefs dialog
1930			if (this.mediaType === 'video') {
1931				this.$sampleCapsDiv = $('<div>',{
1932					'class': 'able-captions-sample'
1933				}).text(this.tt.sampleCaptionText);
1934				$prefsDiv.append(this.$sampleCapsDiv);
1935				this.stylizeCaptions(this.$sampleCapsDiv);
1936			}
1937		}
1938		else if (form === 'keyboard') {
1939			// add a current list of keyboard shortcuts
1940			$kbHeading = $('<h2>',{
1941				text: this.tt.prefHeadingKeyboard2
1942			});
1943			$kbList = $('<ul>');
1944			// create arrays of kbLabels and keys
1945			kbLabels = [];
1946			keys = [];
1947			for (i=0; i<this.controls.length; i++) {
1948				if (this.controls[i] === 'play') {
1949					kbLabels.push(this.tt.play + '/' + this.tt.pause);
1950					keys.push('p</span> <em>' + this.tt.or + '</em> <span class="able-help-modifiers"> ' + this.tt.spacebar);
1951				}
1952				else if (this.controls[i] === 'restart') {
1953					kbLabels.push(this.tt.restart);
1954					keys.push('s');
1955				}
1956				else if (this.controls[i] === 'previous') {
1957					kbLabels.push(this.tt.prevTrack);
1958					keys.push('b'); // b = back
1959				}
1960				else if (this.controls[i] === 'next') {
1961					kbLabels.push(this.tt.nextTrack);
1962					keys.push('n');
1963				}
1964				else if (this.controls[i] === 'rewind') {
1965					kbLabels.push(this.tt.rewind);
1966					keys.push('r');
1967				}
1968				else if (this.controls[i] === 'forward') {
1969					kbLabels.push(this.tt.forward);
1970					keys.push('f');
1971				}
1972				else if (this.controls[i] === 'volume') {
1973					kbLabels.push(this.tt.volume);
1974					keys.push('v</span> <em>' + this.tt.or + '</em> <span class="able-modkey">1-9');
1975					// mute toggle
1976					kbLabels.push(this.tt.mute + '/' + this.tt.unmute);
1977					keys.push('m');
1978				}
1979				else if (this.controls[i] === 'captions') {
1980					if (this.captions.length > 1) {
1981						// caption button launches a Captions popup menu
1982						kbLabels.push(this.tt.captions);
1983					}
1984					else {
1985						// there is only one caption track
1986						// therefore caption button is a toggle
1987						if (this.captionsOn) {
1988							kbLabels.push(this.tt.hideCaptions);
1989						}
1990						else {
1991							kbLabels.push(this.tt.showCaptions);
1992						}
1993					}
1994					keys.push('c');
1995				}
1996				else if (this.controls[i] === 'descriptions') {
1997					if (this.descOn) {
1998						kbLabels.push(this.tt.turnOffDescriptions);
1999					}
2000					else {
2001						kbLabels.push(this.tt.turnOnDescriptions);
2002					}
2003					keys.push('d');
2004				}
2005				else if (this.controls[i] === 'prefs') {
2006					kbLabels.push(this.tt.preferences);
2007					keys.push('e');
2008				}
2009				else if (this.controls[i] === 'help') {
2010					kbLabels.push(this.tt.help);
2011					keys.push('h');
2012				}
2013			}
2014			for (i=0; i<keys.length; i++) {
2015				// alt
2016				kbListText = '<span class="able-modkey-alt">';
2017				if (this.prefAltKey === 1) {
2018					kbListText += this.tt.prefAltKey + ' + ';
2019				}
2020				kbListText += '</span>';
2021				// ctrl
2022				kbListText += '<span class="able-modkey-ctrl">';
2023				if (this.prefCtrlKey === 1) {
2024					kbListText += this.tt.prefCtrlKey + ' + ';
2025				}
2026				kbListText += '</span>';
2027				// shift
2028				kbListText += '<span class="able-modkey-shift">';
2029				if (this.prefShiftKey === 1) {
2030					kbListText += this.tt.prefShiftKey + ' + ';
2031				}
2032				kbListText += '</span>';
2033				kbListText += '<span class="able-modkey">' + keys[i] + '</span>';
2034				kbListText += ' = ' + kbLabels[i];
2035				$kbListItem = $('<li>',{
2036					html: kbListText
2037				});
2038				$kbList.append($kbListItem);
2039			}
2040			// add Escape key
2041			kbListText = '<span class="able-modkey">' + this.tt.escapeKey + '</span>';
2042			kbListText += ' = ' + this.tt.escapeKeyFunction;
2043			$kbListItem = $('<li>',{
2044				html: kbListText
2045			});
2046			$kbList.append($kbListItem);
2047			// put it all together
2048			$prefsDiv.append($kbHeading,$kbList);
2049		}
2050
2051		// $prefsDiv (dialog) must be appended to the BODY!
2052		// otherwise when aria-hidden="true" is applied to all background content
2053		// that will include an ancestor of the dialog,
2054		// which will render the dialog unreadable by screen readers
2055		$('body').append($prefsDiv);
2056		dialog = new AccessibleDialog($prefsDiv, this.$prefsButton, 'dialog', formTitle, $prefsIntro, thisObj.tt.closeButtonLabel, '32em');
2057
2058		// Add save and cancel buttons.
2059		$prefsDiv.append('<hr>');
2060		saveButton = $('<button class="modal-button">' + this.tt.save + '</button>');
2061		cancelButton = $('<button class="modal-button">' + this.tt.cancel + '</button>');
2062		saveButton.click(function () {
2063			dialog.hide();
2064		  thisObj.savePrefsFromForm();
2065		});
2066		cancelButton.click(function () {
2067			dialog.hide();
2068			thisObj.resetPrefsForm();
2069		});
2070
2071		$prefsDiv.append(saveButton);
2072		$prefsDiv.append(cancelButton);
2073
2074		// add global reference for future control
2075		if (form === 'captions') {
2076			this.captionPrefsDialog = dialog;
2077		}
2078		else if (form === 'descriptions') {
2079			this.descPrefsDialog = dialog;
2080		}
2081		else if (form === 'keyboard') {
2082			this.keyboardPrefsDialog = dialog;
2083		}
2084		else if (form === 'transcript') {
2085			this.transcriptPrefsDialog = dialog;
2086		}
2087
2088		// Add click handler for dialog close button
2089		// (button is added in dialog.js)
2090		$('div.able-prefs-form button.modalCloseButton').click(function() {
2091			thisObj.resetPrefsForm();
2092		})
2093		// Add handler for escape key
2094		$('div.able-prefs-form').keydown(function(e) {
2095			if (e.which === 27) { // escape
2096				thisObj.resetPrefsForm();
2097			}
2098		});
2099	};
2100
2101	 AblePlayer.prototype.resetPrefsForm = function () {
2102
2103  	 // Reset preferences form with default values from cookie
2104     // Called when:
2105     // User clicks cancel or close button in Prefs Dialog
2106     // User presses Escape to close Prefs dialog
2107     // User clicks Save in Prefs dialog, & there's more than one player on page
2108
2109		 var thisObj, cookie, available, i, prefName, prefId, thisDiv, thisId;
2110
2111		 thisObj = this;
2112		 cookie = this.getCookie();
2113		 available = this.getAvailablePreferences();
2114		 for (i=0; i<available.length; i++) {
2115			 prefName = available[i]['name'];
2116			 prefId = this.mediaId + '_' + prefName;
2117			 if ((prefName.indexOf('Captions') !== -1) && (prefName !== 'prefCaptions')) {
2118				 // this is a caption-related select box
2119				 $('select[name="' + prefName + '"]').val(cookie.preferences[prefName]);
2120			 }
2121			 else { // all others are checkboxes
2122				 if (this[prefName] === 1) {
2123					 $('input[name="' + prefName + '"]').prop('checked',true);
2124					}
2125					else {
2126						$('input[name="' + prefName + '"]').prop('checked',false);
2127					}
2128				}
2129			}
2130			// also restore style of sample caption div
2131			this.stylizeCaptions(this.$sampleCapsDiv);
2132	 };
2133
2134	AblePlayer.prototype.savePrefsFromForm = function () {
2135
2136  	// Return a prefs object constructed from the form.
2137		// called when user saves the Preferences form
2138		// update cookie with new value
2139		var cookie, available, prefName, prefId, numChanges,
2140		  numCapChanges, capSizeChanged, capSizeValue, newValue;
2141
2142		numChanges = 0;
2143		numCapChanges = 0; // changes to caption-style-related preferences
2144		capSizeChanged = false;
2145		cookie = this.getCookie();
2146		available = this.getAvailablePreferences();
2147		for (var i=0; i < available.length; i++) {
2148			// only prefs with labels are used in the Prefs form
2149			if (available[i]['label']) {
2150				prefName = available[i]['name'];
2151				prefId = this.mediaId + '_' + prefName;
2152				if (prefName == 'prefDescFormat') {
2153  				// As of v4.0.10, prefDescFormat is no longer a choice
2154					// this.prefDescFormat = $('input[name="' + prefName + '"]:checked').val();
2155					this.prefDescFormat = 'video';
2156					if (this.prefDescFormat !== cookie.preferences['prefDescFormat']) { // user's preference has changed
2157						cookie.preferences['prefDescFormat'] = this.prefDescFormat;
2158						numChanges++;
2159					}
2160				}
2161				else if ((prefName.indexOf('Captions') !== -1) && (prefName !== 'prefCaptions')) {
2162					// this is one of the caption-related select fields
2163					newValue = $('select[id="' + prefId + '"]').val();
2164					if (cookie.preferences[prefName] !== newValue) { // user changed setting
2165						cookie.preferences[prefName] = newValue;
2166						// also update global var for this pref (for caption fields, not done elsewhere)
2167						this[prefName] = newValue;
2168						numChanges++;
2169						numCapChanges++;
2170					}
2171					if (prefName === 'prefCaptionsSize') {
2172						capSizeChanged = true;
2173						capSizeValue = newValue;
2174					}
2175				}
2176				else { // all other fields are checkboxes
2177					if ($('input[id="' + prefId + '"]').is(':checked')) {
2178						cookie.preferences[prefName] = 1;
2179						if (this[prefName] === 1) {
2180							// nothing has changed
2181						}
2182						else {
2183							// user has just turned this pref on
2184							this[prefName] = 1;
2185							numChanges++;
2186						}
2187					}
2188					else { // thisPref is not checked
2189						cookie.preferences[prefName] = 0;
2190						if (this[prefName] === 1) {
2191							// user has just turned this pref off
2192							this[prefName] = 0;
2193							numChanges++;
2194						}
2195						else {
2196							// nothing has chaged
2197						}
2198					}
2199				}
2200			}
2201		}
2202		if (numChanges > 0) {
2203			this.setCookie(cookie);
2204			this.showAlert(this.tt.prefSuccess);
2205		}
2206		else {
2207			this.showAlert(this.tt.prefNoChange);
2208		}
2209		if (this.player === 'youtube' &&
2210			(typeof this.usingYouTubeCaptions !== 'undefined' && this.usingYouTubeCaptions) &&
2211			capSizeChanged) {
2212				// update font size of YouTube captions
2213				this.youTubePlayer.setOption(this.ytCaptionModule,'fontSize',this.translatePrefs('size',capSizeValue,'youtube'));
2214		}
2215    if (AblePlayerInstances.length > 1) {
2216      // there are multiple players on this page.
2217      // update prefs for ALL of them
2218      for (var i=0; i<AblePlayerInstances.length; i++) {
2219        AblePlayerInstances[i].updatePrefs();
2220        AblePlayerInstances[i].loadCurrentPreferences();
2221        AblePlayerInstances[i].resetPrefsForm();
2222        if (numCapChanges > 0) {
2223          AblePlayerInstances[i].stylizeCaptions(AblePlayerInstances[i].$captionsDiv);
2224          // also apply same changes to descriptions, if present
2225          if (typeof AblePlayerInstances[i].$descDiv !== 'undefined') {
2226            AblePlayerInstances[i].stylizeCaptions(AblePlayerInstances[i].$descDiv);
2227			    }
2228        }
2229      }
2230    }
2231    else {
2232      // there is only one player
2233      this.updatePrefs();
2234      if (numCapChanges > 0) {
2235        this.stylizeCaptions(this.$captionsDiv);
2236        // also apply same changes to descriptions, if present
2237        if (typeof this.$descDiv !== 'undefined') {
2238          this.stylizeCaptions(this.$descDiv);
2239			  }
2240      }
2241    }
2242	}
2243
2244	AblePlayer.prototype.updatePrefs = function () {
2245
2246  	// Update player based on current prefs. Safe to call multiple times.
2247
2248		// tabbable transcript
2249		if (this.prefTabbable === 1) {
2250			this.$transcriptDiv.find('span.able-transcript-seekpoint').attr('tabindex','0');
2251		}
2252		else {
2253			this.$transcriptDiv.find('span.able-transcript-seekpoint').removeAttr('tabindex');
2254		}
2255
2256		// transcript highlights
2257		if (this.prefHighlight === 0) {
2258			// user doesn't want highlights; remove any existing highlights
2259			this.$transcriptDiv.find('span').removeClass('able-highlight');
2260		}
2261
2262		// Re-initialize caption and description in case relevant settings have changed
2263		this.updateCaption();
2264		this.refreshingDesc = true;
2265		this.initDescription();
2266	};
2267
2268	AblePlayer.prototype.usingModifierKeys = function(e) {
2269
2270		// return true if user is holding down required modifier keys
2271		if ((this.prefAltKey === 1) && !e.altKey) {
2272			return false;
vendor: 9,603 bytes, lines 2273-2640
2273		}
2274		if ((this.prefCtrlKey === 1) && !e.ctrlKey) {
2275			return false;
2276		}
2277		if ((this.prefShiftKey === 1) && !e.shiftKey) {
2278			return false;
2279		}
2280		return true;
2281	};
2282
2283})(jQuery);
2284
2285(function ($) {
2286	// See section 4.1 of dev.w3.org/html5/webvtt for format details.
2287	AblePlayer.prototype.parseWebVTT = function(srcFile,text) {
2288
2289//		var deferred = new $.Deferred();
2290//		var promise = deferred.promise();
2291
2292		// Normalize line ends to \n.
2293		text = text.replace(/(\r\n|\n|\r)/g,'\n');
2294
2295		var parserState = {
2296			src: srcFile,
2297			text: text,
2298			error: null,
2299			metadata: {},
2300			cues: [],
2301			line: 1,
2302			column: 1
2303		};
2304
2305		try {
2306			act(parserState, parseFileBody);
2307		}
2308		catch (err) {
2309			var errString = 'Invalid WebVTT file: ' + parserState.src + '\n';
2310			errString += 'Line: ' + parserState.line + ', ';
2311			errString += 'Column: ' + parserState.column + '\n';
2312			errString += err;
2313			if (console.warn) {
2314				console.warn(errString);
2315			}
2316			else if (console.log) {
2317				console.log(errString);
2318			}
2319		}
2320		return parserState;
2321	}
2322
2323	function actList(state, list) {
2324		var results = [];
2325		for (var ii = 0; ii < list.length; ii++) {
2326			results.push(act(state, list[ii]));
2327		}
2328		return results;
2329	}
2330
2331	// Applies the action and checks for errors.
2332	function act(state, action) {
2333		var val = action(state);
2334		if (state.error !== null) {
2335			throw state.error;
2336		}
2337		return val;
2338	}
2339
2340	function updatePosition(state, cutText) {
2341		for (var ii = 0; ii < cutText.length; ii++) {
2342			if (cutText[ii] === '\n') {
2343				state.column = 1;
2344				state.line += 1;
2345			}
2346			else {
2347				state.column += 1;
2348			}
2349		}
2350	}
2351
2352	function cut(state, length) {
2353		var returnText = state.text.substring(0, length);
2354		updatePosition(state, returnText);
2355		state.text = state.text.substring(length);
2356		return returnText;
2357	}
2358
2359	function cutLine(state, length) {
2360		var nextEOL = state.text.indexOf('\n');
2361		var returnText;
2362		if (nextEOL === -1) {
2363			returnText = state.text;
2364			updatePosition(state, returnText);
2365			state.text = '';
2366		}
2367		else {
2368			returnText = state.text.substring(0, nextEOL);
2369			updatePosition(state, returnText + '\n');
2370			state.text = state.text.substring(nextEOL + 1);
2371		}
2372		return returnText;
2373	}
2374
2375	function peekLine(state) {
2376		var nextEOL = state.text.indexOf('\n');
2377		if (nextEOL === -1) {
2378			return state.text;
2379		}
2380		else {
2381			return state.text.substring(0, nextEOL);
2382		}
2383	}
2384
2385	function parseFileBody(state) {
2386		actList(state, [
2387			eatOptionalBOM,
2388			eatSignature]);
2389		var c = state.text[0];
2390		if (c === ' ' || c === '\t' || c === '\n') {
2391			actList(state, [
2392				eatUntilEOLInclusive,
2393				parseMetadataHeaders,
2394				eatAtLeast1EmptyLines,
2395				parseCuesAndComments]);
2396		}
2397		else {
2398			state.error = "WEBVTT signature not followed by whitespace.";
2399		}
2400	}
2401
2402	// Parses all metadata headers until a cue is discovered.
2403	function parseMetadataHeaders(state) {
2404		while (true) {
2405			var nextLine = peekLine(state);
2406			if (nextLine.indexOf('-->') !== -1) {
2407				return;
2408			}
2409			else if (nextLine.length === 0) {
2410				return;
2411			}
2412			else {
2413				var keyValue = act(state, getMetadataKeyValue);
2414				state.metadata[keyValue[0]] = keyValue[1];
2415				act(state, eatUntilEOLInclusive);
2416			}
2417		}
2418	}
2419
2420	function nextSpaceOrNewline(s) {
2421		var possible = [];
2422		var spaceIndex = s.indexOf(' ');
2423		if (spaceIndex >= 0) {
2424			possible.push(spaceIndex);
2425		}
2426		var tabIndex = s.indexOf('\t');
2427		if (tabIndex >= 0) {
2428			possible.push(tabIndex);
2429		}
2430		var lineIndex = s.indexOf('\n');
2431		if (lineIndex >= 0) {
2432			possible.push(lineIndex);
2433		}
2434
2435		return Math.min.apply(null, possible);
2436	}
2437
2438	function getMetadataKeyValue(state) {
2439		var next = state.text.indexOf('\n');
2440		var pair = cut(state, next);
2441		var colon = pair.indexOf(':');
2442		if (colon === -1) {
2443			state.error = 'Missing colon.';
2444			return;
2445		}
2446		else {
2447			var pairName = pair.substring(0, colon);
2448			var pairValue = pair.substring(colon + 1);
2449			return [pairName, pairValue];
2450		}
2451	}
2452
2453	function getSettingsKeyValue(state) {
2454		var next = nextSpaceOrNewline(state.text);
2455		var pair = cut(state, next);
2456		var colon = pair.indexOf(':');
2457		if (colon === -1) {
2458			state.error = 'Missing colon.';
2459			return;
2460		}
2461		else {
2462			var pairName = pair.substring(0, colon);
2463			var pairValue = pair.substring(colon + 1);
2464			return [pairName, pairValue];
2465		}
2466	}
2467
2468	function parseCuesAndComments(state) {
2469		while (true) {
2470			var nextLine = peekLine(state);
2471			// If NOTE is not on a line all its own, it must be followed by a space or tab.
2472			if (nextLine.indexOf('NOTE') === 0 && ((nextLine.length === 4) || (nextLine[4] === ' ') || (nextLine[4] === '\t'))) {
2473				actList(state, [eatComment, eatEmptyLines]);
2474			}
2475			else if ($.trim(nextLine).length === 0 && state.text.length > 0) {
2476				act(state, eatEmptyLines);
2477			}
2478			else if ($.trim(nextLine).length > 0) {
2479				act(state, parseCue);
2480			}
2481			else {
2482				// Everythings parsed!
2483				return;
2484			}
2485		}
2486	}
2487
2488	function parseCue(state) {
2489
2490		var nextLine = peekLine(state);
2491		var cueId;
2492		var errString;
2493
2494		if(nextLine.indexOf('-->') === -1) {
2495			cueId = cutLine(state);
2496			nextLine = peekLine(state);
2497			if(nextLine.indexOf('-->') === -1) {
2498				errString = 'Invalid WebVTT file: ' + state.src + '\n';
2499				errString += 'Line: ' + state.line + ', ';
2500				errString += 'Column: ' + state.column + '\n';
2501				errString += 'Expected cue timing for cueId \''+cueId+'\' but found: ' + nextLine + '\n';
2502				if (console.warn) {
2503					console.warn(errString);
2504				}
2505				else if (console.log) {
2506					console.log(errString);
2507				}
2508				return; // Return leaving line for parseCuesAndComments to handle
2509			}
2510		}
2511
2512		var cueTimings = actList(state, [getTiming,
2513																		 eatAtLeast1SpacesOrTabs,
2514																		 eatArrow,
2515																		 eatAtLeast1SpacesOrTabs,
2516																		 getTiming]);
2517
2518		var startTime = cueTimings[0];
2519		var endTime = cueTimings[4];
2520		if (startTime >= endTime) {
2521			state.error = 'Start time is not sooner than end time.';
2522			return;
2523		}
2524
2525		act(state, eatSpacesOrTabs);
2526		var cueSettings = act(state, getCueSettings);
2527		// Cut the newline.
2528		cut(state, 1);
2529		var components = act(state, getCuePayload);
2530
2531		if (typeof cueId === 'undefined') {
2532			cueId = state.cues.length + 1;
2533		}
2534		state.cues.push({
2535			id: cueId,
2536			start: startTime,
2537			end: endTime,
2538			settings: cueSettings,
2539			components: components
2540		});
2541	}
2542
2543	function getCueSettings(state) {
2544		var cueSettings = {};
2545		while (state.text.length > 0 && state.text[0] !== '\n') {
2546			var keyValue = act(state, getSettingsKeyValue);
2547			cueSettings[keyValue[0]] = keyValue[1];
2548			act(state, eatSpacesOrTabs);
2549		}
2550		return cueSettings;
2551	}
2552
2553	function getCuePayload(state) {
2554		// Parser based on instructions in draft.
2555		var result = {type: 'internal', tagName: '', value: '', classes: [], annotation: '', parent: null, children: [], language: ''};
2556		var current = result;
2557		var languageStack = [];
2558		while (state.text.length > 0) {
2559			var nextLine = peekLine(state);
2560			if (nextLine.indexOf('-->') !== -1 || /^\s*$/.test(nextLine)) {
2561				break; // Handle empty cues
2562			}
2563			// Have to separately detect double-lines ending cue due to our non-standard parsing.
2564			// TODO: Redo outer algorithm to conform to W3 spec?
2565			if (state.text.length >= 2 && state.text[0] === '\n' && state.text[1] === '\n') {
2566				cut(state, 2);
2567				break;
2568			}
2569
2570			var token = getCueToken(state);
2571			// We'll use the tokens themselves as objects where possible.
2572			if (token.type === 'string') {
2573				current.children.push(token);
2574			}
2575			else if (token.type === 'startTag') {
2576				token.type = token.tagName;
2577				// Define token.parent; added by Terrill to fix bug end 'endTag' loop
2578				token.parent = current;
2579				if ($.inArray(token.tagName, ['i', 'b', 'u', 'ruby']) !== -1) {
2580					if (languageStack.length > 0) {
2581						current.language = languageStack[languageStack.length - 1];
2582					}
2583					current.children.push(token);
2584					current = token;
2585				}
2586				else if (token.tagName === 'rt' && current.tagName === 'ruby') {
2587					if (languageStack.length > 0) {
2588						current.language = languageStack[languageStack.length - 1];
2589					}
2590					current.children.push(token);
2591					current = token;
2592				}
2593				else if (token.tagName === 'c') {
2594					token.value = token.annotation;
2595					if (languageStack.length > 0) {
2596						current.language = languageStack[languageStack.length - 1];
2597					}
2598					current.children.push(token);
2599					current = token;
2600				}
2601				else if (token.tagName === 'v') {
2602					token.value = token.annotation;
2603					if (languageStack.length > 0) {
2604						current.language = languageStack[languageStack.length - 1];
2605					}
2606					current.children.push(token);
2607					current = token;
2608				}
2609				else if (token.tagName === 'lang') {
2610					languageStack.push(token.annotation);
2611					if (languageStack.length > 0) {
2612						current.language = languageStack[languageStack.length - 1];
2613					}
2614					current.children.push(token);
2615					current = token;
2616				}
2617			}
2618			else if (token.type === 'endTag') {
2619				if (token.tagName === current.type && $.inArray(token.tagName, ['c', 'i', 'b', 'u', 'ruby', 'rt', 'v']) !== -1) {
2620					// NOTE from Terrill: This was resulting in an error because current.parent was undefined
2621					// Fixed (I think) by assigning current token to token.parent in 'startTag' loop
2622					current = current.parent;
2623				}
2624				else if (token.tagName === 'lang' && current.type === 'lang') {
2625					current = current.parent;
2626					languageStack.pop();
2627				}
2628				else if (token.tagName === 'ruby' && current.type === 'rt') {
2629					current = current.parent.parent;
2630				}
2631			}
2632			else if (token.type === 'timestampTag') {
2633				var tempState = {
2634					text: token.value,
2635					error: null,
2636					metadata: {},
2637					cues: [],
2638					line: 1,
2639					column: 1
2640				};
vendor: 7,277 bytes, lines 2641-2972
2641				try {
2642					var timing = act(tempState, getTiming);
2643					if (tempState.text.length === 0) {
2644						token.value = timing;
2645						current.push(token);
2646					}
2647				}
2648				catch (err) {
2649				}
2650			}
2651		}
2652		return result;
2653	}
2654
2655	// Gets a single cue token; uses the method in the w3 specification.
2656	function getCueToken(state) {
2657		var tokenState = 'data';
2658		var result = [];
2659		var buffer = '';
2660		var token = {type: '', tagName: '', value: '', classes: [], annotation: '', children: []}
2661
2662		while (true) {
2663			var c;
2664			// Double newlines indicate end of token.
2665			if (state.text.length >= 2 && state.text[0] === '\n' && state.text[1] === '\n') {
2666				c = '\u0004';
2667			}
2668			else if (state.text.length > 0) {
2669				c = state.text[0];
2670			}
2671			else {
2672				// End of file.
2673				c = '\u0004';
2674			}
2675			if (tokenState === 'data') {
2676				if (c === '&') {
2677					buffer = '&';
2678					tokenState = 'escape';
2679				}
2680				else if (c === '<') {
2681					if (result.length === 0) {
2682						tokenState = 'tag';
2683					}
2684					else {
2685						token.type = 'string';
2686						token.value = result.join('');
2687						return token;
2688					}
2689				}
2690				else if (c === '\u0004') {
2691					return {type: 'string', value: result.join('')};
2692				}
2693				else {
2694					result.push(c);
2695				}
2696			}
2697			else if (tokenState === 'escape') {
2698				if (c === '&') {
2699					result.push(buffer);
2700					buffer = '&';
2701				}
2702				else if (c.match(/[0-9a-z]/)) {
2703					buffer += c;
2704				}
2705				else if (c === ';') {
2706					if (buffer === '&amp') {
2707						result.push('&');
2708					}
2709					else if (buffer === '&lt') {
2710						result.push('<');
2711					}
2712					else if (buffer === '&gt') {
2713						result.push('>');
2714					}
2715					else if (buffer === '&lrm') {
2716						result.push('\u200e');
2717					}
2718					else if (buffer === '&rlm') {
2719						result.push('\u200f');
2720					}
2721					else if (buffer === '&nbsp') {
2722						result.push('\u00a0');
2723					}
2724					else {
2725						result.push(buffer);
2726						result.push(';');
2727					}
2728					tokenState = 'data';
2729				}
2730				else if (c === '<' || c === '\u0004') {
2731					result.push(buffer);
2732					token.type = 'string';
2733					token.value = result.join('');
2734					return token;
2735				}
2736				else if (c === '\t' || c === '\n' || c === '\u000c' || c === ' ') { // Handle unescaped & chars as strings
2737					result.push(buffer);
2738					token.type = 'string';
2739					token.value = result.join('');
2740					return token;
2741				}
2742				else {
2743					result.push(buffer);
2744					tokenState = 'data';
2745				}
2746			}
2747			else if (tokenState === 'tag') {
2748				if (c === '\t' || c === '\n' || c === '\u000c' || c === ' ') {
2749					tokenState = 'startTagAnnotation';
2750				}
2751				else if (c === '.') {
2752					tokenState = 'startTagClass';
2753				}
2754				else if (c === '/') {
2755					tokenState = 'endTag';
2756				}
2757				else if (c.match('[0-9]')) {
2758					tokenState = 'timestampTag';
2759					result.push(c);
2760				}
2761				else if (c === '>') {
2762					cut(state, 1);
2763					break;
2764				}
2765				else if (c === '\u0004') {
2766					token.tagName = '';
2767					token.type = 'startTag';
2768					return token;
2769				}
2770				else {
2771					result.push(c);
2772					tokenState = 'startTag';
2773				}
2774			}
2775			else if (tokenState === 'startTag') {
2776				if (c === '\t' || c === '\u000c' || c === ' ') {
2777					tokenState = 'startTagAnnotation';
2778				}
2779				else if (c === '\n') {
2780					buffer = c;
2781					tokenState = 'startTagAnnotation';
2782				}
2783				else if (c === '.') {
2784					tokenState = 'startTagClass';
2785				}
2786				else if (c === '>') {
2787					cut(state, 1);
2788					token.tagName = result.join('');
2789					token.type = 'startTag';
2790					return token;
2791				}
2792				else if (c === '\u0004') {
2793					token.tagName = result.join('');
2794					token.type = 'startTag';
2795					return token;
2796				}
2797				else {
2798					result.push(c);
2799				}
2800			}
2801			else if (tokenState === 'startTagClass') {
2802				if (c === '\t' || c === '\u000c' || c === ' ') {
2803					token.classes.push(buffer);
2804					buffer = '';
2805					tokenState = 'startTagAnnotation';
2806				}
2807				else if (c === '\n') {
2808					token.classes.push(buffer);
2809					buffer = c;
2810					tokenState = 'startTagAnnotation';
2811				}
2812				else if (c === '.') {
2813					token.classes.push(buffer);
2814					buffer = "";
2815				}
2816				else if (c === '>') {
2817					cut(state, 1);
2818					token.classes.push(buffer);
2819					token.type = 'startTag';
2820					token.tagName = result.join('');
2821					return token;
2822				}
2823				else if (c === '\u0004') {
2824					token.classes.push(buffer);
2825					token.type = 'startTag';
2826					token.tagName = result.join('');
2827					return token;
2828				}
2829				else {
2830					buffer += 'c';
2831				}
2832			}
2833			else if (tokenState === 'startTagAnnotation') {
2834				if (c === '>') {
2835					cut(state, 1);
2836					buffer = $.trim(buffer).replace(/ +/, ' ');
2837					token.type = 'startTag';
2838					token.tagName = result.join('');
2839					token.annotation = buffer;
2840					return token;
2841				}
2842				else if (c === '\u0004') {
2843					buffer = $.trim(buffer).replace(/ +/, ' ');
2844					token.type = 'startTag';
2845					token.tagName = result.join('');
2846					token.annotation = buffer;
2847					return token;
2848				}
2849				else {
2850					buffer += c;
2851				}
2852			}
2853			else if (tokenState === 'endTag') {
2854				if (c === '>') {
2855					cut(state, 1);
2856					token.type = 'endTag';
2857					token.tagName = result.join('');
2858					return token;
2859				}
2860				else if (c === '\u0004') {
2861					token.type = 'endTag';
2862					token.tagName = result.join('');
2863					return token;
2864				}
2865				else {
2866					result.push(c);
2867				}
2868			}
2869			else if (tokenState === 'timestampTag') {
2870				if (c === '>') {
2871					cut(state, 1);
2872					token.type = 'timestampTag';
2873					token.name = result.join('');
2874					return token;
2875				}
2876				else if (c === '\u0004') {
2877					token.type = 'timestampTag';
2878					token.name = result.join('');
2879					return token;
2880				}
2881				else {
2882					result.push(c);
2883				}
2884			}
2885			else {
2886				throw 'Unknown tokenState ' + tokenState;
2887			}
2888
2889			cut(state, 1);
2890		}
2891	}
2892
2893	function eatComment(state) {
2894		// Cut the NOTE line.
2895		var noteLine = cutLine(state);
2896		if (noteLine.indexOf('-->') !== -1) {
2897			state.error = 'Invalid syntax: --> in NOTE line.';
2898			return;
2899		}
2900		while (true) {
2901			var nextLine = peekLine(state);
2902			if ($.trim(nextLine).length === 0) {
2903				// End of comment.
2904				return;
2905			}
2906			else if (nextLine.indexOf('-->') !== -1) {
2907				state.error = 'Invalid syntax: --> in comment.';
2908				return;
2909			}
2910			else {
2911				cutLine(state);
2912			}
2913		}
2914	}
2915
2916	// Initial byte order mark.
2917	function eatOptionalBOM(state) {
2918		if (state.text[0] === '\ufeff') {
2919			cut(state, 1);
2920		}
2921
2922	}
2923
2924	// "WEBVTT" string.
2925	function eatSignature(state) {
2926		if (state.text.substring(0,6) === 'WEBVTT') {
2927			cut(state, 6);
2928		}
2929		else {
2930			state.error = 'Invalid signature.';
2931		}
2932	}
2933
2934	function eatArrow(state) {
2935		if (state.text.length < 3 || state.text.substring(0,3) !== '-->') {
2936			state.error = 'Missing -->';
2937		}
2938		else {
2939			cut(state, 3);
2940		}
2941	}
2942
2943	function eatSingleSpaceOrTab(state) {
2944		if (state.text[0] === '\t' || state.text[0] === ' ') {
2945			cut(state, 1);
2946		}
2947		else {
2948			state.error = 'Missing space.';
2949		}
2950	}
2951
2952	function eatSpacesOrTabs(state) {
2953		while (state.text[0] === '\t' || state.text[0] === ' ') {
2954			cut(state, 1);
2955		}
2956	}
2957
2958	function eatAtLeast1SpacesOrTabs(state) {
2959		var numEaten = 0;
2960		while (state.text[0] === '\t' || state.text[0] === ' ') {
2961			cut(state, 1);
2962			numEaten += 1;
2963		}
2964		if (numEaten === 0) {
2965			state.error = 'Missing space.';
2966		}
2967	}
2968
2969	function eatUntilEOLInclusive(state) {
2970		var nextEOL = state.text.indexOf('\n');
2971		if (nextEOL === -1) {
2972			state.error = 'Missing EOL.';
2973		}
2974		else {
2975			cut(state, nextEOL + 1);
2976		}
2977	}
2978
2979	function eatEmptyLines(state) {
2980		while (state.text.length > 0) {
2981			var nextLine = peekLine(state);
2982			if ($.trim(nextLine).length === 0) {
2983				cutLine(state);
2984			}
2985			else {
2986				break;
2987			}
2988		}
2989	}
2990
2991	// Eats empty lines, but throws an error if there's not at least one.
2992	function eatAtLeast1EmptyLines(state) {
2993		var linesEaten = 0;
2994		while (state.text.length > 0) {
2995			var nextLine = peekLine(state);
2996			if ($.trim(nextLine).length === 0) {
2997				cutLine(state);
2998				linesEaten += 1;
2999			}
3000			else {
3001				break;
3002			}
3003		}
3004		if (linesEaten === 0) {
3005			state.error = 'Missing empty line.';
3006		}
3007	}
3008
3009	function getTiming(state) {
3010		var nextSpace = nextSpaceOrNewline(state.text);
3011		if (nextSpace === -1) {
3012			state.error('Missing timing.');
3013			return;
3014		}
3015		var timestamp = cut(state, nextSpace);
3016
3017		var results = /((\d\d):)?((\d\d):)(\d\d).(\d\d\d)|(\d+).(\d\d\d)/.exec(timestamp);
3018
3019		if (!results) {
3020			state.error = 'Unable to parse timestamp';
3021			return;
3022		}
3023		var time = 0;
3024		var hours = results[2];
3025		var minutes = results[4];
3026
3027		if (minutes) {
3028			if (parseInt(minutes, 10) > 59) {
3029				state.error = 'Invalid minute range';
3030				return;
3031			}
3032			if (hours) {
3033				time += 3600 * parseInt(hours, 10);
3034			}
3035			time += 60 * parseInt(minutes, 10);
3036			var seconds = results[5];
3037			if (parseInt(seconds, 10) > 59) {
3038				state.error = 'Invalid second range';
3039				return;
3040			}
3041
3042			time += parseInt(seconds, 10);
3043			time += parseInt(results[6], 10) / 1000;
3044		}
3045		else {
3046			time += parseInt(results[7], 10);
3047			time += parseInt(results[8], 10) / 1000;
3048		}
3049
3050		return time;
3051	}
3052})(jQuery);
3053
3054(function ($) {
3055
3056	AblePlayer.prototype.injectPlayerCode = function() {
3057
3058		// create and inject surrounding HTML structure
3059		// If IOS:
3060		//	If video:
3061		//	 IOS does not support any of the player's functionality
3062		//	 - everything plays in its own player
3063		//	 Therefore, AblePlayer is not loaded & all functionality is disabled
3064		//	 (this all determined. If this is IOS && video, this function is never called)
3065		//	If audio:
3066		//	 HTML cannot be injected as a *parent* of the <audio> element
3067		//	 It is therefore injected *after* the <audio> element
3068		//	 This is only a problem in IOS 6 and earlier,
3069		//	 & is a known bug, fixed in IOS 7
3070
3071		var thisObj, vidcapContainer, prefsGroups, i;
3072		thisObj = this;
3073
3074		// create three wrappers and wrap them around the media element. From inner to outer:
3075		// $mediaContainer - contains the original media element
3076		// $ableDiv - contains the media player and all its objects (e.g., captions, controls, descriptions)
3077		// $ableWrapper - contains additional widgets (e.g., transcript window, sign window)
3078		this.$mediaContainer = this.$media.wrap('<div class="able-media-container"></div>').parent();
3079		this.$ableDiv = this.$mediaContainer.wrap('<div class="able"></div>').parent();
3080		this.$ableWrapper = this.$ableDiv.wrap('<div class="able-wrapper"></div>').parent();
3081    this.$ableWrapper.addClass('able-skin-' + this.skin);
3082
3083		// NOTE: Excluding the following from youtube was resulting in a player
3084		// that exceeds the width of the YouTube video
3085		// Unclear why it was originally excluded; commented out in 3.1.20
3086		// if (this.player !== 'youtube') {
3087		this.$ableWrapper.css({
3088			'max-width': this.playerMaxWidth + 'px'
3089		});
3090
3091		this.injectOffscreenHeading();
3092
3093		if (this.mediaType === 'video') {
3094			// youtube adds its own big play button
3095			// don't show ours *unless* video has a poster attribute
3096			// (which obstructs the YouTube poster & big play button)
3097			if (this.iconType != 'image' && (this.player !== 'youtube' || this.hasPoster)) {
3098				this.injectBigPlayButton();
3099			}
3100
3101			// add container that captions or description will be appended to
3102			// Note: new Jquery object must be assigned _after_ wrap, hence the temp vidcapContainer variable
3103			vidcapContainer = $('<div>',{
3104				'class' : 'able-vidcap-container'
3105			});
3106			this.$vidcapContainer = this.$mediaContainer.wrap(vidcapContainer).parent();
3107		}
3108		this.injectPlayerControlArea();
3109		this.injectTextDescriptionArea();
3110		this.injectAlert();
3111		this.injectPlaylist();
3112	};
3113
3114	AblePlayer.prototype.injectOffscreenHeading = function () {
3115		// Inject an offscreen heading to the media container.
3116		// If heading hasn't already been manually defined via data-heading-level,
3117		// automatically assign a level that is one level deeper than the closest parent heading
3118		// as determined by getNextHeadingLevel()
3119		var headingType;
3120		if (this.playerHeadingLevel == '0') {
3121			// do NOT inject a heading (at author's request)
3122		}
3123		else {
3124			if (typeof this.playerHeadingLevel === 'undefined') {
3125				this.playerHeadingLevel = this.getNextHeadingLevel(this.$ableDiv); // returns in integer 1-6
3126			}
3127			headingType = 'h' + this.playerHeadingLevel.toString();
3128			this.$headingDiv = $('<' + headingType + '>');
3129			this.$ableDiv.prepend(this.$headingDiv);
3130			this.$headingDiv.addClass('able-offscreen');
3131			this.$headingDiv.text(this.tt.playerHeading);
3132		}
3133	};
3134
3135	AblePlayer.prototype.injectBigPlayButton = function () {
3136
3137		this.$bigPlayButton = $('<button>', {
3138			'class': 'able-big-play-button icon-play',
3139			'aria-hidden': true,
3140			'tabindex': -1
3141		});
3142
3143		var thisObj = this;
3144		this.$bigPlayButton.click(function () {
3145			thisObj.handlePlay();
3146		});
3147
3148		this.$mediaContainer.append(this.$bigPlayButton);
3149	};
3150
3151	AblePlayer.prototype.injectPlayerControlArea = function () {
3152
3153		this.$playerDiv = $('<div>', {
3154			'class' : 'able-player',
3155			'role' : 'region',
3156			'aria-label' : this.mediaType + ' player'
3157		});
3158		this.$playerDiv.addClass('able-'+this.mediaType);
3159
3160		// The default skin depends a bit on a Now Playing div
3161		// so go ahead and add one
3162		// However, it's only populated if this.showNowPlaying = true
3163		this.$nowPlayingDiv = $('<div>',{
3164			'class' : 'able-now-playing',
3165			'aria-live' : 'assertive',
3166			'aria-atomic': 'true'
3167		});
3168
3169		this.$controllerDiv = $('<div>',{
3170			'class' : 'able-controller'
3171		});
3172		this.$controllerDiv.addClass('able-' + this.iconColor + '-controls');
3173
3174		this.$statusBarDiv = $('<div>',{
3175			'class' : 'able-status-bar'
3176		});
3177		this.$timer = $('<span>',{
3178			'class' : 'able-timer'
3179		});
3180		this.$elapsedTimeContainer = $('<span>',{
3181			'class': 'able-elapsedTime',
3182			text: '0:00'
3183		});
3184		this.$durationContainer = $('<span>',{
3185			'class': 'able-duration'
3186		});
3187		this.$timer.append(this.$elapsedTimeContainer).append(this.$durationContainer);
3188
3189		this.$speed = $('<span>',{
3190			'class' : 'able-speed',
3191			'aria-live' : 'assertive'
3192		}).text(this.tt.speed + ': 1x');
3193
3194		this.$status = $('<span>',{
3195			'class' : 'able-status',
3196			'aria-live' : 'polite'
3197		});
3198
3199		// Put everything together.
3200		this.$statusBarDiv.append(this.$timer, this.$speed, this.$status);
3201		this.$playerDiv.append(this.$nowPlayingDiv, this.$controllerDiv, this.$statusBarDiv);
3202		this.$ableDiv.append(this.$playerDiv);
3203	};
3204
3205	AblePlayer.prototype.injectTextDescriptionArea = function () {
3206
3207		// create a div for exposing description
3208		// description will be exposed via role="alert" & announced by screen readers
3209		this.$descDiv = $('<div>',{
3210			'class': 'able-descriptions'
3211		});
3212		if (this.exposeTextDescriptions) {
3213  		this.$descDiv.attr({
3214  			'aria-live': 'assertive',
3215        'aria-atomic': 'true'
3216		  });
3217		}
3218		// Start off with description hidden.
3219		// It will be exposed conditionally within description.js > initDescription()
3220		this.$descDiv.hide();
3221		this.$ableDiv.append(this.$descDiv);
3222	};
3223
3224	AblePlayer.prototype.getDefaultWidth = function(which) {
3225
3226		// return default width of resizable elements
3227		// these values are somewhat arbitrary, but seem to result in good usability
3228		// if users disagree, they can resize (and resposition) them
3229		if (which === 'transcript') {
3230			return 450;
3231		}
3232		else if (which === 'sign') {
3233			return 400;
3234		}
3235	};
3236
3237	AblePlayer.prototype.positionDraggableWindow = function (which, width) {
3238
3239		// which is either 'transcript' or 'sign'
3240
3241		var cookie, cookiePos, $window, dragged, windowPos, currentWindowPos, firstTime, zIndex;
3242
3243		cookie = this.getCookie();
3244		if (which === 'transcript') {
3245			$window = this.$transcriptArea;
3246			if (typeof cookie.transcript !== 'undefined') {
3247				cookiePos = cookie.transcript;
3248			}
3249		}
3250		else if (which === 'sign') {
3251			$window = this.$signWindow;
3252			if (typeof cookie.transcript !== 'undefined') {
3253				cookiePos = cookie.sign;
3254			}
3255		}
3256		if (typeof cookiePos !== 'undefined' && !($.isEmptyObject(cookiePos))) {
3257			// position window using stored values from cookie
3258			$window.css({
3259				'position': cookiePos['position'],
3260				'width': cookiePos['width'],
3261				'z-index': cookiePos['zindex']
3262			});
3263			if (cookiePos['position'] === 'absolute') {
3264				$window.css({
3265					'top': cookiePos['top'],
3266					'left': cookiePos['left']
3267				});
3268			}
3269			// since cookie is not page-specific, z-index needs may vary across different pages
3270			this.updateZIndex(which);
3271		}
3272		else {
3273			// position window using default values
3274			windowPos = this.getOptimumPosition(which, width);
3275			if (typeof width === 'undefined') {
3276				width = this.getDefaultWidth(which);
3277			}
3278			$window.css({
3279				'position': windowPos[0],
3280				'width': width,
3281				'z-index': windowPos[3]
3282			});
3283			if (windowPos[0] === 'absolute') {
3284				$window.css({
3285					'top': windowPos[1] + 'px',
3286					'left': windowPos[2] + 'px',
3287				});
3288			}
3289		}
3290	};
3291
3292	AblePlayer.prototype.getOptimumPosition = function (targetWindow, targetWidth) {
3293
3294		// returns optimum position for targetWindow, as an array with the following structure:
3295		// 0 - CSS position ('absolute' or 'relative')
3296		// 1 - top
3297		// 2 - left
3298		// 3 - zindex (if not default)
3299		// targetWindow is either 'transcript' or 'sign'
3300		// if there is room to the right of the player, position element there
3301		// else if there is room the left of the player, position element there
3302		// else position element beneath player
3303
3304		var gap, position, ableWidth, ableHeight, ableOffset, ableTop, ableLeft,
3305			 windowWidth, otherWindowWidth, zIndex;
3306
3307		if (typeof targetWidth === 'undefined') {
3308			targetWidth = this.getDefaultWidth(targetWindow);
3309		}
3310
3311		gap = 5; // number of pixels to preserve between Able Player objects
3312
3313		position = []; // position, top, left
3314
3315		ableWidth = this.$ableDiv.width();
3316		ableHeight = this.$ableDiv.height();
3317		ableOffset = this.$ableDiv.offset();
3318		ableTop = ableOffset.top;
3319		ableLeft = ableOffset.left;
3320		windowWidth = $(window).width();
3321		otherWindowWidth = 0; // width of other visiable draggable windows will be added to this
3322
3323		if (targetWindow === 'transcript') {
3324			if (typeof this.$signWindow !== 'undefined') {
3325				if (this.$signWindow.is(':visible')) {
3326					otherWindowWidth = this.$signWindow.width() + gap;
3327				}
3328			}
3329		}
3330		else if (targetWindow === 'sign') {
3331			if (typeof this.$transcriptArea !== 'undefined') {
3332				if (this.$transcriptArea.is(':visible')) {
3333					otherWindowWidth = this.$transcriptArea.width() + gap;
3334				}
3335			}
3336		}
3337		if (targetWidth < (windowWidth - (ableLeft + ableWidth + gap + otherWindowWidth))) {
3338			// there's room to the left of $ableDiv
3339			position[0] = 'absolute';
3340			position[1] = 0;
3341			position[2] = ableWidth + otherWindowWidth + gap;
3342		}
3343		else if (targetWidth + gap < ableLeft) {
3344			// there's room to the right of $ableDiv
3345			position[0] = 'absolute';
3346			position[1] = 0;
3347			position[2] = ableLeft - targetWidth - gap;
3348		}
3349		else {
3350			// position element below $ableDiv
3351			position[0] = 'relative';
3352			// no need to define top, left, or z-index
3353		}
3354		return position;
3355	};
3356
3357	AblePlayer.prototype.injectPoster = function ($element, context) {
3358
3359		// get poster attribute from media element and append that as an img to $element
3360		// context is either 'youtube' or 'fallback'
3361		var poster, width, height;
3362
3363		if (context === 'youtube') {
3364			if (typeof this.ytWidth !== 'undefined') {
3365				width = this.ytWidth;
3366				height = this.ytHeight;
3367			}
3368			else if (typeof this.playerMaxWidth !== 'undefined') {
3369				width = this.playerMaxWidth;
3370				height = this.playerMaxHeight;
3371			}
3372			else if (typeof this.playerWidth !== 'undefined') {
3373				width = this.playerWidth;
3374				height = this.playerHeight;
3375			}
3376		}
3377		else if (context === 'fallback') {
3378			width = '100%';
3379			height = 'auto';
3380		}
3381
3382		if (this.hasPoster) {
3383			poster = this.$media.attr('poster');
3384			this.$posterImg = $('<img>',{
3385				'class': 'able-poster',
3386				'src' : poster,
3387				'alt' : "",
3388				'role': "presentation",
3389				'width': width,
3390				'height': height
3391			});
3392			$element.append(this.$posterImg);
3393		}
3394	};
3395
3396	AblePlayer.prototype.injectAlert = function () {
3397
3398		// inject two alerts, one visible for all users and one for screen reader users only
3399
3400		var top;
3401
3402		this.$alertBox = $('<div role="alert"></div>');
3403		this.$alertBox.addClass('able-alert');
3404		this.$alertBox.hide();
3405		this.$alertBox.appendTo(this.$ableDiv);
3406		if (this.mediaType == 'audio') {
3407			top = '-10';
3408		}
3409		else {
3410			// position just below the vertical center of the mediaContainer
3411			// hopefully above captions, but not too far from the controller bar
3412			top = Math.round(this.$mediaContainer.height() / 3) * 2;
3413		}
3414		this.$alertBox.css({
3415			top: top + 'px'
3416		});
3417
3418		this.$srAlertBox = $('<div role="alert"></div>');
3419		this.$srAlertBox.addClass('able-screenreader-alert');
3420		this.$srAlertBox.appendTo(this.$ableDiv);
3421	};
3422
3423	AblePlayer.prototype.injectPlaylist = function () {
3424
3425		if (this.playlistEmbed === true) {
3426			// move playlist into player, immediately before statusBarDiv
3427			var playlistClone = this.$playlistDom.clone();
3428			playlistClone.insertBefore(this.$statusBarDiv);
3429			// Update to the new playlist copy.
3430			this.$playlist = playlistClone.find('li');
3431		}
3432	};
3433
3434	AblePlayer.prototype.createPopup = function (which, tracks) {
3435
3436		// Create popup menu and append to player
3437		// 'which' parameter is either 'captions', 'chapters', 'prefs', 'transcript-window' or 'sign-window'
3438		// 'tracks', if provided, is a list of tracks to be used as menu items
3439
3440		var thisObj, $menu, prefCats, i, $menuItem, prefCat, whichPref,
3441			hasDefault, track, windowOptions, whichPref, whichMenu,
3442			$thisItem, $prevItem, $nextItem;
3443
3444		thisObj = this;
3445
3446		$menu = $('<ul>',{
3447			'id': this.mediaId + '-' + which + '-menu',
3448			'class': 'able-popup',
3449			'role': 'menu'
3450		}).hide();
3451
3452		if (which === 'captions') {
3453			$menu.addClass('able-popup-captions');
3454		}
3455
3456		// Populate menu with menu items
3457		if (which === 'prefs') {
3458      if (this.prefCats.length > 1) {
3459  			for (i = 0; i < this.prefCats.length; i++) {
3460	  			$menuItem = $('<li></li>',{
3461		  			'role': 'menuitem',
3462            'tabindex': '-1'
3463				  });
3464          prefCat = this.prefCats[i];
3465          if (prefCat === 'captions') {
3466					  $menuItem.text(this.tt.prefMenuCaptions);
3467				  }
3468          else if (prefCat === 'descriptions') {
3469					  $menuItem.text(this.tt.prefMenuDescriptions);
3470				  }
3471          else if (prefCat === 'keyboard') {
3472					  $menuItem.text(this.tt.prefMenuKeyboard);
3473				  }
3474          else if (prefCat === 'transcript') {
3475					  $menuItem.text(this.tt.prefMenuTranscript);
3476				  }
3477          $menuItem.on('click',function() {
3478					  whichPref = $(this).text();
3479					  thisObj.showingPrefsDialog = true;
3480            thisObj.setFullscreen(false);
3481            if (whichPref === thisObj.tt.prefMenuCaptions) {
3482						  thisObj.captionPrefsDialog.show();
3483					  }
3484            else if (whichPref === thisObj.tt.prefMenuDescriptions) {
3485						  thisObj.descPrefsDialog.show();
3486					  }
3487            else if (whichPref === thisObj.tt.prefMenuKeyboard) {
3488						  thisObj.keyboardPrefsDialog.show();
3489					  }
3490            else if (whichPref === thisObj.tt.prefMenuTranscript) {
3491						  thisObj.transcriptPrefsDialog.show();
3492					  }
3493            thisObj.closePopups();
3494            thisObj.showingPrefsDialog = false;
3495				  });
3496          $menu.append($menuItem);
3497			  }
3498			  this.$prefsButton.attr('data-prefs-popup','menu');
3499      }
3500      else if (this.prefCats.length == 1) {
3501        // only 1 category, so don't create a popup menu.
3502        // Instead, open dialog directly when user clicks Prefs button
3503        this.$prefsButton.attr('data-prefs-popup',this.prefCats[0]);
3504      }
3505		}
3506		else if (which === 'captions' || which === 'chapters') {
3507			hasDefault = false;
3508			for (i = 0; i < tracks.length; i++) {
3509				track = tracks[i];
3510				$menuItem = $('<li></li>',{
3511					'role': 'menuitemradio',
3512					'tabindex': '-1',
3513					'lang': track.language
3514				});
3515				if (track.def && this.prefCaptions == 1) {
3516					$menuItem.attr('aria-checked','true');
3517					hasDefault = true;
3518				}
3519				else {
3520					$menuItem.attr('aria-checked','false');
3521				}
3522				// Get a label using track data
3523				if (which == 'captions') {
3524					$menuItem.text(track.label);
3525					$menuItem.on('click',this.getCaptionClickFunction(track));
3526				}
3527				else if (which == 'chapters') {
3528					$menuItem.text(this.flattenCueForCaption(track) + ' - ' + this.formatSecondsAsColonTime(track.start));
3529					$menuItem.on('click',this.getChapterClickFunction(track.start));
3530				}
3531				$menu.append($menuItem);
3532			}
3533			if (which === 'captions') {
3534				// add a 'captions off' menu item
3535				$menuItem = $('<li></li>',{
3536					'role': 'menuitemradio',
3537					'tabindex': '-1',
3538				}).text(this.tt.captionsOff);
3539				if (this.prefCaptions === 0) {
3540					$menuItem.attr('aria-checked','true');
3541					hasDefault = true;
3542				}
3543				$menuItem.on('click',this.getCaptionOffFunction());
3544				$menu.append($menuItem);
3545			}
3546		}
3547		else if (which === 'transcript-window' || which === 'sign-window') {
3548			windowOptions = [];
3549			windowOptions.push({
3550				'name': 'move',
3551				'label': this.tt.windowMove
3552			});
3553			windowOptions.push({
3554				'name': 'resize',
3555				'label': this.tt.windowResize
3556			});
3557			windowOptions.push({
3558				'name': 'close',
3559				'label': this.tt.windowClose
3560			});
3561			for (i = 0; i < windowOptions.length; i++) {
3562				$menuItem = $('<li></li>',{
3563					'role': 'menuitem',
3564					'tabindex': '-1',
3565					'data-choice': windowOptions[i].name
3566				});
3567				$menuItem.text(windowOptions[i].label);
3568				$menuItem.on('click mousedown',function(e) {
3569					e.stopPropagation();
3570					if (typeof e.button !== 'undefined' && e.button !== 0) {
3571  					// this was a mouse click (if click is triggered by keyboard, e.button is undefined)
3572  					// and the button was not a left click (left click = 0)
3573  					// therefore, ignore this click
3574						return false;
3575					}
3576					if (!thisObj.windowMenuClickRegistered && !thisObj.finishingDrag) {
3577						thisObj.windowMenuClickRegistered = true;
3578						thisObj.handleMenuChoice(which.substr(0, which.indexOf('-')), $(this).attr('data-choice'), e);
3579					}
3580				});
3581				$menu.append($menuItem);
3582			}
3583		}
3584		// assign default item, if there isn't one already
3585		if (which === 'captions' && !hasDefault) {
3586			// check the menu item associated with the default language
3587			// as determined in control.js > syncTrackLanguages()
3588			if ($menu.find('li[lang=' + this.captionLang + ']')) {
3589				// a track exists for the default language. Check that item in the menu
3590				$menu.find('li[lang=' + this.captionLang + ']').attr('aria-checked','true');
3591			}
3592			else {
3593				// check the last item (captions off)
3594				$menu.find('li').last().attr('aria-checked','true');
3595			}
3596		}
3597		else if (which === 'chapters') {
3598			if ($menu.find('li:contains("' + this.defaultChapter + '")')) {
3599				$menu.find('li:contains("' + this.defaultChapter + '")').attr('aria-checked','true').addClass('able-focus');
3600			}
3601			else {
3602				$menu.find('li').first().attr('aria-checked','true').addClass('able-focus');
3603			}
3604		}
3605		// add keyboard handlers for navigating within popups
3606		$menu.on('keydown',function (e) {
3607
3608			whichMenu = $(this).attr('id').split('-')[1];
3609			$thisItem = $(this).find('li:focus');
3610			if ($thisItem.is(':first-child')) {
3611				// this is the first item in the menu
3612				$prevItem = $(this).find('li').last(); // wrap to bottom
3613				$nextItem = $thisItem.next();
3614			}
3615			else if ($thisItem.is(':last-child')) {
3616				// this is the last Item
3617				$prevItem = $thisItem.prev();
3618				$nextItem = $(this).find('li').first(); // wrap to top
3619			}
3620			else {
3621				$prevItem = $thisItem.prev();
3622				$nextItem = $thisItem.next();
3623			}
3624			if (e.which === 9) { // Tab
3625				if (e.shiftKey) {
3626					$thisItem.removeClass('able-focus');
3627					$prevItem.focus().addClass('able-focus');
3628				}
3629				else {
3630					$thisItem.removeClass('able-focus');
3631					$nextItem.focus().addClass('able-focus');
3632				}
3633			}
3634			else if (e.which === 40 || e.which === 39) { // down or right arrow
3635				$thisItem.removeClass('able-focus');
3636				$nextItem.focus().addClass('able-focus');
3637			}
3638			else if (e.which == 38 || e.which === 37) { // up or left arrow
3639				$thisItem.removeClass('able-focus');
3640				$prevItem.focus().addClass('able-focus');
3641			}
3642			else if (e.which === 32 || e.which === 13) { // space or enter
3643				$thisItem.click();
3644			}
3645			else if (e.which === 27) {	// Escape
3646				$thisItem.removeClass('able-focus');
3647				thisObj.closePopups();
3648			}
3649			e.preventDefault();
3650		});
3651		this.$controllerDiv.append($menu);
3652		return $menu;
3653	};
3654
3655	AblePlayer.prototype.closePopups = function () {
3656
3657    var thisObj = this;
3658
3659		if (this.chaptersPopup && this.chaptersPopup.is(':visible')) {
3660			this.chaptersPopup.hide();
3661			this.$chaptersButton.removeAttr('aria-expanded').focus();
3662		}
3663		if (this.captionsPopup && this.captionsPopup.is(':visible')) {
3664			this.captionsPopup.hide();
3665			this.$ccButton.removeAttr('aria-expanded').focus();
3666		}
3667		if (this.prefsPopup && this.prefsPopup.is(':visible') && !this.hidingPopup) {
3668      this.hidingPopup = true; // stopgap to prevent popup from re-opening again on keypress
3669			this.prefsPopup.hide();
3670			// restore menu items to their original state
3671			this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
3672			this.$prefsButton.removeAttr('aria-expanded');
3673			if (!this.showingPrefsDialog) {
3674  			this.$prefsButton.focus();
3675			}
3676			// wait briefly, then reset hidingPopup
3677			setTimeout(function() {
3678  			thisObj.hidingPopup = false;
3679  		},100);
3680		}
3681		if (this.$volumeSlider && this.$volumeSlider.is(':visible')) {
3682			this.$volumeSlider.hide().attr('aria-hidden','true');
3683			this.$volumeAlert.text(this.tt.volumeSliderClosed);
3684			this.$volumeButton.removeAttr('aria-expanded').focus();
3685		}
3686		if (this.$transcriptPopup && this.$transcriptPopup.is(':visible')) {
3687			this.$transcriptPopup.hide();
3688			// restore menu items to their original state
3689			this.$transcriptPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
3690			this.$transcriptPopupButton.removeAttr('aria-expanded').focus();
3691		}
3692		if (this.$signPopup && this.$signPopup.is(':visible')) {
3693			this.$signPopup.hide();
3694			// restore menu items to their original state
3695			this.$signPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
3696			this.$signPopupButton.removeAttr('aria-expanded').focus();
3697		}
3698	};
3699
3700	AblePlayer.prototype.setupPopups = function (which) {
3701
3702		// Create and fill in the popup menu forms for various controls.
3703		// parameter 'which' is passed if refreshing content of an existing popup ('captions' or 'chapters')
3704		// If which is undefined, automatically setup 'captions', 'chapters', and 'prefs' popups
3705		// However, only setup 'transcript-window' and 'sign-window' popups if passed as value of which
3706		var popups, thisObj, hasDefault, i, j,
3707				tracks, track, $trackButton, $trackLabel,
3708				radioName, radioId, $menu, $menuItem,
3709				prefCats, prefCat, prefLabel;
3710
3711		popups = [];
3712		if (typeof which === 'undefined') {
3713			popups.push('prefs');
3714		}
3715
3716		if (which === 'captions' || (typeof which === 'undefined')) {
3717			if (this.captions.length > 0) {
3718				popups.push('captions');
3719			}
3720		}
3721		if (which === 'chapters' || (typeof which === 'undefined')) {
3722			if (this.chapters.length > 0 && this.useChaptersButton) {
3723				popups.push('chapters');
3724			}
3725		}
3726		if (which === 'transcript-window' && this.transcriptType === 'popup') {
3727			popups.push('transcript-window');
3728		}
3729		if (which === 'sign-window' && this.hasSignLanguage) {
3730			popups.push('sign-window');
3731		}
3732		if (popups.length > 0) {
3733			thisObj = this;
3734			for (var i=0; i<popups.length; i++) {
3735				var popup = popups[i];
3736				hasDefault = false;
3737				if (popup == 'prefs') {
3738					this.prefsPopup = this.createPopup('prefs');
3739				}
3740				else if (popup == 'captions') {
3741					if (typeof this.captionsPopup === 'undefined' || !this.captionsPopup) {
3742						this.captionsPopup = this.createPopup('captions',this.captions);
3743					}
3744				}
3745				else if (popup == 'chapters') {
3746					if (this.selectedChapters) {
3747						tracks = this.selectedChapters.cues;
3748					}
3749					else if (this.chapters.length >= 1) {
3750						tracks = this.chapters[0].cues;
3751					}
3752					else {
3753						tracks = [];
3754					}
3755					if (typeof this.chaptersPopup === 'undefined' || !this.chaptersPopup) {
3756						this.chaptersPopup = this.createPopup('chapters',tracks);
3757					}
3758				}
3759				else if (popup == 'transcript-window') {
3760					return this.createPopup('transcript-window');
3761				}
3762				else if (popup == 'sign-window') {
3763					return this.createPopup('sign-window');
3764				}
3765			}
3766		}
3767	};
3768
3769	AblePlayer.prototype.provideFallback = function() {
3770
3771		// provide ultimate fallback for users who are unable to play the media
3772		// If there is HTML content nested within the media element, display that
3773		// Otherwise, display standard localized error text
3774
3775		var $fallbackDiv, width, mediaClone, fallback, fallbackText,
3776		showBrowserList, browsers, i, b, browserList;
3777
3778		// Could show list of supporting browsers if 99.9% confident the error is truly an outdated browser
3779		// Too many sites say "You need to update your browser" when in fact I'm using a current version
3780		showBrowserList = false;
3781
3782		$fallbackDiv = $('<div>',{
3783			'class' : 'able-fallback',
3784			'role' : 'alert',
3785		});
3786		// override default width of .able-fallback with player width, if known
3787		if (typeof this.playerMaxWidth !== 'undefined') {
3788			width = this.playerMaxWidth + 'px';
3789		}
3790		else if (this.$media.attr('width')) {
3791			width = parseInt(this.$media.attr('width'), 10) + 'px';
3792		}
3793		else {
3794			width = '100%';
3795		}
3796		$fallbackDiv.css('max-width',width);
3797
3798		// use fallback content that's nested inside the HTML5 media element, if there is any
3799		mediaClone = this.$media.clone();
3800		$('source, track', mediaClone).remove();
3801		fallback = mediaClone.html().trim();
3802		if (fallback.length) {
3803			$fallbackDiv.html(fallback);
3804		}
3805		else {
3806			// use standard localized error message
3807			fallbackText =	this.tt.fallbackError1 + ' ' + this.tt[this.mediaType] + '. ';
3808			fallbackText += this.tt.fallbackError2 + ':';
3809			fallback = $('<p>').text(fallbackText);
3810			$fallbackDiv.html(fallback);
3811			showBrowserList = true;
3812		}
3813
3814		if (showBrowserList) {
3815			browserList = $('<ul>');
3816			browsers = this.getSupportingBrowsers();
3817			for (i=0; i<browsers.length; i++) {
3818				b = $('<li>');
3819				b.text(browsers[i].name + ' ' + browsers[i].minVersion + ' ' + this.tt.orHigher);
3820				browserList.append(b);
3821			}
3822			$fallbackDiv.append(browserList);
3823		}
3824
3825		// if there's a poster, show that as well
3826		this.injectPoster($fallbackDiv, 'fallback');
3827
3828		// inject $fallbackDiv into the DOM and remove broken content
3829		if (typeof this.$ableWrapper !== 'undefined') {
3830			this.$ableWrapper.before($fallbackDiv);
3831			this.$ableWrapper.remove();
3832		}
3833		else if (typeof this.$media !== 'undefined') {
3834			this.$media.before($fallbackDiv);
3835			this.$media.remove();
3836		}
3837		else {
3838			$('body').prepend($fallbackDiv);
3839		}
3840	};
3841
3842	AblePlayer.prototype.getSupportingBrowsers = function() {
3843
3844		var browsers = [];
3845		browsers[0] = {
3846			name:'Chrome',
3847			minVersion: '31'
3848		};
3849		browsers[1] = {
3850			name:'Firefox',
3851			minVersion: '34'
3852		};
3853		browsers[2] = {
3854			name:'Internet Explorer',
3855			minVersion: '10'
3856		};
3857		browsers[3] = {
3858			name:'Opera',
3859			minVersion: '26'
3860		};
3861		browsers[4] = {
3862			name:'Safari for Mac OS X',
3863			minVersion: '7.1'
3864		};
3865		browsers[5] = {
3866			name:'Safari for iOS',
3867			minVersion: '7.1'
3868		};
3869		browsers[6] = {
3870			name:'Android Browser',
3871			minVersion: '4.1'
3872		};
3873		browsers[7] = {
3874			name:'Chrome for Android',
3875			minVersion: '40'
3876		};
3877		return browsers;
3878	}
3879
3880	AblePlayer.prototype.calculateControlLayout = function () {
3881
3882		// Calculates the layout for controls based on media and options.
3883		// Returns an array with 4 keys (for legacy skin) or 2 keys (for 2020 skin)
3884		// Keys are the following order:
3885		// 0 = Top left
3886		// 1 = Top right
3887		// 2 = Bottom left (legacy skin only)
3888		// 3 = Bottom right (legacy skin only)
3889		// Each key contains an array of control names to put in that section.
3890
3891		var controlLayout, volumeSupported, playbackSupported, totalButtonWidth, numA11yButtons;
3892
3893		controlLayout = [];
3894		controlLayout[0] = [];
3895		controlLayout[1] = [];
3896    if (this.skin === 'legacy') {
3897		  controlLayout[2] = [];
3898		  controlLayout[3] = [];
3899		}
3900
3901		controlLayout[0].push('play');
3902		controlLayout[0].push('restart');
3903		controlLayout[0].push('rewind');
3904		controlLayout[0].push('forward');
3905
3906    if (this.skin === 'legacy') {
3907      controlLayout[1].push('seek');
3908    }
3909
3910		if (this.hasPlaylist) {
3911  		if (this.skin === 'legacy') {
3912    		controlLayout[0].push('previous');
3913        controlLayout[0].push('next');
3914      }
3915      else if (this.skin == '2020') {
3916    		controlLayout[0].push('previous');
3917        controlLayout[0].push('next');
3918      }
3919		}
3920
3921		if (this.isPlaybackRateSupported()) {
3922  		playbackSupported = true;
3923  		if (this.skin === 'legacy') {
3924  			controlLayout[2].push('slower');
3925  			controlLayout[2].push('faster');
3926  		}
3927		}
3928		else {
3929  		playbackSupported = false;
3930		}
3931
3932		if (this.mediaType === 'video') {
3933  		numA11yButtons = 0;
3934			if (this.hasCaptions) {
3935  			numA11yButtons++;
3936  			if (this.skin === 'legacy') {
3937				  controlLayout[2].push('captions');
3938				}
3939				else if (this.skin == '2020') {
3940  				controlLayout[1].push('captions');
3941        }
3942			}
3943			if (this.hasSignLanguage) {
3944  			numA11yButtons++;
3945  			if (this.skin === 'legacy') {
3946				  controlLayout[2].push('sign');
3947				}
3948				else if (this.skin == '2020') {
3949  				controlLayout[1].push('sign');
3950        }
3951			}
3952			if ((this.hasOpenDesc || this.hasClosedDesc) && (this.useDescriptionsButton)) {
3953  			numA11yButtons++;
3954  			if (this.skin === 'legacy') {
3955				  controlLayout[2].push('descriptions');
3956				}
3957				else if (this.skin == '2020') {
3958  				controlLayout[1].push('descriptions');
3959        }
3960			}
3961		}
3962		if (this.transcriptType === 'popup' && !(this.hideTranscriptButton)) {
3963  		numA11yButtons++;
3964  		if (this.skin === 'legacy') {
3965				controlLayout[2].push('transcript');
3966		  }
3967      else if (this.skin == '2020') {
3968  		  controlLayout[1].push('transcript');
3969      }
3970		}
3971
3972		if (this.mediaType === 'video' && this.hasChapters && this.useChaptersButton) {
3973  		numA11yButtons++;
3974  		if (this.skin === 'legacy') {
3975				controlLayout[2].push('chapters');
3976		  }
3977			else if (this.skin == '2020') {
3978  		  controlLayout[1].push('chapters');
3979      }
3980		}
3981
3982    if (this.skin == '2020' && numA11yButtons > 0) {
3983  		controlLayout[1].push('pipe');
3984		}
3985
3986    if (playbackSupported && this.skin === '2020') {
3987      controlLayout[1].push('faster');
3988    	controlLayout[1].push('slower');
3989    	controlLayout[1].push('pipe');
3990		}
3991
3992    if (this.skin === 'legacy') {
3993  		controlLayout[3].push('preferences');
3994    }
3995    else if (this.skin == '2020') {
3996      controlLayout[1].push('preferences');
3997    }
3998
3999		if (this.mediaType === 'video' && this.allowFullScreen) {
4000      if (this.skin === 'legacy') {
4001    		controlLayout[3].push('fullscreen');
4002      }
4003      else {
4004        controlLayout[1].push('fullscreen');
4005      }
4006		}
4007
4008		if (this.browserSupportsVolume()) {
4009  		volumeSupported = true; // defined in case we decide to move volume button elsewhere
4010			this.volumeButton = 'volume-' + this.getVolumeName(this.volume);
4011			if (this.skin === 'legacy') {
4012  			controlLayout[1].push('volume');
4013  		}
4014  		else if (this.skin == '2020') {
4015    		controlLayout[1].push('volume');
4016  		}
4017		}
4018		else {
4019  		volumeSupported = false;
4020			this.volume = false;
4021		}
4022		return controlLayout;
4023	};
4024
4025	AblePlayer.prototype.addControls = function() {
4026
4027		// determine which controls to show based on several factors:
4028		// mediaType (audio vs video)
4029		// availability of tracks (e.g., for closed captions & audio description)
4030		// browser support (e.g., for sliders and speedButtons)
4031		// user preferences (???)
4032		// some controls are aligned on the left, and others on the right
4033
4034		var thisObj, baseSliderWidth, controlLayout, numSections,
4035		i, j, k, controls, $controllerSpan, $sliderDiv, sliderLabel, $pipe, $pipeImg,
4036		svgData, svgPath, control,
4037    $buttonLabel, $buttonImg, buttonImgSrc, buttonTitle, $newButton, iconClass, buttonIcon,
4038    buttonUse, buttonText, position, buttonHeight, buttonWidth, buttonSide, controllerWidth,
4039    tooltipId, tooltipY, tooltipX, tooltipWidth, tooltipStyle, tooltip,
4040    captionLabel, popupMenuId;
4041
4042		thisObj = this;
4043
4044		baseSliderWidth = 100; // arbitrary value, will be recalculated in refreshControls()
4045
4046		// Initialize the layout into the this.controlLayout variable.
4047		controlLayout = this.calculateControlLayout();
4048		numSections = controlLayout.length;
4049
4050		// add an empty div to serve as a tooltip
4051		tooltipId = this.mediaId + '-tooltip';
4052		this.$tooltipDiv = $('<div>',{
4053			'id': tooltipId,
4054			'class': 'able-tooltip'
4055		}).hide();
4056		this.$controllerDiv.append(this.$tooltipDiv);
4057
4058		if (this.skin == '2020') {
4059  		// add a full-width seek bar
4060      $sliderDiv = $('<div class="able-seekbar"></div>');
4061			sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel;
4062			this.$controllerDiv.append($sliderDiv);
4063			this.seekBar = new AccessibleSlider(this.mediaType, $sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
4064		}
4065
4066		// step separately through left and right controls
4067		for (i = 0; i < numSections; i++) {
4068			controls = controlLayout[i];
4069			if ((i % 2) === 0) { // even keys on the left
4070				$controllerSpan = $('<div>',{
4071					'class': 'able-left-controls'
4072				});
4073			}
4074			else { // odd keys on the right
4075				$controllerSpan = $('<div>',{
4076					'class': 'able-right-controls'
4077				});
4078			}
4079			this.$controllerDiv.append($controllerSpan);
4080			for (j=0; j<controls.length; j++) {
4081				control = controls[j];
4082				if (control === 'seek') {
4083					$sliderDiv = $('<div class="able-seekbar"></div>');
4084					sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel;
4085					$controllerSpan.append($sliderDiv);
4086					if (typeof this.duration === 'undefined' || this.duration === 0) {
4087						// set arbitrary starting duration, and change it when duration is known
4088						this.duration = 60;
4089						// also set elapsed to 0
4090						this.elapsed = 0;
4091					}
4092					this.seekBar = new AccessibleSlider(this.mediaType, $sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
4093				}
4094				else if (control === 'pipe') {
4095					$pipe = $('<span>', {
4096						'tabindex': '-1',
4097						'aria-hidden': 'true'
4098					});
4099					if (this.iconType === 'font') {
4100						$pipe.addClass('icon-pipe');
4101					}
4102					else {
4103						$pipeImg = $('<img>', {
4104							src: this.rootPath + 'button-icons/' + this.iconColor + '/pipe.png',
4105							alt: '',
4106							role: 'presentation'
4107						});
4108						$pipe.append($pipeImg);
4109					}
4110					$controllerSpan.append($pipe);
4111				}
4112				else {
4113					// this control is a button
4114					if (control === 'volume') {
4115						buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + this.volumeButton + '.png';
4116					}
4117					else if (control === 'fullscreen') {
4118						buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/fullscreen-expand.png';
4119					}
4120					else if (control === 'slower') {
4121						if (this.speedIcons === 'animals') {
4122							buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/turtle.png';
4123						}
4124						else {
4125							buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/slower.png';
4126						}
4127					}
4128					else if (control === 'faster') {
4129						if (this.speedIcons === 'animals') {
4130							buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/rabbit.png';
4131						}
4132						else {
4133							buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/faster.png';
4134						}
4135					}
4136					else {
4137						buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + control + '.png';
4138					}
4139					buttonTitle = this.getButtonTitle(control);
4140
4141					// icomoon documentation recommends the following markup for screen readers:
4142					// 1. link element (or in our case, button). Nested inside this element:
4143					// 2. span that contains the icon font (in our case, buttonIcon)
4144					// 3. span that contains a visually hidden label for screen readers (buttonLabel)
4145					// In addition, we are adding aria-label to the button (but not title)
4146					// And if iconType === 'image', we are replacing #2 with an image (with alt="" and role="presentation")
4147					// This has been thoroughly tested and works well in all screen reader/browser combinations
4148					// See https://github.com/ableplayer/ableplayer/issues/81
4149
4150          // NOTE: Changed from <button> to <div role="button" as of 4.2.18
4151          // because <button> elements are rendered poorly in high contrast mode
4152          // in some OS/browser/plugin combinations
4153					$newButton = $('<div>',{
4154						'role': 'button',
4155						'tabindex': '0',
4156						'aria-label': buttonTitle,
4157						'class': 'able-button-handler-' + control
4158					});
4159
4160					if (control === 'volume' || control === 'preferences') {
4161						if (control == 'preferences') {
4162  						this.prefCats = this.getPreferencesGroups();
4163              if (this.prefCats.length > 1) {
4164  						  // Prefs button will trigger a menu
4165                popupMenuId = this.mediaId + '-prefs-menu';
4166                $newButton.attr({
4167							    'aria-controls': popupMenuId,
4168                  'aria-haspopup': 'menu'
4169                });
4170						  }
4171              else if (this.prefCats.length === 1) {
4172  						  // Prefs button will trigger a dialog
4173                $newButton.attr({
4174    						  'aria-haspopup': 'dialog'
4175                });
4176						  }
4177						}
4178						else if (control === 'volume') {
4179							popupMenuId = this.mediaId + '-volume-slider';
4180							// volume slider popup is not a menu or a dialog
4181							// therefore, using aria-expanded rather than aria-haspopup to communicate properties/state
4182              $newButton.attr({
4183                'aria-controls': popupMenuId,
4184    						'aria-expanded': 'false'
4185              });
4186						}
4187					}
4188					if (this.iconType === 'font') {
4189						if (control === 'volume') {
4190							iconClass = 'icon-' + this.volumeButton;
4191						}
4192						else if (control === 'slower') {
4193							if (this.speedIcons === 'animals') {
4194								iconClass = 'icon-turtle';
4195							}
4196							else {
4197								iconClass = 'icon-slower';
4198							}
4199						}
4200						else if (control === 'faster') {
4201							if (this.speedIcons === 'animals') {
4202								iconClass = 'icon-rabbit';
4203							}
4204							else {
4205								iconClass = 'icon-faster';
4206							}
4207						}
4208						else {
4209							iconClass = 'icon-' + control;
4210						}
4211						buttonIcon = $('<span>',{
4212							'class': iconClass,
4213							'aria-hidden': 'true'
4214						});
4215						$newButton.append(buttonIcon);
4216					}
4217					else if (this.iconType === 'svg') {
4218
4219					/*
4220						// Unused option for adding SVG:
4221						// Use <use> element to link to button-icons/able-icons.svg
4222						// Advantage: SVG file can be cached
4223						// Disadvantage: Not supported by Safari 6, IE 6-11, or Edge 12
4224						// Instead, adding <svg> element within each <button>
4225						if (control === 'volume') {
4226							iconClass = 'svg-' + this.volumeButton;
4227						}
4228						else if (control === 'fullscreen') {
4229							iconClass = 'svg-fullscreen-expand';
4230						}
4231						else if (control === 'slower') {
4232							if (this.speedIcons === 'animals') {
4233								iconClass = 'svg-turtle';
4234							}
4235							else {
4236								iconClass = 'svg-slower';
4237							}
4238						}
4239						else if (control === 'faster') {
4240							if (this.speedIcons === 'animals') {
4241								iconClass = 'svg-rabbit';
4242							}
4243							else {
4244								iconClass = 'svg-faster';
4245							}
4246						}
4247						else {
4248							iconClass = 'svg-' + control;
4249						}
4250						buttonIcon = $('<svg>',{
4251							'class': iconClass
4252						});
4253						buttonUse = $('<use>',{
4254							'xlink:href': this.rootPath + 'button-icons/able-icons.svg#' + iconClass
4255						});
4256						buttonIcon.append(buttonUse);
4257						*/
4258						var svgData;
4259						if (control === 'volume') {
4260							svgData = this.getSvgData(this.volumeButton);
4261						}
4262						else if (control === 'fullscreen') {
4263							svgData = this.getSvgData('fullscreen-expand');
4264						}
4265						else if (control === 'slower') {
4266							if (this.speedIcons === 'animals') {
4267								svgData = this.getSvgData('turtle');
4268							}
4269							else {
4270								svgData = this.getSvgData('slower');
4271							}
4272						}
4273						else if (control === 'faster') {
4274							if (this.speedIcons === 'animals') {
4275								svgData = this.getSvgData('rabbit');
4276							}
4277							else {
4278								svgData = this.getSvgData('faster');
4279							}
4280						}
4281						else {
4282							svgData = this.getSvgData(control);
4283						}
4284						buttonIcon = $('<svg>',{
4285							'focusable': 'false',
4286							'aria-hidden': 'true',
4287							'viewBox': svgData[0]
4288						});
4289						svgPath = $('<path>',{
4290							'd': svgData[1]
4291						});
4292						buttonIcon.append(svgPath);
4293						$newButton.html(buttonIcon);
4294
4295						// Final step: Need to refresh the DOM in order for browser to process & display the SVG
4296						$newButton.html($newButton.html());
4297					}
4298					else {
4299						// use images
4300						$buttonImg = $('<img>',{
4301							'src': buttonImgSrc,
4302							'alt': '',
4303							'role': 'presentation'
4304						});
4305						$newButton.append($buttonImg);
4306					}
4307					// add the visibly-hidden label for screen readers that don't support aria-label on the button
4308					var $buttonLabel = $('<span>',{
4309						'class': 'able-clipped'
4310					}).text(buttonTitle);
4311					$newButton.append($buttonLabel);
4312					// add an event listener that displays a tooltip on mouseenter or focus
4313					$newButton.on('mouseenter focus',function(e) {
4314						var buttonText = $(this).attr('aria-label');
4315						// get position of this button
4316						var position = $(this).position();
4317						var buttonHeight = $(this).height();
4318						var buttonWidth = $(this).width();
4319						// position() is expressed using top and left (of button);
4320						// add right (of button) too, for convenience
4321						var controllerWidth = thisObj.$controllerDiv.width();
4322						position.right = controllerWidth - position.left - buttonWidth;
4323						var tooltipY = position.top - buttonHeight - 15;
4324
4325						if ($(this).parent().hasClass('able-right-controls')) {
4326							// this control is on the right side
4327              var buttonSide = 'right';
4328						}
4329						else {
4330							// this control is on the left side
4331              var buttonSide = 'left';
4332						}
4333						// populate tooltip, then calculate its width before showing it
4334						var tooltipWidth = AblePlayer.localGetElementById($newButton[0], tooltipId).text(buttonText).width();
4335						// center the tooltip horizontally over the button
4336            if (buttonSide == 'left') {
4337    				  var tooltipX = position.left - tooltipWidth/2;
4338              if (tooltipX < 0) {
4339                // tooltip would exceed the bounds of the player. Adjust.
4340                tooltipX = 2;
4341              }
4342              var tooltipStyle = {
4343							  left: tooltipX + 'px',
4344								right: '',
4345								top: tooltipY + 'px'
4346						  };
4347            }
4348            else {
4349              var tooltipX = position.right - tooltipWidth/2;
4350              if (tooltipX < 0) {
4351                // tooltip would exceed the bounds of the player. Adjust.
4352                tooltipX = 2;
4353              }
4354              var tooltipStyle = {
4355								left: '',
4356								right: tooltipX + 'px',
4357								top: tooltipY + 'px'
4358						  };
4359            }
4360						var tooltip = AblePlayer.localGetElementById($newButton[0], tooltipId).text(buttonText).css(tooltipStyle);
4361						thisObj.showTooltip(tooltip);
4362						$(this).on('mouseleave blur',function() {
4363							AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
4364						})
4365					});
4366
4367					if (control === 'captions') {
4368						if (!this.prefCaptions || this.prefCaptions !== 1) {
4369							// captions are available, but user has them turned off
4370							if (this.captions.length > 1) {
4371								captionLabel = this.tt.captions;
4372							}
4373							else {
4374								captionLabel = this.tt.showCaptions;
4375							}
4376							$newButton.addClass('buttonOff').attr('title',captionLabel);
4377						}
4378					}
4379					else if (control === 'descriptions') {
4380						if (!this.prefDesc || this.prefDesc !== 1) {
4381							// user prefer non-audio described version
4382							// Therefore, load media without description
4383							// Description can be toggled on later with this button
4384							$newButton.addClass('buttonOff').attr('title',this.tt.turnOnDescriptions);
4385						}
4386					}
4387
4388					$controllerSpan.append($newButton);
4389
4390					// create variables of buttons that are referenced throughout the AblePlayer object
4391					if (control === 'play') {
4392						this.$playpauseButton = $newButton;
4393					}
4394					else if (control == 'previous') {
4395  					this.$prevButton = $newButton;
4396            // if player is being rebuilt because user clicked the Prev button
4397            // return focus to that (newly built) button
4398            if (this.buttonWithFocus == 'previous') {
4399              this.$prevButton.focus();
4400              this.buttonWithFocus = null;
4401            }
4402					}
4403					else if (control == 'next') {
4404  					this.$nextButton = $newButton;
4405            // if player is being rebuilt because user clicked the Next button
4406            // return focus to that (newly built) button
4407            if (this.buttonWithFocus == 'next') {
4408              this.$nextButton.focus();
4409              this.buttonWithFocus = null;
4410            }
4411					}
4412					else if (control === 'captions') {
4413						this.$ccButton = $newButton;
4414					}
4415					else if (control === 'sign') {
4416						this.$signButton = $newButton;
4417						// gray out sign button if sign language window is not active
4418						if (!(this.$signWindow.is(':visible'))) {
4419							this.$signButton.addClass('buttonOff');
4420						}
4421					}
4422					else if (control === 'descriptions') {
4423						this.$descButton = $newButton;
4424						// button will be enabled or disabled in description.js > initDescription()
4425					}
4426					else if (control === 'mute') {
4427						this.$muteButton = $newButton;
4428					}
4429					else if (control === 'transcript') {
4430						this.$transcriptButton = $newButton;
4431						// gray out transcript button if transcript is not active
4432						if (!(this.$transcriptDiv.is(':visible'))) {
4433							this.$transcriptButton.addClass('buttonOff').attr('title',this.tt.showTranscript);
4434						}
4435					}
4436					else if (control === 'fullscreen') {
4437						this.$fullscreenButton = $newButton;
4438					}
4439					else if (control === 'chapters') {
4440						this.$chaptersButton = $newButton;
4441					}
4442					else if (control === 'preferences') {
4443						this.$prefsButton = $newButton;
4444					}
4445					else if (control === 'volume') {
4446						this.$volumeButton = $newButton;
4447					}
4448				}
4449				if (control === 'volume') {
4450					// in addition to the volume button, add a hidden slider
4451					this.addVolumeSlider($controllerSpan);
4452				}
4453			}
4454			if ((i % 2) == 1) {
4455				this.$controllerDiv.append('<div style="clear:both;"></div>');
4456			}
4457		}
4458
4459		if (this.mediaType === 'video') {
4460
4461			if (typeof this.$captionsDiv !== 'undefined') {
4462				// stylize captions based on user prefs
4463				this.stylizeCaptions(this.$captionsDiv);
4464			}
4465			if (typeof this.$descDiv !== 'undefined') {
4466				// stylize descriptions based on user's caption prefs
4467				this.stylizeCaptions(this.$descDiv);
4468			}
4469		}
4470
4471		// combine left and right controls arrays for future reference
4472		this.controls = [];
4473		for (var sec in controlLayout) if (controlLayout.hasOwnProperty(sec)) {
4474			this.controls = this.controls.concat(controlLayout[sec]);
4475		}
4476
4477		// Update state-based display of controls.
4478		this.refreshControls('init');
4479	};
4480
4481	AblePlayer.prototype.useSvg = function () {
4482
4483		// Modified from IcoMoon.io svgxuse
4484		// @copyright Copyright (c) 2016 IcoMoon.io
4485		// @license		Licensed under MIT license
4486		// See https://github.com/Keyamoon/svgxuse
4487		// @version		1.1.16
4488
4489		var cache = Object.create(null); // holds xhr objects to prevent multiple requests
4490		var checkUseElems,
4491				tid; // timeout id
4492		var debouncedCheck = function () {
4493			clearTimeout(tid);
4494			tid = setTimeout(checkUseElems, 100);
4495		};
4496		var unobserveChanges = function () {
4497			return;
4498		};
4499		var observeChanges = function () {
4500			var observer;
4501			window.addEventListener('resize', debouncedCheck, false);
4502			window.addEventListener('orientationchange', debouncedCheck, false);
4503			if (window.MutationObserver) {
4504				observer = new MutationObserver(debouncedCheck);
4505				observer.observe(document.documentElement, {
4506					childList: true,
4507					subtree: true,
4508					attributes: true
4509				});
4510				unobserveChanges = function () {
4511					try {
4512						observer.disconnect();
4513						window.removeEventListener('resize', debouncedCheck, false);
4514						window.removeEventListener('orientationchange', debouncedCheck, false);
4515					} catch (ignore) {}
4516				};
4517			}
4518			else {
4519				document.documentElement.addEventListener('DOMSubtreeModified', debouncedCheck, false);
4520				unobserveChanges = function () {
4521					document.documentElement.removeEventListener('DOMSubtreeModified', debouncedCheck, false);
4522					window.removeEventListener('resize', debouncedCheck, false);
4523					window.removeEventListener('orientationchange', debouncedCheck, false);
4524				};
4525			}
4526		};
4527		var xlinkNS = 'http://www.w3.org/1999/xlink';
4528		checkUseElems = function () {
4529			var base,
4530					bcr,
4531					fallback = '', // optional fallback URL in case no base path to SVG file was given and no symbol definition was found.
4532					hash,
4533					i,
4534					Request,
4535					inProgressCount = 0,
4536					isHidden,
4537					url,
4538					uses,
4539					xhr;
4540			if (window.XMLHttpRequest) {
4541				Request = new XMLHttpRequest();
4542				if (Request.withCredentials !== undefined) {
4543					Request = XMLHttpRequest;
4544				}
4545				else {
4546					Request = XDomainRequest || undefined;
4547				}
4548			}
4549			if (Request === undefined) {
4550				return;
4551			}
4552			function observeIfDone() {
4553				// If done with making changes, start watching for chagnes in DOM again
4554				inProgressCount -= 1;
4555				if (inProgressCount === 0) { // if all xhrs were resolved
4556					observeChanges(); // watch for changes to DOM
4557				}
4558			}
4559			function attrUpdateFunc(spec) {
4560				return function () {
4561					if (cache[spec.base] !== true) {
4562						spec.useEl.setAttributeNS(xlinkNS, 'xlink:href', '#' + spec.hash);
4563					}
4564				};
4565			}
4566			function onloadFunc(xhr) {
4567				return function () {
4568					var body = document.body;
4569					var x = document.createElement('x');
4570					var svg;
4571					xhr.onload = null;
4572					x.innerHTML = xhr.responseText;
4573					svg = x.getElementsByTagName('svg')[0];
4574					if (svg) {
4575						svg.setAttribute('aria-hidden', 'true');
4576						svg.style.position = 'absolute';
4577						svg.style.width = 0;
4578						svg.style.height = 0;
4579						svg.style.overflow = 'hidden';
4580						body.insertBefore(svg, body.firstChild);
4581					}
4582					observeIfDone();
4583				};
4584			}
4585			function onErrorTimeout(xhr) {
4586				return function () {
4587					xhr.onerror = null;
4588					xhr.ontimeout = null;
4589					observeIfDone();
4590				};
4591			}
4592			unobserveChanges(); // stop watching for changes to DOM
4593			// find all use elements
4594			uses = document.getElementsByTagName('use');
4595			for (i = 0; i < uses.length; i += 1) {
4596				try {
4597					bcr = uses[i].getBoundingClientRect();
4598				} catch (ignore) {
4599					// failed to get bounding rectangle of the use element
4600					bcr = false;
4601				}
4602				url = uses[i].getAttributeNS(xlinkNS, 'href').split('#');
4603				base = url[0];
4604				hash = url[1];
4605				isHidden = bcr && bcr.left === 0 && bcr.right === 0 && bcr.top === 0 && bcr.bottom === 0;
4606				if (bcr && bcr.width === 0 && bcr.height === 0 && !isHidden) {
4607					// the use element is empty
4608					// if there is a reference to an external SVG, try to fetch it
4609					// use the optional fallback URL if there is no reference to an external SVG
4610					if (fallback && !base.length && hash && !document.getElementById(hash)) {
4611						base = fallback;
4612					}
4613					if (base.length) {
4614						// schedule updating xlink:href
4615						xhr = cache[base];
4616						if (xhr !== true) {
4617							// true signifies that prepending the SVG was not required
4618							setTimeout(attrUpdateFunc({
4619								useEl: uses[i],
4620								base: base,
4621								hash: hash
4622							}), 0);
4623						}
4624						if (xhr === undefined) {
4625							xhr = new Request();
4626							cache[base] = xhr;
4627							xhr.onload = onloadFunc(xhr);
4628							xhr.onerror = onErrorTimeout(xhr);
4629							xhr.ontimeout = onErrorTimeout(xhr);
4630							xhr.open('GET', base);
4631							xhr.send();
4632							inProgressCount += 1;
4633						}
4634					}
4635				}
4636				else {
4637					if (!isHidden) {
4638						if (cache[base] === undefined) {
4639							// remember this URL if the use element was not empty and no request was sent
4640							cache[base] = true;
4641						}
4642						else if (cache[base].onload) {
4643							// if it turns out that prepending the SVG is not necessary,
4644							// abort the in-progress xhr.
4645							cache[base].abort();
4646							cache[base].onload = undefined;
4647							cache[base] = true;
4648						}
4649					}
4650				}
4651			}
4652			uses = '';
4653			inProgressCount += 1;
4654			observeIfDone();
4655		};
4656/*
4657		// The load event fires when all resources have finished loading, which allows detecting whether SVG use elements are empty.
4658		window.addEventListener('load', function winLoad() {
4659			window.removeEventListener('load', winLoad, false); // to prevent memory leaks
4660			tid = setTimeout(checkUseElems, 0);
4661		}, false);
4662*/
4663	};
4664
4665	AblePlayer.prototype.cuePlaylistItem = function(sourceIndex) {
4666
4667		// Move to a new item in a playlist.
4668		// NOTE: Swapping source for audio description is handled elsewhere;
4669		// see description.js > swapDescription()
4670
4671		/*
4672			// Decided against preventing a reload of the current item in the playlist.
4673			// If it's clickable, users should be able to click on it and expect something to happen.
4674			// Leaving here though in case it's determined to be desirable.
4675		if (sourceIndex === this.playlistItemIndex) {
4676			// user has requested the item that's currently playing
4677			// just ignore the request
4678			return;
4679		}
4680		this.playlistItemIndex = sourceIndex;
4681		*/
4682
4683		var $newItem, prevPlayer, newPlayer, itemTitle, itemLang, sources, s, i, $newSource, nowPlayingSpan;
4684
4685		var thisObj = this;
4686
4687		prevPlayer = this.player;
4688
4689		if (this.initializing) { // this is the first track - user hasn't pressed play yet
4690			// do nothing.
4691		}
4692		else {
4693				if (this.playerCreated) {
4694				// remove the old
4695				this.deletePlayer();
4696			}
4697		}
4698
4699		// Determine appropriate player to play this media
4700		$newItem = this.$playlist.eq(sourceIndex);
4701		if (this.hasAttr($newItem,'data-youtube-id')) {
4702			this.youTubeId = $newItem.attr('data-youtube-id');
4703			newPlayer = 'youtube';
4704		}
4705		else {
4706				newPlayer = 'html5';
4707		}
4708
4709		if (newPlayer === 'youtube') {
4710			if (prevPlayer === 'html5') {
4711				// pause and hide the previous media
4712				if (this.playing) {
4713					this.pauseMedia();
4714				}
4715				this.$media.hide();
4716			}
4717		}
4718		else {
4719				// the new player is not youtube
4720			this.youTubeId = false;
4721			if (prevPlayer === 'youtube') {
4722				// unhide the media element
4723				this.$media.show();
4724			}
4725		}
4726		this.player = newPlayer;
4727
4728		// set swappingSrc; needs to be true within recreatePlayer(), called below
4729		this.swappingSrc = true;
4730
4731		// transfer media attributes from playlist to media element
4732		if (this.hasAttr($newItem,'data-poster')) {
4733			this.$media.attr('poster',$newItem.attr('data-poster'));
4734		}
4735		if (this.hasAttr($newItem,'data-width')) {
4736			this.$media.attr('width',$newItem.attr('data-width'));
4737		}
4738		if (this.hasAttr($newItem,'data-height')) {
4739			this.$media.attr('height',$newItem.attr('data-height'));
4740		}
4741		if (this.hasAttr($newItem,'data-youtube-desc-id')) {
4742			this.$media.attr('data-youtube-desc-id',$newItem.attr('data-youtube-desc-id'));
4743		}
4744		if (this.youTubeId) {
4745			this.$media.attr('data-youtube-id',$newItem.attr('data-youtube-id'));
4746		}
4747
4748		// add new <source> elements from playlist data
4749		var $sourceSpans = $newItem.children('span.able-source');
4750		if ($sourceSpans.length) {
4751			$sourceSpans.each(function() {
4752				if (thisObj.hasAttr($(this),'data-src')) {
4753					// this is the only required attribute
4754					var $newSource = $('<source>',{
4755						'src': $(this).attr('data-src')
4756					});
4757					if (thisObj.hasAttr($(this),'data-type')) {
4758						$newSource.attr('type',$(this).attr('data-type'));
4759					}
4760					if (thisObj.hasAttr($(this),'data-desc-src')) {
4761						$newSource.attr('data-desc-src',$(this).attr('data-desc-src'));
4762					}
4763					if (thisObj.hasAttr($(this),'data-sign-src')) {
4764						$newSource.attr('data-sign-src',$(this).attr('data-sign-src'));
4765					}
4766					thisObj.$media.append($newSource);
4767				}
4768			});
4769		}
4770
4771		// add new <track> elements from playlist data
4772		var $trackSpans = $newItem.children('span.able-track');
4773		if ($trackSpans.length) {
4774			 // for each element in $trackSpans, create a new <track> element
4775			$trackSpans.each(function() {
4776				if (thisObj.hasAttr($(this),'data-src') &&
4777					thisObj.hasAttr($(this),'data-kind') &&
4778					thisObj.hasAttr($(this),'data-srclang')) {
4779					// all required attributes are present
4780					var $newTrack = $('<track>',{
4781						'src': $(this).attr('data-src'),
4782						'kind': $(this).attr('data-kind'),
4783						'srclang': $(this).attr('data-srclang')
4784					});
4785					if (thisObj.hasAttr($(this),'data-label')) {
4786						$newTrack.attr('label',$(this).attr('data-label'));
4787					}
4788					thisObj.$media.append($newTrack);
4789				}
4790			});
4791		}
4792
4793		itemTitle = $newItem.text();
4794		if (this.hasAttr($newItem,'lang')) {
4795			itemLang = $newItem.attr('lang');
4796		}
4797		// Update relevant arrays
4798		this.$sources = this.$media.find('source');
4799
4800		// recreate player, informed by new attributes and track elements
4801		this.recreatePlayer();
4802
4803		// update playlist to indicate which item is playing
4804		//$('.able-playlist li').removeClass('able-current');
4805		this.$playlist.removeClass('able-current');
4806		this.$playlist.eq(sourceIndex).addClass('able-current');
4807
4808		// update Now Playing div
4809		if (this.showNowPlaying === true) {
4810			if (typeof this.$nowPlayingDiv !== 'undefined') {
4811				nowPlayingSpan = $('<span>');
4812				if (typeof itemLang !== 'undefined') {
4813					nowPlayingSpan.attr('lang',itemLang);
4814				}
4815				nowPlayingSpan.html('<span>' + this.tt.selectedTrack + ':</span>' + itemTitle);
4816				this.$nowPlayingDiv.html(nowPlayingSpan);
4817			}
4818		}
4819
4820		// if this.swappingSrc is true, media will autoplay when ready
4821		if (this.initializing) { // this is the first track - user hasn't pressed play yet
4822			this.swappingSrc = false;
4823		}
4824		else {
4825			this.swappingSrc = true;
4826			if (this.player === 'html5') {
4827				this.media.load();
4828			}
4829			else if (this.player === 'youtube') {
4830				this.okToPlay = true;
4831			}
4832		}
4833	};
4834
4835	AblePlayer.prototype.deletePlayer = function() {
4836
4837		// remove previous video's attributes and child elements from media element
4838		if (this.player == 'youtube') {
4839				var $youTubeIframe = this.$mediaContainer.find('iframe');
4840				$youTubeIframe.remove();
4841		}
4842		this.$media.removeAttr('poster width height');
4843		this.$media.empty();
4844
4845		// Empty elements that will be rebuilt
4846		this.$controllerDiv.empty();
4847		// this.$statusBarDiv.empty();
4848		// this.$timer.empty();
4849		this.$elapsedTimeContainer.empty().text('0:00'); // span.able-elapsedTime
4850		this.$durationContainer.empty(); // span.able-duration
4851
4852		// Remove popup windows and modal dialogs; these too will be rebuilt
4853		if (this.$signWindow) {
4854				this.$signWindow.remove();
4855		}
4856		if (this.$transcriptArea) {
4857				this.$transcriptArea.remove();
4858		}
4859		$('.able-modal-dialog').remove();
4860
4861		// reset key variables
4862		this.hasCaptions = false;
4863		this.hasChapters = false;
4864		this.captionsPopup = null;
4865		this.chaptersPopup = null;
4866	};
4867
4868	AblePlayer.prototype.getButtonTitle = function(control) {
4869
4870		if (control === 'playpause') {
4871			return this.tt.play;
4872		}
4873		else if (control === 'play') {
4874			return this.tt.play;
4875		}
4876		else if (control === 'pause') {
4877			return this.tt.pause;
4878		}
4879		else if (control === 'restart') {
4880			return this.tt.restart;
4881		}
4882		else if (control === 'previous') {
4883			return this.tt.prevTrack;
4884		}
4885		else if (control === 'next') {
4886			return this.tt.nextTrack;
4887		}
4888		else if (control === 'rewind') {
4889			return this.tt.rewind;
4890		}
4891		else if (control === 'forward') {
4892			return this.tt.forward;
4893		}
4894		else if (control === 'captions') {
4895			if (this.captions.length > 1) {
4896				return this.tt.captions;
4897			}
4898			else {
4899				if (this.captionsOn) {
4900					return this.tt.hideCaptions;
4901				}
4902				else {
4903					return this.tt.showCaptions;
4904				}
4905			}
4906		}
4907		else if (control === 'descriptions') {
4908			if (this.descOn) {
4909				return this.tt.turnOffDescriptions;
4910			}
4911			else {
4912				return this.tt.turnOnDescriptions;
4913			}
4914		}
4915		else if (control === 'transcript') {
4916			if (this.$transcriptDiv.is(':visible')) {
4917				return this.tt.hideTranscript;
4918			}
4919			else {
4920				return this.tt.showTranscript;
4921			}
4922		}
4923		else if (control === 'chapters') {
4924			return this.tt.chapters;
4925		}
4926		else if (control === 'sign') {
4927			return this.tt.sign;
4928		}
4929		else if (control === 'volume') {
4930			return this.tt.volume;
4931		}
4932		else if (control === 'faster') {
4933			return this.tt.faster;
4934		}
4935		else if (control === 'slower') {
4936			return this.tt.slower;
4937		}
4938		else if (control === 'preferences') {
4939			return this.tt.preferences;
4940		}
4941		else if (control === 'help') {
4942			// return this.tt.help;
4943		}
4944		else {
4945			// there should be no other controls, but just in case:
4946			// return the name of the control with first letter in upper case
4947			// ultimately will need to get a translated label from this.tt
4948			if (this.debug) {
4949				console.log('Found an untranslated label: ' + control);
4950			}
4951			return control.charAt(0).toUpperCase() + control.slice(1);
4952		}
4953	};
4954
4955
4956})(jQuery);
4957
4958(function ($) {
4959	// Loads files referenced in track elements, and performs appropriate setup.
4960	// For example, captions and text descriptions.
4961	// This will be called whenever the player is recreated.
4962	// Added in v2.2.23: Also handles YouTube caption tracks
4963
4964	AblePlayer.prototype.setupTracks = function() {
4965
4966		var thisObj, deferred, promise, loadingPromises, loadingPromise, i, tracks, track;
4967
4968		thisObj = this;
4969
4970		deferred = new $.Deferred();
4971		promise = deferred.promise();
4972
4973		loadingPromises = [];
4974
4975		this.captions = [];
4976		this.captionLabels = [];
4977		this.descriptions = [];
4978		this.chapters = [];
4979		this.meta = [];
4980
4981		if ($('#able-vts').length) {
4982			// Page includes a container for a VTS instance
4983			this.vtsTracks = [];
4984			this.hasVts = true;
4985		}
4986		else {
4987			this.hasVts = false;
4988		}
4989
4990		this.getTracks().then(function() {
4991
4992			tracks = thisObj.tracks;
4993
4994			if (thisObj.player === 'youtube') {
4995				// If captions have been loaded into the captions array (either from YouTube or a local source),
4996				// we no longer have a need to use YouTube captions
4997				// TODO: Consider whether this is the right place to make this decision
4998				// Probably better to make it when cues are identified from YouTube caption sources
4999				if (tracks.length) {
5000					thisObj.usingYouTubeCaptions = false;
5001				}
5002			}
5003
5004			for (i=0; i < tracks.length; i++) {
5005
5006				track = tracks[i];
5007
5008				var kind = track.kind;
5009				var trackLang = track.language;
5010				var trackLabel = track.label;
5011
5012				if (!track.src) {
5013					if (thisObj.usingYouTubeCaptions || thisObj.usingVimeoCaptions) {
5014						// skip all the hullabaloo and go straight to setupCaptions
5015						thisObj.setupCaptions(track,trackLang,trackLabel);
5016					}
5017					else {
5018						// Nothing to load!
5019						// Skip this track; move on to next i
5020					}
5021					continue;
5022				}
5023
5024				var trackSrc = track.src;
5025
5026				loadingPromise = thisObj.loadTextObject(trackSrc); // resolves with src, trackText
5027				loadingPromises.push(loadingPromise);
5028
5029				loadingPromise.then((function (track, kind) {
5030
5031					var trackSrc = track.src;
5032					var trackLang = track.language;
5033					var trackLabel = track.label;
5034
5035					return function (trackSrc, trackText) { // these are the two vars returned from loadTextObject
5036
5037						var trackContents = trackText;
5038						var cues = thisObj.parseWebVTT(trackSrc, trackContents).cues;
5039
5040						if (thisObj.hasVts) {
5041							 // setupVtsTracks() is in vts.js
5042							thisObj.setupVtsTracks(kind, trackLang, trackLabel, trackSrc, trackContents);
5043						}
5044
5045						if (kind === 'captions' || kind === 'subtitles') {
5046							thisObj.setupCaptions(track, trackLang, trackLabel, cues);
5047						}
5048						else if (kind === 'descriptions') {
5049							thisObj.setupDescriptions(track, cues, trackLang);
5050						}
5051						else if (kind === 'chapters') {
5052							thisObj.setupChapters(track, cues, trackLang);
5053						}
5054						else if (kind === 'metadata') {
5055							thisObj.setupMetadata(track, cues);
5056						}
5057					}
5058				})(track, kind));
5059			}
5060			$.when.apply($, loadingPromises).then(function () {
5061				deferred.resolve();
5062			});
5063		});
5064
5065		return promise;
5066	};
5067
5068	AblePlayer.prototype.getTracks = function() {
5069
5070		// define an array tracks with the following structure:
5071		// kind - string, e.g. "captions", "descriptions"
5072		// src - string, URL of WebVTT source file
5073		// language - string, lang code
5074		// label - string to display, e.g., in CC menu
5075		// def - Boolean, true if this is the default track
5076		// cues - array with startTime, endTime, and payload
5077
5078		var thisObj, deferred, promise, captionTracks, trackLang, trackLabel, isDefault;
5079
5080		thisObj = this;
5081
5082		deferred = new $.Deferred();
5083		promise = deferred.promise();
5084
5085		this.$tracks = this.$media.find('track');
5086		this.tracks = [];
5087
5088		if (this.$tracks.length) {
5089
5090			// create object from HTML5 tracks
5091			this.$tracks.each(function() {
5092
5093				// srcLang should always be included with <track>, but HTML5 spec doesn't require it
5094				// if not provided, assume track is the same language as the default player language
5095				if ($(this).attr('srclang')) {
5096					trackLang = $(this).attr('srclang');
5097				}
5098				else {
5099					trackLang = thisObj.lang;
5100				}
5101
5102				if ($(this).attr('label')) {
5103					trackLabel = $(this).attr('label');
5104				}
5105				else {
5106					trackLabel = thisObj.getLanguageName(trackLang);
5107				}
5108
5109				if ($(this).attr('default')) {
5110					isDefault = true;
5111				}
5112				else if (trackLang === thisObj.lang) {
5113					// There is no @default attribute,
5114					// but this is the user's/browser's default language
5115					// so make it the default caption track
5116					isDefault = true;
5117				}
5118				else {
5119					isDefault = false;
5120				}
5121
5122				if (isDefault) {
5123					// this.captionLang will also be the default language for non-caption tracks
5124					thisObj.captionLang = trackLang;
5125				}
5126
5127				thisObj.tracks.push({
5128					'kind': $(this).attr('kind'),
5129					'src': $(this).attr('src'),
5130					'language': trackLang,
5131					'label': trackLabel,
5132					'def': isDefault
5133				});
5134			});
5135		}
5136
5137		// check to see if any HTML caption or subitle tracks were found.
5138		captionTracks = this.$media.find('track[kind="captions"],track[kind="subtitles"]');
5139		if (captionTracks.length) {
5140			// HTML captions or subtitles were found. Use those.
5141			deferred.resolve();
5142		}
5143		else {
5144			// if this is a youtube or vimeo player, check there for captions/subtitles
5145			if (this.player === 'youtube') {
5146				this.getYouTubeCaptionTracks(this.youTubeId).then(function() {
5147					deferred.resolve();
5148				});
5149			}
5150			else if (this.player === 'vimeo') {
5151				this.getVimeoCaptionTracks().then(function() {
5152					deferred.resolve();
5153				});
5154			}
5155			else {
5156				// this is neither YouTube nor Vimeo
5157				// there just ain't no caption tracks
5158				deferred.resolve();
5159			}
5160		}
5161		return promise;
5162	};
5163
5164	AblePlayer.prototype.setupCaptions = function (track, trackLang, trackLabel, cues) {
5165
5166		var thisObj, inserted, i, capLabel;
5167
5168		thisObj = this;
5169
5170		if (typeof cues === 'undefined') {
5171			cues = null;
5172		}
5173
5174		this.hasCaptions = true;
5175
5176		// Remove 'default' attribute from all <track> elements
5177		// This data has already been saved to this.tracks
5178		// and some browsers will display the default captions, despite all standard efforts to suppress them
5179		this.$media.find('track').removeAttr('default');
5180
5181		// caption cues from WebVTT are used to build a transcript for both audio and video
5182		// but captions are currently only supported for video
5183		if (this.mediaType === 'video') {
5184
5185			if (!(this.usingYouTubeCaptions || this.usingVimeoCaptions)) {
5186				// create a pair of nested divs for displaying captions
5187				// includes aria-hidden="true" because otherwise
5188				// captions being added and removed causes sporadic changes to focus in JAWS
5189				// (not a problem in NVDA or VoiceOver)
5190				if (!this.$captionsDiv) {
5191					this.$captionsDiv = $('<div>',{
5192						'class': 'able-captions',
5193					});
5194					this.$captionsWrapper = $('<div>',{
5195						'class': 'able-captions-wrapper',
5196						'aria-hidden': 'true'
5197					}).hide();
5198					if (this.prefCaptionsPosition === 'below') {
5199						this.$captionsWrapper.addClass('able-captions-below');
5200					}
5201					else {
5202						this.$captionsWrapper.addClass('able-captions-overlay');
5203					}
5204					this.$captionsWrapper.append(this.$captionsDiv);
5205					this.$vidcapContainer.append(this.$captionsWrapper);
5206				}
5207			}
5208		}
5209
5210		this.currentCaption = -1;
5211		if (this.prefCaptions === 1) {
5212			// Captions default to on.
5213			this.captionsOn = true;
5214		}
5215		else {
5216			this.captionsOn = false;
5217		}
5218		if (this.captions.length === 0) { // this is the first
5219			this.captions.push({
5220				'cues': cues,
5221				'language': trackLang,
5222				'label': trackLabel,
5223				'def': track.def
5224			});
5225			this.captionLabels.push(trackLabel);
5226		}
5227		else { // there are already tracks in the array
5228			inserted = false;
5229			for (i = 0; i < this.captions.length; i++) {
5230				capLabel = this.captionLabels[i];
5231				if (trackLabel.toLowerCase() < this.captionLabels[i].toLowerCase()) {
5232					// insert before track i
5233					this.captions.splice(i,0,{
5234						'cues': cues,
5235						'language': trackLang,
5236						'label': trackLabel,
5237						'def': track.def
5238					});
5239					this.captionLabels.splice(i,0,trackLabel);
5240					inserted = true;
5241					break;
5242				}
5243			}
5244			if (!inserted) {
5245				// just add track to the end
5246				this.captions.push({
5247					'cues': cues,
5248					'language': trackLang,
5249					'label': trackLabel,
5250					'def': track.def
5251				});
5252				this.captionLabels.push(trackLabel);
5253			}
5254		}
5255	};
5256
5257	AblePlayer.prototype.setupDescriptions = function (track, cues, trackLang) {
5258
5259		// called via setupTracks() only if there is track with kind="descriptions"
5260		// prepares for delivery of text description , in case it's needed
5261		// whether and how it's delivered is controlled within description.js > initDescription()
5262
5263		this.hasClosedDesc = true;
5264		this.currentDescription = -1;
5265		this.descriptions.push({
5266			cues: cues,
5267			language: trackLang
5268		});
5269	};
5270
5271	AblePlayer.prototype.setupChapters = function (track, cues, trackLang) {
5272
5273		// NOTE: WebVTT supports nested timestamps (to form an outline)
5274		// This is not currently supported.
5275
5276		this.hasChapters = true;
5277
5278		this.chapters.push({
5279			cues: cues,
5280			language: trackLang
5281		});
5282	};
5283
5284	AblePlayer.prototype.setupMetadata = function(track, cues) {
5285
5286		if (this.metaType === 'text') {
5287			// Metadata is only supported if data-meta-div is provided
5288			// The player does not display metadata internally
5289			if (this.metaDiv) {
5290				if ($('#' + this.metaDiv)) {
5291					// container exists
5292					this.$metaDiv = $('#' + this.metaDiv);
5293					this.hasMeta = true;
5294					this.meta = cues;
5295				}
5296			}
5297		}
5298		else if (this.metaType === 'selector') {
5299			this.hasMeta = true;
5300			this.visibleSelectors = [];
5301			this.meta = cues;
5302		}
5303	};
5304
5305	AblePlayer.prototype.loadTextObject = function(src) {
5306
5307// TODO: Incorporate the following function, moved from setupTracks()
5308// convert XMl/TTML captions file
5309/*
5310if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('<?xml'))) {
5311	trackContents = thisObj.ttml2webvtt(trackText);
5312}
5313*/
5314		var deferred, promise, thisObj, $tempDiv;
5315
5316		deferred = new $.Deferred();
5317		promise = deferred.promise();
5318		thisObj = this;
5319
5320		// create a temp div for holding data
5321		$tempDiv = $('<div>',{
5322			style: 'display:none'
5323		});
5324		$tempDiv.load(src, function (trackText, status, req) {
5325			if (status === 'error') {
5326				if (thisObj.debug) {
5327					console.log ('error reading file ' + src + ': ' + status);
5328				}
5329				deferred.fail();
5330			}
5331			else {
5332				deferred.resolve(src, trackText);
5333			}
5334			$tempDiv.remove();
5335		});
5336		return promise;
5337	};
5338
5339	AblePlayer.prototype.setupAltCaptions = function() {
5340
5341		// setup captions from an alternative source (not <track> elements)
5342		// only do this if no <track> captions are provided
5343		// currently supports: YouTube, Vimeo
5344		var deferred = new $.Deferred();
5345		var promise = deferred.promise();
5346		if (this.captions.length === 0) {
5347			if (this.player === 'youtube' && this.usingYouTubeCaptions) {
5348				this.setupYouTubeCaptions().done(function() {
5349					deferred.resolve();
5350				});
5351			}
5352			else if (this.player === 'vimeo' && this.usingVimeoCaptions) {
5353				this.setupVimeoCaptions().done(function() {
5354					deferred.resolve();
5355				});
5356			}
5357
5358			else {
5359				// repeat for other alt sources once supported (e.g., Vimeo, DailyMotion)
5360				deferred.resolve();
5361			}
5362		}
5363		else { // there are <track> captions, so no need for alt source captions
5364			deferred.resolve();
5365		}
5366		return promise;
5367	};
5368
5369})(jQuery);
5370
5371
5372(function ($) {
5373	AblePlayer.prototype.initYouTubePlayer = function () {
5374
5375		var thisObj, deferred, promise, youTubeId, googleApiPromise, json;
5376		thisObj = this;
5377
5378		deferred = new $.Deferred();
5379		promise = deferred.promise();
5380
5381		// if a described version is available && user prefers desription
5382		// init player using the described version
5383		if (this.youTubeDescId && this.prefDesc) {
5384			youTubeId = this.youTubeDescId;
5385		}
5386		else {
5387			youTubeId = this.youTubeId;
5388		}
5389		this.activeYouTubeId = youTubeId;
5390		if (AblePlayer.youtubeIframeAPIReady) {
5391			// Script already loaded and ready.
5392			this.finalizeYoutubeInit().then(function() {
5393				deferred.resolve();
5394			});
5395		}
5396		else {
5397			// Has another player already started loading the script? If so, abort...
5398			if (!AblePlayer.loadingYoutubeIframeAPI) {
5399				$.getScript('https://www.youtube.com/iframe_api').fail(function () {
5400					deferred.fail();
5401				});
5402			}
5403
5404			// Otherwise, keeping waiting for script load event...
5405			$('body').on('youtubeIframeAPIReady', function () {
5406				thisObj.finalizeYoutubeInit().then(function() {
5407					deferred.resolve();
5408				});
5409			});
5410		}
5411		return promise;
5412	};
5413
5414	AblePlayer.prototype.finalizeYoutubeInit = function () {
5415
5416		// This is called once we're sure the Youtube iFrame API is loaded -- see above
5417		var deferred, promise, thisObj, containerId, ccLoadPolicy, videoDimensions, autoplay;
5418
5419		deferred = new $.Deferred();
5420		promise = deferred.promise();
5421
5422		thisObj = this;
5423
5424		containerId = this.mediaId + '_youtube';
5425
5426		this.$mediaContainer.prepend($('<div>').attr('id', containerId));
5427		// NOTE: Tried the following in place of the above in January 2016
5428		// because in some cases two videos were being added to the DOM
5429		// However, once v2.2.23 was fairly stable, unable to reproduce that problem
5430		// so maybe it's not an issue. This is preserved here temporarily, just in case it's needed...
5431		// thisObj.$mediaContainer.html($('<div>').attr('id', containerId));
5432
5433		// cc_load_policy:
5434		// 0 - show captions depending on user's preference on YouTube
5435		// 1 - show captions by default, even if the user has turned them off
5436		// For Able Player, init player with value of 0
5437		// and will turn them on or off after player is initialized
5438		// based on availability of local tracks and user's Able Player prefs
5439		ccLoadPolicy = 0;
5440
5441		videoDimensions = this.getYouTubeDimensions(this.activeYouTubeId, containerId);
5442		if (videoDimensions) {
5443			this.ytWidth = videoDimensions[0];
5444			this.ytHeight = videoDimensions[1];
5445			this.aspectRatio = thisObj.ytWidth / thisObj.ytHeight;
5446		}
5447		else {
5448			// dimensions are initially unknown
5449			// sending null values to YouTube results in a video that uses the default YouTube dimensions
5450			// these can then be scraped from the iframe and applied to this.$ableWrapper
5451			this.ytWidth = null;
5452			this.ytHeight = null;
5453		}
5454
5455		if (this.okToPlay) {
5456			autoplay = 1;
5457		}
5458		else {
5459			autoplay = 0;
5460		}
5461
5462		// NOTE: YouTube is changing the following parameters on or after Sep 25, 2018:
5463		// rel - No longer able to prevent YouTube from showing related videos
5464		//			value of 0 now limits related videos to video's same channel
5465		// showinfo - No longer supported (previously, value of 0 hid title, share, & watch later buttons
5466		// Documentation https://developers.google.com/youtube/player_parameters
5467
5468		this.youTubePlayer = new YT.Player(containerId, {
5469			videoId: this.activeYouTubeId,
5470			host: this.youTubeNoCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com',
5471			width: this.ytWidth,
5472			height: this.ytHeight,
5473			playerVars: {
5474				autoplay: autoplay,
5475				enablejsapi: 1,
5476				disableKb: 1, // disable keyboard shortcuts, using our own
5477				playsinline: this.playsInline,
5478				start: this.startTime,
5479				controls: 0, // no controls, using our own
5480				cc_load_policy: ccLoadPolicy,
5481				hl: this.lang, // use the default language UI
5482				modestbranding: 1, // no YouTube logo in controller
5483				rel: 0, // do not show related videos when video ends
5484				html5: 1, // force html5 if browser supports it (undocumented parameter; 0 does NOT force Flash)
5485				iv_load_policy: 3 // do not show video annotations
5486			},
5487			events: {
5488				onReady: function () {
5489					if (thisObj.swappingSrc) {
5490						// swap is now complete
5491						thisObj.swappingSrc = false;
5492						thisObj.cueingPlaylistItem = false;
5493						if (thisObj.playing) {
5494							// resume playing
5495							thisObj.playMedia();
5496						}
5497					}
5498					if (thisObj.userClickedPlaylist) {
5499						thisObj.userClickedPlaylist = false; // reset
5500					}
5501					if (typeof thisObj.aspectRatio === 'undefined') {
5502						thisObj.resizeYouTubePlayer(thisObj.activeYouTubeId, containerId);
5503					}
5504					deferred.resolve();
5505				},
5506				onError: function (x) {
5507					deferred.fail();
5508				},
5509				onStateChange: function (x) {
5510					thisObj.getPlayerState().then(function(playerState) {
5511						// values of playerState: 'playing','paused','buffering','ended'
5512						if (playerState === 'playing') {
5513							thisObj.playing = true;
5514							thisObj.startedPlaying = true;
5515							thisObj.paused = false;
5516						}
5517						else if (playerState == 'ended') {
5518							thisObj.onMediaComplete();
5519						}
5520						else {
5521							thisObj.playing = false;
5522							thisObj.paused = true;
5523						}
5524						if (thisObj.stoppingYouTube && playerState === 'paused') {
5525							if (typeof thisObj.$posterImg !== 'undefined') {
5526								thisObj.$posterImg.show();
5527							}
5528							thisObj.stoppingYouTube = false;
5529							thisObj.seeking = false;
5530							thisObj.playing = false;
5531							thisObj.paused = true;
5532						}
5533					});
5534				},
5535				onPlaybackQualityChange: function () {
5536					// do something
5537				},
5538				onApiChange: function (x) {
5539					// As of Able Player v2.2.23, we are now getting caption data via the YouTube Data API
5540					// prior to calling initYouTubePlayer()
5541					// Previously we got caption data via the YouTube iFrame API, and doing so was an awful mess.
5542					// onApiChange fires to indicate that the player has loaded (or unloaded) a module with exposed API methods
5543					// it isn't fired until the video starts playing
5544					// if captions are available for this video (automated captions don't count)
5545					// the 'captions' (or 'cc') module is loaded. If no captions are available, this event never fires
5546					// So, to trigger this event we had to play the video briefly, then pause, then reset.
5547					// During that brief moment of playback, the onApiChange event was fired and we could setup captions
5548					// The 'captions' and 'cc' modules are very different, and have different data and methods
5549					// NOW, in v2.2.23, we still need to initialize the caption modules in order to control captions
5550					// but we don't have to do that on load in order to get caption data
5551					// Instead, we can wait until the video starts playing normally, then retrieve the modules
5552					thisObj.initYouTubeCaptionModule();
5553				}
5554			}
5555		});
5556
5557		this.injectPoster(this.$mediaContainer, 'youtube');
5558		if (!this.hasPlaylist) {
5559			// remove the media element, since YouTube replaces that with its own element in an iframe
5560			// this is handled differently for playlists. See buildplayer.js > cuePlaylistItem()
5561			this.$media.remove();
5562		}
5563		return promise;
5564	};
5565
5566	AblePlayer.prototype.getYouTubeDimensions = function (youTubeContainerId) {
5567
5568		// get dimensions of YouTube video, return array with width & height
5569		// Sources, in order of priority:
5570		// 1. The width and height attributes on <video>
5571		// 2. YouTube (not yet supported; can't seem to get this data via YouTube Data API without OAuth!)
5572
5573		var d, url, $iframe, width, height;
5574
5575		d = [];
5576
5577		if (typeof this.playerMaxWidth !== 'undefined') {
5578			d[0] = this.playerMaxWidth;
5579			// optional: set height as well; not required though since YouTube will adjust height to match width
5580			if (typeof this.playerMaxHeight !== 'undefined') {
5581				d[1] = this.playerMaxHeight;
5582			}
5583			return d;
5584		}
5585		else {
5586			if (typeof $('#' + youTubeContainerId) !== 'undefined') {
5587				$iframe = $('#' + youTubeContainerId);
5588				width = $iframe.width();
5589				height = $iframe.height();
5590				if (width > 0 && height > 0) {
5591					d[0] = width;
5592					d[1] = height;
5593					return d;
5594				}
5595			}
5596		}
5597		return false;
5598	};
5599
5600	AblePlayer.prototype.resizeYouTubePlayer = function(youTubeId, youTubeContainerId) {
5601
5602		// called after player is ready, if youTube dimensions were previously unknown
5603		// Now need to get them from the iframe element that YouTube injected
5604		// and resize Able Player to match
5605		var d, width, height;
5606		if (typeof this.aspectRatio !== 'undefined') {
5607			// video dimensions have already been collected
5608			if (this.restoringAfterFullScreen) {
5609				// restore using saved values
5610				if (this.youTubePlayer) {
5611					this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
5612				}
5613				this.restoringAfterFullScreen = false;
5614			}
5615			else {
5616				// recalculate with new wrapper size
5617				width = this.$ableWrapper.parent().width();
5618				height = Math.round(width / this.aspectRatio);
5619				this.$ableWrapper.css({
5620					'max-width': width + 'px',
5621					'width': ''
5622				});
5623				this.youTubePlayer.setSize(width, height);
5624				if (this.fullscreen) {
5625					this.youTubePlayer.setSize(width, height);
5626				}
5627				else {
5628					// resizing due to a change in window size, not full screen
5629					this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
5630				}
5631			}
5632		}
5633		else {
5634			d = this.getYouTubeDimensions(youTubeContainerId);
5635			if (d) {
5636				width = d[0];
5637				height = d[1];
5638				if (width > 0 && height > 0) {
5639					this.aspectRatio = width / height;
5640					this.ytWidth = width;
5641					this.ytHeight = height;
5642					if (width !== this.$ableWrapper.width()) {
5643						// now that we've retrieved YouTube's default width,
5644						// need to adjust to fit the current player wrapper
5645						width = this.$ableWrapper.width();
5646						height = Math.round(width / this.aspectRatio);
5647						if (this.youTubePlayer) {
5648							this.youTubePlayer.setSize(width, height);
5649						}
5650					}
5651				}
5652			}
5653		}
5654	};
5655
5656	AblePlayer.prototype.setupYouTubeCaptions = function () {
5657
5658		// called from setupAltCaptions if player is YouTube and there are no <track> captions
5659
5660		// use YouTube Data API to get caption data from YouTube
5661		// function is called only if these conditions are met:
5662		// 1. this.player === 'youtube'
5663		// 2. there are no <track> elements with kind="captions"
5664		// 3. youTubeDataApiKey is defined
5665
5666		var deferred = new $.Deferred();
5667		var promise = deferred.promise();
5668
5669		var thisObj, googleApiPromise, youTubeId, i;
5670
5671		thisObj = this;
5672
5673		// if a described version is available && user prefers desription
5674		// Use the described version, and get its captions
5675		if (this.youTubeDescId && this.prefDesc) {
5676			youTubeId = this.youTubeDescId;
5677		}
5678		else {
5679			youTubeId = this.youTubeId;
5680		}
5681
5682		if (typeof youTubeDataAPIKey !== 'undefined') {
5683			// Wait until Google Client API is loaded
5684			// When loaded, it sets global var googleApiReady to true
5685
5686			// Thanks to Paul Tavares for $.doWhen()
5687			// https://gist.github.com/purtuga/8257269
5688			$.doWhen({
5689				when: function(){
5690					return googleApiReady;
5691				},
5692				interval: 100, // ms
5693				attempts: 1000
5694			})
5695			.done(function(){
5696					deferred.resolve();
5697			})
5698			.fail(function(){
5699				console.log('Unable to initialize Google API. YouTube captions are currently unavailable.');
5700			});
5701		}
5702		else {
5703			deferred.resolve();
5704		}
5705		return promise;
5706	};
5707
5708	AblePlayer.prototype.waitForGapi = function () {
5709
5710		// wait for Google API to initialize
5711
5712		var thisObj, deferred, promise, maxWaitTime, maxTries, tries, timer, interval;
5713
5714		thisObj = this;
5715		deferred = new $.Deferred();
5716		promise = deferred.promise();
5717		maxWaitTime = 5000; // 5 seconds
5718		maxTries = 100; // number of tries during maxWaitTime
5719		tries = 0;
5720		interval = Math.floor(maxWaitTime/maxTries);
5721
5722		timer = setInterval(function() {
5723			tries++;
5724			if (googleApiReady || tries >= maxTries) {
5725				clearInterval(timer);
5726				if (googleApiReady) { // success!
5727					deferred.resolve(true);
5728				}
5729				else { // tired of waiting
5730					deferred.resolve(false);
5731				}
5732			}
5733			else {
5734				thisObj.waitForGapi();
5735			}
5736		}, interval);
5737		return promise;
5738	};
5739
5740	AblePlayer.prototype.getYouTubeCaptionTracks = function (youTubeId) {
5741
5742		// get data via YouTube Data API, and push data to this.captions
5743		var deferred = new $.Deferred();
5744		var promise = deferred.promise();
5745
5746		var thisObj, useGoogleApi, i, trackId, trackLang, trackName, trackLabel, trackKind, isDraft, isDefaultTrack;
5747
5748		thisObj = this;
5749
5750		if (typeof youTubeDataAPIKey !== 'undefined') {
5751			this.waitForGapi().then(function(waitResult) {
5752
5753				useGoogleApi = waitResult;
5754
5755				// useGoogleApi returns false if API failed to initalize after max wait time
5756				// Proceed only if true. Otherwise can still use fallback method (see else loop below)
5757				if (useGoogleApi === true) {
5758					gapi.client.setApiKey(youTubeDataAPIKey);
5759					gapi.client
5760						.load('youtube', 'v3')
5761						.then(function() {
5762							var request = gapi.client.youtube.captions.list({
5763								'part': 'id, snippet',
5764								'videoId': youTubeId
5765							});
5766							request.then(function(json) {
5767								if (json.result.items.length) { // video has captions!
5768									thisObj.hasCaptions = true;
5769									thisObj.usingYouTubeCaptions = true;
5770									if (thisObj.prefCaptions === 1) {
5771										thisObj.captionsOn = true;
5772									}
5773									else {
5774										thisObj.captionsOn = false;
5775									}
5776									// Step through results and add them to cues array
5777									for (i=0; i < json.result.items.length; i++) {
5778										trackName = json.result.items[i].snippet.name; // usually seems to be empty
5779										trackLang = json.result.items[i].snippet.language;
5780										trackKind = json.result.items[i].snippet.trackKind; // ASR, standard, forced
5781										isDraft = json.result.items[i].snippet.isDraft; // Boolean
5782										// Other variables that could potentially be collected from snippet:
5783										// isCC - Boolean, always seems to be false
5784										// isLarge - Boolean
5785										// isEasyReader - Boolean
5786										// isAutoSynced	 Boolean
5787										// status - string, always seems to be "serving"
5788
5789										var srcUrl = thisObj.getYouTubeTimedTextUrl(youTubeId,trackName,trackLang);
5790										if (trackKind !== 'ASR' && !isDraft) {
5791
5792											if (trackName !== '') {
5793												 trackLabel = trackName;
5794											}
5795											else {
5796												 // if track name is empty (it always seems to be), assign a label based on trackLang
5797												 trackLabel = thisObj.getLanguageName(trackLang);
5798											}
5799
5800											// assign the default track based on language of the player
5801											if (trackLang === thisObj.lang) {
5802												isDefaultTrack = true;
5803											}
5804											else {
5805												isDefaultTrack = false;
5806											}
5807											thisObj.tracks.push({
5808												'kind': 'captions',
5809												'src': srcUrl,
5810												'language': trackLang,
5811												'label': trackLabel,
5812												'def': isDefaultTrack
5813											});
5814										}
5815									}
5816									// setupPopups again with new captions array, replacing original
5817									thisObj.setupPopups('captions');
5818									deferred.resolve();
5819								}
5820								else {
5821									thisObj.hasCaptions = false;
5822									thisObj.usingYouTubeCaptions = false;
5823									deferred.resolve();
5824								}
5825							}, function (reason) {
5826								// If video has no captions, YouTube returns an error.
5827								// Should still proceed, but with captions disabled
5828								// The specific error, if needed: reason.result.error.message
5829								// If no captions, the error is: "The video identified by the <code>videoId</code> parameter could not be found."
5830								console.log('Error retrieving captions.');
5831								console.log('Check your video on YouTube to be sure captions are available and published.');
5832								thisObj.hasCaptions = false;
5833								thisObj.usingYouTubeCaptions = false;
5834								deferred.resolve();
5835							});
5836						})
5837				}
5838				else {
5839					// googleAPi never loaded.
5840					this.getYouTubeCaptionTracks2(youTubeId).then(function() {
5841						deferred.resolve();
5842					});
5843				}
5844			});
5845		}
5846		else {
5847			// web owner hasn't provided a Google API key
5848			// attempt to get YouTube captions via the backup method
5849			this.getYouTubeCaptionTracks2(youTubeId).then(function() {
5850				deferred.resolve();
5851			});
5852		}
5853		return promise;
5854	};
5855
5856	AblePlayer.prototype.getYouTubeCaptionTracks2 = function (youTubeId) {
5857
5858	 	// Use alternative backup method of getting caption tracks from YouTube
5859	 	// and pushing them to this.captions
5860	 	// Called from getYouTubeCaptionTracks if no Google API key is defined
5861	 	// or if Google API failed to initiatlize
5862	 	// This method seems to be undocumented, but is referenced on StackOverflow
5863	 	// We'll use that as a fallback but it could break at any moment
5864
5865		var deferred = new $.Deferred();
5866		var promise = deferred.promise();
5867
5868		var thisObj, useGoogleApi, i, trackId, trackLang, trackName, trackLabel, trackKind, isDraft, isDefaultTrack;
5869
5870		thisObj = this;
5871
5872		$.ajax({
5873			type: 'get',
5874			url: 'https://www.youtube.com/api/timedtext?type=list&v=' + youTubeId,
5875			dataType: 'xml',
5876			success: function(xml) {
5877				var $tracks = $(xml).find('track');
5878				if ($tracks.length > 0) {	 // video has captions!
5879					thisObj.hasCaptions = true;
5880					thisObj.usingYouTubeCaptions = true;
5881					if (thisObj.prefCaptions === 1) {
5882						thisObj.captionsOn = true;
5883					}
5884					else {
5885						thisObj.captionsOn = false;
5886					}
5887					// Step through results and add them to tracks array
5888					$tracks.each(function() {
5889						trackId = $(this).attr('id');
5890						trackLang = $(this).attr('lang_code');
5891						if ($(this).attr('name') !== '') {
5892							trackName = $(this).attr('name');
5893							trackLabel = trackName;
5894						}
5895						else {
5896							// @name is typically null except for default track
5897							// but lang_translated seems to be reliable
5898							trackName = '';
5899							trackLabel = $(this).attr('lang_translated');
5900						}
5901						if (trackLabel === '') {
5902							trackLabel = thisObj.getLanguageName(trackLang);
5903						}
5904						// assign the default track based on language of the player
5905						if (trackLang === thisObj.lang) {
5906							isDefaultTrack = true;
5907						}
5908						else {
5909							isDefaultTrack = false;
5910						}
5911
5912						// Build URL for retrieving WebVTT source via YouTube's timedtext API
5913						var srcUrl = thisObj.getYouTubeTimedTextUrl(youTubeId,trackName,trackLang);
5914						thisObj.tracks.push({
5915							'kind': 'captions',
5916							'src': srcUrl,
5917							'language': trackLang,
5918							'label': trackLabel,
5919							'def': isDefaultTrack
5920						});
5921
5922					});
5923					// setupPopups again with new captions array, replacing original
5924					thisObj.setupPopups('captions');
5925					deferred.resolve();
5926				}
5927				else {
5928					thisObj.hasCaptions = false;
5929					thisObj.usingYouTubeCaptions = false;
5930					deferred.resolve();
5931				}
5932			},
5933			error: function(xhr, status) {
5934				console.log('Error retrieving YouTube caption data for video ' + youTubeId);
5935				deferred.resolve();
5936			}
5937		});
5938		return promise;
5939	};
5940
5941	AblePlayer.prototype.getYouTubeTimedTextUrl = function (youTubeId, trackName, trackLang) {
5942
5943		// return URL for retrieving WebVTT source via YouTube's timedtext API
5944		// Note: This API seems to be undocumented, and could break anytime
5945		var url = 'https://www.youtube.com/api/timedtext?fmt=vtt';
5946		url += '&v=' + youTubeId;
5947		url += '&lang=' + trackLang;
5948		// if track has a value in the name field, it's *required* in the URL
5949		if (trackName !== '') {
5950			url += '&name=' + trackName;
5951		}
5952		return url;
5953	};
5954
5955
5956	AblePlayer.prototype.getYouTubeCaptionCues = function (youTubeId) {
5957
5958		var deferred, promise, thisObj;
5959
5960		var deferred = new $.Deferred();
5961		var promise = deferred.promise();
5962
5963		thisObj = this;
5964
5965		this.tracks = [];
5966		this.tracks.push({
5967			'kind': 'captions',
5968			'src': 'some_file.vtt',
5969			'language': 'en',
5970			'label': 'Fake English captions'
5971		});
5972
5973		deferred.resolve();
5974		return promise;
5975	};
5976
5977	AblePlayer.prototype.initYouTubeCaptionModule = function () {
5978
5979		// This function is called when YouTube onApiChange event fires
5980		// to indicate that the player has loaded (or unloaded) a module with exposed API methods
5981		// it isn't fired until the video starts playing
5982		// and only fires if captions are available for this video (automated captions don't count)
5983		// If no captions are available, onApichange event never fires & this function is never called
5984
5985		// YouTube iFrame API documentation is incomplete related to captions
5986		// Found undocumented features on user forums and by playing around
5987		// Details are here: http://terrillthompson.com/blog/648
5988		// Summary:
5989		// User might get either the AS3 (Flash) or HTML5 YouTube player
5990		// The API uses a different caption module for each player (AS3 = 'cc'; HTML5 = 'captions')
5991		// There are differences in the data and methods available through these modules
5992		// This function therefore is used to determine which captions module is being used
5993		// If it's a known module, this.ytCaptionModule will be used elsewhere to control captions
5994		var options, fontSize, displaySettings;
5995
5996		options = this.youTubePlayer.getOptions();
5997		if (options.length) {
5998			for (var i=0; i<options.length; i++) {
5999				if (options[i] == 'cc') { // this is the AS3 (Flash) player
6000					this.ytCaptionModule = 'cc';
6001					if (!this.hasCaptions) {
6002						// there are captions available via other sources (e.g., <track>)
6003						// so use these
6004						this.hasCaptions = true;
6005						this.usingYouTubeCaptions = true;
6006					}
6007					break;
6008				}
6009				else if (options[i] == 'captions') { // this is the HTML5 player
6010					this.ytCaptionModule = 'captions';
6011					if (!this.hasCaptions) {
6012						// there are captions available via other sources (e.g., <track>)
6013						// so use these
6014						this.hasCaptions = true;
6015						this.usingYouTubeCaptions = true;
6016					}
6017					break;
6018				}
6019			}
6020			if (typeof this.ytCaptionModule !== 'undefined') {
6021				if (this.usingYouTubeCaptions) {
6022					// set default languaage
6023					this.youTubePlayer.setOption(this.ytCaptionModule, 'track', {'languageCode': this.captionLang});
6024					// set font size using Able Player prefs (values are -1, 0, 1, 2, and 3, where 0 is default)
6025					this.youTubePlayer.setOption(this.ytCaptionModule,'fontSize',this.translatePrefs('size',this.prefCaptionsSize,'youtube'));
6026					// ideally could set other display options too, but no others seem to be supported by setOption()
6027				}
6028				else {
6029					// now that we know which cc module was loaded, unload it!
6030					// we don't want it if we're using local <track> elements for captions
6031					this.youTubePlayer.unloadModule(this.ytCaptionModule)
6032				}
6033			}
6034		}
6035		else {
6036			// no modules were loaded onApiChange
6037			// unfortunately, gonna have to disable captions if we can't control them
6038			this.hasCaptions = false;
6039			this.usingYouTubeCaptions = false;
6040		}
6041		this.refreshControls('captions');
6042	};
6043
6044	AblePlayer.prototype.getYouTubePosterUrl = function (youTubeId, width) {
6045
6046			 // return a URL for retrieving a YouTube poster image
6047			 // supported values of width: 120, 320, 480, 640
6048
6049			 var url = 'https://img.youtube.com/vi/' + youTubeId;
6050			 if (width == '120') {
6051				 // default (small) thumbnail, 120 x 90
6052				 return url + '/default.jpg';
6053			 }
6054			 else if (width == '320') {
6055				 // medium quality thumbnail, 320 x 180
6056				 return url + '/hqdefault.jpg';
6057			 }
6058			 else if (width == '480') {
6059				 // high quality thumbnail, 480 x 360
6060				 return url + '/hqdefault.jpg';
6061			 }
6062			 else if (width == '640') {
6063				 // standard definition poster image, 640 x 480
6064				 return url + '/sddefault.jpg';
6065			 }
6066			 return false;
6067	};
6068
6069})(jQuery);
6070
6071(function ($) {
6072
6073
6074	// Events:
6075	//	 startTracking(event, position)
6076	//	 tracking(event, position)
6077	//	 stopTracking(event, position)
6078
6079	window. AccessibleSlider = function(mediaType, div, orientation, length, min, max, bigInterval, label, className, trackingMedia, initialState) {
6080
6081		// mediaType is either 'audio' or 'video'
6082		// div is the host element around which the slider will be built
6083		// orientation is either 'horizontal' or 'vertical'
6084		// length is the width or height of the slider, depending on orientation
6085		// min is the low end of the slider scale
6086		// max is the high end of the slider scale
6087		// bigInterval is the number of steps supported by page up/page down (set to 0 if not supported)
6088		// (smallInterval, defined as nextStep below, is always set to 1) - this is the interval supported by arrow keys
6089		// label is used within an aria-label attribute to identify the slider to screen reader users
6090		// className is used as the root within class names (e.g., 'able-' + classname + '-head')
6091		// trackingMedia is true if this is a media timeline; otherwise false
6092		// initialState is either 'visible' or 'hidden'
6093
6094		var thisObj, coords;
6095
6096		thisObj = this;
6097
6098		// Initialize some variables.
6099		this.position = 0; // Note: position does not change while tracking.
6100		this.tracking = false;
6101		this.trackDevice = null; // 'mouse' or 'keyboard'
6102		this.keyTrackPosition = 0;
6103		this.lastTrackPosition = 0;
6104		this.nextStep = 1;
6105		this.inertiaCount = 0;
6106
6107		this.bodyDiv = $(div);
6108
6109		// Add divs for tracking amount of media loaded and played
6110		if (trackingMedia) {
6111			this.loadedDiv = $('<div></div>');
6112			this.playedDiv = $('<div></div>');
6113		}
6114
6115		// Add a seekhead
6116		this.seekHead = $('<div>',{
6117			'orientation': orientation,
6118			'class': 'able-' + className + '-head'
6119		});
6120
6121		if (initialState === 'visible') {
6122			this.seekHead.attr('tabindex', '0');
6123		}
6124		else {
6125			this.seekHead.attr('tabindex', '-1');
6126		}
6127		// Since head is focusable, it gets the aria roles/titles.
6128		this.seekHead.attr({
6129			'role': 'slider',
6130			'aria-label': label,
6131			'aria-valuemin': min,
6132			'aria-valuemax': max
6133		});
6134
6135		this.timeTooltip = $('<div>');
6136		this.bodyDiv.append(this.timeTooltip);
6137
6138		this.timeTooltip.attr('role', 'tooltip');
6139		this.timeTooltip.addClass('able-tooltip');
6140		this.timeTooltip.hide();
6141
6142		this.bodyDiv.append(this.loadedDiv);
6143		this.bodyDiv.append(this.playedDiv);
6144		this.bodyDiv.append(this.seekHead);
6145
6146		this.bodyDiv.wrap('<div></div>');
6147		this.wrapperDiv = this.bodyDiv.parent();
6148
6149    if (this.skin === 'legacy') {
6150  		if (orientation === 'horizontal') {
6151	  		this.wrapperDiv.width(length);
6152        this.loadedDiv.width(0);
6153		  }
6154      else {
6155			  this.wrapperDiv.height(length);
6156        this.loadedDiv.height(0);
6157		  }
6158		}
6159		this.wrapperDiv.addClass('able-' + className + '-wrapper');
6160
6161		if (trackingMedia) {
6162			this.loadedDiv.addClass('able-' + className + '-loaded');
6163
6164			this.playedDiv.width(0);
6165			this.playedDiv.addClass('able-' + className + '-played');
6166
6167			// Set a default duration. User can call this dynamically if duration changes.
6168			this.setDuration(max);
6169		}
6170
6171    // handle seekHead events
6172		this.seekHead.on('mouseenter mouseleave mousemove mousedown mouseup focus blur touchstart touchmove touchend', function (e) {
6173
6174      coords = thisObj.pointerEventToXY(e);
6175
6176		  if (e.type === 'mouseenter' || e.type === 'focus') {
6177  			thisObj.overHead = true;
6178      }
6179      else if (e.type === 'mouseleave' || e.type === 'blur') {
6180  			thisObj.overHead = false;
vendor: 1,955 bytes, lines 6181-6232
6181        if (!thisObj.overBody && thisObj.tracking && thisObj.trackDevice === 'mouse') {
6182				  thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
6183			  }
6184      }
6185      else if (e.type === 'mousemove' || e.type === 'touchmove') {
6186  			if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
6187	  			thisObj.trackHeadAtPageX(coords.x);
6188        }
6189      }
6190      else if (e.type === 'mousedown' || e.type === 'touchstart') {
6191  			thisObj.startTracking('mouse', thisObj.pageXToPosition(thisObj.seekHead.offset() + (thisObj.seekHead.width() / 2)));
6192        if (!thisObj.bodyDiv.is(':focus')) {
6193				  thisObj.bodyDiv.focus();
6194			  }
6195        e.preventDefault();
6196      }
6197      else if (e.type === 'mouseup' || e.type === 'touchend') {
6198  			if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
6199	  			thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
6200        }
6201      }
6202      if (e.type !== 'mousemove' && e.type !== 'mousedown' && e.type !== 'mouseup' && e.type !== 'touchstart' && e.type !== 'touchend') {
6203  			thisObj.refreshTooltip();
6204  		}
6205		});
6206
6207    // handle bodyDiv events
6208		this.bodyDiv.on(
6209		  'mouseenter mouseleave mousemove mousedown mouseup keydown keyup touchstart touchmove touchend', function (e) {
6210
6211      coords = thisObj.pointerEventToXY(e);
6212
6213  		if (e.type === 'mouseenter') {
6214  			thisObj.overBody = true;
6215		  }
6216		  else if (e.type === 'mouseleave') {
6217  			thisObj.overBody = false;
6218        thisObj.overBodyMousePos = null;
6219  			if (!thisObj.overHead && thisObj.tracking && thisObj.trackDevice === 'mouse') {
6220	  			thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
6221			  }
6222      }
6223      else if (e.type === 'mousemove' || e.type === 'touchmove') {
6224  			thisObj.overBodyMousePos = {
6225	  			x: coords.x,
6226          y: coords.y
6227			  };
6228        if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
6229				  thisObj.trackHeadAtPageX(coords.x);
6230			  }
6231      }
6232      else if (e.type === 'mousedown' || e.type === 'touchstart') {
6233  			thisObj.startTracking('mouse', thisObj.pageXToPosition(coords.x));
6234        thisObj.trackHeadAtPageX(coords.x);
6235        if (!thisObj.seekHead.is(':focus')) {
6236				  thisObj.seekHead.focus();
6237			  }
6238        e.preventDefault();
6239      }
6240      else if (e.type === 'mouseup' || e.type === 'touchend') {
6241        if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
6242				  thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
6243			  }
6244      }
6245      else if (e.type === 'keydown') {
6246	  		// Home
6247  			if (e.which === 36) {
6248		  		thisObj.trackImmediatelyTo(0);
6249			  }
6250        // End
6251        else if (e.which === 35) {
6252				  thisObj.trackImmediatelyTo(thisObj.duration);
6253			  }
6254        // Left arrow or down arrow
6255        else if (e.which === 37 || e.which === 40) {
6256				  thisObj.arrowKeyDown(-1);
6257			  }
6258        // Right arrow or up arrow
6259        else if (e.which === 39 || e.which === 38) {
6260				  thisObj.arrowKeyDown(1);
6261			  }
6262        // Page up
6263        else if (e.which === 33 && bigInterval > 0) {
6264				  thisObj.arrowKeyDown(bigInterval);
6265			  }
6266        // Page down
6267        else if (e.which === 34 && bigInterval > 0) {
6268				  thisObj.arrowKeyDown(-bigInterval);
6269			  }
6270        else {
6271				  return;
6272			  }
6273        e.preventDefault();
6274      }
6275      else if (e.type === 'keyup') {
6276  			if (e.which >= 33 && e.which <= 40) {
6277	  			if (thisObj.tracking && thisObj.trackDevice === 'keyboard') {
6278		  			thisObj.stopTracking(thisObj.keyTrackPosition);
6279          }
6280          e.preventDefault();
6281			  }
6282      }
6283      if (e.type !== 'mouseup' && e.type !== 'keydown' && e.type !== 'keydown') {
6284  			thisObj.refreshTooltip();
6285  		}
6286		});
6287	}
6288
6289	AccessibleSlider.prototype.arrowKeyDown = function (multiplier) {
6290		if (this.tracking && this.trackDevice === 'keyboard') {
6291			this.keyTrackPosition = this.boundPos(this.keyTrackPosition + (this.nextStep * multiplier));
6292			this.inertiaCount += 1;
6293			if (this.inertiaCount === 20) {
6294				this.inertiaCount = 0;
6295				this.nextStep *= 2;
6296			}
6297			this.trackHeadAtPosition(this.keyTrackPosition);
6298		}
6299		else {
6300			this.nextStep = 1;
6301			this.inertiaCount = 0;
6302			this.keyTrackPosition = this.boundPos(this.position + (this.nextStep * multiplier));
6303			this.startTracking('keyboard', this.keyTrackPosition);
6304			this.trackHeadAtPosition(this.keyTrackPosition);
6305		}
6306	};
6307/*
6308	AccessibleSlider.prototype.pageUp = function (multiplier) {
6309		if (this.tracking && this.trackDevice === 'keyboard') {
6310			this.keyTrackPosition = this.boundPos(this.keyTrackPosition + (this.nextStep * multiplier));
6311			this.inertiaCount += 1;
6312			if (this.inertiaCount === 20) {
6313				this.inertiaCount = 0;
6314				this.nextStep *= 2;
6315			}
6316			this.trackHeadAtPosition(this.keyTrackPosition);
6317		}
6318		else {
6319			this.nextStep = 1;
6320			this.inertiaCount = 0;
6321			this.keyTrackPosition = this.boundPos(this.position + (this.nextStep * multiplier));
6322			this.startTracking('keyboard', this.keyTrackPosition);
6323			this.trackHeadAtPosition(this.keyTrackPosition);
6324		}
6325	};
6326*/
6327	AccessibleSlider.prototype.pageXToPosition = function (pageX) {
6328		var offset = pageX - this.bodyDiv.offset().left;
6329		var position = this.duration * (offset / this.bodyDiv.width());
6330		return this.boundPos(position);
6331	};
6332
6333	AccessibleSlider.prototype.boundPos = function (position) {
6334		return Math.max(0, Math.min(position, this.duration));
6335	}
6336
6337	AccessibleSlider.prototype.setDuration = function (duration) {
6338		if (duration !== this.duration) {
6339			this.duration = duration;
6340			this.resetHeadLocation();
6341			this.seekHead.attr('aria-valuemax', duration);
6342		}
6343	};
6344
6345	AccessibleSlider.prototype.setWidth = function (width) {
6346		this.wrapperDiv.width(width);
6347		this.resizeDivs();
6348		this.resetHeadLocation();
6349	};
6350
6351	AccessibleSlider.prototype.getWidth = function () {
6352		return this.wrapperDiv.width();
6353	};
6354
6355	AccessibleSlider.prototype.resizeDivs = function () {
6356		this.playedDiv.width(this.bodyDiv.width() * (this.position / this.duration));
6357		this.loadedDiv.width(this.bodyDiv.width() * this.buffered);
6358	};
6359
6360	// Stops tracking, sets the head location to the current position.
6361	AccessibleSlider.prototype.resetHeadLocation = function () {
6362		var ratio = this.position / this.duration;
6363		var center = this.bodyDiv.width() * ratio;
6364		this.seekHead.css('left', center - (this.seekHead.width() / 2));
6365
6366		if (this.tracking) {
6367			this.stopTracking(this.position);
6368		}
6369	};
6370
6371	AccessibleSlider.prototype.setPosition = function (position, updateLive) {
6372		this.position = position;
6373		this.resetHeadLocation();
6374		this.refreshTooltip();
6375		this.resizeDivs();
6376		this.updateAriaValues(position, updateLive);
6377	}
6378
6379	// TODO: Native HTML5 can have several buffered segments, and this actually happens quite often.	Change this to display them all.
6380	AccessibleSlider.prototype.setBuffered = function (ratio) {
vendor: 5,965 bytes, lines 6381-6563
6381    if (!isNaN(ratio)) {
6382  		this.buffered = ratio;
6383      this.redrawDivs;
6384    }
6385	}
6386
6387	AccessibleSlider.prototype.startTracking = function (device, position) {
6388		if (!this.tracking) {
6389			this.trackDevice = device;
6390			this.tracking = true;
6391			this.bodyDiv.trigger('startTracking', [position]);
6392		}
6393	};
6394
6395	AccessibleSlider.prototype.stopTracking = function (position) {
6396		this.trackDevice = null;
6397		this.tracking = false;
6398		this.bodyDiv.trigger('stopTracking', [position]);
6399		this.setPosition(position, true);
6400	};
6401
6402	AccessibleSlider.prototype.trackHeadAtPageX = function (pageX) {
6403		var position = this.pageXToPosition(pageX);
6404		var newLeft = pageX - this.bodyDiv.offset().left - (this.seekHead.width() / 2);
6405		newLeft = Math.max(0, Math.min(newLeft, this.bodyDiv.width() - this.seekHead.width()));
6406		this.lastTrackPosition = position;
6407		this.seekHead.css('left', newLeft);
6408		this.reportTrackAtPosition(position);
6409	};
6410
6411	AccessibleSlider.prototype.trackHeadAtPosition = function (position) {
6412		var ratio = position / this.duration;
6413		var center = this.bodyDiv.width() * ratio;
6414		this.lastTrackPosition = position;
6415		this.seekHead.css('left', center - (this.seekHead.width() / 2));
6416		this.reportTrackAtPosition(position);
6417	};
6418
6419	AccessibleSlider.prototype.reportTrackAtPosition = function (position) {
6420		this.bodyDiv.trigger('tracking', [position]);
6421		this.updateAriaValues(position, true);
6422	};
6423
6424	AccessibleSlider.prototype.updateAriaValues = function (position, updateLive) {
6425		// TODO: Localize, move to another function.
6426		var pHours = Math.floor(position / 3600);
6427		var pMinutes = Math.floor((position % 3600) / 60);
6428		var pSeconds = Math.floor(position % 60);
6429
6430		var pHourWord = pHours === 1 ? 'hour' : 'hours';
6431		var pMinuteWord = pMinutes === 1 ? 'minute' : 'minutes';
6432		var pSecondWord = pSeconds === 1 ? 'second' : 'seconds';
6433
6434		var descriptionText;
6435		if (pHours > 0) {
6436			descriptionText = pHours +
6437				' ' + pHourWord +
6438				', ' + pMinutes +
6439				' ' + pMinuteWord +
6440				', ' + pSeconds +
6441				' ' + pSecondWord;
6442		}
6443		else if (pMinutes > 0) {
6444			descriptionText	 = pMinutes +
6445				' ' + pMinuteWord +
6446				', ' + pSeconds +
6447				' ' + pSecondWord;
6448		}
6449		else {
6450			descriptionText = pSeconds + ' ' + pSecondWord;
6451		}
6452
6453		/* Comment to stop live region from generating or being used. */
6454		if (!this.liveAriaRegion) {
6455			this.liveAriaRegion = $('<span>', {
6456				'class': 'able-offscreen',
6457				'aria-live': 'polite'
6458			});
6459			this.wrapperDiv.append(this.liveAriaRegion);
6460		}
6461		if (updateLive && (this.liveAriaRegion.text() !== descriptionText)) {
6462			this.liveAriaRegion.text(descriptionText);
6463		}
6464
6465		// Uncomment the following lines to use aria values instead of separate live region.
6466		this.seekHead.attr('aria-valuetext', descriptionText);
6467		this.seekHead.attr('aria-valuenow', Math.floor(position).toString());
6468	};
6469
6470	AccessibleSlider.prototype.trackImmediatelyTo = function (position) {
6471		this.startTracking('keyboard', position);
6472		this.trackHeadAtPosition(position);
6473		this.keyTrackPosition = position;
6474	};
6475
6476	AccessibleSlider.prototype.refreshTooltip = function () {
6477		if (this.overHead) {
6478			this.timeTooltip.show();
6479			if (this.tracking) {
6480  		  this.timeTooltip.text(this.positionToStr(this.lastTrackPosition));
6481			}
6482			else {
6483  		  this.timeTooltip.text(this.positionToStr(this.position));
6484			}
6485			this.setTooltipPosition(this.seekHead.position().left + (this.seekHead.width() / 2));
6486		}
6487		else if (this.overBody && this.overBodyMousePos) {
6488			this.timeTooltip.show();
6489			this.timeTooltip.text(this.positionToStr(this.pageXToPosition(this.overBodyMousePos.x)));
6490			this.setTooltipPosition(this.overBodyMousePos.x - this.bodyDiv.offset().left);
6491		}
6492		else {
6493			this.timeTooltip.hide();
6494		}
6495	};
6496
6497	AccessibleSlider.prototype.setTooltipPosition = function (x) {
6498		this.timeTooltip.css({
6499			left: x - (this.timeTooltip.width() / 2) - 10,
6500			bottom: this.seekHead.height() + 10
6501		});
6502	};
6503
6504	AccessibleSlider.prototype.positionToStr = function (seconds) {
6505
6506		// same logic as misc.js > formatSecondsAsColonTime()
6507		var dHours = Math.floor(seconds / 3600);
6508		var dMinutes = Math.floor(seconds / 60) % 60;
6509		var dSeconds = Math.floor(seconds % 60);
6510		if (dSeconds < 10) {
6511			dSeconds = '0' + dSeconds;
6512		}
6513		if (dHours > 0) {
6514			if (dMinutes < 10) {
6515				dMinutes = '0' + dMinutes;
6516			}
6517			return dHours + ':' + dMinutes + ':' + dSeconds;
6518		}
6519		else {
6520			return dMinutes + ':' + dSeconds;
6521		}
6522	};
6523
6524  AccessibleSlider.prototype.pointerEventToXY = function(e) {
6525
6526    // returns array of coordinates x and y in response to both mouse and touch events
6527    // for mouse events, this comes from e.pageX and e.pageY
6528    // for touch events, it's a bit more complicated
6529    var out = {x:0, y:0};
6530    if (e.type == 'touchstart' || e.type == 'touchmove' || e.type == 'touchend' || e.type == 'touchcancel') {
6531      var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
6532      out.x = touch.pageX;
6533      out.y = touch.pageY;
6534    }
6535    else if (e.type == 'mousedown' || e.type == 'mouseup' || e.type == 'mousemove' || e.type == 'mouseover'|| e.type=='mouseout' || e.type=='mouseenter' || e.type=='mouseleave') {
6536      out.x = e.pageX;
6537      out.y = e.pageY;
6538    }
6539    return out;
6540  };
6541
6542})(jQuery);
6543
6544(function ($) {
6545
6546	AblePlayer.prototype.addVolumeSlider = function($div) {
6547
6548		// input type="range" requires IE10 and later
6549		// and still isn't supported by Opera Mini as of v8
6550		// Also, vertical orientation of slider requires CSS hacks
6551		// and causes problems in some screen readers
6552		// Therefore, building a custom vertical volume slider
6553		var thisObj, volumeSliderId, volumeHelpId, x, y, volumePct;
6554
6555		thisObj = this;
6556
6557		// define a few variables
6558		volumeSliderId = this.mediaId + '-volume-slider';
6559		volumeHelpId = this.mediaId + '-volume-help';
6560		this.volumeTrackHeight = 50; // must match CSS height for .able-volume-slider
6561		this.volumeHeadHeight = 7; // must match CSS height for .able-volume-head
6562		this.volumeTickHeight = this.volumeTrackHeight / 10;
6563
6564		this.$volumeSlider = $('<div>',{
6565			'id': volumeSliderId,
6566			'class': 'able-volume-slider',
6567			'aria-hidden': 'true'
6568		}).hide();
6569		this.$volumeSliderTooltip = $('<div>',{
6570			'class': 'able-tooltip',
6571			'role': 'tooltip'
6572		}).hide();
6573		this.$volumeSliderTrack = $('<div>',{
6574			'class': 'able-volume-track'
6575		});
6576		this.$volumeSliderTrackOn = $('<div>',{
6577			'class': 'able-volume-track able-volume-track-on'
6578		});
6579		this.$volumeSliderHead = $('<div>',{
6580			'class': 'able-volume-head',
6581			'role': 'slider',
6582			'aria-orientation': 'vertical',
6583			'aria-label': this.tt.volumeUpDown,
6584			'aria-valuemin': 0,
6585			'aria-valuemax': 10,
6586			'aria-valuenow': this.volume,
6587			'tabindex': -1
6588		});
6589		this.$volumeSliderTrack.append(this.$volumeSliderTrackOn,this.$volumeSliderHead);
6590		this.$volumeAlert = $('<div>',{
6591			'class': 'able-offscreen',
6592			'aria-live': 'assertive',
6593			'aria-atomic': 'true'
6594		});
6595		volumePct = parseInt(thisObj.volume) / 10 * 100;
6596		this.$volumeHelp = $('<div>',{
6597			'id': volumeHelpId,
6598			'class': 'able-volume-help'
6599		}).text(volumePct + '%, ' + this.tt.volumeHelp);
6600		this.$volumeButton.attr({
6601			'aria-describedby': volumeHelpId
6602		});
6603		this.$volumeSlider.append(this.$volumeSliderTooltip,this.$volumeSliderTrack,this.$volumeAlert,this.$volumeHelp)
6604		$div.append(this.$volumeSlider);
6605		this.refreshVolumeSlider(this.volume);
6606
6607		// add event listeners
6608		this.$volumeSliderHead.on('mousedown',function (e) {
6609			e.preventDefault(); // prevent text selection (implications?)
6610			thisObj.draggingVolume = true;
6611			thisObj.volumeHeadPositionTop = $(this).offset().top;
6612		});
6613
6614		// prevent dragging after mouseup as mouseup not detected over iframe (YouTube)
6615		this.$mediaContainer.on('mouseover',function (e) {
6616			if(thisObj.player == 'youtube'){
6617				thisObj.draggingVolume = false;
6618			}
6619		});
6620
6621		$(document).on('mouseup',function (e) {
6622			thisObj.draggingVolume = false;
6623		});
6624
6625		$(document).on('mousemove',function (e) {
6626			if (thisObj.draggingVolume) {
6627				x = e.pageX;
6628				y = e.pageY;
6629				thisObj.moveVolumeHead(y);
6630			}
6631		});
6632
6633		this.$volumeSliderHead.on('keydown',function (e) {
6634
6635			// Left arrow or down arrow
6636			if (e.which === 37 || e.which === 40) {
6637				thisObj.handleVolume('down');
6638			}
6639			// Right arrow or up arrow
6640			else if (e.which === 39 || e.which === 38) {
6641				thisObj.handleVolume('up');
6642			}
6643			// Escape key or Enter key or Tab key
6644			else if (e.which === 27 || e.which === 13 || e.which === 9) {
6645				// close popup
6646				if (thisObj.$volumeSlider.is(':visible')) {
6647  				thisObj.closingVolume = true; // stopgap
6648					thisObj.hideVolumePopup();
6649				}
6650				else {
6651  				if (!thisObj.closingVolume) {
6652  					thisObj.showVolumePopup();
6653  				}
6654				}
6655			}
6656			else {
6657				return;
6658			}
6659			e.preventDefault();
6660		});
6661	};
6662
6663	AblePlayer.prototype.refreshVolumeSlider = function(volume) {
6664
6665		// adjust slider position based on current volume
6666		var volumePct, volumePctText;
6667		volumePct = (volume/10) * 100;
6668		volumePctText = volumePct + '%';
6669
6670		var trackOnHeight, trackOnTop, headTop;
6671		trackOnHeight = volume * this.volumeTickHeight;
6672		trackOnTop = this.volumeTrackHeight - trackOnHeight;
6673		headTop = trackOnTop - this.volumeHeadHeight;
6674
6675    if (this.$volumeSliderTrackOn) {
6676  		this.$volumeSliderTrackOn.css({
6677	  		'height': trackOnHeight + 'px',
6678        'top': trackOnTop + 'px'
6679		  });
6680		}
6681		if (this.$volumeSliderHead) {
6682      this.$volumeSliderHead.attr({
6683			  'aria-valuenow': volume,
6684        'aria-valuetext': volumePctText
6685		  });
6686      this.$volumeSliderHead.css({
6687			  'top': headTop + 'px'
6688		  });
6689		}
6690		if (this.$volumeAlert) {
6691  		this.$volumeAlert.text(volumePct + '%');
6692    }
6693	};
6694
6695	AblePlayer.prototype.refreshVolumeButton = function(volume) {
6696
6697		var volumeName, volumePct, volumeLabel, volumeIconClass, volumeImg, newSvgData;
6698
6699		volumeName = this.getVolumeName(volume);
6700		volumePct = (volume/10) * 100;
6701		volumeLabel = this.tt.volume + ' ' + volumePct + '%';
6702
6703		if (this.iconType === 'font') {
6704			volumeIconClass = 'icon-volume-' + volumeName;
6705			this.$volumeButton.find('span').first().removeClass().addClass(volumeIconClass);
6706			this.$volumeButton.find('span.able-clipped').text(volumeLabel);
6707		}
6708		else if (this.iconType === 'image') {
6709			volumeImg = this.imgPath + 'volume-' + volumeName + '.png';
6710			this.$volumeButton.find('img').attr('src',volumeImg);
6711		}
6712		else if (this.iconType === 'svg') {
6713  		if (volumeName !== 'mute') {
6714    		volumeName = 'volume-' + volumeName;
6715      }
6716		  newSvgData = this.getSvgData(volumeName);
6717      this.$volumeButton.find('svg').attr('viewBox',newSvgData[0]);
6718      this.$volumeButton.find('path').attr('d',newSvgData[1]);
6719    }
6720	};
6721
6722	AblePlayer.prototype.moveVolumeHead = function(y) {
6723
6724		// y is current position after mousemove
6725		var diff, direction, ticksDiff, newVolume, maxedOut;
6726
6727		var diff = this.volumeHeadPositionTop - y;
6728
6729		// only move the volume head if user had dragged at least one tick
6730		// this is more efficient, plus creates a "snapping' effect
6731		if (Math.abs(diff) > this.volumeTickHeight) {
6732			if (diff > 0) {
6733				direction = 'up';
6734			}
6735			else {
6736				direction = 'down';
6737			}
6738			if (direction == 'up' && this.volume == 10) {
6739				// can't go any higher
6740				return;
6741			}
6742			else if (direction == 'down' && this.volume == 0) {
6743				// can't go any lower
6744				return;
6745			}
6746			else {
6747				ticksDiff = Math.round(Math.abs(diff) / this.volumeTickHeight);
6748				if (direction == 'up') {
6749					newVolume = this.volume + ticksDiff;
6750					if (newVolume > 10) {
6751						newVolume = 10;
6752					}
6753				}
6754				else { // direction is down
6755					newVolume = this.volume - ticksDiff;
6756					if (newVolume < 0) {
6757						newVolume = 0;
6758					}
6759				}
6760				this.setVolume(newVolume); // this.volume will be updated after volumechange event fires (event.js)
6761				this.refreshVolumeSlider(newVolume);
6762				this.refreshVolumeButton(newVolume);
6763				this.volumeHeadPositionTop = y;
6764			}
6765		}
6766	};
6767
6768	AblePlayer.prototype.handleVolume = function(direction) {
6769
6770		// 'direction is either 'up','down', or an ASCII key code 49-57 (numeric keys 1-9)
6771		// Action: calculate and change the volume
6772		// Don't change this.volume and this.volumeButton yet - wait for 'volumechange' event to fire (event.js)
6773
6774		// If NO direction is provided, user has just clicked on the Volume button
6775		// Action: show slider
6776		var volume;
6777
6778		if (typeof direction === 'undefined') {
6779			if (this.$volumeSlider.is(':visible')) {
6780				this.hideVolumePopup();
6781			}
6782			else {
6783        if (!this.closingVolume) {
6784  				this.showVolumePopup();
6785        }
6786			}
6787			return;
6788		}
6789
6790		if (direction >= 49 && direction <= 57) {
6791			volume = direction - 48;
6792		}
6793		else {
6794
6795			volume = this.getVolume();
6796
6797			if (direction === 'up' && volume < 10) {
6798				volume += 1;
6799			}
6800			else if (direction === 'down' && volume > 0) {
6801				volume -= 1;
6802			}
6803		}
6804
6805		if (this.isMuted() && volume > 0) {
6806			this.setMute(false);
6807		}
6808		else if (volume === 0) {
6809			this.setMute(true);
6810		}
6811		else {
6812			this.setVolume(volume); // this.volume will be updated after volumechange event fires (event.js)
6813			this.refreshVolumeSlider(volume);
6814			this.refreshVolumeButton(volume);
6815		}
6816	};
6817
6818	AblePlayer.prototype.handleMute = function() {
6819
6820		if (this.isMuted()) {
6821			this.setMute(false);
6822		}
6823		else {
6824			this.setMute(true);
6825		}
6826	};
6827
6828	AblePlayer.prototype.showVolumePopup = function() {
6829
6830		this.closePopups();
6831		this.$tooltipDiv.hide();
6832		this.$volumeSlider.show().attr('aria-hidden','false');
6833		this.$volumeButton.attr('aria-expanded','true');
6834		this.$volumeSliderHead.attr('tabindex','0').focus();
6835	};
6836
6837	AblePlayer.prototype.hideVolumePopup = function() {
6838
6839    var thisObj = this;
6840
6841		this.$volumeSlider.hide().attr('aria-hidden','true');
6842		this.$volumeSliderHead.attr('tabindex','-1');
6843		this.$volumeButton.attr('aria-expanded','false').focus();
6844    // wait a second before resetting stopgap var
6845    // otherwise the keypress used to close volume popup will trigger the volume button
6846    setTimeout(function() {
6847      thisObj.closingVolume = false;
6848    }, 1000);
6849	};
6850
6851	AblePlayer.prototype.isMuted = function () {
6852
6853		if (this.player === 'html5') {
6854			return this.media.muted;
6855		}
6856		else if (this.player === 'jw' && this.jwPlayer) {
6857			return this.jwPlayer.getMute();
6858		}
6859		else if (this.player === 'youtube') {
6860			return this.youTubePlayer.isMuted();
6861		}
6862	};
6863
6864	AblePlayer.prototype.setMute = function(mute) {
6865
6866		// mute is either true (muting) or false (unmuting)
6867		if (mute) {
6868			// save current volume so it can be restored after unmute
6869			this.lastVolume = this.volume;
6870			this.volume = 0;
6871		}
6872		else { // restore to previous volume
6873			if (typeof this.lastVolume !== 'undefined') {
6874				this.volume = this.lastVolume;
6875			}
6876		}
6877
6878		if (this.player === 'html5') {
6879			this.media.muted = mute;
6880		}
6881		else if (this.player === 'jw' && this.jwPlayer) {
6882			this.jwPlayer.setMute(mute);
6883		}
6884		else if (this.player === 'youtube') {
6885			if (mute) {
6886				this.youTubePlayer.mute();
6887			}
6888			else {
6889				this.youTubePlayer.unMute();
6890			}
6891		}
6892		this.refreshVolumeSlider(this.volume);
6893		this.refreshVolumeButton(this.volume);
6894	};
6895
6896	AblePlayer.prototype.setVolume = function (volume) {
6897
6898		// volume is 1 to 10
6899		// convert as needed depending on player
6900
6901		if (this.player === 'html5') {
6902			// volume is 0 to 1
6903			this.media.volume = volume / 10;
6904			if (this.hasSignLanguage && this.signVideo) {
6905				this.signVideo.volume = 0; // always mute
6906			}
6907		}
6908		else if (this.player === 'youtube') {
6909			// volume is 0 to 100
6910			this.youTubePlayer.setVolume(volume * 10);
6911			this.volume = volume;
6912		}
6913		else if (this.player === 'vimeo') {
6914			// volume is 0 to 1
6915			this.vimeoPlayer.setVolume(volume / 10).then(function() {
6916				// setVolume finished.
6917				// could do something here
6918				// successful completion also fires a 'volumechange' event (see event.js)
6919			});
6920		}
6921		else if (this.player === 'jw' && this.jwPlayer) {
6922			// volume is 0 to 100
6923			this.jwPlayer.setVolume(volume * 10);
6924		}
6925		this.lastVolume = volume;
6926	};
6927
6928	AblePlayer.prototype.getVolume = function (volume) {
6929
6930		// return volume using common audio control scale 1 to 10
6931		if (this.player === 'html5') {
6932			// uses 0 to 1 scale
6933			return this.media.volume * 10;
6934		}
6935		else if (this.player === 'youtube') {
6936			// uses 0 to 100 scale
6937			return this.youTubePlayer.getVolume() / 10;
6938		}
6939		if (this.player === 'vimeo') {
6940			// uses 0 to 1 scale
6941			// this.vimeoPlayer.getVolume() takes too long to resolve with a value
6942			// Just use variable that's already been defined (should be the same value anyway)
6943			return this.volume;
6944		}
6945		else if (this.player === 'jw' && this.jwPlayer) {
6946			// uses 0 to 100 scale
6947			return this.jwPlayer.getVolume() / 10;
6948		}
6949	};
6950
6951	AblePlayer.prototype.getVolumeName = function (volume) {
6952
6953		// returns 'mute','soft','medium', or 'loud' depending on volume level
6954		if (volume == 0) {
6955			return 'mute';
6956		}
6957		else if (volume == 10) {
6958			return 'loud';
6959		}
6960		else if (volume < 5) {
6961			return 'soft';
6962		}
6963		else {
6964			return 'medium';
6965		}
6966	};
6967
6968})(jQuery);
6969
6970(function ($) {
6971	var focusableElementsSelector = "a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]";
6972
6973	// Based on the incredible accessible modal dialog.
6974	window.AccessibleDialog = function(modalDiv, $returnElement, dialogRole, title, $descDiv, closeButtonLabel, width, fullscreen, escapeHook) {
6975
6976		this.title = title;
6977		this.closeButtonLabel = closeButtonLabel;
6978		this.focusedElementBeforeModal = $returnElement;
6979		this.escapeHook = escapeHook;
6980		this.baseId = $(modalDiv).attr('id') || Math.floor(Math.random() * 1000000000).toString();
6981		var thisObj = this;
6982		var modal = modalDiv;
6983		this.modal = modal;
6984		modal.css({
6985			'width': width || '50%',
6986			'top': (fullscreen ? '0' : '5%')
6987		});
6988		modal.addClass('able-modal-dialog');
6989
6990		if (!fullscreen) {
6991			var closeButton = $('<button>',{
6992				 'class': 'modalCloseButton',
6993				 'title': thisObj.closeButtonLabel,
6994				 'aria-label': thisObj.closeButtonLabel
6995			}).text('X');
6996			closeButton.keydown(function (e) {
6997				// Space key down
6998				if (e.which === 32) {
6999					thisObj.hide();
7000				}
7001			}).click(function () {
7002				thisObj.hide();
7003			});
7004
7005			var titleH1 = $('<h1></h1>');
7006			titleH1.attr('id', 'modalTitle-' + this.baseId);
7007			titleH1.css('text-align', 'center');
7008			titleH1.text(title);
7009
7010			$descDiv.attr('id', 'modalDesc-' + this.baseId);
7011
7012			modal.attr({
7013				'aria-labelledby': 'modalTitle-' + this.baseId,
7014				'aria-describedby': 'modalDesc-' + this.baseId
7015			});
7016			modal.prepend(titleH1);
7017			modal.prepend(closeButton);
7018		}
7019
7020		modal.attr({
7021			'aria-hidden': 'true',
7022			'role': dialogRole
7023		});
7024
7025		modal.keydown(function (e) {
7026			// Escape
7027			if (e.which === 27) {
7028				if (thisObj.escapeHook) {
7029					thisObj.escapeHook(e, this);
7030				}
7031				else {
7032					thisObj.hide();
7033					e.preventDefault();
7034				}
7035			}
7036			// Tab
7037			else if (e.which === 9) {
7038				// Manually loop tab navigation inside the modal.
7039				var parts = modal.find('*');
7040				var focusable = parts.filter(focusableElementsSelector).filter(':visible');
7041
7042				if (focusable.length === 0) {
7043					return;
7044				}
7045
7046				var focused = $(':focus');
7047				var currentIndex = focusable.index(focused);
7048				if (e.shiftKey) {
7049					// If backwards from first element, go to last.
7050					if (currentIndex === 0) {
7051						focusable.get(focusable.length - 1).focus();
7052						e.preventDefault();
7053					}
7054				}
7055				else {
7056					if (currentIndex === focusable.length - 1) {
7057						focusable.get(0).focus();
7058						e.preventDefault();
7059					}
7060				}
7061			}
7062			e.stopPropagation();
7063		});
7064
7065		$('body > *').not('.able-modal-overlay').not('.able-modal-dialog').removeAttr('aria-hidden');
7066	};
7067
7068	AccessibleDialog.prototype.show = function () {
7069		if (!this.overlay) {
7070			// Generate overlay.
7071			var overlay = $('<div></div>').attr({
7072				 'class': 'able-modal-overlay',
7073				 'tabindex': '-1'
7074			});
7075			this.overlay = overlay;
7076			$('body').append(overlay);
7077
7078			// Keep from moving focus out of dialog when clicking outside of it.
7079			overlay.on('mousedown.accessibleModal', function (e) {
7080				e.preventDefault();
7081			});
7082		}
7083
7084		$('body > *').not('.able-modal-overlay').not('.able-modal-dialog').attr('aria-hidden', 'true');
7085
7086		this.overlay.css('display', 'block');
7087		this.modal.css('display', 'block');
7088		this.modal.attr({
7089			'aria-hidden': 'false',
7090			'tabindex': '-1'
7091		});
7092
7093		var focusable = this.modal.find("*").filter(focusableElementsSelector).filter(':visible');
7094		if (focusable.length === 0) {
7095			this.focusedElementBeforeModal.blur();
7096		}
7097		var thisObj = this;
7098		setTimeout(function () {
7099			// set focus on the first focusable element
7100			thisObj.modal.find('button.modalCloseButton').first().focus();
7101		}, 300);
7102	};
7103
7104	AccessibleDialog.prototype.hide = function () {
7105		if (this.overlay) {
7106			this.overlay.css('display', 'none');
7107		}
7108		this.modal.css('display', 'none');
7109		this.modal.attr('aria-hidden', 'true');
7110		$('body > *').not('.able-modal-overlay').not('.able-modal-dialog').removeAttr('aria-hidden');
7111
7112		this.focusedElementBeforeModal.focus();
7113	};
7114
7115  AccessibleDialog.prototype.getInputs = function () {
7116
7117    // return an array of input elements within this dialog
7118    if (this.modal) {
7119		  var inputs = this.modal.find('input');
7120			return inputs;
7121		}
7122		return false;
7123	};
7124
7125})(jQuery);
7126
7127(function ($) {
7128
7129	AblePlayer.prototype.getNextHeadingLevel = function($element) {
7130
7131		// Finds the nearest heading in the ancestor tree
7132		// Loops over each parent of the current element until a heading is found
7133		// If multiple headings are found beneath a given parent, get the closest
7134		// Returns an integer (1-6) representing the next available heading level
7135
7136		var $parents, $foundHeadings, numHeadings, headingType, headingNumber;
7137
7138		$parents = $element.parents();
7139		$parents.each(function(){
7140			$foundHeadings = $(this).children(':header');
7141			numHeadings = $foundHeadings.length;
7142			if (numHeadings) {
7143				headingType = $foundHeadings.eq(numHeadings-1).prop('tagName');
7144				return false;
7145			}
7146		});
7147		if (typeof headingType === 'undefined') {
7148			// page has no headings
7149			headingNumber = 1;
7150		}
7151		else {
7152			// Increment closest heading by one if less than 6.
7153			headingNumber = parseInt(headingType[1]);
7154			headingNumber += 1;
7155			if (headingNumber > 6) {
7156				headingNumber = 6;
7157			}
7158		}
7159		return headingNumber;
7160	};
7161
7162	AblePlayer.prototype.countProperties = function(obj) {
7163
7164		// returns the number of properties in an object
7165		var count, prop;
7166		count = 0;
7167		for (prop in obj) {
7168			if (obj.hasOwnProperty(prop)) {
7169				++count;
7170			}
7171		}
7172		return count;
7173	};
7174
7175	AblePlayer.prototype.formatSecondsAsColonTime = function (seconds, showFullTime) {
7176
7177		// Takes seconds and converts to string of form hh:mm:ss
7178		// If showFullTime is true, shows 00 for hours if time is less than an hour
7179		//	 and show milliseconds	(e.g., 00:00:04.123 as in Video Track Sorter)
7180		// Otherwise, omits empty hours and milliseconds (e.g., 00:04 as in timer on controller)
7181
7182		var dHours, dMinutes, dSeconds,
7183				parts, milliSeconds, numShort, i;
7184
7185		if (showFullTime) {
7186			// preserve milliseconds, if included in seconds
7187			parts = seconds.toString().split('.');
7188			if (parts.length === 2) {
7189				milliSeconds = parts[1];
7190				if (milliSeconds.length < 3) {
7191					numShort = 3 - milliSeconds.length;
7192					for (i=1; i <= numShort; i++) {
7193						milliSeconds += '0';
7194					}
7195				}
7196			}
7197			else {
7198				milliSeconds = '000';
7199			}
7200		}
7201		dHours = Math.floor(seconds / 3600);
7202		dMinutes = Math.floor(seconds / 60) % 60;
7203		dSeconds = Math.floor(seconds % 60);
7204		if (dSeconds < 10) {
7205			dSeconds = '0' + dSeconds;
7206		}
7207		if (dHours > 0) {
7208			if (dMinutes < 10) {
7209				dMinutes = '0' + dMinutes;
7210			}
7211			if (showFullTime) {
7212				return dHours + ':' + dMinutes + ':' + dSeconds + '.' + milliSeconds;
7213			}
7214			else {
7215				return dHours + ':' + dMinutes + ':' + dSeconds;
7216			}
7217		}
7218		else {
7219			if (showFullTime) {
7220				if (dHours < 1) {
7221					dHours = '00';
7222				}
7223				else if (dHours < 10) {
7224					dHours = '0' + dHours;
7225				}
7226				if (dMinutes < 1) {
7227					dMinutes = '00';
7228				}
7229				else if (dMinutes < 10) {
7230					dMinutes = '0' + dMinutes;
7231				}
7232				return dHours + ':' + dMinutes + ':' + dSeconds + '.' + milliSeconds;
7233			}
7234			else {
7235				return dMinutes + ':' + dSeconds;
7236			}
7237		}
7238	};
7239
7240	AblePlayer.prototype.getSecondsFromColonTime = function (timeStr) {
7241
7242		// Converts string of form hh:mm:ss to seconds
7243		var timeParts, hours, minutes, seconds, newTime;
7244
7245		timeParts = timeStr.split(':');
7246		if (timeParts.length === 3) {
7247			hours = parseInt(timeParts[0]);
7248			minutes = parseInt(timeParts[1]);
7249			seconds = parseFloat(timeParts[2]);
7250			return ((hours * 3600) + (minutes * 60) + (seconds));
7251		}
7252		else if (timeParts.length === 2) {
7253			minutes = parseInt(timeParts[0]);
7254			seconds = parseFloat(timeParts[1]);
7255			return ((minutes * 60) + (seconds));
7256		}
7257		else if (timeParts.length === 1) {
7258			seconds = parseFloat(timeParts[0]);
7259			return seconds;
7260		}
7261	};
7262
7263	AblePlayer.prototype.capitalizeFirstLetter = function (string) {
7264		return string.charAt(0).toUpperCase() + string.slice(1);
7265	};
7266
7267	AblePlayer.prototype.roundDown = function (value, decimals) {
7268
7269		// round value down to the nearest X decimal points
7270		// where X is the value of the decimals parameter
7271		return Number(Math.floor(value+'e'+decimals)+'e-'+decimals);
7272	};
7273
7274	AblePlayer.prototype.hasAttr = function (object, attribute) {
7275
7276		 // surprisingly, there is no hasAttr() function in Jquery as of 3.2.1
7277		 // return true if object has attribute; otherwise false
7278		 // selector is a Jquery object
7279		 // attribute is a string
7280
7281		var attr = object.attr(attribute);
7282
7283		// For some browsers, `attr` is undefined; for others,
7284		// `attr` is false.	 Check for both.
7285		if (typeof attr !== typeof undefined && attr !== false) {
7286			return true;
7287		}
7288		else {
7289			 return false;
7290		}
7291	};
7292
7293	Number.isInteger = Number.isInteger || function(value) {
7294
7295		// polyfill for IE11, which doesn't otherwise support Number.isInteger
7296		// https://stackoverflow.com/a/31720368/744281
7297		return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
7298	};
7299
7300})(jQuery);
7301
7302(function ($) {
7303	AblePlayer.prototype.initDescription = function() {
7304
7305		// set default mode for delivering description (open vs closed)
7306		// based on availability and user preference
7307
7308		// called when player is being built, or when a user
7309		// toggles the Description button or changes a description-related preference
7310		// In the latter two scendarios, this.refreshingDesc == true via control.js > handleDescriptionToggle()
7311
7312		// The following variables are applicable to delivery of description:
7313		// prefDesc == 1 if user wants description (i.e., Description button is on); else 0
7314		// prefDescFormat == either 'video' or 'text' (as of v4.0.10, prefDescFormat is always 'video')
7315		// prefDescPause == 1 to pause video when description starts; else 0
7316		// prefVisibleDesc == 1 to visibly show text-based description area; else 0
7317		// hasOpenDesc == true if a described version of video is available via data-desc-src attribute
7318		// hasClosedDesc == true if a description text track is available
7319		// this.useDescFormat == either 'video' or 'text'; the format ultimately delivered
7320		// descOn == true if description of either type is on
7321		// exposeTextDescriptions == true if text description is to be announced audibly; otherwise false
7322
7323		var thisObj = this;
7324    if (this.refreshingDesc) {
7325		  this.prevDescFormat = this.useDescFormat;
7326    }
7327		else {
7328			// this is the initial build
7329			// first, check to see if there's an open-described version of this video
7330			// checks only the first source since if a described version is provided,
7331			// it must be provided for all sources
7332			this.descFile = this.$sources.first().attr('data-desc-src');
7333			if (typeof this.descFile !== 'undefined') {
7334				this.hasOpenDesc = true;
7335			}
7336			else {
7337				// there's no open-described version via data-desc-src,
7338				// but what about data-youtube-desc-src or data-vimeo-desc-src?
7339				if (this.youTubeDescId || this.vimeoDescId) {
7340					this.hasOpenDesc = true;
7341				}
7342				else { // there are no open-described versions from any source
7343					this.hasOpenDesc = false;
7344				}
7345			}
7346		}
7347
7348		// update this.useDescFormat based on media availability & user preferences
7349		if (this.prefDesc) {
7350			if (this.hasOpenDesc && this.hasClosedDesc) {
7351				// both formats are available. Always use 'video'
7352				this.useDescFormat = this.prefDescFormat;
7353				this.descOn = true;
7354				// Do not pause during descriptions when playing described video
7355				this.prefDescPause = false;
7356			}
7357			else if (this.hasOpenDesc) {
7358				this.useDescFormat = 'video';
7359				this.descOn = true;
7360			}
7361			else if (this.hasClosedDesc) {
7362				this.useDescFormat = 'text';
7363				this.descOn = true;
7364			}
7365		}
7366		else { // description button is off
7367      this.useDescFormat = false;
7368			this.descOn = false;
7369		}
7370
7371		if (this.useDescFormat === 'text') {
7372			// check whether browser supports the Web Speech API
7373			if (window.speechSynthesis) {
7374				// It does!
7375				this.synth = window.speechSynthesis;
7376				this.descVoices = this.synth.getVoices();
7377				// select the first voice that matches the track language
7378				// available languages are identified with local suffixes (e.g., en-US)
7379				// in case no matching voices are found, use the first voice in the voices array
7380				this.descVoiceIndex = 0;
7381				for (var i=0; i<this.descVoices.length; i++) {
7382					if (this.captionLang.length === 2) {
7383						// match only the first 2 characters of the lang code
7384						if (this.descVoices[i].lang.substr(0,2).toLowerCase() === this.captionLang.toLowerCase()) {
7385							this.descVoiceIndex = i;
7386							break;
7387						}
7388					}
7389					else {
7390						// match the entire lang code
7391						if (this.descVoices[i].lang.toLowerCase() === this.captionLang.toLowerCase()) {
7392							this.descVoiceIndex = i;
7393							break;
7394						}
7395					}
7396				}
7397			}
7398		}
7399		if (this.descOn) {
7400			if (this.useDescFormat === 'video') {
7401				if (!this.usingAudioDescription()) {
7402					// switched from non-described to described version
7403					this.swapDescription();
7404				}
7405			}
7406			if (this.hasClosedDesc) {
7407				if (this.prefVisibleDesc) {
7408  				// make description text visible
7409  				// New in v4.0.10: Do this regardless of useDescFormat
7410  				this.$descDiv.show();
7411					this.$descDiv.removeClass('able-clipped');
7412				}
7413				else {
7414  				// keep it visible to screen readers, but hide it visibly
7415					this.$descDiv.addClass('able-clipped');
7416				}
7417				if (!this.swappingSrc) {
7418					this.showDescription(this.elapsed);
7419				}
7420			}
7421		}
7422		else { // description is off.
7423			if (this.prevDescFormat === 'video') { // user was previously using description via video
7424				if (this.usingAudioDescription()) {
7425					this.swapDescription();
7426				}
7427			}
7428			else if (this.prevDescFormat === 'text') { // user was previously using text description
7429				// hide description div from everyone, including screen reader users
7430				this.$descDiv.hide();
7431				this.$descDiv.removeClass('able-clipped');
7432			}
7433		}
7434		this.refreshingDesc = false;
7435	};
7436
7437	// Returns true if currently using audio description, false otherwise.
7438	AblePlayer.prototype.usingAudioDescription = function () {
7439
7440		if (this.player === 'youtube') {
7441			return (this.activeYouTubeId === this.youTubeDescId);
7442		}
7443		else if (this.player === 'vimeo') {
7444			return (this.activeVimeoId === this.vimeoDescId);
7445		}
7446		else {
7447			return (this.$sources.first().attr('data-desc-src') === this.$sources.first().attr('src'));
7448		}
7449	};
7450
7451	AblePlayer.prototype.swapDescription = function() {
7452		// swap described and non-described source media, depending on which is playing
7453		// this function is only called in two circumstances:
7454		// 1. Swapping to described version when initializing player (based on user prefs & availability)
7455		// 2. User is toggling description
7456		var thisObj, i, origSrc, descSrc, srcType, newSource;
7457
7458		thisObj = this;
7459
7460		// get current time, and start new video at the same time
7461		// NOTE: There is some risk in resuming playback at the same start time
7462		// since the described version might include extended audio description (with pauses)
7463		// and might therefore be longer than the non-described version
7464		// The benefits though would seem to outweigh this risk
7465
7466		this.swapTime = this.elapsed; // video will scrub to this time after loaded (see event.js)
7467		if (this.descOn) {
7468			// user has requested the described version
7469			this.showAlert(this.tt.alertDescribedVersion);
7470		}
7471		else {
7472			// user has requested the non-described version
7473			this.showAlert(this.tt.alertNonDescribedVersion);
7474		}
7475		if (this.player === 'html5') {
7476
7477			if (this.usingAudioDescription()) {
7478				// the described version is currently playing. Swap to non-described
7479				for (i=0; i < this.$sources.length; i++) {
7480					// for all <source> elements, replace src with data-orig-src
7481					origSrc = this.$sources[i].getAttribute('data-orig-src');
7482					srcType = this.$sources[i].getAttribute('type');
7483					if (origSrc) {
7484						this.$sources[i].setAttribute('src',origSrc);
7485					}
7486				}
7487				// No need to check for this.initializing
7488				// This function is only called during initialization
7489				// if swapping from non-described to described
7490				this.swappingSrc = true;
7491			}
7492			else {
7493				// the non-described version is currently playing. Swap to described.
7494				for (i=0; i < this.$sources.length; i++) {
7495					// for all <source> elements, replace src with data-desc-src (if one exists)
7496					// then store original source in a new data-orig-src attribute
7497					origSrc = this.$sources[i].getAttribute('src');
7498					descSrc = this.$sources[i].getAttribute('data-desc-src');
7499					srcType = this.$sources[i].getAttribute('type');
7500					if (descSrc) {
7501						this.$sources[i].setAttribute('src',descSrc);
7502						this.$sources[i].setAttribute('data-orig-src',origSrc);
7503					}
7504				}
7505				this.swappingSrc = true;
7506			}
7507
7508			// now reload the source file.
7509			if (this.player === 'html5') {
7510				this.media.load();
7511			}
7512		}
7513		else if (this.player === 'youtube') {
7514
7515			if (this.usingAudioDescription()) {
7516				// the described version is currently playing. Swap to non-described
7517				this.activeYouTubeId = this.youTubeId;
7518				this.showAlert(this.tt.alertNonDescribedVersion);
7519			}
7520			else {
7521				// the non-described version is currently playing. Swap to described.
7522				this.activeYouTubeId = this.youTubeDescId;
7523				this.showAlert(this.tt.alertDescribedVersion);
7524			}
7525			if (typeof this.youTubePlayer !== 'undefined') {
7526
7527				// retrieve/setup captions for the new video from YouTube
7528				this.setupAltCaptions().then(function() {
7529
7530					if (thisObj.playing) {
7531						// loadVideoById() loads and immediately plays the new video at swapTime
7532						thisObj.youTubePlayer.loadVideoById(thisObj.activeYouTubeId,thisObj.swapTime);
7533					}
7534					else {
7535						// cueVideoById() loads the new video and seeks to swapTime, but does not play
7536						thisObj.youTubePlayer.cueVideoById(thisObj.activeYouTubeId,thisObj.swapTime);
7537					}
7538				});
7539			}
7540		}
7541		else if (this.player === 'vimeo') {
7542			if (this.usingAudioDescription()) {
7543				// the described version is currently playing. Swap to non-described
7544				this.activeVimeoId = this.vimeoId;
7545				this.showAlert(this.tt.alertNonDescribedVersion);
7546			}
7547			else {
7548				// the non-described version is currently playing. Swap to described.
7549				this.activeVimeoId = this.vimeoDescId;
7550				this.showAlert(this.tt.alertDescribedVersion);
7551			}
7552			// load the new video source
7553			this.vimeoPlayer.loadVideo(this.activeVimeoId).then(function() {
7554
7555				if (thisObj.playing) {
7556					// video was playing when user requested an alternative version
7557					// seek to swapTime and continue playback (playback happens automatically)
7558					thisObj.vimeoPlayer.setCurrentTime(thisObj.swapTime);
7559				}
7560				else {
7561					// Vimeo autostarts immediately after video loads
7562					// The "Described" button should not trigger playback, so stop this before the user notices.
7563					thisObj.vimeoPlayer.pause();
7564				}
7565			});
7566		}
7567	};
7568
7569	AblePlayer.prototype.showDescription = function(now) {
7570
7571		// there's a lot of redundancy between this function and showCaptions
7572		// Trying to combine them ended up in a mess though. Keeping as is for now.
7573
7574		if (this.swappingSrc || !this.descOn) {
7575			return;
7576		}
7577
7578		var thisObj, i, cues, d, thisDescription, descText, msg;
7579		thisObj = this;
7580
7581		var flattenComponentForDescription = function (component) {
7582			var result = [];
7583			if (component.type === 'string') {
7584				result.push(component.value);
7585			}
7586			else {
7587				for (var i = 0; i < component.children.length; i++) {
7588					result.push(flattenComponentForDescription(component.children[i]));
7589				}
7590			}
7591			return result.join('');
7592		};
7593
7594		if (this.selectedDescriptions) {
7595			cues = this.selectedDescriptions.cues;
7596		}
7597		else if (this.descriptions.length >= 1) {
7598			cues = this.descriptions[0].cues;
7599		}
7600		else {
7601			cues = [];
7602		}
7603		for (d = 0; d < cues.length; d++) {
7604			if ((cues[d].start <= now) && (cues[d].end > now)) {
7605				thisDescription = d;
7606				break;
7607			}
7608		}
7609		if (typeof thisDescription !== 'undefined') {
7610			if (this.currentDescription !== thisDescription) {
7611				// temporarily remove aria-live from $status in order to prevent description from being interrupted
7612				this.$status.removeAttr('aria-live');
7613				descText = flattenComponentForDescription(cues[thisDescription].components);
7614				if (
7615  				this.exposeTextDescriptions &&
7616				  typeof this.synth !== 'undefined' &&
7617          typeof this.descVoiceIndex !== 'undefined') {
7618					// browser supports speech synthesis and a voice has been selected in initDescription()
7619					// use the web speech API
7620					msg = new SpeechSynthesisUtterance();
7621					msg.voice = this.descVoices[this.descVoiceIndex]; // Note: some voices don't support altering params
7622					msg.voiceURI = 'native';
7623					msg.volume = 1; // 0 to 1
7624					msg.rate = 1.5; // 0.1 to 10 (1 is normal human speech; 2 is fast but easily decipherable; anything above 2 is blazing fast)
7625					msg.pitch = 1; //0 to 2
7626					msg.text = descText;
7627					msg.lang = this.captionLang;
7628					msg.onend = function(e) {
7629						// NOTE: e.elapsedTime might be useful
7630						if (thisObj.pausedForDescription) {
7631							thisObj.playMedia();
7632						}
7633      		};
7634					this.synth.speak(msg);
7635					if (this.prefVisibleDesc) {
7636						// write description to the screen for sighted users
7637						// but remove ARIA attributes since it isn't intended to be read by screen readers
7638						this.$descDiv.html(descText).removeAttr('aria-live aria-atomic');
7639					}
7640				}
7641				else {
7642					// browser does not support speech synthesis
7643					// load the new description into the container div for screen readers to read
7644					this.$descDiv.html(descText);
7645				}
7646				if (this.prefDescPause && this.exposeTextDescriptions) {
7647					this.pauseMedia();
7648					this.pausedForDescription = true;
7649				}
7650				this.currentDescription = thisDescription;
7651			}
7652		}
7653		else {
7654			this.$descDiv.html('');
7655			this.currentDescription = -1;
7656			// restore aria-live to $status
7657			this.$status.attr('aria-live','polite');
7658		}
7659	};
7660
7661})(jQuery);
7662
7663(function ($) {
7664
7665	AblePlayer.prototype.getUserAgent = function() {
7666
7667		// Whenever possible we avoid browser sniffing. Better to do feature detection.
7668		// However, in case it's needed...
7669		// this function defines a userAgent array that can be used to query for common browsers and OSs
7670		// NOTE: This would be much simpler with jQuery.browser but that was removed from jQuery 1.9
7671		// http://api.jquery.com/jQuery.browser/
7672		this.userAgent = {};
7673		this.userAgent.browser = {};
7674
7675		// Test for common browsers
7676		if (/Firefox[\/\s](\d+\.\d+)/.test(navigator.userAgent)){ //test for Firefox/x.x or Firefox x.x (ignoring remaining digits);
7677			this.userAgent.browser.name = 'Firefox';
7678			this.userAgent.browser.version = RegExp.$1; // capture x.x portion
7679		}
7680		else if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) { //test for MSIE x.x (IE10 or lower)
7681			this.userAgent.browser.name = 'Internet Explorer';
7682			this.userAgent.browser.version = RegExp.$1;
7683		}
7684		else if (/Trident.*rv[ :]*(\d+\.\d+)/.test(navigator.userAgent)) { // test for IE11 or higher
7685			this.userAgent.browser.name = 'Internet Explorer';
7686			this.userAgent.browser.version = RegExp.$1;
7687		}
7688		else if (/Edge[\/\s](\d+\.\d+)/.test(navigator.userAgent)) { // test for MS Edge
7689			this.userAgent.browser.name = 'Edge';
7690			this.userAgent.browser.version = RegExp.$1;
7691		}
7692		else if (/OPR\/(\d+\.\d+)/i.test(navigator.userAgent)) { // Opera 15 or over
7693			this.userAgent.browser.name = 'Opera';
7694			this.userAgent.browser.version = RegExp.$1;
7695		}
7696		else if (/Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)) {
7697			this.userAgent.browser.name = 'Chrome';
7698			if (/Chrome[\/\s](\d+\.\d+)/.test(navigator.userAgent)) {
7699				this.userAgent.browser.version = RegExp.$1;
7700			}
7701		}
7702		else if (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)) {
7703			this.userAgent.browser.name = 'Safari';
7704			if (/Version[\/\s](\d+\.\d+)/.test(navigator.userAgent)) {
7705				this.userAgent.browser.version = RegExp.$1;
7706			}
7707		}
7708		else {
7709			this.userAgent.browser.name = 'Unknown';
7710			this.userAgent.browser.version = 'Unknown';
7711		}
7712
7713		// Now test for common operating systems
7714		if (window.navigator.userAgent.indexOf("Windows NT 6.2") != -1) {
7715			this.userAgent.os = "Windows 8";
7716		}
7717		else if (window.navigator.userAgent.indexOf("Windows NT 6.1") != -1) {
7718			this.userAgent.os = "Windows 7";
7719		}
7720		else if (window.navigator.userAgent.indexOf("Windows NT 6.0") != -1) {
7721			this.userAgent.os = "Windows Vista";
7722		}
7723		else if (window.navigator.userAgent.indexOf("Windows NT 5.1") != -1) {
7724			this.userAgent.os = "Windows XP";
7725		}
7726		else if (window.navigator.userAgent.indexOf("Windows NT 5.0") != -1) {
7727			this.userAgent.os = "Windows 2000";
7728		}
7729		else if (window.navigator.userAgent.indexOf("Mac")!=-1) {
7730			this.userAgent.os = "Mac/iOS";
7731		}
7732		else if (window.navigator.userAgent.indexOf("X11")!=-1) {
7733			this.userAgent.os = "UNIX";
7734		}
7735		else if (window.navigator.userAgent.indexOf("Linux")!=-1) {
7736			this.userAgent.os = "Linux";
7737		}
7738		if (this.debug) {
7739			console.log('User agent:' + navigator.userAgent);
7740			console.log('Vendor: ' + navigator.vendor);
7741			console.log('Browser: ' + this.userAgent.browser.name);
7742			console.log('Version: ' + this.userAgent.browser.version);
7743			console.log('OS: ' + this.userAgent.os);
7744		}
7745	};
7746
7747	AblePlayer.prototype.isUserAgent = function(which) {
7748
7749		var userAgent = navigator.userAgent.toLowerCase();
7750		if (this.debug) {
7751			console.log('User agent: ' + userAgent);
7752		}
7753		if (userAgent.indexOf(which.toLowerCase()) !== -1) {
7754			return true;
7755		}
7756		else {
7757			return false;
7758		}
7759	};
7760
7761	AblePlayer.prototype.isIOS = function(version) {
7762
7763		// return true if this is IOS
7764		// if version is provided check for a particular version
7765
7766		var userAgent, iOS;
7767
7768		userAgent = navigator.userAgent.toLowerCase();
7769		iOS = /ipad|iphone|ipod/.exec(userAgent);
7770		if (iOS) {
7771			if (typeof version !== 'undefined') {
7772				if (userAgent.indexOf('os ' + version) !== -1) {
7773					// this is the target version of iOS
7774					return true;
7775				}
7776				else {
7777					return false;
7778				}
7779			}
7780			else {
7781				// no version was specified
7782				return true;
7783			}
7784		}
7785		else {
7786			// this is not IOS
7787			return false;
7788		}
7789	};
7790
7791	AblePlayer.prototype.browserSupportsVolume = function() {
7792
7793		// ideally we could test for volume support
7794		// However, that doesn't seem to be reliable
7795		// http://stackoverflow.com/questions/12301435/html5-video-tag-volume-support
7796
7797		var userAgent, noVolume;
7798
7799		userAgent = navigator.userAgent.toLowerCase();
7800		noVolume = /ipad|iphone|ipod|android|blackberry|windows ce|windows phone|webos|playbook/.exec(userAgent);
7801		if (noVolume) {
7802			if (noVolume[0] === 'android' && /firefox/.test(userAgent)) {
7803				// Firefox on android DOES support changing the volume:
7804				return true;
7805			}
7806			else {
7807				return false;
7808			}
7809		}
7810		else {
7811			// as far as we know, this userAgent supports volume control
7812			return true;
7813		}
7814	};
7815
7816	AblePlayer.prototype.nativeFullscreenSupported = function () {
7817
7818		return document.fullscreenEnabled ||
7819			document.webkitFullscreenEnabled ||
7820			document.mozFullScreenEnabled ||
7821			document.msFullscreenEnabled;
7822	};
7823
7824})(jQuery);
7825
7826(function ($) {
7827	AblePlayer.prototype.seekTo = function (newTime) {
7828
7829    var thisObj = this;
7830
7831		// define variables to be used for analytics
7832		// e.g., to measure the extent to which users seek back and forward
7833		this.seekFromTime = this.media.currentTime;
7834		this.seekToTime = newTime;
7835
7836		this.seeking = true;
7837		this.liveUpdatePending = true;
7838		if (this.player === 'html5') {
7839			var seekable;
7840
7841			this.startTime = newTime;
7842			// Check HTML5 media "seekable" property to be sure media is seekable to startTime
7843			seekable = this.media.seekable;
7844			if (seekable.length > 0 && this.startTime >= seekable.start(0) && this.startTime <= seekable.end(0)) {
7845				// ok to seek to startTime
7846				// canplaythrough will be triggered when seeking is complete
7847				// this.seeking will be set to false at that point
7848				this.media.currentTime = this.startTime;
7849				if (this.hasSignLanguage && this.signVideo) {
7850					// keep sign languge video in sync
7851					this.signVideo.currentTime = this.startTime;
7852				}
7853			}
7854		}
7855		else if (this.player === 'youtube') {
7856			this.youTubePlayer.seekTo(newTime,true);
7857			if (newTime > 0) {
7858				if (typeof this.$posterImg !== 'undefined') {
7859					this.$posterImg.hide();
7860				}
7861			}
7862		}
7863		else if (this.player === 'vimeo') {
7864			this.vimeoPlayer.setCurrentTime(newTime).then(function() {
7865				// seek finished.
7866				// successful completion also fires a 'seeked' event (see event.js)
7867				thisObj.elapsed = newTime;
7868				thisObj.refreshControls('timeline');
7869			})
7870		}
7871		this.refreshControls('timeline');
7872	};
7873
7874	AblePlayer.prototype.getMediaTimes = function (duration, elapsed) {
7875
7876		 // Returns an array with keys 'duration' and 'elapsed'
7877		 // Vars passed to this function come courtesy of select Vimeo events
7878		 // Use those if they're available.
7879		 // Otherwise, will need to call the relevant media API
7880		 // This function should only be called from onMediaUpdateTime()
7881		 // If duration and elapsed are needed other times, use this.duration and this.elapsed
7882
7883		// both values are expressed in seconds, and all player APIs are similar:
7884		// they return a value that is rounded to the nearest second before playback begins,
7885		// then to the nearest thousandth of a second after playback begins
7886		// With HTML5 media API, some browsers are more precise (e.g., Firefox rounds to 6 decimal points)
7887		// but inconsistent (values with 9 decimal points have been sporadically observed in Safari)
7888		// For standardization, values are rounded to 6 decimal points before they're returned
7889
7890		var deferred, promise, thisObj, mediaTimes;
7891		mediaTimes = {};
7892
7893		deferred = new $.Deferred();
7894		promise = deferred.promise();
7895		thisObj = this;
7896		if (typeof duration !== 'undefined' && typeof elapsed !== 'undefined') {
7897			mediaTimes['duration'] = duration;
7898			mediaTimes['elapsed'] = elapsed;
7899			deferred.resolve(mediaTimes);
7900		}
7901		else {
7902			this.getDuration().then(function(duration) {
7903				mediaTimes['duration'] = thisObj.roundDown(duration,6);
7904				thisObj.getElapsed().then(function(elapsed) {
vendor: 4,077 bytes, lines 7905-8065
7905					mediaTimes['elapsed'] = thisObj.roundDown(elapsed,6);
7906					deferred.resolve(mediaTimes);
7907				});
7908			});
7909		}
7910		return promise;
7911	};
7912
7913	AblePlayer.prototype.getDuration = function () {
7914
7915		// returns duration of the current media, expressed in seconds
7916		// function is called by getMediaTimes, and return value is sanitized there
7917
7918		var deferred, promise, thisObj;
7919
7920		deferred = new $.Deferred();
7921		promise = deferred.promise();
7922		thisObj = this;
7923
7924		if (this.player === 'vimeo') {
7925			if (this.vimeoPlayer) {
7926				 this.vimeoPlayer.getDuration().then(function(duration) {
7927					if (duration === undefined || isNaN(duration) || duration === -1) {
7928						deferred.resolve(0);
7929					}
7930					else {
7931						deferred.resolve(duration);
7932					}
7933				});
7934			}
7935			else { // vimeoPlayer hasn't been initialized yet.
7936				deferred.resolve(0);
7937			}
7938		}
7939		else {
7940			var duration;
7941			if (this.player === 'html5') {
7942				duration = this.media.duration;
7943			}
7944			else if (this.player === 'youtube') {
7945				if (this.youTubePlayer) {
7946					duration = this.youTubePlayer.getDuration();
7947				}
7948				else { // the YouTube player hasn't initialized yet
7949					duration = 0;
7950				}
7951			}
7952			if (duration === undefined || isNaN(duration) || duration === -1) {
7953				deferred.resolve(0);
7954			}
7955			else {
7956				deferred.resolve(duration);
7957			}
7958		}
7959		return promise;
7960	};
7961
7962	AblePlayer.prototype.getElapsed = function () {
7963
7964		// returns elapsed time of the current media, expressed in seconds
7965		// function is called by getMediaTimes, and return value is sanitized there
7966
7967		var deferred, promise, thisObj;
7968
7969		deferred = new $.Deferred();
7970		promise = deferred.promise();
7971		thisObj = this;
7972
7973		if (this.player === 'vimeo') {
7974			if (this.vimeoPlayer) {
7975				this.vimeoPlayer.getCurrentTime().then(function(elapsed) {
7976					if (elapsed === undefined || isNaN(elapsed) || elapsed === -1) {
7977						deferred.resolve(0);
7978					}
7979					else {
7980						deferred.resolve(elapsed);
7981					}
7982				});
7983			}
7984			else { // vimeoPlayer hasn't been initialized yet.
7985				deferred.resolve(0);
7986			}
7987		}
7988		else {
7989			var elapsed;
7990			if (this.player === 'html5') {
7991				elapsed = this.media.currentTime;
7992			}
7993			else if (this.player === 'youtube') {
7994				if (this.youTubePlayer) {
7995					elapsed = this.youTubePlayer.getCurrentTime();
7996				}
7997				else { // the YouTube player hasn't initialized yet
7998					elapsed = 0;
7999				}
8000			}
8001			if (elapsed === undefined || isNaN(elapsed) || elapsed === -1) {
8002				deferred.resolve(0);
8003			}
8004			else {
8005				deferred.resolve(elapsed);
8006			}
8007		}
8008		return promise;
8009	};
8010
8011	AblePlayer.prototype.getPlayerState = function () {
8012
8013		// Returns one of the following states:
8014		//	'stopped' - Not yet played for the first time, or otherwise reset to unplayed.
8015		//	'ended' - Finished playing.
8016		//	'paused' - Not playing, but not stopped or ended.
8017		//	'buffering' - Momentarily paused to load, but will resume once data is loaded.
8018		//	'playing' - Currently playing.
8019
8020		// Commented out the following in 3.2.1 - not sure of its intended purpose
8021		// It can be useful to know player state even when swapping src
8022		// and the overhead is seemingly minimal
8023		/*
8024		if (this.swappingSrc) {
8025			return;
8026		}
8027		*/
8028		var deferred, promise, thisObj, duration, elapsed;
8029		deferred = new $.Deferred();
8030		promise = deferred.promise();
8031		thisObj = this;
8032
8033		if (this.player === 'html5') {
8034			if (this.media.ended) {
8035				deferred.resolve('ended');
8036			}
8037			else if (this.media.paused) {
8038  		  deferred.resolve('paused');
8039			}
8040			else if (this.media.readyState !== 4) {
8041				deferred.resolve('buffering');
8042			}
8043			else {
8044				deferred.resolve('playing');
8045			}
8046		}
8047		else if (this.player === 'youtube' && this.youTubePlayer) {
8048			var state = this.youTubePlayer.getPlayerState();
8049			if (state === -1 || state === 5) {
8050				deferred.resolve('stopped');
8051			}
8052			else if (state === 0) {
8053				deferred.resolve('ended');
8054			}
8055			else if (state === 1) {
8056				deferred.resolve('playing');
8057			}
8058			else if (state === 2) {
8059				deferred.resolve('paused');
8060			}
8061			else if (state === 3) {
8062				deferred.resolve('buffering');
8063			}
8064		}
8065		else if (this.player === 'vimeo' && this.vimeoPlayer) {
8066				// curiously, Vimeo's API has no getPlaying(), getBuffering(), or getState() methods
8067			// so maybe if it's neither paused nor ended, it must be playing???
8068			this.vimeoPlayer.getPaused().then(function(paused) {
8069				if (paused) {
8070					deferred.resolve('paused');
8071				}
8072				else {
8073					thisObj.vimeoPlayer.getEnded().then(function(ended) {
8074						if (ended) {
8075							deferred.resolve('ended');
8076						}
8077						else {
8078							deferred.resolve('playing');
8079						}
8080					});
8081				}
8082			});
8083		}
8084		return promise;
8085	};
8086
8087	AblePlayer.prototype.isPlaybackRateSupported = function () {
8088
8089		if (this.player === 'html5') {
8090			if (this.media.playbackRate) {
8091				return true;
8092			}
8093			else {
8094				return false;
8095			}
8096		}
8097		else if (this.player === 'youtube') {
8098			// Youtube supports varying playback rates per video.	 Only expose controls if more than one playback rate is available.
8099			if (this.youTubePlayer.getAvailablePlaybackRates().length > 1) {
8100				return true;
8101			}
8102			else {
8103				return false;
8104			}
8105		}
8106		else if (this.player === 'vimeo') {
8107			// since this takes longer to determine, it was set previously in initVimeoPlayer()
8108			return this.vimeoSupportsPlaybackRateChange;
8109		}
8110	};
8111
8112	AblePlayer.prototype.setPlaybackRate = function (rate) {
8113
8114		rate = Math.max(0.5, rate);
8115		if (this.player === 'html5') {
8116			this.media.playbackRate = rate;
8117		}
8118		else if (this.player === 'youtube') {
8119			this.youTubePlayer.setPlaybackRate(rate);
8120		}
8121		else if (this.player === 'vimeo') {
8122			this.vimeoPlayer.setPlaybackRate(rate);
8123		}
8124		if (this.hasSignLanguage && this.signVideo) {
8125			this.signVideo.playbackRate = rate;
8126		}
8127		this.playbackRate = rate;
8128		this.$speed.text(this.tt.speed + ': ' + rate.toFixed(2).toString() + 'x');
8129	};
8130
8131	AblePlayer.prototype.getPlaybackRate = function () {
8132
8133		if (this.player === 'html5') {
8134			return this.media.playbackRate;
8135		}
8136		else if (this.player === 'youtube') {
8137			return this.youTubePlayer.getPlaybackRate();
8138		}
8139	};
8140
8141	AblePlayer.prototype.isPaused = function () {
8142
8143		 // Note there are three player states that count as paused in this sense,
8144		// and one of them is named 'paused'.
8145		// A better name would be 'isCurrentlyNotPlayingOrBuffering'
8146
8147		var state;
8148
8149		if (this.player === 'vimeo') {
8150			// just rely on value of this.playing
8151			if (this.playing) {
8152				return false;
8153			}
8154			else {
8155				return true;
8156			}
8157		}
8158		else {
8159			this.getPlayerState().then(function(state) {
8160				// if any of the following is true, consider the media 'paused'
8161				return state === 'paused' || state === 'stopped' || state === 'ended';
8162			});
8163		}
8164	};
8165
8166	AblePlayer.prototype.pauseMedia = function () {
8167
8168		var thisObj = this;
8169
8170		if (this.player === 'html5') {
8171			this.media.pause(true);
8172			if (this.hasSignLanguage && this.signVideo) {
8173				this.signVideo.pause(true);
8174			}
8175		}
8176		else if (this.player === 'youtube') {
8177			this.youTubePlayer.pauseVideo();
8178		}
8179		else if (this.player === 'vimeo') {
8180			this.vimeoPlayer.pause();
8181		}
8182	};
8183
8184	AblePlayer.prototype.playMedia = function () {
8185
8186		var thisObj = this;
8187
8188		if (this.player === 'html5') {
8189			this.media.play(true);
8190			if (this.hasSignLanguage && this.signVideo) {
8191				this.signVideo.play(true);
8192			}
8193		}
8194		else if (this.player === 'youtube') {
8195			this.youTubePlayer.playVideo();
8196			if (typeof this.$posterImg !== 'undefined') {
8197				this.$posterImg.hide();
8198			}
8199			this.stoppingYouTube = false;
8200		}
8201		else if (this.player === 'vimeo') {
8202			 this.vimeoPlayer.play();
8203		}
8204		this.startedPlaying = true;
8205		if (this.hideControls) {
8206			// wait briefly after playback begins, then hide controls
8207			this.hidingControls = true;
8208			this.invokeHideControlsTimeout();
8209		}
8210	};
8211
8212	AblePlayer.prototype.fadeControls = function(direction) {
8213
8214		// Visibly fade controls without hiding them from screen reader users
8215
8216		// direction is either 'out' or 'in'
8217
8218		// TODO: This still needs work.
8219		// After the player fades, it's replaced by an empty space
8220		// Would be better if the video and captions expanded to fill the void
8221		// Attempted to fade out to 0 opacity, then move the playerDiv offscreen
8222		// and expand the mediaContainer to fill the vacated space
8223		// However, my attempts to do this have been choppy and buggy
8224		// Code is preserved below and commented out
8225
8226		var thisObj, mediaHeight, playerHeight, newMediaHeight;
8227		var thisObj = this;
8228
8229		if (direction == 'out') {
8230			// get the original height of two key components:
8231			mediaHeight = this.$mediaContainer.height();
8232			playerHeight = this.$playerDiv.height();
8233			newMediaHeight = mediaHeight + playerHeight;
8234
8235			// fade slowly to transparency
8236			this.$playerDiv.fadeTo(2000,0,function() {
8237				/*
8238				// when finished, position playerDiv offscreen
8239				// thisObj.$playerDiv.addClass('able-offscreen');
8240				// Expand the height of mediaContainer to fill the void (needs work)
8241				thisObj.$mediaContainer.animate({
8242					height: newMediaHeight
8243				},500);
8244				*/
8245			});
8246		}
8247		else if (direction == 'in') {
8248			// restore vidcapContainer to its original height (needs work)
8249			// this.$mediaContainer.removeAttr('style');
8250			// fade relatively quickly back to its original position with full opacity
8251			// this.$playerDiv.removeClass('able-offscreen').fadeTo(100,1);
8252			this.$playerDiv.fadeTo(100,1);
8253		}
8254	};
8255
8256	AblePlayer.prototype.invokeHideControlsTimeout = function () {
8257
8258		// invoke timeout for waiting a few seconds after a mouse move or key down
8259		// before hiding controls again
8260		var thisObj = this;
8261		this.hideControlsTimeout = window.setTimeout(function() {
8262			if (typeof thisObj.playing !== 'undefined' && thisObj.playing === true && thisObj.hideControls) {
8263				thisObj.fadeControls('out');
8264				thisObj.controlsHidden = true;
8265			}
8266		},5000);
8267		this.hideControlsTimeoutStatus = 'active';
8268	};
8269
8270	AblePlayer.prototype.refreshControls = function(context, duration, elapsed) {
8271
8272		// context is one of the following:
8273		// 'init' - initial build (or subsequent change that requires full rebuild)
8274		// 'timeline' - a change may effect time-related controls
8275		// 'captions' - a change may effect caption-related controls
8276		// 'descriptions' - a change may effect description-related controls
8277		// 'transcript' - a change may effect the transcript window or button
8278		// 'fullscreen' - a change has been triggered by full screen toggle
8279		// 'playpause' - a change triggered by either a 'play' or 'pause' event
8280
8281		// NOTE: context is not currently supported.
8282		// The steps in this function have too many complex interdependencies
8283		// The gains in efficiency are offset by the possibility of introducing bugs
8284		// For now, executing everything
8285		context = 'init';
8286
8287		// duration and elapsed are passed from callback functions of Vimeo API events
8288		// duration is expressed as sss.xxx
8289		// elapsed is expressed as sss.xxx
8290
8291		var thisObj, duration, elapsed, lastChapterIndex, displayElapsed,
8292			updateLive, textByState, timestamp, widthUsed,
8293			leftControls, rightControls, seekbarWidth, seekbarSpacer, captionsCount,
8294			buffered, newTop, statusBarHeight, speedHeight, statusBarWidthBreakpoint,
8295			newSvgData;
8296
8297		thisObj = this;
8298		if (this.swappingSrc) {
8299  		if (this.playing) {
8300  			// wait until new source has loaded before refreshing controls
8301  			// can't wait if player is NOT playing because some critical events
8302  			// won't fire until playback of new media starts
8303        return;
8304		  }
8305		}
8306
8307		if (context === 'timeline' || context === 'init') {
8308			// all timeline-related functionality requires both duration and elapsed
8309			if (typeof this.duration === 'undefined') {
8310			 	// wait until duration is known before proceeding with refresh
8311			 	return;
8312			}
8313			if (this.useChapterTimes) {
8314				this.chapterDuration = this.getChapterDuration();
8315				this.chapterElapsed = this.getChapterElapsed();
8316			}
8317
8318      if (this.useFixedSeekInterval === false && this.seekIntervalCalculated === false && this.duration > 0) {
8319  		  // couldn't calculate seekInterval previously; try again.
8320        this.setSeekInterval();
8321		  }
8322
8323			if (this.seekBar) {
8324				if (this.useChapterTimes) {
8325					lastChapterIndex = this.selectedChapters.cues.length-1;
8326					if (this.selectedChapters.cues[lastChapterIndex] == this.currentChapter) {
8327						// this is the last chapter
8328						if (this.currentChapter.end !== this.duration) {
8329							// chapter ends before or after video ends
8330							// need to adjust seekbar duration to match video end
8331							this.seekBar.setDuration(this.duration - this.currentChapter.start);
8332						}
8333						else {
8334							this.seekBar.setDuration(this.chapterDuration);
8335						}
8336					}
8337					else {
8338						// this is not the last chapter
8339						this.seekBar.setDuration(this.chapterDuration);
8340					}
8341				}
8342				else {
8343					if (!(this.duration === undefined || isNaN(this.duration) || this.duration === -1)) {
8344						this.seekBar.setDuration(this.duration);
8345					}
8346				}
8347				if (!(this.seekBar.tracking)) {
8348					// Only update the aria live region if we have an update pending
8349					// (from a seek button control) or if the seekBar has focus.
8350					// We use document.activeElement instead of $(':focus') due to a strange bug:
8351					// 	When the seekHead element is focused, .is(':focus') is failing and $(':focus') is returning an undefined element.
8352					updateLive = this.liveUpdatePending || this.seekBar.seekHead.is($(document.activeElement));
8353					this.liveUpdatePending = false;
8354					if (this.useChapterTimes) {
8355						this.seekBar.setPosition(this.chapterElapsed, updateLive);
8356					}
8357					else {
8358						this.seekBar.setPosition(this.elapsed, updateLive);
8359					}
8360				}
8361
8362				// When seeking, display the seek bar time instead of the actual elapsed time.
8363				if (this.seekBar.tracking) {
8364					displayElapsed = this.seekBar.lastTrackPosition;
8365				}
8366				else {
8367					if (this.useChapterTimes) {
8368						displayElapsed = this.chapterElapsed;
8369					}
8370					else {
8371						displayElapsed = this.elapsed;
8372					}
8373				}
8374			}
8375			// update elapsed & duration
8376			if (typeof this.$durationContainer !== 'undefined') {
8377				if (this.useChapterTimes) {
8378					this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.chapterDuration));
8379				}
8380				else {
8381					this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.duration));
8382				}
8383			}
8384			if (typeof this.$elapsedTimeContainer !== 'undefined') {
8385				this.$elapsedTimeContainer.text(this.formatSecondsAsColonTime(displayElapsed));
8386			}
8387
8388			if (this.skin === 'legacy') {
8389  			// Update seekbar width.
8390        // To do this, we need to calculate the width of all buttons surrounding it.
8391        if (this.seekBar) {
8392				  widthUsed = 0;
8393          leftControls = this.seekBar.wrapperDiv.parent().prev('div.able-left-control
8393s');
8394          rightControls = leftControls.next('div.able-right-controls');
8395          leftControls.children().each(function () {
8396					  if ($(this).attr('role')=='button') {
8397						  widthUsed += $(this).outerWidth(true); // true = include margin
8398					  }
8399				  });
8400          rightControls.children().each(function () {
8401					  if ($(this).attr('role')=='button') {
8402						  widthUsed += $(this).outerWidth(true);
8403					  }
8404				  });
8405          if (this.fullscreen) {
8406					  seekbarWidth = $(window).width() - widthUsed;
8407				  }
8408          else {
8409					  seekbarWidth = this.$ableWrapper.width() - widthUsed;
8410				  }
8411          // Sometimes some minor fluctuations based on browser weirdness, so set a threshold.
8412          if (Math.abs(seekbarWidth - this.seekBar.getWidth()) > 5) {
8413					  this.seekBar.setWidth(seekbarWidth);
8414				  }
8415			  }
8416      }
8417
8418			// Update buffering progress.
8419			// TODO: Currently only using the first HTML5 buffered interval,
8420			// but this fails sometimes when buffering is split into two or more intervals.
8421			if (this.player === 'html5') {
8422				if (this.media.buffered.length > 0) {
8423					buffered = this.media.buffered.end(0);
8424					if (this.useChapterTimes) {
8425						if (buffered > this.chapterDuration) {
8426							buffered = this.chapterDuration;
8427						}
8428						if (this.seekBar) {
8429							this.seekBar.setBuffered(buffered / this.chapterDuration);
8430						}
8431					}
8432					else {
8433						if (this.seekBar) {
8434              if (!isNaN(buffered)) {
8435  							this.seekBar.setBuffered(buffered / duration);
8436  						}
8437						}
8438					}
8439				}
8440			}
8441			else if (this.player === 'youtube') {
8442				if (this.seekBar) {
8443					this.seekBar.setBuffered(this.youTubePlayer.getVideoLoadedFraction());
8444				}
8445			}
8446			else if (this.player === 'vimeo') {
8447				// TODO: Add support for Vimeo buffering update
8448			}
8449		} // end if context == 'timeline' or 'init'
8450
8451		if (context === 'descriptions' || context == 'init'){
8452
8453			if (this.$descButton) {
8454				if (this.descOn) {
8455					this.$descButton.removeClass('buttonOff').attr('aria-label',this.tt.turnOffDescriptions);
8456					this.$descButton.find('span.able-clipped').text(this.tt.turnOffDescriptions);
8457				}
8458				else {
8459					this.$descButton.addClass('buttonOff').attr('aria-label',this.tt.turnOnDescriptions);
8460					this.$descButton.find('span.able-clipped').text(this.tt.turnOnDescriptions);
8461				}
8462			}
8463		}
8464
8465		if (context === 'captions' || context == 'init'){
8466
8467			if (this.$ccButton) {
8468
8469				captionsCount = this.captions.length;
8470
8471				// Button has a different title depending on the number of captions.
8472				// If only one caption track, this is "Show captions" and "Hide captions"
8473				// Otherwise, it is just always "Captions"
8474				if (!this.captionsOn) {
8475					this.$ccButton.addClass('buttonOff');
8476					if (captionsCount === 1) {
8477						this.$ccButton.attr('aria-label',this.tt.showCaptions);
8478						this.$ccButton.find('span.able-clipped').text(this.tt.showCaptions);
8479					}
8480				}
8481				else {
8482					this.$ccButton.removeClass('buttonOff');
8483					if (captionsCount === 1) {
8484						this.$ccButton.attr('aria-label',this.tt.hideCaptions);
8485						this.$ccButton.find('span.able-clipped').text(this.tt.hideCaptions);
8486					}
8487				}
8488
8489				if (captionsCount > 1) {
8490					this.$ccButton.attr({
8491						'aria-label': this.tt.captions,
8492						'aria-haspopup': 'true',
8493						'aria-controls': this.mediaId + '-captions-menu'
8494					});
8495					this.$ccButton.find('span.able-clipped').text(this.tt.captions);
8496				}
8497			}
8498		}
8499
8500		if (context === 'fullscreen' || context == 'init'){
8501
8502			if (this.$fullscreenButton) {
8503				if (!this.fullscreen) {
8504					this.$fullscreenButton.attr('aria-label', this.tt.enterFullScreen);
8505					if (this.iconType === 'font') {
8506						this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-collapse').addClass('icon-fullscreen-expand');
8507						this.$fullscreenButton.find('span.able-clipped').text(this.tt.enterFullScreen);
8508					}
8509					else if (this.iconType === 'svg') {
8510						newSvgData = this.getSvgData('fullscreen-expand');
8511						this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
8512						this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
8513					}
8514					else {
8515						this.$fullscreenButton.find('img').attr('src',this.fullscreenExpandButtonImg);
8516					}
8517				}
8518				else {
8519					this.$fullscreenButton.attr('aria-label',this.tt.exitFullScreen);
8520					if (this.iconType === 'font') {
8521						this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-expand').addClass('icon-fullscreen-collapse');
8522						this.$fullscreenButton.find('span.able-clipped').text(this.tt.exitFullScreen);
8523					}
8524					else if (this.iconType === 'svg') {
8525						newSvgData = this.getSvgData('fullscreen-collapse');
8526						this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
8527						this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
8528					}
8529					else {
8530						this.$fullscreenButton.find('img').attr('src',this.fullscreenCollapseButtonImg);
8531					}
8532				}
8533			}
8534		}
8535		if (context === 'playpause' || context == 'init'){
8536			if (typeof this.$bigPlayButton !== 'undefined' && typeof this.seekBar !== 'undefined') {
8537				// Choose show/hide for big play button and adjust position.
8538				if (this.paused && !this.seekBar.tracking) {
8539					if (!this.hideBigPlayButton) {
8540						this.$bigPlayButton.show();
8541					}
8542					if (this.fullscreen) {
8543						this.$bigPlayButton.width($(window).width());
8544						this.$bigPlayButton.height($(window).height());
8545					}
8546					else {
8547						this.$bigPlayButton.width(this.$mediaContainer.width());
8548						this.$bigPlayButton.height(this.$mediaContainer.height());
8549					}
8550				}
8551				else {
8552					this.$bigPlayButton.hide();
8553				}
8554			}
8555		}
8556
8557		if (context === 'transcript' || context == 'init'){
8558
8559			if (this.transcriptType) {
8560				// Sync checkbox and autoScrollTranscript with user preference
8561				if (this.prefAutoScrollTranscript === 1) {
8562					this.autoScrollTranscript = true;
8563					this.$autoScrollTranscriptCheckbox.prop('checked',true);
8564				}
8565				else {
8566					this.autoScrollTranscript = false;
8567					this.$autoScrollTranscriptCheckbox.prop('checked',false);
8568				}
8569
8570				// If transcript locked, scroll transcript to current highlight location.
8571				if (this.autoScrollTranscript && this.currentHighlight) {
8572					newTop = Math.floor(this.$transcriptDiv.scrollTop() +
8573						$(this.currentHighlight).position().top -
8574						(this.$transcriptDiv.height() / 2) +
8575						($(this.currentHighlight).height() / 2));
8576					if (newTop !== Math.floor(this.$transcriptDiv.scrollTop())) {
8577						// Set a flag to ignore the coming scroll event.
8578						// there's no other way I know of to differentiate programmatic and user-initiated scroll events.
8579						this.scrollingTranscript = true;
8580						// only scroll once after moving a highlight
8581						if (this.movingHighlight) {
8582							this.$transcriptDiv.scrollTop(newTop);
8583			                this.movingHighlight = false;
8584			            }
8585					}
8586				}
8587			}
8588		}
8589
8590		if (context === 'init') {
8591
8592			if (this.$chaptersButton) {
8593				this.$chaptersButton.attr({
8594					'aria-label': this.tt.chapters,
8595					'aria-haspopup': 'true',
8596					'aria-controls': this.mediaId + '-chapters-menu'
8597				});
8598			}
8599		}
8600
8601		if (context === 'timeline' || context === 'playpause' || context === 'init') {
8602
8603			// update status
8604			textByState = {
8605				'stopped': this.tt.statusStopped,
8606				'paused': this.tt.statusPaused,
8607				'playing': this.tt.statusPlaying,
8608				'buffering': this.tt.statusBuffering,
8609				'ended': this.tt.statusEnd
8610			};
8611
8612			if (this.stoppingYouTube) {
8613				// stoppingYouTube is true temporarily while video is paused and seeking to 0
8614				// See notes in handleRestart()
8615				// this.stoppingYouTube will be reset when seek to 0 is finished (in event.js > onMediaUpdateTime())
8616				if (this.$status.text() !== this.tt.statusStopped) {
8617					this.$status.text(this.tt.statusStopped);
8618				}
8619				if (this.$playpauseButton.find('span').first().hasClass('icon-pause')) {
8620					if (this.iconType === 'font') {
8621						this.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
8622						this.$playpauseButton.find('span.able-clipped').text(this.tt.play);
8623					}
8624					else if (this.iconType === 'svg') {
8625						newSvgData = this.getSvgData('play');
8626						this.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
8627						this.$playpauseButton.find('path').attr('d',newSvgData[1]);
8628					}
8629					else {
8630						this.$playpauseButton.find('img').attr('src',this.playButtonImg);
8631					}
8632				}
8633			}
8634			else {
8635				if (typeof this.$status !== 'undefined' && typeof this.seekBar !== 'undefined') {
8636					// Update the text only if it's changed since it has role="alert";
8637					// also don't update while tracking, since this may Pause/Play the player but we don't want to send a Pause/Play update.
8638					this.getPlayerState().then(function(currentState) {
8639						if (thisObj.$status.text() !== textByState[currentState] && !thisObj.seekBar.tracking) {
8640							// Debounce updates; only update after status has stayed steadily different for a while
8641							// "A while" is defined differently depending on context
8642							if (thisObj.swappingSrc) {
8643                // this is where most of the chatter occurs (e.g., playing, paused, buffering, playing),
8644                // so set a longer wait time before writing a status message
8645                if (!thisObj.debouncingStatus) {
8646                  thisObj.statusMessageThreshold = 2000; // in ms (2 seconds)
8647                }
8648              }
8649              else {
8650                // for all other contexts (e.g., users clicks Play/Pause)
8651                // user should receive more rapid feedback
8652                if (!thisObj.debouncingStatus) {
8653                  thisObj.statusMessageThreshold = 250; // in ms
8654                }
8655              }
8656  						timestamp = (new Date()).getTime();
8657							if (!thisObj.statusDebounceStart) {
8658								thisObj.statusDebounceStart = timestamp;
8659								// Call refreshControls() again after allotted time has passed
8660								thisObj.debouncingStatus = true;
8661								thisObj.statusTimeout = setTimeout(function () {
8662                  thisObj.debouncingStatus = false;
8663									thisObj.refreshControls(context);
8664								}, thisObj.statusMessageThreshold);
8665							}
8666							else if ((timestamp - thisObj.statusDebounceStart) > thisObj.statusMessageThreshold) {
8667    						thisObj.$status.text(textByState[currentState]);
8668                thisObj.statusDebounceStart = null;
8669                clearTimeout(thisObj.statusTimeout);
8670                thisObj.statusTimeout = null;
8671						  }
8672						}
8673						else {
8674							thisObj.statusDebounceStart = null;
8675							thisObj.debouncingStatus = false;
8676							clearTimeout(thisObj.statusTimeout);
8677							thisObj.statusTimeout = null;
8678						}
8679						// Don't change play/pause button display while using the seek bar (or if YouTube stopped)
8680						if (!thisObj.seekBar.tracking && !thisObj.stoppingYouTube) {
8681							if (currentState === 'paused' || currentState === 'stopped' || currentState === 'ended') {
8682								thisObj.$playpauseButton.attr('aria-label',thisObj.tt.play);
8683
8684								if (thisObj.iconType === 'font') {
8685									thisObj.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
8686									thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.play);
8687								}
8688								else if (thisObj.iconType === 'svg') {
8689									newSvgData = thisObj.getSvgData('play');
8690									thisObj.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
8691									thisObj.$playpauseButton.find('path').attr('d',newSvgData[1]);
8692								}
8693								else {
8694									thisObj.$playpauseButton.find('img').attr('src',thisObj.playButtonImg);
8695								}
8696							}
8697							else {
8698								thisObj.$playpauseButton.attr('aria-label',thisObj.tt.pause);
8699
8700								if (thisObj.iconType === 'font') {
8701									thisObj.$playpauseButton.find('span').first().removeClass('icon-play').addClass('icon-pause');
8702									thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.pause);
8703								}
8704								else if (thisObj.iconType === 'svg') {
8705									newSvgData = thisObj.getSvgData('pause');
8706									thisObj.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
8707									thisObj.$playpauseButton.find('path').attr('d',newSvgData[1]);
8708								}
8709								else {
8710									thisObj.$playpauseButton.find('img').attr('src',thisObj.pauseButtonImg);
8711								}
8712							}
8713						}
8714					});
8715				}
8716			}
8717		}
8718
8719		// Show/hide status bar content conditionally
8720		if (!this.fullscreen) {
8721			statusBarWidthBreakpoint = 300;
8722			statusBarHeight = this.$statusBarDiv.height();
8723			speedHeight = this.$statusBarDiv.find('span.able-speed').height();
8724			if (speedHeight > (statusBarHeight + 5)) {
8725				// speed bar is wrapping (happens often in German player)
8726				this.$statusBarDiv.find('span.able-speed').hide();
8727				this.hidingSpeed = true;
8728			}
8729			else {
8730				if (this.hidingSpeed) {
8731					this.$statusBarDiv.find('span.able-speed').show();
8732					this.hidingSpeed = false;
8733				}
8734				if (this.$statusBarDiv.width() < statusBarWidthBreakpoint) {
8735					// Player is too small for a speed span
8736					this.$statusBarDiv.find('span.able-speed').hide();
8737					this.hidingSpeed = true;
8738				}
8739				else {
8740					if (this.hidingSpeed) {
8741						this.$statusBarDiv.find('span.able-speed').show();
8742						this.hidingSpeed = false;
8743					}
8744				}
8745			}
8746		}
8747
8748	};
8749
8750	AblePlayer.prototype.getHiddenWidth = function($el) {
8751
8752		// jQuery returns for width() if element is hidden
8753		// this function is a workaround
8754
8755		// save a reference to a cloned element that can be measured
8756		var $hiddenElement = $el.clone().appendTo('body');
8757
8758		// calculate the width of the clone
8759		var width = $hiddenElement.outerWidth();
8760
8761		// remove the clone from the DOM
8762		$hiddenElement.remove();
8763
8764		return width;
8765	};
8766
8767	AblePlayer.prototype.handlePlay = function(e) {
8768
8769		if (this.paused) {
8770			this.playMedia();
8771		}
8772		else {
8773			this.pauseMedia();
8774		}
8775	};
8776
8777	AblePlayer.prototype.handleRestart = function() {
8778
8779		this.seekTo(0);
8780	};
8781
8782	AblePlayer.prototype.handlePrevTrack = function() {
8783
8784    if (this.playlistIndex === 0) {
8785      // currently on the first track
8786      // wrap to bottom and play the last track
8787      this.playlistIndex = this.$playlist.length - 1;
8788    }
8789    else {
8790		  this.playlistIndex--;
8791    }
8792		this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
8793    this.cuePlaylistItem(this.playlistIndex);
8794	};
8795
8796	AblePlayer.prototype.handleNextTrack = function() {
8797
8798    if (this.playlistIndex === this.$playlist.length - 1) {
8799      // currently on the last track
8800      // wrap to top and play the forst track
8801      this.playlistIndex = 0;
8802    }
8803    else {
8804		  this.playlistIndex++;
8805    }
8806		this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
8807    this.cuePlaylistItem(this.playlistIndex);
8808	};
8809
8810	AblePlayer.prototype.handleRewind = function() {
8811
8812		var targetTime;
8813
8814		targetTime = this.elapsed - this.seekInterval;
8815		if (this.useChapterTimes) {
8816			if (targetTime < this.currentChapter.start) {
8817				targetTime = this.currentChapter.start;
8818			}
8819		}
8820		else {
8821			if (targetTime < 0) {
8822				targetTime = 0;
8823			}
8824		}
8825		this.seekTo(targetTime);
8826	};
8827
8828	AblePlayer.prototype.handleFastForward = function() {
8829
8830		var targetTime, lastChapterIndex;
8831
8832		lastChapterIndex = this.chapters.length-1;
8833		targetTime = this.elapsed + this.seekInterval;
8834		if (this.useChapterTimes) {
8835			if (this.chapters[lastChapterIndex] == this.currentChapter) {
8836				// this is the last chapter
8837				if (targetTime > this.duration || targetTime > this.currentChapter.end) {
8838					// targetTime would exceed the end of the video (or chapter)
8839					// scrub to end of whichever is earliest
8840					targetTime = Math.min(this.duration, this.currentChapter.end);
8841				}
8842				else if (this.duration % targetTime < this.seekInterval) {
8843					// nothing left but pocket change after seeking to targetTime
8844					// go ahead and seek to end of video (or chapter), whichever is earliest
8845					targetTime = Math.min(this.duration, this.currentChapter.end);
8846				}
8847			}
8848			else {
8849				// this is not the last chapter
8850				if (targetTime > this.currentChapter.end) {
8851					// targetTime would exceed the end of the chapter
8852					// scrub exactly to end of chapter
8853					targetTime = this.currentChapter.end;
8854				}
8855			}
8856		}
8857		else {
8858			// not using chapter times
8859			if (targetTime > this.duration) {
8860				targetTime = this.duration;
8861			}
8862		}
8863		this.seekTo(targetTime);
8864	};
8865
8866	AblePlayer.prototype.handleRateIncrease = function() {
8867		this.changeRate(1);
8868	};
8869
8870	AblePlayer.prototype.handleRateDecrease = function() {
8871		this.changeRate(-1);
8872	};
8873
8874	// Increases or decreases playback rate, where dir is 1 or -1 indication direction.
8875	AblePlayer.prototype.changeRate = function (dir) {
8876
8877		var rates, currentRate, index, newRate, vimeoMin, vimeoMax;
8878
8879		if (this.player === 'html5') {
8880			this.setPlaybackRate(this.getPlaybackRate() + (0.25 * dir));
8881		}
8882		else if (this.player === 'youtube') {
8883			rates = this.youTubePlayer.getAvailablePlaybackRates();
8884			currentRate = this.getPlaybackRate();
8885			index = rates.indexOf(currentRate);
8886			if (index === -1) {
8887				console.log('ERROR: Youtube returning unknown playback rate ' + currentRate.toString());
8888			}
8889			else {
8890				index += dir;
8891				// Can only increase or decrease rate if there's another rate available.
8892				if (index < rates.length && index >= 0) {
8893					this.setPlaybackRate(rates[index]);
8894				}
8895			}
8896		}
8897		else if (this.player === 'vimeo') {
8898			// range is 0.5 to 2
8899			// increase/decrease in inrements of 0.5
8900			vimeoMin = 0.5;
8901			vimeoMax = 2;
8902			if (dir === 1) {
8903				if (this.vimeoPlaybackRate + 0.5 <= vimeoMax) {
8904					newRate = this.vimeoPlaybackRate + 0.5;
8905				}
8906				else {
8907					newRate = vimeoMax;
8908				}
8909			}
8910			else if (dir === -1) {
8911				if (this.vimeoPlaybackRate - 0.5 >= vimeoMin) {
8912					newRate = this.vimeoPlaybackRate - 0.5;
8913				}
8914				else {
8915					newRate = vimeoMin;
8916				}
8917			}
8918			this.setPlaybackRate(newRate);
8919		}
8920	};
8921
8922	AblePlayer.prototype.handleCaptionToggle = function() {
8923
8924		var captions;
8925		if (this.hidingPopup) {
8926			// stopgap to prevent spacebar in Firefox from reopening popup
8927			// immediately after closing it
8928			this.hidingPopup = false;
8929			return false;
8930		}
8931		if (this.captions.length) {
8932			captions = this.captions;
8933		}
8934		else {
8935			captions = [];
8936		}
8937		if (captions.length === 1) {
8938			// When there's only one set of captions, just do an on/off toggle.
8939			if (this.captionsOn === true) {
8940				// turn them off
8941				this.captionsOn = false;
8942				this.prefCaptions = 0;
8943				this.updateCookie('prefCaptions');
8944				if (this.usingYouTubeCaptions) {
8945					this.youTubePlayer.unloadModule(this.ytCaptionModule);
8946				}
8947				else {
8948					this.$captionsWrapper.hide();
8949				}
8950			}
8951			else {
8952				// captions are off. Turn them on.
8953				this.captionsOn = true;
8954				this.prefCaptions = 1;
8955				this.updateCookie('prefCaptions');
8956				if (this.usingYouTubeCaptions) {
8957					if (typeof this.ytCaptionModule !== 'undefined') {
8958						this.youTubePlayer.loadModule(this.ytCaptionModule);
8959					}
8960				}
8961				else {
8962					this.$captionsWrapper.show();
8963				}
8964				for (var i=0; i<captions.length; i++) {
8965					if (captions[i].def === true) { // this is the default language
8966						this.selectedCaptions = captions[i];
8967					}
8968				}
8969				this.selectedCaptions = this.captions[0];
8970				if (this.descriptions.length >= 0) {
8971					this.selectedDescriptions = this.descriptions[0];
8972				}
8973			}
8974			this.refreshControls('captions');
8975		}
8976		else {
8977			// there is more than one caption track.
8978			// clicking on a track is handled via caption.js > getCaptionClickFunction()
8979			if (this.captionsPopup && this.captionsPopup.is(':visible')) {
8980				this.captionsPopup.hide();
8981				this.hidingPopup = false;
8982				this.$ccButton.removeAttr('aria-expanded').focus();
8983			}
8984			else {
8985				this.closePopups();
8986				if (this.captionsPopup) {
8987					this.captionsPopup.show();
8988					this.$ccButton.attr('aria-expanded','true');
8989					this.captionsPopup.css('top', this.$ccButton.position().top - this.captionsPopup.outerHeight());
8990					this.captionsPopup.css('left', this.$ccButton.position().left)
8991					// Place focus on the first button (even if another button is checked)
8992					this.captionsPopup.find('li').removeClass('able-focus');
8993					this.captionsPopup.find('li').first().focus().addClass('able-focus');
8994				}
8995			}
8996		}
8997	};
8998
8999	AblePlayer.prototype.handleChapters = function () {
9000		if (this.hidingPopup) {
9001			// stopgap to prevent spacebar in Firefox from reopening popup
9002			// immediately after closing it
9003			this.hidingPopup = false;
9004			return false;
9005		}
9006		if (this.chaptersPopup.is(':visible')) {
9007			this.chaptersPopup.hide();
9008			this.hidingPopup = false;
9009			this.$chaptersButton.removeAttr('aria-expanded').focus();
9010		}
9011		else {
9012			this.closePopups();
9013			this.chaptersPopup.show();
9014			this.$chaptersButton.attr('aria-expanded','true');
9015			this.chaptersPopup.css('top', this.$chaptersButton.position().top - this.chaptersPopup.outerHeight());
9016			this.chaptersPopup.css('left', this.$chaptersButton.position().left)
9017
9018			// Highlight the current chapter, if any chapters are checked
9019			// Otherwise, place focus on the first chapter
9020			this.chaptersPopup.find('li').removeClass('able-focus');
9021			if (this.chaptersPopup.find('li[aria-checked="true"]').length) {
9022				this.chaptersPopup.find('li[aria-checked="true"]').focus().addClass('able-focus');
9023			}
9024			else {
9025				this.chaptersPopup.find('li').first().addClass('able-focus').attr('aria-checked','true').focus();
9026			}
9027		}
9028	};
9029
9030	AblePlayer.prototype.handleDescriptionToggle = function() {
9031
9032		this.descOn = !this.descOn;
9033		this.prefDesc = + this.descOn; // convert boolean to integer
9034		this.updateCookie('prefDesc');
9035		if (!this.$descDiv.is(':hidden')) {
9036			this.$descDiv.hide();
9037		}
9038		this.refreshingDesc = true;
9039		this.initDescription();
9040		this.refreshControls('descriptions');
9041	};
9042
9043	AblePlayer.prototype.handlePrefsClick = function(pref) {
9044
9045		// NOTE: the prefs menu is positioned near the right edge of the player
9046		// This assumes the Prefs button is also positioned in that vicinity
9047		// (last or second-last button the right)
9048		var thisObj, prefsButtonPosition, prefsMenuRight, prefsMenuLeft;
9049
9050		thisObj = this;
9051
9052		if (this.hidingPopup) {
9053			// stopgap to prevent spacebar in Firefox from reopening popup
9054			// immediately after closing it
9055			this.hidingPopup = false;
9056			return false;
9057		}
9058		if (this.prefsPopup.is(':visible')) {
9059			this.prefsPopup.hide();
9060			this.$prefsButton.removeAttr('aria-expanded');
9061			// restore each menu item to original hidden state
9062			this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
9063			if (!this.showingPrefsDialog) {
9064  			this.$prefsButton.focus();
9065			}
9066			// wait briefly, then reset hidingPopup
9067			setTimeout(function() {
9068  			thisObj.hidingPopup = false;
9069  		},100);
9070		}
9071		else {
9072			this.closePopups();
9073			this.prefsPopup.show();
9074			this.$prefsButton.attr('aria-expanded','true');
9075			prefsButtonPosition = this.$prefsButton.position();
9076			prefsMenuRight = this.$ableDiv.width() - 5;
9077			prefsMenuLeft = prefsMenuRight - this.prefsPopup.width();
9078			this.prefsPopup.css('top', prefsButtonPosition.top - this.prefsPopup.outerHeight());
9079			this.prefsPopup.css('left', prefsMenuLeft);
9080			// remove prior focus and set focus on first item; also change tabindex from -1 to 0
9081			this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','0');
9082			this.prefsPopup.find('li').first().focus().addClass('able-focus');
9083
9084		}
9085	};
9086
9087	AblePlayer.prototype.handleHelpClick = function() {
9088		this.setFullscreen(false);
9089		this.helpDialog.show();
9090	};
9091
9092	AblePlayer.prototype.handleTranscriptToggle = function () {
9093
9094  	var thisObj = this;
9095
9096		if (this.$transcriptDiv.is(':visible')) {
9097			this.$transcriptArea.hide();
9098			this.$transcriptButton.addClass('buttonOff').attr('aria-label',this.tt.showTranscript);
9099			this.$transcriptButton.find('span.able-clipped').text(this.tt.showTranscript);
9100			this.prefTranscript = 0;
9101			this.$transcriptButton.focus().addClass('able-focus');
9102      // wait briefly before resetting stopgap var
9103      // otherwise the keypress used to select 'Close' will trigger the transcript button
9104      // Benchmark tests: If this is gonna happen, it typically happens in around 3ms; max 12ms
9105      // Setting timeout to 100ms is a virtual guarantee of proper functionality
9106      setTimeout(function() {
9107        thisObj.closingTranscript = false;
9108      }, 100);
9109		}
9110		else {
9111			this.positionDraggableWindow('transcript');
9112			this.$transcriptArea.show();
9113			// showing transcriptArea has a cascading effect of showing all content *within* transcriptArea
9114			// need to re-hide the popup menu
9115			this.$transcriptPopup.hide();
9116			this.$transcriptButton.removeClass('buttonOff').attr('aria-label',this.tt.hideTranscript);
9117			this.$transcriptButton.find('span.able-clipped').text(this.tt.hideTranscript);
9118			this.prefTranscript = 1;
9119			// move focus to first focusable element (window options button)
9120      this.focusNotClick = true;
9121			this.$transcriptArea.find('button').first().focus();
9122      // wait briefly before resetting stopgap var
9123      setTimeout(function() {
9124        thisObj.focusNotClick = false;
9125      }, 100);
9126		}
9127		this.updateCookie('prefTranscript');
9128	};
9129
9130	AblePlayer.prototype.handleSignToggle = function () {
9131
9132  	var thisObj = this;
9133
9134		if (this.$signWindow.is(':visible')) {
9135			this.$signWindow.hide();
9136			this.$signButton.addClass('buttonOff').attr('aria-label',this.tt.showSign);
9137			this.$signButton.find('span.able-clipped').text(this.tt.showSign);
9138			this.prefSign = 0;
9139			this.$signButton.focus().addClass('able-focus');
9140      // wait briefly before resetting stopgap var
9141      // otherwise the keypress used to select 'Close' will trigger the transcript button
9142      setTimeout(function() {
9143        thisObj.closingSign = false;
9144      }, 100);
9145		}
9146		else {
9147			this.positionDraggableWindow('sign');
9148			this.$signWindow.show();
9149			// showing signWindow has a cascading effect of showing all content *within* signWindow
9150			// need to re-hide the popup menu
9151			this.$signPopup.hide();
9152			this.$signButton.removeClass('buttonOff').attr('aria-label',this.tt.hideSign);
9153			this.$signButton.find('span.able-clipped').text(this.tt.hideSign);
9154			this.prefSign = 1;
9155      this.focusNotClick = true;
9156			this.$signWindow.find('button').first().focus();
9157      // wait briefly before resetting stopgap var
9158      // otherwise the keypress used to select 'Close' will trigger the transcript button
9159      setTimeout(function() {
9160        thisObj.focusNotClick = false;
9161      }, 100);
9162		}
9163		this.updateCookie('prefSign');
9164	};
9165
9166	AblePlayer.prototype.isFullscreen = function () {
9167
9168		// NOTE: This has been largely replaced as of 3.2.5 with a Boolean this.fullscreen,
9169		// which is defined in setFullscreen()
9170		// This function returns true if *any* element is fullscreen
9171		// but doesn't tell us whether a particular element is in fullscreen
9172		// (e.g., if there are multiple players on the page)
9173		// The Boolean this.fullscreen is defined separately for each player instance
9174
9175		if (this.nativeFullscreenSupported()) {
9176			return (document.fullscreenElement ||
9177							document.webkitFullscreenElement ||
9178							document.webkitCurrentFullScreenElement ||
9179							document.mozFullScreenElement ||
9180							document.msFullscreenElement) ? true : false;
9181		}
9182		else {
9183			return this.modalFullscreenActive ? true : false;
9184		}
9185	}
9186
9187	AblePlayer.prototype.setFullscreen = function (fullscreen) {
9188
9189		if (this.fullscreen == fullscreen) {
9190			// replace isFullscreen() with a Boolean. see function for explanation
9191			return;
9192		}
9193		var thisObj = this;
9194		var $el = this.$ableWrapper;
9195		var el = $el[0];
9196
9197		if (this.nativeFullscreenSupported()) {
9198			// Note: many varying names for options for browser compatibility.
9199			if (fullscreen) {
9200				// Initialize fullscreen
9201
9202				// But first, capture current settings so they can be restored later
9203				this.preFullScreenWidth = this.$ableWrapper.width();
9204				this.preFullScreenHeight = this.$ableWrapper.height();
9205				if (el.requestFullscreen) {
9206					el.requestFullscreen();
9207				}
9208				else if (el.webkitRequestFullscreen) {
9209					el.webkitRequestFullscreen();
9210				}
9211				else if (el.mozRequestFullScreen) {
9212					el.mozRequestFullScreen();
9213				}
9214				else if (el.msRequestFullscreen) {
9215					el.msRequestFullscreen();
9216				}
9217				this.fullscreen = true;
9218			}
9219			else {
9220				// Exit fullscreen
9221				if (document.exitFullscreen) {
9222					document.exitFullscreen();
9223				}
9224				else if (document.webkitExitFullscreen) {
9225					document.webkitExitFullscreen();
9226				}
9227				else if (document.webkitCancelFullScreen) {
9228					document.webkitCancelFullScreen();
9229				}
9230				else if (document.mozCancelFullScreen) {
9231					document.mozCancelFullScreen();
9232				}
9233				else if (document.msExitFullscreen) {
9234					document.msExitFullscreen();
9235				}
9236				this.fullscreen = false;
9237			}
9238			// add event handlers for changes in full screen mode
9239			// currently most changes are made in response to windowResize event
9240			// However, that alone is not resulting in a properly restored player size in Opera Mac
9241			// More on the Opera Mac bug: https://github.com/ableplayer/ableplayer/issues/162
9242			// this fullscreen event handler added specifically for Opera Mac,
9243			// but includes event listeners for all browsers in case its functionality could be expanded
9244			// Added functionality in 2.3.45 for handling YouTube return from fullscreen as well
9245			$(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange', function(e) {
9246				// NOTE: e.type = the specific event that fired (in case needing to control for browser-specific idiosyncrasies)
9247				if (!thisObj.fullscreen) {
9248					// user has just exited full screen
9249					thisObj.restoringAfterFullScreen = true;
9250					thisObj.resizePlayer(thisObj.preFullScreenWidth,thisObj.preFullScreenHeight);
9251				}
9252				else if (!thisObj.clickedFullscreenButton) {
9253					// user triggered fullscreenchange without clicking (or pressing) fullscreen button
9254					// this is only possible if they pressed Escape to exit fullscreen mode
9255					thisObj.fullscreen = false;
9256					thisObj.restoringAfterFullScreen = true;
9257					thisObj.resizePlayer(thisObj.preFullScreenWidth,thisObj.preFullScreenHeight);
9258				}
9259				// NOTE: The fullscreenchange (or browser-equivalent) event is triggered twice
9260				// when exiting fullscreen via the "Exit fullscreen" button (only once if using Escape)
9261				// Not sure why, but consequently we need to be sure thisObj.clickedFullScreenButton
9262				// continues to be true through both events
9263				// Could use a counter variable to control that (reset to false after the 2nd trigger)
9264				// However, since I don't know why it's happening, and whether it's 100% reliable
9265				// resetting clickedFullScreenButton after a timeout seems to be better approach
9266				setTimeout(function() {
9267					thisObj.clickedFullscreenButton = false;
9268				},1000);
9269			});
9270		}
9271		else {
9272			// Non-native fullscreen support through modal dialog.
9273			// Create dialog on first run through.
9274			if (!this.fullscreenDialog) {
9275				var $dialogDiv = $('<div>');
9276				// create a hidden alert, communicated to screen readers via aria-describedby
9277				var $fsDialogAlert = $('<p>',{
9278					'class': 'able-screenreader-alert'
9279				}).text(this.tt.fullscreen); // In English: "Full screen"; TODO: Add alert text that is more descriptive
9280				$dialogDiv.append($fsDialogAlert);
9281				// now render this as a dialog
9282				this.fullscreenDialog = new AccessibleDialog($dialogDiv, this.$fullscreenButton, 'dialog', 'Fullscreen video player', $fsDialogAlert, this.tt.exitFullScreen, '100%', true, function () { thisObj.handleFullscreenToggle() });
9283				$('body').append($dialogDiv);
9284			}
9285
9286			// Track whether paused/playing before moving element; moving the element can stop playback.
9287			var wasPaused = this.paused;
9288
9289			if (fullscreen) {
9290				this.modalFullscreenActive = true;
9291				this.fullscreenDialog.show();
9292
9293				// Move player element into fullscreen dialog, then show.
9294				// Put a placeholder element where player was.
9295				this.$modalFullscreenPlaceholder = $('<div class="placeholder">');
9296				this.$modalFullscreenPlaceholder.insertAfter($el);
9297				$el.appendTo(this.fullscreenDialog.modal);
9298
9299				// Column left css is 50% by default; set to 100% for full screen.
9300				if ($el === this.$ableColumnLeft) {
9301					$el.width('100%');
9302				}
9303				var newHeight = $(window).height() - this.$playerDiv.height();
9304				if (!this.$descDiv.is(':hidden')) {
9305					newHeight -= this.$descDiv.height();
9306				}
9307				this.resizePlayer($(window).width(), newHeight);
9308			}
9309			else {
9310				this.modalFullscreenActive = false;
9311				if ($el === this.$ableColumnLeft) {
9312					$el.width('50%');
9313				}
9314				$el.insertAfter(this.$modalFullscreenPlaceholder);
9315				this.$modalFullscreenPlaceholder.remove();
9316				this.fullscreenDialog.hide();
9317				this.resizePlayer(this.$ableWrapper.width(), this.$ableWrapper.height());
9318			}
9319
9320			// Resume playback if moving stopped it.
9321			if (!wasPaused && this.paused) {
9322				this.playMedia();
9323			}
9324		}
9325		this.refreshControls('fullscreen');
9326	};
9327
9328	AblePlayer.prototype.handleFullscreenToggle = function () {
9329
9330		var stillPaused = this.paused;
9331		this.setFullscreen(!this.fullscreen);
9332		if (stillPaused) {
9333			this.pauseMedia(); // when toggling fullscreen and media is just paused, keep media paused.
9334		}
9335		else if (!stillPaused) {
9336			this.playMedia(); // when toggling fullscreen and media is playing, continue playing.
9337		}
9338		// automatically hide controller in fullscreen mode
9339		// then reset back to original setting after exiting fullscreen mode
9340		if (this.fullscreen) {
9341			this.hideControls = true;
9342			if (this.playing) {
9343				// go ahead and hide the controls
9344				this.fadeControls('out');
9345				this.controlsHidden = true;
9346			}
9347		}
9348		else {
9349			// exit fullscreen mode
9350			this.hideControls = this.hideControlsOriginal;
9351			if (!this.hideControls) { // do not hide controls
9352				if (this.controlsHidden) {
9353					this.fadeControls('in');
9354					this.controlsHidden = false;
9355				}
9356				// if there's an active timeout to fade controls out again, clear it
9357				if (this.hideControlsTimeoutStatus === 'active') {
9358					window.clearTimeout(this.hideControlsTimeout);
9359					this.hideControlsTimeoutStatus = 'clear';
9360				}
9361			}
9362		}
9363	};
9364
9365	AblePlayer.prototype.handleTranscriptLockToggle = function (val) {
9366
9367		this.autoScrollTranscript = val; // val is boolean
9368		this.prefAutoScrollTranscript = +val; // convert boolean to numeric 1 or 0 for cookie
9369		this.updateCookie('prefAutoScrollTranscript');
9370		this.refreshControls('transcript');
9371	};
9372
9373
9374	AblePlayer.prototype.showTooltip = function($tooltip) {
9375
9376		if (($tooltip).is(':animated')) {
9377			$tooltip.stop(true,true).show().delay(4000).fadeOut(1000);
9378		}
9379		else {
9380			$tooltip.stop().show().delay(4000).fadeOut(1000);
9381		}
9382	};
9383
9384	AblePlayer.prototype.showAlert = function( msg, location ) {
9385
9386		// location is either of the following:
9387		// 'main' (default)
9388		// 'screenreader (visibly hidden)
9389		// 'sign' (sign language window)
9390		// 'transcript' (trasncript window)
9391		var thisObj, $alertBox, $parentWindow, alertLeft, alertTop;
9392
9393		thisObj = this;
9394
9395		if (location === 'transcript') {
9396			$alertBox = this.$transcriptAlert;
9397			$parentWindow = this.$transcriptArea;
9398		}
9399		else if (location === 'sign') {
9400			$alertBox = this.$signAlert;
9401			$parentWindow = this.$signWindow;
9402		}
9403		else if (location === 'screenreader') {
9404			$alertBox = this.$srAlertBox;
9405		}
9406		else {
9407			$alertBox = this.$alertBox;
9408		}
9409		$alertBox.text(msg).show();
9410		if (location == 'transcript' || location === 'sign') {
9411			if ($parentWindow.width() > $alertBox.width()) {
9412				alertLeft = $parentWindow.width() / 2 - $alertBox.width() / 2;
9413			}
9414			else {
9415				// alert box is wider than its container. Position it far left and let it wrap
9416				alertLeft = 10;
9417			}
9418			if (location === 'sign') {
9419				// position alert in the lower third of the sign window (to avoid covering the signer)
9420				alertTop = ($parentWindow.height() / 3) * 2;
9421			}
9422			else if (location === 'transcript') {
9423				// position alert just beneath the toolbar to avoid getting lost among transcript text
9424				alertTop = this.$transcriptToolbar.height() + 30;
9425			}
9426			$alertBox.css({
9427				top: alertTop + 'px',
9428				left: alertLeft + 'px'
9429			});
9430		}
9431		else if (location !== 'screenreader') {
9432			// The original formula incorporated offset() into the calculation
9433			// but at some point this began resulting in an alert that's off-centered
9434			// Changed in v2.2.17, but here's the original for reference in case needed:
9435			// left: this.$playerDiv.offset().left + (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
9436			$alertBox.css({
9437				left: (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
9438			});
9439		}
9440		if (location !== 'screenreader') {
9441			setTimeout(function () {
9442				$alertBox.fadeOut(300);
9443			}, 3000);
9444		}
9445	};
9446
9447	AblePlayer.prototype.showedAlert = function (which) {
9448
9449		// returns true if the target alert has already been shown
9450		// useful for throttling alerts that only need to be shown once
9451		// e.g., move alerts with instructions for dragging a window
9452
9453		if (which === 'transcript') {
9454			if (this.showedTranscriptAlert) {
9455				return true;
9456			}
9457			else {
9458				return false;
9459			}
9460		}
9461		else if (which === 'sign') {
9462			if (this.showedSignAlert) {
9463				return true;
9464			}
9465			else {
9466				return false;
9467			}
9468		}
9469		return false;
9470	}
9471
9472	// Resizes all relevant player attributes.
9473	AblePlayer.prototype.resizePlayer = function (width, height) {
9474
9475		var captionSizeOkMin, captionSizeOkMax, captionSize, newCaptionSize, newLineHeight;
9476
9477		if (this.fullscreen) { // replace isFullscreen() with a Boolean. see function for explanation
9478			if (typeof this.$vidcapContainer !== 'undefined') {
9479				this.$ableWrapper.css({
9480					'width': width + 'px',
9481					'max-width': ''
9482				})
9483				this.$vidcapContainer.css({
9484					'height': height-50 + 'px',
9485					'width': width
9486				});
9487				this.$media.css({
9488					'height': height + 'px',
9489					'width': width
9490				})
9491			}
9492			if (typeof this.$transcriptArea !== 'undefined') {
9493				this.retrieveOffscreenWindow('transcript',width,height);
9494			}
9495			if (typeof this.$signWindow !== 'undefined') {
9496				this.retrieveOffscreenWindow('sign',width,height);
9497			}
9498		}
9499		else {
9500			// player resized
9501			if (this.restoringAfterFullScreen) {
9502				// User has just exited fullscreen mode. Restore to previous settings
9503				width = this.preFullScreenWidth;
9504				height = this.preFullScreenHeight;
9505				this.restoringAfterFullScreen = false;
9506				this.$ableWrapper.css({
9507					'max-width': width + 'px',
9508					'width': ''
9509				});
9510				if (typeof this.$vidcapContainer !== 'undefined') {
9511					this.$vidcapContainer.css({
9512						'height': '',
9513						'width': ''
9514					});
9515				}
9516				this.$media.css({
9517					'width': '100%',
9518					'height': 'auto'
9519				});
9520			}
9521		}
9522
9523		// resize YouTube
9524		if (this.player === 'youtube' && this.youTubePlayer) {
9525			this.youTubePlayer.setSize(width, height);
9526		}
9527
9528		// Resize captions
9529		if (typeof this.$captionsDiv !== 'undefined') {
9530
9531			// Font-size is too small in full screen view & too large in small-width view
9532			// The following vars define a somewhat arbitary zone outside of which
9533			// caption size requires adjustment
9534			captionSizeOkMin = 400;
9535			captionSizeOkMax = 1000;
9536			captionSize = parseInt(this.prefCaptionsSize,10);
9537
9538			// TODO: Need a better formula so that it scales proportionally to viewport
9539			if (width > captionSizeOkMax) {
9540				newCaptionSize = captionSize * 1.5;
9541			}
9542			else if (width < captionSizeOkMin) {
9543				newCaptionSize = captionSize / 1.5;
9544			}
9545			else {
9546				newCaptionSize = captionSize;
9547			}
9548			newLineHeight = newCaptionSize + 25;
9549			this.$captionsDiv.css('font-size',newCaptionSize + '%');
9550			this.$captionsWrapper.css('line-height',newLineHeight + '%');
9551		}
9552		this.refreshControls('captions');
9553	};
9554
9555	AblePlayer.prototype.retrieveOffscreenWindow = function( which, width, height ) {
9556
9557		// check to be sure popup windows ('transcript' or 'sign') are positioned on-screen
9558		// (they sometimes disappear off-screen when entering fullscreen mode)
9559		// if off-screen, recalculate so they are back on screen
9560
9561		var window, windowPos, windowTop, windowLeft, windowRight, windowWidth, windowBottom, windowHeight;
9562
9563		if (which == 'transcript') {
9564			window = this.$transcriptArea;
9565		}
9566		else if (which == 'sign') {
9567			window = this.$signWindow;
9568		}
9569		windowWidth = window.width();
9570		windowHeight = window.height();
9571		windowPos = window.position();
9572		windowTop = windowPos.top;
9573		windowLeft = windowPos.left;
9574		windowRight = windowLeft + windowWidth;
9575		windowBottom = windowTop + windowHeight;
9576
9577		if (windowTop < 0) { // off-screen to the top
9578			windowTop = 10;
9579			window.css('top',windowTop);
9580		}
9581		if (windowLeft < 0) { // off-screen to the left
9582			windowLeft = 10;
9583			window.css('left',windowLeft);
9584		}
9585		if (windowRight > width) { // off-screen to the right
9586			windowLeft = (width - 20) - windowWidth;
9587			window.css('left',windowLeft);
9588		}
9589		if (windowBottom > height) { // off-screen to the bottom
9590			windowTop = (height - 10) - windowHeight;
9591			window.css('top',windowTop);
9592		}
9593	};
9594
9595	AblePlayer.prototype.getHighestZIndex = function() {
9596
9597		// returns the highest z-index on page
9598		// used to ensure dialogs (or potentially other windows) are on top
9599
9600		var max, $elements, z;
9601		max = 0;
9602
9603		// exclude the Able Player dialogs and windows
9604		$elements = $('body *').not('.able-modal-dialog,.able-modal-dialog *,.able-modal-overlay,.able-modal-overlay *,.able-sign-window,.able-transcript-area');
9605
9606		$elements.each(function(){
9607			z = $(this).css('z-index');
9608			if (Number.isInteger(+z)) { // work only with integer values, not 'auto'
9609				if (parseInt(z) > max) {
9610					max = parseInt(z);
9611				}
9612			}
9613		});
9614		return max;
9615	};
9616
9617	AblePlayer.prototype.updateZIndex = function(which) {
9618
9619		// update z-index of 'transcript' or 'sign', relative to each other
9620		// direction is always 'up' (i.e., move window to top)
9621		// windows come to the top when the user clicks on them
9622		var defHighZ, defLowZ, highestZ, transcriptZ, signZ, newHighZ, newLowZ;
9623
9624		// set the default z-indexes, as defined in ableplayer.css
9625		defHighZ = 8000; // by default, assigned to the sign window
9626		defLowZ = 7000; // by default, assigned to the transcript area
9627		highestZ = this.getHighestZIndex(); // highest z-index on the page, excluding Able Player windows & modals
9628
9629		// NOTE: Although highestZ is collected here, it currently isn't used.
9630		// If something on the page has a higher z-index than the transcript or sign window, do we care?
9631		// Excluding it here assumes "No". Our immediate concern is with the relationship between our own components.
9632		// If we elevate our z-indexes so our content is on top, we run the risk of starting a z-index war.
9633
9634		if (typeof this.$transcriptArea === 'undefined' || typeof this.$signWindow === 'undefined' ) {
9635			// at least one of the windows doesn't exist, so there's no conflict
9636			// since z-index may have been stored to a cookie on another page, need to restore default
9637			if (typeof this.$transcriptArea !== 'undefined') {
9638				transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
9639				if (transcriptZ > defLowZ) {
9640					// restore to the default
9641					this.$transcriptArea.css('z-index',defLowZ);
9642				}
9643			}
9644			else if (typeof this.$signWindow !== 'undefined') {
9645				signZ = parseInt(this.$signWindow.css('z-index'));
9646				if (signZ > defHighZ) {
9647					// restore to the default
9648					this.$signWindow.css('z-index',defHighZ);
9649				}
9650			}
9651			return false;
9652		}
9653
9654		// both windows exist
9655
9656		// get current values
9657		transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
9658		signZ = parseInt(this.$signWindow.css('z-index'));
9659
9660		if (transcriptZ === signZ) {
9661			// the two windows are equal; restore defaults (the target window will be on top)
9662			newHighZ = defHighZ;
9663			newLowZ = defLowZ;
9664		}
9665		else if (transcriptZ > signZ) {
9666			if (which === 'transcript') {
9667				// transcript is already on top; nothing to do
9668				return false;
9669			}
9670			else {
9671				// swap z's
9672				newHighZ = transcriptZ;
9673				newLowZ = signZ;
9674			}
9675		}
9676		else { // signZ is greater
9677			if (which === 'sign') {
9678				// sign is already on top; nothing to do
9679				return false;
9680			}
9681			else {
9682				newHighZ = signZ;
9683				newLowZ = transcriptZ;
9684			}
9685		}
9686		// now assign the new values
9687		if (which === 'transcript') {
9688			this.$transcriptArea.css('z-index',newHighZ);
9689			this.$signWindow.css('z-index',newLowZ);
9690		}
9691		else if (which === 'sign') {
9692			this.$signWindow.css('z-index',newHighZ);
9693			this.$transcriptArea.css('z-index',newLowZ);
9694		}
9695	};
9696
9697	AblePlayer.prototype.syncTrackLanguages = function (source, language) {
9698
9699		// this function is called when the player is built (source == 'init')
9700		// and again when user changes the language of either 'captions' or 'transcript'
9701		// It syncs the languages of chapters, descriptions, and metadata tracks
9702		// NOTE: Caption and transcript languages are somewhat independent from one another
9703		// If a user changes the caption language, the transcript follows
9704		// However, if a user changes the transcript language, this only affects the transcript
9705		// This was a group decision based on the belief that users may want a transcript
9706		// that is in a different language than the captions
9707
9708		var i, captions, descriptions, chapters, meta;
9709
9710		// Captions
9711		for (i = 0; i < this.captions.length; i++) {
9712			if (this.captions[i].language === language) {
9713				captions = this.captions[i];
9714			}
9715		}
9716		// Chapters
9717		for (i = 0; i < this.chapters.length; i++) {
9718			if (this.chapters[i].language === language) {
9719				chapters = this.chapters[i];
9720			}
9721		}
9722		// Descriptions
9723		for (i = 0; i < this.descriptions.length; i++) {
9724			if (this.descriptions[i].language === language) {
9725				descriptions = this.descriptions[i];
9726			}
9727		}
9728		// Metadata
9729		for (i = 0; i < this.meta.length; i++) {
9730			if (this.meta[i].language === language) {
9731				meta = this.meta[i];
9732			}
9733		}
9734
9735		// regardless of source...
9736		this.transcriptLang = language;
9737
9738		if (source === 'init' || source === 'captions') {
9739			this.captionLang = language;
9740			this.selectedCaptions = captions;
9741			this.selectedChapters = chapters;
9742			this.selectedDescriptions = descriptions;
9743			this.selectedMeta = meta;
9744			this.transcriptCaptions = captions;
9745			this.transcriptChapters = chapters;
9746			this.transcriptDescriptions = descriptions;
9747			this.updateChaptersList();
9748			// the following was commented out in Oct/Nov 2018.
9749			// chapters popup is setup automatically when setupPopups() is called later with no param
9750			// not sure why it was included here.
9751			// this.setupPopups('chapters');
9752		}
9753		else if (source === 'transcript') {
9754			this.transcriptCaptions = captions;
9755			this.transcriptChapters = chapters;
9756			this.transcriptDescriptions = descriptions;
9757		}
9758		this.updateTranscript();
9759	};
9760
9761})(jQuery);
9762
9763(function ($) {
9764	AblePlayer.prototype.updateCaption = function (time) {
9765
9766		if (!this.usingYouTubeCaptions && (typeof this.$captionsWrapper !== 'undefined')) {
9767			if (this.captionsOn) {
9768				this.$captionsWrapper.show();
9769				if (typeof time !== 'undefined') {
9770					this.showCaptions(time);
9771				}
9772			}
9773			else if (this.$captionsWrapper) {
9774				this.$captionsWrapper.hide();
9775				this.prefCaptions = 0;
9776			}
9777		}
9778	};
9779
9780	AblePlayer.prototype.updateCaptionsMenu = function (lang) {
9781
9782		// uncheck all previous menu items
9783		this.captionsPopup.find('li').attr('aria-checked','false');
9784		if (typeof lang === 'undefined') {
9785			// check the last menu item (captions off)
9786			this.captionsPopup.find('li').last().attr('aria-checked','true');
9787		}
9788		else {
9789			// check the newly selected lang
9790			this.captionsPopup.find('li[lang=' + lang + ']').attr('aria-checked','true');
9791		}
9792	};
9793
9794	// Returns the function used when a caption is clicked in the captions menu.
9795	// Not called if user clicks "Captions off". Instead, that triggers getCaptionOffFunction()
9796	AblePlayer.prototype.getCaptionClickFunction = function (track) {
9797
9798		var thisObj = this;
9799		return function () {
9800			thisObj.selectedCaptions = track;
9801			thisObj.captionLang = track.language;
9802			thisObj.currentCaption = -1;
9803			if (thisObj.usingYouTubeCaptions) {
9804				if (thisObj.captionsOn) {
9805					if (typeof thisObj.ytCaptionModule !== 'undefined') {
9806						// captions are already on. Just need to change the language
9807						thisObj.youTubePlayer.setOption(thisObj.ytCaptionModule, 'track', {'languageCode': thisObj.captionLang});
9808					}
9809					else {
9810						// need to wait for caption module to be loaded to change the language
9811						// caption module will be loaded after video starts playing, triggered by onApiChange event
9812						// at that point, thosObj.captionLang will be passed to the module as the default language
9813					}
9814				}
9815				else {
9816					// captions are off (i.e., captions module has been unloaded; need to reload it)
9817					// user's selected language will be reset after module has successfully loaded
9818					// (the onApiChange event will be fired -- see initialize.js > initYouTubePlayer())
9819					thisObj.resettingYouTubeCaptions = true;
9820					thisObj.youTubePlayer.loadModule(thisObj.ytCaptionModule);
9821				}
9822			}
9823			else if (thisObj.usingVimeoCaptions) {
9824				thisObj.vimeoPlayer.enableTextTrack(thisObj.captionLang).then(function(track) {
9825					// track.language = the iso code for the language
9826					// track.kind = 'captions' or 'subtitles'
9827					// track.label = the human-readable label
9828				}).catch(function(error) {
9829					switch (error.name) {
9830						case 'InvalidTrackLanguageError':
9831							// no track was available with the specified language
9832							console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
9833							break;
9834						case 'InvalidTrackError':
9835							// no track was available with the specified language and kind
9836							console.log('No ' + track.kind + ' track is available in the specified language (' + track.label + ')');
9837							break;
9838						default:
9839							// some other error occurred
9840							console.log('Error loading ' + track.label + ' ' + track.kind + ' track');
9841							break;
9842    				}
9843				});
9844			}
9845			else { // using local track elements for captions/subtitles
9846				thisObj.syncTrackLanguages('captions',thisObj.captionLang);
9847				if (!thisObj.swappingSrc) {
9848					thisObj.updateCaption(thisObj.elapsed);
9849					thisObj.showDescription(thisObj.elapsed);
9850				}
9851			}
9852			thisObj.captionsOn = true;
9853			// stopgap to prevent spacebar in Firefox from reopening popup
9854			// immediately after closing it (used in handleCaptionToggle(
9854))
9855			thisObj.hidingPopup = true;
9856			thisObj.captionsPopup.hide();
9857			// Ensure stopgap gets cancelled if handleCaptionToggle() isn't called
9858			// e.g., if user triggered button with Enter or mouse click, not spacebar
9859			setTimeout(function() {
9860				thisObj.hidingPopup = false;
9861			}, 100);
9862			thisObj.updateCaptionsMenu(thisObj.captionLang);
9863			thisObj.$ccButton.focus();
9864
9865			// save preference to cookie
9866			thisObj.prefCaptions = 1;
9867			thisObj.updateCookie('prefCaptions');
9868			thisObj.refreshControls('captions');
9869		}
9870	};
9871
9872	// Returns the function used when the "Captions Off" button is clicked in the captions tooltip.
9873	AblePlayer.prototype.getCaptionOffFunction = function () {
9874		var thisObj = this;
9875		return function () {
9876			if (thisObj.player == 'youtube') {
9877				thisObj.youTubePlayer.unloadModule(thisObj.ytCaptionModule);
9878			}
9879			else if (thisObj.usingVimeoCaptions) {
9880        thisObj.vimeoPlayer.disableTextTrack();
9881			}
9882			thisObj.captionsOn = false;
9883			thisObj.currentCaption = -1;
9884			// stopgap to prevent spacebar in Firefox from reopening popup
9885			// immediately after closing it (used in handleCaptionToggle())
9886			thisObj.hidingPopup = true;
9887			thisObj.captionsPopup.hide();
9888			// Ensure stopgap gets cancelled if handleCaptionToggle() isn't called
9889			// e.g., if user triggered button with Enter or mouse click, not spacebar
9890			setTimeout(function() {
9891				thisObj.hidingPopup = false;
9892			}, 100);
9893			thisObj.updateCaptionsMenu();
9894			thisObj.$ccButton.focus();
9895
9896			// save preference to cookie
9897			thisObj.prefCaptions = 0;
9898			thisObj.updateCookie('prefCaptions');
9899			if (!this.swappingSrc) {
9900				thisObj.refreshControls('captions');
9901				thisObj.updateCaption();
9902			}
9903		}
9904	};
9905
9906	AblePlayer.prototype.showCaptions = function(now) {
9907
9908		var c, thisCaption, captionText;
9909		var cues;
9910		if (this.selectedCaptions) {
9911			cues = this.selectedCaptions.cues;
9912		}
9913		else if (this.captions.length >= 1) {
9914			cues = this.captions[0].cues;
9915		}
9916		else {
9917			cues = [];
9918		}
9919		for (c = 0; c < cues.length; c++) {
9920			if ((cues[c].start <= now) && (cues[c].end > now)) {
9921				thisCaption = c;
9922				break;
9923			}
9924		}
9925		if (typeof thisCaption !== 'undefined') {
9926			if (this.currentCaption !== thisCaption) {
9927				// it's time to load the new caption into the container div
9928				captionText = this.flattenCueForCaption(cues[thisCaption]).replace('\n', '<br>');
9929				this.$captionsDiv.html(captionText);
9930				this.currentCaption = thisCaption;
9931				if (captionText.length === 0) {
9932					// hide captionsDiv; otherwise background-color is visible due to padding
9933					this.$captionsDiv.css('display','none');
9934				}
9935				else {
9936					this.$captionsDiv.css('display','inline-block');
9937				}
9938			}
9939		}
9940		else {
9941			this.$captionsDiv.html('');
9942			this.currentCaption = -1;
9943		}
9944	};
9945
9946	AblePlayer.prototype.flattenCueForCaption = function (cue) {
9947
9948		// Takes a cue and returns the caption text to display
9949		// Also used for chapters
9950
9951		// Support for 'i' and 'b' tags added in 2.3.66
9952		// TODO: Add support for 'c' (class) and 'ruby'
9953
9954		// c (class): <c.myClass1.myClass2>Some text</c>
9955		// Classes can be used to modify other tags too (e.g., <v.loud>)
9956		// If <c> tag, should be rendered as a <span>
9957
9958		// ruby: http://www.w3schools.com/tags/tag_ruby.asp
9959
9960		// WebVTT also supports 'u' (underline)
9961		// I see no reason to support that in Able Player.
9962		// If it's available authors are likely to use it incorrectly
9963		// where <i> or <b> should be used instead
9964		// Here are the rare use cases where an underline is appropriate on the web:
9965		// http://html5doctor.com/u-element/
9966
9967		var result = [];
9968
9969		var flattenComponent = function (component) {
9970			var result = [], ii;
9971			if (component.type === 'string') {
9972				result.push(component.value);
9973			}
9974			else if (component.type === 'v') {
9975				result.push('(' + component.value + ')');
9976				for (ii = 0; ii < component.children.length; ii++) {
9977					result.push(flattenComponent(component.children[ii]));
9978				}
9979			}
9980			else if (component.type === 'i') {
9981				result.push('<em>');
9982				for (ii = 0; ii < component.children.length; ii++) {
9983					result.push(flattenComponent(component.children[ii]));
9984				}
9985				result.push('</em>');
9986			}
9987			else if (component.type === 'b') {
9988				result.push('<strong>');
9989				for (ii = 0; ii < component.children.length; ii++) {
9990					result.push(flattenComponent(component.children[ii]));
9991				}
9992				result.push('</strong>');
9993			}
9994			else {
9995				for (ii = 0; ii < component.children.length; ii++) {
9996					result.push(flattenComponent(component.children[ii]));
9997				}
9998			}
9999			return result.join('');
10000		};
10001
10002		if (typeof cue.components !== 'undefined') {
10003			for (var ii = 0; ii < cue.components.children.length; ii++) {
10004				result.push(flattenComponent(cue.components.children[ii]));
10005			}
10006		}
10007		return result.join('');
10008	};
10009
10010	AblePlayer.prototype.getCaptionsOptions = function(pref) {
10011
10012		var options = [];
10013
10014		switch (pref) {
10015
10016			case 'prefCaptionsFont':
10017				options[0] = ['serif',this.tt.serif];
10018				options[1] = ['sans-serif',this.tt.sans];
10019				options[2] = ['cursive',this.tt.cursive];
10020				options[3] = ['fantasy',this.tt.fantasy];
10021				options[4] = ['monospace',this.tt.monospace];
10022				break;
10023
10024			case 'prefCaptionsColor':
10025			case 'prefCaptionsBGColor':
10026				// HTML color values must be in English
10027				options[0] = ['white',this.tt.white];
10028				options[1] = ['yellow',this.tt.yellow];
10029				options[2] = ['green',this.tt.green];
10030				options[3] = ['cyan',this.tt.cyan];
10031				options[4] = ['blue',this.tt.blue];
10032				options[5] = ['magenta',this.tt.magenta];
10033				options[6] = ['red',this.tt.red];
10034				options[7] = ['black',this.tt.black];
10035				break;
10036
10037			case 'prefCaptionsSize':
10038				options[0] = '75%';
10039				options[1] = '100%';
10040				options[2] = '125%';
10041				options[3] = '150%';
10042				options[4] = '200%';
10043				break;
10044
10045			case 'prefCaptionsOpacity':
10046				options[0] = '0%';
10047				options[1] = '25%';
10048				options[2] = '50%';
10049				options[3] = '75%';
10050				options[4] = '100%';
10051				break;
10052
10053			case 'prefCaptionsStyle':
10054				options[0] = this.tt.captionsStylePopOn;
10055				options[1] = this.tt.captionsStyleRollUp;
10056				break;
10057
10058			case 'prefCaptionsPosition':
10059				options[0] = 'overlay';
10060				options[1] = 'below';
10061				break;
10062
10063		}
10064		return options;
10065	};
10066
10067	AblePlayer.prototype.translatePrefs = function(pref, value, outputFormat) {
10068
10069		// translate current value of pref to a value supported by outputformat
10070		if (outputFormat == 'youtube') {
10071			if (pref === 'size') {
10072				// YouTube font sizes are a range from -1 to 3 (0 = default)
10073				switch (value) {
10074					case '75%':
10075						return -1;
10076					case '100%':
10077						return 0;
10078					case '125%':
10079						return 1;
10080					case '150%':
10081						return 2;
10082					case '200%':
10083						return 3;
10084				}
10085			}
10086		}
10087		return false;
10088	}
10089
10090	AblePlayer.prototype.stylizeCaptions = function($element, pref) {
10091
10092		// $element is the jQuery element containing the captions
10093		// this function handles stylizing of the sample caption text in the Prefs dialog
10094		// plus the actual production captions
10095		// TODO: consider applying the same user prefs to visible text-based description
10096		var property, newValue, opacity, lineHeight;
10097
10098		if (typeof $element !== 'undefined') {
10099			if (pref == 'prefCaptionsPosition') {
10100				this.positionCaptions();
10101			}
10102			else if (typeof pref !== 'undefined') {
10103				// just change the one property that user just changed
10104				if (pref === 'prefCaptionsFont') {
10105					property = 'font-family';
10106				}
10107				else if (pref === 'prefCaptionsSize') {
10108					property = 'font-size';
10109				}
10110				else if (pref === 'prefCaptionsColor') {
10111					property = 'color';
10112				}
10113				else if (pref === 'prefCaptionsBGColor') {
10114					property = 'background-color';
10115				}
10116				else if (pref === 'prefCaptionsOpacity') {
10117					property = 'opacity';
10118				}
10119				if (pref === 'prefCaptionsOpacity') {
10120					newValue = parseFloat($('#' + this.mediaId + '_' + pref).val()) / 100.0;
10121				}
10122				else {
10123					newValue = $('#' + this.mediaId + '_' + pref).val();
10124				}
10125				$element.css(property, newValue);
10126			}
10127			else { // no property was specified, update all styles with current saved prefs
10128				opacity = parseFloat(this.prefCaptionsOpacity) / 100.0;
10129				$element.css({
10130					'font-family': this.prefCaptionsFont,
10131					'color': this.prefCaptionsColor,
10132					'background-color': this.prefCaptionsBGColor,
10133					'opacity': opacity
10134				});
10135				if ($element === this.$captionsDiv) {
10136					if (typeof this.$captionsWrapper !== 'undefined') {
10137						this.$captionsWrapper.css({
10138  						'font-size': this.prefCaptionsSize
10139            });
10140					}
10141				}
10142				if (this.prefCaptionsPosition === 'below') {
10143					// also need to add the background color to the wrapper div
10144					if (typeof this.$captionsWrapper !== 'undefined') {
10145						this.$captionsWrapper.css({
10146							'background-color': this.prefCaptionsBGColor,
10147							'opacity': '1'
10148						});
10149					}
10150				}
10151				else if (this.prefCaptionsPosition === 'overlay') {
10152					// no background color for overlay wrapper, captions are displayed in-line
10153					if (typeof this.$captionsWrapper !== 'undefined') {
10154						this.$captionsWrapper.css({
10155							'background-color': 'transparent',
10156							'opacity': ''
10157						});
10158					}
10159				}
10160				this.positionCaptions();
10161			}
10162		}
10163	};
10164	AblePlayer.prototype.positionCaptions = function(position) {
10165
10166		// set caption position to either 'overlay' or 'below'
10167		// if position parameter was passed to this function, use that
10168		// otherwise use user preference
10169		if (typeof position === 'undefined') {
10170			position = this.prefCaptionsPosition;
10171		}
10172		if (typeof this.$captionsWrapper !== 'undefined') {
10173
10174			if (position == 'below') {
10175				this.$captionsWrapper.removeClass('able-captions-overlay').addClass('able-captions-below');
10176				// also need to update in-line styles
10177				this.$captionsWrapper.css({
10178					'background-color': this.prefCaptionsBGColor,
10179					'opacity': '1'
10180				});
10181			}
10182			else {
10183				this.$captionsWrapper.removeClass('able-captions-below').addClass('able-captions-overlay');
10184				this.$captionsWrapper.css({
10185					'background-color': 'transparent',
10186					'opacity': ''
10187				});
10188			}
10189		}
10190	};
10191
10192})(jQuery);
10193
10194(function ($) {
10195
10196	AblePlayer.prototype.populateChaptersDiv = function() {
10197
10198		var headingLevel, headingType, headingId, $chaptersHeading,
10199			$chaptersList;
10200
10201		if ($('#' + this.chaptersDivLocation)) {
10202			this.$chaptersDiv = $('#' + this.chaptersDivLocation);
10203			this.$chaptersDiv.addClass('able-chapters-div');
10204
10205			// add optional header
10206			if (this.chaptersTitle) {
10207				headingLevel = this.getNextHeadingLevel(this.$chaptersDiv);
10208				headingType = 'h' + headingLevel.toString();
10209				headingId = this.mediaId + '-chapters-heading';
10210				$chaptersHeading = $('<' + headingType + '>', {
10211					'class': 'able-chapters-heading',
10212					'id': headingId
10213				}).text(this.chaptersTitle);
10214				this.$chaptersDiv.append($chaptersHeading);
10215			}
10216
10217			this.$chaptersNav = $('<nav>');
10218			if (this.chaptersTitle) {
10219				this.$chaptersNav.attr('aria-labelledby',headingId);
10220			}
10221			else {
10222				this.$chaptersNav.attr('aria-label',this.tt.chapters);
10223			}
10224			this.$chaptersDiv.append(this.$chaptersNav);
10225
10226			// populate this.$chaptersNav with a list of chapters
10227			this.updateChaptersList();
10228		}
10229	};
10230
10231	AblePlayer.prototype.updateChaptersList = function() {
10232
10233		var thisObj, cues, $chaptersList, c, thisChapter,
10234			$chapterItem, $chapterButton, buttonId, hasDefault,
10235			getClickFunction, $clickedItem, $chaptersList, thisChapterIndex;
10236
10237		thisObj = this;
10238
10239		if (!this.$chaptersNav) {
10240			return false;
10241		}
10242
10243		if (typeof this.useChapterTimes === 'undefined') {
10244			if (this.seekbarScope === 'chapter' && this.selectedChapters.cues.length) {
10245				this.useChapterTimes = true;
10246			}
10247			else {
10248				this.useChapterTimes = false;
10249			}
10250		}
10251
10252		if (this.useChapterTimes) {
10253			cues = this.selectedChapters.cues;
10254		}
10255		else if (this.chapters.length >= 1) {
10256			cues = this.chapters[0].cues;
10257		}
10258		else {
10259			cues = [];
10260		}
10261		if (cues.length > 0) {
10262			$chaptersList = $('<ul>');
10263			for (c = 0; c < cues.length; c++) {
10264				thisChapter = c;
10265				$chapterItem = $('<li></li>');
10266				$chapterButton = $('<button>',{
10267					'type': 'button',
10268					'val': thisChapter
10269				}
vendor: 8,788 bytes, lines 10269-10546
10269).text(this.flattenCueForCaption(cues[thisChapter]));
10270
10271				// add event listeners
10272				getClickFunction = function (time) {
10273					return function () {
10274						thisObj.seekTrigger = 'chapter';
10275						$clickedItem = $(this).closest('li');
10276						$chaptersList = $(this).closest('ul').find('li');
10277						thisChapterIndex = $chaptersList.index($clickedItem);
10278						$chaptersList.removeClass('able-current-chapter').attr('aria-selected','');
10279						$clickedItem.addClass('able-current-chapter').attr('aria-selected','true');
10280						// Need to updateChapter before seeking to it
10281						// Otherwise seekBar is redrawn with wrong chapterDuration and/or chapterTime
10282						thisObj.updateChapter(time);
10283						thisObj.seekTo(time);
10284					}
10285				};
10286				$chapterButton.on('click',getClickFunction(cues[thisChapter].start)); // works with Enter too
10287				$chapterButton.on('focus',function() {
10288					$(this).closest('ul').find('li').removeClass('able-focus');
10289					$(this).closest('li').addClass('able-focus');
10290				});
10291				$chapterItem.on('hover',function() {
10292					$(this).closest('ul').find('li').removeClass('able-focus');
10293					$(this).addClass('able-focus');
10294				});
10295				$chapterItem.on('mouseleave',function() {
10296					$(this).removeClass('able-focus');
10297				});
10298				$chapterButton.on('blur',function() {
10299					$(this).closest('li').removeClass('able-focus');
10300				});
10301
10302				// put it all together
10303				$chapterItem.append($chapterButton);
10304				$chaptersList.append($chapterItem);
10305				if (this.defaultChapter === cues[thisChapter].id) {
10306					$chapterButton.attr('aria-selected','true').parent('li').addClass('able-current-chapter');
10307					this.currentChapter = cues[thisChapter];
10308					hasDefault = true;
10309				}
10310			}
10311			if (!hasDefault) {
10312				// select the first chapter
10313				this.currentChapter = cues[0];
10314				$chaptersList.find('button').first().attr('aria-selected','true')
10315					.parent('li').addClass('able-current-chapter');
10316			}
10317			this.$chaptersNav.html($chaptersList);
10318		}
10319		return false;
10320	};
10321
10322	AblePlayer.prototype.seekToChapter = function(chapterId) {
10323
10324		// step through chapters looking for matching ID
10325		var i=0;
10326		while (i < this.selectedChapters.cues.length) {
10327			if (this.selectedChapters.cues[i].id == chapterId) {
10328				// found the target chapter! Seek to it
10329				this.seekTo(this.selectedChapters.cues[i].start);
10330				this.updateChapter(this.selectedChapters.cues[i].start);
10331				break;
10332			}
10333			i++;
10334		}
10335	};
10336
10337	AblePlayer.prototype.updateChapter = function (now) {
10338
10339		// as time-synced chapters change during playback, track changes in current chapter
10340		if (typeof this.selectedChapters === 'undefined') {
10341			return;
10342		}
10343
10344		var chapters, i, thisChapterIndex, chapterLabel;
10345
10346		chapters = this.selectedChapters.cues;
10347		for (i = 0; i < chapters.length; i++) {
10348			if ((chapters[i].start <= now) && (chapters[i].end > now)) {
10349				thisChapterIndex = i;
10350				break;
10351			}
10352		}
10353		if (typeof thisChapterIndex !== 'undefined') {
10354			if (this.currentChapter !== chapters[thisChapterIndex]) {
10355				// this is a new chapter
10356				this.currentChapter = chapters[thisChapterIndex];
10357				if (this.useChapterTimes) {
10358					this.chapterDuration = this.getChapterDuration();
10359					this.seekIntervalCalculated = false; // will be recalculated in setSeekInterval()
10360				}
10361				if (typeof this.$chaptersDiv !== 'undefined') {
10362					// chapters are listed in an external container
10363					this.$chaptersDiv.find('ul').find('li').removeClass('able-current-chapter').attr('aria-selected','');
10364					this.$chaptersDiv.find('ul').find('li').eq(thisChapterIndex)
10365						.addClass('able-current-chapter').attr('aria-selected','true');
10366				}
10367			}
10368		}
10369	};
10370
10371	AblePlayer.prototype.getChapterDuration = function () {
10372
10373		// called if this.seekbarScope === 'chapter'
10374		// get duration of the current chapter
10375
10376		var lastChapterIndex, chapterEnd;
10377
10378		if (typeof this.currentChapter === 'undefined') {
10379			return 0;
10380		}
10381		if (typeof this.duration === 'undefined') {
10382			return 0;
10383		}
10384		lastChapterIndex = this.selectedChapters.cues.length-1;
10385		if (this.selectedChapters.cues[lastChapterIndex] == this.currentChapter) {
10386			// this is the last chapter
10387			if (this.currentChapter.end !== this.duration) {
10388				// chapter ends before or after video ends, adjust chapter end to match video end
10389				chapterEnd = this.duration;
10390				this.currentChapter.end = this.duration;
10391			}
10392			else {
10393				chapterEnd = this.currentChapter.end;
10394			}
10395		}
10396		else { // this is not the last chapter
10397			chapterEnd = this.currentChapter.end;
10398		}
10399		return chapterEnd - this.currentChapter.start;
10400	};
10401
10402	AblePlayer.prototype.getChapterElapsed = function () {
10403		// called if this.seekbarScope === 'chapter'
10404		// get current elapsed time, relative to the current chapter duration
10405
10406		if (typeof this.currentChapter === 'undefined') {
10407			return 0;
10408		}
10409
10410		if (this.elapsed > this.currentChapter.start) {
10411			return this.elapsed - this.currentChapter.start;
10412		}
10413		else {
10414			return 0;
10415		}
10416	};
10417
10418	AblePlayer.prototype.convertChapterTimeToVideoTime = function (chapterTime) {
10419
10420		// chapterTime is the time within the current chapter
10421		// return the same time, relative to the entire video
10422		if (typeof this.currentChapter !== 'undefined') {
10423			var newTime = this.currentChapter.start + chapterTime;
10424			if (newTime > this.currentChapter.end) {
10425				return this.currentChapter.end;
10426			}
10427			else {
10428				return newTime;
10429			}
10430		}
10431		else {
10432			return chapterTime;
10433		}
10434	};
10435
10436	AblePlayer.prototype.getChapterClickFunction = function (time) {
10437
10438		// Returns the function used when a chapter is clicked in the chapters menu.
10439		var thisObj = this;
10440		return function () {
10441			thisObj.seekTrigger = 'chapter';
10442			thisObj.seekTo(time);
10443			// stopgap to prevent spacebar in Firefox from reopening popup
10444			// immediately after closing it (used in handleChapters())
10445			thisObj.hidingPopup = true;
10446			thisObj.chaptersPopup.hide();
10447			// Ensure stopgap gets cancelled if handleChapters() isn't called
10448			// e.g., if user triggered button with Enter or mouse click, not spacebar
10449			setTimeout(function() {
10450				thisObj.hidingPopup = false;
10451			}, 100);
10452			thisObj.$chaptersButton.focus();
10453		}
10454	};
10455
10456})(jQuery);
10457
10458(function ($) {
10459	AblePlayer.prototype.updateMeta = function (time) {
10460		if (this.hasMeta) {
10461			if (this.metaType === 'text') {
10462				this.$metaDiv.show();
10463				this.showMeta(time || this.elapsed);
10464			}
10465			else {
10466				this.showMeta(time || this.elapsed);
10467			}
10468		}
10469	};
10470
10471	AblePlayer.prototype.showMeta = function(now) {
10472		var tempSelectors, m, thisMeta,
10473			cues, cueText, cueLines, i, line,
10474			showDuration, focusTarget;
10475
10476		tempSelectors = [];
10477		if (this.meta.length >= 1) {
10478			cues = this.meta;
10479		}
10480		else {
10481			cues = [];
10482		}
10483		for (m = 0; m < cues.length; m++) {
10484			if ((cues[m].start <= now) && (cues[m].end > now)) {
10485				thisMeta = m;
10486				break;
10487			}
10488		}
10489		if (typeof thisMeta !== 'undefined') {
10490			if (this.currentMeta !== thisMeta) {
10491				if (this.metaType === 'text') {
10492					// it's time to load the new metadata cue into the container div
10493					this.$metaDiv.html(this.flattenCueForMeta(cues[thisMeta]).replace('\n', '<br>'));
10494				}
10495				else if (this.metaType === 'selector') {
10496					// it's time to show content referenced by the designated selector(s)
10497					cueText = this.flattenCueForMeta(cues[thisMeta]);
10498					cueLines = cueText.split('\n');
10499					for (i=0; i<cueLines.length; i++) {
10500						line = $.trim(cueLines[i]);
10501						if (line.toLowerCase().trim() === 'pause') {
10502							// don't show big play button when pausing via metadata
10503							this.hideBigPlayButton = true;
10504							this.pauseMedia();
10505						}
10506						else if (line.toLowerCase().substring(0,6) == 'focus:') {
10507							focusTarget = line.substring(6).trim();
10508							if ($(focusTarget).length) {
10509								$(focusTarget).focus();
10510							}
10511						}
10512						else {
10513							if ($(line).length) {
10514								// selector exists
10515                this.currentMeta = thisMeta;
10516								showDuration = parseInt($(line).attr('data-duration'));
10517								if (typeof showDuration !== 'undefined' && !isNaN(showDuration)) {
10518									$(line).show().delay(showDuration).fadeOut();
10519								}
10520								else {
10521									// no duration specified. Just show the element until end time specified in VTT file
10522									$(line).show();
10523								}
10524								// add to array of visible selectors so it can be hidden at end time
10525								this.visibleSelectors.push(line);
10526								tempSelectors.push(line);
10527
10528							}
10529						}
10530					}
10531					// now step through this.visibleSelectors and remove anything that's stale
10532					if (this.visibleSelectors && this.visibleSelectors.length) {
10533						if (this.visibleSelectors.length !== tempSelectors.length) {
10534							for (i=this.visibleSelectors.length-1; i>=0; i--) {
10535								if ($.inArray(this.visibleSelectors[i],tempSelectors) == -1) {
10536									$(this.visibleSelectors[i]).hide();
10537									this.visibleSelectors.splice(i,1);
10538								}
10539							}
10540						}
10541					}
10542
10543				}
10544			}
10545		}
10546		else {
10547			// there is currently no metadata. Empty stale content
10548			if (typeof this.$metaDiv !== 'undefined') {
10549				this.$metaDiv.html('');
10550			}
10551			if (this.visibleSelectors && this.visibleSelectors.length) {
10552				for (i=0; i<this.visibleSelectors.length; i++) {
10553					$(this.visibleSelectors[i]).hide();
10554				}
10555				// reset array
10556				this.visibleSelectors = [];
10557			}
10558			this.currentMeta = -1;
10559		}
10560	};
10561
10562	// Takes a cue and returns the metadata text to display for it.
10563	AblePlayer.prototype.flattenCueForMeta = function (cue) {
10564		var result = [];
10565
10566		var flattenComponent = function (component) {
10567			var result = [], ii;
10568			if (component.type === 'string') {
10569				result.push(component.value);
10570			}
10571			else if (component.type === 'v') {
10572				result.push('[' + component.value + ']');
10573				for (ii = 0; ii < component.children.length; ii++) {
10574					result.push(flattenComponent(component.children[ii]));
10575				}
10576			}
10577			else {
10578				for (ii = 0; ii < component.children.length; ii++) {
10579					result.push(flattenComponent(component.children[ii]));
10580				}
10581			}
10582			return result.join('');
10583		}
10584
10585		for (var ii = 0; ii < cue.components.children.length; ii++) {
10586			result.push(flattenComponent(cue.components.children[ii]));
10587		}
10588
10589		return result.join('');
10590	};
10591
10592})(jQuery);
10593
10594(function ($) {
10595
10596	AblePlayer.prototype.setupTranscript = function() {
10597
10598		var deferred = new $.Deferred();
10599		var promise = deferred.promise();
10600
10601		if (!this.transcriptType) {
10602			// previously set transcriptType to null since there are no <track> elements
10603			// check again to see if captions have been collected from other sources (e.g., YouTube)
10604
10605			if (this.captions.length && (!(this.usingYouTubeCaptions || this.usingVimeoCaptions))) {
10606				// captions are possible! Use the default type (popup)
10607				// if other types ('external' and 'manual') were desired, transcriptType would not be null here
10608				this.transcriptType = 'popup';
10609			}
10610		}
10611
10612		if (this.transcriptType) {
10613			if (this.transcriptType === 'popup' || this.transcriptType === 'external') {
10614				 this.injectTranscriptArea();
10615		 			deferred.resolve();
10616			}
10617			else if (this.transcriptType === 'manual') {
10618				this.setupManualTranscript();
10619				deferred.resolve();
10620			}
10621		}
10622		else {
10623			// there is no transcript
10624			deferred.resolve();
10625		}
10626		return promise;
10627	};
10628
10629	AblePlayer.prototype.injectTranscriptArea = function() {
10630
10631		var thisObj, $autoScrollLabel, $languageSelectWrapper, $languageSelectLabel, i, $option;
10632
10633		thisObj = this;
10634		this.$transcriptArea = $('<div>', {
10635			'class': 'able-transcript-area',
10636  		'role': 'dialog',
10637      'aria-label': this.tt.transcriptTitle
10638		});
10639
10640		this.$transcriptToolbar = $('<div>', {
10641			'class': 'able-window-toolbar able-' + this.toolbarIconColor + '-controls'
10642		});
10643
10644		this.$transcriptDiv = $('<div>', {
10645			'class' : 'able-transcript'
10646		});
10647
10648		// Transcript toolbar content
10649
10650		// Add auto Scroll checkbox
10651		this.$autoScrollTranscriptCheckbox = $('<input>', {
10652		 	'id': 'autoscroll-transcript-checkbox',
10653		 	'type': 'checkbox'
10654		 });
10655		$autoScrollLabel = $('<label>', {
10656			 'for': 'autoscroll-transcript-checkbox'
10657			}).text(this.tt.autoScroll);
10658    this.$transcriptToolbar.append($autoScrollLabel,this.$autoScrollTranscriptCheckbox);
10659
10660		// Add field for selecting a transcript language
10661		// Only necessary if there is more than one language
10662		if (this.captions.length > 1) {
10663			$languageSelectWrapper = $('<div>',{
10664				'class': 'transcript-language-select-wrapper'
10665			});
10666			$languageSelectLabel = $('<label>',{
10667				'for': 'transcript-language-select'
10668			}).text(this.tt.language);
10669			this.$transcriptLanguageSelect = $('<select>',{
10670				'id': 'transcript-language-select'
10671			});
10672			for (i=0; i < this.captions.length; i++) {
10673				$option = $('<option></option>',{
10674					value: this.captions[i]['language'],
10675					lang: this.captions[i]['language']
10676				}).text(this.captions[i]['label']);
10677				if (this.captions[i]['def']) {
10678				 	$option.prop('selected',true);
10679				 }
10680				this.$transcriptLanguageSelect.append($option);
10681			 }
10682		}
10683		if ($languageSelectWrapper) {
10684			$languageSelectWrapper.append($languageSelectLabel,this.$transcriptLanguageSelect);
10685			this.$transcriptToolbar.append($languageSelectWrapper);
10686		}
10687		this.$transcriptArea.append(this.$transcriptToolbar, this.$transcriptDiv);
10688
10689		// If client has provided separate transcript location, put it there.
10690		// Otherwise append it to the body
10691		if (this.transcriptDivLocation) {
10692			$('#' + this.transcriptDivLocation).append(this.$transcriptArea);
10693		}
10694		else {
10695			this.$ableWrapper.append(this.$transcriptArea);
10696		}
10697
10698		// make it draggable (popup only; NOT external transcript)
10699		if (!this.transcriptDivLocation) {
10700			this.initDragDrop('transcript');
10701			if (this.prefTranscript === 1) {
10702				// transcript is on. Go ahead and position it
10703				this.positionDraggableWindow('transcript',this.getDefaultWidth('transcript'));
10704			}
10705		}
10706
10707		// If client has provided separate transcript location, override user's preference for hiding transcript
10708		if (!this.prefTranscript && !this.transcriptDivLocation) {
10709			this.$transcriptArea.hide();
10710		}
10711	};
10712
10713	AblePlayer.prototype.addTranscriptAreaEvents = function() {
10714
10715		var thisObj = this;
10716
10717		this.$autoScrollTranscriptCheckbox.click(function () {
10718			thisObj.handleTranscriptLockToggle(thisObj.$autoScrollTranscriptCheckbox.prop('checked'));
10719		});
10720
10721		this.$transcriptDiv.on('mousewheel DOMMouseScroll click scroll', function (e) {
10722			// Propagation is stopped in transcript click handler, so clicks are on the scrollbar
10723			// or outside of a clickable span.
10724			if (!thisObj.scrollingTranscript) {
10725				thisObj.autoScrollTranscript = false;
vendor: 5,159 bytes, lines 10726-10876
10726				thisObj.refreshControls('transcript');
10727			}
10728			thisObj.scrollingTranscript = false;
10729		});
10730
10731		if (typeof this.$transcriptLanguageSelect !== 'undefined') {
10732
10733			this.$transcriptLanguageSelect.on('click mousedown',function (e) {
10734				// execute default behavior
10735				// prevent propagation of mouse event to toolbar or window
10736				e.stopPropagation();
10737			});
10738
10739			this.$transcriptLanguageSelect.on('change',function () {
10740
10741				var language = thisObj.$transcriptLanguageSelect.val();
10742
10743				thisObj.syncTrackLanguages('transcript',language);
10744			});
10745		}
10746	};
10747
10748	AblePlayer.prototype.transcriptSrcHasRequiredParts = function() {
10749
10750		// check the external transcript to be sure it has all required components
10751		// return true or false
10752		// in the process, define all the needed variables and properties
10753
10754		if ($('#' + this.transcriptSrc).length) {
10755			this.$transcriptArea = $('#' + this.transcriptSrc);
10756			if (this.$transcriptArea.find('.able-window-toolbar').length) {
10757				this.$transcriptToolbar = this.$transcriptArea.find('.able-window-toolbar').eq(0);
10758				if (this.$transcriptArea.find('.able-transcript').length) {
10759					this.$transcriptDiv = this.$transcriptArea.find('.able-transcript').eq(0);
10760					if (this.$transcriptArea.find('.able-transcript-seekpoint').length) {
10761						this.$transcriptSeekpoints = this.$transcriptArea.find('.able-transcript-seekpoint');
10762						return true;
10763					}
10764				}
10765			}
10766		}
10767		return false;
10768	}
10769
10770	AblePlayer.prototype.setupManualTranscript = function() {
10771
10772		// Add an auto-scroll checkbox to the toolbar
10773
10774		this.$autoScrollTranscriptCheckbox = $('<input id="autoscroll-transcript-checkbox" type="checkbox">');
10775		this.$transcriptToolbar.append($('<label for="autoscroll-transcript-checkbox">' + this.tt.autoScroll + ': </label>'), this.$autoScrollTranscriptCheckbox);
10776
10777	};
10778
10779	AblePlayer.prototype.updateTranscript = function() {
10780
10781		if (!this.transcriptType) {
10782			return;
10783		}
10784
10785		if (this.transcriptType === 'external' || this.transcriptType === 'popup') {
10786
10787			var chapters, captions, descriptions;
10788
10789			// Language of transcript might be different than language of captions
10790			// But both are in sync by default
10791			if (this.transcriptLang) {
10792				captions = this.transcriptCaptions.cues;
10793			}
10794			else {
10795				if (this.transcriptCaptions) {
10796					this.transcriptLang = this.transcriptCaptions.language;
10797					captions = this.transcriptCaptions.cues;
10798				}
10799				else if (this.selectedCaptions) {
10800					this.transcriptLang = this.captionLang;
10801					captions = this.selectedCaptions.cues;
10802				}
10803			}
10804
10805			// setup chapters
10806			if (this.transcriptChapters) {
10807				chapters = this.transcriptChapters.cues;
10808			}
10809			else if (this.chapters.length > 0) {
10810				// Try and match the caption language.
10811				if (this.transcriptLang) {
10812					for (var i = 0; i < this.chapters.length; i++) {
10813						if (this.chapters[i].language === this.transcriptLang) {
10814							chapters = this.chapters[i].cues;
10815						}
10816					}
10817				}
10818				if (typeof chapters === 'undefined') {
10819					chapters = this.chapters[0].cues || [];
10820				}
10821			}
10822
10823			// setup descriptions
10824			if (this.transcriptDescriptions) {
10825				descriptions = this.transcriptDescriptions.cues;
10826			}
10827			else if (this.descriptions.length > 0) {
10828				// Try and match the caption language.
10829				if (this.transcriptLang) {
10830					for (var i = 0; i < this.descriptions.length; i++) {
10831						if (this.descriptions[i].language === this.transcriptLang) {
10832							descriptions = this.descriptions[i].cues;
10833						}
10834					}
10835				}
10836				if (!descriptions) {
10837					descriptions = this.descriptions[0].cues || [];
10838				}
10839			}
10840
10841			var div = this.generateTranscript(chapters || [], captions || [], descriptions || []);
10842
10843			this.$transcriptDiv.html(div);
10844			// reset transcript selected <option> to this.transcriptLang
10845			if (this.$transcriptLanguageSelect) {
10846				this.$transcriptLanguageSelect.find('option:selected').prop('selected',false);
10847				this.$transcriptLanguageSelect.find('option[lang=' + this.transcriptLang + ']').prop('selected',true);
10848			}
10849		}
10850
10851		var thisObj = this;
10852
10853		// Make transcript tabbable if preference is turned on.
10854		if (this.prefTabbable === 1) {
10855			this.$transcriptDiv.find('span.able-transcript-seekpoint').attr('tabindex','0');
10856		}
10857
10858		// handle clicks on text within transcript
10859		// Note: This event listeners handles clicks only, not keydown events
10860		// Pressing Enter on an element that is not natively clickable does NOT trigger click()
10861		// Keydown events are handled elsehwere, both globally (ableplayer-base.js) and locally (event.js)
10862		if (this.$transcriptArea.length > 0) {
10863			this.$transcriptArea.find('span.able-transcript-seekpoint').click(function(e) {
10864				thisObj.seekTrigger = 'transcript';
10865				var spanStart = parseFloat($(this).attr('data-start'));
10866				// Add a tiny amount so that we're inside the span.
10867				spanStart += .01;
10868				// Each click within the transcript triggers two click events (not sure why)
10869				// this.seekingFromTranscript is a stopgab to prevent two calls to SeekTo()
10870				if (!thisObj.seekingFromTranscript) {
10871					thisObj.seekingFromTranscript = true;
10872					thisObj.seekTo(spanStart);
10873				}
10874				else {
10875					// don't seek a second time, but do reset var
10876					thisObj.seekingFromTranscript = false;
10877				}
10878			});
10879		}
10880	};
10881
10882	AblePlayer.prototype.highlightTranscript = function (currentTime) {
10883
10884		//show highlight in transcript marking current caption
10885
10886		if (!this.transcriptType) {
10887			return;
10888		}
10889
10890		var start, end, isChapterHeading;
10891		var thisObj = this;
10892
10893		currentTime = parseFloat(currentTime);
10894
10895		// Highlight the current transcript item.
10896		this.$transcriptArea.find('span.able-transcript-seekpoint').each(function() {
10897			start = parseFloat($(this).attr('data-start'));
10898			end = parseFloat($(this).attr('data-end'));
10899			// be sure this isn't a chapter (don't highlight chapter headings)
10900			if ($(this).parent().hasClass('able-transcript-chapter-heading')) {
10901				isChapterHeading = true;
10902			}
10903			else {
10904				isChapterHeading = false;
10905			}
10906
10907			if (currentTime >= start && currentTime <= end && !isChapterHeading) {
10908
10909  		  // If this item isn't already highlighted, it should be
10910  		  if (!($(this).hasClass('able-highlight'))) {
10911  				// remove all previous highlights before adding one to current span
10912          thisObj.$transcriptArea.find('.able-highlight').removeClass('able-highlight');
10913          $(this).addClass('able-highlight');
10914          thisObj.movingHighlight = true;
10915        }
10916        return false;
10917			}
10918		});
10919		thisObj.currentHighlight = thisObj.$transcriptArea.find('.able-highlight');
10920		if (thisObj.currentHighlight.length === 0) {
10921			// Nothing highlighted.
10922			thisObj.currentHighlight = null;
10923		}
10924	};
10925
10926	AblePlayer.prototype.generateTranscript = function(chapters, captions, descriptions) {
10927
10928		var thisObj = this;
10929
10930		var $main = $('<div class="able-transcript-container"></div>');
10931		var transcriptTitle;
10932
10933		// set language for transcript container
10934		$main.attr('lang', this.transcriptLang);
10935
10936		if (typeof this.transcriptTitle !== 'undefined') {
10937			transcriptTitle = this.transcriptTitle;
10938		}
10939		else if (this.lyricsMode) {
10940			transcriptTitle = this.tt.lyricsTitle;
10941		}
10942		else {
10943			transcriptTitle = this.tt.transcriptTitle;
10944		}
10945
10946		if (typeof this.transcriptDivLocation === 'undefined') {
10947			// only add an HTML heading to internal transcript
10948			// external transcript is expected to have its own heading
10949			var headingNumber = this.playerHeadingLevel;
10950			headingNumber += 1;
10951			var chapterHeadingNumber = headingNumber + 1;
10952
10953			if (headingNumber <= 6) {
10954				var transcriptHeading = 'h' + headingNumber.toString();
10955			}
10956			else {
10957				var transcriptHeading = 'div';
10958			}
10959			// var transcriptHeadingTag = '<' + transcriptHeading + ' class="able-transcript-heading">';
10960			var $transcriptHeadingTag = $('<' + transcriptHeading + '>');
10961			$transcriptHeadingTag.addClass('able-transcript-heading');
10962			if (headingNumber > 6) {
10963				$transcriptHeadingTag.attr({
10964					'role': 'heading',
10965					'aria-level': headingNumber
10966				});
10967			}
10968			$transcriptHeadingTag.text(transcriptTitle);
10969
10970			// set language of transcript heading to language of player
10971			// this is independent of language of transcript
10972			$transcriptHeadingTag.attr('lang', this.lang);
10973
10974			$main.append($transcriptHeadingTag);
10975		}
10976
10977		var nextChapter = 0;
10978		var nextCap = 0;
10979		var nextDesc = 0;
10980
10981		var addChapter = function(div, chap) {
10982
10983			if (chapterHeadingNumber <= 6) {
10984				var chapterHeading = 'h' + chapterHeadingNumber.toString();
10985			}
10986			else {
10987				var chapterHeading = 'div';
10988			}
10989
10990			var $chapterHeadingTag = $('<' + chapterHeading + '>',{
10991				'class': 'able-transcript-chapter-heading'
10992			});
10993			if (chapterHeadingNumber > 6) {
10994				$chapterHeadingTag.attr({
10995					'role': 'heading',
10996					'aria-level': chapterHeadingNumber
10997				});
10998			}
10999
11000			var flattenComponentForChapter = function(comp) {
11001
11002				var result = [];
11003				if (comp.type === 'string') {
11004					result.push(comp.value);
11005				}
11006				else {
11007					for (var i = 0; i < comp.children.length; i++) {
11008						result = result.concat(flattenComponentForChapter(comp.children[i]));
11009					}
11010				}
11011				return result;
11012			}
11013
11014			var $chapSpan = $('<span>',{
11015				'class': 'able-transcript-seekpoint'
11016			});
11017			for (var i = 0; i < chap.components.children.length; i++) {
11018				var results = flattenComponentForChapter(chap.components.children[i]);
11019				for (var jj = 0; jj < results.length; jj++) {
11020					$chapSpan.append(results[jj]);
11021				}
11022			}
11023			$chapSpan.attr('data-start', chap.start.toString());
11024			$chapSpan.attr('data-end', chap.end.toString());
11025			$chapterHeadingTag.append($chapSpan);
11026
11027			div.append($chapterHeadingTag);
11028		};
11029
11030		var addDescription = function(div, desc) {
11031			var $descDiv = $('<div>', {
11032				'class': 'able-transcript-desc'
11033			});
11034			var $descHiddenSpan = $('<span>',{
11035				'class': 'able-hidden'
11036			});
11037			$descHiddenSpan.attr('lang', thisObj.lang);
11038			$descHiddenSpan.text(thisObj.tt.prefHeadingDescription + ': ');
11039			$descDiv.append($descHiddenSpan);
11040
11041			var flattenComponentForDescription = function(comp) {
11042
11043				var result = [];
11044				if (comp.type === 'string') {
11045					result.push(comp.value);
11046				}
11047				else {
11048					for (var i = 0; i < comp.children.length; i++) {
11049						result = result.concat(flattenComponentForDescription(comp.children[i]));
11050					}
11051				}
11052				return result;
11053			}
11054
11055			var $descSpan = $('<span>',{
11056				'class': 'able-transcript-seekpoint'
11057			});
11058			for (var i = 0; i < desc.components.children.length; i++) {
11059				var results = flattenComponentForDescription(desc.components.children[i]);
11060				for (var jj = 0; jj < results.length; jj++) {
11061					$descSpan.append(results[jj]);
11062				}
11063			}
11064			$descSpan.attr('data-start', desc.start.toString());
11065			$descSpan.attr('data-end', desc.end.toString());
11066			$descDiv.append($descSpan);
11067
11068			div.append($descDiv);
11069		};
11070
11071		var addCaption = function(div, cap) {
11072
11073			var $capSpan = $('<span>',{
11074				'class': 'able-transcript-seekpoint able-transcript-caption'
11075			});
11076
11077			var flattenComponentForCaption = function(comp) {
11078
11079				var result = [];
11080
11081				var parts = 0;
11082
11083				var flattenString = function (str) {
11084
11085					parts++;
11086
11087					var flatStr;
11088					var result = [];
11089					if (str === '') {
11090						return result;
11091					}
11092
11093					var openBracket = str.indexOf('[');
11094					var closeBracket = str.indexOf(']');
11095					var openParen = str.indexOf('(');
11096					var closeParen = str.indexOf(')');
11097
11098					var hasBrackets = openBracket !== -1 && closeBracket !== -1;
11099					var hasParens = openParen !== -1 && closeParen !== -1;
11100
11101					if (hasParens || hasBrackets) {
11102						if (parts > 1) {
11103							// force a line break between sections that contain parens or brackets
11104							var silentSpanBreak = '<br/>';
11105						}
11106						else {
11107							var silentSpanBreak = '';
11108						}
11109						var silentSpanOpen = silentSpanBreak + '<span class="able-unspoken">';
11110						var silentSpanClose = '</span>';
11111						if (hasParens && hasBrackets) {
11112							// string has both!
11113							if (openBracket < openParen) {
11114								// brackets come first. Parse parens separately
11115								hasParens = false;
11116							}
11117							else {
11118								// parens come first. Parse brackets separately
11119								hasBrackets = false;
11120							}
11121						}
11122					}
11123					if (hasParens) {
11124						flatStr = str.substring(0, openParen);
11125						flatStr += silentSpanOpen;
11126						flatStr += str.substring(openParen, closeParen + 1);
11127						flatStr += silentSpanClose;
11128						flatStr += flattenString(str.substring(closeParen + 1));
11129						result.push(flatStr);
11130					}
11131					else if (hasBrackets) {
11132						flatStr = str.substring(0, openBracket);
11133						flatStr += silentSpanOpen;
11134						flatStr += str.substring(openBracket, closeBracket + 1);
11135						flatStr += silentSpanClose;
11136						flatStr += flattenString(str.substring(closeBracket + 1));
11137						result.push(flatStr);
11138					}
11139					else {
11140						result.push(str);
11141					}
11142					return result;
11143				};
11144
11145				if (comp.type === 'string') {
11146					result = result.concat(flattenString(comp.value));
11147				}
11148				else if (comp.type === 'v') {
11149					var $vSpan = $('<span>',{
11150						'class': 'able-unspoken'
11151					});
11152					$vSpan.text('(' + comp.value + ')');
11153					result.push($vSpan);
11154					for (var i = 0; i < comp.children.length; i++) {
11155						var subResults = flattenComponentForCaption(comp.children[i]);
11156						for (var jj = 0; jj < subResults.length; jj++) {
11157							result.push(subResults[jj]);
11158						}
11159					}
11160				}
11161				else if (comp.type === 'b' || comp.type === 'i') {
11162					if (comp.type === 'b') {
11163						var $tag = $('<strong>');
11164					}
11165					else if (comp.type === 'i') {
11166						var $tag = $('<em>');
11167					}
11168					for (var i = 0; i < comp.children.length; i++) {
11169						var subResults = flattenComponentForCaption(comp.children[i]);
11170						for (var jj = 0; jj < subResults.length; jj++) {
11171							$tag.append(subResults[jj]);
11172						}
11173					}
11174					if (comp.type === 'b' || comp.type == 'i') {
11175						result.push($tag,' ');
11176					}
11177				}
11178				else {
11179					for (var i = 0; i < comp.children.length; i++) {
11180						result = result.concat(flattenComponentForCaption(comp.children[i]));
11181					}
11182				}
11183				return result;
11184			};
11185
11186			for (var i = 0; i < cap.components.children.length; i++) {
11187				var results = flattenComponentForCaption(cap.components.children[i]);
11188				for (var jj = 0; jj < results.length; jj++) {
11189					var result = results[jj];
11190					if (typeof result === 'string') {
11191						if (thisObj.lyricsMode) {
11192							// add <br> BETWEEN each caption and WITHIN each caption (if payload includes "\n")
11193							result = result.replace('\n','<br>') + '<br>';
11194						}
11195						else {
11196							// just add a space between captions
11197							result += ' ';
11198						}
11199					}
11200					$capSpan.append(result);
11201				}
11202			}
11203			$capSpan.attr('data-start', cap.start.toString());
11204			$capSpan.attr('data-end', cap.end.toString());
11205			div.append($capSpan);
11206			div.append(' \n');
11207		};
11208
11209		// keep looping as long as any one of the three arrays has content
11210		while ((nextChapter < chapters.length) || (nextDesc < descriptions.length) || (nextCap < captions.length)) {
11211
11212			if ((nextChapter < chapters.length) && (nextDesc < descriptions.length) && (nextCap < captions.length)) {
11213				// they all three have content
11214				var firstStart = Math.min(chapters[nextChapter].start,descriptions[nextDesc].start,captions[nextCap].start);
11215			}
11216			else if ((nextChapter < chapters.length) && (nextDesc < descriptions.length)) {
11217				// chapters & descriptions have content
11218				var firstStart = Math.min(chapters[nextChapter].start,descriptions[nextDesc].start);
11219			}
11220			else if ((nextChapter < chapters.length) && (nextCap < captions.length)) {
11221				// chapters & captions have content
11222				var firstStart = Math.min(chapters[nextChapter].start,captions[nextCap].start);
11223			}
11224			else if ((nextDesc < descriptions.length) && (nextCap < captions.length)) {
11225				// descriptions & captions have content
11226				var firstStart = Math.min(descriptions[nextDesc].start,captions[nextCap].start);
11227			}
11228			else {
11229				var firstStart = null;
11230			}
11231			if (firstStart !== null) {
11232				if (typeof chapters[nextChapter] !== 'undefined' && chapters[nextChapter].start === firstStart) {
11233					addChapter($main, chapters[nextChapter]);
11234					nextChapter += 1;
11235				}
11236				else if (typeof descriptions[nextDesc] !== 'undefined' && descriptions[nextDesc].start === firstStart) {
11237					addDescription($main, descriptions[nextDesc]);
11238					nextDesc += 1;
11239				}
11240				else {
11241					addCaption($main, captions[nextCap]);
11242					nextCap += 1;
11243				}
11244			}
11245			else {
11246				if (nextChapter < chapters.length) {
11247					addChapter($main, chapters[nextChapter]);
11248					nextChapter += 1;
11249				}
11250				else if (nextDesc < descriptions.length) {
11251					addDescription($main, descriptions[nextDesc]);
11252					nextDesc += 1;
11253				}
11254				else if (nextCap < captions.length) {
11255					addCaption($main, captions[nextCap]);
11256					nextCap += 1;
11257				}
11258			}
11259		}
11260		// organize transcript into blocks using [] and () as starting points
11261		var $components = $main.children();
11262		var spanCount = 0;
11263		var openBlock = true;
11264		$components.each(function() {
11265			if ($(this).hasClass('able-transcript-caption')) {
11266				if ($(this).text().indexOf('[') !== -1 || $(this).text().indexOf('(') !== -1) {
11267					// this caption includes a bracket or parenth. Start a new block
11268					// close the previous block first
11269					if (spanCount > 0) {
11270						$main.find('.able-block-temp').removeClass('able-block-temp').wrapAll('<div class="able-transcript-block"></div>');
11271						spanCount = 0;
11272					}
11273				}
11274				$(this).addClass('able-block-temp');
11275				spanCount++;
11276			}
11277			else {
11278				// this is not a caption. Close the caption block
11279				if (spanCount > 0) {
11280					$main.find('.able-block-temp').removeClass('able-block-temp').wrapAll('<div class="able-transcript-block"></div>');
11281					spanCount = 0;
11282				}
11283			}
11284		});
11285		return $main;
11286	};
11287
11288})(jQuery);
11289
11290(function ($) {
11291	AblePlayer.prototype.showSearchResults = function() {
11292
11293		// search VTT file for all instances of searchTerms
11294		// Currently just supports search terms separated with one or more spaces
11295
11296		// TODO: Add support for more robust search syntax:
11297		// Search terms wrapped in quotation marks ("") must occur exactly as they appear in the quotes
11298		// Search terms with an attached minus sign (e.g., -term) are to be excluded from results
11299		// Boolean AND/OR operators
11300		// ALSO: Add localization support
11301
11302		var thisObj = this;
11303		if (this.searchDiv && this.searchString) {
11304			if ($('#' + this.SearchDiv)) {
11305  			var searchStringHtml = '<p>' + this.tt.resultsSummary1 + ' ';
11306  			searchStringHtml += '<span id="able-search-term-echo">' + this.searchString + '</span>';
11307  			searchStringHtml += '</p>';
11308				var resultsArray = this.searchFor(this.searchString);
11309				if (resultsArray.length > 0) {
11310					var $resultsSummary = $('<p>',{
11311						'class': 'able-search-results-summary'
11312					});
11313					var resultsSummaryText = this.tt.resultsSummary2;
11314					resultsSummaryText += ' <strong>' + resultsArray.length + '</strong> ';
11315					resultsSummaryText += this.tt.resultsSummary3 + ' ';
11316					resultsSummaryText += this.tt.resultsSummary4;
11317					$resultsSummary.html(resultsSummaryText);
11318					var $resultsList = $('<ul>');
11319					for (var i = 0; i < resultsArray.length; i++) {
11320  					var resultId = 'aria-search-result-' + i;
11321						var $resultsItem = $('<li>',{});
11322						var itemStartTime = this.secondsToTime(resultsArray[i]['start']);
11323						var itemLabel = this.tt.searchButtonLabel + ' ' + itemStartTime['title'];
11324						var itemStartSpan = $('<button>',{
11325							'class': 'able-search-results-time',
11326							'data-start': resultsArray[i]['start'],
11327							'title': itemLabel,
11328							'aria-label': itemLabel,
11329							'aria-describedby': resultId
11330						});
11331						itemStartSpan.text(itemStartTime['value']);
11332						// add a listener for clisk on itemStart
11333						itemStartSpan.on('click',function(e) {
11334              thisObj.seekTrigger = 'search';
11335							var spanStart = parseFloat($(this).attr('data-start'));
11336							// Add a tiny amount so that we're inside the span.
11337							spanStart += .01;
11338							thisObj.seeking = true;
11339							thisObj.seekTo(spanStart);
11340						});
11341
11342						var itemText = $('<span>',{
11343							'class': 'able-search-result-text',
11344							'id': resultId
11345						})
11346						itemText.html('...' + resultsArray[i]['caption'] + '...');
11347						$resultsItem.append(itemStartSpan, itemText);
11348						$resultsList.append($resultsItem);
11349					}
11350					$('#' + this.searchDiv).append(searchStringHtml,$resultsSummary,$resultsList);
11351				}
11352				else {
11353					var noResults = $('<p>').text(this.tt.noResultsFound);
11354					$('#' + this.searchDiv).append(noResults);
11355				}
11356			}
11357		}
11358	};
11359
11360	AblePlayer.prototype.searchFor = function(searchString) {
11361
11362		// return chronological array of caption cues that match searchTerms
11363		var captionLang, captions, results, caption, c, i, j;
11364		results = [];
11365		// split searchTerms into an array
11366		var searchTerms = searchString.split(' ');
11367		if (this.captions.length > 0) {
11368			// Get caption track that matches this.searchLang
11369			for (i=0; i < this.captions.length; i++) {
11370				if (this.captions[i].language === this.searchLang) {
11371					captionLang = this.searchLang;
11372					captions = this.captions[i].cues;
11373				}
11374			}
11375			if (captions.length > 0) {
11376				c = 0;
11377				for (i = 0; i < captions.length; i++) {
11378					if ($.inArray(captions[i].components.children[0]['type'], ['string','i','b','u','v','c']) !== -1) {
11379						caption = this.flattenCueForCaption(captions[i]);
11380						for (j = 0; j < searchTerms.length; j++) {
11381							if (caption.indexOf(searchTerms[j]) !== -1) {
11382								results[c] = [];
11383								results[c]['start'] = captions[i].start;
11384								results[c]['lang'] = captionLang;
11385								results[c]['caption'] = this.highlightSearchTerm(searchTerms,j,caption);
11386								c++;
11387								break;
11388							}
11389						}
11390					}
11391				}
11392			}
11393		}
11394		return results;
11395	};
11396
11397	AblePlayer.prototype.highlightSearchTerm = function(searchTerms, index, resultString) {
11398
11399		// highlight ALL found searchTerms in the current resultString
11400		// index is the first index in the searchTerm array where a match has already been found
11401		// Need to step through the remaining terms to see if they're present as well
11402
11403		var i, searchTerm, termIndex, termLength, str1, str2, str3;
11404
11405		for (i=index; i<searchTerms.length; i++) {
11406
11407			searchTerm = searchTerms[i];
11408			termIndex = resultString.indexOf(searchTerm);
11409			if (termIndex !== -1) {
11410				termLength = searchTerm.length;
11411				if (termLength > 0) {
11412					str1 = resultString.substring(0, termIndex);
11413					str2 = '<span class="able-search-term">' + searchTerm + '</span>';
11414					str3 = resultString.substring(termIndex+termLength);
11415					resultString = str1 + str2 + str3;
11416				}
11417				else {
11418					str1 = '<span class="able-search-term">' + searchTerm + '</span>';
11419					str2 = resultString.substring(termIndex+termLength);
11420					resultString = str1 + str2;
11421				}
11422			}
11423		}
11424		return resultString;
11425	};
11426
11427	AblePlayer.prototype.secondsToTime = function(totalSeconds) {
11428
11429		// return an array of totalSeconds converted into two formats
11430		// time['value'] = HH:MM:SS with hours dropped if there are none
11431		// time['title'] = a speakable rendering, so speech rec users can easily speak the link
11432
11433		// first, round down to nearest second
11434		var totalSeconds = Math.floor(totalSeconds);
11435
11436		var hours = parseInt( totalSeconds / 3600 , 10) % 24;
11437		var minutes = parseInt( totalSeconds / 60 , 10) % 60;
11438		var seconds = totalSeconds % 60;
11439		var value = '';
11440		var title = '';
11441		if (hours > 0) {
11442			value += hours + ':';
11443      if (hours == 1) {
11444  			title += '1 ' + this.tt.hour + ' ';
11445      }
11446      else {
11447  			title += hours + ' ' + this.tt.hours + ' ';
11448		  }
11449		}
11450		if (minutes < 10) {
11451			value += '0' + minutes + ':';
11452			if (minutes > 0) {
11453  			if (minutes == 1) {
11454  				title += '1 ' + this.tt.minute + ' ';
11455        }
11456        else {
11457  				title += minutes + ' ' + this.tt.minutes + ' ';
11458        }
11459			}
11460		}
11461		else {
11462			value += minutes + ':';
11463			title += minutes + ' ' + this.tt.minutes + ' ';
11464		}
11465		if (seconds < 10) {
11466			value += '0' + seconds;
11467			if (seconds > 0) {
11468  			if (seconds == 1) {
11469  				title += '1 ' + this.tt.second + ' ';
11470  			}
11471  			else {
11472  				title += seconds + ' ' + this.tt.seconds + ' ';
11473        }
11474			}
11475		}
11476		else {
11477			value += seconds;
11478			title += seconds + ' ' + this.tt.seconds + ' ';
11479		}
11480		var time = [];
11481		time['value'] = value;
11482		time['title'] = title;
11483		return time;
11484	};
11485})(jQuery);
11486
11487(function ($) {
11488	// Media events
11489	AblePlayer.prototype.onMediaUpdateTime = function (duration, elapsed) {
11490
11491		// duration and elapsed are passed from callback functions of Vimeo API events
11492		// duration is expressed as sss.xxx
11493		// elapsed is expressed as sss.xxx
11494		var thisObj = this;
11495		this.getMediaTimes(duration,elapsed).then(function(mediaTimes) {
11496		  thisObj.duration = mediaTimes['duration'];
11497      thisObj.elapsed = mediaTimes['elapsed'];
11498			if (thisObj.swappingSrc && (typeof thisObj.swapTime !== 'undefined')) {
11499				if (thisObj.swapTime === thisObj.elapsed) {
11500					// described version been swapped and media has scrubbed to time of previous version
11501					if (thisObj.playing) {
11502						// resume playback
11503						thisObj.playMedia();
11504						// reset vars
11505						thisObj.swappingSrc = false;
11506						thisObj.swapTime = null;
11507					}
11508				}
11509			}
11510      else {
11511				// do all the usual time-sync stuff during playback
11512				if (thisObj.prefHighlight === 1) {
11513					thisObj.highlightTranscript(thisObj.elapsed);
11514				}
11515				thisObj.updateCaption(thisObj.elapsed);
11516				thisObj.showDescription(thisObj.elapsed);
11517				thisObj.updateChapter(thisObj.elapsed);
11518				thisObj.updateMeta(thisObj.elapsed);
11519				thisObj.refreshControls('timeline', thisObj.duration, thisObj.elapsed);
11520			}
11521		});
11522	};
11523
11524	AblePlayer.prototype.onMediaPause = function () {
11525
11526		if (this.controlsHidden) {
11527			this.fadeControls('in');
11528			this.controlsHidden = false;
11529		}
11530		if (this.hideControlsTimeoutStatus === 'active') {
11531			window.clearTimeout(this.hideControlsTimeout);
11532			this.hideControlsTimeoutStatus = 'clear';
11533
11534		}
11535		this.refreshControls('playpause');
11536	};
11537
11538	AblePlayer.prototype.onMediaComplete = function () {
11539		// if there's a playlist, advance to next item and start playing
11540		if (this.hasPlaylist && !this.cueingPlaylistItem) {
11541			if (this.playlistIndex === (this.$playlist.length - 1)) {
11542				// this is the last track in the playlist
11543				if (this.loop) {
11544					this.playlistIndex = 0;
11545					this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
11546					this.cuePlaylistItem(0);
11547				}
11548				else {
11549  				this.playing = false;
11550  				this.paused = true;
11551				}
11552			}
11553			else {
11554				// this is not the last track. Play the next one.
11555				this.playlistIndex++;
11556				this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
11557				this.cuePlaylistItem(this.playlistIndex)
11558			}
11559		}
11560		this.refreshControls('init');
11561	};
11562
11563	AblePlayer.prototype.onMediaNewSourceLoad = function () {
11564
11565		if (this.cueingPlaylistItem) {
11566			// this variable was set in order to address bugs caused by multiple firings of media 'end' event
11567			// safe to reset now
11568			this.cueingPlaylistItem = false;
11569		}
11570		if (this.swappingSrc === true) {
11571			// new source file has just been loaded
11572			if (this.swapTime > 0) {
11573				// this.swappingSrc will be set to false after seek is complete
11574				// see onMediaUpdateTime()
11575				this.seekTo(this.swapTime);
11576			}
11577			else {
11578				if (this.playing) {
11579					// should be able to resume playback
11580					this.playMedia();
11581				}
11582				this.swappingSrc = false; // swapping is finished
11583			}
11584		}
11585		this.refreshControls('init');
11586	};
11587
11588	// End Media events
11589
11590	AblePlayer.prototype.onWindowResize = function () {
11591
11592		if (this.fullscreen) { // replace isFullscreen() with a Boolean. see function for explanation
11593
11594			var newWidth, newHeight;
11595
11596			newWidth = $(window).width();
11597
11598			// haven't isolated why, but some browsers return an innerHeight that's 20px too tall in fullscreen mode
11599			// Test results:
11600			// Browsers that require a 20px adjustment: Firefox, IE11 (Trident), Edge
11601			if (this.isUserAgent('Firefox') || this.isUserAgent('Trident') || this.isUserAgent('Edge')) {
11602				newHeight = window.innerHeight - this.$playerDiv.outerHeight() - 20;
11603			}
11604			else if (window.outerHeight >= window.innerHeight) {
11605				// Browsers that do NOT require adjustment: Chrome, Safari, Opera, MSIE 10
11606				newHeight = window.innerHeight - this.$playerDiv.outerHeight();
11607			}
11608			else {
11609				// Observed in Safari 9.0.1 on Mac OS X: outerHeight is actually less than innerHeight
11610				// Maybe a bug, or maybe window.outerHeight is already adjusted for controller height(?)
11611				// No longer observed in Safari 9.0.2
11612				newHeight = window.outerHeight;
11613			}
11614			if (!this.$descDiv.is(':hidden')) {
11615				newHeight -= this.$descDiv.height();
11616			}
11617			this.positionCaptions('overlay');
11618		}
11619		else { // not fullscreen
11620			if (this.restoringAfterFullScreen) {
11621				newWidth = this.preFullScreenWidth;
11622				newHeight = this.preFullScreenHeight;
11623			}
11624			else {
11625				// not restoring after full screen
11626				newWidth = this.$ableWrapper.width();
11627				if (typeof this.aspectRatio !== 'undefined') {
11628					newHeight = Math.round(newWidth / this.aspectRatio);
11629				}
11630				else {
11631					// not likely, since this.aspectRatio is defined during intialization
11632					// however, this is a fallback scenario just in case
11633					newHeight = this.$ableWrapper.height();
11634				}
11635				this.positionCaptions(); // reset with this.prefCaptionsPosition
11636			}
11637		}
11638		this.resizePlayer(newWidth, newHeight);
11639	};
11640
11641	AblePlayer.prototype.addSeekbarListeners = function () {
11642
11643		var thisObj = this;
11644
11645		// Handle seek bar events.
11646		this.seekBar.bodyDiv.on('startTracking', function (e) {
11647			thisObj.pausedBeforeTracking = thisObj.paused;
11648			thisObj.pauseMedia();
11649		}).on('tracking', function (e, position) {
11650			// Scrub transcript, captions, and metadata.
11651			thisObj.highlightTranscript(position);
11652			thisObj.updateCaption(position);
11653			thisObj.showDescription(position);
11654			thisObj.updateChapter(thisObj.convertChapterTimeToVideoTime(position));
11655			thisObj.updateMeta(position);
11656			thisObj.refreshControls('init');
11657		}).on('stopTracking', function (e, position) {
11658			if (thisObj.useChapterTimes) {
11659				thisObj.seekTo(thisObj.convertChapterTimeToVideoTime(position));
11660			}
11661			else {
11662				thisObj.seekTo(position);
11663			}
11664			if (!thisObj.pausedBeforeTracking) {
11665				setTimeout(function () {
11666					thisObj.playMedia();
11667				}, 200);
11668			}
11669		});
11670	};
11671
11672	AblePlayer.prototype.onClickPlayerButton = function (el) {
11673
11674		// TODO: This is super-fragile since we need to know the length of the class name to split off; update this to other way of dispatching?
11675
11676		var whichButton, prefsPopup;
11677		whichButton = $(el).attr('class').split(' ')[0].substr(20);
11678		if (whichButton === 'play') {
11679  		this.clickedPlay = true;
11680			this.handlePlay();
11681		}
11682		else if (whichButton === 'restart') {
11683			this.seekTrigger = 'restart';
11684			this.handleRestart();
11685		}
11686		else if (whichButton === 'previous') {
11687  		this.userClickedPlaylist = true;
11688			this.seekTrigger = 'previous';
11689			this.buttonWithFocus = 'previous';
11690			this.handlePrevTrack();
11691		}
11692		else if (whichButton === 'next') {
11693  		this.userClickedPlaylist = true;
11694			this.seekTrigger = 'next';
11695			this.buttonWithFocus = 'next';
11696			this.handleNextTrack();
11697		}
11698		else if (whichButton === 'rewind') {
11699			this.seekTrigger = 'rewind';
11700			this.handleRewind();
11701		}
11702		else if (whichButton === 'forward') {
11703			this.seekTrigger = 'forward';
11704			this.handleFastForward();
11705		}
11706		else if (whichButton === 'mute') {
11707			this.handleMute();
11708		}
11709		else if (whichButton === 'volume') {
11710			this.handleVolume();
11711		}
11712		else if (whichButton === 'faster') {
11713			this.handleRateIncrease();
11714		}
11715		else if (whichButton === 'slower') {
11716			this.handleRateDecrease();
11717		}
11718		else if (whichButton === 'captions') {
11719			this.handleCaptionToggle();
11720		}
11721		else if (whichButton === 'chapters') {
11722			this.handleChapters();
11723		}
11724		else if (whichButton === 'descriptions') {
11725			this.handleDescriptionToggle();
11726		}
11727		else if (whichButton === 'sign') {
11728  		if (!this.closingSign) {
11729  			this.handleSignToggle();
11730  		}
11731		}
11732		else if (whichButton === 'preferences') {
11733      if ($(el).attr('data-prefs-popup') === 'menu') {
11734  			this.handlePrefsClick();
11735  		}
11736  		else {
11737    		this.showingPrefsDialog = true; // stopgap
11738        this.closePopups();
11739    		prefsPopup = $(el).attr('data-prefs-popup');
11740        if (prefsPopup === 'keyboard') {
11741				  this.keyboardPrefsDialog.show();
11742				}
11743        else if (prefsPopup === 'captions') {
11744				  this.captionPrefsDialog.show();
11745				}
11746        else if (prefsPopup === 'descriptions') {
11747				  this.descPrefsDialog.show();
11748				}
11749        else if (prefsPopup === 'transcript') {
11750				  this.transcriptPrefsDialog.show();
11751				}
11752        this.showingPrefsDialog = false;
11753  		}
11754		}
11755		else if (whichButton === 'help') {
11756			this.handleHelpClick();
11757		}
11758		else if (whichButton === 'transcript') {
11759      if (!this.closingTranscript) {
11760  			this.handleTranscriptToggle();
11761  		}
11762		}
11763		else if (whichButton === 'fullscreen') {
11764			this.clickedFullscreenButton = true;
11765			this.handleFullscreenToggle();
11766		}
11767	};
11768
11769	AblePlayer.prototype.okToHandleKeyPress = function () {
11770
11771		// returns true unless user's focus is on a UI element
11772		// that is likely to need supported keystrokes, including space
11773
11774		var activeElement = AblePlayer.getActiveDOMElement();
11775
11776		if ($(activeElement).prop('tagName') === 'INPUT') {
11777			return false;
11778		}
11779		else {
11780			return true;
11781		}
11782	};
11783
11784	AblePlayer.prototype.onPlayerKeyPress = function (e) {
11785
11786		// handle keystrokes (using DHTML Style Guide recommended key combinations)
11787		// https://web.archive.org/web/20130127004544/http://dev.aol.com/dhtml_style_guide/#mediaplayer
11788		// Modifier keys Alt + Ctrl are on by default, but can be changed within Preferences
11789		// NOTE #1: Style guide only supports Play/Pause, Stop, Mute, Captions, & Volume Up & Down
11790		// The rest are reasonable best choices
11791		// NOTE #2: If there are multiple players on a single page, keystroke handlers
11792		// are only bound to the FIRST player
11793		// NOTE #3: The DHTML Style Guide is now the W3C WAI-ARIA Authoring Guide and has undergone many revisions
11794		// including removal of the "media player" design pattern. There's an issue about that:
11795		// https://github.com/w3c/aria-practices/issues/27
11796
11797		var which, $thisElement;
11798
11799		// Convert to lower case.
11800		which = e.which;
11801		if (which >= 65 && which <= 90) {
11802			which += 32;
11803		}
11804		$thisElement = $(document.activeElement);
11805
11806    if (which === 27) { // escape
11807console.log('onPlayerKeyPress, you pressed Escape');
11808      if ($.contains(this.$transcriptArea[0],$thisElement[0])) {
11809console.log('element is part of the transcript area');
11810        // This element is part of transcript area.
11811        this.handleTranscriptToggle();
11812        return false;
11813      }
11814    }
11815		if (!this.okToHandleKeyPress()) {
11816console.log('NOT ok!');
11817			return false;
11818		}
11819
11820		// Only use keypress to control player if focus is NOT on a form field or contenteditable element
11821		// (or a textarea element with player in stenoMode)
11822		if (!(
11823			$(':focus').is('[contenteditable]') ||
11824			$(':focus').is('input') ||
11825			($(':focus').is('textarea') && !this.stenoMode) ||
11826			$(':focus').is('select') ||
11827			e.target.hasAttribute('contenteditable') ||
11828			e.target.tagName === 'INPUT' ||
11829			(e.target.tagName === 'TEXTAREA' && !this.stenoMode) ||
11830			e.target.tagName === 'SELECT'
11831		)){
11832			if (which === 27) { // escape
11833console.log('You pushed ESC');
11834				this.closePopups();
11835			}
11836			else if (which === 32) { // spacebar = play/pause
11837  			// disable spacebar support for play/pause toggle as of 4.2.10
11838  			// spacebar should not be handled everywhere on the page, since users use that to scroll the page
11839  			// when the player has focus, most controls are buttons so spacebar should be used to trigger the buttons
11840				if ($thisElement.attr('role') === 'button') {
11841					// register a click on this element
11842					e.preventDefault();
11843					$thisElement.click();
11844				}
11845			}
11846			else if (which === 112) { // p = play/pause
11847				if (this.usingModifierKeys(e)) {
11848  				e.preventDefault();
11849					this.handlePlay();
11850				}
11851			}
11852			else if (which === 115) { // s = stop (now restart)
11853				if (this.usingModifierKeys(e)) {
11854  				e.preventDefault();
11855					this.handleRestart();
11856				}
11857			}
11858			else if (which === 109) { // m = mute
11859				if (this.usingModifierKeys(e)) {
11860  				e.preventDefault();
11861					this.handleMute();
11862				}
11863			}
11864			else if (which === 118) { // v = volume
11865				if (this.usingModifierKeys(e)) {
11866  				e.preventDefault();
11867					this.handleVolume();
11868				}
11869			}
11870			else if (which >= 49 && which <= 57) { // set volume 1-9
11871				if (this.usingModifierKeys(e)) {
11872  				e.preventDefault();
11873					this.handleVolume(which);
11874				}
11875			}
11876			else if (which === 99) { // c = caption toggle
11877				if (this.usingModifierKeys(e)) {
11878  				e.preventDefault();
11879					this.handleCaptionToggle();
11880				}
11881			}
11882			else if (which === 100) { // d = description
11883				if (this.usingModifierKeys(e)) {
11884  				e.preventDefault();
11885					this.handleDescriptionToggle();
11886				}
11887			}
11888			else if (which === 102) { // f = forward
11889				if (this.usingModifierKeys(e)) {
11890  				e.preventDefault();
11891					this.handleFastForward();
11892				}
11893			}
11894			else if (which === 114) { // r = rewind
11895				if (this.usingModifierKeys(e)) {
11896  				e.preventDefault();
11897					this.handleRewind();
11898				}
11899			}
11900			else if (which === 98) { // b = back (previous track)
11901				if (this.usingModifierKeys(e)) {
11902  				e.preventDefault();
11903					this.handlePrevTrack();
11904				}
11905			}
11906			else if (which === 110) { // n = next track
11907				if (this.usingModifierKeys(e)) {
11908  				e.preventDefault();
11909					this.handleNextTrack();
11910				}
11911			}
11912			else if (which === 101) { // e = preferences
11913				if (this.usingModifierKeys(e)) {
11914  				e.preventDefault();
11915					this.handlePrefsClick();
11916				}
11917			}
11918			else if (which === 13) { // Enter
11919				if ($thisElement.attr('role') === 'button' || $thisElement.prop('tagName') === 'SPAN') {
11920					// register a click on this element
11921					// if it's a transcript span the transcript span click handler will take over
11922					$thisElement.click();
11923				}
11924				else if ($thisElement.prop('tagName') === 'LI') {
11925					$thisElement.click();
11926				}
11927			}
11928		}
11929	};
11930
11931	AblePlayer.prototype.addHtml5MediaListeners = function () {
11932
11933		var thisObj = this;
11934
11935		// NOTE: iOS and some browsers do not support autoplay
11936		// and no events are triggered until media begins to play
11937		// Able Player gets around this by automatically loading media in some circumstances
11938		// (see initialize.js > initPlayer() for details)
11939		this.$media
11940			.on('emptied',function() {
11941				// do something
11942			})
11943			.on('loadedmetadata',function() {
11944        // should be able to get duration now
11945        thisObj.duration = thisObj.media.duration;
11946				thisObj.onMediaNewSourceLoad();
11947			})
11948			.on('canplay',function() {
11949				// previously handled seeking to startTime here
11950				// but it's probably safer to wait for canplaythrough
11951				// so we know player can seek ahead to anything
11952			})
11953			.on('canplaythrough',function() {
11954  		  if (thisObj.playbackRate) {
11955          // user has set playbackRate on a previous src or track
11956          // use that setting on the new src or track too
11957          thisObj.setPlaybackRate(thisObj.playbackRate);
11958  		  }
11959				if (thisObj.userClickedPlaylist) {
11960					if (!thisObj.startedPlaying) {
11961						// start playing; no further user action is required
11962						thisObj.playMedia();
11963				 	}
11964					thisObj.userClickedPlaylist = false; // reset
11965				}
11966				if (thisObj.seekTrigger == 'restart' ||
11967				    thisObj.seekTrigger == 'chapter' ||
11968				    thisObj.seekTrigger == 'transcript' ||
11969				    thisObj.seekTrigger == 'search'
11970            ) {
11971					// by clicking on any of these elements, user is likely intending to play
11972					// Not included: elements where user might click multiple times in succession
11973					// (i.e., 'rewind', 'forward', or seekbar); for these, video remains paused until user initiates play
11974					thisObj.playMedia();
11975				}
11976				else if (!thisObj.startedPlaying) {
11977					if (thisObj.startTime > 0) {
11978						if (thisObj.seeking) {
11979							// a seek has already been initiated
11980							// since canplaythrough has been triggered, the seek is complete
11981							thisObj.seeking = false;
11982							if (thisObj.autoplay || thisObj.okToPlay) {
11983								thisObj.playMedia();
11984							}
11985						}
11986						else {
11987							// haven't started seeking yet
11988							thisObj.seekTo(thisObj.startTime);
11989						}
11990					}
11991					else if (thisObj.defaultChapter && typeof thisObj.selectedChapters !== 'undefined') {
11992						thisObj.seekToChapter(thisObj.defaultChapter);
11993					}
11994					else {
11995						// there is no startTime, therefore no seeking required
11996						if (thisObj.autoplay || thisObj.okToPlay) {
11997							thisObj.playMedia();
11998						}
11999					}
12000				}
12001				else if (thisObj.hasPlaylist) {
12002					if ((thisObj.playlistIndex !== thisObj.$playlist.length) || thisObj.loop) {
12003						// this is not the last track in the playlist (OR playlist is looping so it doesn't matter)
12004						thisObj.playMedia();
12005					}
12006				}
12007				else {
12008					// already started playing
12009					// we're here because a new media source has been loaded and is ready to resume playback
12010					thisObj.getPlayerState().then(function(currentState) {
12011						if (thisObj.swappingSrc && (currentState === 'stopped' || currentState === 'paused')) {
12012							thisObj.startedPlaying = false;
12013							if (thisObj.swapTime > 0) {
12014								thisObj.seekTo(thisObj.swapTime);
12015							}
12016							else {
12017								thisObj.playMedia();
12018							}
12019						}
12020					});
12021				}
12022			})
12023			.on('play',function() {
12024				// both 'play' and 'playing' seem to be fired in all browsers (including IE11)
12025				// therefore, doing nothing here & doing everything when 'playing' is triggered
12026				 thisObj.refreshControls('playpause');
12027			})
12028			.on('playing',function() {
12029				thisObj.playing = true;
12030				thisObj.paused = false;
12031				thisObj.refreshControls('playpause');
12032			})
12033			.on('ended',function() {
12034				thisObj.playing = false;
12035				thisObj.paused = true;
12036				thisObj.onMediaComplete();
12037			})
12038			.on('progress', function() {
12039				thisObj.refreshControls('timeline');
12040			})
12041			.on('waiting',function() {
12042				 // do something
12043				 // previously called refreshControls() here but this event probably doesn't warrant a refresh
12044			})
12045			.on('durationchange',function() {
12046				// Display new duration.
12047				thisObj.refreshControls('timeline');
12048			})
12049			.on('timeupdate',function() {
12050				thisObj.onMediaUpdateTime();
12050 // includes a call to refreshControls()
12051			})
12052			.on('pause',function() {
12053				if (!thisObj.clickedPlay) {
12054					// 'pause' was triggered automatically, not initiated by user
12055					// this happens in some browsers when swapping source
12056					// (e.g., between tracks in a playlist or swapping description)
12057					if (thisObj.hasPlaylist || thisObj.swappingSrc) {
12058						// do NOT set playing to false.
12059						// doing so prevents continual playback after new track is loaded
12060					}
12061					else {
12062						thisObj.playing = false;
12063						thisObj.paused = true;
12064					}
12065				}
12066				else {
12067					thisObj.playing = false;
12068					thisObj.paused = true;
12069				}
12070				thisObj.clickedPlay = false; // done with this variable
12071				thisObj.onMediaPause(); // includes a call to refreshControls()
12072			})
12073			.on('ratechange',function() {
12074				// do something
12075			})
12076			.on('volumechange',function() {
12077				thisObj.volume = thisObj.getVolume();
12078				if (thisObj.debug) {
12079					console.log('media volume change to ' + thisObj.volume + ' (' + thisObj.volumeButton + ')');
12080				}
12081			})
12082			.on('error',function() {
12083				if (thisObj.debug) {
12084					switch (thisObj.media.error.code) {
12085						case 1:
12086							console.log('HTML5 Media Error: MEDIA_ERR_ABORTED');
12087							break;
12088						case 2:
12089							console.log('HTML5 Media Error: MEDIA_ERR_NETWORK ');
12090							break;
12091						case 3:
12092							console.log('HTML5 Media Error: MEDIA_ERR_DECODE ');
12093							break;
12094						case 4:
12095							console.log('HTML5 Media Error: MEDIA_ERR_SRC_NOT_SUPPORTED ');
12096							break;
12097					}
12098				}
12099			});
12100	};
12101
12102	AblePlayer.prototype.addVimeoListeners = function () {
12103
12104// The following content is orphaned. It was in 'canplaythrough' but there's no equivalent event in Vimeo.
12105// Maybe it should go under 'loaded' or 'progress' ???
12106/*
12107				if (thisObj.userClickedPlaylist) {
12108					if (!thisObj.startedPlaying) {
12109							// start playing; no further user action is required
12110						thisObj.playMedia();
12111				 		}
12112					thisObj.userClickedPlaylist = false; // reset
12113				}
12114				if (thisObj.seekTrigger == 'restart' || thisObj.seekTrigger == 'chapter' || thisObj.seekTrigger == 'transcript') {
12115					// by clicking on any of these elements, user is likely intending to play
12116					// Not included: elements where user might click multiple times in succession
12117					// (i.e., 'rewind', 'forward', or seekbar); for these, video remains paused until user initiates play
12118					thisObj.playMedia();
12119				}
12120				else if (!thisObj.startedPlaying) {
12121					if (thisObj.startTime > 0) {
12122						if (thisObj.seeking) {
12123							// a seek has already been initiated
12124							// since canplaythrough has been triggered, the seek is complete
12125							thisObj.seeking = false;
12126							if (thisObj.autoplay || thisObj.okToPlay) {
12127								thisObj.playMedia();
12128							}
12129						}
12130						else {
12131							// haven't started seeking yet
12132							thisObj.seekTo(thisObj.startTime);
12133						}
12134					}
12135					else if (thisObj.defaultChapter && typeof thisObj.selectedChapters !== 'undefined') {
12136						thisObj.seekToChapter(thisObj.defaultChapter);
12137					}
12138					else {
12139						// there is no startTime, therefore no seeking required
12140						if (thisObj.autoplay || thisObj.okToPlay) {
12141							thisObj.playMedia();
12142						}
12143					}
12144				}
12145				else if (thisObj.hasPlaylist) {
12146					if ((thisObj.playlistIndex !== thisObj.$playlist.length) || thisObj.loop) {
12147						// this is not the last track in the playlist (OR playlist is looping so it doesn't matter)
12148						thisObj.playMedia();
12149					}
12150				}
12151				else {
12152					// already started playing
12153					// we're here because a new media source has been loaded and is ready to resume playback
12154					thisObj.getPlayerState().then(function(currentState) {
12155						if (thisObj.swappingSrc && currentState === 'stopped') {
12156							// Safari is the only browser that returns value of 'stopped' (observed in 12.0.1 on MacOS)
12157							// This prevents 'timeupdate' events from triggering, which prevents the new media src
12158							// from resuming playback at swapTime
12159							// This is a hack to jump start Safari
12160							thisObj.startedPlaying = false;
12161							if (thisObj.swapTime > 0) {
12162								thisObj.seekTo(thisObj.swapTime);
12163							}
12164							else {
12165								thisObj.playMedia();
12166							}
12167						}
12168					});
12169				}
12170
12171*/
12172
12173		var thisObj = this;
12174
vendor: 4,204 bytes, lines 12175-12277
12175		// Vimeo doesn't seem to support chaining of on() functions
12176		// so each event listener must be attached separately
12177		this.vimeoPlayer.on('loaded', function(vimeoId) {
12178			 // Triggered when a new video is loaded in the player
12179			thisObj.onMediaNewSourceLoad();
12180		 });
12181		this.vimeoPlayer.on('play', function(data) {
12182			// Triggered when the video plays
12183			thisObj.playing = true;
12184			thisObj.startedPlaying = true;
12185			thisObj.paused = false;
12186			thisObj.refreshControls('playpause');
12187		});
12188		this.vimeoPlayer.on('ended', function(data) {
12189			// Triggered any time the video playback reaches the end.
12190			// Note: when loop is turned on, the ended event will not fire.
12191			thisObj.playing = false;
12192			thisObj.paused = true;
12193			thisObj.onMediaComplete();
12194		});
12195		this.vimeoPlayer.on('bufferstart', function() {
12196			// Triggered when buffering starts in the player.
12197			// This is also triggered during preload and while seeking.
12198			// There is no associated data with this event.
12199		});
12200		this.vimeoPlayer.on('bufferend', function() {
12201			// Triggered when buffering ends in the player.
12202			// This is also triggered at the end of preload and seeking.
12203			// There is no associated data with this event.
12204		});
12205		this.vimeoPlayer.on('progress', function(data) {
12206			// Triggered as the video is loaded.
12207			 // Reports back the amount of the video that has been buffered (NOT the amount played)
12208			 // Data has keys duration, percent, and seconds
12209	 	});
12210		this.vimeoPlayer.on('seeking', function(data) {
12211		 	// Triggered when the player starts seeking to a specific time.
12212			 // A timeupdate event will also be fired at the same time.
12213	 	});
12214		this.vimeoPlayer.on('seeked', function(data) {
12215			// Triggered when the player seeks to a specific time.
12216			// A timeupdate event will also be fired at the same time.
12217		});
12218		this.vimeoPlayer.on('timeupdate',function(data) {
12219			// Triggered as the currentTime of the video updates.
12220			 // It generally fires every 250ms, but it may vary depending on the browser.
12221			thisObj.onMediaUpdateTime(data['duration'], data['seconds']);
12222		});
12223		this.vimeoPlayer.on('pause',function(data) {
12224			// Triggered when the video pauses
12225			if (!thisObj.clickedPlay) {
12226					// 'pause' was triggered automatically, not initiated by user
12227				// this happens in some browsers (not Chrome, as of 70.x)
12228				// when swapping source (e.g., between tracks in a playlist, or swapping description)
12229				if (thisObj.hasPlaylist || thisObj.swappingSrc) {
12230						// do NOT set playing to false.
12231					// doing so prevents continual playback after new track is loaded
12232				}
12233				else {
12234					thisObj.playing = false;
12235					thisObj.paused = true;
12236				}
12237			}
12238			else {
12239				thisObj.playing = false;
12240				thisObj.paused = true;
12241			}
12242			thisObj.clickedPlay = false; // done with this variable
12243			thisObj.onMediaPause();
12244			thisObj.refreshControls('playpause');
12245		});
12246		this.vimeoPlayer.on('playbackratechange',function(data) {
12247			// Triggered when the playback rate of the video in the player changes.
12248			// The ability to change rate can be disabled by the creator
12249			// and the event will not fire for those videos.
12250			// data contains one key: 'playbackRate'
12251			thisObj.vimeoPlaybackRate = data['playbackRate'];
12252		});
12253		this.vimeoPlayer.on('texttrackchange', function(data) {
12254			// Triggered when the active text track (captions/subtitles) changes.
12255			// The values will be null if text tracks are turned off.
12256			// data contains three keys: kind, label, language
12257		});
12258		this.vimeoPlayer.on('volumechange',function(data) {
12259			// Triggered when the volume in the player changes.
12260			// Some devices do not support setting the volume of the video
12261			// independently from the system volume,
12262			// so this event will never fire on those devices.
12263			thisObj.volume = data['volume'] * 10;
12264		});
12265		this.vimeoPlayer.on('error',function(data) {
12266			// do something with the available data
12267			// data contains three keys: message, method, name
12268			// message: A user-friendly error message
12269			// method: The Vimeo API method call that triggered the error
12270			// name: Name of the error (not necesssarily user-friendly)
12271		});
12272	};
12273
12274	AblePlayer.prototype.addEventListeners = function () {
12275
12276		var thisObj, whichButton, thisElement;
12277
12278		// Save the current object context in thisObj for use with inner functions.
12279		thisObj = this;
12280
12281		// Appropriately resize media player for full screen.
12282		$(window).resize(function () {
12283			thisObj.onWindowResize();
12284		});
12285
12286		// Refresh player if it changes from hidden to visible
12287		// There is no event triggered by a change in visibility
12288		// but MutationObserver works in most browsers (but NOT in IE 10 or earlier)
12289		// http://caniuse.com/#feat=mutationobserver
12290		if (window.MutationObserver) {
12291			var target = this.$ableDiv[0];
12292			var observer = new MutationObserver(function(mutations) {
12293				mutations.forEach(function(mutation) {
12294					if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
12295						// the player's style attribute has changed. Check to see if it's visible
12296						if (thisObj.$ableDiv.is(':visible')) {
12297							thisObj.refreshControls('init');
12298						}
12299					}
12300				});
12301			});
12302			var config = { attributes: true, childList: true, characterData: true };
12303			observer.observe(target, config);
12304		}
12305		else {
12306			// browser doesn't support MutationObserver
12307			// TODO: Figure out an alternative solution for this rare use case in older browsers
12308			// See example in buildplayer.js > useSvg()
12309		}
12310		if (typeof this.seekBar !== 'undefined') {
12311			this.addSeekbarListeners();
12312		}
12313		else {
12314			// wait a bit and try again
12315			// TODO: Should set this up to keep trying repeatedly.
12316			// Seekbar listeners are critical.
12317			setTimeout(function() {
12318				if (typeof thisObj.seekBar !== 'undefined') {
12319					thisObj.addSeekbarListeners();
12320				}
12321			},2000);
12322		}
12323
12324		// handle clicks on player buttons
12325		this.$controllerDiv.find('div[role="button"]').on('click',function(e){
12326			e.stopPropagation();
12327			thisObj.onClickPlayerButton(this);
12328		});
12329
12330		// handle clicks (left only) anywhere on the page. If any popups are open, close them.
12331		$(document).on('click',function(e) {
12332
12333			if (e.button !== 0) { // not a left click
12334				return false;
12335			}
12336			if ($('.able-popup:visible').length || $('.able-volume-popup:visible')) {
12337				// at least one popup is visible
12338				thisObj.closePopups();
12339			}
12340		});
12341
12342		// handle mouse movement over player; make controls visible again if hidden
12343		this.$ableDiv.on('mousemove',function() {
12344			if (thisObj.controlsHidden) {
12345				thisObj.fadeControls('in');
12346				thisObj.controlsHidden = false;
12347				// if there's already an active timeout, clear it and start timer again
12348				if (thisObj.hideControlsTimeoutStatus === 'active') {
12349					window.clearTimeout(thisObj.hideControlsTimeout);
12350					thisObj.hideControlsTimeoutStatus = 'clear';
12351				}
12352				if (thisObj.hideControls) {
12353					// after showing controls, hide them again after a brief timeout
12354					thisObj.invokeHideControlsTimeout();
12355				}
12356			}
12357			else {
12358				// if there's already an active timeout, clear it and start timer again
12359				if (thisObj.hideControlsTimeoutStatus === 'active') {
12360					window.clearTimeout(thisObj.hideControlsTimeout);
12361					thisObj.hideControlsTimeoutStatus = 'clear';
12362					if (thisObj.hideControls) {
12363						thisObj.invokeHideControlsTimeout();
12364					}
12365				}
12366			}
12367		});
12368
12369		// if user presses a key from anywhere on the page, show player controls
12370		$(document).keydown(function(e) {
12371
12372			if (thisObj.controlsHidden) {
12373				thisObj.fadeControls('in');
12374				thisObj.controlsHidden = false;
12375				if (thisObj.hideControlsTimeoutStatus === 'active') {
12376					window.clearTimeout(thisObj.hideControlsTimeout);
12377					thisObj.hideControlsTimeoutStatus = 'clear';
12378				}
12379				if (thisObj.hideControls) {
12380					// after showing controls, hide them again after a brief timeout
12381					thisObj.invokeHideControlsTimeout();
12382				}
12383			}
12384			else {
12385				// controls are visible
12386				// if there's already an active timeout, clear it and start timer again
12387				if (thisObj.hideControlsTimeoutStatus === 'active') {
12388					window.clearTimeout(thisObj.hideControlsTimeout);
12389					thisObj.hideControlsTimeoutStatus = 'clear';
12390
12391					if (thisObj.hideControls) {
12392						thisObj.invokeHideControlsTimeout();
12393					}
12394				}
12395			}
12396		});
12397
12398		// handle local keydown events if this isn't the only player on the page;
12399		// otherwise these are dispatched by global handler (see ableplayer-base,js)
12400		this.$ableDiv.keydown(function (e) {
12401
12402			if (AblePlayer.nextIndex > 1) {
12403				thisObj.onPlayerKeyPress(e);
12404			}
12405		});
12406
12407		// transcript is not a child of this.$ableDiv
12408		// therefore, must be added separately
12409		if (this.$transcriptArea) {
12410			this.$transcriptArea.on('keydown',function (e) {
12411				if (AblePlayer.nextIndex > 1) {
12412					thisObj.onPlayerKeyPress(e);
12413				}
12414			});
12415		}
12416
12417		// handle clicks on playlist items
12418		if (this.$playlist) {
12419			this.$playlist.click(function(e) {
12420				if (!thisObj.userClickedPlaylist) {
12421					// stopgap in case multiple clicks are fired on the same playlist item
12422					thisObj.userClickedPlaylist = true; // will be set to false after new src is loaded & canplaythrough is triggered
12423					thisObj.playlistIndex = $(this).index();
12424					thisObj.cuePlaylistItem(thisObj.playlistIndex);
12425				}
12426			});
12427		}
12428
12429		// Also play/pause when clicking on the media.
12430		this.$media.click(function () {
12431			thisObj.handlePlay();
12432		});
12433
12434		// add listeners for media events
12435		if (this.player === 'html5') {
12436			this.addHtml5MediaListeners();
12437		}
12438		else if (this.player === 'vimeo') {
12439			 this.addVimeoListeners();
12440		}
12441		else if (this.player === 'youtube') {
12442			// Youtube doesn't give us time update events, so we just periodically generate them ourselves
12443			setInterval(function () {
12444				thisObj.onMediaUpdateTime();
12445			}, 300);
12446		}
12447	};
12448})(jQuery);
12449
12450(function ($) {
12451
12452	AblePlayer.prototype.initDragDrop = function ( which ) {
12453
12454		// supported values of which: 'sign', 'transcript'
12455
12456		// NOTE: "Drag and Drop" for Able Player is a metaphor only!!!
12457		// HTML5 Drag & Drop API enables moving elements to new locations in the DOM
12458		// Thats not our purpose; we're simply changing the visible position on-screen
12459		// Therefore, the drag & drop interface was overhauled in v2.3.41 to simple
12460		// use mouse (and keyboard) events to change CSS positioning properties
12461
12462		// There are nevertheless lessons to be learned from Drag & Drop about accessibility:
12463		// http://dev.opera.com/articles/accessible-drag-and-drop/
12464
12465		var thisObj, $window, $toolbar, windowName, $resizeHandle, resizeZIndex;
12466
12467		thisObj = this;
12468
12469		if (which === 'transcript') {
12470			$window = this.$transcriptArea;
12471			windowName = 'transcript-window';
12472			$toolbar = this.$transcriptToolbar;
12473		}
12474		else if (which === 'sign') {
12475			$window = this.$signWindow;
12476			windowName = 'sign-window';
12477			$toolbar = this.$signToolbar;
12478		}
12479
12480		// add class to trigger change in cursor on hover
12481		$toolbar.addClass('able-draggable');
12482
12483		// add resize handle selector to bottom right corner
12484		$resizeHandle = $('<div>',{
12485			'class': 'able-resizable'
12486		});
12487		// assign z-index that's slightly higher than parent window
12488		resizeZIndex = parseInt($window.css('z-index')) + 100;
12489		$resizeHandle.css('z-index',resizeZIndex);
12490		$window.append($resizeHandle);
12491
12492		// add event listener to toolbar to start and end drag
12493		// other event listeners will be added when drag starts
12494		$toolbar.on('mousedown mouseup touchstart touchend', function(e) {
12495			e.stopPropagation();
12496			if (e.type === 'mousedown' || e.type === 'touchstart') {
12497  			if (!thisObj.windowMenuClickRegistered) {
12498	  			thisObj.windowMenuClickRegistered = true;
12499          thisObj.startMouseX = e.pageX;
12500          thisObj.startMouseY = e.pageY;
12501          thisObj.dragDevice = 'mouse'; // ok to use this even if device is a touchpad
12502          thisObj.startDrag(which, $window);
12503			  }
12504      }
12505      else if (e.type === 'mouseup' || e.type === 'touchend') {
12506        if (thisObj.dragging && thisObj.dragDevice === 'mouse') {
12507				  thisObj.endDrag(which);
12508			  }
12509      }
12510      return false;
12511		});
12512
12513		// add event listeners for resizing
12514		$resizeHandle.on('mousedown mouseup touchstart touchend', function(e) {
12515			e.stopPropagation();
12516			if (e.type === 'mousedown' || e.type === 'touchstart') {
12517  			if (!thisObj.windowMenuClickRegistered) {
12518	  			thisObj.windowMenuClickRegistered = true;
12519          thisObj.startMouseX = e.pageX;
12520          thisObj.startMouseY = e.pageY;
12521          thisObj.startResize(which, $window);
12522			  }
12523      }
12524      else if (e.type === 'mouseup' || e.type === 'touchend') {
12525  			if (thisObj.resizing) {
12526	  			thisObj.endResize(which);
12527        }
12528      }
12529      return false;
12530		});
12531
12532		// whenever a window is clicked, bring it to the foreground
12533		$window.on('click', function() {
12534
12535			if (!thisObj.windowMenuClickRegistered && !thisObj.finishingDrag) {
12536				thisObj.updateZIndex(which);
12537			}
12538			thisObj.finishingDrag = false;
12539		});
12540
12541		this.addWindowMenu(which,$window,windowName);
12542	};
12543
12544	AblePlayer.prototype.addWindowMenu = function(which, $window, windowName) {
12545
12546		var thisObj, $windowAlert, menuId, $newButton, $buttonIcon, buttonImgSrc, $buttonImg,
12547			$buttonLabel, tooltipId, $tooltip, $popup,
12548			label, position, buttonHeight, buttonWidth, tooltipY, tooltipX, tooltipStyle, tooltip,
12549			$optionList, menuBaseId, options, i, $optionItem, option, menuId;
12550
12551		thisObj = this;
12552
12553		// Add a Boolean that will be set to true temporarily if window button or a menu item is clicked
12554		// This will prevent the click event from also triggering a mousedown event on the toolbar
12555		// (which would unexpectedly send the window into drag mode)
12556		this.windowMenuClickRegistered = false;
12557
12558		// Add another Boolean that will be set to true temporarily when mouseup fires at the end of a drag
12559		// this will prevent the click event from being triggered
12560		this.finishingDrag = false;
12561
12562		// create an alert div and add it to window
12563		$windowAlert = $('<div role="alert"></div>');
12564		$windowAlert.addClass('able-alert');
12565		$windowAlert.hide();
12566		$windowAlert.appendTo(this.$activeWindow);
12567		$windowAlert.css({
12568			top: $window.offset().top
12569		});
12570
12571		// add button to draggable window which triggers a popup menu
12572		// for now, re-use preferences icon for this purpose
12573		menuId = this.mediaId + '-' + windowName + '-menu';
12574		$newButton = $('<button>',{
12575			'type': 'button',
12576			'tabindex': '0',
12577			'aria-label': this.tt.windowButtonLabel,
12578			'aria-haspopup': 'true',
12579			'aria-controls': menuId,
12580			'class': 'able-button-handler-preferences'
12581		});
12582		if (this.iconType === 'font') {
12583			$buttonIcon = $('<span>',{
12584				'class': 'icon-preferences',
12585				'aria-hidden': 'true'
12586			});
12587			$newButton.append($buttonIcon);
12588		}
12589		else {
12590			// use image
12591			buttonImgSrc = this.rootPath + 'button-icons/' + this.toolbarIconColor + '/preferences.png';
12592			$buttonImg = $('<img>',{
12593				'src': buttonImgSrc,
12594				'alt': '',
12595				'role': 'presentation'
12596			});
12597			$newButton.append($buttonImg);
12598		}
12599
12600		// add the visibly-hidden label for screen readers that don't support aria-label on the button
12601		$buttonLabel = $('<span>',{
12602			'class': 'able-clipped'
12603		}).text(this.tt.windowButtonLabel);
12604		$newButton.append($buttonLabel);
12605
12606		// add a tooltip that displays aria-label on mouseenter or focus
12607		tooltipId = this.mediaId + '-' + windowName + '-tooltip';
12608		$tooltip = $('<div>',{
12609			'class' : 'able-tooltip',
12610			'id' : tooltipId
12611		}).hide();
12612		$newButton.on('mouseenter focus',function(e) {
12613			var label = $(this).attr('aria-label');
12614			// get position of this button
12615			var position = $(this).position();
12616			var buttonHeight = $(this).height();
12617			var buttonWidth = $(this).width();
12618			var tooltipY = position.top - buttonHeight - 5;
12619			var tooltipX = 0;
12620			var tooltipStyle = {
12621				left: '',
12622				right: tooltipX + 'px',
12623				top: tooltipY + 'px'
12624			};
12625			var tooltip = AblePlayer.localGetElementById($newButton[0], tooltipId).text(label).css(tooltipStyle);
12626			thisObj.showTooltip(tooltip);
12627			$(this).on('mouseleave blur',function() {
12628				AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
12629			});
12630		});
12631
12632		// setup popup menu
12633		$popup = this.setupPopups(windowName); // 'transcript-window' or 'sign-window'
12634		// define vars and assemble all the parts
12635		if (which === 'transcript') {
12636			this.$transcriptAlert = $windowAlert;
12637			this.$transcriptPopupButton = $newButton;
12638			this.$transcriptPopup = $popup;
12639			this.$transcriptToolbar.prepend($windowAlert,$newButton,$tooltip,$popup);
12640		}
12641		else if (which === 'sign') {
12642			this.$signAlert = $windowAlert;
12643			this.$signPopupButton = $newButton;
12644			this.$signPopup = $popup;
12645			this.$signToolbar.append($windowAlert,$newButton,$tooltip,$popup);
12646		}
12647
12648		// handle button click
12649		$newButton.on('click mousedown keydown',function(e) {
12650
12651      if (thisObj.focusNotClick) {
12652        return false;
12653      }
12654      if (thisObj.dragging) {
12655				thisObj.dragKeys(which, e);
12656				return false;
12657		  }
12658      e.stopPropagation();
12659			if (!thisObj.windowMenuClickRegistered && !thisObj.finishingDrag) {
12660				// don't set windowMenuClickRegistered yet; that happens in handler function
12661				thisObj.handleWindowButtonClick(which, e);
12662			}
12663			thisObj.finishingDrag = false;
12664		});
12665
12666		this.addResizeDialog(which, $window);
12667	};
12668
12669	AblePlayer.prototype.addResizeDialog = function (which, $window) {
12670
12671		var thisObj, $windowPopup, $windowButton,
12672			widthId, heightId, startingWidth, startingHeight, aspectRatio,
12673			$resizeForm, $resizeWrapper,
12674			$resizeWidthDiv, $resizeWidthInput, $resizeWidthLabel,
12675			$resizeHeightDiv, $resizeHeightInput, $resizeHeightLabel,
12676			tempWidth, tempHeight,
12677			$saveButton, $cancelButton, newWidth, newHeight, resizeDialog;
12678
12679		thisObj = this;
12680
12681		if (which === 'transcript') {
12682			$windowPopup = this.$transcriptPopup;
12683			$windowButton = this.$transcriptPopupButton;
12684		}
12685		else if (which === 'sign') {
12686			$windowPopup = this.$signPopup;
12687			$windowButton = this.$signPopupButton;
12688		}
12689
12690		widthId = this.mediaId + '-resize-' + which + '-width';
12691		heightId = this.mediaId + '-resize-' + which + '-height';
12692		startingWidth = $window.width();
12693		startingHeight = $window.height();
12694		aspectRatio = startingWidth / startingHeight;
12695
12696		$resizeForm = $('<div></div>',{
12697			'class' : 'able-resize-form'
12698		});
12699
12700		// inner container for all content, will be assigned to modal div's aria-describedby
12701		$resizeWrapper = $('<div></div>');
12702
12703		// width field
12704		$resizeWidthDiv = $('<div></div>');
12705		$resizeWidthInput = $('<input>',{
12706			'type': 'text',
12707			'id': widthId,
12708			'value': startingWidth
12709		});
12710		$resizeWidthLabel = $('<label>',{
12711			'for': widthId
12712		}).text(this.tt.width);
12713
12714		// height field
12715		$resizeHeightDiv = $('<div></div>');
12716		$resizeHeightInput = $('<input>',{
12717			'type': 'text',
12718			'id': heightId,
12719			'value': startingHeight
12720		});
12721		$resizeHeightLabel = $('<label>',{
12722			'for': heightId
12723		}).text(this.tt.height);
12724
12725		if (which === 'sign') {
12726			// make height a read-only field
12727			// and calculate its value based on width to preserve aspect ratio
12728			$resizeHeightInput.prop('readonly',true);
12729			$resizeWidthInput.on('input',function() {
12730				tempWidth = $(this).val();
12731				tempHeight = Math.round(tempWidth/aspectRatio, 0);
12732				$resizeHeightInput.val(tempHeight);
12733			})
12734		}
12735
12736		// Add save and cancel buttons.
12737		$saveButton = $('<button class="modal-button">' + this.tt.save + '</button>');
12738		$cancelButton = $('<button class="modal-button">' + this.tt.cancel + '</button>');
12739		$saveButton.on('click',function () {
12740			newWidth = $('#' + widthId).val();
12741			newHeight = $('#' + heightId).val();
12742			if (newWidth !== startingWidth || newHeight !== startingHeight) {
12743				thisObj.resizeObject(which,newWidth,newHeight);
12744				thisObj.updateCookie(which);
12745			}
12746			resizeDialog.hide();
12747			$windowPopup.hide();
12748			$windowButton.focus();
12749		});
12750		$cancelButton.on('click',function () {
12751			resizeDialog.hide();
12752			$windowPopup.hide();
12753			$windowButton.focus();
12754		});
12755
12756		// Now assemble all the parts
12757		$resizeWidthDiv.append($resizeWidthLabel,$resizeWidthInput);
12758		$resizeHeightDiv.append($resizeHeightLabel,$resizeHeightInput);
12759		$resizeWrapper.append($resizeWidthDiv,$resizeHeightDiv);
12760		$resizeForm.append($resizeWrapper,'<hr>',$saveButton,$cancelButton);
12761
12762		// must be appended to the BODY!
12763		// otherwise when aria-hidden="true" is applied to all background content
12764		// that will include an ancestor of the dialog,
12765		// which will render the dialog unreadable by screen readers
12766		$('body').append($resizeForm);
12767		resizeDialog = new AccessibleDialog($resizeForm, $windowButton, 'alert', this.tt.windowResizeHeading, $resizeWrapper, this.tt.closeButtonLabel, '20em');
12768		if (which === 'transcript') {
12769			this.transcriptResizeDialog = resizeDialog;
12770		}
12771		else if (which === 'sign') {
12772			this.signResizeDialog = resizeDialog;
12773		}
12774	};
12775
12776	AblePlayer.prototype.handleWindowButtonClick = function (which, e) {
12777
12778		var thisObj, $windowPopup, $windowButton, $toolbar, popupTop;
12779
12780		thisObj = this;
12781
12782		if (this.focusNotClick) {
12783  		// transcript or sign window has just opened,
12784  		// and focus moved to the window button
12785  		// ignore the keystroke that triggered the popup
12786  		return false;
12787		}
12788
12789		if (which === 'transcript') {
12790			$windowPopup = this.$transcriptPopup;
12791			$windowButton = this.$transcriptPopupButton;
12792			$toolbar = this.$transcriptToolbar;
12793		}
12794		else if (which === 'sign') {
12795			$windowPopup = this.$signPopup;
12796			$windowButton = this.$signPopupButton;
12797			$toolbar = this.$signToolbar;
12798		}
12799
12800		if (e.type === 'keydown') {
12801			// user pressed a key
12802			if (e.which === 32 || e.which === 13) {
12803				// this was Enter or space
12804				this.windowMenuClickRegistered = true;
12805			}
12806			else if (e.which === 27) { // escape
12807        if ($windowPopup.is(':visible')) {
12808  				// close the popup menu
12809          $windowPopup.hide('fast', function() {
12810					  // also reset the Boolean
12811            thisObj.windowMenuClickRegistered = false;
12812            // also restore menu items to their original state
12813            $windowPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
12814            // also return focus to window options button
12815            $windowButton.focus();
12816				  });
12817				}
12818				else {
12819  				// popup isn't open. Close the window
12820          if (which === 'sign') {
12821            this.handleSignToggle();
12822          }
12823          else if (which === 'transcript') {
12824            this.handleTranscriptToggle();
12825          }
12826				}
12827			}
12828			else {
12829				return false;
12830			}
12831		}
12832		else {
12833			// this was a mouse event
12834			this.windowMenuClickRegistered = true;
12835		}
12836
12837		if ($windowPopup.is(':visible')) {
12838			$windowPopup.hide(200,'',function() {
12839				thisObj.windowMenuClickRegistered = false; // reset
12840			});
12841			$windowPopup.find('li').removeClass('able-focus');
12842			$windowButton.attr('aria-expanded','false').focus();
12843		}
12844		else {
12845			// first, be sure window is on top
12846			this.updateZIndex(which);
12847			popupTop = $windowButton.position().top + $windowButton.outerHeight();
12848			$windowPopup.css('top', popupTop);
12849			$windowPopup.show(200,'',function() {
12850				$windowButton.attr('aria-expanded','true');
12851				$(this).find('li').first().focus().addClass('able-focus');
12852				thisObj.windowMenuClickRegistered = false; // reset
12853			});
12854		}
12855	};
12856
12857	AblePlayer.prototype.handleMenuChoice = function (which, choice, e) {
12858
12859		var thisObj, $window, $windowPopup, $windowButton, resizeDialog, width, height, $thisRadio;
12860
12861		thisObj = this;
12862		if (which === 'transcript') {
12863			$window = this.$transcriptArea;
12864			$windowPopup = this.$transcriptPopup;
12865			$windowButton = this.$transcriptPopupButton;
12866			resizeDialog = this.transcriptResizeDialog;
12867		}
12868		else if (which === 'sign') {
12869			$window = this.$signWindow;
12870			$windowPopup = this.$signPopup;
12871			$windowButton = this.$signPopupButton;
12872			resizeDialog = this.signResizeDialog;
12873		}
12874		this.$activeWindow = $window;
12875
12876		if (e.type === 'keydown') {
12877			if (e.which === 27) { // escape
12878				// hide the popup menu
12879				$windowPopup.hide('fast', function() {
12880					// also reset the Boolean
12881					thisObj.windowMenuClickRegistered = false;
12882					// also restore menu items to their original state
12883					$windowPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
12884					// also return focus to window options button
12885					$windowButton.focus();
12886				});
12887				return false;
12888			}
12889			else {
12890				// all other keys will be handled by upstream functions
12891        if (choice !== 'close') {
12892          this.$activeWindow = $window;
12893		    }
12894				return false;
12895			}
12896		}
12897
12898		// hide the popup menu
12899		$windowPopup.hide('fast', function() {
12900			// also reset the boolean
12901			thisObj.windowMenuClickRegistered = false;
12902			// also restore menu items to their original state
12903			$windowPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
12904		});
12905		if (choice !== 'close') {
12906			$windowButton.focus();
12907		}
12908		if (choice === 'move') {
12909
12910      // temporarily add role="application" to activeWindow
12911      // otherwise, screen readers incercept arrow keys and moving window will not work
12912      this.$activeWindow.attr('role','application');
12913
12914			if (!this.showedAlert(which)) {
12915				this.showAlert(this.tt.windowMoveAlert,which);
12916				if (which === 'transcript') {
12917					this.showedTranscriptAlert = true;
12918				}
12919				else if (which === 'sign') {
12920					this.showedSignAlert = true;
12921				}
12922			}
12923			if (e.type === 'keydown') {
12924				this.dragDevice = 'keyboard';
12925			}
12926			else {
12927				this.dragDevice = 'mouse';
12928			}
12929			this.startDrag(which, $window);
12930			$windowPopup.hide().parent().focus();
12931		}
12932		else if (choice == 'resize') {
12933			// resize through the menu uses a form, not drag
12934      var resizeFields = resizeDialog.getInputs();
12935      if (resizeFields) {
12936        // reset width and height values in form
12937        resizeFields[0].value = $window.width();
12938        resizeFields[1].value = $window.height();
12939      }
12940			resizeDialog.show();
12941		}
12942		else if (choice == 'close') {
12943			// close window, place focus on corresponding button on controller bar
12944			if (which === 'transcript') {
12945        this.closingTranscript = true; // stopgrap to prevent double-firing of keypress
12946				this.handleTranscriptToggle();
12947			}
12948			else if (which === 'sign') {
12949        this.closingSign = true; // stopgrap to prevent double-firing of keypress
12950				this.handleSignToggle();
12951			}
12952		}
12953	};
12954
12955	AblePlayer.prototype.startDrag = function(which, $element) {
12956
12957		var thisObj, $windowPopup, zIndex, startPos, newX, newY;
12958
12959		thisObj = this;
12960
12961    if (!this.$activeWindow) {
12962  		this.$activeWindow = $element;
12963    }
12964    this.dragging = true;
12965
12966		if (which === 'transcript') {
12967			$windowPopup = this.$transcriptPopup;
12968		}
12969		else if (which === 'sign') {
12970			$windowPopup = this.$signPopup;
12971		}
12972
12973		if (!this.showedAlert(which)) {
12974			this.showAlert(this.tt.windowMoveAlert,which);
12975			if (which === 'transcript') {
12976				this.showedTranscriptAlert = true;
12977			}
12978			else if (which === 'sign') {
12979				this.showedSignAlert = true;
12980			}
12981		}
12982
12983		// if window's popup menu is open, close it
12984		if ($windowPopup.is(':visible')) {
12985			$windowPopup.hide();
12986		}
12987
12988		// be sure this window is on top
12989		this.updateZIndex(which);
12990
12991		// get starting position of element
12992		startPos = this.$activeWindow.position();
12993		this.dragStartX = startPos.left;
12994		this.dragStartY = startPos.top;
12995
12996		if (typeof this.startMouseX === 'undefined') {
12997			this.dragDevice = 'keyboard';
12998			this.dragKeyX = this.dragStartX;
12999			this.dragKeyY = this.dragStartY;
13000			// add stopgap to prevent the Enter that triggered startDrag() from also triggering dragEnd()
13001			this.startingDrag = true;
13002		}
13003		else {
13004			this.dragDevice = 'mouse';
13005			// get offset between mouse position and top left corner of draggable element
13006			this.dragOffsetX = this.startMouseX - this.dragStartX;
13007			this.dragOffsetY = this.startMouseY - this.dragStartY;
13008		}
13009
13010		// prepare element for dragging
13011		this.$activeWindow.addClass('able-drag').css({
13012			'position': 'absolute',
13013			'top': this.dragStartY + 'px',
13014			'left': this.dragStartX + 'px'
13015		}).focus();
13016
13017		// add device-specific event listeners
13018		if (this.dragDevice === 'mouse') { // might also be a touchpad
13019			$(document).on('mousemove touchmove',function(e) {
13020				if (thisObj.dragging) {
13021					// calculate new top left based on current mouse position - offset
13022					newX = e.pageX - thisObj.dragOffsetX;
13023					newY = e.pageY - thisObj.dragOffsetY;
13024					thisObj.resetDraggedObject( newX, newY );
13025				}
13026			});
13027		}
13028		else if (this.dragDevice === 'keyboard') {
13029			this.$activeWindow.on('keydown',function(e) {
13030				if (thisObj.dragging) {
13031					thisObj.dragKeys(which, e);
13032				}
13033			});
13034		}
13035		return false;
13036	};
13037
13038	AblePlayer.prototype.dragKeys = function(which, e) {
13039
13040		var key, keySpeed;
13041
13042		var thisObj = this;
13043
13044		// stopgap to prevent firing on initial Enter or space
13045		// that selected "Move" from menu
13046		if (this.startingDrag) {
13047			this.startingDrag = false;
13048			return false;
13049		}
13050		key = e.which;
13051		keySpeed = 10; // pixels per keypress event
13052
13053		switch (key) {
13054			case 37:	// left
13055			case 63234:
13056				 this.dragKeyX -= keySpeed;
13057				break;
13058			case 38:	// up
13059			case 63232:
13060				this.dragKeyY -= keySpeed;
13061				break;
13062			case 39:	// right
13063			case 63235:
13064				this.dragKeyX += keySpeed;
13065				break;
13066			case 40:	// down
13067			case 63233:
13068				this.dragKeyY += keySpeed;
13069				break;
13070			case 13: 	// enter
13071			case 27: 	// escape
13072				this.endDrag(which);
13073				return false;
13074			default:
13075				return false;
13076		}
13077		this.resetDraggedObject(this.dragKeyX,this.dragKeyY);
13078		if (e.preventDefault) {
13079			e.preventDefault();
13080		}
13081		return false;
13082	};
13083
13084	AblePlayer.prototype.resetDraggedObject = function ( x, y) {
13085
13086		this.$activeWindow.css({
13087			'left': x + 'px',
13088			'top': y + 'px'
13089		});
13090	},
13091
13092	AblePlayer.prototype.resizeObject = function ( which, width, height ) {
13093
13094		var innerHeight;
13095
13096		// which is either 'transcript' or 'sign'
13097		this.$activeWindow.css({
13098			'width': width + 'px',
13099			'height': height + 'px'
13100		});
13101
13102		if (which === 'transcript') {
13103			// $activeWindow is the outer $transcriptArea
13104			// but the inner able-transcript also needs to be resized proporitionally
13105			// (it's 50px less than its outer container)
13106			innerHeight = height - 50;
13107			this.$transcriptDiv.css('height', innerHeight + 'px');
13108		}
13109	};
13110
13111	AblePlayer.prototype.endDrag = function(which) {
13112
13113		var thisObj, $window, $windowPopup, $windowButton;
13114    thisObj = this;
13115
13116		if (which === 'transcript') {
13117			$windowPopup = this.$transcriptPopup;
13118			$windowButton = this.$transcriptPopupButton;
13119		}
13120		else if (which === 'sign') {
13121			$windowPopup = this.$signPopup;
13122			$windowButton = this.$signPopupButton;
13123		}
13124
13125		$(document).off('mousemove mouseup touchmove touchup');
13126		this.$activeWindow.off('keydown').removeClass('able-drag');
13127		// restore activeWindow role from 'application' to 'dialog'
13128		this.$activeWindow.attr('role','dialog');
13129		this.$activeWindow = null;
13130
13131		if (this.dragDevice === 'keyboard') {
13132			$windowButton.focus();
13133		}
13134		this.dragging = false;
13135
13136		// save final position of dragged element
13137		this.updateCookie(which);
13138
13139		// reset starting mouse positions
13140		this.startMouseX = undefined;
13141		this.startMouseY = undefined;
13142
13143		// Boolean to stop stray events from firing
13144		this.windowMenuClickRegistered = false;
13145		this.finishingDrag = true; // will be reset after window click event
13146		// finishingDrag should e reset after window click event,
13147		// which is triggered automatically after mouseup
13148		// However, in case that's not reliable in some browsers
13149		// need to ensure this gets cancelled
13150		setTimeout(function() {
13151			thisObj.finishingDrag = false;
13152		}, 100);
13153	};
13154
13155	AblePlayer.prototype.isCloseToCorner = function($window, mouseX, mouseY) {
13156
13157		// return true if mouse is close to bottom right corner (resize target)
13158		var tolerance, position, top, left, width, height, bottom, right;
13159
13160		tolerance = 10; // number of pixels in both directions considered "close enough"
13161
13162		// first, get position of element
13163		position = $window.offset();
13164		top = position.top;
13165		left = position.left;
13166		width = $window.width();
13167		height = $window.height();
13168		bottom = top + height;
13169		right = left + width;
13170		if ((Math.abs(bottom-mouseY) <= tolerance) && (Math.abs(right-mouseX) <= tolerance)) {
13171			return true;
13172		}
13173		return false;
13174	};
13175
13176	AblePlayer.prototype.startResize = function(which, $element) {
13177
13178		var thisObj, $windowPopup, zIndex, startPos, newWidth, newHeight;
13179
13180		thisObj = this;
13181		this.$activeWindow = $element;
13182		this.resizing = true;
13183
13184		if (which === 'transcript') {
13185			$windowPopup = this.$transcriptPopup;
13186		}
13187		else if (which === 'sign') {
13188			$windowPopup = this.$signPopup;
13189		}
13190
13191		// if window's popup menu is open, close it & place focus on button (???)
13192		if ($windowPopup.is(':visible')) {
13193			$windowPopup.hide().parent().focus();
13194		}
13195
13196		// get starting width and height
13197		startPos = this.$activeWindow.position();
13198		this.dragKeyX = this.dragStartX;
13199		this.dragKeyY = this.dragStartY;
13200		this.dragStartWidth = this.$activeWindow.width();
13201		this.dragStartHeight = this.$activeWindow.height();
13202
13203		// add event listeners
13204		$(document).on('mousemove touchmove',function(e) {
13205			if (thisObj.resizing) {
13206				// calculate new width and height based on changes to mouse position
13207				newWidth = thisObj.dragStartWidth + (e.pageX - thisObj.startMouseX);
13208				newHeight = thisObj.dragStartHeight + (e.pageY - thisObj.startMouseY);
13209				thisObj.resizeObject( which, newWidth, newHeight );
13210			}
13211		});
13212		return false;
13213	};
13214
13215	AblePlayer.prototype.endResize = function(which) {
13216
13217		var $window, $windowPopup, $windowButton;
13218
13219		if (which === 'transcript') {
13220			$windowPopup = this.$transcriptPopup;
13221			$windowButton = this.$transcriptPopupButton;
13222		}
13223		else if (which === 'sign') {
13224			$windowPopup = this.$signPopup;
13225			$windowButton = this.$signPopupButton;
13226		}
13227
13228		$(document).off('mousemove mouseup touchmove touchup');
13229		this.$activeWindow.off('keydown');
13230		$windowButton.show().focus();
13231		this.resizing = false;
13232		this.$activeWindow.removeClass('able-resize');
13233
13234		// save final width and height of dragged element
13235		this.updateCookie(which);
13236
13237		// Booleans for preventing stray events
13238		this.windowMenuClickRegistered = false;
13239		this.finishingDrag = true;
13240
13241		// finishingDrag should e reset after window click event,
13242		// which is triggered automatically after mouseup
13243		// However, in case that's not reliable in some browsers
13244		// need to ensure this gets cancelled
13245		setTimeout(function() {
13246			this.finishingDrag = false;
13247		}, 100);
13248	};
13249
13250})(jQuery);
13251
13252(function ($) {
13253	AblePlayer.prototype.initSignLanguage = function() {
13254
13255		// Sign language is only currently supported in HTML5 player, not YouTube or Vimeo
13256		if (this.player === 'html5') {
13257			// check to see if there's a sign language video accompanying this video
13258			// check only the first source
13259			// If sign language is provided, it must be provided for all sources
13260			this.signFile = this.$sources.first().attr('data-sign-src');
13261			if (this.signFile) {
13262  		  if (this.isIOS()) {
13263    		  // IOS does not allow multiple videos to play simultaneously
13264    		  // Therefore, sign language as rendered by Able Player unfortunately won't work
13265          this.hasSignLanguage = false;
13266          if (this.debug) {
13267            console.log('Sign language has been disabled due to IOS restrictions');
13268          }
13269        }
13270        else {
13271  				if (this.debug) {
13272	  				console.log('This video has an accompanying sign language video: ' + this.signFile);
13273          }
13274          this.hasSignLanguage = true;
13275          this.injectSignPlayerCode();
13276        }
13277			}
13278			else {
13279				this.hasSignLanguage = false;
13280			}
13281		}
13282		else {
13283			this.hasSignLanguage = false;
13284		}
13285	};
13286
13287	AblePlayer.prototype.injectSignPlayerCode = function() {
13288
13289		// create and inject surrounding HTML structure
13290
13291		var thisObj, signVideoId, signVideoWidth, i, signSrc, srcType, $signSource;
13292
13293		thisObj = this;
13294
13295		signVideoWidth = this.getDefaultWidth('sign');
13296
13297		signVideoId = this.mediaId + '-sign';
13298		this.$signVideo = $('<video>',{
13299			'id' : signVideoId,
13300			'tabindex' : '-1'
13301		});
13302		this.signVideo = this.$signVideo[0];
13303		// for each original <source>, add a <source> to the sign <video>
13304		for (i=0; i < this.$sources.length; i++) {
13305			signSrc = this.$sources[i].getAttribute('data-sign-src');
13306			srcType = this.$sources[i].getAttribute('type');
13307			if (signSrc) {
13308				$signSource = $('<source>',{
13309					'src' : signSrc,
13310					'type' : srcType
13311				});
13312				this.$signVideo.append($signSource);
13313			}
13314			else {
13315				// source is missing a sign language version
13316				// can't include sign language
13317				this.hasSignLanguage = false;
13318				break;
13319			}
13320		}
13321
13322		this.$signWindow = $('<div>',{
13323			'class' : 'able-sign-window',
13324  		'role': 'dialog',
13325      'aria-label': this.tt.sign
13326		});
13327		this.$signToolbar = $('<div>',{
13328			'class': 'able-window-toolbar able-' + this.toolbarIconColor + '-controls'
13329		});
13330
13331		this.$signWindow.append(this.$signToolbar, this.$signVideo);
13332
13333		this.$ableWrapper.append(this.$signWindow);
13334
13335		// make it draggable
13336		this.initDragDrop('sign');
13337
13338		if (this.prefSign === 1) {
13339			// sign window is on. Go ahead and position it and show it
13340			this.positionDraggableWindow('sign',this.getDefaultWidth('sign'));
13341		}
13342		else {
13343			this.$signWindow.hide();
13344		}
13345	};
13346
13347})(jQuery);
13348
13349(function ($
vendor: 18,947 bytes, lines 13349-14325
13349) {
13350	// Look up ISO 639-1 language codes to be used as subtitle labels
13351	// In some instances "name" has been trunctation for readability
13352	// Sources:
13353	// http://stackoverflow.com/questions/3217492/list-of-language-codes-in-yaml-or-json/4900304#4900304
13354	// https://www.venea.net/web/culture_code
13355
13356	var isoLangs = {
13357		"ab":{
13358				"name":"Abkhaz",
13359				"nativeName":"аҧсуа"
13360		},
13361		"aa":{
13362				"name":"Afar",
13363				"nativeName":"Afaraf"
13364		},
13365		"af":{
13366				"name":"Afrikaans",
13367				"nativeName":"Afrikaans"
13368		},
13369		"ak":{
13370				"name":"Akan",
13371				"nativeName":"Akan"
13372		},
13373		"sq":{
13374				"name":"Albanian",
13375				"nativeName":"Shqip"
13376		},
13377		"am":{
13378				"name":"Amharic",
13379				"nativeName":"አማርኛ"
13380		},
13381		"ar":{
13382				"name":"Arabic",
13383				"nativeName":"العربية"
13384		},
13385		"an":{
13386				"name":"Aragonese",
13387				"nativeName":"Aragonés"
13388		},
13389		"hy":{
13390				"name":"Armenian",
13391				"nativeName":"Հայերեն"
13392		},
13393		"as":{
13394				"name":"Assamese",
13395				"nativeName":"অসমীয়া"
13396		},
13397		"av":{
13398				"name":"Avaric",
13399				"nativeName":"авар мацӀ, магӀарул мацӀ"
13400		},
13401		"ae":{
13402				"name":"Avestan",
13403				"nativeName":"avesta"
13404		},
13405		"ay":{
13406				"name":"Aymara",
13407				"nativeName":"aymar aru"
13408		},
13409		"az":{
13410				"name":"Azerbaijani",
13411				"nativeName":"azərbaycan dili"
13412		},
13413		"bm":{
13414				"name":"Bambara",
13415				"nativeName":"bamanankan"
13416		},
13417		"ba":{
13418				"name":"Bashkir",
13419				"nativeName":"башҡорт теле"
13420		},
13421		"eu":{
13422				"name":"Basque",
13423				"nativeName":"euskara, euskera"
13424		},
13425		"be":{
13426				"name":"Belarusian",
13427				"nativeName":"Беларуская"
13428		},
13429		"bn":{
13430				"name":"Bengali",
13431				"nativeName":"বাংলা"
13432		},
13433		"bh":{
13434				"name":"Bihari",
13435				"nativeName":"भोजपुरी"
13436		},
13437		"bi":{
13438				"name":"Bislama",
13439				"nativeName":"Bislama"
13440		},
13441		"bs":{
13442				"name":"Bosnian",
13443				"nativeName":"bosanski jezik"
13444		},
13445		"br":{
13446				"name":"Breton",
13447				"nativeName":"brezhoneg"
13448		},
13449		"bg":{
13450				"name":"Bulgarian",
13451				"nativeName":"български език"
13452		},
13453		"my":{
13454				"name":"Burmese",
13455				"nativeName":"ဗမာစာ"
13456		},
13457		"ca":{
13458				"name":"Catalan",
13459				"nativeName":"Català"
13460		},
13461		"ch":{
13462				"name":"Chamorro",
13463				"nativeName":"Chamoru"
13464		},
13465		"ce":{
13466				"name":"Chechen",
13467				"nativeName":"нохчийн мотт"
13468		},
13469		"ny":{
13470				"name":"Chichewa",
13471				"nativeName":"chiCheŵa, chinyanja"
13472		},
13473		"zh":{
13474				"name":"Chinese",
13475				"nativeName":"中文 (Zhōngwén), 汉语, 漢語"
13476		},
13477		"cv":{
13478				"name":"Chuvash",
13479				"nativeName":"чӑваш чӗлхи"
13480		},
13481		"kw":{
13482				"name":"Cornish",
13483				"nativeName":"Kernewek"
13484		},
13485		"co":{
13486				"name":"Corsican",
13487				"nativeName":"corsu, lingua corsa"
13488		},
13489		"cr":{
13490				"name":"Cree",
13491				"nativeName":"ᓀᐦᐃᔭᐍᐏᐣ"
13492		},
13493		"hr":{
13494				"name":"Croatian",
13495				"nativeName":"hrvatski"
13496		},
13497		"cs":{
13498				"name":"Czech",
13499				"nativeName":"česky, čeština"
13500		},
13501		"da":{
13502				"name":"Danish",
13503				"nativeName":"dansk"
13504		},
13505		"dv":{
13506				"name":"Divehi",
13507				"nativeName":"ދިވެހި"
13508		},
13509		"nl":{
13510				"name":"Dutch",
13511				"nativeName":"Nederlands, Vlaams"
13512		},
13513		"en":{
13514				"name":"English",
13515				"nativeName":"English"
13516		},
13517		"eo":{
13518				"name":"Esperanto",
13519				"nativeName":"Esperanto"
13520		},
13521		"et":{
13522				"name":"Estonian",
13523				"nativeName":"eesti, eesti keel"
13524		},
13525		"ee":{
13526				"name":"Ewe",
13527				"nativeName":"Eʋegbe"
13528		},
13529		"fo":{
13530				"name":"Faroese",
13531				"nativeName":"føroyskt"
13532		},
13533		"fj":{
13534				"name":"Fijian",
13535				"nativeName":"vosa Vakaviti"
13536		},
13537		"fi":{
13538				"name":"Finnish",
13539				"nativeName":"suomi, suomen kieli"
13540		},
13541		"fr":{
13542				"name":"French",
13543				"nativeName":"français, langue française"
13544		},
13545		"ff":{
13546				"name":"Fula",
13547				"nativeName":"Fulfulde, Pulaar, Pular"
13548		},
13549		"gl":{
13550				"name":"Galician",
13551				"nativeName":"Galego"
13552		},
13553		"ka":{
13554				"name":"Georgian",
13555				"nativeName":"ქართული"
13556		},
13557		"de":{
13558				"name":"German",
13559				"nativeName":"Deutsch"
13560		},
13561		"el":{
13562				"name":"Greek",
13563				"nativeName":"Ελληνικά"
13564		},
13565		"gn":{
13566				"name":"Guaraní",
13567				"nativeName":"Avañeẽ"
13568		},
13569		"gu":{
13570				"name":"Gujarati",
13571				"nativeName":"ગુજરાતી"
13572		},
13573		"ht":{
13574				"name":"Haitian",
13575				"nativeName":"Kreyòl ayisyen"
13576		},
13577		"ha":{
13578				"name":"Hausa",
13579				"nativeName":"Hausa, هَوُسَ"
13580		},
13581		"he":{
13582				"name":"Hebrew",
13583				"nativeName":"עברית"
13584		},
13585		"hz":{
13586				"name":"Herero",
13587				"nativeName":"Otjiherero"
13588		},
13589		"hi":{
13590				"name":"Hindi",
13591				"nativeName":"हिन्दी, हिंदी"
13592		},
13593		"ho":{
13594				"name":"Hiri Motu",
13595				"nativeName":"Hiri Motu"
13596		},
13597		"hu":{
13598				"name":"Hungarian",
13599				"nativeName":"Magyar"
13600		},
13601		"ia":{
13602				"name":"Interlingua",
13603				"nativeName":"Interlingua"
13604		},
13605		"id":{
13606				"name":"Indonesian",
13607				"nativeName":"Bahasa Indonesia"
13608		},
13609		"ie":{
13610				"name":"Interlingue",
13611				"nativeName":"Originally called Occidental; then Interlingue after WWII"
13612		},
13613		"ga":{
13614				"name":"Irish",
13615				"nativeName":"Gaeilge"
13616		},
13617		"ig":{
13618				"name":"Igbo",
13619				"nativeName":"Asụsụ Igbo"
13620		},
13621		"ik":{
13622				"name":"Inupiaq",
13623				"nativeName":"Iñupiaq, Iñupiatun"
13624		},
13625		"io":{
13626				"name":"Ido",
13627				"nativeName":"Ido"
13628		},
13629		"is":{
13630				"name":"Icelandic",
13631				"nativeName":"Íslenska"
13632		},
13633		"it":{
13634				"name":"Italian",
13635				"nativeName":"Italiano"
13636		},
13637		"iu":{
13638				"name":"Inuktitut",
13639				"nativeName":"ᐃᓄᒃᑎᑐᑦ"
13640		},
13641		"ja":{
13642				"name":"Japanese",
13643				"nativeName":"日本語 (にほんご/にっぽんご)"
13644		},
13645		"jv":{
13646				"name":"Javanese",
13647				"nativeName":"basa Jawa"
13648		},
13649		"kl":{
13650				"name":"Kalaallisut",
13651				"nativeName":"kalaallisut, kalaallit oqaasii"
13652		},
13653		"kn":{
13654				"name":"Kannada",
13655				"nativeName":"ಕನ್ನಡ"
13656		},
13657		"kr":{
13658				"name":"Kanuri",
13659				"nativeName":"Kanuri"
13660		},
13661		"ks":{
13662				"name":"Kashmiri",
13663				"nativeName":"कश्मीरी, كشميري‎"
13664		},
13665		"kk":{
13666				"name":"Kazakh",
13667				"nativeName":"Қазақ тілі"
13668		},
13669		"km":{
13670				"name":"Khmer",
13671				"nativeName":"ភាសាខ្មែរ"
13672		},
13673		"ki":{
13674				"name":"Kikuyu",
13675				"nativeName":"Gĩkũyũ"
13676		},
13677		"rw":{
13678				"name":"Kinyarwanda",
13679				"nativeName":"Ikinyarwanda"
13680		},
13681		"ky":{
13682				"name":"Kyrgyz",
13683				"nativeName":"кыргыз тили"
13684		},
13685		"kv":{
13686				"name":"Komi",
13687				"nativeName":"коми кыв"
13688		},
13689		"kg":{
13690				"name":"Kongo",
13691				"nativeName":"KiKongo"
13692		},
13693		"ko":{
13694				"name":"Korean",
13695				"nativeName":"한국어 (韓國語), 조선말 (朝鮮語)"
13696		},
13697		"ku":{
13698				"name":"Kurdish",
13699				"nativeName":"Kurdî, كوردی‎"
13700		},
13701		"kj":{
13702				"name":"Kuanyama",
13703				"nativeName":"Kuanyama"
13704		},
13705		"la":{
13706				"name":"Latin",
13707				"nativeName":"latine, lingua latina"
13708		},
13709		"lb":{
13710				"name":"Luxembourgish",
13711				"nativeName":"Lëtzebuergesch"
13712		},
13713		"lg":{
13714				"name":"Luganda",
13715				"nativeName":"Luganda"
13716		},
13717		"li":{
13718				"name":"Limburgish",
13719				"nativeName":"Limburgs"
13720		},
13721		"ln":{
13722				"name":"Lingala",
13723				"nativeName":"Lingála"
13724		},
13725		"lo":{
13726				"name":"Lao",
13727				"nativeName":"ພາສາລາວ"
13728		},
13729		"lt":{
13730				"name":"Lithuanian",
13731				"nativeName":"lietuvių kalba"
13732		},
13733		"lu":{
13734				"name":"Luba-Katanga",
13735				"nativeName":""
13736		},
13737		"lv":{
13738				"name":"Latvian",
13739				"nativeName":"latviešu valoda"
13740		},
13741		"gv":{
13742				"name":"Manx",
13743				"nativeName":"Gaelg, Gailck"
13744		},
13745		"mk":{
13746				"name":"Macedonian",
13747				"nativeName":"македонски јазик"
13748		},
13749		"mg":{
13750				"name":"Malagasy",
13751				"nativeName":"Malagasy fiteny"
13752		},
13753		"ms":{
13754				"name":"Malay",
13755				"nativeName":"bahasa Melayu, بهاس ملايو‎"
13756		},
13757		"ml":{
13758				"name":"Malayalam",
13759				"nativeName":"മലയാളം"
13760		},
13761		"mt":{
13762				"name":"Maltese",
13763				"nativeName":"Malti"
13764		},
13765		"mi":{
13766				"name":"Māori",
13767				"nativeName":"te reo Māori"
13768		},
13769		"mr":{
13770				"name":"Marathi",
13771				"nativeName":"मराठी"
13772		},
13773		"mh":{
13774				"name":"Marshallese",
13775				"nativeName":"Kajin M̧ajeļ"
13776		},
13777		"mn":{
13778				"name":"Mongolian",
13779				"nativeName":"монгол"
13780		},
13781		"na":{
13782				"name":"Nauru",
13783				"nativeName":"Ekakairũ Naoero"
13784		},
13785		"nv":{
13786				"name":"Navajo",
13787				"nativeName":"Diné bizaad, Dinékʼehǰí"
13788		},
13789		"nb":{
13790				"name":"Norwegian Bokmål",
13791				"nativeName":"Norsk bokmål"
13792		},
13793		"nd":{
13794				"name":"North Ndebele",
13795				"nativeName":"isiNdebele"
13796		},
13797		"ne":{
13798				"name":"Nepali",
13799				"nativeName":"नेपाली"
13800		},
13801		"ng":{
13802				"name":"Ndonga",
13803				"nativeName":"Owambo"
13804		},
13805		"nn":{
13806				"name":"Norwegian Nynorsk",
13807				"nativeName":"Norsk nynorsk"
13808		},
13809		"no":{
13810				"name":"Norwegian",
13811				"nativeName":"Norsk"
13812		},
13813		"ii":{
13814				"name":"Nuosu",
13815				"nativeName":"ꆈꌠ꒿ Nuosuhxop"
13816		},
13817		"nr":{
13818				"name":"South Ndebele",
13819				"nativeName":"isiNdebele"
13820		},
13821		"oc":{
13822				"name":"Occitan",
13823				"nativeName":"Occitan"
13824		},
13825		"oj":{
13826				"name":"Ojibwe",
13827				"nativeName":"ᐊᓂᔑᓈᐯᒧᐎᓐ"
13828		},
13829		"cu":{
13830				"name":"Church Slavonic",
13831				"nativeName":"ѩзыкъ словѣньскъ"
13832		},
13833		"om":{
13834				"name":"Oromo",
13835				"nativeName":"Afaan Oromoo"
13836		},
13837		"or":{
13838				"name":"Oriya",
13839				"nativeName":"ଓଡ଼ିଆ"
13840		},
13841		"os":{
13842				"name":"Ossetian",
13843				"nativeName":"ирон æвзаг"
13844		},
13845		"pa":{
13846				"name":"Punjabi",
13847				"nativeName":"ਪੰਜਾਬੀ, پنجابی‎"
13848		},
13849		"pi":{
13850				"name":"Pāli",
13851				"nativeName":"पाऴि"
13852		},
13853		"fa":{
13854				"name":"Persian",
13855				"nativeName":"فارسی"
13856		},
13857		"pl":{
13858				"name":"Polish",
13859				"nativeName":"polski"
13860		},
13861		"ps":{
13862				"name":"Pashto",
13863				"nativeName":"پښتو"
13864		},
13865		"pt":{
13866				"name":"Portuguese",
13867				"nativeName":"Português"
13868		},
13869		"qu":{
13870				"name":"Quechua",
13871				"nativeName":"Runa Simi, Kichwa"
13872		},
13873		"rm":{
13874				"name":"Romansh",
13875				"nativeName":"rumantsch grischun"
13876		},
13877		"rn":{
13878				"name":"Kirundi",
13879				"nativeName":"kiRundi"
13880		},
13881		"ro":{
13882				"name":"Romanian",
13883				"nativeName":"română"
13884		},
13885		"ru":{
13886				"name":"Russian",
13887				"nativeName":"русский язык"
13888		},
13889		"sa":{
13890				"name":"Sanskrit",
13891				"nativeName":"संस्कृतम्"
13892		},
13893		"sc":{
13894				"name":"Sardinian",
13895				"nativeName":"sardu"
13896		},
13897		"sd":{
13898				"name":"Sindhi",
13899				"nativeName":"सिन्धी, سنڌي، سندھی‎"
13900		},
13901		"se":{
13902				"name":"Northern Sami",
13903				"nativeName":"Davvisámegiella"
13904		},
13905		"sm":{
13906				"name":"Samoan",
13907				"nativeName":"gagana faa Samoa"
13908		},
13909		"sg":{
13910				"name":"Sango",
13911				"nativeName":"yângâ tî sängö"
13912		},
13913		"sr":{
13914				"name":"Serbian",
13915				"nativeName":"српски језик"
13916		},
13917		"gd":{
13918				"name":"Gaelic",
13919				"nativeName":"Gàidhlig"
13920		},
13921		"sn":{
13922				"name":"Shona",
13923				"nativeName":"chiShona"
13924		},
13925		"si":{
13926				"name":"Sinhalese",
13927				"nativeName":"සිංහල"
13928		},
13929		"sk":{
13930				"name":"Slovak",
13931				"nativeName":"slovenčina"
13932		},
13933		"sl":{
13934				"name":"Slovene",
13935				"nativeName":"slovenščina"
13936		},
13937		"so":{
13938				"name":"Somali",
13939				"nativeName":"Soomaaliga, af Soomaali"
13940		},
13941		"st":{
13942				"name":"Southern Sotho",
13943				"nativeName":"Sesotho"
13944		},
13945		"es":{
13946				"name":"Spanish",
13947				"nativeName":"español, castellano"
13948		},
13949		"su":{
13950				"name":"Sundanese",
13951				"nativeName":"Basa Sunda"
13952		},
13953		"sw":{
13954				"name":"Swahili",
13955				"nativeName":"Kiswahili"
13956		},
13957		"ss":{
13958				"name":"Swati",
13959				"nativeName":"SiSwati"
13960		},
13961		"sv":{
13962				"name":"Swedish",
13963				"nativeName":"svenska"
13964		},
13965		"ta":{
13966				"name":"Tamil",
13967				"nativeName":"தமிழ்"
13968		},
13969		"te":{
13970				"name":"Telugu",
13971				"nativeName":"తెలుగు"
13972		},
13973		"tg":{
13974				"name":"Tajik",
13975				"nativeName":"тоҷикӣ, toğikī, تاجیکی‎"
13976		},
13977		"th":{
13978				"name":"Thai",
13979				"nativeName":"ไทย"
13980		},
13981		"ti":{
13982				"name":"Tigrinya",
13983				"nativeName":"ትግርኛ"
13984		},
13985		"bo":{
13986				"name":"Tibetan",
13987				"nativeName":"བོད་ཡིག"
13988		},
13989		"tk":{
13990				"name":"Turkmen",
13991				"nativeName":"Türkmen, Түркмен"
13992		},
13993		"tl":{
13994				"name":"Tagalog",
13995				"nativeName":"Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔"
13996		},
13997		"tn":{
13998				"name":"Tswana",
13999				"nativeName":"Setswana"
14000		},
14001		"to":{
14002				"name":"Tonga",
14003				"nativeName":"faka Tonga"
14004		},
14005		"tr":{
14006				"name":"Turkish",
14007				"nativeName":"Türkçe"
14008		},
14009		"ts":{
14010				"name":"Tsonga",
14011				"nativeName":"Xitsonga"
14012		},
14013		"tt":{
14014				"name":"Tatar",
14015				"nativeName":"татарча, tatarça, تاتارچا‎"
14016		},
14017		"tw":{
14018				"name":"Twi",
14019				"nativeName":"Twi"
14020		},
14021		"ty":{
14022				"name":"Tahitian",
14023				"nativeName":"Reo Tahiti"
14024		},
14025		"ug":{
14026				"name":"Uyghur",
14027				"nativeName":"Uyƣurqə, ئۇيغۇرچە‎"
14028		},
14029		"uk":{
14030				"name":"Ukrainian",
14031				"nativeName":"українська"
14032		},
14033		"ur":{
14034				"name":"Urdu",
14035				"nativeName":"اردو"
14036		},
14037		"uz":{
14038				"name":"Uzbek",
14039				"nativeName":"zbek, Ўзбек, أۇزبېك‎"
14040		},
14041		"ve":{
14042				"name":"Venda",
14043				"nativeName":"Tshivenḓa"
14044		},
14045		"vi":{
14046				"name":"Vietnamese",
14047				"nativeName":"Tiếng Việt"
14048		},
14049		"vo":{
14050				"name":"Volapük",
14051				"nativeName":"Volapük"
14052		},
14053		"wa":{
14054				"name":"Walloon",
14055				"nativeName":"Walon"
14056		},
14057		"cy":{
14058				"name":"Welsh",
14059				"nativeName":"Cymraeg"
14060		},
14061		"wo":{
14062				"name":"Wolof",
14063				"nativeName":"Wollof"
14064		},
14065		"fy":{
14066				"name":"Western Frisian",
14067				"nativeName":"Frysk"
14068		},
14069		"xh":{
14070				"name":"Xhosa",
14071				"nativeName":"isiXhosa"
14072		},
14073		"yi":{
14074				"name":"Yiddish",
14075				"nativeName":"ייִדיש"
14076		},
14077		"yo":{
14078				"name":"Yoruba",
14079				"nativeName":"Yorùbá"
14080		},
14081		"za":{
14082				"name":"Zhuang",
14083				"nativeName":"Saɯ cueŋƅ, Saw cuengh"
14084		},
14085		"ar-dz":{
14086				"name":"Arabic (Algeria)",
14087				"nativeName":"العربية (الجزائر)"
14088		},
14089		"ar-bh":{
14090				"name":"Arabic (Bahrain)",
14091				"nativeName":"العربية (البحرين)"
14092		},
14093		"ar-eg":{
14094				"name":"Arabic (Egypt)",
14095				"nativeName":"العربية (مصر)"
14096		},
14097		"ar-iq":{
14098				"name":"Arabic (Iraq)",
14099				"nativeName":"العربية (العراق)"
14100		},
14101		"ar-jo":{
14102				"name":"Arabic (Jordan)",
14103				"nativeName":"العربية (الأردن)"
14104		},
14105		"ar-kw":{
14106				"name":"Arabic (Kuwait)",
14107				"nativeName":"العربية (الكويت)"
14108		},
14109		"ar-lb":{
14110				"name":"Arabic (Lebanon)",
14111				"nativeName":"العربية (لبنان)"
14112		},
14113		"ar-ly":{
14114				"name":"Arabic (Libya)",
14115				"nativeName":"العربية (ليبيا)"
14116		},
14117		"ar-ma":{
14118				"name":"Arabic (Morocco)",
14119				"nativeName":"العربية (المملكة المغربية)"
14120		},
14121		"ar-om":{
14122				"name":"Arabic (Oman)",
14123				"nativeName":"العربية (عمان)"
14124		},
14125		"ar-qa":{
14126				"name":"Arabic (Qatar)",
14127				"nativeName":"العربية (قطر)"
14128		},
14129		"ar-sa":{
14130				"name":"Arabic (Saudi Arabia)",
14131				"nativeName":"العربية (المملكة العربية السعودية)"
14132		},
14133		"ar-sy":{
14134				"name":"Arabic (Syria)",
14135				"nativeName":"العربية (سوريا)"
14136		},
14137		"ar-tn":{
14138				"name":"Arabic (Tunisia)",
14139				"nativeName":"العربية (تونس)"
14140		},
14141		"ar-ae":{
14142				"name":"Arabic (U.A.E.)",
14143				"nativeName":"العربية (الإمارات العربية المتحدة)"
14144		},
14145		"ar-ye":{
14146				"name":"Arabic (Yemen)",
14147				"nativeName":"العربية (اليمن)"
14148		},
14149		"de-at":{
14150				"name":"German (Austria)",
14151				"nativeName":"Deutsch (Österreich)"
14152		},
14153		"de-li":{
14154				"name":"German (Liechtenstein)",
14155				"nativeName":"Deutsch (Liechtenstein)"
14156		},
14157		"de-lu":{
14158				"name":"German (Luxembourg)",
14159				"nativeName":"Deutsch (Luxemburg)"
14160		},
14161		"de-ch":{
14162				"name":"German (Switzerland)",
14163				"nativeName":"Deutsch (Schweiz)"
14164		},
14165		"en-au":{
14166				"name":"English (Australia)",
14167				"nativeName":"English (Australia)"
14168		},
14169		"en-bz":{
14170				"name":"English (Belize)",
14171				"nativeName":"English (Belize)"
14172		},
14173		"en-ca":{
14174				"name":"English (Canada)",
14175				"nativeName":"English (Canada)"
14176		},
14177		"en-ie":{
14178				"name":"English (Ireland)",
14179				"nativeName":"English (Ireland)"
14180		},
14181		"en-jm":{
14182				"name":"English (Jamaica)",
14183				"nativeName":"English (Jamaica)"
14184		},
14185		"en-nz":{
14186				"name":"English (New Zealand)",
14187				"nativeName":""
14188		},
14189		"en-za":{
14190				"name":"English (South Africa)",
14191				"nativeName":"English (South Africa)"
14192		},
14193		"en-tt":{
14194				"name":"English (Trinidad)",
14195				"nativeName":"English (Trinidad y Tobago)"
14196		},
14197		"en-gb":{
14198				"name":"English (United Kingdom)",
14199				"nativeName":"English (United Kingdom)"
14200		},
14201		"en-us":{
14202				"name":"English (United States)",
14203				"nativeName":"English (United States)"
14204		},
14205		"es-ar":{
14206				"name":"Spanish (Argentina)",
14207				"nativeName":"Español (Argentina)"
14208		},
14209		"es-bo":{
14210				"name":"Spanish (Bolivia)",
14211				"nativeName":"Español (Bolivia)"
14212		},
14213		"es-cl":{
14214				"name":"Spanish (Chile)",
14215				"nativeName":"Español (Chile)"
14216		},
14217		"es-co":{
14218				"name":"Spanish (Colombia)",
14219				"nativeName":"Español (Colombia)"
14220		},
14221		"es-cr":{
14222				"name":"Spanish (Costa Rica)",
14223				"nativeName":"Español (Costa Rica)"
14224		},
14225		"es-do":{
14226				"name":"Spanish (Dominican Republic)",
14227				"nativeName":"Español (República Dominicana)"
14228		},
14229		"es-ec":{
14230				"name":"Spanish (Ecuador)",
14231				"nativeName":"Español (Ecuador)"
14232		},
14233		"es-sv":{
14234				"name":"Spanish (El Salvador)",
14235				"nativeName":"Español (El Salvador)"
14236		},
14237		"es-gt":{
14238				"name":"Spanish (Guatemala)",
14239				"nativeName":"Español (Guatemala)"
14240		},
14241		"es-hn":{
14242				"name":"Spanish (Honduras)",
14243				"nativeName":"Español (Honduras)"
14244		},
14245		"es-mx":{
14246				"name":"Spanish (Mexico)",
14247				"nativeName":"Español (México)"
14248		},
14249		"es-ni":{
14250				"name":"Spanish (Nicaragua)",
14251				"nativeName":"Español (Nicaragua)"
14252		},
14253		"es-pa":{
14254				"name":"Spanish (Panama)",
14255				"nativeName":"Español (Panamá)"
14256		},
14257		"es-py":{
14258				"name":"Spanish (Paraguay)",
14259				"nativeName":"Español (Paraguay)"
14260		},
14261		"es-pe":{
14262				"name":"Spanish (Peru)",
14263				"nativeName":"Español (Perú)"
14264		},
14265		"es-pr":{
14266				"name":"Spanish (Puerto Rico)",
14267				"nativeName":"Español (Puerto Rico)"
14268		},
14269		"es-uy":{
14270				"name":"Spanish (Uruguay)",
14271				"nativeName":"Español (Uruguay)"
14272		},
14273		"es-ve":{
14274				"name":"Spanish (Venezuela)",
14275				"nativeName":"Español (Venezuela)"
14276		},
14277		"fr-be":{
14278				"name":"French (Belgium)",
14279				"nativeName":"français (Belgique)"
14280		},
14281		"fr-ca":{
14282				"name":"French (Canada)",
14283				"nativeName":"français (Canada)"
14284		},
14285		"fr-lu":{
14286				"name":"French (Luxembourg)",
14287				"nativeName":"français (Luxembourg)"
14288		},
14289		"fr-ch":{
14290				"name":"French (Switzerland)",
14291				"nativeName":"français (Suisse)"
14292		},
14293		"it-ch":{
14294				"name":"Italian (Switzerland)",
14295				"nativeName":"italiano (Svizzera)"
14296		},
14297		"nl-be":{
14298				"name":"Dutch (Belgium)",
14299				"nativeName":"Nederlands (België)"
14300		},
14301		"pt-br":{
14302				"name":"Portuguese (Brazil)",
14303				"nativeName":"Português (Brasil)"
14304		},
14305		"sv-fi":{
14306				"name":"Swedish (Finland)",
14307				"nativeName":"svenska (Finland)"
14308		},
14309		"zh-hk":{
14310				"name":"Chinese (Hong Kong)",
14311				"nativeName":"中文(香港特别行政區)"
14312		},
14313		"zh-cn":{
14314				"name":"Chinese (PRC)",
14315				"nativeName":"中文(中华人民共和国)"
14316		},
14317		"zh-sg":{
14318				"name":"Chinese (Singapore)",
14319				"nativeName":"中文(新加坡)"
14320		},
14321		"zh-tw":{
14322				"name":"Chinese Traditional (Taiwan)",
14323				"nativeName":"中文(台灣)"
14324		}
14325	}
14325
14326
14327	AblePlayer.prototype.getLanguageName = function (key) {
14328		// key = key.slice(0,2);
14329		var lang = isoLangs[key];
14330		return lang ? lang.name : undefined;
14331	};
14332	AblePlayer.prototype.getLanguageNativeName = function (key) {
14333		// key = key.slice(0,2);
14334		var lang = isoLangs[key];
14335		return lang ? lang.nativeName : undefined;
14336	}
14337
14338})(jQuery);
14339(function ($) {
14340	AblePlayer.prototype.getSupportedLangs = function() {
14341		// returns an array of languages for which AblePlayer has translation tables
14342		var langs = ['ca','de','en','es','fr','he','it','ja','nb','nl','pt-br','tr','zh-tw'];
14343		return langs;
14344	};
14345
14346	AblePlayer.prototype.getTranslationText = function() {
14347		// determine language, then get labels and prompts from corresponding translation var
14348		var deferred, thisObj, lang, thisObj, msg, translationFile, collapsedLang;
14349		deferred = $.Deferred();
14350
14351		thisObj = this;
14352		// get language of the web page, if specified
14353		if ($('body').attr('lang')) {
14354			lang = $('body').attr('lang').toLowerCase();
14355		}
14356		else if ($('html').attr('lang')) {
14357			lang = $('html').attr('lang').toLowerCase();
14358		}
14359		else {
14360			lang = null;
14361		}
14362
14363		// override this.lang to language of the web page, if known and supported
14364		// otherwise this.lang will continue using default
14365		if (!this.forceLang) {
14366			if (lang) {
14367				if (lang !== this.lang) {
14368					if ($.inArray(lang,this.getSupportedLangs()) !== -1) {
14369						// this is a supported lang
14370						this.lang = lang;
14371					}
14372					else {
14373						msg = lang + ' is not currently supported. Using default language (' + this.lang + ')';
14374						if (this.debug) {
14375							console.log(msg);
14376						}
14377					}
14378				}
14379			}
14380		}
14381		if (!this.searchLang) {
14382			this.searchLang = this.lang;
14383		}
14384		translationFile = this.rootPath + 'translations/' + this.lang + '.js';
14385		this.importTranslationFile(translationFile).then(function(result) {
14386			collapsedLang = thisObj.lang.replace('-','');
14387			thisObj.tt = eval(collapsedLang);
14388			deferred.resolve();
14389		});
14390		return deferred.promise();
14391	};
14392
14393	AblePlayer.prototype.importTranslationFile = function(translationFile) {
14394
14395		var deferred = $.Deferred();
14396		$.getScript(translationFile)
14397			.done(function(translationVar,textStatus) {
14398				// translation file successfully retrieved
14399				deferred.resolve(translationVar);
14400			})
14401			.fail(function(jqxhr, settings, exception) {
14402				deferred.fail();
14403				// error retrieving file
14404				// TODO: handle this
14405			});
14406		return deferred.promise();
14407	};
14408
14409})(jQuery);
14410
14411(function($) {
14412	AblePlayer.prototype.computeEndTime = function(startTime, durationTime) {
14413		var SECONDS = 0;
14414		var MINUTES = 1;
14415		var HOURS = 2;
14416
14417		var startParts = startTime
14418			.split(':')
14419			.reverse()
14420			.map(function(value) {
14421				return parseFloat(value);
14422			});
14423
14424		var durationParts = durationTime
14425			.split(':')
14426			.reverse()
14427			.map(function(value) {
14428				return parseFloat(value);
14429			});
14430
14431		var endTime = startParts
14432			.reduce(function(acc, val, index) {
14433				var sum = val + durationParts[index];
14434
14435				if (index === SECONDS) {
14436					if (sum > 60) {
14437						durationParts[index + 1] += 1;
14438						sum -= 60;
14439					}
14440
14441					sum = sum.toFixed(3);
14442				}
14443
14444				if (index === MINUTES) {
14445					if (sum > 60) {
14446						durationParts[index + 1] += 1;
14447						sum -= 60;
14448					}
14449				}
14450
14451				if (sum < 10) {
14452					sum = '0' + sum;
14453				}
14454
14455				acc.push(sum);
14456
14457				return acc;
14458			}, [])
14459			.reverse()
14460			.join(':');
14461
14462		return endTime;
14463	};
14464
14465	AblePlayer.prototype.ttml2webvtt = function(contents) {
14466		var thisObj = this;
14467
14468		var xml = thisObj.convert.xml2json(contents, {
14469			ignoreComment: true,
14470			alwaysChildren: true,
14471			compact: true,
14472			spaces: 2
14473		});
14474
14475		var vttHeader = 'WEBVTT\n\n\n';
14476		var captions = JSON.parse(xml).tt.body.div.p;
14477
14478		var vttCaptions = captions.reduce(function(acc, value, index) {
14479			var text = value._text;
14480			var isArray = Array.isArray(text);
14481			var attributes = value._attributes;
14482			var endTime = thisObj.computeEndTime(attributes.begin, attributes.dur);
14483
14484			var caption =
14485				thisObj.computeEndTime(attributes.begin, '00:00:0') +
14486				' --> ' +
14487				thisObj.computeEndTime(attributes.begin, attributes.dur) +
14488				'\n' +
14489				(isArray ? text.join('\n') : text) +
14490				'\n\n';
14491
14492			return acc + caption;
14493		}, vttHeader);
14494
14495		return vttCaptions;
14496	};
14497})(jQuery);
14498
14499/*! Copyright (c) 2014 - Paul Tavares - purtuga - @paul_tavares - MIT License */
14500;(function($){
14501
14502    /**
14503     * Delays the execution of a function until an expression returns true.
14504     * The expression is checked every 100 milliseconds for as many tries
14505     * as defined in in the attempts option
14506     *
14507     * @param {Object} options
14508     * @param {Function} options.when
14509     *                      Function to execute on every interval.
14510     *                      Must return true (boolean) in order for
14511     *                      options.do to be executed.
14512     * @param {Function} [options.exec]
14513     *                      Function to be executed once options.when()
14514     *                      returns true.
14515     * @param {Interger} [options.interval=100]
14516     *                      How long to wait in-between tries.
14517     * @param {Interger} [options.attempts=100]
14518     *                      How many tries to use before its considered
14519     *                      a failure.
14520     * @param {Interger} [options.delayed=0]
14521     *                      Number of miliseconds to wait before execution
14522                            is started. Default is imediately.
14523     *
14524     * @return {jQuery.Promise}
14525     *
14526     * @example
14527     *
14528     *      $.doWhen({
14529     *          when: function(){
14530     *              return false;
14531     *          },
14532     *          exec: function(){
14533     *              alert("never called given false response on when param!");
14534     *          }
14535     *      })
14536     *      .fail(function(){
14537     *          alert('ALERT: FAILED CONDITION');
14538     *      })
14539     *      .then(function(){
14540     *          alert("resolved.");
14541     *      });
14542     *
14543     */
14544    $.doWhen = function(options) {
14545
14546        return $.Deferred(function(dfd){
14547
14548            var opt = $.extend({}, {
14549                    when:       null,
14550                    exec:       function(){},
14551                    interval:   100,
14552                    attempts:   100,
14553                    delayed:    0
14554                },
14555                options,
14556                {
14557                    checkId: null
14558                }),
14559                startChecking = function(){
14560
14561                    // Check condition now and if true, then resolve object
14562                    if (opt.when() === true) {
14563
14564                        opt.exec.call(dfd.promise());
14565                        dfd.resolve();
14566                        return;
14567
14568                    }
14569
14570                    // apply minimal UI and hide the overlay
14571                    opt.checkId = setInterval(function(){
14572
14573                            if (opt.attempts === 0) {
14574
14575                                clearInterval(opt.checkId);
14576                                dfd.reject();
14577
14578                            } else {
14579
14580                                --opt.attempts;
14581
14582                                if (opt.when() === true) {
14583
14584                                    opt.attempts = 0;
14585                                    clearInterval(opt.checkId);
14586                                    opt.exec.call(dfd.promise());
14587                                    dfd.resolve();
14588
14589                                }
14590
14591                            }
14592
14593                        }, opt.interval);
14594
14595                };
14596
14597            if (opt.delayed > 0) {
14598
14599                setTimeout(startChecking, Number(opt.delayed));
14600
14601            } else {
14602
14603                startChecking();
14604
14605            }
14606
14607        }).promise();
14608
14609    };
14610
14611})(jQuery);
14612/* Video Transcript Sorter (VTS)
14613 * Used to synchronize time stamps from WebVTT resources
14614 * so they appear in the proper sequence within an auto-generated interactive transcript
14615*/
14616
14617(function ($) {
14618	AblePlayer.prototype.injectVTS = function() {
14619
14620		// To add a transcript sorter to a web page:
14621		// Add <div id="able-vts"></div> to the web page
14622
14623		// Define all variables
14624		var thisObj, tracks, $heading;
14625		var $instructions, $p1, $p2, $ul, $li1, $li2, $li3;
14626		var $fieldset, $legend, i, $radioDiv, radioId, $label, $radio;
14627		var $saveButton, $savedTable;
14628
14629		thisObj = this;
14630
14631		if ($('#able-vts').length) {
14632			// Page includes a container for a VTS instance
14633
14634			// Are they qualifying tracks?
14635			if (this.vtsTracks.length) {
14636				// Yes - there are!
14637
14638				// Build an array of unique languages
14639				this.langs = [];
14640				this.getAllLangs(this.vtsTracks);
14641
14642				// Set the default VTS language
14643				this.vtsLang = this.lang;
14644
14645				// Inject a heading
14646				$heading = $('<h2>').text('Video Transcript Sorter'); // TODO: Localize; intelligently assign proper heading level
14647				$('#able-vts').append($heading);
14648
14649				// Inject an empty div for writing messages
14650				this.$vtsAlert = $('<div>',{
14651					'id': 'able-vts-alert',
14652					'aria-live': 'polite',
14653					'aria-atomic': 'true'
14654				})
14655				$('#able-vts').append(this.$vtsAlert);
14656
14657				// Inject instructions (TODO: Localize)
14658				$instructions = $('<div>',{
14659					'id': 'able-vts-instructions'
14660				});
14661				$p1 = $('<p>').text('Use the Video Transcript Sorter to perform any of the following tasks:');
14662				$ul = $('<ul>');
14663				$li1 = $('<li>').text('Reorder chapters, descriptions, captions, and/or subtitles so they appear in the proper sequence in Able Player\'s auto-generated transcript.');
14664				$li2 = $('<li>').text('Modify content or start/end times (all are directly editable within the table).');
14665				$li3 = $('<li>').text('Insert new content, such as chapters or descriptions.');
14666				$p2 = $('<p>').text('When finished editing, click the "Save Changes" button. This will auto-generate new content for all relevant timed text files (chapter
14666s, descriptions, captions, and/or subtitles), which can be copied and pasted into separate WebVTT files for use by Able Player.');
14667				$ul.append($li1,$li2,$li3);
14668				$instructions.append($p1,$ul,$p2);
14669				$('#able-vts').append($instructions);
14670
14671				// Inject a fieldset with radio buttons for each language
14672				$fieldset = $('<fieldset>');
14673				$legend = $('<legend>').text('Select a language'); // TODO: Localize this
14674				$fieldset.append($legend)
14675				for (i in this.langs) {
14676					radioId = 'vts-lang-radio-' + this.langs[i];
14677					$radioDiv = $('<div>',{
14678						// uncomment the following if label is native name
14679						// 'lang': this.langs[i]
14680					});
14681					$radio = $('<input>', {
14682						'type': 'radio',
14683						'name': 'vts-lang',
14684						'id': radioId,
14685						'value': this.langs[i]
14686					}).on('click',function() {
14687						thisObj.vtsLang = $(this).val();
14688						thisObj.showVtsAlert('Loading ' + thisObj.getLanguageName(thisObj.vtsLang) + ' tracks');
14689						thisObj.injectVtsTable('update',thisObj.vtsLang);
14690					});
14691					if (this.langs[i] == this.lang) {
14692						// this is the default language.
14693						$radio.prop('checked',true);
14694					}
14695					$label = $('<label>', {
14696						'for': radioId
14697						// Two options for label:
14698						// getLanguageNativeName() - returns native name; if using this be sure to add lang attr to <div> (see above)
14699						// getLanguageName() - returns name in English; doesn't require lang attr on <label>
14700					}).text(this.getLanguageName(this.langs[i]));
14701					$radioDiv.append($radio,$label);
14702					$fieldset.append($radioDiv);
14703				}
14704				$('#able-vts').append($fieldset);
14705
14706				// Inject a 'Save Changes' button
14707				$saveButton = $('<button>',{
14708					'type': 'button',
14709					'id': 'able-vts-save',
14710					'value': 'save'
14711				}).text('Save Changes'); // TODO: Localize this
14712				$('#able-vts').append($saveButton);
14713
14714				// Inject a table with one row for each cue in the default language
14715				this.injectVtsTable('add',this.vtsLang);
14716
14717				// TODO: Add drag/drop functionality for mousers
14718
14719				// Add event listeners for contenteditable cells
14720				var kindOptions, beforeEditing, editedCell, editedContent, i, closestKind;
14721				kindOptions = ['captions','chapters','descriptions','subtitles'];
14722				$('td[contenteditable="true"]').on('focus',function() {
14723					beforeEditing = $(this).text();
14724				}).on('blur',function() {
14725					if (beforeEditing != $(this).text()) {
14726						editedCell = $(this).index();
14727						editedContent = $(this).text();
14728						if (editedCell === 1) {
14729							// do some simple spelling auto-correct
14730							if ($.inArray(editedContent,kindOptions) === -1) {
14731								// whatever user typed is not a valid kind
14732								// assume they correctly typed the first character
14733								if (editedContent.substr(0,1) === 's') {
14734									$(this).text('subtitles');
14735								}
14736								else if (editedContent.substr(0,1) === 'd') {
14737									$(this).text('descriptions');
14738								}
14739								else if (editedContent.substr(0,2) === 'ch') {
14740									$(this).text('chapters');
14741								}
14742								else {
14743									// whatever else they types, assume 'captions'
14744									$(this).text('captions');
14745								}
14746							}
14747						}
14748						else if (editedCell === 2 || editedCell === 3) {
14749							// start or end time
14750							// ensure proper formatting (with 3 decimal places)
14751							$(this).text(thisObj.formatTimestamp(editedContent));
14752						}
14753					}
14754				}).on('keydown',function(e) {
14755					// don't allow keystrokes to trigger Able Player (or other) functions
14756					// while user is editing
14757					e.stopPropagation();
14758				});
14759
14760				// handle click on the Save button
14761
14762				// handle click on the Save button
14763				$('#able-vts-save').on('click',function(e) {
14764					e.stopPropagation();
14765					if ($(this).attr('value') == 'save') {
14766						// replace table with WebVTT output in textarea fields (for copying/pasting)
14767						$(this).attr('value','cancel').text('Return to Editor'); // TODO: Localize this
14768						$savedTable = $('#able-vts table');
14769						$('#able-vts-instructions').hide();
14770						$('#able-vts > fieldset').hide();
14771						$('#able-vts table').remove();
14772						$('#able-vts-icon-credit').remove();
14773						thisObj.parseVtsOutput($savedTable);
14774					}
14775					else {
14776						// cancel saving, and restore the table using edited content
14777						$(this).attr('value','save').text('Save Changes'); // TODO: Localize this
14778						$('#able-vts-output').remove();
vendor: 10,571 bytes, lines 14779-15126
14779						$('#able-vts-instructions').show();
14780						$('#able-vts > fieldset').show();
14781						$('#able-vts').append($savedTable);
14782						$('#able-vts').append(thisObj.getIconCredit());
14783						thisObj.showVtsAlert('Cancelling saving. Any edits you made have been restored in the VTS table.'); // TODO: Localize this
14784					}
14785				});
14786			}
14787		}
14788	};
14789
14790	AblePlayer.prototype.setupVtsTracks = function(kind, lang, label, src, contents) {
14791
14792		// Called from tracks.js
14793
14794		var srcFile, vtsCues;
14795
14796		srcFile = this.getFilenameFromPath(src);
14797		vtsCues = this.parseVtsTracks(contents);
14798
14799		this.vtsTracks.push({
14800			'kind': kind,
14801			'language': lang,
14802			'label': label,
14803			'srcFile': srcFile,
14804			'cues': vtsCues
14805		});
14806	};
14807
14808	AblePlayer.prototype.getFilenameFromPath = function(path) {
14809
14810		var lastSlash;
14811
14812		lastSlash = path.lastIndexOf('/');
14813		if (lastSlash === -1) {
14814			// there are no slashes in path.
14815			return path;
14816		}
14817		else {
14818			return path.substr(lastSlash+1);
14819		}
14820	};
14821
14822	AblePlayer.prototype.getFilenameFromTracks = function(kind,lang) {
14823
14824		for (var i=0; i<this.vtsTracks.length; i++) {
14825			if (this.vtsTracks[i].kind === kind && this.vtsTracks[i].language === lang) {
14826				// this is a matching track
14827				// srcFile has already been converted to filename from path before saving to vtsTracks
14828				return this.vtsTracks[i].srcFile;
14829			}
14830		}
14831		// no matching track found
14832		return false;
14833	};
14834
14835	AblePlayer.prototype.parseVtsTracks = function(contents) {
14836
14837		var rows, timeParts, cues, i, j, thisRow, nextRow, content, blankRow;
14838		rows = contents.split("\n");
14839		cues = [];
14840		i = 0;
14841		while (i < rows.length) {
14842			thisRow = rows[i];
14843			if (thisRow.indexOf(' --> ') !== -1) {
14844				// this is probably a time row
14845				timeParts = thisRow.trim().split(' ');
14846				if (this.isValidTimestamp(timeParts[0]) && this.isValidTimestamp(timeParts[2])) {
14847					// both timestamps are valid. This is definitely a time row
14848					content = '';
14849					j = i+1;
14850					blankRow = false;
14851					while (j < rows.length && !blankRow) {
14852						nextRow = rows[j].trim();
14853						if (nextRow.length > 0) {
14854							if (content.length > 0) {
14855								// add back the EOL between rows of content
14856								content += "\n" + nextRow;
14857							}
14858							else {
14859								// this is the first row of content. No need for an EOL
14860								content += nextRow;
14861							}
14862						}
14863						else {
14864							blankRow = true;
14865						}
14866						j++;
14867					}
14868					cues.push({
14869						'start': timeParts[0],
14870						'end': timeParts[2],
14871						'content': content
14872					});
14873					i = j; //skip ahead
14874				}
14875			}
14876			else {
14877				i++;
14878			}
14879		}
14880		return cues;
14881	};
14882
14883	AblePlayer.prototype.isValidTimestamp = function(timestamp) {
14884
14885		// return true if timestamp contains only numbers or expected punctuation
14886		if (/^[0-9:,.]*$/.test(timestamp)) {
14887			return true;
14888		}
14889		else {
14890			return false;
14891		}
14892	};
14893
14894	AblePlayer.prototype.formatTimestamp = function(timestamp) {
14895
14896		// timestamp is a string in the form "HH:MM:SS.xxx"
14897		// Take some simple steps to ensure edited timestamp values still adhere to expected format
14898
14899		var firstPart, lastPart;
14900
14901		var firstPart = timestamp.substr(0,timestamp.lastIndexOf('.')+1);
14902		var lastPart = timestamp.substr(timestamp.lastIndexOf('.')+1);
14903
14904		// TODO: Be sure each component within firstPart has only exactly two digits
14905		// Probably can't justify doing this automatically
14906		// If users enters '5' for minutes, that could be either '05' or '50'
14907		// This should trigger an error and prompt the user to correct the value before proceeding
14908
14909		// Be sure lastPart has exactly three digits
14910		if (lastPart.length > 3) {
14911			// chop off any extra digits
14912			lastPart = lastPart.substr(0,3);
14913		}
14914		else if (lastPart.length < 3) {
14915			// add trailing zeros
14916			while (lastPart.length < 3) {
14917				lastPart += '0';
14918			}
14919		}
14920		return firstPart + lastPart;
14921	};
14922
14923
14924	AblePlayer.prototype.injectVtsTable = function(action,lang) {
14925
14926		// action is either 'add' (for a new table) or 'update' (if user has selected a new lang)
14927
14928		var $table, headers, i, $tr, $th, $td, rows, rowNum, rowId;
14929
14930		if (action === 'update') {
14931			// remove existing table
14932			$('#able-vts table').remove();
14933			$('#able-vts-icon-credit').remove();
14934		}
14935
14936		$table = $('<table>',{
14937			'lang': lang
14938		});
14939		$tr = $('<tr>',{
14940			'lang': 'en' // TEMP, until header row is localized
14941		});
14942		headers = ['Row #','Kind','Start','End','Content','Actions']; // TODO: Localize this
14943		for (i=0; i < headers.length; i++) {
14944			$th = $('<th>', {
14945				'scope': 'col'
14946			}).text(headers[i]);
14947			if (headers[i] === 'Actions') {
14948				$th.addClass('actions');
14949			}
14950			$tr.append($th);
14951		}
14952		$table.append($tr);
14953
14954		// Get all rows (sorted by start time), and inject them into table
14955		rows = this.getAllRows(lang);
14956		for (i=0; i < rows.length; i++) {
14957			rowNum = i + 1;
14958			rowId = 'able-vts-row-' + rowNum;
14959			$tr = $('<tr>',{
14960				'id': rowId,
14961				'class': 'kind-' + rows[i].kind
14962			});
14963			// Row #
14964			$td = $('<td>').text(rowNum);
14965			$tr.append($td);
14966
14967			// Kind
14968			$td = $('<td>',{
14969				'contenteditable': 'true'
14970			}).text(rows[i].kind);
14971			$tr.append($td);
14972
14973			// Start
14974			$td = $('<td>',{
14975				'contenteditable': 'true'
14976			}).text(rows[i].start);
14977			$tr.append($td);
14978
14979			// End
14980			$td = $('<td>',{
14981				'contenteditable': 'true'
14982			}).text(rows[i].end);
14983			$tr.append($td);
14984
14985			// Content
14986			$td = $('<td>',{
14987				'contenteditable': 'true'
14988			}).text(rows[i].content); // TODO: Preserve tags
14989			$tr.append($td);
14990
14991					// Actions
14992			$td = this.addVtsActionButtons(rowNum,rows.length);
14993			$tr.append($td);
14994
14995			$table.append($tr);
14996		}
14997		$('#able-vts').append($table);
14998
14999		// Add credit for action button SVG icons
15000		$('#able-vts').append(this.getIconCredit());
15001
15002	};
15003
15004	AblePlayer.prototype.addVtsActionButtons = function(rowNum,numRows) {
15005
15006		// rowNum is the number of the current table row (starting with 1)
15007		// numRows is the total number of rows (excluding the header row)
15008		// TODO: Position buttons so they're vertically aligned, even if missing an Up or Down button
15009		var thisObj, $td, buttons, i, button, $button, $svg, $g, pathString, pathString2, $path, $path2;
15010		thisObj = this;
15011		$td = $('<td>');
15012		buttons = ['up','down','insert','delete'];
15013
15014		for (i=0; i < buttons.length; i++) {
15015			button = buttons[i];
15016			if (button === 'up') {
15017				if (rowNum > 1) {
15018					$button = $('<button>',{
15019						'id': 'able-vts-button-up-' + rowNum,
15020						'title': 'Move up',
15021						'aria-label': 'Move Row ' + rowNum + ' up'
15022					}).on('click', function(el) {
15023						thisObj.onClickVtsActionButton(el.currentTarget);
15024					});
15025					$svg = $('<svg>',{
15026						'focusable': 'false',
15027						'aria-hidden': 'true',
15028						'x': '0px',
15029						'y': '0px',
15030						'width': '254.296px',
15031						'height': '254.296px',
15032						'viewBox': '0 0 254.296 254.296',
15033						'style': 'enable-background:new 0 0 254.296 254.296'
15034					});
15035					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'
15036						+ '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'
15037						+ 'l0.064,0.064C255.857,194.153,255.857,182.998,249.628,176.101z';
15038					$path = $('<path>',{
15039						'd': pathString
15040					});
15041					$g = $('<g>').append($path);
15042					$svg.append($g);
15043					$button.append($svg);
15044					// Refresh button in the DOM in order for browser to process & display the SVG
15045					$button.html($button.html());
15046					$td.append($button);
15047				}
15048			}
15049			else if (button === 'down') {
15050				if (rowNum < numRows) {
15051					$button = $('<button>',{
15052						'id': 'able-vts-button-down-' + rowNum,
15053						'title': 'Move down',
15054						'aria-label': 'Move Row ' + rowNum + ' down'
15055					}).on('click', function(el) {
15056						thisObj.onClickVtsActionButton(el.currentTarget);
15057					});
15058					$svg = $('<svg>',{
15059						'focusable': 'false',
15060						'aria-hidden': 'true',
15061						'x': '0px',
15062						'y': '0px',
15063						'width': '292.362px',
15064						'height': '292.362px',
15065						'viewBox': '0 0 292.362 292.362',
15066						'style': 'enable-background:new 0 0 292.362 292.362'
15067					});
15068					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'
15069						+ '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'
15070						+ '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';
15071					$path = $('<path>',{
15072						'd': pathString
15073					});
15074					$g = $('<g>').append($path);
15075					$svg.append($g);
15076					$button.append($svg);
15077					// Refresh button in the DOM in order for browser to process & display the SVG
15078					$button.html($button.html());
15079					$td.append($button);
15080				}
15081			}
15082			else if (button === 'insert') {
15083				// Add Insert button to all rows
15084				$button = $('<button>',{
15085					'id': 'able-vts-button-insert-' + rowNum,
15086					'title': 'Insert row below',
15087					'aria-label': 'Insert row before Row ' + rowNum
15088				}).on('click', function(el) {
15089					thisObj.onClickVtsActionButton(el.currentTarget);
15090				});
15091				$svg = $('<svg>',{
15092					'focusable': 'false',
15093					'aria-hidden': 'true',
15094					'x': '0px',
15095					'y': '0px',
15096					'width': '401.994px',
15097					'height': '401.994px',
15098					'viewBox': '0 0 401.994 401.994',
15099					'style': 'enable-background:new 0 0 401.994 401.994'
15100				});
15101				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'
15102					+ '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'
15103					+ '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'
15104					+ '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'
15105					+ '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'
15106					+ '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';
15107				$path = $('<path>',{
15108					'd': pathString
15109				});
15110				$g = $('<g>').append($path);
15111				$svg.append($g);
15112				$button.append($svg);
15113				// Refresh button in the DOM in order for browser to process & display the SVG
15114				$button.html($button.html());
15115				$td.append($button);
15116			}
15117			else if (button === 'delete') {
15118				// Add Delete button to all rows
15119				$button = $('<button>',{
15120					'id': 'able-vts-button-delete-' + rowNum,
15121					'title': 'Delete row ',
15122					'aria-label': 'Delete Row ' + rowNum
15123				}).on('click', function(el) {
15124					thisObj.onClickVtsActionButton(el.currentTarget);
15125				});
15126				$svg = $('<svg>',{
15127					'focusable': 'false',
15128					'aria-hidden': 'true',
15129					'x': '0px',
15130					'y': '0px',
15131					'width': '508.52px',
15132					'height': '508.52px',
15133					'viewBox': '0 0 508.52 508.52',
15134					'style': 'enable-background:new 0 0 508.52 508.52'
15135				});
15136				pathString = 'M397.281,31.782h-63.565C333.716,14.239,319.478,0,301.934,0h-95.347'
15137					+ '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'
15138					+ 'C429.063,46.021,414.825,31.782,397.281,31.782z';
15139				$path = $('<path>',{
15140					'd': pathString
15141				});
15142				pathString2 = 'M79.456,476.737c0,17.544,14.239,31.782,31.782,31.782h286.042'
15143					+ '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'
15144					+ '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'
15145					+ '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'
15146					+ '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'
15147					+ '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'
15148					+ 'c-8.772,0-15.891-7.151-15.891-15.891V174.804z';
15149				$path2 = $('<path>',{
15150					'd': pathString2
15151				});
15152
15153				$g = $('<g>').append($path,$path2);
15154				$svg.append($g);
15155				$button.append($svg);
15156				// Refresh button in the DOM in order for browser to process & display the SVG
15157				$button.html($button.html());
15158				$td.append($button);
15159			}
15160		}
15161		return $td;
15162	};
15163
15164	AblePlayer.prototype.updateVtsActionButtons = function($buttons,nextRowNum) {
15165
15166		// TODO: Add some filters to this function to add or delete 'Up' and 'Down' buttons
15167		// if row is moved to/from the first/last rows
15168		var i, $thisButton, id, label, newId, newLabel;
15169		for (i=0; i < $buttons.length; i++) {
15170			$thisButton = $buttons.eq(i);
15171			id = $thisButton.attr('id');
15172			label = $thisButton.attr('aria-label');
15173			// replace the integer (id) within each of the above strings
15174			newId = id.replace(/[0-9]+/g, nextRowNum);
15175			newLabel = label.replace(/[0-9]+/g, nextRowNum);
15176			$thisButton.attr('id',newId);
15177			$thisButton.attr('aria-label',newLabel);
15178		}
15179	}
15180
15181	AblePlayer.prototype.getIconCredit = function() {
15182
15183		var credit;
15184		credit = '<div id="able-vts-icon-credit">'
15185			+ 'Action buttons made by <a href="https://www.flaticon.com/authors/elegant-themes">Elegant Themes</a> '
15186			+ 'from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> '
15187			+ 'are licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" '
15188			+ 'target="_blank">CC 3.0 BY</a>'
15189			+ '</div>';
15190			return credit;
15191	};
15192
15193	AblePlayer.prototype.getAllLangs = function(tracks) {
15194
15195		// update this.langs with any unique languages found in tracks
15196		var i;
15197		for (i in tracks) {
15198			if (tracks[i].hasOwnProperty('language')) {
15199				if ($.inArray(tracks[i].language,this.langs) === -1) {
15200					// this language is not already in the langs array. Add it.
15201					this.langs[this.langs.length] = tracks[i].language;
15202				}
15203			}
15204		}
15205	};
15206
15207	AblePlayer.prototype.getAllRows = function(lang) {
15208
15209		// returns an array of data to be displayed in VTS table
15210		// includes all cues for tracks of any type with matching lang
15211		// cues are sorted by start time
15212		var i, track, c, cues;
15213		cues = [];
15214		for (i=0; i < this.vtsTracks.length; i++) {
15215			track = this.vtsTracks[i];
15216			if (track.language == lang) {
15217				// this track matches the language. Add its cues to array
15218				for (c in track.cues) {
15219					cues.push({
15220						'kind': track.kind,
15221						'lang': lang,
15222						'id': track.cues[c].id,
15223						'start': track.cues[c].start,
15224						'end': track.cues[c].end,
15225						'content': track.cues[c].content
15226					});
15227				}
15228			}
15229		}
15230		// Now sort cues by start time
15231		cues.sort(function(a,b) {
15232			return a.start > b.start ? 1 : -1;
15233		});
15234		return cues;
15235	};
15236
15237
15238	AblePlayer.prototype.onClickVtsActionButton = function(el) {
15239
15240		// handle click on up, down, insert, or delete button
15241		var idParts, action, rowNum;
15242		idParts = $(el).attr('id').split('-');
15243		action = idParts[3];
15244		rowNum = idParts[4];
15245		if (action == 'up') {
15246			// move the row up
15247			this.moveRow(rowNum,'up');
15248		}
15249		else if (action == 'down') {
15250			// move the row down
15251			this.moveRow(rowNum,'down');
15252		}
15253		else if (action == 'insert') {
15254			// insert a row below
15255			this.insertRow(rowNum);
15256		}
15257		else if (action == 'delete') {
15258			// delete the row
15259			this.deleteRow(rowNum);
15260		}
15261	};
15262
15263	AblePlayer.prototype.insertRow = function(rowNum) {
15264
15265		// Insert empty row below rowNum
15266		var $table, $rows, numRows, newRowNum, newRowId, newTimes, $tr, $td;
15267		var $select, options, i, $option, newKind, newClass, $parentRow;
15268		var i, nextRowNum, $buttons;
15269
15270		$table = $('#able-vts table');
15271		$rows = $table.find('tr');
15272
15273		numRows = $rows.length - 1; // exclude header row
15274
15275		newRowNum = parseInt(rowNum) + 1;
15276		newRowId = 'able-vts-row-' + newRowNum;
15277
15278		// Create an empty row
15279		$tr = $('<tr>',{
15280			'id': newRowId
15281		});
15282
15283		// Row #
15284		$td = $('<td>').text(newRowNum);
15285		$tr.append($td);
15286
15287		// Kind (add a select field for chosing a kind)
15288		newKind = null;
15289		$select = $('<select>',{
15290			'id': 'able-vts-kind-' + newRowNum,
15291			'aria-label': 'What kind of track is this?',
15292			'placeholder': 'Select a kind'
15293		}).on('change',function() {
15294			newKind = $(this).val();
15295			newClass = 'kind-' + newKind;
15296			$parentRow = $(this).closest('tr');
15297			// replace the select field with the chosen value as text
15298			$(this).parent().text(newKind);
15299			// add a class to the parent row
15300			$parentRow.addClass(newClass);
15301		});
15302		options = ['','captions','chapters','descriptions','subtitles'];
15303		for (i=0; i<options.length; i++) {
15304			$option = $('<option>',{
15305				'value': options[i]
15306			}).text(options[i]);
15307			$select.append($option);
15308		}
15309		$td = $('<td>').append($select);
15310		$tr.append($td);
15311
15312		// Start
15313		$td = $('<td>',{
15314			'contenteditable': 'true'
15315		}); // TODO; Intelligently assign a new start time (see getAdjustedTimes())
15316		$tr.append($td);
15317
15318		// End
15319		$td = $('<td>',{
15320			'contenteditable': 'true'
15321		}); // TODO; Intelligently assign a new end time (see getAdjustedTimes())
15322		$tr.append($td);
15323
15324		// Content
15325		$td = $('<td>',{
15326			'contenteditable': 'true'
15327		});
15328		$tr.append($td);
15329
15330		// Actions
15331		$td = this.addVtsActionButtons(newRowNum,numRows);
15332		$tr.append($td);
15333
15334		// Now insert the new row
15335		$table.find('tr').eq(rowNum).after($tr);
15336
15337		// Update row.id, Row # cell, & action items for all rows after the inserted one
15338		for (i=newRowNum; i <= numRows; i++) {
15339			nextRowNum = i + 1;
15340			$rows.eq(i).attr('id','able-vts-row-' + nextRowNum); // increment tr id
15341			$rows.eq(i).find('td').eq(0).text(nextRowNum); // increment Row # as expressed in first td
15342			$buttons = $rows.eq(i).find('button');
15343			this.updateVtsActionButtons($buttons,nextRowNum);
15344		}
15345
15346		// Auto-adjust times
15347		this.adjustTimes(newRowNum);
15348
15349		// Announce the insertion
15350		this.showVtsAlert('A new row ' + newRowNum + ' has been inserted'); // TODO: Localize this
15351
15352		// Place focus in new select field
15353		$select.focus();
15354
15355	};
15356
15357	AblePlayer.prototype.deleteRow = function(rowNum) {
15358
15359		var $table, $rows, numRows, i, nextRowNum, $buttons;
15360
15361		$table = $('#able-vts table');
15362		$table[0].deleteRow(rowNum);
15363		$rows = $table.find('tr'); // this does not include the deleted row
15364		numRows = $rows.length - 1; // exclude header row
15365
15366		// Update row.id, Row # cell, & action buttons for all rows after the deleted one
15367		for (i=rowNum; i <= numRows; i++) {
15368			nextRowNum = i;
15369			$rows.eq(i).attr('id','able-vts-row-' + nextRowNum); // increment tr id
15370			$rows.eq(i).find('td').eq(0).text(nextRowNum); // increment Row # as expressed in first td
15371			$buttons = $rows.eq(i).find('button');
15372			this.updateVtsActionButtons($buttons,nextRowNum);
15373		}
15374
15375		// Announce the deletion
15376		this.showVtsAlert('Row ' + rowNum + ' has been deleted'); // TODO: Localize this
15377
15378	};
15379
15380	AblePlayer.prototype.moveRow = function(rowNum,direction) {
15381
15382		// swap two rows
15383		var $rows, $thisRow, otherRowNum, $otherRow, newTimes, msg;
15384
15385		$rows = $('#able-vts table').find('tr');
15386		$thisRow = $('#able-vts table').find('tr').eq(rowNum);
15387		if (direction == 'up') {
15388			otherRowNum = parseInt(rowNum) - 1;
15389			$otherRow = $('#able-vts table').find('tr').eq(otherRowNum);
15390			$otherRow.before($thisRow);
15391		}
15392		else if (direction == 'down') {
15393			otherRowNum = parseInt(rowNum) + 1;
15394			$otherRow = $('#able-vts table').find('tr').eq(otherRowNum);
15395			$otherRow.after($thisRow);
15396		}
15397		// Update row.id, Row # cell, & action buttons for the two swapped rows
15398		$thisRow.attr('id','able-vts-row-' + otherRowNum);
15399		$thisRow.find('td').eq(0).text(otherRowNum);
15400		this.updateVtsActionButtons($thisRow.find('button'),otherRowNum);
15401		$otherRow.attr('id','able-vts-row-' + rowNum);
15402		$otherRow.find('td').eq(0).text(rowNum);
15403		this.updateVtsActionButtons($otherRow.find('button'),rowNum);
15404
15405		// auto-adjust times
15406		this.adjustTimes(otherRowNum);
15407
15408		// Announce the move (TODO: Localize this)
15409		msg = 'Row ' + rowNum + ' has been moved ' + direction;
15410		msg += ' and is now Row ' + otherRowNum;
15411		this.showVtsAlert(msg);
15412	};
15413
15414	AblePlayer.prototype.adjustTimes = function(rowNum) {
15415
15416		// Adjusts start and end times of the current, previous, and next rows in VTS table
15417		// after a move or insert
15418		// NOTE: Fully automating this process would be extraordinarily complicated
15419		// The goal here is simply to make subtle tweaks to ensure rows appear
15420		// in the new order within the Able Player transcript
15421		// Additional tweaking will likely be required by the user
15422
15423		// HISTORY: Originally set minDuration to 2 seconds for captions and .500 for descriptions
15424		// However, this can results in significant changes to existing caption timing,
15425		// with not-so-positive results.
15426		// As of 3.1.15, setting minDuration to .001 for all track kinds
15427		// Users will have to make further adjustments manually if needed
15428
15429		// TODO: Add WebVTT validation on save, since tweaking times is risky
15430
15431		var	 minDuration, $rows, prevRowNum, nextRowNum, $row, $prevRow, $nextRow,
15432				kind, prevKind, nextKind,
15433				start, prevStart, nextStart,
15434				end, prevEnd, nextEnd;
15435
15436		// Define minimum duration (in seconds) for each kind of track
15437		minDuration = [];
15438		minDuration['captions'] = .001;
15439		minDuration['descriptions'] = .001;
15440		minDuration['chapters'] = .001;
15441
15442		// refresh rows object
15443		$rows = $('#able-vts table').find('tr');
15444
15445		// Get kind, start, and end from current row
15446		$row = $rows.eq(rowNum);
15447		if ($row.is('[class^="kind-"]')) {
15448			// row has a class that starts with "kind-"
15449			// Extract kind from the class name
15450			kind = this.getKindFromClass($row.attr('class'));
15451		}
15452		else {
15453			// Kind has not been assigned (e.g., newly inserted row)
15454			// Set as captions row by default
15455			kind = 'captions';
15456		}
15457		start = this.getSecondsFromColonTime($row.find('td').eq(2).text());
15458		end = this.getSecondsFromColonTime($row.find('td').eq(3).text());
15459
15460		// Get kind, start, and end from previous row
15461		if (rowNum > 1) {
15462			// this is not the first row. Include the previous row
15463			prevRowNum = rowNum - 1;
15464			$prevRow = $rows.eq(prevRowNum);
15465			if ($prevRow.is('[class^="kind-"]')) {
15466				// row has a class that starts with "kind-"
15467				// Extract kind from the class name
15468			 prevKind = this.getKindFromClass($prevRow.attr('class'));
15469			}
15470			else {
15471				// Kind has not been assigned (e.g., newly inserted row)
15472				prevKind = null;
15473			}
15474			prevStart = this.getSecondsFromColonTime($prevRow.find('td').eq(2).text());
15475			prevEnd = this.getSecondsFromColonTime($prevRow.find('td').eq(3).text());
15476		}
15477		else {
15478			// this is the first row
15479			prevRowNum = null;
15480			$prevRow = null;
15481			prevKind = null;
15482			prevStart = null;
15483			prevEnd = null;
15484		}
15485
15486		// Get kind, start, and end from next row
15487		if (rowNum < ($rows.length - 1)) {
15488			// this is not the last row. Include the next row
15489			nextRowNum = rowNum + 1;
15490			$nextRow = $rows.eq(nextRowNum);
15491			if ($nextRow.is('[class^="kind-"]')) {
15492				// row has a class that starts with "kind-"
15493				// Extract kind from the class name
15494			 nextKind = this.getKindFromClass($nextRow.attr('class'));
15495			}
15496			else {
15497				// Kind has not been assigned (e.g., newly inserted row)
15498				nextKind = null;
15499			}
15500			nextStart = this.getSecondsFromColonTime($nextRow.find('td').eq(2).text());
15501			nextEnd = this.getSecondsFromColonTime($nextRow.find('td').eq(3).text());
15502		}
15503		else {
15504			// this is the last row
15505			nextRowNum = null;
15506			$nextRow = null;
15507			nextKind = null;
15508			nextStart = null;
15509			nextEnd = null;
15510		}
15511
15512		if (isNaN(start)) {
15513			if (prevKind == null) {
15514				// The previous row was probably inserted, and user has not yet selected a kind
15515				// automatically set it to captions
15516				prevKind = 'captions';
15517				$prevRow.attr('class','kind-captions');
15518				$prevRow.find('td').eq(1).html('captions');
15519			}
15520			// Current row has no start time (i.e., it's an inserted row)
15521			if (prevKind === 'captions') {
15522				// start the new row immediately after the captions end
15523				start = (parseFloat(prevEnd) + .001).toFixed(3);
15524				if (nextStart) {
15525					// end the new row immediately before the next row starts
15526					end = (parseFloat(nextStart) - .001).toFixed(3);
15527				}
15528				else {
15529					// this is the last row. Use minDuration to calculate end time.
15530					end = (parseFloat(start) + minDuration[kind]).toFixed(3);
15531				}
15532			}
15533			else if (prevKind === 'chapters') {
15534				// start the new row immediately after the chapter start (not end)
15535				start = (parseFloat(prevStart) + .001).toFixed(3);
15536				if (nextStart) {
15537					// end the new row immediately before the next row starts
15538					end = (parseFloat(nextStart) - .001).toFixed(3);
15539				}
15540				else {
15541					// this is the last row. Use minDuration to calculate end time.
15542					end = (parseFloat(start) + minDurartion[kind]).toFixed(3);
15543				}
15544			}
15545			else if (prevKind === 'descriptions') {
15546				// start the new row minDuration['descriptions'] after the description starts
15547				// this will theoretically allow at least a small cushion for the description to be read
15548				start = (parseFloat(prevStart) + minDuration['descriptions']).toFixed(3);
15549				end = (parseFloat(start) + minDuration['descriptions']).toFixed(3);
15550			}
15551		}
15552		else {
15553			// current row has a start time (i.e., an existing row has been moved))
15554			if (prevStart) {
15555				// this is not the first row.
15556				if (prevStart < start) {
15557					if (start < nextStart) {
15558						// No change is necessary
15559					}
15560					else {
15561						// nextStart needs to be incremented
15562						nextStart = (parseFloat(start) + minDuration[kind]).toFixed(3);
15563						nextEnd = (parseFloat(nextStart) + minDuration[nextKind]).toFixed(3);
15564						// TODO: Ensure nextEnd does not exceed the following start (nextNextStart)
15565						// Or... maybe this is getting too complicated and should be left up to the user
15566					}
15567				}
15568				else {
15569					// start needs to be incremented
15570					start = (parseFloat(prevStart) + minDuration[prevKind]).toFixed(3);
15571					end = (parseFloat(start) + minDuration[kind]).toFixed(3);
15572				}
15573			}
15574			else {
15575				// this is the first row
15576				if (start < nextStart) {
15577					// No change is necessary
15578				}
15579				else {
15580					// nextStart needs to be incremented
15581					nextStart = (parseFloat(start) + minDuration[kind]).toFixed(3);
15582					nextEnd = (parseFloat(nextStart) + minDuration[nextKind]).toFixed(3);
15583				}
15584			}
15585		}
15586
15587		// check to be sure there is sufficient duration between new start & end times
15588		if (end - start < minDuration[kind]) {
15589			// duration is too short. Change end time
15590			end = (parseFloat(start) + minDuration[kind]).toFixed(3);
15591			if (nextStart) {
15592				// this is not the last row
15593				// increase start time of next row
15594				nextStart = (parseFloat(end) + .001).toFixed(3);
15595			}
15596		}
15597
15598		// Update all affected start/end times
15599		$row.find('td').eq(2).text(this.formatSecondsAsColonTime(start,true));
15600		$row.find('td').eq(3).text(this.formatSecondsAsColonTime(end,true));
15601		if ($prevRow) {
15602			$prevRow.find('td').eq(2).text(this.formatSecondsAsColonTime(prevStart,true));
15603			$prevRow.find('td').eq(3).text(this.formatSecondsAsColonTime(prevEnd,true));
15604		}
15605		if ($nextRow) {
15606			$nextRow.find('td').eq(2).text(this.formatSecondsAsColonTime(nextStart,true));
15607			$nextRow.find('td').eq(3).text(this.formatSecondsAsColonTime(nextEnd,true));
15608		}
15609	};
15610
15611	AblePlayer.prototype.getKindFromClass = function(myclass) {
15612
15613		// This function is called when a class with prefix "kind-" is found in the class attribute
15614		// TODO: Rewrite this using regular expressions
15615		var kindStart, kindEnd, kindLength, kind;
15616
15617		kindStart = myclass.indexOf('kind-')+5;
15618		kindEnd = myclass.indexOf(' ',kindStart);
15619		if (kindEnd == -1) {
15620			// no spaces found, "kind-" must be the only myclass
15621			kindLength = myclass.length - kindStart;
15622		}
15623		else {
15624			kindLength = kindEnd - kindStart;
15625		}
15626		kind = myclass.substr(kindStart,kindLength);
15627		return kind;
15628	};
15629
15630	AblePlayer.prototype.showVtsAlert = function(message) {
15631
15632		// this is distinct from greater Able Player showAlert()
15633		// because it's positioning needs are unique
15634		// For now, alertDiv is fixed at top left of screen
15635		// but could ultimately be modified to appear near the point of action in the VTS table
15636		this.$vtsAlert.text(message).show().delay(3000).fadeOut('slow');
15637	};
15638
15639	AblePlayer.prototype.parseVtsOutput = function($table) {
15640
15641		// parse table into arrays, then into WebVTT content, for each kind
15642		// Display the WebVTT content in textarea fields for users to copy and paste
15643		var lang, i, kinds, kind, vtt, $rows, start, end, content, $output;
15644
15645		lang = $table.attr('lang');
15646		kinds = ['captions','chapters','descriptions','subtitles'];
15647		vtt = {};
15648		for (i=0; i < kinds.length; i++) {
15649			kind = kinds[i];
15650			vtt[kind] = 'WEBVTT' + "\n\n";
15651		}
15652		$rows = $table.find('tr');
15653		if ($rows.length > 0) {
15654			for (i=0; i < $rows.length; i++) {
15655				kind = $rows.eq(i).find('td').eq(1).text();
15656				if ($.inArray(kind,kinds) !== -1) {
15657					start = $rows.eq(i).find('td').eq(2).text();
15658					end = $rows.eq(i).find('td').eq(3).text();
15659					content = $rows.eq(i).find('td').eq(4).text();
15660					if (start !== undefined && end !== undefined) {
15661						vtt[kind] += start + ' --> ' + end + "\n";
15662						if (content !== 'undefined') {
15663							vtt[kind] += content;
15664						}
15665						vtt[kind] += "\n\n";
15666					}
15667				}
15668			}
15669		}
15670		$output = $('<div>',{
15671			'id': 'able-vts-output'
15672		})
15673		$('#able-vts').append($output);
15674		for (i=0; i < kinds.length; i++) {
15675			kind = kinds[i];
15676			if (vtt[kind].length > 8) {
15677				// some content has been added
15678				this.showWebVttOutput(kind,vtt[kind],lang)
15679			}
15680		}
15681	};
15682
15683	AblePlayer.prototype.showWebVttOutput = function(kind,vttString,lang) {
15684
15685		var $heading, filename, $p, pText, $textarea;
15686
15687		$heading = $('<h3>').text(kind.charAt(0).toUpperCase() + kind.slice(1));
15688		filename = this.getFilenameFromTracks(kind,lang);
15689		pText = 'If you made changes, copy/paste the following content ';
15690		if (filename) {
15691			pText += 'to replace the original content of your ' + this.getLanguageName(lang) + ' ';
15692			pText += '<em>' + kind + '</em> WebVTT file (<strong>' + filename + '</strong>).';
15693		}
15694		else {
15695			pText += 'into a new ' + this.getLanguageName(lang) + ' <em>' + kind + '</em> WebVTT file.';
15696		}
15697		$p = $('<p>',{
15698			'class': 'able-vts-output-instructions'
15699		}).html(pText);
15700		$textarea = $('<textarea>').text(vttString);
15701		$('#able-vts-output').append($heading,$p,$textarea);
15702	};
15703
15704})(jQuery);
15705
15706
15707(function ($) {
15708	AblePlayer.prototype.initVimeoPlayer = function () {
15709
15710		var thisObj, deferred, promise, containerId, vimeoId, autoplay, videoDimensions, options;
15711		thisObj = this;
15712
15713		deferred = new $.Deferred();
15714		promise = deferred.promise();
15715
15716		containerId = this.mediaId + '_vimeo';
15717
15718		// add container to which Vimeo player iframe will be appended
15719		this.$mediaContainer.prepend($('<div>').attr('id', containerId));
15720
15721		// if a described version is available && user prefers desription
15722		// init player using the described version
15723		if (this.vimeoDescId && this.prefDesc) {
15724			vimeoId = this.vimeoDescId;
15725		}
15726		else {
15727			vimeoId = this.vimeoId;
15728		}
15729		this.activeVimeoId = vimeoId;
15730
15731		// Notes re. Vimeo Embed Options:
15732		// If a video is owned by a user with a paid Plus, PRO, or Business account,
15733    // setting the "controls" option to "false" will hide the default controls, without hiding captions.
15734		// This is a new option from Vimeo; previously used "background:true" to hide the controller,
15735		// but that had unwanted side effects:
15736		//  - In addition to hiding the controls, it also hides captions
15737		//  - It automatically autoplays (initializing the player with autoplay:false does not override this)
15738		//  - It automatically loops (but this can be overridden by initializing the player with loop:false)
15739		//  - It automatically sets volume to 0 (not sure if this can be overridden, since no longer using the background option)
15740
15741		if (this.autoplay && this.okToPlay) {
15742			autoplay = 'true';
15743		}
15744		else {
15745			autoplay = 'false';
15746		}
15747
15748		videoDimensions = this.getVimeoDimensions(this.activeVimeoId, containerId);
15749		if (videoDimensions) {
15750			this.vimeoWidth = videoDimensions[0];
15751			this.vimeoHeight = videoDimensions[1];
15752			this.aspectRatio = thisObj.ytWidth / thisObj.ytHeight;
15753		}
15754		else {
15755			// dimensions are initially unknown
15756			// sending null values to Vimeo results in a video that uses the default Vimeo dimensions
15757			// these can then be scraped from the iframe and applied to this.$ableWrapper
15758			this.vimeoWidth = null;
15759			this.vimeoHeight = null;
15760		}
15761
15762		options = {
15763		  id: vimeoId,
15764			width: this.vimeoWidth,
15765			controls: false
15766		};
15767
15768		this.vimeoPlayer = new Vimeo.Player(containerId, options);
15769
15770		this.vimeoPlayer.ready().then(function() {
15771
15772			if (!thisObj.hasPlaylist) {
15773				// remove the media element, since Vimeo replaces that with its own element in an iframe
15774				// this is handled differently for playlists. See buildplayer.js > cuePlaylistItem()
15775				thisObj.$media.remove();
15776
15777				// define variables that will impact player setup
15778
15779				// vimeoSupportsPlaybackRateChange
15780				// changing playbackRate is only supported if the video is hosted on a Pro or Business account
15781				// unfortunately there is no direct way to query for that information.
15782				// this.vimeoPlayer.getPlaybackRate() returns a value, regardless of account type
15783				// This is a hack:
15784				// Attempt to change the playbackRate. If it results in an error, assume changing playbackRate is not supported.
15785				// Supported playbackRate values are 0.5 to 2.
15786				thisObj.vimeoPlaybackRate = 1;
15787				thisObj.vimeoPlayer.setPlaybackRate(thisObj.vimeoPlaybackRate).then(function(playbackRate) {
15788				// playback rate was set
15789					thisObj.vimeoSupportsPlaybackRateChange = true;
15790				}).catch(function(error) {
15791					thisObj.vimeoSupportsPlaybackRateChange = false;
15792				});
15793				deferred.resolve();
15794			}
15795		});
15796		return promise;
15797	};
15798
15799	AblePlayer.prototype.getVimeoPaused = function () {
15800
15801		var deferred, promise;
15802		deferred = new $.Deferred();
15803		promise = deferred.promise();
15804
15805		this.vimeoPlayer.getPaused().then(function (paused) {
15806			// paused is Boolean
15807			deferred.resolve(paused);
15808		});
15809
15810		return promise;
15811	}
15812
15813	AblePlayer.prototype.getVimeoEnded = function () {
15814
15815		var deferred, promise;
15816		deferred = new $.Deferred();
15817		promise = deferred.promise();
15818
15819		this.vimeoPlayer.getEnded().then(function (ended) {
15820			// ended is Boolean
15821			deferred.resolve(ended);
15822		});
15823
15824		return promise;
15825	}
15826
15827	AblePlayer.prototype.getVimeoState = function () {
15828
15829		var thisObj, deferred, promise, promises, gettingPausedPromise, gettingEndedPromise;
15830
15831		thisObj = this;
15832
15833		deferred = new $.Deferred();
15834		promise = deferred.promise();
15835		promises = [];
15836
15837		gettingPausedPromise = this.vimeoPlayer.getPaused();
15838		gettingEndedPromise = this.vimeoPlayer.getEnded();
15839
15840		promises.push(gettingPausedPromise);
15841		promises.push(gettingEndedPromise);
15842
15843		gettingPausedPromise.then(function (paused) {
15844			deferred.resolve(paused);
15845		});
15846		gettingEndedPromise.then(function (ended) {
15847			deferred.resolve(ended);
15848		});
15849		$.when.apply($, promises).then(function () {
15850			deferred.resolve();
15851		});
15852		return promise;
15853	}
15854
15855	AblePlayer.prototype.getVimeoDimensions = function (vimeoContainerId) {
15856
15857		// get dimensions of Vimeo video, return array with width & height
15858		// Sources, in order of priority:
15859		// 1. The width and height attributes on <video>
15860		// 2. YouTube (not yet supported; can't seem to get this data via YouTube Data API without OAuth!)
15861
15862		var d, url, $iframe, width, height;
15863
15864		d = [];
15865
15866		if (typeof this.playerMaxWidth !== 'undefined') {
15867			d[0] = this.playerMaxWidth;
15868			// optional: set height as well; not required though since YouTube will adjust height to match width
15869			if (typeof this.playerMaxHeight !== 'undefined') {
15870				d[1] = this.playerMaxHeight;
15871			}
15872			return d;
15873		}
15874		else {
15875			if (typeof $('#' + vimeoContainerId) !== 'undefined') {
15876				$iframe = $('#' + vimeoContainerId);
15877				width = $iframe.width();
15878				height = $iframe.height();
15879				if (width > 0 && height > 0) {
15880					d[0] = width;
15881					d[1] = height;
15882					return d;
15883				}
15884			}
15885		}
15886		return false;
15887	};
15888
15889	AblePlayer.prototype.resizeVimeoPlayer = function(youTubeId, youTubeContainerId) {
15890
15891    // NOTE: This function is modeled after same function in youtube.js
15892    // in case useful for Vimeo, but is not currently used
15893
15894		// called after player is ready, if youTube dimensions were previously unknown
15895		// Now need to get them from the iframe element that YouTube injected
15896		// and resize Able Player to match
15897		var d, width, height;
15898		if (typeof this.aspectRatio !== 'undefined') {
15899			// video dimensions have already been collected
15900			if (this.restoringAfterFullScreen) {
15901				// restore using saved values
15902				if (this.youTubePlayer) {
15903					this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
15904				}
15905				this.restoringAfterFullScreen = false;
15906			}
15907			else {
15908				// recalculate with new wrapper size
15909				width = this.$ableWrapper.parent().width();
15910				height = Math.round(width / this.aspectRatio);
15911				this.$ableWrapper.css({
15912					'max-width': width + 'px',
15913					'width': ''
15914				});
15915				this.youTubePlayer.setSize(width, height);
15916				if (this.fullscreen) {
15917					this.youTubePlayer.setSize(width, height);
15918				}
15919				else {
15920					// resizing due to a change in window size, not full screen
15921					this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
15922				}
15923			}
15924		}
15925		else {
15926			d = this.getYouTubeDimensions(youTubeId, youTubeContainerId);
15927			if (d) {
15928				width = d[0];
15929				height = d[1];
15930				if (width > 0 && height > 0) {
15931					this.aspectRatio = width / height;
15932					this.ytWidth = width;
15933					this.ytHeight = height;
15934					if (width !== this.$ableWrapper.width()) {
15935						// now that we've retrieved YouTube's default width,
15936						// need to adjust to fit the current player wrapper
15937						width = this.$ableWrapper.width();
15938						height = Math.round(width / this.aspectRatio);
15939						if (this.youTubePlayer) {
15940							this.youTubePlayer.setSize(width, height);
15941						}
15942					}
15943				}
15944			}
15945		}
15946	};
15947
15948	AblePlayer.prototype.setupVimeoCaptions = function () {
15949
15950		// called from setupAltCaptions if player is YouTube and there are no <track> captions
15951
15952		// use YouTube Data API to get caption data from YouTube
15953		// function is called only if these conditions are met:
15954		// 1. this.player === 'youtube'
15955		// 2. there are no <track> elements with kind="captions"
15956		// 3. youTubeDataApiKey is defined
15957
15958		var deferred = new $.Deferred();
15959		var promise = deferred.promise();
15960
15961		var thisObj, googleApiPromise, youTubeId, i;
15962
15963		thisObj = this;
15964
15965		// if a described version is available && user prefers desription
15966		// Use the described version, and get its captions
15967		if (this.youTubeDescId && this.prefDesc) {
15968			youTubeId = this.youTubeDescId;
15969		}
15970		else {
15971			youTubeId = this.youTubeId;
15972		}
15973		if (typeof youTubeDataAPIKey !== 'undefined') {
15974			// Wait until Google Client API is loaded
15975			// When loaded, it sets global var googleApiReady to true
15976
15977			// Thanks to Paul Tavares for $.doWhen()
15978			// https://gist.github.com/purtuga/8257269
15979			$.doWhen({
15980				when: function(){
15981					return googleApiReady;
15982				},
15983				interval: 100, // ms
15984				attempts: 1000
15985			})
15986			.done(function(){
15987					deferred.resolve();
15988			})
15989			.fail(function(){
15990				console.log('Unable to initialize Google API. YouTube captions are currently unavailable.');
15991			});
15992		}
15993		else {
15994			deferred.resolve();
15995		}
15996		return promise;
15997	};
15998
15999	AblePlayer.prototype.getVimeoCaptionTracks = function () {
16000
16001		// get data via Vimeo Player API, and push data to this.captions
16002		// Note: Vimeo doesn't expose the caption cues themselves
16003		// so this.captions will only include metadata about caption tracks; not cues
16004		var deferred = new $.Deferred();
16005		var promise = deferred.promise();
16006
16007		var thisObj, i, trackId, isDefaultTrack;
16008
16009		thisObj = this;
16010
16011		this.vimeoPlayer.getTextTracks().then(function(tracks) {
16012
16013				// each Vimeo track includes the following:
16014				// label (local name of the language)
16015				// language (2-character code)
16016				// kind (captions or subtitles, as declared by video owner)
16017				// mode ('disabled' or 'showing')
16018
16019				if (tracks.length) {
16020
16021					// create a new button for each caption track
16022					for (i=0; i<tracks.length; i++) {
16023
16024						thisObj.hasCaptions = true;
16025						thisObj.usingVimeoCaptions = true;
16026						if (thisObj.prefCaptions === 1) {
16027								thisObj.captionsOn = true;
16028						}
16029						else {
16030							thisObj.captionsOn = false;
16031						}
16032						// assign the default track based on language of the player
16033						if (tracks[i]['language'] === thisObj.lang) {
16034							isDefaultTrack = true;
16035						}
16036						else {
16037								isDefaultTrack = false;
16038						}
16039						thisObj.tracks.push({
16040						  'kind': tracks[i]['kind'],
16041							'language': tracks[i]['language'],
16042							'label': tracks[i]['label'],
16043							'def': isDefaultTrack
16044						});
16045					}
16046
16047					// setupPopups again with new captions array, replacing original
16048					thisObj.setupPopups('captions');
16049					deferred.resolve();
16050			 	}
16051			 	else {
16052				  thisObj.hasCaptions = false;
16053					thisObj.usingVimeoCaptions = false;
16054					deferred.resolve();
16055				}
16056			});
16057
16058		return promise;
16059	};
16060
16061	AblePlayer.prototype.initVimeoCaptionModule = function () {
16062
16063    // NOTE: This function is modeled after same function in youtube.js
16064    // in case useful for Vimeo, but is not currently used
16065
16066		// This function is called when YouTube onApiChange event fires
16067		// to indicate that the player has loaded (or unloaded) a module with exposed API methods
16068		// it isn't fired until the video starts playing
16069		// and only fires if captions are available for this video (automated captions don't count)
16070		// If no captions are available, onApichange event never fires & this function is never called
16071
16072		// YouTube iFrame API documentation is incomplete related to captions
16073		// Found undocumented features on user forums and by playing around
16074		// Details are here: http://terrillthompson.com/blog/648
16075		// Summary:
16076		// User might get either the AS3 (Flash) or HTML5 YouTube player
16077		// The API uses a different caption module for each player (AS3 = 'cc'; HTML5 = 'captions')
16078		// There are differences in the data and methods available through these modules
16079		// This function therefore is used to determine which captions module is being used
16080		// If it's a known module, this.ytCaptionModule will be used elsewhere to control captions
16081		var options, fontSize, displaySettings;
16082
16083		options = this.youTubePlayer.getOptions();
16084		if (options.length) {
16085			for (var i=0; i<options.length; i++) {
16086				if (options[i] == 'cc') { // this is the AS3 (Flash) player
16087					this.ytCaptionModule = 'cc';
16088					if (!this.hasCaptions) {
16089						// there are captions available via other sources (e.g., <track>)
16090						// so use these
16091						this.hasCaptions = true;
16092						this.usingYouTubeCaptions = true;
16093					}
16094					break;
16095				}
16096				else if (options[i] == 'captions') { // this is the HTML5 player
16097					this.ytCaptionModule = 'captions';
16098					if (!this.hasCaptions) {
16099						// there are captions available via other sources (e.g., <track>)
16100						// so use these
16101						this.hasCaptions = true;
16102						this.usingYouTubeCaptions = true;
16103					}
16104					break;
16105				}
16106			}
16107			if (typeof this.ytCaptionModule !== 'undefined') {
16108				if (this.usingYouTubeCaptions) {
16109					// set default languaage
16110					this.youTubePlayer.setOption(this.ytCaptionModule, 'track', {'languageCode': this.captionLang});
16111					// set font size using Able Player prefs (values are -1, 0, 1, 2, and 3, where 0 is default)
16112					this.youTubePlayer.setOption(this.ytCaptionModule,'fontSize',this.translatePrefs('size',this.prefCaptionsSize,'youtube'));
16113					// ideally could set other display options too, but no others seem to be supported by setOption()
16114				}
16115				else {
16116					// now that we know which cc module was loaded, unload it!
16117					// we don't want it if we're using local <track> elements for captions
16118					this.youTubePlayer.unloadModule(this.ytCaptionModule)
16119				}
16120			}
16121		}
16122		else {
16123			// no modules were loaded onApiChange
16124			// unfortunately, gonna have to disable captions if we can't control them
16125			this.hasCaptions = false;
16126			this.usingYouTubeCaptions = false;
16127		}
16128		this.refreshControls('captions');
16129	};
16130
16131	AblePlayer.prototype.getVimeoPosterUrl = function (youTubeId, width) {
16132
16133    // NOTE: This function is modeled after same function in youtube.js
16134    // in case useful for Vimeo, but is not currently used
16135
16136			 // return a URL for retrieving a YouTube poster image
16137			 // supported values of width: 120, 320, 480, 640
16138
16139			 var url = 'https://img.youtube.com/vi/' + youTubeId;
16140			 if (width == '120') {
16141				 // default (small) thumbnail, 120 x 90
16142				 return url + '/default.jpg';
16143			 }
16144			 else if (width == '320') {
16145				 // medium quality thumbnail, 320 x 180
16146				 return url + '/hqdefault.jpg';
16147			 }
16148			 else if (width == '480') {
16149				 // high quality thumbnail, 480 x 360
16150				 return url + '/hqdefault.jpg';
16151			 }
16152			 else if (width == '640') {
16153				 // standard definition poster image, 640 x 480
16154				 return url + '/sddefault.jpg';
16155			 }
16156			 return false;
16157	};
16158
16159})(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.