Muted elements do not have their Now Playing status updated when unmuted.
authorjer.noble@apple.com <jer.noble@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 29 Aug 2018 19:39:37 +0000 (19:39 +0000)
committerjer.noble@apple.com <jer.noble@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 29 Aug 2018 19:39:37 +0000 (19:39 +0000)
https://bugs.webkit.org/show_bug.cgi?id=189069

Reviewed by Eric Carlson.

Source/WebCore:

Schedule an updateNowPlayingInfo() when an element becomes unmuted.

* platform/audio/PlatformMediaSessionManager.h:
* platform/audio/mac/MediaSessionManagerMac.h:
* platform/audio/mac/MediaSessionManagerMac.mm:
(WebCore::MediaSessionManagerMac::sessionCanProduceAudioChanged):

Source/WebCore/PAL:

* pal/spi/mac/MediaRemoteSPI.h:

Tools:

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKitCocoa/NowPlaying.mm: Added.
(userInfoHasNowPlayingApplicationPID):
(getNowPlayingClient):
(getNowPlayingClientPid):
(NowPlayingTest::webView):
(NowPlayingTest::configuration):
(NowPlayingTest::webViewPid):
(NowPlayingTest::loadPage):
(NowPlayingTest::runScriptWithUserGesture):
(NowPlayingTest::runScriptWithoutUserGesture):
(NowPlayingTest::executeAndWaitForPlaying):
(NowPlayingTest::executeAndWaitForWebViewToBecomeNowPlaying):
(NowPlayingTest::observers):
(NowPlayingTest::addObserver):
(NowPlayingTest::removeObserver):
(NowPlayingTest::notificationCallback):
(NowPlayingTest::receivedNotification):
(NowPlayingTest::performAfterReceivingNotification):
(TEST_F):
* TestWebKitAPI/Tests/WebKitCocoa/now-playing.html: Added.

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

