VLCWatchCommunication.m 12 KB

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