音频视频文件帧信息的获取

上面分析的是非音频视频图像文件的扫描流程,在接下来的章节将介绍多媒体文件信息的获取,接下来的部分以MP3格式的音频文件作为分析对象进行介绍:
对于MP3格式大家可以在网上找下,这里就不展开介绍了。

音频视频文件的TAG解析流程分析

在doScanFile中会执行对应的判断,如果当前扫描文件为音频或者视频文件则调用processFile方法进行处理。

public Uri doScanFile(String path, String mimeType, long lastModified,long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {

try {
FileEntry entry = beginFile(path, mimeType, lastModified,
fileSize, isDirectory, noMedia);
//......
if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
if (noMedia) {
//.........
} else {
//........
// 如果是音频文件或者视频文件调用processFile抽取元数据
// we only extract metadata for audio and video files
if (isaudio || isvideo) {
processFile(path, mimeType, this);
}
//.........
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
}
}
} catch (RemoteException e) {
//...................
}
return result;
}

在doScanFile调用的processFile是native方法,
因此首先调用android_media_Mediascanner.cpp.中的android_media_MediaScanner_processFile方法,在该方法中就直接调用StageFrightMediaScanner的processFile方法。

static void
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path,
jstring mimeType, jobject client)
{
MediaScanner *mp = getNativeScanner_l(env, thiz);
//........
//获取MimeType
const char *mimeTypeStr = (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
//...........
MyMediaScannerClient myClient(env, client);
//调用StageFrightMediaScanner的processFile方法
MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
//..........
}

我们先来看下getNativeScanner_l:

static MediaScanner *getNativeScanner_l(JNIEnv* env, jobject thiz) {
return (MediaScanner *) env->GetLongField(thiz, fields.context);
}

它是将fields.context转换为MediaScanner,fields.context这个是怎么来的还记得吧:

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
ALOGV("native_setup");
MediaScanner *mp = new StagefrightMediaScanner;
//..............
env->SetLongField(thiz, fields.context, (jlong)mp);
}

所以这里的getNativeScanner_l返回的是StagefrightMediaScanner
而在StagefrightMediaScanner的processFile方法中则直接调用MediaScannerClient类中的beginFile以及endFile方法还有StagefrightMediaScanner 的processFileInternal方法。

MediaScanResult StagefrightMediaScanner::processFile(
const char *path, const char *mimeType,
MediaScannerClient &client) {
ALOGV("processFile '%s'.", path);

client.setLocale(locale());
//client = MyMediaScannerClient
client.beginFile();
MediaScanResult result = processFileInternal(path, mimeType, client);
client.endFile();
return result;
}

我们先来看下MyMediaScannerClient::beginFile

public FileEntry beginFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean noMedia) {
mMimeType = mimeType;
mFileType = 0;
mFileSize = fileSize;
mIsDrm = false;

if (!isDirectory) {
if (!noMedia && isNoMediaFile(path)) {
noMedia = true;
}
mNoMedia = noMedia;

// try mimeType first, if it is specified
if (mimeType != null) {
mFileType = MediaFile.getFileTypeForMimeType(mimeType);
}

// if mimeType was not specified, compute file type based on file extension.
if (mFileType == 0) {
MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
if (mediaFileType != null) {
mFileType = mediaFileType.fileType;
if (mMimeType == null) {
mMimeType = mediaFileType.mimeType;
}
}
}

if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
mFileType = getFileTypeFromDrm(path);
}
}

FileEntry entry = makeEntryFor(path);
// add some slack to avoid a rounding error
long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
boolean wasModified = delta > 1 || delta < -1;
if (entry == null || wasModified) {
if (wasModified) {
entry.mLastModified = lastModified;
} else {
entry = new FileEntry(0, path, lastModified,
(isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
}
entry.mLastModifiedChanged = true;
}

if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
mPlayLists.add(entry);
// we don't process playlists in the main scan, so return null
return null;
}

// clear all the metadata
mArtist = null;
mAlbumArtist = null;
mAlbum = null;
mTitle = null;
mComposer = null;
mGenre = null;
mTrack = 0;
mYear = 0;
mDuration = 0;
mPath = path;
mLastModified = lastModified;
mWriter = null;
mCompilation = 0;
mWidth = 0;
mHeight = 0;

return entry;
}

从processFileInternal方法开始就开始获取音频视频内部的数据了,在processFileInternal中对文件的处理分如下几个部分:

  • 获取当前音频视频文件的扩展名,看是否是平台所支持的文件类型,如果为空或者不支持就返回MEDIA_SCAN_RESULT_SKIPPED。
  • 根据文件扩展名判断当前扫描的文件是否是DRM文件,如果是的话则开始获取DRM的信息。DRM信息是通过调用drmManagerClient->getMetadata(&tmp)方法来获取的,获取到的信息封装在DrmMetadata对象中。这部分将放在今后的DRM部分介绍。
  • 获取音频视频的TAG信息:
    这部分最重要的是如下两个方法:
    StagefrightMetadataRetriever ::setDataSource
    StagefrightMetadataRetriever ::extractMetadata

这部分代码是比较重要的所以我们将对下面的方法进行详细介绍:

MediaScanResult StagefrightMediaScanner::processFileInternal(
const char *path, const char * /* mimeType */,
MediaScannerClient &client) {

//判断当前文件是否有扩展名
const char *extension = strrchr(path, '.');
if (!extension) {
return MEDIA_SCAN_RESULT_SKIPPED;
}
//判断当前扩展名十分支持
if (!FileHasAcceptableExtension(extension)) {
return MEDIA_SCAN_RESULT_SKIPPED;
}
//创建媒体抽取器
sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
//打开文件
int fd = open(path, O_RDONLY | O_LARGEFILE);
status_t status;
if (fd < 0) {
// couldn't open it locally, maybe the media server can?
//打不开尝试从网络上获取
status = mRetriever->setDataSource(NULL /* httpService */, path);
} else {
//设置数据源
status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);
close(fd);
}