Source/WebCore/ChangeLog
Source/WebCore/PAL/ChangeLog
Source/WebCore/PAL/pal/spi/mac/MediaRemoteSPI.h
Source/WebCore/platform/audio/PlatformMediaSessionManager.h
Source/WebCore/platform/audio/mac/MediaSessionManagerMac.h
Source/WebCore/platform/audio/mac/MediaSessionManagerMac.mm
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKitCocoa/NowPlaying.mm [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/now-playing.html [new file with mode: 0644]

index a7e7ed8..d372527 100644 (file)
@@ -1,3 +1,17 @@
+2018-08-29  Jer Noble  <jer.noble@apple.com>
+
+        Muted elements do not have their Now Playing status updated when unmuted.
+        https://bugs.webkit.org/show_bug.cgi?id=189069
+
+        Reviewed by Eric Carlson.
+
+        Schedule an updateNowPlayingInfo() when an element becomes unmuted.
+
+        * platform/audio/PlatformMediaSessionManager.h:
+        * platform/audio/mac/MediaSessionManagerMac.h:
+        * platform/audio/mac/MediaSessionManagerMac.mm:
+        (WebCore::MediaSessionManagerMac::sessionCanProduceAudioChanged):
+
 2018-08-29  Wenson Hsieh  <wenson_hsieh@apple.com>
 
         Use the null string instead of std::nullopt for missing attachment file names and content types
index 42825d3..7cc5f63 100644 (file)
@@ -1,3 +1,12 @@
+2018-08-29  Jer Noble  <jer.noble@apple.com>
+
+        Muted elements do not have their Now Playing status updated when unmuted.
+        https://bugs.webkit.org/show_bug.cgi?id=189069
+
+        Reviewed by Eric Carlson.
+
+        * pal/spi/mac/MediaRemoteSPI.h:
+
 2018-08-27  Keith Rollin  <krollin@apple.com>
 
         Unreviewed build fix -- disable LTO for production builds
index 3d00d0b..cee829f 100644 (file)
@@ -103,6 +103,7 @@ typedef uint32_t MRMediaRemoteCommandHandlerStatus;
 typedef uint32_t MRMediaRemoteError;
 typedef struct _MROrigin *MROriginRef;
 typedef struct _MRMediaRemoteCommandInfo *MRMediaRemoteCommandInfoRef;
+typedef void *MRNowPlayingClientRef;
 typedef void(^MRMediaRemoteAsyncCommandHandlerBlock)(MRMediaRemoteCommand command, CFDictionaryRef options, void(^completion)(CFArrayRef));
 
 WTF_EXTERN_C_BEGIN
index 4c9fe6e..c7d0bc8 100644 (file)
@@ -98,6 +98,7 @@ public:
     virtual void sessionStateChanged(PlatformMediaSession&);
     virtual void sessionDidEndRemoteScrubbing(const PlatformMediaSession&) { };
     virtual void clientCharacteristicsChanged(PlatformMediaSession&) { }
+    virtual void sessionCanProduceAudioChanged(PlatformMediaSession&);
 
 #if PLATFORM(IOS)
     virtual void configureWireLessTargetMonitoring() { }
@@ -110,7 +111,6 @@ public:
     Vector<PlatformMediaSession*> currentSessionsMatching(const WTF::Function<bool(const PlatformMediaSession&)>&);
 
     void sessionIsPlayingToWirelessPlaybackTargetChanged(PlatformMediaSession&);
-    void sessionCanProduceAudioChanged(PlatformMediaSession&);
 
 protected:
     friend class PlatformMediaSession;
index 2907695..44cd9c7 100644 (file)
@@ -55,6 +55,7 @@ private:
     void sessionWillEndPlayback(PlatformMediaSession&) override;
     void sessionDidEndRemoteScrubbing(const PlatformMediaSession&) override;
     void clientCharacteristicsChanged(PlatformMediaSession&) override;
+    void sessionCanProduceAudioChanged(PlatformMediaSession&) override;
 
     void updateNowPlayingInfo();
 
index cb83304..1962569 100644 (file)
@@ -104,6 +104,12 @@ void MediaSessionManagerMac::clientCharacteristicsChanged(PlatformMediaSession&)
     LOG(Media, "MediaSessionManagerMac::clientCharacteristicsChanged");
     scheduleUpdateNowPlayingInfo();
 }
+    
+void MediaSessionManagerMac::sessionCanProduceAudioChanged(PlatformMediaSession& session)
+{
+    PlatformMediaSessionManager::sessionCanProduceAudioChanged(session);
+    scheduleUpdateNowPlayingInfo();
+}
 
 PlatformMediaSession* MediaSessionManagerMac::nowPlayingEligibleSession()
 {
index 8cac11a..472bc3e 100644 (file)
@@ -1,3 +1,32 @@
+2018-08-29  Jer Noble  <jer.noble@apple.com>
+
+        Muted elements do not have their Now Playing status updated when unmuted.
+        https://bugs.webkit.org/show_bug.cgi?id=189069
+
+        Reviewed by Eric Carlson.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKitCocoa/NowPlaying.mm: Added.
+        (userInfoHasNowPlayingApplicationPID):
+        (getNowPlayingClient):
+        (getNowPlayingClientPid):
+        (NowPlayingTest::webView):
+        (NowPlayingTest::configuration):
+        (NowPlayingTest::webViewPid):
+        (NowPlayingTest::loadPage):
+        (NowPlayingTest::runScriptWithUserGesture):
+        (NowPlayingTest::runScriptWithoutUserGesture):
+        (NowPlayingTest::executeAndWaitForPlaying):
+        (NowPlayingTest::executeAndWaitForWebViewToBecomeNowPlaying):
+        (NowPlayingTest::observers):
+        (NowPlayingTest::addObserver):
+        (NowPlayingTest::removeObserver):
+        (NowPlayingTest::notificationCallback):
+        (NowPlayingTest::receivedNotification):
+        (NowPlayingTest::performAfterReceivingNotification):
+        (TEST_F):
+        * TestWebKitAPI/Tests/WebKitCocoa/now-playing.html: Added.
+
 2018-08-29  Thomas Denney  <tdenney@apple.com>
 
         [WHLSL] Ensure that isLValue is copied by the rewriter
index 82328d8..cc640c5 100644 (file)
                CD0BD0A61F79924D001AB2CF /* ContextMenuImgWithVideo.mm in Sources */ = {isa = PBXBuildFile; fileRef = CD0BD0A51F799220001AB2CF /* ContextMenuImgWithVideo.mm */; };
                CD0BD0A81F79982D001AB2CF /* ContextMenuImgWithVideo.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CD0BD0A71F7997C2001AB2CF /* ContextMenuImgWithVideo.html */; };
                CD227E44211A4D5D00D285AF /* PreferredAudioBufferSize.mm in Sources */ = {isa = PBXBuildFile; fileRef = CD227E43211A4D5D00D285AF /* PreferredAudioBufferSize.mm */; };
