1. 程式人生 > >Android多媒體檔案掃描流程

Android多媒體檔案掃描流程

插入u盤後,系統會發送廣播android.intent.action.MEDIA_MOUNTED
位於
packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java
MediaScannerService可以接收開機廣播和u盤插入廣播,收到廣播後執行scan方法,將檔案或資料夾路徑儲存到Bundle,並啟動MediaScannerService,我們看MediaScannerService中ServiceHandler的程式碼

private final class ServiceHandler
extends Handler {
@Override public void handleMessage(Message msg) { Bundle arguments = (Bundle) msg.obj; String filePath = arguments.getString("filepath");//單個檔案 String path = arguments.getString("path");//檔案目錄 try { if
(filePath != null) { IBinder binder = arguments.getIBinder("listener"); IMediaScannerListener listener = (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder)); Uri uri = null; try
{ uri = scanFile(filePath, arguments.getString("mimetype"));//掃描單個檔案 } catch (Exception e) { Log.e(TAG, "Exception scanning file", e); } if (listener != null) { listener.scanCompleted(filePath, uri); } } else { String volume = arguments.getString("volume"); String[] directories = null; if (MediaProvider.INTERNAL_VOLUME.equals(volume)) { // scan internal media storage directories = new String[] { Environment.getRootDirectory() + "/media", }; } else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) { // scan external storage volumes if (path == null) { directories = mExternalStoragePaths; } else if (path.startsWith("/mnt/usb_storage")) { directories = new String[] {path};//{"/mnt/usb_storage"}; } else { directories = new String[] {path}; } } if (directories != null) { if (true) Log.d(TAG, "start scanning volume " + volume + ": " + Arrays.toString(directories)); scan(directories, volume);//掃描檔案列表 if (true) Log.d(TAG, "done scanning volume " + volume); } } } catch (Exception e) { Log.e(TAG, "Exception in handleMessage", e); } stopSelf(msg.arg1); } };

可以看到程式碼會呼叫scan或者scanFile掃描

 private void scan(String[] directories, String volumeName) {
        Uri uri = Uri.parse("file://" + directories[0]);
        // don't sleep while scanning
        mWakeLock.acquire();

        try {
            ContentValues values = new ContentValues();
            values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
            Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);

            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

            try {
                if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
                    openDatabase(volumeName);
                }

            MediaScanner scanner = createMediaScanner();
        if(directories!=null){
              scanner.scanDirectories(directories, volumeName);
        }
        } catch (Exception e) {
            Log.e(TAG, "exception in MediaScanner.scan()", e);
        }

            getContentResolver().delete(scanUri, null, null);

        } finally {
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
            mWakeLock.release();
        }
    }
 private Uri scanFile(String path, String mimeType) {
        String volumeName = MediaProvider.EXTERNAL_VOLUME;
        openDatabase(volumeName);
        MediaScanner scanner = createMediaScanner();
        try {
            // make sure the file path is in canonical form
            String canonicalPath = new File(path).getCanonicalPath();
            return scanner.scanSingleFile(canonicalPath, volumeName, mimeType);
        } catch (Exception e) {
            Log.e(TAG, "bad path " + path + " in scanFile()", e);
            return null;
        }
    }

下一步進入到MediaScanner.java,檔案在

frameworks/base/media/java/android/media/MediaScanner.java

下面是掃描單個檔案的程式碼

// this function is used to scan a single file
    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
        try {
            initialize(volumeName);
            prescan(path, true);

            File file = new File(path);
            if (!file.exists()) {
                return null;
            }

            // lastModified is in milliseconds on Files.
            long lastModifiedSeconds = file.lastModified() / 1000;

            // always scan the file, so we can return the content://media Uri for existing files
            return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
                    false, true, MediaScanner.isNoMediaPath(path));
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
            return null;
        }
    }
//初始化預設的uri
private void initialize(String volumeName) {
        mMediaProvider = mContext.getContentResolver().acquireProvider("media");

        mAudioUri = Audio.Media.getContentUri(volumeName);
        mVideoUri = Video.Media.getContentUri(volumeName);
        mImagesUri = Images.Media.getContentUri(volumeName);
        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
        mFilesUri = Files.getContentUri(volumeName);
        mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();

        if (!volumeName.equals("internal")) {
            // we only support playlists on external media
            mProcessPlaylists = true;
            mProcessGenres = true;
            mPlaylistsUri = Playlists.getContentUri(volumeName);

            mCaseInsensitivePaths = true;
        }
    }
