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