前言
最近有个项目需要更新,发现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不好用,新项目都会报错,晚辈表示不会用。
网友评论