VLCMovieViewController.m 47 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/*****************************************************************************
 * VLCMovieViewController.m
 * VLC for iOS
 *****************************************************************************
 * Copyright (c) 2013 VideoLAN. All rights reserved.
 * $Id$
 *
 * Authors: Felix Paul Kühne <fkuehne # videolan.org>
 *          Gleb Pinigin <gpinigin # gmail.com>
 *          Ahmad Harb <harb.dev.leb # gmail.com>
 *          Fabio Ritrovato <sephiroth87 # videolan.org>
 *          Pierre SAGASPE <pierre.sagaspe # me.com>
 *          Jean-Baptiste Kempf <jb # videolan.org>
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/
Felix Paul Kühne's avatar
Felix Paul Kühne committed
17

18
#import "VLCMovieViewController.h"
19
#import "VLCExternalDisplayController.h"
20
#import <AVFoundation/AVFoundation.h>
21
#import <CommonCrypto/CommonDigest.h>
22
#import "UIDevice+SpeedCategory.h"
23
#import "VLCBugreporter.h"
24 25 26

#import "OBSlider.h"
#import "VLCStatusLabel.h"
Felix Paul Kühne's avatar
Felix Paul Kühne committed
27

28
#define INPUT_RATE_DEFAULT  1000.
29 30
#define FORWARD_SWIPE_DURATION 30
#define BACKWARD_SWIPE_DURATION 10
31

32
@interface VLCMovieViewController () <UIGestureRecognizerDelegate, AVAudioSessionDelegate>
33
{
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
    VLCMediaPlayer *_mediaPlayer;

    BOOL _controlsHidden;
    BOOL _videoFiltersHidden;
    BOOL _playbackSpeedViewHidden;

    UIActionSheet *_subtitleActionSheet;
    UIActionSheet *_audiotrackActionSheet;

    float _currentPlaybackRate;
    NSArray *_aspectRatios;
    NSUInteger _currentAspectRatioMask;

    NSTimer *_idleTimer;

49
    BOOL _shouldResumePlaying;
50
    BOOL _viewAppeared;
51
    BOOL _displayRemainingTime;
52
    BOOL _positionSet;
53
    BOOL _playerIsSetup;
54
    BOOL _isScrubbing;
Ahmad Harb's avatar
Ahmad Harb committed
55 56 57 58 59 60

    BOOL _swipeGesturesEnabled;
    NSString * panType;
    UIPanGestureRecognizer *_panRecognizer;
    UISwipeGestureRecognizer *_swipeRecognizerLeft;
    UISwipeGestureRecognizer *_swipeRecognizerRight;
61
    UITapGestureRecognizer *_tapRecognizer;
62 63
}

Felix Paul Kühne's avatar
Felix Paul Kühne committed
64 65
@property (nonatomic, strong) UIPopoverController *masterPopoverController;
@property (nonatomic, strong) UIWindow *externalWindow;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
66 67
@end

68
@implementation VLCMovieViewController
Felix Paul Kühne's avatar
Felix Paul Kühne committed
69

70 71 72 73 74 75 76
+ (void)initialize
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *appDefaults = @{kVLCShowRemainingTime : @(YES)};
    [defaults registerDefaults:appDefaults];
}

Felix Paul Kühne's avatar
Felix Paul Kühne committed
77 78
- (void)dealloc
{
79
    if (_tapRecognizer)
80
        [self.view removeGestureRecognizer:_tapRecognizer];
Ahmad Harb's avatar
Ahmad Harb committed
81
    if (_swipeRecognizerLeft)
82
        [self.view removeGestureRecognizer:_swipeRecognizerLeft];
Ahmad Harb's avatar
Ahmad Harb committed
83
    if (_swipeRecognizerRight)
84
        [self.view removeGestureRecognizer:_swipeRecognizerRight];
Ahmad Harb's avatar
Ahmad Harb committed
85
    if (_panRecognizer)
86
        [self.view removeGestureRecognizer:_panRecognizer];
Ahmad Harb's avatar
Ahmad Harb committed
87

88
    [[NSNotificationCenter defaultCenter] removeObserver:self];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
89 90
}

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

93
- (void)setMediaItem:(id)newMediaItem
Felix Paul Kühne's avatar
Felix Paul Kühne committed
94
{
95 96
    if (_mediaItem != newMediaItem) {
        [self _stopPlayback];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
97
        _mediaItem = newMediaItem;
98 99 100
        if (_viewAppeared)
            [self _startPlayback];
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
101

102
    if (self.masterPopoverController != nil)
Felix Paul Kühne's avatar
Felix Paul Kühne committed
103 104 105
        [self.masterPopoverController dismissPopoverAnimated:YES];
}

106 107
- (void)setUrl:(NSURL *)url
{
108 109 110 111 112
    [self _stopPlayback];
    _url = url;
    _playerIsSetup = NO;
    if (_viewAppeared)
        [self _startPlayback];
113 114
}

115
- (void)viewDidLoad
Felix Paul Kühne's avatar
Felix Paul Kühne committed
116
{
117
    [super viewDidLoad];
118 119
    self.wantsFullScreenLayout = YES;

120 121
    self.videoFilterView.hidden = YES;
    _videoFiltersHidden = YES;
122
    _hueLabel.text = NSLocalizedString(@"VFILTER_HUE", @"");
123 124
    _hueSlider.accessibilityLabel = _hueLabel.text;
    _hueSlider.isAccessibilityElement = YES;
125
    _contrastLabel.text = NSLocalizedString(@"VFILTER_CONTRAST", @"");
126 127
    _contrastSlider.accessibilityLabel = _contrastLabel.text;
    _contrastSlider.isAccessibilityElement = YES;
128
    _brightnessLabel.text = NSLocalizedString(@"VFILTER_BRIGHTNESS", @"");
129 130
    _brightnessSlider.accessibilityLabel = _brightnessLabel.text;
    _brightnessSlider.isAccessibilityElement = YES;
131
    _saturationLabel.text = NSLocalizedString(@"VFILTER_SATURATION", @"");
132 133
    _saturationSlider.accessibilityLabel = _saturationLabel.text;
    _saturationSlider.isAccessibilityElement = YES;
134
    _gammaLabel.text = NSLocalizedString(@"VFILTER_GAMMA", @"");
135 136
    _gammaSlider.accessibilityLabel = _gammaLabel.text;
    _gammaSlider.isAccessibilityElement = YES;
137
    _playbackSpeedLabel.text = NSLocalizedString(@"PLAYBACK_SPEED", @"");
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
    _playbackSpeedSlider.accessibilityLabel = _playbackSpeedLabel.text;
    _playbackSpeedSlider.isAccessibilityElement = YES;

    _positionSlider.accessibilityLabel = NSLocalizedString(@"PLAYBACK_POSITION", @"");
    _positionSlider.isAccessibilityElement = YES;
    _timeDisplay.isAccessibilityElement = YES;

    _audioSwitcherButton.accessibilityLabel = NSLocalizedString(@"CHOOSE_AUDIO_TRACK", @"");
    _audioSwitcherButton.isAccessibilityElement = YES;
    _subtitleSwitcherButton.accessibilityLabel = NSLocalizedString(@"CHOOSE_SUBTITLE_TRACK", @"");
    _subtitleSwitcherButton.isAccessibilityElement = YES;
    _playbackSpeedButton.accessibilityLabel = _playbackSpeedLabel.text;
    _playbackSpeedButton.isAccessibilityElement = YES;
    _videoFilterButton.accessibilityLabel = NSLocalizedString(@"VIDEO_FILTER", @"");
    _videoFilterButton.isAccessibilityElement = YES;
    _resetVideoFilterButton.accessibilityLabel = NSLocalizedString(@"VIDEO_FILTER_RESET_BUTTON", @"");
    _resetVideoFilterButton.isAccessibilityElement = YES;
    _aspectRatioButton.accessibilityLabel = NSLocalizedString(@"VIDEO_ASPECT_RATIO_BUTTON", @"");
    _aspectRatioButton.isAccessibilityElement = YES;
    _playPauseButton.accessibilityLabel = NSLocalizedString(@"PLAY_PAUSE_BUTTON", @"");
    _playPauseButton.isAccessibilityElement = YES;
    _bwdButton.accessibilityLabel = NSLocalizedString(@"BWD_BUTTON", @"");
    _bwdButton.isAccessibilityElement = YES;
    _fwdButton.accessibilityLabel = NSLocalizedString(@"FWD_BUTTON", @"");
    _fwdButton.isAccessibilityElement = YES;
163

164 165
    _scrubHelpLabel.text = NSLocalizedString(@"PLAYBACK_SCRUB_HELP", @"");

166 167
    self.playbackSpeedView.hidden = YES;
    _playbackSpeedViewHidden = YES;
168

169 170 171 172 173
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(handleExternalScreenDidConnect:)
                   name:UIScreenDidConnectNotification object:nil];
    [center addObserver:self selector:@selector(handleExternalScreenDidDisconnect:)
                   name:UIScreenDidDisconnectNotification object:nil];
174 175 176 177 178 179
    [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];
180

181 182
    _playingExternallyTitle.text = NSLocalizedString(@"PLAYING_EXTERNALLY_TITLE", @"");
    _playingExternallyDescription.text = NSLocalizedString(@"PLAYING_EXTERNALLY_DESC", @"");
183
    if ([self hasExternalDisplay])
184
        [self showOnExternalDisplay];
185

186 187
    self.trackNameLabel.text = self.artistNameLabel.text = self.albumNameLabel.text = @"";

188
    _movieView.userInteractionEnabled = NO;
189 190 191 192 193
    UITapGestureRecognizer *tapOnVideoRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleControlsVisible)];
    tapOnVideoRecognizer.delegate = self;
    [self.view addGestureRecognizer:tapOnVideoRecognizer];

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

195 196 197 198
    UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
    pinchRecognizer.delegate = self;
    [self.view addGestureRecognizer:pinchRecognizer];

Ahmad Harb's avatar
Ahmad Harb committed
199 200
    _swipeGesturesEnabled = YES;
    if (_swipeGesturesEnabled) {
201 202
        _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRecognized)];
        [_tapRecognizer setNumberOfTouchesRequired:2];
Ahmad Harb's avatar
Ahmad Harb committed
203 204 205 206 207 208 209 210 211
        _panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panRecognized:)];
        [_panRecognizer setMinimumNumberOfTouches:1];
        [_panRecognizer setMaximumNumberOfTouches:1];

        _swipeRecognizerLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRecognized:)];
        _swipeRecognizerLeft.direction = UISwipeGestureRecognizerDirectionLeft;
        _swipeRecognizerRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRecognized:)];
        _swipeRecognizerRight.direction = UISwipeGestureRecognizerDirectionRight;

212 213 214 215
        [self.view addGestureRecognizer:_swipeRecognizerLeft];
        [self.view addGestureRecognizer:_swipeRecognizerRight];
        [self.view addGestureRecognizer:_panRecognizer];
        [self.view addGestureRecognizer:_tapRecognizer];
Ahmad Harb's avatar
Ahmad Harb committed
216 217 218 219 220 221
        [_panRecognizer requireGestureRecognizerToFail:_swipeRecognizerLeft];
        [_panRecognizer requireGestureRecognizerToFail:_swipeRecognizerRight];

        _panRecognizer.delegate = self;
        _swipeRecognizerRight.delegate = self;
        _swipeRecognizerLeft.delegate = self;
222
        _tapRecognizer.delegate = self;
Ahmad Harb's avatar
Ahmad Harb committed
223
    }
224

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

227 228 229
    [self.aspectRatioButton setBackgroundImage:[UIImage imageNamed:@"ratioButton"] forState:UIControlStateNormal];
    [self.aspectRatioButton setBackgroundImage:[UIImage imageNamed:@"ratioButtonHighlight"] forState:UIControlStateHighlighted];
    [self.aspectRatioButton setImage:[UIImage imageNamed:@"ratioIcon"] forState:UIControlStateNormal];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
230
    if (SYSTEM_RUNS_IOS7_OR_LATER) {
231
        self.backButton.tintColor = [UIColor colorWithRed:(190.0f/255.0f) green:(190.0f/255.0f) blue:(190.0f/255.0f) alpha:1.];
232 233 234
        self.toolbar.tintColor = [UIColor whiteColor];
        self.toolbar.barTintColor = [UIColor colorWithWhite:0.f alpha:1.f];

235
        CGRect rect = self.positionSlider.frame;
236 237 238 239 240 241 242
        rect.origin.y = rect.origin.y - 5.;
        self.positionSlider.frame = rect;
        rect = self.resetVideoFilterButton.frame;
        rect.origin.y = rect.origin.y + 5.;
        self.resetVideoFilterButton.frame = rect;
    } else {
        [self.toolbar setBackgroundImage:[UIImage imageNamed:@"seekbarBg"] forBarMetrics:UIBarMetricsDefault];
243 244 245
        [self.backButton setBackgroundImage:[UIImage imageNamed:@"playbackDoneButton"] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
        [self.backButton setBackgroundImage:[UIImage imageNamed:@"playbackDoneButtonHighlight"] forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
    }
246

247 248 249 250 251 252 253 254 255 256
    /* 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];
257
    [volumeSlider setThumbImage:[UIImage imageNamed:@"volumeballslider"] forState:UIControlStateNormal];
258 259 260
    [volumeSlider addTarget:self
                     action:@selector(volumeSliderAction:)
           forControlEvents:UIControlEventValueChanged];
261

262
    [[AVAudioSession sharedInstance] setDelegate:self];
263 264 265

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
        self.positionSlider.scrubbingSpeedChangePositions = @[@(0.), @(100.), @(200.), @(300)];
266 267

    _playerIsSetup = NO;
268 269 270

    [self.movieView setAccessibilityLabel:NSLocalizedString(@"VO_VIDEOPLAYER_TITLE", @"")];
    [self.movieView setAccessibilityHint:NSLocalizedString(@"VO_VIDEOPLAYER_DOUBLETAP", @"")];
271
}
Felix Paul Kühne's avatar
Felix Paul Kühne committed
272

273 274 275 276 277
- (BOOL)_blobCheck
{
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *directoryPath = searchPaths[0];

278
    if (![[NSFileManager defaultManager] fileExistsAtPath:[directoryPath stringByAppendingPathComponent:@"blob.bin"]])
279 280
        return NO;

281
    NSData *data = [NSData dataWithContentsOfFile:[directoryPath stringByAppendingPathComponent:@"blob.bin"]];
282 283 284 285 286 287 288 289 290 291 292 293 294 295
    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;
}

296 297
- (void)viewWillAppear:(BOOL)animated
{
298
    [super viewWillAppear:animated];
299

300
    [self.navigationController setNavigationBarHidden:YES animated:YES];
301

Felix Paul Kühne's avatar
Felix Paul Kühne committed
302
    if (!SYSTEM_RUNS_IOS7_OR_LATER) {
303 304 305
        if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
            [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackTranslucent;
    }
306

307 308 309 310 311 312 313 314 315 316 317
    [self _startPlayback];

    [self setControlsHidden:NO animated:YES];
    _viewAppeared = YES;
}

- (void)_startPlayback
{
    if (_playerIsSetup)
        return;

318 319
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

320 321
    if (!self.mediaItem && !self.url) {
        [self _stopPlayback];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
322
        return;
323
    }
324

325 326 327
    _mediaPlayer = [[VLCMediaPlayer alloc] initWithOptions:@[[NSString stringWithFormat:@"--%@=%@", kVLCSettingSubtitlesFont, [defaults objectForKey:kVLCSettingSubtitlesFont]], [NSString stringWithFormat:@"--%@=%@", kVLCSettingSubtitlesFontColor, [defaults objectForKey:kVLCSettingSubtitlesFontColor]], [NSString stringWithFormat:@"--%@=%@", kVLCSettingSubtitlesFontSize, [defaults objectForKey:kVLCSettingSubtitlesFontSize]], [NSString stringWithFormat:@"--%@=%@", kVLCSettingDeinterlace, [defaults objectForKey:kVLCSettingDeinterlace]]]];
    [_mediaPlayer setDelegate:self];
    [_mediaPlayer setDrawable:self.movieView];
328
    self.trackNameLabel.text = self.artistNameLabel.text = self.albumNameLabel.text = @"";
329

330
    VLCMedia *media;
331 332
    if (self.mediaItem) {
        self.title = [self.mediaItem title];
333 334 335 336 337 338 339 340
        MLFile *item = self.mediaItem;
        media = [VLCMedia mediaWithURL:[NSURL URLWithString:item.url]];
        item.unread = @(NO);

        if (item.isAlbumTrack) {
            self.trackNameLabel.text = item.albumTrack.title;
            self.artistNameLabel.text = item.albumTrack.artist;
            self.albumNameLabel.text = item.albumTrack.album.name;
341
        }
342
    } else {
343
        media = [VLCMedia mediaWithURL:self.url];
344
        self.title = NSLocalizedString(@"NETWORK_TITLE",nil);
345
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
346

347 348
    [media addOptions:
     @{kVLCSettingStretchAudio :
349
     [[defaults objectForKey:kVLCSettingStretchAudio] boolValue] ? kVLCSettingStretchAudioOnValue : kVLCSettingStretchAudioOffValue, kVLCSettingTextEncoding : [defaults objectForKey:kVLCSettingTextEncoding], kVLCSettingSkipLoopFilter : [defaults objectForKey:kVLCSettingSkipLoopFilter]}];
350

351 352 353 354
    [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"];

355
    if ([tzNames containsObject:tzName] || [[tzName stringByDeletingLastPathComponent] isEqualToString:@"US"]) {
356 357 358 359 360 361 362 363 364 365 366 367
        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 862151013:
368 369
                    case 1684566644:
                    case 2126701:
370
                    {
371 372 373 374
                        if (![self _blobCheck]) {
                            [media addOptions:@{@"no-audio" : [NSNull null]}];
                            APLog(@"audio playback disabled because an unsupported codec was found");
                        }
375 376 377 378 379 380 381 382 383 384
                        break;
                    }

                    default:
                        break;
                }
            }
        }
    }

385 386
    [_mediaPlayer setMedia:media];

387
    self.positionSlider.value = 0.;
388
    [self.timeDisplay setTitle:@"" forState:UIControlStateNormal];
389
    self.timeDisplay.accessibilityLabel = @"";
390

391 392 393 394 395
    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];
396

397 398
    if (![self hasExternalDisplay])
        self.brightnessSlider.value = [UIScreen mainScreen].brightness * 2.;
399 400 401 402 403 404 405 406 407
}

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

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

408
    NSInteger speedCategory = [[UIDevice currentDevice] speedCategory];
409

410
    if (speedCategory == 1) {
411 412
        // iPhone 3GS, iPhone 4, first gen. iPad, 3rd and 4th generation iPod touch
        return (totalNumberOfPixels < 600000); // between 480p and 720p
413
    } else if (speedCategory == 2) {
414 415
        // iPhone 4S, iPad 2 and 3, iPod 4 and 5
        return (totalNumberOfPixels < 922000); // 720p
416
    } else if (speedCategory == 3) {
417 418 419 420 421 422 423 424 425 426 427
        // iPhone 5, iPad 4
        return (totalNumberOfPixels < 2074000); // 1080p
    }

    return YES;
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1)
        [self _playNewMedia];
428 429
    else {
        [self _stopPlayback];
430
        [self closePlayback:nil];
431
    }
432 433 434 435
}

- (void)_playNewMedia
{
436
    NSNumber *playbackPositionInTime = @(0);
437
    if (self.mediaItem.lastPosition && [self.mediaItem.lastPosition floatValue] < .95) {
438 439
        if (self.mediaItem.duration.intValue != 0)
            playbackPositionInTime = @(self.mediaItem.lastPosition.floatValue * (self.mediaItem.duration.intValue / 1000.));
440
    }
441 442 443 444
    if (playbackPositionInTime.intValue > 0) {
        [_mediaPlayer.media addOptions:@{@"start-time": playbackPositionInTime}];
        APLog(@"set starttime to %i", playbackPositionInTime.intValue);
    }
445

446 447 448
    [_mediaPlayer addObserver:self forKeyPath:@"time" options:0 context:nil];
    [_mediaPlayer addObserver:self forKeyPath:@"remainingTime" options:0 context:nil];

Felix Paul Kühne's avatar
Felix Paul Kühne committed
449
    [_mediaPlayer play];
450

451 452 453 454 455 456
    if (self.mediaItem) {
        if (self.mediaItem.lastAudioTrack.intValue > 0)
            _mediaPlayer.currentAudioTrackIndex = self.mediaItem.lastAudioTrack.intValue;
        if (self.mediaItem.lastSubtitleTrack.intValue > 0)
            _mediaPlayer.currentVideoSubTitleIndex = self.mediaItem.lastSubtitleTrack.intValue;
    }
457

458 459
    self.playbackSpeedSlider.value = [self _playbackSpeed];
    [self _updatePlaybackSpeedIndicator];
460

461 462
    [self performSelectorInBackground:@selector(_updateExportedPlaybackInformation) withObject:nil];

463 464
    _currentAspectRatioMask = 0;
    _mediaPlayer.videoAspectRatio =  NULL;
465

466
    [self _resetIdleTimer];
467
    _playerIsSetup = YES;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
468 469
}

470
- (void)viewWillDisappear:(BOOL)animated
Felix Paul Kühne's avatar
Felix Paul Kühne committed
471
{
472
    [self _stopPlayback];
473
    _viewAppeared = NO;
474
    if (_idleTimer) {
475
        [_idleTimer invalidate];
476 477
        _idleTimer = nil;
    }
478
    [self.navigationController setNavigationBarHidden:NO animated:YES];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
479
    if (!SYSTEM_RUNS_IOS7_OR_LATER)
480
        [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackOpaque;
481
    [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
482
    [super viewWillDisappear:animated];
483

Felix Paul Kühne's avatar
Felix Paul Kühne committed
484 485 486 487 488 489
    // 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
490 491
}

492 493 494
- (void)_stopPlayback
{
    if (_mediaPlayer) {
495 496 497 498 499 500 501 502
        @try {
            [_mediaPlayer removeObserver:self forKeyPath:@"time"];
            [_mediaPlayer removeObserver:self forKeyPath:@"remainingTime"];
        }
        @catch (NSException *exception) {
            APLog(@"we weren't an observer yet");
        }

503
        if (_mediaPlayer.media) {
504 505 506
            [_mediaPlayer pause];
            [self _saveCurrentState];
            [_mediaPlayer stop];
507
        }
508 509
        if (_mediaPlayer)
            _mediaPlayer = nil;
510 511 512 513 514 515 516
    }
    if (_mediaItem)
        _mediaItem = nil;

    _playerIsSetup = NO;
}

517 518 519
- (void)_saveCurrentState
{
    if (self.mediaItem) {
520 521 522 523 524 525 526 527 528
        @try {
            MLFile *item = self.mediaItem;
            item.lastPosition = @([_mediaPlayer position]);
            item.lastAudioTrack = @(_mediaPlayer.currentAudioTrackIndex);
            item.lastSubtitleTrack = @(_mediaPlayer.currentVideoSubTitleIndex);
        }
        @catch (NSException *exception) {
            APLog(@"failed to save current media state - file removed?");
        }
529 530 531
    }
}

532 533 534 535 536 537 538 539 540 541 542 543 544 545
#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];
546 547

    [[NSUserDefaults standardUserDefaults] setBool:_displayRemainingTime forKey:kVLCShowRemainingTime];
548 549 550 551 552 553 554 555 556 557 558 559 560
}

- (BOOL)canBecomeFirstResponder
{
    return YES;
}

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

562 563 564 565 566
        case UIEventSubtypeRemoteControlPause:
            [_mediaPlayer pause];
            break;

        case UIEventSubtypeRemoteControlTogglePlayPause:
567
            [self playPause];
568 569 570 571 572 573 574
            break;

        default:
            break;
    }
}

575 576
#pragma mark - controls visibility

577 578 579 580 581 582
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)recognizer
{
    if (recognizer.velocity < 0.)
        [self closePlayback:nil];
}

583 584
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
585
    if (touch.view != self.view)
586 587 588 589 590
        return NO;

    return YES;
}

Ahmad Harb's avatar
Ahmad Harb committed
591 592 593 594 595
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

596
- (void)setControlsHidden:(BOOL)hidden animated:(BOOL)animated
597
{
598
    _controlsHidden = hidden;
599
    CGFloat alpha = _controlsHidden? 0.0f: 1.0f;
600

601 602
    if (!_controlsHidden) {
        _controllerPanel.alpha = 0.0f;
603
        _controllerPanel.hidden = !_videoFiltersHidden;
604 605
        _toolbar.alpha = 0.0f;
        _toolbar.hidden = NO;
606 607
        _videoFilterView.alpha = 0.0f;
        _videoFilterView.hidden = _videoFiltersHidden;
608 609
        _playbackSpeedView.alpha = 0.0f;
        _playbackSpeedView.hidden = _playbackSpeedViewHidden;
610
    }
611

612 613
    void (^animationBlock)() = ^() {
        _controllerPanel.alpha = alpha;
614
        _toolbar.alpha = alpha;
615
        _videoFilterView.alpha = alpha;
616
        _playbackSpeedView.alpha = alpha;
617
    };
618

619
    void (^completionBlock)(BOOL finished) = ^(BOOL finished) {
620
        if (_videoFiltersHidden)
621
            _controllerPanel.hidden = _controlsHidden;
622
        else
623
            _controllerPanel.hidden = NO;
624
        _toolbar.hidden = _controlsHidden;
625
        _videoFilterView.hidden = _videoFiltersHidden;
626
        _playbackSpeedView.hidden = _playbackSpeedViewHidden;
627
    };
628

629 630
    UIStatusBarAnimation animationType = animated? UIStatusBarAnimationFade: UIStatusBarAnimationNone;
    NSTimeInterval animationDuration = animated? 0.3: 0.0;
631 632

    [[UIApplication sharedApplication] setStatusBarHidden:_viewAppeared ? _controlsHidden : NO withAnimation:animationType];
633
    [UIView animateWithDuration:animationDuration animations:animationBlock completion:completionBlock];
634 635

    _volumeView.hidden = _controllerPanel.hidden;
636
}
637

638
- (void)toggleControlsVisible
639
{
640 641 642
    if (_controlsHidden && !_videoFiltersHidden)
        _videoFiltersHidden = YES;

643
    [self setControlsHidden:!_controlsHidden animated:YES];
644 645
}

646
- (void)_resetIdleTimer
647 648
{
    if (!_idleTimer)
649
        _idleTimer = [NSTimer scheduledTimerWithTimeInterval:4.
650 651 652 653 654
                                                      target:self
                                                    selector:@selector(idleTimerExceeded)
                                                    userInfo:nil
                                                     repeats:NO];
    else {
655 656
        if (fabs([_idleTimer.fireDate timeIntervalSinceNow]) < 4.)
            [_idleTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:4.]];
657 658 659 660 661 662 663
    }
}

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

666 667 668 669 670 671
    if (!_videoFiltersHidden)
        _videoFiltersHidden = YES;

    if (!_playbackSpeedViewHidden)
        _playbackSpeedViewHidden = YES;

Felix Paul Kühne's avatar
Felix Paul Kühne committed
672 673
    if (self.scrubIndicatorView.hidden == NO)
        self.scrubIndicatorView.hidden = YES;
674 675
}

676 677
- (UIResponder *)nextResponder
{
678
    [self _resetIdleTimer];
679 680 681
    return [super nextResponder];
}

682 683 684 685
#pragma mark - controls

- (IBAction)closePlayback:(id)sender
{
686
    [self setControlsHidden:NO animated:NO];
687
    [self.navigationController dismissViewControllerAnimated:YES completion:nil];
688 689 690 691
}

- (IBAction)positionSliderAction:(UISlider *)sender
{
692 693 694
    /* 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. */
