【问题标题】:Recording video on Android using JavaCV (Updated 2014 02 17)使用 JavaCV 在 Android 上录制视频(2014 年 2 月 17 日更新)
【发布时间】:2014-09-25 06:16:58
【问题描述】:

我正在尝试使用 JavaCV 库在 Android 中录制视频。 我需要录制 640x360 的视频。

我已按照 README.txt 文件中的说明安装了所有内容,并按照以下示例进行操作: https://code.google.com/p/javacv/source/browse/samples/RecordActivity.java 在这个例子中,视频大小是这样的: 私有 int imageWidth = 320; 私有 int imageHeight = 240;

就我而言,我需要以 640x360 H.264 录制视频。

(更新)我已恢复我的代码并保持与示例中的完全相同,只是将 imageWidth 和 imageHeight 更改为 640x360。 现在我得到这样的视频: http://bergmann.net.br/img/screenshot_video_error.png

这是我的代码:

import static com.googlecode.javacv.cpp.opencv_core.IPL_DEPTH_8U;

import java.io.IOException;
import java.nio.ShortBuffer;

import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.PowerManager;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;

import com.autosonvideo.helpers.Helpers;
import com.autosonvideo.logic.CameraHelpers;
import com.googlecode.javacv.FFmpegFrameRecorder;
import com.googlecode.javacv.cpp.opencv_core.IplImage;

public class FFmpegRecordActivity extends Activity implements OnClickListener {

    private final static String CLASS_LABEL = "RecordActivity";
    private final static String LOG_TAG = CLASS_LABEL;

    private PowerManager.WakeLock mWakeLock;

    private String ffmpeg_link;

    long startTime = 0;
    boolean recording = false;

    private volatile FFmpegFrameRecorder recorder;

    private boolean isPreviewOn = false;

    private int sampleAudioRateInHz = 44100;
    private int imageWidth = 640;
    private int imageHeight = 480;

    private int finalImageWidth = 640;
    private int finalImageHeight = 360;

    private int frameRate = 30;

    /* audio data getting thread */
    private AudioRecord audioRecord;
    private AudioRecordRunnable audioRecordRunnable;
    private Thread audioThread;
    volatile boolean runAudioThread = true;

    /* video data getting thread */
    private Camera cameraDevice;
    private CameraView cameraView;

    private IplImage yuvIplimage = null;

    /* layout setting */
    private final int bg_screen_bx = 232;
    private final int bg_screen_by = 128;
    private final int bg_screen_width = 700;
    private final int bg_screen_height = 500;
    private final int bg_width = 1123;
    private final int bg_height = 715;
    private final int live_width = 1280;
    private final int live_height = 960;
    private int screenWidth, screenHeight;
    private Button btnRecorderControl;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

