VLCMovieViewController.m 34.8 KB
Newer Older
Felix Paul Kühne's avatar
Felix Paul Kühne committed
1
//
2
//  VLCMovieViewController.m
Felix Paul Kühne's avatar
Felix Paul Kühne committed
3
4
5
6
7
//  AspenProject
//
//  Created by Felix Paul Kühne on 27.02.13.
//  Copyright (c) 2013 VideoLAN. All rights reserved.
//
8
9
//  Refer to the COPYING file of the official project for license.
//
Felix Paul Kühne's avatar
Felix Paul Kühne committed
10

11
#import "VLCMovieViewController.h"
12
#import "VLCExternalDisplayController.h"
13
#import <AVFoundation/AVFoundation.h>
14
#import <CommonCrypto/CommonDigest.h>
15
#import "UIDevice+SpeedCategory.h"
16
#import "VLCBugreporter.h"
Felix Paul Kühne's avatar
Felix Paul Kühne committed
17

18
19
#define INPUT_RATE_DEFAULT  1000.

20
@interface VLCMovieViewController () <UIGestureRecognizerDelegate, AVAudioSessionDelegate>
21
{
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    VLCMediaPlayer *_mediaPlayer;

    BOOL _controlsHidden;
    BOOL _videoFiltersHidden;
    BOOL _playbackSpeedViewHidden;

    UIActionSheet *_subtitleActionSheet;
    UIActionSheet *_audiotrackActionSheet;

    float _currentPlaybackRate;
    NSArray *_aspectRatios;
    NSUInteger _currentAspectRatioMask;

    NSTimer *_idleTimer;

37
    BOOL _shouldResumePlaying;
38
    BOOL _viewAppeared;
39
    BOOL _displayRemainingTime;
40
    BOOL _positionSet;
41
42
}

Felix Paul Kühne's avatar
Felix Paul Kühne committed
43
44
@property (nonatomic, strong) UIPopoverController *masterPopoverController;
@property (nonatomic, strong) UIWindow *externalWindow;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
45
46
@end

47
@implementation VLCMovieViewController
Felix Paul Kühne's avatar
Felix Paul Kühne committed
48

49
50
51
52
53
54
55
+ (void)initialize
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *appDefaults = @{kVLCShowRemainingTime : @(YES)};
    [defaults registerDefaults:appDefaults];
}

Felix Paul Kühne's avatar
Felix Paul Kühne committed
56
57
- (void)dealloc
{
58
    [[NSNotificationCenter defaultCenter] removeObserver:self];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
59
60
}

61
#pragma mark - Managing the media item
Felix Paul Kühne's avatar
Felix Paul Kühne committed
62

63
- (void)setMediaItem:(id)newMediaItem
Felix Paul Kühne's avatar
Felix Paul Kühne committed
64
{
65
    if (_mediaItem != newMediaItem)
Felix Paul Kühne's avatar
Felix Paul Kühne committed
66
        _mediaItem = newMediaItem;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
67

68
    if (self.masterPopoverController != nil)
Felix Paul Kühne's avatar
Felix Paul Kühne committed
69
70
71
        [self.masterPopoverController dismissPopoverAnimated:YES];
}

72
- (void)viewDidLoad
Felix Paul Kühne's avatar
Felix Paul Kühne committed
73
{
74
    [super viewDidLoad];
75
76
    self.wantsFullScreenLayout = YES;

77
78
    self.videoFilterView.hidden = YES;
    _videoFiltersHidden = YES;
79
80
81
82
83
    _hueLabel.text = NSLocalizedString(@"VFILTER_HUE", @"");
    _contrastLabel.text = NSLocalizedString(@"VFILTER_CONTRAST", @"");
    _brightnessLabel.text = NSLocalizedString(@"VFILTER_BRIGHTNESS", @"");
    _saturationLabel.text = NSLocalizedString(@"VFILTER_SATURATION", @"");
    _gammaLabel.text = NSLocalizedString(@"VFILTER_GAMMA", @"");
84
    _playbackSpeedLabel.text = NSLocalizedString(@"PLAYBACK_SPEED", @"");
85

86
87
    _scrubHelpLabel.text = NSLocalizedString(@"PLAYBACK_SCRUB_HELP", @"");

88
89
    self.playbackSpeedView.hidden = YES;
    _playbackSpeedViewHidden = YES;
90

91
92
93
94
95
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(handleExternalScreenDidConnect:)
                   name:UIScreenDidConnectNotification object:nil];
    [center addObserver:self selector:@selector(handleExternalScreenDidDisconnect:)
                   name:UIScreenDidDisconnectNotification object:nil];
96
97
98
99
100
101
    [center addObserver:self selector:@selector(applicationWillResignActive:)
                   name:UIApplicationWillResignActiveNotification object:nil];
    [center addObserver:self selector:@selector(applicationDidBecomeActive:)
                   name:UIApplicationDidBecomeActiveNotification object:nil];
    [center addObserver:self selector:@selector(applicationDidEnterBackground:)
                   name:UIApplicationDidEnterBackgroundNotification object:nil];
102

103
104
    _playingExternallyTitle.text = NSLocalizedString(@"PLAYING_EXTERNALLY_TITLE", @"");
    _playingExternallyDescription.text = NSLocalizedString(@"PLAYING_EXTERNALLY_DESC", @"");
