diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoCoreEncoder.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoCoreEncoder.java index 55da8c2f..e2c80111 100644 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoCoreEncoder.java +++ b/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoCoreEncoder.java @@ -58,7 +58,7 @@ class VideoCoreEncoder { /** * Configures encoder and muxer state, and prepares the input Surface. */ - public VideoCoreEncoder(int width, int height, int bitRate, int frameRate, File outputFile) + public VideoCoreEncoder(int width, int height, int bitRate, int frameRate, int rotation, File outputFile) throws IOException { mBufferInfo = new MediaCodec.BufferInfo(); @@ -71,6 +71,7 @@ class VideoCoreEncoder { format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5); + format.setInteger("rotation-degrees", rotation); // Create a MediaCodec encoder, and configure it with our format. Get a Surface // we can use for input and wrap it with a class that handles the EGL work. diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoTextureEncoder.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoTextureEncoder.java index 27e51e20..e7adf75c 100644 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoTextureEncoder.java +++ b/cameraview/src/main/gles/com/otaliastudios/cameraview/VideoTextureEncoder.java @@ -98,12 +98,14 @@ class VideoTextureEncoder implements Runnable { final int mHeight; final int mBitRate; final int mFrameRate; + final int mRotation; final float mScaleX; final float mScaleY; final EGLContext mEglContext; Config(File outputFile, int width, int height, int bitRate, int frameRate, + int rotation, float scaleX, float scaleY, EGLContext sharedEglContext) { mOutputFile = outputFile; @@ -114,6 +116,7 @@ class VideoTextureEncoder implements Runnable { mEglContext = sharedEglContext; mScaleX = scaleX; mScaleY = scaleY; + mRotation = rotation; } @Override @@ -125,7 +128,10 @@ class VideoTextureEncoder implements Runnable { private void prepareEncoder(Config config) { try { - mVideoEncoder = new VideoCoreEncoder(config.mWidth, config.mHeight, config.mBitRate, config.mFrameRate, config.mOutputFile); + mVideoEncoder = new VideoCoreEncoder(config.mWidth, config.mHeight, + config.mBitRate, config.mFrameRate, + config.mRotation, + config.mOutputFile); } catch (IOException ioe) { throw new RuntimeException(ioe); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index f41b06f0..9b113f68 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -719,7 +719,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera videoResult.isSnapshot = true; videoResult.codec = mVideoCodec; videoResult.location = mLocation; - videoResult.rotation = offset(REF_SENSOR, REF_OUTPUT); // 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. @@ -730,15 +729,30 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera Size preview = getPreviewSize(REF_VIEW); // The preview stream size in REF_VIEW Size view = mPreview.getOutputSurfaceSize(); // The view size in REF_VIEW Rect crop = CropHelper.computeCrop(preview, AspectRatio.of(view.getWidth(), view.getHeight())); - Size cropSize = new Size(crop.width(), crop.height()); // The visible size in REF_VIEW - Size finalSize = flip(REF_VIEW, REF_OUTPUT) ? cropSize.flip() : cropSize; // Move the REF_VIEW size to REF_OUTPUT + Size finalSize = new Size(crop.width(), crop.height()); // The visible size in REF_VIEW + // ^ We could flip to REF_OUTPUT here and this is what we are doing, but since the video codec recorder + // does not account rotation, we must stay in REF_VIEW for this to work well. - // Without cropping (missing ATM), the actual size is the preview size with no crops. + // Without cropping (missing ATM - Edit: DONE), the actual size is the preview size with no crops. // Passing a cropped size while the cropping is not implemented at the encoder surface level, // would cause distortions and crashes in the video encoder. - // Size finalSize2 = getPreviewSize(REF_OUTPUT); + // Size finalSize2 = getPreviewSize(REF_VIEW); + + // With phone landscape on the left: + // offset(REF_SENSOR, REF_VIEW) -> 270 (correct) + // offset(REF_VIEW, REF_OUTPUT) -> 270 (correct) + // offset(REF_SENSOR, REF_OUTPUT) -> 180 (not correct) + + // With straight phone: + // offset(REF_SENSOR, REF_VIEW) -> 270 (not correct) + // offset(REF_VIEW, REF_OUTPUT) -> 0 (correct) + // offset(REF_SENSOR, REF_OUTPUT) -> 270 (not correct) + // So it looks like REF_VIEW REF_OUTPUT is the correct one, meaning that + // the input data in this case is not in the REF_SENSOR coordinates but rather + // in the REF_VIEW ones. videoResult.size = finalSize; + videoResult.rotation = offset(REF_VIEW, REF_OUTPUT); videoResult.audio = mAudio; videoResult.maxSize = mVideoMaxSize; videoResult.maxDuration = mVideoMaxDuration; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/MediaCodecVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/MediaCodecVideoRecorder.java index 733e4f23..d8c05a3b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/MediaCodecVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/MediaCodecVideoRecorder.java @@ -9,6 +9,10 @@ import android.support.annotation.RequiresApi; /** * A {@link VideoRecorder} that uses {@link android.media.MediaCodec} APIs. + * + * TODO rotation support. Currently we pass the wrong size + * TODO audio support + * 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 MediaCodecVideoRecorder extends VideoRecorder implements GLCameraPreview.RendererFrameCallback { @@ -19,7 +23,6 @@ class MediaCodecVideoRecorder extends VideoRecorder implements GLCameraPreview.R private static final int STATE_RECORDING = 0; private static final int STATE_NOT_RECORDING = 1; - private CamcorderProfile mProfile; private VideoTextureEncoder mEncoder; private GLCameraPreview mPreview; @@ -29,7 +32,6 @@ class MediaCodecVideoRecorder extends VideoRecorder implements GLCameraPreview.R MediaCodecVideoRecorder(VideoResult stub, VideoResultListener listener, GLCameraPreview preview, int cameraId) { super(stub, listener); - mProfile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); mEncoder = new VideoTextureEncoder(); mPreview = preview; mPreview.setRendererFrameCallback(this); @@ -38,6 +40,8 @@ class MediaCodecVideoRecorder extends VideoRecorder implements GLCameraPreview.R @Override void start() { mDesiredState = STATE_RECORDING; + // TODO respect maxSize by doing inspecting frameRate, bitRate and frame size? + // TODO do this at the encoder level, not here with a handler. if (mResult.maxDuration > 0) { new Handler().postDelayed(new Runnable() { @Override @@ -61,17 +65,20 @@ class MediaCodecVideoRecorder extends VideoRecorder implements GLCameraPreview.R @Override public void onRendererFrame(SurfaceTexture surfaceTexture, float scaleX, float scaleY) { if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) { + // Size must not be flipped based on rotation, unlike MediaRecorderVideoRecorder + Size size = mResult.getSize(); // Ensure width and height are divisible by 2, as I have read somewhere. - int width = mResult.size.getWidth(); - int height = mResult.size.getHeight(); + int width = size.getWidth(); + int height = size.getHeight(); width = width % 2 == 0 ? width : width + 1; height = height % 2 == 0 ? height : height + 1; VideoTextureEncoder.Config configuration = new VideoTextureEncoder.Config( - mResult.file, + mResult.getFile(), width, height, 1000000, 30, + mResult.getRotation(), scaleX, scaleY, EGL14.eglGetCurrentContext()