Skip to content

Commit

Permalink
[iOS] AudioContext is getting suspended when page goes in the backgro…
Browse files Browse the repository at this point in the history
…und even if navigator.audioSession.type is set to playback

https://bugs.webkit.org/show_bug.cgi?id=261554
rdar://115485355

Reviewed by Eric Carlson.

Implement isNowPlayingEligible, nowPlayingInfo and selectBestMediaSession to enable NowPlayingInfo handling.
Only activated by JS AudioContext(s) are NowPlayingInfo-able if DOM AudioSession type is playback or plabyack and record.

Implement canReceiveRemoteControlCommands and didReceiveRemoteControlCommand to be able to play/pause according commands.
Update AudioContext::mayResumePlayback to only resume for activated by JS AudioContext(s).
Update AudioContext::shouldOverrideBackgroundPlaybackRestriction to be able to continue in the background in case of
playback or plabyack and record DOM AudioSession(s).

Update bug in PlatformMediaSessionManager::bestEligibleSessionForRemoteControls late time optimization to only add web audio sessions to its list
when there is no media element eligible session. If there is an eligible media element session, we do not want to add web audio session to the media session list.

* LayoutTests/media/now-playing-webaudio-expected.txt: Added.
* LayoutTests/media/now-playing-webaudio.html: Added.
* LayoutTests/media/webaudio-background-playback-expected.txt:
* LayoutTests/media/webaudio-background-playback.html:
* LayoutTests/platform/glib/TestExpectations:
* LayoutTests/platform/mac/TestExpectations:
* Source/WebCore/Modules/mediasession/NavigatorMediaSession.cpp:
(WebCore::NavigatorMediaSession::mediaSessionIfExists):
* Source/WebCore/Modules/mediasession/NavigatorMediaSession.h:
* Source/WebCore/Modules/webaudio/AudioContext.cpp:
(WebCore::AudioContext::AudioContext):
(WebCore::AudioContext::mayResumePlayback):
(WebCore::AudioContext::canReceiveRemoteControlCommands const):
(WebCore::AudioContext::didReceiveRemoteControlCommand):
(WebCore::hasPlayBackAudioSession):
(WebCore::AudioContext::isNowPlayingEligible const):
(WebCore::AudioContext::nowPlayingInfo const):
(WebCore::AudioContext::selectBestMediaSession):
(WebCore::AudioContext::shouldOverrideBackgroundPlaybackRestriction const):
(WebCore::AudioContext::defaultDestinationWillBecomeConnected):
* Source/WebCore/Modules/webaudio/AudioContext.h:
* Source/WebCore/Modules/webaudio/BaseAudioContext.cpp:
(WebCore::BaseAudioContext::setState):
* Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp:
(WebCore::PlatformMediaSessionManager::bestEligibleSessionForRemoteControls):

Canonical link: https://commits.webkit.org/275558@main
  • Loading branch information
youennf committed Mar 1, 2024
1 parent 8c7b841 commit b848143
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 18 deletions.
4 changes: 4 additions & 0 deletions LayoutTests/media/now-playing-webaudio-expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

PASS AudioContext as the now playing info source
PASS HTMLMediaElement will become the now playing info source over playing AudioContext

104 changes: 104 additions & 0 deletions LayoutTests/media/now-playing-webaudio.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Testing basic video exchange from offerer to receiver</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
</head>
<body>
<script>
function waitFor(delay)
{
return new Promise(resolve => setTimeout(resolve, delay));
}

async function waitForCriteria(test, criteria)
{
let counter = 0;
while (!criteria() && ++counter < 100)
await waitFor(50);
}

