android Media原理學(xué)習(xí)

思考Android安裝的app如何快速辨別磁盤上的文件哪些是多媒體文件咽袜,并且存放在哪個位置丸卷?

通常情況下,我們是使用Android系統(tǒng)自帶的音樂或者視頻播放器询刹,它里面就包含了磁盤上所有的音樂視頻文件谜嫉,它是怎么快速獲取到這些文件的呢?不可能每次打開都去掃描一次系統(tǒng)存儲的文件凹联,這樣是很慢沐兰,原理上分析,應(yīng)該在打開之前系統(tǒng)就已經(jīng)為它掃描好并且把這些媒體文件的位置存儲好了蔽挠,音樂播放器只需要去存儲的地方去取就好了住闯;事實上Android也是這樣去做的,如何去做的呢澳淑?本文將從源碼的角度去分析運作流程比原,以及Android sdk自帶的媒體API的使用方法;

Android 接口類關(guān)鍵字

MediaPlayer MediaStore MediaScannerService MediaScannerReciver MediaProvider

Android系統(tǒng)掃描文件的時機

什么情況下偶惠,系統(tǒng)決定要去掃描文件春寿,統(tǒng)計媒體數(shù)據(jù)?Android系統(tǒng)提供了三種方式:

String flag_a = Intent.ACTION_MEDIA_MOUNTED;    //插拔sdk后觸發(fā)
String flag_b = Intent.ACTION_BOOT_COMPLETED;   //系統(tǒng)完成后觸發(fā)
String flag_c = Intent.ACTION_MEDIA_SCANNER_SCAN_FILE;  //磁盤文件發(fā)生變化時觸發(fā)
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));

由上代碼可以發(fā)現(xiàn)忽孽,觸發(fā)相應(yīng)的事件后是通過一個廣播把消息發(fā)送出去的绑改,那么肯定存在對應(yīng)的廣播接收,接收代碼就在MediaScannerReciver里面兄一,收到消息后它會啟動一個Service厘线,由Service完成文件掃描,MediaScannerReciver源碼:

public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        final Uri uri = intent.getData();
        //系統(tǒng)觸發(fā)事件出革,內(nèi)部存儲和外部存儲均要掃描
        if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
            // Scan both internal and external storage
            scan(context, MediaProvider.INTERNAL_VOLUME);
            scan(context, MediaProvider.EXTERNAL_VOLUME);
        } else { 
            if (uri.getScheme().equals("file")) {
                // handle intents related to external storage
                String path = uri.getPath();
                String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
                String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();
                try {
                    path = new File(path).getCanonicalPath();
                } catch (IOException e) {
                    Log.e(TAG, "couldn't canonicalize " + path);
                    return;
                }
                if (path.startsWith(legacyPath)) {
                    path = externalStoragePath + path.substring(legacyPath.length());
                }
                Log.d(TAG, "action: " + action + " path: " + path);
                if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                    // scan whenever any volume is mounted
                    scan(context, MediaProvider.EXTERNAL_VOLUME);  //插拔卡掃描sdk路徑
                } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                        path != null && path.startsWith(externalStoragePath + "/")) {
                    scanFile(context, path);   //掃描自定義路徑
                }
            }
        }
    }
   
    private void scan(Context context, String volume) {
        Bundle args = new Bundle();
        args.putString("volume", volume);
        //開啟掃描服務(wù)Service
        context.startService(
                new Intent(context, MediaScannerService.class).putExtras(args));
             
    }    
    private void scanFile(Context context, String path) {
        Bundle args = new Bundle();
        args.putString("filepath", path);
        //開啟掃描服務(wù)Service
        context.startService(
                new Intent(context, MediaScannerService.class).putExtras(args));
    }    

上面代碼造壮,在ACTION_BOOT_COMPLETED方式內(nèi),INTERNAL_VOLUME指的是$(ANDROID_ROOT)/media位置,EXTERNAL_VOLUME是外部sdk路徑耳璧;從上面代碼可以看到開啟了一個MediaScannerService的服務(wù)成箫,那這個服務(wù)是如何掃描的呢?看源碼:

public int onStartCommand(Intent intent, int flags, int startId) {
        ............
        if (intent == null) {
            Log.e(TAG, "Intent is null in onStartCommand: ",
                new NullPointerException());
            return Service.START_NOT_STICKY;
        }
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent.getExtras();
        mServiceHandler.sendMessage(msg);
        // Try again later if we are killed before we can finish scanning.
        return Service.START_REDELIVER_INTENT;
    }

從上面代碼可以看出旨枯,startCommand的時候會發(fā)送一個handler消息蹬昌,在收到消息后,如何處理的呢攀隔?

public void handleMessage(Message msg) {
            Bundle arguments = (Bundle) msg.obj;
            if (arguments == null) {
                Log.e(TAG, "null intent, b/20953950");
                return;
            }
            String filePath = arguments.getString("filepath");
            
            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",
                                Environment.getOemDirectory() + "/media",
                        };
                    }
                    else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
                        // scan external storage volumes
                        directories = mExternalStoragePaths;
                    }
                    if (directories != null) {
                        if (false) Log.d(TAG, "start scanning volume " + volume + ": "
                                + Arrays.toString(directories));
                        scan(directories, volume);
                        if (false) Log.d(TAG, "done scanning volume " + volume);
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Exception in handleMessage", e);
            }

上面代碼主要是拿到你需要掃描的位置參數(shù)皂贩,然后在調(diào)用scan()和scanFiles()這兩個方法去掃描,而且真正執(zhí)行的掃描操作就在這里面

 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);
                }
                try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                    scanner.scanDirectories(directories);
                }
            } 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();
        }
    }

scan方法內(nèi)部昆汹,首先在掃描過程中不允許休眠明刷,然后把掃描路徑參數(shù)交給MediaScanner去掃描,完成后在廣播消息掃描完成满粗,MediaScanner已經(jīng)在JNI層了辈末,就不繼續(xù)分析了;在最后掃描完成后映皆,系統(tǒng)受到結(jié)果會把它存儲在sqlite數(shù)據(jù)庫中本冲,在由MediaProvider提供查詢接口,我們開發(fā)的時候就可使用ContentProvider去提取我們的東西了

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

scannFile方法和Scan類似劫扒,只是最后會返回一個uri,你查看上面的handler獲取消息部分可以看到狸膏,還有一個回調(diào)監(jiān)聽沟饥,通過那個監(jiān)聽可以把查詢的結(jié)果返回回去

總結(jié)

將以上過程規(guī)劃成一個流程圖如下:


這里寫圖片描述

android媒體開發(fā)

MediaPlayer和VideoView

Android上的視頻播放可以用MediaPlayer、VideView和WebView提供的JS播放器湾戳,他們各有異同贤旷,具體參考下面;
視頻或者音頻播放時砾脑,一般步驟是:

  • 創(chuàng)建相關(guān)播放類
  • 初始化并設(shè)置播放源
  • 開始播放
  • 結(jié)束釋放資源
    將視頻流顯示到界面上去時幼驶,有一個繪制過程,而媒體流數(shù)據(jù)都是異步來繪制的韧衣,需要在子線程完成盅藻,如果放到主線程去回出現(xiàn)ANR,從類的結(jié)構(gòu)上看VideoView繼承了SurfaceView所以它可以直接設(shè)置播放源進行播放畅铭,MediaPlayer則不行氏淑,需要自己創(chuàng)建一個視圖表面SurfaceView并與之綁定,進行播放硕噩;以MediaPlayer為例進行視頻播放:
    1 創(chuàng)建相關(guān)類
public static MediaPlayer create (Context context, Uri uri, SurfaceHolder holder);//綁定holder
public static MediaPlayer create (Context context, int resid);//resid是你要播放的流R.raw
public static MediaPlayer create (Context context, Uri uri)

2 設(shè)置視頻/音頻播放流假残,同時可以設(shè)置一些監(jiān)聽如錯誤、啟動或者緩沖監(jiān)聽炉擅,當(dāng)出現(xiàn)這些動作后會執(zhí)行相應(yīng)的回調(diào)函數(shù)辉懒,如啟動監(jiān)聽:

OnPreparedListener mPreParedListener = new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mediaPlayer) {
                
            };
mPlayer.setOnPreparedListener(mPreParedListener);         

