VLCPlaybackController.m 47.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*****************************************************************************
 * VLCPlaybackController.m
 * VLC for iOS
 *****************************************************************************
 * Copyright (c) 2013-2015 VideoLAN. All rights reserved.
 * $Id$
 *
 * Authors: Felix Paul Kühne <fkuehne # videolan.org>
 *          Carola Nitz <caro # videolan.org>
 *          Gleb Pinigin <gpinigin # gmail.com>
 *          Pierre Sagaspe <pierre.sagaspe # me.com>
 *          Tobias Conradi <videolan # tobias-conradi.de>
 *          Sylver Bruneau <sylver.bruneau # gmail dot com>
 *          Winston Weinert <winston # ml1 dot net>
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/

#import "VLCPlaybackController.h"
#import <CommonCrypto/CommonDigest.h>
#import "UIDevice+VLC.h"
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>
24
#import "VLCPlayerDisplayController.h"
Felix Paul Kühne's avatar
Felix Paul Kühne committed
25
26
27

#if TARGET_OS_IOS
#import "VLCKeychainCoordinator.h"
28
#import "VLCThumbnailsCache.h"
29
#import "VLCLibraryViewController.h"
Felix Paul Kühne's avatar
Felix Paul Kühne committed
30
31
#import <WatchKit/WatchKit.h>
#endif
32

33
34
35
36
37
38
NSString *const VLCPlaybackControllerPlaybackDidStart = @"VLCPlaybackControllerPlaybackDidStart";
NSString *const VLCPlaybackControllerPlaybackDidPause = @"VLCPlaybackControllerPlaybackDidPause";
NSString *const VLCPlaybackControllerPlaybackDidResume = @"VLCPlaybackControllerPlaybackDidResume";
NSString *const VLCPlaybackControllerPlaybackDidStop = @"VLCPlaybackControllerPlaybackDidStop";
NSString *const VLCPlaybackControllerPlaybackMetadataDidChange = @"VLCPlaybackControllerPlaybackMetadataDidChange";
NSString *const VLCPlaybackControllerPlaybackDidFail = @"VLCPlaybackControllerPlaybackDidFail";
39
NSString *const VLCPlaybackControllerPlaybackPositionUpdated = @"VLCPlaybackControllerPlaybackPositionUpdated";
40

Felix Paul Kühne's avatar
Felix Paul Kühne committed
41
@interface VLCPlaybackController () <VLCMediaPlayerDelegate, VLCMediaDelegate>
42
43
44
45
{
    BOOL _playerIsSetup;
    BOOL _playbackFailed;
    BOOL _shouldResumePlaying;
46
    BOOL _shouldResumePlayingAfterInteruption;
47
    NSTimer *_sleepTimer;
48
49
50
51
52
53
54

    NSArray *_aspectRatios;
    NSUInteger _currentAspectRatioMask;

    float _currentPlaybackRate;
    UIView *_videoOutputViewWrapper;
    UIView *_actualVideoOutputView;
55
    UIView *_preBackgroundWrapperView;
56
57
58
59
60
61
62

    /* cached stuff for the VC */
    NSString *_title;
    UIImage *_artworkImage;
    NSString *_artist;
    NSString *_albumName;
    BOOL _mediaIsAudioOnly;
63
64

    BOOL _needsMetadataUpdate;
65
    BOOL _mediaWasJustStarted;
66
    BOOL _recheckForExistingThumbnail;
67
    BOOL _activeSession;
68
69

    NSLock *_playbackSessionManagementLock;
70
71

    VLCDialogProvider *_dialogProvider;
72
73
74
75
76
77
78
79
80
81
82
83
84
85
}

@end

@implementation VLCPlaybackController

#pragma mark instance management

+ (VLCPlaybackController *)sharedInstance
{
    static VLCPlaybackController *sharedInstance = nil;
    static dispatch_once_t pred;

    dispatch_once(&pred, ^{
86
        sharedInstance = [VLCPlaybackController new];
87
88
89
90
91
92
93
    });

    return sharedInstance;
}

- (void)dealloc
{
94
    _dialogProvider = nil;
95
96
97
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

Tobias's avatar
Tobias committed
98
99
100
101
102
103
104
105
106
107
108
109
110
- (instancetype)init
{
    self = [super init];
    if (self) {
        NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
        [defaultCenter addObserver:self selector:@selector(audioSessionRouteChange:)
                              name:AVAudioSessionRouteChangeNotification object:nil];
        [defaultCenter addObserver:self selector:@selector(applicationWillResignActive:)
                              name:UIApplicationWillResignActiveNotification object:nil];
        [defaultCenter addObserver:self selector:@selector(applicationDidBecomeActive:)
                              name:UIApplicationDidBecomeActiveNotification object:nil];
        [defaultCenter addObserver:self selector:@selector(applicationDidEnterBackground:)
                              name:UIApplicationDidEnterBackgroundNotification object:nil];
111

112
113
        _dialogProvider = [[VLCDialogProvider alloc] initWithLibrary:[VLCLibrary sharedLibrary] customUI:NO];

114
        _playbackSessionManagementLock = [[NSLock alloc] init];
Tobias's avatar
Tobias committed
115
116
117
118
    }
    return self;
}

119
120
#pragma mark - playback management

121
- (BOOL)_isMediaSuitableForDevice:(VLCMedia *)media
122
{
123
    NSArray *tracksInfo = media.tracksInformation;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
124
    double width = 0.0, height = 0.0;
125
    NSDictionary *track;
126

127
128
129
130
131
132
133
    for (NSUInteger x = 0; x < tracksInfo.count; x++) {
        track = tracksInfo[x];
        if ([track[VLCMediaTracksInformationType] isEqualToString:VLCMediaTracksInformationTypeVideo]) {
            width = [track[VLCMediaTracksInformationVideoWidth] doubleValue];
            height = [track[VLCMediaTracksInformationVideoHeight] doubleValue];
        }
    }
134

135
    NSUInteger totalNumberOfPixels = width * height;
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155

    NSInteger speedCategory = [[UIDevice currentDevice] speedCategory];

    if (speedCategory == 1) {
        // iPhone 3GS, iPhone 4, first gen. iPad, 3rd and 4th generation iPod touch
        return (totalNumberOfPixels < 600000); // between 480p and 720p
    } else if (speedCategory == 2) {
        // iPhone 4S, iPad 2 and 3, iPod 4 and 5
        return (totalNumberOfPixels < 922000); // 720p
    } else if (speedCategory == 3) {
        // iPhone 5, iPad 4
        return (totalNumberOfPixels < 2074000); // 1080p
    } else if (speedCategory == 4) {
        // iPhone 6, 2014 iPads
        return (totalNumberOfPixels < 8850000); // 4K
    }

    return YES;
}

156
- (void)playMediaList:(VLCMediaList *)mediaList firstIndex:(NSInteger)index
157
158
{
    self.mediaList = mediaList;
159
    self.itemInMediaListToBePlayedFirst = (int)index;
160
161
162
163
    self.pathToExternalSubtitlesFile = nil;

    if (self.activePlaybackSession) {
        self.sessionWillRestart = YES;
164
        [self stopPlayback];
165
166
    } else {
        self.sessionWillRestart = NO;
167
        [self startPlayback];
168
169
170
    }
}

171
172
173
174
175
176
177
178
- (void)playURL:(NSURL *)url successCallback:(NSURL*)successCallback errorCallback:(NSURL *)errorCallback
{
    self.url = url;
    self.successCallback = successCallback;
    self.errorCallback = errorCallback;

    if (self.activePlaybackSession) {
        self.sessionWillRestart = YES;
179
        [self stopPlayback];
180
181
    } else {
        self.sessionWillRestart = NO;
182
        [self startPlayback];
183
184
185
186
187
188
189
190
191
192
    }
}

- (void)playURL:(NSURL *)url subtitlesFilePath:(NSString *)subsFilePath
{
    self.url = url;
    self.pathToExternalSubtitlesFile = subsFilePath;

    if (self.activePlaybackSession) {
        self.sessionWillRestart = YES;
193
        [self stopPlayback];
194
195
    } else {
        self.sessionWillRestart = NO;
196
        [self startPlayback];
197
198
199
    }
}

200
201
- (void)startPlayback
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
202
203
    if (_playerIsSetup) {
        APLog(@"%s: player is already setup, bailing out", __PRETTY_FUNCTION__);
204
        return;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
205
    }
206
207
208
209
210
211
212

    BOOL ret = [_playbackSessionManagementLock tryLock];
    if (!ret) {
        APLog(@"%s: locking failed", __PRETTY_FUNCTION__);
        return;
    }

213
    _activeSession = YES;
214

Felix Paul Kühne's avatar
Felix Paul Kühne committed
215
#if TARGET_OS_IOS
216
    [[AVAudioSession sharedInstance] setDelegate:self];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
217
#endif
218
219
220
221
222

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    _aspectRatios = @[@"DEFAULT", @"FILL_TO_SCREEN", @"4:3", @"16:9", @"16:10", @"2.21:1"];

223
    if (!self.url && !self.mediaList) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
224
        APLog(@"%s: no URL and no media list set, stopping playback", __PRETTY_FUNCTION__);
225
        [_playbackSessionManagementLock unlock];
226
227
228
        [self stopPlayback];
        return;
    }
