CGEventTapCreate breaks down mysteriously with "key down" events
Asked Answered
W

3

7

I'm using CGEventTapCreate to "steal" media keys from iTunes when my app is running. The code inside of the callback that I pass to CGEventTapCreate examines the event, and if it finds that it's one of the media keys, posts an appropriate notification to the default notification center.

Now, this works fine if I post a notification for the "key up" event. If I do that for "key down" events, eventually my app stops getting media key events and iTunes takes over. Any ideas on what can be causing this? The relevant part of the code is below

enum { 
...
  PlayPauseKeyDown = 0x100A00,
  PlayPauseKeyUp = 0x100B00,
...
};

static CGEventRef event_tap_callback(CGEventTapProxy proxy,
                                     CGEventType type,
                                     CGEventRef event,
                                     void *refcon)
{
  if (!(type == NX_SYSDEFINED) || (type == NX_KEYUP) || (type == NX_KEYDOWN))
      return event;

  NSEvent* keyEvent = [NSEvent eventWithCGEvent: event];
  if (keyEvent.type != NSSystemDefined) return event;

  switch(keyEvent.data1)
  {
    case PlayPauseKeyUp:  // <--- this works reliably
    //case PlayPauseKeyDown:  // <--- this will break eventually
      post_notification(@"PlayPauseMediaKeyPressed", nil, nil);
      return NULL;

    ... and so on ...
Wiggins answered 3/6, 2010 at 19:27 Comment(1)
Seems to be a timing issue. Replaced post_notification by sleep(1) and there it is, after a few keypresses iTunes steals the media keys back if I'm using PlayPauseKeyDown. Still works if I'm using PlayPauseKeyUp.Wiggins
E
12

Does something kill my event tap if the callback takes too long?

Some people suspect that Snow Leopard has a bug that sometimes disables event taps even if they don't take too long. To handle that, you can watch for the event type kCGEventTapDisabledByTimeout, and respond by re-enabling your tap with CGEventTapEnable.

Eunuchize answered 4/6, 2010 at 2:56 Comment(1)
Yep, this was it. Handling kCGEventTapDisabledByTimeout does the trick.Wiggins
S
4

First of all, why is your first "if" allowing key-down and key-up events to pass? Your second "if" only lets system events pass through anyway. So for all key-down/-up events you create a NSEvent, just to drop the event one line further downwards. That makes little sense. An Event Tap should always be as fast as possible, otherwise it will slow down all event processing of the whole system. Your callback should not even be called for key-down/-up events, since system events are not key-down/-up events, they are system events. If they were key events, you would for sure never access data1, but instead use the "type" and "keyCode" methods to get the relevant information from them.

static CGEventRef event_tap_callback(CGEventTapProxy proxy,
                                     CGEventType type,
                                     CGEventRef event,
                                     void *refcon)
{
  NSEvent * sysEvent;

  // No event we care for? return ASAP
  if (type != NX_SYSDEFINED) return event;

  sysEvent = [NSEvent eventWithCGEvent:event];
  // No need to test event type, we know it is NSSystemDefined,
  // becuase that is the same as NX_SYSDEFINED

Also you cannot determine if that is the right kind of event by just looking at the data, you must also verify the subtype, that must be 8 for this kind of event:

  if ([sysEvent subtype] != 8) return event;

The next logical step is to split the data up into its components:

  int data = [sysEvent data1];
  int keyCode = (data & 0xFFFF0000) >> 16;
  int keyFlags = (data & 0xFFFF);
  int keyState = (keyFlags & 0xFF00) >> 8;
  BOOL keyIsRepeat = (keyFlags & 0x1) > 0;

And you probably don't care for repeating key events (that is when I keep the key pressed and it keeps sending the same event over and over again).

  // You probably won't care for repeating events
  if (keyIsRepeat) return event;

Finally you should not define any own constant, the system has ready to use constants for those keys:

  // Analyze the key
  switch (keyCode) {
    case NX_KEYTYPE_PLAY:
      // Play/Pause key
      if (keyState == 0x0A) {
        // Key down
        // ...do your stuff here...
        return NULL;
      } else if (keyState == 0x0B) {
        // Key Up
        // ...do your stuff here...
        return NULL;
      }
      // If neither down nor up, we don't know
      // what it is and better ignore it
      break;


    case NX_KEYTYPE_FAST:
      // (Fast) Forward
      break;

    case NX_KEYTYPE_REWIND:
       // Rewind key
       break;
  }

  // If we get here, we have not handled
  // the event and want system to handle it
  return event;
}

And if this still not works, my next question would be what your post_notification function looks like and do you also see the described problem if you don't call post_notification there, but just make a NSLog call about the event you just saw?

Sylviasylviculture answered 3/6, 2010 at 20:25 Comment(1)
Thanks, Mecki! I've fixed my code, but the problem persists. If I put post_notification under the "if (keyState == 0x0A) ...", iTunes will "take over" media keys eventually, if I put it under " if(keyState == 0x0B)", everything seems to work fine. post_notification simply calls postNotificationName: ... of the default NSNotificationCenter. Now, if I replace post_notification with sleep(1), the problem occurs much quicker. Does something kill my event tap if the callback takes too long? Is sending a notification too slow to be called from the callback?Wiggins
H
1

In your handler, check for the following type, and just re-enable the listener.

if (type == kCGEventTapDisabledByTimeout) {
    NSLog(@"Event Taps Disabled! Re-enabling");
            CGEventTapEnable(eventTap, true);
    return event;
}
Hula answered 13/3, 2015 at 9:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.