VLCMovieViewController.m 39.9 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"
17
#import <MediaPlayer/MediaPlayer.h>
Felix Paul Kühne's avatar
Felix Paul Kühne committed
18

19 20
#define INPUT_RATE_DEFAULT  1000.

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

    BOOL _controlsHidden;
    BOOL _videoFiltersHidden;
    BOOL _playbackSpeedViewHidden;

    UIActionSheet *_subtitleActionSheet;
    UIActionSheet *_audiotrackActionSheet;

    float _currentPlaybackRate;
    NSArray *_aspectRatios;
    NSUInteger _currentAspectRatioMask;

    NSTimer *_idleTimer;

38
    BOOL _shouldResumePlaying;
39
    BOOL _viewAppeared;
40
    BOOL _displayRemainingTime;
41
    BOOL _positionSet;
42
    BOOL _playerIsSetup;
43
    BOOL _isScrubbing;
44 45
}

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

50
@implementation VLCMovieViewController
Felix Paul Kühne's avatar
Felix Paul Kühne committed
51

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

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

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

66
- (void)setMediaItem:(id)newMediaItem
Felix Paul Kühne's avatar
Felix Paul Kühne committed
67
{
68 69
    if (_mediaItem != newMediaItem) {
        [self _stopPlayback];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
70
        _mediaItem = newMediaItem;
71 72 73
        if (_viewAppeared)
            [self _startPlayback];
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
74

75
    if (self.masterPopoverController != nil)
Felix Paul Kühne's avatar
Felix Paul Kühne committed
76 77 78
        [self.masterPopoverController dismissPopoverAnimated:YES];
}

79 80 81 82 83 84 85 86 87 88
- (void)setUrl:(NSURL *)url
{
    if (_url != url) {
        [self _stopPlayback];
        _url = url;
        if (_viewAppeared)
            [self _startPlayback];
    }
}

89
- (void)viewDidLoad
Felix Paul Kühne's avatar
Felix Paul Kühne committed
90
{
91
    [super viewDidLoad];
92 93
    self.wantsFullScreenLayout = YES;

94 95
    self.videoFilterView.hidden = YES;
    _videoFiltersHidden = YES;
96
    _hueLabel.text = NSLocalizedString(@"VFILTER_HUE", @"");
97 98
    _hueSlider.accessibilityLabel = _hueLabel.text;
    _hueSlider.isAccessibilityElement = YES;
99
    _contrastLabel.text = NSLocalizedString(@"VFILTER_CONTRAST", @"");
100 101
    _contrastSlider.accessibilityLabel = _contrastLabel.text;
    _contrastSlider.isAccessibilityElement = YES;
102
    _brightnessLabel.text = NSLocalizedString(@"VFILTER_BRIGHTNESS", @"");
103 104
    _brightnessSlider.accessibilityLabel = _brightnessLabel.text;
    _brightnessSlider.isAccessibilityElement = YES;
105
    _saturationLabel.text = NSLocalizedString(@"VFILTER_SATURATION", @"");
106 107
    _saturationSlider.accessibilityLabel = _saturationLabel.text;
    _saturationSlider.isAccessibilityElement = YES;
108
    _gammaLabel.text = NSLocalizedString(@"VFILTER_GAMMA", @"");
109 110
    _gammaSlider.accessibilityLabel = _gammaLabel.text;
    _gammaSlider.isAccessibilityElement = YES;
111
    _playbackSpeedLabel.text = NSLocalizedString(@"PLAYBACK_SPEED", @"");
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
    _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;
137

138 139
    _scrubHelpLabel.text = NSLocalizedString(@"PLAYBACK_SCRUB_HELP", @"");

140 141
    self.playbackSpeedView.hidden = YES;
    _playbackSpeedViewHidden = YES;
142

143 144 145 146 147
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(handleExternalScreenDidConnect:)
                   name:UIScreenDidConnectNotification object:nil];
    [center addObserver:self selector:@selector(handleExternalScreenDidDisconnect:)
                   name:UIScreenDidDisconnectNotification object:nil];
148 149 150 151 152 153
    [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];
154

155 156
    _playingExternallyTitle.text = NSLocalizedString(@"PLAYING_EXTERNALLY_TITLE", @"");
    _playingExternallyDescription.text = NSLocalizedString(@"PLAYING_EXTERNALLY_DESC", @"");
157
    if ([self hasExternalDisplay])
158
        [self showOnExternalDisplay];
159

160 161
    self.trackNameLabel.text = self.artistNameLabel.text = self.albumNameLabel.text = @"";

162
    _movieView.userInteractionEnabled = NO;
163 164 165 166 167
    UITapGestureRecognizer *tapOnVideoRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleControlsVisible)];
    tapOnVideoRecognizer.delegate = self;
    [self.view addGestureRecognizer:tapOnVideoRecognizer];

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

