VLCInputManager.m 18.1 KB
Newer Older
1
/*****************************************************************************
2
 * VLCInputManager.m: MacOS X interface module
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 *****************************************************************************
 * Copyright (C) 2015 VLC authors and VideoLAN
 * $Id$
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

22
#import "VLCInputManager.h"
23

24
#import "VLCCoreInteraction.h"
25
#import "CompatibilityFixes.h"
26
#import "VLCExtensionsManager.h"
27
#import "VLCMain.h"
28
#import "VLCMainMenu.h"
29
#import "VLCMainWindow.h"
30 31
#import "VLCPlaylist.h"
#import "VLCPlaylistInfo.h"
32
#import "VLCResumeDialogController.h"
33
#import "VLCTrackSynchronizationWindowController.h"
34
#import "VLCVoutView.h"
35 36 37 38 39 40 41 42 43 44

#import "iTunes.h"
#import "Spotify.h"

#pragma mark Callbacks

static int InputThreadChanged(vlc_object_t *p_this, const char *psz_var,
                              vlc_value_t oldval, vlc_value_t new_val, void *param)
{
    @autoreleasepool {
45
        VLCInputManager *inputManager = (__bridge VLCInputManager *)param;
46 47 48 49 50 51
        [inputManager performSelectorOnMainThread:@selector(inputThreadChanged) withObject:nil waitUntilDone:NO];
    }

    return VLC_SUCCESS;
}

52
static NSDate *lastPositionUpdate = nil;
53 54 55 56

static int InputEvent(vlc_object_t *p_this, const char *psz_var,
                      vlc_value_t oldval, vlc_value_t new_val, void *param)
{
57
    @autoreleasepool {
58
        VLCInputManager *inputManager = (__bridge VLCInputManager *)param;
59 60 61 62 63 64 65 66 67

        switch (new_val.i_int) {
            case INPUT_EVENT_STATE:
                [inputManager performSelectorOnMainThread:@selector(playbackStatusUpdated) withObject: nil waitUntilDone:NO];
                break;
            case INPUT_EVENT_RATE:
                [[[VLCMain sharedInstance] mainMenu] performSelectorOnMainThread:@selector(updatePlaybackRate) withObject: nil waitUntilDone:NO];
                break;
            case INPUT_EVENT_POSITION:
68 69 70 71 72 73 74 75

                // Rate limit to 100 ms
                if (lastPositionUpdate && fabs([lastPositionUpdate timeIntervalSinceNow]) < 0.1)
                    break;

                lastPositionUpdate = [NSDate date];

                [inputManager performSelectorOnMainThread:@selector(playbackPositionUpdated) withObject:nil waitUntilDone:NO];
76 77 78 79 80 81 82 83 84 85
                break;
            case INPUT_EVENT_TITLE:
            case INPUT_EVENT_CHAPTER:
                [inputManager performSelectorOnMainThread:@selector(updateMainMenu) withObject: nil waitUntilDone:NO];
                break;
            case INPUT_EVENT_CACHE:
                [inputManager performSelectorOnMainThread:@selector(updateMainWindow) withObject:nil waitUntilDone:NO];
                break;
            case INPUT_EVENT_STATISTICS:
                dispatch_async(dispatch_get_main_queue(), ^{
86
                    [[[VLCMain sharedInstance] currentMediaInfoPanel] updateStatistics];
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
                });
                break;
            case INPUT_EVENT_ES:
                break;
            case INPUT_EVENT_TELETEXT:
                break;
            case INPUT_EVENT_AOUT:
                break;
            case INPUT_EVENT_VOUT:
                break;
            case INPUT_EVENT_ITEM_META:
            case INPUT_EVENT_ITEM_INFO:
                [inputManager performSelectorOnMainThread:@selector(updateMainMenu) withObject: nil waitUntilDone:NO];
                [inputManager performSelectorOnMainThread:@selector(updateName) withObject: nil waitUntilDone:NO];
                [inputManager performSelectorOnMainThread:@selector(updateMetaAndInfo) withObject: nil waitUntilDone:NO];
                break;
            case INPUT_EVENT_BOOKMARK:
                break;
            case INPUT_EVENT_RECORD:
                dispatch_async(dispatch_get_main_queue(), ^{
                    [[[VLCMain sharedInstance] mainMenu] updateRecordState: var_InheritBool(p_this, "record")];
                });
                break;
            case INPUT_EVENT_PROGRAM:
                [inputManager performSelectorOnMainThread:@selector(updateMainMenu) withObject: nil waitUntilDone:NO];
                break;
            case INPUT_EVENT_ITEM_EPG:
                break;
            case INPUT_EVENT_SIGNAL:
                break;

            case INPUT_EVENT_AUDIO_DELAY:
            case INPUT_EVENT_SUBTITLE_DELAY:
                [inputManager performSelectorOnMainThread:@selector(updateDelays) withObject:nil waitUntilDone:NO];
                break;

            case INPUT_EVENT_DEAD:
                [inputManager performSelectorOnMainThread:@selector(updateName) withObject: nil waitUntilDone:NO];
                [[[VLCMain sharedInstance] mainWindow] performSelectorOnMainThread:@selector(updateTimeSlider) withObject:nil waitUntilDone:NO];
                break;

            default:
                break;
        }
131

132 133
        return VLC_SUCCESS;
    }
134 135 136 137 138
}

#pragma mark -
#pragma mark InputManager implementation

139
@interface VLCInputManager()
140
{
141
    __weak VLCMain *o_main;
142 143 144 145 146 147

    input_thread_t *p_current_input;
    dispatch_queue_t informInputChangedQueue;

    /* sleep management */
    IOPMAssertionID systemSleepAssertionID;
