引言
之前學習和使用過EXOPlayer,並結合Shared Elements效果在公司的項目中有應用。文章寫的很爛直接看github代碼吧!
相比EXOPlayer,B站的IjkPlayer逼格很高,是基於ffmpeg開源的輕量級視頻播放器支持Android&iOS。源碼在GitHub,down下來後需要編譯才能運行,具體操作官方都有說明且網上資料很多。
我所編譯的版本是0.8.2,本文會對其大體流程梳理一遍並封裝一個實用性較高的控件
正文
官方的demo跑起來第一個界面形同文件管理
找到本機的視頻文件就可以播放了
也可以通過ActionBar中的Sample選擇網絡資源。通過後綴.m3u8可以看出是HLS的資源
還有ActionBar中的Setting,這裏是一些播放時所用到的參數後文會有詳解。
播放操作涉及到的界面是VideoActivity,這裏有官方封裝的播放控件IjkVideoView,在學習了官方設計後,我結合自身的實際需求自己封裝了一個控件在後文會提到,這裏先來學習一下官方的設計。
IjkVideoView
使用時:初始化控件–>設置資源路徑–>start。
控件內部的主要邏輯順序有以下:
初始化:
initRenders() 根據設置初始化渲染器類型(渲染器即SurfaceView、TextureView)
setRender(int render) 根據渲染器類型初始化渲染器
setRenderView(IRenderView renderView) 將渲染器添加到視圖
開始播放:
setVideoURI() 設置資源路徑
openVideo() 初始化播放
-->createPlayer() 創建播放器
-->bindSurfaceHolder() 播放器與渲染器綁定
private void initRenders() {
mAllRenders.clear();//渲染器列表
//根據設置界面所選的渲染器,將其加入列表。
//這麼做其實是爲了在demo播放的時候手動切換渲染器,用以觀察畢竟是demo
//切換到時所用到的方法 toggleRender()
if (mSettings.getEnableSurfaceView())
mAllRenders.add(RENDER_SURFACE_VIEW);
if (mSettings.getEnableTextureView() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
mAllRenders.add(RENDER_TEXTURE_VIEW);
if (mSettings.getEnableNoView())
mAllRenders.add(RENDER_NONE);
if (mAllRenders.isEmpty())
mAllRenders.add(RENDER_SURFACE_VIEW);
mCurrentRender = mAllRenders.get(mCurrentRenderIndex);
setRender(mCurrentRender);
}
//根據類型初始化渲染器
//這裏將SurfaceView、TextureView進行了封裝,用到了模板設計模式,目的是將同一目的不同的操作交由具體的子類
public void setRender(int render) {
switch (render) {
case RENDER_NONE:
setRenderView(null);
break;
case RENDER_TEXTURE_VIEW: {
TextureRenderView renderView = new TextureRenderView(getContext());
if (mMediaPlayer != null) {
renderView.getSurfaceHolder().bindToMediaPlayer(mMediaPlayer);
renderView.setVideoSize(mMediaPlayer.getVideoWidth(), mMediaPlayer.getVideoHeight());
renderView.setVideoSampleAspectRatio(mMediaPlayer.getVideoSarNum(), mMediaPlayer.getVideoSarDen());
renderView.setAspectRatio(mCurrentAspectRatio);
}
setRenderView(renderView);
break;
}
case RENDER_SURFACE_VIEW: {
SurfaceRenderView renderView = new SurfaceRenderView(getContext());
setRenderView(renderView);
break;
}
default:
Log.e(TAG, String.format(Locale.getDefault(), "invalid render %d\n", render));
break;
}
}
public void setRenderView(IRenderView renderView) {
...
//切換渲染器時清楚之前的渲染器
...
if (renderView == null)
return;
mRenderView = renderView;
...
//簡單起見,將視圖的顯示比例代碼忽略
...
View renderUIView = mRenderView.getView();
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER);
renderUIView.setLayoutParams(lp);
addView(renderUIView);
mRenderView.addRenderCallback(mSHCallback);//SurfaceView的回調,不詳細展開
mRenderView.setVideoRotation(mVideoRotationDegree);//旋轉角度,橫豎屏
}
到此控件已經初始化完畢,在視圖上就可以看到自定義控件。但此時並沒有初始化播放器,視圖顯示的只是一個SurfaceView或TextureView。這時就需要給控件設置播放的資源地址了。
//原類中重載了幾次
private void setVideoURI(Uri uri, Map<String, String> headers) {
mUri = uri;
mHeaders = headers;
mSeekWhenPrepared = 0;
openVideo();
requestLayout();
invalidate();
}
緊接着就調用了openVideo()方法,在此之前先剖析下createPlayer(),此方法在openVideo()中調用
public IMediaPlayer createPlayer(int playerType) {
IMediaPlayer mediaPlayer = null;
//根據設置界面所選的播放器進行創建,有EXOPlayer和原生的MediaPlayer,這裏不是重點直接跳到IjkPlayer
switch (playerType) {
...省略其他播放器...
case Settings.PV_PLAYER__IjkMediaPlayer:
default: {
IjkMediaPlayer ijkMediaPlayer = null;
if (mUri != null) {
ijkMediaPlayer = new IjkMediaPlayer();
ijkMediaPlayer.native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG);
...
此處省略了一堆設置裏面設置的播放參數
...
}
mediaPlayer = ijkMediaPlayer;
}
break;
}
//這裏是一個關於TextureView的代理寫法
if (mSettings.getEnableDetachedSurfaceTextureView()) {
mediaPlayer = new TextureMediaPlayer(mediaPlayer);
}
return mediaPlayer;
}
private void openVideo() {
if (mUri == null || mSurfaceHolder == null) {
// not ready for playback just yet, will try again later
return;
}
// we shouldn't clear the target state, because somebody might have
// called start() previously
//demo切換播放時用
release(false);
AudioManager am = (AudioManager) mAppContext.getSystemService(Context.AUDIO_SERVICE);
am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
try {
mMediaPlayer = createPlayer(mSettings.getPlayer());
// TODO: create SubtitleController in MediaPlayer, but we need
// a context for the subtitle renderers
final Context context = getContext();
// REMOVED: SubtitleController
// REMOVED: mAudioSession
//一堆監聽
mMediaPlayer.setOnPreparedListener(mPreparedListener);
mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
mMediaPlayer.setOnCompletionListener(mCompletionListener);
mMediaPlayer.setOnErrorListener(mErrorListener);
mMediaPlayer.setOnInfoListener(mInfoListener);
mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
mMediaPlayer.setOnTimedTextListener(mOnTimedTextListener);
mCurrentBufferPercentage = 0;
//設置資源URI
String scheme = mUri.getScheme();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
mSettings.getUsingMediaDataSource() &&
(TextUtils.isEmpty(scheme) || scheme.equalsIgnoreCase("file"))) {
IMediaDataSource dataSource = new FileMediaDataSource(new File(mUri.toString()));
mMediaPlayer.setDataSource(dataSource);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mMediaPlayer.setDataSource(mAppContext, mUri, mHeaders);
} else {
mMediaPlayer.setDataSource(mUri.toString());
}
//將渲染器與播放器綁定
bindSurfaceHolder(mMediaPlayer, mSurfaceHolder);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setScreenOnWhilePlaying(true);
mPrepareStartTime = System.currentTimeMillis();
mMediaPlayer.prepareAsync();//這裏已經開始異步緩衝了,會回調到OnPreparedListener,根據具體狀態開始播放
if (mHudViewHolder != null)
mHudViewHolder.setMediaPlayer(mMediaPlayer);
// REMOVED: mPendingSubtitleTracks
// we don't set the target state here either, but preserve the
// target state that was there before.
mCurrentState = STATE_PREPARING;
attachMediaController();
} catch (IOException ex) {
Log.w(TAG, "Unable to open content: " + mUri, ex);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
} catch (IllegalArgumentException ex) {
Log.w(TAG, "Unable to open content: " + mUri, ex);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
} finally {
// REMOVED: mPendingSubtitleTracks.clear();
}
}
//具體的操作已經轉到了相應的子類TextureRenderView、SurfaceRenderView
private void bindSurfaceHolder(IMediaPlayer mp, IRenderView.ISurfaceHolder holder) {
if (mp == null)
return;
if (holder == null) {
mp.setDisplay(null);
return;
}
holder.bindToMediaPlayer(mp);
}
到此官方的IjkVideoView就已經初始化完成並開始播放資源,其餘public方法是爲了操作控件或增加播放控制控件所使用的。根據其主體思路我自己封裝了VideoViewIjk。
VideoViewIjk
我命名時習慣功能放前別名放後,大體的思路如下:
初始化播放器–>初始化播放視圖SurfaceView–>實現必要的監聽–>公開操作方法
private void initPlayer() {
IjkMediaPlayer ijkMediaPlayer = new IjkMediaPlayer();
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", mediacodec);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", mediacodec_auto_rotate);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", mediacodec_handle_resolution_change);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", opensles);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", overlay_format);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", framedrop);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", start_on_prepared);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "http-detect-range-support", http_detect_range_support);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "skip_loop_filter", skip_loop_filter);
mediaPlayer = ijkMediaPlayer;
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.setOnVideoSizeChangedListener(this);
mediaPlayer.setOnCompletionListener(this);
mediaPlayer.setOnErrorListener(this);
mediaPlayer.setOnInfoListener(this);
mediaPlayer.setOnBufferingUpdateListener(this);
mediaPlayer.setOnSeekCompleteListener(this);
}
private void initView() {
surfaceView = new SurfaceView(getContext());
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
mediaPlayer.setDisplay(surfaceView.getHolder());
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
addView(surfaceView, 0, layoutParams);
}
這裏其實沒什麼好貼的,只是將官方demo中不需要的方法移除了,具體的可以看我的GitHub