Android MediaCodec解析

这篇具有很好参考价值的文章主要介绍了Android MediaCodec解析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Android MediaCodec解析

1 引言

MediaCodec是Android平台提供的一个底层的音视频编解码框架,它是安卓底层多媒体基础框架的重要组成部分。它经常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, AudioTrack一起使用。解码的作用,就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。反之,编码的作用,就是将非压缩的视频/音频原始数据转为视频/音频压缩编码数据。

1.1编写目的

本文档编制旨在说明和总结Android 音视频编解码相关知识,供开发人员查阅与参考。

1.2术语定义及说明

编码:编码就是将原始音频数据也就是PCM压缩的一个过程;或者是将原始的视频数据RGB或YUV压缩的一个过程。

解码:解码就是编码一个逆过程,比如将编码后的数据AAC解码成PCM给播放器播放;或者将编码后的H264数据解码成YUV或RGB给播放器渲染的过程

编解码又分为硬件编解码和软件编解码:

软编软解码:使用CPU进行编码,一般是执行代码运行算法指令编码。
硬编硬解码:使用非CPU进行编码,如显卡GPU、专用的DSP、FPGA、ASIC芯片等,一般是算法已经固化在芯片中。

一般来说,软编码会使CPU负载更重,所以性能相对比硬编要低,不过兼容性一般比硬编好,低码率下质量通常比硬编码要好一点。而硬编码一般性能比软编码好一些,但是兼容性就差一些,低码率下通常质量低于软编码的。

视频帧:视频的每一张静态图片就叫一帧

视频帧又分为I帧、B帧和P帧:

I帧:帧内编码帧,大多数情况下I帧就是关键帧,就是一个完整帧,无需任何辅助就能独立完整显示的画面。

B帧:帧是双向预测帧。参考前后图像帧编码生成。需要前面的 I/P 帧或者后面的 P 帧来协助形成一个画面。

P帧:前向预测编码帧。是一个非完整帧,通过参考前面的I帧或P帧生成画面。

所以 I 帧是很关键的存在,压缩 I 帧就可以很容易压制掉空间的大小,而压缩P帧和B帧可以压缩掉时间上的冗余信息 。

GOP:group of picture,就是两个 I 帧之间的距离,一般 GOP 设置得越大,画面的效果就会越好,到那时需要解码的时间就会越长。所以如果码率固定而 GOP 值越大,P/B帧 数量会越多,画面质量就会越高
mediacodec,android,ffmpeg

所以在视频 seek 的时候,I 帧很关键,如果视频 seek 之后发生往前的跳动,有可能就是你要seek到的位置没用关键帧,这就需要处理了。好像Android自带的播放器就会有这个问题,有时候无法精确地seek到某个位置。

封装格式:封装格式业界也有人称音视频容器,比如我们经常看到的视频后缀名:mp4、rmvb、avi、mkv、mov等就是音视频的容器,它们将音频和视频甚至是字幕一起打包进去,封装成一个文件。

2.MediaCodec工作流程

mediacodec,android,ffmpeg

左边是输入端,右边是输出端。其中有输入和输出端各有若干个buffer,输入端不断拿到一个空buffer,装上数据,再传入MediaCodec直到所有数据输入为止。输出端不断从MediaCodec获取到buffer,每次得到处理好的数据后,再将buffer交还给MediaCodec。

mediacodec接受三种数据格式:压缩数据、原始音频数据和原始视频数据。压缩数据一般是解码端的输入和编码端的输出,反之原始音频数据和原始视频数据一般是编码的输入和解码端的输出。

3.MediaCodec工作生命周期

mediacodec,android,ffmpeg

MediaCodec就是一个状态机,在工作期间会经历多个状态阶段。具体来说是总共有三个大状态:Stopped, Executing ,Released,其中Stopped包含Uninitialized, Configured and Error三个小状态,Executing包含Flushed, Running and End-of-Stream三个小状态。

