本地媒体扫描:
在应用启动时,启动自己的一个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工具进行获取,下载方式可以自行去百度;

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