]> git.sesse.net Git - vlc/blob - modules/gui/macosx/AppleRemote.m
* configure.ac: enabled Obj-C exceptions in the OBJCFLAGS (they're currently used...
[vlc] / modules / gui / macosx / AppleRemote.m
1 /*****************************************************************************
2  * AppleRemote.m
3  * AppleRemote
4  * $Id:$
5  *
6  * Created by Martin Kahr on 11.03.06 under a MIT-style license. 
7  * Copyright (c) 2006 martinkahr.com. All rights reserved.
8  *
9  * Permission is hereby granted, free of charge, to any person obtaining a 
10  * copy of this software and associated documentation files (the "Software"),
11  * to deal in the Software without restriction, including without limitation
12  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13  * and/or sell copies of the Software, and to permit persons to whom the
14  * Software is furnished to do so, subject to the following conditions:
15  *
16  * The above copyright notice and this permission notice shall be included
17  * in all copies or substantial portions of the Software.
18  *
19  * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
22  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25  * THE SOFTWARE.
26  *
27  *****************************************************************************
28  *
29  * Note that changes made by any members or contributors of the VideoLAN team
30  * (i.e. changes that were checked in to one of VideoLAN's source code
31  * repositories are licensed under the GNU General Public License version 2,
32  * or (at your option) any later version. 
33  * Thus, the following statements apply to our changes:
34  *
35  * Copyright (C) 2006 the VideoLAN team
36  * Authors: Eric Petit <titer@m0k.org>
37  *          Felix Kühne <fkuehne at videolan dot org>
38  *
39  * This program is free software; you can redistribute it and/or modify
40  * it under the terms of the GNU General Public License as published by
41  * the Free Software Foundation; either version 2 of the License, or
42  * (at your option) any later version.
43  *
44  * This program is distributed in the hope that it will be useful,
45  * but WITHOUT ANY WARRANTY; without even the implied warranty of
46  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
47  * GNU General Public License for more details.
48  *
49  * You should have received a copy of the GNU General Public License
50  * along with this program; if not, write to the Free Software
51  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
52  *****************************************************************************/
53
54 #import "AppleRemote.h"
55
56 const char* AppleRemoteDeviceName = "AppleIRController";
57 const int REMOTE_SWITCH_COOKIE=19;
58
59 @implementation AppleRemote
60
61 - (id) init
62 {       
63     self = [super init];
64
65     if ( self == [super init] ) {
66                 openInExclusiveMode = YES;
67                 queue = NULL;
68                 hidDeviceInterface = NULL;
69                 cookieToButtonMapping = [[NSMutableDictionary alloc] init];
70                 
71                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonVolume_Plus]      forKey:@"14_12_11_6_5_"];
72                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonVolume_Minus] forKey:@"14_13_11_6_5_"];           
73                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu]                     forKey:@"14_7_6_5_14_7_6_5_"];          
74                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay]                     forKey:@"14_8_6_5_14_8_6_5_"];
75                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight]            forKey:@"14_9_6_5_14_9_6_5_"];
76                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft]                     forKey:@"14_10_6_5_14_10_6_5_"];
77                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold]       forKey:@"14_6_5_4_2_"];
78                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold]        forKey:@"14_6_5_3_2_"];
79                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold]        forKey:@"14_6_5_14_6_5_"];
80                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Sleep]       forKey:@"18_14_6_5_18_14_6_5_"];
81                 [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched]       forKey:@"19_"];         
82         }
83         
84         return self;
85 }
86
87 - (void) dealloc {
88         [self stopListening:self];
89         [cookieToButtonMapping release];
90         [super dealloc];
91 }
92
93 - (void) setRemoteId: (int) value {
94         remoteId = value;
95 }
96 - (int) remoteId {
97         return remoteId;
98 }
99
100 - (BOOL) isRemoteAvailable {    
101         io_object_t hidDevice = [self findAppleRemoteDevice];
102         if (hidDevice != 0) {
103                 IOObjectRelease(hidDevice);
104                 return YES;
105         } else {
106                 return NO;              
107         }
108 }
109
110 - (BOOL) isListeningToRemote {
111         return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL);     
112 }
113
114 - (void) setListeningToRemote: (BOOL) value {
115         if (value == NO) {
116                 [self stopListening:self];
117         } else {
118                 [self startListening:self];
119         }
120 }
121
122 - (void) setDelegate: (id) _delegate {
123         if ([_delegate respondsToSelector:@selector(appleRemoteButton:pressedDown:)]==NO) return;
124         
125         [_delegate retain];
126         [delegate release];
127         delegate = _delegate;
128 }
129 - (id) delegate {
130         return delegate;
131 }
132
133 - (BOOL) isOpenInExclusiveMode {
134         return openInExclusiveMode;
135 }
136 - (void) setOpenInExclusiveMode: (BOOL) value {
137         openInExclusiveMode = value;
138 }
139
140 - (IBAction) startListening: (id) sender {      
141         if ([self isListeningToRemote]) return;
142         
143         io_object_t hidDevice = [self findAppleRemoteDevice];
144         if (hidDevice == 0) return;
145         
146         if ([self createInterfaceForDevice:hidDevice] == NULL) {
147                 goto error;
148         }
149         
150         if ([self initializeCookies]==NO) {
151                 goto error;
152         }
153
154         if ([self openDevice]==NO) {
155                 goto error;
156         }
157         goto cleanup;
158         
159 error:
160         [self stopListening:self];
161         
162 cleanup:        
163         IOObjectRelease(hidDevice);
164 }
165
166 - (IBAction) stopListening: (id) sender {
167         if (queue != NULL) {
168                 (*queue)->stop(queue);          
169                 
170                 //dispose of queue
171                 (*queue)->dispose(queue);               
172                 
173                 //release the queue we allocated
174                 (*queue)->Release(queue);       
175                 
176                 queue = NULL;
177         }
178         
179         if (allCookies != nil) {
180                 [allCookies autorelease];
181                 allCookies = nil;
182         }
183         
184         if (hidDeviceInterface != NULL) {
185                 //close the device
186                 (*hidDeviceInterface)->close(hidDeviceInterface);
187                 
188                 //release the interface 
189                 (*hidDeviceInterface)->Release(hidDeviceInterface);
190                 
191                 hidDeviceInterface = NULL;              
192         }       
193 }
194
195 @end
196
197 @implementation AppleRemote (Singleton) 
198
199 static AppleRemote* sharedInstance=nil;
200
201 + (AppleRemote*) sharedRemote { 
202         @synchronized(self) {
203         if (sharedInstance == nil) {
204             sharedInstance = [[self alloc] init];
205         }
206     }
207         return sharedInstance;
208 }
209 + (id)allocWithZone:(NSZone *)zone {
210     @synchronized(self) {
211         if (sharedInstance == nil) {
212             return [super allocWithZone:zone];
213         }
214     }   
215     return sharedInstance;
216 }
217 - (id)copyWithZone:(NSZone *)zone {
218     return self;
219 }
220 - (id)retain {
221     return self;
222 }
223 - (unsigned)retainCount {
224     return UINT_MAX;  //denotes an object that cannot be released
225 }
226 - (void)release {
227     //do nothing
228 }
229 - (id)autorelease {
230     return self;
231 }
232
233 @end
234
235 @implementation AppleRemote (PrivateMethods) 
236
237 - (IOHIDQueueInterface**) queue {
238         return queue;
239 }
240
241 - (IOHIDDeviceInterface**) hidDeviceInterface {
242         return hidDeviceInterface;
243 }
244
245
246 - (NSDictionary*) cookieToButtonMapping {
247         return cookieToButtonMapping;
248 }
249
250 - (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues {
251         NSNumber* buttonId = [[self cookieToButtonMapping] objectForKey: cookieString];
252         if (buttonId != nil) {
253                 if (delegate) {         
254                         [delegate appleRemoteButton:[buttonId intValue] pressedDown: (sumOfValues>0)];
255                 }               
256         } else {
257                 NSLog(@"Unknown button for cookiestring %@", cookieString);
258         }
259 }
260
261 @end
262
263 /*      Callback method for the device queue
264 Will be called for any event of any type (cookie) to which we subscribe
265 */
266 static void QueueCallbackFunction(void* target,  IOReturn result, void* refcon, void* sender) { 
267         AppleRemote* remote = (AppleRemote*)target;
268         
269         IOHIDEventStruct event; 
270         AbsoluteTime     zeroTime = {0,0};
271         NSMutableString* cookieString = [NSMutableString string];
272         SInt32                   sumOfValues = 0;
273         while (result == kIOReturnSuccess)
274         {
275                 result = (*[remote queue])->getNextEvent([remote queue], &event, zeroTime, 0);          
276                 if ( result != kIOReturnSuccess )
277                         continue;
278                 
279                 if (REMOTE_SWITCH_COOKIE == (int)event.elementCookie) {
280                         [remote setRemoteId: event.value];
281                         [remote handleEventWithCookieString: @"19_" sumOfValues: 0];
282                 } else {
283                         sumOfValues+=event.value;
284                         [cookieString appendString:[NSString stringWithFormat:@"%d_", event.elementCookie]];                                    
285                 }
286                 
287                 //printf("%d %d %d\n", event.elementCookie, event.value, event.longValue);              
288         }
289         
290         [remote handleEventWithCookieString: cookieString sumOfValues: sumOfValues];    
291         
292 }
293
294 @implementation AppleRemote (IOKitMethods)
295
296 - (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice {
297         io_name_t                               className;
298         IOCFPlugInInterface**   plugInInterface = NULL;
299         HRESULT                                 plugInResult = S_OK;
300         SInt32                                  score = 0;
301         IOReturn                                ioReturnValue = kIOReturnSuccess;
302         
303         hidDeviceInterface = NULL;
304         
305         ioReturnValue = IOObjectGetClass(hidDevice, className);
306         
307         if (ioReturnValue != kIOReturnSuccess) {
308                 NSLog(@"Error: Failed to get class name.");
309                 return NULL;
310         }
311         
312         ioReturnValue = IOCreatePlugInInterfaceForService(hidDevice,
313                                                                                                           kIOHIDDeviceUserClientTypeID,
314                                                                                                           kIOCFPlugInInterfaceID,
315                                                                                                           &plugInInterface,
316                                                                                                           &score);
317         if (ioReturnValue == kIOReturnSuccess)
318         {
319                 //Call a method of the intermediate plug-in to create the device interface
320                 plugInResult = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &hidDeviceInterface);
321                 
322                 if (plugInResult != S_OK) {
323                         NSLog(@"Error: Couldn't create HID class device interface");
324                 }
325                 // Release
326                 if (plugInInterface) (*plugInInterface)->Release(plugInInterface);
327         }
328         return hidDeviceInterface;
329 }
330
331 - (io_object_t) findAppleRemoteDevice {
332         CFMutableDictionaryRef hidMatchDictionary = NULL;
333         IOReturn ioReturnValue = kIOReturnSuccess;      
334         io_iterator_t hidObjectIterator = 0;
335         io_object_t     hidDevice = 0;
336         
337         // Set up a matching dictionary to search the I/O Registry by class
338         // name for all HID class devices
339         hidMatchDictionary = IOServiceMatching(AppleRemoteDeviceName);
340         
341         // Now search I/O Registry for matching devices.
342         ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator);
343         
344         if ((ioReturnValue == kIOReturnSuccess) && (hidObjectIterator != 0)) {
345                 hidDevice = IOIteratorNext(hidObjectIterator);
346         }
347         
348         // release the iterator
349         IOObjectRelease(hidObjectIterator);
350         
351         return hidDevice;
352 }
353
354 - (BOOL) initializeCookies {
355         IOHIDDeviceInterface122** handle = (IOHIDDeviceInterface122**)hidDeviceInterface;
356         IOHIDElementCookie              cookie;
357         long                                    usage;
358         long                                    usagePage;
359         id                                              object;
360         NSArray*                                elements = nil;
361         NSDictionary*                   element;
362         IOReturn success;
363         
364         if (!handle || !(*handle)) return NO;
365         
366         // Copy all elements, since we're grabbing most of the elements
367         // for this device anyway, and thus, it's faster to iterate them
368         // ourselves. When grabbing only one or two elements, a matching
369         // dictionary should be passed in here instead of NULL.
370         success = (*handle)->copyMatchingElements(handle, NULL, (CFArrayRef*)&elements);
371         
372         if (success == kIOReturnSuccess) {
373                 
374                 [elements autorelease];         
375                 /*
376                 cookies = calloc(NUMBER_OF_APPLE_REMOTE_ACTIONS, sizeof(IOHIDElementCookie)); 
377                 memset(cookies, 0, sizeof(IOHIDElementCookie) * NUMBER_OF_APPLE_REMOTE_ACTIONS);
378                 */
379                 allCookies = [[NSMutableArray alloc] init];
380                 unsigned int i;
381                 for (i=0; i< [elements count]; i++) {
382                         element = [elements objectAtIndex:i];
383                                                 
384                         //Get cookie
385                         object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementCookieKey) ];
386                         if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;
387                         if (object == 0 || CFGetTypeID(object) != CFNumberGetTypeID()) continue;
388                         cookie = (IOHIDElementCookie) [object longValue];
389                         
390                         //Get usage
391                         object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsageKey) ];
392                         if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;                        
393                         usage = [object longValue];
394                         
395                         //Get usage page
396                         object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsagePageKey) ];
397                         if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;                        
398                         usagePage = [object longValue];
399
400                         [allCookies addObject: [NSNumber numberWithInt:(int)cookie]];
401                 }
402         } else {
403                 return NO;
404         }
405         
406         return YES;
407 }
408
409 - (BOOL) openDevice {
410         HRESULT  result;
411         
412         IOHIDOptionsType openMode = kIOHIDOptionsTypeNone;
413         if ([self isOpenInExclusiveMode]) openMode = kIOHIDOptionsTypeSeizeDevice;      
414         IOReturn ioReturnValue = (*hidDeviceInterface)->open(hidDeviceInterface, openMode);     
415         
416         if (ioReturnValue == KERN_SUCCESS) {
417                 queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface);
418                 if (queue) {
419                         result = (*queue)->create(queue, 0, 12);        //depth: maximum number of elements in queue before oldest elements in queue begin to be lost.
420
421                         unsigned int i=0;
422                         for(i=0; i<[allCookies count]; i++) {
423                                 IOHIDElementCookie cookie = (IOHIDElementCookie)[[allCookies objectAtIndex:i] intValue];
424                                 (*queue)->addElement(queue, cookie, 0);
425                         }
426                                                                           
427                         // add callback for async events
428                         CFRunLoopSourceRef eventSource;
429                         ioReturnValue = (*queue)->createAsyncEventSource(queue, &eventSource);
430                         if (ioReturnValue == KERN_SUCCESS) {
431                                 ioReturnValue = (*queue)->setEventCallout(queue,QueueCallbackFunction, self, NULL);
432                                 if (ioReturnValue == KERN_SUCCESS) {
433                                         CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);                                  
434                                         //start data delivery to queue
435                                         (*queue)->start(queue); 
436                                         return YES;
437                                 } else {
438                                         NSLog(@"Error when setting event callout");
439                                 }
440                         } else {
441                                 NSLog(@"Error when creating async event source");
442                         }
443                 } else {
444                         NSLog(@"Error when opening device");
445                 }
446         }
447         return NO;                              
448 }
449
450 @end