229

230
231
232
233
234
235
236
    /* video decoding permanently fails if we don't provide a UIView to draw into on init
     * hence we provide one which is not attached to any view controller for off-screen drawing
     * and disable video decoding once playback started */
    _actualVideoOutputView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    _actualVideoOutputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    _actualVideoOutputView.autoresizesSubviews = YES;

237
238
239
240
241
242
243
244
245
246
    if (self.pathToExternalSubtitlesFile)
        _listPlayer = [[VLCMediaListPlayer alloc] initWithOptions:@[[NSString stringWithFormat:@"--%@=%@", kVLCSettingSubtitlesFilePath, self.pathToExternalSubtitlesFile]] andDrawable:_actualVideoOutputView];
    else
        _listPlayer = [[VLCMediaListPlayer alloc] initWithDrawable:_actualVideoOutputView];

    /* to enable debug logging for the playback library instance, switch the boolean below
     * note that the library instance used for playback may not necessarily match the instance
     * used for media discovery or thumbnailing */
    _listPlayer.mediaPlayer.libraryInstance.debugLogging = NO;

247
248
249
250
251
252
253
254
255
256
257
    _mediaPlayer = _listPlayer.mediaPlayer;
    [_mediaPlayer setDelegate:self];
    if ([[defaults objectForKey:kVLCSettingPlaybackSpeedDefaultValue] floatValue] != 0)
        [_mediaPlayer setRate: [[defaults objectForKey:kVLCSettingPlaybackSpeedDefaultValue] floatValue]];
    if ([[defaults objectForKey:kVLCSettingDeinterlace] intValue] != 0)
        [_mediaPlayer setDeinterlaceFilter:@"blend"];
    else
        [_mediaPlayer setDeinterlaceFilter:nil];
    if (self.pathToExternalSubtitlesFile)
        [_mediaPlayer openVideoSubTitlesFromFile:self.pathToExternalSubtitlesFile];

258
259
260
261
262
263
#if TARGET_OS_TV
    /* set audio delay to the default latency of the output device */
    self.audioDelay = [[AVAudioSession sharedInstance] outputLatency];
    APLog(@"Enforcing an audio output latency of %fs", [[AVAudioSession sharedInstance] outputLatency]);
#endif

264
    VLCMedia *media;
265
266
    if (_mediaList) {
        media = [_mediaList mediaAtIndex:_itemInMediaListToBePlayedFirst];
267
        [media parseWithOptions:VLCMediaParseLocal];
268
269
270
271
        media.delegate = self;
    } else {
        media = [VLCMedia mediaWithURL:self.url];
        media.delegate = self;
272
        [media parseWithOptions:VLCMediaParseLocal];
273
        [media addOptions:self.mediaOptionsDictionary];
274
    }
275

276
277
278
279
280
281
282
    if (self.mediaList) {
        [_listPlayer setMediaList:self.mediaList];
    } else {
        [_listPlayer setRootMedia:media];
    }
    [_listPlayer setRepeatMode:VLCDoNotRepeat];

283
284
    [_playbackSessionManagementLock unlock];

285
    if (![self _isMediaSuitableForDevice:media]) {
286
#if TARGET_OS_IOS
287
        VLCAlertView *alert = [[VLCAlertView alloc] initWithTitle:NSLocalizedString(@"DEVICE_TOOSLOW_TITLE", nil)
288
                                                          message:[NSString stringWithFormat:NSLocalizedString(@"DEVICE_TOOSLOW", nil), [[UIDevice currentDevice] model], media.url.lastPathComponent]
289
290
291
                                                         delegate:self
                                                cancelButtonTitle:NSLocalizedString(@"BUTTON_CANCEL", nil)
                                                otherButtonTitles:NSLocalizedString(@"BUTTON_OPEN", nil), nil];
292
        [alert show];
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#else
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"DEVICE_TOOSLOW_TITLE", nil)
                                                                       message:[NSString stringWithFormat:NSLocalizedString(@"DEVICE_TOOSLOW", nil), [[UIDevice currentDevice] model], media.url.lastPathComponent]
                                                                preferredStyle:UIAlertControllerStyleAlert];

        UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_OPEN", nil)
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                                                                      [self _playNewMedia];
                                                              }];

        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_CANCEL", nil)
                                                               style:UIAlertActionStyleDestructive
                                                            handler:^(UIAlertAction * action) {
                                                                [self stopPlayback];
                                                            }];
        [alert addAction:defaultAction];
        [alert addAction:cancelAction];
