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 === '&') { 2707 result.push('&'); 2708 } 2709 else if (buffer === '<') { 2710 result.push('<'); 2711 } 2712 else if (buffer === '>') { 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 === ' ') { 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.