Deactivate audio session whenever possible
authoreric.carlson@apple.com <eric.carlson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 7 Jan 2019 21:47:51 +0000 (21:47 +0000)
committereric.carlson@apple.com <eric.carlson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 7 Jan 2019 21:47:51 +0000 (21:47 +0000)
https://bugs.webkit.org/show_bug.cgi?id=193188
<rdar://problem/42678977>

Reviewed by Jer Noble.

Source/WebCore:

Test: media/deactivate-audio-session.html

* platform/audio/AudioSession.cpp:
(WebCore::AudioSession::tryToSetActive):
(WebCore::AudioSession::tryToSetActiveInternal):
* platform/audio/AudioSession.h:
(WebCore::AudioSession::isActive const):

* platform/audio/PlatformMediaSessionManager.cpp:
(WebCore::PlatformMediaSessionManager::removeSession):
(WebCore::deactivateAudioSession):
(WebCore::PlatformMediaSessionManager::shouldDeactivateAudioSession):
(WebCore::PlatformMediaSessionManager::setShouldDeactivateAudioSession):
* platform/audio/PlatformMediaSessionManager.h:

* platform/audio/ios/AudioSessionIOS.mm:
(WebCore::AudioSession::tryToSetActiveInternal):
(WebCore::AudioSession::tryToSetActive): Deleted.

* platform/audio/mac/AudioSessionMac.cpp:
(WebCore::AudioSession::tryToSetActiveInternal):
(WebCore::AudioSession::tryToSetActive): Deleted.

* testing/Internals.cpp:
(WebCore::Internals::audioSessionActive const):
* testing/Internals.h:
* testing/Internals.idl:

Source/WebKit:

* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::WebPage):

LayoutTests:

* TestExpectations: Skip the new test.
* media/deactivate-audio-session-expected.txt: Added.
* media/deactivate-audio-session.html: Added.
* platform/ios/TestExpectations: Run the new test.
* platform/mac-wk2/TestExpectations: Ditto.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@239694 268f45cc-cd09-0410-ab3c-d52691b4dbfc

18 files changed:
LayoutTests/ChangeLog
LayoutTests/TestExpectations
LayoutTests/media/deactivate-audio-session-expected.txt [new file with mode: 0644]
LayoutTests/media/deactivate-audio-session.html [new file with mode: 0644]
LayoutTests/platform/ios/TestExpectations
LayoutTests/platform/mac-wk2/TestExpectations
Source/WebCore/ChangeLog
Source/WebCore/platform/audio/AudioSession.cpp
Source/WebCore/platform/audio/AudioSession.h
Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp
Source/WebCore/platform/audio/PlatformMediaSessionManager.h
Source/WebCore/platform/audio/ios/AudioSessionIOS.mm
Source/WebCore/platform/audio/mac/AudioSessionMac.cpp
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl
Source/WebKit/ChangeLog
Source/WebKit/WebProcess/WebPage/WebPage.cpp

index ccb3f98..bbdd94e 100644 (file)
@@ -1,3 +1,17 @@
+2019-01-07  Eric Carlson  <eric.carlson@apple.com>
+
+        Deactivate audio session whenever possible
+        https://bugs.webkit.org/show_bug.cgi?id=193188
+        <rdar://problem/42678977>
+
+        Reviewed by Jer Noble.
+
+        * TestExpectations: Skip the new test.
+        * media/deactivate-audio-session-expected.txt: Added.
+        * media/deactivate-audio-session.html: Added.
+        * platform/ios/TestExpectations: Run the new test.
+        * platform/mac-wk2/TestExpectations: Ditto.
+
 2019-01-07  Youenn Fablet  <youenn@apple.com>
 
         Resync WPT fetch tests to 834eac4
index 2e835ad..beb25c5 100644 (file)
@@ -161,6 +161,7 @@ fast/media/mq-prefers-reduced-motion-live-update.html [ Skip ]
 http/tests/loading/basic-auth-remove-credentials.html [ Skip ]
 http/tests/security/strip-referrer-to-origin-for-third-party-redirects-in-private-mode.html [ Skip ]
 http/tests/security/strip-referrer-to-origin-for-third-party-requests-in-private-mode.html [ Skip ]