695
    [self performSelector:@selector(_setPositionForReal) withObject:nil afterDelay:0.3];
696
    VLCTime *newPosition = [VLCTime timeWithInt:(int)(_positionSlider.value * self.mediaItem.duration.intValue)];
697
    [self.timeDisplay setTitle:newPosition.stringValue forState:UIControlStateNormal];
698
    self.timeDisplay.accessibilityLabel = [NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"PLAYBACK_POSITION", @""), newPosition.stringValue];
699
    _positionSet = NO;
700
    [self _resetIdleTimer];
701 702
}

703 704 705 706 707 708 709 710
- (void)_setPositionForReal
{
    if (!_positionSet) {
        _mediaPlayer.position = _positionSlider.value;
        _positionSet = YES;
    }
}

711 712 713 714
- (IBAction)positionSliderTouchDown:(id)sender
{
    [self _updateScrubLabel];
    self.scrubIndicatorView.hidden = NO;
715
    _isScrubbing = YES;
716 717 718 719 720
}

- (IBAction)positionSliderTouchUp:(id)sender
{
    self.scrubIndicatorView.hidden = YES;
721
    _isScrubbing = NO;
722 723 724 725 726 727 728 729 730 731 732 733 734
}

- (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", @"");
735 736

    [self _resetIdleTimer];
737 738 739 740 741 742 743
}

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

