1/*
2 * Copyright (C) 2014 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)
30
31#import "Logging.h"
32#import "MediaPlayer.h"
33#import "MediaSession.h"
34#import "SoftLinking.h"
35#import "WebCoreSystemInterface.h"
36#import "WebCoreThreadRun.h"
37#import <AVFoundation/AVAudioSession.h>
38#import <MediaPlayer/MPAVRoutingController.h>
39#import <MediaPlayer/MPMediaItem.h>
40#import <MediaPlayer/MPNowPlayingInfoCenter.h>
41#import <MediaPlayer/MPVolumeView.h>
42#import <UIKit/UIApplication.h>
43#import <objc/runtime.h>
44#import <wtf/RetainPtr.h>
45
46SOFT_LINK_FRAMEWORK(AVFoundation)
47SOFT_LINK_CLASS(AVFoundation, AVAudioSession)
48SOFT_LINK_POINTER(AVFoundation, AVAudioSessionInterruptionNotification, NSString *)
49SOFT_LINK_POINTER(AVFoundation, AVAudioSessionInterruptionTypeKey, NSString *)
50SOFT_LINK_POINTER(AVFoundation, AVAudioSessionInterruptionOptionKey, NSString *)
51
52#define AVAudioSession getAVAudioSessionClass()
53#define AVAudioSessionInterruptionNotification getAVAudioSessionInterruptionNotification()
54#define AVAudioSessionInterruptionTypeKey getAVAudioSessionInterruptionTypeKey()
55#define AVAudioSessionInterruptionOptionKey getAVAudioSessionInterruptionOptionKey()
56
57SOFT_LINK_FRAMEWORK(UIKit)
58SOFT_LINK_CLASS(UIKit, UIApplication)
59SOFT_LINK_POINTER(UIKit, UIApplicationWillResignActiveNotification, NSString *)
60SOFT_LINK_POINTER(UIKit, UIApplicationWillEnterForegroundNotification, NSString *)
61SOFT_LINK_POINTER(UIKit, UIApplicationDidBecomeActiveNotification, NSString *)
62
63#define UIApplication getUIApplicationClass()
64#define UIApplicationWillResignActiveNotification getUIApplicationWillResignActiveNotification()
65#define UIApplicationWillEnterForegroundNotification getUIApplicationWillEnterForegroundNotification()
66#define UIApplicationDidBecomeActiveNotification getUIApplicationDidBecomeActiveNotification()
67
68SOFT_LINK_FRAMEWORK(MediaPlayer)
69SOFT_LINK_CLASS(MediaPlayer, MPAVRoutingController)
70SOFT_LINK_CLASS(MediaPlayer, MPNowPlayingInfoCenter)
71SOFT_LINK_CLASS(MediaPlayer, MPVolumeView)
72SOFT_LINK_POINTER(MediaPlayer, MPMediaItemPropertyTitle, NSString *)
73SOFT_LINK_POINTER(MediaPlayer, MPMediaItemPropertyPlaybackDuration, NSString *)
74SOFT_LINK_POINTER(MediaPlayer, MPNowPlayingInfoPropertyElapsedPlaybackTime, NSString *)
75SOFT_LINK_POINTER(MediaPlayer, MPNowPlayingInfoPropertyPlaybackRate, NSString *)
76SOFT_LINK_POINTER(MediaPlayer, MPVolumeViewWirelessRoutesAvailableDidChangeNotification, NSString *)
77
78
79#define MPMediaItemPropertyTitle getMPMediaItemPropertyTitle()
80#define MPMediaItemPropertyPlaybackDuration getMPMediaItemPropertyPlaybackDuration()
81#define MPNowPlayingInfoPropertyElapsedPlaybackTime getMPNowPlayingInfoPropertyElapsedPlaybackTime()
82#define MPNowPlayingInfoPropertyPlaybackRate getMPNowPlayingInfoPropertyPlaybackRate()
83#define MPVolumeViewWirelessRoutesAvailableDidChangeNotification getMPVolumeViewWirelessRoutesAvailableDidChangeNotification()
84
85NSString* WebUIApplicationWillResignActiveNotification = @"WebUIApplicationWillResignActiveNotification";
86NSString* WebUIApplicationWillEnterForegroundNotification = @"WebUIApplicationWillEnterForegroundNotification";
87NSString* WebUIApplicationDidBecomeActiveNotification = @"WebUIApplicationDidBecomeActiveNotification";
88
89using namespace WebCore;
90
91@interface WebMediaSessionHelper : NSObject {
92    MediaSessionManageriOS* _callback;
93    RetainPtr<MPVolumeView> _volumeView;
94    RetainPtr<MPAVRoutingController> _airPlayPresenceRoutingController;
95}
96
97- (id)initWithCallback:(MediaSessionManageriOS*)callback;
98- (void)clearCallback;
99- (void)interruption:(NSNotification *)notification;
100- (void)applicationWillEnterForeground:(NSNotification *)notification;
101- (void)applicationWillResignActive:(NSNotification *)notification;
102- (BOOL)hasWirelessTargetsAvailable;
103- (void)startMonitoringAirPlayRoutes;
104- (void)stopMonitoringAirPlayRoutes;
105@end
106
107namespace WebCore {
108
109MediaSessionManager& MediaSessionManager::sharedManager()
110{
111    DEPRECATED_DEFINE_STATIC_LOCAL(MediaSessionManageriOS, manager, ());
112    return manager;
113}
114
115MediaSessionManageriOS::MediaSessionManageriOS()
116    :MediaSessionManager()
117    , m_objcObserver(adoptNS([[WebMediaSessionHelper alloc] initWithCallback:this]))
118{
119    resetRestrictions();
120}
121
122MediaSessionManageriOS::~MediaSessionManageriOS()
123{
124    [m_objcObserver clearCallback];
125}
126
127void MediaSessionManageriOS::resetRestrictions()
128{
129    MediaSessionManager::resetRestrictions();
130
131    static wkDeviceClass deviceClass = iosDeviceClass();
132    if (deviceClass == wkDeviceClassiPhone || deviceClass == wkDeviceClassiPod)
133        addRestriction(MediaSession::Video, InlineVideoPlaybackRestricted);
134
135    addRestriction(MediaSession::Video, ConcurrentPlaybackNotPermitted);
136    addRestriction(MediaSession::Video, BackgroundPlaybackNotPermitted);
137
138    removeRestriction(MediaSession::Audio, ConcurrentPlaybackNotPermitted);
139    removeRestriction(MediaSession::Audio, BackgroundPlaybackNotPermitted);
140
141    removeRestriction(MediaSession::WebAudio, ConcurrentPlaybackNotPermitted);
142    removeRestriction(MediaSession::WebAudio, BackgroundPlaybackNotPermitted);
143
144    removeRestriction(MediaSession::Audio, MetadataPreloadingNotPermitted);
145    removeRestriction(MediaSession::Video, MetadataPreloadingNotPermitted);
146
147    addRestriction(MediaSession::Audio, AutoPreloadingNotPermitted);
148    addRestriction(MediaSession::Video, AutoPreloadingNotPermitted);
149}
150
151#if ENABLE(IOS_AIRPLAY)
152
153bool MediaSessionManageriOS::hasWirelessTargetsAvailable()
154{
155    return [m_objcObserver hasWirelessTargetsAvailable];
156}
157
158void MediaSessionManageriOS::startMonitoringAirPlayRoutes()
159{
160    [m_objcObserver startMonitoringAirPlayRoutes];
161}
162
163void MediaSessionManageriOS::stopMonitoringAirPlayRoutes()
164{
165    [m_objcObserver stopMonitoringAirPlayRoutes];
166}
167#endif
168
169void MediaSessionManageriOS::sessionWillBeginPlayback(MediaSession& session)
170{
171    MediaSessionManager::sessionWillBeginPlayback(session);
172    updateNowPlayingInfo();
173}
174
175void MediaSessionManageriOS::sessionWillEndPlayback(MediaSession& session)
176{
177    MediaSessionManager::sessionWillEndPlayback(session);
178    updateNowPlayingInfo();
179}
180
181void MediaSessionManageriOS::updateNowPlayingInfo()
182{
183    MPNowPlayingInfoCenter *nowPlaying = (MPNowPlayingInfoCenter *)[getMPNowPlayingInfoCenterClass() defaultCenter];
184    const MediaSession* currentSession = this->currentSession();
185
186    if (!currentSession) {
187        [nowPlaying setNowPlayingInfo:nil];
188        return;
189    }
190
191    RetainPtr<NSMutableDictionary> info = adoptNS([[NSMutableDictionary alloc] init]);
192
193    String title = currentSession->title();
194    if (!title.isEmpty())
195        [info setValue:static_cast<NSString *>(title) forKey:MPMediaItemPropertyTitle];
196
197    double duration = currentSession->duration();
198    if (std::isfinite(duration) && duration != MediaPlayer::invalidTime())
199        [info setValue:@(duration) forKey:MPMediaItemPropertyPlaybackDuration];
200
201    double currentTime = currentSession->currentTime();
202    if (std::isfinite(currentTime) && currentTime != MediaPlayer::invalidTime())
203        [info setValue:@(currentTime) forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
204
205    [info setValue:(currentSession->state() == MediaSession::Playing ? @YES : @NO) forKey:MPNowPlayingInfoPropertyPlaybackRate];
206    [nowPlaying setNowPlayingInfo:info.get()];
207}
208
209} // namespace WebCore
210
211@implementation WebMediaSessionHelper
212
213- (id)initWithCallback:(MediaSessionManageriOS*)callback
214{
215    if (!(self = [super init]))
216        return nil;
217
218    _callback = callback;
219    _volumeView = adoptNS([[getMPVolumeViewClass() alloc] init]);
220
221    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
222    [center addObserver:self selector:@selector(interruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
223
224    [center addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
225    [center addObserver:self selector:@selector(applicationWillEnterForeground:) name:WebUIApplicationWillEnterForegroundNotification object:nil];
226    [center addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
227    [center addObserver:self selector:@selector(applicationDidBecomeActive:) name:WebUIApplicationDidBecomeActiveNotification object:nil];
228    [center addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
229    [center addObserver:self selector:@selector(applicationWillResignActive:) name:WebUIApplicationWillResignActiveNotification object:nil];
230    [center addObserver:self selector:@selector(wirelessRoutesAvailableDidChange:) name:MPVolumeViewWirelessRoutesAvailableDidChangeNotification object:_volumeView.get()];
231
232    // Now playing won't work unless we turn on the delivery of remote control events.
233    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
234
235    return self;
236}
237
238- (void)dealloc
239{
240    [[NSNotificationCenter defaultCenter] removeObserver:self];
241    [super dealloc];
242}
243
244- (void)clearCallback
245{
246    _callback = nil;
247}
248
249- (BOOL)hasWirelessTargetsAvailable
250{
251    return [_volumeView areWirelessRoutesAvailable];
252}
253
254- (void)startMonitoringAirPlayRoutes
255{
256    if (_airPlayPresenceRoutingController)
257        return;
258
259    _airPlayPresenceRoutingController = adoptNS([[getMPAVRoutingControllerClass() alloc] initWithName:@"WebCore - HTML media element checking for AirPlay route presence"]);
260    [_airPlayPresenceRoutingController setDiscoveryMode:MPRouteDiscoveryModePresence];
261}
262
263- (void)stopMonitoringAirPlayRoutes
264{
265    _airPlayPresenceRoutingController = nil;
266}
267
268- (void)interruption:(NSNotification *)notification
269{
270    if (!_callback)
271        return;
272
273    NSUInteger type = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
274    MediaSession::EndInterruptionFlags flags = MediaSession::NoFlags;
275
276    if (type == AVAudioSessionInterruptionTypeEnded && [[[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey] unsignedIntegerValue] == AVAudioSessionInterruptionOptionShouldResume)
277        flags = MediaSession::MayResumePlaying;
278
279    WebThreadRun(^{
280        if (!_callback)
281            return;
282
283        if (type == AVAudioSessionInterruptionTypeBegan)
284            _callback->beginInterruption(MediaSession::SystemInterruption);
285        else
286            _callback->endInterruption(flags);
287
288    });
289}
290
291- (void)applicationWillEnterForeground:(NSNotification *)notification
292{
293    UNUSED_PARAM(notification);
294
295    if (!_callback)
296        return;
297
298    WebThreadRun(^{
299        if (!_callback)
300            return;
301
302        _callback->applicationWillEnterForeground();
303    });
304}
305
306- (void)applicationDidBecomeActive:(NSNotification *)notification
307{
308    UNUSED_PARAM(notification);
309
310    if (!_callback)
311        return;
312
313    WebThreadRun(^{
314        if (!_callback)
315            return;
316
317        _callback->applicationWillEnterForeground();
318    });
319}
320
321- (void)applicationWillResignActive:(NSNotification *)notification
322{
323    UNUSED_PARAM(notification);
324
325    if (!_callback)
326        return;
327
328    WebThreadRun(^{
329        if (!_callback)
330            return;
331
332        _callback->applicationWillEnterBackground();
333    });
334}
335
336- (void)wirelessRoutesAvailableDidChange:(NSNotification *)notification
337{
338    UNUSED_PARAM(notification);
339
340    if (!_callback)
341        return;
342
343    WebThreadRun(^{
344        if (!_callback)
345            return;
346
347        _callback->wirelessRoutesAvailableChanged();
348    });
349}
350@end
351
352#endif // PLATFORM(IOS)
353