package com.serenegiant.timelapserecordingsample; /* * TimeLapseRecordingSample * Sample project to capture audio and video periodically from internal mic/camera * and save as time lapsed MPEG4 file. * * Copyright (c) 2015 saki [email protected] * * File name: CameraGLView.java * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * All files in the folder are under this Apache License, Version 2.0. */ import android.content.Context; import android.graphics.SurfaceTexture; import android.hardware.Camera; import android.opengl.EGL14; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.opengl.Matrix; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.Display; import android.view.Surface; import android.view.SurfaceHolder; import android.view.WindowManager; import com.serenegiant.glutils.GLDrawer2D; import com.serenegiant.media.TLMediaVideoEncoder; import java.io.IOException; import java.util.List; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** * Sub class of GLSurfaceView to display camera preview and write video frame to capturing surface */ public final class CameraGLView extends GLSurfaceView { private static final boolean DEBUG = false; // TODO set false on releasing private static final String TAG = "CameraGLView"; private static final int CAMERA_ID = 0; private static final int SCALE_STRETCH_FIT = 0; private static final int SCALE_KEEP_ASPECT_VIEWPORT = 1; private static final int SCALE_KEEP_ASPECT = 2; private static final int SCALE_CROP_CENTER = 3; private final CameraSurfaceRenderer mRenderer; private boolean mHasSurface; private final CameraHandler mCameraHandler; private int mVideoWidth, mVideoHeight; private int mRotation; private int mScaleMode = SCALE_CROP_CENTER; public CameraGLView(Context context) { this(context, null, 0); } public CameraGLView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CameraGLView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); if (DEBUG) Log.v(TAG, "CameraGLView:"); mRenderer = new CameraSurfaceRenderer(); setEGLContextClientVersion(2); // GLES 2.0, API >= 8 setRenderer(mRenderer); final CameraThread thread = new CameraThread(); thread.start(); mCameraHandler = thread.getHandler(); } @Override public void onResume() { if (DEBUG) Log.v(TAG, "onResume:"); super.onResume(); if (mHasSurface) { if (DEBUG) Log.v(TAG, "surface already exist"); mCameraHandler.startPreview(getWidth(), getHeight()); } } @Override public void onPause() { if (DEBUG) Log.v(TAG, "onPause:"); // just request stop previewing mCameraHandler.stopPreview(); super.onPause(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { if (DEBUG) Log.v(TAG, "surfaceDestroyed:"); // wait for finish previewing here // otherwise camera try to display on un-exist Surface and some error will occur mCameraHandler.release(true); mHasSurface = false; mRenderer.onSurfaceDestroyed(); super.surfaceDestroyed(holder); } public void setScaleMode(final int mode) { if (mScaleMode != mode) { mScaleMode = mode; queueEvent(new Runnable() { @Override public void run() { mRenderer.updateViewport(); } }); } } public int getScaleMode() { return mScaleMode; } @SuppressWarnings("SuspiciousNameCombination") public void setVideoSize(final int width, final int height) { if ((mRotation % 180) == 0) { mVideoWidth = width; mVideoHeight = height; } else { mVideoWidth = height; mVideoHeight = width; } queueEvent(new Runnable() { @Override public void run() { mRenderer.updateViewport(); } }); } public int getVideoWidth() { return mVideoWidth; } public int getVideoHeight() { return mVideoHeight; } public SurfaceTexture getSurfaceTexture() { if (DEBUG) Log.v(TAG, "getSurfaceTexture:"); return mRenderer != null ? mRenderer.mSTexture : null; } public void setVideoEncoder(final TLMediaVideoEncoder encoder) { if (DEBUG) Log.v(TAG, "setVideoEncoder:tex_id=" + mRenderer.hTex); queueEvent(new Runnable() { @Override public void run() { synchronized (mRenderer) { try { if (encoder != null) { encoder.setEglContext(EGL14.eglGetCurrentContext(), mRenderer.hTex); } mRenderer.mVideoEncoder = encoder; } catch (RuntimeException e) { mRenderer.mVideoEncoder = null; } } } }); } //******************************************************************************** //******************************************************************************** /** * GLSurfaceViewのRenderer */ private final class CameraSurfaceRenderer implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener { // API >= 11 private SurfaceTexture mSTexture; // API >= 11 private int hTex; private GLDrawer2D mDrawer; private final float[] mStMatrix = new float[16]; private final float[] mMvpMatrix = new float[16]; private TLMediaVideoEncoder mVideoEncoder; public CameraSurfaceRenderer() { if (DEBUG) Log.v(TAG, "CameraSurfaceRenderer:"); Matrix.setIdentityM(mMvpMatrix, 0); } @Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { if (DEBUG) Log.v(TAG, "onSurfaceCreated:"); // This renderer required OES_EGL_image_external extension final String extensions = GLES20.glGetString(GLES20.GL_EXTENSIONS); // API >= 8 // if (DEBUG) Log.i(TAG, "onSurfaceCreated:Gl extensions: " + extensions); if (!extensions.contains("OES_EGL_image_external")) throw new RuntimeException("This system does not support OES_EGL_image_external."); // create texture ID hTex = GLDrawer2D.initTex(); // create SurfaceTexture using the texture ID. mSTexture = new SurfaceTexture(hTex); mSTexture.setOnFrameAvailableListener(this); // XXX clear screen with yellow color // so that let easy to see the actual view rectangle and camera images for testing. GLES20.glClearColor(1.0f, 1.0f, 0.0f, 1.0f); mHasSurface = true; // create object for preview display mDrawer = new GLDrawer2D(); mDrawer.setMatrix(mMvpMatrix, 0); } @Override public void onSurfaceChanged(GL10 unused, int width, int height) { if (DEBUG) Log.v(TAG, "onSurfaceChanged:"); // if at least with or height is zero, initialization of this view is still progress. if ((width == 0) || (height == 0)) return; updateViewport(); mCameraHandler.startPreview(width, height); } public void onSurfaceDestroyed() { if (DEBUG) Log.v(TAG, "onSurfaceDestroyed:"); mDrawer = null; if (mSTexture != null) { mSTexture.release(); mSTexture = null; } } private final void updateViewport() { final int view_width = getWidth(); final int view_height = getHeight(); if (view_width == 0 || view_height == 0) return; GLES20.glViewport(0, 0, view_width, view_height); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); final float video_width = mVideoWidth; final float video_height = mVideoHeight; if (video_width == 0 || video_height == 0) return; Matrix.setIdentityM(mMvpMatrix, 0); final float view_aspect = view_width / (float)view_height; switch (mScaleMode) { case SCALE_STRETCH_FIT: break; case SCALE_KEEP_ASPECT_VIEWPORT: { final float req = video_width / video_height; int x, y; int width, height; if (view_aspect > req) { // if view is wider than camera image, calc width of drawing area based on view height y = 0; height = view_height; width = (int) (req * view_height); x = (view_width - width) / 2; } else { // if view is higher than camera image, calc height of drawing area based on view width x = 0; width = view_width; height = (int) (view_width / req); y = (view_height - height) / 2; } // set viewport to draw keeping aspect ration of camera image GLES20.glViewport(x, y, width, height); break; } case SCALE_KEEP_ASPECT: case SCALE_CROP_CENTER: { final float scale_x = view_width / video_width; final float scale_y = view_height / video_height; final float scale = (mScaleMode == SCALE_CROP_CENTER ? Math.max(scale_x, scale_y) : Math.min(scale_x, scale_y)); final float width = scale * video_width; final float height = scale * video_height; Matrix.scaleM(mMvpMatrix, 0, width / view_width, height / view_height, 1.0f); break; } } if (mDrawer != null) mDrawer.setMatrix(mMvpMatrix, 0); } private volatile boolean requestUpdateTex = false; private boolean flip = true; /** * drawing to GLSurface * we set renderMode to GLSurfaceView.RENDERMODE_WHEN_DIRTY, * this method is only called when #requestRender is called(= when texture is required to update) * if you don't set RENDERMODE_WHEN_DIRTY, this method is called at maximum 60fps */ @Override public void onDrawFrame(GL10 unused) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); if (requestUpdateTex) { requestUpdateTex = false; // update texture(came from camera) mSTexture.updateTexImage(); // get texture matrix mSTexture.getTransformMatrix(mStMatrix); } // draw to preview screen mDrawer.draw(hTex, mStMatrix); flip = !flip; if (flip) { // ~30fps synchronized (this) { if (mVideoEncoder != null) { // notify to capturing thread that the camera frame is available. mVideoEncoder.frameAvailableSoon(mStMatrix); } } } } @Override public void onFrameAvailable(SurfaceTexture st) { requestUpdateTex = true; } } /** * Handler class for asynchronous camera operation */ private static final class CameraHandler extends Handler { private static final int MSG_PREVIEW_START = 1; private static final int MSG_PREVIEW_STOP = 2; private static final int MSG_RELEASE = 9; private CameraThread mThread; public CameraHandler(CameraThread thread) { mThread = thread; } public void startPreview(int width, int height) { sendMessage(obtainMessage(MSG_PREVIEW_START, width, height)); } /** * request to stop camera preview */ public void stopPreview() { synchronized (this) { if (mThread != null && mThread.mIsRunning) { sendEmptyMessage(MSG_PREVIEW_STOP); } } } /** * request to release camera thread and handler * @param needWait need to wait */ public void release(boolean needWait) { synchronized (this) { if (mThread != null && mThread.mIsRunning) { sendEmptyMessage(MSG_RELEASE); if (needWait) { try { if (DEBUG) Log.d(TAG, "wait for terminating of camera thread"); wait(); } catch (InterruptedException e) { } } } } } /** * message handler for camera thread */ @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_PREVIEW_START: mThread.startPreview(msg.arg1, msg.arg2); break; case MSG_PREVIEW_STOP: mThread.stopPreview(); break; case MSG_RELEASE: mThread.stopPreview(); Looper.myLooper().quit(); synchronized (this) { notifyAll(); mThread = null; } break; default: throw new RuntimeException("unknown message:what=" + msg.what); } } } /** * Thread for asynchronous operation of camera preview */ @SuppressWarnings("deprecation") private final class CameraThread extends Thread { private final Object mReadyFence = new Object(); private CameraHandler mHandler; private volatile boolean mIsRunning = false; private Camera mCamera; private boolean mIsFrontFace; public CameraThread() { super("Camera thread"); } public CameraHandler getHandler() { synchronized (mReadyFence) { try { mReadyFence.wait(); } catch (InterruptedException e) { } } return mHandler; } /** * message loop * prepare Looper and create Handler for this thread */ @Override public void run() { if (DEBUG) Log.d(TAG, "Camera thread start"); Looper.prepare(); synchronized (mReadyFence) { mHandler = new CameraHandler(this); mIsRunning = true; mReadyFence.notify(); } Looper.loop(); if (DEBUG) Log.d(TAG, "Camera thread finish"); synchronized (mReadyFence) { mHandler = null; mIsRunning = false; } } /** * start camera preview * @param width * @param height */ private final void startPreview(int width, int height) { if (DEBUG) Log.v(TAG, "startPreview:"); if (mCamera == null) { // This is a sample project so just use 0 as camera ID. // it is better to selecting camera is available try { mCamera = Camera.open(CAMERA_ID); final Camera.Parameters params = mCamera.getParameters(); final List<String> focusModes = params.getSupportedFocusModes(); if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); } else if(focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); } else { if (DEBUG) Log.i(TAG, "Camera does not support autofocus"); } // let's try fastest frame rate. You will get near 60fps, but your device become hot. final List<int[]> supportedFpsRange = params.getSupportedPreviewFpsRange(); // final int n = supportedFpsRange != null ? supportedFpsRange.size() : 0; // int[] range; // for (int i = 0; i < n; i++) { // range = supportedFpsRange.get(i); // Log.i(TAG, String.format("supportedFpsRange(%d)=(%d,%d)", i, range[0], range[1])); // } final int[] max_fps = supportedFpsRange.get(supportedFpsRange.size() - 1); if (DEBUG) Log.i(TAG, String.format("fps:%d-%d", max_fps[0], max_fps[1])); params.setPreviewFpsRange(max_fps[0], max_fps[1]); params.setRecordingHint(true); // request preview size // this is a sample project and just use fixed value // if you want to use other size, you also need to change the recording size. params.setPreviewSize(1280, 720); // final Size sz = params.getPreferredPreviewSizeForVideo(); // if (sz != null) // params.setPreviewSize(sz.width, sz.height); // rotate camera preview according to the device orientation setRotation(params); mCamera.setParameters(params); // get the actual preview size final Camera.Size previewSize = mCamera.getParameters().getPreviewSize(); Log.i(TAG, String.format("previewSize(%d, %d)", previewSize.width, previewSize.height)); // adjust view size with keeping the aspect ration of camera preview. // here is not a UI thread and we should request parent view to execute. CameraGLView.this.post(new Runnable() { @Override public void run() { setVideoSize(previewSize.width, previewSize.height); } }); final SurfaceTexture st = getSurfaceTexture(); st.setDefaultBufferSize(previewSize.width, previewSize.height); mCamera.setPreviewTexture(st); } catch (IOException e) { Log.e(TAG, "startPreview:", e); if (mCamera != null) { mCamera.release(); mCamera = null; } } catch (RuntimeException e) { Log.e(TAG, "startPreview:", e); if (mCamera != null) { mCamera.release(); mCamera = null; } } if (mCamera != null) { // start camera preview display mCamera.startPreview(); } } // if (mCamera == null) } /** * stop camera preview */ private void stopPreview() { if (DEBUG) Log.v(TAG, "stopPreview:"); if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); mCamera = null; } } /** * rotate preview screen according to the device orientation * @param params */ private final void setRotation(Camera.Parameters params) { if (DEBUG) Log.v(TAG, "setRotation:"); final Display display = ((WindowManager)getContext() .getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); int rotation = display.getRotation(); int degrees = 0; switch (rotation) { case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; } // get whether the camera is front camera or back camera final Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); android.hardware.Camera.getCameraInfo(CAMERA_ID, info); mIsFrontFace = (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT); if (mIsFrontFace) { // front camera degrees = (info.orientation + degrees) % 360; degrees = (360 - degrees) % 360; // reverse } else { // back camera degrees = (info.orientation - degrees + 360) % 360; } // apply rotation setting mCamera.setDisplayOrientation(degrees); mRotation = degrees; // XXX This method fails to call and camera stops working on some devices. // params.setRotation(degrees); } } }