VDLPlaybackViewController.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /* Copyright (c) 2013, Felix Paul Kühne and VideoLAN
  2. * All rights reserved.
  3. *
  4. * Redistribution and use in source and binary forms, with or without
  5. * modification, are permitted provided that the following conditions are met:
  6. *
  7. * 1. Redistributions of source code must retain the above copyright notice,
  8. * this list of conditions and the following disclaimer.
  9. *
  10. * 2. Redistributions in binary form must reproduce the above copyright notice,
  11. * this list of conditions and the following disclaimer in the documentation
  12. * and/or other materials provided with the distribution.
  13. *
  14. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  15. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  16. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  17. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  18. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  19. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  20. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  21. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  22. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  23. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  24. * POSSIBILITY OF SUCH DAMAGE. */
  25. #import "VDLPlaybackViewController.h"
  26. #import <AVFoundation/AVFoundation.h>
  27. @interface VDLPlaybackViewController () <UIGestureRecognizerDelegate, UIActionSheetDelegate, VLCMediaPlayerDelegate>
  28. {
  29. VLCMediaPlayer *_mediaplayer;
  30. BOOL _setPosition;
  31. BOOL _displayRemainingTime;
  32. int _currentAspectRatioMask;
  33. NSArray *_aspectRatios;
  34. UIActionSheet *_audiotrackActionSheet;
  35. UIActionSheet *_subtitleActionSheet;
  36. NSURL *_url;
  37. NSTimer *_idleTimer;
  38. }
  39. @end
  40. @implementation VDLPlaybackViewController
  41. - (void)viewDidLoad
  42. {
  43. [super viewDidLoad];
  44. /* fix-up UI */
  45. self.wantsFullScreenLayout = YES;
  46. [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
  47. /* we want to influence the system volume */
  48. [[AVAudioSession sharedInstance] setDelegate:self];
  49. /* populate array of supported aspect ratios (there are more!) */
  50. _aspectRatios = @[@"DEFAULT", @"FILL_TO_SCREEN", @"4:3", @"16:9", @"16:10", @"2.21:1"];
  51. /* fix-up the UI */
  52. CGRect rect = self.toolbar.frame;
  53. rect.size.height += 20.;
  54. self.toolbar.frame = rect;
  55. [self.timeDisplay setTitle:@"" forState:UIControlStateNormal];
  56. /* this looks a bit weird, but let's try to support iOS 5 */
  57. UISlider *volumeSlider = nil;
  58. for (id aView in self.volumeView.subviews){
  59. if ([[[aView class] description] isEqualToString:@"MPVolumeSlider"]){
  60. volumeSlider = (UISlider *)aView;
  61. break;
  62. }
  63. }
  64. [volumeSlider addTarget:self
  65. action:@selector(volumeSliderAction:)
  66. forControlEvents:UIControlEventValueChanged];
  67. /* setup gesture recognizer to toggle controls' visibility */
  68. _movieView.userInteractionEnabled = NO;
  69. UITapGestureRecognizer *tapOnVideoRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleControlsVisible)];
  70. tapOnVideoRecognizer.delegate = self;
  71. [self.view addGestureRecognizer:tapOnVideoRecognizer];
  72. }
  73. - (void)playMediaFromURL:(NSURL*)theURL
  74. {
  75. _url = theURL;
  76. }
  77. - (IBAction)playandPause:(id)sender
  78. {
  79. if (_mediaplayer.isPlaying)
  80. [_mediaplayer pause];
  81. [_mediaplayer play];
  82. }
  83. - (IBAction)closePlayback:(id)sender
  84. {
  85. [self.navigationController dismissViewControllerAnimated:YES completion:nil];
  86. }
  87. - (void)viewWillAppear:(BOOL)animated
  88. {
  89. [super viewWillAppear:animated];
  90. [self.navigationController setNavigationBarHidden:YES animated:YES];
  91. /* setup the media player instance, give it a delegate and something to draw into */
  92. _mediaplayer = [[VLCMediaPlayer alloc] init];
  93. _mediaplayer.delegate = self;
  94. _mediaplayer.drawable = self.movieView;
  95. /* enable debug logging from libvlc here */
  96. _mediaplayer.libraryInstance.debugLogging = YES;
  97. /* listen for notifications from the player */
  98. [_mediaplayer addObserver:self forKeyPath:@"time" options:0 context:nil];
  99. [_mediaplayer addObserver:self forKeyPath:@"remainingTime" options:0 context:nil];
  100. /* create a media object and give it to the player */
  101. _mediaplayer.media = [VLCMedia mediaWithURL:_url];
  102. [_mediaplayer play];
  103. if (self.controllerPanel.hidden)
  104. [self toggleControlsVisible];
  105. [self _resetIdleTimer];
  106. }
  107. - (void)viewWillDisappear:(BOOL)animated
  108. {
  109. [super viewWillDisappear:animated];
  110. if (_mediaplayer) {
  111. @try {
  112. [_mediaplayer removeObserver:self forKeyPath:@"time"];
  113. [_mediaplayer removeObserver:self forKeyPath:@"remainingTime"];
  114. }
  115. @catch (NSException *exception) {
  116. NSLog(@"we weren't an observer yet");
  117. }
  118. if (_mediaplayer.media)
  119. [_mediaplayer stop];
  120. if (_mediaplayer)
  121. _mediaplayer = nil;
  122. }
  123. if (_idleTimer) {
  124. [_idleTimer invalidate];
  125. _idleTimer = nil;
  126. }
  127. [self.navigationController setNavigationBarHidden:NO animated:YES];
  128. [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
  129. }
  130. - (UIResponder *)nextResponder
  131. {
  132. [self _resetIdleTimer];
  133. return [super nextResponder];
  134. }
  135. - (IBAction)positionSliderAction:(UISlider *)sender
  136. {
  137. [self _resetIdleTimer];
  138. /* we need to limit the number of events sent by the slider, since otherwise, the user
  139. * wouldn't see the I-frames when seeking on current mobile devices. This isn't a problem
  140. * within the Simulator, but especially on older ARMv7 devices, it's clearly noticeable. */
  141. [self performSelector:@selector(_setPositionForReal) withObject:nil afterDelay:0.3];
  142. _setPosition = NO;
  143. }
  144. - (void)_setPositionForReal
  145. {
  146. if (!_setPosition) {
  147. _mediaplayer.position = _positionSlider.value;
  148. _setPosition = YES;
  149. }
  150. }
  151. - (IBAction)positionSliderDrag:(id)sender
  152. {
  153. [self _resetIdleTimer];
  154. }
  155. - (IBAction)volumeSliderAction:(id)sender
  156. {
  157. [self _resetIdleTimer];
  158. }
  159. - (void)mediaPlayerStateChanged:(NSNotification *)aNotification
  160. {
  161. VLCMediaPlayerState currentState = _mediaplayer.state;
  162. if (currentState == VLCMediaPlayerStateBuffering) {
  163. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  164. [_mediaplayer performSelector:@selector(setTextRendererFont:) withObject:[defaults objectForKey:kVLCSettingSubtitlesFont]];
  165. [_mediaplayer performSelector:@selector(setTextRendererFontSize:) withObject:[defaults objectForKey:kVLCSettingSubtitlesFontSize]];
  166. [_mediaplayer performSelector:@selector(setTextRendererFontColor:) withObject:[defaults objectForKey:kVLCSettingSubtitlesFontColor]];
  167. [_mediaplayer performSelector:@selector(setTextRendererFontForceBold:) withObject:[defaults objectForKey:kVLCSettingSubtitlesBoldFont]];
  168. }
  169. /* distruct view controller on error */
  170. if (currentState == VLCMediaPlayerStateError)
  171. [self performSelector:@selector(closePlayback:) withObject:nil afterDelay:2.];
  172. /* or if playback ended */
  173. if (currentState == VLCMediaPlayerStateEnded || currentState == VLCMediaPlayerStateStopped)
  174. [self performSelector:@selector(closePlayback:) withObject:nil afterDelay:2.];
  175. [self.playPauseButton setTitle:[_mediaplayer isPlaying]? @"Pause" : @"Play" forState:UIControlStateNormal];
  176. }
  177. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  178. {
  179. self.positionSlider.value = [_mediaplayer position];
  180. if (_displayRemainingTime)
  181. [self.timeDisplay setTitle:[[_mediaplayer remainingTime] stringValue] forState:UIControlStateNormal];
  182. else
  183. [self.timeDisplay setTitle:[[_mediaplayer time] stringValue] forState:UIControlStateNormal];
  184. }
  185. - (IBAction)toggleTimeDisplay:(id)sender
  186. {
  187. [self _resetIdleTimer];
  188. _displayRemainingTime = !_displayRemainingTime;
  189. }
  190. - (void)toggleControlsVisible
  191. {
  192. BOOL controlsHidden = !self.controllerPanel.hidden;
  193. self.controllerPanel.hidden = controlsHidden;
  194. self.toolbar.hidden = controlsHidden;
  195. [[UIApplication sharedApplication] setStatusBarHidden:controlsHidden withAnimation:UIStatusBarAnimationFade];
  196. }
  197. - (void)_resetIdleTimer
  198. {
  199. if (!_idleTimer)
  200. _idleTimer = [NSTimer scheduledTimerWithTimeInterval:5.
  201. target:self
  202. selector:@selector(idleTimerExceeded)
  203. userInfo:nil
  204. repeats:NO];
  205. else {
  206. if (fabs([_idleTimer.fireDate timeIntervalSinceNow]) < 5.)
  207. [_idleTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:5.]];
  208. }
  209. }
  210. - (void)idleTimerExceeded
  211. {
  212. _idleTimer = nil;
  213. if (!self.controllerPanel.hidden)
  214. [self toggleControlsVisible];
  215. }
  216. - (IBAction)switchVideoDimensions:(id)sender
  217. {
  218. [self _resetIdleTimer];
  219. NSUInteger count = [_aspectRatios count];
  220. if (_currentAspectRatioMask + 1 > count - 1) {
  221. _mediaplayer.videoAspectRatio = NULL;
  222. [_mediaplayer setCropRatioWithNumerator:1 denominator:0];
  223. _currentAspectRatioMask = 0;
  224. NSLog(@"crop disabled");
  225. } else {
  226. _currentAspectRatioMask++;
  227. if ([_aspectRatios[_currentAspectRatioMask] isEqualToString:@"FILL_TO_SCREEN"]) {
  228. UIScreen *screen = [UIScreen mainScreen];
  229. float f_ar = screen.bounds.size.width / screen.bounds.size.height;
  230. if (f_ar == (float)(640./1136.)) { // iPhone 5 aka 16:9.01
  231. [_mediaplayer setCropRatioWithNumerator:16 denominator:9];
  232. } else if (f_ar == (float)(2./3.)) { // all other iPhones
  233. [_mediaplayer setCropRatioWithNumerator:2 denominator:3];
  234. } else if (f_ar == .75) { // all iPads
  235. [_mediaplayer setCropRatioWithNumerator:4 denominator:3];
  236. } else if (f_ar == .5625) { // AirPlay
  237. [_mediaplayer setCropRatioWithNumerator:4 denominator:3];
  238. } else {
  239. NSLog(@"unknown screen format %f, trying a best effort crop", f_ar);
  240. [_mediaplayer setCropRatioWithNumerator:screen.bounds.size.width denominator:screen.bounds.size.height];
  241. }
  242. NSLog(@"FILL_TO_SCREEN");
  243. return;
  244. }
  245. [_mediaplayer setCropRatioWithNumerator:1 denominator:0];
  246. _mediaplayer.videoAspectRatio = (char *)[_aspectRatios[_currentAspectRatioMask] UTF8String];
  247. NSLog(@"crop switched to %@", _aspectRatios[_currentAspectRatioMask]);
  248. }
  249. }
  250. - (IBAction)switchAudioTrack:(id)sender
  251. {
  252. _audiotrackActionSheet = [[UIActionSheet alloc] initWithTitle:@"audio track selector" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles: nil];
  253. NSArray *audioTracks = [_mediaplayer audioTrackNames];
  254. NSArray *audioTrackIndexes = [_mediaplayer audioTrackIndexes];
  255. NSUInteger count = [audioTracks count];
  256. for (NSUInteger i = 0; i < count; i++) {
  257. NSString *indexIndicator = ([audioTrackIndexes[i] intValue] == [_mediaplayer currentAudioTrackIndex])? @"\u2713": @"";
  258. NSString *buttonTitle = [NSString stringWithFormat:@"%@ %@", indexIndicator, audioTracks[i]];
  259. [_audiotrackActionSheet addButtonWithTitle:buttonTitle];
  260. }
  261. [_audiotrackActionSheet addButtonWithTitle:@"Cancel"];
  262. [_audiotrackActionSheet setCancelButtonIndex:[_audiotrackActionSheet numberOfButtons] - 1];
  263. [_audiotrackActionSheet showInView:self.audioSwitcherButton];
  264. }
  265. - (IBAction)switchSubtitleTrack:(id)sender
  266. {
  267. NSArray *spuTracks = [_mediaplayer videoSubTitlesNames];
  268. NSArray *spuTrackIndexes = [_mediaplayer videoSubTitlesIndexes];
  269. NSUInteger count = [spuTracks count];
  270. if (count <= 1)
  271. return;
  272. _subtitleActionSheet = [[UIActionSheet alloc] initWithTitle:@"subtitle track selector" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles: nil];
  273. for (NSUInteger i = 0; i < count; i++) {
  274. NSString *indexIndicator = ([spuTrackIndexes[i] intValue] == [_mediaplayer currentVideoSubTitleIndex])? @"\u2713": @"";
  275. NSString *buttonTitle = [NSString stringWithFormat:@"%@ %@", indexIndicator, spuTracks[i]];
  276. [_subtitleActionSheet addButtonWithTitle:buttonTitle];
  277. }
  278. [_subtitleActionSheet addButtonWithTitle:@"Cancel"];
  279. [_subtitleActionSheet setCancelButtonIndex:[_subtitleActionSheet numberOfButtons] - 1];
  280. [_subtitleActionSheet showInView: self.subtitleSwitcherButton];
  281. }
  282. - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
  283. if (buttonIndex == [actionSheet cancelButtonIndex])
  284. return;
  285. NSArray *indexArray;
  286. if (actionSheet == _subtitleActionSheet) {
  287. indexArray = _mediaplayer.videoSubTitlesIndexes;
  288. if (buttonIndex <= indexArray.count) {
  289. _mediaplayer.currentVideoSubTitleIndex = [indexArray[buttonIndex] intValue];
  290. }
  291. } else if (actionSheet == _audiotrackActionSheet) {
  292. indexArray = _mediaplayer.audioTrackIndexes;
  293. if (buttonIndex <= indexArray.count) {
  294. _mediaplayer.currentAudioTrackIndex = [indexArray[buttonIndex] intValue];
  295. }
  296. }
  297. }
  298. - (void)didReceiveMemoryWarning
  299. {
  300. [super didReceiveMemoryWarning];
  301. // Dispose of any resources that can be recreated.
  302. }
  303. @end