惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
T
Troy Hunt's Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Vercel News
Vercel News
T
Threatpost
G
Google Developers Blog
T
Threat Research - Cisco Blogs
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
The Exploit Database - CXSecurity.com
H
Heimdal Security Blog
Google DeepMind News
Google DeepMind News
Cyberwarzone
Cyberwarzone
T
The Blog of Author Tim Ferriss
Know Your Adversary
Know Your Adversary
Hacker News: Ask HN
Hacker News: Ask HN
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Schneier on Security
B
Blog
V2EX - 技术
V2EX - 技术
NISL@THU
NISL@THU
C
CERT Recently Published Vulnerability Notes
W
WeLiveSecurity
C
Cybersecurity and Infrastructure Security Agency CISA
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Y
Y Combinator Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Spread Privacy
Spread Privacy
The Last Watchdog
The Last Watchdog
V
Vulnerabilities – Threatpost
N
Netflix TechBlog - Medium
Schneier on Security
Schneier on Security
F
Fortinet All Blogs
N
News | PayPal Newsroom
Attack and Defense Labs
Attack and Defense Labs
Blog — PlanetScale
Blog — PlanetScale
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Security Blog
Microsoft Security Blog
S
Security @ Cisco Blogs
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
P
Privacy & Cybersecurity Law Blog
P
Proofpoint News Feed
Project Zero
Project Zero
I
Intezer
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
SecWiki News
SecWiki News
Martin Fowler
Martin Fowler

音视频开发进阶

音视频教程-第三节 音视频教程-第二节 真的,AI 可能就是新时代的信息差 充值 Cursor 之后,工作有了哪些变化?🤔 个人'蒸馏'大模型能做哪些有意思的事情 DeepSeek 大模型在 Mac 上的部署和运行 音视频教程-第一节 【WebRTC 专栏】-- Android 开发集成 WebRTC 库的几种方式 【WebRTC 专栏】-- 在 Mac M1 等系列芯片编译和开发 WebRTC-Android 库 Meta Llama3 大模型在 Mac 上的部署和运行 iOS VideoToolBox 解码 HEVC Open-GOP 视频的问题排查 Flutter 状态管理之 InheritedWidget 使用和分析 用 ChatGPT 回答技术问题怎么样 ? 音视频开发系统入门大致路线 UE 4.27 添加自定义 ShadingModel 用 UE4 虚幻引擎做个捏脸小功能~~ UE4 材质练习 之 凹凸贴图偏移的使用 UE4 材质练习系列基础 OpenGL上下文创建以及共享机制 007 | 播放器系列专栏-解析 MP4 文件读取信息 006 | 播放器系列专栏-在 Mac 上查看 MP4 格式信息 干货 | 快速抽取缩略图是怎么练成的? 关于直播、WebRTC、FFmpeg 的那些事 005 | 播放器系列专栏-在 Windows 上查看 MP4 格式信息 将音视频中的花屏、绿屏、黑屏问题一网打尽 关于音视频里面的解码帧率和渲染帧率 004 | 播放器系列专栏-认识MP4视频(下) 003 | 播放器系列专栏-认识MP4视频(上) 入门或者转行音视频,应该要怎么做? H264视频文件如何缩放分辨率 002 | 播放器系列专栏-FFmpeg依赖库的配置 001 | 播放器系列专栏-关于播放器项目的一个小实践 Seek策略以及在有B帧情况下的处理 目前流媒体开发工程师工作内容主要是什么? 一个音视频领域专业问答的小圈子 干货收藏 || Vulkan Game Engine 视频教程 音视频春节假期内卷指南(实操) Vulkan 在 FFmpeg 中的支持 Windows 下 FFmpeg 和 LibX264 的编译和配置 Metal 开发 | 使用 C++ 进行接口调用 音视频开发工作经验分享 || 视频版 FFmpeg 调用 MediaCodec 硬解码到 Surface 上 代码吸猫 | 用 OpenGL 图像渲染的养猫计划 百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!! 老生常谈-FFmpeg 的编译问题轻松搞定 FFmpeg 调用 Android MediaCodec 进行硬解码(附源码) 【WebRTC 专栏】--创建相机预览 Unity Shader 光照基础之 Half Lambert 光照模型 Unity Shader 光照基础之Lambert光照模型 Unity Shader 光照基础内容 Unity Shader 显示一张图片纹理 UnityShader 的基本概念 Unity 物体的基本操作 C++ 模板系列小结07-尾置返回类型 C++ 模板系列小结06-可变参数模板特性 C++ 中的多线程的使用和线程池建设 C++ 模板系列小结05-模板类型作为模板参数 C++ 模板系列小结04-类模板中的成员模板 C++ 模板系列小结03-在模板中指定变量类型 C++ 模板系列小结02-非类型模板参数 C++ 模板系列小结01-函数模板和类模板 从零打造渲染引擎系列01-什么是渲染引擎 iOS开发 - 在 Swift 中去调用 C/C++ 代码 2021 技术新番 - 从零打造渲染引擎系列 iOS 音视频开发的一些基础准备工作 音视频交流群又来啦~~~ 【WebRTC 专栏】WebRTC & Android 开发学习环境搭建~ 【喜大普奔】域名终于备案通过啦 Shader 优化 | OpenGL 绘制网格效果 【音视频连载-011】第二季 FFmpeg 一层一层获取文件信息 KodeLife | Shader 实时编辑预览的强大工具使用实践 推荐几个堪称教科书级别的 Android 音视频入门项目 【音视频连载-010】第二季 FFmpeg 日志打印 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下) 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上) 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示 【音视频连载-004】基础学习篇-SDL 加载图片并显示 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色 【音视频连载-001】基础学习篇- SDL 介绍以及工程配置 LearnOpenGL 源码在 MAC 上的编译与调试 2019 年终总结与回顾 Android NDK 开发的免费技术视频来啦~~ OpenGL 实现视频编辑中的转场效果 OpenGL 实践之贝塞尔曲线绘制 图像库 libjpeg-turbo 编译与实践 图像库 libpng 编译与实践 rust 开发编译 Android 动态库实践 Android NDK 开发 —— 从 Assets 文件夹加载图片并上传纹理 简单易用的图像解码库介绍 —— stb_image 博客图床迁移记 进击的 Vulkan 移动开发之 SwapChain 进击的 Vulkan 移动开发之 Command Buffer 进击的 Vulkan 移动开发之 Instance & Device & Queue 进击的 Vulkan 移动开发(一)之今生前世 Java 显式锁 Lock 与条件队列 C++ 标准容器库小结 一文读懂 YUV 的采样与格式 《OpenGL ES 3.x 游戏开发》碰撞检测之 AABB 包围盒
MediaCodec 硬编码之相机内容编码成 H264 文件
音视频开发进阶 · 2018-09-19 · via 音视频开发进阶

一个专注音视频领域的小圈子

避免图片丢失,建议阅读微信原文:

https://mp.weixin.qq.com/s/8Kq9JgvGhlJCpNIyb7zK2w 在 Android 4.1 版本提供了 MediaCodec 接口来访问设备的编解码器,不同于 FFmpeg 的软件编解码,它采用的是硬件编解码能力,因此在速度上会比软解更具有优势,但是由于 Android 的碎片化问题,机型众多,版本各异,导致 MediaCodec 在机型兼容性上需要花精力去适配,并且编解码流程不可控,全交由厂商的底层硬件去实现,最终得到的视频质量不一定很理想。 虽然 MediaCodec 仍然存在一定的弊端,但是对于快速实现编解码需求,还是很值得参考的。 以将相机预览的 YUV 数据编码成 H264 视频流为例来解析 MediaCodec 的使用。

使用解析

MediaCodec 工作模型

下图展示了 MediaCodec 的工作方式,一个典型的生产者消费者模型,两边的 Client 分别代表输入端和输出端,输入端将数据交给 MediaCodec 进行编码或者解码,而输出端就得到编码或者解码后的内容。

输入端和输出端是通过输入队列缓冲区和输出队列缓冲区,两条缓冲区队列的形式来和 MediaCodec 传递数据。

首先从输入队列中出队得到一个可用的缓冲区,将它填满数据之后,再将缓冲区入队,交由 MediaCodec 去处理。

MediaCodec 处理完了之后,再从输出队列中出队得到一个可用的缓冲区,这个缓冲里面的数据就是编码或者解码后的数据了,把这些数据进行相应的处理之后,还需要释放这个缓冲区,让它回到队列中去,可供下一次使用。

MediaCodec 生命周期

另外,MediaCodec 也存在相应的 生命周期,如下图所示:

当创建了 MediaCodec 之后,是处于未初始化的 Uninitialized 状态,调用 configure 方法之后就处于 Configured 状态,调用了 start 方法之后,就处于 Executing 状态。

Executing 状态下开始处理数据,它又有三个子状态,分别是:

  • Flushed
  • Running
  • End of Stream

当一调用 start 方法之后,就进入了 Flushed 状态,从输入缓冲区队列中取出一个缓冲区就进入了 Running 状态,当入队的缓冲区带有 EOS 标志时, 就会切换到 End of Stream 状态, MediaCodec 不再接受入队的缓冲区,但是仍然会对已入队的且没有进行编解码操作的缓冲区进行操作、输出,直到输出的缓冲区带有 EOS 标志,表示编解码操作完成了。

Executing 状态下可以调用 flush 方法,使 MediaCodec 切换到 Flushed 状态。

Executing 状态下可以调用 stop 方法,使 MediaCodec 切换到 Uninitialized 状态,然后再次调用 configure 方法进入 Configured 状态。另外,当调用 reset 方法也会进入到 Uninitialized 状态。

当不再需要 MediaCodec 时,调用 release 方法将它释放掉,进入 Released 状态。

当 MediaCodec 工作发生异常时,会进入到 Error 状态,此时还是可以通过 reset 方法恢复过来,进入 Uninitialized 状态。

MediaCodec 调用流程

理解了 MediaCodec 的生命周期和工作流程之后,就可以上手来进行编码工作了。

以 MediaCodec 同步调用为例,使用过程如下:

 // 创建 MediaCodec,此时是 Uninitialized 状态
 MediaCodec codec = MediaCodec.createByCodecName(name);
 // 调用 configure 进入 Configured 状态
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 // 调用 start 进入 Executing 状态,开始编解码工作
 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
   }
 }
 // 调用 stop 方法进入 Uninitialized 状态
 codec.stop();
 // 调用 release 方法释放,结束操作
 codec.release();

代码解析

MediaFormat 设置

首先需要创建并设置好 MediaFormat 对象,它表示媒体数据格式的相关信息,对于视频主要有以下信息要设置:

  • 颜色格式
  • 码率
  • 码率控制模式
  • 帧率
  • I 帧间隔

其中,码率就是指单位传输时间传送的数据位数,一般用 kbps 即千位每秒来表示。而帧率就是指每秒显示的帧数。

其实对于码率有三种模式可以控制:

  • BITRATE_MODE_CQ
    • 表示不控制码率,尽最大可能保证图像质量
  • BITRATE_MODE_VBR
    • 表示 MediaCodec 会根据图像内容的复杂度来动态调整输出码率,图像负责则码率高,图像简单则码率低
  • BITRATE_MODE_CBR
    • 表示 MediaCodec 会把输出的码率控制为设定的大小

对于颜色格式,由于是将 YUV 数据编码成 H264,而 YUV 格式又有很多,这又涉及到机型兼容性问题。在对相机编码时要做好格式的处理,比如相机使用的是 NV21 格式,MediaFormat 使用的是 COLOR_FormatYUV420SemiPlanar,也就是 NV12 模式,那么就得做一个转换,把 NV21 转换到 NV12

对于 I 帧间隔,也就是隔多久出现一个 H264 编码中的 I 帧。

完整 MediaFormat 设置示例:

        MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        // 马率
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
        // 调整码率的控流模式
        mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
        // 设置帧率 
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        // 设置 I 帧间隔
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

当开始编解码操作时,开启编解码线程,处理相机预览返回的 YUV 数据。

在这里用到了相机的一个封装库:

https://github.com/glumes/EzCameraKit

编解码操作

编解码操作代码如下:

while (isEncoding) {
    // YUV 颜色格式转换
    if (!mEncodeDataQueue.isEmpty()) {
        input = mEncodeDataQueue.poll();
        byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2];
        NV21ToNV12(input, yuv420sp, mWidth, mHeight);
        input = yuv420sp;
    }
    if (input != null) {
        try {
            // 从输入缓冲区队列中拿到可用缓冲区,填充数据,再入队
            ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
            ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
            int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
            if (inputBufferIndex >= 0) {
                // 计算时间戳
                pts = computePresentationTime(generateIndex);
                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                inputBuffer.clear();
                inputBuffer.put(input);
                mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
                generateIndex += 1;
            }
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
            // 从输出缓冲区队列中拿到编码好的内容,对内容进行相应处理后在释放
            while (outputBufferIndex >= 0) {
                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                byte[] outData = new byte[bufferInfo.size];
                outputBuffer.get(outData);
                // flags 利用位操作,定义的 flag 都是 2 的倍数
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // 配置相关的内容,也就是 SPS,PPS
                    mOutputStream.write(outData, 0, outData.length);
                } else if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { // 关键帧
					mOutputStream.write(outData, 0, outData.length);
                } else {
                    // 非关键帧和SPS、PPS,直接写入文件,可能是B帧或者P帧
                    mOutputStream.write(outData, 0, outData.length);
                }
                mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
            }
        } catch (IOException e) {
            Log.e(TAG, e.getMessage());
        }
    } else {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Log.e(TAG, e.getMessage());
        }
    }
}

首先,要把要把相机的 NV21 格式转换成 NV12 格式,然后 通过 dequeueInputBuffer 方法去从可用的输入缓冲区队列中出队取出缓冲区,填充完数据后再通过 queueInputBuffer 方法入队。

dequeueInputBuffer 返回缓冲区索引,如果索引小于 0 ,则表示当前没有可用的缓冲区。它的参数 timeoutUs 表示超时时间 ,毕竟用的是 MediaCodec 的同步模式,如果没有可用缓冲区,就会阻塞指定参数时间,如果参数为负数,则会一直阻塞下去。

queueInputBuffer 方法将数据入队时,除了要传递出队时的索引值,然后还需要传入当前缓冲区的时间戳 presentationTimeUs 和当前缓冲区的一个标识 flag

其中,时间戳通常是缓冲区渲染的时间,而标识则有多种标识,标识当前缓冲区属于那种类型:

  • BUFFER_FLAG_CODEC_CONFIG
    • 标识当前缓冲区携带的是编解码器的初始化信息,并不是媒体数据
  • BUFFER_FLAG_END_OF_STREAM
    • 结束标识,当前缓冲区是最后一个了,到了流的末尾
  • BUFFER_FLAG_KEY_FRAME
    • 表示当前缓冲区是关键帧信息,也就是 I 帧信息

在编码的时候可以计算当前缓冲区的时间戳,也可以直接传递 0 就好了,对于标识也可以直接传递 0 作为参数。

把数据传入给 MediaCodec 之后,通过 dequeueOutputBuffer 方法取出编解码后的数据,除了指定超时时间外,还需要传入 MediaCodec.BufferInfo 对象,这个对象里面有着编码后数据的长度、偏移量以及标识符。

取出 MediaCodec.BufferInfo 内的数据之后,根据不同的标识符进行不同的操作:

  • BUFFER_FLAG_CODEC_CONFIG
    • 表示当前数据是一些配置数据,在 H264 编码中就是 SPS 和 PPS 数据,也就是 00 00 00 01 6700 00 00 01 68 开头的数据,这个数据是必须要有的,它里面有着视频的宽、高信息。
  • BUFFER_FLAG_KEY_FRAME
    • 关键帧数据,对于 I 帧数据,也就是开头是 00 00 00 01 65 的数据,
  • BUFFER_FLAG_END_OF_STREAM
    • 表示结束,MediaCodec 工作结束

对于返回的 flags ,不符合预定义的标识,则可以直接写入,那些数据可能代表的是 H264 中的 P 帧 或者 B 帧。

对于编解码后的数据,进行操作后,通过 releaseOutputBuffer 方法释放对应的缓冲区,其中第二个参数 render 代表是否要渲染到 surface 上,这里暂时不需要就为 false 。

停止编码

当想要停止编码时,通过 MediaCodec 的 stop 方法切换到 Uninitialized 状态,然后再调用 release 方法释放掉。

这里并没有采用使用 BUFFER_FLAG_END_OF_STREAM 标识符的方式来停止编码,而是直接切换状态了,在通过 Surface 方式进行录制时,再去采用这种方式了。

对于 MediaCodec 硬编码解析之相机内容编码成 H264 文件就到这里了,主要还是讲述了关于 MediaCodec 的使用,一旦熟悉使用了,完成编码工作也就很简单了。

参考

  1. android MediaCodec解析
  2. 谈谈关于Android视频编码的那些坑

原创文章,转载请注明来源:    MediaCodec 硬编码之相机内容编码成 H264 文件