/*
* Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com
*
* This file is part of libstreaming (https://github.com/fyhertz/libstreaming)
*
* Spydroid 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 source code 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 source code; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package net.majorkernelpanic.streaming.gl;
import java.util.concurrent.Semaphore;
import net.majorkernelpanic.streaming.MediaStream;
import net.majorkernelpanic.streaming.video.VideoStream;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.graphics.SurfaceTexture.OnFrameAvailableListener;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
/**
* An enhanced SurfaceView in which the camera preview will be rendered.
* This class was needed for two reasons. <br />
*
* First, it allows to use to feed MediaCodec with the camera preview
* using the surface-to-buffer method while rendering it in a surface
* visible to the user. To force the surface-to-buffer method in
* libstreaming, call {@link MediaStream#setStreamingMethod(byte)}
* with {@link MediaStream#MODE_MEDIACODEC_API_2}. <br />
*
* Second, it allows to force the aspect ratio of the SurfaceView
* to match the aspect ratio of the camera preview, so that the
* preview do not appear distorted to the user of your app. To do
* that, call {@link SurfaceView#setAspectRatioMode(int)} with
* {@link SurfaceView#ASPECT_RATIO_PREVIEW} after creating your
* {@link SurfaceView}. <br />
*
*/
public class SurfaceView extends android.view.SurfaceView implements Runnable, OnFrameAvailableListener, SurfaceHolder.Callback {
public final static String TAG = "SurfaceView";
/**
* The aspect ratio of the surface view will be equal
* to the aspect ration of the camera preview.
**/
public static final int ASPECT_RATIO_PREVIEW = 0x01;
/** The surface view will fill completely fill its parent. */
public static final int ASPECT_RATIO_STRETCH = 0x00;
private Thread mThread = null;
private Handler mHandler = null;
private boolean mFrameAvailable = false;
private boolean mRunning = true;
private int mAspectRatioMode = ASPECT_RATIO_STRETCH;
// The surface in which the preview is rendered
private SurfaceManager mViewSurfaceManager = null;
// The input surface of the MediaCodec
private SurfaceManager mCodecSurfaceManager = null;
// Handles the rendering of the SurfaceTexture we got
// from the camera, onto a Surface
private TextureManager mTextureManager = null;
private final Semaphore mLock = new Semaphore(0);
private final Object mSyncObject = new Object();
// Allows to force the aspect ratio of the preview
private ViewAspectRatioMeasurer mVARM = new ViewAspectRatioMeasurer();
public SurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
mHandler = new Handler();
getHolder().addCallback(this);
}
public void setAspectRatioMode(int mode) {
mAspectRatioMode = mode;
}
public SurfaceTexture getSurfaceTexture() {
return mTextureManager.getSurfaceTexture();
}
public void addMediaCodecSurface(Surface surface) {
synchronized (mSyncObject) {
mCodecSurfaceManager = new SurfaceManager(surface,mViewSurfaceManager);
}
}
public void removeMediaCodecSurface() {
synchronized (mSyncObject) {
if (mCodecSurfaceManager != null) {
mCodecSurfaceManager.release();
mCodecSurfaceManager = null;
}
}
}
public void startGLThread() {
Log.d(TAG,"Thread started.");
if (mTextureManager == null) {
mTextureManager = new TextureManager();
}
if (mTextureManager.getSurfaceTexture() == null) {
mThread = new Thread(SurfaceView.this);
mRunning = true;
mThread.start();
mLock.acquireUninterruptibly();
}
}
@Override
public void run() {
mViewSurfaceManager = new SurfaceManager(getHolder().getSurface());
mViewSurfaceManager.makeCurrent();
mTextureManager.createTexture().setOnFrameAvailableListener(this);
mLock.release();
try {
long ts = 0, oldts = 0;
while (mRunning) {
synchronized (mSyncObject) {
mSyncObject.wait(2500);
if (mFrameAvailable) {
mFrameAvailable = false;
mViewSurfaceManager.makeCurrent();
mTextureManager.updateFrame();
mTextureManager.drawFrame();
mViewSurfaceManager.swapBuffer();
if (mCodecSurfaceManager != null) {
mCodecSurfaceManager.makeCurrent();
mTextureManager.drawFrame();
oldts = ts;
ts = mTextureManager.getSurfaceTexture().getTimestamp();
//Log.d(TAG,"FPS: "+(1000000000/(ts-oldts)));
mCodecSurfaceManager.setPresentationTime(ts);
mCodecSurfaceManager.swapBuffer();
}
} else {
Log.e(TAG,"No frame received !");
}
}
}
} catch (InterruptedException ignore) {
} finally {
mViewSurfaceManager.release();
mTextureManager.release();
}
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
synchronized (mSyncObject) {
mFrameAvailable = true;
mSyncObject.notifyAll();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mThread != null) {
mThread.interrupt();
}
mRunning = false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mVARM.getAspectRatio() > 0 && mAspectRatioMode == ASPECT_RATIO_PREVIEW) {
mVARM.measure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mVARM.getMeasuredWidth(), mVARM.getMeasuredHeight());
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* Requests a certain aspect ratio for the preview. You don't have to call this yourself,
* the {@link VideoStream} will do it when it's needed.
*/
public void requestAspectRatio(double aspectRatio) {
if (mVARM.getAspectRatio() != aspectRatio) {
mVARM.setAspectRatio(aspectRatio);
mHandler.post(new Runnable() {
@Override
public void run() {
if (mAspectRatioMode == ASPECT_RATIO_PREVIEW) {
requestLayout();
}
}
});
}
}
/**
* This class is a helper to measure views that require a specific aspect ratio.
* @author Jesper Borgstrup
*/
public class ViewAspectRatioMeasurer {
private double aspectRatio;
public void setAspectRatio(double aspectRatio) {
this.aspectRatio = aspectRatio;
}
public double getAspectRatio() {
return this.aspectRatio;
}
/**
* Measure with the aspect ratio given at construction.<br />
* <br />
* After measuring, get the width and height with the {@link #getMeasuredWidth()}
* and {@link #getMeasuredHeight()} methods, respectively.
* @param widthMeasureSpec The width <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method
* @param heightMeasureSpec The height <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method
*/
public void measure(int widthMeasureSpec, int heightMeasureSpec) {
measure(widthMeasureSpec, heightMeasureSpec, this.aspectRatio);
}
/**
* Measure with a specific aspect ratio<br />
* <br />
* After measuring, get the width and height with the {@link #getMeasuredWidth()}
* and {@link #getMeasuredHeight()} methods, respectively.
* @param widthMeasureSpec The width <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method
* @param heightMeasureSpec The height <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method
* @param aspectRatio The aspect ratio to calculate measurements in respect to
*/
public void measure(int widthMeasureSpec, int heightMeasureSpec, double aspectRatio) {
int widthMode = MeasureSpec.getMode( widthMeasureSpec );
int widthSize = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( widthMeasureSpec );
int heightMode = MeasureSpec.getMode( heightMeasureSpec );
int heightSize = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( heightMeasureSpec );
if ( heightMode == MeasureSpec.EXACTLY && widthMode == MeasureSpec.EXACTLY ) {
/*
* Possibility 1: Both width and height fixed
*/
measuredWidth = widthSize;
measuredHeight = heightSize;
} else if ( heightMode == MeasureSpec.EXACTLY ) {
/*
* Possibility 2: Width dynamic, height fixed
*/
measuredWidth = (int) Math.min( widthSize, heightSize * aspectRatio );
measuredHeight = (int) (measuredWidth / aspectRatio);
} else if ( widthMode == MeasureSpec.EXACTLY ) {
/*
* Possibility 3: Width fixed, height dynamic
*/
measuredHeight = (int) Math.min( heightSize, widthSize / aspectRatio );
measuredWidth = (int) (measuredHeight * aspectRatio);
} else {
/*
* Possibility 4: Both width and height dynamic
*/
if ( widthSize > heightSize * aspectRatio ) {
measuredHeight = heightSize;
measuredWidth = (int)( measuredHeight * aspectRatio );
} else {
measuredWidth = widthSize;
measuredHeight = (int) (measuredWidth / aspectRatio);
}
}
}
private Integer measuredWidth = null;
/**
* Get the width measured in the latest call to <tt>measure()</tt>.
*/
public int getMeasuredWidth() {
if ( measuredWidth == null ) {
throw new IllegalStateException( "You need to run measure() before trying to get measured dimensions" );
}
return measuredWidth;
}
private Integer measuredHeight = null;
/**
* Get the height measured in the latest call to <tt>measure()</tt>.
*/
public int getMeasuredHeight() {
if ( measuredHeight == null ) {
throw new IllegalStateException( "You need to run measure() before trying to get measured dimensions" );
}
return measuredHeight;
}
}
}