The security guys at work love to find unlocked computers and ‘Hoff’ them, basically leave a browser open with a picture of David Hasselhoff. I decided to fight back and created the Spy Screen Saver.
There are two ways to create a screensaver on MacOSX. One is using Quartz Composer which allows you to build the screen saver by chaining nodes together. The other is by building a cocoa based screen saver.
The advantage of a cocoa based screen saver is that it gives you full access to all of the MacOSX APIs that you would use in an ordinary app. In the case of the Spy Screen Saver we want access to QTKit which gives us an easy to use high level access to the Quicktime subsystem. If you want to get a handle on Screen Savers you should have a look at the two part series on Cocoa Dev Central:
Write a Screen Saver: Part I
Write a Screen Saver: Part II
I also used the LotsaBlanker Screensavers as example code, especially there image well and defaults handling code. I will give the users the option to display pictures for the screen saver.
The screensaver shouldn’t exit when the keyboard was pressed or the mouse was moved. To do this we have can capture the keypress and mouse move events and not pass those onto our superview. I made it so the ‘g’ key was an option to unlock the screensaver, other modifier keys also cause the screensaver to exit.
[code language=”objc”]- (void)keyDown:(NSEvent *)theEvent {
if ([[theEvent charactersIgnoringModifiers] isEqualTo: @"g"]) {
[super keyDown: theEvent];
[self finishRecording];
} else {
[self triggerRecording];
}
}
– (void)keyUp:(NSEvent *)theEvent {
if ([[theEvent charactersIgnoringModifiers] isEqualTo: @"g"]) {
[super keyUp: theEvent];
[self finishRecording];
}
}
– (void)mouseMoved:(NSEvent *)theEvent {
[self triggerRecording];
}
– (void) mouseDown:(NSEvent *)theEvent {
[self triggerRecording];
}[/code]
When the ‘victim’ has been detected we need set up the recording of the screensaver. The triggerRecording method starts the recording. To avoid recording for a long period of time we keep track of the user interactions with the userTriggers variable. Each time a mouse or keyboard is touched this is incremented.
[code language=”objc”]- (void) triggerRecording {
if (self.userTriggers == 0) {
self.captureSession = [[QTCaptureSession alloc] init];
BOOL success = NO;
NSError *error;
QTCaptureDevice *device = [QTCaptureDevice defaultInputDeviceWithMediaType:QTMediaTypeVideo];
if (device) {
success = [device open:&error];
if (!success) {
}
self.captureDeviceInput = [[QTCaptureDeviceInput alloc] initWithDevice:device];
success = [self.captureSession addInput:self.captureDeviceInput error:&error];
if (!success) {
// Handle error
}
self.captureMovieFileOutput = [[QTCaptureMovieFileOutput alloc] init];
success = [self.captureSession addOutput:self.captureMovieFileOutput error:&error];
if (!success) {
}
[self.captureMovieFileOutput setDelegate:self];
NSEnumerator *connectionEnumerator = [[self.captureMovieFileOutput connections] objectEnumerator];
QTCaptureConnection *connection;
while ((connection = [connectionEnumerator nextObject])) {
NSString *mediaType = [connection mediaType];
QTCompressionOptions *compressionOptions = nil;
if ([mediaType isEqualToString:QTMediaTypeVideo]) {
compressionOptions = [QTCompressionOptions compressionOptionsWithIdentifier:@"QTCompressionOptionsSD480SizeH264Video"];
} else if ([mediaType isEqualToString:QTMediaTypeSound]) {
compressionOptions = [QTCompressionOptions compressionOptionsWithIdentifier:@"QTCompressionOptionsHighQualityAACAudio"];
}
[self.captureMovieFileOutput setCompressionOptions:compressionOptions forConnection:connection];
}
[self.captureSession startRunning];
}
NSString *filePath = [NSString stringWithFormat:@"/Users/tupps/Desktop/Spy Saver Capture %f.mov", [NSDate timeIntervalSinceReferenceDate]];
[self.captureMovieFileOutput recordToOutputFileURL:[NSURL fileURLWithPath:filePath]];
}
self.userTriggers++;
[self performSelector:@selector(stopRecording) withObject:nil afterDelay:15.0];
}[/code]
The QTKit code is easy to follow. We basically we find a capture device capable of capturing video and then setting it up. We use 480 pixel wide video with H264 compression to keep the file sizes down.
The final step is to stop the recording. In this case it is a matter of finalising the capture file and then closing all the capture devices.
[code language=”objc”]- (void) stopRecording {
self.userTriggers–;
if (self.userTriggers < 1) {
//Stop
[self.captureMovieFileOutput recordToOutputFileURL:nil];
self.captureMovieFileOutput = nil;
[[self.captureDeviceInput device] close];
self.captureDeviceInput = nil;
[self.captureSession stopRunning];
self.captureSession = nil;
self.userTriggers = 0;
}
}[/code]
A final note, I use the LWImagePicker which I have renamed LTImagePicker. This isn’t an attempt to rip off the work Dag Ã…gren, but a quirk of the way system preferences work. The preferences and all the screen savers, are loaded in a single app. If you load the Spy Saver and the LotsaWater screen saver at the same time you will get an error because there are two implementations of the LWImagePicker, and which is used is undefined.
Update: Code available on GitHub, and the SpySaver Screen Saver can be downloaded.
Leave a Reply