VLCWatchCommunication.m 12.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/*****************************************************************************
 * VLCWatchCommunication.m
 * VLC for iOS
 *****************************************************************************
 * Copyright (c) 2015 VideoLAN. All rights reserved.
 * $Id$
 *
 * Author: Tobias Conradi <videolan # tobias-conradi.de>
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/


#import "VLCWatchCommunication.h"
#import "VLCWatchMessage.h"
#import "VLCPlaybackController+MediaLibrary.h"
#import <MediaPlayer/MediaPlayer.h>
Tobias's avatar
Tobias committed
18 19
#import <MediaLibraryKit/UIImage+MLKit.h>
#import <WatchKit/WatchKit.h>
20
#import "VLCThumbnailsCache.h"
Tobias's avatar
Tobias committed
21 22 23 24 25

@interface VLCWatchCommunication()
@property (nonatomic, strong) NSOperationQueue *thumbnailingQueue;

@end
26 27 28

@implementation VLCWatchCommunication

Tobias's avatar
Tobias committed
29 30 31 32
+ (BOOL)isSupported {
    return [WCSession class] != nil && [WCSession isSupported];
}

33 34 35 36
- (instancetype)init
{
    self = [super init];
    if (self) {
Tobias's avatar
Tobias committed
37 38

        if ([VLCWatchCommunication isSupported]) {
39 40 41
            WCSession *session = [WCSession defaultSession];
            session.delegate = self;
            [session activateSession];
Tobias's avatar
Tobias committed
42 43
            _thumbnailingQueue = [NSOperationQueue new];
            _thumbnailingQueue.name = @"org.videolan.vlc.watch-thumbnailing";
44 45 46 47 48
        }
    }
    return self;
}

49 50 51 52
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil];
}

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
static VLCWatchCommunication *_singeltonInstance = nil;

+ (VLCWatchCommunication *)sharedInstance
{
    @synchronized(self) {
        static dispatch_once_t pred;
        dispatch_once(&pred, ^{
            _singeltonInstance = [[self alloc] init];
        });
    }
    return _singeltonInstance;
}

- (void)playFileFromWatch:(VLCWatchMessage *)message
{
    NSManagedObject *managedObject = nil;
    NSString *uriString = (id)message.payload;
    if ([uriString isKindOfClass:[NSString class]]) {
        NSURL *uriRepresentation = [NSURL URLWithString:uriString];
        managedObject = [[MLMediaLibrary sharedMediaLibrary] objectForURIRepresentation:uriRepresentation];
    }
    if (managedObject == nil) {
        APLog(@"%s file not found: %@",__PRETTY_FUNCTION__,message);
        return;
    }

    VLCPlaybackController *vpc = [VLCPlaybackController sharedInstance];
    [vpc playMediaLibraryObject:managedObject];
}

83
#pragma mark - WCSessionDelegate
84
- (NSDictionary *)handleMessage:(nonnull VLCWatchMessage *)message {
85 86 87 88 89 90 91 92 93
    UIApplication *application = [UIApplication sharedApplication];
    /* dispatch background task */
    __block UIBackgroundTaskIdentifier taskIdentifier = [application beginBackgroundTaskWithName:nil
                                                                               expirationHandler:^{
                                                                                   [application endBackgroundTask:taskIdentifier];
                                                                                   taskIdentifier = UIBackgroundTaskInvalid;
                                                                               }];

    NSString *name = message.name;
94
    NSDictionary *responseDict = @{};
95 96 97 98 99 100 101 102 103 104 105 106 107
    if ([name isEqualToString:VLCWatchMessageNameGetNowPlayingInfo]) {
        responseDict = [self nowPlayingResponseDict];
    } else if ([name isEqualToString:VLCWatchMessageNamePlayPause]) {
        [[VLCPlaybackController sharedInstance] playPause];
        responseDict = @{@"playing": @([VLCPlaybackController sharedInstance].isPlaying)};
    } else if ([name isEqualToString:VLCWatchMessageNameSkipForward]) {
        [[VLCPlaybackController sharedInstance] forward];
    } else if ([name isEqualToString:VLCWatchMessageNameSkipBackward]) {
        [[VLCPlaybackController sharedInstance] backward];
    } else if ([name isEqualToString:VLCWatchMessageNamePlayFile]) {
        [self playFileFromWatch:message];
    } else if ([name isEqualToString:VLCWatchMessageNameSetVolume]) {
        [self setVolumeFromWatch:message];
108 109
    } else if ([name isEqualToString:VLCWatchMessageNameRequestThumbnail]) {
        [self requestThumnail:message];
110 111
    } else if([name isEqualToString:VLCWatchMessageNameRequestDB]) {
        [self copyCoreDataToWatch];
112
    } else {
113
        APLog(@"Did not handle request from WatchKit Extension: %@",message);
114
    }
115 116 117 118 119 120
    return responseDict;
}