311
        [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
312
#endif
313
    } else
314
315
316
317
318
        [self _playNewMedia];
}

- (void)_playNewMedia
{
319
320
    BOOL ret = [_playbackSessionManagementLock tryLock];
    if (!ret) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
321
        APLog(@"%s: locking failed", __PRETTY_FUNCTION__);
322
323
324
        return;
    }

325
326
327
328
329
    // Set last selected equalizer profile
    unsigned int profile = (unsigned int)[[[NSUserDefaults standardUserDefaults] objectForKey:kVLCSettingEqualizerProfile] integerValue];
    [_mediaPlayer resetEqualizerFromProfile:profile];
    [_mediaPlayer setPreAmplification:[_mediaPlayer preAmplification]];

330
331
    _mediaWasJustStarted = YES;

332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
    [_mediaPlayer addObserver:self forKeyPath:@"time" options:0 context:nil];
    [_mediaPlayer addObserver:self forKeyPath:@"remainingTime" options:0 context:nil];

    if (self.mediaList)
        [_listPlayer playItemAtIndex:self.itemInMediaListToBePlayedFirst];
    else
        [_listPlayer playMedia:_listPlayer.rootMedia];

    if ([self.delegate respondsToSelector:@selector(prepareForMediaPlayback:)])
        [self.delegate prepareForMediaPlayback:self];

    _currentAspectRatioMask = 0;
    _mediaPlayer.videoAspectRatio = NULL;

    [self subscribeRemoteCommands];
347

348
    _playerIsSetup = YES;
349
350

    [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackDidStart object:self];
351
    [_playbackSessionManagementLock unlock];
352
353
}

Felix Paul Kühne's avatar
Felix Paul Kühne committed
354
#if TARGET_OS_IOS
355
356
357
358
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1)
        [self _playNewMedia];
359
    else
360
361
        [self stopPlayback];
}
Felix Paul Kühne's avatar
Felix Paul Kühne committed
362
#endif
363
364
365

- (void)stopPlayback
{
366
367
368
369
370
371
    BOOL ret = [_playbackSessionManagementLock tryLock];
    if (!ret) {
        APLog(@"%s: locking failed", __PRETTY_FUNCTION__);
        return;
    }

372
373
374
375
376
377
378
379
380
381
382
    if (_mediaPlayer) {
        @try {
            [_mediaPlayer removeObserver:self forKeyPath:@"time"];
            [_mediaPlayer removeObserver:self forKeyPath:@"remainingTime"];
        }
        @catch (NSException *exception) {
            APLog(@"we weren't an observer yet");
        }

        if (_mediaPlayer.media) {
            [_mediaPlayer pause];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
383
#if TARGET_OS_IOS
384
            [self _savePlaybackState];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
385
#endif
386
387
388
389
390
391
392
            [_mediaPlayer stop];
        }
        if (_mediaPlayer)
            _mediaPlayer = nil;
        if (_listPlayer)
            _listPlayer = nil;
    }
393
394
395
396
397
398
399
400
401
402
403
    if (!_sessionWillRestart) {
        if (_mediaList)
            _mediaList = nil;
        if (_url)
            _url = nil;
        if (_pathToExternalSubtitlesFile) {
            NSFileManager *fileManager = [NSFileManager defaultManager];
            if ([fileManager fileExistsAtPath:_pathToExternalSubtitlesFile])
                [fileManager removeItemAtPath:_pathToExternalSubtitlesFile error:nil];
            _pathToExternalSubtitlesFile = nil;
        }
404
405
406
    }
    _playerIsSetup = NO;

407
    if (self.errorCallback && _playbackFailed && !_sessionWillRestart)
408
        [[UIApplication sharedApplication] openURL:self.errorCallback];
409
    else if (self.successCallback && !_sessionWillRestart)
410
411
        [[UIApplication sharedApplication] openURL:self.successCallback];

412
413
    [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil;
    [self unsubscribeFromRemoteCommand];
414
    _activeSession = NO;
415

416
    [_playbackSessionManagementLock unlock];
417
418
    if (_playbackFailed) {
        [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackDidFail object:self];
419
    } else if (!_sessionWillRestart) {
420
        [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackDidStop object:self];
421
422
    } else {
        self.sessionWillRestart = NO;
423
        [self startPlayback];
424
    }
425
426
}

Felix Paul Kühne's avatar
Felix Paul Kühne committed
427
#if TARGET_OS_IOS
428
429
- (void)_savePlaybackState
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
430
431
432
433
434
435
436
    @try {
        [[MLMediaLibrary sharedMediaLibrary] save];
    }
    @catch (NSException *exception) {
        APLog(@"saving playback state failed");
    }

437
438
439
440
    MLFile *fileItem;
    NSArray *files = [MLFile fileForURL:_mediaPlayer.media.url];
    if (files.count > 0)
        fileItem = files.firstObject;
441

442
443
    if (!fileItem) {
        APLog(@"couldn't find file, not saving playback progress");
444
        return;
445
    }
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472

    @try {
        float position = _mediaPlayer.position;
        fileItem.lastPosition = @(position);
        fileItem.lastAudioTrack = @(_mediaPlayer.currentAudioTrackIndex);
        fileItem.lastSubtitleTrack = @(_mediaPlayer.currentVideoSubTitleIndex);

        if (position > .95)
            return;

        NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        NSString* newThumbnailPath = [searchPaths[0] stringByAppendingPathComponent:@"VideoSnapshots"];
        NSFileManager *fileManager = [NSFileManager defaultManager];

        if (![fileManager fileExistsAtPath:newThumbnailPath])
            [fileManager createDirectoryAtPath:newThumbnailPath withIntermediateDirectories:YES attributes:nil error:nil];

        newThumbnailPath = [newThumbnailPath stringByAppendingPathComponent:fileItem.objectID.URIRepresentation.lastPathComponent];
        [_mediaPlayer saveVideoSnapshotAt:newThumbnailPath withWidth:0 andHeight:0];

        _recheckForExistingThumbnail = YES;
        [self performSelector:@selector(_updateStoredThumbnailForFile:) withObject:fileItem afterDelay:.25];
    }
    @catch (NSException *exception) {
        APLog(@"failed to save current media state - file removed?");
    }
}
Felix Paul Kühne's avatar
Felix Paul Kühne committed
473
#endif
474

