Android中使用MediaCodec视频编码异步实现

Android中使用MediaCodec进行视频编解码异步实现

简单的介绍一下MediaCodec:本文主要讲述的是博主自己在用MediaCodec进行编解码过程中分别用同步和异步两种方式实现了硬编解码的过程,因为之前自己在用异步实现的过程中经常会出现解码黑屏的情况,百度和博客上也没有相关的实例,找到的只有一个示例代码,并没有提供什么帮助。所以写了代码去实现,Mediacodec的简单使用我先介绍一下,并且把Mediacodec的异步实现的流程介绍一下。

这是谷歌官网给出的Mediacodec的编解码数据流向,Mediacodec类可用于访问底层的媒体编解码器,即编码器/解码器组件。这是Android底层的多媒体支持基础设施的一部分(通常与MediaExtractor,MediaSync,mediamuxer,mediacrypto,mediadrm)。
从广义上讲,一个编解码器处理输入数据来生成输出数据。它处理异步数据和使用一组输入和输出缓冲器。在一个简单的级别上,您请求(或接收)一个空的输入缓冲区,用数据填充它并将其发送到编解码器进行处理。该编解码器使用的数据并将其转换为它的一个空的输出缓冲器。最后,你请求(或接收)一个填充的输出缓冲区,消耗它的内容并将其释放回编解码器。

Mediacodec的基本使用介绍

  • MedicCodec硬编码的创建
      MediaCodec mediaCodec = MediaCodec.createDecoderByType("video/avc");
      //实例化的首选解码器支持的MIME类型的输入数据,最好使用finddecoderforformat(mediaformat)和createbycodecname(字符串)来保证解码器能够处理一个给定的格式.
      MediaFormat mediaFormat = android.media.MediaFormat.createVideoFormat("video/avc", 1280, 720);
      mediaFormat.setInteger(android.media.MediaFormat.KEY_BIT_RATE, 512 * 2 * 1000);
      //设置码率
      mediaFormat.setInteger(android.media.MediaFormat.KEY_FRAME_RATE, 20);
      //设置帧率
      mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
      //关键帧间隔时间 单位s
      mediaCodec.configure(mediaFormat, new Surface(mSurfaceView.getSurfaceTexture()), null, 0);
      //配置mediaformat并且传入需要显示解码画面的SurfaceView
      mediaCodec.start();
      // start();
  • MediaCodec编码同步的官方实现
 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is equivalent to mOutputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   }

   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; // option B
   }

   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();
  • MediaCodec编码异步的官方实现
 MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

介绍完官方的使用规范,下来我来讲讲自己的通过两个TextureView来异步实现硬编码的过程。

两个TextureView,一个通过系统Camera来获取摄像头的数据,然后通过同步和异步两种编解码方式,通过另一个TextureView来将画面呈现出来,这里只介绍异步编码的过程,解码原理相同。


xml配置

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
>
    <TextView
            android:textSize="18sp"
            android:layout_marginTop="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="预览图像:" />
    <TextureView android:id="@+id/v1"
            android:layout_marginTop="5dp"
            android:layout_width="match_parent"
            android:layout_height="200dp"/>
    <TextView
            android:textSize="18sp"
            android:layout_marginTop="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="解码后渲染图像:" />
    <TextureView android:id="@+id/v2"
            android:layout_marginTop="5dp"
            android:layout_width="match_parent"
            android:layout_height="200dp"/>
</LinearLayout>

