Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enable AirPlay in MSE #7431

Merged
merged 25 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3600b70
feat: Enable AirPlay in MSE
tykus160 Oct 18, 2024
4ca1f72
Fix remote button. Only allow MSE when using Safari
avelad Oct 18, 2024
6b5d08d
Make sure Safari is an Apple device when adding a second source.
avelad Oct 18, 2024
0198f5c
Update UI according to RemotePlayback API
avelad Oct 18, 2024
2ba307c
Fix getTracks and selectTracks remotely
avelad Oct 18, 2024
c5a2bc0
Revert "Update UI according to RemotePlayback API"
avelad Oct 18, 2024
ea24f01
isSafari also checks isApple
avelad Oct 18, 2024
281a8bb
Simplify listener management to be able to use the same basic listene…
avelad Oct 18, 2024
01b897d
Fix tests
avelad Oct 18, 2024
c6db611
Add usingRemotePlayback_
avelad Oct 22, 2024
e08fedd
Fix reset streamingAllowed
avelad Oct 22, 2024
8657d6e
Fix usingRemotePlayback_ when connecting
avelad Oct 22, 2024
3e1158a
Fix some tests in Safari
avelad Oct 22, 2024
6729984
Merge remote-tracking branch 'hugo/master' into wt-airplay-mse
tykus160 Oct 22, 2024
99053b2
Don't remove remote playback if there is alternative source
avelad Oct 23, 2024
bdf9f4a
Fix use HomePod as remote AirPlay receiver
avelad Oct 23, 2024
e9d5ddc
Reset MSE on disconnect
avelad Oct 23, 2024
d8b743a
Merge remote-tracking branch 'hugo/master' into wt-airplay-mse
tykus160 Oct 24, 2024
1936a83
Fix merge
avelad Oct 24, 2024
b2f1cd3
Merge branch 'master' into wt-airplay-mse
avelad Oct 25, 2024
bcc99ad
Merge branch 'master' into wt-airplay-mse
avelad Oct 25, 2024
fe13d3a
Revert remote button changes
avelad Oct 28, 2024
2927a2b
Merge branch 'master' into wt-airplay-mse
avelad Oct 28, 2024
33bd74e
When using MSE + remote we need to set tracks for both MSE and native…
avelad Oct 28, 2024
1d00725
Fix lint errors
avelad Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -4955,12 +4961,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 @@ -4980,25 +4988,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();
joeyparrish marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -5048,11 +5072,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 @@ -5076,8 +5102,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 @@ -5090,18 +5123,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();
joeyparrish marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -5162,20 +5207,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 @@ -5209,19 +5254,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();
joeyparrish marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -5235,10 +5293,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 @@ -5255,13 +5313,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();
joeyparrish marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -5279,7 +5347,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 @@ -5312,20 +5380,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();
joeyparrish marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
Loading