Felix Paul Kühne's avatar
Felix Paul Kühne committed
475
#if TARGET_OS_IOS
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
- (void)_updateStoredThumbnailForFile:(MLFile *)fileItem
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString* newThumbnailPath = [searchPaths[0] stringByAppendingPathComponent:@"VideoSnapshots"];
    newThumbnailPath = [newThumbnailPath stringByAppendingPathComponent:fileItem.objectID.URIRepresentation.lastPathComponent];

    if (![fileManager fileExistsAtPath:newThumbnailPath]) {
        if (_recheckForExistingThumbnail) {
            [self performSelector:@selector(_updateStoredThumbnailForFile:) withObject:fileItem afterDelay:1.];
            _recheckForExistingThumbnail = NO;
        } else
            return;
    }

    UIImage *newThumbnail = [UIImage imageWithContentsOfFile:newThumbnailPath];
    if (!newThumbnail) {
        if (_recheckForExistingThumbnail) {
            [self performSelector:@selector(_updateStoredThumbnailForFile:) withObject:fileItem afterDelay:1.];
            _recheckForExistingThumbnail = NO;
        } else
            return;
    }

    @try {
        [fileItem setComputedThumbnailScaledForDevice:newThumbnail];
    }
    @catch (NSException *exception) {
        APLog(@"updating thumbnail failed");
    }

    [fileManager removeItemAtPath:newThumbnailPath error:nil];
508
}
Felix Paul Kühne's avatar
Felix Paul Kühne committed
509
#endif
510
511
512

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
513
514
    if (_mediaWasJustStarted) {
        _mediaWasJustStarted = NO;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
515
#if TARGET_OS_IOS
516
517
518
519
520
521
        if (self.mediaList) {
            MLFile *item;
            NSArray *matches = [MLFile fileForURL:_mediaPlayer.media.url];
            item = matches.firstObject;
            [self _recoverLastPlaybackStateOfItem:item];
        }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
522
523
524
525
526
527
#else
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        BOOL bValue = [defaults boolForKey:kVLCSettingUseSPDIF];
        if (bValue) {
            [_mediaPlayer performSelector:@selector(setPassthroughAudio:) withObject:@(bValue)];
        }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
528
#endif
529
530
    }

531
532
    if ([self.delegate respondsToSelector:@selector(playbackPositionUpdated:)])
        [self.delegate playbackPositionUpdated:self];
533
534
535

    [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackPositionUpdated
                                                        object:self];
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
}

- (NSInteger)mediaDuration
{
    return _listPlayer.mediaPlayer.media.length.intValue;;
}

- (BOOL)isPlaying
{
    return _mediaPlayer.isPlaying;
}

- (VLCRepeatMode)repeatMode
{
    return _listPlayer.repeatMode;
}

- (void)setRepeatMode:(VLCRepeatMode)repeatMode
{
    _listPlayer.repeatMode = repeatMode;
}

- (BOOL)currentMediaHasChapters
{
560
    return [_mediaPlayer numberOfTitles] > 1 || [_mediaPlayer numberOfChaptersForTitle:_mediaPlayer.currentTitleIndex] > 1;
561
562
563
564
565
566
567
}

- (BOOL)currentMediaHasTrackToChooseFrom
{
    return [[_mediaPlayer audioTrackIndexes] count] > 2 || [[_mediaPlayer videoSubTitlesIndexes] count] > 1;
}

568
569
- (BOOL)activePlaybackSession
{
570
    return _activeSession;
571
572
573
574
575
576
577
}

- (BOOL)audioOnlyPlaybackSession
{
    return _mediaIsAudioOnly;
}

578
579
580
581
582
- (NSString *)mediaTitle
{
    return _title;
}

583
584
585
- (float)playbackRate
{
    float f_rate = _mediaPlayer.rate;
586
587
    _currentPlaybackRate = f_rate;
    return f_rate;
588
589
590
591
592
593
594
595
596
}

- (void)setPlaybackRate:(float)playbackRate
{
    if (_currentPlaybackRate != playbackRate)
        [_mediaPlayer setRate:playbackRate];
    _currentPlaybackRate = playbackRate;
}

597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
- (void)setAudioDelay:(float)audioDelay
{
    _mediaPlayer.currentAudioPlaybackDelay = 1000000.*audioDelay;
}
- (float)audioDelay
{
    return _mediaPlayer.currentAudioPlaybackDelay/1000000.;
}
-(void)setSubtitleDelay:(float)subtitleDeleay
{
    _mediaPlayer.currentVideoSubTitleDelay = 1000000.*subtitleDeleay;
}
- (float)subtitleDelay
{
    return _mediaPlayer.currentVideoSubTitleDelay/1000000.;
}

614
615
616
- (void)mediaPlayerStateChanged:(NSNotification *)aNotification
{
    VLCMediaPlayerState currentState = _mediaPlayer.state;
617

618
619
620
621
622
    if (currentState == VLCMediaPlayerStateBuffering) {
        /* attach delegate */
        _mediaPlayer.media.delegate = self;

        /* on-the-fly values through hidden API */
623
624
625
626
627
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        [_mediaPlayer performSelector:@selector(setTextRendererFont:) withObject:[defaults objectForKey:kVLCSettingSubtitlesFont]];
        [_mediaPlayer performSelector:@selector(setTextRendererFontSize:) withObject:[defaults objectForKey:kVLCSettingSubtitlesFontSize]];
        [_mediaPlayer performSelector:@selector(setTextRendererFontColor:) withObject:[defaults objectForKey:kVLCSettingSubtitlesFontColor]];
        [_mediaPlayer performSelector:@selector(setTextRendererFontForceBold:) withObject:[defaults objectForKey:kVLCSettingSubtitlesBoldFont]];
628
    } else if (currentState == VLCMediaPlayerStateError) {
629
        APLog(@"Playback failed");
630
        _playbackFailed = YES;
631
        self.sessionWillRestart = NO;
632
        [self stopPlayback];
633
    } else if (currentState == VLCMediaPlayerStateEnded || currentState == VLCMediaPlayerStateStopped) {
634
635
        [_listPlayer.mediaList lock];
        NSUInteger listCount = _listPlayer.mediaList.count;
636
        if ([_listPlayer.mediaList indexOfMedia:_mediaPlayer.media] == listCount - 1 && self.repeatMode == VLCDoNotRepeat) {
637
            [_listPlayer.mediaList unlock];
638
            self.sessionWillRestart = NO;
639
640
            [self stopPlayback];
            return;
641
642
643
        } else if (listCount > 1) {
            [_listPlayer.mediaList unlock];
            [_listPlayer next];
644
645
        } else
            [_listPlayer.mediaList unlock];
646
647
    }

648
649
650
651
652
653
    if ([self.delegate respondsToSelector:@selector(mediaPlayerStateChanged:isPlaying:currentMediaHasTrackToChooseFrom:currentMediaHasChapters:forPlaybackController:)])
        [self.delegate mediaPlayerStateChanged:currentState
                                     isPlaying:_mediaPlayer.isPlaying
              currentMediaHasTrackToChooseFrom:self.currentMediaHasTrackToChooseFrom
                       currentMediaHasChapters:self.currentMediaHasChapters
                         forPlaybackController:self];
654
655

    [self setNeedsMetadataUpdate];
656
657
658
659
660
}