148 149
    IOPMAssertionID monitorSleepAssertionID;

150 151 152 153 154
    IOPMAssertionID userActivityAssertionID;

    /* iTunes/Spotify play/pause support */
    BOOL b_has_itunes_paused;
    BOOL b_has_spotify_paused;
155 156

    NSTimer *hasEndedTimer;
157 158 159
}
@end

160
@implementation VLCInputManager
161 162 163 164 165

- (id)initWithMain:(VLCMain *)o_mainObj
{
    self = [super init];
    if(self) {
166
        msg_Dbg(getIntf(), "Initializing input manager");
167

168
        o_main = o_mainObj;
169
        var_AddCallback(pl_Get(getIntf()), "input-current", InputThreadChanged, (__bridge void *)self);
170 171 172 173 174 175 176 177 178

        informInputChangedQueue = dispatch_queue_create("org.videolan.vlc.inputChangedQueue", DISPATCH_QUEUE_SERIAL);

    }
    return self;
}

- (void)dealloc
{
179
    msg_Dbg(getIntf(), "Deinitializing input manager");
180 181 182 183
    if (p_current_input) {
        /* continue playback where you left off */
        [[o_main playlist] storePlaybackPositionForItem:p_current_input];

184
        var_DelCallback(p_current_input, "intf-event", InputEvent, (__bridge void *)self);
185 186 187 188
        vlc_object_release(p_current_input);
        p_current_input = NULL;
    }

189
    var_DelCallback(pl_Get(getIntf()), "input-current", InputThreadChanged, (__bridge void *)self);
190

191
#if !OS_OBJECT_USE_OBJC
192
    dispatch_release(informInputChangedQueue);
193
#endif
194 195 196 197 198
}

