一、文章说明
最近工作实在太忙,很久没有更新文章了,收到很多小伙伴催更的消息,心中实在惭愧,趁着今天有空赶紧更新。
第一篇文章从总体上介绍了Android手机直播,之后两篇文章分别介绍了视频和音频采集,这篇文章便开始介绍编解码相关的知识。Android提供很多和视音频处理相关的类,熟练使用这些相关的类,其实是能实现很强大的功能。
视音频编解码一般分为两种,一种是硬编实现,一种是软编实现。这两种方式各有优缺点,硬编性能好,但是需要对兼容性进行相应处理;软编兼容性好,可以进行一些参数设置,但是软编一般性能较差,引入相关的编解码库往往会增大app的整体体积,而且还需要写相应的jni接口。
这篇文章主要讲述使用Android原生提供的各种类来实现对视音频进行处理,相信各位看完整篇文章后会感受到这几个类配合使用的强大。
直播项目已经开源,开源地址:SopCastComponent
Github地址:https://github.com/SuperJim123
二、几个类
很多时候我们往往会忽略很多事情,就比如说Android系统已经给我们提供了对视音频的强大支持,我们往往还不知道,没有专心去研究。这篇文章先介绍几个和视音频相关的类,通过这几个类的组合使用,其实是能变换出许多视音频处理的相关功能,下面就对这几个类进行简单介绍。
MediaMetadataRetriever::用来获取视频的相关信息,例如视频宽高、时长、旋转角度、码率等等。
MediaExtractor::视音频分离器,将一些格式的视频分离出视频轨道和音频轨道。
MediaCodec:视音频相应的编解码类。
MediaMuxer:视音频合成器,将视频和音频合成相应的格式。
MediaFormat:视音频相应的格式信息。
MediaCodec.BufferInfo:存放ByteBuffer相应信息的类。
MediaCrypto:视音频加密解密处理的类。
MediaCodecInfo:视音频编解码相关信息的类。
MediaFormat和MediaCodec.BufferInfo是串起上面几个类的桥梁,上面几个视音频处理的类通过这两个桥梁建立起联系,从而变化出相应的功能,认真分析的话会感觉到Google设计的精妙。
三、MediaMetadataRetriever
MediaMetadataRetriever用来获取视音频的相关信息,MediaMetadataRetriever的使用十分简单,传入相应的文件路径创建MediaMetadataRetriever,之后便可以得到视频的相关参数。
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
metadataRetriever.setDataSource(file.getAbsolutePath());
String widthString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
if(!TextUtils.isEmpty(widthString)) {
width = Integer.valueOf(widthString);
}
String heightString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
if(!TextUtils.isEmpty(heightString)) {
height = Integer.valueOf(heightString);
}
String durationString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if(!TextUtils.isEmpty(durationString)) {
duration = Long.valueOf(durationString);
}
String bitrateString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
if(!TextUtils.isEmpty(bitrateString)) {
bitrate = Integer.valueOf(bitrateString);
}
String degreeStr = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
if (!TextUtils.isEmpty(degreeStr)) {
degree = Integer.valueOf(degreeStr);
}
metadataRetriever.release();
四、MediaExtractor
MediaExtractor用来对视音频进行分离,对文件中的视频音频轨道进行分离能做大量的事情,比如说要写一个播放器,那么首先的第一个步骤是分离出视频音频轨道,然后进行相应的处理。MediaExtractor的创建和MediaMetadataRetriever一样十分简单,只需要传入相应的文件路径。通过getTrackCount()可以得到相应的轨道数量,一般情况下视音频轨道都有,有些时候可能只有视频,有些时候可能只有音频。轨道的序号从0开始,通过getTrackFormat(int index)方法可以得到相应的MediaFormat,而通过MediaFormat可以判断出轨道是视频还是音频。通过selectTrack(int index)方法选择相应序号的轨道。
public static MediaExtractor createExtractor(String path) throws IOException {
MediaExtractor extractor;
File inputFile = new File(path); // must be an absolute path
if (!inputFile.canRead()) {
throw new FileNotFoundException("Unable to read " + inputFile);
}
extractor = new MediaExtractor();
extractor.setDataSource(inputFile.toString());
return extractor;
}
public static String getMimeTypeFor(MediaFormat format) {
return format.getString(MediaFormat.KEY_MIME);
}
public static int getAndSelectVideoTrackIndex(MediaExtractor extractor) {
for (int index = 0; index < extractor.getTrackCount(); ++index) {
if (isVideoFormat(extractor.getTrackFormat(index))) {
extractor.selectTrack(index);
return index;
}
}
return -1;
}
public static int getAndSelectAudioTrackIndex(MediaExtractor extractor) {
for (int index = 0; index < extractor.getTrackCount(); ++index) {
if (isAudioFormat(extractor.getTrackFormat(index))) {
extractor.selectTrack(index);
return index;
}
}
return -1;
}
public static boolean isVideoFormat(MediaFormat format) {
return getMimeTypeFor(format).startsWith("video/");
}
public static boolean isAudioFormat(MediaFormat format) {
return getMimeTypeFor(format).startsWith("audio/");
}
选择好一个轨道后,便可以通过相应方法提取出相应轨道的数据。extractor.seekTo(startTime, SEEK_TO_PREVIOUS_SYNC)方法可以直接跳转到开始解析的位置。extractor.readSampleData(byteBuffer, 0)方法则可以将数据解析到byteBuffer中。extractor.advance()方法则将解析位置进行前移,准备下一次解析。
下面是MediaExtractor一般的使用方法。
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(...);
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (weAreInterestedInThisTrack) {
extractor.selectTrack(i);
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
extractor.advance();
}
extractor.release();
extractor = null;
五、MediaCodec
MediaCodec是Android视音频里面最为重要的类,它主要实现的功能是对视音频进行编解码处理。在编码方面,可以对采集的视音频数据进行编码处理,这样的话可以对数据进行压缩,从而实现以较少的数据量存储视音频信息。在解码方面,可以解码相应格式的视音频数据,从而得到原始的可以渲染的数据,从而实现视音频的播放。
一般场景下音频使用的是AAC-LC的格式,而视频使用的是H264格式。这两种格式在MediaCodec支持的版本(Api 16)也都得到了很好的支持。在直播过程中,先采集视频和音频数据,然后将原始的数据塞给编码器进行硬编,然后得到相应的编码后的AAC-LC和H264数据。
在Android系统中,MediaCodec支持的格式有限,在使用MediaCodec之前需要对硬编类型的支持进行检测,如果MediaCodec支持再进行使用。
1、检查
在使用硬编编码器之前需要对编码器支持的格式进行检查,在Android中可以使用MediaCodecInfo这个类来获取系统对视音频硬编的支持情况。
下面的代码是判断MediaCodec是否支持某个MIME:
private static MediaCodecInfo selectCodec(String mimeType) {
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i < numCodecs; i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder()) {
continue;
}
String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
return codecInfo;
}
}
}
return null;
}
根据之前的讲述,在Android系统中有着不同的颜色格式,有着各种类型的YUV颜色格式和RGB颜色格式。在摄像头采集的文章中已经讲述,需要设置摄像头采集的图像颜色格式,一般来说设置为ImageFormat.NV21,之后在摄像头PreView的回调中得到相应的图像数据。
在Android系统中不同手机中的编码器支持着不同的颜色格式,一般情况下并不直接支持NV21的格式,这时候需要将NV21格式转换成为编码器支持的颜色格式。在摄像头采集的文章中已经详细讲述YUV图像格式和相应的存储规则,YUV图像格式的转换可以使用LibYuv。
这里说一下MediaCodec支持的图像格式。一般来说Android MediaCodec支持如下几种格式:
/**
* Returns true if this is a color format that this test code understands (i.e. we know how
* to read and generate frames in this format).
*/
private static boolean isRecognizedFormat(int colorFormat) {
switch (colorFormat) {
// these are the formats we know how to handle for this test
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
return true;
default:
return false;
}
}
之前有看过一篇文章,里面大致统计了Android各种手机MediaCodec支持的各种颜色格式,上面5个类型是比较常用的类型。
另外MediaCodec支持Surface的方式输入和输出,当编码的时候只需要在Surface上进行绘制就可以输入到编码器,而解码的时候可以将解码图像直接输出到Surface上,使用起来相当方便,需要在Api 18或以上。
2、创建
当需要使用MediaCodec的时候,首先需要根据视音频的类型创建相应的MediaCodec。在直播项目中视频使用了H264,而音频使用了AAC-LC。在Android中创建直播的音频编码器需要传入相应的MIME,AAC-LC对应的是audio/mp4a-latm,而H264对应的是video/avc。如下的代码展示了两个编码器的创建,其中视频编码器的输入设置成为了Surface的方式。
//Audio
public static MediaCodec getAudioMediaCodec() throws IOException {
int size = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
MediaFormat format = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 1);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, 64 * 1000);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, size);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return mediaCodec;
}
//Video
public static MediaCodec getVideoMediaCodec() throws IOException {
int videoWidth = getVideoSize(1280);
int videoHeight = getVideoSize(720);
MediaFormat format = MediaFormat.createVideoFormat("video/avc", videoWidth, videoHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 1300* 1000);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
format.setInteger(MediaFormat.KEY_BITRATE_MODE,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
format.setInteger(MediaFormat.KEY_COMPLEXITY,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
MediaCodec mediaCodec = MediaCodec.createEncoderByType("video/avc");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return mediaCodec;
}
// We avoid the device-specific limitations on width and height by using values that
// are multiples of 16, which all tested devices seem to be able to handle.
public static int getVideoSize(int size) {
int multiple = (int) Math.ceil(size/16.0);
return multiple*16;
}
3、使用
MediaCodec创建之后,需要通过start()方法进行开启。MediaCodec有输入缓冲区队列和输出缓冲区队列,不断通过往输入缓冲区队列传递数据,经过MediaCodec处理后就可以得到响应的输出数据。当在编码的时候,需要向输入缓冲区传入采集到的原始的视音频数据,然后获取输出缓冲区的数据,输出出来的数据也就是编码处理后的数据。当在解码的时候,往输入缓冲区输入需要解码的数据,然后获取输出缓冲区的数据,输出出来的数据也就是解码后得到的原始的视音频数据。当需要清空输入和输出缓冲区的时候,可以调用MediaCodec的flush()方法。当编码或者解码结束时,通过往输入缓冲区输入带结束标记的数据,然后从输出缓冲区可以得到这个结束标记,从而完成整个编解码过程。下面一张图片很好地展示了MediaCodec的状态变化。
MediaCodec状态
对于MediaCodec通过处理输入的数据,从而得到输出数据。MediaCodec通过一系列的输入和输出缓冲区来处理数据。如下图所示,输入客户端通过查询得到空的输入缓冲区,然后往里面填充数据,然后将输入缓冲区传递给MediaCodec;输出客户端通过查询得到塞满的输出缓冲区,然后得到里面的数据,然后通知MediaCodec释放这个输出缓冲区。
MediaCodec过程
在API 21及以后可以通过下面这种异步的方式来使用MediaCodec。
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback(new MediaCodec.Callback() {
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
}
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
@Override
void onError(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
从API 21开始,可以使用下面这种同步的方式来使用MediaCodec。
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
在API版本21之前,获取缓冲区的方式有所不同,不能直接得到相应的缓冲区,需要根据索引序号从缓冲区列表中得到相应的缓冲区,具体的代码如下所示:
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(…);
if (inputBufferId >= 0) {
// fill inputBuffers[inputBufferId] with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
// outputBuffers[outputBufferId] is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
MediaFormat format = codec.getOutputFormat();
}
}
codec.stop();
codec.release();
当数据输入结束的时候,通过queueInputBuffer的输入标记设置为BUFFER_FLAG_END_OF_STREAM来通知MediaCodec结束编码。当MediaCodec作为编码器的时候, dequeueOutputBuffer方法能够得到当前编码输出缓冲区数据的相关信息,这些信息存储在bufferInfo里面,通过bufferInfo信息能够得到数据的真实长度,当前数据为关键帧或者非关键帧等等信息。
当使用Output Surface作为解码的输出的时候,可以根据以下情况来设置是否将视频渲染到Surface上。
releaseOutputBuffer(bufferId, false) //不渲染buffer里面的数据
releaseOutputBuffer(bufferId, true) //渲染buffer里面的数据
releaseOutputBuffer(bufferId, timestamp) //在特定时间渲染buffer里面的数据
当使用Input Surface作为编码器输入的时候,不允许使用dequeueInputBuffer。当输入结束的时候,使用signalEndOfInputStream()来使得编码器停止。
六、MediaMuxer
前面讲述了MediaExtractor(视音频分离器),现在讲述MediaMuxer(视音频合成器)。MediaMuxer是Android提供的视音频合成器,目前只支持mp4和webm两种格式的视音频合成。一般来时视音频媒体都有视频轨道和音频轨道,有些时候也还有字母轨道,MediaMuxer将这些轨道糅合在一起存储在一个文件中。
MediaMuxer在Android中一个最常使用的场景是录制mp4文件。一般来说当存储为mp4文件时,视频轨道一般是经过编码处理后的h264视频,音频轨道一般是经过编码后处理的aac音频。前面已经讲述了如何对采集的视频和音频进行硬编,那么这时候如果对硬编后的视频和音频使用MediaMuxer进行合成,那么就可以合成为mp4文件。
下面是MediaMuxer一般的使用方法。
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
// or MediaExtractor.getTrackFormat().
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();
muxer.start();
while(!finished) {
// getInputBuffer() will fill the inputBuffer with one frame of encoded
// sample from either MediaCodec or MediaExtractor, set isAudioSample to
// true when the sample is audio data, set up all the fields of bufferInfo,
// and return true if there are no more samples.
finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
if (!finished) {
int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
}
};
muxer.stop();
muxer.release();
其实上面的注释很好说明了MediaMuxer的使用场景。视音频轨道的初始化需要传入MediaFormat,而MediaFormat可以通过MediaCodec.getOutputFormat()获取(采集后进行硬编得到MediaFormat),也可以通过MediaExtractor.getTrackFormat()获取(分离器分离出视音频得到MediaFormat)。上面包含了两个应用场景,一个是采集,一个是转码。
七、结合
MediaExtractor和MediaCodec结合使用可以实现视频的播放功能,MediaCodec和MediaMuxer结合使用可以实现视频的录制功能,MediaExtractor、MediaCodec和MediaMuxer三者一起使用可以实现视频的转码功能。下面讲述一下这几个功能的实现。
1、视音频录制
之前讲述了视频的采集和音频的采集,将采集到的视音频通过MediaCodec进行编码处理,之后将编码数据传递到MediaMuxer进行合成,也就完成了视音频录制的功能。
视频录制
根据视音频采集的相关参数创建MediaCodec,当MediaCodec的outputBufferId为INFO_OUTPUT_FORMAT_CHANGED时,可以通过codec.getOutputFormat()得到相应的MediaFormat,之后便可以用这个MediaFormat为MediaMuxer添加相应的视音频轨道。通过codec.dequeueOutputBuffer(…)可以得到编码后的数据的bufferInfo信息和相应的数据,之后将这个数据和bufferInfo通过muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo)传递给Muxer,也就将整个视音频数据合成到了mp4中。
2、视音频播放
视音频播放利用Android提供的Media API来实现一个播放器也是可以的,实际上Google著名的开源项目ExoPlayer就是这么做的。
上面的示意图简要描述了一个简单的本地播放器的结构。利用MediaExtractor分离视音频文件,得到相应的音频轨道和视频轨道。之后通过MediaExtractor从相应的轨道中获取数据,并且将这些数据传递给MediaCodec的输入缓冲区,经过MediaCodec的解码便可以得到相应的原始数据。音频解码后可以得到PCM数据,从而可以传递给AudioTrack进行播放。视频解码后可以渲染到相应的Surface,这个Surface可以是通过SurfaceTexture创建,而SurfaceTexture是可以通过纹理创建的,从而将解码后的视频数据传递到纹理上了。
MediaExtractor解析视音频文件,可以得到相应数据的pts,之后pts可以传输到MediaCodec,之后在MediaCodec的输出里面可以得到相应的pts,之后在根据视音频的pts来控制视音频的渲染,从而实现视音频的同步。
3、视音频转码
视音频的转码,其实就是通过MediaExtractor解析相应的文件,之后得到相应的视频轨道和音频轨道,之后将轨道里的数据传输到MediaCodec进行解码,然后将解码后的数据进行相应的处理(例如音频变声、视频裁剪、视频滤镜),之后将处理后的数据传递给MediaCodec进行编码,最后利用MediaMuxer将视频轨道和音频轨道进行合成,从而完成了整个转码过程。
视音频转码
八、展望
文中讲述了,如何使用Media API进行相应的录制、播放、转码,讲述了如何将视音频编解码和纹理相结合,但是由于篇幅原因并未讲述如何通过OpenGL对视频进行相应的处理,在之后的文章中会进一步讲述。
文中讲述了很多如何实现的原理和过程,但是并未提供相应的实现源码,其实这些程序的编写并不复杂,之后会写相应的开源代码,写完之后会更新到这篇文章上。
九、相关链接
Android手机直播(一)总览
Android手机直播(二)摄像机
Android手机直播(三)声音采集
Android手机直播(四)Android Media API
十、结束语
终于写完了,各位看官觉得文章不错的话不妨点个喜欢~
网友评论
1.解码器dequeueInputBuffer,这里是不是dequeueInputBuffer一次就是解码一帧数据?也就是说MediaExtractor.readSampleData每次采样是不是采样一帧数据?
2.后面dequeueOutputBuffer获取有效解码的数据也是一次?如果是一次,为什么我看其他demo有人这里用死循环去获取,返回-1才跳出
强烈建议赶快出OpenGL篇!