105
    if ([self hasExternalDisplay])
106
        [self showOnExternalDisplay];
107
108

    _movieView.userInteractionEnabled = NO;
109
110
111
112
113
    UITapGestureRecognizer *tapOnVideoRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleControlsVisible)];
    tapOnVideoRecognizer.delegate = self;
    [self.view addGestureRecognizer:tapOnVideoRecognizer];

    _displayRemainingTime = [[[NSUserDefaults standardUserDefaults] objectForKey:kVLCShowRemainingTime] boolValue];
114

115
116
117
118
    UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
    pinchRecognizer.delegate = self;
    [self.view addGestureRecognizer:pinchRecognizer];

119
#if 0 // FIXME: trac #8742
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    UISwipeGestureRecognizer *leftSwipeRecognizer = [[VLCHorizontalSwipeGestureRecognizer alloc] initWithTarget:self action:nil];
    leftSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
    leftSwipeRecognizer.delegate = self;
    [self.view addGestureRecognizer:leftSwipeRecognizer];
    UISwipeGestureRecognizer *rightSwipeRecognizer = [[VLCHorizontalSwipeGestureRecognizer alloc] initWithTarget:self action:nil];
    rightSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
    rightSwipeRecognizer.delegate = self;
    [self.view addGestureRecognizer:rightSwipeRecognizer];
    UISwipeGestureRecognizer *upSwipeRecognizer = [[VLCVerticalSwipeGestureRecognizer alloc] initWithTarget:self action:nil];
    upSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionUp;
    upSwipeRecognizer.delegate = self;
    [self.view addGestureRecognizer:upSwipeRecognizer];
    UISwipeGestureRecognizer *downSwipeRecognizer = [[VLCVerticalSwipeGestureRecognizer alloc] initWithTarget:self action:nil];
    downSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
    downSwipeRecognizer.delegate = self;
    [self.view addGestureRecognizer:downSwipeRecognizer];
136
#endif
137

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

140
141
142
    [self.aspectRatioButton setBackgroundImage:[UIImage imageNamed:@"ratioButton"] forState:UIControlStateNormal];
    [self.aspectRatioButton setBackgroundImage:[UIImage imageNamed:@"ratioButtonHighlight"] forState:UIControlStateHighlighted];
    [self.aspectRatioButton setImage:[UIImage imageNamed:@"ratioIcon"] forState:UIControlStateNormal];
143
    [self.toolbar setBackgroundImage:[UIImage imageNamed:@"seekbarBg"] forBarMetrics:UIBarMetricsDefault];
144
145
    [self.backButton setBackgroundImage:[UIImage imageNamed:@"playbackDoneButton"] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    [self.backButton setBackgroundImage:[UIImage imageNamed:@"playbackDoneButtonHighlight"] forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
146

147
148
149
150
151
152
153
154
155
156
    /* this looks a bit weird, but we need to support iOS 5 and should show the same appearance */
    UISlider *volumeSlider = nil;
    for (id aView in self.volumeView.subviews){
        if ([[[aView class] description] isEqualToString:@"MPVolumeSlider"]){
            volumeSlider = (UISlider *)aView;
            break;
        }
    }
    [volumeSlider setMinimumTrackImage:[[UIImage imageNamed:@"sliderminiValue"]resizableImageWithCapInsets:UIEdgeInsetsMake(0, 4, 0, 0)] forState:UIControlStateNormal];
    [volumeSlider setMaximumTrackImage:[[UIImage imageNamed:@"slidermaxValue"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 4)] forState:UIControlStateNormal];
157
    [volumeSlider setThumbImage:[UIImage imageNamed:@"volumeballslider"] forState:UIControlStateNormal];
158
159
160
    [volumeSlider addTarget:self
                     action:@selector(volumeSliderAction:)
           forControlEvents:UIControlEventValueChanged];
161

162
    [[AVAudioSession sharedInstance] setDelegate:self];
163
164
165

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
        self.positionSlider.scrubbingSpeedChangePositions = @[@(0.), @(100.), @(200.), @(300)];
166
}
Felix Paul Kühne's avatar
Felix Paul Kühne committed
167

168
169
170
171
172
- (BOOL)_blobCheck
{
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *directoryPath = searchPaths[0];

173
    if (![[NSFileManager defaultManager] fileExistsAtPath:[directoryPath stringByAppendingPathComponent:@"blob.bin"]])
174
175
        return NO;

176
    NSData *data = [NSData dataWithContentsOfFile:[directoryPath stringByAppendingPathComponent:@"blob.bin"]];
177
178
179
180
181
182
183
184
185
186
187
188
189
190
    uint8_t digest[CC_SHA1_DIGEST_LENGTH];
    CC_SHA1(data.bytes, data.length, digest);

    NSMutableString *hash = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];

    for (unsigned int u = 0; u < CC_SHA1_DIGEST_LENGTH; u++)
        [hash appendFormat:@"%02x", digest[u]];

    if ([hash isEqualToString:kBlobHash])
        return YES;
    else
        return NO;
}

