Android|Android MediaExtractor + MediaCodec构建简单播放器

对于一个播放器,基本上可以分为以下模块:数据接收(网络/本地)->解复用->音视频解码->音视频同步->音视频输出。
今天我们介绍Android系统中提供的两个播放器模块MediaExtractor 和MediaCodec的简单使用,利用他们来完成一个简易的播放器。
其中MediaExtractor完成解复用工作,而MediaCodec则完成音视频解码工作。
Android|Android MediaExtractor + MediaCodec构建简单播放器
文章图片

1、MediaExtractor简介 MediaExtractor主要负责解复用工作,在我们的简易播放其中,有以下两个作用:
1、获取媒体文件的格式,包括音视频轨道,编码格式、宽、高、采样率、声道数等等。
2、分离音频流、视频流,读取分离后的音视频数据。
MediaExtractor模块使用比较简单,但存在以下不足:

  • 支持格式较少;
  • 对于网络流的支持十分有限,大部分平台上支持http,不包括hls
  • 无法从内存中读取,或者是通过buffer写入。
ps: 这些原因让我会感觉这个模块比FFmpeg逊色不少,特别是无法像FFmpeg那样从内存中读取,导致要扩展一个网络流(如rtsp)十分麻烦,有以下两种方式:
1、 继承Android提供的DataSource类实现,但该类目前Android未开放。
2、 从framework层扩展支持。
使用步骤及关键接口:
1、设置数据源,即可以设置本地文件又可以设置网络文件,仅http。一般使用以下接口,实际上还可以传入Uri或者FileDescriptor。
void setDataSource(String path)

2、获取媒体文件音视频轨道数。
int getTrackCount(); //返回值为轨道数

3、遍历所有轨道,获取音视频格式
MediaFormat getTrackFormat(int index)//获取指定index的音视频格式

4、选定一跳音频或者视频轨道,这样后面从MediaExtractor读取数据就只会从该轨道中读取
void selectTrack(int index);

5、读取数据
int readSampleData(ByteBuffer byteBuf, int offset)

ByteBuffer为MediaExtractor从指定轨道中解复用出来的数据;
返回值为-1表示已全部读完。
6、跳转到下一个数据
boolean advance();

返回值为false表示已全部读完
7、释放资源
void release()

8、其他
  • 获取时间戳,单位为微秒
    long getSampleTime()
特别说明的是Android提供了不开放的接口setDataSource(DataSource source)及DataSource类,是在MediaExtractor上拓展流媒体最简便的方式。
DataSource类(Android不开放):
package android.media; import java.io.Closeable; /** * An abstraction for a media data source, e.g. a file or an http stream * {@hide} */ public interface DataSource extends Closeable { /** * Reads data from the data source at the requested position * * @param offset where in the source to read * @param buffer the buffer to read the data into * @param size how many bytes to read * @return the number of bytes read, or -1 if there was an error */ public int readAt(long offset, byte[] buffer, int size); /** * Gets the size of the data source. * * @return size of data source, or -1 if the length is unknown */ public long getSize(); }