744 745 746 747 748
- (IBAction)volumeSliderAction:(id)sender
{
    [self _resetIdleTimer];
}

749 750
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
751 752 753 754
    if (!_isScrubbing) {
        self.positionSlider.value = [_mediaPlayer position];
    }

755
    if (_displayRemainingTime)
756
        [self.timeDisplay setTitle:[[_mediaPlayer remainingTime] stringValue] forState:UIControlStateNormal];
757
    else
758
        [self.timeDisplay setTitle:[[_mediaPlayer time] stringValue] forState:UIControlStateNormal];
759 760
}

761 762
- (void)mediaPlayerStateChanged:(NSNotification *)aNotification
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
763 764 765 766
    VLCMediaPlayerState currentState = _mediaPlayer.state;

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

770 771 772
    if (currentState == VLCMediaPlayerStateEnded || currentState == VLCMediaPlayerStateStopped)
        [self performSelector:@selector(closePlayback:) withObject:nil afterDelay:2.];

773
    UIImage *playPauseImage = [_mediaPlayer isPlaying]? [UIImage imageNamed:@"pauseIcon"] : [UIImage imageNamed:@"playIcon"];
Gleb Pinigin's avatar
Gleb Pinigin committed
774
    [_playPauseButton setImage:playPauseImage forState:UIControlStateNormal];
