硬件解封装
Android平台针对音视频封装提供了MediaMuxer API,支持.mp4格式的封装;针对解封装提供了MediaExtractor API,支持.mp4等格式。
API架构
在 frameworks/base/media 文件夹中提供了所有音视频相关的Java层API,本文中讲述的解封装API都定义在该处。而具体的服务实现都在 frameworks/av 文件夹中
自7.0开始,MediaService被拆分成多个服务,每个服务都运行在各自的进程中。
MediaExtractor类是官方提供的音视频解封装类。其官方使用Demo如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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;
|
支持的格式
注册解封装器
在MediaExtractorService被创建的时候,会注册所有的Extractor实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| void MediaExtractorFactory::RegisterExtractors( const char *libDirPath, const android_dlextinfo* dlextinfo, std::list<sp<ExtractorPlugin>> &pluginList) { DIR *libDir = opendir(libDirPath); if (libDir) { struct dirent* libEntry; while ((libEntry = readdir(libDir))) { if (libEntry->d_name[0] == '.') { continue; } String8 libPath = String8(libDirPath) + "/" + libEntry->d_name; if (!libPath.contains("extractor.so")) { continue; } void *libHandle = android_dlopen_ext( libPath.string(), RTLD_NOW | RTLD_LOCAL, dlextinfo); GetExtractorDef getDef = (GetExtractorDef) dlsym(libHandle, "GETEXTRACTORDEF"); RegisterExtractor( new ExtractorPlugin(getDef(), libHandle, libPath), pluginList); } closedir(libDir); } else { ALOGE("couldn't opendir(%s)", libDirPath); } }
|
MPEG-4的GETEXTRACTORDEF函数,其中的 Sniff方法就是创建对象的构造器。
1 2 3 4 5 6 7 8 9
| ExtractorDef GETEXTRACTORDEF() { return { EXTRACTORDEF_VERSION, UUID("27575c67-4417-4c54-8d3d-8e626985a164"), 2, "MP4 Extractor", { .v3 = {Sniff, extensions} }, }; }
|
创建解封装器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| sp<IMediaExtractor> MediaExtractorFactory::CreateFromService( const sp<DataSource> &source, const char *mime) {
creator = sniff(source, &confidence, &meta, &freeMeta, plugin, &creatorVersion); if (!creator) { ALOGV("FAILED to autodetect media content."); return NULL; }
MediaExtractor *ex = nullptr; if (creatorVersion == EXTRACTORDEF_VERSION_NDK_V1 || creatorVersion == EXTRACTORDEF_VERSION_NDK_V2) { CMediaExtractor *ret = ((CreatorFunc)creator)(source->wrap(), meta); if (meta != nullptr && freeMeta != nullptr) { freeMeta(meta); } ex = ret != nullptr ? new MediaExtractorCUnwrapper(ret) : nullptr; } return CreateIMediaExtractorFromMediaExtractor(ex, source, plugin); }
|
选择最合适的解封装器实例
选择具体格式的封装器是通过将DataSource依次给所有已经注册的解封装器的Sniff函数进行尝试,每次尝试都会有一个分数返回,保存在confidence中,最终会选择分数最大的做为最终的解封装器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| void *MediaExtractorFactory::sniff( const sp<DataSource> &source, float *confidence, void **meta, FreeMetaFunc *freeMeta, sp<ExtractorPlugin> &plugin, uint32_t *creatorVersion) { *confidence = 0.0f; *meta = nullptr; std::shared_ptr<std::list<sp<ExtractorPlugin>>> plugins; { Mutex::Autolock autoLock(gPluginMutex); if (!gPluginsRegistered) { return NULL; } plugins = gPlugins; } void *bestCreator = NULL; for (auto it = plugins->begin(); it != plugins->end(); ++it) { ALOGV("sniffing %s", (*it)->def.extractor_name); float newConfidence; void *newMeta = nullptr; FreeMetaFunc newFreeMeta = nullptr; void *curCreator = NULL; if ((*it)->def.def_version == EXTRACTORDEF_VERSION_NDK_V1) { curCreator = (void*) (*it)->def.u.v2.sniff( source->wrap(), &newConfidence, &newMeta, &newFreeMeta); } else if ((*it)->def.def_version == EXTRACTORDEF_VERSION_NDK_V2) { curCreator = (void*) (*it)->def.u.v3.sniff( source->wrap(), &newConfidence, &newMeta, &newFreeMeta); } if (curCreator) { if (newConfidence > *confidence) { *confidence = newConfidence; if (*meta != nullptr && *freeMeta != nullptr) { (*freeMeta)(*meta); } *meta = newMeta; *freeMeta = newFreeMeta; plugin = *it; bestCreator = curCreator; *creatorVersion = (*it)->def.def_version; } else { if (newMeta != nullptr && newFreeMeta != nullptr) { newFreeMeta(newMeta); } } } } return bestCreator; }
|
WAV解封装器
下面就是WAV格式的解封装器的Sniff函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static CreatorFunc Sniff( CDataSource *source, float *confidence, void **, FreeMetaFunc *) { DataSourceHelper *helper = new DataSourceHelper(source); char header[12]; if (helper->readAt(0, header, sizeof(header)) < (ssize_t)sizeof(header)) { delete helper; return NULL; } if (memcmp(header, "RIFF", 4) || memcmp(&header[8], "WAVE", 4)) { delete helper; return NULL; } WAVExtractor *extractor = new WAVExtractor(helper); int numTracks = extractor->countTracks(); delete extractor; if (numTracks == 0) { return NULL; } *confidence = 0.3f; return CreateExtractor; }
|
重要方法详解
setDataSource
方法很简单,就是设置数据源,对于数据源的协议目前只支持本地文件和HTTP、HTTPS。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| sp<DataSource> DataSourceFactory::CreateFromURI( const sp<MediaHTTPService> &httpService, const char *uri, const KeyedVector<String8, String8> *headers, String8 *contentType, HTTPBase *httpSource) { if (contentType != NULL) { *contentType = ""; }
sp<DataSource> source; if (!strncasecmp("file://", uri, 7)) { source = CreateFileSource(uri + 7); } else if (!strncasecmp("http://", uri, 7) || !strncasecmp("https://", uri, 8)) { sp<HTTPBase> mediaHTTP = httpSource; if (mediaHTTP == NULL) { mediaHTTP = static_cast<HTTPBase *>(CreateMediaHTTP(httpService).get()); } String8 cacheConfig; bool disconnectAtHighwatermark = false; KeyedVector<String8, String8> nonCacheSpecificHeaders; if (headers != NULL) { nonCacheSpecificHeaders = *headers; NuCachedSource2::RemoveCacheSpecificHeaders( &nonCacheSpecificHeaders, &cacheConfig, &disconnectAtHighwatermark); }
if (mediaHTTP->connect(uri, &nonCacheSpecificHeaders) != OK) { ALOGE("Failed to connect http source!"); return NULL; }
if (contentType != NULL) { *contentType = mediaHTTP->getMIMEType(); }
source = NuCachedSource2::Create( mediaHTTP, cacheConfig.isEmpty() ? NULL : cacheConfig.string(), disconnectAtHighwatermark); } else if (!strncasecmp("data:", uri, 5)) { source = DataURISource::Create(uri); } else { source = CreateFileSource(uri); } if (source == NULL || source->initCheck() != OK) { return NULL; } return source; }
|
selectTrack、unselectTrack
选中指定轨道和解除选中。一个媒体文件中会包含着多个轨道,比如音频轨道和视频轨道等,在MediaExtractor中对样本数据的操作都是基于指定的轨道来的,所以在操作样本数据之前,必须先选中一个轨道。
当一个轨道被选中后,后续的getSamplexxx方法都是针对该轨道,如果想换一个轨道进行处理,则必须先解除当前轨道的选中,调用unselectTrack函数。
对于如何选择想要的轨道,可以通过getTrackFormat方法来获取每一个轨道的详细信息,通过这些信息筛选出想要处理的轨道。
getTrackCount
获取当前媒体文件中的轨道数量,通常用来遍历所有的轨道。
获取指定索引的轨道的详细信息,索引的值从0开始递增。轨道信息在不同Android版本上有不同的支持,在使用的时候需要注意。
这里有一点需要注意,就是在使用轨道的MediaFormat对象去创建解码器的时候,最好先将KEY_LEVEL的值设置为null(MediaFormat.setString(KEY_LEVEL, null)
),因为这个值经常不准。
readSampleData
读取当前选中的轨道的一个样本数据,返回读取的size,这里要注意的是buffer的大小,需要足够大,否则无法一次读取完一个样本,通常可以使用MediaFormat.KEY_MAX_INPUT_SIZE来获取。
返回-1则表示当前轨道已经没有可读取的样本数据了。
getSampleTrackIndex
查询当前样本来源的轨道索引,也就是当前选中的轨道索引。如果当前轨道没有可读取的样本数据,会返回-1。
getSampleTime
返回当前样本的PTS,微秒。
getSmapleFlags
返回当前样本的标记,通过标记可以判断当前样本的一些特征。
- SAMPLE_FLAG_ENCRYPTED 样本数据被加密
- SAMPLE_FLAG_SYNC 样本为关键帧
seekTo
根据指定模式跳转到指定时间,只能跳转到关键帧,所以最终时间可能会和指定时间有误差。模式有三种:
- SEEK_TO_CLOSEST_SYNC 在指定时间的前后查找最近关键帧
- SEEK_TO_NEXT_SYNC 在指定时间的后面查找最近关键帧
- SEEK_TO_PREVIOUS_SYNC 在指定时间的前面查找最近关键帧
advance
移动到下一个样本,每次读取完样本数据后,都需要调用该方法,此时调用getSmaplexxx方法读取到的才是下一个样本的数据。
如果当前轨道没有可读样本,则返回false。
release
释放资源
MediaMuxer类是官方提供的音视频封装类。其官方使用Demo如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
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) { finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo); if (!finished) { int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex; muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo); } };
muxer.stop();
muxer.release();
|
支持的格式
创建封装器
封装器的创建比较简单,根据指定的OutputFormat来创建对应格式的封装器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| static bool isMp4Format(MediaMuxer::OutputFormat format) { return format == MediaMuxer::OUTPUT_FORMAT_MPEG_4 || format == MediaMuxer::OUTPUT_FORMAT_THREE_GPP || format == MediaMuxer::OUTPUT_FORMAT_HEIF; }
MediaMuxer::MediaMuxer(int fd, OutputFormat format) : mFormat(format), mState(UNINITIALIZED) { if (isMp4Format(format)) { mWriter = new MPEG4Writer(fd); } else if (format == OUTPUT_FORMAT_WEBM) { mWriter = new WebmWriter(fd); } else if (format == OUTPUT_FORMAT_OGG) { mWriter = new OggWriter(fd); }
if (mWriter != NULL) { mFileMeta = new MetaData; if (format == OUTPUT_FORMAT_HEIF) { mFileMeta->setInt32(kKeyFileType, output_format::OUTPUT_FORMAT_HEIF); } else if (format == OUTPUT_FORMAT_OGG) { mFileMeta->setInt32(kKeyFileType, output_format::OUTPUT_FORMAT_OGG); } mState = INITIALIZED; } }
|
重要方法详解
addTrack
创建了封装器后,需要添加对应的轨道,轨道的信息通过MediaFormat来描述,也就是上面解封装器中通过getTrackFormat方法获取到的信息,通过这些信息添加一个轨道,添加成功返回轨道的索引,后续write数据的时候使用该索引即可。
对于音频轨道,需要添加的是KEY_SAMPLE_RATE 采样率以及KEY_CHANNEL_COUT声道数,需要注意的是CSD的添加。
对于视频轨道,处理必要的KEY_WIDTH和KEY_HEIGHT外,需要注意的有KEY_BIT_RATE码率和CSD的添加。
关于CSD这里简单介绍下,也就是Codec-Specific Data,对于H.264来说,csd-0的数据为SPS序列参数集,csd-1的数据为PPS图像参数集,而对于AAC来说,csd-0的数据为ESDS。
那么对于MediaFormat的key在不同版本中的支持情况与接封装器中的又不太一样,使用的时候需要注意参考下表。
writeSampleData
将编码后的样本数据写入到封装器中的指定轨道中,参数为轨道索引、数据buffer、bufferInfo。buffer和bufferInfo都是通过MediaCodec编码后产生的,需要注意的是bufferInfo的flag标记.
- BUFFER_FLAG_CODEC_CONFIG 表明buffer中包含CSD数据,而并非样本数据。
- BUFFER_FLAG_END_OF_STREAM 表明流结束。
- BUFFER_FLAG_KEY_FRAME 关键帧
- BUFFER_FLAG_SYNC_FRAME 关键帧,API21废弃
- BUFFER_FLAG_PARTIAL_FRAME 表明当前数据并非完整的一帧,API26添加
这里需要注意的一点是,CSD数据是不能通过该方法来写入的,所以在调用该方法的时候需要判断bufferInfo的flag是否有BUFFER_FLAG_CODEC_CONFIG标记,没有BUFFER_FLAG_CODEC_CONFIG标记的数据才可以用该方法写入。CSD数据需要通过添加到轨道的MediaFormat中,跟随MediaFormt被addTrack到封装器中。
start
启动封装器,启动后无法再添加轨道。
stop
停止封装器。
release
释放封装器。
MediaMetadataRetriever是用于获取媒体文件信息和指定帧数据的API。
重要方法详解
查询指定的媒体信息。常用的有以下几种KEY,需要注意返回的值很有可能为空。
- METADATA_KEY_BITRATE 如果可用,此键检索平均比特率(以比特/秒为单位)。
- METADATA_KEY_CAPTURE_FRAMERATE 该键可检索原始捕获帧率(如果可用)
- METADATA_KEY_DATE 用于检索数据源创建或修改日期的元数据键。
- METADATA_KEY_HAS_AUDIO 如果此键存在,则媒体包含音频内容。
- METADATA_KEY_HAS_VIDEO 如果该键存在,则媒体包含视频内容。
- METADATA_KEY_LOCATION 该键检索位置信息(如果可用)。
- METADATA_KEY_MIMETYPE 用于检索数据源的MIME类型的元数据密钥。
- METADATA_KEY_TITLE 用于检索数据源标题的元数据键。
- METADATA_KEY_VIDEO_HEIGHT 如果媒体包含视频,则此键检索其高度。
- METADATA_KEY_VIDEO_ROTATION 如果可用,此键以度数检索视频旋转角度。
- METADATA_KEY_VIDEO_WIDTH 如果媒体包含视频,则此键检索其宽度。
getFrameAtTime
根据指定模式,返回接近指定时间的关键帧数据,以bitmap格式返回。一般使用该方法获取封面图。
- OPTION_CLOSEST_SYNC 在指定时间的前后查找最近关键帧
- OPTION_NEXT_SYNC 在指定时间的后面查找最近关键帧
- OPTION_PREVIOUS_SYNC 在指定时间的前面查找最近关键帧