Android|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()
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主要的状态为:Stopped、Executing、Released。
- Stopped的状态下也分为三种子状态:Uninitialized、Configured、Error。
- Executing的状态下也分为三种子状态:Flushed、 Running、End-of-Stream。
文章图片
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();
}
};
推荐阅读
- android第三方框架(五)ButterKnife
- Android中的AES加密-下
- 带有Hilt的Android上的依赖注入
- android|android studio中ndk的使用
- Android事件传递源码分析
- RxJava|RxJava 在Android项目中的使用(一)
- Android7.0|Android7.0 第三方应用无法访问私有库
- 深入理解|深入理解 Android 9.0 Crash 机制(二)
- android防止连续点击的简单实现(kotlin)
- Android|Android install 多个设备时指定设备