        setContentView(R.layout.main);

        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
                CLASS_LABEL);
        mWakeLock.acquire();

        initLayout();
        initRecorder();
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mWakeLock == null) {
            PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
            mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
                    CLASS_LABEL);
            mWakeLock.acquire();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();

        if (mWakeLock != null) {
            mWakeLock.release();
            mWakeLock = null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        recording = false;

        if (cameraView != null) {
            cameraView.stopPreview();
            cameraDevice.release();
            cameraDevice = null;
        }

        if (mWakeLock != null) {
            mWakeLock.release();
            mWakeLock = null;
        }
    }

    private void initLayout() {

        /* get size of screen */
        Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE))
                .getDefaultDisplay();
        screenWidth = display.getWidth();
        screenHeight = display.getHeight();
        RelativeLayout.LayoutParams layoutParam = null;
        LayoutInflater myInflate = null;
        myInflate = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        RelativeLayout topLayout = new RelativeLayout(this);
        setContentView(topLayout);
        LinearLayout preViewLayout = (LinearLayout) myInflate.inflate(
                R.layout.main, null);
        layoutParam = new RelativeLayout.LayoutParams(screenWidth, screenHeight);
        topLayout.addView(preViewLayout, layoutParam);

        /* add control button: start and stop */
        btnRecorderControl = (Button) findViewById(R.id.recorder_control);
        btnRecorderControl.setText("Start");
        btnRecorderControl.setOnClickListener(this);

        /* add camera view */
        int display_width_d = (int) (1.0 * bg_screen_width * screenWidth / bg_width);
        int display_height_d = (int) (1.0 * bg_screen_height * screenHeight / bg_height);
        int prev_rw, prev_rh;
        if (1.0 * display_width_d / display_height_d > 1.0 * live_width
                / live_height) {
            prev_rh = display_height_d;
            prev_rw = (int) (1.0 * display_height_d * live_width / live_height);
        } else {
            prev_rw = display_width_d;
            prev_rh = (int) (1.0 * display_width_d * live_height / live_width);
        }
        layoutParam = new RelativeLayout.LayoutParams(prev_rw, prev_rh);
        layoutParam.topMargin = (int) (1.0 * bg_screen_by * screenHeight / bg_height);
        layoutParam.leftMargin = (int) (1.0 * bg_screen_bx * screenWidth / bg_width);

        cameraDevice = Camera.open();
        Log.i(LOG_TAG, "cameara open");
        cameraView = new CameraView(this, cameraDevice);
        topLayout.addView(cameraView, layoutParam);
        Log.i(LOG_TAG, "cameara preview start: OK");
    }

    // ---------------------------------------
    // initialize ffmpeg_recorder
    // ---------------------------------------
    private void initRecorder() {

        Log.w(LOG_TAG, "init recorder");

        if (yuvIplimage == null) {
            yuvIplimage = IplImage.create(finalImageWidth, finalImageHeight,
                    IPL_DEPTH_8U, 2);
            Log.i(LOG_TAG, "create yuvIplimage");
        }

        ffmpeg_link = CameraHelpers.getOutputMediaFile(
                CameraHelpers.MEDIA_TYPE_VIDEO).toString();

        Log.i(LOG_TAG, "ffmpeg_url: " + ffmpeg_link);
        recorder = new FFmpegFrameRecorder(ffmpeg_link, finalImageWidth,
                finalImageHeight, 1);
        recorder.setFormat("mp4");
        recorder.setSampleRate(sampleAudioRateInHz);
        // Set in the surface changed method
        recorder.setFrameRate(frameRate);

        Log.i(LOG_TAG, "recorder initialize success");

        audioRecordRunnable = new AudioRecordRunnable();
        audioThread = new Thread(audioRecordRunnable);
    }

    public void startRecording() {

        try {
            recorder.start();
            startTime = System.currentTimeMillis();
            recording = true;
            audioThread.start();

        } catch (FFmpegFrameRecorder.Exception e) {
            e.printStackTrace();
        }
    }

    public void stopRecording() {

        runAudioThread = false;

        if (recorder != null && recording) {
            recording = false;
            Log.v(LOG_TAG,
                    "Finishing recording, calling stop and release on recorder");
            try {
                recorder.stop();
                recorder.release();
            } catch (FFmpegFrameRecorder.Exception e) {
                e.printStackTrace();
            }
            recorder = null;

        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (recording) {
                stopRecording();
            }

            finish();

            return true;
        }

        return super.onKeyDown(keyCode, event);
    }

    // ---------------------------------------------
    // audio thread, gets and encodes audio data
    // ---------------------------------------------
    class AudioRecordRunnable implements Runnable {

        @Override
        public void run() {
            android.os.Process
                    .setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);

            // Audio
            int bufferSize;
            short[] audioData;
            int bufferReadResult;

            bufferSize = AudioRecord
                    .getMinBufferSize(sampleAudioRateInHz,
                            AudioFormat.CHANNEL_IN_MONO,
                            AudioFormat.ENCODING_PCM_16BIT);
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                    sampleAudioRateInHz, AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT, bufferSize);

            audioData = new short[bufferSize];

            Log.d(LOG_TAG, "audioRecord.startRecording()");
            audioRecord.startRecording();

            /* ffmpeg_audio encoding loop */
            while (runAudioThread) {
                // Log.v(LOG_TAG,"recording? " + recording);
                bufferReadResult = audioRecord.read(audioData, 0,
                        audioData.length);
                if (bufferReadResult > 0) {
                    Log.v(LOG_TAG, "bufferReadResult: " + bufferReadResult);
                    // If "recording" isn't true when start this thread, it
                    // never get's set according to this if statement...!!!
                    // Why? Good question...
                    if (recording) {
                        try {
                            recorder.record(ShortBuffer.wrap(audioData, 0,
                                    bufferReadResult));
                            // Log.v(LOG_TAG,"recording " + 1024*i + " to " +
                            // 1024*i+1024);
                        } catch (FFmpegFrameRecorder.Exception e) {
                            Log.v(LOG_TAG, e.getMessage());
                            e.printStackTrace();
                        }
                    }
                }
            }
            Log.v(LOG_TAG, "AudioThread Finished, release audioRecord");

            /* encoding finish, release recorder */
            if (audioRecord != null) {
                audioRecord.stop();
                audioRecord.release();
                audioRecord = null;
                Log.v(LOG_TAG, "audioRecord released");
            }
        }
    }

    // ---------------------------------------------
    // camera thread, gets and encodes video data
    // ---------------------------------------------
    class CameraView extends SurfaceView implements SurfaceHolder.Callback,
            PreviewCallback {

        private SurfaceHolder mHolder;
        private Camera mCamera;

        public CameraView(Context context, Camera camera) {
            super(context);
            Log.w("camera", "camera view");
            mCamera = camera;
            mHolder = getHolder();
            mHolder.addCallback(CameraView.this);
            mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
            mCamera.setPreviewCallback(CameraView.this);
        }

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            try {
                stopPreview();
                mCamera.setPreviewDisplay(holder);
            } catch (IOException exception) {
                mCamera.release();
                mCamera = null;
            }
        }

        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                int height) {
            Log.v(LOG_TAG, "Setting imageWidth: " + imageWidth
                    + " imageHeight: " + imageHeight + " frameRate: "
                    + frameRate);
            Camera.Parameters camParams = mCamera.getParameters();
            camParams.setPreviewSize(imageWidth, imageHeight);

            Log.v(LOG_TAG,
                    "Preview Framerate: " + camParams.getPreviewFrameRate());

            camParams.setPreviewFrameRate(frameRate);
            mCamera.setParameters(camParams);
            startPreview();
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            try {
                mHolder.addCallback(null);
                mCamera.setPreviewCallback(null);
            } catch (RuntimeException e) {
                // The camera has probably just been released, ignore.
            }
        }

        public void startPreview() {
            if (!isPreviewOn && mCamera != null) {
                isPreviewOn = true;
                mCamera.startPreview();
            }
        }

        public void stopPreview() {
            if (isPreviewOn && mCamera != null) {
                isPreviewOn = false;
                mCamera.stopPreview();
            }
        }

        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
            /* get video data */
            if (yuvIplimage != null && recording) {
                // yuvIplimage.getByteBuffer().put(data);

                final int startY = 640 * (480 - 360) / 2;
                final int lenY = 640 * 360;
                yuvIplimage.getByteBuffer().put(data, startY, lenY);
                final int startVU = 640 * 480 + 320 * 2 * (240 - 180) / 2;
                final int lenVU = 320 * 180 * 2;
                yuvIplimage.getByteBuffer().put(data, startVU, lenVU);

                Log.v(LOG_TAG, "Writing Frame");
                try {
                    long t = 1000 * (System.currentTimeMillis() - startTime);
                    if (t > recorder.getTimestamp()) {
                        recorder.setTimestamp(t);
                    }
                    recorder.record(yuvIplimage);
                } catch (FFmpegFrameRecorder.Exception e) {
                    Log.v(LOG_TAG, e.getMessage());
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void onClick(View v) {
        if (!recording) {
            startRecording();
            Log.w(LOG_TAG, "Start Button Pushed");
            btnRecorderControl.setText("Stop");
        } else {
            // This will trigger the audio recording loop to stop and then set
            // isRecorderStart = false;
            stopRecording();
            Log.w(LOG_TAG, "Stop Button Pushed");
            btnRecorderControl.setText("Start");
        }
    }
}

【问题讨论】:

  • 您的设备是否支持 640x360 录制?请通过 Camera.Parameters params = mCamera.getParameters(); List<Size> sizeList = params.getSupportedVideoSizes(); 进行检查
  • 不是这样的。在以支持的分辨率录制后,我尝试使用 ffmpeg 转换我的视频,但它太慢并且需要很长时间才能完成。我不明白为什么它在iOS中如此之快。无论如何,我放弃了那个决议,现在我只使用一个受支持的决议。谢谢
  • 可能iOS相机本身就支持这个分辨率。
  • @Apoorv 如何使用FFmpeg 以纵向模式捕捉方形视频。就像 Instagram 一样。现在我不能把它拍成肖像。

标签: android video ffmpeg javacv


【解决方案1】:

您的相机很可能可以提供 640x480 的预览帧。解决方法是在录制之前剪辑此帧,如下所示:

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    /* get video data */
    if (yuvIplimage != null && recording) {
        ByteBuffer bb = yuvIplimage.getByteBuffer(); // resets the buffer
        final int startY = imageWidth*(imageHeight-finalImageHeight)/2;
        final int lenY = imageWidth*finalImageHeight;
        bb.put(data, startY, lenY);
        final int startVU = imageWidth*imageHeight + imageWidth*(imageHeight-finalImageHeight)/4;
        final int lenVU = imageWidth* finalImageHeight/2;
        bb.put(data, startVU, lenVU);

//      Log.v(LOG_TAG, "Writing Frame");
        try {
            long t = 1000 * (System.currentTimeMillis() - startTime);
            if (t > recorder.getTimestamp()) {
                recorder.setTimestamp(t);
             }
             recorder.record(yuvIplimage);
        } catch (FFmpegFrameRecorder.Exception e) {
             Log.e(LOG_TAG, "problem with recorder():", e);
        }
    }
}

