diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index 4711bd14..765c80ed 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -809,7 +809,8 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac // Reset facing and start. mFacing = realFacing; GlCameraPreview cameraPreview = (GlCameraPreview) mPreview; - mVideoRecorder = new SnapshotVideoRecorder(stub, Camera1Engine.this, cameraPreview); + mVideoRecorder = new SnapshotVideoRecorder(stub, + Camera1Engine.this, Camera1Engine.this, cameraPreview); mVideoRecorder.start(); } }); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java index 6348a69d..74349519 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java @@ -18,7 +18,7 @@ import androidx.annotation.NonNull; /** * Wraps the {@link android.media.CamcorderProfile} static utilities. */ -class CamcorderProfiles { +public class CamcorderProfiles { @SuppressLint("UseSparseArrays") private static Map sizeToProfileMap = new HashMap<>(); @@ -44,7 +44,7 @@ class CamcorderProfiles { * @return a profile */ @NonNull - static CamcorderProfile get(int cameraId, @NonNull Size targetSize) { + public static CamcorderProfile get(int cameraId, @NonNull Size targetSize) { final int targetArea = targetSize.getWidth() * targetSize.getHeight(); List sizes = new ArrayList<>(sizeToProfileMap.keySet()); Collections.sort(sizes, new Comparator() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java index 090210b9..4c200690 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -7,10 +7,9 @@ import android.media.MediaRecorder; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.controls.Audio; -import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.engine.Camera1Engine; +import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; import com.otaliastudios.cameraview.size.Size; -import com.otaliastudios.cameraview.utils.CamcorderProfiles; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -39,15 +38,13 @@ public class FullVideoRecorder extends VideoRecorder { mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); // Get a profile of quality compatible with the chosen size. - mSize = mResult.getRotation() % 180 != 0 ? mResult.getSize().flip() : mResult.getSize(); + mSize = mResult.rotation % 180 != 0 ? mResult.size.flip() : mResult.size; mProfile = CamcorderProfiles.get(cameraId, mSize); } - // Camera2 constructor here... - @Override - void start() { - if (mResult.getAudio() == Audio.ON) { + public void start() { + if (mResult.audio == Audio.ON) { // Must be called before setOutputFormat. mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); } @@ -60,10 +57,10 @@ public class FullVideoRecorder extends VideoRecorder { mMediaRecorder.setVideoFrameRate(mResult.videoFrameRate); } mMediaRecorder.setVideoSize(mSize.getWidth(), mSize.getHeight()); - switch (mResult.getVideoCodec()) { - case VideoCodec.H_263: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263); break; - case VideoCodec.H_264: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); break; - case VideoCodec.DEVICE_DEFAULT: mMediaRecorder.setVideoEncoder(mProfile.videoCodec); break; + switch (mResult.videoCodec) { + case H_263: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263); break; + case H_264: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); break; + case DEVICE_DEFAULT: mMediaRecorder.setVideoEncoder(mProfile.videoCodec); break; } if (mResult.videoBitRate <= 0) { mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate); @@ -71,7 +68,7 @@ public class FullVideoRecorder extends VideoRecorder { } else { mMediaRecorder.setVideoEncodingBitRate(mResult.videoBitRate); } - if (mResult.getAudio() == Audio.ON) { + if (mResult.audio == Audio.ON) { mMediaRecorder.setAudioChannels(mProfile.audioChannels); mMediaRecorder.setAudioSamplingRate(mProfile.audioSampleRate); mMediaRecorder.setAudioEncoder(mProfile.audioCodec); @@ -82,15 +79,15 @@ public class FullVideoRecorder extends VideoRecorder { mMediaRecorder.setAudioEncodingBitRate(mResult.audioBitRate); } } - if (mResult.getLocation() != null) { + if (mResult.location != null) { mMediaRecorder.setLocation( - (float) mResult.getLocation().getLatitude(), - (float) mResult.getLocation().getLongitude()); + (float) mResult.location.getLatitude(), + (float) mResult.location.getLongitude()); } - mMediaRecorder.setOutputFile(mResult.getFile().getAbsolutePath()); - mMediaRecorder.setOrientationHint(mResult.getRotation()); - mMediaRecorder.setMaxFileSize(mResult.getMaxSize()); - mMediaRecorder.setMaxDuration(mResult.getMaxDuration()); + mMediaRecorder.setOutputFile(mResult.file.getAbsolutePath()); + mMediaRecorder.setOrientationHint(mResult.rotation); + mMediaRecorder.setMaxFileSize(mResult.maxSize); + mMediaRecorder.setMaxDuration(mResult.maxDuration); mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { @Override public void onInfo(MediaRecorder mediaRecorder, int what, int extra) { @@ -120,7 +117,7 @@ public class FullVideoRecorder extends VideoRecorder { } @Override - void stop() { + public void stop() { if (mMediaRecorder != null) { try { mMediaRecorder.stop(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java index 78808139..56a6b5af 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -4,17 +4,17 @@ import android.graphics.SurfaceTexture; import android.opengl.EGL14; import android.os.Build; -import com.otaliastudios.cameraview.AudioMediaEncoder; import com.otaliastudios.cameraview.CameraLogger; -import com.otaliastudios.cameraview.MediaEncoderEngine; -import com.otaliastudios.cameraview.TextureMediaEncoder; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.controls.Audio; -import com.otaliastudios.cameraview.controls.VideoCodec; +import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.RendererFrameCallback; import com.otaliastudios.cameraview.preview.RendererThread; import com.otaliastudios.cameraview.size.Size; +import com.otaliastudios.cameraview.video.encoding.AudioMediaEncoder; +import com.otaliastudios.cameraview.video.encoding.MediaEncoderEngine; +import com.otaliastudios.cameraview.video.encoding.TextureMediaEncoder; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -39,24 +39,27 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram private MediaEncoderEngine mEncoderEngine; private GlCameraPreview mPreview; + private boolean mFlipped; private int mCurrentState = STATE_NOT_RECORDING; private int mDesiredState = STATE_NOT_RECORDING; private int mTextureId = 0; - public SnapshotVideoRecorder(@NonNull VideoResult.Stub stub, @Nullable VideoResultListener listener, @NonNull GlCameraPreview preview) { + public SnapshotVideoRecorder(@NonNull VideoResult.Stub stub, @Nullable VideoResultListener listener, + @NonNull CameraEngine engine, @NonNull GlCameraPreview preview) { super(stub, listener); mPreview = preview; mPreview.addRendererFrameCallback(this); + mFlipped = engine.flip(CameraEngine.REF_SENSOR, CameraEngine.REF_VIEW); } @Override - void start() { + public void start() { mDesiredState = STATE_RECORDING; } @Override - void stop() { + public void stop() { mDesiredState = STATE_NOT_RECORDING; } @@ -76,16 +79,16 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram if (mResult.audioBitRate <= 0) mResult.audioBitRate = DEFAULT_AUDIO_BITRATE; // Video. Ensure width and height are divisible by 2, as I have read somewhere. - Size size = mResult.getSize(); + Size size = mResult.size; int width = size.getWidth(); int height = size.getHeight(); width = width % 2 == 0 ? width : width + 1; height = height % 2 == 0 ? height : height + 1; String type = ""; - switch (mResult.codec) { - case VideoCodec.H_263: type = "video/3gpp"; break; // MediaFormat.MIMETYPE_VIDEO_H263; - case VideoCodec.H_264: type = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC: - case VideoCodec.DEVICE_DEFAULT: type = "video/avc"; break; + switch (mResult.videoCodec) { + case H_263: type = "video/3gpp"; break; // MediaFormat.MIMETYPE_VIDEO_H263; + case H_264: type = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC: + case DEVICE_DEFAULT: type = "video/avc"; break; } LOG.w("Creating frame encoder. Rotation:", mResult.rotation); TextureMediaEncoder.Config config = new TextureMediaEncoder.Config(width, height, @@ -94,7 +97,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram mResult.rotation, type, mTextureId, scaleX, scaleY, - mPreview.mInputFlipped, + mFlipped, EGL14.eglGetCurrentContext() ); TextureMediaEncoder videoEncoder = new TextureMediaEncoder(config); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java index 029910e2..e855ebdf 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java @@ -4,6 +4,7 @@ import com.otaliastudios.cameraview.VideoResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; /** * Interface for video recording. @@ -12,19 +13,47 @@ import androidx.annotation.Nullable; */ public abstract class VideoRecorder { - /* tests */ VideoResult.Stub mResult; - /* tests */ VideoResultListener mListener; + /** + * Listens for video recorder events. + */ + public interface VideoResultListener { + + /** + * The operation was completed, either with success or with an error. + * @param result the result or null if error + * @param exception the error or null if everything went fine + */ + void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) VideoResult.Stub mResult; + @VisibleForTesting VideoResultListener mListener; protected Exception mError; + /** + * Creates a new video recorder. + * @param stub a video stub + * @param listener a listener + */ VideoRecorder(@NonNull VideoResult.Stub stub, @Nullable VideoResultListener listener) { mResult = stub; mListener = listener; } + /** + * Starts recording a video. + */ public abstract void start(); + /** + * Stops recording. + */ public abstract void stop(); + /** + * Subclasses can call this to notify that the result was obtained, + * either with some error (null result) or with the actual stub, filled. + */ @SuppressWarnings("WeakerAccess") protected void dispatchResult() { if (mListener != null) { @@ -34,8 +63,4 @@ public abstract class VideoRecorder { mError = null; } } - - public interface VideoResultListener { - void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception); - } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java index 6df5360a..fe5c750e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java @@ -25,7 +25,7 @@ import java.util.concurrent.LinkedBlockingQueue; // TODO create onVideoRecordingStart/onVideoRecordingEnd callbacks @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) -class AudioMediaEncoder extends MediaEncoder { +public class AudioMediaEncoder extends MediaEncoder { private static final String TAG = AudioMediaEncoder.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); @@ -59,14 +59,14 @@ class AudioMediaEncoder extends MediaEncoder { private ByteBufferPool mByteBufferPool; private Config mConfig; - static class Config { + public static class Config { int bitRate; - Config(int bitRate) { + public Config(int bitRate) { this.bitRate = bitRate; } } - AudioMediaEncoder(@NonNull Config config) { + public AudioMediaEncoder(@NonNull Config config) { mConfig = config; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java index 48b24303..e719e191 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java @@ -15,15 +15,15 @@ import java.io.IOException; import java.util.ArrayList; @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) -class MediaEncoderEngine { +public class MediaEncoderEngine { private final static String TAG = MediaEncoderEngine.class.getSimpleName(); private final static CameraLogger LOG = CameraLogger.create(TAG); @SuppressWarnings("WeakerAccess") - final static int STOP_BY_USER = 0; - final static int STOP_BY_MAX_DURATION = 1; - final static int STOP_BY_MAX_SIZE = 2; + public final static int STOP_BY_USER = 0; + public final static int STOP_BY_MAX_DURATION = 1; + public final static int STOP_BY_MAX_SIZE = 2; private ArrayList mEncoders; private MediaMuxer mMediaMuxer; @@ -36,7 +36,7 @@ class MediaEncoderEngine { private int mPossibleStopReason; private final Object mControllerLock = new Object(); - MediaEncoderEngine(@NonNull File file, @NonNull VideoMediaEncoder videoEncoder, @Nullable AudioMediaEncoder audioEncoder, + public MediaEncoderEngine(@NonNull File file, @NonNull VideoMediaEncoder videoEncoder, @Nullable AudioMediaEncoder audioEncoder, final int maxDuration, final long maxSize, @Nullable Listener listener) { mListener = listener; mController = new Controller(); @@ -164,14 +164,14 @@ class MediaEncoderEngine { } } - final void start() { + public final void start() { for (MediaEncoder encoder : mEncoders) { encoder.start(); } } @SuppressWarnings("SameParameterValue") - final void notify(final String event, final Object data) { + public final void notify(final String event, final Object data) { for (MediaEncoder encoder : mEncoders) { encoder.notify(event, data); } @@ -181,7 +181,7 @@ class MediaEncoderEngine { * This just asks the encoder to stop. We will wait for them to call {@link Controller#requestRelease(int)} * to actually stop the muxer, as there might be async stuff going on. */ - final void stop() { + public final void stop() { for (MediaEncoder encoder : mEncoders) { encoder.stop(); } @@ -212,12 +212,12 @@ class MediaEncoderEngine { } @NonNull - VideoMediaEncoder getVideoEncoder() { + public VideoMediaEncoder getVideoEncoder() { return (VideoMediaEncoder) mEncoders.get(0); } @Nullable - AudioMediaEncoder getAudioEncoder() { + public AudioMediaEncoder getAudioEncoder() { if (mEncoders.size() > 1) { return (AudioMediaEncoder) mEncoders.get(1); } else { @@ -225,7 +225,7 @@ class MediaEncoderEngine { } } - interface Listener { + public interface Listener { @EncoderThread void onEncoderStop(int stopReason, @Nullable Exception e); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java index d8ebb6f8..e15a954d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java @@ -15,14 +15,14 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) -class TextureMediaEncoder extends VideoMediaEncoder { +public class TextureMediaEncoder extends VideoMediaEncoder { private static final String TAG = TextureMediaEncoder.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); - final static String FRAME_EVENT = "frame"; + public final static String FRAME_EVENT = "frame"; - static class Config extends VideoMediaEncoder.Config { + public static class Config extends VideoMediaEncoder.Config { int textureId; float scaleX; float scaleY; @@ -30,7 +30,7 @@ class TextureMediaEncoder extends VideoMediaEncoder EGLContext eglContext; int transformRotation; - Config(int width, int height, int bitRate, int frameRate, int rotation, String mimeType, + public Config(int width, int height, int bitRate, int frameRate, int rotation, String mimeType, int textureId, float scaleX, float scaleY, boolean scaleFlipped, EGLContext eglContext) { // We rotate the texture using transformRotation. Pass rotation=0 to super so that // no rotation metadata is written into the output file. @@ -54,20 +54,20 @@ class TextureMediaEncoder extends VideoMediaEncoder } }); - TextureMediaEncoder(@NonNull Config config) { + public TextureMediaEncoder(@NonNull Config config) { super(config); } - static class TextureFrame { + public static class TextureFrame { private TextureFrame() {} // Nanoseconds, in no meaningful time-base. Should be for offsets only. // Typically coming from SurfaceTexture.getTimestamp(). - long timestamp; - float[] transform = new float[16]; + public long timestamp; + public float[] transform = new float[16]; } @NonNull - TextureFrame acquireFrame() { + public TextureFrame acquireFrame() { if (mFramePool.isEmpty()) { throw new RuntimeException("Need more frames than this! Please increase the pool size."); } else {