Capturing Webcam Images with JSCocoa

Content

Capturing Webcam Images with JSCocoa

Posted in:

The Obj-J syntax supported by JSCocoa makes it easy for Cocoa programmers to whip up a quick script that makes use of core frameworks. In this example we use QTKit to access the webcam and capture a series of still images.

The QTCapture delegate that features here can get called off the main thread hence we make use of NSLock to produce atomic accessors where required.

This script is designed to be run within KosmicTask, hence the KosmicTaskController references. JSCocoa tasks run within KosmicTask execute as applets that are fully formed instances of NSApplication. This means that they are 100% Cocoa applications and can make use of all the Cocoa framework and Obj-C runtime resources. JSCocoa's support for BridgeSupport means that we can also access a wide range of C defined functions and constants such as QTMediaTypeVideo, an NSString constant.

Utilising this script in KosmicTask enables the capturing of webcam images from remote machines.

/*

Created by Mugginsoft on 17 Nov 2012 16:39:44

Script: JavaScript Cocoa

Credits: Erik Rothoff http://erikrothoff.com/2011/07/capturing-a-photo-programmatically-with-o...

Notes:

*/

// load the QTKit framework
// required for basic device capture and acquisition
loadFramework("QTKit")

// load the CoreVideo framework
// required for CVBufferRetain et al
loadFramework("CoreVideo")
	
// define a Cocoa Obj-C class
class KosmicTask < NSObject
{
    // start method
    - (void)start
    {    	
    	// enable debug logging if required
    	this.debugLogging = false
    	
    	// log task started
		log("Task started...");

		// array to hold image file paths
		this.paths = []
		
		// locks
		this.frameLock = NSLock.instance
		this.captureLock = NSLock.instance
		
		// enable frame capture
		this.captureFrame = true

 	    // setup for video capture	
    	if (![this setupVideoCapture]) return
    	  
    	// start video capture
    	[this startVideoCapture]
    	
  		// keep the task running after the entry point function exits
  		// as the video capture API will callback to this class as its delegate
  		KosmicTaskController.keepTaskAlive = true      
    }
       
    // setup for video capture
    - (BOOL)setupVideoCapture
    {
    	 this._count = 0

    	// get video device
 		// QTMediaTypeVideo is a string defined by QTKit
 		// we mix Obj-J and JavaScript dot syntax here!
  		this.videoDevice = [QTCaptureDevice defaultInputDeviceWithMediaType:QTMediaTypeVideo] 
  		if (!this.videoDevice) {
  			this.error = "Cannot connect to webcam video device. Check webcam is attached."
  			return false
  		}
  		
  		log("Default video device name = " + this.videoDevice.localizedDisplayName)
  		
  		// check if we can access
  		if (this.videoDevice.isInUseByAnotherApplication) {
  			this.error = "Cannot access device. Another application has exclusive control."
  			return false
  		}
  		
  		// the open method argument is **NSError.
  		// we define a new outArgument variable to handle this
  		var errorOut = new outArgument
  		var success = this.videoDevice.open(errorOut)
  		if (!success) {
  			log("Cannot open default video device")
  			var error = errorOut.outValue
  			this.error = "Error : " + error.code + " " + error.localizedDescription
  			return false

  		}

    	// log video device opened
    	if (this.videoDevice.isOpen) {
			log(this.videoDevice.localizedDisplayName + " is open")
    	}
    	
    	// configure the capture session
    	this.videoInput = [[QTCaptureDeviceInput alloc] initWithDevice:this.videoDevice]
    	this.videoSession = [[QTCaptureSession alloc] init]
    	success = [this.videoSession addInput:this.videoInput error:errorOut]
    	if (!success) {
  			log("Could not connect video input to session")
  			var error = errorOut.outValue
  			this.error = "Error : " + error.code + " " + error.localizedDescription
  			return false  		
    	}
    	
    	// configure the video output
    	this.output = [[QTCaptureDecompressedVideoOutput alloc] init]
    	[this.output setDelegate:this];
    	success = [this.videoSession addOutput:this.output error:errorOut];
    	if (!success) {
  			log("Could not connect video output to session")
  			var error = errorOut.outValue
  			this.error = "Error : " + error.code + " " + error.localizedDescription
  			return false
    	}

		return true
    }
    
    // start capturing video  	
    - (void)startVideoCapture {
    	this.captureFrame = true	// this should likely be atomic
    	[this.videoSession startRunning];
    	
    }
    
    // suspend video capture
    - (void)suspendVideoCapture {
    	this.captureFrame = false
    	[this.videoSession stopRunning];
    	
    }

    // end video capture
    - (void)endVideoCapture {
    	this.captureFrame = false
    	[this.videoSession stopRunning];

		// close the video device
  		this.videoDevice.close
  		[this.videoSession removeInput:this.videoInput]
  		[this.videoSession removeOutput:this.videoOutput]
  		
  		// log video device closed
  		if (!this.videoDevice.isOpen) {
			log(this.videoDevice.localizedDisplayName + " is closed");
  		}
    	
    }
    