169 170 171 172
    UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
    pinchRecognizer.delegate = self;
    [self.view addGestureRecognizer:pinchRecognizer];

173
#if 0 // FIXME: trac #8742
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
    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];
190
#endif
191

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

194 195 196
    [self.aspectRatioButton setBackgroundImage:[UIImage imageNamed:@"ratioButton"] forState:UIControlStateNormal];
    [self.aspectRatioButton setBackgroundImage:[UIImage imageNamed:@"ratioButtonHighlight"] forState:UIControlStateHighlighted];
    [self.aspectRatioButton setImage:[UIImage imageNamed:@"ratioIcon"] forState:UIControlStateNormal];
197
    [self.toolbar setBackgroundImage:[UIImage imageNamed:@"seekbarBg"] forBarMetrics:UIBarMetricsDefault];
198 199
    [self.backButton setBackgroundImage:[UIImage imageNamed:@"playbackDoneButton"] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    [self.backButton setBackgroundImage:[UIImage imageNamed:@"playbackDoneButtonHighlight"] forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
200

201 202 203 204 205 206 207 208 209 210
    /* 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];
211
    [volumeSlider setThumbImage:[UIImage imageNamed:@"volumeballslider"] forState:UIControlStateNormal];
212 213 214
    [volumeSlider addTarget:self
                     action:@selector(volumeSliderAction:)
           forControlEvents:UIControlEventValueChanged];
215

216
    [[AVAudioSession sharedInstance] setDelegate:self];
217 218 219

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

    _playerIsSetup = NO;
222 223 224

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

227 228 229 230 231
- (BOOL)_blobCheck
{
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *directoryPath = searchPaths[0];

232
    if (![[NSFileManager defaultManager] fileExistsAtPath:[directoryPath stringByAppendingPathComponent:@"blob.bin"]])
233 234
        return NO;

235
    NSData *data = [NSData dataWithContentsOfFile:[directoryPath stringByAppendingPathComponent:@"blob.bin"]];
236 237 238 239 240 241 242 243 244 245 246 247 248 249
    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;
}

250 251
- (void)viewWillAppear:(BOOL)animated
{
252
    [super viewWillAppear:animated];
253

254
    [self.navigationController setNavigationBarHidden:YES animated:YES];
255

256 257
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
        [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackTranslucent;
258

259 260 261 262 263 264 265 266 267 268 269
    [self _startPlayback];

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

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

270 271
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

272
    _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]]]];
273 274 275 276 277
    [_mediaPlayer setDelegate:self];
    [_mediaPlayer setDrawable:self.movieView];

    if (!self.mediaItem && !self.url) {
        [self _stopPlayback];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
278
        return;
279
    }
280

281
    VLCMedia *media;
282 283
    if (self.mediaItem) {
        self.title = [self.mediaItem title];
284
        media = [VLCMedia mediaWithURL:[NSURL URLWithString:self.mediaItem.url]];
285
        self.mediaItem.unread = @(NO);
286 287 288 289 290 291 292

        if (self.mediaItem.isAlbumTrack) {
            self.trackNameLabel.text = self.mediaItem.albumTrack.title;
            self.artistNameLabel.text = self.mediaItem.albumTrack.artist;
            self.albumNameLabel.text = self.mediaItem.albumTrack.album.name;
        } else
            self.trackNameLabel.text = self.artistNameLabel.text = self.albumNameLabel.text = @"";
293
    } else {
294
        media = [VLCMedia mediaWithURL:self.url];
295 296
        self.title = @"Network Stream";
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
297

298 299
    [media addOptions:
     @{kVLCSettingStretchAudio :
300
     [[defaults objectForKey:kVLCSettingStretchAudio] boolValue] ? kVLCSettingStretchAudioOnValue : kVLCSettingStretchAudioOffValue, kVLCSettingTextEncoding : [defaults objectForKey:kVLCSettingTextEncoding], kVLCSettingSkipLoopFilter : [defaults objectForKey:kVLCSettingSkipLoopFilter]}];
301

302 303 304 305
    [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"];

306
    if ([tzNames containsObject:tzName] || [[tzName stringByDeletingLastPathComponent] isEqualToString:@"US"]) {
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
        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:
                    {
329 330 331 332
                        if (![self _blobCheck]) {
                            [media addOptions:@{@"no-audio" : [NSNull null]}];
                            APLog(@"audio playback disabled because an unsupported codec was found");
                        }
333 334 335 336 337 338 339 340 341 342
                        break;
                    }

                    default:
                        break;
                }
            }
        }
    }

343 344
    [_mediaPlayer setMedia:media];

345
    self.positionSlider.value = 0.;
346
    [self.timeDisplay setTitle:@"" forState:UIControlStateNormal];
347
    self.timeDisplay.accessibilityLabel = @"";
348

349 350 351 352 353
    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];
354

355 356
    if (![self hasExternalDisplay])
        self.brightnessSlider.value = [UIScreen mainScreen].brightness * 2.;
357 358 359 360 361 362 363 364 365
}

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

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

366
    NSInteger speedCategory = [[UIDevice currentDevice] speedCategory];
367

368
    if (speedCategory == 1) {
369 370
        // iPhone 3GS, iPhone 4, first gen. iPad, 3rd and 4th generation iPod touch
        return (totalNumberOfPixels < 600000); // between 480p and 720p
371
    } else if (speedCategory == 2) {
372 373
        // iPhone 4S, iPad 2 and 3, iPod 4 and 5
        return (totalNumberOfPixels < 922000); // 720p
374
    } else if (speedCategory == 3) {
375 376 377 378 379 380 381 382 383 384 385
        // iPhone 5, iPad 4
        return (totalNumberOfPixels < 2074000); // 1080p
    }

    return YES;
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1)
        [self _playNewMedia];
386 387
    else {
        [self _stopPlayback];
388
        [self closePlayback:nil];
389
    }
390 391 392 393
}

- (void)_playNewMedia
{
394
    NSNumber *playbackPositionInTime = @(0);
395
    if (self.mediaItem.lastPosition && [self.mediaItem.lastPosition floatValue] < .95) {
396 397
        if (self.mediaItem.duration.intValue != 0)
            playbackPositionInTime = @(self.mediaItem.lastPosition.floatValue * (self.mediaItem.duration.intValue / 1000.));
398
    }
399 400
    [_mediaPlayer.media addOptions:@{@"start-time": playbackPositionInTime}];
    APLog(@"set starttime to %i", playbackPositionInTime.intValue);
401

Felix Paul Kühne's avatar
Felix Paul Kühne committed
402
    [_mediaPlayer play];
403

404 405 406 407 408 409
    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;
    }
410

411 412
    self.playbackSpeedSlider.value = [self _playbackSpeed];
    [self _updatePlaybackSpeedIndicator];
413

414 415
    [self performSelectorInBackground:@selector(_updateExportedPlaybackInformation) withObject:nil];

416 417
    _currentAspectRatioMask = 0;
    _mediaPlayer.videoAspectRatio =  NULL;
418

419
    [self _resetIdleTimer];
420
    _playerIsSetup = YES;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
421 422
}

423
- (void)viewWillDisappear:(BOOL)animated
Felix Paul Kühne's avatar
Felix Paul Kühne committed
424
{
425
    [self _stopPlayback];
426
    _viewAppeared = NO;
427
    if (_idleTimer) {
428
        [_idleTimer invalidate];
429 430
        _idleTimer = nil;
    }
431
    [self.navigationController setNavigationBarHidden:NO animated:YES];
432
    [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleBlackOpaque;
433
    [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
434
    [super viewWillDisappear:animated];
435

Felix Paul Kühne's avatar
Felix Paul Kühne committed
436 437 438 439 440 441
    // 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
442 443
}

444 445 446 447 448 449 450 451 452 453 454 455 456 457
- (void)_stopPlayback
{
    if (_mediaPlayer) {
        [_mediaPlayer pause];
        [self _saveCurrentState];
        [_mediaPlayer stop];
        _mediaPlayer = nil; // save memory and some CPU time
    }
    if (_mediaItem)
        _mediaItem = nil;

    _playerIsSetup = NO;
}

458 459 460 461 462 463 464 465 466
- (void)_saveCurrentState
{
    if (self.mediaItem) {
        self.mediaItem.lastPosition = @([_mediaPlayer position]);
        self.mediaItem.lastAudioTrack = @(_mediaPlayer.currentAudioTrackIndex);
        self.mediaItem.lastSubtitleTrack = @(_mediaPlayer.currentVideoSubTitleIndex);
    }
}

467 468 469 470 471 472 473 474 475 476 477 478 479 480
#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];
481 482

    [[NSUserDefaults standardUserDefaults] setBool:_displayRemainingTime forKey:kVLCShowRemainingTime];
483 484 485 486 487 488 489
}

- (BOOL)canBecomeFirstResponder
{
    return YES;
}

490 491 492 493 494 495 496

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

497 498 499 500 501 502
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{
    switch (event.subtype) {
        case UIEventSubtypeRemoteControlPlay:
            [_mediaPlayer play];
            break;
503

504 505 506 507 508
        case UIEventSubtypeRemoteControlPause:
            [_mediaPlayer pause];
            break;

        case UIEventSubtypeRemoteControlTogglePlayPause:
509
            [self playPause];
510 511 512 513 514 515 516
            break;

        default:
            break;
    }
}

517 518
#pragma mark - controls visibility

519 520 521 522 523 524
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)recognizer
{
    if (recognizer.velocity < 0.)
        [self closePlayback:nil];
}

525 526
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
527
    if (touch.view != self.view)
528 529 530 531 532
        return NO;

    return YES;
}

533
- (void)setControlsHidden:(BOOL)hidden animated:(BOOL)animated
534
{
535
    _controlsHidden = hidden;
536
    CGFloat alpha = _controlsHidden? 0.0f: 1.0f;
537

538 539
    if (!_controlsHidden) {
        _controllerPanel.alpha = 0.0f;
540
        _controllerPanel.hidden = !_videoFiltersHidden;
541 542
        _toolbar.alpha = 0.0f;
        _toolbar.hidden = NO;
543 544
        _videoFilterView.alpha = 0.0f;
        _videoFilterView.hidden = _videoFiltersHidden;
545 546
        _playbackSpeedView.alpha = 0.0f;
        _playbackSpeedView.hidden = _playbackSpeedViewHidden;
547
    }
548

549 550
    void (^animationBlock)() = ^() {
        _controllerPanel.alpha = alpha;
551
        _toolbar.alpha = alpha;
552
        _videoFilterView.alpha = alpha;
553
        _playbackSpeedView.alpha = alpha;
554
    };
555

556
    void (^completionBlock)(BOOL finished) = ^(BOOL finished) {
557
        if (_videoFiltersHidden)
558
            _controllerPanel.hidden = _controlsHidden;
559
        else
560
            _controllerPanel.hidden = NO;
561
        _toolbar.hidden = _controlsHidden;
562
        _videoFilterView.hidden = _videoFiltersHidden;
563
        _playbackSpeedView.hidden = _playbackSpeedViewHidden;
564
    };
565

566 567
    UIStatusBarAnimation animationType = animated? UIStatusBarAnimationFade: UIStatusBarAnimationNone;
    NSTimeInterval animationDuration = animated? 0.3: 0.0;
568 569

    [[UIApplication sharedApplication] setStatusBarHidden:_viewAppeared ? _controlsHidden : NO withAnimation:animationType];
570
    [UIView animateWithDuration:animationDuration animations:animationBlock completion:completionBlock];
571 572

    _volumeView.hidden = _controllerPanel.hidden;
573
}
574

575
- (void)toggleControlsVisible
576
{
577 578 579
    if (_controlsHidden && !_videoFiltersHidden)
        _videoFiltersHidden = YES;

580
    [self setControlsHidden:!_controlsHidden animated:YES];
581 582
}

583
- (void)_resetIdleTimer
584 585
{
    if (!_idleTimer)
586
        _idleTimer = [NSTimer scheduledTimerWithTimeInterval:4.
587 588 589 590 591
                                                      target:self
                                                    selector:@selector(idleTimerExceeded)
                                                    userInfo:nil
                                                     repeats:NO];
    else {
592 593
        if (fabs([_idleTimer.fireDate timeIntervalSinceNow]) < 4.)
            [_idleTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:4.]];
594 595 596 597 598 599 600
    }
}

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

603 604 605 606 607 608
    if (!_videoFiltersHidden)
        _videoFiltersHidden = YES;

    if (!_playbackSpeedViewHidden)
        _playbackSpeedViewHidden = YES;

Felix Paul Kühne's avatar
Felix Paul Kühne committed
609 610
    if (self.scrubIndicatorView.hidden == NO)
        self.scrubIndicatorView.hidden = YES;
611 612
}

613 614
- (UIResponder *)nextResponder
{
615
    [self _resetIdleTimer];
616 617 618
    return [super nextResponder];
}

619 620 621 622
#pragma mark - controls

- (IBAction)closePlayback:(id)sender
{
623
    [self setControlsHidden:NO animated:NO];
624 625 626 627 628
    [self.navigationController popViewControllerAnimated:YES];
}

- (IBAction)positionSliderAction:(UISlider *)sender
{
629 630 631
    /* 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. */
632
    [self performSelector:@selector(_setPositionForReal) withObject:nil afterDelay:0.3];
633
    VLCTime *newPosition = [VLCTime timeWithInt:(int)(_positionSlider.value * self.mediaItem.duration.intValue)];
634
    [self.timeDisplay setTitle:newPosition.stringValue forState:UIControlStateNormal];
635
    self.timeDisplay.accessibilityLabel = [NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"PLAYBACK_POSITION", @""), newPosition.stringValue];
636
    _positionSet = NO;
637
    [self _resetIdleTimer];
638 639
}

