faf5b44db7d3a97da9d27e27da949be98332e449
[WebKit-https.git] / Source / WebCore / platform / audio / ios / MediaSessionManagerIOS.mm
1 /*
2  * Copyright (C) 2014-2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "MediaSessionManagerIOS.h"
28
29 #if PLATFORM(IOS_FAMILY)
30
31 #import "Logging.h"
32 #import "MediaPlayer.h"
33 #import "PlatformMediaSession.h"
34 #import "RuntimeApplicationChecks.h"
35 #import "SystemMemory.h"
36 #import "WebCoreThreadRun.h"
37 #import <AVFoundation/AVAudioSession.h>
38 #import <AVFoundation/AVRouteDetector.h>
39 #import <objc/runtime.h>
40 #import <pal/ios/UIKitSoftLink.h>
41 #import <pal/spi/ios/CelestialSPI.h>
42 #import <pal/spi/ios/UIKitSPI.h>
43 #import <wtf/BlockObjCExceptions.h>
44 #import <wtf/MainThread.h>
45 #import <wtf/RAMSize.h>
46 #import <wtf/RetainPtr.h>
47
48 #import <pal/cocoa/AVFoundationSoftLink.h>
49
50 WEBCORE_EXPORT NSString* WebUIApplicationWillResignActiveNotification = @"WebUIApplicationWillResignActiveNotification";
51 WEBCORE_EXPORT NSString* WebUIApplicationWillEnterForegroundNotification = @"WebUIApplicationWillEnterForegroundNotification";
52 WEBCORE_EXPORT NSString* WebUIApplicationDidBecomeActiveNotification = @"WebUIApplicationDidBecomeActiveNotification";
53 WEBCORE_EXPORT NSString* WebUIApplicationDidEnterBackgroundNotification = @"WebUIApplicationDidEnterBackgroundNotification";
54
55 #if HAVE(CELESTIAL)
56 SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(Celestial)
57 SOFT_LINK_CLASS_OPTIONAL(Celestial, AVSystemController)
58 SOFT_LINK_CONSTANT_MAY_FAIL(Celestial, AVSystemController_PIDToInheritApplicationStateFrom, NSString *)
59 #endif
60
61 using namespace WebCore;
62
63 @interface WebMediaSessionHelper : NSObject {
64     MediaSessionManageriOS* _callback;
65
66 #if HAVE(MEDIA_PLAYER) && !PLATFORM(WATCHOS)
67     RetainPtr<AVRouteDetector> _routeDetector;
68 #endif
69     bool _monitoringAirPlayRoutes;
70     bool _startMonitoringAirPlayRoutesPending;
71 }
72
73 - (id)initWithCallback:(MediaSessionManageriOS*)callback;
74
75 - (void)clearCallback;
76 - (void)interruption:(NSNotification *)notification;
77 - (void)applicationWillEnterForeground:(NSNotification *)notification;
78 - (void)applicationWillResignActive:(NSNotification *)notification;
79 - (void)applicationDidEnterBackground:(NSNotification *)notification;
80 - (BOOL)hasWirelessTargetsAvailable;
81
82 #if HAVE(MEDIA_PLAYER) && !PLATFORM(WATCHOS)
83 - (void)startMonitoringAirPlayRoutes;
84 - (void)stopMonitoringAirPlayRoutes;
85 #endif
86
87 @end
88
89 namespace WebCore {
90
91 static MediaSessionManageriOS* platformMediaSessionManager = nullptr;
92
93 PlatformMediaSessionManager& PlatformMediaSessionManager::sharedManager()
94 {
95     if (!platformMediaSessionManager)
96         platformMediaSessionManager = new MediaSessionManageriOS;
97     return *platformMediaSessionManager;
98 }
99
100 PlatformMediaSessionManager* PlatformMediaSessionManager::sharedManagerIfExists()
101 {
102     return platformMediaSessionManager;
103 }
104
105 MediaSessionManageriOS::MediaSessionManageriOS()
106     : MediaSessionManagerCocoa()
107 {
108     BEGIN_BLOCK_OBJC_EXCEPTIONS
109     m_objcObserver = adoptNS([[WebMediaSessionHelper alloc] initWithCallback:this]);
110     END_BLOCK_OBJC_EXCEPTIONS
111     resetRestrictions();
112 }
113
114 MediaSessionManageriOS::~MediaSessionManageriOS()
115 {
116     BEGIN_BLOCK_OBJC_EXCEPTIONS
117     [m_objcObserver clearCallback];
118     m_objcObserver = nil;
119     END_BLOCK_OBJC_EXCEPTIONS
120 }
121
122 void MediaSessionManageriOS::resetRestrictions()
123 {
124     static const size_t systemMemoryRequiredForVideoInBackgroundTabs = 1024 * 1024 * 1024;
125
126     ALWAYS_LOG(LOGIDENTIFIER);
127
128     PlatformMediaSessionManager::resetRestrictions();
129
130     if (ramSize() < systemMemoryRequiredForVideoInBackgroundTabs) {
131         ALWAYS_LOG(LOGIDENTIFIER, "restricting video in background tabs because system memory = ", ramSize());
132         addRestriction(PlatformMediaSession::Video, BackgroundTabPlaybackRestricted);
133     }
134
135     addRestriction(PlatformMediaSession::Video, BackgroundProcessPlaybackRestricted);
136     addRestriction(PlatformMediaSession::VideoAudio, ConcurrentPlaybackNotPermitted | BackgroundProcessPlaybackRestricted | SuspendedUnderLockPlaybackRestricted);
137 }
138
139 bool MediaSessionManageriOS::hasWirelessTargetsAvailable()
140 {
141     BEGIN_BLOCK_OBJC_EXCEPTIONS
142     return [m_objcObserver hasWirelessTargetsAvailable];
143     END_BLOCK_OBJC_EXCEPTIONS
144 }
145
146 void MediaSessionManageriOS::configureWireLessTargetMonitoring()
147 {
148 #if HAVE(MEDIA_PLAYER) && !PLATFORM(WATCHOS)
149     bool requiresMonitoring = anyOfSessions([] (PlatformMediaSession& session, size_t) {
150         return session.requiresPlaybackTargetRouteMonitoring();
151     });
152
153     ALWAYS_LOG(LOGIDENTIFIER, "requiresMonitoring = ", requiresMonitoring);
154
155     BEGIN_BLOCK_OBJC_EXCEPTIONS
156
157     if (requiresMonitoring)
158         [m_objcObserver startMonitoringAirPlayRoutes];
159     else
160         [m_objcObserver stopMonitoringAirPlayRoutes];
161
162     END_BLOCK_OBJC_EXCEPTIONS
163 #endif
164 }
165
166 void MediaSessionManageriOS::providePresentingApplicationPIDIfNecessary()
167 {
168 #if HAVE(CELESTIAL)
169     if (m_havePresentedApplicationPID)
170         return;
171     m_havePresentedApplicationPID = true;
172
173     if (!canLoadAVSystemController_PIDToInheritApplicationStateFrom())
174         return;
175
176     NSError *error = nil;
177     [[getAVSystemControllerClass() sharedAVSystemController] setAttribute:@(presentingApplicationPID()) forKey:getAVSystemController_PIDToInheritApplicationStateFrom() error:&error];
178     if (error)
179         WTFLogAlways("Failed to set up PID proxying: %s", error.localizedDescription.UTF8String);
180 #endif
181 }
182
183 void MediaSessionManageriOS::externalOutputDeviceAvailableDidChange()
184 {
185     BEGIN_BLOCK_OBJC_EXCEPTIONS
186     forEachSession([haveTargets = [m_objcObserver hasWirelessTargetsAvailable]] (PlatformMediaSession& session, size_t) {
187         session.externalOutputDeviceAvailableDidChange(haveTargets);
188     });
189     END_BLOCK_OBJC_EXCEPTIONS
190 }
191
192 } // namespace WebCore
193
194 @implementation WebMediaSessionHelper
195
196 - (id)initWithCallback:(MediaSessionManageriOS*)callback
197 {
198     LOG(Media, "-[WebMediaSessionHelper initWithCallback]");
199
200     if (!(self = [super init]))
201         return nil;
202     
203     _callback = callback;
204
205     NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
206     [center addObserver:self selector:@selector(interruption:) name:AVAudioSessionInterruptionNotification object:[PAL::getAVAudioSessionClass() sharedInstance]];
207
208     [center addObserver:self selector:@selector(applicationWillEnterForeground:) name:PAL::get_UIKit_UIApplicationWillEnterForegroundNotification() object:nil];
209     [center addObserver:self selector:@selector(applicationWillEnterForeground:) name:WebUIApplicationWillEnterForegroundNotification object:nil];
210     [center addObserver:self selector:@selector(applicationDidBecomeActive:) name:PAL::get_UIKit_UIApplicationDidBecomeActiveNotification() object:nil];
211     [center addObserver:self selector:@selector(applicationDidBecomeActive:) name:WebUIApplicationDidBecomeActiveNotification object:nil];
212     [center addObserver:self selector:@selector(applicationWillResignActive:) name:PAL::get_UIKit_UIApplicationWillResignActiveNotification() object:nil];
213     [center addObserver:self selector:@selector(applicationWillResignActive:) name:WebUIApplicationWillResignActiveNotification object:nil];
214     [center addObserver:self selector:@selector(applicationDidEnterBackground:) name:PAL::get_UIKit_UIApplicationDidEnterBackgroundNotification() object:nil];
215     [center addObserver:self selector:@selector(applicationDidEnterBackground:) name:WebUIApplicationDidEnterBackgroundNotification object:nil];
216
217     // Now playing won't work unless we turn on the delivery of remote control events.
218     dispatch_async(dispatch_get_main_queue(), ^ {
219         BEGIN_BLOCK_OBJC_EXCEPTIONS
220         [[PAL::getUIApplicationClass() sharedApplication] beginReceivingRemoteControlEvents];
221         END_BLOCK_OBJC_EXCEPTIONS
222     });
223
224     return self;
225 }
226
227 - (void)dealloc
228 {
229     LOG(Media, "-[WebMediaSessionHelper dealloc]");
230
231 #if HAVE(MEDIA_PLAYER) && !PLATFORM(WATCHOS)
232     if (!pthread_main_np()) {
233         dispatch_async(dispatch_get_main_queue(), [routeDetector = WTFMove(_routeDetector)] () mutable {
234             LOG(Media, "safelyTearDown - dipatched to UI thread.");
235             BEGIN_BLOCK_OBJC_EXCEPTIONS
236             routeDetector.get().routeDetectionEnabled = NO;
237             routeDetector.clear();
238             END_BLOCK_OBJC_EXCEPTIONS
239         });
240     } else
241         _routeDetector.get().routeDetectionEnabled = NO;
242 #endif
243
244     [[NSNotificationCenter defaultCenter] removeObserver:self];
245     [super dealloc];
246 }
247
248 - (void)clearCallback
249 {
250     LOG(Media, "-[WebMediaSessionHelper clearCallback]");
251     _callback = nil;
252 }
253
254 - (BOOL)hasWirelessTargetsAvailable
255 {
256     LOG(Media, "-[WebMediaSessionHelper hasWirelessTargetsAvailable]");
257 #if HAVE(MEDIA_PLAYER) && !PLATFORM(WATCHOS)
258     return _routeDetector.get().multipleRoutesDetected;
259 #else
260     return NO;
261 #endif
262 }
263
264 #if HAVE(MEDIA_PLAYER) && !PLATFORM(WATCHOS)
265 - (void)startMonitoringAirPlayRoutes
266 {
267     if (_monitoringAirPlayRoutes)
268         return;
269
270     _monitoringAirPlayRoutes = true;
271
272     if (_startMonitoringAirPlayRoutesPending)
273         return;
274
275     if (_routeDetector) {
276         _routeDetector.get().routeDetectionEnabled = YES;
277         return;
278     }
279
280     _startMonitoringAirPlayRoutesPending = true;
281
282     LOG(Media, "-[WebMediaSessionHelper startMonitoringAirPlayRoutes]");
283
284     callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = WTFMove(self)]() mutable {
285         ASSERT(!protectedSelf->_routeDetector);
286
287         if (protectedSelf->_callback) {
288             BEGIN_BLOCK_OBJC_EXCEPTIONS
289             protectedSelf->_routeDetector = adoptNS([PAL::allocAVRouteDetectorInstance() init]);
290             protectedSelf->_routeDetector.get().routeDetectionEnabled = protectedSelf->_monitoringAirPlayRoutes;
291             [[NSNotificationCenter defaultCenter] addObserver:protectedSelf selector:@selector(wirelessRoutesAvailableDidChange:) name:AVRouteDetectorMultipleRoutesDetectedDidChangeNotification object:protectedSelf->_routeDetector.get()];
292             END_BLOCK_OBJC_EXCEPTIONS
293         }
294
295         protectedSelf->_startMonitoringAirPlayRoutesPending = false;
296     });
297 }
298
299 - (void)stopMonitoringAirPlayRoutes
300 {
301     if (!_monitoringAirPlayRoutes)
302         return;
303
304     LOG(Media, "-[WebMediaSessionHelper stopMonitoringAirPlayRoutes]");
305
306     _monitoringAirPlayRoutes = false;
307     _routeDetector.get().routeDetectionEnabled = NO;
308 }
309 #endif // HAVE(MEDIA_PLAYER) && !PLATFORM(WATCHOS)
310
311 - (void)interruption:(NSNotification *)notification
312 {
313     if (!_callback || _callback->willIgnoreSystemInterruptions())
314         return;
315
316     NSUInteger type = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
317     PlatformMediaSession::EndInterruptionFlags flags = PlatformMediaSession::NoFlags;
318
319     LOG(Media, "-[WebMediaSessionHelper interruption] - type = %i", (int)type);
320
321     if (type == AVAudioSessionInterruptionTypeEnded && [[[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey] unsignedIntegerValue] == AVAudioSessionInterruptionOptionShouldResume)
322         flags = PlatformMediaSession::MayResumePlaying;
323
324     callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = WTFMove(self), type, flags]() mutable {
325         auto* callback = protectedSelf->_callback;
326         if (!callback)
327             return;
328
329         if (type == AVAudioSessionInterruptionTypeBegan)
330             callback->beginInterruption(PlatformMediaSession::SystemInterruption);
331         else
332             callback->endInterruption(flags);
333
334     });
335 }
336
337 - (void)applicationWillEnterForeground:(NSNotification *)notification
338 {
339     UNUSED_PARAM(notification);
340
341     if (!_callback || _callback->willIgnoreSystemInterruptions())
342         return;
343
344     LOG(Media, "-[WebMediaSessionHelper applicationWillEnterForeground]");
345
346     BOOL isSuspendedUnderLock = [[[notification userInfo] objectForKey:@"isSuspendedUnderLock"] boolValue];
347     callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = WTFMove(self), isSuspendedUnderLock]() mutable {
348         if (auto* callback = protectedSelf->_callback)
349             callback->applicationWillEnterForeground(isSuspendedUnderLock);
350     });
351 }
352
353 - (void)applicationDidBecomeActive:(NSNotification *)notification
354 {
355     UNUSED_PARAM(notification);
356
357     if (!_callback || _callback->willIgnoreSystemInterruptions())
358         return;
359
360     LOG(Media, "-[WebMediaSessionHelper applicationDidBecomeActive]");
361
362     callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = WTFMove(self)]() mutable {
363         if (auto* callback = protectedSelf->_callback)
364             callback->applicationDidBecomeActive();
365     });
366 }
367
368 - (void)applicationWillResignActive:(NSNotification *)notification
369 {
370     UNUSED_PARAM(notification);
371
372     if (!_callback || _callback->willIgnoreSystemInterruptions())
373         return;
374
375     LOG(Media, "-[WebMediaSessionHelper applicationWillResignActive]");
376
377     callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = WTFMove(self)]() mutable {
378         if (auto* callback = protectedSelf->_callback)
379             callback->applicationWillBecomeInactive();
380     });
381 }
382
383 - (void)wirelessRoutesAvailableDidChange:(NSNotification *)notification
384 {
385     UNUSED_PARAM(notification);
386
387     if (!_callback || !_monitoringAirPlayRoutes)
388         return;
389
390     LOG(Media, "-[WebMediaSessionHelper wirelessRoutesAvailableDidChange]");
391
392     callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = WTFMove(self)]() mutable {
393         if (auto* callback = protectedSelf->_callback)
394             callback->externalOutputDeviceAvailableDidChange();
395     });
396 }
397
398 - (void)applicationDidEnterBackground:(NSNotification *)notification
399 {
400     if (!_callback || _callback->willIgnoreSystemInterruptions())
401         return;
402
403     LOG(Media, "-[WebMediaSessionHelper applicationDidEnterBackground]");
404
405     BOOL isSuspendedUnderLock = [[[notification userInfo] objectForKey:@"isSuspendedUnderLock"] boolValue];
406     callOnWebThreadOrDispatchAsyncOnMainThread([protectedSelf = WTFMove(self), isSuspendedUnderLock]() mutable {
407         if (auto* callback = protectedSelf->_callback)
408             callback->applicationDidEnterBackground(isSuspendedUnderLock);
409     });
410 }
411 @end
412
413 #endif // PLATFORM(IOS_FAMILY)