Skip to content

Commit

Permalink
Don't allow playback to start when in the background and interrupted …
Browse files Browse the repository at this point in the history
…by the system

https://bugs.webkit.org/show_bug.cgi?id=269081
rdar://117928506

Reviewed by Jer Noble.

Don't allow playback to begin when WebKit is in the background and has also been interrupted
by the audio session. This can happen, for example, when playback is started from Control
Center on the lock screen and the user switches to the camera app and activates the video
camera. Allowing playback activates WebKit's AVAudioSession and prevents the camera app
from using the volume buttons to trigger start/stop recording.

* LayoutTests/media/video-playback-system-interruption-expected.txt: Added.
* LayoutTests/media/video-playback-system-interruption.html: Added.

* Source/WebCore/html/HTMLMediaElement.cpp:
(WebCore::HTMLMediaElement::couldPlayIfEnoughData const): Return false if the element
needs an active audio session and it was paused by a system interruption.

* Source/WebCore/platform/audio/PlatformMediaSession.cpp: Keep a vector of interruption types
instead of an interruption count so we know the type of the most recent interruption.
(WebCore::PlatformMediaSession::interruptionType const): Return the type of the most recent
interruption, not the first.
(WebCore::PlatformMediaSession::beginInterruption): Update for interruption stack.
(WebCore::PlatformMediaSession::endInterruption): Ditto.
(WebCore::PlatformMediaSession::isPlayingToWirelessPlaybackTargetChanged): Can no longer
save and restore the interruption count, but the issue that caused the change appears to
have been fixed.
(WebCore::PlatformMediaSession::blockedBySystemInterruption const):
* Source/WebCore/platform/audio/PlatformMediaSession.h:
(WebCore::PlatformMediaSession::interruptionCount const):
(WebCore::PlatformMediaSession::interruptionType const): Deleted.

Canonical link: https://commits.webkit.org/274438@main
  • Loading branch information
eric-carlson committed Feb 11, 2024
1 parent b0a7f8b commit 537043c
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 18 deletions.
14 changes: 14 additions & 0 deletions LayoutTests/media/video-playback-system-interruption-expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

RUN(internals.setMediaElementRestrictions(video, 'NoRestrictions'))
RUN(internals.beginMediaSessionInterruption("EnteringBackground"))
RUN(internals.beginMediaSessionInterruption("System"))
RUN(video.volume = 0.1)
RUN(video.src = findMediaFile("video", "content/audio-tracks"))
EVENT(canplaythrough)
EXPECTED (video.paused == 'true') OK
EXPECTED (internals.mediaSessionState(video) == 'Interrupted') OK
RUN(internals.endMediaSessionInterruption("MayResumePlaying"))
EVENT(playing)
EXPECTED (internals.mediaSessionState(video) != 'Interrupted') OK
END OF TEST

40 changes: 40 additions & 0 deletions LayoutTests/media/video-playback-system-interruption.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<script src="media-file.js"></script>
<script src="video-test.js"></script>
<script>

window.addEventListener('load', async event => {
if (!window.internals) {
failTest('This test must be run in DumpRenderTree or WebKitTestRunner.');
return;
}

findMediaElement();
run(`internals.setMediaElementRestrictions(video, 'NoRestrictions')`);
run('internals.beginMediaSessionInterruption("EnteringBackground")');
run('internals.beginMediaSessionInterruption("System")');

run('video.volume = 0.1');
run('video.src = findMediaFile("video", "content/audio-tracks")');

waitForEvent('playing', () => { testExpected('internals.mediaSessionState(video)', 'Interrupted', '!=') });
await waitFor(video, 'canplaythrough');

await sleepFor(250);
testExpected('video.paused', true);
testExpected('internals.mediaSessionState(video)', 'Interrupted');

run('internals.endMediaSessionInterruption("MayResumePlaying")');

// Wait some time before ending the test to ensure the session interruption is cancelled.
endTestLater();
});

</script>
</head>
<body>
<video autoplay controls></video>
</body>
</html>
6 changes: 6 additions & 0 deletions Source/WebCore/html/HTMLMediaElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5872,6 +5872,12 @@ bool HTMLMediaElement::couldPlayIfEnoughData() const
if (pausedForUserInteraction())
return false;

if (!canProduceAudio() || PlatformMediaSessionManager::sharedManager().hasActiveAudioSession())
return true;

if (mediaSession().activeAudioSessionRequired() && mediaSession().blockedBySystemInterruption())
return false;

return true;
}

Expand Down
40 changes: 24 additions & 16 deletions Source/WebCore/platform/audio/PlatformMediaSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,22 @@ void PlatformMediaSession::setState(State state)
PlatformMediaSessionManager::sharedManager().sessionStateChanged(*this);
}

PlatformMediaSession::InterruptionType PlatformMediaSession::interruptionType() const
{
if (!m_interruptionStack.size())
return InterruptionType::NoInterruption;

return m_interruptionStack.last();
}

