MediaCodec 也能播视频?
2020-12-14 本文已影响0人
毛先森
目录
- 是什么
- 重要方法
- 解码
- 最佳实践:解码 h264 进行播放
- 总结
是什么
MediaCodec 是 Android 原生提供的音视频编解码框架,由于直接与硬件交互(dsp),所以是以硬件编解码的方式工作
重要方法
- 创建编解码器 createDecoderByType(int type)、createEncoderByType(int type)
- 创建多媒体格式 createVideoFormat,主要有帧率、帧宽高等信息,用于 MediaCodec.configure() 配置初始化信息
- 初始化配置信息 configure(),设置格式,surface,加密,标志
- start()启动
- 获取所有 ByteBuffer 输入队列 dequeueInputBuffer(),该队列是由 dsp 内部维护,大小固定,我们先获取所有队列,然后判断哪些是当前未被占用的,如果返回为 -1 ,则没有可用的
- 获取输出 ByteBuffer 的索引 dequeueOutputBuffer(),这里的输出队列的索引和输入队列并不一一对应,所以我们先去获取到输出的索引
- 获取 ByteBuffer 中的数据内容 releaseOutputBuffer()
解码
将 h264等编码数据转换为 YUV 等原始数据,如果我们怎样手动把 h264 数据渲染到 SurfaceView 上呢?
- 创建 SurfaceView 控件,并获取到 Surface
- 初始化 MediaCodec ,获取解码器并且配置
- 获取文件路径,将文件转换为 byte 数组
- 获取当前可用的 ByteBuffer,并填入原始数据
- 获取输出 ByteBuffer,取出数据
- MediaCodec 初始化时与 Surface 绑定,只要在 releaseOutputBuffer() 中开启渲染,此时 SurfaceView 在就能正常出现解码后的视频数据
最佳实践
- 目标: 使用 MediaCodec 解码本地 h264 文件,并且播放视频到 SurfaceView
- 通过这个实践,能够对 MediaCodec 的解码流程有个基本了解
// 解码工具类
public class H264Player implements Runnable {
// 本地 h264 文件路径
private String path;
private Surface surface;
private MediaCodec mediaCodec;
private Context context;
public H264Player(Context context, String path, Surface surface) {
this.context = context;
this.path = path;
this.surface = surface;
try {
this.mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
// 视频宽高暂时写死
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 368, 384);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
mediaCodec.configure(mediaFormat, surface, null, 0);
} catch (IOException e) {
// 解码芯片不支持,走软解
e.printStackTrace();
}
}
public void play() {
mediaCodec.start();
new Thread(this::run).start();
}
@Override
public void run() {
// 解码 h264
decodeH264();
}
private void decodeH264() {
byte[] bytes = null;
try {
bytes = getBytes(path);
} catch (IOException e) {
e.printStackTrace();
}
// 获取队列
ByteBuffer[] byteBuffers = mediaCodec.getInputBuffers();
int startIndex = 0;
int nextFrameStart;
int totalCount = bytes.length;
while (true) {
if (startIndex >= totalCount) {
break;
}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
nextFrameStart = findFrame(bytes, startIndex+1,totalCount);
// 往 ByteBuffer 中塞入数据
int index = mediaCodec.dequeueInputBuffer(10 * 1000);
Log.e("index",index+"");
// 获取 dsp 成功
if (index >= 0) {
// 拿到可用的 ByteBuffer
ByteBuffer byteBuffer = byteBuffers[index];
byteBuffer.clear();
byteBuffer.put(bytes, startIndex, nextFrameStart - startIndex);
// 识别分隔符,找到分隔符对应的索引
mediaCodec.queueInputBuffer(index, 0, nextFrameStart - startIndex, 0, 0);
startIndex = nextFrameStart;
}else {
continue;
}
// 从 ByteBuffer 中获取解码好的数据
int outIndex = mediaCodec.dequeueOutputBuffer(info,10 * 1000);
if (outIndex > 0){
try {
Thread.sleep(33);
} catch (InterruptedException e) {
e.printStackTrace();
}
mediaCodec.releaseOutputBuffer(outIndex, true);
}
}
}
private int findFrame(byte[] bytes, int startIndex, int totalSize) {
for (int i = startIndex; i < totalSize - 4; i++) {
if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01) {
return i;
}
}
return -1;
}
/**
* 一次性读取文件
*
* @param path
* @return
* @throws IOException
*/
public byte[] getBytes(String path) throws IOException {
InputStream is = new DataInputStream(new FileInputStream(new File(path)));
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;
}
}
在 Activity 中播放
public class MainActivity extends AppCompatActivity {
private H264Player h264Player;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
checkPermission();
initSurface();
}
private void initSurface() {
SurfaceView surfaceView = findViewById(R.id.surface);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
h264Player = new H264Player(MainActivity.this, new File(Environment.getExternalStorageDirectory(), "test.h264").getAbsolutePath(), surfaceHolder.getSurface());
h264Player.play();
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
});
}
private boolean checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
}, 1);
}
return false;
}
}
总结
这次我们播放的 h264 文件,需要先使用 FFmpeg 从 mp4 文件分离出 h264,放到手机储存卡根目录(也可以放到本地其他文件夹,文件路径对应修改就好了),通过创建 MediaCodec 解码器,绑定 Surface,获取 ByteBuffer 等一系列操作,最终实现了手动解码视频文件,并且渲染到 SurfaceView,这个 Demo 主要用于熟悉 Api,不适宜用于实际工业级别开发(比如整个读取文件是不可取的)