Skip to content

Commit

Permalink
feat: Enable AirPlay in MSE (#7431)
Browse files Browse the repository at this point in the history
Fixes #5022

---------

Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com>
  • Loading branch information
tykus160 and avelad authored Oct 31, 2024
1 parent 5ee6a4d commit a6cf9cb
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 55 deletions.
43 changes: 40 additions & 3 deletions lib/media/media_source_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_);

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
184 changes: 132 additions & 52 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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;
}

Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -5176,20 +5221,20 @@ 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= */ '',
/* audioLabel= */ '',
/* videoLabel= */ '',
this.config_.mediaSource.codecSwitchingStrategy,
this.config_.manifest.dash.enableAudioGroups,
codec);
codec || '');

const diff = (a, b) => {
if (!a.video && !b.video) {
Expand Down Expand Up @@ -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();
}

/**
Expand All @@ -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) {
Expand All @@ -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();
}

/**
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
}

/**
Expand Down

0 comments on commit a6cf9cb

Please sign in to comment.