基于MediaPlayer实现视频播放

article/2025/10/1 22:19:00

一、概述

一个简单的视频播放器,满足一般的需求。使用原生的 MediaPlayer 和 TextureView来实现。

功能点:

  1. 获取视频的首帧进行展示,网络视频的首帧会缓存
  2. 视频播放,本地视频或者网络视频
  3. 感知生命周期,页面不可见自动暂停播放,页面关闭,自动释放
  4. 可以在RecyclerView的item中使用
  5. 网络视频可配置下载(如果网络视频地址可以下载),下次再播放时播放下载好的视频。

演示图:
在这里插入图片描述

二、使用

VideoPlayView videoPlayView = findViewById(R.id.videoPlayView);
getLifecycle().addObserver(videoPlayView);
//设置视频文件路径
videoPlayView.setFileDataSource(filePath);
//设置网络视频地址
//videoPlayView.setNetDataSource(netAddress);
int position = intent.getIntExtra("position", 0);
videoPlayView.setTargetPosition(position);

三、实现代码

主要涉及三个类:

  1. VideoPlayView 播放器
  2. VideoRepository 获取视频首帧,缓存视频首帧,判断网络视频是否有缓存等处理
  3. 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"><TextureViewandroid:id="@+id/textureView"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="center" /><ImageViewandroid:id="@+id/previewIv"android:layout_width="match_parent"android:layout_height="match_parent"android:contentDescription="@null" /><ProgressBarandroid:id="@+id/loadProgressBar"android:layout_width="wrap_content"android:layout_height="wrap_content"android:visibility="gone"android:layout_gravity="center" /><FrameLayoutandroid:id="@+id/mediaControllerView"android:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:id="@+id/ivPlay"android:layout_width="60dp"android:layout_height="60dp"android:layout_gravity="center"android:contentDescription="@null"android:src="@drawable/play" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="30dp"android:layout_gravity="bottom"android:orientation="horizontal"><TextViewandroid: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.AppCompatSeekBarandroid:id="@+id/seekBar"android:layout_width="0dp"android:layout_height="match_parent"android:layout_marginHorizontal="4dp"android:layout_weight="1" /><TextViewandroid:id="@+id/tvDuration"android:layout_width="60dp"android:layout_height="match_parent"android:gravity="center"android:textColor="@color/white"tools:text="2:40:10" /><ImageViewandroid: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;//刷新播放进度@Nullableprivate 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;//页面是否pauseprivate 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();}}}@Overridepublic 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);}}}@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}//开始拖动进度条@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {cancelCloseController();cancelRefreshSeekBarProgress();}//结束拖动进度条@Overridepublic 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();}}}@Overridepublic 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();}@Overridepublic void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}@Overridepublic boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {hideController();if (mediaPlayer != null) {if (mediaPlayer.isPlaying()) {targetPosition = mediaPlayer.getCurrentPosition();}mediaPlayer.release();mediaPlayer = null;}return true;}@Overridepublic void onSurfaceTextureUpdated(SurfaceTexture surface) {}//视频prepare成功,可以开始播放@Overridepublic 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;}}}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic void onCompletion(MediaPlayer mp) {ivPlay.setImageResource(R.drawable.play);targetRatio = 0;targetPosition = 0;pausePosition = 0;}@Overridepublic void onSeekComplete(MediaPlayer mp) {}@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {if (duration != 0) {int progress = (int) (duration * percent / 100f);seekBar.setSecondaryProgress(progress);}}@Overridepublic void onVideoSizeChanged(MediaPlayer mp, int width, int height) {if (textureView != null) {updateSurfaceSize(textureView, width, height);}}@Overridepublic 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() {}@Overrideprotected 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());}}}@Overrideprotected void onSizeChanged(int w, int h, int oldW, int oldH) {super.onSizeChanged(w, h, oldW, oldH);mWidth = w;mHeight = h;}@Nullable@Overrideprotected Parcelable onSaveInstanceState() {//保存当前播放的进度Parcelable parcelable = super.onSaveInstanceState();Bundle bundle = new Bundle();bundle.putParcelable("super", parcelable);bundle.putInt("position", pausePosition);return bundle;}@Overrideprotected 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);}}@Overrideprotected 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()) {@Overridepublic 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;@Nullableprivate VideoFrameCallback videoFrameCallback = null;private Handler mUiHandler = new Handler(Looper.getMainLooper()) {@Overridepublic 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*/@NullableUri 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,边下边播放*/@NullableUri 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);}//返回网络urireturn 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 @NullableFile 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 @NullableFile 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();}}}}
}