+               CD2D0D1A213465560018C784 /* NowPlaying.mm in Sources */ = {isa = PBXBuildFile; fileRef = CD2D0D19213465560018C784 /* NowPlaying.mm */; };
                CD321B041E3A85FA00EB21C8 /* video-with-muted-audio-and-webaudio.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CD321B031E3A84B700EB21C8 /* video-with-muted-audio-and-webaudio.html */; };
                CD577799211CE0E4001B371E /* web-audio-only.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CD577798211CDE8F001B371E /* web-audio-only.html */; };
                CD57779C211CE91F001B371E /* audio-with-web-audio.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CD57779A211CE6B7001B371E /* audio-with-web-audio.html */; };
                CDA3159A1ED548F1009F60D3 /* MediaPlaybackSleepAssertion.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CDA315991ED540A5009F60D3 /* MediaPlaybackSleepAssertion.html */; };
                CDA3159D1ED5643F009F60D3 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDA3159C1ED5643F009F60D3 /* IOKit.framework */; };
                CDB4115A1E0B00DB00EAD352 /* video-with-muted-audio.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CDB411591E09DA8E00EAD352 /* video-with-muted-audio.html */; };
+               CDB5DFFF213610FA00D3E189 /* now-playing.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CDB5DFFE21360ED800D3E189 /* now-playing.html */; };
                CDBFCC451A9FF45300A7B691 /* FullscreenZoomInitialFrame.mm in Sources */ = {isa = PBXBuildFile; fileRef = CDBFCC431A9FF44800A7B691 /* FullscreenZoomInitialFrame.mm */; };
                CDBFCC461A9FF49E00A7B691 /* FullscreenZoomInitialFrame.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CDBFCC421A9FF44800A7B691 /* FullscreenZoomInitialFrame.html */; };
                CDC8E48D1BC5CB4500594FEC /* AudioSessionCategoryIOS.mm in Sources */ = {isa = PBXBuildFile; fileRef = CDC8E4851BC5B19400594FEC /* AudioSessionCategoryIOS.mm */; };
                                5797FE331EB15AB100B2F4A0 /* navigation-client-default-crypto.html in Copy Resources */,
                                C99B675F1E39736F00FC6C80 /* no-autoplay-with-controls.html in Copy Resources */,
                                466C3843210637DE006A88DE /* notify-resourceLoadObserver.html in Copy Resources */,