//抽取媒体头数据
const char *value;
if ((value = mRetriever->extractMetadata(METADATA_KEY_MIMETYPE)) != NULL) {
status = client.setMimeType(value);
if (status) {
return MEDIA_SCAN_RESULT_ERROR;
}
}

struct KeyMap {
const char *tag;
int key;
};
static const KeyMap kKeyMap[] = {
{ "tracknumber", METADATA_KEY_CD_TRACK_NUMBER },
{ "discnumber", METADATA_KEY_DISC_NUMBER },
{ "album", METADATA_KEY_ALBUM },
{ "artist", METADATA_KEY_ARTIST },
{ "albumartist", METADATA_KEY_ALBUMARTIST },
{ "composer", METADATA_KEY_COMPOSER },
{ "genre", METADATA_KEY_GENRE },
{ "title", METADATA_KEY_TITLE },
{ "year", METADATA_KEY_YEAR },
{ "duration", METADATA_KEY_DURATION },
{ "writer", METADATA_KEY_WRITER },
{ "compilation", METADATA_KEY_COMPILATION },
{ "isdrm", METADATA_KEY_IS_DRM },
{ "width", METADATA_KEY_VIDEO_WIDTH },
{ "height", METADATA_KEY_VIDEO_HEIGHT },
};
static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]);
//按照kKeyMap表格的顺序从媒体文件中获取TAG并添加到client中
for (size_t i = 0; i < kNumEntries; ++i) {
const char *value;
if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
status = client.addStringTag(kKeyMap[i].tag, value);
if (status != OK) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
}
return MEDIA_SCAN_RESULT_OK;
}

我们先来看下能够支持的媒体格式:

static bool FileHasAcceptableExtension(const char *extension) {
static const char *kValidExtensions[] = {
".mp3", ".mp4", ".m4a", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".mpeg", ".ogg", ".mid", ".smf", ".imy", ".wma", ".aac",
".wav", ".amr", ".midi", ".xmf", ".rtttl", ".rtx", ".ota",
".mkv", ".mka", ".webm", ".ts", ".fl", ".flac", ".mxmf",
".avi", ".mpeg", ".mpg", ".awb", ".mpga"
};
static const size_t kNumValidExtensions =
sizeof(kValidExtensions) / sizeof(kValidExtensions[0]);

for (size_t i = 0; i < kNumValidExtensions; ++i) {
if (!strcasecmp(extension, kValidExtensions[i])) {
return true;
}
}
return false;
}

我们看下setDataSource,它首先会创建一个FileSource。

public native void setDataSource(FileDescriptor fd, long offset, long length)
throws IllegalArgumentException;
}

在setDataSource中我们创建一个FileSource并将其传递到MediaExtractor::Create中根据FileSource来创建出合适的MediaExtractor

status_t StagefrightMetadataRetriever::setDataSource(int fd, int64_t offset, int64_t length) {
fd = dup(fd);
ALOGV("setDataSource(%d, %" PRId64 ", %" PRId64 ")", fd, offset, length);
clearMetadata();
mSource = new FileSource(fd, offset, length);
status_t err;
//检查是否打开文件成功
if ((err = mSource->initCheck()) != OK) {
mSource.clear();
return err;
}
//将FileSource传入后根据FileSource的信息创建出合适的MediaExtractor
mExtractor = MediaExtractor::Create(mSource);
if (mExtractor == NULL) {
mSource.clear();
return UNKNOWN_ERROR;
}
return OK;
}

我们接下来看下是如何创建对应的MediaExtractor的,首先会先调用DataSource的sniff,在DataSource的sniff中会调用每个注册的Sniffers对其进行探测,来选出最匹配的。以mimetype形式返回。根据返回的mimetype创建MediaExtractor

// static
sp<MediaExtractor> MediaExtractor::Create(const sp<DataSource> &source, const char *mime) {
sp<AMessage> meta;
String8 tmp;
if (mime == NULL) {
float confidence;
//首先我们会调用DataSource的sniff
if (!source->sniff(&tmp, &confidence, &meta)) {
ALOGV("FAILED to autodetect media content.");
return NULL;
}

mime = tmp.string();
ALOGV("Autodetected media content as '%s' with confidence %.2f",
mime, confidence);
}

bool isDrm = false;
// DRM MIME type syntax is "drm+type+original" where
// type is "es_based" or "container_based" and
// original is the content's cleartext MIME type
if (!strncmp(mime, "drm+", 4)) {
const char *originalMime = strchr(mime+4, '+');
if (originalMime == NULL) {
// second + not found
return NULL;
}
++originalMime;
if (!strncmp(mime, "drm+es_based+", 13)) {
// DRMExtractor sets container metadata kKeyIsDRM to 1
return new DRMExtractor(source, originalMime);
} else if (!strncmp(mime, "drm+container_based+", 20)) {
mime = originalMime;
isDrm = true;
} else {
return NULL;
}
}

MediaExtractor *ret = NULL;
if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG4)
|| !strcasecmp(mime, "audio/mp4")) {
ret = new MPEG4Extractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_MPEG)) {
ret = new MP3Extractor(source, meta);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_NB)
|| !strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_WB)) {
ret = new AMRExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_FLAC)) {
ret = new FLACExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WAV)) {
ret = new WAVExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_OGG)) {
ret = new OggExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MATROSKA)) {
ret = new MatroskaExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2TS)) {
ret = new MPEG2TSExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WVM)) {
// Return now. WVExtractor should not have the DrmFlag set in the block below.
return new WVMExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AAC_ADTS)) {
ret = new AACExtractor(source, meta);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2PS)) {
ret = new MPEG2PSExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_MIDI)) {
ret = new MidiExtractor(source);
}
if (ret != NULL) {
if (isDrm) {
ret->setDrmFlag(true);
} else {
ret->setDrmFlag(false);
}
}
return ret;
}

下面是sniff方法。