+media/deactivate-audio-session.html [ Skip ]
 
 # ApplePay is only available on iOS (greater than iOS 10) and macOS (greater than macOS 10.12) and only for WebKit2.
 http/tests/ssl/applepay/ [ Skip ]
diff --git a/LayoutTests/media/deactivate-audio-session-expected.txt b/LayoutTests/media/deactivate-audio-session-expected.txt
new file mode 100644 (file)
index 0000000..78b4d9c
--- /dev/null
@@ -0,0 +1,8 @@
+Test that audio session is deactivated when the last media element with audio is deleted.
+
+** iframe loaded.
+EVENT(playing)
+EXPECTED (internals.audioSessionActive() == 'true') OK
+EXPECTED (internals.audioSessionActive() == 'false') OK
+END OF TEST
+
diff --git a/LayoutTests/media/deactivate-audio-session.html b/LayoutTests/media/deactivate-audio-session.html
new file mode 100644 (file)
index 0000000..3d88ed0
--- /dev/null
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>'play' event</title>
+        <script src=media-file.js></script>
+        <script src=video-test.js></script>
+        <script src=../resources/gc.js></script>
+
+        <script>
+            async function waitForTestToFinish()
+            {
+                gc();
+
+                if (window.internals)
+                    await testExpectedEventually('internals.audioSessionActive()', false);
+
+                endTest();
+            }
+
+            function playing()
+            {
+                consoleWrite("EVENT(playing)");
+
+                if (window.internals)
+                    testExpected('internals.audioSessionActive()', true);
+
+                let frame = document.getElementById("frame");
+                frame.parentNode.innerHTML = '';
+                
+                setTimeout(waitForTestToFinish, 100);
+            }
+
+            function frameLoaded()
+            {
+                consoleWrite("** iframe loaded.");
+
+                var standaloneMediaDocument = document.getElementById("frame").contentDocument;
+                let video = standaloneMediaDocument.querySelector("video");
+                if (!video) {
+                    failTest("ERROR: Video element was not found in frameLoaded().");
+                    return;
+                }
+
+                video.pause();
+                video.addEventListener('playing', playing);
+                video.play();
+            }
+
+            function start()
+            {
+                let frame = document.getElementById("frame");
+                frame.onload = frameLoaded;
+                frame.src = findMediaFile('audio', 'content/test');
+            }    
+        </script>
+    </head>
+
+    <body onload="start()">
+        <p>Test that audio session is deactivated when the last media element with audio is deleted.</p>
+        <div>
+        <iframe id="frame" width=400 height=300"></iframe>
+        </div>
+    </body>
+</html>
index bd747f7..95ad372 100644 (file)
@@ -2815,6 +2815,7 @@ http/tests/resourceLoadStatistics/cap-cache-max-age-for-prevalent-resource.html
 # Skipped in general expectations since they only work on iOS and Mac, WK2.
 http/tests/security/strip-referrer-to-origin-for-third-party-redirects-in-private-mode.html [ Pass ]
 http/tests/security/strip-referrer-to-origin-for-third-party-requests-in-private-mode.html [ Pass ]
+media/deactivate-audio-session.html [ Pass ]
 
 webkit.org/b/175273 imported/w3c/web-platform-tests/html/browsers/windows/noreferrer-window-name.html [ Failure ]
 
index caaa4c1..714d9e9 100644 (file)
@@ -788,6 +788,7 @@ webkit.org/b/185994 [ Debug ] fast/text/user-installed-fonts/shadow-postscript-f
 # Skipped in general expectations since they only work on iOS and Mac, WK2.
 http/tests/security/strip-referrer-to-origin-for-third-party-redirects-in-private-mode.html [ Pass ]
 http/tests/security/strip-referrer-to-origin-for-third-party-requests-in-private-mode.html [ Pass ]