+                               CDB5DFFF213610FA00D3E189 /* now-playing.html in Copy Resources */,
                                93E2D2761ED7D53200FA76F6 /* offscreen-iframe-of-media-document.html in Copy Resources */,
                                CEA6CF2819CCF69D0064F5A7 /* open-and-close-window.html in Copy Resources */,
                                7CCB99231D3B4A46003922F6 /* open-multiple-external-url.html in Copy Resources */,
                CD0BD0A71F7997C2001AB2CF /* ContextMenuImgWithVideo.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = ContextMenuImgWithVideo.html; sourceTree = "<group>"; };
                CD225C071C45A69200140761 /* ParsedContentRange.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ParsedContentRange.cpp; sourceTree = "<group>"; };
                CD227E43211A4D5D00D285AF /* PreferredAudioBufferSize.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PreferredAudioBufferSize.mm; sourceTree = "<group>"; };
+               CD2D0D19213465560018C784 /* NowPlaying.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = NowPlaying.mm; sourceTree = "<group>"; };
                CD321B031E3A84B700EB21C8 /* video-with-muted-audio-and-webaudio.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "video-with-muted-audio-and-webaudio.html"; sourceTree = "<group>"; };
                CD5393C71757BA9700C07123 /* MD5.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MD5.cpp; sourceTree = "<group>"; };
                CD5393C91757BAC400C07123 /* SHA1.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SHA1.cpp; sourceTree = "<group>"; };
                CDA315991ED540A5009F60D3 /* MediaPlaybackSleepAssertion.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = MediaPlaybackSleepAssertion.html; sourceTree = "<group>"; };
                CDA3159C1ED5643F009F60D3 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
                CDB411591E09DA8E00EAD352 /* video-with-muted-audio.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "video-with-muted-audio.html"; sourceTree = "<group>"; };
+               CDB5DFFE21360ED800D3E189 /* now-playing.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "now-playing.html"; sourceTree = "<group>"; };
                CDBFCC421A9FF44800A7B691 /* FullscreenZoomInitialFrame.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = FullscreenZoomInitialFrame.html; sourceTree = "<group>"; };
                CDBFCC431A9FF44800A7B691 /* FullscreenZoomInitialFrame.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FullscreenZoomInitialFrame.mm; sourceTree = "<group>"; };
                CDC2C7141797089D00E627FB /* TimeRanges.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TimeRanges.cpp; sourceTree = "<group>"; };
                                51CD1C6A1B38CE3600142CA5 /* ModalAlerts.mm */,
                                1ABC3DED1899BE6D004F0626 /* Navigation.mm */,
                                5CAE4637201937CD0051610F /* NetworkProcessCrashNonPersistentDataStore.mm */,
+                               CD2D0D19213465560018C784 /* NowPlaying.mm */,
                                2ECFF5541D9B12F800B55394 /* NowPlayingControlsTests.mm */,
                                A10F047C1E3AD29C00C95E19 /* NSFileManagerExtras.mm */,
                                37A22AA51DCAA27200AFBFC4 /* ObservedRenderingProgressEventsAfterCrash.mm */,
                                9BCD4119206D5ED7001D71BE /* mso-list-on-h4.html */,
                                9BF356CC202D44F200F71160 /* mso-list.html */,
                                466C3842210637CE006A88DE /* notify-resourceLoadObserver.html */,
+                               CDB5DFFE21360ED800D3E189 /* now-playing.html */,
                                93E2D2751ED7D51700FA76F6 /* offscreen-iframe-of-media-document.html */,
                                7CCB99221D3B44E7003922F6 /* open-multiple-external-url.html */,
                                CEBCA1341E3A803400C73293 /* page-with-csp-iframe.html */,
                                7CCE7F051A411AE600447C4C /* NewFirstVisuallyNonEmptyLayoutFrames.cpp in Sources */,
                                0F5651F71FCE4DDC00310FBC /* NoHistoryItemScrollToFragment.mm in Sources */,
                                83F22C6420B355F80034277E /* NoPolicyDelegateResponse.mm in Sources */,
