Android视频相关学习
初步学习视频相关的东西,看了一些源码,讲解之类的东西,对于整个一个流程其实并不清楚。因此一步一步来,先看视频的播放,过去都是使用三方播放器或是系统自带的,或是videoview这样的控件实现,而mediacodec这类API之前没有用过,编解码相关的基础知识也只是看了一些理论框架性的东西。
因此首先从最基本,最简单的功能,播放一个视频的实现流程着手来理解对我本身会很有帮助,其他一些功能,如缩放也好,camera作为源头,美颜等等其实都是在这套流程的基础上。
先放代码:
package com.lawson.videodemo;
import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
public class MainActivity extends AppCompatActivity {
private TextureView mTextureView;
private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextureView = findViewById(R.id.movie_texture_view);
}
public void clickPlayStop(@SuppressWarnings("unused") View unused) {
SurfaceTexture st = mTextureView.getSurfaceTexture();
final Surface surface = new Surface(st);
new Thread(new Runnable() {
@Override public void run() {
decode(surface);
}
}).start();
}
public void decode(Surface surface) {
MediaExtractor extractor = null;
MediaCodec decoder = null;
try {
extractor = new MediaExtractor();
extractor.setDataSource(new File(getFilesDir(), "VID_20190418_091844271.mp4").toString());
int numTracks = extractor.getTrackCount();
int trackIndex = -1;
for (int i = 0; i < numTracks; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
trackIndex = i;
break;
}
}
if (trackIndex < 0) {
throw new RuntimeException("No video track found");
}
extractor.selectTrack(trackIndex);
MediaFormat format = extractor.getTrackFormat(trackIndex);
String mime = format.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, surface, null, 0);
decoder.start();
processQueue(extractor, decoder);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (decoder != null) {
decoder.stop();
decoder.release();
decoder = null;
}
if (extractor != null) {
extractor.release();
extractor = null;
}
}
}
private void processQueue(MediaExtractor extractor, MediaCodec decoder) {
boolean outputDone = false;
boolean inputDone = false;
while (!outputDone) {
if (!inputDone) {
int inputBufIndex = decoder.dequeueInputBuffer(10000);
if (inputBufIndex >= 0) {
ByteBuffer inputBuf = decoder.getInputBuffer(inputBufIndex);
int chunkSize = extractor.readSampleData(inputBuf, 0);
if (chunkSize < 0) {
decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
} else {
long presentationTimeUs = extractor.getSampleTime();
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
presentationTimeUs, 0 /*flags*/);
extractor.advance();
}
}
}
if (!outputDone) {
int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, 10000);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
} else if (decoderStatus < 0) {
throw new RuntimeException(
"unexpected result from decoder.dequeueOutputBuffer: " +
decoderStatus);
} else { // decoderStatus >= 0
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
outputDone = true;
}
decoder.releaseOutputBuffer(decoderStatus, (mBufferInfo.size != 0));
}
}
}
}
}
这块代码是从grafika里扣出来的,是最基本的播放视频的流程。
它分为几个步骤:
- 首先是画布的初始化准备,即先安排好视频播放的区域,xml里面使用的是TextureView,而初始化工作如代码:
SurfaceTexture st = mTextureView.getSurfaceTexture();
final Surface surface = new Surface(st);
换句话说,后续并不需要TextureView的参与,而是需要Surface,后面可以看到是MediaCodec需要它。从另一个角度来说,我们可以得知视频最终无论经谁处理都是需要展示在这个Surface上的。
- 第二,配置MediaExtractor
为什么要配置这个类,因为它是专门用来与源头打交道的,可以这样理解,上一步是把画布准备好,这一步是把播放源准备好。关键代码如下:
extractor = new MediaExtractor();
extractor.setDataSource(new File(getFilesDir(), "VID_20190418_091844271.mp4").toString());
int numTracks = extractor.getTrackCount();
int trackIndex = -1;
for (int i = 0; i < numTracks; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
trackIndex = i;
break;
}
}
if (trackIndex < 0) {
throw new RuntimeException("No video track found");
}
extractor.selectTrack(trackIndex);
首先是初始化MediaExtractor,设置视频的路径作为播放源,接下来是获取视频轨道。从代码上看,视频的轨道只会有一条,因为遍历到第一个就退出遍历了。最后在MediaExtractor里选中这个轨道,这句代码的意义在于后续的读数据也是这个类对象来做,它只会读取选中的轨道数据,选中另一个音频轨道的话,后续做读取操作也只会读取这个音频轨道的数据。
轨道与视频的内容有关系,可以看成,视频文件里除了视频数据,肯定还有声音数据,这两者肯定是不能搞混的,因此为了方便区分与读取,就设置了轨道的概念。
- 第三,开始解码
这里MediaCodec就开始做解码的工作了。
MediaFormat format = extractor.getTrackFormat(trackIndex);
String mime = format.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, surface, null, 0);
decoder.start();
首先获取上一步选中轨道的一些信息,包括MediaFormat,然后通过得到的mime类型进行初始化MediaCodec,然后将format,surface等传入。到这里,画布就与解码器搭上线了,那么一旦解码器完成相应的解码工作就会直接将数据传入surface进行渲染,而这个工作我们不用参与。最后,解码器自己开始工作。
- 最后,搬运数据
最后这一步要分成两个部分来说,第一个部分主要处理把视频数据从视频文件传到解码器里面,第二个部分主要是做结束工作的。
boolean outputDone = false;
boolean inputDone = false;
while (!outputDone) {
if (!inputDone) {
int inputBufIndex = decoder.dequeueInputBuffer(10000);
if (inputBufIndex >= 0) {
ByteBuffer inputBuf = decoder.getInputBuffer(inputBufIndex);
int chunkSize = extractor.readSampleData(inputBuf, 0);
if (chunkSize < 0) {
decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
} else {
long presentationTimeUs = extractor.getSampleTime();
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
presentationTimeUs, 0 /*flags*/);
extractor.advance();
}
}
}
if (!outputDone) {
int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, 10000);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
} else if (decoderStatus < 0) {
throw new RuntimeException(
"unexpected result from decoder.dequeueOutputBuffer: " +
decoderStatus);
} else { // decoderStatus >= 0
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
outputDone = true;
}
decoder.releaseOutputBuffer(decoderStatus, (mBufferInfo.size != 0));
}
}
}
首先是一个死循环,目的是不断的读取视频数据直到读完为止。
先说第一部分,即输入部分,这里的输入是指的从视频文件向解码器输入的意思,if (!inputDone) 这句判断可以看出,也是读到没有数据了为止,另外纵观这一部分代码可以看到解码器内部有维护两个buffer,一个用于存放输入数据一个用于存放输出数据。而这一步的逻辑都是围绕这两个buffer展开的。第一部分首先把缓冲的有效索引拿到再根据索引获取对应的缓冲空间,接下来,将这个缓冲拿给MediaExtractor,通过它进行视频数据读取工作,读到数据都会给到这个缓冲,当缓冲读满了,就把这块缓冲还给解码器。这样一来,解码器就成功得到视频数据了。需要说明的是,这里之所以会要先拿到有效索引,我想主要还是因为有效缓冲的重复利用。最后视频数据读取完后就告知解码器。
第二部分相对简单,主要就是通过releaseOutputBuffer() 方法,将输出缓冲里面的数据渲染到surface上,同时释放对应的缓冲空间,if (!outputDone) 同第一部分一样,也是要到输出缓冲没有数据了为止。
还需要说明的是,这样看下来解码器整个分为三个部分,数据来源,处理,数据渲染。数据来源是开发人员的工作,需要进行配置,而处理和数据渲染都是解码器和系统完成的。
这里就说完了,了解了最基本的流程,接下来看其他的东西都会轻松很多,比如这个例子执行时会发现视频的速度太快,这就是因为还有一些处理工作需要做,但都不会离开这个流程。