#pragma mark - playback controls
- (void)playPause
{
661
    if ([_mediaPlayer isPlaying]) {
662
        [_listPlayer pause];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
663
#if TARGET_OS_IOS
664
        [self _savePlaybackState];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
665
#endif
666
667
        [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackDidPause object:self];
    } else {
668
        [_listPlayer play];
669
670
        [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackDidResume object:self];
    }
671
672
673
674
}

- (void)forward
{
675
    if (_mediaList.count > 1) {
676
        [_listPlayer next];
677
        [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackMetadataDidChange object:self];
678
679
680
681
682
683
684
685
    } else {
        NSNumber *skipLength = [[NSUserDefaults standardUserDefaults] valueForKey:kVLCSettingPlaybackForwardSkipLength];
        [_mediaPlayer jumpForward:skipLength.intValue];
    }
}

- (void)backward
{
686
    if (_mediaList.count > 1) {
687
        [_listPlayer previous];
688
        [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackMetadataDidChange object:self];
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
    }
    else {
        NSNumber *skipLength = [[NSUserDefaults standardUserDefaults] valueForKey:kVLCSettingPlaybackBackwardSkipLength];
        [_mediaPlayer jumpBackward:skipLength.intValue];
    }
}

- (void)switchAspectRatio
{
    NSUInteger count = [_aspectRatios count];

    if (_currentAspectRatioMask + 1 > count - 1) {
        _mediaPlayer.videoAspectRatio = NULL;
        _mediaPlayer.videoCropGeometry = NULL;
        _currentAspectRatioMask = 0;
        if ([self.delegate respondsToSelector:@selector(showStatusMessage:forPlaybackController:)])
            [self.delegate showStatusMessage:[NSString stringWithFormat:NSLocalizedString(@"AR_CHANGED", nil), NSLocalizedString(@"DEFAULT", nil)] forPlaybackController:self];
    } else {
        _currentAspectRatioMask++;

        if ([_aspectRatios[_currentAspectRatioMask] isEqualToString:@"FILL_TO_SCREEN"]) {
            UIScreen *screen;
            if (![[UIDevice currentDevice] hasExternalDisplay])
                screen = [UIScreen mainScreen];
            else
                screen = [UIScreen screens][1];

            float f_ar = screen.bounds.size.width / screen.bounds.size.height;

            if (f_ar == (float)(640./1136.)) // iPhone 5 aka 16:9.01
                _mediaPlayer.videoCropGeometry = "16:9";
            else if (f_ar == (float)(2./3.)) // all other iPhones
                _mediaPlayer.videoCropGeometry = "16:10"; // libvlc doesn't support 2:3 crop
722
            else if (f_ar == (float)(1. + (1./3.))) // all iPads
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
                _mediaPlayer.videoCropGeometry = "4:3";
            else if (f_ar == .5625) // AirPlay
                _mediaPlayer.videoCropGeometry = "16:9";
            else
                APLog(@"unknown screen format %f, can't crop", f_ar);

            if ([self.delegate respondsToSelector:@selector(showStatusMessage:forPlaybackController:)])
                [self.delegate showStatusMessage:NSLocalizedString(@"FILL_TO_SCREEN", nil) forPlaybackController:self];
            return;
        }

        _mediaPlayer.videoCropGeometry = NULL;
        _mediaPlayer.videoAspectRatio = (char *)[_aspectRatios[_currentAspectRatioMask] UTF8String];

        if ([self.delegate respondsToSelector:@selector(showStatusMessage:forPlaybackController:)])
            [self.delegate showStatusMessage:[NSString stringWithFormat:NSLocalizedString(@"AR_CHANGED", nil), _aspectRatios[_currentAspectRatioMask]] forPlaybackController:self];
    }
}

- (void)setVideoOutputView:(UIView *)videoOutputView
{
    if (videoOutputView) {
745
746
747
        if ([_actualVideoOutputView superview] != nil)
            [_actualVideoOutputView removeFromSuperview];

748
749
        _actualVideoOutputView.frame = (CGRect){CGPointZero, videoOutputView.frame.size};

750
751
752
        if (_mediaPlayer.currentVideoTrackIndex == -1)
            _mediaPlayer.currentVideoTrackIndex = 0;

753
        [videoOutputView addSubview:_actualVideoOutputView];
754
755
756
        [_actualVideoOutputView layoutSubviews];
        [_actualVideoOutputView updateConstraints];
        [_actualVideoOutputView setNeedsLayout];
757
    } else
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
        [_actualVideoOutputView removeFromSuperview];

    _videoOutputViewWrapper = videoOutputView;
}

- (UIView *)videoOutputView
{
    return _videoOutputViewWrapper;
}

#pragma mark - equalizer

- (void)setAmplification:(CGFloat)amplification forBand:(unsigned int)index
{
    if (!_mediaPlayer.equalizerEnabled)
        [_mediaPlayer setEqualizerEnabled:YES];

    [_mediaPlayer setAmplification:amplification forBand:index];

    // For some reason we have to apply again preamp to apply change
    [_mediaPlayer setPreAmplification:[_mediaPlayer preAmplification]];
}

- (CGFloat)amplificationOfBand:(unsigned int)index
{
    return [_mediaPlayer amplificationOfBand:index];
}

- (NSArray *)equalizerProfiles
{
    return _mediaPlayer.equalizerProfiles;
}

- (void)resetEqualizerFromProfile:(unsigned int)profile
{
    [[NSUserDefaults standardUserDefaults] setObject:@(profile) forKey:kVLCSettingEqualizerProfile];
    [_mediaPlayer resetEqualizerFromProfile:profile];
}

- (void)setPreAmplification:(CGFloat)preAmplification
{
    if (!_mediaPlayer.equalizerEnabled)
        [_mediaPlayer setEqualizerEnabled:YES];

    [_mediaPlayer setPreAmplification:preAmplification];
}

- (CGFloat)preAmplification
{
    return [_mediaPlayer preAmplification];
}

#pragma mark - AVSession delegate
- (void)beginInterruption
{
    if ([_mediaPlayer isPlaying]) {
        [_mediaPlayer pause];
815
        _shouldResumePlayingAfterInteruption = YES;
816
817
818
819
820
    }
}

- (void)endInterruption
{
821
    if (_shouldResumePlayingAfterInteruption) {
822
        [_mediaPlayer play];
823
        _shouldResumePlayingAfterInteruption = NO;
824
825
826
827
828
829
    }
}

