Android 源码分析之MediaScanner[2]
音频视频文件帧信息的获取
上面分析的是非音频视频图像文件的扫描流程,在接下来的章节将介绍多媒体文件信息的获取,接下来的部分以MP3格式的音频文件作为分析对象进行介绍:
对于MP3格式大家可以在网上找下,这里就不展开介绍了。
音频视频文件的TAG解析流程分析
在doScanFile中会执行对应的判断,如果当前扫描文件为音频或者视频文件则调用processFile方法进行处理。
public Uri doScanFile(String path, String mimeType, long lastModified,long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { |
在doScanFile调用的processFile是native方法,
因此首先调用android_media_Mediascanner.cpp.中的android_media_MediaScanner_processFile方法,在该方法中就直接调用StageFrightMediaScanner的processFile方法。
static void |
我们先来看下getNativeScanner_l:
static MediaScanner *getNativeScanner_l(JNIEnv* env, jobject thiz) { |
它是将fields.context转换为MediaScanner,fields.context这个是怎么来的还记得吧:
static void |
所以这里的getNativeScanner_l返回的是StagefrightMediaScanner
而在StagefrightMediaScanner的processFile方法中则直接调用MediaScannerClient类中的beginFile以及endFile方法还有StagefrightMediaScanner 的processFileInternal方法。
MediaScanResult StagefrightMediaScanner::processFile( |
我们先来看下MyMediaScannerClient::beginFile
public FileEntry beginFile(String path, String mimeType, long lastModified, |
从processFileInternal方法开始就开始获取音频视频内部的数据了,在processFileInternal中对文件的处理分如下几个部分:
- 获取当前音频视频文件的扩展名,看是否是平台所支持的文件类型,如果为空或者不支持就返回MEDIA_SCAN_RESULT_SKIPPED。
- 根据文件扩展名判断当前扫描的文件是否是DRM文件,如果是的话则开始获取DRM的信息。DRM信息是通过调用drmManagerClient->getMetadata(&tmp)方法来获取的,获取到的信息封装在DrmMetadata对象中。这部分将放在今后的DRM部分介绍。
- 获取音频视频的TAG信息:
这部分最重要的是如下两个方法:
StagefrightMetadataRetriever ::setDataSource
StagefrightMetadataRetriever ::extractMetadata
这部分代码是比较重要的所以我们将对下面的方法进行详细介绍:
MediaScanResult StagefrightMediaScanner::processFileInternal( |
我们先来看下能够支持的媒体格式:
static bool FileHasAcceptableExtension(const char *extension) { |
我们看下setDataSource,它首先会创建一个FileSource。
public native void setDataSource(FileDescriptor fd, long offset, long length) |
在setDataSource中我们创建一个FileSource并将其传递到MediaExtractor::Create中根据FileSource来创建出合适的MediaExtractor
status_t StagefrightMetadataRetriever::setDataSource(int fd, int64_t offset, int64_t length) { |
我们接下来看下是如何创建对应的MediaExtractor的,首先会先调用DataSource的sniff,在DataSource的sniff中会调用每个注册的Sniffers对其进行探测,来选出最匹配的。以mimetype形式返回。根据返回的mimetype创建MediaExtractor
// static |
下面是sniff方法。
bool DataSource::sniff(String8 *mimeType, float *confidence, sp<AMessage> *meta) { |
这些sniff是在StagefrightMetadataRetriever创建的时候注册的:
StagefrightMetadataRetriever::StagefrightMetadataRetriever() |
下面是注册的sniff
//static |
那么sniff又是怎样完成文件类型判断的任务的呢?我们看到上面的RegisterDefaultSniffers我们以MP3为例子,那么使用的sniff为SniffMP3
这里我们只关注MP3的sniff–SniffMP3。SniffMP3首先会调用Resync方法对音乐内容帧的帧头进行重新重定位,它会在音乐内容帧内对帧头进行搜索:每次读入1024字节的内容到待检测缓冲区,再从缓冲区中每次读取4个字节32位进行匹配,如果找到匹配的话还会读入后续的连续3个帧的帧头数据进行检测。如果4次检测均成功的话将当前位置作为音乐内容帧的起始位置返回,检测过程是通过GetMPEGAudioFrameSize方法来完成的,如果当前待检测缓冲区内数据没有匹配的则再次读入数据到缓冲区直到检测的位置达到最大检测字节数128*1024字节为止。
bool SniffMP3(const sp<DataSource> &source, String8 *mimeType, |
|
在MediaExtractor::Create 方法中我们通过sniff来识别当前音频的文件类型,并根据返回的mimeType以及可行度来创建具体的Extractor ,我们这里以MP3文件为例,根据sniff判断后在MediaExtractor::Create中将会创建MP3Extractor对象,在MP3Extractor构造方法中,主要是从传入的header中获取采样频率,通道数,帧大小等数据,并将其存入MetaData对象中。
MP3Extractor::MP3Extractor( |
到目前为止我们已经识别出了当前扫描音频文件的格式类型,并从对应的音乐内容数据帧头部(HEAD)获取到了采样率,比特率等信息,但是我们还有一部分非常重要的信息需要获取,那就是标签帧的信息,在那里记录者歌曲作者,专辑名,歌曲名,甚至专辑封面图片和内嵌歌词等信息,下面部分我们就重点介绍这些信息的获取过程。
我们再次回到processFileInternal方法,Tags文件的获取是在mRetriever->extractMetadata(kKeyMap[i].key)中完成的,
在extractMetadata方法传入的参数为要寻找的那个Tag的key,如果当前尚未对帧标签帧进行解析则先调用parseMetaData方法对Tag标签进行解析,如果已经解析过了则这时候就使用传人到keyCode 到mMetaData中进行查找,并返回的需要查找到那个Tag的值.
const char *StagefrightMetadataRetriever::extractMetadata(int keyCode) { |
void StagefrightMetadataRetriever::parseMetaData() { |
从上面代码中可以看出完成关键工作就是从Meta中取出各个TAG,经过字符编码转换后添加到mMetaData中。
我们看到这里不要忽略了getMetaData,getMetaData方法中通过创建用于对存储在文件头的ID3V2标签帧和存储在文件尾部的ID3V1帧进行解析的ID3对象来完成标签帧到解析,这里解析的内容包括基本的歌曲信息标签以及歌词,专辑封面等信息。想要真正了解整个解析过程我们还得继续看下ID3类的构造方法和ID3::Iterator方法。
sp<MetaData> MP3Extractor::getMetaData() { |
我们先看下ID3的构造方法,在ID3方法中,会先调用parseV2,如果parseV2返回false的话会调用parseV1,也就是说ID3会首先解析位于文件头到ID3V2标签帧,如果解析失败则会尝试解析ID3V1帧。
ID3::ID3(const sp<DataSource> &source, bool ignoreV1, off64_t offset) |
而在parseV2以及parseV1方法中实际上也还没开始解析,它们只是开辟个mData 空间将对应的TAG标签加载到mData中,后续的解析工作将会针对mData中的这些数据进行解析。
bool ID3::parseV2(const sp<DataSource> &source) { |
bool ID3::parseV1(const sp<DataSource> &source) { |
真正的解析流程是从Iterator开始的,在Iterator中先是调用strdup(id)获取当前要获取的Tag到ID值放到mID中,接着调用findFrame找到对应到帧。
ID3::Iterator::Iterator(const ID3 &parent, const char *id) |
在findFrame中将当前ID与存储着全部标签帧数据的mData空间中的每个帧进行对比,如果找到帧标志等于当前ID的数据的时候就退出遍历循环,这时候mID就指向要寻找标签数据的位置。
void ID3::Iterator::findFrame() { |
获取专辑图片:
专辑图片的获取过程实际和获取其他TAG的方式是一样的,也是通过遍历存放ID3标签原始数据的mData空间,找到标签为”APIC”的数据,然后再将指向专辑图片的数据的首地址返回。
const void * |
上述就是获取各个标签帧数据的流程,找出标签帧的值后就将可以将其取出存储到mMetaData中了。
我们再回到processFileInternal方法,看下接下来需要做哪些操作:它将会调用status = client.addStringTag(kKeyMap[i].tag, value);将该Tag传递到MediaScannerClient中进行处理,那么在MediaScannerClient中会做哪些处理呢?
status_t MediaScannerClient::addStringTag(const char* name, const char* value) |
在MediaScanner.java中将从底层传来的对应值赋给对应的成员变量。至此就完成了音频视频文件的Tag标签的获取过程。接下来和其他普通文件的处理方式一样,就是通过MediaProvider将其存储到数据库中。从而完成媒体扫描的整个过程。
public void handleStringTag(String name, String value) { |
至此TAG的解析以及解码过程已经结束,我们最后还需要看下handleStringTag这个方法,这个方法中会将解码后的TAG如何做处理呢?
我们首先需要看下这个方法是如何从native层上升到Java层的,说到这个就必须考虑android_media_MediaScanner.cpp这个过渡类,在这个文件中我们可以找到handleStringTag这个方法,和其他一样在这里调用了CallVoidMethod方法进入到java层。这样java层中的handleStringTag就会被调用。
virtual status_t handleStringTag(const char* name, const char* value) |
附录:帧标识的含义
AENC Audio encryption |