最近在項目中要加入視頻直播和點播功能,那麼問題來了,我須要一個播放器來播放視頻流,那該如何選擇呢?除了原生的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結果