775 776 777 778 779 780 781 782 783 784

    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;
785 786
}

787
- (IBAction)playPause
788
{
789
    if ([_mediaPlayer isPlaying])
790
        [_mediaPlayer pause];
791
    else
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
        [_mediaPlayer play];
}

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

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

- (IBAction)switchAudioTrack:(id)sender
{
807
    _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
808
    NSArray *audioTracks = [_mediaPlayer audioTrackNames];
809 810
    NSArray *audioTrackIndexes = [_mediaPlayer audioTrackIndexes];

811
    NSUInteger count = [audioTracks count];
812 813 814 815 816
    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
817

818
    [_audiotrackActionSheet addButtonWithTitle:NSLocalizedString(@"BUTTON_CANCEL", @"cancel button")];
819
    [_audiotrackActionSheet setCancelButtonIndex:[_audiotrackActionSheet numberOfButtons] - 1];
820
    [_audiotrackActionSheet showInView:self.audioSwitcherButton];
821 822 823 824
}

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

828
    NSUInteger count = [spuTracks count];
829 830
    if (count <= 1)
        return;
831
    _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
832

833 834 835 836 837 838
    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];
    }

839
    [_subtitleActionSheet addButtonWithTitle:NSLocalizedString(@"BUTTON_CANCEL", @"cancel button")];
840
    [_subtitleActionSheet setCancelButtonIndex:[_subtitleActionSheet numberOfButtons] - 1];
841
    [_subtitleActionSheet showInView: self.subtitleSwitcherButton];
842 843 844
}

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

Felix Paul Kühne's avatar
Felix Paul Kühne committed
848
    NSArray *indexArray;
849
    if (actionSheet == _subtitleActionSheet) {
850 851 852
        indexArray = _mediaPlayer.videoSubTitlesIndexes;
        if (buttonIndex <= indexArray.count) {
            _mediaPlayer.currentVideoSubTitleIndex = [indexArray[buttonIndex] intValue];
853
        }
854
    } else if (actionSheet == _audiotrackActionSheet) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
855
        indexArray = _mediaPlayer.audioTrackIndexes;
856 857
        if (buttonIndex <= indexArray.count) {
            _mediaPlayer.currentAudioTrackIndex = [indexArray[buttonIndex] intValue];
858
        }
859 860 861
    }
}

862
- (IBAction)toggleTimeDisplay:(id)sender
863 864
{
    _displayRemainingTime = !_displayRemainingTime;
865 866

    [self _resetIdleTimer];
867 868
}

Ahmad Harb's avatar
Ahmad Harb committed
869
#pragma mark - multi-touch gestures
870

