使用MediaExtractor , MediaCodec对音视频的解码实现简单播放器

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43846184/article/details/96481807

使用MediaExtractor , MediaCodec对音视频的解码实现简单播放器

在上一篇博文中记录了Android音视频的可以直接使用MediaPlayer对音视频进行处理播放,了解播放音乐和视频所用的一些接口,同时搭配SeekBar,VideoView,SurfaceView等可实现一个简单的播放器。
而这篇文章主要是深入到MediaPlayer里面具体的实现,先来回顾一下音视频播放的原理图

在这里插入图片描述
对于一个本地视频文件不需要进行解协议,以封装格式数据导入。
解封装的步骤体现在MediaExtractor中,MediaExtractor从封装格式中分离获取不同format的数据(video/audio)
解码的步骤体现在MediaCodec中,MediaCodec对特定格式的数据进行解码获取音频原始数据,结合Surface和AudioTrack进行音视频播放

视频

1.MediaExtractor的使用

在音视频播放中MediaExtractor的使用主要是解封装。大致过程如下:
1.为MediaExtractor设置所要解封装的音视频数据

MediaExtractor() //只有一个无参构造函数
mMediaExtractor.setDataSource(String path)  //有其他多个重载方法,这里只列举其一

2.获取音频数据和视频数据
步骤1进行setDataSource设置了之后,MediaExtractor就可以分离出多个FormatTrack(可理解为上图分解为音频压缩数据和视频压缩数据两条Track),我们需要分别获取音频,视频所对应的Track。
该步骤所要用到的接口如下

MediaExtractor.getTrackCount() : int //获取解封装后数据流的数目
(上图为2,但视频文件不一定都为2,音频可以有多条(国语/英语/..)视频也可以有多条流(清晰度720p/1080p/..))


MediaExtractor.getTrackFormat(int TrackIndex) : MediaFormat
//获取索引为TrackIndex的数据流所对应的MediaFormat,有很多种format


MediaFormat.getString(MediaFormat.KEY_MIME) : String
//可获取该format所对应的数据类型(以键值对实现,KEY_MIME这个key所对应value记录的是该数据流类型,
视频以video/开头,音频以audio/开头)
// MediaFormat中有许多键值,各个键值对应着该数据的一些信息,比如数据类型,声道,采样率等等
//具体format要查阅MediaFormat的源码


MediaExtractor.selectTrack(int TrackIndex) : void
//这是主要将该extractor设置其将要提取(extract的中文意思就是提取/抽取)数据的Track
//即告诉extractor我要提取的是该track所对应的数据

实现代码如下

 for(int i=0;i<audioExtractor.getTrackCount();i++){
 //MediaFor
    if(audioExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME).startsWith("video/"))
    {                                                                         //   "audio/"则表示获取音频流对应的Track
         TrackIndex = i;
         break;
    }
 }
 // 获取到了video所对应的format,因此可以从这个videoFormat中通过key获取想要的该视频压缩数据的信息
 //具体有什么key,可以查阅MediaFormat的源码
 MediaFormat videoForma t= audioExtractor.getTrackFormat(TrackIndex);
 videoExtractor.selectTrack(int TrackIndex) 

2.MediaCodec的使用

在音视频播放中MediaCodec的使用主要是解码。MediaCodec有两个ByteBuffer数组,分别是InputBuffers,OutputBuffers用来导入导出解码前后的数据。大致过程是
a.向MediaCodec中传入带解码的数据
dequeueInputBuffe() : int
queueInputBuffer() : void

b.解码(自动进行)

c.获取解码后的数据
dequeueOutputBuffer() : int

d.渲染到Surface 或 载入到 AudioTrack

代码实现过程大致如下

1.MediaCodec的初始化与配置

//根据KEY_MIME所对应的value创建,即根据我需要解码的对象是什类型,就初始化什么类型的MediaCodec(这里是解码器Decoder)
videoCodec = MediaCodec.createDecodeByType(videoFormat.getString(MediaFormat.KEY_MIME)); 

//配置该decoder并且绑定一个surface用来渲染画面
videoCodec.configure(videoFormat,mSurface,null,0);

//配置完就可以启动该decoder
videoCodec.start(); 
//解码器已启动,并不是说他马上工作,只是启动,有数据输入才解码。
//可以认为是就绪状态,有数据就工作,无数据则等待数据

2.向MediaCodec输入数据进行解码
(1)找到可用输入缓冲区(buffer出队),并将数据放入缓冲区

 //获取输入缓冲数组(获取所有的输入缓冲)
ByteBuffer[] InputBuffers = videoCodec.getInputBuffers();

//dequeueInputBuffer() : int 返回的是所有的输入缓冲区中可用的(空的)一个缓冲区的索引,则inputBuffers[bufferIndex]即为可用的输入缓冲区
int bufferIndex = decoder.dequeueInputBuffer(TIME_OUT); //TIME_OUT指的是超时时间,单位us

//该inputBuffer为 即将输入数据到的输入缓冲区
ByteBuffer inputBuffer = inputBuffers[bufferIndex];
//MediaExtractor从其数据流中提取一帧到inputBuffer中,并返回读入的大小(那个selectTrack在这里体现了,表明是从设定的Track中提取数据)
int sampleSize = extractor.readSampleData(inputBuffer,0);

(2)将填了数据的缓存区送入Decoder中进行解码(buffer进队)