- (void)audioSessionRouteChange:(NSNotification *)notification
{
    NSArray *outputs = [[AVAudioSession sharedInstance] currentRoute].outputs;
830
    NSString *portName = [[outputs firstObject] portName];
831

832
833
834
835
836
837
#if TARGET_OS_TV
    /* adapt audio delay to the default latency of the new output device */
    self.audioDelay = [[AVAudioSession sharedInstance] outputLatency];
    APLog(@"newly enforced audio output latency of %fs", [[AVAudioSession sharedInstance] outputLatency]);
#endif

838
839
    if (![portName isEqualToString:@"Headphones"] && [_mediaPlayer isPlaying]) {
        [_mediaPlayer pause];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
840
#if TARGET_OS_IOS
841
        [self _savePlaybackState];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
842
#endif
843
844
        [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackDidPause object:self];
    }
845
846
847
848
}

#pragma mark - Managing the media item

Felix Paul Kühne's avatar
Felix Paul Kühne committed
849
#if TARGET_OS_IOS
850
- (MLFile *)currentlyPlayingMediaFile {
851
    if (self.mediaList) {
852
        NSArray *results = [MLFile fileForURL:_mediaPlayer.media.url];
853
        return results.firstObject;
854
855
856
857
    }

    return nil;
}
Felix Paul Kühne's avatar
Felix Paul Kühne committed
858
#endif
859
860
861
862

#pragma mark - metadata handling
- (void)mediaDidFinishParsing:(VLCMedia *)aMedia
{
863
    [self setNeedsMetadataUpdate];
864
865
866
867
}

- (void)mediaMetaDataDidChange:(VLCMedia*)aMedia
{
868
869
870
871
872
873
874
875
876
877
878
    [self setNeedsMetadataUpdate];
}

- (void)setNeedsMetadataUpdate
{
    if (_needsMetadataUpdate == NO) {
        _needsMetadataUpdate = YES;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self _updateDisplayedMetadata];
        });
    }
879
880
881
882
}

- (void)_updateDisplayedMetadata
{
883
884
    _needsMetadataUpdate = NO;

885
    NSNumber *trackNumber;
886
887
888
889
890

    NSString *title;
    NSString *artist;
    NSString *albumName;
    UIImage* artworkImage;
891
    BOOL mediaIsAudioOnly = NO;
892

Felix Paul Kühne's avatar
Felix Paul Kühne committed
893
894
895
#if TARGET_OS_IOS
    MLFile *item;

896
    if (self.mediaList) {
897
        NSArray *matches = [MLFile fileForURL:_mediaPlayer.media.url];
898
        item = matches.firstObject;
899
900
901
902
    }

    if (item) {
        if (item.isAlbumTrack) {
903
904
905
            title = item.albumTrack.title;
            artist = item.albumTrack.artist;
            albumName = item.albumTrack.album.name;
906
        } else
907
            title = item.title;
908
909

        /* MLKit knows better than us if this thing is audio only or not */
910
        mediaIsAudioOnly = [item isSupportedAudioFile];
911
    } else {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
912
#endif
913
914
        NSDictionary * metaDict = _mediaPlayer.media.metaDictionary;

915
916
917
918
919
920
        if (metaDict) {
            title = metaDict[VLCMetaInformationNowPlaying] ? metaDict[VLCMetaInformationNowPlaying] : metaDict[VLCMetaInformationTitle];
            artist = metaDict[VLCMetaInformationArtist];
            albumName = metaDict[VLCMetaInformationAlbum];
            trackNumber = metaDict[VLCMetaInformationTrackNumber];
        }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
921
#if TARGET_OS_IOS
922
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
923
#endif
924
925
926
927
928

    if (!mediaIsAudioOnly) {
        /* either what we are playing is not a file known to MLKit or
         * MLKit fails to acknowledge that it is audio-only.
         * Either way, do a more expensive check to see if it is really audio-only */
929
930
        NSArray *tracks = _mediaPlayer.media.tracksInformation;
        NSUInteger trackCount = tracks.count;
931
        mediaIsAudioOnly = YES;
932
933
        for (NSUInteger x = 0 ; x < trackCount; x++) {
            if ([[tracks[x] objectForKey:VLCMediaTracksInformationType] isEqualToString:VLCMediaTracksInformationTypeVideo]) {
934
                mediaIsAudioOnly = NO;
935
936
937
938
939
                break;
            }
        }
    }

940
    if (mediaIsAudioOnly) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
941
#if TARGET_OS_IOS
942
943
        artworkImage = [VLCThumbnailsCache thumbnailForManagedObject:item];

944
945
946
947
948
        if (artworkImage) {
            if (artist)
                title = [title stringByAppendingFormat:@" — %@", artist];
            if (albumName)
                title = [title stringByAppendingFormat:@" — %@", albumName];
949
        }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
950
#endif
951

952
953
        if (title.length < 1)
            title = [[_mediaPlayer.media url] lastPathComponent];
954
955
956
957
958
    }

    /* populate delegate with metadata info */
    if ([self.delegate respondsToSelector:@selector(displayMetadataForPlaybackController:title:artwork:artist:album:audioOnly:)])
        [self.delegate displayMetadataForPlaybackController:self
959
960
961
962
963
                                                      title:title
                                                    artwork:artworkImage
                                                     artist:artist
                                                      album:albumName
                                                  audioOnly:mediaIsAudioOnly];
964
965
966
967
968
969
970
971

    /* populate now playing info center with metadata information */
    NSMutableDictionary *currentlyPlayingTrackInfo = [NSMutableDictionary dictionary];
    currentlyPlayingTrackInfo[MPMediaItemPropertyPlaybackDuration] = @(_mediaPlayer.media.length.intValue / 1000.);
    currentlyPlayingTrackInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] =  @(_mediaPlayer.time.intValue / 1000.);
    currentlyPlayingTrackInfo[MPNowPlayingInfoPropertyPlaybackRate] = @(_mediaPlayer.isPlaying ? _mediaPlayer.rate : 0.0);

    /* don't leak sensitive information to the OS, if passcode lock is enabled */
Felix Paul Kühne's avatar
Felix Paul Kühne committed
972
#if TARGET_OS_IOS
973
    if (![[VLCKeychainCoordinator defaultCoordinator] passcodeLockEnabled]) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
974
#endif
975
976
977
978
979
980
        if (title)
            currentlyPlayingTrackInfo[MPMediaItemPropertyTitle] = title;
        if (artist.length > 0)
            currentlyPlayingTrackInfo[MPMediaItemPropertyArtist] = artist;
        if (albumName.length > 0)
            currentlyPlayingTrackInfo[MPMediaItemPropertyAlbumTitle] = albumName;
981
982
983
984

        if ([trackNumber intValue] > 0)
            currentlyPlayingTrackInfo[MPMediaItemPropertyAlbumTrackNumber] = trackNumber;

Felix Paul Kühne's avatar
Felix Paul Kühne committed
985
#if TARGET_OS_IOS
986
987
988
989
990
991
992
993
        /* FIXME: UGLY HACK
         * iOS 8.2 and 8.3 include an issue which will lead to a termination of the client app if we set artwork
         * when the playback initialized through the watch extension
         * radar://pending */
        if ([WKInterfaceDevice class] != nil) {
            if ([WKInterfaceDevice currentDevice] != nil)
                goto setstuff;
        }