预览帧具有半平面 YVU 格式:640x480 亮度 (Y) 字节,然后是 320x240 色度对(V 和 U)字节。我们首先将相关的 Y 复制到yuvIpImage,然后再复制相关的 VU 对。请注意,它既简单又快速,因为您想要的宽度与原生宽度相同。

您的摄像头和摄像头视图应初始化为 640x480,录像机应初始化为 640x360。请注意,只有在 imageWidth==finalImageWidth 时才能进行有效裁剪。

FIX 发生这种情况时 IplImage.getByteBuffer() 会重置缓冲区,因此解决方法是使用临时 bb 对象。

请注意,您可能希望用一个框架覆盖预览,该框架将“隐藏”您以这种方式裁剪的边距:我们的操作只会更改记录的帧,而不是 CameraView

【讨论】:

  • 感谢您的回复。但是,如果我改变 imageWidth = 320;图像高度 = 240;到 640 x 480,我得到一个带有绿框的视频。
  • 您的相机是否支持 640x480 预览?并请分享您的代码,它给出了绿色框架。
  • 您是否更改了initRecorder() 方法?您应该将yuvIplimagerecorder 都初始化为640x360
  • 我通过更改变量 imageWidth 和 imageHeight 中的值来更改分辨率。所以它改变了yuvIplimage和recorder的初始化。我尝试了很多事情都没有成功。我总是得到一个混合颜色的视频。
  • 您的代码中有两个 imageHeight 用途。这些必须解耦才能使用我的裁剪解决方案。
