美文网首页
Android下的视频播放器MediaPlayer相关

Android下的视频播放器MediaPlayer相关

作者: 小狗砸他爹 | 来源:发表于2022-07-19 14:32 被阅读0次

本地媒体扫描:

在应用启动时,启动自己的一个service,在service中注册对应的广播用来监听外部存储或媒体库的变化,为了防止出现U盘等存储在应用启动之前已经挂载过了,可以在service启动的时候,主动查询一下当前存储的挂载状态:
主动查询存储相关状态:

StorageManager storageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE);
        List<VolumeInfo> volumeInfos = storageManager.getVolumes();
        for (VolumeInfo volumeInfo : volumeInfos) {
            if (volumeInfo == null) {
                continue;
            }
            //  这里通过volumeinfo来获取相关存储的状态和信息;以下省略xxxx
          }

如果部分代码涉及到弃用或隐藏API,可以尝试以下方法:

StorageManager storageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE);
List<StorageVolume> volumeList = storageManager.getStorageVolumes();
        if (null == volumeList || volumeList.isEmpty()) {
            return MusicLocalContants.UNMOUNT;
        }
        for (StorageVolume volume : volumeList) {
            // 是否是可移除的外部存储设备
            if (null != volume && volume.isRemovable()) {
                // 设备挂载的状态,如:mounted、unmounted
                String status = volume.getState();
                String usbPath = null;
                try {
                    //使用反射调用被隐藏的方法
                    Method pathMethod = StorageVolume.class.getDeclaredMethod("getPath", (Class[]) null);
                    pathMethod.setAccessible(true);
                    usbPath = (String) pathMethod.invoke(volume, (Object[]) null);
                } catch (Exception e) {
                   
                }
                if (TextUtils.equals(getUsbPath(usbIndex), usbPath)) {
                    if (TextUtils.equals("mounted", status)) {
                       //已挂载;
                    }
                    if (TextUtils.equals("unmounted", status)) {
                       //未挂载;
                    }
                }
            }
        }

外部存储状态监听的广播接收器:

private final BroadcastReceiver usbStatusReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.d(TAG, "action:" + action);
            String path = intent.getData().toString();
            Log.d(TAG, "path:" + path);
            switch (action) {
                case Intent.ACTION_MEDIA_SCANNER_STARTED:
                   //媒体开始扫描
                    break;
                case Intent.ACTION_MEDIA_SCANNER_FINISHED:
                    //媒体扫描结束
                    break;
                case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE:
                    break;
                case Intent.ACTION_MEDIA_MOUNTED:
                    //接收到U盘设备插入广播
                    break;
                case Intent.ACTION_MEDIA_EJECT:
                    //接收到U盘设备拔出广播
                    break;
                default:
                    break;
            }
        }
    };

当接收到SCANNER_FINISHED广播之后,表示媒体库扫描结束,可以直接通过ContentProvider去读取MediaStore.Video.Media.EXTERNAL_CONTENT_URI库表中的对应数据,按照自己的需要进行筛选;
这里有个细节要注意一下,部分视频文件header中的名称信息和视频文件的名称可能存在不一致,如果需要显示原始信息,就读取MediaStore.Video.Media.TITLE字段,如果需要显示视频文件名称,可以从MediaStore.Video.Media.DATA字段中进行截取或采用其他方式;

因不同平台定制规则不同,部分平台会在MOUNTED之后开始扫描,并在SCAN_FINISH之后才会将数据全部写入对应数据库,这种情况下可以直接在FINISH广播之后获取本地视频文件,部分平台是在MOUNTED之后就开始扫描,并实时将对应数据插入VIDEO表中,这种情况下可以通过监听来实时更新数据:

mContext.getContentResolver().registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,true, mContentObserver);

MediaStore.Video.Media.EXTERNAL_CONTENT_URI对应的数据库表,由于Android系统在Android R上做了重构,对应的目录为data/data/com.android.providers.media.module应用下,R之前可以在data/data/com.android.providers.media应用目录下进行提取查看;

MediaPlayer相关:

                      MediaPlayer mMediaPlayer = new MediaPlayer();
                        mMediaPlayer.setDisplay(surfaceHolder);
                        mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
                            @Override
                            public boolean onError(MediaPlayer mediaPlayer, int i, int i1) {
                                //接收并处理错误回调
                            }
                        });
                        mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                            @Override
                            public void onPrepared(MediaPlayer mp) {
                                //接收准备完成的回调
                                mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
                                    @Override
                                    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
                                        //视频大小发生变化
                                    }
                                });
                                mMediaPlayer.start();
                                startUpdateProgress();
                                int mDuration = mMediaPlayer.getDuration();
                            }
                        });
                        //接收播放完成的回调
                        mMediaPlayer.setOnCompletionListener();
                        mMediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
                            @Override
                            public void onBufferingUpdate(MediaPlayer mp, int percent) {
                              //获取缓存到进度
                            }
                        });

                        //是否正在加载
                        mMediaPlayer.setOnInfoListener((mp, what, extra)->{
                            Log.d(TAG,"setOnInfoListener what:"+what+"---extra:"+extra);
                            switch (what) {
                                case MediaPlayer.MEDIA_INFO_BUFFERING_START:
                                     //开始缓冲
                                    break;
                                case MediaPlayer.MEDIA_INFO_BUFFERING_END:
                                    //缓冲结束
                                    break;
                                case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
                                    //监听InfoListener为MEDIA_INFO_VIDEO_RENDERING_START时,说明视频渲染第一帧
                                    break;
                                case MediaPlayer.MEDIA_INFO_NOT_SEEKABLE:
                                    Log.d(TAG,"OnInfo MEDIA_INFO_NOT_SEEKABLE: ");
                                    //当前视频不支持快进快退
                                    break;
                                default:
                                    break;
                            }
                            return false;
                        });

                        mMediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
                            @Override
                            public void onSeekComplete(MediaPlayer mp) {
                               //快进快退完成
                            }
                        });
                        mMediaPlayer.setDataSource(videoPath);
                        mMediaPlayer.prepareAsync();