640 641 642 643 644 645 646 647
- (void)_setPositionForReal
{
    if (!_positionSet) {
        _mediaPlayer.position = _positionSlider.value;
        _positionSet = YES;
    }
}

648 649 650 651
- (IBAction)positionSliderTouchDown:(id)sender
{
    [self _updateScrubLabel];
    self.scrubIndicatorView.hidden = NO;
652
    _isScrubbing = YES;
653 654 655 656 657
}

- (IBAction)positionSliderTouchUp:(id)sender
{
    self.scrubIndicatorView.hidden = YES;
658
    _isScrubbing = NO;
659 660 661 662 663 664 665 666 667 668 669 670 671
}

- (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", @"");
672 673

    [self _resetIdleTimer];
674 675 676 677 678 679 680
}

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

681 682 683 684 685
- (IBAction)volumeSliderAction:(id)sender
{
    [self _resetIdleTimer];
}

686
- (void)mediaPlayerTimeChanged:(NSNotification *)aNotification {
687 688 689 690
    if (!_isScrubbing) {
        self.positionSlider.value = [_mediaPlayer position];
    }

691
    if (_displayRemainingTime)
692
        [self.timeDisplay setTitle:[[_mediaPlayer remainingTime] stringValue] forState:UIControlStateNormal];
693
    else
694
        [self.timeDisplay setTitle:[[_mediaPlayer time] stringValue] forState:UIControlStateNormal];
695 696
}