- (void)inputThreadChanged
{
    if (p_current_input) {
199
        var_DelCallback(p_current_input, "intf-event", InputEvent, (__bridge void *)self);
200 201 202 203 204 205 206 207 208
        vlc_object_release(p_current_input);
        p_current_input = NULL;

        [[o_main mainMenu] setRateControlsEnabled: NO];

        [[NSNotificationCenter defaultCenter] postNotificationName:VLCInputChangedNotification
                                                            object:nil];
    }

209 210 211
    // Cancel pending resume dialogs
    [[[VLCMain sharedInstance] resumeDialog] cancel];

212 213 214
    input_thread_t *p_input_changed = NULL;

    // object is hold here and released then it is dead
215
    p_current_input = playlist_CurrentInput(pl_Get(getIntf()));
216
    if (p_current_input) {
217
        var_AddCallback(p_current_input, "intf-event", InputEvent, (__bridge void *)self);
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
        [self playbackStatusUpdated];
        [[o_main mainMenu] setRateControlsEnabled: YES];

        if ([o_main activeVideoPlayback] && [[[o_main mainWindow] videoView] isHidden]) {
            [[o_main mainWindow] changePlaylistState: psPlaylistItemChangedEvent];
        }

        p_input_changed = vlc_object_hold(p_current_input);

        [[o_main playlist] currentlyPlayingItemChanged];

        [[o_main playlist] continuePlaybackWhereYouLeftOff:p_current_input];

        [[NSNotificationCenter defaultCenter] postNotificationName:VLCInputChangedNotification
                                                            object:nil];
    }

    [self updateMetaAndInfo];

237 238 239
    [self updateMainWindow];
    [self updateDelays];
    [self updateMainMenu];
240 241 242 243 244 245 246

    /*
     * Due to constraints within NSAttributedString's main loop runtime handling
     * and other issues, we need to inform the extension manager on a separate thread.
     * The serial queue ensures that changed inputs are propagated in the same order as they arrive.
     */
    dispatch_async(informInputChangedQueue, ^{
247
        [[o_main extensionsManager] inputChanged:p_input_changed];
248 249 250 251 252
        if (p_input_changed)
            vlc_object_release(p_input_changed);
    });
}

253 254 255 256 257 258
- (void)playbackPositionUpdated
{
    [[[VLCMain sharedInstance] mainWindow] updateTimeSlider];
    [[[VLCMain sharedInstance] statusBarIcon] updateProgress];
}

259 260
- (void)playbackStatusUpdated
{
261 262 263 264 265 266
    // On shutdown, input might not be dead yet. Cleanup actions like inhibit, itunes playback
    // and playback positon are done in different code paths (dealloc and appWillTerminate:).
    if ([[VLCMain sharedInstance] isTerminating]) {
        return;
    }

267
    intf_thread_t *p_intf = getIntf();
268 269 270 271 272 273
    int state = -1;
    if (p_current_input) {
        state = var_GetInteger(p_current_input, "state");
    }

    // cancel itunes timer if next item starts playing
274 275 276 277
    if (state > -1 && state != END_S) {
        if (hasEndedTimer) {
            [hasEndedTimer invalidate];
            hasEndedTimer = nil;
278 279 280 281
        }
    }

    if (state == PLAYING_S) {
282
        [self stopItunesPlayback];
283

284
        [self inhibitSleep];
285 286 287 288 289 290 291 292

        [[o_main mainMenu] setPause];
        [[o_main mainWindow] setPause];
    } else {
        [[o_main mainMenu] setSubmenusEnabled: FALSE];
        [[o_main mainMenu] setPlay];
        [[o_main mainWindow] setPlay];

293 294
        if (state == PAUSE_S)
            [self releaseSleepBlockers];
295 296 297 298 299 300

        if (state == END_S || state == -1) {
            /* continue playback where you left off */
            if (p_current_input)
                [[o_main playlist] storePlaybackPositionForItem:p_current_input];

301 302
            if (hasEndedTimer) {
                [hasEndedTimer invalidate];
303
            }
304 305 306 307 308
            hasEndedTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5
                                                             target: self
                                                           selector: @selector(onPlaybackHasEnded:)
                                                           userInfo: nil
                                                            repeats: NO];
309 310 311
        }
    }

312
    [self updateMainWindow];
313 314 315
    [self sendDistributedNotificationWithUpdatedPlaybackStatus];
}