当MediaCodec对象实例刚创建好的时候,处于Stopped状态中的Uninitialized状态,此时需要调用configure方法,就能进入Configured状态,一个start方法的调用,此时进入Executing状态了,目前暂时处于Flushed状态,dequeueInputBuffer方法的调用返回值为bufferIndex。这也能看出api设计不人性化的地方,不是直接返回对应buffer,还要使用bufferIndex再获取一次buffer。再通过queueInputBuffer正式进入Running状态。MediaCodec工作阶段大部分时间都处于Running状态中,在Running状态不断的由input端queueInputBuffer,output端dequeueOutputBuffer,形成一个循环,直到input端加上BUFFER_FLAG_END_OF_STREAM标签,MediaCodec拿到此状态后不再接受任何新的数据输入,即进入End-of-Stream状态。调用stop此时又回到了Stopped状态中的Uninitialized状态。调用release方法来释放所有的资源进入Released状态。

中间过程可能会出现一些意外,就会进入Stopped中的Error状态,这时候有2个选择,一个是直接关门(release)进入Released状态,一个从Stopped状态中的Uninitialized状态重新开始。

4.MediaCodec代码实例

前面已经叙述了MediaCodec工作流程和工作周期状态机,下面从代码角度详细解析MediaCodec。

4.1解复用代码

复用,也可以叫做封装,即将已经压缩编码的视频数据和音频数据按照一定的格式打包到一起,比如我们都很熟悉的MP4,MKV,RMVB,TS,FLV,AVI,就是复用格式。比如FLV格式的数据,是由H.264编码的视频码流和AAC编码的音频码流打包一起。

通过MediaExtractor来获取视频的宽高:

//解复用
MediaExtractor extractor = null;
try {
    extractor = new MediaExtractor();
    //传入视频文件的路径
    extractor.setDataSource(sourceFile.toString());
    int trackIndex = selectTrack(extractor);
    if (trackIndex < 0) {
        throw new RuntimeException("No video track found in " + mSourceFile);
    }
    //选中得到的轨道(视频轨道),即后面都是对此轨道的处理
    extractor.selectTrack(trackIndex);
    //通过该轨道的MediaFormat得到对视频对应的宽高
    MediaFormat format = extractor.getTrackFormat(trackIndex);

    Log.d(TAG, "extractor.getTrackFormat format" + format);
    //视频对应的宽高
    mVideoWidth = format.getInteger(MediaFormat.KEY_WIDTH);
    mVideoHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
    if (VERBOSE) {
        Log.d(TAG, "Video size is " + mVideoWidth + "x" + mVideoHeight);
    }
} finally {
    if (extractor != null) {
        extractor.release();
    }
}

通过获取到的mime类型来创建一个MediaCodec解码器:

MediaFormat format = extractor.getTrackFormat(trackIndex);
Log.d(TAG, "EgetTrackFormat format:" + format);

// Create a MediaCodec decoder, and configure it with the MediaFormat from the
// extractor.  It's very important to use the format from the extractor because
// it contains a copy of the CSD-0/CSD-1 codec-specific data chunks.
String mime = format.getString(MediaFormat.KEY_MIME);
Log.d(TAG, "createDecoderByType mime:" + mime);
//通过视频mime类型初始化解码器
MediaCodec decoder = MediaCodec.createDecoderByType(mime);

此时MediaCodec处于Stopped状态中的Uninitialized状态,接下来开始启动MediaCodec

 //配置解码器,指定MediaFormat以及视频输出的Surface,解码器进入configure状态
  decoder.configure(format, mOutputSurface, null, 0);
  //启动解码器,开始进入Executing状态
  // Immediately after start() the codec is in the Flushed sub-state, where it holds all the buffers
  decoder.start();
  //具体的解码流程
  doExtract(extractor, trackIndex, decoder, mFrameCallback);

此时MediaCodec已经启动,此时已经进入input端和output端的大循环阶段

/**
 * 循环工作。直到视频用完或被告知停止。
 */
private void doExtract(MediaExtractor extractor, int trackIndex, MediaCodec decoder,
                       FrameCallback frameCallback) {

    //获取解码输出数据的超时时间
    final int TIMEOUT_USEC = 0;
    //输入ByteBuffer数组(较高版本的MediaCodec已经用getInputBuffer取代了,可直接获取buffer)
    ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
    //记录传入了第几块数据
    int inputChunk = 0;
    //用于log每帧解码时间
    long firstInputTimeNsec = -1;

    boolean outputDone = false;
    boolean inputDone = false;
    while (!outputDone) {
        if (VERBOSE) Log.d(TAG, "loop");
        if (mIsStopRequested) {
            Log.d(TAG, "Stop requested");
            return;
        }

        // 将更多数据馈送到解码器.
        if (!inputDone) {
            //拿到可用的ByteBuffer的index
            int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
            if (inputBufIndex >= 0) {
                if (firstInputTimeNsec == -1) {
                    firstInputTimeNsec = System.nanoTime();
                }
                //根据index得到对应的输入ByteBuffer
                ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
                Log.d(TAG, "decoderInputBuffers inputBuf:" + inputBuf + ",inputBufIndex:" + inputBufIndex);

                //从媒体文件中读取的一个sample数据大小
                int chunkSize = extractor.readSampleData(inputBuf, 0);
                if (chunkSize < 0) {
                    //文件读到末尾,设置标志位,发送一个空帧,给后面解码知道具体结束位置

                    decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    Log.d(TAG, "queueInputBuffer");

                    inputDone = true;
                    if (VERBOSE) Log.d(TAG, "sent input EOS");
                } else {
                    if (extractor.getSampleTrackIndex() != trackIndex) {
                        Log.w(TAG, "WEIRD: got sample from track " +
                                extractor.getSampleTrackIndex() + ", expected " + trackIndex);
                    }
                    //得到当前数据的播放时间点
                    long presentationTimeUs = extractor.getSampleTime();
                    //把inputBufIndex对应的数据传入MediaCodec
                    decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
                            presentationTimeUs, 0 /*flags*/);
                    Log.d(TAG, "queueInputBuffer inputBufIndex:" + inputBufIndex);

                    if (VERBOSE) {
                        Log.d(TAG, "submitted frame " + inputChunk + " to dec, size=" +
                                chunkSize);
                    }
                    //记录传入了第几块数据
                    inputChunk++;
                    //extractor读取游标往前挪动
                    extractor.advance();
                }
            } else {
                if (VERBOSE) Log.d(TAG, "input buffer not available");
            }
        }

        if (!outputDone) {
            //如果解码成功,则得到解码出来的数据的buffer在输出buffer中的index。并将解码得到的buffer的相关信息放在mBufferInfo中。
            // 如果不成功,则得到的是解码的一些状态
            int outputBufferIndex = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            Log.d(TAG, "dequeueOutputBuffer decoderBufferIndex:" + outputBufferIndex + ",mBufferInfo:" + mBufferInfo);
            if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                if (VERBOSE) Log.d(TAG, "no output from decoder available");
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                if (VERBOSE) Log.d(TAG, "decoder output buffers changed");
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                MediaFormat newFormat = decoder.getOutputFormat();
                if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
            } else if (outputBufferIndex < 0) {
                throw new RuntimeException(
                        "unexpected result from decoder.dequeueOutputBuffer: " +
                                outputBufferIndex);
            } else { // decoderStatus >= 0
                if (firstInputTimeNsec != 0) {
                    // Log the delay from the first buffer of input to the first buffer
                    // of output.
                    long nowNsec = System.nanoTime();
                    Log.d(TAG, "startup lag " + ((nowNsec - firstInputTimeNsec) / 1000000.0) + " ms");
                    firstInputTimeNsec = 0;
                }
                boolean doLoop = false;
                if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + outputBufferIndex +
                        " (output mBufferInfo size=" + mBufferInfo.size + ")");

                //判断是否到了文件结束,上面设置MediaCodec.BUFFER_FLAG_END_OF_STREAM标志位在这里判断
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (VERBOSE) Log.d(TAG, "output EOS");
                    if (mLoop) {
                        doLoop = true;
                    } else {
                        outputDone = true;
                    }
                }

                //如果解码得到的buffer大小大于0,则需要渲染
                boolean doRender = (mBufferInfo.size != 0);

                if (doRender && frameCallback != null) {
                    //渲染前的回调,这里具体实现是通过一定时长的休眠来尽量确保稳定的帧率
                    frameCallback.preRender(mBufferInfo.presentationTimeUs);
                }
                //得到输出Buffer数组,较高版本已经被getOutputBuffer代替

                ByteBuffer[] decoderOutputBuffers = decoder.getOutputBuffers();
                Log.d(TAG, "ecoderOutputBuffers.length:" + decoderOutputBuffers.length);

             
                //将输出buffer数组的第outputBufferIndex个buffer绘制到surface。doRender为true绘制到配置的surface
                decoder.releaseOutputBuffer(outputBufferIndex, doRender);

                if (doRender && frameCallback != null) {
                    //渲染后的回调
                    frameCallback.postRender();
                }

                if (doLoop) {
                    Log.d(TAG, "Reached EOS, looping");
                    //需要循环的话,重置extractor的游标到初始位置。
                    extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
                    inputDone = false;
                    //重置decoder到Flushed状态,不然就没法开始新一轮播放
                    decoder.flush();    // reset decoder state
                    frameCallback.loopReset();
                }
            }
        }
    }
}