自己扩展DataSource类,可以完成自定义的媒体文件获取方式,主要还是用在流媒体。如扩展从文件读取类,MyDataSource:
public class MyDataSource implements DataSource { MyDataSource(String url){ try { mFile = new File(url); mSize = mFile.length(); } catch (Exception e1) { e1.printStackTrace(); } } private static final String TAG = "testMediaCodec"; private long mSize = 0; private File mFile = null; public int readAt(long offset, byte[] buffer, int size){ int bytes = 0; InputStream in = null; try { in = new FileInputStream(mFile); in.skip(offset); //注意offset是对整个文件的偏移量 bytes= in.read(buffer, 0, size); } catch (Exception e1) { e1.printStackTrace(); } finally { if(in != null){ try { in.close(); } catch (IOException e) { e.printStackTrace(); } } }return bytes; }public long getSize(){ return mSize; }public void close() throws IOException{ } }

2、MediaCodec简介 MediaCodec类可用于编解码。通常与MediaExtractor(解复用器)、MediaMuxer(复用器)、AudioTrack(音频播放接口)结合使用。
需要注意的是MediaCodec并非是编解码器,它是Android封装的API,提供给应用层使用,可用于访问Android底层的多媒体编解码器,这些编解码器基于OMX框架。
MediaCodec是如何使用中OMX的?基本框架如下图所示:
Android|Android MediaExtractor + MediaCodec构建简单播放器
文章图片

1)MediaCodec native层使用的codec类为ACodec类;
2)ACodec通过binder(IOMX)访问OMX适配层;
3)OMX适配层封装OpenMax IL层,底层编解码库对接OpenMax IL层后嵌套到该层。
工作流程
MediaCodec采用异步方式处理数据,并且使用了一组输入输出缓存(input and output buffers)来存放待处理数据以及处理完的数据。开发者只需将待编解码的数据放入输入缓冲区交给编解码器,再从输出缓冲区获取编解码后的数据即可。
其工作方式大致如下:
1、请求或接收一个空的输入缓存(input buffer)。
2、向其中填充待处理的数据,并将它传递给编解码器处理。
3、MediaCodec处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。
4、请求或接收到一个填充了处理后数据的输出缓存(output buffer)。
5、使用完其中的数据,并将其释放给编解码器再次使用。
如下图所示:
Android|Android MediaExtractor + MediaCodec构建简单播放器
文章图片

MediaCodec主要的状态为:Stopped、Executing、Released。
  • Stopped的状态下也分为三种子状态:Uninitialized、Configured、Error。
  • Executing的状态下也分为三种子状态:Flushed、 Running、End-of-Stream。
MediaCodec状态变换图如下:
Android|Android MediaExtractor + MediaCodec构建简单播放器
文章图片

1、当创建编解码器的时候处于未初始化状态。首先需要调用configure(…)方法进入Configured状态,然后调用start()方法让其处于Executing状态。在Executing状态下,就可以缓冲区来处理数据。
2、Executing的状态下也分为三种子状态:Flushed、 Running、End-of-Stream。在start() 调用后,编解码器处于Flushed状态,这个状态下它保存着所有的缓冲区。一旦第一个输入buffer出现了,编解码器就会自动运行到Running的状态。当带有end-of-stream标志的buffer进去后,编解码器会进入End-of-Stream状态,这种状态下编解码器不在接受输入buffer,但是仍然在产生输出的buffer。此时可以调用flush()方法,将编解码器重置于Flushed状态。
3、调用stop()可以将编解码器返回到Uninitialized状态,然后可以重新配置。
4、在底层编解码出错的情况下,MediaCodec会转到错误状态。调用reset()使编解码器再次可用,reset()可以从任何状态将编解码器移Uninitialized状态。
5、当MediaCodec数据处理任务完成时或不再需要MediaCodec时,可使用release()方法释放其资源,到达Released状态。
使用步骤及关键接口:
1、创建MediaCodec。
name指编解码器名字,type指的是MIME类型,支持的name和type具体可见/system/etc/ media_codecs.xml。
MediaCodec createByCodecName(String name) MediaCodec createDecoderByType(String type) MediaCodec createEncoderByType(String type)

2、configure配置编解码器。
format可以用来设置一些属性,如视频的宽,高,帧率,音频的声道,采样率等。surface用于解码器把解码后的视频帧直接显示到屏幕,注意,设置这个参数后,解码出来的ByteBuffers将无法获取到数据,因为解码器为了提高效率并没有把数据copy到ByteBuffer中,但是可以根据ByteBuffer的id,通过getOutputImage(int index)把帧数据取出来。 crypto与解密相关,暂时没有研究。Flags指定当前的是编码器还是解码器,编码器需要使用CONFIGURE_FLAG_ENCODE。
void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)

3、启动编解码器,通知编解码器开始工作,在configure后调用。
void start()

4、进入编解码处理,对应上面流程图:
1)请求一块空闲的输入缓冲区ByteBuffers。
timeoutUs是超时时长,0表示立即返回,-1表示一直等待,其他时间表示等待多少微秒,如果设置为-1或者设置了一个过长的值,有可能会直接导致线程卡住。
返回值是ByteBuffer的索引值。使用索引通过 getInputBuffer(int index)或者数组下标的方式获取到对应ByteBuffers。
int dequeueInputBuffer (long timeoutUs)

2)获取要处理的数据,并将其填充到输入缓冲区(ByteBuffer),提交给编解码器处理。
index是ByteBuffer的索引值,与dequeueInputBuffer返回值对应;
offset是有效数据在buffer中的偏移量;
size是有效数据的长度;
presentationTimeUs是当前数据的时间戳,单位是微秒;
flags是一些标记的位掩码,用于通知编解码当前数据是流结束BUFFER_FLAG_END_OF_STREAM ,编解码需要的Codec-specific数据BUFFER_FLAG_CODEC_CONFIG等等。
void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)

3)queueInputBuffer 后 MediaCodec会对数据进行处理。
4)获取处理后的数据。
info包含了处理结束后的数据以及一些标记值;
timeoutUs是超时时长,0表示立即返回,-1表示一直等待,其他时间表示等待多少微秒,如果设置为-1或者设置了一个过长的值,有可能会直接导致线程卡住。
返回值是ByteBuffer的索引值。使用索引通过 getOutputBuffer(int index)或者数组下标的方式获取到对应ByteBuffers。返回值小于0表示一些错误信息包括输出格式改变,输出buffer改变,稍后重新尝试等等。
int dequeueOutputBuffer (MediaCodec.BufferInfo info, long timeoutUs)

5)释放一个输出ByteBuffers,还给编解码器。
Index是buffer的索引,与dequeueOutputBuffer 获取到的索引向对应;
Render表示是否需要在surface中显示;
renderTimestampNs表示在surface中显示并且设置时间戳。Android 4.0及以前版本没有第二个接口。
void releaseOutputBuffer (int index, boolean render) void releaseOutputBuffer (int index, long renderTimestampNs)

5、停止编解码器,通知编解码器停止工作,与start相对。
void stop ()

6、释放编解码器及其占用资源
void release()

7、重置编解码器
void reset()