if(sampleSize < 0){
	//读到数据末尾了
   decoder.queueInputBuffer(bufferIndex,0,0,0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
   }else{//正常情况下
   
   //一旦queueInputBuffer(),则填满数据的InputBuffer就会将数据送进Decoder中,
   //Decoder会自动解码,并将解码后的数据放到OutputBuffers中其中一个OutputBuffer中
  decoder.queueInputBuffer(bufferIndex,0, sampleSize, extractor.getSampleTime(),0);
  //读入指针后移到下一个数据包/帧
  extractor.advance();
   }

3.解码
此过程Decoder自己完成,这部分解码的实现后续需要深入到底层学习其实现方法

4.获取解码后的数据

//OutputBuffer出队,得到一个已经解码完成了的OutputBuffer的索引,则OutputBuffers[outputIndex] 储存着解码后的数据
int outputIndex = videoCodec.dequeueOutputBuffer(viedoBufferInfo,TIME_OUT);

//将OutputBuffers[outputIndex]中储存的解码后的数据传到surface中渲染
//设置true是指先把视频画面render到surface,然后再把这个buffer里的数据release掉(但要确保在configure的时候有绑定surface)
//设置为false 则只是简单释放资源,在音频解码的情况下设置false,将数据载入AudioTrack后,直接release掉
//这里只需要传outputBuffer的索引即可
videoCodec.releaseOutputBuffer(outputIndex, true);

//音频情况下
audioTrack.write(mAudioOutTempBuf, 0, audioBufferInfo.size);
audioCodec.releaseOutputBuffer(outputIndex, flase);

5.循环重复步骤2-4直到读到数据EOS(END OF STREAM)

6.释放资源

videoCodec.stop();
videoCodec.release();
videoExtractor.release();	

音频

音频的解码过程与视频的类似,下面给出简要的过程

1.MediaExtractor的使用

1.setDataSource();
2.把audio对于的那个track找出来,即找到该mediaFormat
3.获取其audioChannels(声道数) , audioSampleRate(采样率),用来配置后面的AudioTrack

int audioChannels = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int audioSampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);

4.计算关于输入缓冲大小的计算

int minBufferSize = AudioTrack.getMinBufferSize(audioSampleRate,
                            (audioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO),
                            AudioFormat.ENCODING_PCM_16BIT);
int maxInputSize = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);

audioInputBufferSize = minBufferSize > 0 ? minBufferSize * 4 : maxInputSize;
int frameSizeInBytes = audioChannels * 2;   //这里一般是双声道,一帧两个声道,一个声道的一帧16bit的值,一共是所以是4字节
audioInputBufferSize = (audioInputBufferSize / frameSizeInBytes) * frameSizeInBytes;
                    //</这里主要是关于输入缓冲大小的计算,按照官方文档规定,这个size一定要是frameSizeInBytes的整数倍>

2.AudioTrack的配置

5.初始化AudioTrack

audioTrack = new AudioTrack(int streamType(这里是music), int sampleRateInHz (采样率), int channelConfig (声道数), int audioFormat (16bit),
        int bufferSizeInBytes(输入buffer的size), int mode (一般是Stream模式))
        
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                            audioSampleRate, 
                            (audioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO),
                            AudioFormat.ENCODING_PCM_16BIT,
                            audioInputBufferSize,
                            AudioTrack.MODE_STREAM);

6.配置完AudioTrack之后play即可

 audioTrack.play(); //play之后即处于就绪状态,而并不是直接开始工作,只是说现在是play状态,
                    //一旦有数据传入到AudioTrack的中就马上根据数据放出声音,无数据传入则等待

MediaCodec的解码过程

1.初始化

audioCodec = MediaCodec.createDecoderByType(mime);
audioCodec.configure(mediaFormat, null, null, 0);  //第二个参数(surface)为null,这是音频,没有surface

2.解码器start(),与视频一样,就绪状态
3.putBufferToDecoder(),即将InputBuffer里数据向decoder中写入,与视频一样
4.dequeueOutputBuffer()得到outputIndex,即得到outputBuffer = outputBuffers[outputIndex]
5.将该outputbuffer数据复制到byte[]数组中,而视频是直接渲染到surface的

outputBuffer.get(mAudioOutTempBuf, 0, audioBufferInfo.size); //byteBuffer 转 byte[]
从0开始,读audioBufferInfo.size这么大
或者直接outputBuffer.get(mAudioOutTempBuf);  将整块outputBuffer复制到该mAudioOutTempBuf(byte[]数组)中

6.将该byte[]数组的数据写入到AudioTrack中

audioTrack.write(mAudioOutTempBuf, 0, audioBufferInfo.size);  //audioBufferInfo是dequeue时得到的该帧信息,
//mAudioOutTempBuf是复制后得到的byte[]数组

7.释放outputbuffer中的资源

audioCodec.releaseOutputBuffer(outputBufferIndex, false); //false,因为没有绑定surface,直接传false即可

总结

通过对MediaExtractor和MediaCodec的学习,了解到了的音视频播放深一层的实现方法,先后进行接封装和解码的步骤实现。本次博文只是针对播放视频步骤进行解读总结,没有做具体的异常检测,内容主要针对视频解码,音频解码类似,只是音频用AudioTrack,视频用Surface。
具体实现代码可参考我的github
https://github.com/WanZhiZheng/MediaPlayerTest

展开阅读全文

没有更多推荐了,返回首页