    // persist the current video frame
    - (void)persistVideoFrame
    {
     	this._count += 1
     	this.captureFrame = false
     	  	
  		// get image representation
  		imageRep = [NSCIImageRep imageRepWithCIImage:[CIImage imageWithCVImageBuffer: this.videoFrame]]
  		this.videoFrame = nil
  		
    	image = [[NSImage alloc] initWithSize:[imageRep size]]
    	[image addRepresentation:imageRep]
    
    	// get JPEG rep
    	bitmapData = [image TIFFRepresentation]
    	bitmapRep = [NSBitmapImageRep imageRepWithData:bitmapData]   	
    	imageData = [bitmapRep representationUsingType:NSJPEGFileType properties:nil]
    	
    	// write to file
    	var imagePath = KosmicTaskController.resultFileWithName('image' + this._count +'.jpg');
    	
		var taskResult = null
		
    	// save image to file
    	if ([imageData writeToFile:imagePath atomically:false]) {
    		
    		// push path to end of paths array
    		this.paths.push(imagePath)
    	} else {
    		
    		// we have an error
    		taskResult = {kosmicError: "Error writing captured images to file"}
    	}
    	
    	// log image capture
    	log("image " + this._count + " captured")
    	
		// have we fufilled our image quota ?
     	if (this._count >= this.captureCount) {
     		
     		// end the capture
     		[this endVideoCapture]
     		
     		// form the file result
     		taskResult = {kosmicFile: this.paths}
     	}
     	
     	// release the video frame
     	CVBufferRelease(this.videoFrame)
     	this.videoFrame = nil
     	
     	// if we have a result we are done
     	if (taskResult != null) {
     		KosmicTaskController.stopTask(taskResult)
     	} else {
     		
     		// suspend capture
     		[this suspendVideoCapture]
     		
     		// log wait
    		log("Next image will be captured in " + this.captureInterval + " secs")     
    		
    		// schedule next capture
    		var selector = @selector(startVideoCapture)		
     		[this performSelector:selector withObject:null afterDelay: this.captureInterval]
     	}

    }
    
    // delegate method
    // we define videoFrame to be of type id as CVImageBufferRef is not defined in the CoreVideo bridge support
    - (void)captureOutput:(QTCaptureOutput *)captureOutput didOutputVideoFrame:(id)videoFrame withSampleBuffer:(QTSampleBuffer *)sampleBuffer fromConnection:(QTCaptureConnection *)connection
    {    
    	var persistFrame = false
    		
    	// this method may not be called on the main thread hence 
    	// the need to make the videoFrame and captureFrame accessors atomic (i.e: thread safe)

		// capture frame if required
		if (this.captureFrame) {		
    		this.videoFrame = videoFrame
    		CVBufferRetain(this.videoFrame)
    		this.captureFrame = false
    		persistFrame = true
		}
		
    	// persist the video frame on the main thread
    	if (persistFrame) {
    		var selector = @selector(persistVideoFrame)		
    		[this performSelectorOnMainThread:selector withObject:nil waitUntilDone:true]
    	}
    	
    }
    
    // setVideoFrame - atomic
    - (void)setVideoFrame:(id)videoFrame 
    {
    	if (this.debugLogging) log('setVideoFrame:')
    	[this.frameLock lock]
    	this._videoFrame = videoFrame
    	[this.frameLock unlock]
    }

    // videoFrame - atomic
    - (id)videoFrame
    {
    	if (this.debugLogging) log('videoFrame')
    	[this.frameLock lock]
    	var v = this._videoFrame
    	[this.frameLock unlock]
    	return v
    }
    
    // setCaptureFrame - atomic
    - (void)setCaptureFrame:(BOOL)captureFrame
    {
     	if (this.debugLogging) log('setCaptureFrame:')
   		[this.captureLock lock]
    	this._captureFrame = captureFrame
    	[this.captureLock unlock]
    }
    
    // captureFrame - atomic
    - (BOOL)captureFrame
    {
    	if (this.debugLogging) log('captureFrame')
    	[this.captureLock lock]
    	var v = this._captureFrame
    	[this.captureLock unlock]
    	return v
    }

}

// task entry point
function kosmicTask(captureCount, captureInterval) 
{
	
	// allocate a task object
	var task = KosmicTask.alloc.init
	
	// set properties
	task.captureCount = captureCount
	task.captureInterval = captureInterval
	
	// start the task
	task.start

	// return error is it exists
	if (task.error != null) {
		var error = task.error
	}
	
	// check that the task is scheduled to remain alive
	else if (!KosmicTaskController.keepTaskAlive) {
		error = "Task failed" 
	}
	
	// return error result if required
	if (error) {
		return {kosmicError: error}
	}
}