美文网首页
Android使用ExoPlayer(PlayerView)播放

Android使用ExoPlayer(PlayerView)播放

作者: 刘坤林 | 来源:发表于2024-06-25 16:02 被阅读0次

    前言

    最近有个项目需要更新,发现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);
    

    注意事项

    由于我的项目compileSdk是32,但exoplayer要求是33,我就改成了33,改完后出现警告

    We recommend using a newer Android Gradle plugin to use compileSdk = 33
    This Android Gradle plugin (7.2.1) was tested up to compileSdk = 32
    This warning can be suppressed by adding android.suppressUnsupportedCompileSdk=33
    to this project's gradle.properties
    The build will continue, but you are strongly encouraged to update your project to
    use a newer Android Gradle Plugin that has been tested with compileSdk = 33
    Affected Modules: app
    

    根据它的提示,我加了android.suppressUnsupportedCompileSdk=33,而没有去改Gradle plugin插件的版本,因为改了Gradle plugin后,出现了更多没见过的问题,目前我只能这样改,有大佬知道更优解的,请指教。

    当然还可以降低exoplayer:2.19.1的吧虐不版本,比如2.15.1就不用改项目配置。

    虽然ExoPlayer已经不推荐使用了,但我看去年11月份sdk也还在更新,加上目前真的没有更好的解决办法了,只能使用它了。推荐的Media3 ExoPlayer不好用,新项目都会报错,晚辈表示不会用。

    相关文章

      网友评论

          本文标题:Android使用ExoPlayer(PlayerView)播放

          本文链接:https://www.haomeiwen.com/subject/ifljcjtx.html