当前位置: 首页 > news >正文

NDK中使用 MediaCodec 编解码视频

背景

MediaCodec 作为Android自带的视频编解码工具,可以直接利用底层硬件编解码能力,现在已经逐渐成为主流了。API21已经支持NDK方法了,MediaCodec api设计得非常精妙,另一个方面也是很多人觉得不好懂。

内容

MediaCodec的两个Buffer和三板斧

MediaCodec内部包含InputBuffer和OutputBuffer,内部有一个自启线程,不断去查询两个Buffer,是一个生产者消费者模型。

进行数据处理时主要靠三板斧。

  • 第一步:取buffer地址

AMediaCodec_dequeueInputBuffer
  • 第二步:获取buffer数据

AMediaCodec_getInputBuffer
  • 第三步:buffer入队

AMediaCodec_queueInputBuffer

InputBuffer和OutputBuffer基本是对称的:

  • 第一步:取buffer地址

AMediaCodec_dequeueOutputBuffer
  • 第二步:获取buffer数据

AMediaCodec_getOutputBuffer
  • 第三步:buffer释放

AMediaCodec_releaseOutputBuffer

只有第三步不同,AMediaCodec_queueInputBuffer是数据入队等待消费,AMediaCodec_releaseOutputBuffer是释放数据。

编码和解码过程,InputBuffer和OutputBuffer就互相置换下。

解码:原始数据(视频流)-> 提取器AMediaExtractor->InputBuffer->OutputBuffer->帧数据(YUV420sp,PCM)

编码:帧数据(视频流)->InputBuffer->OutputBuffer->合成器AMediaMuxer

解码

解码配置

解码开始需要配置AMediaCodec和AMediaExtractor,MediaCodec start后就可以开始解码。

AMediaExtractor需要设置文件描述符,通过AAssetManager_open或者fopen就可以得到。起始点和长度也同样。然后设置进提取器。

AMediaExtractor_setDataSourceFd(mExtractor, 
                                virtualFile.fd,
                                virtualFile.start,
                                virtualFile.length);

AMediaCodec创建需要设置数据格式,通过AMediaExtractor获取到的AMediaFormat可以得到mime和format。

mCodec = AMediaCodec_createDecoderByType(mime);
AMediaCodec_configure(mCodec, format, NULL, NULL, 0);
AMediaCodec_start(mCodec);

解码配置第三个参数为NativeWindow,加了后解码后可以直接吐到surface上,GPU数据直接渲软,效率高但不够灵活。不加的话解码数据就需要输出拷贝。

解码流程

解码也就是操作两个Buffer的过程,执行玩三板斧就可以,然后有一些状态需要处理。

if (!mInputEof) {
    ssize_t bufidx = AMediaCodec_dequeueInputBuffer(mCodec, 1);
    log_info(NULL, "input buffer %zd", bufidx);
    if (bufidx >= 0) {
        size_t bufsize;
        uint8_t *buf = AMediaCodec_getInputBuffer(mCodec, bufidx, &bufsize);
        int sampleSize = AMediaExtractor_readSampleData(mExtractor, buf, bufsize);
        if (sampleSize < 0) {
            sampleSize = 0;
            mInputEof = true;
            log_info(NULL, "video producer input EOS");
        }
        int64_t presentationTimeUs = AMediaExtractor_getSampleTime(mExtractor);

        AMediaCodec_queueInputBuffer(mCodec, bufidx, 0, sampleSize, presentationTimeUs,
                                     mInputEof ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM
                                               : 0);
        AMediaExtractor_advance(mExtractor);
    }
}

if (!mOutputEof) {
    AMediaCodecBufferInfo info;
    ssize_t status = AMediaCodec_dequeueOutputBuffer(mCodec, &info, 1);

    if (status >= 0) {

        if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) {
            log_info(NULL, "video producer output EOS");

            eof = true;
            mOutputEof = true;
        }

        uint8_t *outputBuf = AMediaCodec_getOutputBuffer(mCodec, status, NULL/* out_size */);
        size_t dataSize = info.size;
        if (outputBuf != nullptr && dataSize != 0) {
            long pts = info.presentationTimeUs;
            int32_t pts32 = (int32_t) pts;

            *buffer = (uint8_t *) mlt_pool_alloc(dataSize);
            memcpy(*buffer, outputBuf + info.offset, dataSize);
            *buffersize = dataSize;
        }

        int64_t presentationNano = info.presentationTimeUs * 1000;
        log_info(NULL, "video pts %lld outsize %d", info.presentationTimeUs, dataSize);
        /*if (delay > 0) {
            usleep(delay / 1000);
        }*/
        AMediaCodec_releaseOutputBuffer(mCodec, status, info.size != 0);
    } else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
        log_info(NULL, "output buffers changed");
    } else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
        AMediaFormat_delete(format);
    } else if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
        log_info(NULL, "video no output buffer right now");
    } else {
        log_info(NULL, "unexpected info code: %zd", status);
    }

}

AMediaCodec和AMediaExtractor是没有直接交流的,AMediaCodec取到InputBuffer后实际数据为空,需要从AMediaExtractor_readSampleData获取到buffer数据。