bool DataSource::sniff(String8 *mimeType, float *confidence, sp<AMessage> *meta) {
*mimeType = "";
*confidence = 0.0f;
meta->clear();
{
Mutex::Autolock autoLock(gSnifferMutex);
if (!gSniffersRegistered) {
return false;
}
}
for (List<SnifferFunc>::iterator it = gSniffers.begin();
it != gSniffers.end(); ++it) {
String8 newMimeType;
float newConfidence;
sp<AMessage> newMeta;
//调用各种嗅探器的方法获取类型和可信度
if ((*it)(this, &newMimeType, &newConfidence, &newMeta)) {
if (newConfidence > *confidence) {
*mimeType = newMimeType;
*confidence = newConfidence;
*meta = newMeta;
}
}
}
return *confidence > 0.0;
}

这些sniff是在StagefrightMetadataRetriever创建的时候注册的:

StagefrightMetadataRetriever::StagefrightMetadataRetriever()
: mParsedMetaData(false),
mAlbumArt(NULL) {
//注册默认的音频格式嗅探器
DataSource::RegisterDefaultSniffers();
CHECK_EQ(mClient.connect(), (status_t)OK);
}

下面是注册的sniff

//static
void DataSource::RegisterDefaultSniffers() {
Mutex::Autolock autoLock(gSnifferMutex);
if (gSniffersRegistered) {
return;
}
RegisterSniffer_l(SniffMPEG4);
RegisterSniffer_l(SniffMatroska);
RegisterSniffer_l(SniffOgg);
RegisterSniffer_l(SniffWAV);
RegisterSniffer_l(SniffFLAC);
RegisterSniffer_l(SniffAMR);
RegisterSniffer_l(SniffMPEG2TS);
RegisterSniffer_l(SniffMP3);
RegisterSniffer_l(SniffAAC);
RegisterSniffer_l(SniffMPEG2PS);
RegisterSniffer_l(SniffWVM);
RegisterSniffer_l(SniffMidi);

char value[PROPERTY_VALUE_MAX];
if (property_get("drm.service.enabled", value, NULL)
&& (!strcmp(value, "1") || !strcasecmp(value, "true"))) {
RegisterSniffer_l(SniffDRM);
}
gSniffersRegistered = true;
}

那么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,
float *confidence, sp<AMessage> *meta) {
off64_t pos = 0;
off64_t post_id3_pos;
uint32_t header;
if (!Resync(source, 0, &pos, &post_id3_pos, &header)) {
return false;
}

*meta = new AMessage;
(*meta)->setInt64("offset", pos);
(*meta)->setInt32("header", header);
(*meta)->setInt64("post-id3-offset", post_id3_pos);

*mimeType = MEDIA_MIMETYPE_AUDIO_MPEG;
*confidence = 0.2f;

return true;
}

static bool Resync(
const sp<DataSource> &source, uint32_t match_header,
off64_t *inout_pos, off64_t *post_id3_pos, uint32_t *out_header) {
if (post_id3_pos != NULL) {
*post_id3_pos = 0;
}
//跳过标题帧的位置开始同步。
if (*inout_pos == 0) {
// Skip an optional ID3 header if syncing at the very beginning
// of the datasource.

for (;;) {
uint8_t id3header[10];
//读取头10个字节,如果不够10个字节表示存在错误返回false
if (source->readAt(*inout_pos, id3header, sizeof(id3header))
< (ssize_t)sizeof(id3header)) {
// If we can't even read these 10 bytes, we might as well bail
// out, even if there _were_ 10 bytes of valid mp3 audio data...
return false;
}
//如果头3个字节不是ID3退出该循环
if (memcmp("ID3", id3header, 3)) {
break;
}

// Skip the ID3v2 header.
// 获取标签帧的长度
size_t len =
((id3header[6] & 0x7f) << 21)
| ((id3header[7] & 0x7f) << 14)
| ((id3header[8] & 0x7f) << 7)
| (id3header[9] & 0x7f);

len += 10;

*inout_pos += len; // inout_pos 指向音乐内容区的首地址

ALOGV("skipped ID3 tag, new starting offset is %lld (0x%016llx)",
(long long)*inout_pos, (long long)*inout_pos);
}

if (post_id3_pos != NULL) {
*post_id3_pos = *inout_pos;
}
}

off64_t pos = *inout_pos;
bool valid = false;

const size_t kMaxReadBytes = 1024;
const size_t kMaxBytesChecked = 128 * 1024;
uint8_t buf[kMaxReadBytes];
ssize_t bytesToRead = kMaxReadBytes;
ssize_t totalBytesRead = 0;
ssize_t remainingBytes = 0;
bool reachEOS = false;
uint8_t *tmp = buf;

do {
//如果当前的扫描位置超过音乐内容区的kMaxBytesChecked(128K)就停止扫描
if (pos >= (off64_t)(*inout_pos + kMaxBytesChecked)) {
// Don't scan forever.
ALOGV("giving up at offset %lld", (long long)pos);
break;
}
//加载一个缓冲区的内容。
if (remainingBytes < 4) {
if (reachEOS) {
break;
} else {
memcpy(buf, tmp, remainingBytes);
bytesToRead = kMaxReadBytes - remainingBytes;

/*
* The next read position should start from the end of
* the last buffer, and thus should include the remaining
* bytes in the buffer.
*/
totalBytesRead = source->readAt(pos + remainingBytes,
buf + remainingBytes,
bytesToRead);
if (totalBytesRead <= 0) {
break;
}
reachEOS = (totalBytesRead != bytesToRead);
totalBytesRead += remainingBytes;
remainingBytes = totalBytesRead;
tmp = buf;
continue;
}
}
//从tmp缓冲区中取出头
uint32_t header = U32_AT(tmp);

if (match_header != 0 && (header & kMask) != (match_header & kMask)) {
//如果帧的格式校验失败则从当前其实位置下一个位置开始获取32字节
++pos;
++tmp;
--remainingBytes;
continue;
}

size_t frame_size;
int sample_rate, num_channels, bitrate;
if (!GetMPEGAudioFrameSize(
header, &frame_size,
&sample_rate, &num_channels, &bitrate)) {
++pos;
++tmp;
--remainingBytes;
continue;
}

ALOGV("found possible 1st frame at %lld (header = 0x%08x)", (long long)pos, header);

// We found what looks like a valid frame,
// now find its successors.

off64_t test_pos = pos + frame_size;

valid = true;
//再连续读取3三个音乐内容区的帧头进行检查,如果都正确就退出检测,将找到的音乐内容区域的位置以及音乐内容帧的帧头返回。
for (int j = 0; j < 3; ++j) {
uint8_t tmp[4];
if (source->readAt(test_pos, tmp, 4) < 4) {
valid = false;
break;
}

uint32_t test_header = U32_AT(tmp);

ALOGV("subsequent header is %08x", test_header);

if ((test_header & kMask) != (header & kMask)) {
valid = false;
break;
}

size_t test_frame_size;
if (!GetMPEGAudioFrameSize(
test_header, &test_frame_size)) {
valid = false;
break;
}

ALOGV("found subsequent frame #%d at %lld", j + 2, (long long)test_pos);

test_pos += test_frame_size;
}

if (valid) {
*inout_pos = pos;

if (out_header != NULL) {
*out_header = header;
}
} else {
ALOGV("no dice, no valid sequence of frames found.");
}

++pos;
++tmp;
--remainingBytes;
} while (!valid);

return valid;
}

在MediaExtractor::Create 方法中我们通过sniff来识别当前音频的文件类型,并根据返回的mimeType以及可行度来创建具体的Extractor ,我们这里以MP3文件为例,根据sniff判断后在MediaExtractor::Create中将会创建MP3Extractor对象,在MP3Extractor构造方法中,主要是从传入的header中获取采样频率,通道数,帧大小等数据,并将其存入MetaData对象中。

MP3Extractor::MP3Extractor(
const sp<DataSource> &source, const sp<AMessage> &meta)
: mInitCheck(NO_INIT),
mDataSource(source),
mFirstFramePos(-1),
mFixedHeader(0) {
off64_t pos = 0;
off64_t post_id3_pos;
uint32_t header;
bool success;

//下面这些值在SniffMP3中已经获取了
int64_t meta_offset;/*表示帧数据的偏移*/
uint32_t meta_header;/*表示帧头*/
int64_t meta_post_id3_offset;/*表示TAB帧的偏移*/
if (meta != NULL&& meta->findInt64("offset", &meta_offset)
&& meta->findInt32("header", (int32_t *)&meta_header)
&& meta->findInt64("post-id3-offset", &meta_post_id3_offset)) {
// The sniffer has already done all the hard work for us, simply
// accept its judgement.
pos = (off64_t)meta_offset;
header = meta_header;
post_id3_pos = (off64_t)meta_post_id3_offset;
success = true;
} else {
}

mFirstFramePos = pos;
mFixedHeader = header;
mMeta = new MetaData;

size_t frame_size;
int sample_rate;
int num_channels;
int bitrate;
GetMPEGAudioFrameSize(header, &frame_size, &sample_rate, &num_channels, &bitrate);

unsigned layer = 4 - ((header >> 17) & 3);

switch (layer) {
case 1:
mMeta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG_LAYER_I);
break;
case 2:
mMeta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG_LAYER_II);
break;
case 3:
mMeta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG);
break;
default:
TRESPASS();
}

mMeta->setInt32(kKeySampleRate, sample_rate);
mMeta->setInt32(kKeyBitRate, bitrate * 1000);
mMeta->setInt32(kKeyChannelCount, num_channels);

int64_t durationUs;

if (durationUs >= 0) {
mMeta->setInt64(kKeyDuration, durationUs);
}

mInitCheck = OK;

// Get iTunes-style gapless info if present.
// When getting the id3 tag, skip the V1 tags to prevent the source cache
// from being iterated to the end of the file.
ID3 id3(mDataSource, true);
if (id3.isValid()) {
ID3::Iterator *com = new ID3::Iterator(id3, "COM");
if (com->done()) {
delete com;
com = new ID3::Iterator(id3, "COMM");
}
while(!com->done()) {
String8 commentdesc;
String8 commentvalue;
com->getString(&commentdesc, &commentvalue);
const char * desc = commentdesc.string();
const char * value = commentvalue.string();

// first 3 characters are the language, which we don't care about
if(strlen(desc) > 3 && strcmp(desc + 3, "iTunSMPB") == 0) {
int32_t delay, padding;
if (sscanf(value, " %*x %x %x %*x", &delay, &padding) == 2) {
mMeta->setInt32(kKeyEncoderDelay, delay);
mMeta->setInt32(kKeyEncoderPadding, padding);
}
break;
}
com->next();
}
delete com;
com = NULL;
}
}

到目前为止我们已经识别出了当前扫描音频文件的格式类型,并从对应的音乐内容数据帧头部(HEAD)获取到了采样率,比特率等信息,但是我们还有一部分非常重要的信息需要获取,那就是标签帧的信息,在那里记录者歌曲作者,专辑名,歌曲名,甚至专辑封面图片和内嵌歌词等信息,下面部分我们就重点介绍这些信息的获取过程。
我们再次回到processFileInternal方法,Tags文件的获取是在mRetriever->extractMetadata(kKeyMap[i].key)中完成的,
在extractMetadata方法传入的参数为要寻找的那个Tag的key,如果当前尚未对帧标签帧进行解析则先调用parseMetaData方法对Tag标签进行解析,如果已经解析过了则这时候就使用传人到keyCode 到mMetaData中进行查找,并返回的需要查找到那个Tag的值.

