一、概述
一个简单的视频播放器,满足一般的需求。使用原生的 MediaPlayer 和 TextureView来实现。
功能点:
- 获取视频的首帧进行展示,网络视频的首帧会缓存
- 视频播放,本地视频或者网络视频
- 感知生命周期,页面不可见自动暂停播放,页面关闭,自动释放
- 可以在RecyclerView的item中使用
- 网络视频可配置下载(如果网络视频地址可以下载),下次再播放时播放下载好的视频。
演示图:
二、使用
VideoPlayView videoPlayView = findViewById(R.id.videoPlayView);
getLifecycle().addObserver(videoPlayView);
//设置视频文件路径
videoPlayView.setFileDataSource(filePath);
//设置网络视频地址
//videoPlayView.setNetDataSource(netAddress);
int position = intent.getIntExtra("position", 0);
videoPlayView.setTargetPosition(position);
三、实现代码
主要涉及三个类:
- VideoPlayView 播放器
- VideoRepository 获取视频首帧,缓存视频首帧,判断网络视频是否有缓存等处理
- VideoDownload 网络视频下载
VideoPlayView
VideoPlayView布局
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frameLayout"
android:background="@color/black"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:id="@+id/textureView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
<ImageView
android:id="@+id/previewIv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null" />
<ProgressBar
android:id="@+id/loadProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center" />
<FrameLayout
android:id="@+id/mediaControllerView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/ivPlay"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center"
android:contentDescription="@null"
android:src="@drawable/play" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="bottom"
android:orientation="horizontal">
<TextView
android:id="@+id/tvTime"
android:layout_width="60dp"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="@color/white"
android:text="00:00"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvDuration"
android:layout_width="60dp"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="@color/white"
tools:text="2:40:10" />
<ImageView
android:id="@+id/ivScreen"
android:layout_width="30dp"
android:layout_height="30dp"
android:contentDescription="@null"
android:padding="5dp"
android:src="@drawable/fullscreen" />
</LinearLayout>
</FrameLayout>
</FrameLayout>
VideoPlayView 代码
public class VideoPlayView extends FrameLayout implements LifecycleObserver,
View.OnClickListener, SeekBar.OnSeekBarChangeListener, TextureView.SurfaceTextureListener,
MediaPlayer.OnInfoListener, MediaPlayer.OnErrorListener,
MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener,
MediaPlayer.OnSeekCompleteListener, MediaPlayer.OnBufferingUpdateListener,
MediaPlayer.OnVideoSizeChangedListener, VideoRepository.VideoFrameCallback {
private final int DURATION_REFRESH_PROGRESS = 1000;//播放进度更新间隔
private final int DURATION_CLOSE_CONTROLLER = 6000;//控制视图显示时长
private final int CLOSE_CONTROLLER = 122;//关闭控制视图消息
private final int REFRESH_PROGRESS = 133;//刷新播放进度
@Nullable
private MediaPlayer mediaPlayer;
private final VideoRepository videoRepository;
public final TextureView textureView;
public final ImageView ivPreview;
public final ImageView ivPlay;
public final AppCompatSeekBar seekBar;
public final FrameLayout mediaControllerView;
public final ProgressBar loadProgressBar;
public final TextView currentTimeTv;
public final TextView durationTimeTv;
public final ImageView ivScreen;
private int mWidth;
private int mHeight;
private int screenOrientation;
private boolean isMediaAutoPausing = false;//是否是自动暂停的(页面在后台时自动暂停,回到前台时自动播放),手动暂停的不算
private boolean isPause = false;//页面是否pause
private int duration;//视频总长度
private int pausePosition;//暂停时的播放进度
private int targetPosition;//目标播放进度,从这个进度开始播放
//目标播放比例,还没prepare之前,不知道视频的总长度。用户拖动了进度条,记住这个比例,等prepare之后根据比例计算出进度
private float targetRatio;
private boolean hadSetDataSource = false;//是否设置了播放的资源
private boolean hadPrepare = false;//是否prepare成功,只有调用过才能正常播放
private String videoSource;//视频源,本地文件路径或者网络地址
//是否是网络视频源
private boolean isNetSource = false;
//是否下载网络视频源
private boolean needDownloadNetSource = false;
public VideoPlayView(@NonNull Context context) {
this(context, null);
}
public VideoPlayView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VideoPlayView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.media_play_layout, this);
textureView = findViewById(R.id.textureView);
textureView.setSurfaceTextureListener(this);
ivPreview = findViewById(R.id.previewIv);
ivPlay = findViewById(R.id.ivPlay);
seekBar = findViewById(R.id.seekBar);
loadProgressBar = findViewById(R.id.loadProgressBar);
currentTimeTv = findViewById(R.id.tvTime);
durationTimeTv = findViewById(R.id.tvDuration);
mediaControllerView = findViewById(R.id.mediaControllerView);
ivScreen = findViewById(R.id.ivScreen);
findViewById(R.id.frameLayout).setOnClickListener(this);
seekBar.setOnSeekBarChangeListener(this);
ivPlay.setOnClickListener(this);
ivScreen.setOnClickListener(this);
videoRepository = new VideoRepository();
initMediaPlayer();
screenOrientation = getResources().getConfiguration().orientation;
}
private void initMediaPlayer() {
mediaPlayer = new MediaPlayer();
mediaPlayer.setScreenOnWhilePlaying(true);
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnInfoListener(this);
mediaPlayer.setOnErrorListener(this);
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.setOnCompletionListener(this);
mediaPlayer.setOnSeekCompleteListener(this);
mediaPlayer.setOnBufferingUpdateListener(this);
mediaPlayer.setOnVideoSizeChangedListener(this);
}
/**
* 给MediaPlayer设置播放源
*
* @param videoSource 视频源
*/
private void realSetDataSource(String videoSource) {
this.videoSource = videoSource;
duration = 0;
durationTimeTv.setText(null);
hadPrepare = false;
Uri mediaUri;
if (isNetSource) {
mediaUri = videoRepository.getMediaUri(getContext(), videoSource);
} else {
mediaUri = videoRepository.getLocalMediaUri(videoSource);
}
if (mediaUri != null && mediaPlayer != null) {
mediaPlayer.reset();
try {
mediaPlayer.setDataSource(getContext(), mediaUri);
hadSetDataSource = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.frameLayout) {
if (mediaControllerView.getVisibility() == VISIBLE) {
hideController();
} else {
showController();
if (mediaPlayer != null && hadPrepare) {
refreshSeekBarProgress();
}
}
return;
}
if (v.getId() == R.id.ivPlay) {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {//正在播放,暂停
mediaPlayer.pause();
pausePosition = mediaPlayer.getCurrentPosition();
ivPlay.setImageResource(R.drawable.play);
} else {
if (hadPrepare) {//已经prepare过,继续播放
seekTo(pausePosition);
mediaPlayer.start();
ivPlay.setImageResource(R.drawable.pause);
refreshSeekBarProgress();
} else {//如果已经prepare,再次调用prepare会报异常
prepareAndPlay();
}
}
}
resetCloseControllerTime();
return;
}
if (v.getId() == R.id.ivScreen) {//全屏播放
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) mediaPlayer.pause();
ivPlay.setImageResource(R.drawable.play);
int currentPosition = getCurrentPosition();
Intent intent = new Intent(getContext(), VideoPlayActivity.class);
if (isNetSource) {
intent.putExtra(VideoPlayActivity.NET_ADDRESS, videoSource);
} else {
intent.putExtra(VideoPlayActivity.FILE_PATH, videoSource);
}
intent.putExtra(VideoPlayActivity.POSITION, currentPosition);
getContext().startActivity(intent);
}
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
//开始拖动进度条
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
cancelCloseController();
cancelRefreshSeekBarProgress();
}
//结束拖动进度条
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
final int progress = seekBar.getProgress();
if (mediaPlayer != null) {
if (!hadPrepare && seekBar.getMax() == 100) {
targetRatio = progress / 100f;
} else if (hadSetDataSource && mediaPlayer.isPlaying()) {
mediaPlayer.seekTo(progress);
refreshSeekBarProgress();
} else if (hadPrepare && duration != 0) {
pausePosition = progress;
}
}
resetCloseControllerTime();
}
/**
* 设置网络视频源
* @param netAddress 网络视频地址
*/
public void setNetDataSource(String netAddress) {
isNetSource = true;
realSetDataSource(netAddress);
//获取视频第一帧,显示视频预览图
videoRepository.getVideoFirstFrame(getContext().getApplicationContext(), netAddress, this);
}
/**
* 设置文件视频源
*
* @param filePath 文件地址
*/
public void setFileDataSource(String filePath) {
isNetSource = false;
realSetDataSource(filePath);
//获取视频第一帧,显示视频预览图
videoRepository.getFileVideoFirstFrame(filePath, this);
}
public void pauseVideo() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
pausePosition = mediaPlayer.getCurrentPosition();
ivPlay.setImageResource(R.drawable.play);
}
}
public void setTargetPosition(int targetPosition) {
this.targetPosition = targetPosition;
}
public void setNeedDownloadNetSource(boolean needDownloadNetSource) {
this.needDownloadNetSource = needDownloadNetSource;
}
public int getCurrentPosition() {
if (mediaPlayer != null) {
return mediaPlayer.getCurrentPosition();
}
return 0;
}
public void prepareAndPlay() {
if (mediaPlayer != null && hadSetDataSource && !hadPrepare) {
ivPlay.setVisibility(GONE);
loadProgressBar.setVisibility(View.VISIBLE);
try {
mediaPlayer.prepareAsync();//调用prepare之后,视频会开始缓冲
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(getContext(), "播放出错", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//onSurfaceTextureDestroyed执行过,重新初始化MediaPlayer,不然无法播放
//放在RecyclerView中时,如果列表刷新,上下滑动,onSurfaceTextureDestroyed 会被执行,可能执行多次
if (mediaPlayer == null) {
initMediaPlayer();
realSetDataSource(videoSource);
ivPlay.setVisibility(VISIBLE);
ivPlay.setImageResource(R.drawable.play);
ivPreview.setVisibility(VISIBLE);
}
mediaPlayer.setSurface(new Surface(surface));
showController();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
hideController();
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {
targetPosition = mediaPlayer.getCurrentPosition();
}
mediaPlayer.release();
mediaPlayer = null;
}
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
//视频prepare成功,可以开始播放
@Override
public void onPrepared(MediaPlayer mp) {
if (mediaPlayer != null) {
hadPrepare = true;
loadProgressBar.setVisibility(View.GONE);
ivPreview.setVisibility(GONE);
duration = mp.getDuration();
durationTimeTv.setText(getShowTime(duration));
seekBar.setMax(duration);
if (targetRatio != 0) {
targetPosition = (int) (duration * targetRatio);
}
if (targetPosition != 0 && targetPosition <= duration) {
mediaPlayer.seekTo(targetPosition);
seekBar.setProgress(targetPosition);
}
if (!isPause) {
ivPlay.setImageResource(R.drawable.pause);
mediaPlayer.start();
} else {
isMediaAutoPausing = true;
}
}
}
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {//视频开始缓冲
loadProgressBar.setVisibility(VISIBLE);
return true;
}
if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {//视频结束缓冲
loadProgressBar.setVisibility(GONE);
return true;
}
if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {//播放器刚刚推送了第一个视频帧进行渲染。
if (needDownloadNetSource) {//如果需要下载网络视频
//播放成功时,开始下载,如果视频无法播放,一开始就下载,下载完也无法播放
videoRepository.downloadIfNotCache(getContext(), videoSource);
}
refreshSeekBarProgress();
return true;
}
return false;
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {//播放出错
loadProgressBar.setVisibility(GONE);
ivPlay.setVisibility(VISIBLE);
if (what == MediaPlayer.MEDIA_ERROR_UNKNOWN) {
videoRepository.deleteCache(getContext(), videoSource);//播放失败,删除本地缓存视频,可能本地缓存的视频文件无法播放
Toast.makeText(getContext(), "播放出错", Toast.LENGTH_SHORT).show();
}
//播放出现错误,恢复mediaPlayer状态,用户可能再次播放
if (mediaPlayer != null) {
mediaPlayer.reset();
realSetDataSource(videoSource);
}
return false;
}
@Override
public void onCompletion(MediaPlayer mp) {
ivPlay.setImageResource(R.drawable.play);
targetRatio = 0;
targetPosition = 0;
pausePosition = 0;
}
@Override
public void onSeekComplete(MediaPlayer mp) {
}
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
if (duration != 0) {
int progress = (int) (duration * percent / 100f);
seekBar.setSecondaryProgress(progress);
}
}
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
if (textureView != null) {
updateSurfaceSize(textureView, width, height);
}
}
@Override
public void onVideoFirstFrameSuccess(Bitmap bitmap) {
if (bitmap != null && isAttachedToWindow()) {
ivPreview.setImageBitmap(bitmap);
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void onResume() {
isPause = false;
if (mediaPlayer != null && isMediaAutoPausing) {
seekTo(pausePosition);
mediaPlayer.start();
isMediaAutoPausing = false;
ivPlay.setImageResource(R.drawable.pause);
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void onPause() {
isPause = true;
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
isMediaAutoPausing = true;
}
pauseVideo();
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void onStart() {
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void onStop() {
}
//onDestroy执行时机可能再页面关闭之后几秒才调用
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy() {
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (screenOrientation != newConfig.orientation) {//屏幕方向发生了变化,交换宽高
screenOrientation = newConfig.orientation;
final int w = mWidth;
mWidth = mHeight;
mHeight = w;
if (textureView != null && mediaPlayer != null && hadPrepare) {
updateSurfaceSize(textureView, mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
super.onSizeChanged(w, h, oldW, oldH);
mWidth = w;
mHeight = h;
}
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
//保存当前播放的进度
Parcelable parcelable = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putParcelable("super", parcelable);
bundle.putInt("position", pausePosition);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
//恢复播放进度
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
Parcelable parcelable = bundle.getParcelable("super");
super.onRestoreInstanceState(parcelable);
targetPosition = bundle.getInt("position");
} else {
super.onRestoreInstanceState(state);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
hadSetDataSource = false;
handler.removeCallbacksAndMessages(null);
videoRepository.close();
}
private void seekTo(int position) {
if (mediaPlayer == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mediaPlayer.seekTo(position, MediaPlayer.SEEK_CLOSEST);
} else {
mediaPlayer.seekTo(position);
}
}
/**
* 根据视频宽高,修改TextureView的宽高,来适应视频大小
*
* @param width 视频宽度
* @param height 视频高度
*/
private void updateSurfaceSize(@NonNull View view, int width, int height) {
final int displayW = mWidth;
final int displayH = mHeight;
if (displayW == 0 || displayH == 0) return;
float ratioW = 1f;
float ratioH = 1f;
if (width != displayW) {
ratioW = width * 1f / displayW;
}
if (height != displayH) {
ratioH = height * 1f / displayH;
}
float ratio = Math.max(ratioW, ratioH);
int finalW = (int) (width / ratio);
int finalH = (int) (height / ratio);
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams.width == finalW && layoutParams.height == finalH) {
return;
}
layoutParams.width = finalW;
layoutParams.height = finalH;
view.setLayoutParams(layoutParams);
}
//显示控制视图
private void showController() {
if (mediaPlayer != null) {
mediaControllerView.setVisibility(VISIBLE);
if (hadPrepare) {
ivPlay.setVisibility(VISIBLE);
}
resetCloseControllerTime();
}
}
//隐藏控制视图
private void hideController() {
mediaControllerView.setVisibility(View.INVISIBLE);
handler.removeMessages(CLOSE_CONTROLLER);
}
private void resetCloseControllerTime() {
cancelCloseController();
handler.sendEmptyMessageDelayed(CLOSE_CONTROLLER, DURATION_CLOSE_CONTROLLER);
}
private void cancelCloseController() {
handler.removeMessages(CLOSE_CONTROLLER);
}
//刷新播放进度条和时间
private void refreshSeekBarProgress() {
if (mediaPlayer != null && seekBar != null) {
final int position = mediaPlayer.getCurrentPosition();
seekBar.setProgress(position);
currentTimeTv.setText(getShowTime(position));
if (mediaControllerView.getVisibility() == View.VISIBLE) {
cancelRefreshSeekBarProgress();
handler.sendEmptyMessageDelayed(REFRESH_PROGRESS, DURATION_REFRESH_PROGRESS);
}
}
}
private void cancelRefreshSeekBarProgress() {
handler.removeMessages(REFRESH_PROGRESS);
}
//根据毫米数,返回时分秒
public String getShowTime(int millisecond) {
int hour = 0, minute = 0;
int second = millisecond / 1000;//总共的秒数
if (second >= 3600) {//超过一小时
hour = second / 3600;//多少个小时
}
int temp = second - hour * 3600;
if (second >= 60) {//超过一分钟
minute = temp / 60;//多少个分钟
}
second = temp - minute * 60;//多少秒
StringBuilder sb = new StringBuilder();
if (hour > 0 && hour < 10) {
sb.append("0").append(hour).append(":");
} else if (hour >= 10) {
sb.append(hour).append(":");
}
if (minute < 10) {
sb.append("0").append(minute).append(":");
} else {
sb.append(minute).append(":");
}
if (second < 10) {
sb.append("0").append(second);
} else {
sb.append(second);
}
return sb.toString();
}
private final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case CLOSE_CONTROLLER://关闭控制视图
if (mediaControllerView != null) {
mediaControllerView.setVisibility(GONE);
}
break;
case REFRESH_PROGRESS://刷新进度
if (mediaPlayer != null) {
refreshSeekBarProgress();
}
break;
}
}
};
}
VideoRepository
class VideoRepository {
//图片缓存文件名尾部后缀
private static final String IMAGE_SUFFIX = "_jpg";
// public static final ExecutorService sCachedThreadPool = Executors.newSingleThreadExecutor();
static final ExecutorService sCachedThreadPool = Executors.newCachedThreadPool();
private final int msgFail = 11;
private final int msgSuccess = 10;
@Nullable
private VideoFrameCallback videoFrameCallback = null;
private Handler mUiHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case msgSuccess:
if (videoFrameCallback != null && msg.obj instanceof Bitmap) {
videoFrameCallback.onVideoFirstFrameSuccess((Bitmap) msg.obj);
}
break;
case msgFail:
if (videoFrameCallback != null) {
videoFrameCallback.onVideoFirstFrameError();
}
break;
}
}
};
void close() {
videoFrameCallback = null;
}
/**
* 返回本地视频地址的Uri
*
* @param filePath 视频文件路径
* @return 返回文件Uri
*/
@Nullable
Uri getLocalMediaUri(String filePath) {
if (TextUtils.isEmpty(filePath)) return null;
return Uri.fromFile(new File(filePath));
}
/**
* 获取视频文件的第一帧 Bitmap
*
* @param filePath 视频文件路径
*/
void getFileVideoFirstFrame(String filePath, @Nullable VideoFrameCallback callback) {
videoFrameCallback = callback;
sCachedThreadPool.execute(() -> {
try {
Bitmap bitmap;
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
mediaMetadataRetriever.setDataSource(filePath);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
bitmap = mediaMetadataRetriever.getScaledFrameAtTime(1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, 800, 320);
} else {
bitmap = mediaMetadataRetriever.getFrameAtTime(1);
}
mediaMetadataRetriever.release();//释放
if (bitmap != null) {
Message message = Message.obtain();
message.what = msgSuccess;
message.obj = bitmap;
mUiHandler.sendMessage(message);
} else {
mUiHandler.sendEmptyMessage(msgFail);
}
} catch (Exception e) {
e.printStackTrace();
mUiHandler.sendEmptyMessage(msgFail);
}
});
}
/**
* 返回网络视频地址的Uri
*
* @param context 上下文
* @param netAddress 媒体文件网络地址
* @return 返回媒体Uri 如果本地缓存有,返回本地地址的Uri;没有缓存返回网络地址的Uri,边下边播放
*/
@Nullable
Uri getMediaUri(Context context, String netAddress) {
if (context == null || TextUtils.isEmpty(netAddress)) return null;
String fileName = getVideoFileName(netAddress);
File localCache = getLocalCacheVideo(context, fileName);
if (localCache != null) {//存在缓存
return Uri.fromFile(localCache);
}
//返回网络uri
return Uri.parse(netAddress);
}
/**
* 获取网络视频的第一帧 Bitmap
* 并将获取的第一帧缓存起来,下次直接用缓存
*
* @param context 上下文
* @param netAddress 视频文件网络地址
*/
void getVideoFirstFrame(Context context, String netAddress, @Nullable VideoFrameCallback callback) {
videoFrameCallback = callback;
sCachedThreadPool.execute(() -> {
try {
Bitmap bitmap = null;
String fileName = getVideoFileName(netAddress);
File localCacheImage = getLocalCacheImage(context, fileName);//存在本地缓存图片
if (localCacheImage != null) {//本地缓存图片不为空
bitmap = BitmapFactory.decodeFile(localCacheImage.getAbsolutePath());
if (bitmap == null) localCacheImage.delete();//缓存无效,删除无用缓存
}
if (bitmap == null) {//重新获取视频图片
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
File localCacheVideo = getLocalCacheVideo(context, fileName);
if (localCacheVideo != null) {//存在视频缓存
mediaMetadataRetriever.setDataSource(localCacheVideo.getPath());
} else {//不存在视频缓存,设置网络视频地址
mediaMetadataRetriever.setDataSource(netAddress, new HashMap<>());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
bitmap = mediaMetadataRetriever.getScaledFrameAtTime(1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, 800, 320);
} else {
bitmap = mediaMetadataRetriever.getFrameAtTime(1);
}
mediaMetadataRetriever.release();//不释放的话,会继续消耗流量
saveLocalCacheImage(context, bitmap, fileName);
}
if (bitmap != null) {
Message message = Message.obtain();
message.what = msgSuccess;
message.obj = bitmap;
mUiHandler.sendMessage(message);
} else {
mUiHandler.sendEmptyMessage(msgFail);
}
} catch (Exception e) {
e.printStackTrace();
mUiHandler.sendEmptyMessage(msgFail);
}
});
}
/**
* 如果网络视频没有缓存,执行下载
*/
void downloadIfNotCache(Context context, String netAddress) {
String fileName = getVideoFileName(netAddress);
if (getLocalCacheVideo(context, fileName) == null) {//没有缓存,执行下载
File cacheDirectory = getCacheDirectory(context);
new VideoDownload().download(cacheDirectory, fileName, netAddress);
}
}
/**
* 删除缓存文件,如果有缓存
*/
void deleteCache(Context context, String netAddress) {
sCachedThreadPool.execute(() -> {
String fileName = getVideoFileName(netAddress);
File localCacheVideo = getLocalCacheVideo(context, fileName);//缓存视频
File localCacheImage = getLocalCacheImage(context, fileName);//缓存图片
if (localCacheVideo != null) {
localCacheVideo.delete();
}
if (localCacheImage != null) {
localCacheImage.delete();
}
});
}
/**
* @return 返回本地缓存的视频文件
*/
private @Nullable
File getLocalCacheVideo(Context context, String fileName) {
if (context == null || TextUtils.isEmpty(fileName)) return null;
File directoryFile = getCacheDirectory(context);
if (directoryFile.exists()) {
File file = new File(directoryFile, fileName);
if (file.exists()) {//文件存在
return file;
}
}
return null;
}
/**
* @return 返回本地缓存的图片文件
*/
private @Nullable
File getLocalCacheImage(Context context, String videoFileName) {
if (context == null || TextUtils.isEmpty(videoFileName)) return null;
File directoryFile = getCacheDirectory(context);
if (directoryFile.exists()) {
File file = new File(directoryFile, videoFileName + IMAGE_SUFFIX);
if (file.exists()) {//文件存在
return file;
}
}
return null;
}
/**
* 将bitmap缓存到本地文件
*/
private void saveLocalCacheImage(Context context, Bitmap bitmap, String videoFileName) {
if (bitmap == null) return;
FileOutputStream fileOutputStream = null;
try {
String name = videoFileName + IMAGE_SUFFIX;
File directory = getCacheDirectory(context);
boolean mkdirSuccess = true;
if (!directory.exists()) {
mkdirSuccess = directory.mkdirs();
}
if (mkdirSuccess) {
File file = new File(directory, name);
boolean deleteSuccess = true;
if (file.exists()) {
deleteSuccess = file.delete();
}
if (deleteSuccess) {
boolean createSuccess = file.createNewFile();
if (createSuccess) {
fileOutputStream = new FileOutputStream(file);
}
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* @return 返回缓存的目录文件夹
*/
private File getCacheDirectory(Context context) {
return new File(context.getExternalCacheDir(), "video");
}
/**
* @param netAddress 网络地址
* @return 返回网络地址对应的视频文件名
*/
private String getVideoFileName(String netAddress) {
MessageDigest messageDigest = null;
try {
messageDigest = MessageDigest.getInstance("MD5");
messageDigest.reset();
messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
e.printStackTrace();
}
if (messageDigest == null) return str;
byte[] byteArray = messageDigest.digest();
StringBuilder sb = new StringBuilder();
for (byte b : byteArray) {
if (Integer.toHexString(0xFF & b).length() == 1) {
sb.append("0").append(Integer.toHexString(0xFF & b));
} else {
sb.append(Integer.toHexString(0xFF & b));
}
}
return sb.toString().toUpperCase();
}
/**
* 获取视频帧的回调
*/
public interface VideoFrameCallback {
/**
* 获取视频首帧图成功
*
* @param bitmap 首帧图
*/
void onVideoFirstFrameSuccess(@Nullable Bitmap bitmap);
/**
* 获取首帧图失败
*/
default void onVideoFirstFrameError() {
}
}
}
VideoDownload
class VideoDownload {
//最大缓存数
private static final int MAX_CACHE_SIZE = 40;
//正在下载的视频列表
private static final ArrayList<String> downloadUrl = new ArrayList<>();
/**
* @param directoryFile 下载文件目录
* @param fileName 下载文件名
* @param urlAddress 下载地址
*/
public void download(File directoryFile, String fileName, String urlAddress) {
if (TextUtils.isEmpty(urlAddress) || directoryFile == null) return;
if (downloadUrl.contains(urlAddress)) {//正在下载,返回
return;
}
if (!directoryFile.exists()) {
try {
if (!directoryFile.mkdirs()) {
return;//创建目录失败,返回
}
} catch (Exception e) {
e.printStackTrace();
return;
}
}
File file = new File(directoryFile, fileName);
if (file.exists()) {//文件已经存在
return;
}
MediaRepository.sCachedThreadPool.execute(() -> {
//判断是否超过最大缓存数,如果超过删除旧的缓存
deleteOldCache(directoryFile);
//执行下载
realDownload(file, urlAddress);
});
}
/**
* 删除旧的缓存
*/
private void deleteOldCache(File directoryFile) {
if (directoryFile == null || !directoryFile.exists()) return;
try {
File[] listFiles = directoryFile.listFiles();
if (listFiles != null && listFiles.length >= MAX_CACHE_SIZE) {//超过最大缓存数,删除时间最早的那一个
File oldestFile = null;
long oldestModified = System.currentTimeMillis();
for (File file : listFiles) {
if (file != null && file.isFile()) {
long lastModified = file.lastModified();
if (lastModified < oldestModified) {
oldestModified = lastModified;
oldestFile = file;
}
}
}
if (oldestFile != null) {
oldestFile.delete(); }
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 执行下载
*
* @param file 下载文件
* @param urlAddress 下载地址
*/
private void realDownload(File file, String urlAddress) {
downloadUrl.add(urlAddress);
File tempFile = null;
InputStream inputStream = null;
FileOutputStream fileOutputStream = null;
try {
//临时文件,下载完成后重命名为正式文件。如果一开始就命名为正式文件,当下载中断(APP闪退或者被杀死),就会导致正式文件是不完整的。
tempFile = new File(file.getParent(), "t_" + file.getName());
if (tempFile.exists()) {//如果存在,删除(可能上次没下载完成,删除重新下载)。
tempFile.delete()
}
try {
if (!tempFile.exists() && !tempFile.createNewFile()) {
return;//创建文件失败
}
} catch (IOException e) {
e.printStackTrace();
return;
}
URL url = new URL(urlAddress);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
connection.setConnectTimeout(0);
connection.setReadTimeout(0);
connection.setRequestMethod("GET");
inputStream = connection.getInputStream();
fileOutputStream = new FileOutputStream(tempFile);
byte[] bytes = new byte[8192];
int len;
while ((len = inputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
fileOutputStream.flush();
tempFile.renameTo(file);
} catch (Exception e) {
e.printStackTrace();
if (tempFile != null) {
tempFile.delete();//下载失败,删除文件
}
} finally {
downloadUrl.remove(urlAddress);
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}