1/***************************************************************************** 2 * VLCInputManager.m: MacOS X interface module 3 ***************************************************************************** 4 * Copyright (C) 2015 VLC authors and VideoLAN 5 * $Id: 4a88a25bb511d5c3f0d361baa15ff9a13f257710 $ 6 * 7 * This program is free software; you can redistribute it and/or modify 8 * it under the terms of the GNU General Public License as published by 9 * the Free Software Foundation; either version 2 of the License, or 10 * (at your option) any later version. 11 * 12 * This program is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with this program; if not, write to the Free Software 19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. 20 *****************************************************************************/ 21 22#import "VLCInputManager.h" 23 24#include <vlc_url.h> 25 26#import "VLCCoreInteraction.h" 27#import "CompatibilityFixes.h" 28#import "VLCExtensionsManager.h" 29#import "VLCMain.h" 30#import "VLCMainMenu.h" 31#import "VLCMainWindow.h" 32#import "VLCPlaylist.h" 33#import "VLCPlaylistInfo.h" 34#import "VLCResumeDialogController.h" 35#import "VLCTrackSynchronizationWindowController.h" 36#import "VLCVoutView.h" 37#import "VLCRemoteControlService.h" 38 39#import "iTunes.h" 40#import "Spotify.h" 41 42NSString *VLCPlayerRateChanged = @"VLCPlayerRateChanged"; 43 44@interface VLCInputManager() 45- (void)updateMainMenu; 46- (void)updateMainWindow; 47- (void)updateMetaAndInfo; 48- (void)updateDelays; 49@end 50 51#pragma mark Callbacks 52 53static int InputThreadChanged(vlc_object_t *p_this, const char *psz_var, 54 vlc_value_t oldval, vlc_value_t new_val, void *param) 55{ 56 @autoreleasepool { 57 VLCInputManager *inputManager = (__bridge VLCInputManager *)param; 58 [inputManager performSelectorOnMainThread:@selector(inputThreadChanged) withObject:nil waitUntilDone:NO]; 59 } 60 61 return VLC_SUCCESS; 62} 63 64static NSDate *lastPositionUpdate = nil; 65 66static int InputEvent(vlc_object_t *p_this, const char *psz_var, 67 vlc_value_t oldval, vlc_value_t new_val, void *param) 68{ 69 @autoreleasepool { 70 VLCInputManager *inputManager = (__bridge VLCInputManager *)param; 71 72 switch (new_val.i_int) { 73 case INPUT_EVENT_STATE: 74 [inputManager performSelectorOnMainThread:@selector(playbackStatusUpdated) withObject: nil waitUntilDone:NO]; 75 break; 76 case INPUT_EVENT_RATE: 77 [[[VLCMain sharedInstance] mainMenu] performSelectorOnMainThread:@selector(updatePlaybackRate) withObject: nil waitUntilDone:NO]; 78 [[NSNotificationCenter defaultCenter] postNotificationName:VLCPlayerRateChanged object:nil]; 79 break; 80 case INPUT_EVENT_POSITION: 81 82 // Rate limit to 100 ms 83 if (lastPositionUpdate && fabs([lastPositionUpdate timeIntervalSinceNow]) < 0.1) 84 break; 85 86 lastPositionUpdate = [NSDate date]; 87 88 [inputManager performSelectorOnMainThread:@selector(playbackPositionUpdated) withObject:nil waitUntilDone:NO]; 89 break; 90 case INPUT_EVENT_TITLE: 91 case INPUT_EVENT_CHAPTER: 92 [inputManager performSelectorOnMainThread:@selector(updateMainMenu) withObject: nil waitUntilDone:NO]; 93 break; 94 case INPUT_EVENT_CACHE: 95 [inputManager performSelectorOnMainThread:@selector(updateMainWindow) withObject:nil waitUntilDone:NO]; 96 break; 97 case INPUT_EVENT_STATISTICS: 98 dispatch_async(dispatch_get_main_queue(), ^{ 99 [[[VLCMain sharedInstance] currentMediaInfoPanel] updateStatistics]; 100 }); 101 break; 102 case INPUT_EVENT_ES: 103 break; 104 case INPUT_EVENT_TELETEXT: 105 break; 106 case INPUT_EVENT_AOUT: 107 break; 108 case INPUT_EVENT_VOUT: 109 break; 110 case INPUT_EVENT_ITEM_META: 111 case INPUT_EVENT_ITEM_INFO: 112 [inputManager performSelectorOnMainThread:@selector(updateMainMenu) withObject: nil waitUntilDone:NO]; 113 [inputManager performSelectorOnMainThread:@selector(updateName) withObject: nil waitUntilDone:NO]; 114 [inputManager performSelectorOnMainThread:@selector(updateMetaAndInfo) withObject: nil waitUntilDone:NO]; 115 break; 116 case INPUT_EVENT_BOOKMARK: 117 break; 118 case INPUT_EVENT_RECORD: 119 dispatch_async(dispatch_get_main_queue(), ^{ 120 [[[VLCMain sharedInstance] mainMenu] updateRecordState: var_InheritBool(p_this, "record")]; 121 }); 122 break; 123 case INPUT_EVENT_PROGRAM: 124 [inputManager performSelectorOnMainThread:@selector(updateMainMenu) withObject: nil waitUntilDone:NO]; 125 break; 126 case INPUT_EVENT_ITEM_EPG: 127 break; 128 case INPUT_EVENT_SIGNAL: 129 break; 130 131 case INPUT_EVENT_AUDIO_DELAY: 132 case INPUT_EVENT_SUBTITLE_DELAY: 133 [inputManager performSelectorOnMainThread:@selector(updateDelays) withObject:nil waitUntilDone:NO]; 134 break; 135 136 case INPUT_EVENT_DEAD: 137 [inputManager performSelectorOnMainThread:@selector(updateName) withObject: nil waitUntilDone:NO]; 138 [[[VLCMain sharedInstance] mainWindow] performSelectorOnMainThread:@selector(updateTimeSlider) withObject:nil waitUntilDone:NO]; 139 break; 140 141 default: 142 break; 143 } 144 145 return VLC_SUCCESS; 146 } 147} 148 149#pragma mark - 150#pragma mark InputManager implementation 151 152@interface VLCInputManager() 153{ 154 __weak VLCMain *o_main; 155 156 input_thread_t *p_current_input; 157 dispatch_queue_t informInputChangedQueue; 158 159 /* sleep management */ 160 IOPMAssertionID systemSleepAssertionID; 161 IOPMAssertionID monitorSleepAssertionID; 162 163 IOPMAssertionID userActivityAssertionID; 164 165 /* iTunes/Apple Music/Spotify play/pause support */ 166 BOOL b_has_itunes_paused; 167 BOOL b_has_applemusic_paused; 168 BOOL b_has_spotify_paused; 169 170 NSTimer *hasEndedTimer; 171 172 VLCRemoteControlService *_remoteControlService; 173} 174@end 175 176@implementation VLCInputManager 177 178+ (void)initialize 179{ 180 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 181 NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys: 182 [NSArray array], @"recentlyPlayedMediaList", 183 [NSDictionary dictionary], @"recentlyPlayedMedia", nil]; 184 185 [defaults registerDefaults:appDefaults]; 186} 187 188- (id)initWithMain:(VLCMain *)o_mainObj 189{ 190 self = [super init]; 191 if(self) { 192 msg_Dbg(getIntf(), "Initializing input manager"); 193 194 o_main = o_mainObj; 195 var_AddCallback(pl_Get(getIntf()), "input-current", InputThreadChanged, (__bridge void *)self); 196 197 informInputChangedQueue = dispatch_queue_create("org.videolan.vlc.inputChangedQueue", DISPATCH_QUEUE_SERIAL); 198 199 if (@available(macOS 10.12.2, *)) { 200 _remoteControlService = [[VLCRemoteControlService alloc] init]; 201 [_remoteControlService subscribeToRemoteCommands]; 202 } 203 } 204 return self; 205} 206 207/* 208 * TODO: Investigate if this can be moved to dealloc again. Current problems: 209 * - dealloc might be never called of this object, as strong references could be in the 210 * (already stopped) main loop, preventing the refcount to go 0. 211 * - Calling var_DelCallback waits for all callbacks to finish. Thus, while dealloc is already 212 * called, callback might grab a reference to this object again, which could cause trouble. 213 */ 214- (void)deinit 215{ 216 msg_Dbg(getIntf(), "Deinitializing input manager"); 217 if (p_current_input) { 218 /* continue playback where you left off */ 219 [self storePlaybackPositionForItem:p_current_input]; 220 221 var_DelCallback(p_current_input, "intf-event", InputEvent, (__bridge void *)self); 222 vlc_object_release(p_current_input); 223 p_current_input = NULL; 224 } 225 226 if (@available(macOS 10.12.2, *)) { 227 [_remoteControlService unsubscribeFromRemoteCommands]; 228 } 229 230 var_DelCallback(pl_Get(getIntf()), "input-current", InputThreadChanged, (__bridge void *)self); 231 232#if !OS_OBJECT_USE_OBJC 233 dispatch_release(informInputChangedQueue); 234#endif 235} 236 237- (void)inputThreadChanged 238{ 239 if (p_current_input) { 240 var_DelCallback(p_current_input, "intf-event", InputEvent, (__bridge void *)self); 241 vlc_object_release(p_current_input); 242 p_current_input = NULL; 243 244 [[o_main mainMenu] setRateControlsEnabled: NO]; 245 246 [[NSNotificationCenter defaultCenter] postNotificationName:VLCInputChangedNotification 247 object:nil]; 248 } 249 250 // Cancel pending resume dialogs 251 [[[VLCMain sharedInstance] resumeDialog] cancel]; 252 253 input_thread_t *p_input_changed = NULL; 254 255 // object is hold here and released then it is dead 256 p_current_input = playlist_CurrentInput(pl_Get(getIntf())); 257 if (p_current_input) { 258 var_AddCallback(p_current_input, "intf-event", InputEvent, (__bridge void *)self); 259 [self playbackStatusUpdated]; 260 [[o_main mainMenu] setRateControlsEnabled: YES]; 261 262 if ([o_main activeVideoPlayback] && [[[o_main mainWindow] videoView] isHidden]) { 263 [[o_main mainWindow] changePlaylistState: psPlaylistItemChangedEvent]; 264 } 265 266 p_input_changed = vlc_object_hold(p_current_input); 267 268 [[o_main playlist] currentlyPlayingItemChanged]; 269 270 [self continuePlaybackWhereYouLeftOff:p_current_input]; 271 272 [[NSNotificationCenter defaultCenter] postNotificationName:VLCInputChangedNotification 273 object:nil]; 274 } 275 276 [self updateMetaAndInfo]; 277 278 [self updateMainWindow]; 279 [self updateDelays]; 280 [self updateMainMenu]; 281 282 /* 283 * Due to constraints within NSAttributedString's main loop runtime handling 284 * and other issues, we need to inform the extension manager on a separate thread. 285 * The serial queue ensures that changed inputs are propagated in the same order as they arrive. 286 */ 287 dispatch_async(informInputChangedQueue, ^{ 288 [[o_main extensionsManager] inputChanged:p_input_changed]; 289 if (p_input_changed) 290 vlc_object_release(p_input_changed); 291 }); 292} 293 294- (void)playbackPositionUpdated 295{ 296 [[[VLCMain sharedInstance] mainWindow] updateTimeSlider]; 297 [[[VLCMain sharedInstance] statusBarIcon] updateProgress]; 298 [_remoteControlService playbackPositionUpdated]; 299} 300 301- (void)playbackStatusUpdated 302{ 303 // On shutdown, input might not be dead yet. Cleanup actions like inhibit, itunes playback 304 // and playback positon are done in different code paths (dealloc and appWillTerminate:). 305 if ([[VLCMain sharedInstance] isTerminating]) { 306 return; 307 } 308 309 intf_thread_t *p_intf = getIntf(); 310 int state = -1; 311 if (p_current_input) { 312 state = var_GetInteger(p_current_input, "state"); 313 } 314 315 // cancel itunes timer if next item starts playing 316 if (state > -1 && state != END_S) { 317 if (hasEndedTimer) { 318 [hasEndedTimer invalidate]; 319 hasEndedTimer = nil; 320 } 321 } 322 323 if (state == PLAYING_S) { 324 [self stopItunesPlayback]; 325 326 [self inhibitSleep]; 327 328 [[o_main mainMenu] setPause]; 329 [[o_main mainWindow] setPause]; 330 } else { 331 [[o_main mainMenu] setSubmenusEnabled: FALSE]; 332 [[o_main mainMenu] setPlay]; 333 [[o_main mainWindow] setPlay]; 334 335 if (state == PAUSE_S) 336 [self releaseSleepBlockers]; 337 338 if (state == END_S || state == -1) { 339 /* continue playback where you left off */ 340 if (p_current_input) 341 [self storePlaybackPositionForItem:p_current_input]; 342 343 if (hasEndedTimer) { 344 [hasEndedTimer invalidate]; 345 } 346 hasEndedTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5 347 target: self 348 selector: @selector(onPlaybackHasEnded:) 349 userInfo: nil 350 repeats: NO]; 351 } 352 } 353 354 [self updateMainWindow]; 355 [self sendDistributedNotificationWithUpdatedPlaybackStatus]; 356 [_remoteControlService playbackStateChangedTo:state]; 357} 358 359// Called when playback has ended and likely no subsequent media will start playing 360- (void)onPlaybackHasEnded:(id)sender 361{ 362 msg_Dbg(getIntf(), "Playback has been ended"); 363 364 [self releaseSleepBlockers]; 365 [self resumeItunesPlayback]; 366 hasEndedTimer = nil; 367} 368 369- (void)stopItunesPlayback 370{ 371 intf_thread_t *p_intf = getIntf(); 372 int controlItunes = var_InheritInteger(p_intf, "macosx-control-itunes"); 373 if (controlItunes <= 0) 374 return; 375 376 // pause iTunes 377 if (!b_has_itunes_paused) { 378 iTunesApplication *iTunesApp = (iTunesApplication *) [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"]; 379 if (iTunesApp && [iTunesApp isRunning]) { 380 if ([iTunesApp playerState] == iTunesEPlSPlaying) { 381 msg_Dbg(p_intf, "pausing iTunes"); 382 [iTunesApp pause]; 383 b_has_itunes_paused = YES; 384 } 385 } 386 } 387 388 // pause Apple Music 389 if (!b_has_applemusic_paused) { 390 iTunesApplication *iTunesApp = (iTunesApplication *) [SBApplication applicationWithBundleIdentifier:@"com.apple.Music"]; 391 if (iTunesApp && [iTunesApp isRunning]) { 392 if ([iTunesApp playerState] == iTunesEPlSPlaying) { 393 msg_Dbg(p_intf, "pausing Apple Music"); 394 [iTunesApp pause]; 395 b_has_applemusic_paused = YES; 396 } 397 } 398 } 399 400 // pause Spotify 401 if (!b_has_spotify_paused) { 402 SpotifyApplication *spotifyApp = (SpotifyApplication *) [SBApplication applicationWithBundleIdentifier:@"com.spotify.client"]; 403 404 if (spotifyApp) { 405 if ([spotifyApp respondsToSelector:@selector(isRunning)] && [spotifyApp respondsToSelector:@selector(playerState)]) { 406 if ([spotifyApp isRunning] && [spotifyApp playerState] == kSpotifyPlayerStatePlaying) { 407 msg_Dbg(p_intf, "pausing Spotify"); 408 [spotifyApp pause]; 409 b_has_spotify_paused = YES; 410 } 411 } 412 } 413 } 414} 415 416- (void)resumeItunesPlayback 417{ 418 intf_thread_t *p_intf = getIntf(); 419 if (var_InheritInteger(p_intf, "macosx-control-itunes") > 1) { 420 if (b_has_itunes_paused) { 421 iTunesApplication *iTunesApp = (iTunesApplication *) [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"]; 422 if (iTunesApp && [iTunesApp isRunning]) { 423 if ([iTunesApp playerState] == iTunesEPlSPaused) { 424 msg_Dbg(p_intf, "unpausing iTunes"); 425 [iTunesApp playpause]; 426 } 427 } 428 } 429 430 if (b_has_applemusic_paused) { 431 iTunesApplication *iTunesApp = (iTunesApplication *) [SBApplication applicationWithBundleIdentifier:@"com.apple.Music"]; 432 if (iTunesApp && [iTunesApp isRunning]) { 433 if ([iTunesApp playerState] == iTunesEPlSPaused) { 434 msg_Dbg(p_intf, "unpausing Apple Music"); 435 [iTunesApp playpause]; 436 } 437 } 438 } 439 440 if (b_has_spotify_paused) { 441 SpotifyApplication *spotifyApp = (SpotifyApplication *) [SBApplication applicationWithBundleIdentifier:@"com.spotify.client"]; 442 if (spotifyApp) { 443 if ([spotifyApp respondsToSelector:@selector(isRunning)] && [spotifyApp respondsToSelector:@selector(playerState)]) { 444 if ([spotifyApp isRunning] && [spotifyApp playerState] == kSpotifyPlayerStatePaused) { 445 msg_Dbg(p_intf, "unpausing Spotify"); 446 [spotifyApp play]; 447 } 448 } 449 } 450 } 451 } 452 453 b_has_itunes_paused = NO; 454 b_has_applemusic_paused = NO; 455 b_has_spotify_paused = NO; 456} 457 458- (void)inhibitSleep 459{ 460 BOOL shouldDisableScreensaver = var_InheritBool(getIntf(), "disable-screensaver"); 461 462 /* Declare user activity. 463 This wakes the display if it is off, and postpones display sleep according to the users system preferences 464 Available from 10.7.3 */ 465 if ([o_main activeVideoPlayback] && &IOPMAssertionDeclareUserActivity && shouldDisableScreensaver) 466 { 467 CFStringRef reasonForActivity = CFStringCreateWithCString(kCFAllocatorDefault, _("VLC media playback"), kCFStringEncodingUTF8); 468 IOReturn success = IOPMAssertionDeclareUserActivity(reasonForActivity, 469 kIOPMUserActiveLocal, 470 &userActivityAssertionID); 471 CFRelease(reasonForActivity); 472 473 if (success != kIOReturnSuccess) 474 msg_Warn(getIntf(), "failed to declare user activity"); 475 476 } 477 478 // Only set assertion if no previous / active assertion exist. This is necessary to keep 479 // audio only playback awake. If playback switched from video to audio or vice vesa, deactivate 480 // the other assertion and activate the needed assertion instead. 481 void(^activateAssertion)(CFStringRef, IOPMAssertionID*, IOPMAssertionID*) = ^void(CFStringRef assertionType, IOPMAssertionID* assertionIdRef, IOPMAssertionID* otherAssertionIdRef) { 482 483 if (*otherAssertionIdRef > 0) { 484 msg_Dbg(getIntf(), "Releasing old IOKit other assertion (%i)" , *otherAssertionIdRef); 485 IOPMAssertionRelease(*otherAssertionIdRef); 486 *otherAssertionIdRef = 0; 487 } 488 489 if (*assertionIdRef) { 490 msg_Dbg(getIntf(), "Continue to use IOKit assertion %s (%i)", [(__bridge NSString *)(assertionType) UTF8String], *assertionIdRef); 491 return; 492 } 493 494 CFStringRef reasonForActivity = CFStringCreateWithCString(kCFAllocatorDefault, _("VLC media playback"), kCFStringEncodingUTF8); 495 496 IOReturn success = IOPMAssertionCreateWithName(assertionType, kIOPMAssertionLevelOn, reasonForActivity, assertionIdRef); 497 CFRelease(reasonForActivity); 498 499 if (success == kIOReturnSuccess) 500 msg_Dbg(getIntf(), "Activated assertion %s through IOKit (%i)", [(__bridge NSString *)(assertionType) UTF8String], *assertionIdRef); 501 else 502 msg_Warn(getIntf(), "Failed to prevent system sleep through IOKit"); 503 }; 504 505 if ([o_main activeVideoPlayback] && shouldDisableScreensaver) { 506 activateAssertion(kIOPMAssertionTypeNoDisplaySleep, &monitorSleepAssertionID, &systemSleepAssertionID); 507 } else { 508 activateAssertion(kIOPMAssertionTypeNoIdleSleep, &systemSleepAssertionID, &monitorSleepAssertionID); 509 } 510 511} 512 513- (void)releaseSleepBlockers 514{ 515 /* allow the system to sleep again */ 516 if (systemSleepAssertionID > 0) { 517 msg_Dbg(getIntf(), "Releasing IOKit system sleep blocker (%i)" , systemSleepAssertionID); 518 IOPMAssertionRelease(systemSleepAssertionID); 519 systemSleepAssertionID = 0; 520 } 521 522 if (monitorSleepAssertionID > 0) { 523 msg_Dbg(getIntf(), "Releasing IOKit monitor sleep blocker (%i)" , monitorSleepAssertionID); 524 IOPMAssertionRelease(monitorSleepAssertionID); 525 monitorSleepAssertionID = 0; 526 } 527} 528 529- (void)updateMetaAndInfo 530{ 531 if (!p_current_input) { 532 [[[VLCMain sharedInstance] currentMediaInfoPanel] updatePanelWithItem:nil]; 533 [_remoteControlService metaDataChangedForCurrentMediaItem:NULL]; 534 return; 535 } 536 537 input_item_t *p_input_item = input_GetItem(p_current_input); 538 539 [[[o_main playlist] model] updateItem:p_input_item]; 540 [[[VLCMain sharedInstance] currentMediaInfoPanel] updatePanelWithItem:p_input_item]; 541 [_remoteControlService metaDataChangedForCurrentMediaItem:p_input_item]; 542} 543 544- (void)updateMainWindow 545{ 546 [[o_main mainWindow] updateWindow]; 547} 548 549- (void)updateName 550{ 551 [[o_main mainWindow] updateName]; 552} 553 554- (void)updateDelays 555{ 556 [[[VLCMain sharedInstance] trackSyncPanel] updateValues]; 557} 558 559- (void)updateMainMenu 560{ 561 [[o_main mainMenu] setupMenus]; 562 [[o_main mainMenu] updatePlaybackRate]; 563 [[VLCCoreInteraction sharedInstance] resetAtoB]; 564} 565 566- (void)sendDistributedNotificationWithUpdatedPlaybackStatus 567{ 568 [[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"VLCPlayerStateDidChange" 569 object:nil 570 userInfo:nil 571 deliverImmediately:YES]; 572} 573 574- (BOOL)hasInput 575{ 576 return p_current_input != NULL; 577} 578 579#pragma mark - 580#pragma mark Resume logic 581 582 583- (BOOL)isValidResumeItem:(input_item_t *)p_item 584{ 585 char *psz_url = input_item_GetURI(p_item); 586 NSString *urlString = toNSStr(psz_url); 587 free(psz_url); 588 589 if ([urlString isEqualToString:@""]) 590 return NO; 591 592 NSURL *url = [NSURL URLWithString:urlString]; 593 594 if (![url isFileURL]) 595 return NO; 596 597 BOOL isDir = false; 598 if (![[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&isDir]) 599 return NO; 600 601 if (isDir) 602 return NO; 603 604 return YES; 605} 606 607- (void)continuePlaybackWhereYouLeftOff:(input_thread_t *)p_input_thread 608{ 609 NSDictionary *recentlyPlayedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"recentlyPlayedMedia"]; 610 if (!recentlyPlayedFiles) 611 return; 612 613 input_item_t *p_item = input_GetItem(p_input_thread); 614 if (!p_item) 615 return; 616 617 /* allow the user to over-write the start/stop/run-time */ 618 if (var_GetFloat(p_input_thread, "run-time") > 0 || 619 var_GetFloat(p_input_thread, "start-time") > 0 || 620 var_GetFloat(p_input_thread, "stop-time") != 0) { 621 return; 622 } 623 624 /* check for file existance before resuming */ 625 if (![self isValidResumeItem:p_item]) 626 return; 627 628 char *psz_url = vlc_uri_decode(input_item_GetURI(p_item)); 629 if (!psz_url) 630 return; 631 NSString *url = toNSStr(psz_url); 632 free(psz_url); 633 634 NSNumber *lastPosition = [recentlyPlayedFiles objectForKey:url]; 635 if (!lastPosition || lastPosition.intValue <= 0) 636 return; 637 638 int settingValue = config_GetInt(getIntf(), "macosx-continue-playback"); 639 if (settingValue == 2) // never resume 640 return; 641 642 CompletionBlock completionBlock = ^(enum ResumeResult result) { 643 644 if (result == RESUME_RESTART) 645 return; 646 647 mtime_t lastPos = (mtime_t)lastPosition.intValue * 1000000; 648 msg_Dbg(getIntf(), "continuing playback at %lld", lastPos); 649 var_SetInteger(p_input_thread, "time", lastPos); 650 }; 651 652 if (settingValue == 1) { // always 653 completionBlock(RESUME_NOW); 654 return; 655 } 656 657 [[[VLCMain sharedInstance] resumeDialog] showWindowWithItem:p_item 658 withLastPosition:lastPosition.intValue 659 completionBlock:completionBlock]; 660 661} 662 663static const int64_t MinimumDuration = 3 * 60 * 1000; 664static const float MinimumStorePercent = 0.05; 665static const float MaximumStorePercent = 0.95; 666static const int64_t MinimumStoreTime = 60 * 1000; 667static const int64_t MinimumStoreRemainingTime = 60 * 1000; 668 669BOOL ShouldStorePlaybackPosition(float position, int64_t duration) 670{ 671 int64_t positionTime = position * duration; 672 int64_t remainingTime = duration - positionTime; 673 674 if (duration < MinimumDuration) { 675 return NO; 676 } 677 678 if (position < MinimumStorePercent && positionTime < MinimumStoreTime) { 679 return NO; 680 } 681 682 if (position > MaximumStorePercent && remainingTime < MinimumStoreRemainingTime) { 683 return NO; 684 } 685 686 return YES; 687} 688 689- (void)storePlaybackPositionForItem:(input_thread_t *)p_input_thread 690{ 691 if (!var_InheritBool(getIntf(), "macosx-recentitems")) 692 return; 693 694 input_item_t *p_item = input_GetItem(p_input_thread); 695 if (!p_item) 696 return; 697 698 if (![self isValidResumeItem:p_item]) 699 return; 700 701 char *psz_url = vlc_uri_decode(input_item_GetURI(p_item)); 702 if (!psz_url) 703 return; 704 NSString *url = toNSStr(psz_url); 705 free(psz_url); 706 707 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 708 NSMutableDictionary *mutDict = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:@"recentlyPlayedMedia"]]; 709 710 float relativePos = var_GetFloat(p_input_thread, "position"); 711 mtime_t pos = var_GetInteger(p_input_thread, "time") / CLOCK_FREQ; 712 mtime_t dur = input_item_GetDuration(p_item) / 1000000; 713 714 NSMutableArray *mediaList = [[defaults objectForKey:@"recentlyPlayedMediaList"] mutableCopy]; 715 716 if (ShouldStorePlaybackPosition(relativePos, dur*1000)) { 717 msg_Dbg(getIntf(), "Store current playback position of %f", relativePos); 718 [mutDict setObject:[NSNumber numberWithInt:pos] forKey:url]; 719 720 [mediaList removeObject:url]; 721 [mediaList addObject:url]; 722 NSUInteger mediaListCount = mediaList.count; 723 if (mediaListCount > 30) { 724 for (NSUInteger x = 0; x < mediaListCount - 30; x++) { 725 [mutDict removeObjectForKey:[mediaList firstObject]]; 726 [mediaList removeObjectAtIndex:0]; 727 } 728 } 729 } else { 730 [mutDict removeObjectForKey:url]; 731 [mediaList removeObject:url]; 732 } 733 [defaults setObject:mutDict forKey:@"recentlyPlayedMedia"]; 734 [defaults setObject:mediaList forKey:@"recentlyPlayedMediaList"]; 735 [defaults synchronize]; 736} 737 738@end 739