191
192
- (void)viewWillAppear:(BOOL)animated
{
193
    _mediaPlayer = [[VLCMediaPlayer alloc] init];
194
195
196
    [_mediaPlayer setDelegate:self];
    [_mediaPlayer setDrawable:self.movieView];

197
    [self.navigationController setNavigationBarHidden:YES animated:YES];
198
199
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
        [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackTranslucent;
200

201
    if (!self.mediaItem && !self.url)
Felix Paul Kühne's avatar
Felix Paul Kühne committed
202
        return;
203

204
    VLCMedia *media;
205
206
    if (self.mediaItem) {
        self.title = [self.mediaItem title];
207
        media = [VLCMedia mediaWithURL:[NSURL URLWithString:self.mediaItem.url]];
208
        self.mediaItem.unread = @(NO);
209
    } else {
210
        media = [VLCMedia mediaWithURL:self.url];
211
212
        self.title = @"Network Stream";
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
213

214
215
216
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [media addOptions:
     @{kVLCSettingStretchAudio :
217
           [[defaults objectForKey:kVLCSettingStretchAudio] boolValue] ? kVLCSettingStretchAudioOnValue : kVLCSettingStretchAudioOffValue, kVLCSettingTextEncoding : [defaults objectForKey:kVLCSettingTextEncoding], kVLCSettingSkipLoopFilter : [defaults objectForKey:kVLCSettingSkipLoopFilter]}];
218

219
220
221
222
    [NSTimeZone resetSystemTimeZone];
    NSString *tzName = [[NSTimeZone systemTimeZone] name];
    NSArray *tzNames = @[@"America/Adak", @"America/Anchorage", @"America/Boise", @"America/Chicago", @"America/Denver", @"America/Detroit", @"America/Indiana/Indianapolis", @"America/Indiana/Knox", @"America/Indiana/Marengo", @"America/Indiana/Petersburg", @"America/Indiana/Tell_City", @"America/Indiana/Vevay", @"America/Indiana/Vincennes", @"America/Indiana/Winamac", @"America/Juneau", @"America/Kentucky/Louisville", @"America/Kentucky/Monticello", @"America/Los_Angeles", @"America/Menominee", @"America/Metlakatla", @"America/New_York", @"America/Nome", @"America/North_Dakota/Beulah", @"America/North_Dakota/Center", @"America/North_Dakota/New_Salem", @"America/Phoenix", @"America/Puerto_Rico", @"America/Shiprock", @"America/Sitka", @"America/St_Thomas", @"America/Thule", @"America/Yakutat", @"Pacific/Guam", @"Pacific/Honolulu", @"Pacific/Johnston", @"Pacific/Kwajalein", @"Pacific/Midway", @"Pacific/Pago_Pago", @"Pacific/Saipan", @"Pacific/Wake"];

223
    if ([tzNames containsObject:tzName] || [[tzName stringByDeletingLastPathComponent] isEqualToString:@"US"]) {
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
        NSArray *tracksInfo = media.tracksInformation;
        for (NSUInteger x = 0; x < tracksInfo.count; x++) {
            if ([[tracksInfo[x] objectForKey:VLCMediaTracksInformationType] isEqualToString:VLCMediaTracksInformationTypeAudio])
            {
                NSInteger fourcc = [[tracksInfo[x] objectForKey:VLCMediaTracksInformationCodec] integerValue];

                switch (fourcc) {
                    case 540161377:
                    case 1647457633:
                    case 858612577:
                    case 862151027:
                    case 2126701:
                    case 544437348:
                    case 542331972:
                    case 1651733604:
                    case 1668510820:
                    case 1702065252:
                    case 1752396900:
                    case 1819505764:
                    case 18903917:
                    case 862151013:
                    {
246
247
248
249
                        if (![self _blobCheck]) {
                            [media addOptions:@{@"no-audio" : [NSNull null]}];
                            APLog(@"audio playback disabled because an unsupported codec was found");
                        }
250
251
252
253
254
255
256
257
258
259
                        break;
                    }

                    default:
                        break;
                }
            }
        }
    }

260
261
    [_mediaPlayer setMedia:media];

262
    self.positionSlider.value = 0.;
263
    [self.timeDisplay setTitle:@"" forState:UIControlStateNormal];
264

265
266
267
268
269
270
271
    [super viewWillAppear:animated];

    if (![self _isMediaSuitableForDevice]) {
        UIAlertView * alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"DEVICE_TOOSLOW_TITLE", @"") message:[NSString stringWithFormat:NSLocalizedString(@"DEVICE_TOOSLOW", @""), [[UIDevice currentDevice] model], self.mediaItem.title] delegate:self cancelButtonTitle:NSLocalizedString(@"BUTTON_CANCEL", @"") otherButtonTitles:NSLocalizedString(@"BUTTON_OPEN", @""), nil];
        [alert show];
    } else
        [self _playNewMedia];
272

273
274
275
    if (![self hasExternalDisplay])
        self.brightnessSlider.value = [UIScreen mainScreen].brightness * 2.;

276
277
    [self setControlsHidden:NO animated:YES];
    _viewAppeared = YES;
278
279
280
281
282
283
284
285
286
}