http://chatgpt.dhexx.cn/article/MS0KGcUT.shtml

相关文章

玩转 Android MediaPlayer之Media Proxy

本文来自http://blog.csdn.net/hellogv/ &#xff0c;引用必须注明出处&#xff01; 本文是在《Android MediaPlayer与Http Proxy结合之提高篇》基础上&#xff0c;进一步优化代理服务器&#xff0c;支持了Http的302、301重定向&#xff0c;获取Http Request和Http Response的文…

MediaPlayer播放assets文件夹下的音频

一、先放张图&#xff0c;MediaPlayer的生命周期图如下&#xff1a; 二、mediaPlayer方法 setDataSource() //设置要播放的音频文件的位置 prepare() //在开始播放之前调用这个方法完成准备工作 start() //开始或继续播放音频 pause() //暂停播放音频 reset() //将MediaPlayer…

Android openGL+MediaPlayer播放视频

在Android平台折腾了好多次openGL MediaPlayer播放视频的事情&#xff0c;openGL本身是套API规范&#xff0c;其身后的计算机图形学还是比较难缠&#xff0c;但只是播放视频用不了太多东西&#xff0c;没有过多的坐标转换、简单2D纹理展示&#xff0c;实现起来还是比较简单&am…

Android-MediaPlayer播放网络音频

官方文档&#xff1a;https://developer.android.google.cn/reference/android/media/MediaPlayer 运行截图&#xff1a; 主布局文件&#xff1a; <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.a…

深入Android MediaPlayer的使用方法详解

1&#xff09;如何获得MediaPlayer实例&#xff1a; 可以使用直接new的方式&#xff1a; MediaPlayer mp new MediaPlayer(); 也可以使用create的方式&#xff0c;如&#xff1a; MediaPlayer mp MediaPlayer.create(this, R.raw.test);//这时就不用调用setDataSource了 2) 如…

Android MediaPlayer状态机

翻译Android Reference Manual的MediaPlayer的状态机 对播放音频/视频文件和流的控制是通过一个状态机来管理的。下图显示一个MediaPlayer对象被支持的播放控制操作驱动的生命周期和状态。椭圆代表MediaPlayer对象可能驻留的状态。弧线表示驱动MediaPlayer在各个状态之间迁移的…

Android提高第二十一篇之MediaPlayer播放网络视频

本文来自http://blog.csdn.net/hellogv/ &#xff0c;引用必须注明出处&#xff01; 上次讲解了MediaPlayer播放网络音频&#xff0c;介绍了MediaPlayer关于网络音频的缓冲和进度条控制的方法&#xff0c;这次再讲解MediaPlayer播放网络视频。播放网络视频比播放网络音频多需要…

Android开发之MediaPlayer详解

Android开发之MdiaPlayer详解 MediaPlayer类可用于控制音频/视频文件或流的播放&#xff0c;我曾在《Android开发之基于Service的音乐播放器》一文中介绍过它的使用。下面让我们看一下MediaPlayer类的详细介绍。 一、类结构&#xff1a; java.lang.Object ↳ android.med…

Android 9.0 MediaPlayer播放流程分析

1.MediaPlayer初始化流程 EventHandler是后面处理数据回调的handler. 在AudioFlinger.cpp中获取nextUniqueId&#xff1a; audio_unique_id_t AudioFlinger::nextUniqueId(audio_unique_id_use_t use) {// This is the internal API, so it is OK to assert on bad parameter.…

Android的MediaPlayer架构介绍

