Android MediaExtractor + MediaCodec构建简单播放器

对于一个播放器,基本上可以分为以下模块:数据接收(网络/本地)->解复用->音视频解码->音视频同步->音视频输出。
今天我们介绍Android系统中提供的两个播放器模块MediaExtractor 和MediaCodec的简单使用,利用他们来完成一个简易的播放器。
其中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的?基本框架如下图所示:
在这里插入图片描述
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、使用完其中的数据,并将其释放给编解码器再次使用。
如下图所示:
MediaCodec处理流程

MediaCodec主要的状态为:Stopped、Executing、Released。

  • Stopped的状态下也分为三种子状态:Uninitialized、Configured、Error。
  • Executing的状态下也分为三种子状态:Flushed、 Running、End-of-Stream。

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);

音频解码代码如下:

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 = 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 == 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 = null;
			releaseAudio();
	    }
	};
发布了58 篇原创文章 · 获赞 45 · 访问量 16万+
展开阅读全文

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

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览