4.2方法解释

  1. 询问Mediacodec当前有没有可以input的Buffer可以使用:
int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);

TIMEOUT_USEC为等待超时时间。当返回的inputBufIndex大于等于0,则说明当前有可用的Buffer,此时inputBufIndex表示可用Buffer在Mediacodec中的序号。如果等待了TIMEOUT_USEC时间还没找到可用的Buffer,则返回inputBufIndex小于0,等下次循环再来取Buffer。

  1. 每次从MediaExtractor中的readSampleData方法读出视频一段数据放在ByteBuffer中,然后通过Mediacodec的queueInputBuffer将ByteBuffer传给Mediacodec内部处理。

    //从媒体文件中读取的一个sample数据大小到inputBuf中
    int chunkSize = extractor.readSampleData(inputBuf, 0);
    

    readSampleData方法是读取一帧的数据。返回值为读取到数据大小,所以如果返回值大于0,则说明是有读取到数据的,则将数据传入MediaCodec中:

//得到当前数据的播放时间点
long presentationTimeUs = extractor.getSampleTime();
//把inputBufIndex对应的数据传入MediaCodec
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
        presentationTimeUs, 0 /*flags*/);

如果readSampleData方法返回值,即读到的数据大小为负数,则说明已经读到视频文件尾部了,则还是调用queueInputBuffer方法,但是需要特殊处理:

decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
        MediaCodec.BUFFER_FLAG_END_OF_STREAM);

发送一个空帧,标志位传BUFFER_FLAG_END_OF_STREAM,告诉MediaCodec,已经到文件尾部了,这个文件没有剩下需要传的数据了。

input端的代码就到这,然后立刻到ouptut端去尝试获取一下output的buffer。

int outputBufferIndex = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);

如果不成功,则得到的是解码的一些状态,以下几种常见 的状态:

1.MediaCodec.INFO_TRY_AGAIN_LATER:表示等了TIMEOUT_USEC时间长,也暂时还没有解码出成功的数据。一般来说,一个是等待时间还不够,另一个就是输入端是B帧,需要后面一帧P帧来作为参考帧才可以解码。

2.MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:输出Buffer数组已经过时,需要及时更换,由于较新版本已经用getOutputBuffer获取输出Buffer了,所以该标志位也过时了。

3.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:输出数据的MediaFormat发生了变化。

如果解码成功,则得到解码出来的数据的buffer在输出buffer中的index。并将解码得到的buffer的相关信息放在mBufferInfo中。然后执行非常关键的一段代码:

decoder.releaseOutputBuffer(outputBufferIndex, doRender);

将输出buffer数组的第outputBufferIndex个buffer绘制到surface(还记得configure方法传了的Surface对象么)。doRender为true,绘制到配置的surface。可以理解这行代码就类似Android中Canvas的draw方法,调用就绘制一帧,并将Buffer回收。文章来源地址https://www.toymoban.com/news/detail-617332.html