- (BOOL)_isMediaSuitableForDevice
{
    if (!self.mediaItem)
        return YES;

    NSUInteger totalNumberOfPixels = [[[self.mediaItem videoTrack] valueForKey:@"width"] doubleValue] * [[[self.mediaItem videoTrack] valueForKey:@"height"] doubleValue];

287
    NSInteger speedCategory = [[UIDevice currentDevice] speedCategory];
288

289
    if (speedCategory == 1) {
290
291
        // iPhone 3GS, iPhone 4, first gen. iPad, 3rd and 4th generation iPod touch
        return (totalNumberOfPixels < 600000); // between 480p and 720p
292
    } else if (speedCategory == 2) {
293
294
        // iPhone 4S, iPad 2 and 3, iPod 4 and 5
        return (totalNumberOfPixels < 922000); // 720p
295
    } else if (speedCategory == 3) {
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
        // iPhone 5, iPad 4
        return (totalNumberOfPixels < 2074000); // 1080p
    }

    return YES;
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1)
        [self _playNewMedia];
    else
        [self closePlayback:nil];
}

- (void)_playNewMedia
{
313
    NSNumber *playbackPositionInTime = @(0);
314
    if (self.mediaItem.lastPosition && [self.mediaItem.lastPosition floatValue] < .95) {
315
316
        if (self.mediaItem.duration.intValue != 0)
            playbackPositionInTime = @(self.mediaItem.lastPosition.floatValue * (self.mediaItem.duration.intValue / 1000.));
317
    }
318
319
    [_mediaPlayer.media addOptions:@{@"start-time": playbackPositionInTime}];
    APLog(@"set starttime to %i", playbackPositionInTime.intValue);
320

Felix Paul Kühne's avatar
Felix Paul Kühne committed
321
    [_mediaPlayer play];
322

323
324
325
326
327
    if (self.mediaItem.lastAudioTrack.intValue > 0)
        _mediaPlayer.currentAudioTrackIndex = self.mediaItem.lastAudioTrack.intValue;
    if (self.mediaItem.lastSubtitleTrack.intValue > 0)
        _mediaPlayer.currentVideoSubTitleIndex = self.mediaItem.lastSubtitleTrack.intValue;

328
329
    self.playbackSpeedSlider.value = [self _playbackSpeed];
    [self _updatePlaybackSpeedIndicator];
330

331
332
    _currentAspectRatioMask = 0;
    _mediaPlayer.videoAspectRatio =  NULL;
333

334
    [self _resetIdleTimer];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
335
336
}

337
- (void)viewWillDisappear:(BOOL)animated
Felix Paul Kühne's avatar
Felix Paul Kühne committed
338
{
339
    _viewAppeared = NO;
340
    if (_idleTimer) {
341
        [_idleTimer invalidate];
342
343
        _idleTimer = nil;
    }
344
    [self.navigationController setNavigationBarHidden:NO animated:YES];
345
    [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackOpaque;
346
    [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
347
348
    [_mediaPlayer pause];
    [super viewWillDisappear:animated];
349
    [self _saveCurrentState];
350
    [_mediaPlayer stop];
351
    _mediaPlayer = nil; // save memory and some CPU time
352

Felix Paul Kühne's avatar
Felix Paul Kühne committed
353
354
355
356
357
358
    // hide filter UI for next run
    if (!_videoFiltersHidden)
        _videoFiltersHidden = YES;

    if (!_playbackSpeedViewHidden)
        _playbackSpeedViewHidden = YES;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
359
360
361
362
363
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
364
    if (self)
365
        self.title = @"Video Playback";
Felix Paul Kühne's avatar
Felix Paul Kühne committed
366
367
    return self;
}
368

369
370
371
372
373
374
375
376
377
- (void)_saveCurrentState
{
    if (self.mediaItem) {
        self.mediaItem.lastPosition = @([_mediaPlayer position]);
        self.mediaItem.lastAudioTrack = @(_mediaPlayer.currentAudioTrackIndex);
        self.mediaItem.lastSubtitleTrack = @(_mediaPlayer.currentVideoSubTitleIndex);
    }
}

378
379
380
381
382
383
384
385
386
387
388
389
390
391
#pragma mark - remote events

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    [self becomeFirstResponder];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    [self resignFirstResponder];
392
393

    [[NSUserDefaults standardUserDefaults] setBool:_displayRemainingTime forKey:kVLCShowRemainingTime];
394
395
396
397
398
399
400
}

- (BOOL)canBecomeFirstResponder
{
    return YES;
}

401
402
403
404
405
406
407

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
    if (motion == UIEventSubtypeMotionShake)
        [[VLCBugreporter sharedInstance] handleBugreportRequest];
}

408
409
410
411
412
413
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{
    switch (event.subtype) {
        case UIEventSubtypeRemoteControlPlay:
            [_mediaPlayer play];
            break;
414

415
416
417
418
419
        case UIEventSubtypeRemoteControlPause:
            [_mediaPlayer pause];
            break;

        case UIEventSubtypeRemoteControlTogglePlayPause:
420
            [self playPause];
421
422
423
424
425
426
427
            break;

        default:
            break;
    }
}

428
429
#pragma mark - controls visibility

430
431
432
433
434
435
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)recognizer
{
    if (recognizer.velocity < 0.)
        [self closePlayback:nil];
}

436
437
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
438
    if (touch.view != self.view)
439
440
441
442
443
        return NO;

    return YES;
}

