From ea9a7d2b322324d4d7b3d2fad1e4ccfffb1befff Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Fri, 26 Apr 2019 10:30:12 -0700 Subject: [PATCH] Relax restrictions on load state Allow indirect access to members (such as DRM engine, manifest, etc) from Player methods as soon as they become available, instead of waiting until the very end of the load process. This fixes application access to several methods during the "manifestparsed" event. The point of the "manifestparsed" event was to allow early access to manifest and other metadata before streaming begins. This also lays the foundations for improvements in native HLS support in those same methods, including the ability to get track information. Issue #382 Issue #997 Fixes b/131604508 Change-Id: Ifee7b06fc2ccdcf5bcdf1c44f2f851d1d7e67fa1 --- lib/player.js | 565 ++++++++++++-------------- test/player_integration.js | 8 +- test/player_src_equals_integration.js | 12 +- 3 files changed, 272 insertions(+), 313 deletions(-) diff --git a/lib/player.js b/lib/player.js index 40a2dd46f8..630825a430 100644 --- a/lib/player.js +++ b/lib/player.js @@ -2485,26 +2485,22 @@ shaka.Player.prototype.getNetworkingEngine = function() { /** - * Get the uri to the asset that the player has loaded and is ready to play. If - * the player has not loaded or is still loading content, this will return - * |null|. + * Get the uri to the asset that the player has loaded. If the player has not + * loaded content, this will return |null|. * * @return {?string} * @export */ shaka.Player.prototype.getAssetUri = function() { - const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE || - this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS; - - return loaded ? this.assetUri_ : null; + return this.assetUri_; }; /** - * Get the uri to the asset that the player has loaded and is ready to play. If - * the player has not loaded or is still loading content, this will return - * |null|. + * Get the uri to the asset that the player has loaded. If the player has not + * loaded content, this will return |null|. * + * @deprecated * @return {?string} * @export */ @@ -2518,7 +2514,7 @@ shaka.Player.prototype.getManifestUri = function() { /** * Get if the player is playing live content. If the player has not loaded - * content or is still loading content, this will return |false|. + * content, this will return |false|. * * @return {boolean} * @export @@ -2539,13 +2535,13 @@ shaka.Player.prototype.isLive = function() { /** * Get if the player is playing in-progress content. If the player has not - * loaded content or is still loading content, this will return |false|. + * loaded content, this will return |false|. * * @return {boolean} * @export */ shaka.Player.prototype.isInProgress = function() { - return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ? + return this.manifest_ ? this.manifest_.presentationTimeline.isInProgress() : false; }; @@ -2553,7 +2549,7 @@ shaka.Player.prototype.isInProgress = function() { /** * Check if the manifest contains only audio-only content. If the player has not - * loaded content or is still loading content, this will return |false|. + * loaded content, this will return |false|. * * The player does not support content that contain more than one type of * variants (i.e. mixing audio-only, video-only, audio-video). Content will be @@ -2563,30 +2559,28 @@ shaka.Player.prototype.isInProgress = function() { * @export */ shaka.Player.prototype.isAudioOnly = function() { - // TODO: Safari's native HLS has audioTracks/videoTracks on HTMLMediaElement. + if (this.manifest_) { + const periods = this.manifest_.periods; + if (!periods.length) { return false; } - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { + const variants = this.manifest_.periods[0].variants; + if (!variants.length) { return false; } + + // Note that if there are some audio-only variants and some audio-video + // variants, the audio-only variants are removed during filtering. + // Therefore if the first variant has no video, that's sufficient to say it + // is audio-only content. + return !variants[0].video; + } else { + // TODO: Safari's native HLS has audioTracks/videoTracks on the element. return false; } - - const periods = this.manifest_.periods; - if (!periods.length) { return false; } - - const variants = this.manifest_.periods[0].variants; - if (!variants.length) { return false; } - - // Note that if there are some audio-only variants and some audio-video - // variants, the audio-only variants are removed during filtering. - // Therefore if the first variant has no video, that's sufficient to say it - // is audio-only content. - return !variants[0].video; }; /** * Get the range of time (in seconds) that seeking is allowed. If the player has - * not loaded content or is still loading content, this will return a range from - * 0 to 0. + * not loaded content, this will return a range from 0 to 0. * * @return {{start: number, end: number}} * @export @@ -2619,16 +2613,14 @@ shaka.Player.prototype.seekRange = function() { /** * Get the key system currently used by EME. If EME is not being used, this will - * return an empty string. If the player has not loaded content or is still - * loading content, this will return an empty string. + * return an empty string. If the player has not loaded content, this will + * return an empty string. * * @return {string} * @export */ shaka.Player.prototype.keySystem = function() { - return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ? - this.drmEngine_.keySystem() : - ''; + return this.drmEngine_ ? this.drmEngine_.keySystem() : ''; }; @@ -2648,34 +2640,28 @@ shaka.Player.prototype.drmInfo = function() { /** * Get the next known expiration time for any EME session. If the session never * expires, this will return |Infinity|. If there are no EME sessions, this will - * return |Infinity|. If the player has not loaded content or is still loading - * content, this will return |Infinity|. + * return |Infinity|. If the player has not loaded content, this will return + * |Infinity|. * * @return {number} * @export */ shaka.Player.prototype.getExpiration = function() { - return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ? - this.drmEngine_.getExpiration() : - Infinity; + return this.drmEngine_ ? this.drmEngine_.getExpiration() : Infinity; }; /** * Check if the player is currently in a buffering state (has too little content - * to play smoothly). If the player has not loaded content or is still loading - * content, this will return |false|. + * to play smoothly). If the player has not loaded content, this will return + * |false|. * * @return {boolean} * @export */ shaka.Player.prototype.isBuffering = function() { const State = shaka.media.BufferingObserver.State; - - const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE || - this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS; - - return loaded ? + return this.bufferObserver_ ? this.bufferObserver_.getState() == State.STARVING : false; }; @@ -2686,17 +2672,16 @@ shaka.Player.prototype.isBuffering = function() { * play, this will return the trick play rate. If no content is playing, this * will return 0. If content is buffering, this will return 0. * - * If the player has not loaded content or is still loading content, this will - * return a playback rate of |0|. + * If the player has not loaded content, this will return a playback rate of + * |0|. * * @return {number} * @export */ shaka.Player.prototype.getPlaybackRate = function() { - const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE || - this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS; - - return loaded ? this.playRateController_.getActiveRate() : 0; + return this.playRateController_ ? + this.playRateController_.getActiveRate() : + 0; }; @@ -2759,32 +2744,30 @@ shaka.Player.prototype.cancelTrickPlay = function() { * period. If there are multiple periods, you must seek to the period in order * to get variants from that period. * - * If the player has not loaded content or is still loading content, this will - * return an empty list. + * If the player has not loaded content, this will return an empty list. * * @return {!Array.} * @export */ shaka.Player.prototype.getVariantTracks = function() { - // TODO: Safari's native HLS has audioTracks/videoTracks on HTMLMediaElement. - - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return []; - } + if (this.manifest_ && this.playhead_) { + const currentVariant = this.getPresentationVariant_(); - const currentVariant = this.getPresentationVariant_(); + const tracks = []; - const tracks = []; + // Convert each variant to a track. + for (const variant of this.getSelectableVariants_()) { + const track = shaka.util.StreamUtils.variantToTrack(variant); + track.active = variant == currentVariant; - // Convert each variant to a track. - for (const variant of this.getSelectableVariants_()) { - const track = shaka.util.StreamUtils.variantToTrack(variant); - track.active = variant == currentVariant; + tracks.push(track); + } - tracks.push(track); + return tracks; + } else { + // TODO: Safari's native HLS has audioTracks/videoTracks on the element. + return []; } - - return tracks; }; @@ -2793,40 +2776,37 @@ shaka.Player.prototype.getVariantTracks = function() { * If there are multiple periods, you must seek to a period in order to get * text tracks from that period. * - * If the player has not loaded content or is still loading content, this will - * return an empty list. + * If the player has not loaded content, this will return an empty list. * * @return {!Array.} * @export */ shaka.Player.prototype.getTextTracks = function() { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. - - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return []; - } - - const currentText = this.getPresentationText_(); + if (this.manifest_ && this.playhead_) { + const currentText = this.getPresentationText_(); + const tracks = []; - const tracks = []; + // Convert all selectable text streams to tracks. + for (const text of this.getSelectableText_()) { + const track = shaka.util.StreamUtils.textStreamToTrack(text); + track.active = text == currentText; - // Convert all selectable text streams to tracks. - for (const text of this.getSelectableText_()) { - const track = shaka.util.StreamUtils.textStreamToTrack(text); - track.active = text == currentText; + tracks.push(track); + } - tracks.push(track); + return tracks; + } else { + // TODO: Safari's native HLS has textTracks on HTMLMediaElement. + return []; } - - return tracks; }; /** * Select a specific text track from the current period. |track| should come * from a call to |getTextTracks|. If the track is not found in the current - * period, this will be a no-op. If the player has not loaded content or is - * still loading content, this will be a no-op. + * period, this will be a no-op. If the player has not loaded content, this will + * be a no-op. * * Note that AdaptationEvents are not fired for manual track selections. * @@ -2834,34 +2814,36 @@ shaka.Player.prototype.getTextTracks = function() { * @export */ shaka.Player.prototype.selectTextTrack = function(track) { - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return; - } - - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - const stream = period.textStreams.find((stream) => stream.id == track.id); + if (this.manifest_ && this.streamingEngine_) { + /** @type {shaka.extern.Period} */ + const period = this.getPresentationPeriod_(); + const stream = period.textStreams.find((stream) => stream.id == track.id); - if (!stream) { - shaka.log.error('No stream with id', track.id); - return; - } + if (!stream) { + shaka.log.error('No stream with id', track.id); + return; + } - // Add entries to the history. - this.addTextStreamToSwitchHistory_( - period, stream, /* fromAdaptation= */ false); + // Add entries to the history. + this.addTextStreamToSwitchHistory_( + period, stream, /* fromAdaptation= */ false); - this.switchTextStream_(stream); + this.switchTextStream_(stream); - // Workaround for https://github.com/google/shaka-player/issues/1299 - // When track is selected, back-propogate the language to - // currentTextLanguage_. - this.currentTextLanguage_ = stream.language; + // Workaround for https://github.com/google/shaka-player/issues/1299 + // When track is selected, back-propogate the language to + // currentTextLanguage_. + this.currentTextLanguage_ = stream.language; + } else { + // TODO: Safari's native HLS has textTracks on HTMLMediaElement. + } }; /** * Find the CEA 608/708 text stream embedded in video, and switch to it. + * + * @deprecated * @export */ shaka.Player.prototype.selectEmbeddedTextTrack = function() { @@ -2876,6 +2858,7 @@ shaka.Player.prototype.selectEmbeddedTextTrack = function() { const tracks = this.getTextTracks().filter((track) => { return track.mimeType == shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; }); + if (tracks.length > 0) { this.selectTextTrack(tracks[0]); } else { @@ -2886,6 +2869,8 @@ shaka.Player.prototype.selectEmbeddedTextTrack = function() { /** * @return {boolean} True if we are using any embedded text tracks present. + * + * @deprecated * @export */ shaka.Player.prototype.usingEmbeddedTextTrack = function() { @@ -2898,14 +2883,12 @@ shaka.Player.prototype.usingEmbeddedTextTrack = function() { 'the player is playing embedded text.', ].join(' ')); - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return false; - } + const activeTrack = this.getTextTracks().filter((track) => { + return track.active; + })[0]; - const activeText = this.streamingEngine_.getBufferingText(); - - if (activeText) { - return activeText.mimeType == shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; + if (activeTrack) { + return activeTrack.mimeType == shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; } return false; @@ -2916,7 +2899,7 @@ shaka.Player.prototype.usingEmbeddedTextTrack = function() { * Select a specific variant track to play from the current period. |track| * should come from a call to |getVariantTracks|. If |track| cannot be found * in the current variant, this will be a no-op. If the player has not loaded - * content or is loading content, this will be a no-op. + * content, this will be a no-op. * * Changing variants will take effect once the currently buffered content has * been played. To force the change to happen sooner, use |clearBuffer| with @@ -2937,54 +2920,55 @@ shaka.Player.prototype.usingEmbeddedTextTrack = function() { */ shaka.Player.prototype.selectVariantTrack = function( track, clearBuffer, safeMargin = 0) { - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return; - } - - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); + if (this.manifest_ && this.streamingEngine_) { + /** @type {shaka.extern.Period} */ + const period = this.getPresentationPeriod_(); - if (this.config_.abr.enabled) { - shaka.log.alwaysWarn('Changing tracks while abr manager is enabled will ' + - 'likely result in the selected track being ' + - 'overriden. Consider disabling abr before calling ' + - 'selectVariantTrack().'); - } + if (this.config_.abr.enabled) { + shaka.log.alwaysWarn('Changing tracks while abr manager is enabled ' + + 'will likely result in the selected track being ' + + 'overriden. Consider disabling abr before calling ' + + 'selectVariantTrack().'); + } - const variant = period.variants.find((variant) => variant.id == track.id); - if (!variant) { - shaka.log.error('No variant with id', track.id); - return; - } + const variant = period.variants.find((variant) => variant.id == track.id); + if (!variant) { + shaka.log.error('No variant with id', track.id); + return; + } - // Double check that the track is allowed to be played. The track list should - // only contain playable variants, but if restrictions change and - // |selectVariantTrack| is called before the track list is updated, we could - // get a now-restricted variant. - if (!shaka.util.StreamUtils.isPlayable(variant)) { - shaka.log.error('Unable to switch to restricted track', track.id); - return; - } + // Double check that the track is allowed to be played. The track list + // should only contain playable variants, but if restrictions change and + // |selectVariantTrack| is called before the track list is updated, we could + // get a now-restricted variant. + if (!shaka.util.StreamUtils.isPlayable(variant)) { + shaka.log.error('Unable to switch to restricted track', track.id); + return; + } - // Add entries to the history. - this.addVariantToSwitchHistory_(period, variant, /* fromAdaptation */ false); - this.switchVariant_(variant, clearBuffer, safeMargin); + // Add entries to the history. + this.addVariantToSwitchHistory_(period, variant, + /* fromAdaptation= */ false); + this.switchVariant_(variant, clearBuffer, safeMargin); - // Workaround for https://github.com/google/shaka-player/issues/1299 - // When track is selected, back-propogate the language to - // currentAudioLanguage_. - this.currentAdaptationSetCriteria_ = new shaka.media.ExampleBasedCriteria( - variant); + // Workaround for https://github.com/google/shaka-player/issues/1299 + // When track is selected, back-propogate the language to + // currentAudioLanguage_. + this.currentAdaptationSetCriteria_ = new shaka.media.ExampleBasedCriteria( + variant); - // Update AbrManager variants to match these new settings. - this.chooseVariant_(period.variants); + // Update AbrManager variants to match these new settings. + this.chooseVariant_(period.variants); + } else { + // TODO: Safari's native HLS has audioTracks/vidoeTracks on the element. + } }; /** * Return a list of audio language-role combinations available for the current - * period. If the player has not loaded any content or is loading content, this - * will return an empty list. + * period. If the player has not loaded any content, this will return an empty + * list. * * @return {!Array.} * @export @@ -2994,45 +2978,45 @@ shaka.Player.prototype.getAudioLanguagesAndRoles = function() { // 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? - // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return []; - } + // TODO: Make generic through variant tracks instead + if (this.manifest_ && this.playhead_) { + /** @type {!Array.} */ + const audioStreams = []; + for (const variant of this.getSelectableVariants_()) { + audioStreams.push(variant.audio); + } - /** @type {!Array.} */ - const audioStreams = []; - for (const variant of this.getSelectableVariants_()) { - audioStreams.push(variant.audio); + return shaka.Player.getLanguageAndRolesFrom_(audioStreams); + } else { + // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. + return []; } - - return shaka.Player.getLanguageAndRolesFrom_(audioStreams); }; /** * Return a list of text language-role combinations available for the current - * period. If the player has not loaded any content or is loading content, this - * will be return an empty list. + * period. If the player has not loaded any content, this will be return an + * empty list. * * @return {!Array.} * @export */ shaka.Player.prototype.getTextLanguagesAndRoles = function() { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. - - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { + // TODO: Make generic through text tracks instead + if (this.manifest_ && this.playhead_) { + return shaka.Player.getLanguageAndRolesFrom_(this.getSelectableText_()); + } else { + // TODO: Safari's native HLS has textTracks on HTMLMediaElement. return []; } - - return shaka.Player.getLanguageAndRolesFrom_(this.getSelectableText_()); }; /** * Return a list of audio languages available for the current period. If the - * player has not loaded any content or is loading content, this will return an - * empty list. + * player has not loaded any content, this will return an empty list. * * @return {!Array.} * @export @@ -3040,137 +3024,126 @@ shaka.Player.prototype.getTextLanguagesAndRoles = function() { shaka.Player.prototype.getAudioLanguages = function() { // TODO: This assumes that language is always on the audio stream. This is not // true when audio and video are muxed together. - // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return []; - } + // TODO: Make generic through variant tracks instead + if (this.manifest_ && this.playhead_) { + /** @type {!Array.} */ + const audioStreams = []; + for (const variant of this.getSelectableVariants_()) { + audioStreams.push(variant.audio); + } - /** @type {!Array.} */ - const audioStreams = []; - for (const variant of this.getSelectableVariants_()) { - audioStreams.push(variant.audio); + return Array.from(shaka.Player.getLanguagesFrom_(audioStreams)); + } else { + // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. + return []; } - - return Array.from(shaka.Player.getLanguagesFrom_(audioStreams)); }; /** * Return a list of text languages available for the current period. If the - * player has not loaded any content or is loading content, this will return an - * empty list. + * player has not loaded any content, this will return an empty list. * * @return {!Array.} * @export */ shaka.Player.prototype.getTextLanguages = function() { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. - - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { + // TODO: Make generic through text tracks instead + if (this.manifest_ && this.playhead_) { + return Array.from( + shaka.Player.getLanguagesFrom_(this.getSelectableText_())); + } else { + // TODO: Safari's native HLS has textTracks on HTMLMediaElement. return []; } - - return Array.from(shaka.Player.getLanguagesFrom_(this.getSelectableText_())); }; /** * Sets currentAudioLanguage and currentVariantRole to the selected language and * role, and chooses a new variant if need be. If the player has not loaded any - * content or is loading content, this will be a no-op. + * content, this will be a no-op. * * @param {string} language * @param {string=} role * @export */ shaka.Player.prototype.selectAudioLanguage = function(language, role) { - // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. - - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return; - } - - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); + if (this.manifest_ && this.playhead_) { + /** @type {shaka.extern.Period} */ + const period = this.getPresentationPeriod_(); - this.currentAdaptationSetCriteria_ = new shaka.media.PreferenceBasedCriteria( - language, role || '', 0); + this.currentAdaptationSetCriteria_ = + new shaka.media.PreferenceBasedCriteria(language, role || '', 0); - // TODO: Refactor to only change audio and not affect text. - this.chooseStreamsAndSwitch_(period); + // TODO: Refactor to only change audio and not affect text. + this.chooseStreamsAndSwitch_(period); + } else { + // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. + } }; /** * Sets currentTextLanguage and currentTextRole to the selected language and * role, and chooses a new variant if need be. If the player has not loaded any - * content or is loading content, this will be a no-op. + * content, this will be a no-op. * * @param {string} language * @param {string=} role * @export */ shaka.Player.prototype.selectTextLanguage = function(language, role) { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. + if (this.manifest_ && this.playhead_) { + /** @type {shaka.extern.Period} */ + const period = this.getPresentationPeriod_(); - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return; + this.currentTextLanguage_ = language; + this.currentTextRole_ = role || ''; + // TODO: Refactor to only change text and not affect audio. + this.chooseStreamsAndSwitch_(period); + } else { + // TODO: Safari's native HLS has textTracks on HTMLMediaElement. } - - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - - this.currentTextLanguage_ = language; - this.currentTextRole_ = role || ''; - // TODO: Refactor to only change text and not affect audio. - this.chooseStreamsAndSwitch_(period); }; /** - * Check if the player will be trying to display text. While in an unloaded - * state, this will always return |false|. If content has been loaded, but there - * are no text tracks, this will return if text would be visible if there was - * text tracks. + * Check if the text displayer is enabled. * * @return {boolean} * @export */ shaka.Player.prototype.isTextTrackVisible = function() { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. - - // Right now we don't support text with src=. So the only time that text could - // be visible is when we have loaded content with media source. - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return false; - } - - // Make sure our values are still in-sync. - const actual = this.mediaSourceEngine_.getTextDisplayer().isTextVisible(); const expected = this.isTextVisible_; - goog.asserts.assert( - actual == expected, 'text visibility has fallen out of sync'); + if (this.manifest_) { + // Make sure our values are still in-sync. + const actual = this.mediaSourceEngine_.getTextDisplayer().isTextVisible(); + goog.asserts.assert( + actual == expected, 'text visibility has fallen out of sync'); + + // Always return the actual value so that the app has the most accurate + // information (in the case that the values come out of sync in prod). + return actual; + } else { + // TODO: Safari's native HLS has textTracks on HTMLMediaElement. + } - // Always return the actual value so that the app has the most accurate - // information (in the case that the values come out of sync in prod). - return actual; + return expected; }; /** - * Tell the player if it should show text once it has text to show. If the - * player has not loaded any content, the request will be applied next time - * content is loaded. + * Enable or disable the text displayer. If the player is in an unloaded state, + * the request will be applied next time content is loaded. * * @param {boolean} isVisible * @return {!Promise} * @export */ shaka.Player.prototype.setTextTrackVisibility = async function(isVisible) { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. - const oldVisibilty = this.isTextVisible_; const newVisibility = isVisible; @@ -3188,13 +3161,14 @@ shaka.Player.prototype.setTextTrackVisibility = async function(isVisible) { // everything will be in a stable state when the app responds to the // event. this.onTextTrackVisibility_(); + // TODO: Move this inside updateTextVisibility_? It's called after all + // but one call to updateTextVisibility_. }; /** * Update all the components (streaming engine and media source) to change the - * visibility of the text. This should ONLY be called when content was loaded - * with media source. + * visibility of the text. * * @param {boolean} isVisible * @return {!Promise} @@ -3202,95 +3176,88 @@ shaka.Player.prototype.setTextTrackVisibility = async function(isVisible) { shaka.Player.prototype.updateTextVisibility_ = async function(isVisible) { // Hold of on setting the text visibility until we have all the components we // need. This ensures that they stay in-sync. - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { - return; - } - - this.mediaSourceEngine_.getTextDisplayer().setTextVisibility(isVisible); - - // TODO: Verify and ensure correct behaviour when the config is changed during - // playback. - - // When the user wants to see captions, we stream captions. When the user - // doesn't want to see captions, we don't stream captions. The only time this - // is not true is when we are configured to always stream captions. This is to - // give greater control to app developers and content hosts over experience - // and bandwidth. + if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) { + this.mediaSourceEngine_.getTextDisplayer().setTextVisibility(isVisible); + + // When the user wants to see captions, we stream captions. When the user + // doesn't want to see captions, we don't stream captions. This is to avoid + // bandwidth consumption by an unused resource. The app developer can + // override this and configure us to always stream captions. + if (this.config_.streaming.alwaysStreamText) { + return; + } - if (this.config_.streaming.alwaysStreamText) { - return; - } + if (!isVisible) { + this.streamingEngine_.unloadTextStream(); + return; + } - if (!isVisible) { - this.streamingEngine_.unloadTextStream(); - return; - } + // Find the text stream that best matches the user's preferences. + /** @type {shaka.extern.Period} */ + const period = this.getPresentationPeriod_(); + const streams = shaka.util.StreamUtils.filterStreamsByLanguageAndRole( + period.textStreams, this.currentTextLanguage_, this.currentTextRole_); - // Find the text stream that best matches the user's preferences. - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - const streams = shaka.util.StreamUtils.filterStreamsByLanguageAndRole( - period.textStreams, this.currentTextLanguage_, this.currentTextRole_); + // It is possible that there are no streams to play. + if (streams.length == 0) { + return; + } - // It is possible that there are no streams to play. - if (streams.length == 0) { - return; + await this.streamingEngine_.loadNewTextStream(streams[0]); + } else { // if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) { + // TODO: Safari's native HLS has textTracks on HTMLMediaElement. } - - await this.streamingEngine_.loadNewTextStream(streams[0]); }; /** * Get the current playhead position as a date. This should only be called when * the player has loaded a live stream. If the player has not loaded a live - * stream or is still loading content, this will return |null|. + * stream, this will return |null|. * * @return {Date} * @export */ shaka.Player.prototype.getPlayheadTimeAsDate = function() { - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { + if (!this.isLive()) { + shaka.log.warning('getPlayheadTimeAsDate is for live streams!'); return null; } - const timeline = this.manifest_.presentationTimeline; - - if (!timeline.isLive()) { - shaka.log.warning('getPlayheadTimeAsDate is for live streams!'); + if (this.manifest_) { + const timeline = this.manifest_.presentationTimeline; + const startTime = timeline.getPresentationStartTime(); + const presentationTime = this.video_.currentTime; + return new Date(/* ms= */ (startTime + presentationTime) * 1000); + } else { + shaka.log.warning('No way to get playhead time as date!'); return null; } - - const startTime = timeline.getPresentationStartTime(); - const presentationTime = this.video_.currentTime; - - return new Date(/* ms= */ (startTime + presentationTime) * 1000); }; /** * Get the presentation start time as a date. This should only be called when * the player has loaded a live stream. If the player has not loaded a live - * stream or is still loading content, this will return |null|. + * stream, this will return |null|. * * @return {Date} * @export */ shaka.Player.prototype.getPresentationStartTimeAsDate = function() { - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { + if (!this.isLive()) { + shaka.log.warning('getPresentationStartTimeAsDate is for live streams!'); return null; } - const timeline = this.manifest_.presentationTimeline; - - if (!timeline.isLive()) { - shaka.log.warning('getPresentationStartTimeAsDate is for live streams!'); + if (this.manifest_) { + const timeline = this.manifest_.presentationTimeline; + const startTime = timeline.getPresentationStartTime(); + return new Date(/* ms= */ startTime * 1000); + } else { + shaka.log.warning('No way to get presentation start time as date!'); return null; } - - const startTime = timeline.getPresentationStartTime(); - - return new Date(/* ms= */ startTime * 1000); }; @@ -3331,13 +3298,10 @@ shaka.Player.prototype.getBufferedInfo = function() { * @export */ shaka.Player.prototype.getStats = function() { - // If we are not loaded, then return an stats blob so that this call will - // never fail. - // - // TODO: Should we include the bandwidth information with the empty blob? + // If the Player is not in a fully-loaded state, then return an empty stats + // blob so that this call will never fail. const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE || this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS; - if (!loaded) { return shaka.util.Stats.getEmptyBlob(); } @@ -3401,7 +3365,7 @@ shaka.Player.prototype.addTextTrack = async function( shaka.log.error('Cannot add text when loaded with src='); return Promise.reject(); } - if (this.manifest_ == null) { + if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { shaka.log.error( 'Must call load() and wait for it to resolve before adding text ' + 'tracks.'); @@ -4849,8 +4813,7 @@ shaka.Player.prototype.getSelectableText_ = function() { * @private */ shaka.Player.prototype.getPresentationPeriod_ = function() { - goog.asserts.assert( - this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE, + goog.asserts.assert(this.manifest_ && this.playhead_, 'Only ask for the presentation period when loaded with media source.'); const presentationTime = this.playhead_.getTime(); @@ -4879,10 +4842,6 @@ shaka.Player.prototype.getPresentationPeriod_ = function() { * @private */ shaka.Player.prototype.getPresentationVariant_ = function() { - goog.asserts.assert( - this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE, - 'Only ask for the presentation variant when loaded with media source.'); - /** @type {shaka.extern.Period} */ const currentPeriod = this.getPresentationPeriod_(); return this.activeStreams_.getVariant(currentPeriod); diff --git a/test/player_integration.js b/test/player_integration.js index 7a2f459b23..7fa4771b16 100644 --- a/test/player_integration.js +++ b/test/player_integration.js @@ -233,16 +233,14 @@ describe('Player', function() { describe('plays', function() { it('with external text tracks', async () => { await player.load('test:sintel_no_text_compiled'); + // For some reason, using path-absolute URLs (i.e. without the hostname) // like this doesn't work on Safari. So manually resolve the URL. let locationUri = new goog.Uri(location.href); let partialUri = new goog.Uri('/base/test/test/assets/text-clip.vtt'); let absoluteUri = locationUri.resolve(partialUri); - player.addTextTrack(absoluteUri.toString(), 'en', 'subtitles', - 'text/vtt'); - - video.play(); - await Util.delay(5); + await player.addTextTrack(absoluteUri.toString(), 'en', 'subtitles', + 'text/vtt'); let textTracks = player.getTextTracks(); expect(textTracks).toBeTruthy(); diff --git a/test/player_src_equals_integration.js b/test/player_src_equals_integration.js index af4ccf4a51..d34e5d4d00 100644 --- a/test/player_src_equals_integration.js +++ b/test/player_src_equals_integration.js @@ -308,14 +308,16 @@ describe('Player Src Equals', () => { expect(player.getTextLanguagesAndRoles()).toEqual([]); }); - // Since we are not managing the tracks, we can't control whether or not we - // are showing text. Trying to show the tracks should do nothing. - it('never shows text', async () => { + it('persists the text visibility setting', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); - expect(player.isTextTrackVisible()).toBeFalsy(); + expect(player.isTextTrackVisible()).toBe(false); + player.setTextTrackVisibility(true); - expect(player.isTextTrackVisible()).toBeFalsy(); + expect(player.isTextTrackVisible()).toBe(true); + + player.setTextTrackVisibility(false); + expect(player.isTextTrackVisible()).toBe(false); }); // Even though we loaded content using |src=| we should still be able to get