+media/deactivate-audio-session.html [ Pass ]
 
 # Link preconnect is disabled on pre-High Sierra because the CFNetwork SPI is missing.
 [ Sierra ] http/tests/preconnect [ Skip ]
index 7f29aa7..bd64e96 100644 (file)
@@ -1,3 +1,39 @@
+2019-01-07  Eric Carlson  <eric.carlson@apple.com>
+
+        Deactivate audio session whenever possible
+        https://bugs.webkit.org/show_bug.cgi?id=193188
+        <rdar://problem/42678977>
+
+        Reviewed by Jer Noble.
+
+        Test: media/deactivate-audio-session.html
+
+        * platform/audio/AudioSession.cpp:
+        (WebCore::AudioSession::tryToSetActive):
+        (WebCore::AudioSession::tryToSetActiveInternal):
+        * platform/audio/AudioSession.h:
+        (WebCore::AudioSession::isActive const):
+
+        * platform/audio/PlatformMediaSessionManager.cpp:
+        (WebCore::PlatformMediaSessionManager::removeSession):
+        (WebCore::deactivateAudioSession):
+        (WebCore::PlatformMediaSessionManager::shouldDeactivateAudioSession):
+        (WebCore::PlatformMediaSessionManager::setShouldDeactivateAudioSession):
+        * platform/audio/PlatformMediaSessionManager.h:
+
+        * platform/audio/ios/AudioSessionIOS.mm:
+        (WebCore::AudioSession::tryToSetActiveInternal):
+        (WebCore::AudioSession::tryToSetActive): Deleted.
+
+        * platform/audio/mac/AudioSessionMac.cpp:
+        (WebCore::AudioSession::tryToSetActiveInternal):
+        (WebCore::AudioSession::tryToSetActive): Deleted.
+
+        * testing/Internals.cpp:
+        (WebCore::Internals::audioSessionActive const):
+        * testing/Internals.h:
+        * testing/Internals.idl:
+
 2019-01-07  David Kilzer  <ddkilzer@apple.com>
 
         PlatformECKey should use a std::unique_ptr
index 2cc9791..f60eae8 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2014 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -39,6 +39,15 @@ AudioSession& AudioSession::sharedSession()
     return session;
 }
 
+bool AudioSession::tryToSetActive(bool active)
+{
+    if (!tryToSetActiveInternal(active))
+        return false;
+
+    m_active = active;
+    return true;
+}
+
 #if !PLATFORM(COCOA)
 class AudioSessionPrivate {
 };
@@ -91,7 +100,7 @@ size_t AudioSession::numberOfOutputChannels() const
     return 0;
 }
 
-bool AudioSession::tryToSetActive(bool)
+bool AudioSession::tryToSetActiveInternal(bool)
 {
     notImplemented();
     return true;
index 1b632e1..cabb874 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2018 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -87,13 +87,18 @@ public:
     bool isMuted() const;
     void handleMutedStateChange();
 
+    bool isActive() const { return m_active; }
+
 private:
     friend class NeverDestroyed<AudioSession>;
     AudioSession();
     ~AudioSession();
 
+    bool tryToSetActiveInternal(bool);
+
     std::unique_ptr<AudioSessionPrivate> m_private;
     HashSet<MutedStateObserver*> m_observers;
+    bool m_active { false }; // Used only for testing.
 };
 
 }
index f68f866..2050c5e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2015 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -162,6 +162,12 @@ void PlatformMediaSessionManager::removeSession(PlatformMediaSession& session)
     if (m_sessions.isEmpty() || std::all_of(m_sessions.begin(), m_sessions.end(), std::logical_not<void>())) {
         m_remoteCommandListener = nullptr;
         m_audioHardwareListener = nullptr;
+#if USE(AUDIO_SESSION)
+        if (m_becameActive && shouldDeactivateAudioSession()) {
+            AudioSession::sharedSession().tryToSetActive(false);
+            m_becameActive = false;
+        }
+#endif
     }
 
     updateSessionState();
