Android-APP调用mediacodec录像详解

云景焕
2023-12-01

前言:

      APP录制视频一般调用mediarecorder或者mediacodec接口。mediarecorder的优势就是简单。几行代码就可以搞定。相比于mediarecorder,mediaocdec的优势就是自由度更高,app可以加一些自己的前处理、后处理等算法。所以一般app都会选择mediacodec接口。

  本文将介绍app调用mediacodec录像的调用过程:

代码实现:

实现方式一:  异步方式调用mediacodec,输入为非surface:

private MediaCodec mEncoder;
public init() {
    mEncoder = MediaCodec.createEncoderByType(mime);
    mEncoder.setCallback(new CustomCallback(), handler); // app设置了callback就表示用异步的方式,否则就是同步方式
    mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // 此处format就是码率,帧率,尺寸等信息
}
public void start() throws Exception {
    mEncoder.start();
}
public void stop() {
    mEncoder.stop();
}

主要看callback需要怎么实现

private class CustomCallback extends MediaCodec.Callback {
    @Override
    public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
        // codec有input buffer可用
        ByteBuffer inputBuffer = mEncoder.getInputBuffer(index); // 取出可用的buffer
        inputBuffer.clear(); // 清空buffer
        inputBuffer.put(mEncodeBufferHolder.data); // 把需要处理的数据放进buffer
         mEncoder.queueInputBuffer(index, 0,  // 把buffer传给codec
             mEncodeBufferHolder.data.length, mEncodeBufferHolder.presentationTimeUs, mEncodeBufferHolder.flag);
    }

    @Override
    public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
        // codec有output buffer可用
        ByteBuffer encodedData = mEncoder.getOutputBuffer(index); // 取出可用的buffer
        mediaMuxer.writeSampleData(mTrackIndex, encodedData, info); // 把buffer写入文件
        mEncoder.releaseOutputBuffer(index, false); // 把buffer传给codec
    }
    
    @Override
    public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
        // 收到codec有error的消息,此处会处理error
    }
    
    @Override
    public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
        // outputbuffer 格式有变化
    }
}

实现方式二:   异步方式调用mediacodec,输入为surface:

private MediaCodec mEncoder;
private Surface inputSurface;
private EGLRender mEglRender;
private VirtualDisplay mVirtualDisplay;
public init() {
    mEncoder = MediaCodec.createEncoderByType(mime);
    mEncoder.setCallback(new CustomCallback(), handler); // app设置了callback就表示用异步的方式,否则就是同步方式
    mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // 此处format就是码率,帧率,尺寸等信息
    inputSurface = videoEncoder.createInputSurface(); // 从底层获取surface
    
    // 把surface传给render
    mEglRender = new EGLRender(mVideoEncoder.createInputSurface(), mWidth, mHeight, mFrameRate, offsetTime); 
}
public void start() throws Exception {
    mEglRender.setStartTimeNs(getSystemTimeNanosWithMode());
    mEglRender.start();
    mEncoder.start();
}
public void stop() {
    mEglRender.stopRender();
    mEncoder.stop();
}
private void prepareVirtualDisplay() {
    // creates a VirtualDisplay to capture the contents of the screen
    Surface surface = mEglRender.getDecodeSurface();
    int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
    mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG + "-display",
             mWidth, mHeight, VIRTUAL_DISPLAY_DPI, flags,
             surface, null, null);
    LogUtil.i(TAG, "create virtual display success, virtual display = " + mVirtualDisplay);
}

看看callback的具体实现:

private class CustomCallback extends MediaCodec.Callback {
    @Override
    public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
        // codec有output buffer可用
        ByteBuffer encodedData = mEncoder.getOutputBuffer(index); // 取出可用的buffer
        mediaMuxer.writeSampleData(mTrackIndex, encodedData, info); // 把buffer写入文件
        mEncoder.releaseOutputBuffer(index, false); // 把buffer传给codec
    }
    
    @Override
    public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
        // 收到codec有error的消息,此处会处理error
    }
    
    @Override
    public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
        // outputbuffer 格式有变化
    }
}

此处由于codec的输入为surface不需要app调用queue/dequeue去实现input buffer的回调,所以此处call back不会有onInputBufferAvailable。

tips:

上面的surface是app通过mediacodec createInputSurface获取,然后app把surface传给eglrender还有一种方式是app自己获取surface之后调用mediacodec的setInputSurface把surface传下去。代码简介如下:

encoder = MediaCodec.createByCodecName(codec);

encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

inputSurface = new InputSurface(MediaCodec.createPersistentInputSurface());

encoder.setInputSurface(inputSurface);

encoder.start();

emmm........此处其实也是从mediacodec先获取了surface在设置下去。。。。

实现方式三:   同步方式调用mediacodec,输入为surface:

private MediaCodec mVideoEncoder;
private VideoHandleThread mVideoThread;
private volatile AtomicBoolean mVideoStart = new AtomicBoolean(false);

public init() {
    mVideoEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
    mVideoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // 此处format就是码率,帧率,尺寸等信息
    mEglRender = new EGLRender(mVideoEncoder.createInputSurface(), mWidth, mHeight, mFrameRate, offsetTime);
    mVideoThread = new VideoHandleThread();
}
public void start() throws Exception {
    mVideoEncoder.start();
    mVideoThread.start();
}
public void stop() {
    mVideoEncoder.stop();
}

同步调用的时候会创建一个VideoHandleThread线程专门处理video的数据:

private class VideoHandleThread extends Thread {
    @Override
    public void run() {
        while (mVideoStart.get()) {
            int index = mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, TIMEOUT_US);
            if (index >= 0 && mMuxerStarted) {
                encodeToVideoTrack(index);
                mVideoEncoder.releaseOutputBuffer(index, false);
            } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
                 Thread.sleep(10);
            }     
        }
    }
}
private void encodeToVideoTrack(int index) {
    ByteBuffer encodedData = mVideoEncoder.getOutputBuffer(index);
    mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mVideoBufferInfo);
}

同步处理的时候没有callback,一个while循环就一直在处理,如果获取不到buffer就需要app自己做sleep操作。总之,同步的时候,mediacodec不会告知app啥时候去取buffer,app需要自己实现取出buffer的等待逻辑。

tips:

无论同步还是异步处理,针对video部分buffer的处理一定要开辟一个子线程处理。如果在app主线程处理,很容易出现死锁、block主线程、ANR等问题。

 

 类似资料: