Android视频编解码
简介
从广义上讲,编解码器就是处理输入数据来产生输出数据。MediaCode采用异步方式处理数据,并且使用了一组输入输出缓存(input and output buffers)。简单来讲,你请求或接收到一个空的输入缓存(input buffer),向其中填充满数据并将它传递给编解码器处理。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。最终,你请求或接收到一个填充了结果数据的输出缓存(output buffer),使用完其中的数据,并将其释放给编解码器再次使用。
状态(States)
在编解码器的生命周期内有三种理论状态:停止态-Stopped、执行态-Executing、释放态-Released,停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。执行状态(Executing)在概念上会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream)。
- 当你使用任意一种工厂方法(factory methods)创建了一个编解码器,此时编解码器处于未初始化状态(Uninitialized)。首先,你需要使用configure(…)方法对编解码器进行配置,这将使编解码器转为配置状态(Configured)。然后调用start()方法使其转入执行状态(Executing)。在这种状态下你可以通过上述的缓存队列操作处理数据。
- 执行状态(Executing)包含三个子状态: 刷新(Flushed)、运行( Running) 以及流结束(End-of-Stream)。在调用start()方法后编解码器立即进入刷新子状态(Flushed),此时编解码器会拥有所有的缓存。一旦第一个输入缓存(input buffer)被移出队列,编解码器就转入运行子状态(Running),编解码器的大部分生命周期会在此状态下度过。当你将一个带有end-of-stream 标记的输入缓存入队列时,编解码器将转入流结束子状态(End-of-Stream)。在这种状态下,编解码器不再接收新的输入缓存,但它仍然产生输出缓存(output buffers)直到end-of- stream标记到达输出端。你可以在执行状态(Executing)下的任何时候通过调用flush()方法使编解码器重新返回到刷新子状态(Flushed)。
- 通过调用stop()方法使编解码器返回到未初始化状态(Uninitialized),此时这个编解码器可以再次重新配置 。当你使用完编解码器后,你必须调用release()方法释放其资源。
- 在极少情况下编解码器会遇到错误并进入错误状态(Error)。这个错误可能是在队列操作时返回一个错误的值或者有时候产生了一个异常导致的。通过调用 reset()方法使编解码器再次可用。你可以在任何状态调用reset()方法使编解码器返回到未初始化状态(Uninitialized)。否则,调用 release()方法进入最终的Released状态。
一、编码
初始化编码器
public void prepare(int width, int height) throws IOException {// MIME_TYPE:"video/avc" -> H264 "video/hevc" -> H265MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);mWidth = width;mHeight = height;// Set some properties. Failing to specify some of these can cause the MediaCodec// configure() call to throw an unhelpful exception.format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);format.setInteger(MediaFormat.KEY_BIT_RATE, VideoConfig.BIT_RATE);// FPS 每秒传输帧数(Frames Per Second)format.setInteger(MediaFormat.KEY_FRAME_RATE, 25);// I-frame 关键帧时间间隔,单位minformat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);Log.d(TAG, "format: " + format);// Create a MediaCodec encoder, and configure it with our format. Get a Surface// we can use for input and wrap it with a class that handles the EGL work.mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);mMediaFormat = mEncoder.getOutputFormat();mEncoder.start();}
向InputBuffer输入编码数据
如果从Camera拿到的数据为NV21/NV12格式,可以先通过 YUV库 将NV21/NV12转码为I420格式,再将数据送入编码器编码
/*** 向编码器InputBuffer中填入数据** @param data NV21数据* @param timeSptamp 时间戳 ms*/private void putDataToInputBuffer(byte[] data, long timeSptamp) {int index = mEncoder.dequeueInputBuffer(-1);if (index >= 0) {ByteBuffer buffer = mEncoder.getInputBuffer(index);if (buffer == null) {Log.d(TAG, "InputBuffer is null point");return;}if (yuv == null) {// YUV数据存储空间大小为 Y分量->width * height U、V分量->width * height / 4yuv = new byte[mWidth * mHeight * 3 / 2];}// NV21格式数据转为I420Pnv21ToYuv420p(data, timeSptamp);buffer.clear();buffer.put(yuv);mEncoder.queueInputBuffer(index, 0, data.length, timeSptamp * 1000, 0);}drainEncoder(false);}
说明:视频添加文字/图片水印,可以在将YUV数据送入编码器前,将文字转为Bitmap,通过YUV库将ARGB转码为I420P,再使用YUV图片合成技术合成,这样编码后的H264/H265视频码流就添加上了水印。
处理OutputBuffer
/*** 读取编码后的H264/H265数据** @param endOfStream 标识是否结束*/public void drainEncoder(boolean endOfStream) {final int TIMEOUT_USEC = 10000;if (endOfStream) {Log.d(TAG, "sending EOS to encoder");return;}while (true) {int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {// no output available yetif (!endOfStream) {break; // out of while} else {Log.d(TAG, "no output available, spinning to await EOS");}} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {// should happen before receiving buffers, and should only happen oncemMediaFormat = mEncoder.getOutputFormat();Log.d(TAG, "encoder output format changed: " + mMediaFormat);} else if (encoderStatus < 0) {Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +encoderStatus);// let's ignore it} else {ByteBuffer encodedData = mEncoder.getOutputBuffer(encoderStatus);if (encodedData == null) {Log.w(TAG, "encoderOutputBuffer " + encoderStatus +" was null");break;}if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// The codec config data was pulled out and fed to the muxer when we got// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");mBufferInfo.size = 0;}if (mBufferInfo.size != 0) {// adjust the ByteBuffer values to match BufferInfo (not needed?)encodedData.position(mBufferInfo.offset);encodedData.limit(mBufferInfo.offset + mBufferInfo.size);// 取出编码好的H264数据byte[] data = new byte[mBufferInfo.size];encodedData.get(data);// todo 将编码好的H264/H265数据存储到缓冲区或者传递给MediaMuxer生成视频文件(MP4)// mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME 表示该帧数据为关键帧(I帧)Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +mBufferInfo.presentationTimeUs);}// 释放输出缓冲区mEncoder.releaseOutputBuffer(encoderStatus, false);if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {if (!endOfStream) {Log.d(TAG, "reached end of stream unexpectedly");} else {Log.d(TAG, "end of stream reached");}break; // out of while}}}}
二、解码
基础知识
1、Codec-specific数据
有些格式,特别是ACC音频和MPEG4、H.264和H.265视频格式要求实际数据以若干个包含配置数据或编解码器指定数据的缓存为前缀。当处理这种压缩格式的数据时,这些数据必须在调用start()方法后且在处理任何帧数据之前提交给编解码器。这些数据必须在调用queueInputBuffer方法时使用BUFFER_FLAG_CODEC_CONFIG进行标记。
Codec-specific数据也可以被包含在传递给configure方法的格式信息(MediaFormat)中,在ByteBuffer条目中以"csd-0", "csd-1"等key标记。这些keys一直包含在通过MediaExtractor获得的Audio Track or Video Track的MediaFormat中。一旦调用start()方法,MediaFormat中的Codec-specific数据会自动提交给编解码器;你不能显示的提交这些数据。如果MediaFormat中不包含编解码器指定的数据,你可以根据格式要求,按照正确的顺序使用指定数目的缓存来提交codec-specific数据。在H264 AVC编码格式下,你也可以连接所有的codec-specific数据并作为一个单独的codec-config buffer提交。
Android 使用下列的codec-specific data buffers。对于适当的MediaMuxer轨道配置,这些也要在轨道格式中进行设置。每一个参数集以及被标记为(*)的codec-specific-data段必须以"\x00\x00\x00\x01"字符开头。
注意:当编解码器被立即刷新或start之后不久刷新,并且在任何输出buffer或输出格式变化被返回前需要特别地小心,因为编解码器的codec specific data可能会在flush过程中丢失。为保证编解码器的正常运行,你必须在刷新后使用标记为BUFFER_FLAG_CODEC_CONFIG的buffers再次提交这些数据。
2、流域界与关键帧(Stream Boundary and Key Frames)
调用start()或flush()方法后,输入数据在合适的流边界开始是非常重要的:其第一帧必须是关键帧(key-frame)。一个关键帧能够独立地完全解码(对于大多数编解码器它意味着I-frame),关键帧之后显示的帧不会引用关键帧之前的帧。
下面的表格针对不同的视频格式总结了合适的关键帧:
核心代码
初始化解码器
public void prepare(int width, int height, int fps, byte[] sps, byte[] pps) throws IOException {String mimeType = "video/avc";MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);mWidth = width;mHeight = height;// 参见Codec-specific数据说明,H264数据格式需要 csd-0(sps)、csd-1(pps);// H265数据格式需要 csd-0(vps+sps+pps)if (sps != null) {format.setByteBuffer("csd-0", ByteBuffer.wrap(sps));}if (pps != null) {format.setByteBuffer("csd-1", ByteBuffer.wrap(pps));}format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);format.setInteger(MediaFormat.KEY_PUSH_BLANK_BUFFERS_ON_STOP, 1);Log.i(TAG, String.format("config codec:%s", format));// 创建解码器mDecoder = MediaCodec.createDecoderByType(mimeType);// 配置解码器 formatmDecoder.configure(format, null, null, 0);mDecoder.start();}
注意:在解码H264/H265数据时,传递码流数据前一定要先配置好csd-*,参见Codec-specific说明。
向InputBuffer输入解码数据
/*** 向解码器InputBuffer中填入数据** @param data H264/H265数据* @param timeSptamp 时间戳 us*/private void putDataToInputBuffer(byte[] data, long timeSptamp) {int index = mDecoder.dequeueInputBuffer(-1);if (index >= 0) {ByteBuffer buffer = mDecoder.getInputBuffer(index);if (buffer == null) {LogUtils.d(TAG, "InputBuffer is null point");return;}buffer.clear();buffer.put(data);Log.d(TAG, "queueInputBuffer data length: " + data.length + " timeSptamp: " + timeSptamp);mDecoder.queueInputBuffer(index, 0, data.length, timeSptamp, 0);}drainDecoder(false, timeSptamp);}
注意:传递给解码器的第一帧数据必须是关键帧(I-帧),参见流域界与关键帧说明。
处理OutputBuffer
/*** 读取解码后的H264/H265数据** @param endOfStream 标识是否结束* @param timeSptamp 当前解码的数据的时间戳*/private void drainDecoder(boolean endOfStream, long timeSptamp) {final int TIMEOUT_USEC = 10000;if (endOfStream) {Log.d(TAG, "sending EOS to encoder");return;}while (true) {int decoderStatus = mDecoder.dequeueOutputBuffer(mBufferInfo, 0);if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {// no output available yet 输出为空if (!endOfStream) {break; // out of while} else {Log.d(TAG, "no output available, spinning to await EOS");}} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {// should happen before receiving buffers, and should only happen oncemMediaFormat = mDecoder.getOutputFormat();Log.d(TAG, "encoder output format changed: " + mMediaFormat);} else if (decoderStatus < 0) {Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +decoderStatus);// let's ignore it} else {ByteBuffer decodedData = mDecoder.getOutputBuffer(decoderStatus);if (decodedData == null) {Log.w(TAG, "decoderOutputBuffer " + decoderStatus +" was null");break;}if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// The codec config data was pulled out and fed to the muxer when we got// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");mBufferInfo.size = 0;}if (mBufferInfo.size != 0) {// adjust the ByteBuffer values to match BufferInfo (not needed?)decodedData.position(mBufferInfo.offset);decodedData.limit(mBufferInfo.offset + mBufferInfo.size);// 取出解码好的NV12数据byte[] data = new byte[mBufferInfo.size];decodedData.get(data);// todo 可以将解码后的NV12数据转码为ARGB8888,保存为jpg图片Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +mBufferInfo.presentationTimeUs);}mDecoder.releaseOutputBuffer(decoderStatus, false);if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {if (!endOfStream) {Log.d(TAG, "reached end of stream unexpectedly");} else {Log.d(TAG, "end of stream reached");}break; // out of while}}}}
说明:解码后的NV12数据可以通过YUV库转码为ARGB8888格式,再将ARGB8888转为Bitmap对象,从而保存为jpeg格式的图片文件。
// 将ARGB8888原始数据转为Bitmap对象
Bitmap bitmap= Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(argb));
参考文献
https://developer.android.google.cn/reference/android/media/MediaCodec
https://github.com/google/grafika
Android Camera预览时输出的帧率控制
https://chromium.googlesource.com/libyuv/libyuv/
使用libyuv对YUV数据进行缩放,旋转,镜像,裁剪等操作
YUV图像的水印的添加
EasyPlayer一款精炼、高效、稳定的流媒体播放器
H264(NAL简介与I帧判断)
Android视频编解码相关推荐
- Android视频编解码之MediaCodec简单入门
本篇只是简单入门,后面会继续写文章详细讲解: 由于MediaCodec涉及内容众多,原本想一篇文章把所有内容概括,但是后来发现不太可能,限于自己能力,想要考虑全面太难,我也是刚开始学习需要借助网上的代 ...
- Android 音视频编解码 MediaCodec
MediaCodec 简介 Android中的MediaCodec是一个用于音视频编解码功能的API,使用它可以实现对音视频数据进行压缩.解压缩.编辑和转换.以下是MediaCodec的主要功能: 支 ...
- Android 音视频编解码(一) -- MediaCodec 初探
音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...
- Android 8.1 如何查看系统支持哪些音视频编解码格式
代码路径: frameworks/av/media/libstagefright/omx/SoftOMXPlugin.cpp 在SoftOMXPlugin.cpp文件中kComponents[]结构体 ...
- 视频编解码之理论概述 和即时通信
前言 即时通讯应用中的实时音视频技术,几乎是IM开发中的最后一道高墙.原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的.有关实时 ...
- MediaCodec在Android视频硬解码组件的应用
https://yq.aliyun.com/articles/632892 云栖社区> 博客列表> 正文 MediaCodec在Android视频硬解码组件的应用 cheenc 2018- ...
- AV1:为互联网提供开放、免费的视频编解码工具
从学术研究到进入工业界,Zoe Liu一直在算法和音视频领域,目前在谷歌编解码团队为编解码器AV1做开发支持.Zoe畅谈了评定编解码器的标准,以及AV1的最新进度.本文是『下一代编码器』系列采访之一, ...
- vpu测试_一种普适的手机平台vpu视频编解码性能检测方法
一种普适的手机平台vpu视频编解码性能检测方法 [专利摘要]本发明公开了一种普适手机平台的视频处理单元(VPU)的H.264视频编解码性能检测方法,包括:手机平台利用VPU进行H.264视频编解码的系 ...
- 深入浅出理解视频编解码技术
导读:随着移动互联网技术的蓬勃发展,视频已无处不在.视频直播.视频点播.短视频.视频聊天,已经完全融入了每个人的生活.Cisco 发布的最新报告中写道,到 2022 年,在移动互联网流量中,视频数据占 ...
最新文章
- 270亿参数、刷榜CLUE,阿里达摩院发布最大中文预训练语言模型PLUG(开放测试)...
- c++ const 关键字 学习笔记
- TCP/IP详解学习笔记(12)-TCP的超时与重传
- Imageloader4-ImageLoader中的变量
- 表达式封装和模型驱动封装的区别
- Oracle数据库的数据统计(Analyze)
- 2020年第十八届西电程序设计竞赛网络预选赛之Problem D 由比滨结衣的饼干(二分+前缀后缀)
- soapui 测试soap_使用SoapUI调用不同的安全WCF SOAP服务-基本身份验证,第一部分
- Spring 配置多个数据源,并实现动态切换
- YbSoftwareFactory 代码生成插件【九】:基于JQuery、WebApi的ASP.NET MVC插件的代码生成项目主要技术解析...
- leetcode------Binary Tree Level Order Traversal II
- 【挖坑系列】关于浏览器の缓存机制
- 6个常见的API接口在线管理平台
- 【BJOI2019】勘破神机【数论】
- 中国人是怎样移民到日本,拿到长期居留身份的呢?
- 我的IT成长路——为梦想扬帆起航
- 【软件测试】数据库大厂面试真题解析(二叉树算法纯干货!)
- php 跨站脚本,Piwigo register.php页面多个跨站脚本漏洞
- cocos xcode9 system 废除 xcode9 'system' is unavailable: not available on iOS
- 499张WEBP格式动漫图片
热门文章
- 电子计算机上C1的功能,发布会现场实测搜狗智能录音笔C1:录音转文字效果出人意料...
- Spring的生态圈、Spring全家桶
- Nginx配置+转发8080端口+页面静态缓存+https配置
- 赛尔号什么时候支持html5,赛尔号:这几只精灵必须拥有!无关强度,只因经典情怀!...
- mysql 无我 非我_无我的源头,《庄子•齐物论》原文朗读。非彼无我,非我无所取。...
- 洛谷:ISBN号码(c语言)
- Rockchip开发系列 - 4.2.Uart问题汇总
- Android KTX与Kotlin Android Extensions
- 三国志战略版:S9血刃开荒实录五_血刃实战
- 2022浙江网络安全大赛