在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》。本文是Android音视频任务列表的其中一个, 对应的要学习的内容是:学习MediaCodec API,完成视频H.264的解码。(本文是最基本的H264的解码,进阶内容以后会讲解)
音视频任务列表: 点击此处跳转查看.
视频是由一帧帧图像组成,就如常见的gif图片,如果打开一张gif图片,可以发现里面是由很多张图片组成。一般视频为了不让观众感觉到卡顿,一秒钟至少需要24帧画面(一般是30帧),假如该视频是一个1280x720分辨率的视频,那么不经过编码一秒钟的大小:
结果:1280x720x4x24/(1024*1024)≈84.375M
所以不经过编码的视频根本没法保存,更不用说传输了。
视频中存在很多冗余信息,比如图像相邻像素之间有较强的相关性,视频序列的相邻图像之间内容相似,人的视觉系统对某些细节不敏感等,对这部分冗余信息进行处理的过程就是视频编码。
视频编码主要分为H.26X系列和MPEG-X系列,这篇文章主要讲解大名鼎鼎的H.264
H.264:H.264/MPEG-4第十部分,或称AVC(Advanced Video Coding,高级视频编码),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。
对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧A,随后的B帧就不编码全部图像,只写入与A帧的差别,这样B帧的大小就只有完整帧的1/10或更小,B帧之后的C帧如果变化不大,我们可以继续以参考B的方式编码C帧,这样循环下去。如果发现D帧与C帧差异很大,那就先编码出一个完整的图像帧D,继续重复之前过程。
在H.264中定义了三种帧:
I帧:完整编码的帧叫I帧
P帧:参考之前的I帧生成的只包含差异部分编码的帧叫P帧
B帧:参考前后的帧编码的帧叫B帧
在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符
遍历H.264数据流,获取H.264的每一帧,如果把每一帧填充到MediaCodec,MediaCodec进行相应处理,然后把处理的数据交给控件进行展示或者做一些其他操作
如果不熟悉MediaCodec,请查看: Android音视频开发基础(五):学习MediaCodec API,完成音频AAC硬编、硬解
MediaCodec工作流程一句话总结:把数据填充到MediaCodec,MediaCodec进行相应处理,然后把处理的数据交给控件进行展示或者做一些其他操作
(1)Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]
(2)Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer]
(3)MediaCodec 从 input 缓冲区队列取一帧数据进行编解码处理
(4)处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列
(5)Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]
(6)Client 对编解码后的 buffer 进行渲染/播放
(7)渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]
以上过程为官方提供的,下面讲解一下我理解的MediaCodec的过程
图中1,2,3,4,5,6,7表示各种步骤
(1)获取一组缓冲区队列(8个)
(2)得到可用的缓存区,通过MediaCodec.dequeueInputBuffer(int time)得到,参数为等待的时间,超过该时间,则获取不到缓冲区
(3)将一帧数据传入缓冲区,通过MediaCodec.queueInputBuffer();实现
(4)把数据传递给解码器,进行解码
(5)得到一个新的缓冲区
(6)将处理的数据交给新的缓冲区
(7)将处理的数据交给SurfaceView显示
将源代码中res\raw\hh264.h264放入手机根目录下,源代码地址在文章最后
activity_main:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/SurfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
public class MainActivity extends AppCompatActivity {
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private Thread mDecodeThread;
private MediaCodec mMediaCodec;
private DataInputStream mInputStream;
private final static String SD_PATH = Environment.getExternalStorageDirectory().getPath();
private final static String H264_FILE = SD_PATH + "/hh264.h264";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取权限
verifyStoragePermissions(this);
mSurfaceView = (SurfaceView) findViewById(R.id.SurfaceView);
// 获取文件输入流
getFileInputStream();
// 初始化解码器
initMediaCodec();
}
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"};
public static void verifyStoragePermissions(Activity activity) {
try {
// 检测是否有写的权限
int permission = ActivityCompat.checkSelfPermission(activity,
"android.permission.WRITE_EXTERNAL_STORAGE");
if (permission != PackageManager.PERMISSION_GRANTED) {
// 没有写的权限,去申请写的权限,会弹出对话框
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取需要解码的文件流
*/
public void getFileInputStream() {
try {
File file = new File(H264_FILE);
mInputStream = new DataInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
try {
mInputStream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
/**
* 获得可用的字节数组
* @param is
* @return
* @throws IOException
*/
public static byte[] getBytes(InputStream is) throws IOException {
int len;
int size = 1024;
byte[] buf;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
buf = new byte[size];
while ((len = is.read(buf, 0, size)) != -1) {
// 将读取的数据写入到字节输出流
bos.write(buf, 0, len);
}
// 将这个流转换成字节数组
buf = bos.toByteArray();
return buf;
}
/**
* 初始化解码器
*/
private void initMediaCodec() {
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
// 创建编码器
mMediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
} catch (IOException e) {
e.printStackTrace();
}
// 使用MediaFormat初始化编码器,设置宽,高
final MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height());
// 设置帧率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 40);
// 配置编码器
mMediaCodec.configure(mediaFormat, holder.getSurface(), null, 0);
startDecodingThread();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
}
/**
* 开启解码器并开启读取文件的线程
*/
private void startDecodingThread() {
mMediaCodec.start();
mDecodeThread = new Thread(new DecodeThread());
mDecodeThread.start();
}
private class DecodeThread implements Runnable {
@Override
public void run() {
// 开始解码
decode();
}
private void decode() {
// 获取一组缓存区(8个)
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
// 解码后的数据,包含每一个buffer的元数据信息
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
// 获取缓冲区的时候,需要等待的时间(单位:毫秒)
long timeoutUs = 10000;
byte[] streamBuffer = null;
try {
// 返回可用的字节数组
streamBuffer = getBytes(mInputStream);
} catch (IOException e) {
e.printStackTrace();
}
int bytes_cnt = 0;
// 得到可用字节数组长度
bytes_cnt = streamBuffer.length;
// 没有得到可用数组
if (bytes_cnt == 0) {
streamBuffer = null;
}
// 每帧的开始位置
int startIndex = 0;
// 定义记录剩余字节的变量
int remaining = bytes_cnt;
// while(true)大括号内的内容是获取一帧,解码,然后显示;直到获取最后一帧,解码,结束
while (true) {
// 当剩余的字节=0或者开始的读取的字节下标大于可用的字节数时 不在继续读取
if (remaining == 0 || startIndex >= remaining) {
break;
}
// 寻找帧头部
int nextFrameStart = findHeadFrame(streamBuffer, startIndex + 2, remaining);
// 找不到头部返回-1
if (nextFrameStart == -1) {
nextFrameStart = remaining;
}
// 得到可用的缓存区
int inputIndex = mMediaCodec.dequeueInputBuffer(timeoutUs);
// 有可用缓存区
if (inputIndex >= 0) {
ByteBuffer byteBuffer = inputBuffers[inputIndex];
byteBuffer.clear();
// 将可用的字节数组(一帧),传入缓冲区
byteBuffer.put(streamBuffer, startIndex, nextFrameStart - startIndex);
// 把数据传递给解码器
mMediaCodec.queueInputBuffer(inputIndex, 0, nextFrameStart - startIndex, 0, 0);
// 指定下一帧的位置
startIndex = nextFrameStart;
} else {
continue;
}
int outputIndex = mMediaCodec.dequeueOutputBuffer(info, timeoutUs);
if (outputIndex >= 0) {
// 加入try catch的目的是让界面显示的慢一点,这个步骤可以省略
try {
Thread.sleep(33);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将处理过的数据交给surfaceview显示
mMediaCodec.releaseOutputBuffer(outputIndex, true);
}
}
}
}
/**
* 查找帧头部的位置
* 在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符
* @param bytes
* @param start
* @param totalSize
* @return
*/
private int findHeadFrame(byte[] bytes, int start, int totalSize) {
for (int i = start; i < totalSize - 4; i++) {
if (((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x00) && (bytes[i + 3] == 0x01)) || ((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x01))) {
return i;
}
}
return -1;
}
}
(1)加入权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
(2)解码是耗时操作,所以需要放在子线程中
源代码地址:
Android音视频开发基础(六):学习MediaCodec API,完成视频H264的解码