444
- (void)setControlsHidden:(BOOL)hidden animated:(BOOL)animated
445
{
446
    _controlsHidden = hidden;
447
    CGFloat alpha = _controlsHidden? 0.0f: 1.0f;
448

449
450
    if (!_controlsHidden) {
        _controllerPanel.alpha = 0.0f;
451
        _controllerPanel.hidden = !_videoFiltersHidden;
452
453
        _toolbar.alpha = 0.0f;
        _toolbar.hidden = NO;
454
455
        _videoFilterView.alpha = 0.0f;
        _videoFilterView.hidden = _videoFiltersHidden;
456
457
        _playbackSpeedView.alpha = 0.0f;
        _playbackSpeedView.hidden = _playbackSpeedViewHidden;
458
    }
459

460
461
    void (^animationBlock)() = ^() {
        _controllerPanel.alpha = alpha;
462
        _toolbar.alpha = alpha;
463
        _videoFilterView.alpha = alpha;
464
        _playbackSpeedView.alpha = alpha;
465
    };
466

467
    void (^completionBlock)(BOOL finished) = ^(BOOL finished) {
468
        if (_videoFiltersHidden)
469
            _controllerPanel.hidden = _controlsHidden;
470
        else
471
            _controllerPanel.hidden = NO;
472
        _toolbar.hidden = _controlsHidden;
473
        _videoFilterView.hidden = _videoFiltersHidden;
474
        _playbackSpeedView.hidden = _playbackSpeedViewHidden;
475
    };
476

477
478
    UIStatusBarAnimation animationType = animated? UIStatusBarAnimationFade: UIStatusBarAnimationNone;
    NSTimeInterval animationDuration = animated? 0.3: 0.0;
479
480

    [[UIApplication sharedApplication] setStatusBarHidden:_viewAppeared ? _controlsHidden : NO withAnimation:animationType];
481
    [UIView animateWithDuration:animationDuration animations:animationBlock completion:completionBlock];
482
483

    _volumeView.hidden = _controllerPanel.hidden;
484
}
485

486
- (void)toggleControlsVisible
487
{
488
489
490
    if (_controlsHidden && !_videoFiltersHidden)
        _videoFiltersHidden = YES;

491
    [self setControlsHidden:!_controlsHidden animated:YES];
492
493
}

494
- (void)_resetIdleTimer
495
496
{
    if (!_idleTimer)
497
        _idleTimer = [NSTimer scheduledTimerWithTimeInterval:4.
498
499
500
501
502
                                                      target:self
                                                    selector:@selector(idleTimerExceeded)
                                                    userInfo:nil
                                                     repeats:NO];
    else {
503
504
        if (fabs([_idleTimer.fireDate timeIntervalSinceNow]) < 4.)
            [_idleTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:4.]];
505
506
507
508
509
510
511
    }
}

- (void)idleTimerExceeded
{
    _idleTimer = nil;
    if (!_controlsHidden)
512
        [self toggleControlsVisible];
513

514
515
516
517
518
519
    if (!_videoFiltersHidden)
        _videoFiltersHidden = YES;

    if (!_playbackSpeedViewHidden)
        _playbackSpeedViewHidden = YES;

Felix Paul Kühne's avatar
Felix Paul Kühne committed
520
521
    if (self.scrubIndicatorView.hidden == NO)
        self.scrubIndicatorView.hidden = YES;
522
523
}

524
525
- (UIResponder *)nextResponder
{
526
    [self _resetIdleTimer];
527
528
529
    return [super nextResponder];
}

530
531
532
533
#pragma mark - controls

- (IBAction)closePlayback:(id)sender
{
534
    [self setControlsHidden:NO animated:NO];
535
536
537
538
539
    [self.navigationController popViewControllerAnimated:YES];
}

- (IBAction)positionSliderAction:(UISlider *)sender
{
540
541
542
    /* we need to limit the number of events sent by the slider, since otherwise, the user
     * wouldn't see the I-frames when seeking on current mobile devices. This isn't a problem
     * within the Simulator, but especially on older ARMv7 devices, it's clearly noticeable. */
543
    [self performSelector:@selector(_setPositionForReal) withObject:nil afterDelay:0.3];
544
    VLCTime *newPosition = [VLCTime timeWithInt:(int)(_positionSlider.value * self.mediaItem.duration.intValue)];
545
    [self.timeDisplay setTitle:newPosition.stringValue forState:UIControlStateNormal];
546
    _positionSet = NO;
547
    [self _resetIdleTimer];
548
549
}

550
551
552
553
554
555
556
557
- (void)_setPositionForReal
{
    if (!_positionSet) {
        _mediaPlayer.position = _positionSlider.value;
        _positionSet = YES;
    }
}

558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
- (IBAction)positionSliderTouchDown:(id)sender
{
    [self _updateScrubLabel];
    self.scrubIndicatorView.hidden = NO;
}

- (IBAction)positionSliderTouchUp:(id)sender
{
    self.scrubIndicatorView.hidden = YES;
}

- (void)_updateScrubLabel
{
    float speed = self.positionSlider.scrubbingSpeed;
    if (speed == 1.)
        self.currentScrubSpeedLabel.text = NSLocalizedString(@"PLAYBACK_SCRUB_HIGH", @"");
    else if (speed == .5)
        self.currentScrubSpeedLabel.text = NSLocalizedString(@"PLAYBACK_SCRUB_HALF", @"");
    else if (speed == .25)
        self.currentScrubSpeedLabel.text = NSLocalizedString(@"PLAYBACK_SCRUB_QUARTER", @"");
    else
        self.currentScrubSpeedLabel.text = NSLocalizedString(@"PLAYBACK_SCRUB_FINE", @"");
580
581

    [self _resetIdleTimer];
582
583
584
585
586
587
588
}