吐槽一句,系统的mediaplayer真的非常的差,不知道出于什么考虑,比方说,连最基本的当前播放的进度都没有给回调方法,还需要开发者自己去启动线程去进行定时读取;
Mediaplayer的创建有多种方法,此处直接使用new的方式去创建,创建完之后,将surfaceholder与mediaplayer进行绑定,用于显示图像;
mediaplayer的创建需要在surfaceview创建好之后才能开始,可以通过addCallback()方法添加监听:

SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() {
                @Override
                public void surfaceCreated(SurfaceHolder holder) {
                    
                }

                @Override
                public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
                   
                }

                @Override
                public void surfaceDestroyed(SurfaceHolder holder) {
                    
                }
            };
SurfaceView surfaceView = findViewById(R.ID.XXX);
SurfaceHolder  mSurfaceHolder = surfaceView .getHolder();
mSurfaceHolder.addCallback(surfaceHolderCallback);

prepare的方式选择,个人推荐使用prepareAsync(),而不是直接用prepare()方法;
surfaceDestroyed方法的回调,可以参考官方注释,也可以参照这篇译文(官方注释翻译)

遇到的问题

1、通过MediaPlayer.getDuration()方法获取视频总时长,会收到-38的error回调,因为在mediaplayer未prepare好之前,获取总时长会抛出错误回调,需要在onPrepared()回调之后才能去获取,异常日志如下;

E/MediaPlayer: Attempt to call getDuration without a valid mediaplayer
E/MediaPlayer: error (-38, 0)

2、接着上面说,在收到error回调后,通常的做法是暂停当前播放,并释放掉播放器和媒体资源,但是由于mediaplayer的状态机会在我们未预期的情况下回调error,如上面的获取时长的情况,所以error回调中,仅在自己已经处理并且需要处理某个错误码时,才去将相关资源进行释放,而不是收到error就进行释放,这样会造成很多其他无法正常播放的问题出现;
3、onInfo的回调中MediaPlayer.MEDIA_INFO_BUFFERING_START和MediaPlayer.MEDIA_INFO_BUFFERING_END的两个状态值回调,视频播放和音频播放存在一定的差异,自己的调试设备上,在线音频缓冲中不会收到这两个回调,视频是可以正常收到的,仅供参考;
4、mediaplayer的释放

if (mMediaPlayer.isPlaying()) {
    mMediaPlayer.stop();
 }
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;

起初经常遇到在释放时出现ANR的问题,后来将释放相关的操作改为了异步,但是会出现其他mediaplayer操作出现ANR问题,如播放、暂停等,所以关于mediaplayer的操作,最好是全部能做成异步操作、异步操作、异步操作;

Handler callbackHandler = new Handler(Looper.getMainLooper());
HandlerThread  playHandlerThread = new HandlerThread("playHandlerThread");
playHandlerThread.start();
Handler playHandler = new Handler(playHandlerThread.getLooper());
//此处省略
callbackHandler.post(new Runnable() {
            @Override
            public void run() {
                //执行需要回调到主线程的操作
            }
        });
playHandler.post(new Runnable() {
            @Override
            public void run() {
                //异步执行mediaplayer相关操作
            }
        });

playHandler用来对异步执行mediaplayer的所有操作,callbackHandler用来把上述complete、seekcomplete、prepared、info等接口的回调在主线程上回调给activity或其他方,用来做相关处理;
5、不管是在线视频还是本地视频,执行seekTo(long msec, @SeekMode int mode)方法时,都不是立即就能执行完,在线视频特别明显,需要结合onSeekComplete和BUFFERING_END两个回调同时进行;
6、部分视频刚开始播放时,会出现短暂的黑屏,可以采用播放开始时使用loading弹框或默认封面进行显示,等到info接口的MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START回调后,将遮挡进行移除,并通过sizeChange的回调,根据屏幕宽高比对surfaceview的控件大小进行调整;
7、连续播放两个视频时,可能出现第二个视频未正式开始播放时,会出现上一个视频的画面,建议在第二个视频开始播放前,手动将surfaceview进行隐藏,等到正式开始播放并收到sizeChange回调后,再调整到合适大小;

if(mSurfaceView!=null){
   RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(1, 1);
   params.addRule(RelativeLayout.CENTER_IN_PARENT);
   mSurfaceView.setLayoutParams(params);
}

注意这里不要将宽高参数调整为(0,0),会造成到surfaceholder的surfaceDestroyed和surfaceCreated的回调不了;

视频解码:

以高通某平台为例,可以在/vendor/etc/media_codecs_msmnile.xml文件中查看,只要在这个文件中配置了的视频格式,才支持硬解码,如H264(别名AVC), H265(别名HEVC), MPEG2, VP8, VP9等,值得注意的是,视频硬解码和软解码支持的能力,并不是平台配置越高,支持能力就越好,可能会出现性能好的平台硬解码支持不如老旧破平台的情况;

MediaInfo工具:

视频相关信息和参数,可以使用MediaInfo工具进行获取,下载方式可以自行去百度;


mediainfo截图.png

时间不够,后面再慢慢更新吧,上述内容仅本人个人观点,不喜勿喷,欢迎在评论区提出问题并讨论。

相关文章

网友评论

      本文标题:Android下的视频播放器MediaPlayer相关

      本文链接:https://www.haomeiwen.com/subject/tyiqirtx.html