安卓执法仪编码器之同步/异步模式

Posted John0551

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓执法仪编码器之同步/异步模式相关的知识,希望对你有一定的参考价值。

在安卓执法仪开发过程中,我们发现有些设备的编码器性能较低,后来发现MediaCodec库支持异步模式了, 切换到异步模式果然会提高一些编码帧率.

同步模式

MediaCodec 同步模式调用图如下:

创建,配置,启动AAC编码器:

    MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, spec.getSample(), spec.getChannel());
    format.setInteger(MediaFormat.KEY_BIT_RATE, 16000);
    format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
    format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1920);    // 1920 = 16k的采样,60毫秒长度.

    Timber.i("audio codec:mediaFormat = " + videoFormat);
    mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mediaCodec.start();

获取输入队列并塞入PCM数据:

    int inputBufferIndex = mCodec.dequeueInputBuffer(100);
    if (inputBufferIndex >= 0) 
        if (pcmBuf.capacity() != length) 
            pcmBuf = ByteBuffer.allocate(length * 2).order(ByteOrder.LITTLE_ENDIAN);
        
        pcmBuf.asShortBuffer().put(pcm, 0, length);
        pcmBuf.clear();
        ByteBuffer inputBuffer;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 
            inputBuffer = mCodec.getInputBuffer(inputBufferIndex);
         else 
            final ByteBuffer[] inputBuffers = mCodec.getInputBuffers();
            inputBuffer = inputBuffers[inputBufferIndex];
        
        inputBuffer.clear();
        inputBuffer.put(pcmBuf);
        mCodec.queueInputBuffer(inputBufferIndex, 0, length * 2, timestamp / 1000, 0);// , needKeyFrm
    

inputBufferIndex小于0表示没有可用的输出帧(可能编码缓冲已满).

获取输出的AAC音频帧:

    int aacOutputBufferIndex = aacCodec.dequeueOutputBuffer(mBufferInfo, 0);
    long timeSpend2 = SystemClock.elapsedRealtime() - begin;
    if (timeSpend2 > 200) 
        Timber.w("aac dequeueOutputBuffer spend too much time....%d ms", timeSpend2);
    
    if (aacOutputBufferIndex >= 0) 
        try 
            ByteBuffer outputBuffer;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 
                outputBuffer = aacCodec.getOutputBuffer(aacOutputBufferIndex);
             else 
                outputBuffer = audioOutputBuffers[aacOutputBufferIndex];//outputBuffer保存的就是H264数据了
            
            if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) // sps
                mBufferInfo.size = 0;
            
            if (mBufferInfo.size != 0) 
                outputBuffer.position(mBufferInfo.offset);
                outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                onAudioEncoded(outputBuffer, mBufferInfo);
            
         finally 
            aacCodec.releaseOutputBuffer(aacOutputBufferIndex, false);//释放资源
        
     else if (aacOutputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) 
        MediaFormat outputFormat = aacCodec.getOutputFormat();
        ByteBuffer sps = outputFormat.getByteBuffer("csd-0");
        int sps_size = sps.remaining();
        extra2 = new byte[sps_size];
        sps.get(extra2);

        if (recordReady) 
            startMediaMuxer();
        
    

aacOutputBufferIndex小于0表示还没编出一帧数据;或者没有待编码的输入帧了.

同步模式的输入输出相互影响,其中一方过慢,都会导致另一方无法取到有效Buffer,CPU无意义地消耗在轮训上面.效率较低

异步模式

工作模式如下:

异步模式下,我们需要先把PCM数据先存到缓冲里面,在编码器可用时再存入编码器.

相比同步模式,异步模式要设置一个回调,该调用发生在create之后,configure之前:


mediaCodec.setCallback(mAudioCallback, mAudioHandler);

其中mAudioCallback是回调对象,mAudioHandler是回调函数执行的线程的Handler.

回调函数如下:

private MediaCodec.Callback mAudioCallback = new MediaCodec.Callback() 
    @Override
    public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int id) 
        ByteBuffer inputBuffer = mediaCodec.getInputBuffer(id);
        inputBuffer.clear();
        Pair pair = mInputAudioQueue.poll();
        Timber.d("AudioAvailable.queue size=%d",mInputAudioQueue.size());
        long presentationTimeUs = 0;
        if (pair != null) 
            ByteBuffer dataSources = (ByteBuffer) pair.second;
            presentationTimeUs = (long) pair.first;
            inputBuffer.put(dataSources);
        
        mediaCodec.queueInputBuffer(id, 0, inputBuffer.position(), presentationTimeUs / 1000,  (mExit && pair == null)? MediaCodec.BUFFER_FLAG_END_OF_STREAM:0);
    

    @Override
    public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int id, @NonNull MediaCodec.BufferInfo bufferInfo) 
        try 
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) 
                // 等输出回调中遇到该标志位时,表示缓冲帧已消耗完毕.
                int flag = codecFlag.decrementAndGet();
                Timber.i("audio codec EOS,flag goto:%d.", flag);
                if (flag == 0) 
                   // 当音频,视频的缓冲帧都消耗完毕时,停止录像.
                   Timber.i("收到音频BUFFER_FLAG_END_OF_STREAM标志,即将停止录像.");
                    PublishSubject x = recordSubject;
                    if (x != null) x.onComplete();
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) 
                        quitSafely();
                     else 
                        quit();
                    
                
                return;
            
//                    Timber.i("MediaCodec onOutputBufferAvailable:"+bufferInfo.size+","+id);
            //走到这里的时候,说明数据已经编码成AAC格式了
            ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(id);
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) // sps
                bufferInfo.size = 0;
            
            if (bufferInfo.size != 0) 
                outputBuffer.position(bufferInfo.offset);
                outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                onAudioEncoded(outputBuffer, bufferInfo);
            
         finally 
            mediaCodec.releaseOutputBuffer(id, false);//释放资源
//                    Timber.i("MediaCodec releaseOutputBuffer:"+id);
        
    

    @Override
    public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) 
        Timber.e(e);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) 
            quitSafely();
         else quit();
    

    @Override
    public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat outputFormat) 
        int flag = codecFlag.incrementAndGet();
        Timber.i("Audio MediaCodec onOutputFormatChanged.flag:%d,format:%s",flag, outputFormat);
        //特别注意此处的调用
        ByteBuffer sps = outputFormat.getByteBuffer("csd-0");
        int sps_size = sps.remaining();
        extra2 = new byte[sps_size];
        sps.get(extra2);

        if (recordReady) 
            startMediaMuxer();
        
    
;

其中,onInputBufferAvailable表示编码器空闲了,输入端可输入数据了.因此我们从缓冲区取出一个音频PCM帧存入编码器;此外onOutputBufferAvailable表示编码器编码出一个音频帧了.我们从编码器里面取出帧数据进行muxing.onError表示编码器异常,一般不会出现,出错后停止编码器即可.onOutputFormatChanged表示编码器内部状态更改了,在这里我们取出MetaData.

经过测试异步模式的优势基本符合预期,其CPU消耗低,且效率要高于同步模式.

异步模式录像停止时,我们先把缓存在编码器内部的帧都消耗掉后,再优雅停止,否则缓存在编码器中的媒体帧被丢失,录像会不完整.停止录像正确操作应该这样:

  1. 设置EOF标志位,不再输入.
  2. 等输出回调中遇到该标志位时,表示缓冲帧已消耗完毕.停止编码器
  3. 等音频,视频编码器都停止时,停止录像.

具体见上面onOutputBufferAvailable里的注释.

我们得到编码器输出帧后,下面要进行的就是写入录像文件内.这个过程被称为muxing.

参考资料

  • vlog清新互联执法记录仪介绍设计篇
  • 清新互联执法记录仪功能篇
  • MP4文件格式 https://developer.apple.com/standards/qtff-2001.pdf
  • FFMPEG

以上是关于安卓执法仪编码器之同步/异步模式的主要内容,如果未能解决你的问题,请参考以下文章

安卓执法仪编码器之同步/异步模式

安卓执法仪录像之进程间共享内存

安卓执法仪录像之进程间共享内存

安卓执法仪录像之进程间共享内存

执法记录仪录像模块的设计

执法记录仪录像模块的设计