- (void)session:(nonnull WCSession *)session didReceiveMessage:(nonnull NSDictionary<NSString *,id> *)userInfo replyHandler:(nonnull void (^)(NSDictionary<NSString *,id> * _Nonnull))replyHandler {
    VLCWatchMessage *message = [[VLCWatchMessage alloc] initWithDictionary:userInfo];
    NSDictionary *responseDict = [self handleMessage:message];
121 122 123
    replyHandler(responseDict);
}

124 125 126 127 128
- (void)session:(nonnull WCSession *)session didReceiveMessage:(nonnull NSDictionary<NSString *,id> *)messageDict {
    VLCWatchMessage *message = [[VLCWatchMessage alloc] initWithDictionary:messageDict];
    [self handleMessage:message];
}

129 130 131 132 133 134 135 136 137 138 139 140 141
- (void)sessionWatchStateDidChange:(WCSession *)session {

    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:NSManagedObjectContextDidSaveNotification object:nil];
    [center removeObserver:self name:MLFileThumbnailWasUpdated object:nil];

    if ([[WCSession defaultSession] isPaired] && [[WCSession defaultSession] isWatchAppInstalled]) {
        [center addObserver:self selector:@selector(savedManagedObjectContextNotification:) name:NSManagedObjectContextDidSaveNotification object:nil];
        [center addObserver:self selector:@selector(didUpdateThumbnail:) name:MLFileThumbnailWasUpdated object:nil];
    }
}

#pragma mark -
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167

- (void)setVolumeFromWatch:(VLCWatchMessage *)message
{
    NSNumber *volume = (id)message.payload;
    if ([volume isKindOfClass:[NSNumber class]]) {
        /*
         * Since WatchKit doesn't provide something like MPVolumeView we use deprecated API.
         * rdar://20783803 Feature Request: WatchKit equivalent for MPVolumeView
         */
        [MPMusicPlayerController applicationMusicPlayer].volume = volume.floatValue;
    }
}

- (NSDictionary *)nowPlayingResponseDict {
    NSMutableDictionary *response = [NSMutableDictionary new];
    NSMutableDictionary *nowPlayingInfo = [[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo mutableCopy];
    NSNumber *playbackTime = [VLCPlaybackController sharedInstance].mediaPlayer.time.numberValue;
    if (playbackTime) {
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(playbackTime.floatValue/1000);
    }
    if (nowPlayingInfo) {
        response[@"nowPlayingInfo"] = nowPlayingInfo;
    }
    MLFile *currentFile = [VLCPlaybackController sharedInstance].currentlyPlayingMediaFile;
    NSString *URIString = currentFile.objectID.URIRepresentation.absoluteString;
    if (URIString) {
168
        response[VLCWatchMessageKeyURIRepresentation] = URIString;
169 170 171 172 173 174 175
    }

    response[@"volume"] = @([MPMusicPlayerController applicationMusicPlayer].volume);

    return response;
}

176 177 178 179 180 181 182 183 184
- (void)requestThumnail:(VLCWatchMessage *)message {
    NSString *uriString = message.payload[VLCWatchMessageKeyURIRepresentation];
    NSURL *url = [NSURL URLWithString:uriString];
    NSManagedObject *object = [[MLMediaLibrary sharedMediaLibrary] objectForURIRepresentation:url];
    if (object) {
        [self transferThumbnailForObject:object refreshCache:NO];
    }
}

185 186 187 188 189 190 191 192
#pragma mark - Notifications
- (void)startRelayingNotificationName:(nullable NSString *)name object:(nullable id)object {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(relayNotification:) name:name object:object];
}
- (void)stopRelayingNotificationName:(nullable NSString *)name object:(nullable id)object {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:name object:object];
}
- (void)relayNotification:(NSNotification *)notification {
193 194 195 196 197 198

    NSMutableDictionary *payload = [NSMutableDictionary dictionary];
    payload[@"name"] = notification.name;
    if (notification.userInfo) {
        payload[@"userInfo"] = notification.userInfo;
    }
199
    NSDictionary *dict = [VLCWatchMessage messageDictionaryForName:VLCWatchMessageNameNotification
200
                                                           payload:payload];
Tobias's avatar
Tobias committed
201
    if ([WCSession isSupported] && [[WCSession defaultSession] isWatchAppInstalled] && [[WCSession defaultSession] isReachable]) {
202 203 204 205
        [[WCSession defaultSession] sendMessage:dict replyHandler:nil errorHandler:nil];
    }
}

Tobias's avatar
Tobias committed
206 207 208 209 210 211 212 213 214 215
#pragma mark - Copy CoreData to Watch

- (void)savedManagedObjectContextNotification:(NSNotification *)notification {
    NSManagedObjectContext *moc = notification.object;
    if (moc.persistentStoreCoordinator == [[MLMediaLibrary sharedMediaLibrary] persistentStoreCoordinator]) {
        [self copyCoreDataToWatch];
    }
}

