最近在项目中要加入视频直播和点播功能,那么问题来了,我须要一个播放器来播放视频流,那该如何选择呢?除了原生的VideoView(VideoView表示臣妾作不到啊),还有一些播放器如Vitamio,B站开源的IjkPlayer等,固然各大直播云服务商也提供了本身的播放器。花两天时间调研了几家,顺便记录下来分享给你们,看一看到底哪家强。html
这里选择了开源播放器IjkPlayer和直播云厂商播放器PLDroidPlayer做为测试样本。java
软硬编码 |
IjkPlayer |
PLDroidPlayer |
||||
---|---|---|---|---|---|---|
首开(ms) | 内存 min,avg,max(MB) | CPU min,avg,max(%) | 首开(ms) | 内存 min,avg,max(MB) | CPU min,avg,max(%) | |
软编码 | 1559 | 64.49,110.19,114.92 | 5.00,30.69,80.72 | 198 | 32.34,87.41,93.47 | 3.11,30.25,67.18 |
硬编码 | 2280 | 45.37,48.81,52.34 | 1.36,10.10,17.37 | 174 | 30.98,81.67,85.87 | 2.00,28.00,69.23 |
对比点 |
IjkPlayer |
PLDroidPlayer |
---|---|---|
版本 | 0.8.4 | 2.0.3 |
jar/aar包 | 66KB(java) + 1342KB(armv7a)=1408KB | 80KB |
so库(armeabi-v7a,不带Https功能) | 2.58MB | 2.27MB |
总计 | 3.96MB | 2.35MB |
注:linux
- IjkPlayer经过gradle下载下来为aar包,存放在目录C:\Users\(用户名)\.gradle\caches\modules-2\files-2.1\tv.danmaku.ijk.media。PLDroidPlayer为jar包。
- IjkPlayer至少须要用到两个包,分别是java包和armv7a包。
- IjkPlayer和PLDroid都可以支持Https,IjkPlayer须要单独编译,PLDroid只需添加libqcOpenSSL.so库便可。这里对比的是不带Https功能的。
这里只是对主要功能点进行对比,更多PLDroidPlayer功能点介绍能够查看github.com/pili-engine…,而ijkPlayer并无对其功能进行介绍:github.com/Bilibili/ij…android
功能 |
IjkPlayer |
PLDroidPlayer |
---|---|---|
版本 | 0.8.4 | 2.0.3 |
RTMP | 支持 | 支持 |
HLS | 支持 | 支持 |
HTTP-FLV | 支持 | 支持 |
HTTPS | 支持(须要单独编译) | 支持 |
硬解码 | 支持 | 支持 |
是否须要编译 | 须要 | 不须要 |
播放控件 | 不提供 | 提供 |
UI定制 | 能够 | 能够 |
文档 | 不完善 | 完善 |
是否开源 | 开源 | 不开源 |
集成难度 | 略麻烦 | 容易 |
技术支持 | 无 | 有 |
先说下在测试样本选择上我是如何考虑的:git
固然,这里只是作了几回数据采样,须要结果更具说服力,可能还须要更多的测试条件和测试数据,不过咱们能够从当前获取到的数据推断:github
在进行对比以前,咱们须要对直播相关的基础概念作一些简单介绍,若是对这一块比较熟悉的同窗能够跳过。web
视频直播就是视频数据从采集端(摄像头)经过网络实时推送到播放端(手机,电脑,电视等),咱们最先接触到的视频直播可能就是电视直播了,但随着智能手机发展,移动直播兴起,它的视频采集端是手机,播放端一般也是手机。android-studio
视频点播就是一段已经录制好的视频数据,用户能够点击播放。因为是已经录制完成的视频数据,因此还能够控制播放进度。浏览器
直播协议常见的有三种:RTMP、Http-FLV和HLS。bash
音视频数据在互联网上传输以前,因为存在冗余数据,须要进行压缩编码,编码存在两种方式,一种是软编码一种是硬编码。
数据进行编码以后传输到播放端,就要进行解码,那么解码也有两种方式,一种是软解码一种是硬解码。
IjkPlayer是B站开源播放器,地址为:github.com/Bilibili/ij…,基于音视频编解码库FFmpeg,支持经常使用的直播协议。IjkPlayer只提供播放器引擎库,不提供UI界面,因此使用IjkPlayer时还须要对UI界面进行二次封装,不过Github上有一些基于ijkplayer二次开发的播放器,他们对UI界面作了比较好的封装。
经过git命令:
git clone https://github.com/Bilibili/ijkplayer.git复制代码
下载ijkplayer完整项目,而后使用Android Studio打开目录:ijkplayer\android\ijkplayer,这个是Android的Demo项目,运行以后,效果以下:
可是,咱们暂时仍是没法播放示例视频列表当中的视频,还须要编译so库。在编译so库的过程当中,躺坑躺到怀疑人生,跟你们分享一下,避免跟我同样踩坑。
首先你们最好不要在Windows环境下编译,由于我使用的是Windows系统,因此没有多想,下载代码后安装Cygwin,而后在Cygwin中安装make,yasm,装完以后觉得大功告成,开始在Cygwin的命令行中执行编译脚本,但执行时报错,由于sh脚本还须要转换成unix版本,因而在Cygwin中又装了个dos2unix,将ijkplayer中的全部的sh脚本所有转换了一遍,而后再执行脚本,在读取一个配置文件configure又出了问题,仍是文件格式问题,因此能够预见即便解决了这个文件,后续可能还有一大堆的文件存在这样的问题,细思极恐,果断弃坑。
弃坑Windows后又在电脑的虚拟机上安装了Ubuntu系统,但是等在Ubuntu上面搭建完Android开发环境后,发现硬盘空间不足了,不要问我为何空间不足,反正就是不足了,我能怎么办,我只能选择原谅本身咯。后来想到我原来的一台笔记本里面装了Ubuntu 16.04,因而擦擦上面的灰,开机启动。
1.安装JDK
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer
sudo apt-get install oracle-java8-set-default
//在命令行中使用java -version,java, javac命令不报命令找不到,就算安装成功复制代码
2.安装Android Studio
去官网或者国内站点下载Linux版本的Android Studio,这里,我使用的是Android Studio3.0版本。下载完成后,将Android Studio解压到/opt目录,而后使用命令行执行Andorid Studio中的bin/studio.sh启动Android Studio,进入向导界面,向导界面最后确认去下载SDK。下载完成后SDK路径为/home/xxx(用户名)/Android/Sdk。
3.下载NDK
使用Android Studio下载完SDK后,不用建立项目,直接打开SDK Manager,在里面去下载NDK,下载完成以后存放在sdk目录下的ndk-bundle目录,但这里下载的NDK版本是比较新的版本16,而ijkplayer编译也是不支持的,由于在ijkplayer\android\contrib\tools中有个sh脚本会检查编译环境,其中有一段代码会检查NDK版本:
case "$IJK_NDK_REL" in 10e*) ........ echo "IJK_NDK_REL=$IJK_NDK_REL" case "$IJK_NDK_REL" in 11*|12*|13*|14*) if test -d ${ANDROID_NDK}/toolchains/arm-linux-androideabi-4.9 then echo "NDKr$IJK_NDK_REL detected" else echo "You need the NDKr10e or later" exit 1 fi ;; *) echo "You need the NDKr10e or later" exit 1 ;; esac ;; esac复制代码
能够看出来这里只支持10e,11,12,13,14,因此ndk版本低了不行,高了也不行,没办法,咱们得去从新去官网下载低一点的版本,如r14b。
4.配置SDK和NDK路径
找到/home/(用户名)/目录,使用快捷键Ctrl + H显示隐藏文件,找到.bashrc文件打开,配置本身的SDK和NDK路径,例如:
export ANDROID_NDK=/home/leon/Android/andriod-ndk-r14b export ANDROID_SDK=/home/leon/Android/Sdk export PATH=$ANDROID_NDK:$ANDROID_SDK:$PATH复制代码
配置完成后,重启命令行,输入ndk-build命令,若是不报命令行找不到,说明NDK环境变量配置成功。
Android环境搭建好后,就能够参考官方文档着手编译ijkplayer了。
sudo apt-get update sudo apt-get install git //安装git sudo apt-get install yasm //安装yasm sudo dpkg-reconfigure dash //在弹出提示框选择“否”来使用bash //下载ijkplayer到ijkplayer-android目录 git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android cd ijkplayer-android //使用默认配置 cd config rm module.sh ln -s module-lite.sh module.sh cd .. cd android/contrib ./compile-ffmpeg.sh clean //清理 cd ~/ijkplayer-android //返回源码根目录 ./init-android.sh //主要是去下载ffmpeg cd android/contrib ./compile-ffmpeg.sh clean ./compile-ffmpeg.sh all //编译ffmpeg,all是所有编译,须要等待一段时间 cd .. //回到ijkplayer-android/android ./compile-ijk.sh all //编译ijkplayer复制代码
编译完成后,在android/ijkplayer目录下各个库模块当中找到生成的so库:
PLDroidPlayer 是七牛推出的一款适用于 Android 平台的播放器 SDK,采用全自研的跨平台播放内核,拥有丰富的功能和优异的性能,可高度定制化和二次开发。示例项目地址为:github.com/pili-engine…。
PLDroidPlayer的集成要比ijkPlayer简单不少,不用本身编译so库,不用本身建立SurfaceView和TextureView来播放视频。可参考官方开发指南集成便可。
为了保证测试的变量只是播放器引擎自己(这里暂时将播放器引擎简单的理解为各个播放器的MediaPlayer),咱们定义一个公共的UI界面即VideoView来播放视频流,而后经过代理模式去代理不一样的播放器引擎。这样VideoView在播放视频时,能够经过代理使用不一样的播放引擎(MediaPlayer)来播放。咱们这里主要测试播放器播放视频首开的时间,播放器播放视频过程当中Cpu,内存的占用状况。测试项目地址为:github.com/uncleleonfa…,测试项目运行效果:
定义统一的MediaPlayer接口。
public interface IMediaPlayer {
void prepareAsync() throws IllegalStateException;
void start() throws IllegalStateException;
void stop() throws IllegalStateException;
void pause() throws IllegalStateException;
void release();
void reset();
.......
}复制代码
定义MeidaPlayer的代理接口,全部MediaPlayer的代理必须实现newInstance接口建立MediaPlayer。
interface IMediaPlayerProxy {
IMediaPlayer newInstance();
}复制代码
在使用IjkPlayer以前须要添加依赖,而且将编译好的so库添加到项目中的jniLibs下。
//添加ijkplayer依赖 dependencies { compile 'tv.danmaku.ijk.media:ijkplayer-java:0.8.4' compile 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.4' compile 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.4' } //IjkMediaPlayer代理,实现IMediaPlayer接口 public class IjkMediaPlayerProxy implements IMediaPlayerProxy, IMediaPlayer { //声明一个IjkMediaPlayer对象 private IjkMediaPlayer mIjkMediaPlayer; @Override public IMediaPlayer newInstance() { //建立IjkMeidaPlayer对象 mIjkMediaPlayer = new IjkMediaPlayer(); return this; } @Override public void prepareAsync() throws IllegalStateException { mIjkMediaPlayer.prepareAsync(); } @Override public void start() throws IllegalStateException { mIjkMediaPlayer.start(); } @Override public void stop() throws IllegalStateException { mIjkMediaPlayer.stop(); } ...... }复制代码
在使用PLMediaPlayer以前参考官方文档集成PLDroidPlayer
//PLMediaPlayer代理,实现IMediaPlayer接口 public class PLMediaPlayerProxy implements IMediaPlayerProxy, IMediaPlayer { //定义PLMediaPlayer对象 private PLMediaPlayer mMediaPlayer; //AVOptions为MediaPlayer的选项配置,例如能够配置开启硬解码 private AVOptions mAvOptions; @Override public IMediaPlayer newInstance() { //建立PLDroidPlayer的PLMediaPlayer对象 mMediaPlayer = new PLMediaPlayer(mContext, mAvOptions); return this; } @Override public void prepareAsync() throws IllegalStateException { mMediaPlayer.prepareAsync(); } @Override public void start() throws IllegalStateException { mMediaPlayer.start(); } @Override public void stop() throws IllegalStateException { mMediaPlayer.stop(); } ........ }复制代码
VideoView仿照原生VideoView的实现,这里主要修改的是MediaPlayer的逻辑,方便配置使用不一样播放器的MediaPlayer。
public class VideoView extends SurfaceView implements IMediaPlayer.OnPreparedListener, IMediaPlayer.OnErrorListener, IMediaPlayer.OnCompletionListener, IMediaPlayer.OnInfoListener, IMediaPlayer.OnVideoSizeChangeListener{ //定义MediaPlayer代理 private IMediaPlayerProxy mMediaPlayerProxy; //定义VideoView使用的MediaPlayer private IMediaPlayer mMediaPlayer; //设置MediaPlayer的代理 public void setMediaPlayerProxy(IMediaPlayerProxy mediaPlayerProxy) { mMediaPlayerProxy = mediaPlayerProxy; } //打开视频 private void openVideo() { if (mVideoPath == null) { return; } release(); //使用代理建立对应的MediaPlayer对象 mMediaPlayer = mMediaPlayerProxy.newInstance(); mMediaPlayer.setScreenOnWhilePlaying(true); mMediaPlayer.setDisplay(mSurfaceHolder); mMediaPlayer.setLogEnabled(BuildConfig.DEBUG); mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnInfoListener(this); mMediaPlayer.setOnCompletionListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setOnVideoSizeChangeListener(this); try { mMediaPlayer.setDataSource(mVideoPath); ...... mMediaPlayer.prepareAsync(); } catch (IOException e) { e.printStackTrace(); } } @Override public void onPrepared(IMediaPlayer iMediaPlayer) { iMediaPlayer.start();//开始播放 } }复制代码
LogUtils用于采样cpu和内存数据,里面使用ScheduledThreadPoolExecutor每隔1s采样一次数据。
//开始采样 public void start() { scheduler.scheduleWithFixedDelay(new SampleTask(), 0L, 1000L, TimeUnit.MILLISECONDS); } //中止采样 public void stop() { scheduler.shutdown(); } //采样任务 private class SampleTask implements Runnable { @Override public void run() { float cpu = sampleCPU(); //采样CPU使用 float mem = sampleMemory(); //采样内存使用 } }复制代码
LogView是打印Log的自定义控件,它由一个TextView和ScrollView组成,TextView在ScrollView内部,用来显示log,ScrollView用来滚动。
public class LogView extends RelativeLayout { public LogView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); LayoutInflater.from(context).inflate(R.layout.view_log, this); final TextView textView = findViewById(R.id.tv); final ScrollView scrollView = findViewById(R.id.scroll_view); final StringBuilder stringBuilder = new StringBuilder(); //监听LogUtils的log LogUtils.getInstance().setOnUpdateLogListener(new LogUtils.OnUpdateLogListener() { @Override public void onUpdate(final long timestamp, final String msg) { //在主线程刷新界面 post(new Runnable() { @Override public void run() { String dateString = mSimpleDateFormat.format(new Date(timestamp)); String log = dateString + ": " + msg + "\n"; //添加一行log stringBuilder.append(log); //设置log显示 textView.setText(stringBuilder.toString()); //滚动ScrollView到底部 scrollView.fullScroll(View.FOCUS_DOWN); } }); } }); } }复制代码
测试视频流是:
//点播MP4视频 String path = "http://hc.yinyuetai.com/uploads/videos/common/2B40015FD4683805AAD2D7D35A80F606.mp4?sc=364e86c8a7f42de3&br=783&rd=Android"; //HLS直播流 String path = "http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8";复制代码
在VieoView中,MediaPlayer开始准备播放以前,初始化LogUtils,埋点记录MediaPlayer的准备时间。
try {
//设置视频源
mMediaPlayer.setDataSource(mVideoPath);
//初始化LogUtils
LogUtils.getInstance().init(getContext());
//记录开始准备时间
LogUtils.getInstance().onStartPrepare();
mMediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}复制代码
当MediaPlayer准备好后,会回调onPrepared,再次记录准备结束时间,这样,准备结束时间减去准备开始时间就是MediaPlayer准备耗时,即咱们的首开时间。
//准备好后的回调
@Override
public void onPrepared(IMediaPlayer iMediaPlayer) {
//记录准备结束时间
LogUtils.getInstance().onEndPrepare();
//开始播放
iMediaPlayer.start();
//开始每隔1s采样,播放结束后中止采样,主要用于点播采样
LogUtils.getInstance().start();
//开始每隔1s采样,采样5min,5min以后,自行中止,主要用于直播采样
//LogUtils.getInstance().startForDuration(5);
}
//播放结束
@Override
public void onCompletion(IMediaPlayer iMediaPlayer) {
//播放结束,中止采样
LogUtils.getInstance().stop();
}复制代码
建立一个IjkPlayerActivity使用IjkMediaPlayer来播放视频。
public class IjkPlayerActivity extends AppCompatActivity{ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_ijkplayer); //初始化IjkPlayer IjkMediaPlayer.loadLibrariesOnce(null); IjkMediaPlayer.native_profileBegin("libijkplayer.so"); VideoView videoView = findViewById(R.id.video_view); //设置IjkMediaPlayer代理 videoView.setMediaPlayerProxy(new IjkMediaPlayerProxy()); String path = "视频url" //设置视频url videoView.setVideoPath(path); } @Override protected void onStop() { super.onStop(); //通知IjkMediaPlayer结束 IjkMediaPlayer.native_profileEnd(); } }复制代码
另外,在VideoView初始化MediaPlayer时,能够调用enableMediaCodec()来开启IjkPlayer的硬解码:
private void openVideo() { ....... mMediaPlayer.enableMediaCodec(); }复制代码
建立一个PLDroidPlayerActivity使用PLMediaPlayer来播放视频。
public class PLDroidPlayerActivity extends AppCompatActivity{ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_pldroid); VideoView mVideoView = findViewById(R.id.video_view); //配置AVoptions来开启硬编码,默认为软编码 AVOptions avOptions = new AVOptions(); avOptions.setInteger(AVOptions.KEY_MEDIACODEC, AVOptions.MEDIA_CODEC_HW_DECODE); mVideoView.setMediaPlayerProxy(new PLMediaPlayerProxy(this, avOptions)); String path = "视频url"; mVideoView.setVideoPath(path); } }复制代码
IjkPlayer结果
PLDroidPlayer结果
IjkPlayer结果
PLDroidPlayer结果