Respect VideoCodec from options

pull/360/head
Mattia Iavarone 6 years ago
parent 8662d9ba2e
commit 2999e9aa65
  1. 7
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MapperTest.java
  2. 8
      cameraview/src/main/gles/com/otaliastudios/cameraview/VideoCoreEncoder.java
  3. 96
      cameraview/src/main/gles/com/otaliastudios/cameraview/VideoTextureEncoder.java
  4. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/Mapper.java
  5. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/MediaCodecVideoRecorder.java
  6. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/MediaRecorderVideoRecorder.java
  7. 2
      demo/src/main/java/com/otaliastudios/cameraview/demo/Control.java
  8. 4
      demo/src/main/res/layout/activity_camera.xml

@ -26,11 +26,4 @@ public class MapperTest extends BaseTest {
<T> WhiteBalance unmapWhiteBalance(T cameraConstant) { return null; } <T> WhiteBalance unmapWhiteBalance(T cameraConstant) { return null; }
<T> Hdr unmapHdr(T cameraConstant) { return null; } <T> Hdr unmapHdr(T cameraConstant) { return null; }
}; };
@Test
public void testMap() {
assertEquals(mapper.map(VideoCodec.DEVICE_DEFAULT), MediaRecorder.VideoEncoder.DEFAULT);
assertEquals(mapper.map(VideoCodec.H_263), MediaRecorder.VideoEncoder.H263);
assertEquals(mapper.map(VideoCodec.H_264), MediaRecorder.VideoEncoder.H264);
}
} }

