最近由于项目需要,开始接触音视频流编解码的工作,以前一直使用开源的视频播放器框架,项目都是API堆叠,总是知其然不知其所以然。直到现在使用原生API自己实现,才发现各种坑需要自己去踩,踩完了就学到了更多。好记性不如烂笔头,所以下定决心开始记录自己的踩坑历程,帮助自己也帮助有需要的朋友。闲话不多说,开始干正事,目前的需求是解码远程服务端发过来的H264视频流,数据以byte数组的方式一帧一帧发过来的,所以常用的基于地址的开源播放器并不能满足我的需求,于是就有了下面的事情。
同步解码
public class Decoder {
private static final StringTAG ="Decoder";
private static final int MAX_QUEUE_SIZE =1000;
private ArrayBlockingQueuequeue =new ArrayBlockingQueue<>(MAX_QUEUE_SIZE);
private boolean isRunning =false;
private MediaCodecmediaCodec;
private static final StringTYPE_AVC ="video/avc";
private int width=640, height=480;
private int framerate =24;
private int bitrate =width *height *framerate *8;
private Surfacesurface =null;
public void setSurface(Surface surface) {
this.surface = surface;
}
public void createDecoder(){
MediaFormat mediaFormat = MediaFormat.createVideoFormat(TYPE_AVC, width, height);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
try {
mediaCodec = MediaCodec.createDecoderByType(TYPE_AVC);
}catch (IOException e) {
e.printStackTrace();
}
mediaCodec.configure(mediaFormat, surface, null, 0);
mediaCodec.start();
}
public void putData(byte[] data){
if(queue.size()>=MAX_QUEUE_SIZE){
queue.poll();
}
queue.add(data.clone());
}
private void stopDecoder(){
mediaCodec.stop();
mediaCodec.release();
}
public void stopDecoderThread(){
isRunning =false;
stopDecoder();
}
public void startDecoderThread(){
Thread decoderThread =new Thread(new Runnable() {
@Override
public void run() {
isRunning =true;
byte[] input =null;
while (isRunning){
if(queue.size()>0){
input =queue.poll();
}
if (input!=null){
try {
int inputBufferIndex =mediaCodec.dequeueInputBuffer(0);
ByteBuffer inputBuffer;
if(inputBufferIndex>=0){
inputBuffer = getInputBuffer(mediaCodec, inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(input, 0, input.length);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length,
computePresentationTime(inputBufferIndex), 0);
}
MediaCodec.BufferInfo bufferInfo =new MediaCodec.BufferInfo();
int outputBufferIndex =mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
while (outputBufferIndex>=0){
mediaCodec.releaseOutputBuffer(outputBufferIndex, true);
outputBufferIndex =mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}catch (Exception e){
e.printStackTrace();
}
}else{
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
decoderThread.start();
}
private long computePresentationTime(long frameIndex){
return 132+frameIndex*1000000/framerate;
}
private ByteBuffergetInputBuffer(MediaCodec codec, int index){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
return codec.getInputBuffer(index);
}else{
return codec.getInputBuffers()[index];
}
}
}
由于视频流是一帧一帧的发的,所以为了更流畅地播放,我选择了一个队列来缓存数据,然后解码线程一直从缓存队列获取帧数据进行解码渲染。
private Decoderdecoder =new Decoder();
surfaceView = findViewById(R.id.surfaceVideo);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.i(TAG, "surfaceCreated!");
decoder.setSurface(holder.getSurface());
decoder.createDecoder();
decoder.startDecoderThread();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.i(TAG, "surfaceDestroyed!");
decoder.stopDecoderThread();
}
});
解码器的生命周期管理看需求来处理,我这里是初始化完成后就直接开启解码。当然一开始是没有任何数据的,因为缓存队列是空的,我们还没往里面存数据。然后数据写入就很简单了,直接调用下面的方法就可以往里存数据了:
decoder.putData(data);
异步解码
public class AsyncDecoder {
private static final StringTAG ="AsyncDecoder";
private MediaFormatmediaFormat;
private MediaCodecmediaCodec;
private static final StringTYPE_AVC ="video/avc";
private int width=640, height=480;
private int framerate =24;
private int bitrate =width *height *framerate *8;
private static final int MAX_QUEUE_SIZE =1000;
private ArrayBlockingQueue =new ArrayBlockingQueue<>(MAX_QUEUE_SIZE);
private Surfacesurface;
public void createDecoder(){
try {
mediaCodec = MediaCodec.createDecoderByType(TYPE_AVC);
mediaFormat = MediaFormat.createVideoFormat(TYPE_AVC, width, height);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
}catch (IOException e) {
e.printStackTrace();
}
}
public void putData(byte[] data){
if(queue.size()>=MAX_QUEUE_SIZE){
queue.poll();
}
queue.add(data.clone());
}
public void setSurface(Surface surface) {
this.surface = surface;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void decode(){
mediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
ByteBuffer decoderInputBuffer =mediaCodec.getInputBuffer(index);
decoderInputBuffer.clear();
byte[] input =queue.poll();
if(input!=null) {
decoderInputBuffer.limit(input.length);
decoderInputBuffer.put(input, 0, input.length);
mediaCodec.queueInputBuffer(index, 0, input.length, computePresentationTime(index), 0);
}
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
mediaCodec.releaseOutputBuffer(index, true);
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
mediaCodec.reset();
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
}
});
mediaCodec.configure(mediaFormat, surface, null, 0);
mediaCodec.start();
}
private long computePresentationTime(long frameIndex){
return 132+frameIndex*1000000/framerate;
}
public void stop(){
mediaCodec.stop();
mediaCodec.release();
mediaFormat =null;
mediaCodec =null;
}
}
Android5.0之后推出了异步的方式解码,官方推荐我们使用这种方式,所以我又尝试了一下异步的方式。可是跑起来之后就遇到了一问题,这个onInputBufferAvailable方法在一开始没有喂数据的情况下,只回调4次就不回调了,为此让我很苦恼。于是我各种百度各种查,才找到问题所在,成功跑起来,将原来的代码:
if(input!=null) {
decoderInputBuffer.limit(input.length);
decoderInputBuffer.put(input, 0, input.length);
mediaCodec.queueInputBuffer(index, 0, input.length, computePresentationTime(index), 0);
}
改成:
if(input!=null) {
decoderInputBuffer.limit(input.length);
decoderInputBuffer.put(input, 0, input.length);
mediaCodec.queueInputBuffer(index, 0, input.length, computePresentationTime(index), 0);
}else{
mediaCodec.queueInputBuffer(index, 0, 0, computePresentationTime(index), 0);
}
就是在没有数据的时候,也需要给解码器送一个长度为0的数据进去,解码器才不会罢工,别问我为什么,问就是不知道。调用的方式是一样的,就不贴代码了,至此同步和异步解码的方式就完成了,后面还会持续更新优化,有什么意见和建议可以评论区告诉我。