【解决方案2】:

@Fabio 看到您的代码来自这个Open Source Android Touch-To-Record library,我也使用过它。这是我在 CameraPreview 类中的 onPreviewFrame 方法的修改版本,用于在捕获的视频横向播放(应用被锁定为纵向)并具有绿色输出时对捕获的帧进行转置和调整大小。。 p>

我在 setCameraParams() 方法中定义了“yuvIplImage”,如下所示。

IplImage yuvIplImage = IplImage.create(mPreviewSize.height, mPreviewSize.width, opencv_core.IPL_DEPTH_8U, 2);

同时初始化你的 videoRecorder 对象如下,宽度为高度,反之亦然。

//call initVideoRecorder() method like this to initialize videoRecorder object of FFmpegFrameRecorder class.
initVideoRecorder(strVideoPath, mPreview.getPreviewSize().height, mPreview.getPreviewSize().width, recorderParameters);

//method implementation
public void initVideoRecorder(String videoPath, int width, int height, RecorderParameters recorderParameters)
{
    Log.e(TAG, "initVideoRecorder");

    videoRecorder = new FFmpegFrameRecorder(videoPath, width, height, 1);
    videoRecorder.setFormat(recorderParameters.getVideoOutputFormat());
    videoRecorder.setSampleRate(recorderParameters.getAudioSamplingRate());
    videoRecorder.setFrameRate(recorderParameters.getVideoFrameRate());
    videoRecorder.setVideoCodec(recorderParameters.getVideoCodec());
    videoRecorder.setVideoQuality(recorderParameters.getVideoQuality());
    videoRecorder.setAudioQuality(recorderParameters.getVideoQuality());
    videoRecorder.setAudioCodec(recorderParameters.getAudioCodec());
    videoRecorder.setVideoBitrate(1000000);
    videoRecorder.setAudioBitrate(64000);
}

