blob: 9c3e96856fece020b0ced984296742bfd4628732 [file] [log] [blame]
/*
* Copyright (C) 2020-2022 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"
#import "AVRoutePickerViewTargetPicker.h"
#if ENABLE(WIRELESS_PLAYBACK_TARGET) && HAVE(AVROUTEPICKERVIEW)
#import "FloatRect.h"
#import "Logging.h"
#import <AVFoundation/AVRouteDetector.h>
#import <pal/spi/cocoa/AVFoundationSPI.h>
#import <pal/spi/cocoa/AVKitSPI.h>
#import <wtf/MainThread.h>
#import <pal/cf/CoreMediaSoftLink.h>
#import <pal/cocoa/AVFoundationSoftLink.h>
SOFTLINK_AVKIT_FRAMEWORK()
SOFT_LINK_CLASS_OPTIONAL(AVKit, AVRoutePickerView)
using namespace WebCore;
@interface WebAVRoutePickerViewHelper : NSObject <AVRoutePickerViewDelegate> {
WeakPtr<AVRoutePickerViewTargetPicker> m_callback;
}
- (instancetype)initWithCallback:(WeakPtr<AVRoutePickerViewTargetPicker>&&)callback;
- (void)clearCallback;
- (void)notificationHandler:(NSNotification *)notification;
- (void)routePickerViewDidEndPresentingRoutes:(AVRoutePickerView *)routePickerView;
@end
namespace WebCore {
bool AVRoutePickerViewTargetPicker::isAvailable()
{
static bool available;
static std::once_flag flag;
std::call_once(flag, [] () {
if (!getAVRoutePickerViewClass())
return;
if (auto picker = adoptNS([allocAVRoutePickerViewInstance() init]))
available = [picker respondsToSelector:@selector(showRoutePickingControlsForOutputContext:relativeToRect:ofView:)];
});
return available;
}
AVRoutePickerViewTargetPicker::AVRoutePickerViewTargetPicker(AVPlaybackTargetPicker::Client& client)
: AVPlaybackTargetPicker(client)
, m_routePickerViewDelegate(adoptNS([[WebAVRoutePickerViewHelper alloc] initWithCallback:*this]))
{
ASSERT(isAvailable());
}
AVRoutePickerViewTargetPicker::~AVRoutePickerViewTargetPicker()
{
[m_routePickerViewDelegate clearCallback];
}
AVOutputContext * AVRoutePickerViewTargetPicker::outputContextInternal()
{
if (!m_outputContext) {
m_outputContext = [PAL::getAVOutputContextClass() iTunesAudioContext];
ASSERT(m_outputContext);
if (m_outputContext)
[[NSNotificationCenter defaultCenter] addObserver:m_routePickerViewDelegate.get() selector:@selector(notificationHandler:) name:PAL::AVOutputContextOutputDevicesDidChangeNotification object:m_outputContext.get()];
}
return m_outputContext.get();
}
AVRoutePickerView *AVRoutePickerViewTargetPicker::devicePicker()
{
if (!m_routePickerView) {
m_routePickerView = adoptNS([allocAVRoutePickerViewInstance() init]);
[m_routePickerView setDelegate:m_routePickerViewDelegate.get()];
}
return m_routePickerView.get();
}
AVRouteDetector *AVRoutePickerViewTargetPicker::routeDetector()
{
if (!m_routeDetector) {
m_routeDetector = adoptNS([PAL::allocAVRouteDetectorInstance() init]);
[[NSNotificationCenter defaultCenter] addObserver:m_routePickerViewDelegate.get() selector:@selector(notificationHandler:) name:PAL::AVRouteDetectorMultipleRoutesDetectedDidChangeNotification object:m_routeDetector.get()];
if ([m_routeDetector multipleRoutesDetected])
availableDevicesDidChange();
}
return m_routeDetector.get();
}
void AVRoutePickerViewTargetPicker::showPlaybackTargetPicker(NSView *view, const FloatRect& rectInScreenCoordinates, bool hasActiveRoute, bool useDarkAppearance)
{
if (!client())
return;
auto *picker = devicePicker();
if (useDarkAppearance)
picker.routeListAlwaysHasDarkAppearance = YES;
m_hadActiveRoute = hasActiveRoute;
auto rectInWindowCoordinates = [view.window convertRectFromScreen:NSMakeRect(rectInScreenCoordinates.x(), rectInScreenCoordinates.y(), 1.0, 1.0)];
auto rectInViewCoordinates = [view convertRect:rectInWindowCoordinates fromView:view];
[picker showRoutePickingControlsForOutputContext:outputContextInternal() relativeToRect:rectInViewCoordinates ofView:view];
}
void AVRoutePickerViewTargetPicker::startingMonitoringPlaybackTargets()
{
m_ignoreNextMultipleRoutesDetectedDidChangeNotification = false;
routeDetector().routeDetectionEnabled = YES;
}
void AVRoutePickerViewTargetPicker::stopMonitoringPlaybackTargets()
{
if (!m_routeDetector)
return;
// `-[AVRouteDetector multipleRoutesDetected]` will always return `NO` if route detection is
// disabled and `-[AVRouteDetector setRouteDetectionEnabled:]` will always dispatch a
// `AVRouteDetectorMultipleRoutesDetectedDidChange` notification, so ignore the next one in
// order to prevent the cached value in the WebProcess from always being `false` when the last
// JS `"webkitplaybacktargetavailabilitychanged"` event listener is removed.
m_ignoreNextMultipleRoutesDetectedDidChangeNotification = true;
[m_routeDetector setRouteDetectionEnabled:NO];
}
bool AVRoutePickerViewTargetPicker::externalOutputDeviceAvailable()
{
return routeDetector().multipleRoutesDetected;
}
AVOutputContext * AVRoutePickerViewTargetPicker::outputContext()
{
return m_outputContext.get();
}
void AVRoutePickerViewTargetPicker::invalidatePlaybackTargets()
{
if (m_routeDetector) {
[[NSNotificationCenter defaultCenter] removeObserver:m_routePickerViewDelegate.get() name:PAL::AVRouteDetectorMultipleRoutesDetectedDidChangeNotification object:m_routeDetector.get()];
[m_routeDetector setRouteDetectionEnabled:NO];
m_routePickerView = nullptr;
}
if (m_outputContext) {
[[NSNotificationCenter defaultCenter] removeObserver:m_routePickerViewDelegate.get() name:PAL::AVOutputContextOutputDevicesDidChangeNotification object:m_outputContext.get()];
m_outputContext = nullptr;
}
if (m_routePickerView) {
[m_routePickerView setDelegate:nil];
m_routePickerView = nullptr;
}
currentDeviceDidChange();
}
void AVRoutePickerViewTargetPicker::availableDevicesDidChange()
{
if (m_ignoreNextMultipleRoutesDetectedDidChangeNotification) {
m_ignoreNextMultipleRoutesDetectedDidChangeNotification = false;
return;
}
if (client())
client()->availableDevicesChanged();
}
bool AVRoutePickerViewTargetPicker::hasActiveRoute() const
{
if (!m_outputContext)
return false;
if ([m_outputContext respondsToSelector:@selector(supportsMultipleOutputDevices)] && [m_outputContext respondsToSelector:@selector(outputDevices)]&& [m_outputContext supportsMultipleOutputDevices]) {
for (AVOutputDevice *outputDevice in [m_outputContext outputDevices]) {
if (outputDevice.deviceFeatures & (AVOutputDeviceFeatureVideo | AVOutputDeviceFeatureAudio))
return true;
}
return false;
}
if ([m_outputContext respondsToSelector:@selector(outputDevice)]) {
if (auto *outputDevice = [m_outputContext outputDevice])
return outputDevice.deviceFeatures & (AVOutputDeviceFeatureVideo | AVOutputDeviceFeatureAudio);
}
return [m_outputContext deviceName];
}
void AVRoutePickerViewTargetPicker::currentDeviceDidChange()
{
auto haveActiveRoute = hasActiveRoute();
if (!client() || m_hadActiveRoute == haveActiveRoute)
return;
m_hadActiveRoute = haveActiveRoute;
client()->currentDeviceChanged();
}
void AVRoutePickerViewTargetPicker::devicePickerWasDismissed()
{
if (!client())
return;
client()->pickerWasDismissed();
currentDeviceDidChange();
}
} // namespace WebCore
@implementation WebAVRoutePickerViewHelper
- (instancetype)initWithCallback:(WeakPtr<AVRoutePickerViewTargetPicker>&&)callback
{
if (!(self = [super init]))
return nil;
m_callback = WTFMove(callback);
return self;
}
- (void)clearCallback
{
m_callback = nil;
}
- (void)routePickerViewDidEndPresentingRoutes:(AVRoutePickerView *)routePickerView
{
UNUSED_PARAM(routePickerView);
if (!m_callback)
return;
callOnMainThread([self, protectedSelf = retainPtr(self)] {
if (!m_callback)
return;
m_callback->devicePickerWasDismissed();
});
}
- (void)notificationHandler:(NSNotification *)notification
{
UNUSED_PARAM(notification);
if (!m_callback)
return;
callOnMainThread([self, protectedSelf = retainPtr(self), notification = retainPtr(notification)] {
if (!m_callback)
return;
if ([[notification name] isEqualToString:PAL::AVOutputContextOutputDevicesDidChangeNotification])
m_callback->currentDeviceDidChange();
else if ([[notification name] isEqualToString:PAL::AVRouteDetectorMultipleRoutesDetectedDidChangeNotification])
m_callback->availableDevicesDidChange();
});
}
@end
#endif // ENABLE(WIRELESS_PLAYBACK_TARGET)