- (IBAction)positionSliderDrag:(id)sender
{
    [self _updateScrubLabel];
}

589
590
591
592
593
- (IBAction)volumeSliderAction:(id)sender
{
    [self _resetIdleTimer];
}

594
595
- (void)mediaPlayerTimeChanged:(NSNotification *)aNotification {
    self.positionSlider.value = [_mediaPlayer position];
596
    if (_displayRemainingTime)
597
        [self.timeDisplay setTitle:[[_mediaPlayer remainingTime] stringValue] forState:UIControlStateNormal];
598
    else
599
        [self.timeDisplay setTitle:[[_mediaPlayer time] stringValue] forState:UIControlStateNormal];
600
601
}

602
603
- (void)mediaPlayerStateChanged:(NSNotification *)aNotification
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
604
605
606
607
    VLCMediaPlayerState currentState = _mediaPlayer.state;

    if (currentState == VLCMediaPlayerStateError) {
        [self.statusLabel showStatusMessage:NSLocalizedString(@"PLAYBACK_FAILED", @"")];
608
        [self performSelector:@selector(closePlayback:) withObject:nil afterDelay:2.];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
609
610
    }

611
612
613
    if (currentState == VLCMediaPlayerStateEnded || currentState == VLCMediaPlayerStateStopped)
        [self performSelector:@selector(closePlayback:) withObject:nil afterDelay:2.];

614
    UIImage *playPauseImage = [_mediaPlayer isPlaying]? [UIImage imageNamed:@"pauseIcon"] : [UIImage imageNamed:@"playIcon"];
Gleb Pinigin's avatar
Gleb Pinigin committed
615
    [_playPauseButton setImage:playPauseImage forState:UIControlStateNormal];
616
617
618
619
620
621
622
623
624
625

    if ([[_mediaPlayer audioTrackIndexes] count] > 2)
        self.audioSwitcherButton.hidden = NO;
    else
        self.audioSwitcherButton.hidden = YES;

    if ([[_mediaPlayer videoSubTitlesIndexes] count] > 1)
        self.subtitleSwitcherButton.hidden = NO;
    else
        self.subtitleSwitcherButton.hidden = YES;
626
627
}

628
- (IBAction)playPause
629
{
630
    if ([_mediaPlayer isPlaying])
631
        [_mediaPlayer pause];
632
    else
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
        [_mediaPlayer play];
}

- (IBAction)forward:(id)sender
{
    [_mediaPlayer mediumJumpForward];
}

- (IBAction)backward:(id)sender
{
    [_mediaPlayer mediumJumpBackward];
}

- (IBAction)switchAudioTrack:(id)sender
{
648
    _audiotrackActionSheet = [[UIActionSheet alloc] initWithTitle:NSLocalizedString(@"CHOOSE_AUDIO_TRACK", @"audio track selector") delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles: nil];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
649
    NSArray *audioTracks = [_mediaPlayer audioTrackNames];
650
651
    NSArray *audioTrackIndexes = [_mediaPlayer audioTrackIndexes];

652
    NSUInteger count = [audioTracks count];
653
654
655
656
657
    for (NSUInteger i = 0; i < count; i++) {
        NSString *indexIndicator = ([audioTrackIndexes[i] intValue] == [_mediaPlayer currentAudioTrackIndex])? @"\u2713": @"";
        NSString *buttonTitle = [NSString stringWithFormat:@"%@ %@", indexIndicator, audioTracks[i]];
        [_audiotrackActionSheet addButtonWithTitle:buttonTitle];
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
658

659
    [_audiotrackActionSheet addButtonWithTitle:NSLocalizedString(@"BUTTON_CANCEL", @"cancel button")];
660
    [_audiotrackActionSheet setCancelButtonIndex:[_audiotrackActionSheet numberOfButtons] - 1];
661
    [_audiotrackActionSheet showInView:self.audioSwitcherButton];
662
663
664
665
}

- (IBAction)switchSubtitleTrack:(id)sender
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
666
    NSArray *spuTracks = [_mediaPlayer videoSubTitlesNames];
667
    NSArray *spuTrackIndexes = [_mediaPlayer videoSubTitlesIndexes];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
668

669
    NSUInteger count = [spuTracks count];
670
671
    if (count <= 1)
        return;
672
    _subtitleActionSheet = [[UIActionSheet alloc] initWithTitle:NSLocalizedString(@"CHOOSE_SUBTITLE_TRACK", @"subtitle track selector") delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles: nil];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
673

674
675
676
677
678
679
    for (NSUInteger i = 0; i < count; i++) {
        NSString *indexIndicator = ([spuTrackIndexes[i] intValue] == [_mediaPlayer currentVideoSubTitleIndex])? @"\u2713": @"";
        NSString *buttonTitle = [NSString stringWithFormat:@"%@ %@", indexIndicator, spuTracks[i]];
        [_subtitleActionSheet addButtonWithTitle:buttonTitle];
    }

680
    [_subtitleActionSheet addButtonWithTitle:NSLocalizedString(@"BUTTON_CANCEL", @"cancel button")];
681
    [_subtitleActionSheet setCancelButtonIndex:[_subtitleActionSheet numberOfButtons] - 1];
682
    [_subtitleActionSheet showInView: self.subtitleSwitcherButton];
683
684
685
}

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
686
687
    if (buttonIndex == [actionSheet cancelButtonIndex])
        return;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