上面的監(jiān)聽阳惹,當(dāng)我們mplayer調(diào)用啟動函數(shù)prepareAsync()或者prepare(),mPlayer啟動完成處于Prepared狀態(tài)下就會執(zhí)行相應(yīng)的動作眶俩,其他的監(jiān)聽用法和這個類似莹汤;

Android視頻支持格式需要特別說明一下: setDataSource()這個方法你可以設(shè)置網(wǎng)上在線視頻或者本地文件路徑等,具體的方法參考Android sdk API仿便,值得注意的是Android的播放器支持視頻:本地MP4和3gp体啰,在線視頻支持http協(xié)議的和RTSP協(xié)議的視頻流播放,http點播用的多嗽仪,而RTSP用于實時直播用的多荒勇,還有一種直播協(xié)議是RTMP;所以從上可知Android自帶的多媒體框架并不是支持多種格式的視頻播放和在線播放闻坚,如果想要做一套全視頻系統(tǒng)的話沽翔,可以找一些開源的庫進行二次開發(fā),這里稍稍講下直播系統(tǒng):

推流端 -- 將本地錄制的視頻以流的方式推到服務(wù)端
可以用ffmpeg自己寫一個推流器窿凤,博主有一篇博客寫了一個簡單的推流器可做參考仅偎;
AnyRTMP推流框架,支持nginx+RTMP的直播協(xié)議

服務(wù)端 -- 收集推流上來的視頻流并轉(zhuǎn)發(fā)到其他接收端,服務(wù)端可以做格式的轉(zhuǎn)換雳殊、數(shù)據(jù)壓縮轉(zhuǎn)碼等
可以用nginx做橘沥,服務(wù)端搭建并配置RTMP服務(wù)可參考這篇文檔

客戶端接收直播流
典型的bilibili的ijkplayer、還有七牛的直播框架等
樓主前段時間仿照bilibili的直播夯秃,做了一個簡易的直播demo座咆,有需要的可以參考我的github

3 開始播放,一般來說當(dāng)你的MediaPlayer處于Prepared狀態(tài),你就可以start開始播放了仓洼,開始播放后你可以控制視頻的播放介陶、暫停、定位等功能色建;如果只是簡單的操作的話哺呜,你就使用Android自帶的媒體控制器就可以了,如果嫌Android原生控制界面太丑了,可以自定義控制界面箕戳,自己操作某残,樓主自己做了一個,可以參考我的github
效果圖:

這里寫圖片描述

4 結(jié)束釋放資源

public void release ();

MediaPlayer生命周期

了解它的生命周期才能更好的使用MediaPlayer漂羊,以及在遇到問題更好的解決問題驾锰,話不多說,看圖:


這里寫圖片描述
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末走越,一起剝皮案震驚了整個濱河市椭豫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖赏酥,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喳整,死亡現(xiàn)場離奇詭異,居然都是意外死亡裸扶,警方通過查閱死者的電腦和手機框都,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呵晨,“玉大人魏保,你說我怎么就攤上這事∶溃” “怎么了谓罗?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長季二。 經(jīng)常有香客問我檩咱,道長,這世上最難降的妖魔是什么胯舷? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任刻蚯,我火速辦了婚禮,結(jié)果婚禮上桑嘶,老公的妹妹穿的比我還像新娘炊汹。我一直安慰自己,他們只是感情好逃顶,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布兵扬。 她就那樣靜靜地躺著,像睡著了一般口蝠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上津坑,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天妙蔗,我揣著相機與錄音,去河邊找鬼疆瑰。 笑死眉反,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的穆役。 我是一名探鬼主播寸五,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼耿币!你這毒婦竟也來了梳杏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎十性,沒想到半個月后叛溢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡劲适,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年楷掉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霞势。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡烹植,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出愕贡,到底是詐尸還是另有隱情草雕,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布颂鸿,位于F島的核電站促绵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏嘴纺。R本人自食惡果不足惜败晴,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望栽渴。 院中可真熱鬧尖坤,春花似錦、人聲如沸闲擦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墅冷。三九已至纯路,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寞忿,已是汗流浹背驰唬。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留腔彰,地道東北人叫编。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像霹抛,于是被迫代替她去往敵國和親搓逾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容