/* * 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. */ #import "config.h" #if !PLATFORM(IOS_FAMILY_SIMULATOR) && !(PLATFORM(MAC) && __MAC_OS_X_VERSION_MAX_ALLOWED < 101304) #import "PlatformUtilities.h" #import "TestWKWebView.h" #import #import #import #import #import #import #import #import #import 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 getNowPlayingClient() { bool gotNowPlaying = false; RetainPtr 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]; #if PLATFORM(IOS_FAMILY) [_configuration setAllowsInlineMediaPlayback:YES]; [_configuration _setInlineMediaPlaybackRequiresPlaysInlineAttribute:NO]; #endif _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(NSString *name) { [_webView synchronouslyLoadTestPageNamed:name]; } void runScriptWithUserGesture(NSString *script) { bool complete = false; [_webView evaluateJavaScript:script completionHandler:[&] (id, NSError *) { complete = true; }]; TestWebKitAPI::Util::run(&complete); } void runScriptWithoutUserGesture(NSString *script) { bool complete = false; [_webView _evaluateJavaScriptWithoutUserGesture:script completionHandler:[&] (id, NSError *) { complete = true; }]; TestWebKitAPI::Util::run(&complete); } void executeAndWaitForPlaying(Function&& callback) { bool isPlaying = false; [_webView performAfterReceivingMessage:@"playing" action:[&] { isPlaying = true; }]; callback(); TestWebKitAPI::Util::run(&isPlaying); } void executeAndWaitForWebViewToBecomeNowPlaying(Function&& 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; static ObserverSet& observers() { static NeverDestroyed observers { }; return observers.get(); } static void addObserver(NowPlayingTest& test) { observers().add(&test); if (observers().size() == 1) { CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), nullptr, ¬ificationCallback, 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; void performAfterReceivingNotification(CFNotificationName name, NotificationCallback&& callback) { _callbacks.add(name, WTFMove(callback)); } RetainPtr _configuration; RetainPtr _webView; HashMap _callbacks; }; TEST_F(NowPlayingTest, DISABLED_AudioElement) { executeAndWaitForWebViewToBecomeNowPlaying([&] { executeAndWaitForPlaying([&] { loadPage(@"now-playing"); runScriptWithoutUserGesture(@"createMediaElement({type:'audio', hasAudio:true})"); runScriptWithUserGesture(@"play()"); }); }); } TEST_F(NowPlayingTest, DISABLED_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, DISABLED_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