最近在專案中要加入視訊直播和點播功能,那麼問題來了,我需要一個播放器來播放視訊流,那該如何選擇呢?除了原生的VideoView(VideoView表示臣妾做不到啊),還有一些播放器如Vitamio,B站開源的IjkPlayer等,當然各大直播雲服務商也提供了自己的播放器。花兩天時間調研了幾家,順便記錄下來分享給大家,看一看到底哪家強。
太長不看版
這裡選擇了開源播放器IjkPlayer和直播雲廠商播放器PLDroidPlayer作為測試樣本。
資料統計
軟硬編碼 | 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 |
注:
- 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…
功能 | IjkPlayer | PLDroidPlayer |
---|---|---|
版本 | 0.8.4 | 2.0.3 |
RTMP | 支援 | 支援 |
HLS | 支援 | 支援 |
HTTP-FLV | 支援 | 支援 |
HTTPS | 支援(需要單獨編譯) | 支援 |
硬解碼 | 支援 | 支援 |
是否需要編譯 | 需要 | 不需要 |
播放控制元件 | 不提供 | 提供 |
UI定製 | 可以 | 可以 |
文件 | 不完善 | 完善 |
是否開源 | 開源 | 不開源 |
整合難度 | 略麻煩 | 容易 |
技術支援 | 無 | 有 |
測試樣本選擇
先說下在測試樣本選擇上我是如何考慮的:
- 開源播放器大V選一家,直播雲廠商選一家。
ijk無可厚非是開源播放器中的首選,關於如何玩轉ijk的文章也很多,口碑也比較好。而云服務廠商好歹是靠賣直播、點播產品賺錢的,播放器作為直播點播服務中重要的一環也是人家吃飯的工具,而且可以說被很多直播大客戶驗證過的,最重要的這些播放器SDK竟然都是免費的,不拿來用太浪費了。 - 視訊雲廠商播放器評測的選擇。
因為我司原生app本身體積比較大了,所以對我來說包體大小是第一要考慮的點。而所有在雲廠商裡,我選了包體最小的七牛作為測試樣本。大家可以根據自己業務需求再選擇測試樣本。
當然,這裡只是做了幾次資料取樣,需要結果更具說服力,可能還需要更多的測試條件和測試資料,不過我們可以從當前獲取到的資料推斷:
- 不管是軟解碼和硬解碼,PLDroidPlayer的首開速度都要遠快於IjkPlayer;
- 在軟解碼條件下,PLDroidPlayer的Cpu和記憶體消耗都要略低於IjkPlayer;
- 在硬解碼條件下,PLDroidPlayer的Cpu和記憶體消耗都要高於IjkPlayer。
基礎
在進行對比之前,我們需要對直播相關的基礎概念做一些簡單介紹,如果對這一塊比較熟悉的同學可以跳過。
視訊直播
視訊直播就是視訊資料從採集端(攝像頭)通過網路實時推送到播放端(手機,電腦,電視等),我們最早接觸到的視訊直播可能就是電視直播了,但隨著智慧手機發展,移動直播興起,它的視訊採集端是手機,播放端通常也是手機。
視訊點播
視訊點播就是一段已經錄製好的視訊資料,使用者可以點選播放。由於是已經錄製完成的視訊資料,所以還可以控制播放進度。
直播協議
直播協議常見的有三種:RTMP、Http-FLV和HLS。
- RTMP: 基於TCP協議,由Adobe設計,將音視訊資料切割成小的資料包在網際網路上傳輸,延時3s以內,但拆包組包複雜,在海量併發情況下不穩定。由於不是基於Http協議,存在被防火牆牆掉的可能性。
- Http-FLV:基於Http協議,由Adobe設計,在大塊音視訊資料頭部新增標記資訊,延時3s以內,海量併發穩定,手機瀏覽器支援不足。
- HLS:基於Http協議,由Apple設計,將視訊資料切分成片段(10s以內),由m3u8索引檔案進行管理,高延時(10s到30s),手機瀏覽器支援較好,可通過網頁轉發直播連結。
軟編碼和硬編碼
音視訊資料在網際網路上傳輸之前,由於存在冗餘資料,需要進行壓縮編碼,編碼存在兩種方式,一種是軟編碼一種是硬編碼。
- 軟編碼:使用CPU進行編碼
- 硬編碼:使用非CPU進行編碼,如GPU等
軟解碼和硬解碼
資料進行編碼之後傳輸到播放端,就要進行解碼,那麼解碼也有兩種方式,一種是軟解碼一種是硬解碼。
- 軟解碼:使用CPU進行解碼
- 硬解碼:使用非CPU進行解碼
IjkPlayer
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下編譯IjkPlayer
首先大家最好不要在Windows環境下編譯,因為我使用的是Windows系統,所以沒有多想,下載程式碼後安裝Cygwin,然後在Cygwin中安裝make,yasm,裝完之後以為大功告成,開始在Cygwin的命令列中執行編譯指令碼,但執行時報錯,因為sh指令碼還需要轉換成unix版本,於是在Cygwin中又裝了個dos2unix,將ijkplayer中的所有的sh指令碼全部轉換了一遍,然後再執行指令碼,在讀取一個配置檔案configure又出了問題,還是檔案格式問題,所以可以預見即使解決了這個檔案,後續可能還有一大堆的檔案存在這樣的問題,細思極恐,果斷棄坑。
Ubuntu下編譯IjkPlayer
棄坑Windows後又在電腦的虛擬機器上安裝了Ubuntu系統,可是等在Ubuntu上面搭建完Android開發環境後,發現硬碟空間不足了,不要問我為什麼空間不足,反正就是不足了,我能怎麼辦,我只能選擇原諒自己咯。後來想到我原來的一臺筆記本里面裝了Ubuntu 16.04,於是擦擦上面的灰,開機啟動。
搭建Android開發環境
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環境變數配置成功。
編譯IjkPlayer
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庫:
既然so庫已經生成,就可以使用Andorid Studio再次開啟ijkplayer中的安卓示例專案(android/ijkplayer),執行後就可以播放示例視訊了。這個帶有so庫的示例專案我已上傳到Github,地址為github.com/uncleleonfa…,歡迎下載。
PLDroidPlayer
PLDroidPlayer 是七牛推出的一款適用於 Android 平臺的播放器 SDK,採用全自研的跨平臺播放核心,擁有豐富的功能和優異的效能,可高度定製化和二次開發。示例專案地址為:github.com/pili-engine…。
PLDroidPlayer的整合要比ijkPlayer簡單很多,不用自己編譯so庫,不用自己建立SurfaceView和TextureView來播放視訊。可參考官方開發指南整合即可。
測試開發
為了保證測試的變數只是播放器引擎本身(這裡暫時將播放器引擎簡單的理解為各個播放器的MediaPlayer),我們定義一個公共的UI介面即VideoView來播放視訊流,然後通過代理模式去代理不同的播放器引擎。這樣VideoView在播放視訊時,可以通過代理使用不同的播放引擎(MediaPlayer)來播放。我們這裡主要測試播放器播放視訊首開的時間,播放器播放視訊過程中Cpu,記憶體的佔用情況。測試專案地址為:github.com/uncleleonfa…,測試專案執行效果:
IMediaPlayer
定義統一的MediaPlayer介面。
public interface IMediaPlayer {
void prepareAsync() throws IllegalStateException;
void start() throws IllegalStateException;
void stop() throws IllegalStateException;
void pause() throws IllegalStateException;
void release();
void reset();
.......
}複製程式碼
IMeidaPlayerProxy
定義MeidaPlayer的代理介面,所有MediaPlayer的代理必須實現newInstance介面建立MediaPlayer。
interface IMediaPlayerProxy {
IMediaPlayer newInstance();
}複製程式碼
IjkPlayer的MediaPlayer的代理
在使用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();
}
......
}複製程式碼
PLDroidPlayer的MediaPlayer代理
在使用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仿照原生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
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
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();
}複製程式碼
測試IjkPlayer
建立一個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();
}複製程式碼
測試PLDroidPlayer
建立一個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);
}
}複製程式碼
測試結果
Round 1
- 機型:Moto G (2代)
- 系統版本:5.1
- 資料來源:String path = "ivi.bupt.edu.cn/hls/cctv1hd…";
- 軟硬編碼:軟解碼
- 取樣時長:5min
- IjkPlayer版本:0.8.4
- PLDroid版本:2.0.3
IjkPlayer結果
- 首開時間:1559ms
- CPU佔比最小值:5.00%
- CPU佔比最大值:80.72%
- 平均CPU佔比:30.69%
- 記憶體使用最小值:63.49MB
- 記憶體使用最大值:114.92MB
- 平均記憶體:110.19MB
PLDroidPlayer結果
- 首開時間:198ms
- CPU佔比最小值:3.11%
- CPU佔比最大值:67.18%
- 平均CPU佔比:30.25%
- 記憶體使用最小值:32.34MB
- 記憶體使用最大值:93.47MB
- 平均記憶體:87.41MB
Round 2
- 機型:Moto G (2代)
- 系統版本:5.1
- 資料來源:String path = "ivi.bupt.edu.cn/hls/cctv1hd…";
- 軟硬編碼:硬解碼
- 取樣時長:5min
- IjkPlayer版本:0.8.4
- PLDroid版本:2.0.3
IjkPlayer結果
- 首開時間:2280ms
- CPU佔比最小值:1.36%
- CPU佔比最大值:17.37%
- 平均CPU佔比:10.10%
- 記憶體使用最小值:45.37MB
- 記憶體使用最大值:52.34MB
- 平均記憶體:48.81MB
PLDroidPlayer結果
- 首開時間:174ms
- CPU佔比最小值:2.00%
- CPU佔比最大值:69.23%
- 平均CPU佔比:28.00%
- 記憶體使用最小值:30.98MB
- 記憶體使用最大值:85.87MB
- 平均記憶體:81.67MB