Android使用ExoPlayer(PlayerView)播放网络视频

880 阅读5分钟

前言

最近有个项目需要更新,发现ijkplayer已经无法继续使用,想要的so库已经找不到了。于是想将其替换成其他播放器,都不尽如人意。原因如下

1、使用Android的VideoView

①缓冲很慢,离开页面后返回,继续播放时总会有很多问题。黑屏、加载缓慢,卡死等
②暂停后过个1~2秒,会出现画面回退现象。

2、使用SurfaceView+MediaPlayer

①画面要自己调整,SurfaceView默认会把内容铺满,导致画面变形
②暂停后过个1~2秒,依然出现画面回退现象。

开始替换

1、将ExoPlayer引入到你的项目中

 implementation 'com.google.android.exoplayer:exoplayer:2.19.1'

2、新建自己的视频播放器
由于业务需求的不同,我需要自定义一个播放器去实现更复杂的功能,所以我把“PlayerView”嵌套在了“RelativeLayout”中,以便后续可自行添加和修改更多功能。完整代码

import android.content.Context;
import android.media.MediaPlayer;
import android.os.CountDownTimer;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.android.exoplayer2.DeviceInfo;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.text.CueGroup;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.video.VideoSize;
import com.lkl.linc.app.utils.LogUtils;

import java.util.Map;

/**
 * createTime   :2024/6/21 9:14
 * createBy     :lkl
 */
public class MyVideoView extends RelativeLayout {
    /**
     * 准备时长,超过这个时间就重试加载
     */
    private static final long INIT_TIME_OUT = 1000 * 15;
    /**
     * 进度回调间隔
     */
    private final static long TimeInterval = 1000;
    //计时器
    private CountDownTimer timer;
    //准备就绪后开始播放(默认false)
    private boolean autoPlay = false;
    //视频时长
    private long duration;
    //回调
    private OnVideoCallBack onVideoCallBack;
    //
    private int videoWidth, videoHeight;
    //是否准备就绪了
    private boolean isReady = false;
    //暂停时的位置、需要恢复的位置
    private long currentPosition = -1;
    //出错时,重试的次数
    private int retryCount = 0;
    //视频连接
    private String videoPath;
    //头部信息
    private Map<String, String> header;
    private final ExoPlayer player;
    private final Context context;

    public MyVideoView(Context context) {
        this(context, null, 0, 0);
    }

    public MyVideoView(Context context, AttributeSet attrs) {
        this(context, attrs, 0, 0);
    }

