diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 0a23404047..dc078a6f93 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -407,9 +407,6 @@ shaka.media.MediaSourceEngine = class { if (this.textEngine_) { cleanup.push(this.textEngine_.destroy()); } - if (this.textDisplayer_) { - cleanup.push(this.textDisplayer_.destroy()); - } for (const contentType in this.transmuxers_) { cleanup.push(this.transmuxers_[contentType].destroy()); @@ -1746,12 +1743,7 @@ shaka.media.MediaSourceEngine = class { * @param {!shaka.extern.TextDisplayer} textDisplayer */ setTextDisplayer(textDisplayer) { - const oldTextDisplayer = this.textDisplayer_; this.textDisplayer_ = textDisplayer; - if (oldTextDisplayer) { - textDisplayer.setTextVisibility(oldTextDisplayer.isTextVisible()); - oldTextDisplayer.destroy(); - } if (this.textEngine_) { this.textEngine_.setDisplayer(textDisplayer); } diff --git a/lib/player.js b/lib/player.js index fcf23fa66e..a041c888eb 100644 --- a/lib/player.js +++ b/lib/player.js @@ -38,6 +38,7 @@ goog.require('shaka.net.NetworkingUtils'); goog.require('shaka.text.SimpleTextDisplayer'); goog.require('shaka.text.StubTextDisplayer'); goog.require('shaka.text.TextEngine'); +goog.require('shaka.text.Utils'); goog.require('shaka.text.UITextDisplayer'); goog.require('shaka.text.WebVttGenerator'); goog.require('shaka.util.BufferUtils'); @@ -50,6 +51,7 @@ goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.FakeEventTarget'); +goog.require('shaka.util.Functional'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); @@ -889,6 +891,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { 'use the attach method instead.'); this.attach(mediaElement, /* initializeMediaSource= */ true); } + + /** @private {?shaka.extern.TextDisplayer} */ + this.textDisplayer_ = null; } /** @@ -1412,6 +1417,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.cmsdManager_.reset(); } + if (this.textDisplayer_) { + await this.textDisplayer_.destroy(); + this.textDisplayer_ = null; + } + if (this.video_) { // Remove all track nodes shaka.util.Dom.removeAllChildren(this.video_); @@ -2307,6 +2317,29 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return false; } + /** + * @private + */ + createTextDisplayer_() { + // When changing text visibility we need to update both the text displayer + // and streaming engine because we don't always stream text. To ensure + // that the text displayer and streaming engine are always in sync, wait + // until they are both initialized before setting the initial value. + const textDisplayerFactory = this.config_.textDisplayFactory; + if (textDisplayerFactory === this.lastTextFactory_) { + return; + } + this.textDisplayer_ = textDisplayerFactory(); + if (this.textDisplayer_.configure) { + this.textDisplayer_.configure(this.config_.textDisplayer); + } else { + shaka.Deprecate.deprecateFeature(5, + 'Text displayer w/ configure', + 'Text displayer should have a "configure" method!'); + } + this.lastTextFactory_ = textDisplayerFactory; + } + /** * Initializes the media source engine. * @@ -2327,24 +2360,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.makeStateChangeEvent_('media-source'); - // When changing text visibility we need to update both the text displayer - // and streaming engine because we don't always stream text. To ensure - // that the text displayer and streaming engine are always in sync, wait - // until they are both initialized before setting the initial value. - const textDisplayerFactory = this.config_.textDisplayFactory; - const textDisplayer = textDisplayerFactory(); - if (textDisplayer.configure) { - textDisplayer.configure(this.config_.textDisplayer); - } else { - shaka.Deprecate.deprecateFeature(5, - 'Text displayer w/ configure', - 'Text displayer should have a "configure" method!'); - } - this.lastTextFactory_ = textDisplayerFactory; - + this.createTextDisplayer_(); + goog.asserts.assert(this.textDisplayer_, + 'Text displayer should be created already'); const mediaSourceEngine = this.createMediaSourceEngine( this.video_, - textDisplayer, + this.textDisplayer_, { getKeySystem: () => this.keySystem(), onMetadata: (metadata, offset, endTime) => { @@ -2851,6 +2872,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } if (mediaElement.textTracks) { + this.createTextDisplayer_(); this.loadEventManager_.listen( mediaElement.textTracks, 'addtrack', (e) => { const trackEvent = /** @type {!TrackEvent} */(e); @@ -2881,6 +2903,22 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.loadEventManager_.listen( mediaElement.textTracks, 'change', () => this.onTracksChanged_()); + this.loadEventManager_.listen( + mediaElement, 'enterpictureinpicture', () => { + const track = this.getFilteredTextTracks_() + .find((t) => t.mode !== 'disabled'); + if (track) { + track.mode = 'showing'; + } + }); + this.loadEventManager_.listen( + mediaElement, 'leavepictureinpicture', () => { + const track = this.getFilteredTextTracks_() + .find((t) => t.mode !== 'disabled'); + if (track) { + track.mode = 'hidden'; + } + }); } // By setting |src| we are done "loading" with src=. We don't need to set @@ -2952,6 +2990,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { }); } else if (textTracks.length > 0) { this.isTextVisible_ = true; + this.textDisplayer_.setTextVisibility(true); } // If we have moved on to another piece of content while waiting for @@ -2959,6 +2998,20 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (unloaded) { return; } + let enabledNativeTrack = false; + for (const track of textTracks) { + if (track.mode !== 'disabled') { + if (!enabledNativeTrack) { + this.enableNativeTrack_(track); + enabledNativeTrack = true; + } else { + track.mode = 'disabled'; + shaka.log.alwaysWarn( + 'Found more than one enabled text track, disabling it', + track); + } + } + } this.setupPreferredTextOnSrc_(); }); @@ -3967,18 +4020,23 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const {segmentRelativeVttTiming} = this.config_.manifest; this.mediaSourceEngine_.setSegmentRelativeVttTiming( segmentRelativeVttTiming); + } + if (this.textDisplayer_) { const textDisplayerFactory = this.config_.textDisplayFactory; if (this.lastTextFactory_ != textDisplayerFactory) { - const displayer = textDisplayerFactory(); - if (displayer.configure) { - displayer.configure(this.config_.textDisplayer); + const oldDisplayer = this.textDisplayer_; + this.textDisplayer_ = textDisplayerFactory(); + if (this.textDisplayer_.configure) { + this.textDisplayer_.configure(this.config_.textDisplayer); } else { shaka.Deprecate.deprecateFeature(5, 'Text displayer w/ configure', 'Text displayer should have a "configure" method!'); } - this.mediaSourceEngine_.setTextDisplayer(displayer); + this.textDisplayer_.setTextVisibility(oldDisplayer.isTextVisible()); + oldDisplayer.destroy(); + this.mediaSourceEngine_.setTextDisplayer(this.textDisplayer_); this.lastTextFactory_ = textDisplayerFactory; if (this.streamingEngine_) { @@ -3986,12 +4044,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.streamingEngine_.reloadTextStream(); } } else { - const displayer = this.mediaSourceEngine_.getTextDisplayer(); - if (displayer.configure) { - displayer.configure(this.config_.textDisplayer); + if (this.textDisplayer_.configure) { + this.textDisplayer_.configure(this.config_.textDisplayer); } } } + if (this.abrManager_) { this.abrManager_.configure(this.config_.abr); // Simply enable/disable ABR with each call, since multiple calls to these @@ -4837,20 +4895,42 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.currentTextLanguage_ = stream.language; } else if (this.video_ && this.video_.src && this.video_.textTracks) { const textTracks = this.getFilteredTextTracks_(); - for (const textTrack of textTracks) { - if (shaka.util.StreamUtils.html5TrackId(textTrack) == track.id) { - // Leave the track in 'hidden' if it's selected but not showing. - textTrack.mode = this.isTextVisible_ ? 'showing' : 'hidden'; - } else { - // Safari allows multiple text tracks to have mode == 'showing', so be - // explicit in resetting the others. - textTrack.mode = 'disabled'; + const oldTrack = textTracks.find((textTrack) => + textTrack.mode !== 'disabled'); + const newTrack = textTracks.find((textTrack) => + shaka.util.StreamUtils.html5TrackId(textTrack) === track.id); + if (oldTrack !== newTrack) { + if (oldTrack) { + oldTrack.mode = 'disabled'; + this.loadEventManager_.unlisten(oldTrack, 'cuechange'); + this.textDisplayer_.remove(0, Infinity); + } + if (newTrack) { + this.enableNativeTrack_(newTrack); } } this.onTextChanged_(); } } + /** + * @param {!TextTrack} track + * @private + */ + enableNativeTrack_(track) { + this.loadEventManager_.listen(track, 'cuechange', () => { + // Always remove cues from the past to avoid memory grow. + const removeEnd = Math.max(0, + this.video_.currentTime - this.config_.streaming.bufferBehind); + this.textDisplayer_.remove(0, removeEnd); + const cues = Array.from(track.activeCues || []) + .map(shaka.text.Utils.mapNativeCueToShakaCue) + .filter(shaka.util.Functional.isNotNull); + this.textDisplayer_.append(cues); + }); + track.mode = document.pictureInPictureElement ? 'showing' : 'hidden'; + } + /** * Select a specific variant track to play. track should come * from a call to getVariantTracks. If track cannot @@ -5167,20 +5247,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget { */ isTextTrackVisible() { const expected = this.isTextVisible_; - - if (this.mediaSourceEngine_ && - this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) { - // Make sure our values are still in-sync. - const actual = this.mediaSourceEngine_.getTextDisplayer().isTextVisible(); + if (this.textDisplayer_) { + const actual = this.textDisplayer_.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 if (this.video_ && this.video_.src && this.video_.textTracks) { - const textTracks = this.getFilteredTextTracks_(); - return textTracks.some((t) => t.mode == 'showing'); } return expected; @@ -5309,8 +5383,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // 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) { - this.mediaSourceEngine_.getTextDisplayer() - .setTextVisibility(newVisibility); + this.textDisplayer_.setTextVisibility(newVisibility); // 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 @@ -5340,15 +5413,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } } } else if (this.video_ && this.video_.src && this.video_.textTracks) { - const textTracks = this.getFilteredTextTracks_(); - // Find the active track by looking for one which is not disabled. This - // is the only way to identify the track which is currently displayed. - // Set it to 'showing' or 'hidden' based on newVisibility. - for (const textTrack of textTracks) { - if (textTrack.mode != 'disabled') { - textTrack.mode = newVisibility ? 'showing' : 'hidden'; - } - } + this.textDisplayer_.setTextVisibility(newVisibility); } // We need to fire the event after we have updated everything so that @@ -6962,7 +7027,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (this.isTextVisible_) { // If the cached value says to show text, then update the text displayer // since it defaults to not shown. - this.mediaSourceEngine_.getTextDisplayer().setTextVisibility(true); + this.textDisplayer_.setTextVisibility(true); goog.asserts.assert(this.shouldStreamText_(), 'Should be streaming text'); } diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js index a72dbf3337..7fdd817d57 100644 --- a/test/media/media_source_engine_unit.js +++ b/test/media/media_source_engine_unit.js @@ -1467,12 +1467,6 @@ describe('MediaSourceEngine', () => { expect(mockTextEngine).toBeTruthy(); expect(mockTextEngine.destroy).toHaveBeenCalled(); }); - - // Regression test for https://github.com/shaka-project/shaka-player/issues/984 - it('destroys TextDisplayer on destroy', async () => { - await mediaSourceEngine.destroy(); - expect(mockTextDisplayer.destroySpy).toHaveBeenCalled(); - }); }); function createMockMediaSource() { diff --git a/test/player_unit.js b/test/player_unit.js index 380d7de575..ae58612c8c 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -228,6 +228,7 @@ describe('Player', () => { expect(playhead.release).toHaveBeenCalled(); expect(mediaSourceEngine.destroy).toHaveBeenCalled(); expect(streamingEngine.destroy).toHaveBeenCalled(); + expect(textDisplayer.destroySpy).toHaveBeenCalled(); for (const segmentIndex of segmentIndexes) { if (segmentIndex) {