把Camera获取到的数据呈现在TextureView上

  • MedicCodec的创建
      camera = Camera.open();
      camera.setPreviewTexture(surfaceView1);
      //显示camera的画面 surfaceView1 是xml声明的第一个TextureView 需要在前面绑定
      Camera.Parameters cp= cam.getParameters();  
      cp.setFlashMode("off"); 
      // 无闪光灯  
      cp.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
      //设置聚焦模式
      cp.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);  
      cp.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);   
      cp.setPreviewFormat(ImageFormat.YV12);       
      cp.setPictureSize(camWidth, camHeight);  
      cp.setPreviewSize(camWidth, camHeight); 
      camera.setParameters(cp);
      camera.startPreview();
  • 异步的方式进行硬编码
  asynccodec = MediaCodec.createEncoderByType("video/avc");
              MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc",mWidth,mHeight);
              mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
              mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
              mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
              mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
              asynccodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
              asynccodec.setCallback(new MediaCodec.Callback() {
                 @Override
                 public void onInputBufferAvailable(MediaCodec mc, int index) {
                     ByteBuffer inputBuffer = asynccodec.getInputBuffer(index);
                     byte[] tmpBuffer = camera.getInstance().getDequeueEncode();
                     // tmpBuffer 主要用户获取camera中获取的到的一帧画面的数据
                     // 博主通过一个encodeBuffer缓存队列进行数据的存储,这里是从缓存里面出一帧的数据
                     //getDequeueEncode()函数是当缓存队列有的时候,会进行数据的出队列
                     inputBuffer.clear();
                     int length = 0;
                     if(tmpBuffer != null) {
                         inputBuffer.put(tmpBuffer);
                         //将获取到的数据放入通过index获取的ByteBuffer中
                         length = tmpBuffer.length;
                         encodedFrameCount++;
                    }
                    asynccodec.queueInputBuffer(index, 0, length, 0, 0);
                    //通过index获取ByteBuffer然后放入编码器中
                }
                @Override
                public void onOutputBufferAvailable(MediaCodec codec, int outputBufferId, MediaCodec.BufferInfo info) {
                //异步实现的关键!!!!
                //onOutputBufferAvailable这个回调函数网上找不到一些实现的例子,主要的问题就出现在TextureView在呈现画面的时候出现掉帧和模糊的的情况。
                //通过异步实现,当mediacodec有数据的时候,就会自动调用这个函数,把编码后的数据进行出队列操作
                    ByteBuffer outputBuffer = asynccodec.getOutputBuffer(outputBufferId);
                    MediaFormat bufferFormat = asynccodec.getOutputFormat(outputBufferId);
                    if (outputBuffer != null && info.size > 0) {
                        byte[] tmpBuffer = new byte[info.size];
                        tmpBuffer = new byte[outputBuffer.remaining()];
                 //ByteBuffer 转化为byte[]数组
                        outputBuffer.get(tmpBuffer);
                        mFrameBuffer.enqueue(tmpBuffer,tmpBuffer.length);
                 //将异步编码后的数据存放到了自己定义的一个缓存队列中,后续TextureView进行显示也是通过从这个缓存队列中获取相应数据然后再进行解码然后呈现出来的
                    }
                    asynccodec.releaseOutputBuffer(outputBufferId, true);
                 //释放当前outputBufferId上的数据
                }

                @Override
                public void onError(MediaCodec codec, MediaCodec.CodecException e) {
                //错误回调
                    Log.d("error", e.getDiagnosticInfo());
                }
                @Override
                public void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat format) {

                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        asynccodec.start();
  • 缓存队列FrameBuffer的介绍

    缓存队列的实现是用的java的数组,自己封装了出队列和进队列的方法。同时加入了ReentrantLock机制提供保证。还有对摄像头数据获取的前几个关键帧进行判断的操作,这里就不贴代码了。 其实不使用队列的话,数据量小的情况下也是可以流畅运行的。

解码的过程跟编码的过程很类似,也是通过MediaCodec来实现的,大家可以试试用同步的方式来把获取的数据来进行解码。

效率方面的话本地预览的情况下,同步和异步的区别不是很大,都可以基本保证画面流畅的预览。

但是在比如网络视频通话开发的过程中,不考虑丢帧而考虑画面流程性的条件下,异步的方式效率是远超于同步的效率。网络测试方面的代码后续有时间回再贴上来的。

Thanks.

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
好的,以下是一个使用 MediaCodec 进行视频编码的示例代码,你可以参考一下: ```java import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.MediaMuxer; import android.os.Environment; import android.util.Log; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; public class VideoEncoder { private static final String TAG = "VideoEncoder"; // 编码器 private MediaCodec mEncoder; // 复用器 private MediaMuxer mMuxer; // 视频轨道 private int mTrackIndex; // 是否已经开始编码 private boolean mIsStarted; // 视频宽度 private int mWidth; // 视频高度 private int mHeight; // 视频帧率 private int mFrameRate; // 视频码率 private int mBitRate; // 编码器输出的 buffer 信息 private MediaCodec.BufferInfo mBufferInfo; // 用于存储编码后的数据 private ByteBuffer mOutputBuffer; public VideoEncoder(int width, int height, int frameRate, int bitRate) { mWidth = width; mHeight = height; mFrameRate = frameRate; mBitRate = bitRate; } /** * 开始编码 */ public void start() { try { // 初始化编码器 mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, mFrameRate); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // 创建一个文件用于存储编码后的数据 String outputPath = new File(Environment.getExternalStorageDirectory(), "output.mp4").getAbsolutePath(); mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); // 开始编码 mEncoder.start(); mIsStarted = true; } catch (IOException e) { e.printStackTrace(); } } /** * 编码一帧数据 * * @param input 输入的数据 * @param pts 每一帧的时间戳 */ public void encodeFrame(byte[] input, long pts) { if (!mIsStarted) { Log.e(TAG, "Encoder is not started."); return; } // 取出可用的输入 buffer int inputBufferIndex = mEncoder.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex); inputBuffer.clear(); inputBuffer.put(input); // 将输入的数据喂给编码器 mEncoder.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0); } // 取出可用的输出 buffer int outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 0); while (outputBufferIndex >= 0) { mOutputBuffer = mEncoder.getOutputBuffer(outputBufferIndex); mOutputBuffer.position(mBufferInfo.offset); mOutputBuffer.limit(mBufferInfo.offset + mBufferInfo.size); // 写入到复用器 mMuxer.writeSampleData(mTrackIndex, mOutputBuffer, mBufferInfo); // 释放输出 buffer mEncoder.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 0); } } /** * 停止编码 */ public void stop() { if (!mIsStarted) { Log.e(TAG, "Encoder is not started."); return; } mEncoder.stop(); mEncoder.release(); mMuxer.stop(); mMuxer.release(); mIsStarted = false; } /** * 初始化视频轨道 */ public void initTrack() { if (!mIsStarted) { Log.e(TAG, "Encoder is not started."); return; } mTrackIndex = mMuxer.addTrack(mEncoder.getOutputFormat()); mMuxer.start(); mBufferInfo = new MediaCodec.BufferInfo(); } } ``` 这段代码,我们使用 MediaCodec 进行视频编码,并将编码后的数据通过 MediaMuxer 写入到文件。 需要注意的是,编码器的输入数据必须是 YUV 格式的数据,而不是一般的 RGB 格式,因此你需要将原始数据转化为 YUV 格式,然后再进行编码。除此之外,还需要注意编码器的配置参数,例如视频宽度、高度、帧率、码率等等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值