android Media原理学习

思考Android安装的app如何快速辨别磁盘上的文件哪些是多媒体文件,并且存放在哪个位置?

通常情况下,我们是使用Android系统自带的音乐或者视频播放器,它里面就包含了磁盘上所有的音乐视频文件,它是怎么快速获取到这些文件的呢?不可能每次打开都去扫描一次系统存储的文件,这样是很慢,原理上分析,应该在打开之前系统就已经为它扫描好并且把这些媒体文件的位置存储好了,音乐播放器只需要去存储的地方去取就好了;事实上Android也是这样去做的,如何去做的呢?本文将从源码的角度去分析运作流程,以及Android sdk自带的媒体API的使用方法;

Android 接口类关键字

MediaPlayer MediaStore MediaScannerService MediaScannerReciver MediaProvider

Android系统扫描文件的时机

什么情况下,系统决定要去扫描文件,统计媒体数据?Android系统提供了三种方式:

String flag_a = Intent.ACTION_MEDIA_MOUNTED;    //插拔sdk后触发
String flag_b = Intent.ACTION_BOOT_COMPLETED;   //系统完成后触发
String flag_c = Intent.ACTION_MEDIA_SCANNER_SCAN_FILE;  //磁盘文件发生变化时触发
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));

由上代码可以发现,触发相应的事件后是通过一个广播把消息发送出去的,那么肯定存在对应的广播接收,接收代码就在MediaScannerReciver里面,收到消息后它会启动一个Service,由Service完成文件扫描,MediaScannerReciver源码:

public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        final Uri uri = intent.getData();
        //系统触发事件,内部存储和外部存储均要扫描
        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);
        //开启扫描服务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);
        //开启扫描服务Service
        context.startService(
                new Intent(context, MediaScannerService.class).putExtras(args));
    }    

上面代码,在ACTION_BOOT_COMPLETED方式内,INTERNAL_VOLUME指的是$(ANDROID_ROOT)/media位置,EXTERNAL_VOLUME是外部sdk路径;从上面代码可以看到开启了一个MediaScannerService的服务,那这个服务是如何扫描的呢?看源码:

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的时候会发送一个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);
            }

上面代码主要是拿到你需要扫描的位置参数,然后在调用scan()和scanFiles()这两个方法去扫描,而且真正执行的扫描操作就在这里面

 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方法内部,首先在扫描过程中不允许休眠,然后把扫描路径参数交给MediaScanner去扫描,完成后在广播消息扫描完成,MediaScanner已经在JNI层了,就不继续分析了;在最后扫描完成后,系统受到结果会把它存储在sqlite数据库中,在由MediaProvider提供查询接口,我们开发的时候就可使用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获取消息部分可以看到,还有一个回调监听,通过那个监听可以把查询的结果返回回去

总结

将以上过程规划成一个流程图如下:


这里写图片描述

android媒体开发

MediaPlayer和VideoView

Android上的视频播放可以用MediaPlayer、VideView和WebView提供的JS播放器,他们各有异同,具体参考下面;
视频或者音频播放时,一般步骤是:

  • 创建相关播放类
  • 初始化并设置播放源
  • 开始播放
  • 结束释放资源
    将视频流显示到界面上去时,有一个绘制过程,而媒体流数据都是异步来绘制的,需要在子线程完成,如果放到主线程去回出现ANR,从类的结构上看VideoView继承了SurfaceView所以它可以直接设置播放源进行播放,MediaPlayer则不行,需要自己创建一个视图表面SurfaceView并与之绑定,进行播放;以MediaPlayer为例进行视频播放:
    1 创建相关类
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 设置视频/音频播放流,同时可以设置一些监听如错误、启动或者缓冲监听,当出现这些动作后会执行相应的回调函数,如启动监听:

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

上面的监听,当我们mplayer调用启动函数prepareAsync()或者prepare(),mPlayer启动完成处于Prepared状态下就会执行相应的动作,其他的监听用法和这个类似;

Android视频支持格式需要特别说明一下: setDataSource()这个方法你可以设置网上在线视频或者本地文件路径等,具体的方法参考Android sdk API,值得注意的是Android的播放器支持视频:本地MP4和3gp,在线视频支持http协议的和RTSP协议的视频流播放,http点播用的多,而RTSP用于实时直播用的多,还有一种直播协议是RTMP;所以从上可知Android自带的多媒体框架并不是支持多种格式的视频播放和在线播放,如果想要做一套全视频系统的话,可以找一些开源的库进行二次开发,这里稍稍讲下直播系统:

推流端 -- 将本地录制的视频以流的方式推到服务端
可以用ffmpeg自己写一个推流器,博主有一篇博客写了一个简单的推流器可做参考;
AnyRTMP推流框架,支持nginx+RTMP的直播协议

服务端 -- 收集推流上来的视频流并转发到其他接收端,服务端可以做格式的转换、数据压缩转码等
可以用nginx做,服务端搭建并配置RTMP服务可参考这篇文档

客户端接收直播流
典型的bilibili的ijkplayer、还有七牛的直播框架等
楼主前段时间仿照bilibili的直播,做了一个简易的直播demo,有需要的可以参考我的github

3 开始播放,一般来说当你的MediaPlayer处于Prepared状态,你就可以start开始播放了,开始播放后你可以控制视频的播放、暂停、定位等功能;如果只是简单的操作的话,你就使用Android自带的媒体控制器就可以了,如果嫌Android原生控制界面太丑了,可以自定义控制界面,自己操作,楼主自己做了一个,可以参考我的github
效果图:

这里写图片描述

4 结束释放资源

public void release ();

MediaPlayer生命周期

了解它的生命周期才能更好的使用MediaPlayer,以及在遇到问题更好的解决问题,话不多说,看图:


这里写图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容