序言
本篇是有关音视频学习系列中的H264 / H265的解码视频部分,文章大部分记录直接上干货,编码原理基础部分【音视频学习H264系列:H264视频编码原理】后续再补上。欢迎留言讨论。
使用MediaCodec 解码H264/H265码流视频,那必须谈下MediaCodec这个神器。附官网数据流程图如下:
input:ByteBuffer输入方;
output:ByteBuffer输出方;
- 使用者从MediaCodec请求一个空的输入buffer(ByteBuffer),填充满数据后将它传递给MediaCodec处理。
- MediaCodec处理完这些数据并将处理结果输出至一个空的输出buffer(ByteBuffer)中。
- 使用者从MediaCodec获取输出buffer的数据,消耗掉里面的数据,使用完输出buffer的数据之后,将其释放回编解码。
H264码流解码示例代码如下(基本都做了注释)
package com.zqfdev.h264decodedemo;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* @author zhangqingfa
* @createDate 2020/12/10 11:39
* @description 解码H264播放
*/
public class H264DeCodePlay {
private static final String TAG = "zqf-dev";
//视频路径
private String videoPath;
//使用android MediaCodec解码
private MediaCodec mediaCodec;
private Surface surface;
H264DeCodePlay(String videoPath, Surface surface) {
this.videoPath = videoPath;
this.surface = surface;
initMediaCodec();
}
private void initMediaCodec() {
try {
Log.e(TAG, "videoPath " + videoPath);
//创建解码器 H264的Type为 AAC
mediaCodec = MediaCodec.createDecoderByType("video/avc");
//创建配置
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 540, 960);
//设置解码预期的帧速率【以帧/秒为单位的视频格式的帧速率的键】
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
//配置绑定mediaFormat和surface
mediaCodec.configure(mediaFormat, surface, null, 0);
} catch (IOException e) {
e.printStackTrace();
//创建解码失败
Log.e(TAG, "创建解码失败");
}
}
/**
* 解码播放
*/
void decodePlay() {
mediaCodec.start();
new Thread(new MyRun()).start();
}
private class MyRun implements Runnable {
@Override
public void run() {
try {
//1、IO流方式读取h264文件【太大的视频分批加载】
byte[] bytes = null;
bytes = getBytes(videoPath);
Log.e(TAG, "bytes size " + bytes.length);
//2、拿到 mediaCodec 所有队列buffer[]
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
//开始位置
int startIndex = 0;
//h264总字节数
int totalSize = bytes.length;
//3、解析
while (true) {
//判断是否符合
if (totalSize == 0 || startIndex >= totalSize) {
break;
}
//寻找索引
int nextFrameStart = findByFrame(bytes, startIndex + 1, totalSize);
if (nextFrameStart == -1) break;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
// 查询10000毫秒后,如果dSP芯片的buffer全部被占用,返回-1;存在则大于0
int inIndex = mediaCodec.dequeueInputBuffer(10000);
if (inIndex >= 0) {
//根据返回的index拿到可以用的buffer
ByteBuffer byteBuffer = inputBuffers[inIndex];
//清空缓存
byteBuffer.clear();
//开始为buffer填充数据
byteBuffer.put(bytes, startIndex, nextFrameStart - startIndex);
//填充数据后通知mediacodec查询inIndex索引的这个buffer,
mediaCodec.queueInputBuffer(inIndex, 0, nextFrameStart - startIndex, 0, 0);
//为下一帧做准备,下一帧首就是前一帧的尾。
startIndex = nextFrameStart;
} else {
//等待查询空的buffer
continue;
}
//mediaCodec 查询 "mediaCodec的输出方队列"得到索引
int outIndex = mediaCodec.dequeueOutputBuffer(info, 10000);
Log.e(TAG, "outIndex " + outIndex);
if (outIndex >= 0) {
try {
//暂时以休眠线程方式放慢播放速度
Thread.sleep(33);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果surface绑定了,则直接输入到surface渲染并释放
mediaCodec.releaseOutputBuffer(outIndex, true);
} else {
Log.e(TAG, "没有解码成功");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//读取一帧数据
private int findByFrame(byte[] bytes, int start, int totalSize) {
for (int i = start; i < totalSize - 4; i++) {
//对output.h264文件分析 可通过分隔符 0x00000001 读取真正的数据
if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01) {
return i;
}
}
return -1;
}
private byte[] getBytes(String videoPath) throws IOException {
InputStream is = new DataInputStream(new FileInputStream(new File(videoPath)));
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;
}
}
H265示例代码如下
package com.zqfdev.h264decodedemo;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* @author zhangqingfa
* @createDate 2020/12/10 11:39
* @description 解码H264播放
*/
public class H265DeCodePlay {
private static final String TAG = "zqf-dev";
//视频路径
private String videoPath;
//使用android MediaCodec解码
private MediaCodec mediaCodec;
private Surface surface;
H265DeCodePlay(String videoPath, Surface surface) {
this.videoPath = videoPath;
this.surface = surface;
initMediaCodec();
}
private void initMediaCodec() {
try {
Log.e(TAG, "videoPath " + videoPath);
//创建解码器 H264的Type为 AAC
mediaCodec = MediaCodec.createDecoderByType("video/hevc");
//创建配置
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/hevc", 368, 384);
//设置解码预期的帧速率【以帧/秒为单位的视频格式的帧速率的键】
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
//配置绑定mediaFormat和surface
mediaCodec.configure(mediaFormat, surface, null, 0);
} catch (IOException e) {
e.printStackTrace();
//创建解码失败
Log.e(TAG, "创建解码失败");
}
}
/**
* 解码播放
*/
void decodePlay() {
mediaCodec.start();
new Thread(new MyRun()).start();
}
private class MyRun implements Runnable {
@Override
public void run() {
try {
//1、IO流方式读取h264文件【太大的视频分批加载】
byte[] bytes = null;
bytes = getBytes(videoPath);
Log.e(TAG, "bytes size " + bytes.length);
//2、拿到 mediaCodec 所有队列buffer[]
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
//开始位置
int startIndex = 0;
//h264总字节数
int totalSize = bytes.length;
//3、解析
while (true) {
//判断是否符合
if (totalSize == 0 || startIndex >= totalSize) {
break;
}
//寻找索引
int nextFrameStart = findByFrame(bytes, startIndex + 1, totalSize);
if (nextFrameStart == -1) break;
Log.e(TAG, "nextFrameStart " + nextFrameStart);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
// 查询10000毫秒后,如果dSP芯片的buffer全部被占用,返回-1;存在则大于0
int inIndex = mediaCodec.dequeueInputBuffer(10000);
if (inIndex >= 0) {
//根据返回的index拿到可以用的buffer
ByteBuffer byteBuffer = inputBuffers[inIndex];
//清空byteBuffer缓存
byteBuffer.clear();
//开始为buffer填充数据
byteBuffer.put(bytes, startIndex, nextFrameStart - startIndex);
//填充数据后通知mediacodec查询inIndex索引的这个buffer,
mediaCodec.queueInputBuffer(inIndex, 0, nextFrameStart - startIndex, 0, 0);
//为下一帧做准备,下一帧首就是前一帧的尾。
startIndex = nextFrameStart;
} else {
//等待查询空的buffer
continue;
}
//mediaCodec 查询 "mediaCodec的输出方队列"得到索引
int outIndex = mediaCodec.dequeueOutputBuffer(info, 10000);
Log.e(TAG, "outIndex " + outIndex);
if (outIndex >= 0) {
try {
//暂时以休眠线程方式放慢播放速度
Thread.sleep(33);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果surface绑定了,则直接输入到surface渲染并释放
mediaCodec.releaseOutputBuffer(outIndex, true);
} else {
Log.e(TAG, "没有解码成功");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//读取一帧数据
private int findByFrame(byte[] bytes, int start, int totalSize) {
for (int i = start; i < totalSize - 4; i++) {
//对output.h264文件分析 可通过分隔符 0x00000001 读取真正的数据
if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01) {
return i;
}
}
return -1;
}
private byte[] getBytes(String videoPath) throws IOException {
InputStream is = new DataInputStream(new FileInputStream(new File(videoPath)));
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;
}
}
MainActivity代码如下
package com.zqfdev.h264decodedemo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.File;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
public class MainActivity extends AppCompatActivity {
private String[] permiss = {"android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE"};
private H264DeCodePlay h264DeCodePlay;
// private H265DeCodePlay h265DeCodePlay;
private String videoPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
checkPermiss();
initView();
}
private void checkPermiss() {
int code = ActivityCompat.checkSelfPermission(this, permiss[0]);
if (code != PackageManager.PERMISSION_GRANTED) {
// 没有写的权限,去申请写的权限
ActivityCompat.requestPermissions(this, permiss, 11);
}
}
private void initView() {
File dir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
if (!dir.exists()) dir.mkdirs();
final File file = new File(dir, "output.h264");
// final File file = new File(dir, "output.h265");
if (!file.exists()) {
Log.e("Tag", "文件不存在");
return;
}
videoPath = file.getAbsolutePath();
final SurfaceView surface = findViewById(R.id.surface);
final SurfaceHolder holder = surface.getHolder();
holder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
h264DeCodePlay = new H264DeCodePlay(videoPath, holder.getSurface());
h264DeCodePlay.decodePlay();
// h265DeCodePlay = new H265DeCodePlay(videoPath, holder.getSurface());
// h265DeCodePlay.decodePlay();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
}
});
}
}
测试的H264 / H265码流视频通过FFmpeg抽取可得到。
命令行:
ffmpeg -i 源视频.mp4 -codec copy -bsf: h264_mp4toannexb -f h264 输出视频.h264
效果如下: