From a6cf9cbfd3d7e9dceeafc5fdc6903e99fbb6c8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Thu, 31 Oct 2024 08:40:23 +0100 Subject: [PATCH] feat: Enable AirPlay in MSE (#7431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #5022 --------- Co-authored-by: Álvaro Velad Galván --- lib/media/media_source_engine.js | 43 +++++++- lib/player.js | 184 ++++++++++++++++++++++--------- 2 files changed, 172 insertions(+), 55 deletions(-) diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index c0f5a9ef2..1518b5ce4 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -127,6 +127,13 @@ shaka.media.MediaSourceEngine = class { /** @private {HTMLSourceElement} */ this.source_ = null; + /** + * Fallback source element with direct media URI, used for casting + * purposes. + * @private {HTMLSourceElement} + */ + this.secondarySource_ = null; + /** @private {MediaSource} */ this.mediaSource_ = this.createMediaSource(this.mediaSourceOpen_); @@ -208,7 +215,9 @@ shaka.media.MediaSourceEngine = class { let mediaSource; if (window.ManagedMediaSource) { - this.video_.disableRemotePlayback = true; + if (!this.secondarySource_) { + this.video_.disableRemotePlayback = true; + } mediaSource = new ManagedMediaSource(); @@ -243,13 +252,37 @@ shaka.media.MediaSourceEngine = class { if (this.source_) { this.video_.removeChild(this.source_); } + if (this.secondarySource_) { + this.video_.removeChild(this.secondarySource_); + } this.source_ = shaka.util.Dom.createSourceElement(this.url_); this.video_.appendChild(this.source_); + if (this.secondarySource_) { + this.video_.appendChild(this.secondarySource_); + } this.video_.load(); return mediaSource; } + /** + * @param {string} uri + * @param {string} mimeType + */ + addSecondarySource(uri, mimeType) { + if (!this.video_ || !(this.mediaSource_ instanceof ManagedMediaSource)) { + shaka.log.warning( + 'Secondary source is used only with ManagedMediaSource'); + return; + } + if (this.secondarySource_) { + this.video_.removeChild(this.secondarySource_); + } + this.secondarySource_ = shaka.util.Dom.createSourceElement(uri, mimeType); + this.video_.appendChild(this.secondarySource_); + this.video_.disableRemotePlayback = false; + } + /** * @param {shaka.util.PublicPromise} p * @private @@ -443,15 +476,19 @@ shaka.media.MediaSourceEngine = class { this.eventManager_ = null; } + if (this.video_ && this.secondarySource_) { + this.video_.removeChild(this.secondarySource_); + } if (this.video_ && this.source_) { // "unload" the video element. this.video_.removeChild(this.source_); this.video_.load(); this.video_.disableRemotePlayback = false; - this.video_ = null; - this.source_ = null; } + this.video_ = null; + this.source_ = null; + this.secondarySource_ = null; this.config_ = null; this.mediaSource_ = null; this.textEngine_ = null; diff --git a/lib/player.js b/lib/player.js index 9544582b6..64ea20550 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1792,6 +1792,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget { startTimeOfLoad, prefetchedVariant, segmentPrefetchById); }, 'loadInner_'); preloadManager.stopQueuingLatePhaseQueuedOperations(); + + if (this.mimeType_ && shaka.util.Platform.isSafari() && + shaka.util.MimeUtils.isHlsType(this.mimeType_)) { + this.mediaSourceEngine_.addSecondarySource( + this.assetUri_, this.mimeType_); + } } this.dispatchEvent(shaka.Player.makeEvent_( shaka.util.FakeEvent.EventName.Loaded)); @@ -4969,12 +4975,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @export */ selectTextTrack(track) { - if (this.manifest_ && this.streamingEngine_&& !this.isRemotePlayback()) { + const selectMediaSourceMode = () => { const stream = this.manifest_.textStreams.find( (stream) => stream.id == track.id); if (!stream) { - shaka.log.error('No stream with id', track.id); + if (!this.isRemotePlayback()) { + shaka.log.error('No stream with id', track.id); + } return; } @@ -4994,25 +5002,41 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // When track is selected, back-propagate the language to // currentTextLanguage_. this.currentTextLanguage_ = stream.language; - } else if (this.video_ && this.video_.src && this.video_.textTracks) { - const textTracks = this.getFilteredTextTracks_(); - 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); + }; + const selectSrcEqualsMode = () => { + if (this.video_ && this.video_.textTracks) { + const textTracks = this.getFilteredTextTracks_(); + const oldTrack = textTracks.find((textTrack) => + textTrack.mode !== 'disabled'); + const newTrack = textTracks.find((textTrack) => + shaka.util.StreamUtils.html5TrackId(textTrack) === track.id); + if (!newTrack) { + shaka.log.error('No track with id', track.id); + return; } - if (newTrack) { - this.enableNativeTrack_(newTrack); + 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_(); + this.setTextDisplayerLanguage_(); + } + }; + if (this.manifest_ && this.playhead_) { + selectMediaSourceMode(); + // When using MSE + remote we need to set tracks for both MSE and native + // apis so that synchronization is maintained. + if (!this.isRemotePlayback()) { + return; } - this.onTextChanged_(); - this.setTextDisplayerLanguage_(); } + selectSrcEqualsMode(); } /** @@ -5062,11 +5086,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @export */ selectVariantTrack(track, clearBuffer = false, safeMargin = 0) { - if (this.manifest_ && this.streamingEngine_&& !this.isRemotePlayback()) { + const selectMediaSourceMode = () => { const variant = this.manifest_.variants.find( (variant) => variant.id == track.id); if (!variant) { - shaka.log.error('No variant with id', track.id); + if (!this.isRemotePlayback()) { + shaka.log.error('No variant with id', track.id); + } return; } @@ -5090,8 +5116,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget { 'calling selectVariantTrack().'); } - this.switchVariant_( - variant, /* fromAdaptation= */ false, clearBuffer, safeMargin); + if (this.isRemotePlayback()) { + this.switchVariant_( + variant, /* fromAdaptation= */ false, + /* clearBuffer= */ false, /* safeMargin= */ 0); + } else { + this.switchVariant_( + variant, /* fromAdaptation= */ false, + clearBuffer || false, safeMargin || 0); + } // Workaround for // https://github.com/shaka-project/shaka-player/issues/1299 @@ -5104,18 +5137,30 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // Update AbrManager variants to match these new settings. this.updateAbrManagerVariants_(); - } else if (this.video_ && this.video_.audioTracks) { - // Safari's native HLS won't let you choose an explicit variant, though - // you can choose audio languages this way. - const audioTracks = Array.from(this.video_.audioTracks); - for (const audioTrack of audioTracks) { - if (shaka.util.StreamUtils.html5TrackId(audioTrack) == track.id) { - // This will reset the "enabled" of other tracks to false. - this.switchHtml5Track_(audioTrack); - return; + }; + const selectSrcEqualsMode = () => { + if (this.video_ && this.video_.audioTracks) { + // Safari's native HLS won't let you choose an explicit variant, though + // you can choose audio languages this way. + const audioTracks = Array.from(this.video_.audioTracks); + for (const audioTrack of audioTracks) { + if (shaka.util.StreamUtils.html5TrackId(audioTrack) == track.id) { + // This will reset the "enabled" of other tracks to false. + this.switchHtml5Track_(audioTrack); + return; + } } } + }; + if (this.manifest_ && this.playhead_) { + selectMediaSourceMode(); + // When using MSE + remote we need to set tracks for both MSE and native + // apis so that synchronization is maintained. + if (!this.isRemotePlayback()) { + return; + } } + selectSrcEqualsMode(); } /** @@ -5176,12 +5221,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget { */ selectAudioLanguage(language, role, channelsCount = 0, safeMargin = 0, codec = '') { - if (this.manifest_ && this.playhead_&& !this.isRemotePlayback()) { + const selectMediaSourceMode = () => { this.currentAdaptationSetCriteria_ = new shaka.media.PreferenceBasedCriteria( language, role || '', - channelsCount, + channelsCount || 0, /* hdrLevel= */ '', /* spatialAudio= */ false, /* videoLayout= */ '', @@ -5189,7 +5234,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { /* videoLabel= */ '', this.config_.mediaSource.codecSwitchingStrategy, this.config_.manifest.dash.enableAudioGroups, - codec); + codec || ''); const diff = (a, b) => { if (!a.video && !b.video) { @@ -5223,19 +5268,32 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } if (bestVariant) { const track = shaka.util.StreamUtils.variantToTrack(bestVariant); - this.selectVariantTrack(track, /* clearBuffer= */ true, safeMargin); + this.selectVariantTrack( + track, /* clearBuffer= */ true, safeMargin || 0); return; } // If we haven't switched yet, just use ABR to find a new track. this.chooseVariantAndSwitch_(); - } else if (this.video_ && this.video_.audioTracks) { - const track = shaka.util.StreamUtils.filterStreamsByLanguageAndRole( - this.getVariantTracks(), language, role || '', false)[0]; - if (track) { - this.selectVariantTrack(track); + }; + const selectSrcEqualsMode = () => { + if (this.video_ && this.video_.audioTracks) { + const track = shaka.util.StreamUtils.filterStreamsByLanguageAndRole( + this.getVariantTracks(), language, role || '', false)[0]; + if (track) { + this.selectVariantTrack(track); + } + } + }; + if (this.manifest_ && this.playhead_) { + selectMediaSourceMode(); + // When using MSE + remote we need to set tracks for both MSE and native + // apis so that synchronization is maintained. + if (!this.isRemotePlayback()) { + return; } } + selectSrcEqualsMode(); } /** @@ -5249,10 +5307,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @export */ selectTextLanguage(language, role, forced = false) { - if (this.manifest_ && this.playhead_ && !this.isRemotePlayback()) { + const selectMediaSourceMode = () => { this.currentTextLanguage_ = language; this.currentTextRole_ = role || ''; - this.currentTextForced_ = forced; + this.currentTextForced_ = forced || false; const chosenText = this.chooseTextStream_(); if (chosenText) { @@ -5269,13 +5327,23 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.setTextDisplayerLanguage_(); } } - } else { + }; + const selectSrcEqualsMode = () => { const track = shaka.util.StreamUtils.filterStreamsByLanguageAndRole( - this.getTextTracks(), language, role || '', forced)[0]; + this.getTextTracks(), language, role || '', forced || false)[0]; if (track) { this.selectTextTrack(track); } + }; + if (this.manifest_ && this.playhead_) { + selectMediaSourceMode(); + // When using MSE + remote we need to set tracks for both MSE and native + // apis so that synchronization is maintained. + if (!this.isRemotePlayback()) { + return; + } } + selectSrcEqualsMode(); } /** @@ -5293,7 +5361,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @export */ selectVariantsByLabel(label, clearBuffer = true, safeMargin = 0) { - if (this.manifest_ && this.playhead_ && !this.isRemotePlayback()) { + const selectMediaSourceMode = () => { let firstVariantWithLabel = null; for (const variant of this.manifest_.variants) { if (variant.audio.label == label) { @@ -5326,20 +5394,32 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.config_.manifest.dash.enableAudioGroups); this.chooseVariantAndSwitch_(clearBuffer, safeMargin); - } else if (this.video_ && this.video_.audioTracks) { - const audioTracks = Array.from(this.video_.audioTracks); + }; + const selectSrcEqualsMode = () => { + if (this.video_ && this.video_.audioTracks) { + const audioTracks = Array.from(this.video_.audioTracks); - let trackMatch = null; + let trackMatch = null; - for (const audioTrack of audioTracks) { - if (audioTrack.label == label) { - trackMatch = audioTrack; + for (const audioTrack of audioTracks) { + if (audioTrack.label == label) { + trackMatch = audioTrack; + } + } + if (trackMatch) { + this.switchHtml5Track_(trackMatch); } } - if (trackMatch) { - this.switchHtml5Track_(trackMatch); + }; + if (this.manifest_ && this.playhead_) { + selectMediaSourceMode(); + // When using MSE + remote we need to set tracks for both MSE and native + // apis so that synchronization is maintained. + if (!this.isRemotePlayback()) { + return; } } + selectSrcEqualsMode(); } /**