3、构建简单播放器 初始化MediaExtractor,遍历所有音视频轨道,创建音频MediaCodec和视频MediaCodec,并完成config。
public int prepare(){ for(int i=0 ; i < mVExtractor.getTrackCount(); i++){ MediaFormat format = mVExtractor.getTrackFormat(i); Log.d(TAG, ">> format" + i + ": " +format); String mime = format.getString(MediaFormat.KEY_MIME); Log.d(TAG, ">> mime i " + i + ": " +mime); if (mime.startsWith("audio/")){ mAExtractor.selectTrack(i); mAMediaCodec = MediaCodec.createDecoderByType(mime); mAMediaCodec.configure(format, null, null, 0); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int buffsize = AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT); mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate*channels/2, //fix to STEREO, so sample-rate maybe change AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, buffsize*10, AudioTrack.MODE_STREAM); }if (mime.startsWith("video/")){ mVExtractor.selectTrack(i); mVMediaCodec = MediaCodec.createDecoderByType(mime); mVMediaCodec.configure(format, mSurface, null, 0); } }return 1 ; }

视频解码线程,不停读取MediaExtractor从视频轨道中分离的视频数据,放入inputbuffer给视频MediaCodec解码,得到解码后数据OutputBuffer,MediaCodec会将其渲染到surface。
没有做音视频同步,仅简单通过视频PTS调整视频播放速率,避免太快。
如何进行音视频同步,参考之前的博客 https://blog.csdn.net/myvest/article/details/97416415
数据读完时,将BUFFER_FLAG_END_OF_STREAM给到解码器,结束线程。
class VDecodeThread extends Thread{ @Override public void run() { long timeout = 10000; //10ms long startMs = System.currentTimeMillis(); boolean isEOS = false; MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); mVMediaCodec.start(); ByteBuffer[] inBuffers =mVMediaCodec.getInputBuffers(); //ByteBuffer[] outBuffers = mMediaCodec.getOutputBuffers(); while(!isEOS){ int inIndex = mVMediaCodec.dequeueInputBuffer(timeout); if(inIndex >= 0){ int size = mVExtractor.readSampleData(inBuffers[inIndex], 0); //demux get video es if(size < 0){ Log.d(TAG, "mybe eos or error"); mVMediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); }else{ mVMediaCodec.queueInputBuffer(inIndex, 0, size, mVExtractor.getSampleTime(), 0); mVExtractor.advance(); inBuffers[inIndex].clear(); } }int outIndex = -1; do{ outIndex = mVMediaCodec.dequeueOutputBuffer(outBufferInfo, timeout); if((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){ Log.d(TAG, "outBufferInfo flag is BUFFER_FLAG_END_OF_STREAM"); isEOS = true; }if(outIndex >= 0){ // We use a very simple clock to keep the video FPS, or the video // playback will be too fast while (outBufferInfo.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) { try { sleep(10); } catch (InterruptedException e) { e.printStackTrace(); break; } } mVMediaCodec.releaseOutputBuffer(outIndex, true); //true means output to surface } }while(outIndex >= 0); } releaseVideo(); } };

音频解码线程和视频类似,不过需要自己处理解码后的数据,我们采用AudioTrack进行播放,需要注意的是,prepare阶段创建AudioTrack时,是固定为2声道,那么需要进行简单的声道、采样率调整处理,否则有些流声道数不为2会播放太快/太慢。
AudioTrack的使用参考之前的博客 https://blog.csdn.net/myvest/article/details/90731805
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate*channels/2, //fix to STEREO, so sample-rate maybe change AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, buffsize*10, AudioTrack.MODE_STREAM);

【Android|Android MediaExtractor + MediaCodec构建简单播放器】音频解码代码如下:
class ADecodeThread extends Thread{ @Override public void run() { long timeout = 1000; //1ms boolean isEOS = false; MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); mAMediaCodec.start(); ByteBuffer[] inBuffers =mAMediaCodec.getInputBuffers(); ByteBuffer[] outBuffers = mAMediaCodec.getOutputBuffers(); mAudioTrack.play(); byte[] data = https://www.it610.com/article/null; while(!isEOS){ int inIndex = mAMediaCodec.dequeueInputBuffer(timeout); if(inIndex>= 0){ int size = mAExtractor.readSampleData(inBuffers[inIndex], 0); //demux get audio es if(size < 0){ Log.d(TAG, "mybe eos or error"); mAMediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); }else{ mAMediaCodec.queueInputBuffer(inIndex, 0, size, mAExtractor.getSampleTime(), 0); mAExtractor.advance(); inBuffers[inIndex].clear(); } }int outIndex = -1; do{ outIndex = mAMediaCodec.dequeueOutputBuffer(outBufferInfo, timeout); if((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){ Log.d(TAG, "outBufferInfo flag is BUFFER_FLAG_END_OF_STREAM"); isEOS = true; }if(outIndex >= 0){if(outBufferInfo.size > 0){ ByteBuffer outBuf = outBuffers[outIndex]; outBuf.position(outBufferInfo.offset); outBuf.limit(outBufferInfo.offset + outBufferInfo.size); if(data =https://www.it610.com/article/= null) data = new byte[outBufferInfo.size]; Arrays.fill(data, (byte) 0); outBuf.get(data); mAudioTrack.write(data, 0, outBufferInfo.size); //output to audio track outBuf.clear(); }mAMediaCodec.releaseOutputBuffer(outIndex, false); } }while(outIndex>= 0); } data = https://www.it610.com/article/null; releaseAudio(); } };

    推荐阅读