VLCWatchCommunication.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /*****************************************************************************
  2. * VLCWatchCommunication.m
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright (c) 2015 VideoLAN. All rights reserved.
  6. * $Id$
  7. *
  8. * Author: Tobias Conradi <videolan # tobias-conradi.de>
  9. *
  10. * Refer to the COPYING file of the official project for license.
  11. *****************************************************************************/
  12. #import "VLCWatchCommunication.h"
  13. #import "VLCWatchMessage.h"
  14. #import "VLCPlaybackController+MediaLibrary.h"
  15. #import <MediaPlayer/MediaPlayer.h>
  16. #import <MediaLibraryKit/UIImage+MLKit.h>
  17. #import <WatchKit/WatchKit.h>
  18. #import "VLCThumbnailsCache.h"
  19. @interface VLCWatchCommunication()
  20. @property (nonatomic, strong) NSOperationQueue *thumbnailingQueue;
  21. @end
  22. @implementation VLCWatchCommunication
  23. + (BOOL)isSupported {
  24. return [WCSession class] != nil && [WCSession isSupported];
  25. }
  26. - (instancetype)init
  27. {
  28. self = [super init];
  29. if (self) {
  30. if ([VLCWatchCommunication isSupported]) {
  31. WCSession *session = [WCSession defaultSession];
  32. session.delegate = self;
  33. [session activateSession];
  34. _thumbnailingQueue = [NSOperationQueue new];
  35. _thumbnailingQueue.name = @"org.videolan.vlc.watch-thumbnailing";
  36. }
  37. }
  38. return self;
  39. }
  40. - (void)dealloc {
  41. [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil];
  42. }
  43. static VLCWatchCommunication *_singeltonInstance = nil;
  44. + (VLCWatchCommunication *)sharedInstance
  45. {
  46. @synchronized(self) {
  47. static dispatch_once_t pred;
  48. dispatch_once(&pred, ^{
  49. _singeltonInstance = [[self alloc] init];
  50. });
  51. }
  52. return _singeltonInstance;
  53. }
  54. - (void)playFileFromWatch:(VLCWatchMessage *)message
  55. {
  56. NSManagedObject *managedObject = nil;
  57. NSString *uriString = (id)message.payload;
  58. if ([uriString isKindOfClass:[NSString class]]) {
  59. NSURL *uriRepresentation = [NSURL URLWithString:uriString];
  60. managedObject = [[MLMediaLibrary sharedMediaLibrary] objectForURIRepresentation:uriRepresentation];
  61. }
  62. if (managedObject == nil) {
  63. APLog(@"%s file not found: %@",__PRETTY_FUNCTION__,message);
  64. return;
  65. }
  66. VLCPlaybackController *vpc = [VLCPlaybackController sharedInstance];
  67. [vpc playMediaLibraryObject:managedObject];
  68. }
  69. #pragma mark - WCSessionDelegate
  70. - (NSDictionary *)handleMessage:(nonnull VLCWatchMessage *)message {
  71. UIApplication *application = [UIApplication sharedApplication];
  72. /* dispatch background task */
  73. __block UIBackgroundTaskIdentifier taskIdentifier = [application beginBackgroundTaskWithName:nil
  74. expirationHandler:^{
  75. [application endBackgroundTask:taskIdentifier];
  76. taskIdentifier = UIBackgroundTaskInvalid;
  77. }];
  78. NSString *name = message.name;
  79. NSDictionary *responseDict = @{};
  80. if ([name isEqualToString:VLCWatchMessageNameGetNowPlayingInfo]) {
  81. responseDict = [self nowPlayingResponseDict];
  82. } else if ([name isEqualToString:VLCWatchMessageNamePlayPause]) {
  83. [[VLCPlaybackController sharedInstance] playPause];
  84. responseDict = @{@"playing": @([VLCPlaybackController sharedInstance].isPlaying)};
  85. } else if ([name isEqualToString:VLCWatchMessageNameSkipForward]) {
  86. [[VLCPlaybackController sharedInstance] forward];
  87. } else if ([name isEqualToString:VLCWatchMessageNameSkipBackward]) {
  88. [[VLCPlaybackController sharedInstance] backward];
  89. } else if ([name isEqualToString:VLCWatchMessageNamePlayFile]) {
  90. [self playFileFromWatch:message];
  91. } else if ([name isEqualToString:VLCWatchMessageNameSetVolume]) {
  92. [self setVolumeFromWatch:message];
  93. } else if ([name isEqualToString:VLCWatchMessageNameRequestThumbnail]) {
  94. [self requestThumnail:message];
  95. } else if([name isEqualToString:VLCWatchMessageNameRequestDB]) {
  96. [self copyCoreDataToWatch];
  97. } else {
  98. APLog(@"Did not handle request from WatchKit Extension: %@",message);
  99. }
  100. return responseDict;
  101. }
  102. - (void)session:(nonnull WCSession *)session didReceiveMessage:(nonnull NSDictionary<NSString *,id> *)userInfo replyHandler:(nonnull void (^)(NSDictionary<NSString *,id> * _Nonnull))replyHandler {
  103. VLCWatchMessage *message = [[VLCWatchMessage alloc] initWithDictionary:userInfo];
  104. NSDictionary *responseDict = [self handleMessage:message];
  105. replyHandler(responseDict);
  106. }
  107. - (void)session:(nonnull WCSession *)session didReceiveMessage:(nonnull NSDictionary<NSString *,id> *)messageDict {
  108. VLCWatchMessage *message = [[VLCWatchMessage alloc] initWithDictionary:messageDict];
  109. [self handleMessage:message];
  110. }
  111. - (void)sessionWatchStateDidChange:(WCSession *)session {
  112. NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
  113. [center removeObserver:self name:NSManagedObjectContextDidSaveNotification object:nil];
  114. [center removeObserver:self name:MLFileThumbnailWasUpdated object:nil];
  115. if ([[WCSession defaultSession] isPaired] && [[WCSession defaultSession] isWatchAppInstalled]) {
  116. [center addObserver:self selector:@selector(savedManagedObjectContextNotification:) name:NSManagedObjectContextDidSaveNotification object:nil];
  117. [center addObserver:self selector:@selector(didUpdateThumbnail:) name:MLFileThumbnailWasUpdated object:nil];
  118. }
  119. }
  120. #pragma mark -
  121. - (void)setVolumeFromWatch:(VLCWatchMessage *)message
  122. {
  123. NSNumber *volume = (id)message.payload;
  124. if ([volume isKindOfClass:[NSNumber class]]) {
  125. /*
  126. * Since WatchKit doesn't provide something like MPVolumeView we use deprecated API.
  127. * rdar://20783803 Feature Request: WatchKit equivalent for MPVolumeView
  128. */
  129. [MPMusicPlayerController applicationMusicPlayer].volume = volume.floatValue;
  130. }
  131. }
  132. - (NSDictionary *)nowPlayingResponseDict {
  133. NSMutableDictionary *response = [NSMutableDictionary new];
  134. NSMutableDictionary *nowPlayingInfo = [[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo mutableCopy];
  135. NSNumber *playbackTime = [VLCPlaybackController sharedInstance].mediaPlayer.time.numberValue;
  136. if (playbackTime) {
  137. nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(playbackTime.floatValue/1000);
  138. }
  139. if (nowPlayingInfo) {
  140. response[@"nowPlayingInfo"] = nowPlayingInfo;
  141. }
  142. MLFile *currentFile = [VLCPlaybackController sharedInstance].currentlyPlayingMediaFile;
  143. NSString *URIString = currentFile.objectID.URIRepresentation.absoluteString;
  144. if (URIString) {
  145. response[VLCWatchMessageKeyURIRepresentation] = URIString;
  146. }
  147. response[@"volume"] = @([MPMusicPlayerController applicationMusicPlayer].volume);
  148. return response;
  149. }
  150. - (void)requestThumnail:(VLCWatchMessage *)message {
  151. NSString *uriString = message.payload[VLCWatchMessageKeyURIRepresentation];
  152. NSURL *url = [NSURL URLWithString:uriString];
  153. NSManagedObject *object = [[MLMediaLibrary sharedMediaLibrary] objectForURIRepresentation:url];
  154. if (object) {
  155. [self transferThumbnailForObject:object refreshCache:NO];
  156. }
  157. }
  158. #pragma mark - Notifications
  159. - (void)startRelayingNotificationName:(nullable NSString *)name object:(nullable id)object {
  160. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(relayNotification:) name:name object:object];
  161. }
  162. - (void)stopRelayingNotificationName:(nullable NSString *)name object:(nullable id)object {
  163. [[NSNotificationCenter defaultCenter] removeObserver:self name:name object:object];
  164. }
  165. - (void)relayNotification:(NSNotification *)notification {
  166. NSMutableDictionary *payload = [NSMutableDictionary dictionary];
  167. payload[@"name"] = notification.name;
  168. if (notification.userInfo) {
  169. payload[@"userInfo"] = notification.userInfo;
  170. }
  171. NSDictionary *dict = [VLCWatchMessage messageDictionaryForName:VLCWatchMessageNameNotification
  172. payload:payload];
  173. if ([WCSession isSupported] && [[WCSession defaultSession] isWatchAppInstalled] && [[WCSession defaultSession] isReachable]) {
  174. [[WCSession defaultSession] sendMessage:dict replyHandler:nil errorHandler:nil];
  175. }
  176. }
  177. #pragma mark - Copy CoreData to Watch
  178. - (void)savedManagedObjectContextNotification:(NSNotification *)notification {
  179. NSManagedObjectContext *moc = notification.object;
  180. if (moc.persistentStoreCoordinator == [[MLMediaLibrary sharedMediaLibrary] persistentStoreCoordinator]) {
  181. [self copyCoreDataToWatch];
  182. }
  183. }
  184. - (void)copyCoreDataToWatch {
  185. if (![[WCSession defaultSession] isPaired] || ![[WCSession defaultSession] isWatchAppInstalled]) return;
  186. MLMediaLibrary *library = [MLMediaLibrary sharedMediaLibrary];
  187. NSPersistentStoreCoordinator *libraryPSC = [library persistentStoreCoordinator];
  188. NSPersistentStore *persistentStore = [libraryPSC persistentStoreForURL:[library persistentStoreURL]];
  189. NSURL *tmpDirectoryURL = [[WCSession defaultSession] watchDirectoryURL];
  190. NSURL *tmpURL = [tmpDirectoryURL URLByAppendingPathComponent:persistentStore.URL.lastPathComponent];
  191. NSMutableDictionary *destOptions = [persistentStore.options mutableCopy] ?: [NSMutableDictionary new];
  192. destOptions[NSSQLitePragmasOption] = @{@"journal_mode": @"DELETE"};
  193. NSError *error;
  194. bool success = [libraryPSC replacePersistentStoreAtURL:tmpURL destinationOptions:destOptions withPersistentStoreFromURL:persistentStore.URL sourceOptions:persistentStore.options storeType:NSSQLiteStoreType error:&error];
  195. if (!success) {
  196. NSLog(@"%s failed to copy persistent store to tmp location for copy to watch with error %@",__PRETTY_FUNCTION__,error);
  197. }
  198. // cancel old transfers
  199. NSArray<WCSessionFileTransfer *> *outstandingtransfers = [[WCSession defaultSession] outstandingFileTransfers];
  200. [outstandingtransfers enumerateObjectsUsingBlock:^(WCSessionFileTransfer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  201. if ([obj.file.metadata[@"filetype"] isEqualToString:@"coredata"]) {
  202. [obj cancel];
  203. }
  204. }];
  205. NSDictionary *metadata = @{@"filetype":@"coredata"};
  206. [[WCSession defaultSession] transferFile:tmpURL metadata:metadata];
  207. }
  208. - (void)transferThumbnailForObject:(NSManagedObject *__nonnull)object refreshCache:(BOOL)refresh{
  209. if (![[WCSession defaultSession] isPaired] || ![[WCSession defaultSession] isWatchAppInstalled]) {
  210. return;
  211. }
  212. CGRect bounds = [WKInterfaceDevice currentDevice].screenBounds;
  213. CGFloat scale = [WKInterfaceDevice currentDevice].screenScale;
  214. [self.thumbnailingQueue addOperationWithBlock:^{
  215. UIImage *scaledImage = [VLCThumbnailsCache thumbnailForManagedObject:object refreshCache:refresh toFitRect:bounds scale:scale shouldReplaceCache:NO];
  216. [self transferImage:scaledImage forObjectID:object.objectID];
  217. }];
  218. }
  219. - (void)didUpdateThumbnail:(NSNotification *)notification {
  220. NSManagedObject *object = notification.object;
  221. if(![object isKindOfClass:[NSManagedObject class]])
  222. return;
  223. [self transferThumbnailForObject:object refreshCache:YES];
  224. }
  225. - (void)transferImage:(UIImage *)image forObjectID:(NSManagedObjectID *)objectID {
  226. if (!image || ![[WCSession defaultSession] isPaired] || ![[WCSession defaultSession] isWatchAppInstalled]) {
  227. return;
  228. }
  229. NSString *imageName = [[NSUUID UUID] UUIDString];
  230. NSURL *tmpDirectoryURL = [[WCSession defaultSession] watchDirectoryURL];
  231. NSURL *tmpURL = [tmpDirectoryURL URLByAppendingPathComponent:imageName];
  232. NSData *data = UIImageJPEGRepresentation(image, 0.7);
  233. [data writeToURL:tmpURL atomically:YES];
  234. NSDictionary *metaData = @{@"filetype" : @"thumbnail",
  235. VLCWatchMessageKeyURIRepresentation : objectID.URIRepresentation.absoluteString};
  236. NSArray<WCSessionFileTransfer *> *outstandingtransfers = [[WCSession defaultSession] outstandingFileTransfers];
  237. [outstandingtransfers enumerateObjectsUsingBlock:^(WCSessionFileTransfer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  238. if ([obj.file.metadata isEqualToDictionary:metaData])
  239. [obj cancel];
  240. }];
  241. [[WCSession defaultSession] transferFile:tmpURL metadata:metaData];
  242. }
  243. @end