关于媒体文件扫描,我们需要弄明白两个问题:
1.什么时候开启媒体文件扫描
2.如何解析媒体文件(音频,视频,图片)信息插入到数据库中,对应code流程
我们现在音乐,视频播放器,图库等应用关于音视频图片等信息都是通过多媒体数据库直接查询
在源码 packages\providers\下有个叫MediaProvider(进程名:android.process.media)
它主要处理管理这个媒体数据库,处理MTP,铃声选择也在当中.
在MediaProvider中有一个广播接收类MediaScannerReceiver,当中监听以下几个广播:
"android.intent.action.BOOT_COMPLETED" /> //开机完成
"android.intent.action.MEDIA_MOUNTED" /> //挂载
"android.intent.action.MEDIA_UNMOUNTED" /> //卸载
"android.intent.action.MEDIA_SCANNER_SCAN_FILE" /> //扫描文件
"android.intent.action.ACTION_SHUTDOWN" /> //关机
--------------------------------------------------------------------------------
//以下两个是我司内部加的
"android.intent.action.ACTION_SHUTDOWN_IPO" />
"android.intent.action.OVERTIME_REMOVAL" /> // 热插拔优化广播
从源码看,会启动扫描动作的是BOOT_COMPLETED(开机),MEDIA_MOUNTED(挂载)
MEDIA_UNMOUNTED(卸载),MEDIA_SCANNER_SCAN_FILE(扫描文件)会执行扫描.
在开机后会执行
scan(context, MediaProvider.INTERNAL_VOLUME); //扫描内部存储
scanUntilAllStorageMounted(context);//挂载大于5秒或者挂载成功后执行
scan(context, MediaProvider.EXTERNAL_VOLUME);//扫描外部存储
private void scan(Context context, String volume) { Bundle args = new Bundle(); args.putString("volume", volume); context.startService( new Intent(context, MediaScannerService.class).putExtras(args));}
启动单个文件扫描
private void scanFile(Context context, String path) { Bundle args = new Bundle(); args.putString(“filepath”, path); context.startService( new Intent(context, MediaScannerService.class).putExtras(args));}
我们可以看到当调用scan(),scanFile()方法,会启动MediaScannerService服务执行扫描
该类继承Service实现Runnable
总结:
MediaScannerReceiver是用来接收任务,它收到广播后,会启动MediaSannerService进行扫描工作。
在MediaScannerService onCreate中开启了线程,Service是跑在主线程的,扫描是个耗时的操作,避免堵塞开启线程
public class MediaScannerService extends Service implements Runnable
Thread thr = new Thread(null, this, "MediaScannerService");thr.start();
//在线程run()中拿到线程消息队列,初始化handler
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE);
Looper.prepare();
mServiceLooper = Looper.myLooper();
mServiceHandler = new ServiceHandler(); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE);
Looper.loop();}
在onStartCommand中会实际执行扫描调用
//在onStartCommand中会实际执行扫描调用
if (arguments.getString(“filepath”) != null) {
//文件扫描
what = MSG_SCAN_SINGLE_FILE;
Message msg = mServiceHandler.obtainMessage(what, startId, -1, arguments); mServiceHandler.sendMessage(msg);
} else {
//存储扫描 // Cancel un-finished task
synchronized (MediaScannerService.this) {
………………………………..
mPrescanTaskList.remove(cancelList);
PrescanTask prescanTask = new PrescanTask(arguments,mServiceHandler,startId); mPrescanTaskList.add(prescanTask);
prescanTask.execute();
//使用AsyncTask
}}
在PrescanTask doInBackground中执行预扫描的操作,在onPostExecute中会发送message执行目录扫描操作
ServiceHandler处理四种message
1.文件路径扫描 handleScanSingleFile(msg);
2.目录扫描 handleScanDirectory(msg);
3.关闭扫描线程池 handleShutdownThreadpool();
4.扫描完成 handleScanFinish();
在PrescanTask doInBackground中执行预扫描的操作,在onPostExecute中会发送message执行目录扫描操作
ServiceHandler处理四种message
1.文件路径扫描 handleScanSingleFile(msg);
2.目录扫描 handleScanDirectory(msg);
3.关闭扫描线程池 handleShutdownThreadpool();
4.扫描完成 handleScanFinish();
让我们看看目录扫描代码
private void handleScanDirectory(Message msg) {
Bundle arguments = (Bundle) msg.obj;
try {
……
if (MediaProvider.INTERNAL_VOLUME.equals(volume))
{
// scan internal media storage
directories = new String[] {
Environment.getRootDirectory() + "/media",
};
// scan external storage volumes
/// M: MediaScanner Performance turning {@
/// Thread pool enable, use threadpool to scan.
//支持线程池 使用线程池扫描
if (mIsThreadPoolEnable) {
mStartId = msg.arg1;
}
}
/// @}
}
if (directories != null) {
long start = System.currentTimeMillis();
scan(directories, volume);
long end = System.currentTimeMillis(); ...............................................
}
} catch (Exception e) {
/// M: MediaScanner Performance turning {@ /// Only stop service when thread pool terminate
stopSelfResult(mStartId);
mStartId = msg.arg1; }
else {
} /// @}
}
1.使用线程池调用scanWithThreadPool()
2.不使用线程池最终都是通过MediaScanner进行扫描处理
1.使用线程池调用scanWithThreadPool()
2.不使用线程池最终都是通过MediaScanner进行扫描处理
private void scan(String[] directories, String volumeName) {
Uri uri = Uri.parse("file://" + directories[0]);
// don't sleep while scanning
mWakeLock.acquire();
try { ………..
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
//发送开始扫描广播
try { ……….
try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
scanner.scanDirectories(directories);
//最终扫描由MediaScanner中的 scanDirectories处理
} } catch (Exception e) {
MtkLog.e(TAG, "exception in MediaScanner.scan()", e);
}}
getContentResolver().delete(scanUri, null, null);
} catch (Exception ex) {
MtkLog.e(TAG, "exception in MediaScanner.scan()", ex);
} finally {
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
//发送结束扫描广播
mWakeLock.release();
MtkLog.d(TAG, "scan>>>: volumeName = " + volumeName + ", directories = " + Arrays.toString(directories));
}}
MediaScanner是多媒体库扫描核心类之一,干实事的类,把解析后的数据插入到数据库当中就是在该类中完成
MediaScanner源码路径在 frameworks\base\media\java\android\media\MediaScanner.java
MediaScanner.java 注释如下:
* In summary:
* Java MediaScannerService calls
* Java MediaScanner scanDirectories, which calls
* native MediaScanner processDirectory, which calls
* native MyMediaScannerClient scanFile, which calls
* Java MyMediaScannerClient scanFile, which calls
* Java MediaScanner processFile (native method), which calls
* native MediaScanner processFile, which calls
* native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
* Java MyMediaScanner handleStringTag.
* Once MediaScanner processFile returns, an entry is inserted in to the database.*
* The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner.*
在MediaScannerService当中最终通过scan()调用到MediaScanner中的scanDirectories()执行扫描
public void scanDirectories(String[] directories) {
try {
long start = System.currentTimeMillis();
prescan(null, true);//扫描前预处理
long prescan = System.currentTimeMillis();
if (ENABLE_BULK_INSERTS) {
// create MediaInserter for bulk inserts mMediaInserter = new MediaInserter(mMediaProvider, 500);
}
for (int i = 0; i < directories.length; i++) {
processDirectory(directories[i], mClient);
//JNI调用最终处理在MediaScanner.cpp中processDirectory()处理
if (ENABLE_BULK_INSERTS) {
// flush remaining inserts
mMediaInserter.flushAll();
mMediaInserter = null;
}
………………………………………….
processDirectory()的调用最终会回到,MyMediaScannerClient(位置在MediaScaner.java中)的doScanFile
MyMediaScannerClient是MediaScanner中的内部类,所有重要关键的操作都在该类中完成。
public Uri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
Uri result = null;
……
boolean isaudio = MediaFile.isAudioFileType(mFileType);
boolean isvideo = MediaFile.isVideoFileType(mFileType);
boolean isimage = MediaFile.isImageFileType(mFileType);
……
// we only extract metadata for audio and video files
if (isaudio || isvideo) {
processFile(path, mimeType, this);
/* processFile 是一个 native 方法,最终实现在 native层StagefrightMediaScanner.cpp 中
}
if (isimage) {
processImageFile(path);
}
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
//当数据全部解析完成后,通过endFile插入到数据库当中
……
return result;
}
在doScanFile()中调用到非常重要的方法processFile,这最终实现是在StagefrightMediaScanner.cpp当中,让我们看看processFile当中做了什么
StagefrightMediaScanner.cpp源码路径frameworks\av\media\libstagefright\StagefrightMediaScanner.cpp
MediaScanResult StagefrightMediaScanner::processFile(
const char *path, const char *mimeType,
MediaScannerClient &client) {#ifdef MTK_AOSP_ENHANCEMENT
ALOGI("processFile '%s'.", path);
#else
ALOGV("processFile '%s'.", path);#endif
// #ifdef MTK_AOSP_ENHANCEMENT
client.setLocale(locale());//设置语言
client.beginFile(); MediaScanResult result = processFileInternal(path, mimeType, client);
//在processFileInternal中调用了两个StagefrightMetadataRetriever中重要的两个方法
setDataSource(),extractMetadata() client.endFile(); return result;}
调用了StagefrightMetadataRetriever.cpp中的setDataSource(),在该方法中会调用MediaExtractor.cpp的Create创建不同格式的解析器,音视频解析就看这些东东了
status_t StagefrightMetadataRetriever::setDataSource(
const sp &httpService,
const char *uri,
const KeyedVector *headers) {
#ifdef MTK_AOSP_ENHANCEMENT
ATRACE_CALL();#endif
ALOGV("setDataSource(%s)", uri);
clearMetadata();
mSource = DataSource::CreateFromURI(httpService, uri, headers); …..
#ifdef MTK_AOSP_ENHANCEMENT
const char * sniffedMIME = NULL;
if ((!strncasecmp("/system/media/audio/", uri, 20)) && (strcasestr(uri,".ogg") != NULL)) {
sniffedMIME = MEDIA_MIMETYPE_CONTAINER_OGG; } mExtractor = MediaExtractor::Create(mSource, sniffedMIME);//创建不同的解析器
#else mExtractor = MediaExtractor::Create(mSource);#endif
在extractMetadata()调用到解析器中parseMetaData()解析文件
解析后的信息如何插入到数据库中呢?
在StagefrightMediaScanner的processFileInternal中会把解析到信息通过addStringTag()传回MyMediaScannerClient中的handleStringTag中
看看handleStringTag做了什么
看以下备注
只是一个简单赋值
最后插入数据库由endFile去完成
通过MTP拷贝文件到手机,这个扫描流程是怎样的呢?
PC发SendObjectInfo命令给MtpServer。MtpServer需要检查存储设备剩余空间、可支持的最大文件大小。如果一切正常的话,它会通过MediaProvider的insert函数往媒体数据库中加入一条数据项。
接着PC通过SendObject将文件内容传递给给MtpServer。而MtpServer就会创建该文件,并把数据写到文件中。
当文件数据发送完毕,MtpServer调用endSendObject。而endObject则会触发MediaScanner进行媒体文件扫描。当然,扫描完后,该文件携带的媒体信息(假如是MP3文件的话,则会把专辑信息、歌手、流派、长度等内容)加入到媒体数据库中。
最终通过调用MtpDatabase.java中的endSendObject方法调用MediaScanner的scanMtpFile执行文件扫描,流程跟上面类似