//掃描預處理,暫時不知道做什麼用
private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
        Cursor c = null;
        String where = null;
        String[] selectionArgs = null;

        if (mPlayLists == null) {
            mPlayLists = new ArrayList<FileEntry>();
        } else {
            mPlayLists.clear();
        }

        if (filePath != null) {
            // query for only one file
            where = MediaStore.Files.FileColumns._ID + ">?" +
                " AND " + Files.FileColumns.DATA + " like '"+filePath+"%' ";
            selectionArgs = new String[] { "" };
        } else {
            where = MediaStore.Files.FileColumns._ID + ">?";
            selectionArgs = new String[] { "" };
        }
        if (DEBUG_MEDIASCANNER)
            Log.d(TAG,"-----------enter prescan,filePath = "+filePath+"where = "+where);

        // Tell the provider to not delete the file.
        // If the file is truly gone the delete is unnecessary, and we want to avoid
        // accidentally deleting files that are really there (this may happen if the
        // filesystem is mounted and unmounted while the scanner is running).
        Uri.Builder builder = mFilesUri.buildUpon();
        builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
        MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, mPackageName,
                builder.build());

        // Build the list of files from the content provider
        try {
            if (prescanFiles) {
                // First read existing files from the files table.
                // Because we'll be deleting entries for missing files as we go,
                // we need to query the database in small batches, to avoid problems
                // with CursorWindow positioning.
                long lastId = Long.MIN_VALUE;
                Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
                mWasEmptyPriorToScan = true;

                while (true) {
                    selectionArgs[0] = "" + lastId;
                    if (c != null) {
                        c.close();
                        c = null;
                    }
                    c = mMediaProvider.query(mPackageName, limitUri, FILES_PRESCAN_PROJECTION,
                            where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
                    if (c == null) {
                        break;
                    }

                    int num = c.getCount();

                    if (num == 0) {
                        break;
                    }
                    mWasEmptyPriorToScan = false;
                    while (c.moveToNext()) {
                        long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
                        String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
                        int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
                        long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
                        lastId = rowId;

                        // Only consider entries with absolute path names.
                        // This allows storing URIs in the database without the
                        // media scanner removing them.
                        if (path != null && path.startsWith("/")) {
                            boolean exists = false;
                            try {
                                exists = Libcore.os.access(path, libcore.io.OsConstants.F_OK);
                            } catch (ErrnoException e1) {
                            }
                            if (!exists && !MtpConstants.isAbstractObject(format)) {
                                // do not delete missing playlists, since they may have been
                                // modified by the user.
                                // The user can delete them in the media player instead.
                                // instead, clear the path and lastModified fields in the row
                                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
                                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);

                                if (!MediaFile.isPlayListFileType(fileType)) {
                                    deleter.delete(rowId);
                                    if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
                                        deleter.flush();
                                        String parent = new File(path).getParent();
                                        mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL,
                                                parent, null);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        finally {
            if (c != null) {
                c.close();
            }
            deleter.flush();
        }

        // compute original size of images
        mOriginalCount = 0;
        c = mMediaProvider.query(mPackageName, mImagesUri, ID_PROJECTION, null, null, null, null);
        if (c != null) {
            mOriginalCount = c.getCount();
            c.close();
        }
    }

下面是真正的掃描實現

public Uri doScanFile(String path, String mimeType, long lastModified,
                long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
            Uri result = null;
//            long t1 = System.currentTimeMillis();
            try {
                FileEntry entry = beginFile(path, mimeType, lastModified,
                        fileSize, isDirectory, noMedia);
                if (path.startsWith("/mnt/usb_storage"))
                    mIsUsbStorage =true;
                else
                    mIsUsbStorage =false;
                // if this file was just inserted via mtp, set the rowid to zero
                // (even though it already exists in the database), to trigger
                // the correct code path for updating its entry
                if (mMtpObjectHandle != 0) {
                    entry.mRowId = 0;
                }
                // rescan for metadata if file was modified since last scan
                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
                    if (noMedia) {
                        result = endFile(entry, false, false, false, false, false);
                    } else {
                        String lowpath = path.toLowerCase(Locale.ROOT);
                        boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
                        boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
                        boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
                        boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
                        boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
                            (!ringtones && !notifications && !alarms && !podcasts);
//判斷是否是音視訊格式,來決定下面是否對其掃描。
 boolean isaudio = MediaFile.isAudioFileType(mFileType);
 boolean isvideo = MediaFile.isVideoFileType(mFileType);
 boolean isimage = MediaFile.isImageFileType(mFileType);

 if (isaudio || isvideo || isimage) {
                            if (mExternalIsEmulated && path.startsWith(mExternalStoragePath)) {
  // try to rewrite the path to bypass the sd card fuse layer
 String directPath = Environment.getMediaStorageDirectory() +                                 path.substring(mExternalStoragePath.length());
       File f = new File(directPath);
   if (f.exists()) {
                   path = directPath;
                      }
               }
       }
 // we only extract metadata for audio and video files
    if (isaudio /*|| isvideo */)
            {
             //jni方法呼叫,真正的掃描開始
             processFile(path, mimeType, this);
             }
   //if (isimage) {
      //    processImageFile(path);
//}
  result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
                    }
                }
            } catch (RemoteException e) {
                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
            }
//            long t2 = System.currentTimeMillis();
//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
            return result;
        }

在路徑frameworks/base/media/java/android/media/MediaFile.java
可以找到音視訊掃描所支援的型別。

 public class MediaFile {
   39 
   40     // Audio file types
   41     public static final int FILE_TYPE_MP3     = 1;
   42     public static final int FILE_TYPE_M4A     = 2;
   43     public static final int FILE_TYPE_WAV     = 3;
   44     public static final int FILE_TYPE_AMR     = 4;
   45     public static final int FILE_TYPE_AWB     = 5;

        ..........................
  static {
  205         addFileType("MP3", FILE_TYPE_MP3, "audio/mpeg", MtpConstants.FORMAT_MP3);
  206         addFileType("MP3", FILE_TYPE_MP3, "audio/lume", MtpConstants.FORMAT_MP3);
  207         addFileType("MPGA", FILE_TYPE_MP3, "audio/mpeg", MtpConstants.FORMAT_MP3);
  208         addFileType("M4A", FILE_TYPE_M4A, "audio/mp4", MtpConstants.FORMAT_MPEG);
    ............................

其中有一段程式碼

  180     private static boolean isWMAEnabled() {
  181     List<AudioDecoder> decoders = DecoderCapabilities.getAudioDecoders();
  182    int count = decoders.size();
  183    for (int i = 0; i < count; i++) {
  184      AudioDecoder decoder = decoders.get(i);
      //當下麵條件成立才行
  185   if (decoder == AudioDecoder.AUDIO_DECODER_WMA) {
  186                 return true;
  187             }
  188         }
  189         return false;
  190     }
        .......................
  212         if (isWMAEnabled()) {                                                                                                        
">213         addFileType("WMA", FILE_TYPE_WMA, "audio/x-ms-wma", MtpConstants.FORMAT_WMA);
  214         }

WMA音訊格式是Google的專利,所以想支援掃描wma格式,可以去grep “AUDIO_DECODER_WMA” -rn .
在frameworks/av/media/libmedia/MediaProfiles.cpp中

const char *defaultXmlFile = "/etc/media_profiles.xml"

在板級中device/XXX/XXXX/config/media_profiles.xml

<AudioDecoderCap name="wma" enabled="false"/>

將false改為true即可。
我們看一下英文說明

 MediaScanner.processFile().
 * - MediaScanner.processFile() calls one of several methods, depending on the type of the
 *   file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
 * - each of these methods gets metadata key/value pairs from the file, and repeatedly
 *   calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
 *   counterparts in this file.

大意就是,processFile會根據檔案型別呼叫不用的方法,並由MyMediaScannerClient.handleStringTag回撥

這段native方法可以在下面的檔案目錄找到

frameworks/base/media/jni/android_media_MediaScanner.cpp
static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    ALOGV("processFile");

    // Lock already hold by processDirectory
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "No scanner available");
        return;
    }

    if (path == NULL) {
        jniThrowException(env, kIllegalArgumentException, NULL);
        return;
    }

    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }

    const char *mimeTypeStr =
        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
    if (mimeType && mimeTypeStr == NULL) {  // Out of memory
        // ReleaseStringUTFChars can be called with an exception pending.
        env->ReleaseStringUTFChars(path, pathStr);
        return;
    }

    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning file '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}

mp->processFile在路徑
通過grep搜尋發現,方法定義在檔案frameworks/av/media/libmedia/MediaScanner.h
可以看到這是一個虛擬函式

virtual MediaScanResult processFile(
            const char *path, const char *mimeType, MediaScannerClient &client) = 0;

虛擬函式定義如下:如果父類的函式(方法)根本沒有必要或者無法實現,完全要依賴子類去實現的話,可以把此函式(方法)設為virtual 函式名=0 我們把這樣的函式(方法)稱為純虛擬函式如果一個類包含了純虛擬函式,稱此類為抽象類。
我們接著往下跟,找到真正實現的地方
回到前一個jni

frameworks/base/media/jni/android_media_MediaScanner.cpp

這裡面有例項化mediascanner的地方

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    ALOGV("native_setup");
    MediaScanner *mp = new StagefrightMediaScanner;

    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "Out of memory");
        return;
    }

    env->SetIntField(thiz, fields.context, (int)mp);
}

