1. 实现效果
效果图中,视频没有铺满 是因为使用了ExoPlayer的RESIZE_MODE_FIT模式, 虽然使用RESIZE_MODE_FILL模式可以填充整个父布局,但是本Demo中使用的视频源本身就不适合全屏,会把视频拉伸,效果不好。 抖音上的视频源应该都有严格的宽高尺寸,才能做到全屏有很好的效果。
2. 技术选型
1)翻页功能:网上有不少例子是使用RecyclerView + PagerSnapHelper 来实现翻页功能,但是笔者认为使用ViewPager2更加简洁。
2)视频播放:选用ExoPlayer, 谷歌亲儿子ExoPlayer | Android 开发者 | Android Developers
此外,Bilibili公司开源ijkPlayer也比较有名,但是和ExoPlayer相比,ExoPlayer导入项目之后APK体积增加小 ,可以按需导入不同的组件。
整个ExoPlayer框架包括5个组件
exoplayer-core
:核心功能exoplayer-dash
:支持DASH内容exoplayer-hls
:支持HLS内容exoplayer-smoothstreaming
:支持SmoothStreaming内容exoplayer-ui
:用于ExoPlayer的UI组件和相关的资源。 参考:
ExoPlayer简单使用 - 简书
Android中视频播放器的选择,MediaPlayer、ExoPlayer、ijkplayer简单对比_Android格调小窝-CSDN博客_exo硬解和ijk硬解哪个好
3) 视频缓存: 选用github上的一个比较有名的开源框架GitHub - danikula/AndroidVideoCache: Cache support for any video player with help of single line
VideoCache的核心原理:
参考:AndroidVideoCache-视频边播放边缓存的代理策略 - 简书
核心原理描述: VideoCache框架在本地构建了一个代理服务器,把VideoView的网络请求拦截转换为代理服务器进行网络请求,请求回包的数据写入到本地的文件缓存,并且缓存到达一定值时候通知客户端进行读取。
3. 核心实现
1)自定义一个VideoPlayManager.java封装好ExoPlayer的调用
public class VideoPlayManager {
private volatile static VideoPlayManager mInstance = null;
private Context mContext;
private SimpleExoPlayer mSimpleExoPlayer;
private VideoPlayTask mCurVideoPlayTask;
/**
* 双重检测
* @return
*/
public static VideoPlayManager getInstance(Context context) {
if (mInstance == null) {
synchronized (VideoPlayManager.class) {
if(mInstance == null) {
mInstance = new VideoPlayManager(context);
}
}
}
return mInstance;
}
public VideoPlayManager(Context context) {
this.mContext = context;
}
/**
* 开始播放
*/
public void startPlay() {
stopPlay();
if(mCurVideoPlayTask == null) {
Log.e("Video_Play_TAG", "start play task is null");
return;
}
//创建带宽对象
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
//根据当前宽带来创建选择磁道工厂对象
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(bandwidthMeter);
//传入工厂对象,以便创建选择磁道对象
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
LoadControl loadControl = new DefaultLoadControl();
mSimpleExoPlayer = ExoPlayerFactory.newSimpleInstance(mContext, trackSelector, loadControl);
//设置是否循环播放
mSimpleExoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
//配置数据源
DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(mContext,
Util.getUserAgent(mContext, "Exo_Video_Play"));
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
//获取代理url
String proxyUrl = getProxy().getProxyUrl(mCurVideoPlayTask.getVideoUrl());
Log.d("Video_Play_TAG", "start play orginal url = " + mCurVideoPlayTask.getVideoUrl() + " , proxy url = " + proxyUrl);
Uri proxyUri = Uri.parse(proxyUrl);
//配置数据源
MediaSource mediaSource = new ExtractorMediaSource(proxyUri, mediaDataSourceFactory, extractorsFactory, null, null);
mSimpleExoPlayer.prepare(mediaSource);
//隐藏播放工具
mCurVideoPlayTask.getSimpleExoPlayerView().setUseController(false);
//设置播放视频的宽高为Fit模式
mCurVideoPlayTask.getSimpleExoPlayerView().setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
//绑定player和playerView
mCurVideoPlayTask.getSimpleExoPlayerView().setPlayer(mSimpleExoPlayer);
mSimpleExoPlayer.setPlayWhenReady(true);
}
/**
* 停止播放
*/
public void stopPlay() {
if(mSimpleExoPlayer != null) {
mSimpleExoPlayer.release();
mSimpleExoPlayer = null;
}
}
public void resumePlay() {
if(mSimpleExoPlayer != null) {
mSimpleExoPlayer.setPlayWhenReady(true);
} else {
startPlay();
}
}
public void pausePlay() {
if(mSimpleExoPlayer != null) {
mSimpleExoPlayer.setPlayWhenReady(false);
}
}
/********************************************* VideoCache start ***************************************/
private HttpProxyCacheServer mHttpProxyCacheServer;
public HttpProxyCacheServer getProxy() {
if(mHttpProxyCacheServer == null) {
mHttpProxyCacheServer = newProxy();
}
return mHttpProxyCacheServer;
}
private HttpProxyCacheServer newProxy() {
//缓存大小512M,缓存文件20
return new HttpProxyCacheServer.Builder(mContext.getApplicationContext())
.maxCacheSize(512 * 1024 * 1024)
.maxCacheFilesCount(20)
.fileNameGenerator(new VideoFileNameGenerator())
.cacheDirectory(new File(mContext.getFilesDir() + "/videoCache/"))
.build();
}
/********************************************* VideoCache end ***************************************/
public VideoPlayTask getCurVideoPlayTask() {
return mCurVideoPlayTask;
}
public void setCurVideoPlayTask(VideoPlayTask mCurVideoPlayTask) {
this.mCurVideoPlayTask = mCurVideoPlayTask;
}
/**
* 构建测试数据
* @return
*/
public static List buildTestVideoUrls() {
List urls = new ArrayList<>();
urls.add("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4");
urls.add("https://vfx.mtime.cn/Video/2019/01/15/mp4/190115161611510728_480.mp4");
urls.add("http://gslb.miaopai.com/stream/oxX3t3Vm5XPHKUeTS-zbXA__.mp4");
urls.add("http://vjs.zencdn.net/v/oceans.mp4 ");
return urls;
}
}
2) 实现ViewPager2的adapter: VideoViewPagerAdapter.java
public class VideoViewPagerAdapter extends RecyclerView.Adapter {
private Context mContext;
private List mVieoUrls = new ArrayList<>();
public VideoViewPagerAdapter(Context context) {
super();
this.mContext = context;
}
public void setDataList(List videoUrls) {
mVieoUrls.clear();
mVieoUrls.addAll(videoUrls);
notifyDataSetChanged();
Log.d("Video_Play_TAG", "setDataList" );
}
public void addDataList(List videoUrls) {
mVieoUrls.addAll(videoUrls);
notifyDataSetChanged();
}
@NonNull
@Override
public VideoViewPagerAdapter.VideoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(mContext).inflate(R.layout.fragment_video_item, parent, false);
return new VideoViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull VideoViewPagerAdapter.VideoViewHolder holder, int position) {
holder.videoUrl = mVieoUrls.get(position);
holder.itemView.setTag(position);
Log.d("Video_Play_TAG", " on bind view holder pos = "+ position + " , url = " + holder.videoUrl);
}
@Override
public int getItemCount() {
return mVieoUrls.size();
}
public class VideoViewHolder extends RecyclerView.ViewHolder {
public SimpleExoPlayerView mVideoView;
public String videoUrl;
VideoViewHolder(View itemView) {
super(itemView);
mVideoView = itemView.findViewById(R.id.video_view);
}
}
public String getUrlByPos(int pos) {
return mVieoUrls.get(pos);
}
}
3) 最后在fragment里调用ViewPager2
public class MediaFragment extends Fragment {
private ViewPager2 mViewPager2;
private VideoViewPagerAdapter mVideoViewPagerAdapter;
private boolean onFragmentResume;
private boolean onFragmentVisible;
public static MediaFragment build() {
return new MediaFragment();
}
@Override
public View onCreateView(@NonNull @NotNull LayoutInflater inflater, @Nullable @org.jetbrains.annotations.Nullable ViewGroup container, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_media, null, true);
initUI(rootView);
return rootView;
}
private void initUI(View rootView) {
mViewPager2 = rootView.findViewById(R.id.viewpager2);
mVideoViewPagerAdapter = new VideoViewPagerAdapter(getActivity());
mVideoViewPagerAdapter.setDataList(VideoPlayManager.buildTestVideoUrls());
mViewPager2.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
mViewPager2.setAdapter(mVideoViewPagerAdapter);
mViewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
Log.d("Video_Play_TAG", " on page selected = " + position);
View itemView = mViewPager2.findViewWithTag(position);
SimpleExoPlayerView simpleExoPlayerView = itemView.findViewById(R.id.video_view);
VideoPlayManager.getInstance(AppUtil.getApplicationContext()).setCurVideoPlayTask(new VideoPlayTask(simpleExoPlayerView,
mVideoViewPagerAdapter.getUrlByPos(position)));
if(onFragmentResume && onFragmentVisible) {
VideoPlayManager.getInstance(AppUtil.getApplicationContext()).startPlay();
}
}
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
}
});
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser) {
onFragmentVisible = true;
VideoPlayManager.getInstance(AppUtil.getApplicationContext()).resumePlay();
Log.d("Video_Play_TAG", " video fragment可见");
}else {
onFragmentVisible = false;
VideoPlayManager.getInstance(AppUtil.getApplicationContext()).pausePlay();
Log.d("Video_Play_TAG", " video fragment不可见 ");
}
}
@Override
public void onResume() {
super.onResume();
onFragmentResume = true;
if(onFragmentVisible) {
VideoPlayManager.getInstance(AppUtil.getApplicationContext()).resumePlay();
}
Log.d("Video_Play_TAG", " video fragment Resume ");
}
@Override
public void onPause() {
super.onPause();
onFragmentResume = false;
VideoPlayManager.getInstance(AppUtil.getApplicationContext()).pausePlay();
Log.d("Video_Play_TAG", " video fragment Pause ");
}
}
4. 后续todo工作:视频的预加载实现
5.参考链接
ExoPlayer简单使用 - 简书
Android中视频播放器的选择,MediaPlayer、ExoPlayer、ijkplayer简单对比_Android格调小窝-CSDN博客_exo硬解和ijk硬解哪个好
AndroidVideoCache-视频边播放边缓存的代理策略 - 简书
6. Demo地址
GitHub - mikelhm/MikelProjectDemo: Personal Android Demo