871 872 873 874 875 876 877
- (void)tapRecognized
{
    if ([_mediaPlayer isPlaying]) {
        [_mediaPlayer pause];
        [self.statusLabel showStatusMessage:@"  ▌▌"];
    } else {
        [_mediaPlayer play];
878
        [self.statusLabel showStatusMessage:@" ►"];
879 880 881
    }
}

882
- (NSString*)detectPanTypeForPan:(UIPanGestureRecognizer*)panRecognizer
Ahmad Harb's avatar
Ahmad Harb committed
883 884 885 886
{
    NSString * type;
    NSString * deviceType = [[UIDevice currentDevice] model];
    type = @"Volume"; // default in case of error
887
    CGPoint location = [panRecognizer locationInView:self.view];
Ahmad Harb's avatar
Ahmad Harb committed
888 889
    CGFloat position = location.x;

890 891 892 893 894 895 896
    UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
    CGRect screenRect = [[UIScreen mainScreen] bounds];
    CGFloat screenWidth = .0;
    if (orientation == UIDeviceOrientationPortrait)
        screenWidth = screenRect.size.width;
    else
        screenWidth = screenRect.size.height;
Ahmad Harb's avatar
Ahmad Harb committed
897

898 899 900
    if (position < screenWidth / 2)
        type = @"Brightness";
    if (position > screenWidth / 2)