blob: 8dee3f4252c15b1a00fca55e13f1ed3c2bf130f0 [file] [log] [blame]
/*
* Copyright (C) 2020 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(MAC) && ENABLE(MEDIA_SESSION)
#import "PlatformUtilities.h"
#import "Test.h"
#import "TestWKWebView.h"
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WebViewPrivate.h>
#import <pal/spi/mac/MediaRemoteSPI.h>
#import <wtf/Function.h>
#import <wtf/HashSet.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/SoftLinking.h>
#import <wtf/text/StringHash.h>
#import <wtf/text/WTFString.h>
SOFT_LINK_PRIVATE_FRAMEWORK(MediaRemote)
SOFT_LINK(MediaRemote, MRMediaRemoteSendCommandToApp, Boolean, (MRMediaRemoteCommand command, CFDictionaryRef options, MROriginRef origin, CFStringRef appDisplayID, MRSendCommandAppOptions appOptions, dispatch_queue_t replyQ, void(^completion)(MRSendCommandError err, CFArrayRef handlerReturnStatuses)), (command, options, origin, appDisplayID, appOptions, replyQ, completion));
#define MRMediaRemoteSendCommandToApp softLinkMRMediaRemoteSendCommandToApp
SOFT_LINK(MediaRemote, MRMediaRemoteGetLocalOrigin, MROriginRef, (), ());
#define MRMediaRemoteGetLocalOrigin softLinkMRMediaRemoteGetLocalOrigin
SOFT_LINK(MediaRemote, MRMediaRemoteGetSupportedCommandsForOrigin, void, (MROriginRef origin, dispatch_queue_t queue, void(^completion)(CFArrayRef commands)), (origin, queue, completion));
#define MRMediaRemoteGetSupportedCommandsForOrigin softLinkMRMediaRemoteGetSupportedCommandsForOrigin
SOFT_LINK(MediaRemote, MRMediaRemoteGetNowPlayingClient, void, (dispatch_queue_t queue, void(^completion)(MRNowPlayingClientRef, CFErrorRef)), (queue, completion))
#define MRMediaRemoteGetNowPlayingClient softLinkMRMediaRemoteGetNowPlayingClient
SOFT_LINK(MediaRemote, MRNowPlayingClientGetProcessIdentifier, pid_t, (MRNowPlayingClientRef client), (client))
#define MRNowPlayingClientGetProcessIdentifier softLinkMRNowPlayingClientGetProcessIdentifier
SOFT_LINK_CONSTANT(MediaRemote, kMRMediaRemoteOptionSkipInterval, CFStringRef)
#define kMRMediaRemoteOptionSkipInterval getkMRMediaRemoteOptionSkipInterval()
SOFT_LINK_CONSTANT(MediaRemote, kMRMediaRemoteOptionPlaybackPosition, CFStringRef)
#define kMRMediaRemoteOptionPlaybackPosition getkMRMediaRemoteOptionPlaybackPosition()
#if !USE(APPLE_INTERNAL_SDK)
@interface MRCommandInfo : NSObject
@property (nonatomic, readonly) MRMediaRemoteCommand command;
@property (nonatomic, readonly, getter=isEnabled) BOOL enabled;
@property (nonatomic, readonly, nullable, copy) NSDictionary *options;
@end
#endif
namespace TestWebKitAPI {
class MediaSessionTest : public testing::Test {
public:
void SetUp() final
{
_configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[_configuration setMediaTypesRequiringUserActionForPlayback:WKAudiovisualMediaTypeAudio];
_webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:_configuration.get() addToWindow:YES]);
WKPreferences *preferences = [_webView configuration].preferences;
preferences._mediaSessionEnabled = YES;
_messageHandlers = adoptNS([[NSMutableArray alloc] init]);
}
void TearDown() override
{
[_webView clearMessageHandlers:_messageHandlers.get()];
}
TestWKWebView* webView() { return _webView.get(); }
pid_t webViewPid() { return [_webView _webProcessIdentifier]; }
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;
}
pid_t getNowPlayingClientPid()
{
return MRNowPlayingClientGetProcessIdentifier(getNowPlayingClient().get());
}
void loadPageAndBecomeNowPlaying(const String& pageName)
{
[_webView synchronouslyLoadTestPageNamed:pageName];
bool canplaythrough = false;
[webView() performAfterReceivingMessage:@"canplaythrough event" action:[&] {
canplaythrough = true;
}];
runScriptWithUserGesture("load()");
Util::run(&canplaythrough);
play();
pause();
ASSERT_EQ(webViewPid(), getNowPlayingClientPid());
}
void runScriptWithUserGesture(const String& script)
{
bool complete = false;
[_webView evaluateJavaScript:script completionHandler:[&] (id, NSError *) { complete = true; }];
TestWebKitAPI::Util::run(&complete);
}
void play()
{
bool playing = false;
[_webView performAfterReceivingMessage:@"play event" action:[&] { playing = true; }];
runScriptWithUserGesture("audio.play()");
Util::run(&playing);
}
void pause()
{
bool paused = false;
[_webView performAfterReceivingMessage:@"pause event" action:[&] { paused = true; }];
runScriptWithUserGesture("audio.pause()");
Util::run(&paused);
}
bool sendMediaRemoteCommand(MRMediaRemoteCommand command, CFDictionaryRef options = nullptr)
{
bool completed = false;
bool success;
MRMediaRemoteSendCommandToApp(command, options, NULL, NULL, static_cast<MRSendCommandAppOptions>(0), NULL, [&] (MRSendCommandError error, CFArrayRef) {
success = !error;
completed = true;
});
TestWebKitAPI::Util::run(&completed);
return success;
}
bool sendMediaRemoteSeekCommand(MRMediaRemoteCommand command, double interval)
{
CFStringRef seekInterval = (command == MRMediaRemoteCommandSeekToPlaybackPosition) ? kMRMediaRemoteOptionPlaybackPosition : kMRMediaRemoteOptionSkipInterval;
NSDictionary *options = @{(__bridge NSString *)seekInterval : @(interval)};
return sendMediaRemoteCommand(command, (__bridge CFDictionaryRef)options);
}
void listenForEventMessages(std::initializer_list<const char*> events)
{
for (auto* event : events) {
auto eventMessage = makeString(event, " event");
[_messageHandlers addObject:eventMessage];
[webView() performAfterReceivingMessage:eventMessage action:[this, eventMessage = WTFMove(eventMessage)] {
_eventListenersCalled.add(eventMessage);
}];
}
}
bool eventListenerWasCalled(const String& event)
{
return _eventListenersCalled.contains(makeString(event, " event"));
}
void clearEventListenerState()
{
_eventListenersCalled.clear();
}
void waitForEventListenerToBeCalled(const String& event)
{
int tries = 0;
do {
if (eventListenerWasCalled(event))
return;
Util::sleep(0.1);
} while (++tries <= 50);
return;
}
void listenForSessionHandlerMessages(std::initializer_list<const char*> handlers)
{
for (auto* handler : handlers) {
auto handlerMessage = makeString(handler, " handler");
[_messageHandlers addObject:handlerMessage];
[webView() performAfterReceivingMessage:handlerMessage action:[this, handlerMessage = WTFMove(handlerMessage)] {
_mediaSessionHandlersCalled.add(handlerMessage);
}];
}
}
bool sessionHandlerWasCalled(const String& handler)
{
return _mediaSessionHandlersCalled.contains(makeString(handler, " handler"));
}
void waitForSessionHandlerToBeCalled(const String& handler)
{
int tries = 0;
do {
if (sessionHandlerWasCalled(handler))
return;
Util::sleep(0.1);
} while (++tries <= 50);
return;
}
RetainPtr<NSArray> getSupportedCommands()
{
bool completed = false;
RetainPtr<NSArray> result;
MRMediaRemoteGetSupportedCommandsForOrigin(MRMediaRemoteGetLocalOrigin(), dispatch_get_main_queue(), [&] (CFArrayRef commands) {
result = (__bridge NSArray *)commands;
completed = true;
});
TestWebKitAPI::Util::run(&completed);
return result;
}
private:
RetainPtr<WKWebViewConfiguration> _configuration;
RetainPtr<TestWKWebView> _webView;
HashSet<String> _mediaSessionHandlersCalled;
HashSet<String> _eventListenersCalled;
RetainPtr<NSMutableArray> _messageHandlers;
};
TEST_F(MediaSessionTest, DISABLED_OnlyOneHandler)
{
loadPageAndBecomeNowPlaying("media-remote");
[webView() objectByEvaluatingJavaScript:@"setEmptyActionHandlers([ 'play' ])"];
listenForSessionHandlerMessages({ "play", "pause", "seekto", "seekforward", "seekbackward", "previoustrack", "nexttrack" });
listenForEventMessages({ "play", "pause", "seeked" });
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101500
static Vector<MRMediaRemoteCommand> registeredCommands = { MRMediaRemoteCommandPlay };
auto currentCommands = getSupportedCommands();
for (MRCommandInfo *command in currentCommands.get()) {
if (!command.enabled)
continue;
ASSERT_TRUE(registeredCommands.contains(command.command));
}
#endif
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandPlay));
waitForSessionHandlerToBeCalled("play");
ASSERT_TRUE(sessionHandlerWasCalled("play"));
ASSERT_FALSE(eventListenerWasCalled("play"));
// The media session only registered for Play, but no other commands should reach HTMLMediaElement.
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSkipForward, 1));
ASSERT_FALSE(sessionHandlerWasCalled("seekforward"));
ASSERT_FALSE(eventListenerWasCalled("seeked"));
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSkipBackward, 10));
ASSERT_FALSE(sessionHandlerWasCalled("seekbackward"));
ASSERT_FALSE(eventListenerWasCalled("seeked"));
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSeekToPlaybackPosition, 6));
ASSERT_FALSE(sessionHandlerWasCalled("seekto"));
ASSERT_FALSE(eventListenerWasCalled("seeked"));
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandNextTrack));
ASSERT_FALSE(sessionHandlerWasCalled("nexttrack"));
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandPreviousTrack));
ASSERT_FALSE(sessionHandlerWasCalled("previoustrack"));
}
TEST_F(MediaSessionTest, DISABLED_RemoteCommands)
{
loadPageAndBecomeNowPlaying("media-remote");
[webView() objectByEvaluatingJavaScript:@"setEmptyActionHandlers([ 'play', 'pause', 'seekto', 'seekforward', 'seekbackward', 'previoustrack', 'nexttrack' ])"];
listenForSessionHandlerMessages({ "play", "pause", "seekto", "seekforward", "seekbackward", "previoustrack", "nexttrack" });
listenForEventMessages({ "play", "pause", "seeked" });
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101500
static Vector<MRMediaRemoteCommand> registeredCommands = { MRMediaRemoteCommandPlay, MRMediaRemoteCommandPause, MRMediaRemoteCommandSeekToPlaybackPosition, MRMediaRemoteCommandSkipForward, MRMediaRemoteCommandSkipBackward, MRMediaRemoteCommandPreviousTrack, MRMediaRemoteCommandNextTrack };
auto currentCommands = getSupportedCommands();
for (MRCommandInfo *command in currentCommands.get()) {
if (!command.enabled)
continue;
ASSERT_TRUE(registeredCommands.contains(command.command));
}
#endif
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandPlay));
waitForSessionHandlerToBeCalled("play");
ASSERT_TRUE(sessionHandlerWasCalled("play"));
ASSERT_FALSE(eventListenerWasCalled("play"));
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandPause));
waitForSessionHandlerToBeCalled("pause");
ASSERT_TRUE(sessionHandlerWasCalled("pause"));
ASSERT_FALSE(eventListenerWasCalled("pause"));
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSkipForward, 1));
waitForSessionHandlerToBeCalled("seekforward");
ASSERT_TRUE(sessionHandlerWasCalled("seekforward"));
ASSERT_FALSE(eventListenerWasCalled("seeked"));
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSkipBackward, 10));
waitForSessionHandlerToBeCalled("seekbackward");
ASSERT_TRUE(sessionHandlerWasCalled("seekbackward"));
ASSERT_FALSE(eventListenerWasCalled("seeked"));
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSeekToPlaybackPosition, 6));
waitForSessionHandlerToBeCalled("seekto");
ASSERT_TRUE(sessionHandlerWasCalled("seekto"));
ASSERT_FALSE(eventListenerWasCalled("seeked"));
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandNextTrack));
waitForSessionHandlerToBeCalled("nexttrack");
ASSERT_TRUE(sessionHandlerWasCalled("nexttrack"));
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandPreviousTrack));
waitForSessionHandlerToBeCalled("previoustrack");
ASSERT_TRUE(sessionHandlerWasCalled("previoustrack"));
// Unregister action handlers, supported commands should go to HTMLMediaElement.
[webView() objectByEvaluatingJavaScript:@"clearActionHandlers()"];
clearEventListenerState();
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandPlay));
waitForEventListenerToBeCalled("play");
ASSERT_TRUE(eventListenerWasCalled("play"));
ASSERT_TRUE(sendMediaRemoteCommand(MRMediaRemoteCommandPause));
waitForEventListenerToBeCalled("pause");
ASSERT_TRUE(eventListenerWasCalled("pause"));
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSkipForward, 1));
waitForEventListenerToBeCalled("seeked");
ASSERT_TRUE(eventListenerWasCalled("seeked"));
clearEventListenerState();
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSkipBackward, 10));
waitForEventListenerToBeCalled("seeked");
ASSERT_TRUE(eventListenerWasCalled("seeked"));
clearEventListenerState();
ASSERT_TRUE(sendMediaRemoteSeekCommand(MRMediaRemoteCommandSeekToPlaybackPosition, 6));
waitForEventListenerToBeCalled("seeked");
ASSERT_TRUE(eventListenerWasCalled("seeked"));
}
}
#endif // PLATFORM(MAC) && ENABLE(MEDIA_SESSION)