assets/ableplayer/scripts/track.js in wai-website-theme-1.3.1 vs assets/ableplayer/scripts/track.js in wai-website-theme-1.4

- old
+ new

@@ -1,312 +1,412 @@ (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() { + // 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 - var thisObj = this; + AblePlayer.prototype.setupTracks = function() { - var deferred = new $.Deferred(); - var promise = deferred.promise(); - this.$tracks = this.$media.find('track'); + var thisObj, deferred, promise, loadingPromises, loadingPromise, i, tracks, track; - this.captions = []; - this.captionLabels = []; - this.descriptions = []; - this.chapters = []; - this.meta = []; - if ($('#able-vts').length) { - // Page includes a container for a VTS instance - this.vtsTracks = []; - this.hasVts = true; - } - else { - this.hasVts = false; - } + thisObj = this; - var loadingPromises = []; - for (var ii = 0; ii < this.$tracks.length; ii++) { - var track = this.$tracks[ii]; - var kind = track.getAttribute('kind'); - var trackSrc = track.getAttribute('src'); - var isDefaultTrack = track.getAttribute('default'); + deferred = new $.Deferred(); + promise = deferred.promise(); - if (!trackSrc) { - // Nothing to load! - continue; - } + loadingPromises = []; - var loadingPromise = this.loadTextObject(trackSrc); - loadingPromises.push(loadingPromise); - loadingPromise.then((function (track, kind, trackLang, trackLabel) { + this.captions = []; + this.captionLabels = []; + this.descriptions = []; + this.chapters = []; + this.meta = []; - // srcLang should always be included with <track>, but HTML5 spec doesn't require it - // if not provided, assume track is the same language as the default player language - var trackLang = track.getAttribute('srclang') || thisObj.lang; - var trackLabel = track.getAttribute('label') || thisObj.getLanguageName(trackLang); + if ($('#able-vts').length) { + // Page includes a container for a VTS instance + this.vtsTracks = []; + this.hasVts = true; + } + else { + this.hasVts = false; + } - return function (trackSrc, trackText) { + this.getTracks().then(function() { - var trackContents = trackText; + tracks = thisObj.tracks; - // convert XMl/TTML captions file - if (thisObj.useTtml && (trackSrc.endsWith('.xml') || trackText.startsWith('<?xml'))) { - trackContents = thisObj.ttml2webvtt(trackText); - } + 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; + } + } - if (thisObj.hasVts) { - // setupVtsTracks() is in vts.js - thisObj.setupVtsTracks(kind, trackLang, trackLabel, trackSrc, trackContents); - } + for (i=0; i < tracks.length; i++) { - var cues = thisObj.parseWebVTT(trackSrc, trackContents).cues; + track = tracks[i]; - if (kind === 'captions' || kind === 'subtitles') { - thisObj.setupCaptions(track, cues, trackLang, trackLabel); - } - 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)); - } + var kind = track.kind; + var trackLang = track.language; + var trackLabel = track.label; - $.when.apply($, loadingPromises).then(function () { - deferred.resolve(); - }); - return promise; - }; + 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; + } - AblePlayer.prototype.setupCaptions = function (track, cues, trackLang, trackLabel) { + var trackSrc = track.src; - this.hasCaptions = true; - if (typeof track.getAttribute('default') == 'string') { - var isDefaultTrack = true; - // Now remove 'default' attribute from <track> - // Otherwise, some browsers will display the track - track.removeAttribute('default'); - } - else { - var isDefaultTrack = false; - } - // 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') { + loadingPromise = thisObj.loadTextObject(trackSrc); // resolves with src, trackText + loadingPromises.push(loadingPromise); - // 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 = $('<div>',{ - 'class': 'able-captions', - }); - this.$captionsWrapper = $('<div>',{ - '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); - } - } + loadingPromise.then((function (track, kind) { - this.currentCaption = -1; - if (this.prefCaptions === 1) { - // Captions default to on. - this.captionsOn = true; - } - else { - this.captionsOn = false; - } + var trackSrc = track.src; + var trackLang = track.language; + var trackLabel = track.label; - if (this.transcriptType === 'external' || this.transcriptType === 'popup') { - // Remove the "Unknown" option from the select box. - if (this.$unknownTranscriptOption) { - this.$unknownTranscriptOption.remove(); - this.$unknownTranscriptOption = null; - } - var option = $('<option></option>',{ - value: trackLang, - lang: trackLang - }).text(trackLabel); - } - // alphabetize tracks by label - if (this.transcriptType === 'external' || this.transcriptType === 'popup') { - var options = this.$transcriptLanguageSelect.find('option'); - } - if (this.captions.length === 0) { // this is the first - this.captions.push({ - 'cues': cues, - 'language': trackLang, - 'label': trackLabel, - 'def': isDefaultTrack - }); - if (this.transcriptType === 'external' || this.transcriptType === 'popup') { - if (isDefaultTrack) { - option.prop('selected', true); - } - this.$transcriptLanguageSelect.append(option); - } - this.captionLabels.push(trackLabel); - } - else { // there are already tracks in the array - var inserted = false; - for (var i = 0; i < this.captions.length; i++) { - var 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': isDefaultTrack - }); - if (this.transcriptType === 'external' || this.transcriptType === 'popup') { - if (isDefaultTrack) { - option.prop('selected', true); - } - option.insertBefore(options.eq(i)); - } - 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': isDefaultTrack - }); - if (this.transcriptType === 'external' || this.transcriptType === 'popup') { - if (isDefaultTrack) { - option.prop('selected', true); - } - this.$transcriptLanguageSelect.append(option); - } - this.captionLabels.push(trackLabel); - } - } - if (this.transcriptType === 'external' || this.transcriptType === 'popup') { - if (this.$transcriptLanguageSelect.find('option').length > 1) { - // More than one option now, so enable the select. - this.$transcriptLanguageSelect.prop('disabled', false); - } - } - }; + return function (trackSrc, trackText) { // these are the two vars returned from loadTextObject + var trackContents = trackText; + var cues = thisObj.parseWebVTT(trackSrc, trackContents).cues; - AblePlayer.prototype.setupDescriptions = function (track, cues, trackLang) { + if (thisObj.hasVts) { + // setupVtsTracks() is in vts.js + thisObj.setupVtsTracks(kind, trackLang, trackLabel, trackSrc, trackContents); + } - // 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() + 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(); + }); + }); - this.hasClosedDesc = true; - this.currentDescription = -1; - this.descriptions.push({ - cues: cues, - language: trackLang - }); - }; + return promise; + }; - AblePlayer.prototype.setupChapters = function (track, cues, trackLang) { + AblePlayer.prototype.getTracks = function() { - // NOTE: WebVTT supports nested timestamps (to form an outline) - // This is not currently supported. + // 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 - this.hasChapters = true; + var thisObj, deferred, promise, captionTracks, trackLang, trackLabel, isDefault; - this.chapters.push({ - cues: cues, - language: trackLang - }); - }; + thisObj = this; - AblePlayer.prototype.setupMetadata = function(track, cues) { + deferred = new $.Deferred(); + promise = deferred.promise(); - 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; - } - }; + this.$tracks = this.$media.find('track'); + this.tracks = []; - AblePlayer.prototype.loadTextObject = function(src) { + if (this.$tracks.length) { - var deferred = new $.Deferred(); - var promise = deferred.promise(); - var thisObj = this; + // create object from HTML5 tracks + this.$tracks.each(function() { - // create a temp div for holding data - var $tempDiv = $('<div>',{ - style: 'display:none' - }); + // srcLang should always be included with <track>, 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; + } - $tempDiv.load(src, function (trackText, status, req) { - if (status === 'error') { - if (thisObj.debug) { - console.log ('error reading file ' + src + ': ' + status); - } - deferred.fail(); - } - else { - deferred.resolve(src, trackText); - } - $tempDiv.remove(); - }); - return promise; - }; + if ($(this).attr('label')) { + trackLabel = $(this).attr('label'); + } + else { + trackLabel = thisObj.getLanguageName(trackLang); + } - AblePlayer.prototype.setupAltCaptions = function() { - // setup captions from an alternative source (not <track> elements) - // only do this if no <track> captions are provided - // currently supports: YouTube - var deferred = new $.Deferred(); - var promise = deferred.promise(); + 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; + } - if (this.captions.length === 0) { - if (this.player === 'youtube' && typeof youTubeDataAPIKey !== 'undefined') { - this.setupYouTubeCaptions().done(function() { - deferred.resolve(); - }); - } - else { - // repeat for other alt sources once supported (e.g., Vimeo, DailyMotion) - deferred.resolve(); - } - } - else { // there are <track> captions, so no need for alt source captions - deferred.resolve(); - } - return promise; - }; + if (isDefault) { + // this.captionLang will also be the default language for non-caption tracks + thisObj.captionLang = trackLang; + } + + thisObj.tracks.push({ + 'kind': $(this).attr('kind'), + 'src': $(this).attr('src'), + 'language': trackLang, + 'label': trackLabel, + 'def': isDefault + }); + }); + } + + // 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.setupCaptions = function (track, trackLang, trackLabel, cues) { + + var thisObj, inserted, i, capLabel; + + thisObj = this; + + if (typeof cues === 'undefined') { + cues = null; + } + + this.hasCaptions = true; + + // Remove 'default' attribute from all <track> 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 = $('<div>',{ + 'class': 'able-captions', + }); + this.$captionsWrapper = $('<div>',{ + '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); + } + } + } + + 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 + }); + 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('<?xml'))) { + trackContents = thisObj.ttml2webvtt(trackText); +} +*/ + var deferred, promise, thisObj, $tempDiv; + + deferred = new $.Deferred(); + promise = deferred.promise(); + thisObj = this; + + // create a temp div for holding data + $tempDiv = $('<div>',{ + style: 'display:none' + }); + $tempDiv.load(src, function (trackText, status, req) { + if (status === 'error') { + if (thisObj.debug) { + console.log ('error reading file ' + src + ': ' + status); + } + deferred.fail(); + } + else { + deferred.resolve(src, trackText); + } + $tempDiv.remove(); + }); + return promise; + }; + + AblePlayer.prototype.setupAltCaptions = function() { + + // setup captions from an alternative source (not <track> elements) + // only do this if no <track> 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 <track> captions, so no need for alt source captions + deferred.resolve(); + } + return promise; + }; })(jQuery);