音视频开发框架主要是ijkplayer、VLC、ExoPlayer和其他付费三方库这几种,本项目采用的是ijkplayer,学习难度低。
首先是ijkplayer的引入,为了支持更多格式,这里使用了GSYVideoPlayer的so包
allprojects {
repositories {
jcenter()
maven { url 'https://jitpack.io' }
maven { url "https://maven.aliyun.com/repository/public" }
}
}
//必须
implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
//更多ijk的编码支持(mpeg,rtsp, concat、crypto协议)
implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.2.0-release-jitpack'
混淆
-keep class tv.danmaku.ijk.media.player.** {*;}
-keep class tv.danmaku.ijk.media.player.IjkMediaPlayer{*;}
-keep class tv.danmaku.ijk.media.player.ffmpeg.FFmpegApi{*;}
1.搭建基类
为了方便以后扩展使用,这里搭建视频基类
public abstract class BaseVideoPlayer extends FrameLayout {
public BaseVideoPlayer(@NonNull Context context) {
this(context, null);
}
public BaseVideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseVideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
}
}
初始化SurfaceView并添加到布局中
private Context mContext;
private SurfaceView mSurfaceView;
private void init(Context context) {
mContext = context;
setBackgroundColor(Color.BLACK);
createSurfaceView();
}
private void createSurfaceView() {
mSurfaceView = new SurfaceView(mContext);
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
});
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT
, Gravity.CENTER);
addView(mSurfaceView, 0, layoutParams);
}
设置播放链接并加载
private IMediaPlayer mMediaPlayer = null;
private Map mHeader;
public void setPathLoad(String path) throws IOException {
setPathLoad(path, null);
}
public void setPathLoad(String path, Map header) throws IOException {
mHeader = header;
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
}
mMediaPlayer = createPlayer();
setListener(mMediaPlayer);
mMediaPlayer.setDisplay(mSurfaceView.getHolder());
mMediaPlayer.setDataSource(mContext, Uri.parse(path), mHeader);
mMediaPlayer.prepareAsync();
}
由子类实现IMediaPlayer
public abstract IMediaPlayer createPlayer();
设置事件回调方法
private void setListener(IMediaPlayer player) {
//在视频预处理完成后被调用。此时视频的宽度、高度、宽高比信息已经获取到,此时可调用seekTo让视频从指定位置开始播放。
player.setOnPreparedListener(iMediaPlayer -> {
if (videoListener != null) {
videoListener.onPrepared(iMediaPlayer);
}
});
player.setOnVideoSizeChangedListener((iMediaPlayer, i, i1, i2, i3) -> {
if (videoListener != null) {
videoListener.onVideoSizeChanged(iMediaPlayer, i, i1, i2, i3);
}
});
player.setOnCompletionListener(iMediaPlayer -> {
if (videoCompletionListener != null) {
videoCompletionListener.onCompletion(iMediaPlayer);
}
});
}
public void setVideoListener(VideoListener listener) {
videoListener = listener;
}
public void setVideoCompletionListener(VideoCompletionListener videoCompletionListener) {
this.videoCompletionListener = videoCompletionListener;
}
private VideoListener videoListener;
public interface VideoListener {
void onPrepared(IMediaPlayer iMediaPlayer);
void onVideoSizeChanged(IMediaPlayer iMediaPlayer, int i, int i1, int i2, int i3);
}
private VideoCompletionListener videoCompletionListener;
public interface VideoCompletionListener {
void onCompletion(IMediaPlayer iMediaPlayer);
}
在SurfaceView的surfaceChanged中获取surfaceHolder
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
if (mMediaPlayer != null) {
mMediaPlayer.setDisplay(surfaceHolder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
});
设置是否开启硬件解码,默认开启
private boolean mEnableMediaCodec = true;
public void setEnableMediaCodec(IjkMediaPlayer ijkMediaPlayer) {
int value = mEnableMediaCodec ? 1 : 0;
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", value);//开启硬解码
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", value);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", value);
}
public void setEnableMediaCodec(boolean isEnable) {
mEnableMediaCodec = isEnable;
}
播放暂停等方法
public void start() {
if (mMediaPlayer != null) {
mMediaPlayer.start();
}
}
public void release() {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
public void pause() {
if (mMediaPlayer != null) {
mMediaPlayer.pause();
}
}
public void stop() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
}
}
public void reset() {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
}
}
public long getDuration() {
if (mMediaPlayer != null) {
return mMediaPlayer.getDuration();
} else {
return 0;
}
}
public long getCurrentPosition() {
if (mMediaPlayer != null) {
return mMediaPlayer.getCurrentPosition();
} else {
return 0;
}
}
public void seekTo(long l) {
if (mMediaPlayer != null) {
mMediaPlayer.seekTo(l);
}
}
public boolean isPlaying() {
if (mMediaPlayer != null) {
return mMediaPlayer.isPlaying();
}
return false;
}
public void setVolume(float var1, float var2) {
if (mMediaPlayer != null) {
mMediaPlayer.setVolume(var1, var2);
}
}
修改视频比例
首先设置视频比例格式
private boolean isOpenProportion = false;
public static final int PROPORTION_16_9 = 1;
public static final int PROPORTION_4_3 = 2;
public static final int PROPORTION_18_9 = 3;
public static final int PROPORTION_2_39 = 4;
@IntDef({PROPORTION_16_9, PROPORTION_4_3, PROPORTION_18_9, PROPORTION_2_39})
@Retention(RetentionPolicy.SOURCE)
private @interface ProportionType {
}
private int proportion = PROPORTION_16_9;
public int getProportion() {
return proportion;
}
public void setInitProportion(@ProportionType int proportion) {
this.proportion = proportion;
}
public void setProportion(@ProportionType int proportion) {
this.proportion = proportion;
setSurfaceViewSize();
}
public void setOpenProportion() {
isOpenProportion = true;
}
设置播放器大小
private int mWidth;
private int mHeight;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int mHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int mHeightSize = MeasureSpec.getSize(heightMeasureSpec);
if (isOpenProportion) {
float proportion1 = getHeightVideoRatio();
float proportion2 = mHeightSize * 1f / mWidthSize;
if (proportion1 > proportion2) {
mHeight = mHeightSize;
if (mWidthMode == MeasureSpec.EXACTLY) {
mWidth = mWidthSize;
} else {
int maxWidth = (int) (mHeight * proportion1 + .5);
mWidth = getPaddingStart() + maxWidth + getPaddingEnd();
if (mWidthMode == MeasureSpec.AT_MOST) {
mWidth = Math.min(mWidth, mWidthSize);
}
}
} else {
mWidth = mWidthSize;
if (mHeightMode == MeasureSpec.EXACTLY) {
mHeight = mHeightSize;
} else {
int maxHeight = (int) (mWidth * proportion1 + .5);
mHeight = getPaddingTop() + maxHeight + getPaddingBottom();
if (mHeightMode == MeasureSpec.AT_MOST) {
mHeight = Math.min(mHeight, mHeightSize);
}
}
}
} else {
if (mWidthMode == MeasureSpec.EXACTLY) {
mWidth = mWidthSize;
} else {
mWidth = getPaddingStart() + getPaddingEnd();
if (mWidthMode == MeasureSpec.AT_MOST) {
mWidth = Math.min(mWidth, mWidthSize);
}
}
if (mHeightMode == MeasureSpec.EXACTLY) {
mHeight = mHeightSize;
} else {
mHeight = getPaddingTop() + getPaddingBottom();
if (mHeightMode == MeasureSpec.AT_MOST) {
mHeight = Math.min(mHeight, mHeightSize);
}
}
}
setMeasuredDimension(mWidth, mHeight);
}
private float getHeightVideoRatio() {
if (proportion == PROPORTION_4_3) {
return 3 / 4f;
} else if (proportion == PROPORTION_18_9) {
return 9 / 18f;
} else if (proportion == PROPORTION_2_39) {
return 1 / 2.39f;
} else {
return 9 / 16f;
}
}
private float getWidthVideoRatio() {
if (proportion == PROPORTION_4_3) {
return 4 / 3f;
} else if (proportion == PROPORTION_18_9) {
return 18 / 9f;
} else if (proportion == PROPORTION_2_39) {
return 2.39f;
} else {
return 16 / 9f;
}
}
设置SurfaceView大小
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
setSurfaceViewSize();
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
if (mMediaPlayer != null) {
mMediaPlayer.setDisplay(surfaceHolder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
});
private void setSurfaceViewSize() {
if (isOpenProportion) {
float proportion1 = getHeightVideoRatio();
float proportion2 = mHeight * 1f / mWidth;
ViewGroup.LayoutParams layoutParam = mSurfaceView.getLayoutParams();
if (proportion1 > proportion2) {
layoutParam.width = (int) (mHeight * getWidthVideoRatio() + .5);
layoutParam.height = mHeight;
} else {
layoutParam.width = mWidth;
layoutParam.height = (int) (mWidth * proportion1 + .5);
}
mSurfaceView.setLayoutParams(layoutParam);
}
}
到此视频播放基类基本完工
2.视频播放类public class VideoPlayer extends BaseVideoPlayer {
public VideoPlayer(@NonNull Context context) {
this(context, null);
}
public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setOpenProportion();
}
@Override
public IMediaPlayer createPlayer() {
IjkMediaPlayer ijkMediaPlayer = new IjkMediaPlayer();
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1);
ijkMediaPlayer.setOption(1, "probesize", 10240L);
ijkMediaPlayer.setOption(1, "flush_packets", 1L);
ijkMediaPlayer.setOption(4, "packet-buffering", 0L);
ijkMediaPlayer.setOption(4, "framedrop", 1L);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fps", 25);
//设置音频输出API
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 1);
//指定视频帧图像格式
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV16);
//预读缓存最小帧数
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "min-frames", 100);
//某些视频在SeekTo的时候,会跳回到拖动前的位置,这是因为视频的关键帧的问题,通俗一点就是FFMPEG不兼容,视频压缩过于厉害,seek只支持关键帧,出现这个情况就是原始的视频文件中i 帧比较少
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1);
// 清空DNS,有时因为在APP里面要播放多种类型的视频(如:MP4,直播,直播平台保存的视频,和其他http视频), 有时会造成因为DNS的问题而报10000问题
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1);
//播放重连次数
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 1);
ijkMediaPlayer.setVolume(1.0f, 1.0f);
setEnableMediaCodec(ijkMediaPlayer);
//不息屏
//ijkMediaPlayer.setScreenOnWhilePlaying(true);
return ijkMediaPlayer;
}
}
加载资源
try {
videoPlayer.setPathLoad("url");
} catch (IOException e) {
e.printStackTrace();
}
异步播放
videoPlayer.setVideoListener(new BaseVideoPlayer.VideoListener() {
@Override
public void onPrepared(IMediaPlayer iMediaPlayer) {
iMediaPlayer.start();
}
@Override
public void onVideoSizeChanged(IMediaPlayer iMediaPlayer, int i, int i1, int i2, int i3) {
}
});
播放完毕回调
videoPlayer.setVideoCompletionListener(iMediaPlayer -> {
});
设置视频比例
videoPlayer.setProportion(VideoPlayer.PROPORTION_16_9);3.流播放器
public class StreamPlayer extends BaseVideoPlayer {
public StreamPlayer(@NonNull Context context) {
super(context);
}
public StreamPlayer(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public StreamPlayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public IMediaPlayer createPlayer() {
IjkMediaPlayer ijkMediaPlayer = new IjkMediaPlayer();
// 如果是rtsp协议,可以优先用tcp(默认是用udp)
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp");
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
ijkMediaPlayer.setOption(1, "analyzemaxduration", 100L);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1);
ijkMediaPlayer.setOption(1, "probesize", 10240L);
ijkMediaPlayer.setOption(1, "flush_packets", 1L);
ijkMediaPlayer.setOption(4, "packet-buffering", 0L);
ijkMediaPlayer.setOption(4, "framedrop", 1L);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fps", 25);
//设置音频输出API
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 1);
//指定视频帧图像格式,SDL_FCC_YV12消耗更低
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV16);
//预读缓存最小帧数
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "min-frames", 100);
//某些视频在SeekTo的时候,会跳回到拖动前的位置,这是因为视频的关键帧的问题,通俗一点就是FFMPEG不兼容,视频压缩过于厉害,seek只支持关键帧,出现这个情况就是原始的视频文件中i 帧比较少
//ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1);
// 清空DNS,有时因为在APP里面要播放多种类型的视频(如:MP4,直播,直播平台保存的视频,和其他http视频), 有时会造成因为DNS的问题而报10000问题
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1);
//播放重连次数
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 1);
//设置没有声音
ijkMediaPlayer.setVolume(0f, 0f);
setEnableMediaCodec(ijkMediaPlayer);
//不息屏
//ijkMediaPlayer.setScreenOnWhilePlaying(true);
return ijkMediaPlayer;
}
}
4.全屏
在清单文件中配置
android:configChanges="orientation|keyboardHidden|screenSize"
在onConfigurationChanged中重新初始化
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
init();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
init();
}
}
全屏切换
private void init() {
setContentView(R.layout.activity_main);
videoPlayer = findViewById(R.id.videoPlayer);
videoPlayer.setVideoListener(new BaseVideoPlayer.VideoListener() {
@Override
public void onPrepared(IMediaPlayer iMediaPlayer) {
//跳转到记录位置
iMediaPlayer.seekTo(currentPosition);
iMediaPlayer.start();
}
@Override
public void onVideoSizeChanged(IMediaPlayer iMediaPlayer, int i, int i1, int i2, int i3) {
}
});
videoPlayer.setPath("http://vfx.mtime.cn/Video/2019/03/19/mp4/190319212559089721.mp4");
try {
videoPlayer.load();
} catch (IOException e) {
e.printStackTrace();
}
findViewById(R.id.button).setOnClickListener(v -> {
videoPlayer.pause();
//获取当前进度
currentPosition = videoPlayer.getCurrentPosition();
//判断当前屏幕方向
if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
//切换竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
} else {
//切换横屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
});
}
建立横竖屏布局
竖屏布局
横屏布局



