diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2c2aff --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. + +fastlane/report.xml +fastlane/screenshots + +# OS X +# +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/ImageSnap.h b/ImageSnap.h index bc9eec9..f3863ad 100644 --- a/ImageSnap.h +++ b/ImageSnap.h @@ -4,82 +4,55 @@ // // Created by Robert Harder on 9/10/09. // + +#import #import -#import #include "ImageSnap.h" #define error(...) fprintf(stderr, __VA_ARGS__) #define console(...) (!g_quiet && printf(__VA_ARGS__)) #define verbose(...) (g_verbose && !g_quiet && fprintf(stderr, __VA_ARGS__)) -BOOL g_verbose = NO; -BOOL g_quiet = NO; -//double g_timelapse = -1; -NSString *VERSION = @"0.2.5"; - +static BOOL g_verbose; +static BOOL g_quiet; -@interface ImageSnap : NSObject { - - QTCaptureSession *mCaptureSession; - QTCaptureDeviceInput *mCaptureDeviceInput; - QTCaptureDecompressedVideoOutput *mCaptureDecompressedVideoOutput; - CVImageBufferRef mCurrentImageBuffer; -} +FOUNDATION_EXPORT NSString *const VERSION; +@interface ImageSnap : NSObject /** * Returns all attached QTCaptureDevice objects that have video. * This includes video-only devices (QTMediaTypeVideo) and * audio/video devices (QTMediaTypeMuxed). * - * @return autoreleased array of video devices + * @return array of video devices */ -+(NSArray *)videoDevices; ++ (NSArray *)videoDevices; /** * Returns the default QTCaptureDevice object for video * or nil if none is found. */ -+(QTCaptureDevice *)defaultVideoDevice; ++ (AVCaptureDevice *)defaultVideoDevice; /** * Returns the QTCaptureDevice with the given name * or nil if the device cannot be found. */ -+(QTCaptureDevice *)deviceNamed:(NSString *)name; - -/** - * Writes an NSImage to disk, formatting it according - * to the file extension. If path is "-" (a dash), then - * an jpeg representation is written to standard out. - */ -+ (BOOL) saveImage:(NSImage *)image toPath: (NSString*)path; - -/** - * Converts an NSImage to raw NSData according to a given - * format. A simple string search is performed for such - * characters as jpeg, tiff, png, and so forth. - */ -+(NSData *)dataFrom:(NSImage *)image asType:(NSString *)format; ++ (AVCaptureDevice *)deviceNamed:(NSString *)name; +- (void)setUpSessionWithDevice:(AVCaptureDevice *)device; +- (void)getReadyToTakePicture; /** * Primary one-stop-shopping message for capturing an image. * Activates the video source, saves a frame, stops the source, * and saves the file. */ -+(BOOL)saveSingleSnapshotFrom:(QTCaptureDevice *)device toFile:(NSString *)path; -+(BOOL)saveSingleSnapshotFrom:(QTCaptureDevice *)device toFile:(NSString *)path withWarmup:(NSNumber *)warmup; -+(BOOL)saveSingleSnapshotFrom:(QTCaptureDevice *)device toFile:(NSString *)path withWarmup:(NSNumber *)warmup withTimelapse:(NSNumber *)timelapse; - --(id)init; --(void)dealloc; - - --(BOOL)startSession:(QTCaptureDevice *)device; --(NSImage *)snapshot; --(void)stopSession; - +- (void)saveSingleSnapshotFrom:(AVCaptureDevice *)device + toFile:(NSString *)path + withWarmup:(NSNumber *)warmup + withTimelapse:(NSNumber *)timelapse; @end diff --git a/ImageSnap.m b/ImageSnap.m index 9ec7da9..a0d00c8 100644 --- a/ImageSnap.m +++ b/ImageSnap.m @@ -7,652 +7,230 @@ #import "ImageSnap.h" +static BOOL g_verbose = NO; +static BOOL g_quiet = NO; + +NSString *const VERSION = @"0.2.5"; @interface ImageSnap() +@property (nonatomic, strong) AVCaptureSession *captureSession; +@property (nonatomic, strong) AVCaptureDeviceInput *captureDeviceInput; +@property (nonatomic, strong) AVCaptureStillImageOutput *captureStillImageOutput; +@property (nonatomic, assign) CVImageBufferRef currentImageBuffer; +@property (nonatomic, strong) AVCaptureConnection *videoConnection; +@property (nonatomic, strong) NSDateFormatter *dateFormatter; -- (void)captureOutput:(QTCaptureOutput *)captureOutput - didOutputVideoFrame:(CVImageBufferRef)videoFrame - withSampleBuffer:(QTSampleBuffer *)sampleBuffer - fromConnection:(QTCaptureConnection *)connection; +#if OS_OBJECT_HAVE_OBJC_SUPPORT == 1 +@property (nonatomic, strong) dispatch_queue_t imageQueue; +#else +@property (nonatomic, assign) dispatch_queue_t imageQueue; +#endif @end - @implementation ImageSnap +#pragma mark - Object Lifecycle + +- (instancetype)init { + self = [super init]; + if (self) { + _dateFormatter = [NSDateFormatter new]; + _dateFormatter.dateFormat = @"yyyy-MM-dd_HH-mm-ss.SSS"; -- (id)init{ - self = [super init]; - mCaptureSession = nil; - mCaptureDeviceInput = nil; - mCaptureDecompressedVideoOutput = nil; - mCurrentImageBuffer = nil; - return self; + _imageQueue = dispatch_queue_create("Image Queue", NULL); + } + + return self; } -- (void)dealloc{ - - if( mCaptureSession ) [mCaptureSession release]; - if( mCaptureDeviceInput ) [mCaptureDeviceInput release]; - if( mCaptureDecompressedVideoOutput ) [mCaptureDecompressedVideoOutput release]; - CVBufferRelease(mCurrentImageBuffer); - - [super dealloc]; +- (void)dealloc { + [self.captureSession stopRunning]; + CVBufferRelease(self.currentImageBuffer); } +#pragma mark - Public Interface + +/** + * Returns all attached AVCaptureDevice objects that have video. + * This includes video-only devices (AVMediaTypeVideo) and + * audio/video devices (AVMediaTypeMuxed). + * + * @return array of video devices + */ ++ (NSArray *)videoDevices { + NSMutableArray *results = [NSMutableArray new]; + + [results addObjectsFromArray:[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]]; + [results addObjectsFromArray:[AVCaptureDevice devicesWithMediaType:AVMediaTypeMuxed]]; -// Returns an array of video devices attached to this computer. -+ (NSArray *)videoDevices{ - NSMutableArray *results = [NSMutableArray arrayWithCapacity:3]; - [results addObjectsFromArray:[QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeVideo]]; - [results addObjectsFromArray:[QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeMuxed]]; return results; } // Returns the default video device or nil if none found. -+ (QTCaptureDevice *)defaultVideoDevice{ - QTCaptureDevice *device = nil; - - device = [QTCaptureDevice defaultInputDeviceWithMediaType:QTMediaTypeVideo]; - if( device == nil ){ - device = [QTCaptureDevice defaultInputDeviceWithMediaType:QTMediaTypeMuxed]; - } ++ (AVCaptureDevice *)defaultVideoDevice { + + AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + + if (device == nil) { + device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeMuxed]; + } + return device; } // Returns the named capture device or nil if not found. -+(QTCaptureDevice *)deviceNamed:(NSString *)name{ - QTCaptureDevice *result = nil; - ++ (AVCaptureDevice *)deviceNamed:(NSString *)name { + AVCaptureDevice *result; + NSArray *devices = [ImageSnap videoDevices]; - for( QTCaptureDevice *device in devices ){ - if ( [name isEqualToString:[device description]] ){ + for (AVCaptureDevice *device in devices) { + if ([name isEqualToString:device.localizedName]) { result = device; - } // end if: match - } // end for: each device - - return result; -} // end - - -// Saves an image to a file or standard out if path is nil or "-" (hyphen). -+ (BOOL) saveImage:(NSImage *)image toPath: (NSString*)path{ - - NSString *ext = [path pathExtension]; - NSData *photoData = [ImageSnap dataFrom:image asType:ext]; - - // If path is a dash, that means write to standard out - if( path == nil || [@"-" isEqualToString:path] ){ - NSUInteger length = [photoData length]; - NSUInteger i; - char *start = (char *)[photoData bytes]; - for( i = 0; i < length; ++i ){ - putc( start[i], stdout ); - } // end for: write out - return YES; - } else { - return [photoData writeToFile:path atomically:NO]; + } } - - return NO; + return result; } +- (void)saveSingleSnapshotFrom:(AVCaptureDevice *)device + toFile:(NSString *)path + withWarmup:(NSNumber *)warmup + withTimelapse:(NSNumber *)timelapse { -/** - * Converts an NSImage into NSData. Defaults to jpeg if - * format cannot be determined. - */ -+(NSData *)dataFrom:(NSImage *)image asType:(NSString *)format{ - - NSData *tiffData = [image TIFFRepresentation]; - - NSBitmapImageFileType imageType = NSJPEGFileType; - NSDictionary *imageProps = nil; - - - // TIFF. Special case. Can save immediately. - if( [@"tif" rangeOfString:format options:NSCaseInsensitiveSearch].location != NSNotFound || - [@"tiff" rangeOfString:format options:NSCaseInsensitiveSearch].location != NSNotFound ){ - return tiffData; - } - - // JPEG - else if( [@"jpg" rangeOfString:format options:NSCaseInsensitiveSearch].location != NSNotFound || - [@"jpeg" rangeOfString:format options:NSCaseInsensitiveSearch].location != NSNotFound ){ - imageType = NSJPEGFileType; - imageProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:0.9] forKey:NSImageCompressionFactor]; - - } - - // PNG - else if( [@"png" rangeOfString:format options:NSCaseInsensitiveSearch].location != NSNotFound ){ - imageType = NSPNGFileType; - } - - // BMP - else if( [@"bmp" rangeOfString:format options:NSCaseInsensitiveSearch].location != NSNotFound ){ - imageType = NSBMPFileType; - } - - // GIF - else if( [@"gif" rangeOfString:format options:NSCaseInsensitiveSearch].location != NSNotFound ){ - imageType = NSGIFFileType; - } - - NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:tiffData]; - NSData *photoData = [imageRep representationUsingType:imageType properties:imageProps]; + double interval = timelapse == nil ? -1 : timelapse.doubleValue; - return photoData; -} // end dataFrom + verbose("Starting device..."); + verbose("Device started.\n"); + if (warmup == nil) { + // Skip warmup + verbose("Skipping warmup period.\n"); + } else { + double delay = warmup.doubleValue; + verbose("Delaying %.2lf seconds for warmup...", delay); + NSDate *now = [[NSDate alloc] init]; + [[NSRunLoop currentRunLoop] runUntilDate:[now dateByAddingTimeInterval:warmup.doubleValue]]; + verbose("Warmup complete.\n"); + } + if (interval > 0) { + verbose("Time lapse: snapping every %.2lf seconds to current directory.\n", interval); -/** - * Primary one-stop-shopping message for capturing an image. - * Activates the video source, saves a frame, stops the source, - * and saves the file. - */ + for (unsigned long seq = 0; ; seq++) { -+(BOOL)saveSingleSnapshotFrom:(QTCaptureDevice *)device toFile:(NSString *)path{ - return [self saveSingleSnapshotFrom:device toFile:path withWarmup:nil]; -} + // capture and write + [self takeSnapshotWithFilename:[self fileNameWithSequenceNumber:seq]]; // Capture a frame -+(BOOL)saveSingleSnapshotFrom:(QTCaptureDevice *)device toFile:(NSString *)path withWarmup:(NSNumber *)warmup{ - return [self saveSingleSnapshotFrom:device toFile:path withWarmup:warmup withTimelapse:nil]; -} - -+(BOOL)saveSingleSnapshotFrom:(QTCaptureDevice *)device - toFile:(NSString *)path - withWarmup:(NSNumber *)warmup - withTimelapse:(NSNumber *)timelapse{ - ImageSnap *snap; - NSImage *image = nil; - double interval = timelapse == nil ? -1 : [timelapse doubleValue]; - - snap = [[ImageSnap alloc] init]; // Instance of this ImageSnap class - verbose("Starting device..."); - if( [snap startSession:device] ){ // Try starting session - verbose("Device started.\n"); - - if( warmup == nil ){ - // Skip warmup - verbose("Skipping warmup period.\n"); - } else { - double delay = [warmup doubleValue]; - verbose("Delaying %.2lf seconds for warmup...",delay); - NSDate *now = [[NSDate alloc] init]; - [[NSRunLoop currentRunLoop] runUntilDate:[now dateByAddingTimeInterval: [warmup doubleValue]]]; - [now release]; - verbose("Warmup complete.\n"); + // sleep + [[NSRunLoop currentRunLoop] runUntilDate:[[NSDate date] dateByAddingTimeInterval:interval]]; } - - if ( interval > 0 ) { - - verbose("Time lapse: snapping every %.2lf seconds to current directory.\n", interval); - - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd_HH-mm-ss.SSS"]; - - // wait a bit to make sure the camera is initialized - //[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 1.0]]; - - for (unsigned long seq=0; ; seq++) - { - NSDate *now = [[NSDate alloc] init]; - NSString *nowstr = [dateFormatter stringFromDate:now]; - - verbose(" - Snapshot %5lu", seq); - verbose(" (%s)\n", [nowstr UTF8String]); - - // create filename - NSString *filename = [NSString stringWithFormat:@"snapshot-%05d-%s.jpg", seq, [nowstr UTF8String]]; - - // capture and write - image = [snap snapshot]; // Capture a frame - if (image != nil) { - [ImageSnap saveImage:image toPath:filename]; - console( "%s\n", [filename UTF8String]); - } else { - error( "Image capture failed.\n" ); - } - - // sleep - [[NSRunLoop currentRunLoop] runUntilDate:[now dateByAddingTimeInterval: interval]]; - - [now release]; - } - } else { - image = [snap snapshot]; // Capture a frame - - } - //NSLog(@"Stopping..."); - [snap stopSession]; // Stop session - //NSLog(@"Stopped."); - } // end if: able to start session - - [snap release]; - - if ( interval > 0 ){ - return YES; } else { - return image == nil ? NO : [ImageSnap saveImage:image toPath:path]; + [self takeSnapshotWithFilename:[self fileNameWithSequenceNumber:0]]; // Capture a frame } -} // end - -/** - * Returns current snapshot or nil if there is a problem - * or session is not started. - */ --(NSImage *)snapshot{ - verbose( "Taking snapshot...\n"); - - CVImageBufferRef frame = nil; // Hold frame we find - while( frame == nil ){ // While waiting for a frame - - //verbose( "\tEntering synchronized block to see if frame is captured yet..."); - @synchronized(self){ // Lock since capture is on another thread - frame = mCurrentImageBuffer; // Hold current frame - CVBufferRetain(frame); // Retain it (OK if nil) - } // end sync: self - //verbose( "Done.\n" ); - - if( frame == nil ){ // Still no frame? Wait a little while. - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 0.1]]; - } // end if: still nothing, wait - - } // end while: no frame yet - - // Convert frame to an NSImage - NSCIImageRep *imageRep = [NSCIImageRep imageRepWithCIImage:[CIImage imageWithCVImageBuffer:frame]]; - NSImage *image = [[[NSImage alloc] initWithSize:[imageRep size]] autorelease]; - [image addRepresentation:imageRep]; - verbose( "Snapshot taken.\n" ); - - return image; + [self stopSession]; } +- (void)setUpSessionWithDevice:(AVCaptureDevice *)device { + NSError *error; + // Create the capture session + self.captureSession = [AVCaptureSession new]; + if ([self.captureSession canSetSessionPreset:AVCaptureSessionPresetPhoto]) { + self.captureSession.sessionPreset = AVCaptureSessionPresetPhoto; + } -/** - * Blocks until session is stopped. - */ --(void)stopSession{ - verbose("Stopping session...\n" ); - - // Make sure we've stopped - while( mCaptureSession != nil ){ - verbose("\tCaptureSession != nil\n"); - - verbose("\tStopping CaptureSession..."); - [mCaptureSession stopRunning]; - verbose("Done.\n"); - - if( [mCaptureSession isRunning] ){ - verbose( "[mCaptureSession isRunning]"); - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 0.1]]; - }else { - verbose( "\tShutting down 'stopSession(..)'" ); - if( mCaptureSession ) [mCaptureSession release]; - if( mCaptureDeviceInput ) [mCaptureDeviceInput release]; - if( mCaptureDecompressedVideoOutput ) [mCaptureDecompressedVideoOutput release]; - - mCaptureSession = nil; - mCaptureDeviceInput = nil; - mCaptureDecompressedVideoOutput = nil; - } // end if: stopped - - } // end while: not stopped -} + // Create input object from the device + self.captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; + if (!error && [self.captureSession canAddInput:self.captureDeviceInput]) { + [self.captureSession addInput:self.captureDeviceInput]; + } + self.captureStillImageOutput = [AVCaptureStillImageOutput new]; + self.captureStillImageOutput.outputSettings = @{ AVVideoCodecKey : AVVideoCodecJPEG}; -/** - * Begins the capture session. Frames begin coming in. - */ --(BOOL)startSession:(QTCaptureDevice *)device{ - - verbose( "Starting capture session...\n" ); - - if( device == nil ) { - verbose( "\tCannot start session: no device provided.\n" ); - return NO; - } - - NSError *error = nil; - - // If we've already started with this device, return - if( [device isEqual:[mCaptureDeviceInput device]] && - mCaptureSession != nil && - [mCaptureSession isRunning] ){ - return YES; - } // end if: already running - - else if( mCaptureSession != nil ){ - verbose( "\tStopping previous session.\n" ); - [self stopSession]; - } // end if: else stop session - - - // Create the capture session - verbose( "\tCreating QTCaptureSession..." ); - mCaptureSession = [[QTCaptureSession alloc] init]; - verbose( "Done.\n"); - if( ![device open:&error] ){ - error( "\tCould not create capture session.\n" ); - [mCaptureSession release]; - mCaptureSession = nil; - return NO; - } - - - // Create input object from the device - verbose( "\tCreating QTCaptureDeviceInput with %s...", [[device description] UTF8String] ); - mCaptureDeviceInput = [[QTCaptureDeviceInput alloc] initWithDevice:device]; - verbose( "Done.\n"); - if (![mCaptureSession addInput:mCaptureDeviceInput error:&error]) { - error( "\tCould not convert device to input device.\n"); - [mCaptureSession release]; - [mCaptureDeviceInput release]; - mCaptureSession = nil; - mCaptureDeviceInput = nil; - return NO; - } - - - // Decompressed video output - verbose( "\tCreating QTCaptureDecompressedVideoOutput..."); - mCaptureDecompressedVideoOutput = [[QTCaptureDecompressedVideoOutput alloc] init]; - [mCaptureDecompressedVideoOutput setDelegate:self]; - verbose( "Done.\n" ); - if (![mCaptureSession addOutput:mCaptureDecompressedVideoOutput error:&error]) { - error( "\tCould not create decompressed output.\n"); - [mCaptureSession release]; - [mCaptureDeviceInput release]; - [mCaptureDecompressedVideoOutput release]; - mCaptureSession = nil; - mCaptureDeviceInput = nil; - mCaptureDecompressedVideoOutput = nil; - return NO; - } - - // Clear old image? - verbose("\tEntering synchronized block to clear memory..."); - @synchronized(self){ - if( mCurrentImageBuffer != nil ){ - CVBufferRelease(mCurrentImageBuffer); - mCurrentImageBuffer = nil; - } // end if: clear old image - } // end sync: self - verbose( "Done.\n"); - - [mCaptureSession startRunning]; - verbose("Session started.\n"); - - return YES; -} // end startSession - - - -// This delegate method is called whenever the QTCaptureDecompressedVideoOutput receives a frame -- (void)captureOutput:(QTCaptureOutput *)captureOutput - didOutputVideoFrame:(CVImageBufferRef)videoFrame - withSampleBuffer:(QTSampleBuffer *)sampleBuffer - fromConnection:(QTCaptureConnection *)connection -{ - verbose( "." ); - if (videoFrame == nil ) { - verbose( "'nil' Frame captured.\n" ); - return; + if ([self.captureSession canAddOutput:self.captureStillImageOutput]) { + [self.captureSession addOutput:self.captureStillImageOutput]; } - - // Swap out old frame for new one - CVImageBufferRef imageBufferToRelease; - CVBufferRetain(videoFrame); - - @synchronized(self){ - imageBufferToRelease = mCurrentImageBuffer; - mCurrentImageBuffer = videoFrame; - } // end sync - CVBufferRelease(imageBufferToRelease); - -} - -@end + for (AVCaptureConnection *connection in self.captureStillImageOutput.connections) { + for (AVCaptureInputPort *port in [connection inputPorts]) { + if ([port.mediaType isEqual:AVMediaTypeVideo] ) { + self.videoConnection = connection; + break; + } + } + if (self.videoConnection) { break; } + } -// ////////////////////////////////////////////////////////// -// -// //////// B E G I N C - L E V E L M A I N //////// // -// -// ////////////////////////////////////////////////////////// - -int processArguments(int argc, const char * argv[]); -void printUsage(int argc, const char * argv[]); -int listDevices(); -NSString *generateFilename(); -QTCaptureDevice *getDefaultDevice(); - - -// Main entry point. Since we're using Cocoa and all kinds of fancy -// classes, we have to set up appropriate pools and loops. -// Thanks to the example http://lists.apple.com/archives/cocoa-dev/2003/Apr/msg01638.html -// for reminding me how to do it. -int main (int argc, const char * argv[]) { - NSApplicationLoad(); // May be necessary for 10.5 not to crash. - - NSAutoreleasePool *pool; - pool = [[NSAutoreleasePool alloc] init]; - [NSApplication sharedApplication]; - - int result = processArguments(argc, argv); - - // [pool release]; - [pool drain]; - return result; + if ([self.captureSession canAddOutput:self.captureStillImageOutput]) { + [self.captureSession addOutput:self.captureStillImageOutput]; + } } +- (void)getReadyToTakePicture { + [self.captureSession startRunning]; +} +#pragma mark - Internal Methods /** - * Process command line arguments and execute program. + * Returns current snapshot or nil if there is a problem + * or session is not started. */ -int processArguments(int argc, const char * argv[] ){ - - NSString *filename = nil; - QTCaptureDevice *device = nil; - NSNumber *warmup = nil; - NSNumber *timelapse = nil; - - - int i; - for( i = 1; i < argc; ++i ){ - - // Handle command line switches - if (argv[i][0] == '-') { - - // Dash only? Means write image to stdout - if( argv[i][1] == 0 ){ - filename = @"-"; - g_quiet = YES; - } else { - - // Which switch was given - switch (argv[i][1]) { - - // Help - case '?': - case 'h': - printUsage( argc, argv ); - return 0; - break; - - - // Verbose - case 'v': - g_verbose = YES; - break; - - case 'q': - g_quiet = YES; - break; - - - // List devices - case 'l': - listDevices(); - return 0; - break; - - // Specify device - case 'd': - if( i+1 < argc ){ - device = [ImageSnap deviceNamed:[NSString stringWithUTF8String:argv[i+1]]]; - if( device == nil ){ - error( "Device \"%s\" not found.\n", argv[i+1] ); - return 11; - } // end if: not found - ++i; // Account for "follow on" argument - } else { - error( "Not enough arguments given with 'd' flag.\n" ); - return (int)'d'; - } - break; - - // Specify a warmup period before picture snaps - case 'w': - if( i+1 < argc ){ - warmup = [NSNumber numberWithFloat:[[NSString stringWithUTF8String:argv[i+1]] floatValue]]; - ++i; // Account for "follow on" argument - } else { - error( "Not enough arguments given with 'w' flag.\n" ); - return (int)'w'; - } - break; - - // Timelapse - case 't': - if( i+1 < argc ){ - timelapse = [NSNumber numberWithDouble:[[NSString stringWithUTF8String:argv[i+1]] doubleValue]]; - //g_timelapse = [timelapse doubleValue]; - ++i; // Account for "follow on" argument - } else { - error( "Not enough arguments given with 't' flag.\n" ); - return (int)'t'; - } - break; - - - - } // end switch: flag value - } // end else: not dash only - } // end if: '-' - - // Else assume it's a filename - else { - filename = [NSString stringWithUTF8String:argv[i]]; - } - - } // end for: each command line argument - - - // Make sure we have a filename - if( filename == nil ){ - filename = generateFilename(); - verbose( "No filename specified. Using %s\n", [filename UTF8String] ); - } // end if: no filename given - - if( filename == nil ){ - error( "No suitable filename could be determined.\n" ); - return 1; - } - - - // Make sure we have a device - if( device == nil ){ - device = getDefaultDevice(); - verbose( "No device specified. Using %s\n", [[device description] UTF8String] ); - } // end if: no device given - - if( device == nil ){ - error( "No video devices found.\n" ); - return 2; - } else { - console( "Capturing image from device \"%s\"...", [[device description] UTF8String] ); - } - - - // Image capture - if( [ImageSnap saveSingleSnapshotFrom:device toFile:filename withWarmup:warmup withTimelapse:timelapse] ){ - console( "%s\n", [filename UTF8String] ); - } else { - error( "Error.\n" ); - } // end else - - return 0; -} +- (void)takeSnapshotWithFilename:(NSString *)filename { + __weak __typeof__(filename) weakFilename = filename; + [self.captureStillImageOutput captureStillImageAsynchronouslyFromConnection:self.videoConnection + completionHandler: + ^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) { + NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; -void printUsage(int argc, const char * argv[]){ - printf( "USAGE: %s [options] [filename]\n", argv[0] ); - printf( "Version: %s\n", [VERSION UTF8String] ); - printf( "Captures an image from a video device and saves it in a file.\n" ); - printf( "If no device is specified, the system default will be used.\n" ); - printf( "If no filename is specfied, snapshot.jpg will be used.\n" ); - printf( "Supported image types: JPEG, TIFF, PNG, GIF, BMP\n" ); - printf( " -h This help message\n" ); - printf( " -v Verbose mode\n"); - printf( " -l List available video devices\n" ); - printf( " -t x.xx Take a picture every x.xx seconds\n" ); - printf( " -q Quiet mode. Do not output any text\n"); - printf( " -w x.xx Warmup. Delay snapshot x.xx seconds after turning on camera\n" ); - printf( " -d device Use named video device\n" ); + dispatch_async(self.imageQueue, ^{ + [imageData writeToFile:weakFilename atomically:YES]; + }); + }]; } +/** + * Blocks until session is stopped. + */ +- (void)stopSession { + verbose("Stopping session...\n" ); + // Make sure we've stopped + while (self.captureSession != nil) { + verbose("\tCaptureSession != nil\n"); + verbose("\tStopping CaptureSession..."); + [self.captureSession stopRunning]; + verbose("Done.\n"); + if ([self.captureSession isRunning]) { + verbose("[captureSession isRunning]"); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } else { + verbose("\tShutting down 'stopSession(..)'" ); -/** - * Prints a list of video capture devices to standard out. - */ -int listDevices(){ - NSArray *devices = [ImageSnap videoDevices]; - - [devices count] > 0 - ? printf("Video Devices:\n") - : printf("No video devices found.\n"); - - for( QTCaptureDevice *device in devices ){ - printf( "%s\n", [[device description] UTF8String] ); - } // end for: each device - return [devices count]; + self.captureSession = nil; + self.captureDeviceInput = nil; + self.captureStillImageOutput = nil; + } + } } -/** - * Generates a filename for saving the image, presumably - * because the user didn't specify a filename. - * Currently returns snapshot.tiff. - */ -NSString *generateFilename(){ - NSString *result = @"snapshot.jpg"; - return result; -} // end - - -/** - * Gets a default video device, or nil if none is found. - * For now, simply queries ImageSnap. May be fancier - * in the future. - */ -QTCaptureDevice *getDefaultDevice(){ - return [ImageSnap defaultVideoDevice]; -} // end +- (NSString *)fileNameWithSequenceNumber:(unsigned long)sequenceNumber { + NSDate *now = [NSDate date]; + NSString *nowstr = [self.dateFormatter stringFromDate:now]; + return [NSString stringWithFormat:@"snapshot-%05lu-%s.jpg", sequenceNumber, nowstr.UTF8String]; +} +@end diff --git a/ImageSnap.xcodeproj/project.pbxproj b/ImageSnap.xcodeproj/project.pbxproj index b2ee9be..98053db 100644 --- a/ImageSnap.xcodeproj/project.pbxproj +++ b/ImageSnap.xcodeproj/project.pbxproj @@ -7,10 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 007BE2EC1C011FD100232768 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 007BE2EB1C011FD100232768 /* main.m */; }; + 007BE2F11C0168F400232768 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007BE2F01C0168F400232768 /* AVFoundation.framework */; }; 8DD76F790486A8DE00D96B5E /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09AB6884FE841BABC02AAC07 /* CoreFoundation.framework */; }; D44480A7105BFF5600756CA8 /* ImageSnap.m in Sources */ = {isa = PBXBuildFile; fileRef = D44480A6105BFF5600756CA8 /* ImageSnap.m */; }; D48D572910598E4000885DA8 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D48D572810598E4000885DA8 /* Cocoa.framework */; }; - D48D572B10598E4000885DA8 /* QTKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D48D572A10598E4000885DA8 /* QTKit.framework */; }; D48D572D10598E4000885DA8 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D48D572C10598E4000885DA8 /* Quartz.framework */; }; /* End PBXBuildFile section */ @@ -27,11 +28,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 007BE2EB1C011FD100232768 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 007BE2EF1C015B1C00232768 /* StyleSettings.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = StyleSettings.plist; sourceTree = ""; }; + 007BE2F01C0168F400232768 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; 09AB6884FE841BABC02AAC07 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = /System/Library/Frameworks/CoreFoundation.framework; sourceTree = ""; }; D44480A5105BFF5600756CA8 /* ImageSnap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageSnap.h; sourceTree = ""; }; D44480A6105BFF5600756CA8 /* ImageSnap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageSnap.m; sourceTree = ""; }; D48D572810598E4000885DA8 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; - D48D572A10598E4000885DA8 /* QTKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QTKit.framework; path = /System/Library/Frameworks/QTKit.framework; sourceTree = ""; }; D48D572C10598E4000885DA8 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = /System/Library/Frameworks/Quartz.framework; sourceTree = ""; }; D49AA9851085011600FE189B /* ReadMeOrDont.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = ReadMeOrDont.rtf; sourceTree = ""; }; D4C1B149105C049B00FCB6A2 /* imagesnap */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = imagesnap; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,9 +45,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 007BE2F11C0168F400232768 /* AVFoundation.framework in Frameworks */, 8DD76F790486A8DE00D96B5E /* CoreFoundation.framework in Frameworks */, D48D572910598E4000885DA8 /* Cocoa.framework in Frameworks */, - D48D572B10598E4000885DA8 /* QTKit.framework in Frameworks */, D48D572D10598E4000885DA8 /* Quartz.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -55,9 +58,10 @@ 08FB7794FE84155DC02AAC07 /* ImageSnap */ = { isa = PBXGroup; children = ( + 007BE2EF1C015B1C00232768 /* StyleSettings.plist */, 08FB7795FE84155DC02AAC07 /* Source */, C6859E96029091FE04C91782 /* Documentation */, - 08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */, + 08FB779DFE84155DC02AAC07 /* Frameworks */, 19C28FBDFE9D53C911CA2CBB /* Products */, ); name = ImageSnap; @@ -68,19 +72,20 @@ children = ( D44480A5105BFF5600756CA8 /* ImageSnap.h */, D44480A6105BFF5600756CA8 /* ImageSnap.m */, + 007BE2EB1C011FD100232768 /* main.m */, ); name = Source; sourceTree = ""; }; - 08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */ = { + 08FB779DFE84155DC02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( + 007BE2F01C0168F400232768 /* AVFoundation.framework */, 09AB6884FE841BABC02AAC07 /* CoreFoundation.framework */, D48D572810598E4000885DA8 /* Cocoa.framework */, - D48D572A10598E4000885DA8 /* QTKit.framework */, D48D572C10598E4000885DA8 /* Quartz.framework */, ); - name = "External Frameworks and Libraries"; + name = Frameworks; sourceTree = ""; }; 19C28FBDFE9D53C911CA2CBB /* Products */ = { @@ -109,13 +114,13 @@ 8DD76F760486A8DE00D96B5E /* Sources */, 8DD76F780486A8DE00D96B5E /* Frameworks */, 8DD76F7B0486A8DE00D96B5E /* CopyFiles */, + 007BE2EE1C01548D00232768 /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = imagesnap; - productInstallPath = "$(HOME)/bin"; productName = ImageSnap; productReference = D4C1B149105C049B00FCB6A2 /* imagesnap */; productType = "com.apple.product-type.tool"; @@ -126,7 +131,7 @@ 08FB7793FE84155DC02AAC07 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0410; + LastUpgradeCheck = 0700; }; buildConfigurationList = 1DEB924B08733DCA0010E9CD /* Build configuration list for PBXProject "ImageSnap" */; compatibilityVersion = "Xcode 3.2"; @@ -147,11 +152,28 @@ }; /* End PBXProject section */ +/* Begin PBXShellScriptBuildPhase section */ + 007BE2EE1C01548D00232768 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -z ${SKIP_OBJCLEAN} || ${SKIP_OBJCLEAN} != 1 ]]; then\nif [[ -d \"${LOCAL_APPS_DIR}/Objective-Clean.app\" ]]; then\n\"${LOCAL_APPS_DIR}\"/Objective-Clean.app/Contents/Resources/ObjClean.app/Contents/MacOS/ObjClean \"${SRCROOT}\"\nelse\necho \"warning: You have to install and set up Objective-Clean to use its features!\"\nfi\nfi"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 8DD76F760486A8DE00D96B5E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 007BE2EC1C011FD100232768 /* main.m in Sources */, D44480A7105BFF5600756CA8 /* ImageSnap.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -163,12 +185,14 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_OBJC_ARC = YES; COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; GCC_MODEL_TUNING = G5; GCC_OPTIMIZATION_LEVEL = 0; GCC_VERSION = com.apple.compilers.llvm.clang.1_0; INSTALL_PATH = /usr/local/bin; + MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_NAME = imagesnap; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = macosx; @@ -179,9 +203,11 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_OBJC_ARC = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_MODEL_TUNING = G5; INSTALL_PATH = /usr/local/bin; + MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_NAME = imagesnap; SDKROOT = macosx; }; @@ -190,13 +216,14 @@ 1DEB924C08733DCA0010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_OPTIMIZATION_LEVEL = 0; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx10.4; + SDKROOT = macosx; VALID_ARCHS = "i386 x86_64"; }; name = Debug; @@ -204,11 +231,11 @@ 1DEB924D08733DCA0010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_32_64_BIT)"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; - SDKROOT = macosx10.4; + MACOSX_DEPLOYMENT_TARGET = 10.10; + SDKROOT = macosx; VALID_ARCHS = "i386 x86_64"; }; name = Release; diff --git a/ReadMeOrDont.rtf b/ReadMeOrDont.rtf index 32c1cba..8718b3b 100644 --- a/ReadMeOrDont.rtf +++ b/ReadMeOrDont.rtf @@ -1,4 +1,4 @@ -{\rtf1\ansi\ansicpg1252\cocoartf1138 +{\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170 {\fonttbl\f0\fnil\fcharset0 Verdana;\f1\fswiss\fcharset0 ArialMT;\f2\fmodern\fcharset0 Courier; \f3\fmodern\fcharset0 Courier-Oblique;} {\colortbl;\red255\green255\blue255;\red0\green16\blue81;} @@ -20,7 +20,7 @@ rob@iHarder.net \fs28 \cf0 Capture Images from the Command Line \f1\fs32 \ \pard\pardeftab720\sa320\qc -{\field{\*\fldinst{HYPERLINK "http://iharder.net/imagesnap"}}{\fldrslt +{\field{\*\fldinst{HYPERLINK "http://iharder.net/imagesnap"}}{\fldrslt \f0\b0 \cf0 http://iharder.net/imagesnap}} \f0\b0 \ \pard\pardeftab720\sa320 @@ -30,11 +30,11 @@ rob@iHarder.net \f1\b\fs38 \cf2 Installation\ \pard\pardeftab720\sa320 -\f0\b0\fs32 \cf0 Copy the +\f0\b0\fs32 \cf0 Copy the \f2\fs26 imagesnap -\f0\fs32 file to someplace on your path like +\f0\fs32 file to someplace on your path like \f2\fs26 /usr/local/bin -\f0\fs32 , or leave it in a "current directory," and call it with +\f0\fs32 , or leave it in a "current directory," and call it with \f2\fs26 ./imagesnap \f0\fs32 instead.\ Enjoy!\ @@ -59,7 +59,7 @@ Capturing image from device "iSight"..................icu.jpg\ \pard\pardeftab720\sa320 \f0\fs32 \cf0 \ -If you have multiple video devices attached to your computer, use the +If you have multiple video devices attached to your computer, use the \f2\fs26 -l \f0\fs32 ("el") flag to list them:\ \pard\pardeftab720 @@ -71,8 +71,8 @@ DV\ \pard\pardeftab720\sa320 \f0\fs32 \cf0 \ -To select a specific video device use the -\f2\fs26 -d +To select a specific video device use the +\f2\fs26 -d \f3\i device \f0\i0\fs32 flag:\ \pard\pardeftab720 @@ -93,7 +93,7 @@ $ open snapshot.jpg\ Image Formats\ \pard\pardeftab720\sa320 -\f0\b0\fs32 \cf0 The following image formats are supported and are determined by the filename extension: JPEG, TIFF, PNG, GIF, BMP.\ +\f0\b0\fs32 \cf0 Only JPEG output is supported.\ \pard\pardeftab720\sa300 \f1\b\fs38 \cf2 Changes\ @@ -103,7 +103,7 @@ Image Formats\ {\listtext \'95 }v0.2.4 - Found bug that caused crash on Mac OS X 10.5 (but not 10.6).\ {\listtext \'95 }v0.2.4beta - Tracking bug that causes crash on Mac OS X 10.5 (but not 10.6).\ {\listtext \'95 }v0.2.3 - Fixed bug that caused all images to be saved as TIFF. Not sure when this bug was introduced.\ -{\listtext \'95 }v0.2.2 - Added ability to output jpeg to standard out. Made executable lowercase +{\listtext \'95 }v0.2.2 - Added ability to output jpeg to standard out. Made executable lowercase \f2\fs26 imagesnap \f0\fs32 .\ {\listtext \'95 }v0.2.1 - Changed name from ImageCapture to ImageSnap to avoid confusion with Apple's Image Capture application.\ diff --git a/StyleSettings.plist b/StyleSettings.plist new file mode 100644 index 0000000..1335883 --- /dev/null +++ b/StyleSettings.plist @@ -0,0 +1,70 @@ + + + + + kRequireSpaceAfterColon + + kRequireOpenBraceOnMethodSignatureLine + + kRequireOpenBraceOnConditionalStatementLine + + kRequireNoEmptyLineAsFirstLineInMethod + + KNumberOfEmptyLinesBetweenMethods + 1 + kNumberOfEmptyLinesBeforeImplementation + 1 + kNumberOfEmptyLinesAfterImplementation + 1 + kMethodParameterPrefix + + kElseFormatRule + 0 + kEqualSpacingRule + 1 + kRequireSpaceAfterConditionalStatement + + kElseIfFormatRule + 0 + kRequireSpaceAboveBreak + + kInterfaceColonSpacingRule + 1 + kRequireSpaceAfterCommas + + kRequireSpaceBeforeCaseColons + + kSignatureAsteriskSpacingRule + 2 + kNumberOfEmptyLinesAboveClassExtension + 1 + kNumberOfEmptyLinesAboveInterface + 1 + kViolationsShouldBeErrors + + kRequireSpaceAfterParameterType + + kNumberOfEmptyLinesAfterPragma + 1 + kNumberOfEmptyLinesAboveEnd + 1 + kComparisonSpacingRule + 1 + kNumberOfEmptyLinesAboveCategory + 1 + kRequireSpaceAfterPropertyDeclaration + + kRequireSpaceAfterMethodType + + kRequireSpaceAfterReturnType + + kRequireSpaceAfterPropertyTypeDeclaration + + kRequireSpaceBeforeProtocolDeclarations + + kRequireSpaceBetweenTernaryOperators + + kRequireSpaceBeforeParameter + + + diff --git a/imagesnap b/imagesnap index d2c65f2..8bcd661 100755 Binary files a/imagesnap and b/imagesnap differ diff --git a/main.m b/main.m new file mode 100644 index 0000000..8783192 --- /dev/null +++ b/main.m @@ -0,0 +1,193 @@ +#import + +#import "ImageSnap.h" + +int processArguments(int argc, const char * argv[]); +void printUsage(int argc, const char * argv[]); +int listDevices(); +NSString *generateFilename(); +AVCaptureDevice *getDefaultDevice(); + +int main(int argc, const char * argv[]) { + NSApplicationLoad(); // May be necessary for 10.5 not to crash. + + @autoreleasepool { + [NSApplication sharedApplication]; + + int result = processArguments(argc, argv); + + return result; + } +} + +/** + * Process command line arguments and execute program. + */ +int processArguments(int argc, const char * argv[]) { + + NSString *filename; + AVCaptureDevice *device; + NSNumber *warmup; + NSNumber *timelapse; + + for (int i = 1; i < argc; ++i) { + + // Handle command line switches + if (argv[i][0] == '-') { + + // Dash only? Means write image to stdout + if (argv[i][1] == 0) { + filename = @"-"; + g_quiet = YES; + } else { + + // Which switch was given + switch (argv[i][1]) { + + // Help + case '?': + case 'h': + printUsage(argc, argv); + return 0; + break; + + + // Verbose + case 'v': + g_verbose = YES; + break; + + case 'q': + g_quiet = YES; + break; + + + // List devices + case 'l': + listDevices(); + return 0; + break; + + // Specify device + case 'd': + if (i+1 < argc) { + device = [ImageSnap deviceNamed:@(argv[i+1])]; + if (device == nil) { + error("Device \"%s\" not found.\n", argv[i+1]); + return 11; + } + ++i; // Account for "follow on" argument + } else { + error("Not enough arguments given with 'd' flag.\n"); + return (int)'d'; + } + break; + + // Specify a warmup period before picture snaps + case 'w': + if (i+1 < argc) { + warmup = @(@(argv[i+1]).floatValue); + ++i; // Account for "follow on" argument + } else { + error("Not enough arguments given with 'w' flag.\n"); + return (int)'w'; + } + break; + + // Timelapse + case 't': + if (i+1 < argc) { + timelapse = @(@(argv[i+1]).doubleValue); + ++i; // Account for "follow on" argument + } else { + error("Not enough arguments given with 't' flag.\n"); + return (int)'t'; + } + break; + } + } + } else { + // assume it's a filename + filename = @(argv[i]); + } + + } + + // Make sure we have a filename + if (filename == nil) { + filename = generateFilename(); + verbose("No filename specified. Using %s\n", [filename UTF8String]); + } + + if (filename == nil) { + error("No suitable filename could be determined.\n"); + return 1; + } + + // Make sure we have a device + if (device == nil) { + device = getDefaultDevice(); + verbose("No device specified. Using %s\n", [device.description UTF8String]); + } + + if (device == nil) { + error("No video devices found.\n"); + return 2; + } else { + console("Capturing image from device \"%s\"...", [device.description UTF8String]); + } + + // Image capture + ImageSnap *imageSnap = [ImageSnap new]; + [imageSnap setUpSessionWithDevice:device]; + [imageSnap getReadyToTakePicture]; + [imageSnap saveSingleSnapshotFrom:device toFile:filename withWarmup:warmup withTimelapse:timelapse]; + + return 0; +} + +void printUsage(int argc, const char * argv[]) { + printf("USAGE: %s [options] [filename]\n", argv[0]); + printf("Version: %s\n", VERSION.UTF8String); + printf("Captures an image from a video device and saves it in a file.\n"); + printf("If no device is specified, the system default will be used.\n"); + printf("If no filename is specfied, snapshot.jpg will be used.\n"); + printf("JPEG is the only supported output type.\n"); + printf(" -h This help message\n"); + printf(" -v Verbose mode\n"); + printf(" -l List available video devices\n"); + printf(" -t x.xx Take a picture every x.xx seconds\n"); + printf(" -q Quiet mode. Do not output any text\n"); + printf(" -w x.xx Warmup. Delay snapshot x.xx seconds after turning on camera\n"); + printf(" -d device Use named video device\n"); +} + +/** + * Prints a list of video capture devices to standard out. + */ +int listDevices() { + NSArray *devices = [ImageSnap videoDevices]; + + printf(devices.count > 0 ? "Video Devices:\n" : "No video devices found.\n"); + + for (AVCaptureDevice *device in devices) { + printf("%s\n", device.description.UTF8String); + } + return devices.count; +} + +/** + * Generates a filename for saving the image, presumably + * because the user didn't specify a filename. + */ +NSString *generateFilename() { + return @"snapshot.jpg"; +} + +/** + * Gets a default video device, or nil if none is found. + * For now, simply queries ImageSnap. + */ +AVCaptureDevice *getDefaultDevice() { + return [ImageSnap defaultVideoDevice]; +}