More asynchronicity: asynchronous image encoding

I have been doing some image encoding lately and, as expected, I run into problems. The problems appear usually when the images are large, and manifest themselves through the usual “UI becomes unresponsive” during the encoding process – which can take up to 12-15 seconds sometime and is not nice at all. I was using the JPEGEncoder inside the flex framework to do the job. Looking for possible alternatives, I found this asynchronous encoder class – takes the class provided by Adobe and makes the encoding asynchronous – something really close to my needs. I was looking for a little bit more flexibility and the ability to cancel an ongoing encoding operation, so I took my own stab at it, adjusting the encoder provided by the Flex framework to be able to control the encoding function calls externally.

The original class has an internalEncode() class which is the encoding workhorse. That can be broken into 3 pieces: start/initialization, the encoding loop and finish/completion. The encoding loop is the more interesting part though. I am using the doing work over frames approach as I did here. I got rid of the internalEncode, promoted some of the method internal vars to class members, added some encoding progress information, provided access to the encoded image  and assigned the original encode name to the asynchronous loop. The new encode() will be called repeatedly by an external class until it completes. On every iteration it checks if it’s still allowed to run.

New code:

// The source is either a BitmapData or a ByteArray.
private var _sourceBitmapData:BitmapData;
private var _sourceByteArray:ByteArray;

//image width and height
private var _width:int;
private var _height:int;

// Encode 8x8 macroblocks
private var DCY:Number;
private var DCU:Number;
private var DCV:Number;

/**
 * @private
 * This is used for caching the the current exit Y position of the pixel for index
 * after each batch of iterations
 */
private var _breakIndex:int;

/**
 * @private
 */
private var mustExit:Function;

/**
 * public mutator for exitCondition function,
 * this is provided by the class client
 */
public function set exitCondition(value:Function):void
{
	if (mustExit != value)
	{
		mustExit = value;
	}
}

/**
 * @private
 */
private var _completed:Boolean;

/**
 * Indicates whether the encoding operation has completed;
 */
public function get completed():Boolean
{
	return _completed;
}

/**
 * Indicates the progress of the encoding operation
 */
public function get progress():Number
{
	return _breakIndex;
}

/**
 * Indicates the total of the encoding operation
 */
public function get total():Number
{
	return _height;
}

/**
 * Holds the encoded image information
 */
public function get imageData():ByteArray
{
	return byteout;
}

Start:

	public function start(bitmapData:BitmapData):void
	{
		setup(bitmapData, bitmapData.width, bitmapData.height);
	}
private function setup(source:Object, width:int, height:int):void
{
	_completed = false;
	_isRunning = true;
	// The source is either a BitmapData or a ByteArray.
	_sourceBitmapData = source as BitmapData;
	_sourceByteArray = source as ByteArray;

	_width = width;
	_height = height;

	// Initialize bit writer
	byteout = new ByteArray();
	bytenew = 0;
	bytepos = 7;

	// Add JPEG headers
	writeWord(0xFFD8); // SOI
	writeAPP0();
	writeDQT();
	writeSOF0(_width, _height);
	writeDHT();
	writeSOS();

	// Encode 8x8 macroblocks
	DCY = 0;
	DCU = 0;
	DCV = 0;
	bytenew = 0;
	bytepos = 7;
}

Asynchronous encoding loop:

public function encode():void
{
	for (var yPos:int = _breakIndex; yPos < _height; yPos+=8)
	{
		if (mustExit())
		{
			_breakIndex = yPos;
			return;
		}

		for (var xPos:int = 0; xPos < _width; xPos+=8) 		
		{ 			 			
			RGB2YUV(_sourceBitmapData, _sourceByteArray, xPos, yPos, _width, height); 			 			
			DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); 			
			DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);
 			DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); 		
		} 	
	} 	 	
	complete(); 
} 

Completion:

 		 private function complete():void { 	// Do the bit alignment of the EOI marker 	if (bytepos >= 0)
	{
		var fillbits:BitString = new BitString();
		fillbits.len = bytepos + 1;
		fillbits.val = (1 << (bytepos + 1)) - 1;
		writeBits(fillbits);
	}

	// Add EOI
	writeWord(0xFFD9);
	_completed = true;

	clean();
}

Usage:

To use the class on needs to supply the 2 external dependencies: the exit condition and an external caller which calls the encoder at regular intervals. I use an Event.ENTER_FRAME handler as the external caller. The exit condition is time based instead of iteration based. The encoder is allowed to run a certain percent of each frame time. Each iteration, the encoder checks the external exit condition and proceeds accordingly. Code snippets below and working example at the end of the post.

When creating the encoder one needs to set the exit condition also:

 /
/init the encoder and setup the exit condition function
//this is an external function which indicates whether the loop is still allowed to run or not 	_
encoder = new AsyncJPEGEncoder(100);

//setup exit condition is checked by the encoder on every loop 	_
encoder.exitCondition = encoderNeedsToExit;

private var _exitTime:Number; //calculated every frame code snippent follows below
protected function encoderNeedsToExit():Boolean
{
	return (getTimer() >= _exitTime);
}

Starting the encoder is a 2 step process:
1. Hook an external caller for the asynchronous encode loop.
2. Call start on the encoder to run its initlaization setup

	//start the encoder
	var capture:BitmapData = someBitmapData; //can be webcamera capture, image etc...

	//register an enter frame listener which will call the encoder on every frame
	systemManager.stage.addEventListener(Event.ENTER_FRAME, encodeAsync, false, 100);
	_encoder.start(capture);

	protected function encodeAsync(event:Event):void
	{
		//calculate estimated exit time
		_exitTime = getTimer() + Math.floor(1000 * .75 / systemManager.stage.frameRate);

		//call loop function
		_encoder.encode();

		//check porgress or completion
		if (_encoder.completed)
		{
			//get the encoded bytes
			_imageBytes = _encoder.imageData;
			systemManager.stage.removeEventListener(Event.ENTER_FRAME, encodeAsync);
		}
		else
		{
			var percent:Number = Math.floor(100 * (_encoder.progress / _encoder.total));
			//handle progress
		}
	}

To stop/cancel the encoding process also requires 2 steps:
1. Remove the external caller for the asynchronous encode loop.
2. Call cancel() on the encoder to cleanup

//stop encoder by removing enter frame listener
systemManager.stage.removeEventListener(Event.ENTER_FRAME, encodeAsync);
_encoder.cancel();

Here is a simple example and the .FXP project available for DOWNLOAD.

PS: There’s an optimized version of the JPEG encoder for FP10 at bytearray.org, recommended to apply the modifications to it if you are working with Flex 4.0 and above.

Leave a Reply

Your email address will not be published. Required fields are marked *

*