697 698
- (void)mediaPlayerStateChanged:(NSNotification *)aNotification
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
699 700 701 702
    VLCMediaPlayerState currentState = _mediaPlayer.state;

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

706 707 708
    if (currentState == VLCMediaPlayerStateEnded || currentState == VLCMediaPlayerStateStopped)
        [self performSelector:@selector(closePlayback:) withObject:nil afterDelay:2.];

709
    UIImage *playPauseImage = [_mediaPlayer isPlaying]? [UIImage imageNamed:@"pauseIcon"] : [UIImage imageNamed:@"playIcon"];
Gleb Pinigin's avatar
Gleb Pinigin committed
710
    [_playPauseButton setImage:playPauseImage forState:UIControlStateNormal];
711 712 713 714 715 716 717 718 719 720

    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;
721 722
}

723
- (IBAction)playPause
724
{
725
    if ([_mediaPlayer isPlaying])
726
        [_mediaPlayer pause];
727
    else
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742
        [_mediaPlayer play];
}

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

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

- (IBAction)switchAudioTrack:(id)sender
{
743
    _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
744
    NSArray *audioTracks = [_mediaPlayer audioTrackNames];
745 746
    NSArray *audioTrackIndexes = [_mediaPlayer audioTrackIndexes];

747
    NSUInteger count = [audioTracks count];
748 749 750 751 752
    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
753

754
    [_audiotrackActionSheet addButtonWithTitle:NSLocalizedString(@"BUTTON_CANCEL", @"cancel button")];
755
    [_audiotrackActionSheet setCancelButtonIndex:[_audiotrackActionSheet numberOfButtons] - 1];
756
    [_audiotrackActionSheet showInView:self.audioSwitcherButton];
757 758 759 760
}

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