const char *StagefrightMetadataRetriever::extractMetadata(int keyCode) {
if (mExtractor == NULL) {
return NULL;
}
//调用parseMetaData()解析标签帧
if (!mParsedMetaData) {
parseMetaData();
mParsedMetaData = true;
}
ssize_t index = mMetaData.indexOfKey(keyCode);
if (index < 0) {
return NULL;
}
/将数据添加到mMetaData
return mMetaData.valueAt(index).string();
}
void StagefrightMetadataRetriever::parseMetaData() {
//获取MetaData
sp<MetaData> meta = mExtractor->getMetaData();

//TAG对应的map
struct Map {
int from;
int to;
const char *name;
};
static const Map kMap[] = {
{ kKeyMIMEType, METADATA_KEY_MIMETYPE, NULL },
{ kKeyCDTrackNumber, METADATA_KEY_CD_TRACK_NUMBER, "tracknumber" },
{ kKeyDiscNumber, METADATA_KEY_DISC_NUMBER, "discnumber" },
{ kKeyAlbum, METADATA_KEY_ALBUM, "album" },
{ kKeyArtist, METADATA_KEY_ARTIST, "artist" },
{ kKeyAlbumArtist, METADATA_KEY_ALBUMARTIST, "albumartist" },
{ kKeyAuthor, METADATA_KEY_AUTHOR, NULL },
{ kKeyComposer, METADATA_KEY_COMPOSER, "composer" },
{ kKeyDate, METADATA_KEY_DATE, NULL },
{ kKeyGenre, METADATA_KEY_GENRE, "genre" },
{ kKeyTitle, METADATA_KEY_TITLE, "title" },
{ kKeyYear, METADATA_KEY_YEAR, "year" },
{ kKeyWriter, METADATA_KEY_WRITER, "writer" },
{ kKeyCompilation, METADATA_KEY_COMPILATION, "compilation" },
{ kKeyLocation, METADATA_KEY_LOCATION, NULL },
};
static const size_t kNumMapEntries = sizeof(kMap) / sizeof(kMap[0]);
//创建判断字符编码检测器
CharacterEncodingDetector *detector = new CharacterEncodingDetector();
//将对应的TAG添加到detector中
for (size_t i = 0; i < kNumMapEntries; ++i) {
const char *value;
if (meta->findCString(kMap[i].from, &value)) {
if (kMap[i].name) {
// add to charset detector
//这里没有直接添加到mMetaData而是先添加到CharacterEncodingDetector的mNames,mValues中等待转换编码
detector->addTag(kMap[i].name, value);
} else {
// directly add to output list
mMetaData.add(kMap[i].to, String8(value));
}
}
}
/*
void CharacterEncodingDetector::addTag(const char *name, const char *value) {
mNames.push_back(name);
mValues.push_back(value);
}
×/
//开始转化编码,这部分放在后面进行介绍
detector->detectAndConvert();
int size = detector->size();
if (size) {
//将转换后的TAG放到mMetaData,这时候整个mMetaData存放的都是正确编码后的内容
for (int i = 0; i < size; i++) {
const char *name;
const char *value;
detector->getTag(i, &name, &value);
for (size_t j = 0; j < kNumMapEntries; ++j) {
if (kMap[j].name && !strcmp(kMap[j].name, name)) {
mMetaData.add(kMap[j].to, String8(value));
}
}
}
}
delete detector;

const void *data;
uint32_t type;
size_t dataSize;
if (meta->findData(kKeyAlbumArt, &type, &data, &dataSize)&& mAlbumArt == NULL) {
mAlbumArt = MediaAlbumArt::fromData(dataSize, data);
}

size_t numTracks = mExtractor->countTracks();

char tmp[32];
sprintf(tmp, "%zu", numTracks);

mMetaData.add(METADATA_KEY_NUM_TRACKS, String8(tmp));

float captureFps;
if (meta->findFloat(kKeyCaptureFramerate, &captureFps)) {
sprintf(tmp, "%f", captureFps);
mMetaData.add(METADATA_KEY_CAPTURE_FRAMERATE, String8(tmp));
}

bool hasAudio = false;
bool hasVideo = false;
int32_t videoWidth = -1;
int32_t videoHeight = -1;
int32_t audioBitrate = -1;
int32_t rotationAngle = -1;

// The overall duration is the duration of the longest track.
int64_t maxDurationUs = 0;
String8 timedTextLang;
for (size_t i = 0; i < numTracks; ++i) {
sp<MetaData> trackMeta = mExtractor->getTrackMetaData(i);
int64_t durationUs;
if (trackMeta->findInt64(kKeyDuration, &durationUs)) {
if (durationUs > maxDurationUs) {
maxDurationUs = durationUs;
}
}
const char *mime;
if (trackMeta->findCString(kKeyMIMEType, &mime)) {
if (!hasAudio && !strncasecmp("audio/", mime, 6)) {
hasAudio = true;

if (!trackMeta->findInt32(kKeyBitRate, &audioBitrate)) {
audioBitrate = -1;
}
} else if (!hasVideo && !strncasecmp("video/", mime, 6)) {
hasVideo = true;
CHECK(trackMeta->findInt32(kKeyWidth, &videoWidth));
CHECK(trackMeta->findInt32(kKeyHeight, &videoHeight));
if (!trackMeta->findInt32(kKeyRotation, &rotationAngle)) {
rotationAngle = 0;
}
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_TEXT_3GPP)) {
const char *lang;
trackMeta->findCString(kKeyMediaLanguage, &lang);
timedTextLang.append(String8(lang));
timedTextLang.append(String8(":"));
}
}
}

// To save the language codes for all timed text tracks
// If multiple text tracks present, the format will look
// like "eng:chi"
if (!timedTextLang.isEmpty()) {
mMetaData.add(METADATA_KEY_TIMED_TEXT_LANGUAGES, timedTextLang);
}

// The duration value is a string representing the duration in ms.
sprintf(tmp, "%" PRId64, (maxDurationUs + 500) / 1000);
mMetaData.add(METADATA_KEY_DURATION, String8(tmp));

if (hasAudio) {
mMetaData.add(METADATA_KEY_HAS_AUDIO, String8("yes"));
}

if (hasVideo) {
mMetaData.add(METADATA_KEY_HAS_VIDEO, String8("yes"));

sprintf(tmp, "%d", videoWidth);
mMetaData.add(METADATA_KEY_VIDEO_WIDTH, String8(tmp));

sprintf(tmp, "%d", videoHeight);
mMetaData.add(METADATA_KEY_VIDEO_HEIGHT, String8(tmp));

sprintf(tmp, "%d", rotationAngle);
mMetaData.add(METADATA_KEY_VIDEO_ROTATION, String8(tmp));
}

if (numTracks == 1 && hasAudio && audioBitrate >= 0) {
sprintf(tmp, "%d", audioBitrate);
mMetaData.add(METADATA_KEY_BITRATE, String8(tmp));
} else {
off64_t sourceSize;
if (mSource->getSize(&sourceSize) == OK) {
int64_t avgBitRate = (int64_t)(sourceSize * 8E6 / maxDurationUs);

sprintf(tmp, "%" PRId64, avgBitRate);
mMetaData.add(METADATA_KEY_BITRATE, String8(tmp));
}
}

if (numTracks == 1) {
const char *fileMIME;
CHECK(meta->findCString(kKeyMIMEType, &fileMIME));

if (!strcasecmp(fileMIME, "video/x-matroska")) {
sp<MetaData> trackMeta = mExtractor->getTrackMetaData(0);
const char *trackMIME;
CHECK(trackMeta->findCString(kKeyMIMEType, &trackMIME));

if (!strncasecmp("audio/", trackMIME, 6)) {
// The matroska file only contains a single audio track,
// rewrite its mime type.
mMetaData.add(
METADATA_KEY_MIMETYPE, String8("audio/x-matroska"));
}
}
}

// To check whether the media file is drm-protected
if (mExtractor->getDrmFlag()) {
mMetaData.add(METADATA_KEY_IS_DRM, String8("1"));
}
}

从上面代码中可以看出完成关键工作就是从Meta中取出各个TAG,经过字符编码转换后添加到mMetaData中。

我们看到这里不要忽略了getMetaData,getMetaData方法中通过创建用于对存储在文件头的ID3V2标签帧和存储在文件尾部的ID3V1帧进行解析的ID3对象来完成标签帧到解析,这里解析的内容包括基本的歌曲信息标签以及歌词,专辑封面等信息。想要真正了解整个解析过程我们还得继续看下ID3类的构造方法和ID3::Iterator方法。

sp<MetaData> MP3Extractor::getMetaData() {
sp<MetaData> meta = new MetaData;

if (mInitCheck != OK) {
return meta;
}
//设置mimetype
meta->setCString(kKeyMIMEType, "audio/mpeg");
//创建id3
ID3 id3(mDataSource);

if (!id3.isValid()) {
return meta;
}

struct Map {
int key;
const char *tag1;
const char *tag2;
};
static const Map kMap[] = {
{ kKeyAlbum, "TALB", "TAL" },
{ kKeyArtist, "TPE1", "TP1" },
{ kKeyAlbumArtist, "TPE2", "TP2" },
{ kKeyComposer, "TCOM", "TCM" },
{ kKeyGenre, "TCON", "TCO" },
{ kKeyTitle, "TIT2", "TT2" },
{ kKeyYear, "TYE", "TYER" },
{ kKeyAuthor, "TXT", "TEXT" },
{ kKeyCDTrackNumber, "TRK", "TRCK" },
{ kKeyDiscNumber, "TPA", "TPOS" },
{ kKeyCompilation, "TCP", "TCMP" },
};
static const size_t kNumMapEntries = sizeof(kMap) / sizeof(kMap[0]);
//获取上面提到的TAG
for (size_t i = 0; i < kNumMapEntries; ++i) {
ID3::Iterator *it = new ID3::Iterator(id3, kMap[i].tag1);
if (it->done()) {
delete it;
it = new ID3::Iterator(id3, kMap[i].tag2);
}
if (it->done()) {
delete it;
continue;
}
String8 s;
it->getString(&s);
delete it;
meta->setCString(kMap[i].key, s);
}

size_t dataSize;
String8 mime;
//获取专辑封面
const void *data = id3.getAlbumArt(&dataSize, &mime);
if (data) {
meta->setData(kKeyAlbumArt, MetaData::TYPE_NONE, data, dataSize);
meta->setCString(kKeyAlbumArtMIME, mime.string());
}
return meta;
}

我们先看下ID3的构造方法,在ID3方法中,会先调用parseV2,如果parseV2返回false的话会调用parseV1,也就是说ID3会首先解析位于文件头到ID3V2标签帧,如果解析失败则会尝试解析ID3V1帧。

ID3::ID3(const sp<DataSource> &source, bool ignoreV1, off64_t offset)
: mIsValid(false),
mData(NULL),
mSize(0),
mFirstFrameOffset(0),
mVersion(ID3_UNKNOWN),
mRawSize(0) {
//解析ID3V2Tab内容
mIsValid = parseV2(source, offset);

if (!mIsValid && !ignoreV1) {
//解析ID3V1 TAB 内容
mIsValid = parseV1(source);
}
}

而在parseV2以及parseV1方法中实际上也还没开始解析,它们只是开辟个mData 空间将对应的TAG标签加载到mData中,后续的解析工作将会针对mData中的这些数据进行解析。

bool ID3::parseV2(const sp<DataSource> &source) {
struct id3_header {
//标签头10个字节
char id[3]; /*必须为"ID3"否则认为标签不存在*/
uint8_t version_major; /*版本号ID3V2.3就记录3*/
uint8_t version_minor; /*副版本号此版本记录为0*/
uint8_t flags; /*存放标志的字节,这个版本只定义了三位*/
uint8_t enc_size[4]; /*标签大小,不包括标签头的10个字节*/
};
id3_header header;
if (source->readAt(0, &header, sizeof(header)) != (ssize_t)sizeof(header)) {
return false;
}
/*必须为"ID3"否则认为标签不存在*/
if (memcmp(header.id, "ID3", 3)) {
return false;
}
//如果主版本号和副版本号都是0xff则表示错误
if (header.version_major == 0xff || header.version_minor == 0xff) {
return false;
}
if (header.version_major == 2) {
//如果主版本号为2
//............................................
} else if (header.version_major == 3) {
//如果主版本号为3
if (header.flags & 0x1f) {
// We only support the 3 high bits, if any of the lower bits are
// set, we cannot guarantee to understand the tag format.
return false;
}
} else if (header.version_major == 4) {
//如果主版本号为4
} else {
//如果主版本号不是上述的则直接返回false
return false;
}
//获取标签大小
size_t size;
if (!ParseSyncsafeInteger(header.enc_size, &size)) {
return false;
}
//static const size_t kMaxMetadataSize = 3 * 1024 * 1024;
//如果大小超过3M则退出
if (size > kMaxMetadataSize) {
return false;
}
//分配用于存放标签内容的空间
mData = (uint8_t *)malloc(size);
mSize = size;
mRawSize = mSize + sizeof(header);
//mRawSize 表示 标签帧大小加上 标签头大小10个字节
//读取标签帧数据
if (source->readAt(sizeof(header), mData, mSize) != (ssize_t)mSize) {
free(mData);
mData = NULL ;
return false;
}
//..........................................................
mFirstFrameOffset = 0;
//..........................................................
if (header.version_major == 2) {
mVersion = ID3_V2_2;
} else if (header.version_major == 3) {
mVersion = ID3_V2_3;
} else {
CHECK_EQ(header.version_major, 4);
mVersion = ID3_V2_4;
}
return true;
}
bool ID3::parseV1(const sp<DataSource> &source) {
const size_t V1_TAG_SIZE = 128;
off64_t size;
if (source->getSize(&size) != OK || size < (off64_t)V1_TAG_SIZE) {
return false;
}

//分配空间
mData = (uint8_t *)malloc(V1_TAG_SIZE);
//读取位于文件尾部128字节的V1标签帧
if (source->readAt(size - V1_TAG_SIZE, mData, V1_TAG_SIZE)
!= (ssize_t)V1_TAG_SIZE) {
free(mData);
mData = NULL;
return false;
}
if (memcmp("TAG", mData, 3)) {
free(mData);
mData = NULL;
return false;
}
mSize = V1_TAG_SIZE;
mFirstFrameOffset = 3;
if (mData[V1_TAG_SIZE - 3] != 0) {
mVersion = ID3_V1;
} else {
mVersion = ID3_V1_1;
}
return true;
}