void PlatformMediaSession::beginInterruption(InterruptionType type)
{
ALWAYS_LOG(LOGIDENTIFIER, "state = ", m_state, ", interruption type = ", type, ", interruption count = ", m_interruptionCount);
ASSERT(type != InterruptionType::NoInterruption);

m_interruptionStack.append(type);
ALWAYS_LOG(LOGIDENTIFIER, "state = ", m_state, ", interruption count = ", interruptionCount(), ", type = ", type);

// When interruptions are overridden, m_interruptionType doesn't get set.
// Give nested interruptions a chance when the previous interruptions were overridden.
if (++m_interruptionCount > 1 && m_interruptionType != InterruptionType::NoInterruption)
if (interruptionCount() > 1)
return;

if (client().shouldOverrideBackgroundPlaybackRestriction(type)) {
Expand All @@ -181,29 +190,27 @@ void PlatformMediaSession::beginInterruption(InterruptionType type)
m_stateToRestore = state();
m_notifyingClient = true;
setState(State::Interrupted);
m_interruptionType = type;
client().suspendPlayback();
m_notifyingClient = false;
}

void PlatformMediaSession::endInterruption(OptionSet<EndInterruptionFlags> flags)
{
ALWAYS_LOG(LOGIDENTIFIER, "flags = ", (int)flags.toRaw(), ", stateToRestore = ", m_stateToRestore, ", interruption count = ", m_interruptionCount);

if (!m_interruptionCount) {
if (!interruptionCount()) {
ALWAYS_LOG(LOGIDENTIFIER, "!! ignoring spurious interruption end !!");
return;
}

if (--m_interruptionCount)
return;
m_interruptionStack.removeLast();
ALWAYS_LOG(LOGIDENTIFIER, "flags = ", (int)flags.toRaw(), ", interruption count = ", interruptionCount(), ", type = ", interruptionType());

if (m_interruptionType == InterruptionType::NoInterruption)
if (interruptionCount())
return;

ALWAYS_LOG(LOGIDENTIFIER, "restoring state ", m_stateToRestore);

State stateToRestore = m_stateToRestore;
m_stateToRestore = State::Idle;
m_interruptionType = InterruptionType::NoInterruption;
setState(stateToRestore);

if (stateToRestore == State::Autoplaying)
Expand Down Expand Up @@ -358,18 +365,19 @@ void PlatformMediaSession::isPlayingToWirelessPlaybackTargetChanged(bool isWirel

m_isPlayingToWirelessPlaybackTarget = isWireless;

// Save and restore the interruption count so it doesn't get out of sync if beginInterruption is called because
// if we in the background.
int interruptionCount = m_interruptionCount;
PlatformMediaSessionManager::sharedManager().sessionIsPlayingToWirelessPlaybackTargetChanged(*this);
m_interruptionCount = interruptionCount;
}

PlatformMediaSession::DisplayType PlatformMediaSession::displayType() const
{
return m_client.displayType();
}

bool PlatformMediaSession::blockedBySystemInterruption() const
{
return interruptionCount() > 1 && interruptionType() == PlatformMediaSession::InterruptionType::SystemInterruption;
}

bool PlatformMediaSession::activeAudioSessionRequired() const
{
if (mediaType() == PlatformMediaSession::MediaType::None)
Expand Down
6 changes: 4 additions & 2 deletions Source/WebCore/platform/audio/PlatformMediaSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class PlatformMediaSession

using InterruptionType = PlatformMediaSessionInterruptionType;

InterruptionType interruptionType() const { return m_interruptionType; }
InterruptionType interruptionType() const;

using EndInterruptionFlags = PlatformMediaSessionEndInterruptionFlags;

Expand Down Expand Up @@ -198,6 +198,7 @@ class PlatformMediaSession
virtual bool requiresPlaybackTargetRouteMonitoring() const { return false; }
#endif

bool blockedBySystemInterruption() const;
bool activeAudioSessionRequired() const;
bool canProduceAudio() const;
bool hasMediaStreamSource() const;
Expand Down Expand Up @@ -241,12 +242,13 @@ class PlatformMediaSession

private:
bool processClientWillPausePlayback(DelayCallingUpdateNowPlaying);
size_t interruptionCount() const { return m_interruptionStack.size(); }

PlatformMediaSessionClient& m_client;
MediaSessionIdentifier m_mediaSessionIdentifier;
State m_state { State::Idle };
State m_stateToRestore { State::Idle };
InterruptionType m_interruptionType { InterruptionType::NoInterruption };
Vector<InterruptionType> m_interruptionStack;
int m_interruptionCount { 0 };
bool m_active { false };
bool m_notifyingClient { false };
Expand Down
9 changes: 9 additions & 0 deletions Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ bool PlatformMediaSessionManager::activeAudioSessionRequired() const
});
}

bool PlatformMediaSessionManager::hasActiveAudioSession() const
{
#if USE(AUDIO_SESSION)
return m_becameActive;
#else
return true;
#endif
}

bool PlatformMediaSessionManager::canProduceAudio() const
{
return anyOfSessions([] (auto& session) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class PlatformMediaSessionManager
bool has(PlatformMediaSession::MediaType) const;
int count(PlatformMediaSession::MediaType) const;
bool activeAudioSessionRequired() const;
bool hasActiveAudioSession() const;
bool canProduceAudio() const;

virtual std::optional<NowPlayingInfo> nowPlayingInfo() const { return { }; }
Expand Down

0 comments on commit 537043c

Please sign in to comment.