764
    NSUInteger count = [spuTracks count];
765 766
    if (count <= 1)
        return;
767
    _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
768

769 770 771 772 773 774
    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];
    }

775
    [_subtitleActionSheet addButtonWithTitle:NSLocalizedString(@"BUTTON_CANCEL", @"cancel button")];
776
    [_subtitleActionSheet setCancelButtonIndex:[_subtitleActionSheet numberOfButtons] - 1];
777
    [_subtitleActionSheet showInView: self.subtitleSwitcherButton];
778 779 780
}

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

Felix Paul Kühne's avatar
Felix Paul Kühne committed
784
    NSArray *indexArray;
785
    if (actionSheet == _subtitleActionSheet) {
786 787 788
        indexArray = _mediaPlayer.videoSubTitlesIndexes;
        if (buttonIndex <= indexArray.count) {
            _mediaPlayer.currentVideoSubTitleIndex = [indexArray[buttonIndex] intValue];
789
        }
790
    } else if (actionSheet == _audiotrackActionSheet) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
791
        indexArray = _mediaPlayer.audioTrackIndexes;
792 793
        if (buttonIndex <= indexArray.count) {
            _mediaPlayer.currentAudioTrackIndex = [indexArray[buttonIndex] intValue];
794
        }
795 796 797
    }
}