真正的解析流程是从Iterator开始的,在Iterator中先是调用strdup(id)获取当前要获取的Tag到ID值放到mID中,接着调用findFrame找到对应到帧。

ID3::Iterator::Iterator(const ID3 &parent, const char *id)
: mParent(parent),
mID(NULL),
mOffset(mParent.mFirstFrameOffset),
mFrameData(NULL),
mFrameSize(0) {
if (id) {
mID = strdup(id);
}
//找到帧对应的位置
findFrame();
}

在findFrame中将当前ID与存储着全部标签帧数据的mData空间中的每个帧进行对比,如果找到帧标志等于当前ID的数据的时候就退出遍历循环,这时候mID就指向要寻找标签数据的位置。

void ID3::Iterator::findFrame() {
for (;;) {
mFrameData = NULL;
mFrameSize = 0;
if (mParent.mVersion == ID3_V2_2) {
//..................................................
} else if (mParent.mVersion == ID3_V2_3
|| mParent.mVersion == ID3_V2_4) {
//这是正常的MP3格式
if (mOffset + 10 > mParent.mSize) {
return;
}
//如果头四个字节为0000则返回
if (!memcmp(&mParent.mData[mOffset], "\0\0\0\0", 4)) {
return;
}
size_t baseSize;
if (mParent.mVersion == ID3_V2_4) {
//.......................................................................
} else {
//获取标签帧的内容大小
baseSize = U32_AT(&mParent.mData[mOffset + 4]);
}
//表示包含帧头的总共大小
mFrameSize = 10 + baseSize;
if (mOffset + mFrameSize > mParent.mSize) {
return;
}
//当前标签帧的内容数据
mFrameData = &mParent.mData[mOffset + 10];
if (!mID) {
break;
}
//将Tab 复制到id上,判断id是否等于mID如果不等则继续查找
char id[5];
memcpy(id, &mParent.mData[mOffset], 4);
id[4] = '\0';
//如果等于要找的ID 则退出循环,这时候mOffset 指向的是要查找帧的位置
if (!strcmp(id, mID)) {
break;
}
} else {
//.................................................
}
mOffset += mFrameSize;
}
}

