Github
地址目前在 github
上有 10.2k
的 star
框架的优缺点不再说了,同学们自己去 github
上阅读 readme.md
文件即可,写的很详细。
我最初按照 github
上的集成说明自己集成,发现了一些问题,不过后面通过自己下载项目源码,自己运行起来后经过阅读,把问题给解决了,不过还是浪费了一点时间。
这里就不得不吐槽一句,网上好多平台的博客文章都不知道作者在写什么东西,我感觉都在复制粘贴 readme.md
文件里的文档说明,人家都已经有了,你为什么又要再写一遍呢?搞得我看标题跟文章内容根本不符合,就很机车!
可能也是有真正教同学们集成使用的,但是我经过那么几篇文章的洗礼后就失去找下去的动力了,想着还不如自己阅读源码,然后自己根据 demo 中的实现方式去自己照搬也会快很多,而且还能加深一些理解,了解开发者们的一些设计思路,为什么要这么设计接口,要怎么封装才能扩展性更强等。
说了这么多题外话,下面开始集成。
模块下添加
implementation 'cn.jzvd:jiaozivideoplayer:7.4.2'
等待 gradle
同步完成
很简单的一些布局组件,如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="250dp">
<cn.jzvd.JzvdStd
android:id="@+id/jz_video"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout>
记得在布局中给引入的 JzvdStd
外面包裹一层 Layout
,这里使用 FrameLayout
。
Mediaplayer
播放内核当上面的步骤你做好之后,下面就只需简单的几行代码即可实现视频的播放功能,清单配置文件简单配置即可。
MainActivity 中代码:
private static final String url = "http://flv2.bn.netease.com/65a92f105d9a0b107f2eaecd6d457ec1054850baab15642cf9c7df558457666d7f9ce8bb1e8c663f1c13193111d55509d89397afa118181dc08e81da0d9bd0b22970e411e6062ebbc276733f362a8510aeba04de8d25fea568426adcac5072ff86fa7750fd41300ba84641dc027c68953d3430c97385d507.mp4";
private JzvdStd jzvdStd;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
jzvdStd = (JzvdStd) findViewById(R.id.jz_video);
jzvdStd.setUp(url, "一直以为男友家真的很穷,今天来到一看,我承认我太羡慕了!", Jzvd.SCREEN_NORMAL);
// 播放前页显示图片我简单的设置了一下。
// 推荐同学们使用Glide加载网络图片或本地图片,Glide优点明显
jzvdStd.posterImageView.setImageResource(R.mipmap.ic_launcher_round);
}
@Override
protected void onPause() {
super.onPause();
Jzvd.releaseAllVideos();
}
@Override
public void onBackPressed() {
if (Jzvd.backPress()){
return;
}
super.onBackPressed();
}
build.gradle
文件添加 java
编译配置:compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
该编译配置为了防止意外报错:
清单配置
<uses-permission android:name="android.permission.INTERNET"/>
application
标签添加属性:android:usesCleartextTraffic="true"
预防高版本 Android
手机因为不信任非加密流量而导致加载http地址开头的视频失败。
activity
标签添加属性: android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="portrait"
防止 activity
在部分状态改变时重新回调各个生命周期,导致播放异常或失败
针对使用 Mediaplayer
播放内核,到这里就完成了所有的集成操作。
怎么看出使用的是 Mediaplayer 播放内核
同学们来到上面的 jzvdStd.setUp(url, "一直以为男友家真的很穷,今天来到一看,我承认我太羡慕了!", Jzvd.SCREEN_NORMAL);
这行代码,我们跟踪一下源码:
我们继续看 JZMediaSystem.class
这个类:
柳暗花明。
gif
图 - 使用 MediaPlayer
播放内核ijk
播放内核ijk 封装成了 ijkplayer
,而 ijkplayer
又是:B 站开源的基于 FFmpeg
的轻量级 Android/iOS
视频播放器。
关于 ijkplayer
的相关资料这里不再说明,同学们自己去 github
上学习,地址为 bilibili/ijkplayer
框架中并没有默认实现 ijk
播放内核,所以这个时候我们就需要自己写一个类继承框架的 JZMediaInterface
来实现该内核
实现过程中需要用到 IjkMediaPlayer
,所以在模块下的 build.gradle
文件中添加相关依赖:
implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
又因为 IjkMediaPlayer
使用过程中需要用到 so
库,so
库可以让我们在 java
中调用 C
代码,C
代码效率高,故 ndk
开发同学们也需要简单的了解。
那么我们需要在项目中引入这个 so
库,IjkMediaPlayer
加载了下面三个 so
库:
ijkffmpeg
ijksdl
ijkplayer
这三个 so
库同学们可以自己在 B 站开源的 ijkplayer
项目中自行下载编译获取,网上的教程很多,这里不说。
或者是直接使用已经编译好的,这里提供给同学们一个地址下载,我把这三个 so
库添上传到了我的百度云盘,方便大家免费下载,资源 - 提取码 - ozbt
将 so 文件放到对应的文件夹与目录下,没有则自行创建即可:
这里采用了 armeabi-v7a
CPU 架构,当然也有其他的架构,不过目前用到的这个并未发现问题,同学们自己权衡添加即可。
ndk {
// add support lib
abiFilters 'armeabi-v7a' //, 'arm64-v8a'//, "mips" //,'armeabi''x86',, 'x86_64',
}
说明:当引入了上面的 so 库后,ijk 就支持 https 协议的播放地址了,否则默认是不支持的,所以引入 so 库还是很必要的。
package com.imxiaoqi.jiaozivideodemo;
import android.graphics.SurfaceTexture;
import android.media.AudioManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.view.Surface;
import java.io.IOException;
import cn.jzvd.JZMediaInterface;
import cn.jzvd.Jzvd;
import tv.danmaku.ijk.media.player.IMediaPlayer;
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
import tv.danmaku.ijk.media.player.IjkTimedText;
/**
* ijk 播放内核
*/
public class JZMediaIjk extends JZMediaInterface implements IMediaPlayer.OnPreparedListener, IMediaPlayer.OnVideoSizeChangedListener, IMediaPlayer.OnCompletionListener, IMediaPlayer.OnErrorListener, IMediaPlayer.OnInfoListener, IMediaPlayer.OnBufferingUpdateListener, IMediaPlayer.OnSeekCompleteListener, IMediaPlayer.OnTimedTextListener {
IjkMediaPlayer ijkMediaPlayer;
public JZMediaIjk(Jzvd jzvd) {
super(jzvd);
}
@Override
public void start() {
if (ijkMediaPlayer != null) ijkMediaPlayer.start();
}
@Override
public void prepare() {
release();
mMediaHandlerThread = new HandlerThread("IMXIAOQI");
mMediaHandlerThread.start();
mMediaHandler = new Handler(mMediaHandlerThread.getLooper());//主线程还是非主线程,就在这里
handler = new Handler();
mMediaHandler.post(new Runnable() {
@Override
public void run() {
ijkMediaPlayer = new IjkMediaPlayer();
ijkMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
////1为硬解 0为软解
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1);
//使用opensles把文件从java层拷贝到native层
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 0);
//视频格式
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
//跳帧处理(-1~120)。CPU处理慢时,进行跳帧处理,保证音视频同步
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
//0为一进入就播放,1为进入时不播放
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0);
////域名检测
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0);
//设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
//最大缓冲大小,单位kb
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", 1024 * 1024);
//某些视频在SeekTo的时候,会跳回到拖动前的位置,这是因为视频的关键帧的问题,通俗一点就是FFMPEG不兼容,视频压缩过于厉害,seek只支持关键帧,出现这个情况就是原始的视频文件中i 帧比较少
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1);
//是否重连
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
//http重定向https
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1);
//设置seekTo能够快速seek到指定位置并播放
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "fastseek");
//播放前的探测Size,默认是1M, 改小一点会出画面更快
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024 * 10);
//1变速变调状态 0变速不变调状态
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1);
ijkMediaPlayer.setOnPreparedListener(JZMediaIjk.this);
ijkMediaPlayer.setOnVideoSizeChangedListener(JZMediaIjk.this);
ijkMediaPlayer.setOnCompletionListener(JZMediaIjk.this);
ijkMediaPlayer.setOnErrorListener(JZMediaIjk.this);
ijkMediaPlayer.setOnInfoListener(JZMediaIjk.this);
ijkMediaPlayer.setOnBufferingUpdateListener(JZMediaIjk.this);
ijkMediaPlayer.setOnSeekCompleteListener(JZMediaIjk.this);
ijkMediaPlayer.setOnTimedTextListener(JZMediaIjk.this);
try {
ijkMediaPlayer.setDataSource(jzvd.jzDataSource.getCurrentUrl().toString());
ijkMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
ijkMediaPlayer.setScreenOnWhilePlaying(true);
ijkMediaPlayer.prepareAsync();
ijkMediaPlayer.setSurface(new Surface(jzvd.textureView.getSurfaceTexture()));
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void pause() {
ijkMediaPlayer.pause();
}
@Override
public boolean isPlaying() {
return ijkMediaPlayer.isPlaying();
}
@Override
public void seekTo(long time) {
ijkMediaPlayer.seekTo(time);
}
@Override
public void release() {
if (mMediaHandler != null && mMediaHandlerThread != null && ijkMediaPlayer != null) {//不知道有没有妖孽
final HandlerThread tmpHandlerThread = mMediaHandlerThread;
final IjkMediaPlayer tmpMediaPlayer = ijkMediaPlayer;
JZMediaInterface.SAVED_SURFACE = null;
mMediaHandler.post(new Runnable() {
@Override
public void run() {
tmpMediaPlayer.setSurface(null);
tmpMediaPlayer.release();
tmpHandlerThread.quit();
}
});
ijkMediaPlayer = null;
}
}
@Override
public long getCurrentPosition() {
return ijkMediaPlayer.getCurrentPosition();
}
@Override
public long getDuration() {
if (ijkMediaPlayer == null) return 0;
return ijkMediaPlayer.getDuration();
}
@Override
public void setVolume(float leftVolume, float rightVolume) {
ijkMediaPlayer.setVolume(leftVolume, rightVolume);
}
@Override
public void setSpeed(float speed) {
ijkMediaPlayer.setSpeed(speed);
}
@Override
public void onPrepared(IMediaPlayer iMediaPlayer) {
handler.post(new Runnable() {
@Override
public void run() {
jzvd.onPrepared();
}
});
}
@Override
public void onVideoSizeChanged(final IMediaPlayer iMediaPlayer, int i, int i1, int i2, int i3) {
handler.post(new Runnable() {
@Override
public void run() {
jzvd.onVideoSizeChanged(iMediaPlayer.getVideoWidth(), iMediaPlayer.getVideoHeight());
}
});
}
@Override
public boolean onError(IMediaPlayer iMediaPlayer, final int what, final int extra) {
handler.post(new Runnable() {
@Override
public void run() {
jzvd.onError(what, extra);
}
});
return true;
}
@Override
public boolean onInfo(IMediaPlayer iMediaPlayer, final int what, final int extra) {
handler.post(new Runnable() {
@Override
public void run() {
jzvd.onInfo(what, extra);
}
});
return false;
}
@Override
public void onBufferingUpdate(IMediaPlayer iMediaPlayer, final int percent) {
handler.post(new Runnable() {
@Override
public void run() {
jzvd.setBufferProgress(percent);
}
});
}
@Override
public void onSeekComplete(IMediaPlayer iMediaPlayer) {
handler.post(new Runnable() {
@Override
public void run() {
jzvd.onSeekComplete();
}
});
}
@Override
public void onTimedText(IMediaPlayer iMediaPlayer, IjkTimedText ijkTimedText) {
}
@Override
public void setSurface(Surface surface) {
ijkMediaPlayer.setSurface(surface);
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
if (SAVED_SURFACE == null) {
SAVED_SURFACE = surface;
prepare();
} else {
jzvd.textureView.setSurfaceTexture(SAVED_SURFACE);
}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
@Override
public void onCompletion(IMediaPlayer iMediaPlayer) {
handler.post(new Runnable() {
@Override
public void run() {
jzvd.onCompletion();
}
});
}
}
上面的类我直接从源码中拿过来用了,同学们自己看源码即可,这里不过多说明了。
下面来分析
so
库的加载
在自定义的 JZMediaIjk
类中,找到如下代码:
我们进入 new IjkMediaPlayer()
方法中,如下:
然后我们进到 this.initPlayer(libLoader);
方法中,如下:
然后我们进入到 loadLibrariesOnce(libLoader);
中,如下:
柳暗花明。
private static final String url = "https://v-cdn.zjol.com.cn/280443.mp4";
private JzvdStd jzvdStd;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
jzvdStd = (JzvdStd) findViewById(R.id.jz_video);
// 切换 ijk 内核
jzvdStd.setUp(url, "买就完事了哦!铁汁们!", Jzvd.SCREEN_NORMAL, JZMediaIjk.class);
jzvdStd.posterImageView.setImageResource(R.mipmap.ic_launcher_round);
}
@Override
protected void onPause() {
super.onPause();
Jzvd.releaseAllVideos();
}
@Override
public void onBackPressed() {
if (Jzvd.backPress()){
return;
}
super.onBackPressed();
}
针对使用 ijk
播放内核,到这里就完成了所有的集成操作。
gif
图 - 使用 ijk
播放内核靠自己没错,多阅读源码,弄懂一些关键的地方,理解实现思路关键且必要。
这篇文章只是简单集成了一个播放的功能,对一些列表项视频或者是仿抖音的视频也都是可以基于该框架去实现的,这里就不再细说了。实现方法就是看源码,运行代码安装到手机上去定位到相关实现代码,没那么多花里胡哨的东西。
依葫芦画瓢这事真的不需要动脑。
技术永不眠!我们下期见!