798
- (IBAction)toggleTimeDisplay:(id)sender
799 800
{
    _displayRemainingTime = !_displayRemainingTime;
801 802

    [self _resetIdleTimer];
803 804
}

805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
#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;
    }
}

830 831 832 833
#pragma mark - Video Filter UI

- (IBAction)videoFilterToggle:(id)sender
{
834 835
    if (!_playbackSpeedViewHidden)
        self.playbackSpeedView.hidden = _playbackSpeedViewHidden = YES;
836

837 838 839 840 841
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
        if (!_controlsHidden)
            self.controllerPanel.hidden = _controlsHidden = YES;
    }

842 843 844 845 846 847 848 849 850 851
    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;
852 853 854 855 856 857
    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)
858 859 860
        _mediaPlayer.saturation = self.saturationSlider.value;
    else if (sender == self.gammaSlider)
        _mediaPlayer.gamma = self.gammaSlider.value;
861 862 863 864
    else if (sender == self.resetVideoFilterButton) {
        _mediaPlayer.hue = self.hueSlider.value = 0.;
        _mediaPlayer.contrast = self.contrastSlider.value = 1.;
        _mediaPlayer.brightness = self.brightnessSlider.value = 1.;
865
        [[UIScreen mainScreen] setBrightness:(self.brightnessSlider.value / 2.)];
866 867 868
        _mediaPlayer.saturation = self.saturationSlider.value = 1.;
        _mediaPlayer.gamma = self.gammaSlider.value = 1.;
    } else
869
        APLog(@"unknown sender for videoFilterSliderAction");
870
    [self _resetIdleTimer];
871 872
}

873 874 875 876 877 878 879 880 881
#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];
882
    [self _resetIdleTimer];
883 884 885 886 887 888 889
}

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

    /* rate changed, so update the exported info */
    [self performSelectorInBackground:@selector(_updateExportedPlaybackInformation) withObject:nil];
893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912
}

- (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
{
913
    if (sender == self.playbackSpeedButton) {
Felix Paul Kühne's avatar