promise_test(async test => {
if (!window.internals)
return;

navigator.audioSession.type = "playback";

let context = new AudioContext();

await waitFor(100);
assert_false(!!internals.nowPlayingState.uniqueIdentifier);

let oscillator = null;
let gainNode = context.createGain();
oscillator = context.createOscillator();
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(440, context.currentTime);

oscillator.connect(gainNode);
gainNode.gain.value = 0.1

internals.withUserGesture(() => {
context.resume();
});

await waitFor(100);
assert_false(!!internals.nowPlayingState.uniqueIdentifier);

gainNode.connect(context.destination);

await waitForCriteria(test, () => {
return !!internals.nowPlayingState.uniqueIdentifier;
});
assert_true(!!internals.nowPlayingState.uniqueIdentifier, "active now playing");

context.suspend();

await waitForCriteria(test, () => {
return !internals.nowPlayingState.uniqueIdentifier;
});
assert_false(!!internals.nowPlayingState.uniqueIdentifier, "inactive now playing");

context.resume();

await waitForCriteria(test, () => {
return !!internals.nowPlayingState.uniqueIdentifier;
});
assert_true(!!internals.nowPlayingState.uniqueIdentifier, "active now playing again");
}, "AudioContext as the now playing info source");

promise_test(async test => {
if (!window.internals)
return;

const identifier = internals.nowPlayingState.uniqueIdentifier;
let mediaElement = document.createElement("audio");
document.body.appendChild(mediaElement);

await waitFor(100);
assert_equals(internals.nowPlayingState.uniqueIdentifier, identifier, "AudioContext identifier");

await waitFor(100);
assert_equals(internals.nowPlayingState.uniqueIdentifier, identifier, "AudioContext identifier 2");

mediaElement.src = "content/test.wav";

await waitForCriteria(test, () => {
return internals.nowPlayingState.uniqueIdentifier !== identifier;
});
assert_not_equals(internals.nowPlayingState.uniqueIdentifier, identifier, "HTMLMediaElement identifier");
const mediaElementIdentifier = internals.nowPlayingState.uniqueIdentifier

document.body.removeChild(mediaElement);

await waitForCriteria(test, () => {
return internals.nowPlayingState.uniqueIdentifier !== mediaElementIdentifier;
});
assert_equals(internals.nowPlayingState.uniqueIdentifier, identifier, "AudioContext identifier 3");
}, "HTMLMediaElement will become the now playing info source over playing AudioContext");

</script>
</body>
</html>
3 changes: 3 additions & 0 deletions LayoutTests/media/webaudio-background-playback-expected.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@

PASS Ensure WebAudio stops playing in the background when the 'BackgroundProcessPlaybackRestricted' restriction is set
PASS Ensure WebAudio does not stop playing in the background when the 'BackgroundProcessPlaybackRestricted' restriction is set and audioSession type is playback
PASS Ensure WebAudio does not stop playing in the background when the 'BackgroundProcessPlaybackRestricted' restriction is set and audioSession type is play-and-record
PASS Ensure WebAudio stops playing in the background when the 'BackgroundProcessPlaybackRestricted' restriction is set and audioSession type is back to default

29 changes: 26 additions & 3 deletions LayoutTests/media/webaudio-background-playback.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
<body>
<script>

