基于MediaPlayer实现视频播放

2023-11-18

一、概述

一个简单的视频播放器,满足一般的需求。使用原生的 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">

    <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();
                }
            }
        }
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于MediaPlayer实现视频播放 的相关文章

  • 找不到参数的方法 dependencyResolutionManagement()

    我正在尝试使用老师给我的一个项目 但它显示了一个错误 Settings file Users admin AndroidStudioProjects HTTPNetworking settings gradle line 1 A probl
  • 类型容器“Android 依赖项”引用不存在的库 android-support-v7-appcompat/bin/android-support-v7-appcompat.jar

    我在尝试在我的项目中使用 Action Bar Compat 支持库时遇到了某种错误 我不知道出了什么问题 因为我已按照此链接中的说明进行操作 gt http developer android com tools support libr
  • StrictMode 策略违规:我的应用程序中存在 android.os.strictmode.LeakedClosableViolation?

    Android 开发新手 第一次在我的应用程序上尝试 StrictMode 我注意到以下内容 并想知道这是否是我的应用程序或库中的问题 我不太清楚 谢谢你 D StrictMode StrictMode policy violation a
  • 卸载后 Web 应用程序不显示“添加到主屏幕”

    这是我第一次创建网络应用程序 我设法解决了这个问题 所以我得到了实际的 chrome 提示 将其添加到主屏幕 然后我从手机上卸载了该网络应用程序 因为我想将其展示给我的同事 但是 屏幕上不再出现提示 问题 这是有意为之的行为还是我的应用程序
  • 找不到 com.google.firebase:firebase-core:9.0.0 [重复]

    这个问题在这里已经有答案了 在遵循有些不一致的指示之后here https firebase google com docs admob android quick start name your project and here http
  • 无法获取log.d或输出Robolectrict + gradle

    有没有人能够将 System out 或 Log d 跟踪从 robolectric 测试输出到 gradle 控制台 我在用Robolectric Gradle 测试插件 https github com robolectric robo
  • Adobe 是否为其 PDF 阅读器提供 Android SDK 或 API? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我希望能够在我们的应用程序内的视图中显示本地 PDF 文件 在 Android 4 03 下的平板电脑上运行 目前 我们将 Adob eR
  • 在画布上绘图

    我正在编写一个 Android 应用程序 它可以在视图的 onDraw 事件上直接绘制到画布上 我正在绘制一些涉及单独绘制每个像素的东西 为此我使用类似的东西 for int x 0 x lt xMax x for int y 0 y lt
  • 无法访问 com.google.android.gms.internal.zzbfm 的 zzbfm 类文件未找到

    我正在将我的 Android 应用程序项目从GCM to FCM 为此 我使用 Android Studio 中的 Firebase 助手工具 并遵循 Google 开发人员指南中的说明 一切都很顺利 并将我的应用程序代码更改为FCM根据助
  • 是否有 ADB 命令来检查媒体是否正在播放

    我想使用 ADB 命令检查根植于终端的外部设备中是否正在播放音频 视频 我无法找到任何 ADB 命令 如果有 我尝试过 adb shell dumpsys media player 我想要一个命令来指定视频是否正在运行 您可以使用以下命令查
  • 在gradle插件中获取应用程序变体的包名称

    我正在构建一个 gradle 插件 为每个应用程序变体添加一个新任务 此新任务需要应用程序变体的包名称 这是我当前的代码 它停止使用最新版本的 android gradle 插件 private String getPackageName
  • 原色(有时)变得透明

    我正在使用最新的 SDK 版本 API 21 和支持库 21 0 2 进行开发 并且在尝试实施新的材料设计指南时遇到了麻烦 材料设计说我需要有我的primary color and my accent color并将它们应用到我的应用程序上
  • 如何发布Android .aar源以使Android Studio自动找到它们?

    我正在将库发布到内部 Sonatype Nexus 存储库 Android Studio 有一个功能 可以自动查找通过 gradle 引用的库的正确源 我将 aar 的源代码作为单独的 jar 发布到 Nexus 但 Android Stu
  • 在两个活动之间传输数据[重复]

    这个问题在这里已经有答案了 我正在尝试在两个不同的活动之间发送和接收数据 我在这个网站上看到了一些其他问题 但没有任何问题涉及保留头等舱的状态 例如 如果我想从 A 类发送一个整数 X 到 B 类 然后对整数 X 进行一些操作 然后将其发送
  • 在 android DatePickerDialog 中将语言设置为法语

    有什么办法可以让日期显示在DatePickerDialog用法语 我已经搜索过这个但没有找到结果 这是我的代码 Calendar c Calendar getInstance picker new DatePickerDialog Paym
  • Android Studio - Windows 7 上的 Android SDK 问题

    我对 Google i o 2013 上发布的最新开发工具 Android Studio 有疑问 我已经成功安装了该程序并且能够正常启动 我可以导入现有项目并对其进行编辑 但是 当我尝试单击 SDK 管理器图标或 AVD 管理器图标时 或者
  • 如何根据 gradle 风格设置变量

    我想传递一个变量test我为每种风格设置了不同的值作为 NDK 的定义 但出于某种原因 他总是忽略了最后味道的价值 这是 build gradle apply plugin com android library def test andr
  • Android 套接字和 asynctask

    我即将开始制作一个应该充当 tcp 聊天客户端的应用程序 我一直在阅读和阅读 我得出的结论是最好 如果不需要 将我的套接字和异步任务中的阅读器 问题是我不确定从哪里开始 因为我是 Android 新手 这至少对我来说是一项艰巨的任务 但据我
  • Firebase 添加新节点

    如何将这些节点放入用户节点中 并创建另一个节点来存储帖子 我的数据库参考 databaseReference child user getUid setValue userInformations 您需要使用以下代码 databaseRef
  • 捕获的图像分辨率太大

    我在做什么 我允许用户捕获图像 将其存储到 SD 卡中并上传到服务器 但捕获图像的分辨率为宽度 4608 像素和高度 2592 像素 现在我想要什么 如何在不影响质量的情况下获得小分辨率图像 例如我可以获取或设置捕获的图像分辨率为原始图像分