@@ -199,6 +205,8 @@ bool PlatformMediaSessionManager::sessionWillBeginPlayback(PlatformMediaSession&
 #if USE(AUDIO_SESSION)
     if (activeAudioSessionRequired() && !AudioSession::sharedSession().tryToSetActive(true))
         return false;
+
+    m_becameActive = true;
 #endif
 
     if (m_interrupted)
@@ -469,6 +477,22 @@ PlatformMediaSession* PlatformMediaSessionManager::findSession(const Function<bo
     return foundSession;
 }
 
+static bool& deactivateAudioSession()
+{
+    static bool deactivate;
+    return deactivate;
+}
+
+bool PlatformMediaSessionManager::shouldDeactivateAudioSession()
+{
+    return deactivateAudioSession();
+}
+
+void PlatformMediaSessionManager::setShouldDeactivateAudioSession(bool deactivate)
+{
+    deactivateAudioSession() = deactivate;
+}
+
 #else // ENABLE(VIDEO) || ENABLE(WEB_AUDIO)
 
 void PlatformMediaSessionManager::updateNowPlayingInfoIfNecessary()
index e670daf..065d94f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2015 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -48,6 +48,8 @@ public:
 
     static void updateNowPlayingInfoIfNecessary();
 
+    WEBCORE_EXPORT static void setShouldDeactivateAudioSession(bool);
+
     virtual ~PlatformMediaSessionManager() = default;
 
     virtual void scheduleUpdateNowPlayingInfo() { }
@@ -146,6 +148,8 @@ private:
     void systemWillSleep() override;
     void systemDidWake() override;
 
+    static bool shouldDeactivateAudioSession();
+
     SessionRestrictions m_restrictions[PlatformMediaSession::MediaStreamCapturingAudio + 1];
     mutable Vector<PlatformMediaSession*> m_sessions;
     std::unique_ptr<RemoteCommandListener> m_remoteCommandListener;
@@ -161,6 +165,10 @@ private:
     mutable bool m_isApplicationInBackground { false };
     bool m_willIgnoreSystemInterruptions { false };
     mutable int m_iteratingOverSessions { 0 };
+
+#if USE(AUDIO_SESSION)
+    bool m_becameActive { false };
+#endif
 };
 
 }
index 6b0f3a0..dd65dae 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2014 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -32,6 +32,7 @@
 #import <AVFoundation/AVAudioSession.h>
 #import <objc/runtime.h>
 #import <pal/spi/mac/AVFoundationSPI.h>
+#import <wtf/OSObjectPtr.h>
 #import <wtf/RetainPtr.h>
 #import <wtf/SoftLinking.h>
 
@@ -82,6 +83,7 @@ class AudioSessionPrivate {
 public:
     AudioSessionPrivate(AudioSession*);
     AudioSession::CategoryType m_categoryOverride;
+    OSObjectPtr<dispatch_queue_t> m_dispatchQueue;
 };
 
 AudioSessionPrivate::AudioSessionPrivate(AudioSession*)
@@ -213,11 +215,29 @@ size_t AudioSession::numberOfOutputChannels() const
     return [[AVAudioSession sharedInstance] outputNumberOfChannels];
 }
 