获取专辑图片:
专辑图片的获取过程实际和获取其他TAG的方式是一样的,也是通过遍历存放ID3标签原始数据的mData空间,找到标签为”APIC”的数据,然后再将指向专辑图片的数据的首地址返回。

const void *
ID3::getAlbumArt(size_t *length, String8 *mime) const {
*length = 0;
mime->setTo("");
//APIC Attached picture 定位专辑图片的位置
Iterator it(*this,(mVersion == ID3_V2_3 || mVersion == ID3_V2_4) ? "APIC" : "PIC");
while (!it.done()) {
size_t size;
//指向专辑图片数据
const uint8_t *data = it.getData(&size);
if (mVersion == ID3_V2_3 || mVersion == ID3_V2_4) {
uint8_t encoding = data[0];
mime->setTo((const char *)&data[1]);
size_t mimeLen = strlen((const char *)&data[1]) + 1;
//返回图片的类型
uint8_t picType = data[1 + mimeLen];
//图片描述的内容长度
size_t descLen = StringSize(&data[2 + mimeLen], encoding);
//图片的实际长度
*length = size - 2 - mimeLen - descLen;
//返回图片的地址
return &data[2 + mimeLen + descLen];
} else {
//...........................................................
}
}
return NULL;
}

上述就是获取各个标签帧数据的流程,找出标签帧的值后就将可以将其取出存储到mMetaData中了。
我们再回到processFileInternal方法,看下接下来需要做哪些操作:它将会调用status = client.addStringTag(kKeyMap[i].tag, value);将该Tag传递到MediaScannerClient中进行处理,那么在MediaScannerClient中会做哪些处理呢?