随机推荐

  • Android 4.4.2引入的超炫动画库

    酷炫 作者博客 http rkhcy github io 源码地址 https github com Rkhcy TransitionNote Google Demo https github com android platform fr
  • 在vue3中使用百度地图

    1 在vue项目public文件夹下的index html中引入script 在需要使用百度地图的地方直接使用 代码如下
  • 移动开发之我见--“Android开发生涯”

    纵观这几年的发展 移动手机的发展真是翻天覆地 前两年诺基亚一统天下 苹果颠覆了整个手机市场 安卓也分得了一杯羹 WindowPhone手机也纯纯欲动 Bada也抓紧推出自己的系统 360也要推出自己的手机系统 百度 腾讯纷拥而至 未来世界是
  • numpy 更新版本后 more than 1 DLL from .libs警告

    问题 解析 由于卸载或者更新旧版本时 未能删掉对应的 dll文件导致 解决 根据警告路径 找到对应的dll文件 根据时间 删掉之前的版本 保留近期版本 直接删除并不会影响新版本
  • VsCode的 code . 失效了?如何解决

    已经安装了vscode 为什么没有 code 命令呢 是因为你下载vscode的时候 是直接拷贝的文件 或者下载失误的问题 从而导致code环境变量没有配置 配置环境变量 path 找到我的电脑 右键 属性 2 选择高级系统设置 单机即可
  • 栅压自举采样电路(bootstrap技术)

    栅压自举采样电路 bootstrap技术 参考 CMOS模 数转换器设计与仿真 编著 张锋 陈铖颖 范军 文章目录 栅压自举采样电路 bootstrap技术 一 电路结构 二 工作原理 一 电路结构 二 工作原理
  • 【C++初阶】右值引用

    一 什么是左值右值 一般来说能取地址的 也就是等号左边的值 比如创建的int变量是左值 而右值就是与他相反 在等号右边 并且不能被取地址 比如说数字10 一般的普通引用只能引用左值 引用和const组合在一起使用的话既可以引用左值也可以引用
  • Python求三位水仙花数

    Python求三位水仙花数 简介 水仙花数 是指一个三位整数 其各位数字的3次方和等于该数本身 例如 ABC是一个 3位水仙花数 则 A的3次方 B的3次方 C的3次方 ABC 基础掌握 Python str 函数 https www ru
  • C++/C++11中头文件iterator的使用

  • 总结c++笔试题目

    若有以下说明和语句 请选出哪个是对c数组元素的正确引用 int c 4 5 cp 5 cp c A cp 1 B cp 3 C cp 1 3 D cp 2 正确答案 D 解析 cp c 这个语句是将数组第0行的地址赋给了cp cp 1使指针
  • Kendo UI开发教程(6): Kendo DataSource 概述

    Kendo 的数据源支持本地数据源 JavaScript 对象数组 或者远程数据源 XML JSON JSONP 支持CRUD操作 创建 读取 更新和删除操作 并支持排序 分页 过滤 分组和集合等 准备开始 下面创建一个本地数据源 1 va
  • insert into select 和 insert into values区别

    INSERT INTO SELECT语句 从一个表复制数据 然后把数据插入到一个已存在的表中 将一个table1的数据的部分字段复制到table2中 或者将整个table1复制到table2中 这时候我们就要使用SELECT INTO 和
  • RS232 Android获取串口数据

    串口 串行接口简称串口 也称串行通信接口或串行通讯接口 通常指COM接口 是采用串行通信方式的扩展接口 串行接口 Serial Interface 是指数据一位一位地顺序传送 其特点是通信线路简单 只要一对传输线就可以实现双向通信 可以直接
  • STM32 printf函数无法使用

    要想STM32使用printf函数打印 需要三个条件 条件1 魔术棒配置 条件2 有以下函数 重定向c库函数printf到串口DEBUG USART 重定向后可使用printf函数 int fputc int ch FILE f 发送一个字
  • Java 8:让你的代码更简洁、高效和灵活的新特性

    Java 8 企业中使用最普遍的版本 那么了解它的新特性是非常有必要的 目录 一 函数式接口 二 Lamdba表达式 三 方法引用 四 Stream API 3 1 创建 方法一 通过集合 方法二 通过数组 方法三 通过Stream的of
  • 零知识证明

    一 概念 证明者能够在不向验证者提供任何有用的信息的情况下 使验证者相信某个论断是正确的 零知识证明 Zero Knowledge Proof 起源于最小泄露证明 设P表示掌握某些信息 并希望证实这一事实的实体 设V是证明这一事实的实体 假
  • 前端例程20220728:点击涟漪效果按钮

    演示 原理 监听按钮点击事件 点击事件中获取点击位置 在点击位置生成一个元素作为水波 水波生成后通过扩散同时变透明 水波根据动画时间变透明后销毁 代码
  • 使用Kotlin 重写毕设项目

    Kotlin目前已经转正 成为 Android 开发一级语言 前段时间不忙 将毕业设计用Kotlin 进行重写 毕业设计 Java 版 https blog csdn net qq 29375837 article details 8265
  • 谁能看懂这幅图?

    谁能看懂这幅图
  • 基于MediaPlayer实现视频播放

    一 概述 一个简单的视频播放器 满足一般的需求 使用原生的 MediaPlayer 和 TextureView来实现 功能点 获取视频的首帧进行展示 网络视频的首帧会缓存 视频播放 本地视频或者网络视频 感知生命周期 页面不可见自动暂停播放