AMediaCodec数据入队后,AMediaExtractor调用 AMediaExtractor_advance前进到下一个数据位置。

OutputBuffer操作时有些不一样,AMediaCodec_dequeueOutputBuffer获取的是解码好的帧,AMediaCodec_getOutputBuffer取到的就已经是解码好的数据了,可以直接拷贝使用。

AMediaCodec_releaseOutputBuffer是释放buffer,如果配置了surface,就会渲软到surface上。

编码

编码配置

编码是解码的逆过程,首先设置格式,然后根据格式创建编码器MediaCodec,再根据文件创建合成器MediaMuxer。

void NativeEncoder::prepareEncoder(int width, int height, int fps, std::string strPath) {

    mWidth = width;
    mHeight = height;
    mFps = fps;

    AMediaFormat *format = AMediaFormat_new();
    AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, mStrMime.c_str());
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, mWidth);
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, mHeight);

    AMediaFormat_setInt32(format,AMEDIAFORMAT_KEY_COLOR_FORMAT, COLOR_FORMAT_SURFACE);
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, mBitRate);
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, mFps);
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, mIFrameInternal);

    const char *s = AMediaFormat_toString(format);
    log_info(NULL, "encoder video format: %s", s);


    mCodec = AMediaCodec_createEncoderByType(mStrMime);


    media_status_t status = AMediaCodec_configure(mCodec, format, NULL, NULL,
                                                  AMEDIACODEC_CONFIGURE_FLAG_ENCODE);
    if (status != 0) {
        log_error(NULL, "AMediaCodec_configure() failed with error %i for format %u",
                      (int) status, 21);
    } else {

    }
    AMediaFormat_delete(format);

    FILE *fp = fopen(strPath.c_str(), "wb");

    if (fp != NULL) {
        mFd = fileno(fp);
    } else {
        mFd = -1;
        log_error(NULL, "create file %s fail", strPath.c_str());
    }

    if(mMuxer == NULL)
        mMuxer = AMediaMuxer_new(mFd, AMEDIAMUXER_OUTPUT_FORMAT_MPEG_4);

    mMuxerStarted = false;

    fclose(fp);

}

这里注意下配置类型是 “video/avc”,基本视频都是这个格式,可以看官网格式支持信息,比特率mBitRate是6000000,这个要根据需求对应配置,I帧间隔mIFrameInternal是1秒,间隔长获取关键帧信息会有问题。

编码准备

编码视频流需要创建一个surface,再把这个surface绑定到共享的EGLContext上。

void NativeEncoder::prepareEncoderWithShareCtx(int width, int height, int fps, std::string strPath,
                                   EGLContext shareCtx) {

    prepareEncoder(width,height,fps,strPath);
    ANativeWindow *surface;
    AMediaCodec_createInputSurface(mCodec, &surface);
    media_status_t status;
    if ((status = AMediaCodec_start(mCodec)) != AMEDIA_OK) {
        log_error(NULL, "AMediaCodec_start: Could not start encoder.");
    } else {
        log_info(NULL, "AMediaCodec_start: encoder successfully started");
    }
    mCodecInputSurface = new CodecInputSurface(surface);
    mCodecInputSurface->setupEGL(shareCtx);
}

编码流程

编码需要先进行渲染,从外部共享的EGLContext传入一个纹理,渲软到编码器对应的surface上,再进行编码。

传入纹理并渲染:

void NativeEncoder::feedFrame(uint64_t pts, int tex) {
    drainEncoder(false);

    mCodecInputSurface->makeCurrent();
    glViewport(0,0,mWidth,mHeight);
    mCodecInputSurface->renderOnSurface(tex);
    mCodecInputSurface->setPresentationTime(pts);
    mCodecInputSurface->swapBuffers();
    mCodecInputSurface->makeNothingCurrent();
}

编码:

void NativeEncoder::drainEncoder(bool eof) {

    if (eof) {

        ssize_t ret = AMediaCodec_signalEndOfInputStream(mCodec);
        log_info(NULL, "drainEncoder eof = %d",ret);
    }

    while (true) {

        AMediaCodecBufferInfo info;
        //time out usec 1
        ssize_t status = AMediaCodec_dequeueOutputBuffer(mCodec, &info, 1);

        if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {

            if (!eof) {
                break;
            } else {
                log_info(NULL, "video no output available, spinning to await EOS");
            }
        } else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
            // not expected for an encoder
        } else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
            if (mMuxerStarted) {
                log_warning(NULL, "format changed twice");
            }

            AMediaFormat *fmt = AMediaCodec_getOutputFormat(mCodec);
            const char *s = AMediaFormat_toString(fmt);
            log_info(NULL, "video output format %s", s);

            mTrackIndex = AMediaMuxer_addTrack(mMuxer, fmt);

            if(mAudioTrackIndex != -1 && mTrackIndex != -1) {

                log_info(NULL,"AMediaMuxer_start");
                AMediaMuxer_start(mMuxer);
                mMuxerStarted = true;
            }

        } else {

            uint8_t *encodeData = AMediaCodec_getOutputBuffer(mCodec, status, NULL/* out_size */);

            if (encodeData == NULL) {
                log_error(NULL, "encoder output buffer was null");
            }

            if ((info.flags & AMEDIACODEC_BUFFER_FLAG_CODEC_CONFIG) != 0) {
                log_info(NULL, "ignoring AMEDIACODEC_BUFFER_FLAG_CODEC_CONFIG");
                info.size = 0;
            }

            size_t dataSize = info.size;

            if (dataSize != 0) {

                if (!mMuxerStarted) {
                    log_error(NULL, "muxer has't started");
                }
                log_info(NULL,"AMediaMuxer_writeSampleData video size %d",dataSize);
                AMediaMuxer_writeSampleData(mMuxer, mTrackIndex, encodeData, &info);
            }

            AMediaCodec_releaseOutputBuffer(mCodec, status, false);

            if ((info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0) {

                if (!eof) {
                    log_warning(NULL, "reached end of stream unexpectly");
                } else {
                    log_info(NULL, "video end of stream reached");
                }

                break;
            }
        }
    }
}