本文主要介绍的是Android中很重要也最为复杂的媒体播放器&#xff08;MediaPlayer&#xff09;部分的架构。对于Android这样一个完整又相对复杂的系统&#xff0c;一个MediaPlayer功能的实现不在其具体的功能&#xff0c;而是具体功能如何适应Android系统Android MediaPlayer的…

Android之MediaPlayer详解

文章转自&#xff1a;http://www.cnblogs.com/gansc23/archive/2011/04/08/2009868.html MediaPlayer类可用于控制音频/视频文件或流的播放。关于如何使用这个类的方法还可以阅读VideoView类的文档。 1&#xff0e;状态图 对播放音频/视频文件和流的控制是通过一个状态机来…

Android MediaPlayer播放视频详细步骤

MediaPlayer类是媒体框架最重要的组成部分之一&#xff0c;此类的对象能够获取&#xff0c;解码以及播放音频和视频&#xff0c;而且只需极少量设置&#xff0c;它支持多种不同的媒体源&#xff0c;例如&#xff1a; 本地资源 内部Url&#xff0c;例如您可能从内容解析器获取U…

Android MediaPlayer

最近在做游戏状态的保存时&#xff0c;需要存储背景音乐是否静音了&#xff0c;一直不成功&#xff0c;并且总是报出如下错误&#xff1a; ERROR/MediaPlayer(9974): start called in state 64 ERROR/MediaPlayer(9974): error (-38, 0) ERROR/MediaPlayer(9974): Error (-38…

MediaPlayer类播放音频

一、MediaPlayer类 1、常用方法 方法名称功能setDataSource()设置要播放的音频文件prepare()在开始播放前。调用该方法准备播放start()开始播放或者继续播放音频pause()暂停播放reset()重置MediaPlayer对象seekTo()从指定位置播放stop()停止播放&#xff0c;调用后MediaPlaye…

Android提高第一篇之MediaPlayer

本文来自http://blog.csdn.net/hellogv/ &#xff0c;引用必须注明出处&#xff01; 前面写了十四篇关于界面的入门文章&#xff0c;大家都看完和跟着练习之后&#xff0c;对于常用的Layout和View都会有一定的了解了,接下来的文章就不再强调介绍界面了&#xff0c;而是针对具体…

Android中的MediaPlayer的使用详解

今天本文介绍的是Andriod系统自带的Mediaplayer,和VideoView实现简单的音乐和视频的播放&#xff0c;至于想做出如酷狗音乐这样的APP的话&#xff0c;只要想做&#xff0c;应该也不难&#xff0c;都是基于此实现了功能的扩展。 Android的MediaPlayer包含了Audio和Video的播放功…

MediaPlayer详解和使用

Android多媒体相关的API&#xff0c;网上基本都能找到很多相关的文章&#xff0c;使用起来也很简单&#xff0c;一直在犹豫要不要写这方面的内容&#xff0c;后来决定还是写一写&#xff0c;一方面算是一个归纳总结&#xff0c;另一方面&#xff0c;也方便以后查阅。这一篇就写…

MediaPlayer的使用

MediaPlayer的使用 MediaPlayer的使用&#xff08;2019.07.16&#xff09;1.视频播放器的原理2.Android系统自带的MediaPlay状态机详解&#xff08;MediaPlay的生命周期&#xff09;3.如何使用MediaPlayer播放音频与视频&#xff08;一 &#xff09;播放音频&#xff08;二&…

MediaPlayer使用以及常见问题

前面已经写过一篇类似的文章&#xff0c;但是还不够细致&#xff1a; 采用Android的MediaPlayerSurfaceView设计视频播放器 这里我们重新理一下&#xff0c;并记录一点实际运用时遇到的问题。 MediaPlayer特性 MediaPlayer类用于控制音频文件、视频文件和流的播放。 Media…

Android MediaPlayer类

1. MediaPlayer方法 MediaPlayer创建 可以直接调用构造函数&#xff0c;利用setDataSource()方法设置资源。MediaPlayer mp new MediaPlayer(); // path可以是本地路径&#xff0c;也可以是网络地址 mp.setDataSource(String path);也可以调用create()方法&#xff0c;create…