/* * 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 #import #import #import #import #import SOFTLINK_AVKIT_FRAMEWORK() SOFT_LINK_CLASS_OPTIONAL(AVKit, AVRoutePickerView) using namespace WebCore; @interface WebAVRoutePickerViewHelper : NSObject { WeakPtr m_callback; } - (instancetype)initWithCallback:(WeakPtr&&)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&&)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)