promise_test(async (test) => {
async function doTest(test, expectToStopInBackground)
{
if (!window.internals)
return Promise.reject("Test requires internals API");

Expand Down Expand Up @@ -80,7 +81,7 @@
break;
}

assert_true(onProcessCount == oldOnProcessCount, "audio playback suspended in background");
assert_equals(onProcessCount == oldOnProcessCount, expectToStopInBackground, "audio playback suspended in background");

// Context state changes are asynchronous, so delay the test to until the context has restarted + 100 additional milli-seconds.
context.onstatechange = () => {
Expand All @@ -91,9 +92,31 @@

if (window.internals)
internals.applicationWillEnterForeground();
}

promise_test(async (test) => {
const expectToStopInBackground = true;
return doTest(test, expectToStopInBackground);
}, "Ensure WebAudio stops playing in the background when the 'BackgroundProcessPlaybackRestricted' restriction is set");

promise_test(async (test) => {
navigator.audioSession.type = "playback";
const expectToStopInBackground = false;
return doTest(test, expectToStopInBackground);
}, "Ensure WebAudio does not stop playing in the background when the 'BackgroundProcessPlaybackRestricted' restriction is set and audioSession type is playback");

promise_test(async (test) => {
navigator.audioSession.type = "play-and-record";
const expectToStopInBackground = false;
return doTest(test, expectToStopInBackground);
}, "Ensure WebAudio does not stop playing in the background when the 'BackgroundProcessPlaybackRestricted' restriction is set and audioSession type is play-and-record");

promise_test(async (test) => {
navigator.audioSession.type = "";
const expectToStopInBackground = false;
return doTest(test, expectToStopInBackground);
}, "Ensure WebAudio stops playing in the background when the 'BackgroundProcessPlaybackRestricted' restriction is set and audioSession type is back to default");

</script>
</body>
</html>
</html>
2 changes: 2 additions & 0 deletions LayoutTests/platform/glib/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ media/mediacapabilities/mediacapabilities-types.html [ Pass ]
media/video-controller-currentTime-rate.html [ Pass ]
webaudio/codec-tests/aac/vbr-128kbps-44khz.html [ Pass ]
webaudio/codec-tests/mp3/128kbps-44khz.html [ Pass ]
media/now-playing-webaudio.html [ Failure ]
media/webaudio-background-playback.html [ Failure ]

# uiController.simulateRotationLikeSafari() is not implemented on glib ports.
fast/screen-orientation/orientation-in-resize-event.html [ Skip ]
Expand Down
2 changes: 0 additions & 2 deletions LayoutTests/platform/mac/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -1936,8 +1936,6 @@ webkit.org/b/221491 animations/keyframe-pseudo-shadow.html [ Pass ImageOnlyFailu

webkit.org/b/221973 [ Monterey+ ] media/media-webm-no-duration.html [ Failure ]

webkit.org/b/221935 media/webaudio-background-playback.html [ Pass Failure ]

webkit.org/b/221218 [ Monterey+ ] imported/w3c/web-platform-tests/media-source/mediasource-config-change-webm-v-framesize.html [ Pass Failure ]

webkit.org/b/221100 imported/w3c/web-platform-tests/webaudio/the-audio-api/the-audioworklet-interface/audioworkletnode-channel-count.https.html [ Pass Failure ]
Expand Down
10 changes: 10 additions & 0 deletions Source/WebCore/Modules/mediasession/NavigatorMediaSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,23 @@ MediaSession& NavigatorMediaSession::mediaSession(Navigator& navigator)
return NavigatorMediaSession::from(navigator)->mediaSession();
}

RefPtr<MediaSession> NavigatorMediaSession::mediaSessionIfExists(Navigator& navigator)
{
return NavigatorMediaSession::from(navigator)->mediaSessionIfExists();
}

MediaSession& NavigatorMediaSession::mediaSession()
{
if (!m_mediaSession)
m_mediaSession = MediaSession::create(m_navigator);
return *m_mediaSession;
}

RefPtr<MediaSession> NavigatorMediaSession::mediaSessionIfExists()
{
return m_mediaSession;
}

NavigatorMediaSession* NavigatorMediaSession::from(Navigator& navigator)
{
auto* supplement = static_cast<NavigatorMediaSession*>(Supplement<Navigator>::from(&navigator, supplementName()));
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/Modules/mediasession/NavigatorMediaSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ class NavigatorMediaSession final : public Supplement<Navigator> {
~NavigatorMediaSession();

WEBCORE_EXPORT static MediaSession& mediaSession(Navigator&);
static RefPtr<MediaSession> mediaSessionIfExists(Navigator&);
MediaSession& mediaSession();
RefPtr<MediaSession> mediaSessionIfExists();

private:
static NavigatorMediaSession* from(Navigator&);
Expand Down
Loading

0 comments on commit b848143

Please sign in to comment.