馬上就能找到例項化的地方了,開啟

frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp
MediaScanResult StagefrightMediaScanner::processFile(
        const char *path, const char *mimeType,
        MediaScannerClient &client) {
    ALOGV("processFile '%s'.", path);

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

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;
    }

    //根據副檔名呼叫不同的解析方法
    if (!strcasecmp(extension, ".mid")
            || !strcasecmp(extension, ".smf")
            || !strcasecmp(extension, ".imy")
            || !strcasecmp(extension, ".midi")
            || !strcasecmp(extension, ".xmf")
            || !strcasecmp(extension, ".rtttl")
            || !strcasecmp(extension, ".rtx")
            || !strcasecmp(extension, ".ota")
            || !strcasecmp(extension, ".mxmf")) {
        return HandleMIDI(path, &client);
    }
    //parse ape audio file
    if (!strcasecmp(extension, ".ape")) {
        return parseAPE(path, client);
    }
    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(path);
    } else {
        status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);
        close(fd);
    }

    if (status) {
        return MEDIA_SCAN_RESULT_ERROR;
    }

    const char *value;
    if ((value = mRetriever->extractMetadata(
                    METADATA_KEY_MIMETYPE)) != NULL) {
        status = client.setMimeType(value);
        if (status) {
            return MEDIA_SCAN_RESULT_ERROR;
        }
    }