+                               CD2D0D1A213465560018C784 /* NowPlaying.mm in Sources */,
                                2ECFF5551D9B12F800B55394 /* NowPlayingControlsTests.mm in Sources */,
                                A10F047E1E3AD29C00C95E19 /* NSFileManagerExtras.mm in Sources */,
                                37A22AA71DCAA27200AFBFC4 /* ObservedRenderingProgressEventsAfterCrash.mm in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/NowPlaying.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/NowPlaying.mm
new file mode 100644 (file)
index 0000000..6860cbd
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#if WK_API_ENABLED && PLATFORM(COCOA) && USE(MEDIAREMOTE)
+
+#import "PlatformUtilities.h"
+#import "TestWKWebView.h"
+#import <WebKit/WKWebViewPrivate.h>
+#import <pal/spi/mac/MediaRemoteSPI.h>
+#import <wtf/Function.h>
+#import <wtf/HashMap.h>
+#import <wtf/HashSet.h>
+#import <wtf/NeverDestroyed.h>
+#import <wtf/SoftLinking.h>
+#import <wtf/text/WTFString.h>
+
+SOFT_LINK_PRIVATE_FRAMEWORK(MediaRemote)
+SOFT_LINK(MediaRemote, MRMediaRemoteSetWantsNowPlayingNotifications, void, (bool wantsNotifications), (wantsNotifications))
+SOFT_LINK(MediaRemote, MRMediaRemoteGetNowPlayingClient, void, (dispatch_queue_t queue, void(^completion)(MRNowPlayingClientRef, CFErrorRef)), (queue, completion))
+SOFT_LINK(MediaRemote, MRNowPlayingClientGetProcessIdentifier, pid_t, (MRNowPlayingClientRef client), (client))
+SOFT_LINK_CONSTANT(MediaRemote, kMRMediaRemoteNowPlayingApplicationDidChangeNotification, CFStringRef)
+SOFT_LINK_CONSTANT(MediaRemote, kMRMediaRemoteNowPlayingApplicationPIDUserInfoKey, CFStringRef)
+#define MRMediaRemoteSetWantsNowPlayingNotifications softLinkMRMediaRemoteSetWantsNowPlayingNotifications
+#define MRMediaRemoteGetNowPlayingClient softLinkMRMediaRemoteGetNowPlayingClient
+#define MRNowPlayingClientGetProcessIdentifier softLinkMRNowPlayingClientGetProcessIdentifier
+#define kMRMediaRemoteNowPlayingApplicationDidChangeNotification getkMRMediaRemoteNowPlayingApplicationDidChangeNotification()
+#define kMRMediaRemoteNowPlayingApplicationPIDUserInfoKey getkMRMediaRemoteNowPlayingApplicationPIDUserInfoKey()
+
+static bool userInfoHasNowPlayingApplicationPID(CFDictionaryRef userInfo, int32_t pid)
+{
+    CFNumberRef nowPlayingPidCF = (CFNumberRef)CFDictionaryGetValue(userInfo, kMRMediaRemoteNowPlayingApplicationPIDUserInfoKey);
+    if (!nowPlayingPidCF)
+        return false;
+
+    int32_t nowPlayingPid = 0;
+    if (!CFNumberGetValue(nowPlayingPidCF, kCFNumberSInt32Type, &nowPlayingPid))
+        return false;
+
+    return pid == nowPlayingPid;
+}
+
+static RetainPtr<MRNowPlayingClientRef> getNowPlayingClient()
+{
+    bool gotNowPlaying = false;
+    RetainPtr<MRNowPlayingClientRef> nowPlayingClient;
+    MRMediaRemoteGetNowPlayingClient(dispatch_get_main_queue(), [&] (MRNowPlayingClientRef player, CFErrorRef error) {
+        if (!error && player)
+            nowPlayingClient = player;
+        gotNowPlaying = true;
+    });
+    TestWebKitAPI::Util::run(&gotNowPlaying);
+    return nowPlayingClient;
+}
+
+static pid_t getNowPlayingClientPid()
+{
+    return MRNowPlayingClientGetProcessIdentifier(getNowPlayingClient().get());
+}
+
+class NowPlayingTest : public testing::Test {
+public:
+    void SetUp() override
+    {
+        addObserver(*this);
+
+        _configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+        [_configuration setMediaTypesRequiringUserActionForPlayback:WKAudiovisualMediaTypeAudio];
+
+        _webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:_configuration.get() addToWindow:YES]);
+    }
+
+    void TearDown() override
+    {
+        removeObserver(*this);
+    }
+
+    TestWKWebView* webView() { return _webView.get(); }
+    WKWebViewConfiguration* configuration() { return _configuration.get(); }
+
+    pid_t webViewPid() { return [_webView _webProcessIdentifier]; }
+
+    void loadPage(const String& name)
+    {
+        [_webView synchronouslyLoadTestPageNamed:name];
+    }
+
+    void runScriptWithUserGesture(const String& script)
+    {
+        bool complete = false;
+        [_webView evaluateJavaScript:script completionHandler:[&] (id, NSError *) { complete = true; }];
+        TestWebKitAPI::Util::run(&complete);
+    }
+
+    void runScriptWithoutUserGesture(const String& script)
+    {
+        bool complete = false;
+        [_webView _evaluateJavaScriptWithoutUserGesture:script completionHandler:[&] (id, NSError *) { complete = true; }];
+        TestWebKitAPI::Util::run(&complete);
+    }
+
+    void executeAndWaitForPlaying(Function<void()>&& callback)
+    {
+        bool isPlaying = false;
+        [_webView performAfterReceivingMessage:@"playing" action:[&] { isPlaying = true; }];
+        callback();
+        TestWebKitAPI::Util::run(&isPlaying);
+    }
+
+    void executeAndWaitForWebViewToBecomeNowPlaying(Function<void()>&& callback)
+    {
+        bool becameNowPlaying = false;
+        performAfterReceivingNotification(kMRMediaRemoteNowPlayingApplicationDidChangeNotification, [&] (CFDictionaryRef userInfo) -> bool {
+            if (!userInfoHasNowPlayingApplicationPID(userInfo, webViewPid()))
+                return false;
+
+            becameNowPlaying = true;
+            return true;
+        });
+        callback();
+        TestWebKitAPI::Util::run(&becameNowPlaying);
+    }
+
+private:
+    using ObserverSet = HashSet<NowPlayingTest*>;
+    static ObserverSet& observers()
+    {
+        static NeverDestroyed<ObserverSet> observers { };
+        return observers.get();
+    }
+
+    static void addObserver(NowPlayingTest& test)
+    {
+        observers().add(&test);
+        if (observers().size() == 1) {
+            CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), nullptr, &notificationCallback, kMRMediaRemoteNowPlayingApplicationDidChangeNotification, nullptr, CFNotificationSuspensionBehaviorDeliverImmediately);
+            MRMediaRemoteSetWantsNowPlayingNotifications(true);
+        }
+    }
+
+    static void removeObserver(NowPlayingTest& test)
+    {
+        observers().remove(&test);
+        if (!observers().size()) {
+            CFNotificationCenterRemoveObserver(CFNotificationCenterGetLocalCenter(), nullptr, kMRMediaRemoteNowPlayingApplicationDidChangeNotification, nullptr);
+            MRMediaRemoteSetWantsNowPlayingNotifications(false);
+        }
+    }
+
+    static void notificationCallback(CFNotificationCenterRef center, void *observer, CFNotificationName name, const void *object, CFDictionaryRef userInfo)
+    {
+        ObserverSet observersCopy = observers();
+        for (auto* observer : observersCopy)
+            observer->receivedNotification(name, userInfo);
+    }
+
+    void receivedNotification(CFNotificationName name, CFDictionaryRef userInfo)
+    {
+        auto callbackIter = _callbacks.find(name);
+        if (callbackIter == _callbacks.end())
+            return;
+        auto& callback = callbackIter->value;
+        if (callback(userInfo))
+            _callbacks.remove(callbackIter);
+    }
+
+    using NotificationCallback = Function<bool(CFDictionaryRef)>;
+    void performAfterReceivingNotification(CFNotificationName name, NotificationCallback&& callback)
+    {
+        _callbacks.add(name, WTFMove(callback));
+    }
+
+    RetainPtr<WKWebViewConfiguration> _configuration;
+    RetainPtr<TestWKWebView> _webView;
+    HashMap<CFNotificationName, NotificationCallback> _callbacks;
+};
+
+TEST_F(NowPlayingTest, AudioElement)
+{
+    executeAndWaitForWebViewToBecomeNowPlaying([&] {
+        executeAndWaitForPlaying([&] {
+            loadPage("now-playing");
+            runScriptWithoutUserGesture("createMediaElement({type:'audio', hasAudio:true})");
+            runScriptWithUserGesture("play()");
+        });
+    });
+}
+
+TEST_F(NowPlayingTest, VideoElementWithAudio)
+{
+    executeAndWaitForWebViewToBecomeNowPlaying([&] {
+        executeAndWaitForPlaying([&] {
+            loadPage("now-playing");
+            runScriptWithoutUserGesture("createMediaElement({type:'video', hasAudio:true})");
+            runScriptWithUserGesture("play()");
+        });
+    });
+}
+
+TEST_F(NowPlayingTest, VideoElementWithoutAudio)
+{
+    executeAndWaitForPlaying([&] {
+        loadPage("now-playing");
+        runScriptWithoutUserGesture("createMediaElement({type:'video', hasAudio:false})");
+        runScriptWithoutUserGesture("play()");
+    });
+
+    ASSERT_NE(webViewPid(), getNowPlayingClientPid());
+}
+
+TEST_F(NowPlayingTest, VideoElementWithMutedAudio)
+{
+    executeAndWaitForPlaying([&] {
+        loadPage("now-playing");
+        runScriptWithoutUserGesture("createMediaElement({type:'video', hasAudio:true, muted:true})");
+        runScriptWithoutUserGesture("play()");
+    });
+    ASSERT_NE(webViewPid(), getNowPlayingClientPid());
+}
+
+TEST_F(NowPlayingTest, VideoElementWithMutedAudioUnmutedWithUserGesture)
+{
+    executeAndWaitForPlaying([&] {
+        loadPage("now-playing");
+        runScriptWithoutUserGesture("createMediaElement({type:'video', hasAudio:true, muted:true})");
+        runScriptWithoutUserGesture("play()");
+    });
+    ASSERT_NE(webViewPid(), getNowPlayingClientPid());
+
+    executeAndWaitForWebViewToBecomeNowPlaying([&] {
+        runScriptWithUserGesture("unmute()");
+    });
+}
+
+TEST_F(NowPlayingTest, VideoElementWithoutAudioPlayWithUserGesture)
+{
+    executeAndWaitForPlaying([&] {
+        loadPage("now-playing");
+        runScriptWithoutUserGesture("createMediaElement({type:'video', hasAudio:true, muted:true})");
+        runScriptWithoutUserGesture("play()");
+    });
+    ASSERT_NE(webViewPid(), getNowPlayingClientPid());
+
+    executeAndWaitForPlaying([&] {
+        runScriptWithUserGesture("play()");
+    });
+
+    ASSERT_NE(webViewPid(), getNowPlayingClientPid());
+}
+
+#endif // WK_API_ENABLED && PLATFORM(COCOA) && USE(MEDIAREMOTE)
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/now-playing.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/now-playing.html
new file mode 100644 (file)
index 0000000..57f38be
--- /dev/null
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script>
+    var mediaElement;
+    function createMediaElement(parameters) {
+        if (parameters.type === 'video')
+            mediaElement = document.createElement('video');
+        else if (parameters.type === 'audio')
+            mediaElement = document.createElement('audio');
+        else
+            throw 'Necessary parameter not provided';
+
+        if (parameters.autoplay)
+            mediaElement.autoplay = true;
+
+        if (parameters.muted)
+            mediaElement.muted = true;
+
+        if (parameters.controls)
+            mediaElement.controls = true;
+
+        if (parameters.hasAudio)
+            mediaElement.src = 'video-with-audio.mp4';
+        else
+            mediaElement.src = 'video-without-audio.mp4';
+
+        document.body.appendChild(mediaElement);
+    }
+
+    function playing() {
+        window.webkit.messageHandlers.testHandler.postMessage('playing');
+    }
+
+    function notPlaying() {
+        window.webkit.messageHandlers.testHandler.postMessage('not playing');
+    }
+
+    function play() {
+        mediaElement.play().then(playing, notPlaying);
+    }
+
+    function unmute() {
+        mediaElement.muted = false;
+    }
+    </script>
+</head>
+<body>
+</body>
+</html>