package com.almalence.sony.cameraremote; import java.io.IOException; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import com.almalence.sony.cameraremote.utils.SimpleLiveviewSlicer; import com.almalence.sony.cameraremote.utils.SimpleLiveviewSlicer.Payload; /** * A SurfaceView based class to draw liveview frames serially. */ public class SimpleStreamSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = SimpleStreamSurfaceView.class.getSimpleName(); private boolean mWhileFetching; private final BlockingQueue<byte[]> mJpegQueue = new ArrayBlockingQueue<byte[]>(2); private final boolean mInMutableAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; private Thread mDrawerThread; private int mPreviousWidth = 0; private int mPreviousHeight = 0; private int mSurfaceWidth = 0; private int mSurfaceHeight = 0; Matrix rotateMatrix = new Matrix(); private final Paint mFramePaint; private StreamErrorListener mErrorListener; private StreamFrameListener mFrameListener; /** * Constructor * * @param context */ public SimpleStreamSurfaceView(Context context) { super(context); getHolder().addCallback(this); mFramePaint = new Paint(); mFramePaint.setDither(true); } /** * Constructor * * @param context * @param attrs */ public SimpleStreamSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); getHolder().addCallback(this); mFramePaint = new Paint(); mFramePaint.setDither(true); } /** * Constructor * * @param context * @param attrs * @param defStyle */ public SimpleStreamSurfaceView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); getHolder().addCallback(this); mFramePaint = new Paint(); mFramePaint.setDither(true); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // do nothing. } @Override public void surfaceCreated(SurfaceHolder holder) { // do nothing. } @Override public void surfaceDestroyed(SurfaceHolder holder) { mWhileFetching = false; } /** * Start retrieving and drawing liveview frame data by new threads. * * @return true if the starting is completed successfully, false otherwise. * @see SimpleLiveviewSurfaceView#bindRemoteApi(SimpleRemoteApi) */ public boolean start(final String streamUrl, StreamFrameListener frameListener, StreamErrorListener errorListener) { mErrorListener = errorListener; mFrameListener = frameListener; rotateMatrix.reset(); rotateMatrix.postRotate(90); if (streamUrl == null) { Log.e(TAG, "start() streamUrl is null."); mWhileFetching = false; mErrorListener.onError(StreamErrorListener.StreamErrorReason.OPEN_ERROR); return false; } if (mWhileFetching) { Log.w(TAG, "start() already starting."); return false; } mWhileFetching = true; // A thread for retrieving liveview data from server. new Thread() { @Override public void run() { Log.d(TAG, "Starting retrieving streaming data from server."); SimpleLiveviewSlicer slicer = null; try { // Create Slicer to open the stream and parse it. slicer = new SimpleLiveviewSlicer(); slicer.open(streamUrl); while (mWhileFetching) { final Payload payload = slicer.nextPayload(); if (payload == null) { // never occurs Log.e(TAG, "Liveview Payload is null."); continue; } if (mJpegQueue.size() == 2) { mJpegQueue.remove(); } mJpegQueue.add(payload.jpegData); if (mFrameListener != null) { mFrameListener.onFrameAvailable(payload.jpegData); } } } catch (IOException e) { Log.w(TAG, "IOException while fetching: " + e.getMessage()); mErrorListener.onError(StreamErrorListener.StreamErrorReason.IO_EXCEPTION); } finally { if (slicer != null) { slicer.close(); } if (mDrawerThread != null) { mDrawerThread.interrupt(); } mJpegQueue.clear(); mWhileFetching = false; } } }.start(); // A thread for drawing liveview frame fetched by above thread. mDrawerThread = new Thread() { @Override public void run() { Log.d(TAG, "Starting drawing stream frame."); Bitmap frameBitmap = null; BitmapFactory.Options factoryOptions = new BitmapFactory.Options(); factoryOptions.inSampleSize = 1; if (mInMutableAvailable) { initInBitmap(factoryOptions); } while (mWhileFetching) { try { byte[] jpegData = mJpegQueue.take(); frameBitmap = BitmapFactory.decodeByteArray(// jpegData, 0, jpegData.length, factoryOptions); if (frameBitmap != null) { Bitmap rotatedBitmap = Bitmap.createBitmap(frameBitmap, 0, 0, frameBitmap.getWidth(), frameBitmap.getHeight(), rotateMatrix, true); frameBitmap.recycle(); frameBitmap = rotatedBitmap; } } catch (IllegalArgumentException e) { if (mInMutableAvailable) { clearInBitmap(factoryOptions); } continue; } catch (InterruptedException e) { Log.i(TAG, "Drawer thread is Interrupted."); break; } if (mInMutableAvailable) { setInBitmap(factoryOptions, frameBitmap); } drawFrame(frameBitmap); } if (frameBitmap != null) { frameBitmap.recycle(); } mWhileFetching = false; } }; mDrawerThread.start(); return true; } /** * Request to stop retrieving and drawing liveview data. */ public void stop() { mWhileFetching = false; } /** * Check to see whether start() is already called. * * @return true if start() is already called, false otherwise. */ public boolean isStarted() { return mWhileFetching; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void initInBitmap(BitmapFactory.Options options) { options.inBitmap = null; options.inMutable = true; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void clearInBitmap(BitmapFactory.Options options) { if (options.inBitmap != null) { options.inBitmap.recycle(); options.inBitmap = null; } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void setInBitmap(BitmapFactory.Options options, Bitmap bitmap) { options.inBitmap = bitmap; } /** * Draw frame bitmap onto a canvas. * * @param frame */ private void drawFrame(Bitmap frame) { if (frame.getWidth() != mPreviousWidth || frame.getHeight() != mPreviousHeight) { onDetectedFrameSizeChanged(frame.getWidth(), frame.getHeight()); return; } Canvas canvas = getHolder().lockCanvas(); if (canvas == null) { return; } int w = frame.getWidth(); int h = frame.getHeight(); Rect src = new Rect(0, 0, w, h); float by = Math.min((float) getWidth() / w, (float) getHeight() / h); int offsetX = (getWidth() - (int) (w * by)) / 2; int offsetY = (getHeight() - (int) (h * by)) / 2; Rect dst = new Rect(offsetX, offsetY, getWidth() - offsetX, getHeight() - offsetY); canvas.drawBitmap(frame, src, dst, mFramePaint); getHolder().unlockCanvasAndPost(canvas); mSurfaceWidth = dst.width(); mSurfaceHeight = dst.height(); } public int getSurfaceWidth() { return mSurfaceWidth; } public int getSurfaceHeight() { return mSurfaceHeight; } /** * Called when the width or height of liveview frame image is changed. * * @param width * @param height */ private void onDetectedFrameSizeChanged(int width, int height) { Log.d(TAG, "Change of aspect ratio detected"); mPreviousWidth = width; mPreviousHeight = height; drawBlackFrame(); drawBlackFrame(); drawBlackFrame(); // delete triple buffers } /** * Draw black screen. */ private void drawBlackFrame() { Canvas canvas = getHolder().lockCanvas(); if (canvas == null) { return; } Paint paint = new Paint(); paint.setColor(Color.BLACK); paint.setStyle(Paint.Style.FILL); canvas.drawRect(new Rect(0, 0, getWidth(), getHeight()), paint); getHolder().unlockCanvasAndPost(canvas); } public interface StreamErrorListener { enum StreamErrorReason { IO_EXCEPTION, OPEN_ERROR, } void onError(StreamErrorReason reason); } public interface StreamFrameListener { void onFrameAvailable(byte[] jpegData); } }