android支援哪些音視訊格式型別在FileHasAcceptableExtension(extension)函式中定義的

static bool FileHasAcceptableExtension(const char *extension) {                                                                        
   41     static const char *kValidExtensions[] = {
   42         ".mp3", ".mp4", ".m4a", ".3gp", ".3gpp", ".3g2", ".3gpp2",
   43         ".mpeg", ".ogg", ".mid", ".smf", ".imy", ".wma", ".aac",
   44         ".wav", ".amr", ".midi", ".xmf", ".rtttl", ".rtx", ".ota",
2> 45         ".mkv", ".mka", ".webm", ".ts", ".fl", ".flac", ".mxmf",
.> 46         ".avi", ".mpeg", ".mpg", ".awb", ".mpga", ".wmv", ".flv", ".rm", ".rmvb", ".mov", ".vob", ".m4v", ".f4v", ".amv", ".dat"
   47     };
   48     static const size_t kNumValidExtensions =
   49         sizeof(kValidExtensions) / sizeof(kValidExtensions[0]);
}> 50 
   51     for (size_t i = 0; i < kNumValidExtensions; ++i) {
   52         if (!strcasecmp(extension, kValidExtensions[i])) {
   53             return true;
   54         }
   55     }
   56 
   57     return false;
   58 }

如果想增加某種音訊或者視訊格式,需要做的就是兩步
1、在kValidExtensions[] 陣列中新增字尾名。如.dat
2、frameworks/base/media/java/android/media/MediaFile.java中新增public static final int FILE_TYPE_ABC = 數字;
addFileType(“DAT”, FILE_TYPE_DAT, “video/dat”);

android-4.4可以在相應的路徑中mm 生成的frameworks.jar
android-5.1不能mm,必須make systemimage 重新燒錄。