994
995
        if (artworkImage) {
            MPMediaItemArtwork *mpartwork = [[MPMediaItemArtwork alloc] initWithImage:artworkImage];
996
997
998
            currentlyPlayingTrackInfo[MPMediaItemPropertyArtwork] = mpartwork;
        }
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
999
#endif
1000
1001
1002

setstuff:
    [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = currentlyPlayingTrackInfo;
1003
    [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlaybackControllerPlaybackMetadataDidChange object:self];
1004
1005
1006
1007
1008
1009

    _title = title;
    _artist = artist;
    _albumName = albumName;
    _artworkImage = artworkImage;
    _mediaIsAudioOnly = mediaIsAudioOnly;
1010
1011
}

Felix Paul Kühne's avatar
Felix Paul Kühne committed
1012
#if TARGET_OS_IOS
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
- (void)_recoverLastPlaybackStateOfItem:(MLFile *)item
{
    if (item) {
        if (_mediaPlayer.numberOfAudioTracks > 2) {
            if (item.lastAudioTrack.intValue > 0)
                _mediaPlayer.currentAudioTrackIndex = item.lastAudioTrack.intValue;
        }
        if (_mediaPlayer.numberOfSubtitlesTracks > 2) {
            if (item.lastSubtitleTrack.intValue > 0)
                _mediaPlayer.currentVideoSubTitleIndex = item.lastSubtitleTrack.intValue;
        }

        CGFloat lastPosition = .0;
        NSInteger duration = 0;

        if (item.lastPosition)
            lastPosition = item.lastPosition.floatValue;
        duration = item.duration.intValue;

1032
1033
        if (lastPosition < .95 && _mediaPlayer.position < lastPosition && (duration * lastPosition - duration) < -50000) {
            NSInteger continuePlayback;
1034
            if ([item isAlbumTrack] || [item isSupportedAudioFile])
1035
1036
1037
1038
                continuePlayback = [[[NSUserDefaults standardUserDefaults] objectForKey:kVLCSettingContinueAudioPlayback] integerValue];
            else
                continuePlayback = [[[NSUserDefaults standardUserDefaults] objectForKey:kVLCSettingContinuePlayback] integerValue];

1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
            if (continuePlayback == 1) {
                _mediaPlayer.position = lastPosition;
            } else if (continuePlayback == 0) {
                VLCAlertView *alert = [[VLCAlertView alloc] initWithTitle:NSLocalizedString(@"CONTINUE_PLAYBACK", nil)
                                                                  message:[NSString stringWithFormat:NSLocalizedString(@"CONTINUE_PLAYBACK_LONG", nil), item.title]
                                                                 delegate:self
                                                        cancelButtonTitle:NSLocalizedString(@"BUTTON_CANCEL", nil)
                                                        otherButtonTitles:NSLocalizedString(@"BUTTON_CONTINUE", nil), nil];
                alert.completion = ^(BOOL cancelled, NSInteger buttonIndex) {
                    if (!cancelled) {
                        _mediaPlayer.position = lastPosition;
                    }
                };
                [alert show];
            }
        }
1055
1056
    }
}
Felix Paul Kühne's avatar
Felix Paul Kühne committed
1057
#endif
1058

1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
- (void)recoverDisplayedMetadata
{
    if ([self.delegate respondsToSelector:@selector(displayMetadataForPlaybackController:title:artwork:artist:album:audioOnly:)])
        [self.delegate displayMetadataForPlaybackController:self
                                                      title:_title
                                                    artwork:_artworkImage
                                                     artist:_artist
                                                      album:_albumName
                                                  audioOnly:_mediaIsAudioOnly];
}

1070
1071
1072
1073
1074
1075
1076
1077
- (void)recoverPlaybackState
{
    if ([self.delegate respondsToSelector:@selector(mediaPlayerStateChanged:isPlaying:currentMediaHasTrackToChooseFrom:currentMediaHasChapters:forPlaybackController:)])
        [self.delegate mediaPlayerStateChanged:_mediaPlayer.state
                                     isPlaying:self.isPlaying
              currentMediaHasTrackToChooseFrom:self.currentMediaHasTrackToChooseFrom
                       currentMediaHasChapters:self.currentMediaHasChapters
                         forPlaybackController:self];
1078
1079
    if ([self.delegate respondsToSelector:@selector(prepareForMediaPlayback:)])
        [self.delegate prepareForMediaPlayback:self];
1080
1081
}

1082
1083
1084
1085
1086
1087
1088
1089
1090
- (void)scheduleSleepTimerWithInterval:(NSTimeInterval)timeInterval
{
    if (_sleepTimer) {
        [_sleepTimer invalidate];
        _sleepTimer = nil;
    }
    _sleepTimer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(stopPlayback) userInfo:nil repeats:NO];
}

1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
#pragma mark - remote events

static inline NSArray * RemoteCommandCenterCommandsToHandle(MPRemoteCommandCenter *cc)
{
    /* commmented out other available commands which we don't support now but may
     * support at some point in the future */
    return @[cc.pauseCommand, cc.playCommand, cc.stopCommand, cc.togglePlayPauseCommand,
             cc.nextTrackCommand, cc.previousTrackCommand,
             cc.skipForwardCommand, cc.skipBackwardCommand,
             //             cc.seekForwardCommand, cc.seekBackwardCommand,
             //             cc.ratingCommand,
             cc.changePlaybackRateCommand,
             //             cc.likeCommand,cc.dislikeCommand,cc.bookmarkCommand,
             ];
}

- (void)subscribeRemoteCommands
{
    /* pre iOS 7.1 */
    if (![MPRemoteCommandCenter class]) {
        [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
        return;
    }
    /* for iOS 7.1 and above: */

    MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];

    /*
     * since the control center and lockscreen shows only either skipForward/Backward
     * or next/previousTrack buttons but prefers skip buttons,
     * we only enable skip buttons if we have a no medialist
     */
1123
    BOOL enableSkip = [VLCPlaybackController sharedInstance].mediaList.count <= 1;
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
    commandCenter.skipForwardCommand.enabled = enableSkip;
    commandCenter.skipBackwardCommand.enabled = enableSkip;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSNumber *forwardSkip = [defaults valueForKey:kVLCSettingPlaybackForwardSkipLength];
    commandCenter.skipForwardCommand.preferredIntervals = @[forwardSkip];
    NSNumber *backwardSkip = [defaults valueForKey:kVLCSettingPlaybackBackwardSkipLength];
    commandCenter.skipBackwardCommand.preferredIntervals = @[backwardSkip];

    NSArray *supportedPlaybackRates = @[@(0.5),@(0.75),@(1.0),@(1.25),@(1.5),@(1.75),@(2.0)];
    commandCenter.changePlaybackRateCommand.supportedPlaybackRates = supportedPlaybackRates;

    NSArray *commandsToSubscribe = RemoteCommandCenterCommandsToHandle(commandCenter);
    for (MPRemoteCommand *command in commandsToSubscribe) {
        [command addTarget:self action:@selector(remoteCommandEvent:)];
    }
}