316 317 318 319 320 321 322 323 324 325
// Called when playback has ended and likely no subsequent media will start playing
- (void)onPlaybackHasEnded:(id)sender
{
    msg_Dbg(getIntf(), "Playback has been ended");

    [self releaseSleepBlockers];
    [self resumeItunesPlayback];
    hasEndedTimer = nil;
}

326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
- (void)stopItunesPlayback
{
    intf_thread_t *p_intf = getIntf();
    int controlItunes = var_InheritInteger(p_intf, "macosx-control-itunes");
    if (controlItunes <= 0)
        return;

    // pause iTunes
    if (!b_has_itunes_paused) {
        iTunesApplication *iTunesApp = (iTunesApplication *) [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"];
        if (iTunesApp && [iTunesApp isRunning]) {
            if ([iTunesApp playerState] == iTunesEPlSPlaying) {
                msg_Dbg(p_intf, "pausing iTunes");
                [iTunesApp pause];
                b_has_itunes_paused = YES;
            }
        }
    }

    // pause Spotify
    if (!b_has_spotify_paused) {
        SpotifyApplication *spotifyApp = (SpotifyApplication *) [SBApplication applicationWithBundleIdentifier:@"com.spotify.client"];

        if (spotifyApp) {
            if ([spotifyApp respondsToSelector:@selector(isRunning)] && [spotifyApp respondsToSelector:@selector(playerState)]) {
                if ([spotifyApp isRunning] && [spotifyApp playerState] == kSpotifyPlayerStatePlaying) {
                    msg_Dbg(p_intf, "pausing Spotify");
                    [spotifyApp pause];
                    b_has_spotify_paused = YES;
                }
            }
        }
    }
}
360

361
- (void)resumeItunesPlayback
362
{
363
    intf_thread_t *p_intf = getIntf();
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
    if (var_InheritInteger(p_intf, "macosx-control-itunes") > 1) {
        if (b_has_itunes_paused) {
            iTunesApplication *iTunesApp = (iTunesApplication *) [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"];
            if (iTunesApp && [iTunesApp isRunning]) {
                if ([iTunesApp playerState] == iTunesEPlSPaused) {
                    msg_Dbg(p_intf, "unpausing iTunes");
                    [iTunesApp playpause];
                }
            }
        }

        if (b_has_spotify_paused) {
            SpotifyApplication *spotifyApp = (SpotifyApplication *) [SBApplication applicationWithBundleIdentifier:@"com.spotify.client"];
            if (spotifyApp) {
                if ([spotifyApp respondsToSelector:@selector(isRunning)] && [spotifyApp respondsToSelector:@selector(playerState)]) {
                    if ([spotifyApp isRunning] && [spotifyApp playerState] == kSpotifyPlayerStatePaused) {
                        msg_Dbg(p_intf, "unpausing Spotify");
                        [spotifyApp play];
                    }
                }
            }
        }
    }

    b_has_itunes_paused = NO;
    b_has_spotify_paused = NO;
}

392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
- (void)inhibitSleep
{
    BOOL shouldDisableScreensaver = var_InheritBool(getIntf(), "disable-screensaver");

    /* Declare user activity.
     This wakes the display if it is off, and postpones display sleep according to the users system preferences
     Available from 10.7.3 */
    if ([o_main activeVideoPlayback] && &IOPMAssertionDeclareUserActivity && shouldDisableScreensaver)
    {
        CFStringRef reasonForActivity = CFStringCreateWithCString(kCFAllocatorDefault, _("VLC media playback"), kCFStringEncodingUTF8);
        IOReturn success = IOPMAssertionDeclareUserActivity(reasonForActivity,
                                                            kIOPMUserActiveLocal,
                                                            &userActivityAssertionID);
        CFRelease(reasonForActivity);

        if (success != kIOReturnSuccess)
            msg_Warn(getIntf(), "failed to declare user activity");

    }

412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
    // Only set assertion if no previous / active assertion exist. This is necessary to keep
    // audio only playback awake. If playback switched from video to audio or vice vesa, deactivate
    // the other assertion and activate the needed assertion instead.
    void(^activateAssertion)(CFStringRef, IOPMAssertionID*, IOPMAssertionID*) = ^void(CFStringRef assertionType, IOPMAssertionID* assertionIdRef, IOPMAssertionID* otherAssertionIdRef) {

        if (*otherAssertionIdRef > 0) {
            msg_Dbg(getIntf(), "Releasing old IOKit other assertion (%i)" , *otherAssertionIdRef);
            IOPMAssertionRelease(*otherAssertionIdRef);
            *otherAssertionIdRef = 0;
        }

        if (*assertionIdRef) {
            msg_Dbg(getIntf(), "Continue to use IOKit assertion %s (%i)", [(__bridge NSString *)(assertionType) UTF8String], *assertionIdRef);
            return;
        }

        CFStringRef reasonForActivity = CFStringCreateWithCString(kCFAllocatorDefault, _("VLC media playback"), kCFStringEncodingUTF8);

        IOReturn success = IOPMAssertionCreateWithName(assertionType, kIOPMAssertionLevelOn, reasonForActivity, assertionIdRef);
        CFRelease(reasonForActivity);

        if (success == kIOReturnSuccess)
            msg_Dbg(getIntf(), "Activated assertion %s through IOKit (%i)", [(__bridge NSString *)(assertionType) UTF8String], *assertionIdRef);
        else
            msg_Warn(getIntf(), "Failed to prevent system sleep through IOKit");
    };

    if ([o_main activeVideoPlayback] && shouldDisableScreensaver) {
        activateAssertion(kIOPMAssertionTypeNoDisplaySleep, &monitorSleepAssertionID, &systemSleepAssertionID);
    } else {
        activateAssertion(kIOPMAssertionTypeNoIdleSleep, &systemSleepAssertionID, &monitorSleepAssertionID);
443 444 445 446 447 448 449 450
    }

}

- (void)releaseSleepBlockers
{
    /* allow the system to sleep again */
    if (systemSleepAssertionID > 0) {
451
        msg_Dbg(getIntf(), "Releasing IOKit system sleep blocker (%i)" , systemSleepAssertionID);
452 453 454
        IOPMAssertionRelease(systemSleepAssertionID);
        systemSleepAssertionID = 0;
    }
455 456 457 458 459 460

    if (monitorSleepAssertionID > 0) {
        msg_Dbg(getIntf(), "Releasing IOKit monitor sleep blocker (%i)" , monitorSleepAssertionID);
        IOPMAssertionRelease(monitorSleepAssertionID);
        monitorSleepAssertionID = 0;
    }
461 462
}

463 464 465
- (void)updateMetaAndInfo
{
    if (!p_current_input) {
466
        [[[VLCMain sharedInstance] currentMediaInfoPanel] updatePanelWithItem:nil];
467 468 469 470 471 472
        return;
    }

    input_item_t *p_input_item = input_GetItem(p_current_input);

    [[[o_main playlist] model] updateItem:p_input_item];
473
    [[[VLCMain sharedInstance] currentMediaInfoPanel] updatePanelWithItem:p_input_item];
474 475
}

476 477 478 479 480 481 482 483 484 485 486 487
- (void)updateMainWindow
{
    [[o_main mainWindow] updateWindow];
}

- (void)updateName
{
    [[o_main mainWindow] updateName];
}

- (void)updateDelays
{
488
    [[[VLCMain sharedInstance] trackSyncPanel] updateValues];
489 490 491 492 493 494 495 496 497
}

- (void)updateMainMenu
{
    [[o_main mainMenu] setupMenus];
    [[o_main mainMenu] updatePlaybackRate];
    [[VLCCoreInteraction sharedInstance] resetAtoB];
}

498 499 500 501 502 503 504 505 506 507 508 509 510 511
- (void)sendDistributedNotificationWithUpdatedPlaybackStatus
{
    [[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"VLCPlayerStateDidChange"
                                                                   object:nil
                                                                 userInfo:nil
                                                       deliverImmediately:YES];
}

- (BOOL)hasInput
{
    return p_current_input != NULL;
}

@end