引言
之前学习和使用过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 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