除了结尾标记,编码时没有操作InputBuffer,因为InputBuffer对应的就是surface的源,所以编码第一步实际是渲软,通过opengl render到surface上再交换缓冲区到surface上。

第二步获取到OutputBuffer数据,调用AMediaCodec_getOutputBuffer;第三步合成器写数据,调用AMediaMuxer_writeSampleData然后释放outputBuffer,调用AMediaCodec_releaseOutputBuffer。

总结

总结了下MediaCodec在ndk中的使用,MediaCodec是一个非常灵活的api,编解码音视频都是同一个,掌握双缓冲和三板斧就对流程有了非常清楚的了解,对编解码代码也可以不再畏惧了。

作者:anddymao

来源:http://anddymao.com/2019/10/16/2019-10-16-ndk-MediaCodec/


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

推荐阅读:

音视频面试基础题

OpenGL ES 学习资源分享

一文读懂 YUV 的采样与格式

OpenGL 之 GPUImage 源码分析

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

相关文章:

  • 【资源分享】免费学 清华大学 · 游戏程序设计公开课啦!!!
  • 谈一谈Android上的SurfaceTexture
  • 你还不知道 OpenGL ES 和 EGL 的关系?
  • 腾讯云视频云巅峰论剑——王者对决,等你来评!
  • 高大上的非线性编辑是怎么一回事?
  • C++ 万字长文第二篇---拿下字节面试
  • Android自定义View-SVG动画
  • 谈一谈Flutter外接纹理
  • Android 11 强制用户使用系统相机?
  • 3A之自动白平衡(AWB)篇
  • Shader基础技巧整理
  • 一起用Gradle Transform API + ASM完成代码织入呀~
  • 用shader做一个柿子颜色的过场动画
  • 只需跟着Google学android:ViewModel篇
  • 我用 OpenGL 实现了那些年流行的相机滤镜
  • SegmentFault for Android 3.0 发布
  • [nginx文档翻译系列] 控制nginx
  • ECMAScript 6 学习之路 ( 四 ) String 字符串扩展
  • HTTP中的ETag在移动客户端的应用
  • Promise面试题,控制异步流程
  • Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 大整数乘法-表格法
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 思考 CSS 架构
  • 通过获取异步加载JS文件进度实现一个canvas环形loading图
  • 微信公众号开发小记——5.python微信红包
  • 一个完整Java Web项目背后的密码
  • [Shell 脚本] 备份网站文件至OSS服务(纯shell脚本无sdk) ...
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • #define、const、typedef的差别
  • (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (附源码)springboot课程在线考试系统 毕业设计 655127
  • (附源码)springboot青少年公共卫生教育平台 毕业设计 643214
  • (附源码)ssm码农论坛 毕业设计 231126
  • (四)Android布局类型(线性布局LinearLayout)
  • (已解决)什么是vue导航守卫
  • (原創) 博客園正式支援VHDL語法著色功能 (SOC) (VHDL)
  • (转)MVC3 类型“System.Web.Mvc.ModelClientValidationRule”同时存在
  • .NetCore实践篇:分布式监控Zipkin持久化之殇
  • .net操作Excel出错解决
  • .vue文件怎么使用_我在项目中是这样配置Vue的
  • /var/spool/postfix/maildrop 下有大量文件
  • []AT 指令 收发短信和GPRS上网 SIM508/548
  • [51nod1610]路径计数
  • [ASP]青辰网络考试管理系统NES X3.5
  • [Big Data - Kafka] kafka学习笔记:知识点整理
  • [BZOJ1178][Apio2009]CONVENTION会议中心
  • [C# 开发技巧]如何使不符合要求的元素等于离它最近的一个元素
  • [c#基础]DataTable的Select方法
  • [c++] C++多态(虚函数和虚继承)
  • [echarts] y轴不显示0
  • [JavaWeb学习] Spring Ioc和DI概念思想
  • [Java开发之路](14)反射机制