diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GLCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GlCameraPreviewTest.java similarity index 69% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/GLCameraPreviewTest.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/GlCameraPreviewTest.java index 24590c96..b20f361f 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GLCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GlCameraPreviewTest.java @@ -10,20 +10,20 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -public class GLCameraPreviewTest extends CameraPreviewTest { +public class GlCameraPreviewTest extends CameraPreviewTest { @Override protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) { - return new GLCameraPreview(context, parent, callback); + return new GlCameraPreview(context, parent, callback); } @Override protected float getCropScaleY() { - return 1F / ((GLCameraPreview) preview).mScaleY; + return 1F / ((GlCameraPreview) preview).mScaleY; } @Override protected float getCropScaleX() { - return 1F / ((GLCameraPreview) preview).mScaleX; + return 1F / ((GlCameraPreview) preview).mScaleX; } } diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/AudioMediaEncoder.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/AudioMediaEncoder.java index 30acf2f7..25a066ac 100644 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/AudioMediaEncoder.java +++ b/cameraview/src/main/gles/com/otaliastudios/cameraview/AudioMediaEncoder.java @@ -17,26 +17,31 @@ class AudioMediaEncoder extends MediaEncoder { } + @EncoderThread @Override void prepare(MediaEncoderEngine.Controller controller) { super.prepare(controller); } + @EncoderThread @Override void start() { } + @EncoderThread @Override void notify(String event, Object data) { } + @EncoderThread @Override void stop() { } + @EncoderThread @Override void release() { diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoder.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoder.java index 17203ef8..2bf5534c 100644 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoder.java +++ b/cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoder.java @@ -21,17 +21,22 @@ abstract class MediaEncoder { private MediaEncoderEngine.Controller mController; private int mTrackIndex; + @EncoderThread void prepare(MediaEncoderEngine.Controller controller) { mController = controller; mBufferInfo = new MediaCodec.BufferInfo(); } + @EncoderThread abstract void start(); + @EncoderThread abstract void notify(String event, Object data); + @EncoderThread abstract void stop(); + @EncoderThread abstract void release(); /** diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoderEngine.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoderEngine.java index e51513e5..0da3e63d 100644 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoderEngine.java +++ b/cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoderEngine.java @@ -15,6 +15,8 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; +@interface EncoderThread {} + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) class MediaEncoderEngine { @@ -60,6 +62,7 @@ class MediaEncoderEngine { mMediaMuxerStartCount++; if (mMediaMuxerStartCount == mEncoders.size()) { mMediaMuxer.start(); + mMediaMuxerStarted = true; } return track; } @@ -110,9 +113,11 @@ class MediaEncoderEngine { encoder.release(); } if (mMediaMuxer != null) { - // TODO: stop() throws an exception if you haven't fed it any data. Keep track - // of frames submitted, and don't call stop() if we haven't written anything. - mMediaMuxer.stop(); + // stop() throws an exception if you haven't fed it any data. + // We can just swallow I think. + try { + mMediaMuxer.stop(); + } catch (Exception e) {}; mMediaMuxer.release(); mMediaMuxer = null; } diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoder.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoder.java index 91dc8d93..1c9251c6 100644 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoder.java +++ b/cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoder.java @@ -327,7 +327,7 @@ class OldMediaEncoder implements Runnable { long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL); float[] transform = (float[]) obj; - // We must scale this matrix like GLCameraPreview does, because it might have some cropping. + // We must scale this matrix like GlCameraPreview does, because it might have some cropping. // Scaling takes place with respect to the (0, 0, 0) point, so we must apply a Translation to compensate. float scaleX = encoder.mTransformationScaleX; @@ -337,7 +337,7 @@ class OldMediaEncoder implements Runnable { Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0); Matrix.scaleM(transform, 0, scaleX, scaleY, 1); - // We also must rotate this matrix. In GLCameraPreview it is not needed because it is a live + // We also must rotate this matrix. In GlCameraPreview it is not needed because it is a live // stream, but the output video, must be correctly rotated based on the device rotation at the moment. // Rotation also takes place with respect to the origin (the Z axis), so we must // translate to origin, rotate, then back to where we were. diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/TextureMediaEncoder.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/TextureMediaEncoder.java index f7f6449d..eb590bca 100644 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/TextureMediaEncoder.java +++ b/cameraview/src/main/gles/com/otaliastudios/cameraview/TextureMediaEncoder.java @@ -48,6 +48,7 @@ class TextureMediaEncoder extends VideoMediaEncoder super(config); } + @EncoderThread @Override void prepare(MediaEncoderEngine.Controller controller) { super.prepare(controller); @@ -58,6 +59,7 @@ class TextureMediaEncoder extends VideoMediaEncoder mViewport = new EglViewport(); } + @EncoderThread @Override void release() { super.release(); @@ -75,12 +77,14 @@ class TextureMediaEncoder extends VideoMediaEncoder } } + @EncoderThread @Override void start() { super.start(); // Nothing to do here. Waiting for the first frame. } + @EncoderThread @Override void notify(String event, Object data) { if (event.equals(FRAME_EVENT)) { @@ -99,7 +103,7 @@ class TextureMediaEncoder extends VideoMediaEncoder long timestamp = (((long) arg1) << 32) | (((long) arg2) & 0xffffffffL); float[] transform = frame.transform; - // We must scale this matrix like GLCameraPreview does, because it might have some cropping. + // We must scale this matrix like GlCameraPreview does, because it might have some cropping. // Scaling takes place with respect to the (0, 0, 0) point, so we must apply a Translation to compensate. float scaleX = mConfig.scaleX; @@ -109,7 +113,7 @@ class TextureMediaEncoder extends VideoMediaEncoder Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0); Matrix.scaleM(transform, 0, scaleX, scaleY, 1); - // We also must rotate this matrix. In GLCameraPreview it is not needed because it is a live + // We also must rotate this matrix. In GlCameraPreview it is not needed because it is a live // stream, but the output video, must be correctly rotated based on the device rotation at the moment. // Rotation also takes place with respect to the origin (the Z axis), so we must // translate to origin, rotate, then back to where we were. diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoMediaEncoder.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoMediaEncoder.java index 251a08d7..334a8c78 100644 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoMediaEncoder.java +++ b/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoMediaEncoder.java @@ -13,6 +13,11 @@ import android.view.Surface; import java.io.IOException; import java.nio.ByteBuffer; +/** + * This alone does nothing. + * Subclasses must make sure they write each frame onto the given Surface {@link #mSurface}. + * @param the config object. + */ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) abstract class VideoMediaEncoder extends MediaEncoder { @@ -42,6 +47,7 @@ abstract class VideoMediaEncoder extends Med mConfig = config; } + @EncoderThread @Override void prepare(MediaEncoderEngine.Controller controller) { super.prepare(controller); @@ -67,18 +73,21 @@ abstract class VideoMediaEncoder extends Med mMediaCodec.start(); } + @EncoderThread @Override void start() { // Nothing to do here. Waiting for the first frame. mFrameNum = 0; } + @EncoderThread @Override void stop() { mFrameNum = -1; drain(true); } + @EncoderThread @Override void release() { if (mMediaCodec != null) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index fbf8d368..7b8c7ddf 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -6,18 +6,14 @@ import android.graphics.ImageFormat; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.SurfaceTexture; -import android.graphics.YuvImage; import android.hardware.Camera; import android.location.Location; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; -import android.support.media.ExifInterface; import android.view.SurfaceHolder; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -645,8 +641,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera @SuppressLint("NewApi") @Override void takeVideoSnapshot(@NonNull final File file) { - if (!(mPreview instanceof GLCameraPreview)) { - throw new IllegalStateException("Video snapshots are only supported with GLCameraPreview."); + if (!(mPreview instanceof GlCameraPreview)) { + throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { throw new IllegalStateException("Video snapshots are only supported starting from API 18."); @@ -663,7 +659,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera videoResult.codec = mVideoCodec; videoResult.location = mLocation; - // What matters as size here is the preview size, which is passed to the GLCameraPreview + // What matters as size here is the preview size, which is passed to the GlCameraPreview // surface texture, which is then passed to the encoder. The view size has no influence. // CROPPING: We must make sure that the encoder applies our special transformation @@ -700,7 +696,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera videoResult.maxSize = mVideoMaxSize; videoResult.maxDuration = mVideoMaxDuration; - GLCameraPreview cameraPreview = (GLCameraPreview) mPreview; + GlCameraPreview cameraPreview = (GlCameraPreview) mPreview; mVideoRecorder = new SnapshotVideoRecorder(videoResult, Camera1.this, cameraPreview); mVideoRecorder.start(); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 62c52fe4..3294876d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -7,7 +7,6 @@ import android.app.Activity; import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleObserver; import android.arch.lifecycle.LifecycleOwner; -import android.arch.lifecycle.Lifecycling; import android.arch.lifecycle.OnLifecycleEvent; import android.content.Context; import android.content.ContextWrapper; @@ -240,7 +239,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } } case GL_SURFACE: default: { - return new GLCameraPreview(context, container, null); + return new GlCameraPreview(context, container, null); } } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java index 75bd8a74..a42b18fd 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java @@ -1,7 +1,6 @@ package com.otaliastudios.cameraview; import android.annotation.SuppressLint; -import android.graphics.Bitmap; import android.graphics.ImageFormat; import android.graphics.Rect; import android.graphics.SurfaceTexture; @@ -9,19 +8,9 @@ import android.graphics.YuvImage; import android.hardware.Camera; import android.opengl.EGL14; import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLSurface; -import android.opengl.GLES11Ext; -import android.opengl.GLES20; -import android.opengl.GLSurfaceView; import android.opengl.Matrix; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; import java.io.ByteArrayOutputStream; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; /** * A {@link PictureResult} that uses standard APIs. @@ -50,21 +39,22 @@ class SnapshotPictureRecorder extends PictureRecorder { @Override void take() { - if (mPreview instanceof GLCameraPreview) { - takeGl((GLCameraPreview) mPreview); + if (mPreview instanceof GlCameraPreview) { + takeGl((GlCameraPreview) mPreview); } else { takeLegacy(); } } @SuppressLint("NewApi") - private void takeGl(final GLCameraPreview preview) { - preview.addRendererFrameCallback(new GLCameraPreview.RendererFrameCallback() { + private void takeGl(final GlCameraPreview preview) { + preview.addRendererFrameCallback(new GlCameraPreview.RendererFrameCallback() { int mTextureId; SurfaceTexture mSurfaceTexture; float[] mTransform; + @RendererThread public void onRendererTextureCreated(int textureId) { mTextureId = textureId; mSurfaceTexture = new SurfaceTexture(mTextureId, true); @@ -75,6 +65,7 @@ class SnapshotPictureRecorder extends PictureRecorder { mTransform = new float[16]; } + @RendererThread @Override public void onRendererFrame(SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) { preview.removeRendererFrameCallback(this); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java index 8b1efa3d..ea7dc79c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java @@ -14,7 +14,7 @@ import android.support.annotation.RequiresApi; * TODO when cropping is huge, the first frame of the video result, noticeably, has no transformation applied. Don't know why. */ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) -class SnapshotVideoRecorder extends VideoRecorder implements GLCameraPreview.RendererFrameCallback { +class SnapshotVideoRecorder extends VideoRecorder implements GlCameraPreview.RendererFrameCallback { private static final String TAG = SnapshotVideoRecorder.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); @@ -25,13 +25,13 @@ class SnapshotVideoRecorder extends VideoRecorder implements GLCameraPreview.Ren private OldMediaEncoder mEncoder; private MediaEncoderEngine mEncoderEngine; - private GLCameraPreview mPreview; + private GlCameraPreview mPreview; private int mCurrentState = STATE_NOT_RECORDING; private int mDesiredState = STATE_NOT_RECORDING; private int mTextureId = 0; - SnapshotVideoRecorder(VideoResult stub, VideoResultListener listener, GLCameraPreview preview) { + SnapshotVideoRecorder(VideoResult stub, VideoResultListener listener, GlCameraPreview preview) { super(stub, listener); if (USE_OLD_ENCODER) { mEncoder = new OldMediaEncoder(); @@ -60,11 +60,13 @@ class SnapshotVideoRecorder extends VideoRecorder implements GLCameraPreview.Ren mDesiredState = STATE_NOT_RECORDING; } + @RendererThread @Override public void onRendererTextureCreated(int textureId) { mTextureId = textureId; } + @RendererThread @Override public void onRendererFrame(SurfaceTexture surfaceTexture, float scaleX, float scaleY) { if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) { diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/GlCameraPreview.java similarity index 97% rename from cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java rename to cameraview/src/main/views/com/otaliastudios/cameraview/GlCameraPreview.java index 623bd182..3765bec3 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/GlCameraPreview.java @@ -21,6 +21,9 @@ import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL10; + +@interface RendererThread {} + /** * - The android camera will stream image to the given {@link SurfaceTexture}. * @@ -53,7 +56,7 @@ import javax.microedition.khronos.opengles.GL10; * Callbacks are guaranteed to be called on the renderer thread, which means that we can fetch * the GL context that was created and is managed by the {@link GLSurfaceView}. */ -class GLCameraPreview extends CameraPreview implements GLSurfaceView.Renderer { +class GlCameraPreview extends CameraPreview implements GLSurfaceView.Renderer { private boolean mDispatched; private final float[] mTransformMatrix = new float[16]; @@ -64,7 +67,7 @@ class GLCameraPreview extends CameraPreview imple /* for tests */ float mScaleX = 1F; /* for tests */ float mScaleY = 1F; - GLCameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) { + GlCameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) { super(context, parent, callback); } @@ -119,7 +122,7 @@ class GLCameraPreview extends CameraPreview imple } } - // Renderer thread + @RendererThread @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { mOutputViewport = new EglViewport(); @@ -144,7 +147,7 @@ class GLCameraPreview extends CameraPreview imple }); } - // Renderer thread + @RendererThread @SuppressWarnings("StatementWithEmptyBody") @Override public void onSurfaceChanged(GL10 gl, final int width, final int height) { @@ -170,7 +173,7 @@ class GLCameraPreview extends CameraPreview imple } } - // Renderer thread + @RendererThread @Override public void onDrawFrame(GL10 gl) { // Latch the latest frame. If there isn't anything new, @@ -255,6 +258,7 @@ class GLCameraPreview extends CameraPreview imple * * @param textureId the GL texture linked to the image stream */ + @RendererThread void onRendererTextureCreated(int textureId); /** @@ -266,6 +270,7 @@ class GLCameraPreview extends CameraPreview imple * @param scaleX the scaleX (in REF_VIEW) value * @param scaleY the scaleY (in REF_VIEW) value */ + @RendererThread void onRendererFrame(SurfaceTexture surfaceTexture, float scaleX, float scaleY); }