这是我的 onPreviewFrame() 方法:

@Override
public void onPreviewFrame(byte[] data, Camera camera)
{

    long frameTimeStamp = 0L;

    if(FragmentCamera.mAudioTimestamp == 0L && FragmentCamera.firstTime > 0L)
    {
        frameTimeStamp = 1000L * (System.currentTimeMillis() - FragmentCamera.firstTime);
    }
    else if(FragmentCamera.mLastAudioTimestamp == FragmentCamera.mAudioTimestamp)
    {
        frameTimeStamp = FragmentCamera.mAudioTimestamp + FragmentCamera.frameTime;
    }
    else
    {
        long l2 = (System.nanoTime() - FragmentCamera.mAudioTimeRecorded) / 1000L;
        frameTimeStamp = l2 + FragmentCamera.mAudioTimestamp;
        FragmentCamera.mLastAudioTimestamp = FragmentCamera.mAudioTimestamp;
    }

    synchronized(FragmentCamera.mVideoRecordLock)
    {
        if(FragmentCamera.recording && FragmentCamera.rec && lastSavedframe != null && lastSavedframe.getFrameBytesData() != null && yuvIplImage != null)
        {
            FragmentCamera.mVideoTimestamp += FragmentCamera.frameTime;

            if(lastSavedframe.getTimeStamp() > FragmentCamera.mVideoTimestamp)
            {
                FragmentCamera.mVideoTimestamp = lastSavedframe.getTimeStamp();
            }

            try
            {
                yuvIplImage.getByteBuffer().put(lastSavedframe.getFrameBytesData());

                IplImage bgrImage = IplImage.create(mPreviewSize.width, mPreviewSize.height, opencv_core.IPL_DEPTH_8U, 4);// In my case, mPreviewSize.width = 1280 and mPreviewSize.height = 720
                IplImage transposed = IplImage.create(mPreviewSize.height, mPreviewSize.width, yuvIplImage.depth(), 4);
                IplImage squared = IplImage.create(mPreviewSize.height, mPreviewSize.height, yuvIplImage.depth(), 4);

                int[] _temp = new int[mPreviewSize.width * mPreviewSize.height];

                Util.YUV_NV21_TO_BGR(_temp, data, mPreviewSize.width,  mPreviewSize.height);

                bgrImage.getIntBuffer().put(_temp);

                opencv_core.cvTranspose(bgrImage, transposed);
                opencv_core.cvFlip(transposed, transposed, 1);

                opencv_core.cvSetImageROI(transposed, opencv_core.cvRect(0, 0, mPreviewSize.height, mPreviewSize.height));
                opencv_core.cvCopy(transposed, squared, null);
                opencv_core.cvResetImageROI(transposed);

                videoRecorder.setTimestamp(lastSavedframe.getTimeStamp());
                videoRecorder.record(squared);
            }
            catch(com.googlecode.javacv.FrameRecorder.Exception e)
            {
                e.printStackTrace();
            }
        }

        lastSavedframe = new SavedFrames(data, frameTimeStamp);
    }
}

这段代码使用了一个方法“YUV_NV21_TO_BGR”,我从这个link找到的

基本上这个方法是用来解决的,我称之为“Android上的绿魔问题”,和你的一样。我遇到了同样的问题,浪费了将近 3-4 天。在我刚刚对 YuvIplImage 进行转置时添加“YUV_NV21_TO_BGR”方法之前,更重要的是转置、翻转(有或没有调整大小)的组合,结果视频中有 绿色输出。这种“YUV_NV21_TO_BGR”方法挽救了这一天。感谢来自上述谷歌群组线程的@David Han。

【讨论】:

  • 我尝试了您的代码完美运行,但我不知道为什么我填充的帧很少但很慢,或者可能在我的输出视频文件(即 mp4)中丢帧。
【解决方案3】:

使用this 链接解决问题。问题在于图像的旋转。YUV 图像处理已完成。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多