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