    public MyVideoView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyVideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.context = context;
        this.player = new ExoPlayer.Builder(context).build();
        PlayerView videoView = new PlayerView(context);
        videoView.setPlayer(player);
        videoView.setUseController(false);
        videoView.setClickable(false);
        videoView.setFocusableInTouchMode(false);
        RelativeLayout.LayoutParams lp = new LayoutParams(-1, -1);
        lp.addRule(CENTER_IN_PARENT);
        videoView.setLayoutParams(lp);
        addView(videoView);
        initListener();
    }

    private void initListener() {
        player.addListener(new Player.Listener() {
            @Override
            public void onEvents(@NonNull Player player, @NonNull Player.Events events) {
                Player.Listener.super.onEvents(player, events);
            }

            @Override
            public void onTimelineChanged(@NonNull Timeline timeline, int reason) {
                Player.Listener.super.onTimelineChanged(timeline, reason);
            }

            @Override
            public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
                Player.Listener.super.onMediaItemTransition(mediaItem, reason);
            }

            @Override
            public void onTracksChanged(@NonNull Tracks tracks) {
                Player.Listener.super.onTracksChanged(tracks);
            }

            @Override
            public void onMediaMetadataChanged(@NonNull MediaMetadata mediaMetadata) {
                Player.Listener.super.onMediaMetadataChanged(mediaMetadata);
            }

            @Override
            public void onPlaylistMetadataChanged(@NonNull MediaMetadata mediaMetadata) {
                Player.Listener.super.onPlaylistMetadataChanged(mediaMetadata);
            }

            @Override
            public void onIsLoadingChanged(boolean isLoading) {
                Player.Listener.super.onIsLoadingChanged(isLoading);
            }

            @Override
            public void onAvailableCommandsChanged(@NonNull Player.Commands availableCommands) {
                Player.Listener.super.onAvailableCommandsChanged(availableCommands);
            }

            @Override
            public void onTrackSelectionParametersChanged(@NonNull TrackSelectionParameters parameters) {
                Player.Listener.super.onTrackSelectionParametersChanged(parameters);
            }

            @Override
            public void onPlaybackStateChanged(int playbackState) {
                Player.Listener.super.onPlaybackStateChanged(playbackState);
                switch (playbackState) {
                    case Player.STATE_READY:
                        onPreparedCallBack();
                        if (onVideoCallBack != null) {
                            onVideoCallBack.onBufferEnd();
                        }
                        if (timer != null) {
                            timer.cancel();
                        }
                        timeOut.cancel();
                        break;
                    case Player.STATE_BUFFERING:
                        if (onVideoCallBack != null) {
                            onVideoCallBack.onBufferStart();
                        }
                        if (timer != null) {
                            timer.cancel();
                        }
                        timeOut.cancel();
                        timeOut.start();
                        break;
                    case Player.STATE_ENDED:
                        if (onVideoCallBack != null) {
                            onVideoCallBack.onComplete();
                        }
                        if (timer != null) {
                            timer.cancel();
                        }
                        break;
                    case Player.STATE_IDLE:
                        //玩家是空闲的,这意味着它只拥有有限的资源。播放器在播放媒体之前必须做好准备。
                        LogUtils.i("初始状态");
                        if (timer != null) {
                            timer.cancel();
                        }
                        break;
                    default:
                        LogUtils.i("播放器状态:" + playbackState);
                        break;
                }
            }

            @Override
            public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
                Player.Listener.super.onPlayWhenReadyChanged(playWhenReady, reason);
            }

            @Override
            public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) {
                Player.Listener.super.onPlaybackSuppressionReasonChanged(playbackSuppressionReason);
            }

            @Override
            public void onIsPlayingChanged(boolean isPlaying) {
                Player.Listener.super.onIsPlayingChanged(isPlaying);
                if (onVideoCallBack != null) {
                    if (isPlaying) {
                        onVideoCallBack.onStart();
                        if (timer != null) {
                            timer.start();
                        }
                    } else {
                        onVideoCallBack.onPause();
                        if (timer != null) {
                            timer.cancel();
                        }
                    }
                }
            }

            @Override
            public void onRepeatModeChanged(int repeatMode) {
                Player.Listener.super.onRepeatModeChanged(repeatMode);
            }

            @Override
            public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
                Player.Listener.super.onShuffleModeEnabledChanged(shuffleModeEnabled);
            }

            @Override
            public void onPlayerError(@NonNull PlaybackException error) {
                Player.Listener.super.onPlayerError(error);
            }

            @Override
            public void onPlayerErrorChanged(@Nullable PlaybackException error) {
                Player.Listener.super.onPlayerErrorChanged(error);
            }

            @Override
            public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
                Player.Listener.super.onPositionDiscontinuity(oldPosition, newPosition, reason);
            }

            @Override
            public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) {
                Player.Listener.super.onPlaybackParametersChanged(playbackParameters);
            }

            @Override
            public void onSeekBackIncrementChanged(long seekBackIncrementMs) {
                Player.Listener.super.onSeekBackIncrementChanged(seekBackIncrementMs);
            }

            @Override
            public void onSeekForwardIncrementChanged(long seekForwardIncrementMs) {
                Player.Listener.super.onSeekForwardIncrementChanged(seekForwardIncrementMs);
            }

            @Override
            public void onMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs) {
                Player.Listener.super.onMaxSeekToPreviousPositionChanged(maxSeekToPreviousPositionMs);
            }

            @Override
            public void onAudioSessionIdChanged(int audioSessionId) {
                Player.Listener.super.onAudioSessionIdChanged(audioSessionId);
            }

            @Override
            public void onAudioAttributesChanged(@NonNull AudioAttributes audioAttributes) {
                Player.Listener.super.onAudioAttributesChanged(audioAttributes);
            }

            @Override
            public void onVolumeChanged(float volume) {
                Player.Listener.super.onVolumeChanged(volume);
            }

            @Override
            public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {
                Player.Listener.super.onSkipSilenceEnabledChanged(skipSilenceEnabled);
            }

            @Override
            public void onDeviceInfoChanged(@NonNull DeviceInfo deviceInfo) {
                Player.Listener.super.onDeviceInfoChanged(deviceInfo);
            }

            @Override
            public void onDeviceVolumeChanged(int volume, boolean muted) {
                Player.Listener.super.onDeviceVolumeChanged(volume, muted);
            }

            @Override
            public void onVideoSizeChanged(@NonNull VideoSize videoSize) {
                Player.Listener.super.onVideoSizeChanged(videoSize);
            }

            @Override
            public void onSurfaceSizeChanged(int width, int height) {
                Player.Listener.super.onSurfaceSizeChanged(width, height);
            }

            @Override
            public void onRenderedFirstFrame() {
                Player.Listener.super.onRenderedFirstFrame();
            }

            @Override
            public void onCues(@NonNull CueGroup cueGroup) {
                Player.Listener.super.onCues(cueGroup);
            }

            @Override
            public void onMetadata(@NonNull Metadata metadata) {
                Player.Listener.super.onMetadata(metadata);
            }
        });
    }

    private void onPreparedCallBack() {
        if (isReady) {
            LogUtils.i("无需重复调用准备就绪回调");
            return;
        }
        LogUtils.i("准备就绪!!!");
        timeOut.cancel();
        isReady = true;
        VideoSize size = player.getVideoSize();
        videoWidth = size.width;
        videoHeight = size.height;
        duration = player.getDuration();
        if (timer != null) {
            timer.cancel();
        }
        timer = new CountDownTimer(duration, TimeInterval) {
            @Override
            public void onTick(long left) {
                if (onVideoCallBack != null) {
                    currentPosition = player.getCurrentPosition();
                    onVideoCallBack.onProgress(currentPosition, duration);
                }
            }

            @Override
            public void onFinish() {

            }
        };
        if (onVideoCallBack != null) {
            onVideoCallBack.onPrepared(duration);
        }
        if (currentPosition != -1) {
            //恢复播放
            LogUtils.i("恢复播放,恢复进度:" + currentPosition);
            seekTo(currentPosition);
            start();
        } else {
            if (autoPlay) {
                start();
            }
        }
    }

    public void timePause() {
        if (timer != null) {
            timer.cancel();
        }
    }

    public void timeContinue() {
        if (timer != null) {
            timer.cancel();
            timer.start();
        }
    }

    public boolean isReady() {
        return isReady;
    }

    public void setAutoPlay(boolean autoPlay) {
        this.autoPlay = autoPlay;
    }

    public int getVideoWidth() {
        return videoWidth;
    }

    public int getVideoHeight() {
        return videoHeight;
    }

    public void setOnVideoCallBack(OnVideoCallBack onVideoCallBack) {
        this.onVideoCallBack = onVideoCallBack;
    }

    public interface OnVideoCallBack {
        void onStartPrepare();

        void onPrepared(long duration);

        void onStart();

        void onPause();

        void onBufferStart();

        void onBufferEnd();

        void onProgress(long progress, long duration);

        void onComplete();

        void onError(String error);
    }

    public void retry() {
        reset();
        setVideoPath(videoPath, header);
    }

    /**
     * 播放出错后,尝试重新播放
     */
    private void retryPlay(int what, String defError) {
        if (timer != null) {
            timer.cancel();
        }
        if (retryCount >= 2) {
            if (onVideoCallBack != null) {
                onVideoCallBack.onError(defError + what);
                onVideoCallBack.onPause();
            }
            retryCount = 0;
            reset();
            LogUtils.e("确实播放出错了,编号:" + what);
        } else {
            retryCount++;
            LogUtils.e("准备重试,第" + retryCount + "次(" + what + ")");
            reset();
            postDelayed(retryRunnable, 3000);
        }
    }

    //超时计时器
    private final CountDownTimer timeOut = new CountDownTimer(INIT_TIME_OUT, 1000) {
        @Override
        public void onTick(long left) {
//            LogUtils.i("超时倒计时:" + (left / 1000));
        }

        @Override
        public void onFinish() {
            onErrorListener.onError(null, MediaPlayer.MEDIA_ERROR_TIMED_OUT, 0);
        }
    };
    //重试倒计时
    private final Runnable retryRunnable = () -> setVideoPath(videoPath, header);
    //错误回调
    private final MediaPlayer.OnErrorListener onErrorListener = (mp, what, extra) -> {
        int p = mp != null ? mp.getCurrentPosition() : 0;
        if (p > 0) {
            currentPosition = p;
        }
        switch (what) {
            case MediaPlayer.MEDIA_ERROR_IO:
                retryPlay(what, "无法加载视频");
                break;
            case MediaPlayer.MEDIA_ERROR_TIMED_OUT:
                retryPlay(what, "加载超时");
                break;
            case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
                retryPlay(what, "拒绝访问");
                break;
            default:
                retryPlay(what, "播放出错,请重试");
                break;
        }
        return true;
    };

    public void setVideoPath(String videoPath, Map<String, String> headers) {
        try {
            if (videoPath == null) {
                return;
            }
            if (!videoPath.equals(this.videoPath)) {
                currentPosition = -1;
            }
            this.videoPath = videoPath;
            this.header = headers;
            reset();
            if (this.header == null) {
                player.setMediaItem(MediaItem.fromUri(videoPath));
            } else {
                MediaItem mediaItem = new MediaItem.Builder().setDrmLicenseRequestHeaders(headers).setUri(videoPath).build();
                player.setMediaItem(mediaItem);
            }
            player.prepare();
//            videoView.setVideoURI(Uri.parse(videoPath), headers);
//            videoView.prepareAsync();
            if (isReady) {
                //曾经准备就绪过,只是因为出错了,要重新准备
                if (onVideoCallBack != null) {
                    onVideoCallBack.onBufferStart();
                }
                LogUtils.i("曾经准备就绪过,只是因为出错了,要重新准备,播放位置为:" + currentPosition);
            } else {
//            lastPausePosition = -1;
                if (onVideoCallBack != null) {
                    onVideoCallBack.onStartPrepare();
                }
            }
            isReady = false;
            timeOut.cancel();
            timeOut.start();
            LogUtils.i("开始准备:" + videoPath);
        } catch (Exception e) {
            LogUtils.e(e);
            if (onVideoCallBack != null) {
                onVideoCallBack.onError(e.toString());
            }
        }

    }

    public void start() {
        if (player == null) {
            LogUtils.e("mediaPlayer不能为空");
            return;
        }
        if (!isReady) {
            return;
        }
        switch (player.getPlaybackState()) {
            case Player.STATE_READY:
            case Player.STATE_BUFFERING:
                player.play();
                break;
            case Player.STATE_ENDED:
            case Player.STATE_IDLE:
                isReady = false;
                currentPosition = -1;
                setVideoPath(videoPath, header);
                player.play();
                break;
        }

    }

    public void pause() {
        if (player == null) {
            LogUtils.e("mediaPlayer不能为空");
            return;
        }
        if (!isReady) {
            return;
        }
        player.pause();
        currentPosition = player.getCurrentPosition();
    }

    public void resume() {
//        mediaPlayer.resume();
        if (currentPosition > 1000) {
            seekTo(currentPosition);
        }
    }

    private void reset() {
        if (player == null) {
            LogUtils.e("mediaPlayer不能为空");
            return;
        }
        try {
            player.stop();
        } catch (Exception e) {
            LogUtils.e(e);
        }

    }

    public void onDestroy() {
        this.reset();
        removeCallbacks(retryRunnable);
        //结束并释放资源
        reset();
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
//        holder.removeCallback(holderCallback);
        timeOut.cancel();
    }

    public void seekTo(long msec) {
        this.seekTo((int) msec);
    }

    public void seekTo(int msec) {
        if (!isReady) {
            return;
        }
        if (player == null) {
            LogUtils.e("mediaPlayer不能为空");
            return;
        }
        player.seekTo(msec);
    }

    public boolean isPlaying() {
        if (player == null) {
            LogUtils.e("mediaPlayer不能为空");
            return false;
        }
        return player.isPlaying();
    }

    public static abstract class MySingleOrDoubleClickListener implements View.OnClickListener {
        /**
         * 双击有效时长(毫秒),建议200~500毫秒内
         */
        private static final long ClickInterval = 210L;
        //上次点击的时间
        private long tLastClick = 0;
        private View v;

        @Override
        public void onClick(View v) {
            this.v = v;
            long t = System.currentTimeMillis();
            if (tLastClick == 0) {
                timer.start();
            } else {
                if (t - tLastClick <= ClickInterval) {
                    onDouble(v);
                    timer.cancel();
                } else {
                    timer.start();
                }
            }
            tLastClick = t;
        }

        /**
         * 单击倒计时
         */
        private final CountDownTimer timer = new CountDownTimer(ClickInterval, ClickInterval) {
            @Override
            public void onTick(long millisUntilFinished) {

            }

            @Override
            public void onFinish() {
                onSingle(v);
            }
        };

        public abstract void onSingle(View v);

        public abstract void onDouble(View v);
    }
}

3、开始使用

//重新加载(封装了超时重试功能,如果超过3次重试都未播放成功,可使用此方法再次手动加载)
binding.btnRetry.setOnClickListener(v -> binding.videoView.retry());
//视频监听(准备就绪、播放、暂停、进度、错误等监听都有)
binding.videoView.setOnVideoCallBack();

控制播放和暂停

            if (!binding.videoView.isReady()) {
                return;
            }
            if (binding.videoView.isPlaying()) {
                binding.videoView.pause();
            } else {
                binding.videoView.start();
            }

设置播放的url

  binding.videoView.setVideoPath(videoUrl, headers);

详细关注公众号:Android老皮