- (void)copyCoreDataToWatch {
Tobias's avatar
Tobias committed
216
    if (![[WCSession defaultSession] isPaired] || ![[WCSession defaultSession] isWatchAppInstalled]) return;
Tobias's avatar
Tobias committed
217 218 219 220

    MLMediaLibrary *library = [MLMediaLibrary sharedMediaLibrary];
    NSPersistentStoreCoordinator *libraryPSC = [library persistentStoreCoordinator];
    NSPersistentStore *persistentStore = [libraryPSC persistentStoreForURL:[library persistentStoreURL]];
221 222
    NSURL *tmpDirectoryURL = [[WCSession defaultSession] watchDirectoryURL];
    NSURL *tmpURL = [tmpDirectoryURL URLByAppendingPathComponent:persistentStore.URL.lastPathComponent];
Tobias's avatar
Tobias committed
223 224

    NSMutableDictionary *destOptions = [persistentStore.options mutableCopy] ?: [NSMutableDictionary new];
Tobias's avatar
Tobias committed
225
    destOptions[NSSQLitePragmasOption] = @{@"journal_mode": @"DELETE"};
Tobias's avatar
Tobias committed
226

Tobias's avatar
Tobias committed
227 228
    NSError *error;
    bool success = [libraryPSC replacePersistentStoreAtURL:tmpURL destinationOptions:destOptions withPersistentStoreFromURL:persistentStore.URL sourceOptions:persistentStore.options storeType:NSSQLiteStoreType error:&error];
Tobias's avatar
Tobias committed
229 230 231 232
    if (!success) {
        NSLog(@"%s failed to copy persistent store to tmp location for copy to watch with error %@",__PRETTY_FUNCTION__,error);
    }

233 234 235 236 237 238 239 240
    // cancel old transfers
    NSArray<WCSessionFileTransfer *> *outstandingtransfers = [[WCSession defaultSession] outstandingFileTransfers];
    [outstandingtransfers enumerateObjectsUsingBlock:^(WCSessionFileTransfer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj.file.metadata[@"filetype"] isEqualToString:@"coredata"]) {
            [obj cancel];
        }
    }];

Tobias's avatar
Tobias committed
241 242 243 244
    NSDictionary *metadata = @{@"filetype":@"coredata"};
    [[WCSession defaultSession] transferFile:tmpURL metadata:metadata];
}

245
- (void)transferThumbnailForObject:(NSManagedObject *__nonnull)object refreshCache:(BOOL)refresh{
Tobias's avatar
Tobias committed
246

247 248 249 250
    if (![[WCSession defaultSession] isPaired] || ![[WCSession defaultSession] isWatchAppInstalled]) {
        return;
    }

Tobias's avatar
Tobias committed
251 252 253
    CGRect bounds = [WKInterfaceDevice currentDevice].screenBounds;
    CGFloat scale = [WKInterfaceDevice currentDevice].screenScale;
    [self.thumbnailingQueue addOperationWithBlock:^{
254 255
        UIImage *scaledImage = [VLCThumbnailsCache thumbnailForManagedObject:object refreshCache:refresh toFitRect:bounds scale:scale shouldReplaceCache:NO];
        [self transferImage:scaledImage forObjectID:object.objectID];
Tobias's avatar
Tobias committed
256
    }];
257 258 259 260 261 262 263 264

}

- (void)didUpdateThumbnail:(NSNotification *)notification {
    NSManagedObject *object = notification.object;
    if(![object isKindOfClass:[NSManagedObject class]])
        return;
    [self transferThumbnailForObject:object refreshCache:YES];
Tobias's avatar
Tobias committed
265 266 267
}

- (void)transferImage:(UIImage *)image forObjectID:(NSManagedObjectID *)objectID {
268 269 270
    if (!image || ![[WCSession defaultSession] isPaired] || ![[WCSession defaultSession] isWatchAppInstalled]) {
        return;
    }
Tobias's avatar
Tobias committed
271 272

    NSString *imageName = [[NSUUID UUID] UUIDString];
273 274
    NSURL *tmpDirectoryURL = [[WCSession defaultSession] watchDirectoryURL];
    NSURL *tmpURL = [tmpDirectoryURL URLByAppendingPathComponent:imageName];
Tobias's avatar
Tobias committed
275 276 277 278 279

    NSData *data = UIImageJPEGRepresentation(image, 0.7);
    [data writeToURL:tmpURL atomically:YES];

    NSDictionary *metaData = @{@"filetype" : @"thumbnail",
280
                               VLCWatchMessageKeyURIRepresentation : objectID.URIRepresentation.absoluteString};
Tobias's avatar
Tobias committed
281 282 283 284 285 286 287 288 289 290 291

    NSArray<WCSessionFileTransfer *> *outstandingtransfers = [[WCSession defaultSession] outstandingFileTransfers];
    [outstandingtransfers enumerateObjectsUsingBlock:^(WCSessionFileTransfer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj.file.metadata isEqualToDictionary:metaData])
            [obj cancel];
    }];

    [[WCSession defaultSession] transferFile:tmpURL metadata:metaData];
}


292
@end