status_t MediaScannerClient::addStringTag(const char* name, const char* value)
{
handleStringTag(name, value);
return OK;
}

在MediaScanner.java中将从底层传来的对应值赋给对应的成员变量。至此就完成了音频视频文件的Tag标签的获取过程。接下来和其他普通文件的处理方式一样,就是通过MediaProvider将其存储到数据库中。从而完成媒体扫描的整个过程。

public void handleStringTag(String name, String value) {
if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
// Don't trim() here, to preserve the special \001 character
// used to force sorting. The media provider will trim() before
// inserting the title in to the database.
mTitle = value;
} else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
mArtist = value.trim();
} else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
|| name.equalsIgnoreCase("band") || name.startsWith("band;")) {
mAlbumArtist = value.trim();
} else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
mAlbum = value.trim();
} else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
mComposer = value.trim();
} else if (mProcessGenres &&
(name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {
mGenre = getGenreName(value);
} else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
mYear = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
// track number might be of the form "2/12"
// we just read the number before the slash
int num = parseSubstring(value, 0, 0);
mTrack = (mTrack / 1000) * 1000 + num;
} else if (name.equalsIgnoreCase("discnumber") ||
name.equals("set") || name.startsWith("set;")) {
// set number might be of the form "1/3"
// we just read the number before the slash
int num = parseSubstring(value, 0, 0);
mTrack = (num * 1000) + (mTrack % 1000);
} else if (name.equalsIgnoreCase("duration")) {
mDuration = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
mWriter = value.trim();
} else if (name.equalsIgnoreCase("compilation")) {
mCompilation = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("isdrm")) {
mIsDrm = (parseSubstring(value, 0, 0) == 1);
} else if (name.equalsIgnoreCase("width")) {
mWidth = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("height")) {
mHeight = parseSubstring(value, 0, 0);
} else {
//Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");
}
}

至此TAG的解析以及解码过程已经结束,我们最后还需要看下handleStringTag这个方法,这个方法中会将解码后的TAG如何做处理呢?
我们首先需要看下这个方法是如何从native层上升到Java层的,说到这个就必须考虑android_media_MediaScanner.cpp这个过渡类,在这个文件中我们可以找到handleStringTag这个方法,和其他一样在这里调用了CallVoidMethod方法进入到java层。这样java层中的handleStringTag就会被调用。

virtual status_t handleStringTag(const char* name, const char* value)
{
ALOGV("MediaScanner handleStringTag: name(%s) and value(%s)", name, value);
jstring nameStr, valueStr;
if ((nameStr = mEnv->NewStringUTF(name)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
char *cleaned = NULL;
//如果不是正常的UTF-8则将其置为?
if (!isValidUtf8(value)) {
cleaned = strdup(value);
char *chp = cleaned;
char ch;
while ((ch = *chp)) {
if (ch & 0x80) {
*chp = '?';
}
chp++;
}
value = cleaned;
}
valueStr = mEnv->NewStringUTF(value);
free(cleaned);
if (valueStr == NULL) {
mEnv->DeleteLocalRef(nameStr);
mEnv->ExceptionClear();
return NO_MEMORY;
}
//调用JAVA层的handleStringTag方法。
mEnv->CallVoidMethod(mClient, mHandleStringTagMethodID, nameStr, valueStr);
mEnv->DeleteLocalRef(nameStr);
mEnv->DeleteLocalRef(valueStr);
return checkAndClearExceptionFromCallback(mEnv, "handleStringTag");
}

最后还是上个图吧,虽然流程简单但是还是很杂的,容易乱:
![](/Android-源码分析之MediaScanner-2.md/1.png)

附录:帧标识的含义

AENC    	Audio encryption
APIC Attached picture
COMM Comments
COMR Commercial
ENCR Encryption method registration
EQUA Equalization
ETCO Event timing codes
GEOB General encapsulated object
GRID Group identification registration
IPLS Involved people list
LINK Linked information
MCDI Music CD identifier
MLLT MPEGlocation lookup table
OWNE Ownership
PRIV Private
PCNT Playcounter
POPM Popularimeter
POSS Position synchronisation
RBUF Recommended buffer size
RVAD Relative volume adjustment
RVRB Reverb
SYLT Synchronized lyric/text
SYTC Synchronized tempo codes
TALB Album/Movie/Show title
TBPM BPM(beats per minute)
TCOM Composer
TCON Content type
TCOP Copyright message
TDAT Date
TDLY Playlist delay
TENC Encoded by
TEXT Lyricist/Text writer
TFLT Filetype
TIME Time
TIT1 Content group deion
TIT2 Title/songname/content deion
TIT3 Subtitle/Deion refinement
TKEY Initial key
TLAN Language(s)
TLEN Length
TMED Media type
TOAL Original album/movie/show title
TOFN Original filename
TOLY Original lyricist(s)/text writer(s)
TOPE Original artist(s)/performer(s)
TORY Original release year
TOWN Fileowner/licensee
TPE1 Leadperformer(s)/Soloist(s)
TPE2 Band/orchestra/accompaniment
TPE3 Conductor/performer refinement
TPE4 Interpreted, remixed, or otherwise modified by
TPOS Partof a set
TPUB Publisher
TRCK Track number/Position in set
TRDA Recording dates
TRSN Internet radio station name
TRSO Internet radio station owner
TSIZ Size
TSRC ISRC(international standard recording code)
TSSE Software/Hardware and settings used for encoding
TYER Year
TXXX Userdefined text information
UFID Unique file identifier
USER Terms of use
USLT Unsychronized lyric/text tranion
WCOM Commercial information
WCOP Copyright/Legal information
WOAF Official audio file webpage
WOAR Official artist/performer webpage
WOAS Official audio source webpage
WORS Official internet radio station homepage
WPAY Payment
WPUB Publishers official webpage
WXXX Userdefined URL link
Contents
  1. 1. 音频视频文件帧信息的获取
    1. 1.1. 音频视频文件的TAG解析流程分析