-bool AudioSession::tryToSetActive(bool active)
+bool AudioSession::tryToSetActiveInternal(bool active)
 {
-    NSError *error = nil;
-    [[AVAudioSession sharedInstance] setActive:active withOptions:active ? 0 : AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
-    return !error;
+    __block NSError* error = nil;
+
+    if (!m_private->m_dispatchQueue)
+        m_private->m_dispatchQueue = adoptOSObject(dispatch_queue_create("AudioSession Activation Queue", DISPATCH_QUEUE_SERIAL));
+
+    // We need to deactivate the session on another queue because the AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation option
+    // means that AVAudioSession may synchronously unduck previously ducked clients. Activation needs to complete before this method
+    // returns, so do it synchronously on the same serial queue.
+    if (active) {
+        dispatch_sync(m_private->m_dispatchQueue.get(), ^{
+            [[AVAudioSession sharedInstance] setActive:YES withOptions:0 error:&error];
+        });
+
+        return !error;
+    }
+
+    dispatch_async(m_private->m_dispatchQueue.get(), ^{
+        [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
+    });
+
+    return true;
 }
 
 size_t AudioSession::preferredBufferSize() const
index e918098..5537c21 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -126,7 +126,7 @@ size_t AudioSession::numberOfOutputChannels() const
     return 0;
 }
 
-bool AudioSession::tryToSetActive(bool)
+bool AudioSession::tryToSetActiveInternal(bool)
 {
     notImplemented();
     return true;
index 3c0a59f..18e5789 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2012 Google Inc. All rights reserved.
- * Copyright (C) 2013-2018 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -4630,6 +4630,14 @@ double Internals::preferredAudioBufferSize() const
     return 0;
 }
 
+bool Internals::audioSessionActive() const
+{
+#if USE(AUDIO_SESSION)
+    return AudioSession::sharedSession().isActive();
+#endif
+    return false;
+}
+
 void Internals::clearCacheStorageMemoryRepresentation(DOMPromiseDeferred<void>&& promise)
 {
     auto* document = contextDocument();
index b37fa69..f70b7b6 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2012 Google Inc. All rights reserved.
- * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -693,6 +693,7 @@ public:
 
     String audioSessionCategory() const;
     double preferredAudioBufferSize() const;
+    bool audioSessionActive() const;
 
     void clearCacheStorageMemoryRepresentation(DOMPromiseDeferred<void>&&);
     void cacheStorageEngineRepresentation(DOMPromiseDeferred<IDLDOMString>&&);
index 7a3d917..0e74659 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2012 Google Inc. All rights reserved.
- * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2019 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -688,6 +688,7 @@ enum CompositingPolicy {
 
     DOMString audioSessionCategory();
     double preferredAudioBufferSize();
+    boolean audioSessionActive();
 
     [Conditional=SERVICE_WORKER] Promise<boolean> hasServiceWorkerRegistration(DOMString scopeURL);
     [Conditional=SERVICE_WORKER] void terminateServiceWorker(ServiceWorker worker);
index 4954a6d..eb8e5ef 100644 (file)
@@ -1,3 +1,14 @@
+2019-01-07  Eric Carlson  <eric.carlson@apple.com>
+
+        Deactivate audio session whenever possible
+        https://bugs.webkit.org/show_bug.cgi?id=193188
+        <rdar://problem/42678977>
+
+        Reviewed by Jer Noble.
+
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::WebPage):
+
 2019-01-07  Joseph Pecoraro  <pecoraro@apple.com>
 
         [Cocoa] Add SPI to check if a WKWebView has an inspector frontend
index f6d4bea..b6c5330 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010-2018 Apple Inc. All rights reserved.
+ * Copyright (C) 2010-2019 Apple Inc. All rights reserved.
  * Copyright (C) 2012 Intel Corporation. All rights reserved.
  * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
  *
 #include <WebCore/PageConfiguration.h>
 #include <WebCore/PingLoader.h>
 #include <WebCore/PlatformKeyboardEvent.h>
+#include <WebCore/PlatformMediaSessionManager.h>
 #include <WebCore/PluginDocument.h>
 #include <WebCore/PrintContext.h>
 #include <WebCore/PromisedAttachmentInfo.h>
@@ -630,6 +631,10 @@ WebPage::WebPage(uint64_t pageID, WebPageCreationParameters&& parameters)
     setViewportConfigurationViewLayoutSize(parameters.viewportConfigurationViewLayoutSize, parameters.viewportConfigurationLayoutSizeScaleFactor, 0);
     setMaximumUnobscuredSize(parameters.maximumUnobscuredSize);
 #endif
+
+#if USE(AUDIO_SESSION)
+    PlatformMediaSessionManager::setShouldDeactivateAudioSession(true);
+#endif
 }
 
 #if ENABLE(WEB_RTC)