diff --git a/lib/player.js b/lib/player.js index 01508ee98a..e5bb1ec8f7 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1624,6 +1624,9 @@ shaka.Player.prototype.cancelTrickPlay = function() { * @export */ shaka.Player.prototype.getVariantTracks = function() { + // TODO: Update to use |getSelectableVariants| so that this and the other + // methods are all getting the variants from the same source. + if (!this.manifest_ || !this.playhead_) { return []; } @@ -1649,6 +1652,9 @@ shaka.Player.prototype.getVariantTracks = function() { * @export */ shaka.Player.prototype.getTextTracks = function() { + // TODO: Update this to use |getSelectableText| so that we are sourcing our + // text streams from a common source. + if (!this.manifest_ || !this.playhead_) { return []; } @@ -1832,7 +1838,18 @@ shaka.Player.prototype.selectVariantTrack = function( * @export */ shaka.Player.prototype.getAudioLanguagesAndRoles = function() { - return shaka.Player.getLanguageAndRolesFromTracks_(this.getVariantTracks()); + // TODO: This assumes that language is always on the audio stream. This is not + // true when audio and video are muxed together. + // TODO: If the language is on the video stream, how do roles affect the + // the language-role pairing? + + /** @type {!Array.} */ + const audioStreams = []; + for (const variant of this.getSelectableVariants_()) { + audioStreams.push(variant.audio); + } + + return shaka.Player.getLanguageAndRolesFrom_(audioStreams); }; @@ -1844,7 +1861,7 @@ shaka.Player.prototype.getAudioLanguagesAndRoles = function() { * @export */ shaka.Player.prototype.getTextLanguagesAndRoles = function() { - return shaka.Player.getLanguageAndRolesFromTracks_(this.getTextTracks()); + return shaka.Player.getLanguageAndRolesFrom_(this.getSelectableText_()); }; @@ -1855,8 +1872,16 @@ shaka.Player.prototype.getTextLanguagesAndRoles = function() { * @export */ shaka.Player.prototype.getAudioLanguages = function() { - const tracks = this.getVariantTracks(); - return Array.from(shaka.Player.getLanguagesFromTracks_(tracks)); + // TODO: This assumes that language is always on the audio stream. This is not + // true when audio and video are muxed together. + + /** @type {!Array.} */ + const audioStreams = []; + for (const variant of this.getSelectableVariants_()) { + audioStreams.push(variant.audio); + } + + return Array.from(shaka.Player.getLanguagesFrom_(audioStreams)); }; @@ -1867,8 +1892,7 @@ shaka.Player.prototype.getAudioLanguages = function() { * @export */ shaka.Player.prototype.getTextLanguages = function() { - const tracks = this.getTextTracks(); - return Array.from(shaka.Player.getLanguagesFromTracks_(tracks)); + return Array.from(shaka.Player.getLanguagesFrom_(this.getSelectableText_())); }; @@ -3525,71 +3549,144 @@ shaka.Player.prototype.waitNextTick_ = function() { }; /** - * Return a list of text language-role combinations available for the given - * tracks. + * Get the normalized languages for a group of streams. If a stream is |null|, + * it means that there is a variant but no audio stream and the language should + * be "und". * - * @param {!Iterable.} tracks + * @param {!Array.} streams + * @return {!Set.} + * @private + */ +shaka.Player.getLanguagesFrom_ = function(streams) { + const languages = new Set(); + + for (const stream of streams) { + if (stream && stream.language) { + languages.add(shaka.util.LanguageUtils.normalize(stream.language)); + } else { + languages.add('und'); + } + } + + return languages; +}; + + +/** + * Get all permutations of normalized languages and role for a group of streams. + * If a stream is |null|, it means that there is a variant but no audio stream + * and the language should be "und". + * + * @param {!Array.} streams * @return {!Array.} * @private */ -shaka.Player.getLanguageAndRolesFromTracks_ = function(tracks) { - /** - * Group together all the roles that are used with each language. Use a set - * for the roles so that we don't track duplicates. - * - * @type {!Map.>} - **/ - const rolesByLanguage = new Map(); - - for (const track of tracks) { - /** @type {string} */ - const language = shaka.util.LanguageUtils.normalize(track.language); - /** @type {!Set.} */ - const roles = rolesByLanguage.get(language) || new Set(); - - for (const role of track.roles) { - roles.add(role); +shaka.Player.getLanguageAndRolesFrom_ = function(streams) { + /** @type {!Map.} */ + const languageToRoles = new Map(); + + // We must have an empty role so that we will still get a language-role entry. + const noRoles = ['']; + + for (const stream of streams) { + let language = 'und'; + let roles = noRoles; + + if (stream && stream.language) { + language = shaka.util.LanguageUtils.normalize(stream.language); } - rolesByLanguage.set(language, roles); - } + if (stream && stream.roles.length) { + roles = stream.roles; + } - // If there are no roles, add an empty one so that combos will still be - // made. - rolesByLanguage.forEach((roles, language) => { - if (roles.size == 0) { - roles.add(''); + if (!languageToRoles.has(language)) { + languageToRoles.set(language, new Set()); } - }); - /** @type {!Array.} */ - const combos = []; - rolesByLanguage.forEach((roles, language) => { for (const role of roles) { - combos.push({ + languageToRoles.get(language).add(role); + } + } + + // Flatten our map to an array of language-role pairs. + const pairings = []; + languageToRoles.forEach((roles, language) => { + for (const role of roles) { + pairings.push({ language: language, role: role, }); } }); + return pairings; +}; + + +/** + * Get the variants that the user can select. The variants will be based on + * the period that the playhead is in and what variants are playable. + * + * @return {!Array.} + * @private + */ +shaka.Player.prototype.getSelectableVariants_ = function() { + // If we have been called before we load content or after we have unloaded + // content, then we should return no streams. + if (!this.manifest_ || !this.playhead_) { + return []; + } + + // Use the period that is currently playing, allowing the change to affect + // the "now". + const currentPeriodIndex = shaka.util.StreamUtils.findPeriodContainingTime( + this.manifest_, + this.playhead_.getTime()); + + const currentPeriod = this.manifest_.periods[currentPeriodIndex]; - return combos; + const variants = []; + + for (const variant of currentPeriod.variants) { + if (shaka.util.StreamUtils.isPlayable(variant)) { + variants.push(variant); + } + } + + return variants; }; /** - * @param {!Iterable.} tracks - * @return {!Set.} + * Get the text streams that the user can select. The streams will be based on + * the period that the playhead is in and what streams have finished loading. + * + * @return {!Array.} * @private */ -shaka.Player.getLanguagesFromTracks_ = function(tracks) { - /** @type {!Set.} */ - const languages = new Set(); +shaka.Player.prototype.getSelectableText_ = function() { + // If we have been called before we load content or after we have unloaded + // content, then we should return no streams. + if (!this.manifest_ || !this.playhead_) { + return []; + } + + // Use the period that is currently playing, allowing the change to affect + // the "now". + const currentPeriodIndex = shaka.util.StreamUtils.findPeriodContainingTime( + this.manifest_, + this.playhead_.getTime()); + + const currentPeriod = this.manifest_.periods[currentPeriodIndex]; - for (const track of tracks) { - const language = shaka.util.LanguageUtils.normalize(track.language); - languages.add(language); + const streams = []; + + for (const stream of currentPeriod.textStreams) { + // Don't show return streams that are still loading. + if (!this.loadingTextStreamIds_.includes(stream.id)) { + streams.push(stream); + } } - return languages; + return streams; };