688

Felix Paul Kühne's avatar
Felix Paul Kühne committed
689
    NSArray *indexArray;
690
    if (actionSheet == _subtitleActionSheet) {
691
692
693
        indexArray = _mediaPlayer.videoSubTitlesIndexes;
        if (buttonIndex <= indexArray.count) {
            _mediaPlayer.currentVideoSubTitleIndex = [indexArray[buttonIndex] intValue];
694
        }
695
    } else if (actionSheet == _audiotrackActionSheet) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
696
        indexArray = _mediaPlayer.audioTrackIndexes;
697
698
        if (buttonIndex <= indexArray.count) {
            _mediaPlayer.currentAudioTrackIndex = [indexArray[buttonIndex] intValue];
699
        }
700
701
702
    }
}

703
- (IBAction)toggleTimeDisplay:(id)sender
704
705
{
    _displayRemainingTime = !_displayRemainingTime;
706
707

    [self _resetIdleTimer];
708
709
}

710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
#pragma mark - swipe gestures

- (void)horizontalSwipePercentage:(CGFloat)percentage inView:(UIView *)view
{
    if (percentage != 0.) {
        _mediaPlayer.position = _mediaPlayer.position + percentage;
    }
}

- (void)verticalSwipePercentage:(CGFloat)percentage inView:(UIView *)view half:(NSUInteger)half
{
    if (percentage != 0.) {
        if (half > 0) {
            CGFloat currentValue = self.brightnessSlider.value;
            currentValue = currentValue + percentage;
            self.brightnessSlider.value = currentValue;
            if ([self hasExternalDisplay])
                _mediaPlayer.brightness = currentValue;
            else
                [[UIScreen mainScreen] setBrightness:currentValue / 2];
        } else
            NSLog(@"volume setting through swipe not implemented");//_mediaPlayer.audio.volume = percentage * 200;
    }
}

735
736
737
738
#pragma mark - Video Filter UI

- (IBAction)videoFilterToggle:(id)sender
{
739
740
    if (!_playbackSpeedViewHidden)
        self.playbackSpeedView.hidden = _playbackSpeedViewHidden = YES;
741

742
743
744
745
746
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
        if (!_controlsHidden)
            self.controllerPanel.hidden = _controlsHidden = YES;
    }

747
748
749
750
751
752
753
754
755
756
    self.videoFilterView.hidden = !_videoFiltersHidden;
    _videoFiltersHidden = self.videoFilterView.hidden;
}

- (IBAction)videoFilterSliderAction:(id)sender
{
    if (sender == self.hueSlider)
        _mediaPlayer.hue = (int)self.hueSlider.value;
    else if (sender == self.contrastSlider)
        _mediaPlayer.contrast = self.contrastSlider.value;
757
758
759
760
761
762
    else if (sender == self.brightnessSlider) {
        if ([self hasExternalDisplay])
            _mediaPlayer.brightness = self.brightnessSlider.value;
        else
            [[UIScreen mainScreen] setBrightness:(self.brightnessSlider.value / 2.)];
    } else if (sender == self.saturationSlider)
763
764
765
        _mediaPlayer.saturation = self.saturationSlider.value;
    else if (sender == self.gammaSlider)
        _mediaPlayer.gamma = self.gammaSlider.value;
766
767
768
769
    else if (sender == self.resetVideoFilterButton) {
        _mediaPlayer.hue = self.hueSlider.value = 0.;
        _mediaPlayer.contrast = self.contrastSlider.value = 1.;
        _mediaPlayer.brightness = self.brightnessSlider.value = 1.;
770
        [[UIScreen mainScreen] setBrightness:(self.brightnessSlider.value / 2.)];
771
772
773
        _mediaPlayer.saturation = self.saturationSlider.value = 1.;
        _mediaPlayer.gamma = self.gammaSlider.value = 1.;
    } else
774
        APLog(@"unknown sender for videoFilterSliderAction");
775
    [self _resetIdleTimer];
776
777
}

778
779
780
781
782
783
784
785
786
#pragma mark - playback view
- (IBAction)playbackSpeedSliderAction:(UISlider *)sender
{
    double speed = pow(2, sender.value / 17.);
    float rate = INPUT_RATE_DEFAULT / speed;
    if (_currentPlaybackRate != rate)
        [_mediaPlayer setRate:INPUT_RATE_DEFAULT / rate];
    _currentPlaybackRate = rate;
    [self _updatePlaybackSpeedIndicator];
787
    [self _resetIdleTimer];
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
}

- (void)_updatePlaybackSpeedIndicator
{
    float f_value = self.playbackSpeedSlider.value;
    double speed =  pow(2, f_value / 17.);
    self.playbackSpeedIndicator.text = [NSString stringWithFormat:@"%.2fx", speed];
}

- (float)_playbackSpeed
{
    float f_rate = _mediaPlayer.rate;

    double value = 17 * log(f_rate) / log(2.);
    float returnValue = (int) ((value > 0) ? value + .5 : value - .5);

    if (returnValue < -34.)
        returnValue = -34.;
    else if (returnValue > 34.)
        returnValue = 34.;

    _currentPlaybackRate = returnValue;
    return returnValue;
}