@ -45,8 +45,6 @@ import java.nio.ByteBuffer;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
class VideoCoreEncoder { class VideoCoreEncoder {
private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
private Surface mInputSurface; private Surface mInputSurface;
private MediaMuxer mMuxer; private MediaMuxer mMuxer;
private MediaCodec mEncoder; private MediaCodec mEncoder;
@ -58,11 +56,11 @@ class VideoCoreEncoder {
/** /**
* Configures encoder and muxer state, and prepares the input Surface. * Configures encoder and muxer state, and prepares the input Surface.
*/ */
public VideoCoreEncoder(int width, int height, int bitRate, int frameRate, int rotation, File outputFile) public VideoCoreEncoder(int width, int height, int bitRate, int frameRate, int rotation, File outputFile, String mimeType)
throws IOException { throws IOException {
mBufferInfo = new MediaCodec.BufferInfo(); mBufferInfo = new MediaCodec.BufferInfo();
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height); MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
// Set some properties. Failing to specify some of these can cause the MediaCodec // Set some properties. Failing to specify some of these can cause the MediaCodec
// configure() call to throw an unhelpful exception. // configure() call to throw an unhelpful exception.
@ -75,7 +73,7 @@ class VideoCoreEncoder {
// Create a MediaCodec encoder, and configure it with our format. Get a Surface // 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. // we can use for input and wrap it with a class that handles the EGL work.
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); mEncoder = MediaCodec.createEncoderByType(mimeType);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mEncoder.createInputSurface(); mInputSurface = mEncoder.createInputSurface();
mEncoder.start(); mEncoder.start();

@ -72,16 +72,17 @@ class VideoTextureEncoder implements Runnable {
private EglCore mEglCore; private EglCore mEglCore;
private EglViewport mFullScreen; private EglViewport mFullScreen;
private int mTextureId; private int mTextureId;
private int mFrameNum; private int mFrameNum = -1; // Important
private VideoCoreEncoder mVideoEncoder; private VideoCoreEncoder mVideoEncoder;
private float mTransformationScaleX = 1F; private float mTransformationScaleX = 1F;
private float mTransformationScaleY = 1F; private float mTransformationScaleY = 1F;
private int mTransformationRotation = 0;
// ----- accessed by multiple threads ----- // ----- accessed by multiple threads -----
private volatile EncoderHandler mHandler; private volatile EncoderHandler mHandler;
private final Object mReadyFence = new Object(); // guards ready/running private final Object mLooperReadyLock = new Object(); // guards ready/running
private boolean mReady; private boolean mLooperReady;
private boolean mRunning; private boolean mRunning;
/** /**
@ -102,11 +103,13 @@ class VideoTextureEncoder implements Runnable {
final float mScaleX; final float mScaleX;
final float mScaleY; final float mScaleY;
final EGLContext mEglContext; final EGLContext mEglContext;
final String mMimeType;
Config(File outputFile, int width, int height, Config(File outputFile, int width, int height,
int bitRate, int frameRate, int bitRate, int frameRate,
int rotation, int rotation,
float scaleX, float scaleY, float scaleX, float scaleY,
String mimeType,
EGLContext sharedEglContext) { EGLContext sharedEglContext) {
mOutputFile = outputFile; mOutputFile = outputFile;
mWidth = width; mWidth = width;
@ -117,6 +120,7 @@ class VideoTextureEncoder implements Runnable {
mScaleX = scaleX; mScaleX = scaleX;
mScaleY = scaleY; mScaleY = scaleY;
mRotation = rotation; mRotation = rotation;
mMimeType = mimeType;
} }
@Override @Override
@ -130,8 +134,9 @@ class VideoTextureEncoder implements Runnable {
try { try {
mVideoEncoder = new VideoCoreEncoder(config.mWidth, config.mHeight, mVideoEncoder = new VideoCoreEncoder(config.mWidth, config.mHeight,
config.mBitRate, config.mFrameRate, config.mBitRate, config.mFrameRate,
config.mRotation, 0, // The video encoder rotation does not work, so we apply it here using Matrix.rotateM().
config.mOutputFile); config.mOutputFile,
config.mMimeType);
} catch (IOException ioe) { } catch (IOException ioe) {
throw new RuntimeException(ioe); throw new RuntimeException(ioe);
} }
@ -141,6 +146,7 @@ class VideoTextureEncoder implements Runnable {
mFullScreen = new EglViewport(); mFullScreen = new EglViewport();
mTransformationScaleX = config.mScaleX; mTransformationScaleX = config.mScaleX;
mTransformationScaleY = config.mScaleY; mTransformationScaleY = config.mScaleY;
mTransformationRotation = config.mRotation;
} }
private void releaseEncoder() { private void releaseEncoder() {
@ -169,16 +175,16 @@ class VideoTextureEncoder implements Runnable {
*/ */
public void startRecording(Config config) { public void startRecording(Config config) {
Log.d(TAG, "Encoder: startRecording()"); Log.d(TAG, "Encoder: startRecording()");
synchronized (mReadyFence) { synchronized (mLooperReadyLock) {
if (mRunning) { if (mRunning) {
Log.w(TAG, "Encoder thread already running"); Log.w(TAG, "Encoder thread already running");
return; return;
} }
mRunning = true; mRunning = true;
new Thread(this, "TextureMovieEncoder").start(); new Thread(this, "TextureMovieEncoder").start();
while (!mReady) { while (!mLooperReady) {
try { try {
mReadyFence.wait(); mLooperReadyLock.wait();
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
// ignore // ignore
} }
@ -205,7 +211,7 @@ class VideoTextureEncoder implements Runnable {
* Returns true if recording has been started. * Returns true if recording has been started.
*/ */
public boolean isRecording() { public boolean isRecording() {
synchronized (mReadyFence) { synchronized (mLooperReadyLock) {
return mRunning; return mRunning;
} }
} }
@ -224,18 +230,14 @@ class VideoTextureEncoder implements Runnable {
* stall the caller while this thread does work. * stall the caller while this thread does work.
*/ */
public void frameAvailable(SurfaceTexture st) { public void frameAvailable(SurfaceTexture st) {
synchronized (mReadyFence) { synchronized (mLooperReadyLock) {
if (!mReady) { if (!mLooperReady) {
return; return;
} }
} }
float[] transform = new float[16]; // TODO - avoid alloc every frame. Not easy, need a pool float[] transform = new float[16]; // TODO - avoid alloc every frame. Not easy, need a pool
st.getTransformMatrix(transform); st.getTransformMatrix(transform);
float translX = (1F - mTransformationScaleX) / 2F;
float translY = (1F - mTransformationScaleY) / 2F;
Matrix.translateM(transform, 0, translX, translY, 0);
Matrix.scaleM(transform, 0, mTransformationScaleX, mTransformationScaleY, 1);
long timestamp = st.getTimestamp(); long timestamp = st.getTimestamp();
if (timestamp == 0) { if (timestamp == 0) {
// Seeing this after device is toggled off/on with power button. The // Seeing this after device is toggled off/on with power button. The
@ -257,8 +259,8 @@ class VideoTextureEncoder implements Runnable {
* TODO: do something less clumsy * TODO: do something less clumsy
*/ */
public void setTextureId(int id) { public void setTextureId(int id) {
synchronized (mReadyFence) { synchronized (mLooperReadyLock) {
if (!mReady) return; if (!mLooperReady) return;
} }
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_TEXTURE_ID, id, 0, null)); mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_TEXTURE_ID, id, 0, null));
} }
@ -272,16 +274,15 @@ class VideoTextureEncoder implements Runnable {
public void run() { public void run() {
// Establish a Looper for this thread, and define a Handler for it. // Establish a Looper for this thread, and define a Handler for it.
Looper.prepare(); Looper.prepare();
synchronized (mReadyFence) { synchronized (mLooperReadyLock) {
mHandler = new EncoderHandler(this); mHandler = new EncoderHandler(this);
mReady = true; mLooperReady = true;
mReadyFence.notify(); mLooperReadyLock.notify();
} }
Looper.loop(); Looper.loop();
Log.d(TAG, "Encoder thread exiting"); Log.d(TAG, "Encoder thread exiting");
synchronized (mReadyFence) { synchronized (mLooperReadyLock) {
mReady = mRunning = false; mLooperReady = mRunning = false;
mHandler = null; mHandler = null;
} }
} }
@ -314,14 +315,63 @@ class VideoTextureEncoder implements Runnable {
encoder.prepareEncoder(config); encoder.prepareEncoder(config);
break; break;
case MSG_STOP_RECORDING: case MSG_STOP_RECORDING:
encoder.mFrameNum = -1;
encoder.mVideoEncoder.drainEncoder(true); encoder.mVideoEncoder.drainEncoder(true);
encoder.releaseEncoder(); encoder.releaseEncoder();
((Runnable) obj).run(); ((Runnable) obj).run();
break; break;
case MSG_FRAME_AVAILABLE: case MSG_FRAME_AVAILABLE:
if (encoder.mFrameNum < 0) break;
encoder.mFrameNum++; encoder.mFrameNum++;
long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL); long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL);
float[] transform = (float[]) obj; float[] transform = (float[]) obj;
// 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.
// 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 apply another Translation to compensate.
// The order of operations must be translate, rotate & scale.
float scaleX = encoder.mTransformationScaleX;
float scaleY = encoder.mTransformationScaleY;
int rotation = encoder.mTransformationRotation;
float W = scaleX;
float H = scaleY;
float scaleTranslX = (1F - scaleX) / 2F;
float scaleTranslY = (1F - scaleY) / 2F;
float rotationTranslX = 0F;
float rotationTranslY = 0F;
boolean flip = false;// rotation % 180 != 0;
Log.e("VideoTextureEncoder", "Rotation is " + rotation);
if (rotation == 90) {
rotationTranslX = W / 2F;
rotationTranslY = W / 2F;
} else if (rotation == 180) {
rotationTranslX = W / 2F;
rotationTranslY = H / 2F;
} else if (rotation == 270) {
rotationTranslX = H / 2F;
rotationTranslY = H / 2F;
Log.e("VideoTextureEncoder", "Rotation translY is" + rotationTranslY + ", h is " + H);
}
// Matrix.translateM(transform, 0, 0, 0, 0); // vedo il lato destro e alto
// Matrix.translateM(transform, 0, 0, 0.5F, 0); // same
// Matrix.translateM(transform, 0, 0, -0.5F, 0); // peggio
// Matrix.translateM(transform, 0, 0.5F, 0, 0); // peggio: vedo il pixel in alto a dx
// Matrix.translateM(transform, 0, -0.5F, 0, 0); // ho aggiustato la VERTICALE
// Matrix.translateM(transform, 0, -0.5F, -1, 0); // no changes
// Matrix.translateM(transform, 0, -0.5F, 1, 0); // ci siamo quasi
Matrix.translateM(transform, 0, -0.5F, 1.75F, 0); // ottimo! ma è scalato male!!
Matrix.rotateM(transform, 0, rotation, 0, 0, 1);
Matrix.translateM(transform, 0, rotationTranslX, rotationTranslY, 0);
Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0);
Matrix.scaleM(transform, 0, scaleX, scaleY, 1);
encoder.mVideoEncoder.drainEncoder(false); encoder.mVideoEncoder.drainEncoder(false);
encoder.mFullScreen.drawFrame(encoder.mTextureId, transform); encoder.mFullScreen.drawFrame(encoder.mTextureId, transform);
encoder.mInputWindowSurface.setPresentationTime(timestamp); encoder.mInputWindowSurface.setPresentationTime(timestamp);

@ -16,13 +16,4 @@ abstract class Mapper {
abstract <T> Facing unmapFacing(T cameraConstant); abstract <T> Facing unmapFacing(T cameraConstant);
abstract <T> WhiteBalance unmapWhiteBalance(T cameraConstant); abstract <T> WhiteBalance unmapWhiteBalance(T cameraConstant);
abstract <T> Hdr unmapHdr(T cameraConstant); abstract <T> Hdr unmapHdr(T cameraConstant);
int map(VideoCodec codec) {
switch (codec) {
case DEVICE_DEFAULT: return MediaRecorder.VideoEncoder.DEFAULT;
case H_263: return MediaRecorder.VideoEncoder.H263;
case H_264: return MediaRecorder.VideoEncoder.H264;
default: return MediaRecorder.VideoEncoder.DEFAULT;
}
}
} }

@ -2,6 +2,7 @@ package com.otaliastudios.cameraview;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.media.CamcorderProfile; import android.media.CamcorderProfile;
import android.media.MediaFormat;
import android.opengl.EGL14; import android.opengl.EGL14;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
@ -65,13 +66,20 @@ class MediaCodecVideoRecorder extends VideoRecorder implements GLCameraPreview.R
@Override @Override
public void onRendererFrame(SurfaceTexture surfaceTexture, float scaleX, float scaleY) { public void onRendererFrame(SurfaceTexture surfaceTexture, float scaleX, float scaleY) {
if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) { if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) {
// Size must not be flipped based on rotation, unlike MediaRecorderVideoRecorder // Size must be flipped based on rotation, because we will rotate the texture in the encoder
Size size = mResult.getSize(); Size size = mResult.getRotation() % 180 == 0 ? mResult.getSize() : mResult.getSize().flip();
// size = mResult.size;
// Ensure width and height are divisible by 2, as I have read somewhere. // Ensure width and height are divisible by 2, as I have read somewhere.
int width = size.getWidth(); int width = size.getWidth();
int height = size.getHeight(); int height = size.getHeight();
width = width % 2 == 0 ? width : width + 1; width = width % 2 == 0 ? width : width + 1;
height = height % 2 == 0 ? height : height + 1; height = height % 2 == 0 ? height : height + 1;
String type = "";
switch (mResult.codec) {
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;
}
VideoTextureEncoder.Config configuration = new VideoTextureEncoder.Config( VideoTextureEncoder.Config configuration = new VideoTextureEncoder.Config(
mResult.getFile(), mResult.getFile(),
width, width,
@ -81,6 +89,7 @@ class MediaCodecVideoRecorder extends VideoRecorder implements GLCameraPreview.R
mResult.getRotation(), mResult.getRotation(),
scaleX, scaleX,
scaleY, scaleY,
type,
EGL14.eglGetCurrentContext() EGL14.eglGetCurrentContext()
); );
mEncoder.startRecording(configuration); mEncoder.startRecording(configuration);

@ -42,10 +42,10 @@ class MediaRecorderVideoRecorder extends VideoRecorder {
mMediaRecorder.setOutputFormat(mProfile.fileFormat); mMediaRecorder.setOutputFormat(mProfile.fileFormat);
mMediaRecorder.setVideoFrameRate(mProfile.videoFrameRate); mMediaRecorder.setVideoFrameRate(mProfile.videoFrameRate);
mMediaRecorder.setVideoSize(size.getWidth(), size.getHeight()); mMediaRecorder.setVideoSize(size.getWidth(), size.getHeight());
if (mResult.getCodec() == VideoCodec.DEFAULT) { switch (mResult.getCodec()) {
mMediaRecorder.setVideoEncoder(mProfile.videoCodec); case H_263: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263); break;
} else { case H_264: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); break;
mMediaRecorder.setVideoEncoder(mMapper.map(mResult.getCodec())); case DEVICE_DEFAULT: mMediaRecorder.setVideoEncoder(mProfile.videoCodec); break;
} }
mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate); mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate);
if (mResult.getAudio() == Audio.ON) { if (mResult.getAudio() == Audio.ON) {

@ -63,7 +63,7 @@ public enum Control {
int boundary = this == WIDTH ? root.getWidth() : root.getHeight(); int boundary = this == WIDTH ? root.getWidth() : root.getHeight();
if (boundary == 0) boundary = 1000; if (boundary == 0) boundary = 1000;
int step = boundary / 10; int step = boundary / 10;
// list.add(this == WIDTH ? 12 : 16); list.add(this == WIDTH ? 300 : 700);
list.add(ViewGroup.LayoutParams.WRAP_CONTENT); list.add(ViewGroup.LayoutParams.WRAP_CONTENT);
list.add(ViewGroup.LayoutParams.MATCH_PARENT); list.add(ViewGroup.LayoutParams.MATCH_PARENT);
for (int i = step; i < boundary; i += step) { for (int i = step; i < boundary; i += step) {

@ -11,8 +11,8 @@
<com.otaliastudios.cameraview.CameraView <com.otaliastudios.cameraview.CameraView
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/camera" android:id="@+id/camera"
android:layout_width="wrap_content" android:layout_width="300px"
android:layout_height="wrap_content" android:layout_height="700px"
android:layout_gravity="center" android:layout_gravity="center"
android:keepScreenOn="true" android:keepScreenOn="true"
app:cameraExperimental="true" app:cameraExperimental="true"

Loading…
Cancel
Save