到了这里,关于Android MediaCodec解析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android MediaCodec 简明教程(五):使用 MediaCodec 编码 ByteBuffer 数据,并保存为 MP4 文件

    Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器 Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力 Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视

    2024年04月13日
    浏览(30)
  • 使用 MediaCodec 在 Android 上进行硬解码

    要使用 MediaCodec 在 Android 上进行硬解码,并获取 RGBA 数据,你可以按照以下步骤进行操作: 创建 MediaExtractor 对象并设置要解码的 MP4 文件路径: 根据需要选择音频或视频轨道: 创建 MediaCodec 对象并配置解码器: 循环解码并获取 RGBA 数据: 在上述代码中,你需要根据解码器

    2024年04月24日
    浏览(23)
  • [Android] 如何编写基于MediaCodec的播放器

    基于MediaCodec JNI的播放器,以 native mediacodec demo 为基础扩展

    2024年02月12日
    浏览(24)
  • android nv21数据用mediacodec编解码

    在 Android 中使用 MediaCodec 进行 NV21 编码和解码的过程如下: 编码 NV21 数据: 解码编码后的数据: 上述代码中的变量和参数需要根据你的实际情况进行调整。此外,NV21 格式的数据需要根据具体需要进行分割和处理传入编码器和解码器。

    2024年02月13日
    浏览(23)
  • Android之MediaCodec::PostAndAwaitResponse消息原理(四十三)

    简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏: Audio工程师进阶系列 【 原创干货持续更新中…… 】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:An

    2024年02月07日
    浏览(33)
  • Android 使用自带的MediaCodec 框架进行本地视频压缩

    原文出自 这是之前的项目 因为找不到原文作者 只是找到了一个转载的文章 所以就把链接放了进来 这个项目个人亲测是可以用的 速度也比较快 但是有一点点小问题 简单的做了一个说明 1.我把项目重新Fock了一下适配到了Androidx 2.经个人测试 当视频文件放到 /storage/emulated/0/

    2024年02月12日
    浏览(29)
  • Android 音视频入门 (四)- 记录一次MediaCodec ,天呐

    isEncoding = true //开始编码 mMediaCodec.start() //构建连接器。 mWorkerThread = HandlerThread(“WorkerThread-Encoder”) mWorkerThread.start() mHandler = Handler(mWorkerThread.looper) } 注意,我们并不在此处就开启Muxer,我们会在子线程中接受数据的时候的某个状态开始进行混合。 mCameraDevice.setPreviewCallback {

    2024年04月16日
    浏览(31)
  • android opencv 调用硬编码mediacodec保存mp4

    目录 c++ opencv部分 java 编码部分 Java jni声明: java调用: 获取类函数签名:

    2024年02月11日
    浏览(36)
  • 【学习】从零开发的Android音视频开发(13)——MediaCodec到OMX框架过程及其硬解码

    在讲NuPlayer时,NuPlayer解码部分会创建MediaCodec,并且最终到达OMX框架,先看MediaCodec的 init 函数 从init函数中可以看到,首先创建了 ACodec ,并且初始化了 ALooper 、 AMessage ,由于ACodec继承自 AHandler ,那么一套消息机制就有了。最后发送 kWhatInit 消息,收到消息的逻辑位于ACodec.

    2023年04月08日
    浏览(33)
  • NDK编译ffmpeg包含硬件加速vulkan和mediacodec

    NDK编译ffmpeg包含硬件加速vulkan和mediacodec flyfish ffmpeg:ffmpeg-6.0 NDK:android-ndk-r25c 硬件加速:vulkan 和 mediacodec target CPU:armv8-a host:Ubuntu 22.04 因为这里要编译硬件加速版本的ffmpeg-6.0,所以需要把vulkan中include下的两个文件夹 vk_video 和 vulkan 拷贝到 android-ndk-r25c/toolchains/llvm/prebuil

    2024年02月12日
    浏览(24)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包