');
- b.text(browsers[i].name + ' ' + browsers[i].minVersion + ' ' + this.tt.orHigher);
- browserList.append(b);
+ this.hasFallback = false;
+ if (this.$media.children().length) {
+ i = 0;
+ while (i < this.$media.children().length && !this.hasFallback) {
+ if (!(this.$media.children()[i].tagName === 'SOURCE' ||
+ this.$media.children()[i].tagName === 'TRACK')) {
+ this.hasFallback = true;
+ }
+ i++;
}
- $fallbackDiv.append(browserList);
}
-
- // if there's a poster, show that as well
- this.injectPoster($fallbackDiv, 'fallback');
-
- // inject $fallbackDiv into the DOM and remove broken content
- if (typeof this.$ableWrapper !== 'undefined') {
- this.$ableWrapper.before($fallbackDiv);
- this.$ableWrapper.remove();
+ if (!this.hasFallback) {
+ $fallback = $('').text('Media player unavailable.');
+ this.$media.append($fallback);
}
- else if (typeof this.$media !== 'undefined') {
- this.$media.before($fallbackDiv);
- this.$media.remove();
+
+ if (this.$media.attr('width')) {
+ this.$media.css('width',this.$media.attr('width') + 'px');
}
- else {
- $('body').prepend($fallbackDiv);
+ if (this.$media.attr('height')) {
+ this.$media.css('height',this.$media.attr('height') + 'px');
}
- };
+ this.$media.removeAttr('data-able-player');
- AblePlayer.prototype.getSupportingBrowsers = function() {
+ this.$media.prop('controls',true);
- var browsers = [];
- browsers[0] = {
- name:'Chrome',
- minVersion: '31'
- };
- browsers[1] = {
- name:'Firefox',
- minVersion: '34'
- };
- browsers[2] = {
- name:'Internet Explorer',
- minVersion: '10'
- };
- browsers[3] = {
- name:'Opera',
- minVersion: '26'
- };
- browsers[4] = {
- name:'Safari for Mac OS X',
- minVersion: '7.1'
- };
- browsers[5] = {
- name:'Safari for iOS',
- minVersion: '7.1'
- };
- browsers[6] = {
- name:'Android Browser',
- minVersion: '4.1'
- };
- browsers[7] = {
- name:'Chrome for Android',
- minVersion: '40'
- };
- return browsers;
- }
+ if (this.testFallback == 2) {
+
+ $(this.$media).replaceWith($(''));
+ this.$newFallbackElement = $('#foobar-' + this.mediaId);
+
+ if (this.$media.children().length) {
+ i = this.$media.children().length - 1;
+ while (i >= 0) {
+ this.$newFallbackElement.prepend($(this.$media.children()[i]));
+ i--;
+ }
+ }
+ if (!this.hasFallback) {
+ this.$newFallbackElement.append($fallback);
+ }
+ }
+ return;
+ };
AblePlayer.prototype.calculateControlLayout = function () {
- // Calculates the layout for controls based on media and options.
- // Returns an array with 4 keys (for legacy skin) or 2 keys (for 2020 skin)
- // Keys are the following order:
- // 0 = Top left
- // 1 = Top right
- // 2 = Bottom left (legacy skin only)
- // 3 = Bottom right (legacy skin only)
- // Each key contains an array of control names to put in that section.
- var controlLayout, volumeSupported, playbackSupported, totalButtonWidth, numA11yButtons;
+ var controlLayout, playbackSupported, numA11yButtons;
controlLayout = [];
controlLayout[0] = [];
controlLayout[1] = [];
- if (this.skin === 'legacy') {
- controlLayout[2] = [];
- controlLayout[3] = [];
+ if (this.skin === 'legacy') {
+ controlLayout[2] = [];
+ controlLayout[3] = [];
}
controlLayout[0].push('play');
@@ -3903,120 +4426,106 @@ var AblePlayerInstances = [];
controlLayout[0].push('rewind');
controlLayout[0].push('forward');
- if (this.skin === 'legacy') {
- controlLayout[1].push('seek');
- }
+ if (this.skin === 'legacy') {
+ controlLayout[1].push('seek');
+ }
if (this.hasPlaylist) {
- if (this.skin === 'legacy') {
- controlLayout[0].push('previous');
- controlLayout[0].push('next');
- }
- else if (this.skin == '2020') {
- controlLayout[0].push('previous');
- controlLayout[0].push('next');
- }
+ if (this.skin === 'legacy') {
+ controlLayout[0].push('previous');
+ controlLayout[0].push('next');
+ } else {
+ controlLayout[0].push('previous');
+ controlLayout[0].push('next');
+ }
}
if (this.isPlaybackRateSupported()) {
- playbackSupported = true;
- if (this.skin === 'legacy') {
- controlLayout[2].push('slower');
- controlLayout[2].push('faster');
- }
- }
- else {
- playbackSupported = false;
+ playbackSupported = true;
+ if (this.skin === 'legacy') {
+ controlLayout[2].push('slower');
+ controlLayout[2].push('faster');
+ }
+ } else {
+ playbackSupported = false;
}
- if (this.mediaType === 'video') {
- numA11yButtons = 0;
- if (this.hasCaptions) {
- numA11yButtons++;
- if (this.skin === 'legacy') {
- controlLayout[2].push('captions');
- }
- else if (this.skin == '2020') {
- controlLayout[1].push('captions');
- }
+ numA11yButtons = 0;
+ if (this.hasCaptions) {
+ numA11yButtons++;
+ if (this.skin === 'legacy') {
+ controlLayout[2].push('captions');
+ } else {
+ controlLayout[1].push('captions');
}
- if (this.hasSignLanguage) {
- numA11yButtons++;
- if (this.skin === 'legacy') {
- controlLayout[2].push('sign');
- }
- else if (this.skin == '2020') {
- controlLayout[1].push('sign');
- }
+ }
+ if (this.hasSignLanguage) {
+ numA11yButtons++;
+ if (this.skin === 'legacy') {
+ controlLayout[2].push('sign');
+ } else {
+ controlLayout[1].push('sign');
}
- if ((this.hasOpenDesc || this.hasClosedDesc) && (this.useDescriptionsButton)) {
- numA11yButtons++;
- if (this.skin === 'legacy') {
- controlLayout[2].push('descriptions');
+ }
+ if (this.mediaType === 'video') {
+ if (this.hasOpenDesc || this.hasClosedDesc) {
+ numA11yButtons++;
+ if (this.skin === 'legacy') {
+ controlLayout[2].push('descriptions');
+ } else {
+ controlLayout[1].push('descriptions');
}
- else if (this.skin == '2020') {
- controlLayout[1].push('descriptions');
- }
}
}
- if (this.transcriptType === 'popup' && !(this.hideTranscriptButton)) {
- numA11yButtons++;
- if (this.skin === 'legacy') {
+ if (this.transcriptType !== null && !(this.hideTranscriptButton)) {
+ numA11yButtons++;
+ if (this.skin === 'legacy') {
controlLayout[2].push('transcript');
- }
- else if (this.skin == '2020') {
- controlLayout[1].push('transcript');
- }
+ } else {
+ controlLayout[1].push('transcript');
+ }
}
-
- if (this.mediaType === 'video' && this.hasChapters && this.useChaptersButton) {
- numA11yButtons++;
- if (this.skin === 'legacy') {
+ if (this.hasChapters && this.useChaptersButton) {
+ numA11yButtons++;
+ if (this.skin === 'legacy') {
controlLayout[2].push('chapters');
- }
- else if (this.skin == '2020') {
- controlLayout[1].push('chapters');
- }
+ } else {
+ controlLayout[1].push('chapters');
+ }
}
- if (this.skin == '2020' && numA11yButtons > 0) {
- controlLayout[1].push('pipe');
+ if (this.skin == '2020' && numA11yButtons > 0) {
+ controlLayout[1].push('pipe');
}
- if (playbackSupported && this.skin === '2020') {
- controlLayout[1].push('faster');
- controlLayout[1].push('slower');
- controlLayout[1].push('pipe');
+ if (playbackSupported && this.skin === '2020') {
+ controlLayout[1].push('faster');
+ controlLayout[1].push('slower');
+ controlLayout[1].push('pipe');
}
- if (this.skin === 'legacy') {
- controlLayout[3].push('preferences');
- }
- else if (this.skin == '2020') {
- controlLayout[1].push('preferences');
- }
+ if (this.skin === 'legacy') {
+ controlLayout[3].push('preferences');
+ } else {
+ controlLayout[1].push('preferences');
+ }
- if (this.mediaType === 'video' && this.allowFullScreen) {
- if (this.skin === 'legacy') {
- controlLayout[3].push('fullscreen');
- }
- else {
- controlLayout[1].push('fullscreen');
- }
+ if (this.mediaType === 'video' && this.allowFullscreen && this.nativeFullscreenSupported() ) {
+ if (this.skin === 'legacy') {
+ controlLayout[3].push('fullscreen');
+ } else {
+ controlLayout[1].push('fullscreen');
+ }
}
if (this.browserSupportsVolume()) {
- volumeSupported = true; // defined in case we decide to move volume button elsewhere
this.volumeButton = 'volume-' + this.getVolumeName(this.volume);
if (this.skin === 'legacy') {
- controlLayout[1].push('volume');
- }
- else if (this.skin == '2020') {
- controlLayout[1].push('volume');
- }
- }
- else {
- volumeSupported = false;
+ controlLayout[1].push('volume');
+ } else {
+ controlLayout[1].push('volume');
+ }
+ } else {
this.volume = false;
}
return controlLayout;
@@ -4024,30 +4533,20 @@ var AblePlayerInstances = [];
AblePlayer.prototype.addControls = function() {
- // determine which controls to show based on several factors:
- // mediaType (audio vs video)
- // availability of tracks (e.g., for closed captions & audio description)
- // browser support (e.g., for sliders and speedButtons)
- // user preferences (???)
- // some controls are aligned on the left, and others on the right
var thisObj, baseSliderWidth, controlLayout, numSections,
- i, j, k, controls, $controllerSpan, $sliderDiv, sliderLabel, $pipe, $pipeImg,
- svgData, svgPath, control,
- $buttonLabel, $buttonImg, buttonImgSrc, buttonTitle, $newButton, iconClass, buttonIcon,
- buttonUse, buttonText, position, buttonHeight, buttonWidth, buttonSide, controllerWidth,
- tooltipId, tooltipY, tooltipX, tooltipWidth, tooltipStyle, tooltip,
- captionLabel, popupMenuId;
+ i, j, controls, $controllerSpan, $sliderDiv, sliderLabel, $pipe, control,
+ buttonTitle, $newButton, buttonText, position, buttonHeight,
+ buttonWidth, buttonSide, controllerWidth, tooltipId, tooltipY, tooltipX,
+ tooltipWidth, tooltipStyle, tooltip, tooltipTimerId, captionLabel, popupMenuId;
thisObj = this;
- baseSliderWidth = 100; // arbitrary value, will be recalculated in refreshControls()
+ baseSliderWidth = 100;
- // Initialize the layout into the this.controlLayout variable.
controlLayout = this.calculateControlLayout();
numSections = controlLayout.length;
- // add an empty div to serve as a tooltip
tooltipId = this.mediaId + '-tooltip';
this.$tooltipDiv = $('',{
'id': tooltipId,
@@ -4056,398 +4555,223 @@ var AblePlayerInstances = [];
this.$controllerDiv.append(this.$tooltipDiv);
if (this.skin == '2020') {
- // add a full-width seek bar
- $sliderDiv = $('
');
- sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel;
+ $sliderDiv = $('
');
+ sliderLabel = this.mediaType + ' ' + this.translate( 'seekbarLabel', 'timeline' );
this.$controllerDiv.append($sliderDiv);
- this.seekBar = new AccessibleSlider(this.mediaType, $sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
+ this.seekBar = new AccessibleSlider($sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
}
- // step separately through left and right controls
+ let $controlRow = $('
');
+ this.$controllerDiv.append($controlRow);
+
for (i = 0; i < numSections; i++) {
controls = controlLayout[i];
- if ((i % 2) === 0) { // even keys on the left
+ if ((i % 2) === 0) {
$controllerSpan = $('
',{
'class': 'able-left-controls'
});
- }
- else { // odd keys on the right
+ } else {
$controllerSpan = $('
',{
'class': 'able-right-controls'
});
}
- this.$controllerDiv.append($controllerSpan);
+ $controlRow.append($controllerSpan);
+
for (j=0; j
');
- sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel;
+ sliderLabel = this.mediaType + ' ' + this.translate( 'seekbarLabel', 'timeline' );
$controllerSpan.append($sliderDiv);
if (typeof this.duration === 'undefined' || this.duration === 0) {
- // set arbitrary starting duration, and change it when duration is known
this.duration = 60;
- // also set elapsed to 0
this.elapsed = 0;
}
- this.seekBar = new AccessibleSlider(this.mediaType, $sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
- }
- else if (control === 'pipe') {
+ this.seekBar = new AccessibleSlider($sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
+ } else if (control === 'pipe') {
$pipe = $('
', {
'tabindex': '-1',
- 'aria-hidden': 'true'
+ 'aria-hidden': 'true',
+ 'class': 'able-pipe',
});
- if (this.iconType === 'font') {
- $pipe.addClass('icon-pipe');
- }
- else {
- $pipeImg = $(' ', {
- src: this.rootPath + 'button-icons/' + this.iconColor + '/pipe.png',
- alt: '',
- role: 'presentation'
- });
- $pipe.append($pipeImg);
- }
+ $pipe.append('|');
$controllerSpan.append($pipe);
- }
- else {
- // this control is a button
- if (control === 'volume') {
- buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + this.volumeButton + '.png';
- }
- else if (control === 'fullscreen') {
- buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/fullscreen-expand.png';
- }
- else if (control === 'slower') {
- if (this.speedIcons === 'animals') {
- buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/turtle.png';
- }
- else {
- buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/slower.png';
- }
- }
- else if (control === 'faster') {
- if (this.speedIcons === 'animals') {
- buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/rabbit.png';
- }
- else {
- buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/faster.png';
- }
- }
- else {
- buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + control + '.png';
- }
+ } else {
buttonTitle = this.getButtonTitle(control);
- // icomoon documentation recommends the following markup for screen readers:
- // 1. link element (or in our case, button). Nested inside this element:
- // 2. span that contains the icon font (in our case, buttonIcon)
- // 3. span that contains a visually hidden label for screen readers (buttonLabel)
- // In addition, we are adding aria-label to the button (but not title)
- // And if iconType === 'image', we are replacing #2 with an image (with alt="" and role="presentation")
- // This has been thoroughly tested and works well in all screen reader/browser combinations
- // See https://github.com/ableplayer/ableplayer/issues/81
-
- // NOTE: Changed from to elements are rendered poorly in high contrast mode
- // in some OS/browser/plugin combinations
+
$newButton = $('
',{
'role': 'button',
'tabindex': '0',
- 'aria-label': buttonTitle,
'class': 'able-button-handler-' + control
});
- if (control === 'volume' || control === 'preferences') {
+ if (control === 'volume' || control === 'preferences' || control === 'captions') {
if (control == 'preferences') {
- this.prefCats = this.getPreferencesGroups();
- if (this.prefCats.length > 1) {
- // Prefs button will trigger a menu
- popupMenuId = this.mediaId + '-prefs-menu';
- $newButton.attr({
- 'aria-controls': popupMenuId,
- 'aria-haspopup': 'menu'
- });
- }
- else if (this.prefCats.length === 1) {
- // Prefs button will trigger a dialog
- $newButton.attr({
- 'aria-haspopup': 'dialog'
- });
- }
- }
- else if (control === 'volume') {
- popupMenuId = this.mediaId + '-volume-slider';
- // volume slider popup is not a menu or a dialog
- // therefore, using aria-expanded rather than aria-haspopup to communicate properties/state
- $newButton.attr({
- 'aria-controls': popupMenuId,
- 'aria-expanded': 'false'
- });
- }
- }
- if (this.iconType === 'font') {
- if (control === 'volume') {
- iconClass = 'icon-' + this.volumeButton;
- }
- else if (control === 'slower') {
- if (this.speedIcons === 'animals') {
- iconClass = 'icon-turtle';
- }
- else {
- iconClass = 'icon-slower';
+ this.prefCats = this.getPreferencesGroups();
+ if (this.prefCats.length > 1) {
+ popupMenuId = this.mediaId + '-prefs-menu';
+ $newButton.attr({
+ 'aria-controls': popupMenuId,
+ 'aria-haspopup': 'menu',
+ 'aria-expanded': 'false'
+ });
+ } else if (this.prefCats.length === 1) {
+ $newButton.attr({
+ 'aria-haspopup': 'dialog'
+ });
}
- }
- else if (control === 'faster') {
- if (this.speedIcons === 'animals') {
- iconClass = 'icon-rabbit';
- }
- else {
- iconClass = 'icon-faster';
+ } else if (control === 'volume') {
+ popupMenuId = this.mediaId + '-volume-slider';
+ $newButton.attr({
+ 'aria-controls': popupMenuId,
+ 'aria-expanded': 'false'
+ });
+ } else if (control === 'captions' && this.captions) {
+ if (this.captions.length > 1) {
+ $newButton.attr('aria-expanded', 'false')
+ } else {
+ $newButton.attr('aria-pressed', 'false')
}
}
- else {
- iconClass = 'icon-' + control;
- }
- buttonIcon = $('
',{
- 'class': iconClass,
- 'aria-hidden': 'true'
- });
- $newButton.append(buttonIcon);
}
- else if (this.iconType === 'svg') {
-
- /*
- // Unused option for adding SVG:
- // Use element to link to button-icons/able-icons.svg
- // Advantage: SVG file can be cached
- // Disadvantage: Not supported by Safari 6, IE 6-11, or Edge 12
- // Instead, adding element within each
- if (control === 'volume') {
- iconClass = 'svg-' + this.volumeButton;
- }
- else if (control === 'fullscreen') {
- iconClass = 'svg-fullscreen-expand';
- }
- else if (control === 'slower') {
- if (this.speedIcons === 'animals') {
- iconClass = 'svg-turtle';
- }
- else {
- iconClass = 'svg-slower';
- }
- }
- else if (control === 'faster') {
- if (this.speedIcons === 'animals') {
- iconClass = 'svg-rabbit';
- }
- else {
- iconClass = 'svg-faster';
- }
- }
- else {
- iconClass = 'svg-' + control;
- }
- buttonIcon = $('',{
- 'class': iconClass
- });
- buttonUse = $('',{
- 'xlink:href': this.rootPath + 'button-icons/able-icons.svg#' + iconClass
- });
- buttonIcon.append(buttonUse);
- */
- var svgData;
- if (control === 'volume') {
- svgData = this.getSvgData(this.volumeButton);
- }
- else if (control === 'fullscreen') {
- svgData = this.getSvgData('fullscreen-expand');
- }
- else if (control === 'slower') {
- if (this.speedIcons === 'animals') {
- svgData = this.getSvgData('turtle');
- }
- else {
- svgData = this.getSvgData('slower');
- }
- }
- else if (control === 'faster') {
- if (this.speedIcons === 'animals') {
- svgData = this.getSvgData('rabbit');
- }
- else {
- svgData = this.getSvgData('faster');
- }
- }
- else {
- svgData = this.getSvgData(control);
- }
- buttonIcon = $('',{
- 'focusable': 'false',
- 'aria-hidden': 'true',
- 'viewBox': svgData[0]
- });
- svgPath = $('',{
- 'd': svgData[1]
- });
- buttonIcon.append(svgPath);
- $newButton.html(buttonIcon);
-
- // Final step: Need to refresh the DOM in order for browser to process & display the SVG
- $newButton.html($newButton.html());
+ var getControl = control;
+ if ( control === 'faster' && this.speedIcons === 'animals' ) {
+ getControl = 'rabbit';
}
- else {
- // use images
- $buttonImg = $(' ',{
- 'src': buttonImgSrc,
- 'alt': '',
- 'role': 'presentation'
- });
- $newButton.append($buttonImg);
+ if ( control === 'slower' && this.speedIcons === 'animals' ) {
+ getControl = 'turtle';
+ }
+ if ( control === 'volume' ) {
+ this.getIcon( $newButton, this.volumeButton );
+ } else {
+ if ( 'fullscreen' === getControl ) {
+ getControl = ( this.fullscreen ) ? 'fullscreen-collapse' : 'fullscreen-expand';
+ }
+ this.getIcon( $newButton, getControl );
}
- // add the visibly-hidden label for screen readers that don't support aria-label on the button
- var $buttonLabel = $('',{
- 'class': 'able-clipped'
- }).text(buttonTitle);
- $newButton.append($buttonLabel);
- // add an event listener that displays a tooltip on mouseenter or focus
+
+ this.setText($newButton,buttonTitle);
$newButton.on('mouseenter focus',function(e) {
- var buttonText = $(this).attr('aria-label');
- // get position of this button
- var position = $(this).position();
- var buttonHeight = $(this).height();
- var buttonWidth = $(this).width();
- // position() is expressed using top and left (of button);
- // add right (of button) too, for convenience
- var controllerWidth = thisObj.$controllerDiv.width();
+
+ clearTimeout(tooltipTimerId);
+
+ buttonText = $(this).attr('aria-label');
+ position = $(this).position();
+ buttonHeight = $(this).height();
+ buttonWidth = $(this).width();
+ controllerWidth = thisObj.$controllerDiv.width();
position.right = controllerWidth - position.left - buttonWidth;
- var tooltipY = position.top - buttonHeight - 15;
+
+ tooltipY = position.top + buttonHeight + 5;
if ($(this).parent().hasClass('able-right-controls')) {
- // this control is on the right side
- var buttonSide = 'right';
- }
- else {
- // this control is on the left side
- var buttonSide = 'left';
+ buttonSide = 'right';
+ } else {
+ buttonSide = 'left';
}
- // populate tooltip, then calculate its width before showing it
- var tooltipWidth = AblePlayer.localGetElementById($newButton[0], tooltipId).text(buttonText).width();
- // center the tooltip horizontally over the button
- if (buttonSide == 'left') {
- var tooltipX = position.left - tooltipWidth/2;
- if (tooltipX < 0) {
- // tooltip would exceed the bounds of the player. Adjust.
- tooltipX = 2;
- }
- var tooltipStyle = {
- left: tooltipX + 'px',
+ tooltipWidth = AblePlayer.localGetElementById($newButton[0], tooltipId).text(buttonText).width();
+ if (buttonSide == 'left') {
+ tooltipX = position.left - tooltipWidth/2;
+ if (tooltipX < 0) {
+ tooltipX = 2;
+ }
+ tooltipStyle = {
+ left: tooltipX + 'px',
right: '',
top: tooltipY + 'px'
- };
- }
- else {
- var tooltipX = position.right - tooltipWidth/2;
- if (tooltipX < 0) {
- // tooltip would exceed the bounds of the player. Adjust.
- tooltipX = 2;
- }
- var tooltipStyle = {
+ };
+ } else {
+ tooltipX = position.right - tooltipWidth/2;
+ if (tooltipX < 0) {
+ tooltipX = 2;
+ }
+ tooltipStyle = {
left: '',
right: tooltipX + 'px',
top: tooltipY + 'px'
- };
- }
- var tooltip = AblePlayer.localGetElementById($newButton[0], tooltipId).text(buttonText).css(tooltipStyle);
+ };
+ }
+ tooltip = AblePlayer.localGetElementById($newButton[0], tooltipId).text(buttonText).css(tooltipStyle);
thisObj.showTooltip(tooltip);
$(this).on('mouseleave blur',function() {
- AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
- })
+
+
+ clearTimeout(tooltipTimerId);
+ tooltipTimerId = setTimeout(function() {
+ AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
+ }, 500);
+
+ thisObj.$tooltipDiv.on('mouseenter focus', function() {
+ clearTimeout(tooltipTimerId);
+ });
+
+ thisObj.$tooltipDiv.on('mouseleave blur', function() {
+ AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
+ });
+
+ });
});
if (control === 'captions') {
if (!this.prefCaptions || this.prefCaptions !== 1) {
- // captions are available, but user has them turned off
if (this.captions.length > 1) {
- captionLabel = this.tt.captions;
- }
- else {
- captionLabel = this.tt.showCaptions;
+ captionLabel = this.translate( 'captions', 'Captions' );
+ } else {
+ captionLabel = this.translate( 'showCaptions', 'Show captions' );
}
$newButton.addClass('buttonOff').attr('title',captionLabel);
+ $newButton.attr('aria-pressed', 'false');
}
- }
- else if (control === 'descriptions') {
+ } else if (control === 'descriptions') {
if (!this.prefDesc || this.prefDesc !== 1) {
- // user prefer non-audio described version
- // Therefore, load media without description
- // Description can be toggled on later with this button
- $newButton.addClass('buttonOff').attr('title',this.tt.turnOnDescriptions);
+ $newButton.addClass('buttonOff').attr( 'title', this.translate( 'turnOnDescriptions', 'Turn on descriptions' ) );
}
}
$controllerSpan.append($newButton);
- // create variables of buttons that are referenced throughout the AblePlayer object
if (control === 'play') {
this.$playpauseButton = $newButton;
- }
- else if (control == 'previous') {
- this.$prevButton = $newButton;
- // if player is being rebuilt because user clicked the Prev button
- // return focus to that (newly built) button
- if (this.buttonWithFocus == 'previous') {
- this.$prevButton.focus();
- this.buttonWithFocus = null;
- }
- }
- else if (control == 'next') {
- this.$nextButton = $newButton;
- // if player is being rebuilt because user clicked the Next button
- // return focus to that (newly built) button
- if (this.buttonWithFocus == 'next') {
- this.$nextButton.focus();
- this.buttonWithFocus = null;
- }
- }
- else if (control === 'captions') {
+ } else if (control == 'previous') {
+ this.$prevButton = $newButton;
+ if (this.buttonWithFocus == 'previous') {
+ this.$prevButton.trigger('focus');
+ this.buttonWithFocus = null;
+ }
+ } else if (control == 'next') {
+ this.$nextButton = $newButton;
+ if (this.buttonWithFocus == 'next') {
+ this.$nextButton.trigger('focus');
+ this.buttonWithFocus = null;
+ }
+ } else if (control === 'captions') {
this.$ccButton = $newButton;
- }
- else if (control === 'sign') {
+ } else if (control === 'sign') {
this.$signButton = $newButton;
- // gray out sign button if sign language window is not active
if (!(this.$signWindow.is(':visible'))) {
this.$signButton.addClass('buttonOff');
}
- }
- else if (control === 'descriptions') {
+ } else if (control === 'descriptions') {
this.$descButton = $newButton;
- // button will be enabled or disabled in description.js > initDescription()
- }
- else if (control === 'mute') {
+ } else if (control === 'mute') {
this.$muteButton = $newButton;
- }
- else if (control === 'transcript') {
+ } else if (control === 'transcript') {
this.$transcriptButton = $newButton;
- // gray out transcript button if transcript is not active
if (!(this.$transcriptDiv.is(':visible'))) {
- this.$transcriptButton.addClass('buttonOff').attr('title',this.tt.showTranscript);
+ this.$transcriptButton.addClass('buttonOff').attr( 'title', this.translate( 'showTranscript', 'Show transcript' ) );
}
- }
- else if (control === 'fullscreen') {
+ } else if (control === 'fullscreen') {
this.$fullscreenButton = $newButton;
- }
- else if (control === 'chapters') {
+ } else if (control === 'chapters') {
this.$chaptersButton = $newButton;
- }
- else if (control === 'preferences') {
+ } else if (control === 'preferences') {
this.$prefsButton = $newButton;
- }
- else if (control === 'volume') {
+ } else if (control === 'volume') {
this.$volumeButton = $newButton;
}
}
if (control === 'volume') {
- // in addition to the volume button, add a hidden slider
this.addVolumeSlider($controllerSpan);
}
}
@@ -4456,288 +4780,83 @@ var AblePlayerInstances = [];
}
}
- if (this.mediaType === 'video') {
-
- if (typeof this.$captionsDiv !== 'undefined') {
- // stylize captions based on user prefs
- this.stylizeCaptions(this.$captionsDiv);
- }
- if (typeof this.$descDiv !== 'undefined') {
- // stylize descriptions based on user's caption prefs
- this.stylizeCaptions(this.$descDiv);
- }
+ if (typeof this.$captionsDiv !== 'undefined') {
+ this.stylizeCaptions(this.$captionsDiv);
+ }
+ if (typeof this.$descDiv !== 'undefined') {
+ this.stylizeCaptions(this.$descDiv);
}
- // combine left and right controls arrays for future reference
this.controls = [];
for (var sec in controlLayout) if (controlLayout.hasOwnProperty(sec)) {
this.controls = this.controls.concat(controlLayout[sec]);
}
- // Update state-based display of controls.
- this.refreshControls('init');
- };
-
- AblePlayer.prototype.useSvg = function () {
-
- // Modified from IcoMoon.io svgxuse
- // @copyright Copyright (c) 2016 IcoMoon.io
- // @license Licensed under MIT license
- // See https://github.com/Keyamoon/svgxuse
- // @version 1.1.16
-
- var cache = Object.create(null); // holds xhr objects to prevent multiple requests
- var checkUseElems,
- tid; // timeout id
- var debouncedCheck = function () {
- clearTimeout(tid);
- tid = setTimeout(checkUseElems, 100);
- };
- var unobserveChanges = function () {
- return;
- };
- var observeChanges = function () {
- var observer;
- window.addEventListener('resize', debouncedCheck, false);
- window.addEventListener('orientationchange', debouncedCheck, false);
- if (window.MutationObserver) {
- observer = new MutationObserver(debouncedCheck);
- observer.observe(document.documentElement, {
- childList: true,
- subtree: true,
- attributes: true
- });
- unobserveChanges = function () {
- try {
- observer.disconnect();
- window.removeEventListener('resize', debouncedCheck, false);
- window.removeEventListener('orientationchange', debouncedCheck, false);
- } catch (ignore) {}
- };
- }
- else {
- document.documentElement.addEventListener('DOMSubtreeModified', debouncedCheck, false);
- unobserveChanges = function () {
- document.documentElement.removeEventListener('DOMSubtreeModified', debouncedCheck, false);
- window.removeEventListener('resize', debouncedCheck, false);
- window.removeEventListener('orientationchange', debouncedCheck, false);
- };
- }
- };
- var xlinkNS = 'http://www.w3.org/1999/xlink';
- checkUseElems = function () {
- var base,
- bcr,
- fallback = '', // optional fallback URL in case no base path to SVG file was given and no symbol definition was found.
- hash,
- i,
- Request,
- inProgressCount = 0,
- isHidden,
- url,
- uses,
- xhr;
- if (window.XMLHttpRequest) {
- Request = new XMLHttpRequest();
- if (Request.withCredentials !== undefined) {
- Request = XMLHttpRequest;
- }
- else {
- Request = XDomainRequest || undefined;
- }
- }
- if (Request === undefined) {
- return;
- }
- function observeIfDone() {
- // If done with making changes, start watching for chagnes in DOM again
- inProgressCount -= 1;
- if (inProgressCount === 0) { // if all xhrs were resolved
- observeChanges(); // watch for changes to DOM
- }
- }
- function attrUpdateFunc(spec) {
- return function () {
- if (cache[spec.base] !== true) {
- spec.useEl.setAttributeNS(xlinkNS, 'xlink:href', '#' + spec.hash);
- }
- };
- }
- function onloadFunc(xhr) {
- return function () {
- var body = document.body;
- var x = document.createElement('x');
- var svg;
- xhr.onload = null;
- x.innerHTML = xhr.responseText;
- svg = x.getElementsByTagName('svg')[0];
- if (svg) {
- svg.setAttribute('aria-hidden', 'true');
- svg.style.position = 'absolute';
- svg.style.width = 0;
- svg.style.height = 0;
- svg.style.overflow = 'hidden';
- body.insertBefore(svg, body.firstChild);
- }
- observeIfDone();
- };
- }
- function onErrorTimeout(xhr) {
- return function () {
- xhr.onerror = null;
- xhr.ontimeout = null;
- observeIfDone();
- };
- }
- unobserveChanges(); // stop watching for changes to DOM
- // find all use elements
- uses = document.getElementsByTagName('use');
- for (i = 0; i < uses.length; i += 1) {
- try {
- bcr = uses[i].getBoundingClientRect();
- } catch (ignore) {
- // failed to get bounding rectangle of the use element
- bcr = false;
- }
- url = uses[i].getAttributeNS(xlinkNS, 'href').split('#');
- base = url[0];
- hash = url[1];
- isHidden = bcr && bcr.left === 0 && bcr.right === 0 && bcr.top === 0 && bcr.bottom === 0;
- if (bcr && bcr.width === 0 && bcr.height === 0 && !isHidden) {
- // the use element is empty
- // if there is a reference to an external SVG, try to fetch it
- // use the optional fallback URL if there is no reference to an external SVG
- if (fallback && !base.length && hash && !document.getElementById(hash)) {
- base = fallback;
- }
- if (base.length) {
- // schedule updating xlink:href
- xhr = cache[base];
- if (xhr !== true) {
- // true signifies that prepending the SVG was not required
- setTimeout(attrUpdateFunc({
- useEl: uses[i],
- base: base,
- hash: hash
- }), 0);
- }
- if (xhr === undefined) {
- xhr = new Request();
- cache[base] = xhr;
- xhr.onload = onloadFunc(xhr);
- xhr.onerror = onErrorTimeout(xhr);
- xhr.ontimeout = onErrorTimeout(xhr);
- xhr.open('GET', base);
- xhr.send();
- inProgressCount += 1;
- }
- }
- }
- else {
- if (!isHidden) {
- if (cache[base] === undefined) {
- // remember this URL if the use element was not empty and no request was sent
- cache[base] = true;
- }
- else if (cache[base].onload) {
- // if it turns out that prepending the SVG is not necessary,
- // abort the in-progress xhr.
- cache[base].abort();
- cache[base].onload = undefined;
- cache[base] = true;
- }
- }
- }
- }
- uses = '';
- inProgressCount += 1;
- observeIfDone();
- };
-/*
- // The load event fires when all resources have finished loading, which allows detecting whether SVG use elements are empty.
- window.addEventListener('load', function winLoad() {
- window.removeEventListener('load', winLoad, false); // to prevent memory leaks
- tid = setTimeout(checkUseElems, 0);
- }, false);
-*/
+ this.refreshControls();
};
AblePlayer.prototype.cuePlaylistItem = function(sourceIndex) {
- // Move to a new item in a playlist.
- // NOTE: Swapping source for audio description is handled elsewhere;
- // see description.js > swapDescription()
-
- /*
- // Decided against preventing a reload of the current item in the playlist.
- // If it's clickable, users should be able to click on it and expect something to happen.
- // Leaving here though in case it's determined to be desirable.
- if (sourceIndex === this.playlistItemIndex) {
- // user has requested the item that's currently playing
- // just ignore the request
- return;
- }
- this.playlistItemIndex = sourceIndex;
- */
- var $newItem, prevPlayer, newPlayer, itemTitle, itemLang, sources, s, i, $newSource, nowPlayingSpan;
+ var $newItem, prevPlayer, newPlayer, itemTitle, itemLang, nowPlayingSpan;
var thisObj = this;
prevPlayer = this.player;
- if (this.initializing) { // this is the first track - user hasn't pressed play yet
- // do nothing.
- }
- else {
- if (this.playerCreated) {
- // remove the old
- this.deletePlayer();
+ if (this.initializing) {
+ } else {
+ if (this.playerCreated) {
+ this.deletePlayer('playlist');
}
}
- // Determine appropriate player to play this media
+ this.swappingSrc = true;
+
+ if (this.startedPlaying) {
+ this.okToPlay = true;
+ } else {
+ this.okToPlay = false;
+ }
+
+ this.loadingMedia = false;
+
$newItem = this.$playlist.eq(sourceIndex);
if (this.hasAttr($newItem,'data-youtube-id')) {
- this.youTubeId = $newItem.attr('data-youtube-id');
+ this.youTubeId = this.getYouTubeId($newItem.attr('data-youtube-id'));
+ if (this.hasAttr($newItem,'data-youtube-desc-id')) {
+ this.youTubeDescId = this.getYouTubeId($newItem.attr('data-youtube-desc-id'));
+ }
newPlayer = 'youtube';
+ } else if (this.hasAttr($newItem,'data-vimeo-id')) {
+ this.vimeoId = this.getVimeoId($newItem.attr('data-vimeo-id'));
+ if (this.hasAttr($newItem,'data-vimeo-desc-id')) {
+ this.vimeoDescId = this.getVimeoId($newItem.attr('data-vimeo-desc-id'));
+ }
+ newPlayer = 'vimeo';
+ } else {
+ newPlayer = 'html5';
}
- else {
- newPlayer = 'html5';
- }
-
if (newPlayer === 'youtube') {
if (prevPlayer === 'html5') {
- // pause and hide the previous media
if (this.playing) {
this.pauseMedia();
}
this.$media.hide();
}
- }
- else {
- // the new player is not youtube
+ } else {
this.youTubeId = false;
if (prevPlayer === 'youtube') {
- // unhide the media element
this.$media.show();
}
}
this.player = newPlayer;
- // set swappingSrc; needs to be true within recreatePlayer(), called below
- this.swappingSrc = true;
+ this.$media.empty();
- // transfer media attributes from playlist to media element
if (this.hasAttr($newItem,'data-poster')) {
this.$media.attr('poster',$newItem.attr('data-poster'));
}
- if (this.hasAttr($newItem,'data-width')) {
- this.$media.attr('width',$newItem.attr('data-width'));
- }
- if (this.hasAttr($newItem,'data-height')) {
- this.$media.attr('height',$newItem.attr('data-height'));
- }
if (this.hasAttr($newItem,'data-youtube-desc-id')) {
this.$media.attr('data-youtube-desc-id',$newItem.attr('data-youtube-desc-id'));
}
@@ -4745,111 +4864,130 @@ var AblePlayerInstances = [];
this.$media.attr('data-youtube-id',$newItem.attr('data-youtube-id'));
}
- // add new elements from playlist data
var $sourceSpans = $newItem.children('span.able-source');
if ($sourceSpans.length) {
$sourceSpans.each(function() {
- if (thisObj.hasAttr($(this),'data-src')) {
- // this is the only required attribute
- var $newSource = $('',{
- 'src': $(this).attr('data-src')
- });
- if (thisObj.hasAttr($(this),'data-type')) {
- $newSource.attr('type',$(this).attr('data-type'));
- }
- if (thisObj.hasAttr($(this),'data-desc-src')) {
- $newSource.attr('data-desc-src',$(this).attr('data-desc-src'));
- }
- if (thisObj.hasAttr($(this),'data-sign-src')) {
- $newSource.attr('data-sign-src',$(this).attr('data-sign-src'));
+ const $this = $(this);
+
+ if (thisObj.hasAttr($this, "data-src")) {
+ const sanitizedSrc = DOMPurify.sanitize($this.attr("data-src"));
+
+ if (validate.isProtocolSafe(sanitizedSrc)) {
+ const $newSource = $("", { src: sanitizedSrc });
+
+ const optionalAttributes = [
+ "data-type",
+ "data-desc-src",
+ "data-sign-src",
+ ];
+
+ optionalAttributes.forEach((attr) => {
+ if (thisObj.hasAttr($this, attr)) {
+ const attrValue = $this.attr(attr);
+ const sanitizedValue = DOMPurify.sanitize(attrValue);
+
+ if (attr.endsWith("-src") && validate.isProtocolSafe(sanitizedValue)) {
+ $newSource.attr(attr, sanitizedValue);
+ } else if (!attr.endsWith("-src")) {
+ $newSource.attr(attr, sanitizedValue);
+ }
+ }
+ });
+
+ thisObj.$media.append($newSource);
}
- thisObj.$media.append($newSource);
}
});
}
- // add new elements from playlist data
var $trackSpans = $newItem.children('span.able-track');
if ($trackSpans.length) {
- // for each element in $trackSpans, create a new element
$trackSpans.each(function() {
- if (thisObj.hasAttr($(this),'data-src') &&
- thisObj.hasAttr($(this),'data-kind') &&
- thisObj.hasAttr($(this),'data-srclang')) {
- // all required attributes are present
- var $newTrack = $('',{
- 'src': $(this).attr('data-src'),
- 'kind': $(this).attr('data-kind'),
- 'srclang': $(this).attr('data-srclang')
- });
- if (thisObj.hasAttr($(this),'data-label')) {
- $newTrack.attr('label',$(this).attr('data-label'));
+ const $this = $(this);
+ if (thisObj.hasAttr($this, "data-src") && thisObj.hasAttr($this, "data-kind") && thisObj.hasAttr($this, "data-srclang")) {
+ const sanitizedSrc = DOMPurify.sanitize($this.attr("data-src"));
+ if (validate.isProtocolSafe(sanitizedSrc)) {
+ const $newTrack = $("", {
+ src: sanitizedSrc,
+ kind: $this.attr("data-kind"),
+ srclang: $this.attr("data-srclang"),
+ });
+ const optionalAttributes = [
+ "data-label",
+ "data-desc",
+ "data-default",
+ ];
+ optionalAttributes.forEach((attr) => {
+ if (thisObj.hasAttr($this, attr)) {
+ $newTrack.attr(attr, DOMPurify.sanitize($this.attr(attr)));
+ }
+ });
+ thisObj.$media.append($newTrack);
}
- thisObj.$media.append($newTrack);
}
});
}
- itemTitle = $newItem.text();
+ itemTitle = DOMPurify.sanitize( $newItem.text() );
if (this.hasAttr($newItem,'lang')) {
itemLang = $newItem.attr('lang');
}
- // Update relevant arrays
this.$sources = this.$media.find('source');
- // recreate player, informed by new attributes and track elements
- this.recreatePlayer();
+ if (this.recreatingPlayer) {
+ return;
+ }
+ this.recreatePlayer().then(function() {
- // update playlist to indicate which item is playing
- //$('.able-playlist li').removeClass('able-current');
- this.$playlist.removeClass('able-current');
- this.$playlist.eq(sourceIndex).addClass('able-current');
+ thisObj.$playlist.removeClass('able-current')
+ .children('button').removeAttr('aria-current');
+ thisObj.$playlist.eq(sourceIndex).addClass('able-current')
+ .children('button').attr('aria-current','true');
- // update Now Playing div
- if (this.showNowPlaying === true) {
- if (typeof this.$nowPlayingDiv !== 'undefined') {
- nowPlayingSpan = $('');
- if (typeof itemLang !== 'undefined') {
- nowPlayingSpan.attr('lang',itemLang);
+ if (thisObj.showNowPlaying === true) {
+ if (typeof thisObj.$nowPlayingDiv !== 'undefined') {
+ nowPlayingSpan = $('');
+ if (typeof itemLang !== 'undefined') {
+ nowPlayingSpan.attr('lang',itemLang);
+ }
+ nowPlayingSpan.html('' + thisObj.tt.selectedTrack + ': ' + itemTitle);
+ thisObj.$nowPlayingDiv.html(nowPlayingSpan);
}
- nowPlayingSpan.html('' + this.tt.selectedTrack + ': ' + itemTitle);
- this.$nowPlayingDiv.html(nowPlayingSpan);
}
- }
- // if this.swappingSrc is true, media will autoplay when ready
- if (this.initializing) { // this is the first track - user hasn't pressed play yet
- this.swappingSrc = false;
- }
- else {
- this.swappingSrc = true;
- if (this.player === 'html5') {
- this.media.load();
- }
- else if (this.player === 'youtube') {
- this.okToPlay = true;
+ if (thisObj.initializing) {
+ thisObj.swappingSrc = false;
+ } else {
+ if (thisObj.player === 'html5') {
+ if (!thisObj.loadingMedia) {
+ thisObj.media.load();
+ thisObj.loadingMedia = true;
+ }
+ } else if (thisObj.player === 'youtube') {
+ thisObj.okToPlay = true;
+ }
}
- }
+ thisObj.initializing = false;
+ thisObj.playerCreated = true;
+ });
};
- AblePlayer.prototype.deletePlayer = function() {
+ AblePlayer.prototype.deletePlayer = function(context) {
+
+
+
+ if (this.player === 'youtube' && this.youTubePlayer) {
+ this.youTubePlayer.destroy();
+ }
- // remove previous video's attributes and child elements from media element
- if (this.player == 'youtube') {
- var $youTubeIframe = this.$mediaContainer.find('iframe');
- $youTubeIframe.remove();
+ if (this.player === 'vimeo' && this.vimeoPlayer) {
+ this.vimeoPlayer.destroy();
}
- this.$media.removeAttr('poster width height');
- this.$media.empty();
- // Empty elements that will be rebuilt
this.$controllerDiv.empty();
- // this.$statusBarDiv.empty();
- // this.$timer.empty();
- this.$elapsedTimeContainer.empty().text('0:00'); // span.able-elapsedTime
- this.$durationContainer.empty(); // span.able-duration
+ this.$elapsedTimeContainer.empty().text('0:00');
+ this.$durationContainer.empty();
- // Remove popup windows and modal dialogs; these too will be rebuilt
if (this.$signWindow) {
this.$signWindow.remove();
}
@@ -4858,551 +4996,639 @@ var AblePlayerInstances = [];
}
$('.able-modal-dialog').remove();
- // reset key variables
+ if (this.$captionsWrapper) {
+ this.$captionsWrapper.remove();
+ }
+ if (this.$descDiv) {
+ this.$descDiv.remove();
+ }
+
this.hasCaptions = false;
this.hasChapters = false;
+ this.hasDescTracks = false;
+ this.hasOpenDesc = false;
+ this.hasClosedDesc = false;
+
this.captionsPopup = null;
this.chaptersPopup = null;
+ this.transcriptType = null;
+
+ this.playerDeleted = true;
};
AblePlayer.prototype.getButtonTitle = function(control) {
if (control === 'playpause') {
- return this.tt.play;
- }
- else if (control === 'play') {
- return this.tt.play;
- }
- else if (control === 'pause') {
- return this.tt.pause;
- }
- else if (control === 'restart') {
- return this.tt.restart;
- }
- else if (control === 'previous') {
- return this.tt.prevTrack;
- }
- else if (control === 'next') {
- return this.tt.nextTrack;
- }
- else if (control === 'rewind') {
- return this.tt.rewind;
- }
- else if (control === 'forward') {
- return this.tt.forward;
- }
- else if (control === 'captions') {
+ return this.translate( 'play', 'Play' );
+ } else if (control === 'play') {
+ return this.translate( 'play', 'Play' );
+ } else if (control === 'pause') {
+ return this.translate( 'pause', 'Pause' );
+ } else if (control === 'restart') {
+ return this.translate( 'restart', 'Restart' );
+ } else if (control === 'previous') {
+ return this.translate( 'prevTrack', 'Previous track' );
+ } else if (control === 'next') {
+ return this.translate( 'nextTrack', 'Next track' );
+ } else if (control === 'rewind') {
+ return this.translate( 'rewind', 'Rewind' );
+ } else if (control === 'forward') {
+ return this.translate( 'forward', 'Forward' );
+ } else if (control === 'captions') {
if (this.captions.length > 1) {
- return this.tt.captions;
- }
- else {
- if (this.captionsOn) {
- return this.tt.hideCaptions;
- }
- else {
- return this.tt.showCaptions;
- }
- }
- }
- else if (control === 'descriptions') {
- if (this.descOn) {
- return this.tt.turnOffDescriptions;
- }
- else {
- return this.tt.turnOnDescriptions;
- }
- }
- else if (control === 'transcript') {
- if (this.$transcriptDiv.is(':visible')) {
- return this.tt.hideTranscript;
- }
- else {
- return this.tt.showTranscript;
- }
- }
- else if (control === 'chapters') {
- return this.tt.chapters;
- }
- else if (control === 'sign') {
- return this.tt.sign;
- }
- else if (control === 'volume') {
- return this.tt.volume;
- }
- else if (control === 'faster') {
- return this.tt.faster;
- }
- else if (control === 'slower') {
- return this.tt.slower;
- }
- else if (control === 'preferences') {
- return this.tt.preferences;
- }
- else if (control === 'help') {
- // return this.tt.help;
- }
- else {
- // there should be no other controls, but just in case:
- // return the name of the control with first letter in upper case
- // ultimately will need to get a translated label from this.tt
+ return this.translate( 'captions', 'Captions' );
+ } else {
+ return (this.captionsOn) ? this.translate( 'hideCaptions', 'Hide captions' ) : this.translate( 'showCaptions', 'Show captions' );
+ }
+ } else if (control === 'descriptions') {
+ return (this.descOn) ? this.translate( 'turnOffDescriptions', 'Turn off descriptions' ) : this.translate( 'turnOnDescriptions', 'Turn on descriptions' );
+ } else if (control === 'transcript') {
+ return (this.$transcriptDiv.is(':visible')) ? this.translate( 'hideTranscript', 'Hide transcript' ) : this.translate( 'showTranscript', 'Show transcript' );
+ } else if (control === 'chapters') {
+ return this.translate( 'chapters', 'Chapters' );
+ } else if (control === 'sign') {
+ return this.translate( 'sign', 'Sign language' );
+ } else if (control === 'volume') {
+ return this.translate( 'volume', 'Volume' );
+ } else if (control === 'faster') {
+ return this.translate( 'faster', 'Faster' );
+ } else if (control === 'slower') {
+ return this.translate( 'slower', 'Slower' );
+ } else if (control === 'preferences') {
+ return this.translate( 'preferences', 'Preferences' );
+ } else if (control === 'fullscreen') {
+ return ( !this.fullscreen ) ? this.translate( 'enterFullScreen', 'Enter full screen' ) : this.translate( 'exitFullScreen', 'Exit full screen' );
+ } else {
if (this.debug) {
-
- }
- return control.charAt(0).toUpperCase() + control.slice(1);
+
+ }
+ return this.capitalizeFirstLetter( control );
}
};
-
-
})(jQuery);
-(function ($) {
- // Loads files referenced in track elements, and performs appropriate setup.
- // For example, captions and text descriptions.
- // This will be called whenever the player is recreated.
- // Added in v2.2.23: Also handles YouTube caption tracks
- AblePlayer.prototype.setupTracks = function() {
+var preProcessing = {
+ transformCSSClasses: function (vttContent) {
+ if ( vttContent.length > 1000 ) {
+ throw new Error( "Input too long" );
+ }
+ return vttContent.replace(
+ /<(v|c|b|i|u|lang|ruby)\.([\w\.]+)([^>]*)>/g,
+ function (_, tag, cssClasses, otherAttrs) {
+ var classAttr = cssClasses.replace(/\./g, " ");
+ return `<${tag} class="${classAttr}"${otherAttrs}>`;
+ }
+ );
+ },
+
+ transformLangTags: function (content) {
+ return content.replace(
+ /]*)>/g,
+ function (_, langCode, otherAttrs) {
+ return '";
+ }
+ );
+ },
+
+ transformVTags: function (content) {
+ return content.replace(/]*?)>/g, function (_, tagAttributes) {
+ var classMatch = tagAttributes.match(/class="([^"]*)"/);
+ var classAttr = classMatch ? classMatch[0] : "";
+ var nonClassAttributes = tagAttributes
+ .replace(/class="[^"]*"/, "")
+ .trim()
+ .split(/\s+/);
+
+ var attributes = [];
+ var titleParts = [];
+
+ nonClassAttributes.forEach(function (token) {
+ if (token.indexOf("=") !== -1) {
+ attributes.push(token);
+ } else {
+ titleParts.push(token);
+ }
+ });
- var thisObj, deferred, promise, loadingPromises, loadingPromise, i, tracks, track;
+ var title = titleParts.join(" ");
+ var newTag = " 0) {
+ newTag += " " + attributes.join(" ");
+ }
- loadingPromises = [];
+ if (classAttr) {
+ newTag += " " + classAttr;
+ }
- this.captions = [];
- this.captionLabels = [];
- this.descriptions = [];
- this.chapters = [];
- this.meta = [];
+ newTag += ">";
+ return newTag;
+ });
+ },
+};
+
+var postProcessing = {
+ postprocessCTag: function (vttContent) {
+ return vttContent.replace(
+ //g,
+ function (_, classNames) {
+ var classes = classNames.replace(/ /g, ".");
+ return "";
+ }
+ );
+ },
+
+ postprocessVTag: function (vttContent) {
+ return vttContent.replace(
+ /]*)class="([\w\s]+)"([^>]*)>/g,
+ function (_, beforeClass, classNames, afterClass) {
+ var classes = classNames.trim().split(/\s+/).join(".");
+ var attrs = (beforeClass + afterClass)
+ .replace(/\s*class="[\w\s]+"/, "")
+ .trim();
+ return "";
+ }
+ );
+ },
+
+ postprocessLangTag: function (vttContent) {
+ return vttContent.replace(
+ /]*)>/g,
+ function (_, langCode, otherAttrs) {
+ return "";
+ }
+ );
+ },
+};
+
+var validate = {
+ preProcessVttContent: function (vttContent) {
+ var processedCSS = preProcessing.transformCSSClasses(vttContent);
+ var processedLang = preProcessing.transformLangTags(processedCSS);
+ var processedVTags = preProcessing.transformVTags(processedLang);
+ return processedVTags;
+ },
+
+ postProcessVttContent: function (sanitizedVttContent, originalVttContent) {
+ var processedCTags = postProcessing.postprocessCTag(sanitizedVttContent);
+ var processedVTags = postProcessing.postprocessVTag(processedCTags);
+ var processedLangTags = postProcessing.postprocessLangTag(processedVTags);
+
+ var arrowReplaced = processedLangTags.replace(/-->/g, "-->");
+ var timestampTagReplaced = arrowReplaced.replace(/<([\d:.]+)>/g, '<$1>');
+
+ var finalContent = timestampTagReplaced.replace(
+ /<\/v>/g,
+ function (match, offset) {
+ return originalVttContent.indexOf(match, offset) !== -1 ? match : "";
+ }
+ );
- if ($('#able-vts').length) {
- // Page includes a container for a VTS instance
- this.vtsTracks = [];
- this.hasVts = true;
- }
- else {
- this.hasVts = false;
- }
+ return finalContent;
+ },
- this.getTracks().then(function() {
+ sanitizeVttContent: function (vttContent) {
+ if (vttContent === null || vttContent === undefined) {
+ return "";
+ }
+ var preSanitizedVttContent = validate.preProcessVttContent(vttContent);
- tracks = thisObj.tracks;
+ var config = {
+ ALLOWED_TAGS: ["b", "i", "u", "v", "c", "lang", "ruby", "rt", "rp"],
+ ALLOWED_ATTR: ["title", "class", "lang"],
+ KEEP_CONTENT: true,
+ };
- if (thisObj.player === 'youtube') {
- // If captions have been loaded into the captions array (either from YouTube or a local source),
- // we no longer have a need to use YouTube captions
- // TODO: Consider whether this is the right place to make this decision
- // Probably better to make it when cues are identified from YouTube caption sources
- if (tracks.length) {
- thisObj.usingYouTubeCaptions = false;
- }
- }
+ var sanitizedVttContent = DOMPurify.sanitize(
+ preSanitizedVttContent,
+ config
+ );
+
+ return validate.postProcessVttContent(sanitizedVttContent, vttContent);
+ },
+ isProtocolSafe: function (url) {
+ try {
+ const parsedUrl = new URL(url, window.location.origin);
+ return ["http:", "https:"].includes(parsedUrl.protocol);
+ } catch (e) {
+ return false;
+ }
+ },
+};
- for (i=0; i < tracks.length; i++) {
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = validate;
+}
- track = tracks[i];
+(function ($) {
- var kind = track.kind;
- var trackLang = track.language;
- var trackLabel = track.label;
+ AblePlayer.prototype.setupTracks = function () {
+ var thisObj, deferred, promise, loadingPromises, loadingPromise, i, tracks, track, kind;
- if (!track.src) {
- if (thisObj.usingYouTubeCaptions || thisObj.usingVimeoCaptions) {
- // skip all the hullabaloo and go straight to setupCaptions
- thisObj.setupCaptions(track,trackLang,trackLabel);
- }
- else {
- // Nothing to load!
- // Skip this track; move on to next i
- }
- continue;
- }
+ thisObj = this;
- var trackSrc = track.src;
+ deferred = new this.defer();
+ promise = deferred.promise();
- loadingPromise = thisObj.loadTextObject(trackSrc); // resolves with src, trackText
- loadingPromises.push(loadingPromise);
+ loadingPromises = [];
- loadingPromise.then((function (track, kind) {
+ if ($("#able-vts").length) {
+ this.vtsTracks = [];
+ this.hasVts = true;
+ } else {
+ this.hasVts = false;
+ }
- var trackSrc = track.src;
- var trackLang = track.language;
- var trackLabel = track.label;
+ if (this.hasDescTracks && this.descOn) {
+ tracks = this.altTracks;
+ } else {
+ tracks = this.tracks;
+ }
+ for (i = 0; i < tracks.length; i++) {
+ track = tracks[i];
+ kind = ( track.kind ) ? track.kind : 'subtitles';
- return function (trackSrc, trackText) { // these are the two vars returned from loadTextObject
+ if (!track.src) {
+ if (thisObj.usingYouTubeCaptions || thisObj.usingVimeoCaptions) {
+ thisObj.setupCaptions(track);
+ }
+ continue;
+ }
+ var trackSrc = track.src;
+ loadingPromise = this.loadTextObject(trackSrc);
+ loadingPromises.push(
+ loadingPromise.catch(function (src) {
+
+ })
+ );
+ loadingPromise.then(
+ (function (track, kind) {
+ trackSrc = track.src;
+ var trackLang = track.language;
+ var trackLabel = track.label;
+ var trackDesc = track.desc;
+
+ return function (data) {
+ var cues = thisObj.parseWebVTT(data).cues;
+ if (thisObj.hasVts) {
+ thisObj.setupVtsTracks(
+ kind,
+ trackLang,
+ trackDesc,
+ trackLabel,
+ trackSrc,
+ data.text
+ );
+ }
+ if (kind === 'captions' || kind === 'subtitles') {
+ thisObj.setupCaptions(track, cues);
+ } else if (kind === 'descriptions') {
+ thisObj.setupDescriptions(track, cues);
+ } else if (kind === 'chapters') {
+ thisObj.setupChapters(track, cues);
+ } else if (kind === 'metadata') {
+ thisObj.setupMetadata(cues);
+ }
+ };
+ })(track, kind)
+ );
+ }
+ if (thisObj.usingYouTubeCaptions || thisObj.usingVimeoCaptions) {
+ deferred.resolve();
+ } else {
+ $.when.apply($, loadingPromises).then(function () {
+ deferred.resolve();
+ });
+ }
+ return promise;
+ };
- var trackContents = trackText;
- var cues = thisObj.parseWebVTT(trackSrc, trackContents).cues;
+ AblePlayer.prototype.getTracks = function () {
- if (thisObj.hasVts) {
- // setupVtsTracks() is in vts.js
- thisObj.setupVtsTracks(kind, trackLang, trackLabel, trackSrc, trackContents);
- }
+ var thisObj, deferred, promise, trackLang, trackLabel, isDefault, forDesc,
+ hasDefault, hasTrackInDefLang, trackFound, i;
- if (kind === 'captions' || kind === 'subtitles') {
- thisObj.setupCaptions(track, trackLang, trackLabel, cues);
- }
- else if (kind === 'descriptions') {
- thisObj.setupDescriptions(track, cues, trackLang);
- }
- else if (kind === 'chapters') {
- thisObj.setupChapters(track, cues, trackLang);
- }
- else if (kind === 'metadata') {
- thisObj.setupMetadata(track, cues);
- }
- }
- })(track, kind));
- }
- $.when.apply($, loadingPromises).then(function () {
- deferred.resolve();
- });
- });
+ thisObj = this;
+ hasDefault = false;
+
+ deferred = new this.defer();
+ promise = deferred.promise();
+
+ this.$tracks = this.$media.find('track');
+ this.tracks = [];
+ this.altTracks = [];
+
+ this.captions = [];
+ this.descriptions = [];
+ this.chapters = [];
+ this.meta = [];
+
+ this.hasCaptionsTrack = false;
+ this.hasDescTracks = false;
+
+ if (this.$tracks.length) {
+ this.usingYouTubeCaptions = false;
+ this.$tracks.each(function (index, element) {
+ if ($(this).attr('kind') === 'captions') {
+ thisObj.hasCaptionsTrack = true;
+ } else if ($(this).attr('kind') === 'descriptions') {
+ thisObj.hasClosedDesc = true;
+ }
- return promise;
- };
+ if ($(this).attr('srclang')) {
+ trackLang = $(this).attr('srclang');
+ } else {
+ trackLang = thisObj.lang;
+ }
+ if ($(this).attr('label')) {
+ trackLabel = $(this).attr('label');
+ } else {
+ trackLabel = thisObj.getLanguageName(trackLang);
+ }
- AblePlayer.prototype.getTracks = function() {
+ if (typeof $(this).attr('default') !== 'undefined' && !hasDefault) {
+ isDefault = true;
+ hasDefault = true;
+ } else if (trackLang === thisObj.lang) {
+ hasTrackInDefLang = true;
+ isDefault = false;
+ } else {
+ isDefault = false;
+ }
+ if (isDefault) {
+ thisObj.captionLang = trackLang;
+ }
- // define an array tracks with the following structure:
- // kind - string, e.g. "captions", "descriptions"
- // src - string, URL of WebVTT source file
- // language - string, lang code
- // label - string to display, e.g., in CC menu
- // def - Boolean, true if this is the default track
- // cues - array with startTime, endTime, and payload
+ if ($(this).data("desc") !== undefined) {
+ forDesc = true;
+ thisObj.hasDescTracks = true;
+ } else {
+ forDesc = false;
+ }
+ if (forDesc) {
+ thisObj.altTracks.push({
+ kind: $(this).attr('kind'),
+ src: $(this).attr('src'),
+ language: trackLang,
+ label: trackLabel,
+ def: isDefault,
+ desc: forDesc,
+ });
+ } else {
+ thisObj.tracks.push({
+ kind: $(this).attr('kind'),
+ src: $(this).attr('src'),
+ language: trackLang,
+ label: trackLabel,
+ def: isDefault,
+ desc: forDesc,
+ });
+ }
- var thisObj, deferred, promise, captionTracks, trackLang, trackLabel, isDefault;
+ if (index == thisObj.$tracks.length - 1) {
+ if (!hasDefault) {
+ if (hasTrackInDefLang) {
+ thisObj.captionLang = thisObj.lang;
+ trackFound = false;
+ i = 0;
+ while (i < thisObj.tracks.length && !trackFound) {
+ if (thisObj.tracks[i]['language'] === thisObj.lang) {
+ thisObj.tracks[i]['def'] = true;
+ trackFound = true;
+ }
+ i++;
+ }
+ } else {
+ thisObj.tracks[0]['def'] = true;
+ thisObj.captionLang = thisObj.tracks[0]['language'];
+ }
+ }
+ thisObj.$media.find("track").removeAttr("default");
+ }
+ });
+ }
+ if (!this.$tracks.length || !this.hasCaptionsTrack) {
+ if (this.player === 'youtube') {
+ this.getYouTubeCaptionTracks().then(function () {
+ if (thisObj.hasCaptions) {
+ thisObj.usingYouTubeCaptions = true;
+ if (thisObj.$captionsWrapper) {
+ thisObj.$captionsWrapper.remove();
+ }
+ }
+ deferred.resolve();
+ });
+ } else if (this.player === 'vimeo') {
+ this.getVimeoCaptionTracks().then(function () {
+ if (thisObj.hasCaptions) {
+ thisObj.usingVimeoCaptions = true;
+ if (thisObj.$captionsWrapper) {
+ thisObj.$captionsWrapper.remove();
+ }
+ }
+ deferred.resolve();
+ });
+ } else {
+ this.hasCaptions = false;
+ if (thisObj.$captionsWrapper) {
+ thisObj.$captionsWrapper.remove();
+ }
+ deferred.resolve();
+ }
+ } else {
+ deferred.resolve();
+ }
+ return promise;
+ };
- thisObj = this;
+ AblePlayer.prototype.setupCaptions = function (track, cues) {
+ var inserted, i, capLabel;
- deferred = new $.Deferred();
- promise = deferred.promise();
+ if (typeof cues === "undefined") {
+ cues = null;
+ }
- this.$tracks = this.$media.find('track');
- this.tracks = [];
+ if (this.usingYouTubeCaptions || this.usingVimeoCaptions) {
+ } else {
+ if (this.captions.length === 0) {
+ this.captions.push({
+ language: track.language,
+ label: track.label,
+ def: track.def,
+ cues: cues,
+ });
+ } else {
+ inserted = false;
+ for (i = 0; i < this.captions.length; i++) {
+ capLabel = track.label;
+ if (capLabel.toLowerCase() < this.captions[i].label.toLowerCase()) {
+ this.captions.splice(i, 0, {
+ language: track.language,
+ label: track.label,
+ def: track.def,
+ cues: cues,
+ });
+ inserted = true;
+ break;
+ }
+ }
+ if (!inserted) {
+ this.captions.push({
+ language: track.language,
+ label: track.label,
+ def: track.def,
+ cues: cues,
+ });
+ }
+ }
+ }
- if (this.$tracks.length) {
+ this.hasCaptions = true;
+ this.currentCaption = -1;
+ if (this.prefCaptions === 1) {
+ this.captionsOn = true;
+ } else if (this.prefCaptions === 0) {
+ this.captionsOn = false;
+ } else {
+ if (this.defaultStateCaptions === 1) {
+ this.captionsOn = true;
+ } else {
+ this.captionsOn = false;
+ }
+ }
+ if (this.mediaType === 'audio' && this.captionsOn) {
+ this.$captionsContainer.removeClass('captions-off');
+ }
- // create object from HTML5 tracks
- this.$tracks.each(function() {
+ if (
+ !this.$captionsWrapper ||
+ (this.$captionsWrapper &&
+ !$.contains(this.$ableDiv[0], this.$captionsWrapper[0]))
+ ) {
+ this.$captionsDiv = $('', {
+ class: "able-captions",
+ });
+ this.$captionsWrapper = $('
', {
+ class: 'able-captions-wrapper',
+ 'aria-hidden': 'true',
+ }).hide();
+ if (this.prefCaptionsPosition === 'below') {
+ this.$captionsWrapper.addClass('able-captions-below');
+ } else {
+ this.$captionsWrapper.addClass('able-captions-overlay');
+ }
+ this.$captionsWrapper.append(this.$captionsDiv);
+ this.$captionsContainer.append(this.$captionsWrapper);
+ }
+ };
- // srcLang should always be included with
, but HTML5 spec doesn't require it
- // if not provided, assume track is the same language as the default player language
- if ($(this).attr('srclang')) {
- trackLang = $(this).attr('srclang');
- }
- else {
- trackLang = thisObj.lang;
- }
+ AblePlayer.prototype.setupDescriptions = function (track, cues) {
- if ($(this).attr('label')) {
- trackLabel = $(this).attr('label');
- }
- else {
- trackLabel = thisObj.getLanguageName(trackLang);
- }
+ this.hasClosedDesc = true;
+ this.currentDescription = -1;
+ this.descriptions.push({
+ cues: cues,
+ language: track.language,
+ });
+ };
- if ($(this).attr('default')) {
- isDefault = true;
- }
- else if (trackLang === thisObj.lang) {
- // There is no @default attribute,
- // but this is the user's/browser's default language
- // so make it the default caption track
- isDefault = true;
- }
- else {
- isDefault = false;
- }
+ AblePlayer.prototype.setupChapters = function (track, cues) {
- if (isDefault) {
- // this.captionLang will also be the default language for non-caption tracks
- thisObj.captionLang = trackLang;
- }
+ this.hasChapters = true;
+ this.chapters.push({
+ cues: cues,
+ language: track.language,
+ });
+ };
- thisObj.tracks.push({
- 'kind': $(this).attr('kind'),
- 'src': $(this).attr('src'),
- 'language': trackLang,
- 'label': trackLabel,
- 'def': isDefault
- });
- });
- }
+ AblePlayer.prototype.setupMetadata = function (cues) {
+ if (this.metaType === 'text') {
+ if (this.metaDiv) {
+ if ($('#' + this.metaDiv)) {
+ this.$metaDiv = $('#' + this.metaDiv);
+ this.hasMeta = true;
+ this.meta = cues;
+ }
+ }
+ } else if (this.metaType === 'selector') {
+ this.hasMeta = true;
+ this.visibleSelectors = [];
+ this.meta = cues;
+ }
+ };
- // check to see if any HTML caption or subitle tracks were found.
- captionTracks = this.$media.find('track[kind="captions"],track[kind="subtitles"]');
- if (captionTracks.length) {
- // HTML captions or subtitles were found. Use those.
- deferred.resolve();
- }
- else {
- // if this is a youtube or vimeo player, check there for captions/subtitles
- if (this.player === 'youtube') {
- this.getYouTubeCaptionTracks(this.youTubeId).then(function() {
- deferred.resolve();
- });
- }
- else if (this.player === 'vimeo') {
- this.getVimeoCaptionTracks().then(function() {
- deferred.resolve();
- });
- }
- else {
- // this is neither YouTube nor Vimeo
- // there just ain't no caption tracks
- deferred.resolve();
- }
- }
- return promise;
- };
+ AblePlayer.prototype.loadTextObject = function (src) {
+ var deferred, promise, thisObj, $tempDiv;
- AblePlayer.prototype.setupCaptions = function (track, trackLang, trackLabel, cues) {
+ deferred = new this.defer();
+ promise = deferred.promise();
+ thisObj = this;
- var thisObj, inserted, i, capLabel;
+ $tempDiv = $('', {
+ style: 'display:none',
+ });
- thisObj = this;
+ fetch(src)
+ .then( response => {
- if (typeof cues === 'undefined') {
- cues = null;
- }
-
- this.hasCaptions = true;
-
- // Remove 'default' attribute from all
elements
- // This data has already been saved to this.tracks
- // and some browsers will display the default captions, despite all standard efforts to suppress them
- this.$media.find('track').removeAttr('default');
-
- // caption cues from WebVTT are used to build a transcript for both audio and video
- // but captions are currently only supported for video
- if (this.mediaType === 'video') {
-
- if (!(this.usingYouTubeCaptions || this.usingVimeoCaptions)) {
- // create a pair of nested divs for displaying captions
- // includes aria-hidden="true" because otherwise
- // captions being added and removed causes sporadic changes to focus in JAWS
- // (not a problem in NVDA or VoiceOver)
- if (!this.$captionsDiv) {
- this.$captionsDiv = $('',{
- 'class': 'able-captions',
- });
- this.$captionsWrapper = $('
',{
- 'class': 'able-captions-wrapper',
- 'aria-hidden': 'true'
- }).hide();
- if (this.prefCaptionsPosition === 'below') {
- this.$captionsWrapper.addClass('able-captions-below');
- }
- else {
- this.$captionsWrapper.addClass('able-captions-overlay');
- }
- this.$captionsWrapper.append(this.$captionsDiv);
- this.$vidcapContainer.append(this.$captionsWrapper);
- }
- }
- }
+ return response.text();
+ })
+ .then( vtt => {
+ var preParsed = vtt.split(/\r?\n\s*\r?\n/);
+ var lines = '', line;
- this.currentCaption = -1;
- if (this.prefCaptions === 1) {
- // Captions default to on.
- this.captionsOn = true;
- }
- else {
- this.captionsOn = false;
- }
- if (this.captions.length === 0) { // this is the first
- this.captions.push({
- 'cues': cues,
- 'language': trackLang,
- 'label': trackLabel,
- 'def': track.def
+ preParsed.forEach((l) => {
+ line = validate.sanitizeVttContent(l);
+ lines += line + "\n\n";
});
- this.captionLabels.push(trackLabel);
- }
- else { // there are already tracks in the array
- inserted = false;
- for (i = 0; i < this.captions.length; i++) {
- capLabel = this.captionLabels[i];
- if (trackLabel.toLowerCase() < this.captionLabels[i].toLowerCase()) {
- // insert before track i
- this.captions.splice(i,0,{
- 'cues': cues,
- 'language': trackLang,
- 'label': trackLabel,
- 'def': track.def
- });
- this.captionLabels.splice(i,0,trackLabel);
- inserted = true;
- break;
- }
- }
- if (!inserted) {
- // just add track to the end
- this.captions.push({
- 'cues': cues,
- 'language': trackLang,
- 'label': trackLabel,
- 'def': track.def
- });
- this.captionLabels.push(trackLabel);
- }
- }
- };
-
- AblePlayer.prototype.setupDescriptions = function (track, cues, trackLang) {
-
- // called via setupTracks() only if there is track with kind="descriptions"
- // prepares for delivery of text description , in case it's needed
- // whether and how it's delivered is controlled within description.js > initDescription()
-
- this.hasClosedDesc = true;
- this.currentDescription = -1;
- this.descriptions.push({
- cues: cues,
- language: trackLang
- });
- };
-
- AblePlayer.prototype.setupChapters = function (track, cues, trackLang) {
-
- // NOTE: WebVTT supports nested timestamps (to form an outline)
- // This is not currently supported.
-
- this.hasChapters = true;
-
- this.chapters.push({
- cues: cues,
- language: trackLang
- });
- };
-
- AblePlayer.prototype.setupMetadata = function(track, cues) {
-
- if (this.metaType === 'text') {
- // Metadata is only supported if data-meta-div is provided
- // The player does not display metadata internally
- if (this.metaDiv) {
- if ($('#' + this.metaDiv)) {
- // container exists
- this.$metaDiv = $('#' + this.metaDiv);
- this.hasMeta = true;
- this.meta = cues;
- }
- }
- }
- else if (this.metaType === 'selector') {
- this.hasMeta = true;
- this.visibleSelectors = [];
- this.meta = cues;
- }
- };
-
- AblePlayer.prototype.loadTextObject = function(src) {
-
-// TODO: Incorporate the following function, moved from setupTracks()
-// convert XMl/TTML captions file
-/*
-if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' {
+ if (thisObj.debug) {
- // create a temp div for holding data
- $tempDiv = $('
',{
- style: 'display:none'
- });
- $tempDiv.load(src, function (trackText, status, req) {
- if (status === 'error') {
- if (thisObj.debug) {
-
- }
- deferred.fail();
- }
- else {
- deferred.resolve(src, trackText);
- }
+ }
+ deferred.reject(src);
$tempDiv.remove();
});
- return promise;
- };
-
- AblePlayer.prototype.setupAltCaptions = function() {
-
- // setup captions from an alternative source (not
elements)
- // only do this if no captions are provided
- // currently supports: YouTube, Vimeo
- var deferred = new $.Deferred();
- var promise = deferred.promise();
- if (this.captions.length === 0) {
- if (this.player === 'youtube' && this.usingYouTubeCaptions) {
- this.setupYouTubeCaptions().done(function() {
- deferred.resolve();
- });
- }
- else if (this.player === 'vimeo' && this.usingVimeoCaptions) {
- this.setupVimeoCaptions().done(function() {
- deferred.resolve();
- });
- }
-
- else {
- // repeat for other alt sources once supported (e.g., Vimeo, DailyMotion)
- deferred.resolve();
- }
- }
- else { // there are captions, so no need for alt source captions
- deferred.resolve();
- }
- return promise;
- };
+ return promise;
+ };
})(jQuery);
-
(function ($) {
+
AblePlayer.prototype.initYouTubePlayer = function () {
- var thisObj, deferred, promise, youTubeId, googleApiPromise, json;
+ var thisObj, deferred, promise, youTubeId;
thisObj = this;
-
- deferred = new $.Deferred();
+ deferred = new this.defer();
promise = deferred.promise();
- // if a described version is available && user prefers desription
- // init player using the described version
- if (this.youTubeDescId && this.prefDesc) {
- youTubeId = this.youTubeDescId;
- }
- else {
- youTubeId = this.youTubeId;
- }
+ this.youTubePlayerReady = false;
+
+ youTubeId = (this.youTubeDescId && this.prefDesc) ? this.youTubeDescId : this.youTubeId;
+
this.activeYouTubeId = youTubeId;
- if (AblePlayer.youtubeIframeAPIReady) {
- // Script already loaded and ready.
- this.finalizeYoutubeInit().then(function() {
+ if (AblePlayer.youTubeIframeAPIReady) {
+ thisObj.finalizeYoutubeInit().then(function() {
deferred.resolve();
});
- }
- else {
- // Has another player already started loading the script? If so, abort...
- if (!AblePlayer.loadingYoutubeIframeAPI) {
- $.getScript('https://www.youtube.com/iframe_api').fail(function () {
- deferred.fail();
- });
+ } else {
+ if (!AblePlayer.loadingYouTubeIframeAPI) {
+ thisObj.getScript('https://www.youtube.com/iframe_api', function () {
+
+ });
}
- // Otherwise, keeping waiting for script load event...
- $('body').on('youtubeIframeAPIReady', function () {
+ $('body').on('youTubeIframeAPIReady', function () {
thisObj.finalizeYoutubeInit().then(function() {
deferred.resolve();
});
@@ -5413,115 +5639,86 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('').attr('id', containerId));
- // NOTE: Tried the following in place of the above in January 2016
- // because in some cases two videos were being added to the DOM
- // However, once v2.2.23 was fairly stable, unable to reproduce that problem
- // so maybe it's not an issue. This is preserved here temporarily, just in case it's needed...
- // thisObj.$mediaContainer.html($('').attr('id', containerId));
-
- // cc_load_policy:
- // 0 - show captions depending on user's preference on YouTube
- // 1 - show captions by default, even if the user has turned them off
- // For Able Player, init player with value of 0
- // and will turn them on or off after player is initialized
- // based on availability of local tracks and user's Able Player prefs
- ccLoadPolicy = 0;
-
- videoDimensions = this.getYouTubeDimensions(this.activeYouTubeId, containerId);
- if (videoDimensions) {
- this.ytWidth = videoDimensions[0];
- this.ytHeight = videoDimensions[1];
- this.aspectRatio = thisObj.ytWidth / thisObj.ytHeight;
- }
- else {
- // dimensions are initially unknown
- // sending null values to YouTube results in a video that uses the default YouTube dimensions
- // these can then be scraped from the iframe and applied to this.$ableWrapper
- this.ytWidth = null;
- this.ytHeight = null;
- }
-
- if (this.okToPlay) {
- autoplay = 1;
- }
- else {
- autoplay = 0;
- }
-
- // NOTE: YouTube is changing the following parameters on or after Sep 25, 2018:
- // rel - No longer able to prevent YouTube from showing related videos
- // value of 0 now limits related videos to video's same channel
- // showinfo - No longer supported (previously, value of 0 hid title, share, & watch later buttons
- // Documentation https://developers.google.com/youtube/player_parameters
+ ccLoadPolicy = 1;
+ autoplay = (this.okToPlay) ? 1 : 0;
+
+
+ if (typeof this.captionLang == 'undefined') {
+ this.captionLang = this.lang;
+ }
this.youTubePlayer = new YT.Player(containerId, {
videoId: this.activeYouTubeId,
host: this.youTubeNoCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com',
- width: this.ytWidth,
- height: this.ytHeight,
playerVars: {
autoplay: autoplay,
+ cc_lang_pref: this.captionLang,
+ cc_load_policy: ccLoadPolicy,
+ controls: 0,
+ disableKb: 1,
enablejsapi: 1,
- disableKb: 1, // disable keyboard shortcuts, using our own
+ hl: this.lang,
+ iv_load_policy: 3,
+ origin: window.location.origin,
playsinline: this.playsInline,
- start: this.startTime,
- controls: 0, // no controls, using our own
- cc_load_policy: ccLoadPolicy,
- hl: this.lang, // use the default language UI
- modestbranding: 1, // no YouTube logo in controller
- rel: 0, // do not show related videos when video ends
- html5: 1, // force html5 if browser supports it (undocumented parameter; 0 does NOT force Flash)
- iv_load_policy: 3 // do not show video annotations
+ rel: 0,
+ start: this.startTime
},
events: {
onReady: function () {
+ thisObj.youTubePlayerReady = true;
+ if (!thisObj.playerWidth || !thisObj.playerHeight) {
+ thisObj.getYouTubeDimensions();
+ }
+ if (thisObj.playerWidth && thisObj.playerHeight) {
+ thisObj.youTubePlayer.setSize(thisObj.playerWidth,thisObj.playerHeight);
+ }
if (thisObj.swappingSrc) {
- // swap is now complete
thisObj.swappingSrc = false;
+ thisObj.restoreFocus();
thisObj.cueingPlaylistItem = false;
- if (thisObj.playing) {
- // resume playing
+ if (thisObj.playing || thisObj.okToPlay) {
thisObj.playMedia();
}
}
if (thisObj.userClickedPlaylist) {
- thisObj.userClickedPlaylist = false; // reset
+ thisObj.userClickedPlaylist = false;
}
- if (typeof thisObj.aspectRatio === 'undefined') {
- thisObj.resizeYouTubePlayer(thisObj.activeYouTubeId, containerId);
+ if (thisObj.recreatingPlayer) {
+ thisObj.recreatingPlayer = false;
}
deferred.resolve();
},
onError: function (x) {
- deferred.fail();
+ deferred.reject();
},
onStateChange: function (x) {
thisObj.getPlayerState().then(function(playerState) {
- // values of playerState: 'playing','paused','buffering','ended'
if (playerState === 'playing') {
+ if (thisObj.hasSignLanguage && thisObj.signVideo) {
+ thisObj.signVideo.play(true);
+ }
thisObj.playing = true;
thisObj.startedPlaying = true;
thisObj.paused = false;
- }
- else if (playerState == 'ended') {
+ } else if (playerState == 'ended') {
thisObj.onMediaComplete();
- }
- else {
+ } else {
thisObj.playing = false;
thisObj.paused = true;
}
if (thisObj.stoppingYouTube && playerState === 'paused') {
+ if (thisObj.hasSignLanguage && thisObj.signVideo) {
+ thisObj.signVideo.pause(true);
+ }
if (typeof thisObj.$posterImg !== 'undefined') {
thisObj.$posterImg.show();
}
@@ -5531,33 +5728,20 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' cuePlaylistItem()
this.$media.remove();
}
return promise;
@@ -5565,540 +5749,146 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('
- // 2. YouTube (not yet supported; can't seem to get this data via YouTube Data API without OAuth!)
-
- var d, url, $iframe, width, height;
-
- d = [];
-
- if (typeof this.playerMaxWidth !== 'undefined') {
- d[0] = this.playerMaxWidth;
- // optional: set height as well; not required though since YouTube will adjust height to match width
- if (typeof this.playerMaxHeight !== 'undefined') {
- d[1] = this.playerMaxHeight;
- }
- return d;
- }
- else {
- if (typeof $('#' + youTubeContainerId) !== 'undefined') {
- $iframe = $('#' + youTubeContainerId);
- width = $iframe.width();
- height = $iframe.height();
- if (width > 0 && height > 0) {
- d[0] = width;
- d[1] = height;
- return d;
- }
- }
- }
- return false;
- };
-
- AblePlayer.prototype.resizeYouTubePlayer = function(youTubeId, youTubeContainerId) {
+ var $iframe, width, height;
- // called after player is ready, if youTube dimensions were previously unknown
- // Now need to get them from the iframe element that YouTube injected
- // and resize Able Player to match
- var d, width, height;
- if (typeof this.aspectRatio !== 'undefined') {
- // video dimensions have already been collected
- if (this.restoringAfterFullScreen) {
- // restore using saved values
- if (this.youTubePlayer) {
- this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
- }
- this.restoringAfterFullScreen = false;
- }
- else {
- // recalculate with new wrapper size
- width = this.$ableWrapper.parent().width();
- height = Math.round(width / this.aspectRatio);
- this.$ableWrapper.css({
- 'max-width': width + 'px',
- 'width': ''
- });
- this.youTubePlayer.setSize(width, height);
- if (this.fullscreen) {
- this.youTubePlayer.setSize(width, height);
- }
- else {
- // resizing due to a change in window size, not full screen
- this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
- }
- }
- }
- else {
- d = this.getYouTubeDimensions(youTubeContainerId);
- if (d) {
- width = d[0];
- height = d[1];
- if (width > 0 && height > 0) {
- this.aspectRatio = width / height;
- this.ytWidth = width;
- this.ytHeight = height;
- if (width !== this.$ableWrapper.width()) {
- // now that we've retrieved YouTube's default width,
- // need to adjust to fit the current player wrapper
- width = this.$ableWrapper.width();
- height = Math.round(width / this.aspectRatio);
- if (this.youTubePlayer) {
- this.youTubePlayer.setSize(width, height);
- }
- }
+ $iframe = this.$ableWrapper.find('iframe');
+ if (typeof $iframe !== 'undefined') {
+ if ($iframe.prop('width')) {
+ width = $iframe.prop('width');
+ if ($iframe.prop('height')) {
+ height = $iframe.prop('height');
+ this.resizePlayer(width,height);
}
}
}
};
- AblePlayer.prototype.setupYouTubeCaptions = function () {
-
- // called from setupAltCaptions if player is YouTube and there are no captions
-
- // use YouTube Data API to get caption data from YouTube
- // function is called only if these conditions are met:
- // 1. this.player === 'youtube'
- // 2. there are no elements with kind="captions"
- // 3. youTubeDataApiKey is defined
-
- var deferred = new $.Deferred();
- var promise = deferred.promise();
-
- var thisObj, googleApiPromise, youTubeId, i;
-
- thisObj = this;
-
- // if a described version is available && user prefers desription
- // Use the described version, and get its captions
- if (this.youTubeDescId && this.prefDesc) {
- youTubeId = this.youTubeDescId;
- }
- else {
- youTubeId = this.youTubeId;
- }
-
- if (typeof youTubeDataAPIKey !== 'undefined') {
- // Wait until Google Client API is loaded
- // When loaded, it sets global var googleApiReady to true
-
- // Thanks to Paul Tavares for $.doWhen()
- // https://gist.github.com/purtuga/8257269
- $.doWhen({
- when: function(){
- return googleApiReady;
- },
- interval: 100, // ms
- attempts: 1000
- })
- .done(function(){
- deferred.resolve();
- })
- .fail(function(){
-
- });
- }
- else {
- deferred.resolve();
- }
- return promise;
- };
-
- AblePlayer.prototype.waitForGapi = function () {
-
- // wait for Google API to initialize
-
- var thisObj, deferred, promise, maxWaitTime, maxTries, tries, timer, interval;
-
- thisObj = this;
- deferred = new $.Deferred();
- promise = deferred.promise();
- maxWaitTime = 5000; // 5 seconds
- maxTries = 100; // number of tries during maxWaitTime
- tries = 0;
- interval = Math.floor(maxWaitTime/maxTries);
-
- timer = setInterval(function() {
- tries++;
- if (googleApiReady || tries >= maxTries) {
- clearInterval(timer);
- if (googleApiReady) { // success!
- deferred.resolve(true);
- }
- else { // tired of waiting
- deferred.resolve(false);
- }
- }
- else {
- thisObj.waitForGapi();
- }
- }, interval);
- return promise;
- };
-
- AblePlayer.prototype.getYouTubeCaptionTracks = function (youTubeId) {
+ AblePlayer.prototype.getYouTubeCaptionTracks = function () {
- // get data via YouTube Data API, and push data to this.captions
- var deferred = new $.Deferred();
+ var deferred = new this.defer();
var promise = deferred.promise();
-
- var thisObj, useGoogleApi, i, trackId, trackLang, trackName, trackLabel, trackKind, isDraft, isDefaultTrack;
+ var thisObj, ytTracks, i, trackLang, trackLabel, isDefaultTrack, apiTriggered = false;
thisObj = this;
-
- if (typeof youTubeDataAPIKey !== 'undefined') {
- this.waitForGapi().then(function(waitResult) {
-
- useGoogleApi = waitResult;
-
- // useGoogleApi returns false if API failed to initalize after max wait time
- // Proceed only if true. Otherwise can still use fallback method (see else loop below)
- if (useGoogleApi === true) {
- gapi.client.setApiKey(youTubeDataAPIKey);
- gapi.client
- .load('youtube', 'v3')
- .then(function() {
- var request = gapi.client.youtube.captions.list({
- 'part': 'id, snippet',
- 'videoId': youTubeId
- });
- request.then(function(json) {
- if (json.result.items.length) { // video has captions!
- thisObj.hasCaptions = true;
- thisObj.usingYouTubeCaptions = true;
- if (thisObj.prefCaptions === 1) {
- thisObj.captionsOn = true;
- }
- else {
- thisObj.captionsOn = false;
- }
- // Step through results and add them to cues array
- for (i=0; i < json.result.items.length; i++) {
- trackName = json.result.items[i].snippet.name; // usually seems to be empty
- trackLang = json.result.items[i].snippet.language;
- trackKind = json.result.items[i].snippet.trackKind; // ASR, standard, forced
- isDraft = json.result.items[i].snippet.isDraft; // Boolean
- // Other variables that could potentially be collected from snippet:
- // isCC - Boolean, always seems to be false
- // isLarge - Boolean
- // isEasyReader - Boolean
- // isAutoSynced Boolean
- // status - string, always seems to be "serving"
-
- var srcUrl = thisObj.getYouTubeTimedTextUrl(youTubeId,trackName,trackLang);
- if (trackKind !== 'ASR' && !isDraft) {
-
- if (trackName !== '') {
- trackLabel = trackName;
- }
- else {
- // if track name is empty (it always seems to be), assign a label based on trackLang
- trackLabel = thisObj.getLanguageName(trackLang);
- }
-
- // assign the default track based on language of the player
- if (trackLang === thisObj.lang) {
- isDefaultTrack = true;
- }
- else {
- isDefaultTrack = false;
- }
- thisObj.tracks.push({
- 'kind': 'captions',
- 'src': srcUrl,
- 'language': trackLang,
- 'label': trackLabel,
- 'def': isDefaultTrack
- });
- }
- }
- // setupPopups again with new captions array, replacing original
- thisObj.setupPopups('captions');
- deferred.resolve();
- }
- else {
- thisObj.hasCaptions = false;
- thisObj.usingYouTubeCaptions = false;
- deferred.resolve();
+ if (!this.youTubePlayer.getOption('captions','tracklist') ) {
+ this.youTubePlayer.addEventListener('onApiChange',function() {
+ apiTriggered = true;
+ thisObj.duration = thisObj.youTubePlayer.getDuration();
+
+ if (thisObj.loadingYouTubeCaptions) {
+ ytTracks = thisObj.youTubePlayer.getOption('captions','tracklist');
+ if ( ! thisObj.okToPlay ) {
+ thisObj.youTubePlayer.pauseVideo();
+ }
+ if (ytTracks && ytTracks.length) {
+ for (i=0; i < ytTracks.length; i++) {
+ trackLang = ytTracks[i].languageCode;
+ trackLabel = ytTracks[i].languageName;
+ isDefaultTrack = false;
+ if (typeof thisObj.captionLang !== 'undefined' && (trackLang === thisObj.captionLang) ) {
+ isDefaultTrack = true;
+ } else if (typeof thisObj.lang !== 'undefined') {
+ if (trackLang === thisObj.lang) {
+ isDefaultTrack = true;
}
- }, function (reason) {
- // If video has no captions, YouTube returns an error.
- // Should still proceed, but with captions disabled
- // The specific error, if needed: reason.result.error.message
- // If no captions, the error is: "The video identified by the videoId parameter could not be found."
-
-
- thisObj.hasCaptions = false;
- thisObj.usingYouTubeCaptions = false;
- deferred.resolve();
+ }
+ thisObj.tracks.push({
+ 'kind': 'captions',
+ 'language': trackLang,
+ 'label': trackLabel,
+ 'def': isDefaultTrack
});
- })
- }
- else {
- // googleAPi never loaded.
- this.getYouTubeCaptionTracks2(youTubeId).then(function() {
- deferred.resolve();
- });
- }
- });
- }
- else {
- // web owner hasn't provided a Google API key
- // attempt to get YouTube captions via the backup method
- this.getYouTubeCaptionTracks2(youTubeId).then(function() {
- deferred.resolve();
- });
- }
- return promise;
- };
-
- AblePlayer.prototype.getYouTubeCaptionTracks2 = function (youTubeId) {
-
- // Use alternative backup method of getting caption tracks from YouTube
- // and pushing them to this.captions
- // Called from getYouTubeCaptionTracks if no Google API key is defined
- // or if Google API failed to initiatlize
- // This method seems to be undocumented, but is referenced on StackOverflow
- // We'll use that as a fallback but it could break at any moment
-
- var deferred = new $.Deferred();
- var promise = deferred.promise();
-
- var thisObj, useGoogleApi, i, trackId, trackLang, trackName, trackLabel, trackKind, isDraft, isDefaultTrack;
-
- thisObj = this;
-
- $.ajax({
- type: 'get',
- url: 'https://www.youtube.com/api/timedtext?type=list&v=' + youTubeId,
- dataType: 'xml',
- success: function(xml) {
- var $tracks = $(xml).find('track');
- if ($tracks.length > 0) { // video has captions!
- thisObj.hasCaptions = true;
- thisObj.usingYouTubeCaptions = true;
- if (thisObj.prefCaptions === 1) {
- thisObj.captionsOn = true;
+ thisObj.captions.push({
+ 'language': trackLang,
+ 'label': trackLabel,
+ 'def': isDefaultTrack,
+ 'cues': null
+ });
+ }
+ thisObj.hasCaptions = true;
+ thisObj.setupPopups('captions');
+ } else {
+ thisObj.usingYouTubeCaptions = false;
+ thisObj.hasCaptions = false;
}
- else {
- thisObj.captionsOn = false;
+ thisObj.loadingYouTubeCaptions = false;
+ if (thisObj.okToPlay) {
+ thisObj.youTubePlayer.playVideo();
}
- // Step through results and add them to tracks array
- $tracks.each(function() {
- trackId = $(this).attr('id');
- trackLang = $(this).attr('lang_code');
- if ($(this).attr('name') !== '') {
- trackName = $(this).attr('name');
- trackLabel = trackName;
- }
- else {
- // @name is typically null except for default track
- // but lang_translated seems to be reliable
- trackName = '';
- trackLabel = $(this).attr('lang_translated');
- }
- if (trackLabel === '') {
- trackLabel = thisObj.getLanguageName(trackLang);
- }
- // assign the default track based on language of the player
- if (trackLang === thisObj.lang) {
- isDefaultTrack = true;
- }
- else {
- isDefaultTrack = false;
- }
-
- // Build URL for retrieving WebVTT source via YouTube's timedtext API
- var srcUrl = thisObj.getYouTubeTimedTextUrl(youTubeId,trackName,trackLang);
- thisObj.tracks.push({
- 'kind': 'captions',
- 'src': srcUrl,
- 'language': trackLang,
- 'label': trackLabel,
- 'def': isDefaultTrack
- });
-
- });
- // setupPopups again with new captions array, replacing original
- thisObj.setupPopups('captions');
- deferred.resolve();
}
- else {
- thisObj.hasCaptions = false;
- thisObj.usingYouTubeCaptions = false;
- deferred.resolve();
+ if (thisObj.captionLangPending) {
+ thisObj.youTubePlayer.setOption('captions', 'track', {'languageCode': thisObj.captionLangPending});
+ thisObj.captionLangPending = null;
+ }
+ if (typeof thisObj.prefCaptionsSize !== 'undefined') {
+ thisObj.youTubePlayer.setOption('captions','fontSize',thisObj.translatePrefs('size',thisObj.prefCaptionsSize,'youtube'));
}
- },
- error: function(xhr, status) {
-
deferred.resolve();
- }
- });
+ });
+ this.loadingYouTubeCaptions = true;
+ this.youTubePlayer.playVideo();
+ setTimeout(() => {
+ if ( ! apiTriggered ) {
+ setTimeout(() => {
+ thisObj.youTubePlayer.pauseVideo();
+ deferred.resolve();
+ }, 500);
+ }
+ },500);
+ }
return promise;
};
- AblePlayer.prototype.getYouTubeTimedTextUrl = function (youTubeId, trackName, trackLang) {
+ AblePlayer.prototype.getYouTubePosterUrl = function (youTubeId, width) {
- // return URL for retrieving WebVTT source via YouTube's timedtext API
- // Note: This API seems to be undocumented, and could break anytime
- var url = 'https://www.youtube.com/api/timedtext?fmt=vtt';
- url += '&v=' + youTubeId;
- url += '&lang=' + trackLang;
- // if track has a value in the name field, it's *required* in the URL
- if (trackName !== '') {
- url += '&name=' + trackName;
+ var url = 'https://img.youtube.com/vi/' + youTubeId;
+ if (width == '120') {
+ return url + '/default.jpg';
+ } else if (width == '320') {
+ return url + '/mqdefault.jpg';
+ } else if (width == '480') {
+ return url + '/hqdefault.jpg';
+ } else if (width == '640') {
+ return url + '/sddefault.jpg';
+ } else if (width == '1280') {
+ return url + '/hq720.jpg';
+ } else if ( width == '1920' ) {
+ return url + '/maxresdefault.jpg';
}
- return url;
+ return false;
};
+ AblePlayer.prototype.getYouTubeId = function (url) {
- AblePlayer.prototype.getYouTubeCaptionCues = function (youTubeId) {
-
- var deferred, promise, thisObj;
-
- var deferred = new $.Deferred();
- var promise = deferred.promise();
-
- thisObj = this;
-
- this.tracks = [];
- this.tracks.push({
- 'kind': 'captions',
- 'src': 'some_file.vtt',
- 'language': 'en',
- 'label': 'Fake English captions'
- });
- deferred.resolve();
- return promise;
- };
+ var idStartPos, id;
- AblePlayer.prototype.initYouTubeCaptionModule = function () {
-
- // This function is called when YouTube onApiChange event fires
- // to indicate that the player has loaded (or unloaded) a module with exposed API methods
- // it isn't fired until the video starts playing
- // and only fires if captions are available for this video (automated captions don't count)
- // If no captions are available, onApichange event never fires & this function is never called
-
- // YouTube iFrame API documentation is incomplete related to captions
- // Found undocumented features on user forums and by playing around
- // Details are here: http://terrillthompson.com/blog/648
- // Summary:
- // User might get either the AS3 (Flash) or HTML5 YouTube player
- // The API uses a different caption module for each player (AS3 = 'cc'; HTML5 = 'captions')
- // There are differences in the data and methods available through these modules
- // This function therefore is used to determine which captions module is being used
- // If it's a known module, this.ytCaptionModule will be used elsewhere to control captions
- var options, fontSize, displaySettings;
-
- options = this.youTubePlayer.getOptions();
- if (options.length) {
- for (var i=0; i)
- // so use these
- this.hasCaptions = true;
- this.usingYouTubeCaptions = true;
- }
- break;
- }
- else if (options[i] == 'captions') { // this is the HTML5 player
- this.ytCaptionModule = 'captions';
- if (!this.hasCaptions) {
- // there are captions available via other sources (e.g., )
- // so use these
- this.hasCaptions = true;
- this.usingYouTubeCaptions = true;
- }
- break;
- }
- }
- if (typeof this.ytCaptionModule !== 'undefined') {
- if (this.usingYouTubeCaptions) {
- // set default languaage
- this.youTubePlayer.setOption(this.ytCaptionModule, 'track', {'languageCode': this.captionLang});
- // set font size using Able Player prefs (values are -1, 0, 1, 2, and 3, where 0 is default)
- this.youTubePlayer.setOption(this.ytCaptionModule,'fontSize',this.translatePrefs('size',this.prefCaptionsSize,'youtube'));
- // ideally could set other display options too, but no others seem to be supported by setOption()
- }
- else {
- // now that we know which cc module was loaded, unload it!
- // we don't want it if we're using local elements for captions
- this.youTubePlayer.unloadModule(this.ytCaptionModule)
- }
- }
- }
- else {
- // no modules were loaded onApiChange
- // unfortunately, gonna have to disable captions if we can't control them
- this.hasCaptions = false;
- this.usingYouTubeCaptions = false;
+ if (url.indexOf('youtu') !== -1) {
+ url = url.trim();
+ idStartPos = url.length - 11;
+ id = url.substring(idStartPos);
+ return id;
+ } else {
+ return url;
}
- this.refreshControls('captions');
- };
-
- AblePlayer.prototype.getYouTubePosterUrl = function (youTubeId, width) {
-
- // return a URL for retrieving a YouTube poster image
- // supported values of width: 120, 320, 480, 640
-
- var url = 'https://img.youtube.com/vi/' + youTubeId;
- if (width == '120') {
- // default (small) thumbnail, 120 x 90
- return url + '/default.jpg';
- }
- else if (width == '320') {
- // medium quality thumbnail, 320 x 180
- return url + '/hqdefault.jpg';
- }
- else if (width == '480') {
- // high quality thumbnail, 480 x 360
- return url + '/hqdefault.jpg';
- }
- else if (width == '640') {
- // standard definition poster image, 640 x 480
- return url + '/sddefault.jpg';
- }
- return false;
- };
+};
})(jQuery);
(function ($) {
- // Events:
- // startTracking(event, position)
- // tracking(event, position)
- // stopTracking(event, position)
- window. AccessibleSlider = function(mediaType, div, orientation, length, min, max, bigInterval, label, className, trackingMedia, initialState) {
+ window.AccessibleSlider = function(div, orientation, length, min, max, bigInterval, label, className, trackingMedia, initialState) {
- // mediaType is either 'audio' or 'video'
- // div is the host element around which the slider will be built
- // orientation is either 'horizontal' or 'vertical'
- // length is the width or height of the slider, depending on orientation
- // min is the low end of the slider scale
- // max is the high end of the slider scale
- // bigInterval is the number of steps supported by page up/page down (set to 0 if not supported)
- // (smallInterval, defined as nextStep below, is always set to 1) - this is the interval supported by arrow keys
- // label is used within an aria-label attribute to identify the slider to screen reader users
- // className is used as the root within class names (e.g., 'able-' + classname + '-head')
- // trackingMedia is true if this is a media timeline; otherwise false
- // initialState is either 'visible' or 'hidden'
var thisObj, coords;
thisObj = this;
- // Initialize some variables.
- this.position = 0; // Note: position does not change while tracking.
+ this.position = 0;
this.tracking = false;
- this.trackDevice = null; // 'mouse' or 'keyboard'
+ this.trackDevice = null;
this.keyTrackPosition = 0;
this.lastTrackPosition = 0;
this.nextStep = 1;
@@ -6106,25 +5896,21 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('
');
this.playedDiv = $('
');
}
- // Add a seekhead
this.seekHead = $('',{
- 'orientation': orientation,
+ 'aria-orientation': orientation,
'class': 'able-' + className + '-head'
});
if (initialState === 'visible') {
this.seekHead.attr('tabindex', '0');
- }
- else {
+ } else {
this.seekHead.attr('tabindex', '-1');
}
- // Since head is focusable, it gets the aria roles/titles.
this.seekHead.attr({
'role': 'slider',
'aria-label': label,
@@ -6132,11 +5918,21 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('');
this.bodyDiv.append(this.timeTooltip);
this.timeTooltip.attr('role', 'tooltip');
this.timeTooltip.addClass('able-tooltip');
+ this.timeTooltip.on('mouseenter focus', function(){
+ thisObj.overTooltip = true;
+ clearInterval(thisObj.timeTooltipTimeoutId);
+ });
+ this.timeTooltip.on('mouseleave blur', function(){
+ thisObj.overTooltip = false;
+ $(this).hide();
+ });
this.timeTooltip.hide();
this.bodyDiv.append(this.loadedDiv);
@@ -6146,15 +5942,14 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('
');
this.wrapperDiv = this.bodyDiv.parent();
- if (this.skin === 'legacy') {
- if (orientation === 'horizontal') {
- this.wrapperDiv.width(length);
- this.loadedDiv.width(0);
- }
- else {
- this.wrapperDiv.height(length);
- this.loadedDiv.height(0);
- }
+ if (this.skin === 'legacy') {
+ if (orientation === 'horizontal') {
+ this.wrapperDiv.width(length);
+ this.loadedDiv.width(0);
+ } else {
+ this.wrapperDiv.height(length);
+ this.loadedDiv.height(0);
+ }
}
this.wrapperDiv.addClass('able-' + className + '-wrapper');
@@ -6164,125 +5959,108 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 0) {
- thisObj.arrowKeyDown(bigInterval);
- }
- // Page down
- else if (e.which === 34 && bigInterval > 0) {
- thisObj.arrowKeyDown(-bigInterval);
- }
- else {
- return;
- }
- e.preventDefault();
- }
- else if (e.type === 'keyup') {
- if (e.which >= 33 && e.which <= 40) {
- if (thisObj.tracking && thisObj.trackDevice === 'keyboard') {
- thisObj.stopTracking(thisObj.keyTrackPosition);
- }
- e.preventDefault();
- }
- }
- if (e.type !== 'mouseup' && e.type !== 'keydown' && e.type !== 'keydown') {
- thisObj.refreshTooltip();
- }
+ 'mouseenter mouseleave mousemove mousedown mouseup keydown keyup touchstart touchmove touchend', function (e) {
+
+ if ( e.button == 2 && e.type == 'mousedown' ) {
+ return;
+ }
+ coords = thisObj.pointerEventToXY(e);
+ let keyPressed = e.key;
+
+ if (e.type === 'mouseenter') {
+ thisObj.overBody = true;
+ thisObj.overBodyMousePos = {
+ x: coords.x,
+ y: coords.y
+ };
+ } else if (e.type === 'mouseleave') {
+ thisObj.overBody = false;
+ thisObj.overBodyMousePos = null;
+ if (!thisObj.overHead && thisObj.tracking && thisObj.trackDevice === 'mouse') {
+ thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
+ }
+ } else if (e.type === 'mousemove' || e.type === 'touchmove') {
+ thisObj.overBodyMousePos = {
+ x: coords.x,
+ y: coords.y
+ };
+ if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
+ thisObj.trackHeadAtPageX(coords.x);
+ }
+ } else if (e.type === 'mousedown' || e.type === 'touchstart') {
+ thisObj.startTracking('mouse', thisObj.pageXToPosition(coords.x));
+ thisObj.trackHeadAtPageX(coords.x);
+ if (!thisObj.seekHead.is(':focus')) {
+ thisObj.seekHead.focus();
+ }
+ e.preventDefault();
+ } else if (e.type === 'mouseup' || e.type === 'touchend') {
+ if (thisObj.tracking && thisObj.trackDevice === 'mouse') {
+ thisObj.stopTracking(thisObj.pageXToPosition(coords.x));
+ }
+ } else if (e.type === 'keydown') {
+ if (e.key === 'Home') {
+ thisObj.trackImmediatelyTo(0);
+ } else if (e.key === 'End') {
+ thisObj.trackImmediatelyTo(thisObj.duration);
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
+ thisObj.arrowKeyDown(-1);
+ } else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
+ thisObj.arrowKeyDown(1);
+ } else if (e.key === 'PageUp' && bigInterval > 0) {
+ thisObj.arrowKeyDown(bigInterval);
+ } else if (e.key === 'PageDown' && bigInterval > 0) {
+ thisObj.arrowKeyDown(-bigInterval);
+ } else {
+ return;
+ }
+ e.preventDefault();
+ } else if (e.type === 'keyup') {
+ if ( keyPressed === e.key ) {
+ if (thisObj.tracking && thisObj.trackDevice === 'keyboard') {
+ thisObj.stopTracking(thisObj.keyTrackPosition);
+ }
+ e.preventDefault();
+ }
+ }
+ if (!thisObj.overTooltip && e.type !== 'mouseup' && e.type !== 'keydown' && e.type !== 'keydown') {
+ thisObj.refreshTooltip();
+ }
});
}
@@ -6295,27 +6073,7 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 0) {
+ } else if (pMinutes > 0) {
descriptionText = pMinutes +
' ' + pMinuteWord +
', ' + pSeconds +
' ' + pSecondWord;
- }
- else {
+ } else {
descriptionText = pSeconds + ' ' + pSecondWord;
}
- /* Comment to stop live region from generating or being used. */
if (!this.liveAriaRegion) {
this.liveAriaRegion = $('', {
'class': 'able-offscreen',
@@ -6462,7 +6216,6 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' formatSecondsAsColonTime()
var dHours = Math.floor(seconds / 3600);
var dMinutes = Math.floor(seconds / 60) % 60;
var dSeconds = Math.floor(seconds % 60);
@@ -6515,29 +6275,24 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('',{
'id': volumeSliderId,
@@ -6570,247 +6317,110 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('',{
- 'class': 'able-volume-track'
- });
- this.$volumeSliderTrackOn = $('',{
- 'class': 'able-volume-track able-volume-track-on'
- });
- this.$volumeSliderHead = $('
',{
- 'class': 'able-volume-head',
- 'role': 'slider',
- 'aria-orientation': 'vertical',
- 'aria-label': this.tt.volumeUpDown,
- 'aria-valuemin': 0,
- 'aria-valuemax': 10,
- 'aria-valuenow': this.volume,
- 'tabindex': -1
- });
- this.$volumeSliderTrack.append(this.$volumeSliderTrackOn,this.$volumeSliderHead);
- this.$volumeAlert = $('
',{
- 'class': 'able-offscreen',
- 'aria-live': 'assertive',
- 'aria-atomic': 'true'
+ this.$volumeRange = $('
',{
+ 'type': 'range',
+ 'min': '0',
+ 'max': '10',
+ 'step': '1',
+ 'orient': 'vertical',
+ 'aria-label': this.translate( 'volumeUpDown', 'Volume up down' ),
+ 'value': this.volume
});
volumePct = parseInt(thisObj.volume) / 10 * 100;
this.$volumeHelp = $('
',{
'id': volumeHelpId,
- 'class': 'able-volume-help'
- }).text(volumePct + '%, ' + this.tt.volumeHelp);
- this.$volumeButton.attr({
- 'aria-describedby': volumeHelpId
- });
- this.$volumeSlider.append(this.$volumeSliderTooltip,this.$volumeSliderTrack,this.$volumeAlert,this.$volumeHelp)
- $div.append(this.$volumeSlider);
- this.refreshVolumeSlider(this.volume);
-
- // add event listeners
- this.$volumeSliderHead.on('mousedown',function (e) {
- e.preventDefault(); // prevent text selection (implications?)
- thisObj.draggingVolume = true;
- thisObj.volumeHeadPositionTop = $(this).offset().top;
- });
+ 'class': 'able-volume-help',
+ 'aria-live': 'polite'
+ }).text(volumePct + '%');
+ volumeLabel = this.$volumeButton.attr( 'aria-label' );
+ this.$volumeButton.attr( 'aria-label', volumeLabel + ' ' + volumePct + '%');
+ this.$volumeSlider.append(this.$volumeSliderTooltip,this.$volumeRange,this.$volumeHelp);
+ volumeHeight = this.$volumeButton.parents( '.able-control-row' )[0];
+ this.$volumeSlider.css( 'bottom', volumeHeight.offsetHeight );
- // prevent dragging after mouseup as mouseup not detected over iframe (YouTube)
- this.$mediaContainer.on('mouseover',function (e) {
- if(thisObj.player == 'youtube'){
- thisObj.draggingVolume = false;
- }
- });
+ $div.append(this.$volumeSlider);
- $(document).on('mouseup',function (e) {
- thisObj.draggingVolume = false;
+ this.$volumeRange.on('change',function (e) {
+ thisObj.handleVolumeChange($(this).val());
});
- $(document).on('mousemove',function (e) {
- if (thisObj.draggingVolume) {
- x = e.pageX;
- y = e.pageY;
- thisObj.moveVolumeHead(y);
- }
+ this.$volumeRange.on('input',function (e) {
+ thisObj.handleVolumeChange($(this).val());
});
- this.$volumeSliderHead.on('keydown',function (e) {
+ this.$volumeRange.on('keydown',function (e) {
- // Left arrow or down arrow
- if (e.which === 37 || e.which === 40) {
- thisObj.handleVolume('down');
- }
- // Right arrow or up arrow
- else if (e.which === 39 || e.which === 38) {
- thisObj.handleVolume('up');
- }
- // Escape key or Enter key or Tab key
- else if (e.which === 27 || e.which === 13 || e.which === 9) {
- // close popup
+ if (e.key === 'Escape' || e.key === 'Tab' || e.key === 'Enter') {
if (thisObj.$volumeSlider.is(':visible')) {
- thisObj.closingVolume = true; // stopgap
+ thisObj.closingVolume = true;
thisObj.hideVolumePopup();
+ } else {
+ if (!thisObj.closingVolume) {
+ thisObj.showVolumePopup();
+ }
}
- else {
- if (!thisObj.closingVolume) {
- thisObj.showVolumePopup();
- }
- }
- }
- else {
+ } else {
return;
}
- e.preventDefault();
});
};
- AblePlayer.prototype.refreshVolumeSlider = function(volume) {
+ AblePlayer.prototype.refreshVolumeHelp = function(volume) {
- // adjust slider position based on current volume
- var volumePct, volumePctText;
+ var volumePct;
volumePct = (volume/10) * 100;
- volumePctText = volumePct + '%';
-
- var trackOnHeight, trackOnTop, headTop;
- trackOnHeight = volume * this.volumeTickHeight;
- trackOnTop = this.volumeTrackHeight - trackOnHeight;
- headTop = trackOnTop - this.volumeHeadHeight;
-
- if (this.$volumeSliderTrackOn) {
- this.$volumeSliderTrackOn.css({
- 'height': trackOnHeight + 'px',
- 'top': trackOnTop + 'px'
- });
- }
- if (this.$volumeSliderHead) {
- this.$volumeSliderHead.attr({
- 'aria-valuenow': volume,
- 'aria-valuetext': volumePctText
- });
- this.$volumeSliderHead.css({
- 'top': headTop + 'px'
- });
- }
- if (this.$volumeAlert) {
- this.$volumeAlert.text(volumePct + '%');
- }
+
+ if (this.$volumeHelp) {
+ this.$volumeHelp.text(volumePct + '%');
+ }
+
+ this.$volumeRange.attr('value',volume);
};
AblePlayer.prototype.refreshVolumeButton = function(volume) {
- var volumeName, volumePct, volumeLabel, volumeIconClass, volumeImg, newSvgData;
+ var volumeName, volumePct, volumeLabel;
volumeName = this.getVolumeName(volume);
volumePct = (volume/10) * 100;
- volumeLabel = this.tt.volume + ' ' + volumePct + '%';
+ volumeLabel = this.translate( 'volume', 'Volume' ) + ' ' + volumePct + '%';
- if (this.iconType === 'font') {
- volumeIconClass = 'icon-volume-' + volumeName;
- this.$volumeButton.find('span').first().removeClass().addClass(volumeIconClass);
- this.$volumeButton.find('span.able-clipped').text(volumeLabel);
- }
- else if (this.iconType === 'image') {
- volumeImg = this.imgPath + 'volume-' + volumeName + '.png';
- this.$volumeButton.find('img').attr('src',volumeImg);
- }
- else if (this.iconType === 'svg') {
- if (volumeName !== 'mute') {
- volumeName = 'volume-' + volumeName;
- }
- newSvgData = this.getSvgData(volumeName);
- this.$volumeButton.find('svg').attr('viewBox',newSvgData[0]);
- this.$volumeButton.find('path').attr('d',newSvgData[1]);
- }
+ this.getIcon( this.$volumeButton, 'volume-' + volumeName );
+ this.$volumeButton.attr( 'aria-label', volumeLabel );
};
- AblePlayer.prototype.moveVolumeHead = function(y) {
-
- // y is current position after mousemove
- var diff, direction, ticksDiff, newVolume, maxedOut;
+ AblePlayer.prototype.handleVolumeButtonClick = function() {
- var diff = this.volumeHeadPositionTop - y;
-
- // only move the volume head if user had dragged at least one tick
- // this is more efficient, plus creates a "snapping' effect
- if (Math.abs(diff) > this.volumeTickHeight) {
- if (diff > 0) {
- direction = 'up';
- }
- else {
- direction = 'down';
- }
- if (direction == 'up' && this.volume == 10) {
- // can't go any higher
- return;
- }
- else if (direction == 'down' && this.volume == 0) {
- // can't go any lower
- return;
- }
- else {
- ticksDiff = Math.round(Math.abs(diff) / this.volumeTickHeight);
- if (direction == 'up') {
- newVolume = this.volume + ticksDiff;
- if (newVolume > 10) {
- newVolume = 10;
- }
- }
- else { // direction is down
- newVolume = this.volume - ticksDiff;
- if (newVolume < 0) {
- newVolume = 0;
- }
- }
- this.setVolume(newVolume); // this.volume will be updated after volumechange event fires (event.js)
- this.refreshVolumeSlider(newVolume);
- this.refreshVolumeButton(newVolume);
- this.volumeHeadPositionTop = y;
- }
+ if (this.$volumeSlider.is(':visible')) {
+ this.hideVolumePopup();
+ } else {
+ this.showVolumePopup();
}
};
- AblePlayer.prototype.handleVolume = function(direction) {
-
- // 'direction is either 'up','down', or an ASCII key code 49-57 (numeric keys 1-9)
- // Action: calculate and change the volume
- // Don't change this.volume and this.volumeButton yet - wait for 'volumechange' event to fire (event.js)
-
- // If NO direction is provided, user has just clicked on the Volume button
- // Action: show slider
- var volume;
-
- if (typeof direction === 'undefined') {
- if (this.$volumeSlider.is(':visible')) {
- this.hideVolumePopup();
- }
- else {
- if (!this.closingVolume) {
- this.showVolumePopup();
- }
- }
- return;
+ AblePlayer.prototype.handleVolumeKeystroke = function(volume) {
+ if (this.isMuted() && volume > 0) {
+ this.setMute(false);
+ } else if (volume === 0) {
+ this.setMute(true);
+ } else {
+ this.setVolume(volume);
+ this.refreshVolumeHelp(volume);
+ this.refreshVolumeButton(volume);
}
+ };
- if (direction >= 49 && direction <= 57) {
- volume = direction - 48;
- }
- else {
- volume = this.getVolume();
+ AblePlayer.prototype.handleVolumeChange = function(volume) {
- if (direction === 'up' && volume < 10) {
- volume += 1;
- }
- else if (direction === 'down' && volume > 0) {
- volume -= 1;
- }
- }
if (this.isMuted() && volume > 0) {
this.setMute(false);
- }
- else if (volume === 0) {
+ } else if (volume === 0) {
this.setMute(true);
- }
- else {
- this.setVolume(volume); // this.volume will be updated after volumechange event fires (event.js)
- this.refreshVolumeSlider(volume);
+ } else {
+ this.setVolume(volume);
+ this.refreshVolumeHelp(volume);
this.refreshVolumeButton(volume);
}
};
@@ -6819,8 +6429,7 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('',{
- 'class': 'modalCloseButton',
- 'title': thisObj.closeButtonLabel,
- 'aria-label': thisObj.closeButtonLabel
- }).text('X');
- closeButton.keydown(function (e) {
- // Space key down
- if (e.which === 32) {
- thisObj.hide();
- }
- }).click(function () {
+ var closeButton = $('
',{
+ 'class': 'modalCloseButton',
+ 'title': thisObj.closeButtonLabel,
+ 'aria-label': thisObj.closeButtonLabel
+ }).text('×');
+ closeButton.on( 'keydown', function (e) {
+ if (e.key === ' ') {
thisObj.hide();
- });
-
- var titleH1 = $(' ');
- titleH1.attr('id', 'modalTitle-' + this.baseId);
- titleH1.css('text-align', 'center');
- titleH1.text(title);
+ }
+ }).on( 'click', function () {
+ thisObj.hide();
+ });
- $descDiv.attr('id', 'modalDesc-' + this.baseId);
+ var titleH1 = $(' ');
+ titleH1.attr('id', 'modalTitle-' + this.baseId);
+ titleH1.text(title);
+ this.titleH1 = titleH1;
- modal.attr({
- 'aria-labelledby': 'modalTitle-' + this.baseId,
- 'aria-describedby': 'modalDesc-' + this.baseId
- });
- modal.prepend(titleH1);
- modal.prepend(closeButton);
- }
+ modal.attr({
+ 'aria-labelledby': 'modalTitle-' + this.baseId,
+ });
+ var modalHeader = $( '', {
+ 'class': 'able-modal-header'
+ });
+ modalHeader.prepend(titleH1);
+ modalHeader.prepend(closeButton);
+ modal.prepend(modalHeader);
modal.attr({
'aria-hidden': 'true',
- 'role': dialogRole
+ 'role': 'dialog',
+ 'aria-modal': 'true'
});
- modal.keydown(function (e) {
- // Escape
- if (e.which === 27) {
- if (thisObj.escapeHook) {
- thisObj.escapeHook(e, this);
- }
- else {
- thisObj.hide();
- e.preventDefault();
- }
- }
- // Tab
- else if (e.which === 9) {
- // Manually loop tab navigation inside the modal.
+ modal.on( 'keydown', function (e) {
+ if (e.key === 'Escape') {
+ thisObj.hide();
+ e.preventDefault();
+ } else if (e.key === 'Tab') {
var parts = modal.find('*');
var focusable = parts.filter(focusableElementsSelector).filter(':visible');
@@ -7046,15 +6602,13 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' *').not('.able-modal-overlay').not('.able-modal-dialog').removeAttr('aria-hidden');
+ if ( $( 'body' ).hasClass( 'able-modal-active' ) ) {
+ $( 'body > *') .not('.able-modal-overlay').not('.able-modal-dialog').removeAttr('inert');
+ $( 'body' ).removeClass( 'able-modal-active' );
+ }
};
AccessibleDialog.prototype.show = function () {
if (!this.overlay) {
- // Generate overlay.
var overlay = $('
').attr({
'class': 'able-modal-overlay',
'tabindex': '-1'
@@ -7075,13 +6631,14 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' *').not('.able-modal-overlay').not('.able-modal-dialog').attr('aria-hidden', 'true');
+ $('body > *').not('.able-modal-overlay').not('.able-modal-dialog').attr('inert', true);
+ $( 'body' ).addClass( 'able-modal-active' );
this.overlay.css('display', 'block');
this.modal.css('display', 'block');
@@ -7096,8 +6653,7 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' *').not('.able-modal-overlay').not('.able-modal-dialog').removeAttr('aria-hidden');
+ $('body > *').not('.able-modal-overlay').not('.able-modal-dialog').removeAttr('inert');
+ $( 'body' ).removeClass( 'able-modal-active' );
- this.focusedElementBeforeModal.focus();
+ this.focusedElementBeforeModal.trigger('focus');
};
- AccessibleDialog.prototype.getInputs = function () {
+ AccessibleDialog.prototype.getInputs = function () {
- // return an array of input elements within this dialog
- if (this.modal) {
- var inputs = this.modal.find('input');
+ if (this.modal) {
+ var inputs = this.modal.find('input');
return inputs;
}
return false;
@@ -7125,481 +6681,525 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 6) {
+ headingNumber = 6;
+ }
+ }
+ return headingNumber;
+ };
- var $parents, $foundHeadings, numHeadings, headingType, headingNumber;
+ AblePlayer.prototype.countProperties = function (obj) {
+ var count, prop;
+ count = 0;
+ for (prop in obj) {
+ if (obj.hasOwnProperty(prop)) {
+ ++count;
+ }
+ }
+ return count;
+ };
- $parents = $element.parents();
- $parents.each(function(){
- $foundHeadings = $(this).children(':header');
- numHeadings = $foundHeadings.length;
- if (numHeadings) {
- headingType = $foundHeadings.eq(numHeadings-1).prop('tagName');
- return false;
- }
- });
- if (typeof headingType === 'undefined') {
- // page has no headings
- headingNumber = 1;
- }
- else {
- // Increment closest heading by one if less than 6.
- headingNumber = parseInt(headingType[1]);
- headingNumber += 1;
- if (headingNumber > 6) {
- headingNumber = 6;
- }
- }
- return headingNumber;
- };
+ AblePlayer.prototype.formatSecondsAsColonTime = function (
+ seconds,
+ showFullTime
+ ) {
+
+ var dHours, dMinutes, dSeconds, parts, milliSeconds, numShort, i;
+
+ if (showFullTime) {
+ parts = seconds.toString().split(".");
+ if (parts.length === 2) {
+ milliSeconds = parts[1];
+ if (milliSeconds.length < 3) {
+ numShort = 3 - milliSeconds.length;
+ for (i = 1; i <= numShort; i++) {
+ milliSeconds += "0";
+ }
+ }
+ } else {
+ milliSeconds = "000";
+ }
+ }
+ dHours = Math.floor(seconds / 3600);
+ dMinutes = Math.floor(seconds / 60) % 60;
+ dSeconds = Math.floor(seconds % 60);
+ if (dSeconds < 10) {
+ dSeconds = "0" + dSeconds;
+ }
+ if (dHours > 0) {
+ if (dMinutes < 10) {
+ dMinutes = "0" + dMinutes;
+ }
+ if (showFullTime) {
+ return dHours + ":" + dMinutes + ":" + dSeconds + "." + milliSeconds;
+ } else {
+ return dHours + ":" + dMinutes + ":" + dSeconds;
+ }
+ } else {
+ if (showFullTime) {
+ if (dHours < 1) {
+ dHours = "00";
+ } else if (dHours < 10) {
+ dHours = "0" + dHours;
+ }
+ if (dMinutes < 1) {
+ dMinutes = "00";
+ } else if (dMinutes < 10) {
+ dMinutes = "0" + dMinutes;
+ }
+ return dHours + ":" + dMinutes + ":" + dSeconds + "." + milliSeconds;
+ } else {
+ return dMinutes + ":" + dSeconds;
+ }
+ }
+ };
- AblePlayer.prototype.countProperties = function(obj) {
+ AblePlayer.prototype.getSecondsFromColonTime = function (timeStr) {
+ var timeParts, hours, minutes, seconds;
+
+ timeParts = timeStr.split(":");
+ if (timeParts.length === 3) {
+ hours = parseInt(timeParts[0]);
+ minutes = parseInt(timeParts[1]);
+ seconds = parseFloat(timeParts[2]);
+ return hours * 3600 + minutes * 60 + seconds;
+ } else if (timeParts.length === 2) {
+ minutes = parseInt(timeParts[0]);
+ seconds = parseFloat(timeParts[1]);
+ return minutes * 60 + seconds;
+ } else if (timeParts.length === 1) {
+ seconds = parseFloat(timeParts[0]);
+ return seconds;
+ }
+ };
- // returns the number of properties in an object
- var count, prop;
- count = 0;
- for (prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- ++count;
- }
- }
- return count;
- };
+ AblePlayer.prototype.capitalizeFirstLetter = function (string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ };
- AblePlayer.prototype.formatSecondsAsColonTime = function (seconds, showFullTime) {
+ AblePlayer.prototype.roundDown = function (value, decimals) {
+ return Number(Math.floor(value + "e" + decimals) + "e-" + decimals);
+ };
- // Takes seconds and converts to string of form hh:mm:ss
- // If showFullTime is true, shows 00 for hours if time is less than an hour
- // and show milliseconds (e.g., 00:00:04.123 as in Video Track Sorter)
- // Otherwise, omits empty hours and milliseconds (e.g., 00:04 as in timer on controller)
+ AblePlayer.prototype.defer = function() {
+ const self = this;
+ const promise = new Promise((resolve, reject) => {
+ self.resolve = resolve;
+ self.reject = reject;
+ self.promise = () => promise;
+ });
+ }
- var dHours, dMinutes, dSeconds,
- parts, milliSeconds, numShort, i;
+ AblePlayer.prototype.getScript = function( source, callback ) {
+ var script = document.createElement('script');
+ var prior = document.getElementsByTagName('script')[0];
+ script.async = 1;
- if (showFullTime) {
- // preserve milliseconds, if included in seconds
- parts = seconds.toString().split('.');
- if (parts.length === 2) {
- milliSeconds = parts[1];
- if (milliSeconds.length < 3) {
- numShort = 3 - milliSeconds.length;
- for (i=1; i <= numShort; i++) {
- milliSeconds += '0';
- }
- }
- }
- else {
- milliSeconds = '000';
- }
- }
- dHours = Math.floor(seconds / 3600);
- dMinutes = Math.floor(seconds / 60) % 60;
- dSeconds = Math.floor(seconds % 60);
- if (dSeconds < 10) {
- dSeconds = '0' + dSeconds;
- }
- if (dHours > 0) {
- if (dMinutes < 10) {
- dMinutes = '0' + dMinutes;
- }
- if (showFullTime) {
- return dHours + ':' + dMinutes + ':' + dSeconds + '.' + milliSeconds;
- }
- else {
- return dHours + ':' + dMinutes + ':' + dSeconds;
- }
- }
- else {
- if (showFullTime) {
- if (dHours < 1) {
- dHours = '00';
- }
- else if (dHours < 10) {
- dHours = '0' + dHours;
- }
- if (dMinutes < 1) {
- dMinutes = '00';
- }
- else if (dMinutes < 10) {
- dMinutes = '0' + dMinutes;
- }
- return dHours + ':' + dMinutes + ':' + dSeconds + '.' + milliSeconds;
- }
- else {
- return dMinutes + ':' + dSeconds;
+ script.onload = script.onreadystatechange = function( _, isAbort ) {
+ if ( isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) {
+ script.onload = script.onreadystatechange = null;
+ script = undefined;
+
+ if ( !isAbort && callback ) {
+ setTimeout(callback, 0);
}
}
};
- AblePlayer.prototype.getSecondsFromColonTime = function (timeStr) {
+ script.src = source;
+ prior.parentNode.insertBefore(script, prior);
+ }
- // Converts string of form hh:mm:ss to seconds
- var timeParts, hours, minutes, seconds, newTime;
+ AblePlayer.prototype.hasAttr = function (object, attribute) {
- timeParts = timeStr.split(':');
- if (timeParts.length === 3) {
- hours = parseInt(timeParts[0]);
- minutes = parseInt(timeParts[1]);
- seconds = parseFloat(timeParts[2]);
- return ((hours * 3600) + (minutes * 60) + (seconds));
- }
- else if (timeParts.length === 2) {
- minutes = parseInt(timeParts[0]);
- seconds = parseFloat(timeParts[1]);
- return ((minutes * 60) + (seconds));
- }
- else if (timeParts.length === 1) {
- seconds = parseFloat(timeParts[0]);
- return seconds;
- }
- };
+ var attr = object.attr(attribute);
- AblePlayer.prototype.capitalizeFirstLetter = function (string) {
- return string.charAt(0).toUpperCase() + string.slice(1);
- };
+ if (typeof attr !== typeof undefined && attr !== false) {
+ return true;
+ } else {
+ return false;
+ }
+ };
- AblePlayer.prototype.roundDown = function (value, decimals) {
+})(jQuery);
- // round value down to the nearest X decimal points
- // where X is the value of the decimals parameter
- return Number(Math.floor(value+'e'+decimals)+'e-'+decimals);
- };
+(function ($) {
+ AblePlayer.prototype.initDescription = function() {
- AblePlayer.prototype.hasAttr = function (object, attribute) {
- // surprisingly, there is no hasAttr() function in Jquery as of 3.2.1
- // return true if object has attribute; otherwise false
- // selector is a Jquery object
- // attribute is a string
- var attr = object.attr(attribute);
- // For some browsers, `attr` is undefined; for others,
- // `attr` is false. Check for both.
- if (typeof attr !== typeof undefined && attr !== false) {
- return true;
- }
- else {
- return false;
- }
- };
+ var deferred, promise, thisObj;
- Number.isInteger = Number.isInteger || function(value) {
+ deferred = new this.defer();
+ promise = deferred.promise();
+ thisObj = this;
- // polyfill for IE11, which doesn't otherwise support Number.isInteger
- // https://stackoverflow.com/a/31720368/744281
- return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
- };
+ if (this.mediaType === 'audio') {
+ deferred.resolve();
+ }
-})(jQuery);
+ this.descFile = this.$sources.first().attr('data-desc-src');
+ if (typeof this.descFile !== 'undefined') {
+ this.hasOpenDesc = true;
+ } else {
+ this.hasOpenDesc = (this.youTubeDescId || this.vimeoDescId) ? true : false;
+ }
-(function ($) {
- AblePlayer.prototype.initDescription = function() {
+ this.descMethod = null;
+ if (this.hasOpenDesc && this.hasClosedDesc) {
+ this.descMethod = (this.prefDescMethod) ? this.prefDescMethod : 'video';
+ } else if (this.hasOpenDesc) {
+ this.descMethod = 'video';
+ } else if (this.hasClosedDesc) {
+ this.descMethod = 'text';
+ }
- // set default mode for delivering description (open vs closed)
- // based on availability and user preference
-
- // called when player is being built, or when a user
- // toggles the Description button or changes a description-related preference
- // In the latter two scendarios, this.refreshingDesc == true via control.js > handleDescriptionToggle()
+ this.descOn = false;
+ if (this.descMethod) {
+ if (this.prefDesc === 1) {
+ this.descOn = true;
+ } else if (this.prefDesc === 0) {
+ this.descOn = false;
+ } else {
+ this.descOn = (this.defaultStateDescriptions === 1) ? true : false;
+ }
+ }
- // The following variables are applicable to delivery of description:
- // prefDesc == 1 if user wants description (i.e., Description button is on); else 0
- // prefDescFormat == either 'video' or 'text' (as of v4.0.10, prefDescFormat is always 'video')
- // prefDescPause == 1 to pause video when description starts; else 0
- // prefVisibleDesc == 1 to visibly show text-based description area; else 0
- // hasOpenDesc == true if a described version of video is available via data-desc-src attribute
- // hasClosedDesc == true if a description text track is available
- // this.useDescFormat == either 'video' or 'text'; the format ultimately delivered
- // descOn == true if description of either type is on
- // exposeTextDescriptions == true if text description is to be announced audibly; otherwise false
+ if (typeof this.$descDiv === 'undefined' && this.hasClosedDesc ) {
+ this.injectTextDescriptionArea();
+ }
- var thisObj = this;
- if (this.refreshingDesc) {
- this.prevDescFormat = this.useDescFormat;
- }
- else {
- // this is the initial build
- // first, check to see if there's an open-described version of this video
- // checks only the first source since if a described version is provided,
- // it must be provided for all sources
- this.descFile = this.$sources.first().attr('data-desc-src');
- if (typeof this.descFile !== 'undefined') {
- this.hasOpenDesc = true;
+ if (this.descOn) {
+ if (this.descMethod === 'video' && !this.usingDescribedVersion() ) {
+ this.swapDescription();
}
- else {
- // there's no open-described version via data-desc-src,
- // but what about data-youtube-desc-src or data-vimeo-desc-src?
- if (this.youTubeDescId || this.vimeoDescId) {
- this.hasOpenDesc = true;
+ if (this.hasClosedDesc) {
+ if (this.prefDescVisible) {
+ if (typeof this.$descDiv !== 'undefined') {
+ this.$descDiv.show();
+ this.$descDiv.removeClass('able-offscreen');
+ }
+ } else {
+ if (typeof this.$descDiv !== 'undefined') {
+ this.$descDiv.addClass('able-offscreen');
+ }
+ }
+ }
+ } else {
+ if (this.descMethod === 'video') {
+ if (this.usingDescribedVersion()) {
+ this.swapDescription();
}
- else { // there are no open-described versions from any source
- this.hasOpenDesc = false;
+ } else if (this.descMethod === 'text') {
+ if (typeof this.$descDiv !== 'undefined') {
+ this.$descDiv.hide();
+ this.$descDiv.removeClass('able-offscreen');
}
}
}
+ deferred.resolve();
+ return promise;
+ };
- // update this.useDescFormat based on media availability & user preferences
- if (this.prefDesc) {
- if (this.hasOpenDesc && this.hasClosedDesc) {
- // both formats are available. Always use 'video'
- this.useDescFormat = this.prefDescFormat;
- this.descOn = true;
- // Do not pause during descriptions when playing described video
- this.prefDescPause = false;
- }
- else if (this.hasOpenDesc) {
- this.useDescFormat = 'video';
- this.descOn = true;
- }
- else if (this.hasClosedDesc) {
- this.useDescFormat = 'text';
- this.descOn = true;
- }
+ AblePlayer.prototype.usingDescribedVersion = function () {
+
+
+ if (this.player === 'youtube') {
+ return (this.activeYouTubeId === this.youTubeDescId);
+ } else if (this.player === 'vimeo') {
+ return (this.activeVimeoId === this.vimeoDescId);
+ } else {
+ return (this.$sources.first().attr('data-desc-src') === this.$sources.first().attr('src'));
+ }
+ };
+
+ AblePlayer.prototype.initSpeech = function (context) {
+ var thisObj = this;
+
+ function attemptEnableSpeech() {
+ var greeting = new SpeechSynthesisUtterance("\x20");
+ greeting.onend = function () {
+ thisObj.getBrowserVoices();
+ if (
+ (Array.isArray(thisObj.descVoices) && thisObj.descVoices.length) ||
+ context !== "init"
+ ) {
+ thisObj.speechEnabled = true;
+ }
+ };
+ thisObj.synth.speak(greeting);
}
- else { // description button is off
- this.useDescFormat = false;
- this.descOn = false;
+
+ function handleInitialClick() {
+ attemptEnableSpeech();
+ $(document).off("click", handleInitialClick);
}
- if (this.useDescFormat === 'text') {
- // check whether browser supports the Web Speech API
+ if (this.speechEnabled === null) {
if (window.speechSynthesis) {
- // It does!
this.synth = window.speechSynthesis;
- this.descVoices = this.synth.getVoices();
- // select the first voice that matches the track language
- // available languages are identified with local suffixes (e.g., en-US)
- // in case no matching voices are found, use the first voice in the voices array
- this.descVoiceIndex = 0;
- for (var i=0; i
0) {
+ this.descVoices = [];
+ for (var i=0; i 0) {
+ if (prefDescVoice) {
+ prefVoiceFound = false;
+ for (var i=0; i 0) {
+ this.swapTime = this.elapsed;
+ } else {
+ this.swapTime = 0;
+ }
+ if (this.duration > 0) {
+ this.prevDuration = this.duration;
+ }
+
+ if (!this.okToPlay) {
+ this.okToPlay = this.playing;
}
- else {
- // user has requested the non-described version
- this.showAlert(this.tt.alertNonDescribedVersion);
+
+ if (this.descOn) {
+ this.showAlert( this.translate( 'alertDescribedVersion', 'Using the audio described version of this video' ) );
+ } else {
+ this.showAlert( this.translate( 'alertNonDescribedVersion', 'Using the non-described version of this video' ) );
}
+
if (this.player === 'html5') {
- if (this.usingAudioDescription()) {
- // the described version is currently playing. Swap to non-described
+ this.swappingSrc = true;
+ this.paused = true;
+
+ if (this.usingDescribedVersion()) {
for (i=0; i < this.$sources.length; i++) {
- // for all elements, replace src with data-orig-src
- origSrc = this.$sources[i].getAttribute('data-orig-src');
+ origSrc = DOMPurify.sanitize( this.$sources[i].getAttribute('data-orig-src') );
srcType = this.$sources[i].getAttribute('type');
if (origSrc) {
this.$sources[i].setAttribute('src',origSrc);
}
}
- // No need to check for this.initializing
- // This function is only called during initialization
- // if swapping from non-described to described
- this.swappingSrc = true;
- }
- else {
- // the non-described version is currently playing. Swap to described.
+ } else {
for (i=0; i < this.$sources.length; i++) {
- // for all elements, replace src with data-desc-src (if one exists)
- // then store original source in a new data-orig-src attribute
- origSrc = this.$sources[i].getAttribute('src');
- descSrc = this.$sources[i].getAttribute('data-desc-src');
+ origSrc = DOMPurify.sanitize( this.$sources[i].getAttribute('src') );
+ descSrc = DOMPurify.sanitize( this.$sources[i].getAttribute('data-desc-src') );
srcType = this.$sources[i].getAttribute('type');
if (descSrc) {
this.$sources[i].setAttribute('src',descSrc);
this.$sources[i].setAttribute('data-orig-src',origSrc);
}
}
- this.swappingSrc = true;
- }
-
- // now reload the source file.
- if (this.player === 'html5') {
- this.media.load();
}
- }
- else if (this.player === 'youtube') {
- if (this.usingAudioDescription()) {
- // the described version is currently playing. Swap to non-described
- this.activeYouTubeId = this.youTubeId;
- this.showAlert(this.tt.alertNonDescribedVersion);
+ if (this.recreatingPlayer) {
+ return;
}
- else {
- // the non-described version is currently playing. Swap to described.
- this.activeYouTubeId = this.youTubeDescId;
- this.showAlert(this.tt.alertDescribedVersion);
+ if (this.playerCreated) {
+ this.deletePlayer('swap-desc-html');
+ this.recreatePlayer().then(function() {
+ if (!thisObj.loadingMedia) {
+ thisObj.media.load();
+ thisObj.loadingMedia = true;
+ }
+ });
+ } else {
}
- if (typeof this.youTubePlayer !== 'undefined') {
+ } else if (this.player === 'youtube') {
- // retrieve/setup captions for the new video from YouTube
- this.setupAltCaptions().then(function() {
+ this.activeYouTubeId = (this.usingDescribedVersion()) ? this.youTubeId : this.youTubeDescId;
- if (thisObj.playing) {
- // loadVideoById() loads and immediately plays the new video at swapTime
- thisObj.youTubePlayer.loadVideoById(thisObj.activeYouTubeId,thisObj.swapTime);
- }
- else {
- // cueVideoById() loads the new video and seeks to swapTime, but does not play
- thisObj.youTubePlayer.cueVideoById(thisObj.activeYouTubeId,thisObj.swapTime);
- }
- });
+ if (typeof this.youTubePlayer !== 'undefined') {
+ thisObj.swappingSrc = true;
+ if (thisObj.playing) {
+ thisObj.youTubePlayer.loadVideoById(thisObj.activeYouTubeId,thisObj.swapTime);
+ } else {
+ thisObj.youTubePlayer.cueVideoById(thisObj.activeYouTubeId,thisObj.swapTime);
+ }
}
- }
- else if (this.player === 'vimeo') {
- if (this.usingAudioDescription()) {
- // the described version is currently playing. Swap to non-described
- this.activeVimeoId = this.vimeoId;
- this.showAlert(this.tt.alertNonDescribedVersion);
+ if (this.playerCreated) {
+ this.deletePlayer('swap-desc-youtube');
}
- else {
- // the non-described version is currently playing. Swap to described.
+ if (this.recreatingPlayer) {
+ return;
+ }
+ this.recreatePlayer().then(function() {
+ });
+ } else if (this.player === 'vimeo') {
+ if (this.usingDescribedVersion()) {
+ this.activeVimeoId = this.vimeoId;
+ this.showAlert( this.translate( 'alertNonDescribedVersion', 'Using the non-described version of this video' ) );
+ } else {
this.activeVimeoId = this.vimeoDescId;
- this.showAlert(this.tt.alertDescribedVersion);
+ this.showAlert( this.translate( 'alertDescribedVersion', 'Using the audio described version of this video' ) );
}
- // load the new video source
- this.vimeoPlayer.loadVideo(this.activeVimeoId).then(function() {
-
- if (thisObj.playing) {
- // video was playing when user requested an alternative version
- // seek to swapTime and continue playback (playback happens automatically)
- thisObj.vimeoPlayer.setCurrentTime(thisObj.swapTime);
- }
- else {
- // Vimeo autostarts immediately after video loads
- // The "Described" button should not trigger playback, so stop this before the user notices.
- thisObj.vimeoPlayer.pause();
- }
+ if (this.playerCreated) {
+ this.deletePlayer('swap-desc-vimeo');
+ }
+ if (this.recreatingPlayer) {
+ return;
+ }
+ this.recreatePlayer().then(function() {
+ thisObj.vimeoPlayer.loadVideo(thisObj.activeVimeoId).then(function() {
+ if (thisObj.playing) {
+ thisObj.vimeoPlayer.setCurrentTime(thisObj.swapTime);
+ } else {
+ thisObj.vimeoPlayer.pause();
+ }
+ });
});
}
};
AblePlayer.prototype.showDescription = function(now) {
-
- // there's a lot of redundancy between this function and showCaptions
- // Trying to combine them ended up in a mess though. Keeping as is for now.
-
- if (this.swappingSrc || !this.descOn) {
+ if (!this.playing || !this.hasClosedDesc || this.swappingSrc || !this.descOn || ( this.descMethod === 'video' && !this.prefDescVisible ) ) {
return;
}
- var thisObj, i, cues, d, thisDescription, descText, msg;
+ var thisObj, cues, d, thisDescription, descText;
thisObj = this;
var flattenComponentForDescription = function (component) {
var result = [];
if (component.type === 'string') {
result.push(component.value);
- }
- else {
+ } else {
for (var i = 0; i < component.children.length; i++) {
result.push(flattenComponentForDescription(component.children[i]));
}
}
return result.join('');
};
-
+ cues = [];
if (this.selectedDescriptions) {
cues = this.selectedDescriptions.cues;
- }
- else if (this.descriptions.length >= 1) {
+ } else if (this.descriptions.length >= 1) {
cues = this.descriptions[0].cues;
}
- else {
- cues = [];
- }
for (d = 0; d < cues.length; d++) {
if ((cues[d].start <= now) && (cues[d].end > now)) {
thisDescription = d;
@@ -7608,160 +7208,151 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('= 2.5) {
+ speechRate = 3;
+ }
+ this.prefDescRate = speechRate;
+ };
- // Whenever possible we avoid browser sniffing. Better to do feature detection.
- // However, in case it's needed...
- // this function defines a userAgent array that can be used to query for common browsers and OSs
- // NOTE: This would be much simpler with jQuery.browser but that was removed from jQuery 1.9
- // http://api.jquery.com/jQuery.browser/
- this.userAgent = {};
- this.userAgent.browser = {};
+ AblePlayer.prototype.announceDescriptionText = function(context, text) {
- // Test for common browsers
- if (/Firefox[\/\s](\d+\.\d+)/.test(navigator.userAgent)){ //test for Firefox/x.x or Firefox x.x (ignoring remaining digits);
- this.userAgent.browser.name = 'Firefox';
- this.userAgent.browser.version = RegExp.$1; // capture x.x portion
- }
- else if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) { //test for MSIE x.x (IE10 or lower)
- this.userAgent.browser.name = 'Internet Explorer';
- this.userAgent.browser.version = RegExp.$1;
- }
- else if (/Trident.*rv[ :]*(\d+\.\d+)/.test(navigator.userAgent)) { // test for IE11 or higher
- this.userAgent.browser.name = 'Internet Explorer';
- this.userAgent.browser.version = RegExp.$1;
- }
- else if (/Edge[\/\s](\d+\.\d+)/.test(navigator.userAgent)) { // test for MS Edge
- this.userAgent.browser.name = 'Edge';
- this.userAgent.browser.version = RegExp.$1;
- }
- else if (/OPR\/(\d+\.\d+)/i.test(navigator.userAgent)) { // Opera 15 or over
- this.userAgent.browser.name = 'Opera';
- this.userAgent.browser.version = RegExp.$1;
+
+ var thisObj, voiceName, i, voice, pitch, rate, volume, utterance,
+ timeElapsed, secondsElapsed;
+
+ thisObj = this;
+
+ var useFirstVoice = false;
+
+ if (!this.speechEnabled) {
+ this.initSpeech('desc');
}
- else if (/Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)) {
- this.userAgent.browser.name = 'Chrome';
- if (/Chrome[\/\s](\d+\.\d+)/.test(navigator.userAgent)) {
- this.userAgent.browser.version = RegExp.$1;
- }
+
+ if (context === 'sample') {
+ voiceName = $('#' + this.mediaId + '_prefDescVoice').val();
+ pitch = $('#' + this.mediaId + '_prefDescPitch').val();
+ rate = $('#' + this.mediaId + '_prefDescRate').val();
+ volume = $('#' + this.mediaId + '_prefDescVolume').val();
+ } else {
+ voiceName = this.prefDescVoice;
+ pitch = this.prefDescPitch;
+ rate = this.prefDescRate;
+ volume = this.prefDescVolume;
}
- else if (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)) {
- this.userAgent.browser.name = 'Safari';
- if (/Version[\/\s](\d+\.\d+)/.test(navigator.userAgent)) {
- this.userAgent.browser.version = RegExp.$1;
+
+ if (this.descVoices) {
+ if (this.descVoices.length > 0) {
+ if (useFirstVoice) {
+ voice = this.descVoices[0];
+ } else if (voiceName) {
+ for (i = 0; i < this.descVoices.length; i++) {
+ if (this.descVoices[i].name == voiceName) {
+ voice = this.descVoices[i];
+ break;
+ }
+ }
+ }
+ if (typeof voice === 'undefined') {
+ voice = this.descVoices[0];
+ }
}
+ } else {
+ voice = null;
}
- else {
- this.userAgent.browser.name = 'Unknown';
- this.userAgent.browser.version = 'Unknown';
+ utterance = new SpeechSynthesisUtterance();
+ if (voice) {
+ utterance.voice = voice;
}
+ utterance.voiceURI = 'native';
+ utterance.volume = volume;
+ utterance.rate = rate;
+ utterance.pitch = pitch;
+ utterance.text = text;
+ utterance.lang = this.lang;
+ utterance.onstart = function(e) {
+ };
+ utterance.onpause = function(e) {
+ };
+ utterance.onend = function(e) {
+ this.speakingDescription = false;
+ timeElapsed = e.elapsedTime;
+ secondsElapsed = (timeElapsed > 100) ? (e.elapsedTime/1000).toFixed(2) : (e.elapsedTime).toFixed(2);
- // Now test for common operating systems
- if (window.navigator.userAgent.indexOf("Windows NT 6.2") != -1) {
- this.userAgent.os = "Windows 8";
- }
- else if (window.navigator.userAgent.indexOf("Windows NT 6.1") != -1) {
- this.userAgent.os = "Windows 7";
- }
- else if (window.navigator.userAgent.indexOf("Windows NT 6.0") != -1) {
- this.userAgent.os = "Windows Vista";
- }
- else if (window.navigator.userAgent.indexOf("Windows NT 5.1") != -1) {
- this.userAgent.os = "Windows XP";
- }
- else if (window.navigator.userAgent.indexOf("Windows NT 5.0") != -1) {
- this.userAgent.os = "Windows 2000";
- }
- else if (window.navigator.userAgent.indexOf("Mac")!=-1) {
- this.userAgent.os = "Mac/iOS";
- }
- else if (window.navigator.userAgent.indexOf("X11")!=-1) {
- this.userAgent.os = "UNIX";
- }
- else if (window.navigator.userAgent.indexOf("Linux")!=-1) {
- this.userAgent.os = "Linux";
- }
- if (this.debug) {
-
-
-
-
-
- }
- };
+ if (this.debug) {
- AblePlayer.prototype.isUserAgent = function(which) {
+ }
+ if (context === 'description') {
+ if (thisObj.prefDescPause) {
+ if (thisObj.pausedForDescription) {
+ thisObj.playMedia();
+ this.pausedForDescription = false;
+ }
+ }
+ }
+ };
+ utterance.onerror = function(e) {
- var userAgent = navigator.userAgent.toLowerCase();
- if (this.debug) {
-
- }
- if (userAgent.indexOf(which.toLowerCase()) !== -1) {
- return true;
- }
- else {
- return false;
+ };
+ if (this.synth.paused) {
+ this.synth.resume();
}
+ this.synth.speak(utterance);
+ this.speakingDescription = true;
};
+})(jQuery);
+
+(function ($) {
+
AblePlayer.prototype.isIOS = function(version) {
- // return true if this is IOS
- // if version is provided check for a particular version
var userAgent, iOS;
@@ -7770,100 +7361,79 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 0 && this.startTime >= seekable.start(0) && this.startTime <= seekable.end(0)) {
- // ok to seek to startTime
- // canplaythrough will be triggered when seeking is complete
- // this.seeking will be set to false at that point
this.media.currentTime = this.startTime;
- if (this.hasSignLanguage && this.signVideo) {
- // keep sign languge video in sync
- this.signVideo.currentTime = this.startTime;
- }
+ this.seekStatus = 'complete';
+ this.syncSignVideo( { 'time' : this.startTime } );
}
- }
- else if (this.player === 'youtube') {
+ } else if (this.player === 'youtube') {
this.youTubePlayer.seekTo(newTime,true);
if (newTime > 0) {
if (typeof this.$posterImg !== 'undefined') {
this.$posterImg.hide();
}
}
- }
- else if (this.player === 'vimeo') {
+ this.syncSignVideo( {'time' : newTime } );
+ } else if (this.player === 'vimeo') {
this.vimeoPlayer.setCurrentTime(newTime).then(function() {
- // seek finished.
- // successful completion also fires a 'seeked' event (see event.js)
thisObj.elapsed = newTime;
thisObj.refreshControls('timeline');
})
@@ -7873,32 +7443,19 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 0) {
+ duration = this.duration;
+ } else {
+ duration = this.youTubePlayer.getDuration();
+ }
+ } else {
duration = 0;
}
}
if (duration === undefined || isNaN(duration) || duration === -1) {
deferred.resolve(0);
- }
- else {
+ } else {
deferred.resolve(duration);
}
}
@@ -7961,12 +7513,10 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 1) ? true : false;
+ } else {
return false;
}
- }
- else if (this.player === 'youtube') {
- // Youtube supports varying playback rates per video. Only expose controls if more than one playback rate is available.
- if (this.youTubePlayer.getAvailablePlaybackRates().length > 1) {
- return true;
- }
- else {
- return false;
- }
- }
- else if (this.player === 'vimeo') {
- // since this takes longer to determine, it was set previously in initVimeoPlayer()
+ } else if (this.player === 'vimeo') {
return this.vimeoSupportsPlaybackRateChange;
}
};
@@ -8112,98 +7619,115 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 0) {
- // couldn't calculate seekInterval previously; try again.
- this.setSeekInterval();
- }
+ if ( !this.useFixedSeekInterval && !this.seekIntervalCalculated && this.duration > 0) {
+ this.setSeekInterval();
+ }
if (this.seekBar) {
if (this.useChapterTimes) {
lastChapterIndex = this.selectedChapters.cues.length-1;
if (this.selectedChapters.cues[lastChapterIndex] == this.currentChapter) {
- // this is the last chapter
if (this.currentChapter.end !== this.duration) {
- // chapter ends before or after video ends
- // need to adjust seekbar duration to match video end
this.seekBar.setDuration(this.duration - this.currentChapter.start);
- }
- else {
+ } else {
this.seekBar.setDuration(this.chapterDuration);
}
- }
- else {
- // this is not the last chapter
+ } else {
this.seekBar.setDuration(this.chapterDuration);
}
- }
- else {
- if (!(this.duration === undefined || isNaN(this.duration) || this.duration === -1)) {
- this.seekBar.setDuration(this.duration);
- }
+ } else if ( !(this.duration === undefined || isNaN(this.duration) || this.duration === -1) ) {
+ this.seekBar.setDuration(this.duration);
}
if (!(this.seekBar.tracking)) {
- // Only update the aria live region if we have an update pending
- // (from a seek button control) or if the seekBar has focus.
- // We use document.activeElement instead of $(':focus') due to a strange bug:
- // When the seekHead element is focused, .is(':focus') is failing and $(':focus') is returning an undefined element.
updateLive = this.liveUpdatePending || this.seekBar.seekHead.is($(document.activeElement));
this.liveUpdatePending = false;
if (this.useChapterTimes) {
this.seekBar.setPosition(this.chapterElapsed, updateLive);
- }
- else {
+ } else {
this.seekBar.setPosition(this.elapsed, updateLive);
}
}
- // When seeking, display the seek bar time instead of the actual elapsed time.
if (this.seekBar.tracking) {
displayElapsed = this.seekBar.lastTrackPosition;
- }
- else {
- if (this.useChapterTimes) {
- displayElapsed = this.chapterElapsed;
- }
- else {
- displayElapsed = this.elapsed;
- }
+ } else {
+ displayElapsed = ( this.useChapterTimes ) ? this.chapterElapsed : this.elapsed;
}
}
- // update elapsed & duration
if (typeof this.$durationContainer !== 'undefined') {
if (this.useChapterTimes) {
this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.chapterDuration));
- }
- else {
+ } else {
this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.duration));
}
}
@@ -8386,170 +7829,102 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 5) {
- this.seekBar.setWidth(seekbarWidth);
- }
- }
- }
-
- // Update buffering progress.
- // TODO: Currently only using the first HTML5 buffered interval,
- // but this fails sometimes when buffering is split into two or more intervals.
- if (this.player === 'html5') {
- if (this.media.buffered.length > 0) {
- buffered = this.media.buffered.end(0);
- if (this.useChapterTimes) {
- if (buffered > this.chapterDuration) {
- buffered = this.chapterDuration;
- }
- if (this.seekBar) {
- this.seekBar.setBuffered(buffered / this.chapterDuration);
+ if (this.seekBar) {
+ let controlWrapper = this.seekBar.wrapperDiv.parent().parent();
+ leftControls = this.seekBar.wrapperDiv.parent().prev('div.able-left-controls');
+ rightControls = leftControls.next('div.able-right-controls');
+ widthUsed = leftControls.outerWidth(true);
+ rightControls.children().each(function () {
+ if ($(this).attr('role')=='button') {
+ widthUsed += $(this).outerWidth(true) + 5;
}
+ });
+ if (this.fullscreen) {
+ seekbarWidth = $(window).width() - widthUsed;
+ } else {
+ seekbarWidth = controlWrapper.width() - widthUsed - 10;
}
- else {
- if (this.seekBar) {
- if (!isNaN(buffered)) {
- this.seekBar.setBuffered(buffered / duration);
- }
- }
+ if (Math.abs(seekbarWidth - this.seekBar.getWidth()) > 5) {
+ this.seekBar.setWidth(seekbarWidth);
}
}
}
- else if (this.player === 'youtube') {
- if (this.seekBar) {
- this.seekBar.setBuffered(this.youTubePlayer.getVideoLoadedFraction());
+
+ if (this.player === 'html5' && this.media.buffered.length > 0) {
+ buffered = this.media.buffered.end(0);
+ if (this.useChapterTimes) {
+ if (buffered > this.chapterDuration) {
+ buffered = this.chapterDuration;
+ }
+ if (this.seekBar) {
+ this.seekBar.setBuffered(buffered / this.chapterDuration);
+ }
+ } else if ( this.seekBar && !isNaN(buffered) ) {
+ this.seekBar.setBuffered(buffered / duration);
}
+ } else if (this.player === 'youtube' && this.seekBar && this.youTubePlayerReady ) {
+ this.seekBar.setBuffered(this.youTubePlayer.getVideoLoadedFraction());
+ } else if (this.player === 'vimeo') {
}
- else if (this.player === 'vimeo') {
- // TODO: Add support for Vimeo buffering update
- }
- } // end if context == 'timeline' or 'init'
-
- if (context === 'descriptions' || context == 'init'){
+ }
+ if (context === 'descriptions' || context == 'init') {
if (this.$descButton) {
- if (this.descOn) {
- this.$descButton.removeClass('buttonOff').attr('aria-label',this.tt.turnOffDescriptions);
- this.$descButton.find('span.able-clipped').text(this.tt.turnOffDescriptions);
- }
- else {
- this.$descButton.addClass('buttonOff').attr('aria-label',this.tt.turnOnDescriptions);
- this.$descButton.find('span.able-clipped').text(this.tt.turnOnDescriptions);
- }
+ this.toggleButtonState(
+ this.$descButton,
+ this.descOn,
+ this.translate( 'turnOffDescriptions', 'Turn off descriptions' ),
+ this.translate( 'turnOnDescriptions', 'Turn on descriptions' ),
+ );
}
}
- if (context === 'captions' || context == 'init'){
+ if (context === 'captions' || context == 'init') {
if (this.$ccButton) {
captionsCount = this.captions.length;
-
- // Button has a different title depending on the number of captions.
- // If only one caption track, this is "Show captions" and "Hide captions"
- // Otherwise, it is just always "Captions"
- if (!this.captionsOn) {
- this.$ccButton.addClass('buttonOff');
- if (captionsCount === 1) {
- this.$ccButton.attr('aria-label',this.tt.showCaptions);
- this.$ccButton.find('span.able-clipped').text(this.tt.showCaptions);
- }
- }
- else {
- this.$ccButton.removeClass('buttonOff');
- if (captionsCount === 1) {
- this.$ccButton.attr('aria-label',this.tt.hideCaptions);
- this.$ccButton.find('span.able-clipped').text(this.tt.hideCaptions);
- }
- }
-
if (captionsCount > 1) {
this.$ccButton.attr({
- 'aria-label': this.tt.captions,
'aria-haspopup': 'true',
'aria-controls': this.mediaId + '-captions-menu'
});
- this.$ccButton.find('span.able-clipped').text(this.tt.captions);
}
+ var ariaLabelOn = ( captionsCount > 1 ) ? this.translate( 'captions', 'Captions' ) : this.translate( 'showCaptions', 'Show captions' );
+ var ariaLabelOff = ( captionsCount > 1 ) ? this.translate( 'captions', 'Captions' ) : this.translate( 'hideCaptions', 'Hide captions' );
+ var ariaPressed = ( captionsCount > 1 ) ? true : false;
+
+ this.toggleButtonState(
+ this.$ccButton,
+ this.captionsOn,
+ ariaLabelOff,
+ ariaLabelOn,
+ ariaPressed
+ );
}
}
if (context === 'fullscreen' || context == 'init'){
-
if (this.$fullscreenButton) {
if (!this.fullscreen) {
- this.$fullscreenButton.attr('aria-label', this.tt.enterFullScreen);
- if (this.iconType === 'font') {
- this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-collapse').addClass('icon-fullscreen-expand');
- this.$fullscreenButton.find('span.able-clipped').text(this.tt.enterFullScreen);
- }
- else if (this.iconType === 'svg') {
- newSvgData = this.getSvgData('fullscreen-expand');
- this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
- this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
- }
- else {
- this.$fullscreenButton.find('img').attr('src',this.fullscreenExpandButtonImg);
- }
- }
- else {
- this.$fullscreenButton.attr('aria-label',this.tt.exitFullScreen);
- if (this.iconType === 'font') {
- this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-expand').addClass('icon-fullscreen-collapse');
- this.$fullscreenButton.find('span.able-clipped').text(this.tt.exitFullScreen);
- }
- else if (this.iconType === 'svg') {
- newSvgData = this.getSvgData('fullscreen-collapse');
- this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
- this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
- }
- else {
- this.$fullscreenButton.find('img').attr('src',this.fullscreenCollapseButtonImg);
- }
+ this.$fullscreenButton.attr( 'aria-label', this.translate( 'enterFullScreen', 'Enter full screen' ) );
+ this.getIcon( this.$fullscreenButton, 'fullscreen-expand' );
+ } else {
+ this.$fullscreenButton.attr('aria-label', this.translate( 'exitFullScreen', 'Exit full screen' ) );
+ this.getIcon( this.$fullscreenButton, 'fullscreen-collapse' );
}
}
}
if (context === 'playpause' || context == 'init'){
if (typeof this.$bigPlayButton !== 'undefined' && typeof this.seekBar !== 'undefined') {
- // Choose show/hide for big play button and adjust position.
if (this.paused && !this.seekBar.tracking) {
if (!this.hideBigPlayButton) {
this.$bigPlayButton.show();
+ this.$bigPlayButton.attr('aria-hidden', 'false');
}
- if (this.fullscreen) {
- this.$bigPlayButton.width($(window).width());
- this.$bigPlayButton.height($(window).height());
- }
- else {
- this.$bigPlayButton.width(this.$mediaContainer.width());
- this.$bigPlayButton.height(this.$mediaContainer.height());
- }
- }
- else {
+ } else {
this.$bigPlayButton.hide();
+ this.$bigPlayButton.attr('aria-hidden', 'true');
}
}
}
@@ -8557,31 +7932,25 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' onMediaUpdateTime())
- if (this.$status.text() !== this.tt.statusStopped) {
- this.$status.text(this.tt.statusStopped);
- }
- if (this.$playpauseButton.find('span').first().hasClass('icon-pause')) {
- if (this.iconType === 'font') {
- this.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
- this.$playpauseButton.find('span.able-clipped').text(this.tt.play);
- }
- else if (this.iconType === 'svg') {
- newSvgData = this.getSvgData('play');
- this.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
- this.$playpauseButton.find('path').attr('d',newSvgData[1]);
- }
- else {
- this.$playpauseButton.find('img').attr('src',this.playButtonImg);
- }
- }
- }
- else {
- if (typeof this.$status !== 'undefined' && typeof this.seekBar !== 'undefined') {
- // Update the text only if it's changed since it has role="alert";
- // also don't update while tracking, since this may Pause/Play the player but we don't want to send a Pause/Play update.
- this.getPlayerState().then(function(currentState) {
- if (thisObj.$status.text() !== textByState[currentState] && !thisObj.seekBar.tracking) {
- // Debounce updates; only update after status has stayed steadily different for a while
- // "A while" is defined differently depending on context
- if (thisObj.swappingSrc) {
- // this is where most of the chatter occurs (e.g., playing, paused, buffering, playing),
- // so set a longer wait time before writing a status message
- if (!thisObj.debouncingStatus) {
- thisObj.statusMessageThreshold = 2000; // in ms (2 seconds)
- }
- }
- else {
- // for all other contexts (e.g., users clicks Play/Pause)
- // user should receive more rapid feedback
- if (!thisObj.debouncingStatus) {
- thisObj.statusMessageThreshold = 250; // in ms
- }
- }
- timestamp = (new Date()).getTime();
- if (!thisObj.statusDebounceStart) {
- thisObj.statusDebounceStart = timestamp;
- // Call refreshControls() again after allotted time has passed
- thisObj.debouncingStatus = true;
- thisObj.statusTimeout = setTimeout(function () {
- thisObj.debouncingStatus = false;
- thisObj.refreshControls(context);
- }, thisObj.statusMessageThreshold);
+ if (this.$status.text() !== this.translate( 'statusStopped', 'Stopped' ) ) {
+ this.$status.text( this.translate( 'statusStopped', 'Stopped' ) );
+ }
+ this.getIcon( this.$playpauseButton, 'play' );
+ } else if (typeof this.$status !== 'undefined' && typeof this.seekBar !== 'undefined') {
+ this.getPlayerState().then(function(currentState) {
+ if (thisObj.$status.text() !== textByState[currentState] && !thisObj.seekBar.tracking) {
+ if (thisObj.swappingSrc) {
+ if (!thisObj.debouncingStatus) {
+ thisObj.statusMessageThreshold = 2000;
}
- else if ((timestamp - thisObj.statusDebounceStart) > thisObj.statusMessageThreshold) {
- thisObj.$status.text(textByState[currentState]);
- thisObj.statusDebounceStart = null;
- clearTimeout(thisObj.statusTimeout);
- thisObj.statusTimeout = null;
- }
+ } else if (!thisObj.debouncingStatus) {
+ thisObj.statusMessageThreshold = 250;
}
- else {
+ timestamp = (new Date()).getTime();
+ if (!thisObj.statusDebounceStart) {
+ thisObj.statusDebounceStart = timestamp;
+ thisObj.debouncingStatus = true;
+ thisObj.statusTimeout = setTimeout(function () {
+ thisObj.debouncingStatus = false;
+ thisObj.refreshControls(context);
+ }, thisObj.statusMessageThreshold);
+ } else if ((timestamp - thisObj.statusDebounceStart) > thisObj.statusMessageThreshold) {
+ thisObj.$status.text(textByState[currentState]);
thisObj.statusDebounceStart = null;
- thisObj.debouncingStatus = false;
clearTimeout(thisObj.statusTimeout);
thisObj.statusTimeout = null;
}
- // Don't change play/pause button display while using the seek bar (or if YouTube stopped)
- if (!thisObj.seekBar.tracking && !thisObj.stoppingYouTube) {
- if (currentState === 'paused' || currentState === 'stopped' || currentState === 'ended') {
- thisObj.$playpauseButton.attr('aria-label',thisObj.tt.play);
-
- if (thisObj.iconType === 'font') {
- thisObj.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
- thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.play);
- }
- else if (thisObj.iconType === 'svg') {
- newSvgData = thisObj.getSvgData('play');
- thisObj.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
- thisObj.$playpauseButton.find('path').attr('d',newSvgData[1]);
- }
- else {
- thisObj.$playpauseButton.find('img').attr('src',thisObj.playButtonImg);
- }
- }
- else {
- thisObj.$playpauseButton.attr('aria-label',thisObj.tt.pause);
-
- if (thisObj.iconType === 'font') {
- thisObj.$playpauseButton.find('span').first().removeClass('icon-play').addClass('icon-pause');
- thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.pause);
- }
- else if (thisObj.iconType === 'svg') {
- newSvgData = thisObj.getSvgData('pause');
- thisObj.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
- thisObj.$playpauseButton.find('path').attr('d',newSvgData[1]);
- }
- else {
- thisObj.$playpauseButton.find('img').attr('src',thisObj.pauseButtonImg);
- }
- }
+ } else {
+ thisObj.statusDebounceStart = null;
+ thisObj.debouncingStatus = false;
+ clearTimeout(thisObj.statusTimeout);
+ thisObj.statusTimeout = null;
+ }
+ if (!thisObj.seekBar.tracking && !thisObj.stoppingYouTube) {
+ if (currentState === 'paused' || currentState === 'stopped' || currentState === 'ended') {
+ thisObj.$playpauseButton.attr('aria-label',thisObj.tt.play);
+ thisObj.getIcon( thisObj.$playpauseButton, 'play' );
+ } else {
+ thisObj.$playpauseButton.attr('aria-label',thisObj.tt.pause);
+ thisObj.getIcon( thisObj.$playpauseButton, 'pause' );
}
- });
- }
+ }
+ });
}
}
- // Show/hide status bar content conditionally
if (!this.fullscreen) {
statusBarWidthBreakpoint = 300;
- statusBarHeight = this.$statusBarDiv.height();
- speedHeight = this.$statusBarDiv.find('span.able-speed').height();
- if (speedHeight > (statusBarHeight + 5)) {
- // speed bar is wrapping (happens often in German player)
+ if (this.$statusBarDiv.width() < statusBarWidthBreakpoint) {
this.$statusBarDiv.find('span.able-speed').hide();
this.hidingSpeed = true;
- }
- else {
+ } else {
if (this.hidingSpeed) {
this.$statusBarDiv.find('span.able-speed').show();
this.hidingSpeed = false;
}
- if (this.$statusBarDiv.width() < statusBarWidthBreakpoint) {
- // Player is too small for a speed span
- this.$statusBarDiv.find('span.able-speed').hide();
- this.hidingSpeed = true;
- }
- else {
- if (this.hidingSpeed) {
- this.$statusBarDiv.find('span.able-speed').show();
- this.hidingSpeed = false;
- }
- }
}
}
};
- AblePlayer.prototype.getHiddenWidth = function($el) {
-
- // jQuery returns for width() if element is hidden
- // this function is a workaround
-
- // save a reference to a cloned element that can be measured
- var $hiddenElement = $el.clone().appendTo('body');
-
- // calculate the width of the clone
- var width = $hiddenElement.outerWidth();
-
- // remove the clone from the DOM
- $hiddenElement.remove();
-
- return width;
- };
-
AblePlayer.prototype.handlePlay = function(e) {
-
if (this.paused) {
+ this.okToPlay = true;
this.playMedia();
- }
- else {
+ if (this.synth.paused) {
+ this.synth.resume();
+ }
+ } else {
+ this.okToPlay = false;
this.pauseMedia();
+ if (this.speakingDescription) {
+ this.synth.pause();
+ }
+ }
+ if (this.speechEnabled === null) {
+ this.initSpeech('play');
}
};
AblePlayer.prototype.handleRestart = function() {
+ if (this.speakingDescription) {
+ this.synth.cancel();
+ }
this.seekTo(0);
};
AblePlayer.prototype.handlePrevTrack = function() {
- if (this.playlistIndex === 0) {
- // currently on the first track
- // wrap to bottom and play the last track
- this.playlistIndex = this.$playlist.length - 1;
- }
- else {
- this.playlistIndex--;
- }
- this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
- this.cuePlaylistItem(this.playlistIndex);
+ this.playlistIndex = (this.playlistIndex === 0) ? this.$playlist.length - 1 : this.playlistIndex--;
+ this.cueingPlaylistItem = true;
+ this.cuePlaylistItem(this.playlistIndex);
};
AblePlayer.prototype.handleNextTrack = function() {
- if (this.playlistIndex === this.$playlist.length - 1) {
- // currently on the last track
- // wrap to top and play the forst track
- this.playlistIndex = 0;
- }
- else {
- this.playlistIndex++;
- }
- this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
- this.cuePlaylistItem(this.playlistIndex);
+ this.playlistIndex = (this.playlistIndex === this.$playlist.length - 1) ? 0 : this.playlistIndex++;
+ this.cueingPlaylistItem = true;
+ this.cuePlaylistItem(this.playlistIndex);
};
AblePlayer.prototype.handleRewind = function() {
@@ -8812,15 +8086,10 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' this.duration || targetTime > this.currentChapter.end) {
- // targetTime would exceed the end of the video (or chapter)
- // scrub to end of whichever is earliest
targetTime = Math.min(this.duration, this.currentChapter.end);
- }
- else if (this.duration % targetTime < this.seekInterval) {
- // nothing left but pocket change after seeking to targetTime
- // go ahead and seek to end of video (or chapter), whichever is earliest
+ } else if (this.duration % targetTime < this.seekInterval) {
targetTime = Math.min(this.duration, this.currentChapter.end);
}
- }
- else {
- // this is not the last chapter
+ } else {
if (targetTime > this.currentChapter.end) {
- // targetTime would exceed the end of the chapter
- // scrub exactly to end of chapter
targetTime = this.currentChapter.end;
}
}
- }
- else {
- // not using chapter times
+ } else {
if (targetTime > this.duration) {
targetTime = this.duration;
}
@@ -8871,49 +8128,33 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('= 0) {
- this.setPlaybackRate(rates[index]);
+ } else if (this.player === 'youtube') {
+ if (this.youTubePlayerReady) {
+ rates = this.youTubePlayer.getAvailablePlaybackRates();
+ currentRate = this.getPlaybackRate();
+ index = rates.indexOf(currentRate);
+ if (index === -1) {
+
+ } else {
+ index += dir;
+ if (index < rates.length && index >= 0) {
+ this.setPlaybackRate(rates[index]);
+ }
}
}
- }
- else if (this.player === 'vimeo') {
- // range is 0.5 to 2
- // increase/decrease in inrements of 0.5
+ } else if (this.player === 'vimeo') {
vimeoMin = 0.5;
vimeoMax = 2;
if (dir === 1) {
- if (this.vimeoPlaybackRate + 0.5 <= vimeoMax) {
- newRate = this.vimeoPlaybackRate + 0.5;
- }
- else {
- newRate = vimeoMax;
- }
- }
- else if (dir === -1) {
- if (this.vimeoPlaybackRate - 0.5 >= vimeoMin) {
- newRate = this.vimeoPlaybackRate - 0.5;
- }
- else {
- newRate = vimeoMin;
- }
+ newRate = (this.vimeoPlaybackRate + 0.5 <= vimeoMax) ? this.vimeoPlaybackRate + 0.5 : vimeoMax;
+ } else if (dir === -1) {
+ newRate = (this.vimeoPlaybackRate - 0.5 >= vimeoMin) ? this.vimeoPlaybackRate - 0.5 : vimeoMin;
}
this.setPlaybackRate(newRate);
}
@@ -8921,48 +8162,54 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' getCaptionClickFunction()
+ } else {
if (this.captionsPopup && this.captionsPopup.is(':visible')) {
this.captionsPopup.hide();
this.hidingPopup = false;
- this.$ccButton.removeAttr('aria-expanded').focus();
- }
- else {
+ this.$ccButton.attr('aria-expanded', 'false')
+ this.waitThenFocus(this.$ccButton);
+ } else {
this.closePopups();
if (this.captionsPopup) {
this.captionsPopup.show();
this.$ccButton.attr('aria-expanded','true');
- this.captionsPopup.css('top', this.$ccButton.position().top - this.captionsPopup.outerHeight());
- this.captionsPopup.css('left', this.$ccButton.position().left)
- // Place focus on the first button (even if another button is checked)
- this.captionsPopup.find('li').removeClass('able-focus');
- this.captionsPopup.find('li').first().focus().addClass('able-focus');
+
+ setTimeout(function() {
+ thisObj.captionsPopup.css('top', thisObj.$ccButton.position().top - thisObj.captionsPopup.outerHeight());
+ thisObj.captionsPopup.css('left', thisObj.$ccButton.position().left)
+ thisObj.captionsPopup.find('li').removeClass('able-focus');
+ thisObj.captionsPopup.find('li').first().trigger('focus').addClass('able-focus');
+ }, 50);
}
}
}
+ var ariaLabelOn = ( captions.length > 1 ) ? this.translate( 'captions', 'Captions' ) : this.translate( 'showCaptions', 'Show captions' );
+ var ariaLabelOff = ( captions.length > 1 ) ? this.translate( 'captions', 'Captions' ) : this.translate( 'hideCaptions', 'Hide captions' );
+
+ this.toggleButtonState(
+ this.$ccButton,
+ this.captionsOn,
+ ariaLabelOff,
+ ariaLabelOn,
+ ariaPressed
+ );
};
+ AblePlayer.prototype.waitThenFocus = function($el, timeout) {
+
+ var _timeout = (timeout === undefined || timeout === null) ? 50 : timeout;
+
+ setTimeout(function() {
+ $el.trigger('focus');
+ }, _timeout);
+ }
+
AblePlayer.prototype.handleChapters = function () {
if (this.hidingPopup) {
- // stopgap to prevent spacebar in Firefox from reopening popup
- // immediately after closing it
this.hidingPopup = false;
return false;
}
if (this.chaptersPopup.is(':visible')) {
this.chaptersPopup.hide();
this.hidingPopup = false;
- this.$chaptersButton.removeAttr('aria-expanded').focus();
- }
- else {
+ this.$chaptersButton.attr('aria-expanded','false').trigger('focus');
+ } else {
this.closePopups();
this.chaptersPopup.show();
this.$chaptersButton.attr('aria-expanded','true');
this.chaptersPopup.css('top', this.$chaptersButton.position().top - this.chaptersPopup.outerHeight());
this.chaptersPopup.css('left', this.$chaptersButton.position().left)
- // Highlight the current chapter, if any chapters are checked
- // Otherwise, place focus on the first chapter
this.chaptersPopup.find('li').removeClass('able-focus');
if (this.chaptersPopup.find('li[aria-checked="true"]').length) {
- this.chaptersPopup.find('li[aria-checked="true"]').focus().addClass('able-focus');
- }
- else {
- this.chaptersPopup.find('li').first().addClass('able-focus').attr('aria-checked','true').focus();
+ this.chaptersPopup.find('li[aria-checked="true"]').trigger('focus').addClass('able-focus');
+ } else {
+ this.chaptersPopup.find('li').first().addClass('able-focus').attr('aria-checked','true').trigger('focus');
}
}
};
@@ -9030,164 +8288,122 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('');
- // create a hidden alert, communicated to screen readers via aria-describedby
- var $fsDialogAlert = $('',{
- 'class': 'able-screenreader-alert'
- }).text(this.tt.fullscreen); // In English: "Full screen"; TODO: Add alert text that is more descriptive
- $dialogDiv.append($fsDialogAlert);
- // now render this as a dialog
- this.fullscreenDialog = new AccessibleDialog($dialogDiv, this.$fullscreenButton, 'dialog', 'Fullscreen video player', $fsDialogAlert, this.tt.exitFullScreen, '100%', true, function () { thisObj.handleFullscreenToggle() });
- $('body').append($dialogDiv);
- }
-
- // Track whether paused/playing before moving element; moving the element can stop playback.
- var wasPaused = this.paused;
-
- if (fullscreen) {
- this.modalFullscreenActive = true;
- this.fullscreenDialog.show();
-
- // Move player element into fullscreen dialog, then show.
- // Put a placeholder element where player was.
- this.$modalFullscreenPlaceholder = $('
');
- this.$modalFullscreenPlaceholder.insertAfter($el);
- $el.appendTo(this.fullscreenDialog.modal);
-
- // Column left css is 50% by default; set to 100% for full screen.
- if ($el === this.$ableColumnLeft) {
- $el.width('100%');
- }
- var newHeight = $(window).height() - this.$playerDiv.height();
- if (!this.$descDiv.is(':hidden')) {
- newHeight -= this.$descDiv.height();
- }
- this.resizePlayer($(window).width(), newHeight);
+ $(document).on('fullscreenchange webkitfullscreenchange', function(e) {
+ if (!thisObj.fullscreen) {
+ thisObj.restoringAfterFullscreen = true;
+ } else if (!thisObj.clickedFullscreenButton) {
+ thisObj.fullscreen = false;
+ thisObj.restoringAfterFullscreen = true;
}
- else {
- this.modalFullscreenActive = false;
- if ($el === this.$ableColumnLeft) {
- $el.width('50%');
- }
- $el.insertAfter(this.$modalFullscreenPlaceholder);
- this.$modalFullscreenPlaceholder.remove();
- this.fullscreenDialog.hide();
- this.resizePlayer(this.$ableWrapper.width(), this.$ableWrapper.height());
- }
-
- // Resume playback if moving stopped it.
- if (!wasPaused && this.paused) {
- this.playMedia();
+ thisObj.resizePlayer();
+ thisObj.refreshControls('fullscreen');
+ if ( thisObj.scrollPosition ) {
+ scroll = thisObj.scrollPosition;
+ window.scrollTo( scroll.x, scroll.y );
}
- }
- this.refreshControls('fullscreen');
+ setTimeout(function() {
+ thisObj.clickedFullscreenButton = false;
+ thisObj.restoringAfterFullscreen = false;
+ },100);
+ });
};
AblePlayer.prototype.handleFullscreenToggle = function () {
@@ -9330,30 +8473,23 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' 0 ) {
+ return;
}
- else {
- $tooltip.stop().show().delay(4000).fadeOut(1000);
+ $button.find('svg, img, span').remove();
+
+ if (iconType === 'font') {
+ var $buttonIcon = $('
', {
+ 'class': iconData[2],
+ });
+ $button.append( $buttonIcon );
+ } else if (iconType === 'svg') {
+ function getNode(n, v) {
+ n = document.createElementNS("http://www.w3.org/2000/svg", n);
+ for (var p in v) {
+ n.setAttributeNS(null, p.replace(/[A-Z]/g, function(m) {
+ return "-" + m.toLowerCase();
+ }), v[p]);
+ }
+ return n;
+ }
+ var icon = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
+ icon.setAttribute( 'focusable', 'false' );
+ icon.setAttribute( 'aria-hidden', 'true');
+ icon.setAttribute( 'viewBox', iconData[0] );
+ icon.setAttribute( 'id', 'ableplayer-' + id );
+ let path = getNode( 'path', { d: iconData[1] } );
+ icon.appendChild( path );
+
+ $button.append( icon );
+ $button.html($button.html());
+ } else {
+ var $buttonImg = $(' ',{
+ 'src': iconData[3],
+ 'alt': '',
+ 'role': 'presentation'
+ });
+ $button.append($buttonImg);
+ $button.find('img').attr('src',iconData[3]);
}
};
- AblePlayer.prototype.showAlert = function( msg, location ) {
-
- // location is either of the following:
- // 'main' (default)
- // 'screenreader (visibly hidden)
- // 'sign' (sign language window)
- // 'transcript' (trasncript window)
- var thisObj, $alertBox, $parentWindow, alertLeft, alertTop;
+ AblePlayer.prototype.setText = function( $button, text ) {
+ $button.attr( 'aria-label', text );
+ };
- thisObj = this;
-
- if (location === 'transcript') {
- $alertBox = this.$transcriptAlert;
- $parentWindow = this.$transcriptArea;
- }
- else if (location === 'sign') {
- $alertBox = this.$signAlert;
- $parentWindow = this.$signWindow;
- }
- else if (location === 'screenreader') {
- $alertBox = this.$srAlertBox;
- }
- else {
- $alertBox = this.$alertBox;
+ AblePlayer.prototype.toggleButtonState = function($button, isOn, onLabel, offLabel, ariaPressed = false, ariaExpanded = false) {
+ let buttonOff = ( $button.hasClass( 'buttonOff' ) ) ? true : false;
+ if ( buttonOff && ! isOn || ! buttonOff && isOn ) {
+ return;
}
- $alertBox.text(msg).show();
- if (location == 'transcript' || location === 'sign') {
- if ($parentWindow.width() > $alertBox.width()) {
- alertLeft = $parentWindow.width() / 2 - $alertBox.width() / 2;
+ if (! isOn) {
+ $button.addClass('buttonOff').attr('aria-label', offLabel);
+ if ( ariaPressed ) {
+ $button.attr('aria-pressed', 'false');
}
- else {
- // alert box is wider than its container. Position it far left and let it wrap
- alertLeft = 10;
+ if ( ariaExpanded ) {
+ $button.attr( 'aria-expanded', 'false' );
}
- if (location === 'sign') {
- // position alert in the lower third of the sign window (to avoid covering the signer)
- alertTop = ($parentWindow.height() / 3) * 2;
+ } else {
+ $button.removeClass('buttonOff').attr('aria-label', onLabel);
+ if ( ariaPressed ) {
+ $button.attr('aria-pressed', 'true');
}
- else if (location === 'transcript') {
- // position alert just beneath the toolbar to avoid getting lost among transcript text
- alertTop = this.$transcriptToolbar.height() + 30;
+ if ( ariaExpanded ) {
+ $button.attr( 'aria-expanded', 'true' );
}
- $alertBox.css({
- top: alertTop + 'px',
- left: alertLeft + 'px'
- });
}
- else if (location !== 'screenreader') {
- // The original formula incorporated offset() into the calculation
- // but at some point this began resulting in an alert that's off-centered
- // Changed in v2.2.17, but here's the original for reference in case needed:
- // left: this.$playerDiv.offset().left + (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
- $alertBox.css({
- left: (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
- });
+ };
+
+ AblePlayer.prototype.showTooltip = function($tooltip) {
+
+ $tooltip.show();
+ };
+
+ AblePlayer.prototype.showAlert = function( msg, location = 'main' ) {
+
+ var thisObj, $alertBox, $parentWindow;
+
+ thisObj = this;
+ $alertBox = thisObj.$alertBox;
+ $parentWindow = thisObj.$ableDiv;
+ if (location === 'transcript') {
+ $parentWindow = thisObj.$transcriptArea;
+ } else if (location === 'sign') {
+ $parentWindow = thisObj.$signWindow;
+ } else if (location === 'screenreader') {
+ $alertBox = thisObj.$srAlertBox;
}
+ $alertBox.find('span').text(msg);
+ $alertBox.appendTo($parentWindow)
+ $alertBox.css( {'display': 'flex'} );
+
if (location !== 'screenreader') {
- setTimeout(function () {
- $alertBox.fadeOut(300);
- }, 3000);
+ setTimeout( function () {
+ $alertBox.hide();
+ }, 30000 );
}
};
AblePlayer.prototype.showedAlert = function (which) {
- // returns true if the target alert has already been shown
- // useful for throttling alerts that only need to be shown once
- // e.g., move alerts with instructions for dragging a window
-
if (which === 'transcript') {
- if (this.showedTranscriptAlert) {
- return true;
- }
- else {
- return false;
- }
- }
- else if (which === 'sign') {
- if (this.showedSignAlert) {
- return true;
- }
- else {
- return false;
- }
+ return this.showedTranscriptAlert ?? false;
+ } else if (which === 'sign') {
+ return this.showedSignAlert ?? false;
}
return false;
}
- // Resizes all relevant player attributes.
AblePlayer.prototype.resizePlayer = function (width, height) {
- var captionSizeOkMin, captionSizeOkMax, captionSize, newCaptionSize, newLineHeight;
+ var captionSize, newWidth, newHeight, $iframe;
- if (this.fullscreen) { // replace isFullscreen() with a Boolean. see function for explanation
- if (typeof this.$vidcapContainer !== 'undefined') {
- this.$ableWrapper.css({
- 'width': width + 'px',
- 'max-width': ''
- })
- this.$vidcapContainer.css({
- 'height': height + 'px',
- 'width': width
- });
- this.$media.css({
- 'height': height + 'px',
- 'width': width
- })
- }
- if (typeof this.$transcriptArea !== 'undefined') {
- this.retrieveOffscreenWindow('transcript',width,height);
- }
- if (typeof this.$signWindow !== 'undefined') {
- this.retrieveOffscreenWindow('sign',width,height);
+ if (this.mediaType === 'audio') {
+ return;
+ }
+ if (typeof width !== 'undefined' && typeof height !== 'undefined') {
+ this.aspectRatio = height / width;
+ if (this.playerWidth) {
+ newWidth = this.playerWidth;
+ if (this.playerHeight) {
+ newHeight = this.playerHeight;
+ } else {
+ newHeight = Math.round(newWidth * this.aspectRatio);
+ this.playerHeight = newHeight;
+ }
+ } else {
+ newWidth = (this.player === 'html5') ? $(window).width() : this.$ableWrapper.width();
+ newHeight = Math.round(newWidth * this.aspectRatio);
+ }
+ } else if (this.fullscreen) {
+ this.$ableWrapper.addClass('fullscreen');
+ newWidth = $(window).width();
+ newHeight = $(window).height() - this.$playerDiv.outerHeight() - 5;
+ this.positionCaptions('overlay');
+ } else {
+ this.$ableWrapper.removeClass('fullscreen');
+ if (this.player === 'html5') {
+ newWidth = (this.playerWidth) ? this.playerWidth : $(window).width();
+ } else {
+ newWidth = this.$ableWrapper.width();
}
+ newHeight = Math.round(newWidth * this.aspectRatio);
+ this.positionCaptions(this.prefCaptionsPosition);
}
- else {
- // player resized
- if (this.restoringAfterFullScreen) {
- // User has just exited fullscreen mode. Restore to previous settings
- width = this.preFullScreenWidth;
- height = this.preFullScreenHeight;
- this.restoringAfterFullScreen = false;
- this.$ableWrapper.css({
- 'max-width': width + 'px',
- 'width': ''
+ if (this.debug) {
+
+ }
+ if (this.player === 'youtube' || this.player === 'vimeo') {
+ $iframe = this.$ableWrapper.find('iframe');
+ if (this.player === 'youtube' && this.youTubePlayer) {
+ this.youTubePlayer.setSize(newWidth,newHeight);
+ } else {
+ $iframe.attr({
+ 'width': newWidth,
+ 'height': newHeight
});
- if (typeof this.$vidcapContainer !== 'undefined') {
- this.$vidcapContainer.css({
- 'height': '',
- 'width': ''
+ }
+ if (this.playerWidth && this.playerHeight) {
+ if (this.fullscreen) {
+ $iframe.css({
+ 'max-width': '',
+ 'max-height': ''
+ });
+ } else {
+ $iframe.css({
+ 'max-width': this.playerWidth + 'px',
+ 'max-height': this.playerHeight + 'px'
});
}
- this.$media.css({
- 'width': '100%',
- 'height': 'auto'
+ }
+ } else if (this.player === 'html5') {
+ if (this.fullscreen) {
+ this.$media.attr({
+ 'width': newWidth,
+ 'height': newHeight
+ });
+ this.$ableWrapper.css({
+ 'width': newWidth,
+ 'height': newHeight
});
+ } else {
+ this.$media.removeAttr('width height');
+ this.$ableWrapper.removeAttr( 'style' );
}
}
-
- // resize YouTube
- if (this.player === 'youtube' && this.youTubePlayer) {
- this.youTubePlayer.setSize(width, height);
- }
-
- // Resize captions
if (typeof this.$captionsDiv !== 'undefined') {
- // Font-size is too small in full screen view & too large in small-width view
- // The following vars define a somewhat arbitary zone outside of which
- // caption size requires adjustment
- captionSizeOkMin = 400;
- captionSizeOkMax = 1000;
- captionSize = parseInt(this.prefCaptionsSize,10);
-
- // TODO: Need a better formula so that it scales proportionally to viewport
- if (width > captionSizeOkMax) {
- newCaptionSize = captionSize * 1.5;
- }
- else if (width < captionSizeOkMin) {
- newCaptionSize = captionSize / 1.5;
- }
- else {
- newCaptionSize = captionSize;
+ var isSmallScreen = false;
+ var windowWidth = window.screen.width;
+ if ( windowWidth < 1200 ) {
+ isSmallScreen = true;
}
- newLineHeight = newCaptionSize + 25;
- this.$captionsDiv.css('font-size',newCaptionSize + '%');
- this.$captionsWrapper.css('line-height',newLineHeight + '%');
+ captionSize = parseInt(this.prefCaptionsSize,10);
+ if (this.fullscreen && ! isSmallScreen ) {
+ captionSize = (captionSize / 100) + 'vw';
+ } else if ( this.fullscreen && isSmallScreen ) {
+ captionSize = '1.2rem';
+ } else {
+ captionSize = captionSize + '%';
+ }
+ this.$captionsDiv.css({
+ 'font-size': captionSize
+ });
}
- this.refreshControls('captions');
+ this.refreshControls();
};
AblePlayer.prototype.retrieveOffscreenWindow = function( which, width, height ) {
- // check to be sure popup windows ('transcript' or 'sign') are positioned on-screen
- // (they sometimes disappear off-screen when entering fullscreen mode)
- // if off-screen, recalculate so they are back on screen
var window, windowPos, windowTop, windowLeft, windowRight, windowWidth, windowBottom, windowHeight;
if (which == 'transcript') {
window = this.$transcriptArea;
- }
- else if (which == 'sign') {
+ } else if (which == 'sign') {
window = this.$signWindow;
}
windowWidth = window.width();
@@ -9574,121 +8739,73 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' width) { // off-screen to the right
+ if (windowRight > width) {
windowLeft = (width - 20) - windowWidth;
window.css('left',windowLeft);
}
- if (windowBottom > height) { // off-screen to the bottom
+ if (windowBottom > height) {
windowTop = (height - 10) - windowHeight;
window.css('top',windowTop);
}
};
- AblePlayer.prototype.getHighestZIndex = function() {
-
- // returns the highest z-index on page
- // used to ensure dialogs (or potentially other windows) are on top
-
- var max, $elements, z;
- max = 0;
-
- // exclude the Able Player dialogs and windows
- $elements = $('body *').not('.able-modal-dialog,.able-modal-dialog *,.able-modal-overlay,.able-modal-overlay *,.able-sign-window,.able-transcript-area');
-
- $elements.each(function(){
- z = $(this).css('z-index');
- if (Number.isInteger(+z)) { // work only with integer values, not 'auto'
- if (parseInt(z) > max) {
- max = parseInt(z);
- }
- }
- });
- return max;
- };
-
AblePlayer.prototype.updateZIndex = function(which) {
- // update z-index of 'transcript' or 'sign', relative to each other
- // direction is always 'up' (i.e., move window to top)
- // windows come to the top when the user clicks on them
- var defHighZ, defLowZ, highestZ, transcriptZ, signZ, newHighZ, newLowZ;
+ var defHighZ, defLowZ, transcriptZ, signZ, newHighZ, newLowZ;
- // set the default z-indexes, as defined in ableplayer.css
- defHighZ = 8000; // by default, assigned to the sign window
- defLowZ = 7000; // by default, assigned to the transcript area
- highestZ = this.getHighestZIndex(); // highest z-index on the page, excluding Able Player windows & modals
+ defHighZ = 8000;
+ defLowZ = 7000;
- // NOTE: Although highestZ is collected here, it currently isn't used.
- // If something on the page has a higher z-index than the transcript or sign window, do we care?
- // Excluding it here assumes "No". Our immediate concern is with the relationship between our own components.
- // If we elevate our z-indexes so our content is on top, we run the risk of starting a z-index war.
if (typeof this.$transcriptArea === 'undefined' || typeof this.$signWindow === 'undefined' ) {
- // at least one of the windows doesn't exist, so there's no conflict
- // since z-index may have been stored to a cookie on another page, need to restore default
if (typeof this.$transcriptArea !== 'undefined') {
transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
if (transcriptZ > defLowZ) {
- // restore to the default
this.$transcriptArea.css('z-index',defLowZ);
}
- }
- else if (typeof this.$signWindow !== 'undefined') {
+ } else if (typeof this.$signWindow !== 'undefined') {
signZ = parseInt(this.$signWindow.css('z-index'));
if (signZ > defHighZ) {
- // restore to the default
this.$signWindow.css('z-index',defHighZ);
}
}
return false;
}
- // both windows exist
- // get current values
transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
signZ = parseInt(this.$signWindow.css('z-index'));
if (transcriptZ === signZ) {
- // the two windows are equal; restore defaults (the target window will be on top)
newHighZ = defHighZ;
newLowZ = defLowZ;
- }
- else if (transcriptZ > signZ) {
+ } else if (transcriptZ > signZ) {
if (which === 'transcript') {
- // transcript is already on top; nothing to do
return false;
- }
- else {
- // swap z's
+ } else {
newHighZ = transcriptZ;
newLowZ = signZ;
}
- }
- else { // signZ is greater
+ } else {
if (which === 'sign') {
- // sign is already on top; nothing to do
return false;
- }
- else {
+ } else {
newHighZ = signZ;
newLowZ = transcriptZ;
}
}
- // now assign the new values
if (which === 'transcript') {
this.$transcriptArea.css('z-index',newHighZ);
this.$signWindow.css('z-index',newLowZ);
- }
- else if (which === 'sign') {
+ } else if (which === 'sign') {
this.$signWindow.css('z-index',newHighZ);
this.$transcriptArea.css('z-index',newLowZ);
}
@@ -9696,45 +8813,30 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' initYouTubePlayer())
- thisObj.resettingYouTubeCaptions = true;
- thisObj.youTubePlayer.loadModule(thisObj.ytCaptionModule);
- }
- }
- else if (thisObj.usingVimeoCaptions) {
- thisObj.vimeoPlayer.enableTextTrack(thisObj.captionLang).then(function(track) {
- // track.language = the iso code for the language
- // track.kind = 'captions' or 'subtitles'
- // track.label = the human-readable label
- }).catch(function(error) {
- switch (error.name) {
- case 'InvalidTrackLanguageError':
- // no track was available with the specified language
-
- break;
- case 'InvalidTrackError':
- // no track was available with the specified language and kind
-
- break;
- default:
- // some other error occurred
-
- break;
- }
- });
- }
- else { // using local track elements for captions/subtitles
- thisObj.syncTrackLanguages('captions',thisObj.captionLang);
- if (!thisObj.swappingSrc) {
- thisObj.updateCaption(thisObj.elapsed);
- thisObj.showDescription(thisObj.elapsed);
- }
- }
- thisObj.captionsOn = true;
- // stopgap to prevent spacebar in Firefox from reopening popup
- // immediately after closing it (used in handleCaptionToggle())
- thisObj.hidingPopup = true;
- thisObj.captionsPopup.hide();
- // Ensure stopgap gets cancelled if handleCaptionToggle() isn't called
- // e.g., if user triggered button with Enter or mouse click, not spacebar
- setTimeout(function() {
- thisObj.hidingPopup = false;
- }, 100);
- thisObj.updateCaptionsMenu(thisObj.captionLang);
- thisObj.$ccButton.focus();
+ var thisObj = this;
+ return function () {
+ thisObj.selectedCaptions = track;
+ thisObj.captionLang = track.language;
+ thisObj.currentCaption = -1;
+ if (thisObj.usingYouTubeCaptions) {
+ if (thisObj.captionsOn) {
+ if (
+ thisObj.youTubePlayer.getOptions("captions") &&
+ thisObj.startedPlaying
+ ) {
+ thisObj.youTubePlayer.setOption("captions", "track", {
+ languageCode: thisObj.captionLang,
+ });
+ } else {
+ thisObj.captionLangPending = thisObj.captionLang;
+ }
+ } else {
+ if (thisObj.youTubePlayer.getOptions("captions")) {
+ thisObj.youTubePlayer.setOption("captions", "track", {
+ languageCode: thisObj.captionLang,
+ });
+ } else {
+ thisObj.youTubePlayer.loadModule("captions");
+ thisObj.captionLangPending = thisObj.captionLang;
+ }
+ }
+ } else if (thisObj.usingVimeoCaptions) {
+ thisObj.vimeoPlayer
+ .enableTextTrack(thisObj.captionLang)
+ .then(function (track) {
+ })
+ .catch(function (error) {
+ switch (error.name) {
+ case "InvalidTrackLanguageError":
+
+ break;
+ case "InvalidTrackError":
+
+ break;
+ default:
+
+ break;
+ }
+ });
+ } else {
+ thisObj.syncTrackLanguages("captions", thisObj.captionLang);
+ if (!thisObj.swappingSrc) {
+ thisObj.updateCaption(thisObj.elapsed);
+ thisObj.showDescription(thisObj.elapsed);
+ }
+ }
+ thisObj.captionsOn = true;
+ thisObj.hidingPopup = true;
+ thisObj.captionsPopup.hide();
+ thisObj.$ccButton.attr("aria-expanded", "false");
+ if (thisObj.mediaType === "audio") {
+ thisObj.$captionsContainer.removeClass("captions-off");
+ }
+ setTimeout(function () {
+ thisObj.hidingPopup = false;
+ }, 100);
+ thisObj.updateCaptionsMenu(thisObj.captionLang);
+ thisObj.waitThenFocus(thisObj.$ccButton);
- // save preference to cookie
- thisObj.prefCaptions = 1;
- thisObj.updateCookie('prefCaptions');
- thisObj.refreshControls('captions');
- }
- };
+ thisObj.prefCaptions = 1;
+ thisObj.updatePreferences("prefCaptions");
+ thisObj.refreshControls("captions");
+ };
+ };
- // Returns the function used when the "Captions Off" button is clicked in the captions tooltip.
- AblePlayer.prototype.getCaptionOffFunction = function () {
- var thisObj = this;
- return function () {
- if (thisObj.player == 'youtube') {
- thisObj.youTubePlayer.unloadModule(thisObj.ytCaptionModule);
- }
- else if (thisObj.usingVimeoCaptions) {
+ AblePlayer.prototype.getCaptionOffFunction = function () {
+ var thisObj = this;
+ return function () {
+ if (thisObj.player == "youtube") {
+ thisObj.youTubePlayer.unloadModule("captions");
+ } else if (thisObj.usingVimeoCaptions) {
thisObj.vimeoPlayer.disableTextTrack();
- }
- thisObj.captionsOn = false;
- thisObj.currentCaption = -1;
- // stopgap to prevent spacebar in Firefox from reopening popup
- // immediately after closing it (used in handleCaptionToggle())
- thisObj.hidingPopup = true;
- thisObj.captionsPopup.hide();
- // Ensure stopgap gets cancelled if handleCaptionToggle() isn't called
- // e.g., if user triggered button with Enter or mouse click, not spacebar
- setTimeout(function() {
- thisObj.hidingPopup = false;
- }, 100);
- thisObj.updateCaptionsMenu();
- thisObj.$ccButton.focus();
+ }
+ thisObj.captionsOn = false;
+ thisObj.currentCaption = -1;
- // save preference to cookie
- thisObj.prefCaptions = 0;
- thisObj.updateCookie('prefCaptions');
- if (!this.swappingSrc) {
- thisObj.refreshControls('captions');
- thisObj.updateCaption();
- }
- }
- };
+ if (thisObj.mediaType === "audio") {
+ thisObj.$captionsContainer.addClass("captions-off");
+ }
- AblePlayer.prototype.showCaptions = function(now) {
+ thisObj.hidingPopup = true;
+ thisObj.captionsPopup.hide();
+ thisObj.$ccButton.attr("aria-expanded", "false");
+ setTimeout(function () {
+ thisObj.hidingPopup = false;
+ }, 100);
+ thisObj.updateCaptionsMenu();
+ thisObj.waitThenFocus(thisObj.$ccButton);
+
+ thisObj.prefCaptions = 0;
+ thisObj.updatePreferences("prefCaptions");
+ if (!this.swappingSrc) {
+ thisObj.refreshControls("captions");
+ thisObj.updateCaption();
+ }
+ };
+ };
- var c, thisCaption, captionText;
- var cues;
- if (this.selectedCaptions) {
- cues = this.selectedCaptions.cues;
- }
- else if (this.captions.length >= 1) {
- cues = this.captions[0].cues;
- }
- else {
- cues = [];
- }
- for (c = 0; c < cues.length; c++) {
- if ((cues[c].start <= now) && (cues[c].end > now)) {
- thisCaption = c;
- break;
- }
- }
- if (typeof thisCaption !== 'undefined') {
- if (this.currentCaption !== thisCaption) {
- // it's time to load the new caption into the container div
- captionText = this.flattenCueForCaption(cues[thisCaption]).replace('\n', ' ');
- this.$captionsDiv.html(captionText);
- this.currentCaption = thisCaption;
- if (captionText.length === 0) {
- // hide captionsDiv; otherwise background-color is visible due to padding
- this.$captionsDiv.css('display','none');
- }
- else {
- this.$captionsDiv.css('display','inline-block');
- }
- }
- }
- else {
- this.$captionsDiv.html('');
- this.currentCaption = -1;
- }
- };
+ AblePlayer.prototype.showCaptions = function (now) {
+ var c, thisCaption, captionText;
+ var cues;
+ if (this.selectedCaptions.cues.length) {
+ cues = this.selectedCaptions.cues;
+ } else if (this.captions.length >= 1) {
+ cues = this.captions[0].cues;
+ } else {
+ cues = [];
+ }
+ for (c = 0; c < cues.length; c++) {
+ if (cues[c].start <= now && cues[c].end > now) {
+ thisCaption = c;
+ break;
+ }
+ }
+ if (typeof thisCaption !== "undefined") {
+ if (this.currentCaption !== thisCaption) {
+ captionText = this.flattenCueForCaption(cues[thisCaption]).replace( /\n/g, " " );
+
+ this.$captionsDiv.html(captionText);
+ this.currentCaption = thisCaption;
+ if (captionText.length === 0) {
+ this.$captionsDiv.css("display", "none");
+ } else {
+ this.$captionsDiv.css("display", "inline-block");
+ }
+ }
+ } else {
+ this.$captionsDiv.html("").css("display", "none");
+ this.currentCaption = -1;
+ }
+ };
- AblePlayer.prototype.flattenCueForCaption = function (cue) {
+ AblePlayer.prototype.flattenCueForCaption = function (cue) {
- // Takes a cue and returns the caption text to display
- // Also used for chapters
- // Support for 'i' and 'b' tags added in 2.3.66
- // TODO: Add support for 'c' (class) and 'ruby'
- // c (class): Some text
- // Classes can be used to modify other tags too (e.g., )
- // If tag, should be rendered as a
- // ruby: http://www.w3schools.com/tags/tag_ruby.asp
- // WebVTT also supports 'u' (underline)
- // I see no reason to support that in Able Player.
- // If it's available authors are likely to use it incorrectly
- // where or should be used instead
- // Here are the rare use cases where an underline is appropriate on the web:
- // http://html5doctor.com/u-element/
+ var result = [];
- var result = [];
+ var flattenComponent = function (component) {
+ var result = [],
+ ii;
+ if (component.type === "string") {
+ result.push(component.value);
+ } else if (component.type === "v") {
+ result.push("(" + component.value + ")");
+ for (ii = 0; ii < component.children.length; ii++) {
+ result.push(flattenComponent(component.children[ii]));
+ }
+ } else if (component.type === "i") {
+ result.push("");
+ for (ii = 0; ii < component.children.length; ii++) {
+ result.push(flattenComponent(component.children[ii]));
+ }
+ result.push(" ");
+ } else if (component.type === "b") {
+ result.push("");
+ for (ii = 0; ii < component.children.length; ii++) {
+ result.push(flattenComponent(component.children[ii]));
+ }
+ result.push(" ");
+ } else {
+ for (ii = 0; ii < component.children.length; ii++) {
+ result.push(flattenComponent(component.children[ii]));
+ }
+ }
+ return result.join("");
+ };
- var flattenComponent = function (component) {
- var result = [], ii;
- if (component.type === 'string') {
- result.push(component.value);
- }
- else if (component.type === 'v') {
- result.push('(' + component.value + ')');
- for (ii = 0; ii < component.children.length; ii++) {
- result.push(flattenComponent(component.children[ii]));
- }
- }
- else if (component.type === 'i') {
- result.push('');
- for (ii = 0; ii < component.children.length; ii++) {
- result.push(flattenComponent(component.children[ii]));
- }
- result.push(' ');
- }
- else if (component.type === 'b') {
- result.push('');
- for (ii = 0; ii < component.children.length; ii++) {
- result.push(flattenComponent(component.children[ii]));
- }
- result.push(' ');
- }
- else {
- for (ii = 0; ii < component.children.length; ii++) {
- result.push(flattenComponent(component.children[ii]));
- }
- }
- return result.join('');
- };
+ if (typeof cue.components !== "undefined") {
+ for (var ii = 0; ii < cue.components.children.length; ii++) {
+ result.push(flattenComponent(cue.components.children[ii]));
+ }
+ }
+ return result.join("");
+ };
- if (typeof cue.components !== 'undefined') {
- for (var ii = 0; ii < cue.components.children.length; ii++) {
- result.push(flattenComponent(cue.components.children[ii]));
- }
- }
- return result.join('');
- };
+ AblePlayer.prototype.getCaptionsOptions = function (pref) {
+ var options = [];
+
+ switch (pref) {
+ case "prefCaptionsFont":
+ options[0] = ["serif", this.translate( 'serif', 'serif' )];
+ options[1] = ["sans-serif", this.translate( 'sans', 'sans-serif' )];
+ options[2] = ["cursive", this.translate( 'cursive', 'cursive' )];
+ options[3] = ["fantasy", this.translate( 'fantasy', 'fantasy' )];
+ options[4] = ["monospace", this.translate( 'monospace', 'monospace' )];
+ break;
+
+ case "prefCaptionsColor":
+ case "prefCaptionsBGColor":
+ options[0] = ["white", this.translate( 'white', 'white' )];
+ options[1] = ["yellow", this.translate( 'yellow', 'yellow' )];
+ options[2] = ["green", this.translate( 'green', 'green' )];
+ options[3] = ["cyan", this.translate( 'cyan', 'cyan' )];
+ options[4] = ["blue", this.translate( 'blue', 'blue' )];
+ options[5] = ["magenta", this.translate( 'magenta', 'magenta' )];
+ options[6] = ["red", this.translate( 'red', 'red' )];
+ options[7] = ["black", this.translate( 'black', 'black' )];
+ break;
+
+ case "prefCaptionsSize":
+ options[0] = "75%";
+ options[1] = "100%";
+ options[2] = "125%";
+ options[3] = "150%";
+ options[4] = "200%";
+ break;
+
+ case "prefCaptionsOpacity":
+ options[0] = "0%";
+ options[1] = "25%";
+ options[2] = "50%";
+ options[3] = "75%";
+ options[4] = "100%";
+ break;
+
+ case "prefCaptionsStyle":
+ options[0] = this.translate( 'captionsStylePopOn', 'Pop-on' );
+ options[1] = this.translate( 'captionsStyleRollUp', 'Roll-up' );
+ break;
+
+ case "prefCaptionsPosition":
+ options[0] = "overlay";
+ options[1] = "below";
+ break;
+ }
+ return options;
+ };
- AblePlayer.prototype.getCaptionsOptions = function(pref) {
+ AblePlayer.prototype.translatePrefs = function (pref, value, outputFormat) {
+ if (outputFormat == "youtube") {
+ if (pref === "size") {
+ switch (value) {
+ case "75%":
+ return -1;
+ case "100%":
+ return 0;
+ case "125%":
+ return 1;
+ case "150%":
+ return 2;
+ case "200%":
+ return 3;
+ }
+ }
+ }
+ return false;
+ };
- var options = [];
+ AblePlayer.prototype.stylizeCaptions = function ($element, pref) {
+ var property, newValue, opacity;
+
+ if (typeof $element !== "undefined") {
+ if (pref == "prefCaptionsPosition") {
+ this.positionCaptions();
+ } else if (typeof pref !== "undefined") {
+ if (pref === "prefCaptionsFont") {
+ property = "font-family";
+ } else if (pref === "prefCaptionsSize") {
+ property = "font-size";
+ } else if (pref === "prefCaptionsColor") {
+ property = "color";
+ } else if (pref === "prefCaptionsBGColor") {
+ property = "background-color";
+ } else if (pref === "prefCaptionsOpacity") {
+ property = "opacity";
+ }
+ if (pref === "prefCaptionsOpacity") {
+ newValue =
+ parseFloat($("#" + this.mediaId + "_" + pref).val()) / 100.0;
+ } else {
+ newValue = $("#" + this.mediaId + "_" + pref).val();
+ }
+ $element.css(property, newValue);
+ } else {
+ opacity = parseFloat(this.prefCaptionsOpacity) / 100.0;
+ $element.css({
+ "font-family": this.prefCaptionsFont,
+ color: this.prefCaptionsColor,
+ "background-color": this.prefCaptionsBGColor,
+ opacity: opacity,
+ });
+ if ($element === this.$captionsDiv) {
+ if (typeof this.$captionsDiv !== "undefined") {
+ this.$captionsDiv.css({
+ "font-size": this.prefCaptionsSize,
+ });
+ }
+ }
+ if (this.prefCaptionsPosition === "below") {
+ if (typeof this.$captionsWrapper !== "undefined") {
+ this.$captionsWrapper.css({
+ "background-color": this.prefCaptionsBGColor,
+ opacity: "1",
+ });
+ }
+ } else if (this.prefCaptionsPosition === "overlay") {
+ if (typeof this.$captionsWrapper !== "undefined") {
+ this.$captionsWrapper.css({
+ "background-color": "transparent",
+ opacity: "",
+ });
+ }
+ }
+ this.positionCaptions();
+ }
+ }
+ };
+ AblePlayer.prototype.positionCaptions = function (position) {
+ if (typeof position === "undefined") {
+ position = this.prefCaptionsPosition;
+ }
+ if (typeof this.$captionsWrapper !== "undefined") {
+ if (position == "below") {
+ this.$captionsWrapper
+ .removeClass("able-captions-overlay")
+ .addClass("able-captions-below");
+ this.$captionsWrapper.css({
+ "background-color": this.prefCaptionsBGColor,
+ opacity: "1",
+ });
+ } else {
+ this.$captionsWrapper
+ .removeClass("able-captions-below")
+ .addClass("able-captions-overlay");
+ this.$captionsWrapper.css({
+ "background-color": "transparent",
+ opacity: "",
+ });
+ }
+ }
+ };
+})(jQuery);
- switch (pref) {
+(function ($) {
- case 'prefCaptionsFont':
- options[0] = ['serif',this.tt.serif];
- options[1] = ['sans-serif',this.tt.sans];
- options[2] = ['cursive',this.tt.cursive];
- options[3] = ['fantasy',this.tt.fantasy];
- options[4] = ['monospace',this.tt.monospace];
- break;
+ AblePlayer.prototype.populateChaptersDiv = function() {
- case 'prefCaptionsColor':
- case 'prefCaptionsBGColor':
- // HTML color values must be in English
- options[0] = ['white',this.tt.white];
- options[1] = ['yellow',this.tt.yellow];
- options[2] = ['green',this.tt.green];
- options[3] = ['cyan',this.tt.cyan];
- options[4] = ['blue',this.tt.blue];
- options[5] = ['magenta',this.tt.magenta];
- options[6] = ['red',this.tt.red];
- options[7] = ['black',this.tt.black];
- break;
+ var headingLevel, headingType, headingId, $chaptersHeading;
+ if ( ! this.chaptersDivLocation ) {
+ return;
+ }
+ if ($('#' + this.chaptersDivLocation)) {
- case 'prefCaptionsSize':
- options[0] = '75%';
- options[1] = '100%';
- options[2] = '125%';
- options[3] = '150%';
- options[4] = '200%';
- break;
+ this.$chaptersDiv = $('#' + this.chaptersDivLocation);
+ this.$chaptersDiv.addClass('able-chapters-div');
- case 'prefCaptionsOpacity':
- options[0] = '0%';
- options[1] = '25%';
- options[2] = '50%';
- options[3] = '75%';
- options[4] = '100%';
- break;
+ this.$chaptersDiv.empty();
- case 'prefCaptionsStyle':
- options[0] = this.tt.captionsStylePopOn;
- options[1] = this.tt.captionsStyleRollUp;
- break;
+ if (this.chaptersTitle) {
+ headingLevel = this.getNextHeadingLevel(this.$chaptersDiv);
+ headingType = 'h' + headingLevel.toString();
+ headingId = this.mediaId + '-chapters-heading';
+ $chaptersHeading = $('<' + headingType + '>', {
+ 'class': 'able-chapters-heading',
+ 'id': headingId
+ }).text(this.chaptersTitle);
+ this.$chaptersDiv.append($chaptersHeading);
+ }
- case 'prefCaptionsPosition':
- options[0] = 'overlay';
- options[1] = 'below';
- break;
+ this.$chaptersNav = $('');
+ if (this.chaptersTitle) {
+ this.$chaptersNav.attr( 'aria-labelledby', headingId );
+ } else {
+ this.$chaptersNav.attr( 'aria-label', this.translate( 'chapters', 'Chapters' ) );
+ }
+ this.$chaptersDiv.append(this.$chaptersNav);
+ this.updateChaptersList();
}
- return options;
};
- AblePlayer.prototype.translatePrefs = function(pref, value, outputFormat) {
-
- // translate current value of pref to a value supported by outputformat
- if (outputFormat == 'youtube') {
- if (pref === 'size') {
- // YouTube font sizes are a range from -1 to 3 (0 = default)
- switch (value) {
- case '75%':
- return -1;
- case '100%':
- return 0;
- case '125%':
- return 1;
- case '150%':
- return 2;
- case '200%':
- return 3;
- }
- }
- }
- return false;
- }
-
- AblePlayer.prototype.stylizeCaptions = function($element, pref) {
-
- // $element is the jQuery element containing the captions
- // this function handles stylizing of the sample caption text in the Prefs dialog
- // plus the actual production captions
- // TODO: consider applying the same user prefs to visible text-based description
- var property, newValue, opacity, lineHeight;
-
- if (typeof $element !== 'undefined') {
- if (pref == 'prefCaptionsPosition') {
- this.positionCaptions();
- }
- else if (typeof pref !== 'undefined') {
- // just change the one property that user just changed
- if (pref === 'prefCaptionsFont') {
- property = 'font-family';
- }
- else if (pref === 'prefCaptionsSize') {
- property = 'font-size';
- }
- else if (pref === 'prefCaptionsColor') {
- property = 'color';
- }
- else if (pref === 'prefCaptionsBGColor') {
- property = 'background-color';
- }
- else if (pref === 'prefCaptionsOpacity') {
- property = 'opacity';
- }
- if (pref === 'prefCaptionsOpacity') {
- newValue = parseFloat($('#' + this.mediaId + '_' + pref).val()) / 100.0;
- }
- else {
- newValue = $('#' + this.mediaId + '_' + pref).val();
- }
- $element.css(property, newValue);
- }
- else { // no property was specified, update all styles with current saved prefs
- opacity = parseFloat(this.prefCaptionsOpacity) / 100.0;
- $element.css({
- 'font-family': this.prefCaptionsFont,
- 'color': this.prefCaptionsColor,
- 'background-color': this.prefCaptionsBGColor,
- 'opacity': opacity
- });
- if ($element === this.$captionsDiv) {
- if (typeof this.$captionsWrapper !== 'undefined') {
- this.$captionsWrapper.css({
- 'font-size': this.prefCaptionsSize
- });
- }
- }
- if (this.prefCaptionsPosition === 'below') {
- // also need to add the background color to the wrapper div
- if (typeof this.$captionsWrapper !== 'undefined') {
- this.$captionsWrapper.css({
- 'background-color': this.prefCaptionsBGColor,
- 'opacity': '1'
- });
- }
- }
- else if (this.prefCaptionsPosition === 'overlay') {
- // no background color for overlay wrapper, captions are displayed in-line
- if (typeof this.$captionsWrapper !== 'undefined') {
- this.$captionsWrapper.css({
- 'background-color': 'transparent',
- 'opacity': ''
- });
- }
- }
- this.positionCaptions();
- }
- }
- };
- AblePlayer.prototype.positionCaptions = function(position) {
-
- // set caption position to either 'overlay' or 'below'
- // if position parameter was passed to this function, use that
- // otherwise use user preference
- if (typeof position === 'undefined') {
- position = this.prefCaptionsPosition;
- }
- if (typeof this.$captionsWrapper !== 'undefined') {
-
- if (position == 'below') {
- this.$captionsWrapper.removeClass('able-captions-overlay').addClass('able-captions-below');
- // also need to update in-line styles
- this.$captionsWrapper.css({
- 'background-color': this.prefCaptionsBGColor,
- 'opacity': '1'
- });
- }
- else {
- this.$captionsWrapper.removeClass('able-captions-below').addClass('able-captions-overlay');
- this.$captionsWrapper.css({
- 'background-color': 'transparent',
- 'opacity': ''
- });
- }
- }
- };
-
-})(jQuery);
-
-(function ($) {
-
- AblePlayer.prototype.populateChaptersDiv = function() {
-
- var headingLevel, headingType, headingId, $chaptersHeading,
- $chaptersList;
-
- if ($('#' + this.chaptersDivLocation)) {
- this.$chaptersDiv = $('#' + this.chaptersDivLocation);
- this.$chaptersDiv.addClass('able-chapters-div');
-
- // add optional header
- if (this.chaptersTitle) {
- headingLevel = this.getNextHeadingLevel(this.$chaptersDiv);
- headingType = 'h' + headingLevel.toString();
- headingId = this.mediaId + '-chapters-heading';
- $chaptersHeading = $('<' + headingType + '>', {
- 'class': 'able-chapters-heading',
- 'id': headingId
- }).text(this.chaptersTitle);
- this.$chaptersDiv.append($chaptersHeading);
- }
-
- this.$chaptersNav = $('');
- if (this.chaptersTitle) {
- this.$chaptersNav.attr('aria-labelledby',headingId);
- }
- else {
- this.$chaptersNav.attr('aria-label',this.tt.chapters);
- }
- this.$chaptersDiv.append(this.$chaptersNav);
-
- // populate this.$chaptersNav with a list of chapters
- this.updateChaptersList();
- }
- };
-
- AblePlayer.prototype.updateChaptersList = function() {
+ AblePlayer.prototype.updateChaptersList = function() {
var thisObj, cues, $chaptersList, c, thisChapter,
- $chapterItem, $chapterButton, buttonId, hasDefault,
- getClickFunction, $clickedItem, $chaptersList, thisChapterIndex;
+ $chapterItem, $chapterButton, hasDefault,
+ getClickFunction, $clickedItem, $chaptersList;
thisObj = this;
@@ -10241,21 +9295,13 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('= 1) {
+ } else if (this.chapters.length >= 1) {
cues = this.chapters[0].cues;
- }
- else {
+ } else {
cues = [];
}
if (cues.length > 0) {
@@ -10268,22 +9314,20 @@ if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith(' this.currentChapter.start) {
return this.elapsed - this.currentChapter.start;
- }
- else {
+ } else {
return 0;
}
};
AblePlayer.prototype.convertChapterTimeToVideoTime = function (chapterTime) {
- // chapterTime is the time within the current chapter
- // return the same time, relative to the entire video
if (typeof this.currentChapter !== 'undefined') {
var newTime = this.currentChapter.start + chapterTime;
if (newTime > this.currentChapter.end) {
return this.currentChapter.end;
- }
- else {
+ } else {
return newTime;
}
- }
- else {
+ } else {
return chapterTime;
}
};
AblePlayer.prototype.getChapterClickFunction = function (time) {
- // Returns the function used when a chapter is clicked in the chapters menu.
var thisObj = this;
return function () {
thisObj.seekTrigger = 'chapter';
thisObj.seekTo(time);
- // stopgap to prevent spacebar in Firefox from reopening popup
- // immediately after closing it (used in handleChapters())
thisObj.hidingPopup = true;
thisObj.chaptersPopup.hide();
- // Ensure stopgap gets cancelled if handleChapters() isn't called
- // e.g., if user triggered button with Enter or mouse click, not spacebar
setTimeout(function() {
thisObj.hidingPopup = false;
}, 100);
- thisObj.$chaptersButton.focus();
+ thisObj.$chaptersButton.trigger('focus');
}
};
})(jQuery);
(function ($) {
- AblePlayer.prototype.updateMeta = function (time) {
- if (this.hasMeta) {
- if (this.metaType === 'text') {
- this.$metaDiv.show();
- this.showMeta(time || this.elapsed);
- }
- else {
- this.showMeta(time || this.elapsed);
- }
- }
- };
-
- AblePlayer.prototype.showMeta = function(now) {
- var tempSelectors, m, thisMeta,
- cues, cueText, cueLines, i, line,
- showDuration, focusTarget;
+ AblePlayer.prototype.updateMeta = function (time) {
+ if (this.hasMeta) {
+ if (this.metaType === "text") {
+ this.$metaDiv.show();
+ this.showMeta(time || this.elapsed);
+ } else {
+ this.showMeta(time || this.elapsed);
+ }
+ }
+ };
- tempSelectors = [];
- if (this.meta.length >= 1) {
- cues = this.meta;
- }
- else {
- cues = [];
- }
- for (m = 0; m < cues.length; m++) {
- if ((cues[m].start <= now) && (cues[m].end > now)) {
- thisMeta = m;
- break;
- }
- }
- if (typeof thisMeta !== 'undefined') {
- if (this.currentMeta !== thisMeta) {
- if (this.metaType === 'text') {
- // it's time to load the new metadata cue into the container div
- this.$metaDiv.html(this.flattenCueForMeta(cues[thisMeta]).replace('\n', ' '));
- }
- else if (this.metaType === 'selector') {
- // it's time to show content referenced by the designated selector(s)
- cueText = this.flattenCueForMeta(cues[thisMeta]);
- cueLines = cueText.split('\n');
- for (i=0; i= 1) {
+ cues = this.meta;
+ } else {
+ cues = [];
+ }
+ for (m = 0; m < cues.length; m++) {
+ if (cues[m].start <= now && cues[m].end > now) {
+ thisMeta = m;
+ break;
+ }
+ }
+ if (typeof thisMeta !== "undefined") {
+ if (this.currentMeta !== thisMeta) {
+ if (this.metaType === "text") {
+ this.$metaDiv.html(
+ this.flattenCueForMeta(cues[thisMeta]).replace(/\n/g, " ")
+ );
+ } else if (this.metaType === "selector") {
+ cueText = this.flattenCueForMeta(cues[thisMeta]);
+ cueLines = cueText.split("\n");
+ for (i = 0; i < cueLines.length; i++) {
+ line = cueLines[i].trim();
+ if (line.toLowerCase().trim() === "pause") {
+ this.hideBigPlayButton = true;
+ this.pauseMedia();
+ } else if (line.toLowerCase().substring(0, 6) == "focus:") {
+ focusTarget = line.substring(6).trim();
+ if ($(focusTarget).length) {
+ $(focusTarget).trigger('focus');
+ }
+ } else {
+ if ($(line).length) {
this.currentMeta = thisMeta;
- showDuration = parseInt($(line).attr('data-duration'));
- if (typeof showDuration !== 'undefined' && !isNaN(showDuration)) {
- $(line).show().delay(showDuration).fadeOut();
- }
- else {
- // no duration specified. Just show the element until end time specified in VTT file
- $(line).show();
- }
- // add to array of visible selectors so it can be hidden at end time
- this.visibleSelectors.push(line);
- tempSelectors.push(line);
-
- }
- }
- }
- // now step through this.visibleSelectors and remove anything that's stale
- if (this.visibleSelectors && this.visibleSelectors.length) {
- if (this.visibleSelectors.length !== tempSelectors.length) {
- for (i=this.visibleSelectors.length-1; i>=0; i--) {
- if ($.inArray(this.visibleSelectors[i],tempSelectors) == -1) {
- $(this.visibleSelectors[i]).hide();
- this.visibleSelectors.splice(i,1);
- }
- }
- }
- }
-
- }
- }
- }
- else {
- // there is currently no metadata. Empty stale content
- if (typeof this.$metaDiv !== 'undefined') {
- this.$metaDiv.html('');
- }
- if (this.visibleSelectors && this.visibleSelectors.length) {
- for (i=0; i new Promise(resolve => setTimeout(resolve, ms));
+ delay(showDuration).then(() => {
+ $(line).hide();
+ });
+ } else {
+ $(line).show();
+ }
+ this.visibleSelectors.push(line);
+ tempSelectors.push(line);
+ }
+ }
+ }
+ if (this.visibleSelectors && this.visibleSelectors.length) {
+ if (this.visibleSelectors.length !== tempSelectors.length) {
+ for (i = this.visibleSelectors.length - 1; i >= 0; i--) {
+ if ($.inArray(this.visibleSelectors[i], tempSelectors) == -1) {
+ $(this.visibleSelectors[i]).hide();
+ this.visibleSelectors.splice(i, 1);
+ }
+ }
+ }
+ }
+ }
+ }
+ } else {
+ if (typeof this.$metaDiv !== "undefined") {
+ this.$metaDiv.html("");
+ }
+ if (this.visibleSelectors && this.visibleSelectors.length) {
+ for (i = 0; i < this.visibleSelectors.length; i++) {
+ $(this.visibleSelectors[i]).hide();
+ }
+ this.visibleSelectors = [];
+ }
+ this.currentMeta = -1;
+ }
+ };
- for (var ii = 0; ii < cue.components.children.length; ii++) {
- result.push(flattenComponent(cue.components.children[ii]));
- }
+ AblePlayer.prototype.flattenCueForMeta = function (cue) {
+ var result = [];
+
+ var flattenComponent = function (component) {
+ var result = [],
+ ii;
+ if (component.type === "string") {
+ result.push(component.value);
+ } else if (component.type === "v") {
+ result.push("[" + component.value + "]");
+ for (ii = 0; ii < component.children.length; ii++) {
+ result.push(flattenComponent(component.children[ii]));
+ }
+ } else {
+ for (ii = 0; ii < component.children.length; ii++) {
+ result.push(flattenComponent(component.children[ii]));
+ }
+ }
+ return result.join("");
+ };
- return result.join('');
- };
+ for (var ii = 0; ii < cue.components.children.length; ii++) {
+ result.push(flattenComponent(cue.components.children[ii]));
+ }
+ return result.join("");
+ };
})(jQuery);
(function ($) {
+ AblePlayer.prototype.setupTranscript = function () {
+ var deferred = new this.defer();
+ var promise = deferred.promise();
+
+ if (this.usingYouTubeCaptions || this.usingVimeoCaptions || this.hideTranscriptButton ) {
+ this.transcriptType = null;
+ deferred.resolve();
+ } else {
+ if (!this.transcriptType) {
+
+ if (this.captions.length) {
+ this.transcriptType = "popup";
+ }
+ }
+ if (this.transcriptType) {
+ if ( this.transcriptType === "popup" || this.transcriptType === "external" ) {
+ this.injectTranscriptArea();
+ deferred.resolve();
+ } else if (this.transcriptType === "manual") {
+ this.setupManualTranscript();
+ deferred.resolve();
+ }
+ } else {
+ deferred.resolve();
+ }
+ }
+ return promise;
+ };
- AblePlayer.prototype.setupTranscript = function() {
-
- var deferred = new $.Deferred();
- var promise = deferred.promise();
-
- if (!this.transcriptType) {
- // previously set transcriptType to null since there are no elements
- // check again to see if captions have been collected from other sources (e.g., YouTube)
-
- if (this.captions.length && (!(this.usingYouTubeCaptions || this.usingVimeoCaptions))) {
- // captions are possible! Use the default type (popup)
- // if other types ('external' and 'manual') were desired, transcriptType would not be null here
- this.transcriptType = 'popup';
- }
- }
-
- if (this.transcriptType) {
- if (this.transcriptType === 'popup' || this.transcriptType === 'external') {
- this.injectTranscriptArea();
- deferred.resolve();
- }
- else if (this.transcriptType === 'manual') {
- this.setupManualTranscript();
- deferred.resolve();
- }
- }
- else {
- // there is no transcript
- deferred.resolve();
- }
- return promise;
- };
-
- AblePlayer.prototype.injectTranscriptArea = function() {
-
- var thisObj, $autoScrollLabel, $languageSelectWrapper, $languageSelectLabel, i, $option;
-
- thisObj = this;
- this.$transcriptArea = $('', {
- 'class': 'able-transcript-area',
- 'role': 'dialog',
- 'aria-label': this.tt.transcriptTitle
- });
-
- this.$transcriptToolbar = $('
', {
- 'class': 'able-window-toolbar able-' + this.toolbarIconColor + '-controls'
- });
-
- this.$transcriptDiv = $('
', {
- 'class' : 'able-transcript'
- });
-
- // Transcript toolbar content
-
- // Add auto Scroll checkbox
- this.$autoScrollTranscriptCheckbox = $('
', {
- 'id': 'autoscroll-transcript-checkbox',
- 'type': 'checkbox'
- });
- $autoScrollLabel = $('
', {
- 'for': 'autoscroll-transcript-checkbox'
- }).text(this.tt.autoScroll);
- this.$transcriptToolbar.append($autoScrollLabel,this.$autoScrollTranscriptCheckbox);
-
- // Add field for selecting a transcript language
- // Only necessary if there is more than one language
- if (this.captions.length > 1) {
- $languageSelectWrapper = $('',{
- 'class': 'transcript-language-select-wrapper'
- });
- $languageSelectLabel = $('
',{
- 'for': 'transcript-language-select'
- }).text(this.tt.language);
- this.$transcriptLanguageSelect = $('',{
- 'id': 'transcript-language-select'
- });
- for (i=0; i < this.captions.length; i++) {
- $option = $(' ',{
- value: this.captions[i]['language'],
- lang: this.captions[i]['language']
- }).text(this.captions[i]['label']);
- if (this.captions[i]['def']) {
- $option.prop('selected',true);
- }
- this.$transcriptLanguageSelect.append($option);
- }
- }
- if ($languageSelectWrapper) {
- $languageSelectWrapper.append($languageSelectLabel,this.$transcriptLanguageSelect);
- this.$transcriptToolbar.append($languageSelectWrapper);
- }
- this.$transcriptArea.append(this.$transcriptToolbar, this.$transcriptDiv);
-
- // If client has provided separate transcript location, put it there.
- // Otherwise append it to the body
- if (this.transcriptDivLocation) {
- $('#' + this.transcriptDivLocation).append(this.$transcriptArea);
- }
- else {
- this.$ableWrapper.append(this.$transcriptArea);
- }
-
- // make it draggable (popup only; NOT external transcript)
- if (!this.transcriptDivLocation) {
- this.initDragDrop('transcript');
- if (this.prefTranscript === 1) {
- // transcript is on. Go ahead and position it
- this.positionDraggableWindow('transcript',this.getDefaultWidth('transcript'));
- }
- }
-
- // If client has provided separate transcript location, override user's preference for hiding transcript
- if (!this.prefTranscript && !this.transcriptDivLocation) {
- this.$transcriptArea.hide();
- }
- };
-
- AblePlayer.prototype.addTranscriptAreaEvents = function() {
-
- var thisObj = this;
-
- this.$autoScrollTranscriptCheckbox.click(function () {
- thisObj.handleTranscriptLockToggle(thisObj.$autoScrollTranscriptCheckbox.prop('checked'));
- });
-
- this.$transcriptDiv.on('mousewheel DOMMouseScroll click scroll', function (e) {
- // Propagation is stopped in transcript click handler, so clicks are on the scrollbar
- // or outside of a clickable span.
- if (!thisObj.scrollingTranscript) {
- thisObj.autoScrollTranscript = false;
- thisObj.refreshControls('transcript');
- }
- thisObj.scrollingTranscript = false;
- });
-
- if (typeof this.$transcriptLanguageSelect !== 'undefined') {
-
- this.$transcriptLanguageSelect.on('click mousedown',function (e) {
- // execute default behavior
- // prevent propagation of mouse event to toolbar or window
- e.stopPropagation();
- });
-
- this.$transcriptLanguageSelect.on('change',function () {
-
- var language = thisObj.$transcriptLanguageSelect.val();
-
- thisObj.syncTrackLanguages('transcript',language);
- });
- }
- };
-
- AblePlayer.prototype.transcriptSrcHasRequiredParts = function() {
-
- // check the external transcript to be sure it has all required components
- // return true or false
- // in the process, define all the needed variables and properties
-
- if ($('#' + this.transcriptSrc).length) {
- this.$transcriptArea = $('#' + this.transcriptSrc);
- if (this.$transcriptArea.find('.able-window-toolbar').length) {
- this.$transcriptToolbar = this.$transcriptArea.find('.able-window-toolbar').eq(0);
- if (this.$transcriptArea.find('.able-transcript').length) {
- this.$transcriptDiv = this.$transcriptArea.find('.able-transcript').eq(0);
- if (this.$transcriptArea.find('.able-transcript-seekpoint').length) {
- this.$transcriptSeekpoints = this.$transcriptArea.find('.able-transcript-seekpoint');
- return true;
- }
- }
- }
- }
- return false;
- }
-
- AblePlayer.prototype.setupManualTranscript = function() {
-
- // Add an auto-scroll checkbox to the toolbar
-
- this.$autoScrollTranscriptCheckbox = $(' ');
- this.$transcriptToolbar.append($('' + this.tt.autoScroll + ': '), this.$autoScrollTranscriptCheckbox);
-
- };
-
- AblePlayer.prototype.updateTranscript = function() {
-
- if (!this.transcriptType) {
- return;
- }
-
- if (this.transcriptType === 'external' || this.transcriptType === 'popup') {
-
- var chapters, captions, descriptions;
-
- // Language of transcript might be different than language of captions
- // But both are in sync by default
- if (this.transcriptLang) {
- captions = this.transcriptCaptions.cues;
- }
- else {
- if (this.transcriptCaptions) {
- this.transcriptLang = this.transcriptCaptions.language;
- captions = this.transcriptCaptions.cues;
- }
- else if (this.selectedCaptions) {
- this.transcriptLang = this.captionLang;
- captions = this.selectedCaptions.cues;
- }
- }
-
- // setup chapters
- if (this.transcriptChapters) {
- chapters = this.transcriptChapters.cues;
- }
- else if (this.chapters.length > 0) {
- // Try and match the caption language.
- if (this.transcriptLang) {
- for (var i = 0; i < this.chapters.length; i++) {
- if (this.chapters[i].language === this.transcriptLang) {
- chapters = this.chapters[i].cues;
- }
- }
- }
- if (typeof chapters === 'undefined') {
- chapters = this.chapters[0].cues || [];
- }
- }
-
- // setup descriptions
- if (this.transcriptDescriptions) {
- descriptions = this.transcriptDescriptions.cues;
- }
- else if (this.descriptions.length > 0) {
- // Try and match the caption language.
- if (this.transcriptLang) {
- for (var i = 0; i < this.descriptions.length; i++) {
- if (this.descriptions[i].language === this.transcriptLang) {
- descriptions = this.descriptions[i].cues;
- }
- }
- }
- if (!descriptions) {
- descriptions = this.descriptions[0].cues || [];
- }
- }
-
- var div = this.generateTranscript(chapters || [], captions || [], descriptions || []);
-
- this.$transcriptDiv.html(div);
- // reset transcript selected to this.transcriptLang
- if (this.$transcriptLanguageSelect) {
- this.$transcriptLanguageSelect.find('option:selected').prop('selected',false);
- this.$transcriptLanguageSelect.find('option[lang=' + this.transcriptLang + ']').prop('selected',true);
- }
- }
-
- var thisObj = this;
+ AblePlayer.prototype.injectTranscriptArea = function () {
+ var thisObj,
+ $autoScrollLabel,
+ $languageSelectWrapper,
+ $languageSelectLabel,
+ i,
+ $option;
- // Make transcript tabbable if preference is turned on.
- if (this.prefTabbable === 1) {
- this.$transcriptDiv.find('span.able-transcript-seekpoint').attr('tabindex','0');
- }
-
- // handle clicks on text within transcript
- // Note: This event listeners handles clicks only, not keydown events
- // Pressing Enter on an element that is not natively clickable does NOT trigger click()
- // Keydown events are handled elsehwere, both globally (ableplayer-base.js) and locally (event.js)
- if (this.$transcriptArea.length > 0) {
- this.$transcriptArea.find('span.able-transcript-seekpoint').click(function(e) {
- thisObj.seekTrigger = 'transcript';
- var spanStart = parseFloat($(this).attr('data-start'));
- // Add a tiny amount so that we're inside the span.
- spanStart += .01;
- // Each click within the transcript triggers two click events (not sure why)
- // this.seekingFromTranscript is a stopgab to prevent two calls to SeekTo()
- if (!thisObj.seekingFromTranscript) {
- thisObj.seekingFromTranscript = true;
- thisObj.seekTo(spanStart);
- }
- else {
- // don't seek a second time, but do reset var
- thisObj.seekingFromTranscript = false;
- }
- });
- }
- };
+ thisObj = this;
+ this.$transcriptArea = $("", {
+ class: "able-transcript-area",
+ role: "dialog",
+ "aria-label": this.translate( 'transcriptTitle', 'Transcript' ),
+ });
+
+ this.$transcriptToolbar = $("
", {
+ class: "able-window-toolbar able-" + this.toolbarIconColor + "-controls",
+ });
+
+ this.$transcriptDiv = $("
", {
+ class: "able-transcript",
+ });
+
+
+ this.$autoScrollTranscriptCheckbox = $("
", {
+ id: "autoscroll-transcript-checkbox-" + this.mediaId,
+ type: "checkbox",
+ });
+ $autoScrollLabel = $("
", {
+ for: "autoscroll-transcript-checkbox-" + this.mediaId,
+ }).text( this.translate( 'autoScroll', 'Auto scroll' ) );
+ $autoScrollContainer = $( '', {
+ 'class': 'autoscroll-transcript'
+ });
+ $autoScrollContainer.append(
+ $autoScrollLabel,
+ this.$autoScrollTranscriptCheckbox
+ );
+ this.$transcriptToolbar.append( $autoScrollContainer );
+
+ if (this.captions.length > 1) {
+ $languageSelectWrapper = $("
", {
+ class: "transcript-language-select-wrapper",
+ });
+ $languageSelectLabel = $("
", {
+ for: "transcript-language-select-" + this.mediaId,
+ }).text( this.translate( 'language', 'Language' ) );
+ this.$transcriptLanguageSelect = $("", {
+ id: "transcript-language-select-" + this.mediaId,
+ });
+ for (i = 0; i < this.captions.length; i++) {
+ $option = $(" ", {
+ value: this.captions[i]["language"],
+ lang: this.captions[i]["language"],
+ }).text(this.captions[i]["label"]);
+ if (this.captions[i]["def"]) {
+ $option.prop("selected", true);
+ }
+ this.$transcriptLanguageSelect.append($option);
+ }
+ }
+ if ($languageSelectWrapper) {
+ $languageSelectWrapper.append(
+ $languageSelectLabel,
+ this.$transcriptLanguageSelect
+ );
+ this.$transcriptToolbar.append($languageSelectWrapper);
+ }
+ this.$transcriptArea.append(this.$transcriptToolbar, this.$transcriptDiv);
+
+ if (this.transcriptDivLocation) {
+ this.$transcriptArea.removeAttr( 'role' );
+ this.$transcriptArea.removeAttr( 'aria-label' );
+ $("#" + this.transcriptDivLocation).append(this.$transcriptArea);
+ } else {
+ this.$ableWrapper.append(this.$transcriptArea);
+ }
- AblePlayer.prototype.highlightTranscript = function (currentTime) {
+ if (!this.transcriptDivLocation) {
+ this.initDragDrop("transcript");
+ if (this.prefTranscript === 1) {
+ this.positionDraggableWindow(
+ "transcript",
+ this.getDefaultWidth("transcript")
+ );
+ }
+ }
- //show highlight in transcript marking current caption
+ if (!this.prefTranscript && !this.transcriptDivLocation) {
+ this.$transcriptArea.hide();
+ }
+ };
- if (!this.transcriptType) {
- return;
- }
+ AblePlayer.prototype.addTranscriptAreaEvents = function () {
+ var thisObj = this;
- var start, end, isChapterHeading;
- var thisObj = this;
+ this.$autoScrollTranscriptCheckbox.on( 'click', function () {
+ thisObj.handleTranscriptLockToggle(
+ thisObj.$autoScrollTranscriptCheckbox.prop("checked")
+ );
+ });
+
+ this.$transcriptDiv.on(
+ "mousewheel DOMMouseScroll click scroll",
+ function (e) {
+ if (!thisObj.scrollingTranscript) {
+ thisObj.autoScrollTranscript = false;
+ thisObj.refreshControls("transcript");
+ }
+ thisObj.scrollingTranscript = false;
+ }
+ );
- currentTime = parseFloat(currentTime);
+ if (typeof this.$transcriptLanguageSelect !== "undefined") {
+ this.$transcriptLanguageSelect.on('click', function (e) {
+ e.stopPropagation();
+ });
- // Highlight the current transcript item.
- this.$transcriptArea.find('span.able-transcript-seekpoint').each(function() {
- start = parseFloat($(this).attr('data-start'));
- end = parseFloat($(this).attr('data-end'));
- // be sure this isn't a chapter (don't highlight chapter headings)
- if ($(this).parent().hasClass('able-transcript-chapter-heading')) {
- isChapterHeading = true;
- }
- else {
- isChapterHeading = false;
- }
+ this.$transcriptLanguageSelect.on("change", function () {
+ var language = thisObj.$transcriptLanguageSelect.val();
- if (currentTime >= start && currentTime <= end && !isChapterHeading) {
+ thisObj.syncTrackLanguages("transcript", language);
+ });
+ }
+ };
- // If this item isn't already highlighted, it should be
- if (!($(this).hasClass('able-highlight'))) {
- // remove all previous highlights before adding one to current span
- thisObj.$transcriptArea.find('.able-highlight').removeClass('able-highlight');
- $(this).addClass('able-highlight');
- thisObj.movingHighlight = true;
+ AblePlayer.prototype.transcriptSrcHasRequiredParts = function () {
+
+ if ($("#" + this.transcriptSrc).length) {
+ this.$transcriptArea = $("#" + this.transcriptSrc);
+ if (this.$transcriptArea.find(".able-window-toolbar").length) {
+ this.$transcriptToolbar = this.$transcriptArea
+ .find(".able-window-toolbar")
+ .eq(0);
+ if (this.$transcriptArea.find(".able-transcript").length) {
+ this.$transcriptDiv = this.$transcriptArea
+ .find(".able-transcript")
+ .eq(0);
+ if (this.$transcriptArea.find(".able-transcript-seekpoint").length) {
+ this.$transcriptSeekpoints = this.$transcriptArea.find(
+ ".able-transcript-seekpoint"
+ );
+ return true;
+ }
}
- return false;
- }
- });
- thisObj.currentHighlight = thisObj.$transcriptArea.find('.able-highlight');
- if (thisObj.currentHighlight.length === 0) {
- // Nothing highlighted.
- thisObj.currentHighlight = null;
- }
- };
+ }
+ }
+ return false;
+ };
- AblePlayer.prototype.generateTranscript = function(chapters, captions, descriptions) {
+ AblePlayer.prototype.setupManualTranscript = function () {
+ var $autoScrollInput, $autoScrollLabel;
+
+ $autoScrollInput = $(" ", {
+ id: "autoscroll-transcript-checkbox-" + this.mediaId,
+ type: "checkbox",
+ });
+ $autoScrollLabel = $("", {
+ for: "autoscroll-transcript-checkbox-" + this.mediaId,
+ }).text( this.translate( 'autoScroll', 'Auto scroll' ) );
+
+ this.$autoScrollTranscriptCheckbox = $autoScrollInput;
+ this.$transcriptToolbar.append(
+ $autoScrollLabel,
+ this.$autoScrollTranscriptCheckbox
+ );
+ };
- var thisObj = this;
+ AblePlayer.prototype.updateTranscript = function () {
+ if (!this.transcriptType) {
+ return;
+ }
+ if (this.playerCreated && !this.$transcriptArea) {
+ return;
+ }
+ if (this.transcriptType === "external" || this.transcriptType === "popup") {
+ var chapters, captions, descriptions;
+
+ if (this.transcriptLang) {
+ captions = this.transcriptCaptions.cues;
+ } else {
+ if (this.transcriptCaptions) {
+ this.transcriptLang = this.transcriptCaptions.language;
+ captions = this.transcriptCaptions.cues;
+ } else if (this.selectedCaptions) {
+ this.transcriptLang = this.captionLang;
+ captions = this.selectedCaptions.cues;
+ }
+ }
- var $main = $('
');
- var transcriptTitle;
+ if (this.transcriptChapters) {
+ chapters = this.transcriptChapters.cues;
+ } else if (this.chapters.length > 0) {
+ if (this.transcriptLang) {
+ for (var i = 0; i < this.chapters.length; i++) {
+ if (this.chapters[i].language === this.transcriptLang) {
+ chapters = this.chapters[i].cues;
+ }
+ }
+ }
+ if (typeof chapters === "undefined") {
+ chapters = this.chapters[0].cues || [];
+ }
+ }
- // set language for transcript container
- $main.attr('lang', this.transcriptLang);
+ if (this.transcriptDescriptions) {
+ descriptions = this.transcriptDescriptions.cues;
+ } else if (this.descriptions.length > 0) {
+ if (this.transcriptLang) {
+ for (var i = 0; i < this.descriptions.length; i++) {
+ if (this.descriptions[i].language === this.transcriptLang) {
+ descriptions = this.descriptions[i].cues;
+ }
+ }
+ }
+ if (!descriptions) {
+ descriptions = this.descriptions[0].cues || [];
+ }
+ }
- if (typeof this.transcriptTitle !== 'undefined') {
- transcriptTitle = this.transcriptTitle;
- }
- else if (this.lyricsMode) {
- transcriptTitle = this.tt.lyricsTitle;
- }
- else {
- transcriptTitle = this.tt.transcriptTitle;
- }
+ var div = this.generateTranscript(
+ chapters || [],
+ captions || [],
+ descriptions || []
+ );
+ this.$transcriptDiv.html(div);
+ if (this.$transcriptLanguageSelect) {
+ this.$transcriptLanguageSelect
+ .find("option:selected")
+ .prop("selected", false);
+ this.$transcriptLanguageSelect
+ .find("option[lang=" + this.transcriptLang + "]")
+ .prop("selected", true);
+ }
+ }
- if (typeof this.transcriptDivLocation === 'undefined') {
- // only add an HTML heading to internal transcript
- // external transcript is expected to have its own heading
- var headingNumber = this.playerHeadingLevel;
- headingNumber += 1;
- var chapterHeadingNumber = headingNumber + 1;
+ var thisObj = this;
- if (headingNumber <= 6) {
- var transcriptHeading = 'h' + headingNumber.toString();
- }
- else {
- var transcriptHeading = 'div';
- }
- // var transcriptHeadingTag = '<' + transcriptHeading + ' class="able-transcript-heading">';
- var $transcriptHeadingTag = $('<' + transcriptHeading + '>');
- $transcriptHeadingTag.addClass('able-transcript-heading');
- if (headingNumber > 6) {
- $transcriptHeadingTag.attr({
- 'role': 'heading',
- 'aria-level': headingNumber
- });
- }
- $transcriptHeadingTag.text(transcriptTitle);
+ if (this.prefTabbable === 1) {
+ this.$transcriptDiv
+ .find("span.able-transcript-seekpoint")
+ .attr("tabindex", "0");
+ }
- // set language of transcript heading to language of player
- // this is independent of language of transcript
- $transcriptHeadingTag.attr('lang', this.lang);
+ if (this.$transcriptArea.length > 0) {
+ this.$transcriptArea
+ .find("span.able-transcript-seekpoint")
+ .on( 'click', function (e) {
+ thisObj.seekTrigger = "transcript";
+ var spanStart = parseFloat($(this).attr("data-start"));
+ spanStart += 0.01;
+ if (!thisObj.seekingFromTranscript) {
+ thisObj.seekingFromTranscript = true;
+ thisObj.seekTo(spanStart);
+ } else {
+ thisObj.seekingFromTranscript = false;
+ }
+ });
+ }
+ };
- $main.append($transcriptHeadingTag);
- }
+ AblePlayer.prototype.highlightTranscript = function (currentTime) {
- var nextChapter = 0;
- var nextCap = 0;
- var nextDesc = 0;
+ if (!this.transcriptType) {
+ return;
+ }
- var addChapter = function(div, chap) {
+ var start, end, isChapterHeading;
+ var thisObj = this;
- if (chapterHeadingNumber <= 6) {
- var chapterHeading = 'h' + chapterHeadingNumber.toString();
- }
- else {
- var chapterHeading = 'div';
- }
+ currentTime = parseFloat(currentTime);
+
+ this.$transcriptArea
+ .find("span.able-transcript-seekpoint")
+ .each(function () {
+ start = parseFloat($(this).attr("data-start"));
+ end = parseFloat($(this).attr("data-end"));
+ if ($(this).parent().hasClass("able-transcript-chapter-heading")) {
+ isChapterHeading = true;
+ } else {
+ isChapterHeading = false;
+ }
- var $chapterHeadingTag = $('<' + chapterHeading + '>',{
- 'class': 'able-transcript-chapter-heading'
- });
- if (chapterHeadingNumber > 6) {
- $chapterHeadingTag.attr({
- 'role': 'heading',
- 'aria-level': chapterHeadingNumber
- });
- }
+ if (currentTime >= start && currentTime <= end && !isChapterHeading) {
+ if (!$(this).hasClass("able-highlight")) {
+ thisObj.$transcriptArea
+ .find(".able-highlight")
+ .removeClass("able-highlight");
+ $(this).addClass("able-highlight");
+ thisObj.movingHighlight = true;
+ }
+ return false;
+ }
+ });
+ thisObj.currentHighlight = thisObj.$transcriptArea.find(".able-highlight");
+ if (thisObj.currentHighlight.length === 0) {
+ thisObj.currentHighlight = null;
+ }
+ };
- var flattenComponentForChapter = function(comp) {
+ AblePlayer.prototype.generateTranscript = function (
+ chapters,
+ captions,
+ descriptions
+ ) {
+ var thisObj = this;
- var result = [];
- if (comp.type === 'string') {
- result.push(comp.value);
- }
- else {
- for (var i = 0; i < comp.children.length; i++) {
- result = result.concat(flattenComponentForChapter(comp.children[i]));
- }
- }
- return result;
- }
+ var $main = $('
');
+ var transcriptTitle;
- var $chapSpan = $('',{
- 'class': 'able-transcript-seekpoint'
- });
- for (var i = 0; i < chap.components.children.length; i++) {
- var results = flattenComponentForChapter(chap.components.children[i]);
- for (var jj = 0; jj < results.length; jj++) {
- $chapSpan.append(results[jj]);
- }
- }
- $chapSpan.attr('data-start', chap.start.toString());
- $chapSpan.attr('data-end', chap.end.toString());
- $chapterHeadingTag.append($chapSpan);
+ $main.attr("lang", this.transcriptLang);
- div.append($chapterHeadingTag);
- };
+ if (typeof this.transcriptTitle !== "undefined") {
+ transcriptTitle = this.transcriptTitle;
+ } else if (this.lyricsMode) {
+ transcriptTitle = this.translate( 'lyricsTitle', 'Lyrics' );
+ } else {
+ transcriptTitle = this.translate( 'transcriptTitle', 'Transcript' );
+ }
- var addDescription = function(div, desc) {
- var $descDiv = $('', {
- 'class': 'able-transcript-desc'
- });
- var $descHiddenSpan = $('
',{
- 'class': 'able-hidden'
- });
- $descHiddenSpan.attr('lang', thisObj.lang);
- $descHiddenSpan.text(thisObj.tt.prefHeadingDescription + ': ');
- $descDiv.append($descHiddenSpan);
+ if (!this.transcriptDivLocation) {
+ var headingNumber = this.playerHeadingLevel;
+ headingNumber += 1;
+ var chapterHeadingNumber = headingNumber + 1;
- var flattenComponentForDescription = function(comp) {
+ if (headingNumber <= 6) {
+ var transcriptHeading = "h" + headingNumber.toString();
+ } else {
+ var transcriptHeading = "div";
+ }
+ var $transcriptHeadingTag = $("<" + transcriptHeading + ">");
+ $transcriptHeadingTag.addClass("able-transcript-heading");
+ if (headingNumber > 6) {
+ $transcriptHeadingTag.attr({
+ role: "heading",
+ "aria-level": headingNumber,
+ });
+ }
+ $transcriptHeadingTag.text(transcriptTitle);
- var result = [];
- if (comp.type === 'string') {
- result.push(comp.value);
- }
- else {
- for (var i = 0; i < comp.children.length; i++) {
- result = result.concat(flattenComponentForDescription(comp.children[i]));
- }
- }
- return result;
- }
+ $transcriptHeadingTag.attr("lang", this.lang);
- var $descSpan = $('',{
- 'class': 'able-transcript-seekpoint'
- });
- for (var i = 0; i < desc.components.children.length; i++) {
- var results = flattenComponentForDescription(desc.components.children[i]);
- for (var jj = 0; jj < results.length; jj++) {
- $descSpan.append(results[jj]);
- }
- }
- $descSpan.attr('data-start', desc.start.toString());
- $descSpan.attr('data-end', desc.end.toString());
- $descDiv.append($descSpan);
+ $main.append($transcriptHeadingTag);
+ }
- div.append($descDiv);
- };
+ var nextChapter = 0;
+ var nextCap = 0;
+ var nextDesc = 0;
- var addCaption = function(div, cap) {
+ var addChapter = function (div, chap) {
+ if (chapterHeadingNumber <= 6) {
+ var chapterHeading = "h" + chapterHeadingNumber.toString();
+ } else {
+ var chapterHeading = "div";
+ }
- var $capSpan = $('',{
- 'class': 'able-transcript-seekpoint able-transcript-caption'
- });
+ var $chapterHeadingTag = $("<" + chapterHeading + ">", {
+ class: "able-transcript-chapter-heading",
+ });
+ if (chapterHeadingNumber > 6) {
+ $chapterHeadingTag.attr({
+ role: "heading",
+ "aria-level": chapterHeadingNumber,
+ });
+ }
- var flattenComponentForCaption = function(comp) {
+ var flattenComponentForChapter = function (comp) {
+ var result = [];
+ if (comp.type === "string") {
+ result.push(comp.value);
+ } else {
+ for (var i = 0; i < comp.children.length; i++) {
+ result = result.concat(
+ flattenComponentForChapter(comp.children[i])
+ );
+ }
+ }
+ return result;
+ };
+
+ var $chapSpan = $("", {
+ class: "able-transcript-seekpoint",
+ });
+ for (var i = 0; i < chap.components.children.length; i++) {
+ var results = flattenComponentForChapter(chap.components.children[i]);
+ for (var jj = 0; jj < results.length; jj++) {
+ $chapSpan.append(results[jj]);
+ }
+ }
+ $chapSpan.attr("data-start", chap.start.toString());
+ $chapSpan.attr("data-end", chap.end.toString());
+ $chapterHeadingTag.append($chapSpan);
- var result = [];
+ div.append($chapterHeadingTag);
+ };
- var parts = 0;
+ var addDescription = function (div, desc) {
+ var $descDiv = $("", {
+ class: "able-transcript-desc",
+ });
+ var $descHiddenSpan = $("
", {
+ class: "able-hidden",
+ });
+ $descHiddenSpan.attr("lang", thisObj.lang);
+ $descHiddenSpan.text(thisObj.tt.prefHeadingDescription + ": ");
+ $descDiv.append($descHiddenSpan);
+
+ var flattenComponentForDescription = function (comp) {
+ var result = [];
+ if (comp.type === "string") {
+ result.push(comp.value);
+ } else {
+ for (var i = 0; i < comp.children.length; i++) {
+ result = result.concat(
+ flattenComponentForDescription(comp.children[i])
+ );
+ }
+ }
+ return result;
+ };
+
+ var $descSpan = $("", {
+ class: "able-transcript-seekpoint",
+ });
+ for (var i = 0; i < desc.components.children.length; i++) {
+ var results = flattenComponentForDescription(
+ desc.components.children[i]
+ );
+ for (var jj = 0; jj < results.length; jj++) {
+ $descSpan.append(results[jj]);
+ }
+ }
+ $descSpan.attr("data-start", desc.start.toString());
+ $descSpan.attr("data-end", desc.end.toString());
+ $descDiv.append($descSpan);
- var flattenString = function (str) {
+ div.append($descDiv);
+ };
- parts++;
+ var addCaption = function (div, cap) {
+ var $capSpan = $("", {
+ class: "able-transcript-seekpoint able-transcript-caption",
+ });
- var flatStr;
- var result = [];
- if (str === '') {
- return result;
- }
+ var flattenComponentForCaption = function (comp) {
+ var result = [];
- var openBracket = str.indexOf('[');
- var closeBracket = str.indexOf(']');
- var openParen = str.indexOf('(');
- var closeParen = str.indexOf(')');
+ var parts = 0;
- var hasBrackets = openBracket !== -1 && closeBracket !== -1;
- var hasParens = openParen !== -1 && closeParen !== -1;
+ var flattenString = function (str) {
+ parts++;
- if (hasParens || hasBrackets) {
- if (parts > 1) {
- // force a line break between sections that contain parens or brackets
- var silentSpanBreak = ' ';
- }
- else {
- var silentSpanBreak = '';
- }
- var silentSpanOpen = silentSpanBreak + '';
- var silentSpanClose = ' ';
- if (hasParens && hasBrackets) {
- // string has both!
- if (openBracket < openParen) {
- // brackets come first. Parse parens separately
- hasParens = false;
- }
- else {
- // parens come first. Parse brackets separately
- hasBrackets = false;
- }
- }
- }
- if (hasParens) {
- flatStr = str.substring(0, openParen);
- flatStr += silentSpanOpen;
- flatStr += str.substring(openParen, closeParen + 1);
- flatStr += silentSpanClose;
- flatStr += flattenString(str.substring(closeParen + 1));
- result.push(flatStr);
- }
- else if (hasBrackets) {
- flatStr = str.substring(0, openBracket);
- flatStr += silentSpanOpen;
- flatStr += str.substring(openBracket, closeBracket + 1);
- flatStr += silentSpanClose;
- flatStr += flattenString(str.substring(closeBracket + 1));
- result.push(flatStr);
- }
- else {
- result.push(str);
- }
- return result;
- };
+ var flatStr;
+ var result = [];
+ if (str === "") {
+ return result;
+ }
- if (comp.type === 'string') {
- result = result.concat(flattenString(comp.value));
- }
- else if (comp.type === 'v') {
- var $vSpan = $('',{
- 'class': 'able-unspoken'
- });
- $vSpan.text('(' + comp.value + ')');
- result.push($vSpan);
- for (var i = 0; i < comp.children.length; i++) {
- var subResults = flattenComponentForCaption(comp.children[i]);
- for (var jj = 0; jj < subResults.length; jj++) {
- result.push(subResults[jj]);
- }
- }
- }
- else if (comp.type === 'b' || comp.type === 'i') {
- if (comp.type === 'b') {
- var $tag = $('');
- }
- else if (comp.type === 'i') {
- var $tag = $('');
- }
- for (var i = 0; i < comp.children.length; i++) {
- var subResults = flattenComponentForCaption(comp.children[i]);
- for (var jj = 0; jj < subResults.length; jj++) {
- $tag.append(subResults[jj]);
- }
- }
- if (comp.type === 'b' || comp.type == 'i') {
- result.push($tag,' ');
- }
- }
- else {
- for (var i = 0; i < comp.children.length; i++) {
- result = result.concat(flattenComponentForCaption(comp.children[i]));
- }
- }
- return result;
- };
+ var openBracket = str.indexOf("[");
+ var closeBracket = str.indexOf("]");
+ var openParen = str.indexOf("(");
+ var closeParen = str.indexOf(")");
- for (var i = 0; i < cap.components.children.length; i++) {
- var results = flattenComponentForCaption(cap.components.children[i]);
- for (var jj = 0; jj < results.length; jj++) {
- var result = results[jj];
- if (typeof result === 'string') {
- if (thisObj.lyricsMode) {
- // add BETWEEN each caption and WITHIN each caption (if payload includes "\n")
- result = result.replace('\n',' ') + ' ';
- }
- else {
- // just add a space between captions
- result += ' ';
- }
- }
- $capSpan.append(result);
- }
- }
- $capSpan.attr('data-start', cap.start.toString());
- $capSpan.attr('data-end', cap.end.toString());
- div.append($capSpan);
- div.append(' \n');
- };
+ var hasBrackets = openBracket !== -1 && closeBracket !== -1;
+ var hasParens = openParen !== -1 && closeParen !== -1;
- // keep looping as long as any one of the three arrays has content
- while ((nextChapter < chapters.length) || (nextDesc < descriptions.length) || (nextCap < captions.length)) {
+ if (hasParens || hasBrackets) {
+ if (parts > 1) {
+ var silentSpanBreak = " ";
+ } else {
+ var silentSpanBreak = "";
+ }
+ var silentSpanOpen =
+ silentSpanBreak + '';
+ var silentSpanClose = " ";
+ if (hasParens && hasBrackets) {
+ if (openBracket < openParen) {
+ hasParens = false;
+ } else {
+ hasBrackets = false;
+ }
+ }
+ }
+ if (hasParens) {
+ flatStr = str.substring(0, openParen);
+ flatStr += silentSpanOpen;
+ flatStr += str.substring(openParen, closeParen + 1);
+ flatStr += silentSpanClose;
+ flatStr += flattenString(str.substring(closeParen + 1));
+ result.push(flatStr);
+ } else if (hasBrackets) {
+ flatStr = str.substring(0, openBracket);
+ flatStr += silentSpanOpen;
+ flatStr += str.substring(openBracket, closeBracket + 1);
+ flatStr += silentSpanClose;
+ flatStr += flattenString(str.substring(closeBracket + 1));
+ result.push(flatStr);
+ } else {
+ result.push(str);
+ }
+ return result;
+ };
+
+ if (comp.type === "string") {
+ result = result.concat(flattenString(comp.value));
+ } else if (comp.type === "v") {
+ var $vSpan = $("", {
+ class: "able-unspoken",
+ });
+ comp.value = comp.value.replace(/^title="|\"$/g, "");
+ $vSpan.text("(" + comp.value + ")");
+ result.push($vSpan);
+ for (var i = 0; i < comp.children.length; i++) {
+ var subResults = flattenComponentForCaption(comp.children[i]);
+ for (var jj = 0; jj < subResults.length; jj++) {
+ result.push(subResults[jj]);
+ }
+ }
+ } else if (comp.type === "b" || comp.type === "i") {
+ if (comp.type === "b") {
+ var $tag = $("");
+ } else if (comp.type === "i") {
+ var $tag = $("");
+ }
+ for (var i = 0; i < comp.children.length; i++) {
+ var subResults = flattenComponentForCaption(comp.children[i]);
+ for (var jj = 0; jj < subResults.length; jj++) {
+ $tag.append(subResults[jj]);
+ }
+ }
+ if (comp.type === "b" || comp.type == "i") {
+ result.push($tag);
+ }
+ } else {
+ for (var i = 0; i < comp.children.length; i++) {
+ result = result.concat(
+ flattenComponentForCaption(comp.children[i])
+ );
+ }
+ }
+ return result;
+ };
- if ((nextChapter < chapters.length) && (nextDesc < descriptions.length) && (nextCap < captions.length)) {
- // they all three have content
- var firstStart = Math.min(chapters[nextChapter].start,descriptions[nextDesc].start,captions[nextCap].start);
- }
- else if ((nextChapter < chapters.length) && (nextDesc < descriptions.length)) {
- // chapters & descriptions have content
- var firstStart = Math.min(chapters[nextChapter].start,descriptions[nextDesc].start);
- }
- else if ((nextChapter < chapters.length) && (nextCap < captions.length)) {
- // chapters & captions have content
- var firstStart = Math.min(chapters[nextChapter].start,captions[nextCap].start);
- }
- else if ((nextDesc < descriptions.length) && (nextCap < captions.length)) {
- // descriptions & captions have content
- var firstStart = Math.min(descriptions[nextDesc].start,captions[nextCap].start);
- }
- else {
- var firstStart = null;
- }
- if (firstStart !== null) {
- if (typeof chapters[nextChapter] !== 'undefined' && chapters[nextChapter].start === firstStart) {
- addChapter($main, chapters[nextChapter]);
- nextChapter += 1;
- }
- else if (typeof descriptions[nextDesc] !== 'undefined' && descriptions[nextDesc].start === firstStart) {
- addDescription($main, descriptions[nextDesc]);
- nextDesc += 1;
- }
- else {
- addCaption($main, captions[nextCap]);
- nextCap += 1;
- }
- }
- else {
- if (nextChapter < chapters.length) {
- addChapter($main, chapters[nextChapter]);
- nextChapter += 1;
- }
- else if (nextDesc < descriptions.length) {
- addDescription($main, descriptions[nextDesc]);
- nextDesc += 1;
- }
- else if (nextCap < captions.length) {
- addCaption($main, captions[nextCap]);
- nextCap += 1;
- }
- }
+ for (var i = 0; i < cap.components.children.length; i++) {
+ var next_child_tagname;
+ if ( i < cap.components.children.length - 1 ) {
+ next_child_tagname = cap.components.children[i + 1].tagName;
}
- // organize transcript into blocks using [] and () as starting points
- var $components = $main.children();
- var spanCount = 0;
- var openBlock = true;
- $components.each(function() {
- if ($(this).hasClass('able-transcript-caption')) {
- if ($(this).text().indexOf('[') !== -1 || $(this).text().indexOf('(') !== -1) {
- // this caption includes a bracket or parenth. Start a new block
- // close the previous block first
- if (spanCount > 0) {
- $main.find('.able-block-temp').removeClass('able-block-temp').wrapAll('
');
- spanCount = 0;
- }
- }
- $(this).addClass('able-block-temp');
- spanCount++;
- }
- else {
- // this is not a caption. Close the caption block
- if (spanCount > 0) {
- $main.find('.able-block-temp').removeClass('able-block-temp').wrapAll('
');
- spanCount = 0;
- }
- }
- });
- return $main;
- };
-
-})(jQuery);
-
-(function ($) {
- AblePlayer.prototype.showSearchResults = function() {
-
- // search VTT file for all instances of searchTerms
- // Currently just supports search terms separated with one or more spaces
-
- // TODO: Add support for more robust search syntax:
- // Search terms wrapped in quotation marks ("") must occur exactly as they appear in the quotes
- // Search terms with an attached minus sign (e.g., -term) are to be excluded from results
- // Boolean AND/OR operators
- // ALSO: Add localization support
-
- var thisObj = this;
- if (this.searchDiv && this.searchString) {
- if ($('#' + this.SearchDiv)) {
- var searchStringHtml = '' + this.tt.resultsSummary1 + ' ';
- searchStringHtml += '' + this.searchString + ' ';
- searchStringHtml += '
';
- var resultsArray = this.searchFor(this.searchString);
- if (resultsArray.length > 0) {
- var $resultsSummary = $('',{
- 'class': 'able-search-results-summary'
- });
- var resultsSummaryText = this.tt.resultsSummary2;
- resultsSummaryText += ' ' + resultsArray.length + ' ';
- resultsSummaryText += this.tt.resultsSummary3 + ' ';
- resultsSummaryText += this.tt.resultsSummary4;
- $resultsSummary.html(resultsSummaryText);
- var $resultsList = $('
');
- for (var i = 0; i < resultsArray.length; i++) {
- var resultId = 'aria-search-result-' + i;
- var $resultsItem = $('',{});
- var itemStartTime = this.secondsToTime(resultsArray[i]['start']);
- var itemLabel = this.tt.searchButtonLabel + ' ' + itemStartTime['title'];
- var itemStartSpan = $('',{
- 'class': 'able-search-results-time',
- 'data-start': resultsArray[i]['start'],
- 'title': itemLabel,
- 'aria-label': itemLabel,
- 'aria-describedby': resultId
- });
- itemStartSpan.text(itemStartTime['value']);
- // add a listener for clisk on itemStart
- itemStartSpan.on('click',function(e) {
- thisObj.seekTrigger = 'search';
- var spanStart = parseFloat($(this).attr('data-start'));
- // Add a tiny amount so that we're inside the span.
- spanStart += .01;
- thisObj.seeking = true;
- thisObj.seekTo(spanStart);
- });
+ var results = flattenComponentForCaption(cap.components.children[i]);
+ for (var jj = 0; jj < results.length; jj++) {
+ var result = results[jj];
+ if (typeof result === "string") {
+ if (thisObj.lyricsMode) {
+ result = result.replace(/\n/g,' ');
- var itemText = $('',{
- 'class': 'able-search-result-text',
- 'id': resultId
- })
- itemText.html('...' + resultsArray[i]['caption'] + '...');
- $resultsItem.append(itemStartSpan, itemText);
- $resultsList.append($resultsItem);
- }
- $('#' + this.searchDiv).append(searchStringHtml,$resultsSummary,$resultsList);
- }
- else {
- var noResults = $('').text(this.tt.noResultsFound);
- $('#' + this.searchDiv).append(noResults);
+ if ( !next_child_tagname || ( next_child_tagname !== 'i' && next_child_tagname !== 'b' ) ) {
+ result += ' ';
}
- }
- }
- };
+ } else {
+ result += " ";
+ }
+ }
+ $capSpan.append(result);
+ }
+ }
+ $capSpan.attr("data-start", cap.start.toString());
+ $capSpan.attr("data-end", cap.end.toString());
+ div.append($capSpan);
+ div.append(" \n");
+ };
- AblePlayer.prototype.searchFor = function(searchString) {
+ while (
+ nextChapter < chapters.length ||
+ nextDesc < descriptions.length ||
+ nextCap < captions.length
+ ) {
+ if (
+ nextChapter < chapters.length &&
+ nextDesc < descriptions.length &&
+ nextCap < captions.length
+ ) {
+ var firstStart = Math.min(
+ chapters[nextChapter].start,
+ descriptions[nextDesc].start,
+ captions[nextCap].start
+ );
+ } else if (
+ nextChapter < chapters.length &&
+ nextDesc < descriptions.length
+ ) {
+ var firstStart = Math.min(
+ chapters[nextChapter].start,
+ descriptions[nextDesc].start
+ );
+ } else if (nextChapter < chapters.length && nextCap < captions.length) {
+ var firstStart = Math.min(
+ chapters[nextChapter].start,
+ captions[nextCap].start
+ );
+ } else if (nextDesc < descriptions.length && nextCap < captions.length) {
+ var firstStart = Math.min(
+ descriptions[nextDesc].start,
+ captions[nextCap].start
+ );
+ } else {
+ var firstStart = null;
+ }
+ if (firstStart !== null) {
+ if (
+ typeof chapters[nextChapter] !== "undefined" &&
+ chapters[nextChapter].start === firstStart
+ ) {
+ addChapter($main, chapters[nextChapter]);
+ nextChapter += 1;
+ } else if (
+ typeof descriptions[nextDesc] !== "undefined" &&
+ descriptions[nextDesc].start === firstStart
+ ) {
+ addDescription($main, descriptions[nextDesc]);
+ nextDesc += 1;
+ } else {
+ addCaption($main, captions[nextCap]);
+ nextCap += 1;
+ }
+ } else {
+ if (nextChapter < chapters.length) {
+ addChapter($main, chapters[nextChapter]);
+ nextChapter += 1;
+ } else if (nextDesc < descriptions.length) {
+ addDescription($main, descriptions[nextDesc]);
+ nextDesc += 1;
+ } else if (nextCap < captions.length) {
+ addCaption($main, captions[nextCap]);
+ nextCap += 1;
+ }
+ }
+ }
+ var $components = $main.children();
+ var spanCount = 0;
+ $components.each(function () {
+ if ($(this).hasClass("able-transcript-caption")) {
+ if (
+ $(this).text().indexOf("[") !== -1 ||
+ $(this).text().indexOf("(") !== -1
+ ) {
+ if (spanCount > 0) {
+ $main = wrapTranscriptBlocks( $main );
+ spanCount = 0;
+ }
+ }
+ $(this).addClass("able-block-temp");
+ spanCount++;
+ } else {
+ if (spanCount > 0) {
+ $main = wrapTranscriptBlocks( $main );
+ spanCount = 0;
+ }
+ }
+ });
+ $main = wrapTranscriptBlocks( $main );
- // return chronological array of caption cues that match searchTerms
- var captionLang, captions, results, caption, c, i, j;
- results = [];
- // split searchTerms into an array
- var searchTerms = searchString.split(' ');
- if (this.captions.length > 0) {
- // Get caption track that matches this.searchLang
- for (i=0; i < this.captions.length; i++) {
- if (this.captions[i].language === this.searchLang) {
- captionLang = this.searchLang;
- captions = this.captions[i].cues;
- }
- }
- if (captions.length > 0) {
- c = 0;
- for (i = 0; i < captions.length; i++) {
- if ($.inArray(captions[i].components.children[0]['type'], ['string','i','b','u','v','c']) !== -1) {
- caption = this.flattenCueForCaption(captions[i]);
- for (j = 0; j < searchTerms.length; j++) {
- if (caption.indexOf(searchTerms[j]) !== -1) {
- results[c] = [];
- results[c]['start'] = captions[i].start;
- results[c]['lang'] = captionLang;
- results[c]['caption'] = this.highlightSearchTerm(searchTerms,j,caption);
- c++;
- break;
- }
- }
- }
- }
- }
- }
- return results;
- };
+ return $main;
+ };
- AblePlayer.prototype.highlightSearchTerm = function(searchTerms, index, resultString) {
+ var wrapTranscriptBlocks = function( $main ) {
+ $main.find(".able-block-temp")
+ .removeClass("able-block-temp")
+ .wrapAll('
');
- // highlight ALL found searchTerms in the current resultString
- // index is the first index in the searchTerm array where a match has already been found
- // Need to step through the remaining terms to see if they're present as well
+ return $main;
+ }
+})(jQuery);
- var i, searchTerm, termIndex, termLength, str1, str2, str3;
+(function ($) {
+ AblePlayer.prototype.showSearchResults = function () {
- for (i=index; i 0) {
- str1 = resultString.substring(0, termIndex);
- str2 = '' + searchTerm + ' ';
- str3 = resultString.substring(termIndex+termLength);
- resultString = str1 + str2 + str3;
- }
- else {
- str1 = '' + searchTerm + ' ';
- str2 = resultString.substring(termIndex+termLength);
- resultString = str1 + str2;
- }
- }
- }
- return resultString;
- };
+ var thisObj = this;
+ if (this.searchDiv && this.searchString) {
+ var cleanSearchString = DOMPurify.sanitize(this.searchString);
+ if ($("#" + this.SearchDiv)) {
+ var searchStringHtml = "" + this.translate( 'resultsSummary1', 'You searched for:') + ' ';
+ searchStringHtml +=
+ '' + cleanSearchString + " ";
+ searchStringHtml += "
";
+ var resultsArray = this.searchFor(
+ cleanSearchString,
+ this.searchIgnoreCaps
+ );
+ if (resultsArray.length > 0) {
+ var $resultsSummary = $("", {
+ class: "able-search-results-summary",
+ });
+ var resultsSummaryText = this.translate( 'resultsSummary2', 'Found %1 matching items.', [ '' + resultsArray.length + ' ' ] );
+ resultsSummaryText += ' ' + this.translate( 'resultsSummary3', 'Click the time associated with any item to play the video from that point.' );
+ $resultsSummary.html( resultsSummaryText );
+ var $resultsList = $("
");
+ for (var i = 0; i < resultsArray.length; i++) {
+ var resultId = "aria-search-result-" + i;
+ var $resultsItem = $("", {});
+ var itemStartTime = this.secondsToTime(resultsArray[i]["start"]);
+ var itemLabel =
+ this.translate( 'searchButtonLabel', 'Play at %1', [ itemStartTime["title"] ] );
+ var itemStartSpan = $("", {
+ class: "able-search-results-time",
+ "data-start": resultsArray[i]["start"],
+ title: itemLabel,
+ "aria-label": itemLabel,
+ "aria-describedby": resultId,
+ });
+ itemStartSpan.text(itemStartTime["value"]);
+ itemStartSpan.on("click", function (e) {
+ thisObj.seekTrigger = "search";
+ var spanStart = parseFloat($(this).attr("data-start"));
+ spanStart += 0.01;
+ thisObj.seeking = true;
+ thisObj.seekTo(spanStart);
+ });
+
+ var itemText = $("