/* Copyright (C) 2010-2011, Mamadou Diop. * Copyright (C) 2011, Doubango Telecom. * Copyright (C) 2011, Philippe Verney <verney(dot)philippe(AT)gmail(dot)com> * * Contact: Mamadou Diop <diopmamadou(at)doubango(dot)org> * * This file is part of Open Source Doubango Framework. * * This is free software: you can redistribute it and/or modify it under the terms of * the GNU General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.doubango.ngn.media; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import org.doubango.ngn.BuildConfig; import org.doubango.ngn.events.NgnMediaPluginEventArgs; import org.doubango.ngn.events.NgnMediaPluginEventTypes; import org.doubango.tinyWRAP.ProxyVideoConsumer; import org.doubango.tinyWRAP.ProxyVideoConsumerCallback; import org.doubango.tinyWRAP.ProxyVideoFrame; import org.doubango.utils.Utils; import java.math.BigInteger; import java.nio.ByteBuffer; /** * Video consumer using SurfaceView */ public class NgnProxyVideoConsumerSV extends NgnProxyVideoConsumer{ private static final String TAG = Utils.getTAG(NgnProxyVideoConsumerSV.class.getCanonicalName()); private static final int DEFAULT_VIDEO_WIDTH = 176; private static final int DEFAULT_VIDEO_HEIGHT = 144; private static final int DEFAULT_VIDEO_FPS = 15; private final MyProxyVideoConsumerCallback mCallback; private final ProxyVideoConsumer mConsumer; private Context mContext; private MyProxyVideoConsumerPreview mPreview; private ByteBuffer mVideoFrame; private Bitmap mRGB565Bitmap; private Bitmap mRGBCroppedBitmap; private Looper mLooper; private Handler mHandler; protected NgnProxyVideoConsumerSV(BigInteger id, ProxyVideoConsumer consumer){ super(id, consumer); mConsumer = consumer; mCallback = new MyProxyVideoConsumerCallback(this); mConsumer.setCallback(mCallback); // Initialize video stream parameters with default values mWidth = NgnProxyVideoConsumerSV.DEFAULT_VIDEO_WIDTH; mHeight = NgnProxyVideoConsumerSV.DEFAULT_VIDEO_HEIGHT; mFps = NgnProxyVideoConsumerSV.DEFAULT_VIDEO_FPS; } @Override public void invalidate(){ super.invalidate(); if(mRGBCroppedBitmap != null){ mRGBCroppedBitmap.recycle(); } if(mRGB565Bitmap != null){ mRGB565Bitmap.recycle(); } mRGBCroppedBitmap = null; mRGB565Bitmap = null; mVideoFrame = null; System.gc(); } @Override public void setContext(Context context){ mContext = context; } /** * * @param context * @return This "View" is used to present the video on screen */ @Override public final View startPreview(Context context){ Log.d(TAG,"start Preview"); mContext = context == null ? mContext : context; if(mPreview == null && mContext != null){ if(mLooper != null){ mLooper.quit(); mLooper = null; } final Thread previewThread = new Thread() { @Override public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY); Looper.prepare(); mLooper = Looper.myLooper(); synchronized (this) { mPreview = new MyProxyVideoConsumerPreview(mContext, mWidth, mHeight, mFps); notify(); } mHandler = new Handler() { public void handleMessage(Message message) { final int nCopiedSize = message.arg1; final int nAvailableSize = message.arg2; long frameWidth = mConsumer.getDisplayWidth(); long frameHeight = mConsumer.getDisplayHeight(); if(mVideoFrame == null || mWidth != frameWidth || frameHeight != mHeight || mVideoFrame.capacity() != nAvailableSize){ if(frameWidth <=0 || frameHeight <= 0){ Log.e(TAG,"nCopiedSize="+nCopiedSize+" and newWidth="+frameWidth+" and newHeight="+frameHeight); return; } Log.d(TAG,"resizing the buffer nAvailableSize="+nAvailableSize+" and newWidth="+frameWidth+" and newHeight="+frameHeight); if(mRGB565Bitmap != null){ mRGB565Bitmap.recycle(); } if(mRGBCroppedBitmap != null){ mRGBCroppedBitmap.recycle(); mRGBCroppedBitmap = null; // do not create the cropped bitmap, wait for drawFrame() } mRGB565Bitmap = Bitmap.createBitmap((int)frameWidth, (int)frameHeight, Bitmap.Config.RGB_565); mVideoFrame = ByteBuffer.allocateDirect((int)nAvailableSize); mConsumer.setConsumeBuffer(mVideoFrame, mVideoFrame.capacity()); mWidth = (int)frameWidth; mHeight = (int)frameHeight; mRenderedAtLeastOneFrame = true; // after "width=x, height=y" and before "broadcastEvent" NgnMediaPluginEventArgs.broadcastEvent(new NgnMediaPluginEventArgs(mConsumer.getId(), NgnMediaType.Video, NgnMediaPluginEventTypes.VIDEO_INPUT_SIZE_CHANGED)); return; // Draw the picture next time } mRenderedAtLeastOneFrame = true; // after "width=x, height=y" drawFrame(); } }; Looper.loop(); mHandler = null; Log.d(TAG, "VideoConsumer::Looper::exit"); } }; previewThread.setPriority(Thread.MAX_PRIORITY); synchronized(previewThread) { previewThread.start(); try { previewThread.wait(); } catch (InterruptedException e) { e.printStackTrace(); Log.e(TAG,"Error:"+e.toString()); return null; } } } else{ Log.e(TAG, "Invalid state"); } Log.d(TAG,"Preview:"+mPreview); return mPreview; } @Override public final View startPreview(){ return startPreview(null); } private int prepareCallback(int width, int height, int fps){ Log.d(TAG, "prepareCallback("+width+","+height+","+fps+")"); // Update video stream parameters with real values (negotiated) mWidth = width; mHeight = height; mFps = fps; mRGB565Bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.RGB_565); mVideoFrame = ByteBuffer.allocateDirect((mWidth * mHeight) << 1); mConsumer.setConsumeBuffer(mVideoFrame, mVideoFrame.capacity()); super.mPrepared = true; return 0; } private int startCallback(){ Log.d(NgnProxyVideoConsumerSV.TAG, "startCallback"); super.mStarted = true; return 0; } private int bufferCopiedCallback(long nCopiedSize, long nAvailableSize) { if(!super.mValid || mRGB565Bitmap == null){ Log.e(TAG, "Invalid state"); return -1; } if(mPreview == null || mPreview.mHolder == null){ // Not on the top return 0; } if(mHandler != null){ final Message message = mHandler.obtainMessage(); message.arg1 = (int)nCopiedSize; message.arg2 = (int)nAvailableSize; mHandler.sendMessage(message); } return 0; } // @deprecated: never called @Deprecated private int consumeCallback(ProxyVideoFrame _frame){ if(!super.mValid || mRGB565Bitmap == null){ Log.e(TAG, "Invalid state"); return -1; } if(mPreview == null || mPreview.mHolder == null){ // Not on the top return 0; } // Get video frame content from native code _frame.getContent(mVideoFrame, mVideoFrame.capacity()); mRGB565Bitmap.copyPixelsFromBuffer(mVideoFrame); drawFrame(); return 0; } private int pauseCallback(){ Log.d(TAG, "pauseCallback"); super.mPaused = true; return 0; } private synchronized int stopCallback(){ if(BuildConfig.DEBUG)Log.d(TAG, "stopCallback: " + "NgnProxyVideoConsumerSV"); Log.d(TAG, "stopCallback"); super.mStarted = false; if(mLooper != null){ mLooper.quit(); mLooper = null; } mPreview = null; return 0; } private synchronized void drawFrame(){ if (mPreview != null && mPreview.mHolder != null){ final Canvas canvas = mPreview.mHolder.lockCanvas(); if(canvas == null){ return; } /** * Copy byteBuffer from MCVideo */ mRGB565Bitmap.copyPixelsFromBuffer(mVideoFrame); if(super.mFullScreenRequired){ // destroy cropped bitmap if surface has changed if(mPreview.isSurfaceChanged()){ mPreview.setSurfaceChanged(false); if(mRGBCroppedBitmap != null){ mRGBCroppedBitmap.recycle(); mRGBCroppedBitmap = null; } } // create new cropped image if doesn't exist yet if(mRGBCroppedBitmap == null){ float ratio = Math.max( (float)mPreview.mSurfFrame.width() / (float)mRGB565Bitmap.getWidth(), (float)mPreview.mSurfFrame.height() / (float)mRGB565Bitmap.getHeight()); mRGBCroppedBitmap = Bitmap.createBitmap( (int)(mPreview.mSurfFrame.width()/ratio), (int)(mPreview.mSurfFrame.height()/ratio), Bitmap.Config.RGB_565); } // crop the image Canvas _canvas = new Canvas(mRGBCroppedBitmap); Bitmap copyOfOriginal = Bitmap.createBitmap(mRGB565Bitmap, Math.abs((mRGBCroppedBitmap.getWidth() - mRGB565Bitmap.getWidth())/2), Math.abs((mRGBCroppedBitmap.getHeight() - mRGB565Bitmap.getHeight())/2), mRGBCroppedBitmap.getWidth(), mRGBCroppedBitmap.getHeight(), null, true);//FIXME: translate the original image instead of creating new one _canvas.drawBitmap(copyOfOriginal, 0.f, 0.f, null); copyOfOriginal.recycle(); // draw the cropped image canvas.drawBitmap(mRGBCroppedBitmap, null, mPreview.mSurfFrame, null); } else{ // display while keeping the ratio // canvas.drawBitmap(mRGB565Bitmap, null, mPreview.mSurfDisplay, null); // Or display "as is" canvas.drawBitmap(mRGB565Bitmap, 0, 0, null); } if(mPreview != null){// it's possible (not synchronized) mPreview.mHolder.unlockCanvasAndPost(canvas); } } } static class MyProxyVideoConsumerCallback extends ProxyVideoConsumerCallback { final NgnProxyVideoConsumerSV myConsumer; public MyProxyVideoConsumerCallback(NgnProxyVideoConsumerSV consumer){ super(); myConsumer = consumer; } @Override public int prepare(int width, int height, int fps){ int ret = myConsumer.prepareCallback(width, height, fps); NgnMediaPluginEventArgs.broadcastEvent(new NgnMediaPluginEventArgs(myConsumer.mId, NgnMediaType.Video, ret == 0 ? NgnMediaPluginEventTypes.PREPARED_OK : NgnMediaPluginEventTypes.PREPARED_NOK)); return ret; } @Override public int start(){ int ret = myConsumer.startCallback(); NgnMediaPluginEventArgs.broadcastEvent(new NgnMediaPluginEventArgs(myConsumer.mId, NgnMediaType.Video, ret == 0 ? NgnMediaPluginEventTypes.STARTED_OK : NgnMediaPluginEventTypes.STARTED_NOK)); return ret; } @Override @Deprecated public int consume(ProxyVideoFrame frame){ return myConsumer.consumeCallback(frame); } @Override public int bufferCopied(long nCopiedSize, long nAvailableSize) { /* the Frame has been copied */ return myConsumer.bufferCopiedCallback(nCopiedSize, nAvailableSize); } @Override public int pause(){ int ret = myConsumer.pauseCallback(); NgnMediaPluginEventArgs.broadcastEvent(new NgnMediaPluginEventArgs(myConsumer.mId, NgnMediaType.Video, ret == 0 ? NgnMediaPluginEventTypes.PAUSED_OK : NgnMediaPluginEventTypes.PAUSED_NOK)); return ret; } @Override public int stop(){ int ret = myConsumer.stopCallback(); NgnMediaPluginEventArgs.broadcastEvent(new NgnMediaPluginEventArgs(myConsumer.mId, NgnMediaType.Video, ret == 0 ? NgnMediaPluginEventTypes.STOPPED_OK : NgnMediaPluginEventTypes.STOPPED_NOK)); return ret; } } /** * MyProxyVideoConsumerPreview * This "View" is used to present the video on screen */ static class MyProxyVideoConsumerPreview extends SurfaceView implements SurfaceHolder.Callback { private final SurfaceHolder mHolder; private Rect mSurfFrame; @SuppressWarnings("unused") private Rect mSurfDisplay; private final float mRatio; private boolean mSurfaceChanged; MyProxyVideoConsumerPreview(Context context, int width, int height, int fps) { super(context); mHolder = getHolder(); mHolder.addCallback(this); // You don't need to enable GPU or Hardware acceleration by yourself mHolder.setType(SurfaceHolder.SURFACE_TYPE_HARDWARE); mRatio = (float)width/(float)height; if(mHolder != null){ mSurfFrame = mHolder.getSurfaceFrame(); } else{ mSurfFrame = null; } mSurfDisplay = mSurfFrame; } public synchronized void setSurfaceChanged(boolean surfaceChanged){ mSurfaceChanged = surfaceChanged; } public synchronized boolean isSurfaceChanged(){ return mSurfaceChanged; } public void surfaceCreated(SurfaceHolder holder) { } public void surfaceDestroyed(SurfaceHolder holder) { } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { if(holder != null){ mSurfFrame = holder.getSurfaceFrame(); // (w/h)=ratio => // 1) h=w/ratio // and // 2) w=h*ratio int newW = (int)(w/mRatio) > h ? (int)(h * mRatio) : w; int newH = (int)(newW/mRatio) > h ? h : (int)(newW/mRatio); mSurfDisplay = new Rect(0, 0, newW, newH); setSurfaceChanged(true); } } } }