Remote Control Wrapper 2.0

More than a year ago I released the first version of a Objective-C class to interact with the Apple Remote Control. With Sofa Control 2.x this wrapper got some great new features. Today these changes are released to the public as a version 2.0.

I strongly recommend to upgrade to the new version of the wrapper. Hopefully the new features awake your interest.

New Features

  • Support for Leopard (9A466, 9A499)
  • Extension mechanism to support different remote controls. Beside the Apple Remote Control the wrapper comes with support for
  • Simulation of hold events and multi-click events
  • Cooperation mode to share the remote control devices among applications
  • Bug fixes

Let’s see how easy it is to use the new wrapper code in your application with some sample code.

How to use the wrapper

Get events from the Apple Remote Control

The class AppleRemote contains the code for interacting with the device. Whenever the user presses a button on the remote control an event is retrieved by the AppleRemote instance. The event is delegated to the delegation method sendRemoteButtonEvent:pressedDown:remoteControl: in your application. The delegation method is defined in RemoteControl.h.

  1. Implement the delegation method in the class where you want to deal with the events of the remote control

    // implementation file
    - (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event 
                       pressedDown: (BOOL) pressedDown 
                     remoteControl: (RemoteControl*) remoteControl 
    {
        NSLog(@"Button %d pressed down %d", event, pressedDown);
    }
    
  2. Start listening for events

    Instantiate the class AppleRemote and provide a reference to the instance of your class that implements the delegation method.

    remoteControl = [[AppleRemote alloc] initWithDelegate: self];
    [remoteControl startListening: self];
    

Using different devices

Right now the wrapper ships with support for three devices:

  • AppleRemote: Apple Remote Control
  • KeyspanFrontRowControl: Keyspan RF Remote for FrontRow
  • GlobalKeyboardDevice: Registers global keyboard shortcuts to provide a virtual remote control

If you want to support more than one device in your application it would be a tedious job to tell each device to start or stop listening. With the RemoteControlContainer you can easily manage a number of devices. Every method that is called on the RemoteControlContainer instance is delegated to every device it manages.

Let’s see an example on how to use the container:

// as in the example above you have to provide a reference to the object 
// that implements the delegation method
RemoteControlContainer* container = [[RemoteControlContainer alloc] initWithDelegate: self];

// tell the container which devices you want to use
[container instantiateAndAddRemoteControlDeviceWithClass: [AppleRemote class]];
[container instantiateAndAddRemoteControlDeviceWithClass: [KeyspanFrontRowControl class]];
[container instantiateAndAddRemoteControlDeviceWithClass: [GlobalKeyboardDevice class]];

// now tell the container that you want to get the events
// this activates every device that is present
[container startListening: self];

Multi Clicks and Hold Events

By default the Apple Remote does not send hold events for the up and down button. The class MultiClickRemoteBehavior acts as a delegate for the Remote Control classes (it implements the delegate method). An instance of that class adds the following behaviors on top of a remote control device:

  • Hold Events for all buttons
  • Multi Click recognition if activated

The MultiClickRemoteBehavior class defines another delegate method that has the click count as a parameter as well. Check out Sample Code 3 in MainController.m for an example.

Exclusive access and cooperation with other applications

The remote controls are shared among all processes. Only one application can interact with the remote control at any time. The interaction with the device is done in an exclusive mode. This ensures that background processes like Front Row don’t receive events.

Because of the exclusive access your application has to release the remote control when it is no longer in use. The best suitable solution for most applications is to implement the following NSApplication delegate methods.

- (void)applicationWillBecomeActive:(NSNotification *)aNotification {
    NSLog(@"Application will become active - Using remote controls");
    [remoteControl startListening: self];
}
- (void)applicationWillResignActive:(NSNotification *)aNotification {
    NSLog(@"Application will resign active - Releasing remote controls");
    [remoteControl stopListening: self];
}

Interprocess Notifications

Some applications want to use the remote control even if their application is not active. Sofa Control is such an application. It provides a way to control other applications with the remote control. As the background process still has exclusive access to the remote control, any other application will fail to get access.

Together with Peter Maurer I tried to come up with a user friendly solution for this problem. We came up with a clever mechanism of sending distributed notifications among the processes. A notification to request the exclusive access to the remote control and a notification that is being send when an application released the remote control.

As an example the following happens when you use Peter Maurer’s yFlicks while Sofa Control is running in the background.

  1. Sofa Control uses the Remote Control in exclusive mode.
  2. The user activates the yFlicks application.
  3. yFlicks tries to get exclusive access to the remote control but fails as Sofa Control has still exclusive access
  4. The Remote Control class in the yFlicks process sends out the distributed notification mac.remotecontrols.RequestForRemoteControl to ask for access to the Remote Control.
  5. Sofa Control receives the distributed notification and stops listening to the Remote Control. By stopping to listen to the Remote Control the distributed notification mac.remotecontrols.FinishedUsingRemoteControl is send by the Sofa Control process.
  6. yFlicks receives the notification and retries to get access to the Remote Control. As Sofa Control has already released the Remote Control yFlicks now has exclusive access to the Remote Control.
  7. When the user leaves the yFlicks application (e.g. press and hold the menu button), yFlicks stops listening to the Remote Control. A notification is send again and this time Sofa Control regains access to the Remote Control.

This complex mechanism is completely hidden in the implementation of the wrapper. There is no special code necessary to use this cooperation feature.

Guidelines

When you add support for the remote control to your application you should think about the following topics:

Provide a logical mapping

The mapping of remote control buttons to the functions of your application shall be logical and consistent with the behavior of other applications like Front Row, Keynote, iPhoto, …

Provide a way to leave your application by just using the remote control

Keep in mind that your application may be used in a media central environment without keyboard and mouse. In these cases your app might me activated by some other application (like Sofa Control). If the user has no chance to leave your application by just using the remote control he is stuck in your app and would need to attach a mouse/keyboard. A somewhat unsatisfying user experience.

Design

The following UML diagram shows the Remote Control Wrapper classes and their relationships. Remote Control Classes

The central class of the system is the Remote Control class. It defines the common interface (methods) for all remote control devices which are represented as subclasses (the leafs of the tree).

The Remote Control Container has a special relationship with the base class Remote Control. On the one side it is a subclass of Remote Control and on the other side it manages a number of Remote Control instances. The Container holds these instances in a list. When a method (that is defined in the base class RemoteControl) is called on the container instance it calls the very same method on each instance it holds in the list. This kind of design is well known as the Composite Pattern.

The MultiClickRemoteBehavior implements the informal protocol of the Remote Control Delegate. It implements the delegate method defined in RemoteControl.h. Plugging an instance of MultiClickRemoteBehavior into an instance of RemoteControl adds new behavior. The dynamics of Objective-C allow a very loose/decoupled implementation of delegate methods but in general this kind of design is named the Strategy Pattern.

Secure Event Input

Apple released a security update earlier this year that introduced an odd behavior. Whenever SecureEventInput is activated or deactivated the exclusive access to the remote control device is lost. This results in a very strange behavior where a press on the Menu button activates FrontRow while your app still gets the event.

There is a simple trick to solve the problem. Enabling the SecureEventInput and keeping it enabled. With that no change of the flag occurs any longer and everything is working fine.

I’m pretty sure this is a kind of bug at Apple and I was in contact with the responsible Apple Engineer about this. This solution is not a perfect one - I know. One of the side effects is that applications that listen for special global keyboard shortcuts (like Quicksilver) may get into problems as they no longer get the events.

By default the fix is disabled. There is a NSUserDefaults setting to enable the fix. Add the following code to the startup code of your application to enable the fix.

NSDictionary* defaultValues = [NSDictionary dictionaryWithObjectsAndKeys: 
    [NSNumber numberWithBool: YES], @"remoteControlWrapperFixSecureEventInputBug", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultValues];

Apple?

I opened a feature request last year asking for an official API to use the remote control (rdar://problem/4632245). The status is still open and probably will be for a longer time.

In the meantime this is the best I can offer for the Mac community to use the remote control. If you find a bug, some crazy code or if you add support for another device please leave a comment or send me an email.

Download the Remote Control Wrapper from the Source Code Page

15 Responses to “Remote Control Wrapper 2.0”

  1. ulion Says:

    why your remote control wrapper does not handle RequestForRemoteControl notify?
    since this wrapper does not handle it, what app will handle it?

  2. martin Says:

    Most applications should release the remote control as soon as the application loses the frontmost state. While they are frontmost they probably want to stick with the remote control.

    Handling the RequestForRemoteControl notification might need some application specific actions as well (closing menus, …).
    As an example, Sofa Control handles the RequestForRemoteControl notificiation. It releases the remote control and brings up a visual that tells the user that the application XYZ is active now.

  3. Chris Says:

    I’m in the process of adding Apple Remote support to a home automation app that I began working on recently (http://shion.aetherial.net). A couple of comments / questions:

    1. I get a 404 error when I try to download the file above. Can you fix this or send me a copy at the address provided?

    2. Under what license are you releasing this? I’m using the MIT license in my own work and I wondered whether your code would be compatible with my license.

    Thanks in advance!

  4. martin Says:

    Thanks for the info that the link is broken. I fixed it. You can download the source from the source code page. Everything is licensed under the MIT license.

  5. Apple Remote support in the next release of Shion Says:

    […] night, I spent some time adding Apple Remote support to Shion using Martin Kahr’s Remote Control Wrapper. I’m happy to report that the functionality works well for both on/off switches and […]

  6. Chris Says:

    Thanks! As the trackback above says, I’m already using it in my own projects. It’s a very nice bit of code that you should be very proud of. I had no problems getting any of it to work.

  7. Thorsten Says:

    Can I access the wrapper from Carbon, too?

  8. martin Says:

    For interacting with the device the IOKit framework is being used. This a C based API. So when you extract the code from the Objective-C classes you can use it in Carbon based applications as well. You have to translate the NSArray/NS… to CFArray/CF… code as well of course.

  9. Nazz Says:

    I used to use the old AppRemote code on Tiger and it worked fine. It doesn’t work on Leopard though. I downloaded the new code you have here too and it doesn’t work under Leopard either.

    I’ve built the RemoteControlWrapper and launched the app it will not recognize the Apple Remote after I click on Listen to Remote. No other apps are running and the remote works fine when using iTunes, etc. No errors are reported in the Console log. I’m on the first generation 24″ iMac. Any ideas?

  10. Nazz Says:

    I take that back, the old code was running simultaneously. The new code is working fine on Leopard. Woohoo!

  11. David M. Cotter Says:

    is there a way to get repeated press events, instead of a single hold event?

    eg: when i hold the vol+ key, i want to get a stream of vol+ key events, at the same pace as the keyboard “autoKey” events.

  12. martin Says:

    There is no such option in the code yet. But it’s easy to do as you get a separate event for button press and button release.
    So you can start executing a method periodically when the button is pressed, and stop it when the button is being released.
    e.g.: in Sofa Control I’m doing it like this
    [self performSelector:@selector(buttonHoldTimer:) withObject:self afterDelay:0.1];

  13. Jim Crompton Says:

    “For interacting with the device the IOKit framework is being used. This a C based API. So when you extract the code from the Objective-C classes you can use it in Carbon based applications as well. You have to translate the NSArray/NS… to CFArray/CF… code as well of course.”

    I’d really, really, really like to be able to understand that - but I’m a pure Carbon head and don’t do Cocoa.
    Any chance of a quick Carbon guide?

  14. martin Says:

    Hi Jim,

    I’m a Cocoa guy and try to avoid Carbon if possible :-) Let’s see if I can help.

    Objective-C is a object orientated language. These means you are executing functions on objects. The Carbon based APIs are in some sense object orientated as well. The main difference is that you are using a number of functions where you provide the data structure in the first parameter.

    Let’s see how some Cocoa code converts to Carbon code:
    // allocate an Array - there are two function calls: alloc and init here
    NSMutableArray* a = [[NSMutableArray alloc] init];
    // allocate a string object - the next line defines a string object (compareable to CFStringRef)
    NSString* value = @”some string”;
    // here we call the function addObject
    [a addObject: @”some string”];

    In Carbon it would look like this:
    CFMutableArrayRef a = CFArrayCreateMutable(NULL, 0, NULL);
    CFStringRef value = CFSTR(”some string”);
    CFArrayAppendValue(a, value);

    So it’s pretty straight forward to convert Cocoa code to Carbon code as long as there are matching methods in both frameworks.
    You can even mix Carbon and Cocoa code pretty easy.

    NSArray is “toll-free bridged” with its Core Foundation counterpart, CFArray. What this means is that the Core Foundation type is interchangeable in function or method calls with the bridged Foundation object, providing you cast one type to the other. Therefore, in an API where you see an NSArray * parameter, you can pass in a CFArrayRef, and in an API where you see a CFArrayRef parameter, you can pass in an NSArray instance. This arrangement also applies to your concrete subclasses of NSArray. See Carbon-Cocoa Integration Guide for more information on toll-free bridging.

    hope this helps.

  15. vinay Says:

    Thanks for publishing this. It will be very useful to my application.