- (IBAction)videoDimensionAction:(id)sender
{
815
    if (sender == self.playbackSpeedButton) {
816
        if (!_videoFiltersHidden)
817
            self.videoFilterView.hidden = _videoFiltersHidden = YES;
818

819
820
        self.playbackSpeedView.hidden = !_playbackSpeedViewHidden;
        _playbackSpeedViewHidden = self.playbackSpeedView.hidden;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
821
        [self _resetIdleTimer];
822
    } else if (sender == self.aspectRatioButton) {
823
824
825
826
        NSUInteger count = [_aspectRatios count];

        if (_currentAspectRatioMask + 1 > count - 1) {
            _mediaPlayer.videoAspectRatio = NULL;
827
            _mediaPlayer.videoCropGeometry = NULL;
828
            _currentAspectRatioMask = 0;
829
            [self.statusLabel showStatusMessage:[NSString stringWithFormat:NSLocalizedString(@"AR_CHANGED", @""), NSLocalizedString(@"DEFAULT", @"")]];
830
831
        } else {
            _currentAspectRatioMask++;
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857

            if ([_aspectRatios[_currentAspectRatioMask] isEqualToString:@"FILL_TO_SCREEN"]) {
                UIScreen *screen;
                if (![self 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
                else if (f_ar == .75) // all iPads
                    _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);

                [self.statusLabel showStatusMessage:NSLocalizedString(@"FILL_TO_SCREEN", @"")];
                return;
            }

            _mediaPlayer.videoCropGeometry = NULL;
858
            _mediaPlayer.videoAspectRatio = (char *)[_aspectRatios[_currentAspectRatioMask] UTF8String];
859
            [self.statusLabel showStatusMessage:[NSString stringWithFormat:NSLocalizedString(@"AR_CHANGED", @""), _aspectRatios[_currentAspectRatioMask]]];
860
        }
861
862
863
    }
}

864
#pragma mark - background interaction
Felix Paul Kühne's avatar
Felix Paul Kühne committed
865

866
- (void)applicationWillResignActive:(NSNotification *)aNotification
867
{
868
    [self _saveCurrentState];
869

870
    if (![[[NSUserDefaults standardUserDefaults] objectForKey:kVLCSettingContinueAudioInBackgroundKey] boolValue]) {
871
872
        [_mediaPlayer pause];
        _shouldResumePlaying = YES;
873
874
    } else
        _mediaPlayer.currentVideoTrackIndex = 0;
875
876
877
878
879
880
881
882
883
884
885
886
}

- (void)applicationDidEnterBackground:(NSNotification *)notification
{
    _shouldResumePlaying = NO;
}

- (void)applicationDidBecomeActive:(NSNotification *)notification
{
    if (_shouldResumePlaying) {
        _shouldResumePlaying = NO;
        [_mediaPlayer play];
887
888
    } else
        _mediaPlayer.currentVideoTrackIndex = 1;
889
890
}

891
892
893
894
#pragma mark - autorotation

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
    return UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad
895
           || toInterfaceOrientation != UIInterfaceOrientationPortraitUpsideDown;
896
897
}

898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
#pragma mark - AVSession delegate
- (void)beginInterruption
{
    if ([[[NSUserDefaults standardUserDefaults] objectForKey:kVLCSettingContinueAudioInBackgroundKey] boolValue])
        _shouldResumePlaying = YES;

    [_mediaPlayer pause];
}

- (void)endInterruption
{
    if (_shouldResumePlaying) {
        [_mediaPlayer play];
        _shouldResumePlaying = NO;
    }
}

915
916
917
918
919
920
921
922
923
#pragma mark - External Display

- (BOOL)hasExternalDisplay
{
    return ([[UIScreen screens] count] > 1);
}

- (void)showOnExternalDisplay
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
924
    UIScreen *screen = [UIScreen screens][1];
925
926
    screen.overscanCompensation = UIScreenOverscanCompensationInsetApplicationFrame;

Felix Paul Kühne's avatar
Felix Paul Kühne committed
927
    self.externalWindow = [[UIWindow alloc] initWithFrame:screen.bounds];
928
929
930
931
932
933
934

    UIViewController *controller = [[VLCExternalDisplayController alloc] init];
    self.externalWindow.rootViewController = controller;
    [controller.view addSubview:_movieView];
    controller.view.frame = screen.bounds;
    _movieView.frame = screen.bounds;

935
    self.playingExternallyView.hidden = NO;
936
937
938
939
940
941
942
943
944
945
    self.externalWindow.screen = screen;
    self.externalWindow.hidden = NO;
}

- (void)hideFromExternalDisplay
{
    [self.view addSubview:_movieView];
    [self.view sendSubviewToBack:_movieView];
    _movieView.frame = self.view.frame;

946
    self.playingExternallyView.hidden = YES;
947
948
949
950
951
952
953
954
955
956
957
958
959
960
    self.externalWindow.hidden = YES;
    self.externalWindow = nil;
}

- (void)handleExternalScreenDidConnect:(NSNotification *)notification
{
    [self showOnExternalDisplay];
}

- (void)handleExternalScreenDidDisconnect:(NSNotification *)notification
{
    [self hideFromExternalDisplay];
}

Felix Paul Kühne's avatar
Felix Paul Kühne committed
961
@end