- (void)unsubscribeFromRemoteCommand
{
    /* pre iOS 7.1 */
    if (![MPRemoteCommandCenter class]) {
        [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
        return;
    }

    /* for iOS 7.1 and above: */
    MPRemoteCommandCenter *cc = [MPRemoteCommandCenter sharedCommandCenter];
    NSArray *commmandsToRemoveFrom = RemoteCommandCenterCommandsToHandle(cc);
    for (MPRemoteCommand *command in commmandsToRemoveFrom) {
        [command removeTarget:self];
    }
}

- (MPRemoteCommandHandlerStatus )remoteCommandEvent:(MPRemoteCommandEvent *)event
{
    MPRemoteCommandCenter *cc = [MPRemoteCommandCenter sharedCommandCenter];
    MPRemoteCommandHandlerStatus result = MPRemoteCommandHandlerStatusSuccess;

    if (event.command == cc.pauseCommand) {
        [_listPlayer pause];
    } else if (event.command == cc.playCommand) {
        [_listPlayer play];
    } else if (event.command == cc.stopCommand) {
        [_listPlayer stop];
    } else if (event.command == cc.togglePlayPauseCommand) {
        [self playPause];
    } else if (event.command == cc.nextTrackCommand) {
        result = [_listPlayer next] ? MPRemoteCommandHandlerStatusSuccess : MPRemoteCommandHandlerStatusNoSuchContent;
    } else if (event.command == cc.previousTrackCommand) {
        result = [_listPlayer previous] ? MPRemoteCommandHandlerStatusSuccess : MPRemoteCommandHandlerStatusNoSuchContent;
    } else if (event.command == cc.skipForwardCommand) {
        if ([event isKindOfClass:[MPSkipIntervalCommandEvent class]]) {
            MPSkipIntervalCommandEvent *skipEvent = (MPSkipIntervalCommandEvent *)event;
            [_mediaPlayer jumpForward:skipEvent.interval];
        } else {
            result = MPRemoteCommandHandlerStatusCommandFailed;
        }
    } else if (event.command == cc.skipBackwardCommand) {
        if ([event isKindOfClass:[MPSkipIntervalCommandEvent class]]) {
            MPSkipIntervalCommandEvent *skipEvent = (MPSkipIntervalCommandEvent *)event;
            [_mediaPlayer jumpBackward:skipEvent.interval];
        } else {
            result = MPRemoteCommandHandlerStatusCommandFailed;
        }
    } else if (event.command == cc.changePlaybackRateCommand) {
        if ([event isKindOfClass:[MPChangePlaybackRateCommandEvent class]]) {
            MPChangePlaybackRateCommandEvent *rateEvent = (MPChangePlaybackRateCommandEvent *)event;
            [_mediaPlayer setRate:rateEvent.playbackRate];
        } else {
            result = MPRemoteCommandHandlerStatusCommandFailed;
        }
        /* stubs for when we want to support the other available commands */
        //    } else if (event.command == cc.seekForwardCommand) {
        //    } else if (event.command == cc.seekBackwardCommand) {
        //    } else if (event.command == cc.ratingCommand) {
        //    } else if (event.command == cc.likeCommand) {
        //    } else if (event.command == cc.dislikeCommand) {
        //    } else if (event.command == cc.bookmarkCommand) {
    } else {
        APLog(@"%s Unsupported remote control event: %@",__PRETTY_FUNCTION__,event);
        result = MPRemoteCommandHandlerStatusCommandFailed;
    }

    if (result == MPRemoteCommandHandlerStatusCommandFailed)
        APLog(@"%s Wasn't able to handle remote control event: %@",__PRETTY_FUNCTION__,event);

    return result;
}

- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{
    switch (event.subtype) {
        case UIEventSubtypeRemoteControlPlay:
            [_listPlayer play];
            break;

        case UIEventSubtypeRemoteControlPause:
            [_listPlayer pause];
            break;

        case UIEventSubtypeRemoteControlTogglePlayPause:
            [self playPause];
            break;

        case UIEventSubtypeRemoteControlNextTrack:
            [self forward];
            break;

        case UIEventSubtypeRemoteControlPreviousTrack:
            [self backward];
            break;

        case UIEventSubtypeRemoteControlStop:
            [self stopPlayback];
            break;

        default:
            break;
    }
}

#pragma mark - background interaction

- (void)applicationWillResignActive:(NSNotification *)aNotification
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
1250
#if TARGET_OS_IOS
1251
    [self _savePlaybackState];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
1252
#endif
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263

    if (![[[NSUserDefaults standardUserDefaults] objectForKey:kVLCSettingContinueAudioInBackgroundKey] boolValue]) {
        if ([_mediaPlayer isPlaying]) {
            [_mediaPlayer pause];
            _shouldResumePlaying = YES;
        }
    }
}

- (void)applicationDidEnterBackground:(NSNotification *)notification
{
1264
1265
1266
1267
1268
    _preBackgroundWrapperView = _videoOutputViewWrapper;
    [self setVideoOutputView:nil];

    if (_mediaPlayer.audioTrackIndexes.count > 0)
        _mediaPlayer.currentVideoTrackIndex = -1;
1269
1270
1271
1272
}

- (void)applicationDidBecomeActive:(NSNotification *)notification
{
1273
1274
1275
1276
    if (_preBackgroundWrapperView) {
        [self setVideoOutputView:_preBackgroundWrapperView];
        _preBackgroundWrapperView = nil;
    }
1277
1278

    if (_mediaPlayer.numberOfVideoTracks > 0) {
1279
        /* re-enable video decoding */
1280
1281
        _mediaPlayer.currentVideoTrackIndex = 1;
    }
1282
1283
1284
1285
1286
1287
1288
1289
1290

    if (_shouldResumePlaying) {
        _shouldResumePlaying = NO;
        [_listPlayer play];
    }
}

#pragma mark - helpers

1291
1292
1293
1294
1295
1296
1297
1298
1299
- (NSDictionary *)mediaOptionsDictionary
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    return @{ kVLCSettingNetworkCaching : [defaults objectForKey:kVLCSettingNetworkCaching],
                                       kVLCSettingStretchAudio : [[defaults objectForKey:kVLCSettingStretchAudio] boolValue] ? kVLCSettingStretchAudioOnValue : kVLCSettingStretchAudioOffValue,
                                       kVLCSettingTextEncoding : [defaults objectForKey:kVLCSettingTextEncoding],
                                       kVLCSettingSkipLoopFilter : [defaults objectForKey:kVLCSettingSkipLoopFilter] };
}

1300
@end