Kaynağa Gözat

更新demo案例

杨充 4 yıl önce
ebeveyn
işleme
d51858fefe

+ 5 - 0
Demo/src/main/java/org/yczbj/ycvideoplayer/newPlayer/clarity/ClarityActivity.java

@@ -14,6 +14,7 @@ import org.yczbj.ycvideoplayerlib.config.ConstantKeys;
 import org.yczbj.ycvideoplayerlib.config.VideoInfoBean;
 import org.yczbj.ycvideoplayerlib.player.VideoPlayer;
 import org.yczbj.ycvideoplayerlib.ui.view.BasisVideoController;
+import org.yczbj.ycvideoplayerlib.ui.view.CustomBottomView;
 
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -80,6 +81,10 @@ public class ClarityActivity extends AppCompatActivity implements View.OnClickLi
 
     private void initVideoPlayer() {
         BasisVideoController controller = new BasisVideoController(this);
+        CustomBottomView bottomView = controller.getBottomView();
+        if (bottomView!=null){
+            controller.removeControlComponent(bottomView);
+        }
         DefinitionControlView mDefinitionControlView = new DefinitionControlView(this);
         mDefinitionControlView.setOnRateSwitchListener(new DefinitionControlView.OnRateSwitchListener() {
             @Override

+ 8 - 3
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/ui/view/BasisVideoController.java

@@ -53,6 +53,7 @@ public class BasisVideoController extends GestureVideoController implements View
     private ImageView mLockButton;
     private ProgressBar mLoadingProgress;
     private ImageView thumb;
+    private CustomBottomView bottomView;
     private CustomTitleView titleView;
 
     public BasisVideoController(@NonNull Context context) {
@@ -140,10 +141,10 @@ public class BasisVideoController extends GestureVideoController implements View
             this.addControlComponent(liveControlView);
         } else {
             //添加底部播放控制条
-            CustomBottomView vodControlView = new CustomBottomView(mContext);
+            bottomView = new CustomBottomView(mContext);
             //是否显示底部进度条。默认显示
-            vodControlView.showBottomProgress(true);
-            this.addControlComponent(vodControlView);
+            bottomView.showBottomProgress(true);
+            this.addControlComponent(bottomView);
         }
         //添加滑动控制视图
         CustomGestureView gestureControlView = new CustomGestureView(mContext);
@@ -309,4 +310,8 @@ public class BasisVideoController extends GestureVideoController implements View
             titleView.setTitle(title);
         }
     }
+
+    public CustomBottomView getBottomView() {
+        return bottomView;
+    }
 }

+ 8 - 1
read/01.视频播放器介绍文档.md

@@ -103,6 +103,7 @@
     - 改变亮度和声音【改变声音视图,改变亮度视图】,改变视频快进和快退,左右滑动快进和快退视图(手势滑动的快进快退提示框)
     - 顶部控制区视图(包含返回健,title等),底部控制区视图(包含进度条,播放暂停,时间,切换全屏等)
     - 锁屏布局视图(全屏时展示,其他隐藏),底部播放进度条视图(很多播放器都有这个),清晰度列表视图(切换清晰度弹窗)
+    - 底部播放进度条视图(很多播放器都有这个),当bottom视图显示时底部进度条隐藏,反之则显示
 - 后期可能涉及的布局视图
     - 手势指导页面(有些播放器有新手指导功能),离线下载的界面(该界面中包含下载列表, 列表的item编辑(全选, 删除))
     - 用户从wifi切换到4g网络,提示网络切换弹窗界面(当网络由wifi变为4g的时候会显示)
@@ -216,7 +217,7 @@
 - 10.VideoPlayer相关Api
 - 11.Controller相关Api
 - 12.仿快手播放视频
-- 具体看这篇文档:[视频播放器Api说明]()
+- 具体看这篇文档:[视频播放器Api说明](https://github.com/yangchong211/YCVideoPlayer/blob/master/read/03.%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E5%99%A8Api%E8%AF%B4%E6%98%8E.md)
 
 
 
@@ -478,6 +479,11 @@
     ```
 - 大概的原理
     - 原始的方式是直接塞播放地址给播放器,它就可以直接播放。现在我们要在中间加一层本地代理,播放器播放的时候(获取数据)是通过我们的本地代理的地址来播放的,这样我们就可以很好的在中间层(本地代理层)做一些处理,比如:文件缓存,预缓存(秒开处理),监控等。
+- 原理详细一点来说
+    - 1.采用了本地代理服务的方式,通过原始url给播放器返回一个本地代理的一个url ,代理URL类似:http://127.0.0.1:port/视频url;(port端口为系统随机分配的有效端口,真实url是为了真正的下载),然后播放器播放的时候请求到了你本地的代理上了。
+    - 2.本地代理采用ServerSocket监听127.0.0.1的有效端口,这个时候手机就是一个服务器了,客户端就是socket,也就是播放器。
+    - 3.读取客户端就是socket来读取数据(http协议请求)解析http协议。
+    - 4.根据url检查视频文件是否存在,读取文件数据给播放器,也就是往socket里写入数据(socket通信)。同时如果没有下载完成会进行断点下载,当然弱网的话数据需要生产消费同步处理。
 - 如何实现预加载
     - 其实预加载的思路很简单,在进行一个播放视频后,再返回接下来需要预加载的视频url,启用线程去请求下载数据
     - 开启一个线程去请求并预加载一部分的数据,可能需要预加载的数据大于>1,利用队列先进入的先进行加载,因此可以采用LinkedHashMap保存正在预加载的task。
@@ -491,6 +497,7 @@
 - 统一管理视频播放器封装库日志,方便后期排查问题
     - 比如,视频内核,日志过滤则是:aaa
     - 比如,视频player,日志过滤则是:bbb
+    - 比如,缓存模块,日志过滤则是:VideoCache
 
 
 

+ 157 - 50
read/02.视频播放器整体结构.md

@@ -1,3 +1,4 @@
+# 02.视频播放器整体结构
 #### 目录介绍
 - 01.视频常见的布局视图
 - 02.后期可能涉及的视图
@@ -5,49 +6,37 @@
 - 04.视频视图层级示意图
 - 05.整体架构思路分析流程
 - 06.如何创建不同播放器
-- 07.如何友好处理状态控制器
+- 07.如何友好处理播放器UI
 - 08.交互交给外部开发者
 - 09.关于优先级视图展示
-- 12.开发者使用方法介绍
+- 10.代码项目lib代码介绍
 
 
 
-### 01.视频常见的布局视图
-- 视频底图(用于显示初始化视频时的封面图)
-- 视频状态视图
-    - 加载loading动画视图(有的会显示加载网速)
-    - 加载或者播放网络异常视图
-    - 加载或者播放视频失败视图
-    - 视频播放完成视图(重播提示对话框,播放结束的时候会显示这个界面)
-- 改变亮度和声音
-    - 改变声音视图(手势滑动的音量提示框)
-    - 改变亮度视图(手势滑动的亮度提示框)
-- 改变视频快进和快退
-    - 左右滑动快进和快退视图(手势滑动的快进快退提示框)
-- 顶部控制区视图(包含返回健,title等)
-- 底部控制区视图(包含进度条,播放暂停,时间,切换全屏等)
-- 锁屏布局视图(全屏时展示,其他隐藏)
-- 底部播放进度条视图(很多播放器都有这个)
+### 00.视频播放器通用框架
+- 基础封装视频播放器player,可以在ExoPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer可以自由切换
+- 对于视图状态切换和后期维护拓展,避免功能和业务出现耦合。比如需要支持播放器UI高度定制,而不是该lib库中UI代码
+- 针对视频播放,音频播放,播放回放,以及视频直播的功能。使用简单,代码拓展性强,封装性好,主要是和业务彻底解耦,暴露接口监听给开发者处理业务具体逻辑
+- 该播放器整体架构:播放器内核(自由切换) +  视频播放器 + 边播边缓存 + 高度定制播放器UI视图层
+- 项目地址:https://github.com/yangchong211/YCVideoPlayer
+- 关于视频播放器整体功能介绍文档:https://juejin.im/post/6883457444752654343
 
 
 
+### 01.视频常见的布局视图
+- 视频底图(用于显示初始化视频时的封面图),视频状态视图【加载loading,播放异常,加载视频失败,播放完成等】
+- 改变亮度和声音【改变声音视图,改变亮度视图】,改变视频快进和快退,左右滑动快进和快退视图(手势滑动的快进快退提示框)
+- 顶部控制区视图(包含返回健,title等),底部控制区视图(包含进度条,播放暂停,时间,切换全屏等)
+- 锁屏布局视图(全屏时展示,其他隐藏),底部播放进度条视图(很多播放器都有这个),清晰度列表视图(切换清晰度弹窗)
+- 底部播放进度条视图(很多播放器都有这个),当bottom视图显示时底部进度条隐藏,反之则显示
+
+
 ### 02.后期可能涉及的视图
-- 手势指导页面(有些播放器有新手指导功能)
-- 离线下载的界面(该界面中包含下载列表, 列表的item编辑(全选, 删除))
+- 手势指导页面(有些播放器有新手指导功能),离线下载的界面(该界面中包含下载列表, 列表的item编辑(全选, 删除))
 - 用户从wifi切换到4g网络,提示网络切换弹窗界面(当网络由wifi变为4g的时候会显示)
-- 图片广告视图(带有倒计时消失)
-- 开始视频广告视图
-- 非会员试看视图
-- 弹幕视图(这个很重要)
-- 水印显示视图
-- 清晰度列表视图(切换清晰度弹窗)
-- 展示更多视图(下载,分享,切换音频等)
-- 倍速播放界面(用于控制倍速)
-- 底部视频列表缩略图视图
-- 投屏视频视图界面
-- 视频直播间刷礼物界面
-- 老师开课界面
-
+- 图片广告视图(带有倒计时消失),开始视频广告视图,非会员试看视图
+- 弹幕视图(这个很重要),水印显示视图,倍速播放界面(用于控制倍速),底部视频列表缩略图视图
+- 投屏视频视图界面,视频直播间刷礼物界面,老师开课界面,展示更多视图(下载,分享,切换音频等)
 
 
 ### 03.需要达到的目的和效果
@@ -58,15 +47,11 @@
 
 
 ### 04.视频视图层级示意图
-#### 4.1 视频层级示意图
-
-
-#### 4.2 部分视图优先级考虑
+![image](https://img-blog.csdnimg.cn/20201012215233584.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3NzAwMjc1,size_16,color_FFFFFF,t_70#pic_center)
 
 
 
 ### 05.整体架构思路分析流程
-#### 5.1 视频分层
 - 播放器内核
     - 可以切换ExoPlayer、MediaPlayer,IjkPlayer,声网视频播放器,这里使用工厂模式Factory + AbstractVideoPlayer + 各个实现AbstractVideoPlayer抽象类的播放器类
     - 定义抽象的播放器,主要包含视频初始化,设置,状态设置,以及播放监听。由于每个内核播放器api可能不一样,所以这里需要实现AbstractVideoPlayer抽象类的播放器类,方便后期统一调用
@@ -82,9 +67,6 @@
     - 播放器切换状态需要改变Controller视图,比如视频异常则需要显示异常视图view,则它们之间的交互是通过ControlWrapper(同时实现Controller接口和Player接口)实现
 
 
-### 5.2 类架构图
-
-
 ### 06.如何创建不同播放器
 - 目标要求
     - 基础播放器封装了包含ExoPlayer、MediaPlayer,ijkPlayer,声网视频播放器等
@@ -99,31 +81,156 @@
     ```
 - 使用那种形式创建播放器
     - 工厂模式
+        - 隐藏内核播放器创建具体细节,开发者只需要关心所需产品对应的工厂,无须关心创建细节即可创建播放器。符合开闭原则
     - 适配器模式
-        - https://www.runoob.com/design-pattern/adapter-pattern.html
-        - 这个也是事后补救模式,但是在该库中,没有尝试这种方式
-
+        - 这个也是事后补救模式,但是在该库中,没有尝试这种方式。https://www.runoob.com/design-pattern/adapter-pattern.html
+    - 如何做到内核无缝切换?
+        - 具体的代码案例,以及具体做法,在下一篇博客中会介绍到。或者直接看代码:[视频播放器](https://github.com/yangchong211/YCVideoPlayer)
+- 播放器内核的架构图如下所示
+    - ![image](https://img-blog.csdnimg.cn/2020101321464162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3NzAwMjc1,size_16,color_FFFFFF,t_70#pic_center)
+
+
+
+### 07.如何友好处理播放器UI
+- 发展中遇到的问题
+    - 播放器可支持多种场景下的播放,多个产品会用到同一个播放器,这样就会带来一个问题,一个播放业务播放器状态发生变化,其他播放业务必须同步更新播放状态,各个播放业务之间互相交叉,随着播放业务的增多,开发和维护成本会急剧增加, 导致后续开发不可持续。 
+- 播放器内核和UI层耦合
+    - 也就是说视频player和ui操作柔和到了一起,尤其是两者之间的交互。比如播放中需要更新UI进度条,播放异常需要显示异常UI,都比较难处理播放器状态变化更新UI操作
+- UI难以自定义或者修改麻烦
+    - 比如常见的视频播放器,会把视频各种视图写到xml中,这种方式在后期代码会很大,而且改动一个小的布局,则会影响大。这样到后期往往只敢加代码,而不敢删除代码……
+    - 有时候难以适应新的场景,比如添加一个播放广告,老师开课,或者视频引导业务需求,则需要到播放器中写一堆业务代码。迭代到后期,违背了开闭原则,视频播放器需要做到和业务分离
+- 视频播放器结构需要清晰
+    - 这个是指该视频播放器能否看了文档后快速上手,知道封装的大概流程。方便后期他人修改和维护,因此需要将视频播放器功能分离。比如切换内核+视频播放器(player+controller+view)
+- 一定要解耦合
+    - 播放器player与视频UI解耦:支持添加自定义视频视图,比如支持添加自定义广告,新手引导,或者视频播放异常等视图,这个需要较强的拓展性
+- 适合多种业务场景
+    - 比如适合播放单个视频,多个视频,以及列表视频,或者类似抖音那种一个页面一个视频,还有小窗口播放视频。也就是适合大多数业务场景
+- 具体操作
+    - 播放状态变化是导致不同播放业务场景之间交叉同步,解除播放业务对播放器的直接操控,采用接口监听进行解耦。比如:player+controller+interface
+    - 具体的代码案例,以及具体做法,在下一篇博客中会介绍到。或者直接看代码:[视频播放器](https://github.com/yangchong211/YCVideoPlayer)
 
-### 07.如何友好处理状态控制器
-- 状态控制器职责
-    - 负责所有视图(自定义view视频视图)的状态管理切换
 
 
 ### 08.交互交给外部开发者
+- 在播放器中,很重要一个就是需要把播放器player的播放模式(小屏幕,正常,全屏模式),以及播放状态(播放,暂停,异常,完成,加载,缓冲等多种状态)暴露给控制层view,方便做UI更新。
+- 比如外部开发者想加一个广告视图,这个时候肯定需要给它播放器的状态
+    - 添加了自定义播放器视图,比如添加视频广告,可以选择跳过,选择播放暂停。那这个视图view,肯定是需要操作player或者获取player的状态的。这个时候就需要暴露监听视频播放的状态接口监听
+    - 首先定义一个InterControlView接口,也就是说所有自定义视频视图view需要实现这个接口,该接口中的核心方法有:绑定视图到播放器,视图显示隐藏变化监听,播放状态监听,播放模式监听,进度监听,锁屏监听等
+    - 在BaseVideoController中的状态监听中,通过InterControlView接口对象就可以把播放器的状态传递到子类中
+- 举一个代码的例子
+    - 比如,现在有个业务需求,需要在视频播放器刚开始添加一个广告视图,等待广告倒计时120秒后,直接进入播放视频逻辑。相信这个业务场景很常见,大家都碰到过,使用该播放器就特别简单,代码如下所示:
+    - 首先创建一个自定义view,需要实现InterControlView接口,重写该接口中所有抽象方法,这里省略了很多代码,具体看demo。
+    ``` java
+    public class AdControlView extends FrameLayout implements InterControlView, View.OnClickListener {
+    
+        private ControlWrapper mControlWrapper;
+        public AdControlView(@NonNull Context context) {
+            super(context);
+            init(context);
+        }
+    
+        private void init(Context context){
+            LayoutInflater.from(getContext()).inflate(R.layout.layout_ad_control_view, this, true);
+        }
+       
+        /**
+         * 播放状态
+         * -1               播放错误
+         * 0                播放未开始
+         * 1                播放准备中
+         * 2                播放准备就绪
+         * 3                正在播放
+         * 4                暂停播放
+         * 5                正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
+         * 6                暂停缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
+         * 7                播放完成
+         * 8                开始播放中止
+         * @param playState                     播放状态,主要是指播放器的各种状态
+         */
+        @Override
+        public void onPlayStateChanged(int playState) {
+            switch (playState) {
+                case ConstantKeys.CurrentState.STATE_PLAYING:
+                    mControlWrapper.startProgress();
+                    mPlayButton.setSelected(true);
+                    break;
+                case ConstantKeys.CurrentState.STATE_PAUSED:
+                    mPlayButton.setSelected(false);
+                    break;
+            }
+        }
+    
+        /**
+         * 播放模式
+         * 普通模式,小窗口模式,正常模式三种其中一种
+         * MODE_NORMAL              普通模式
+         * MODE_FULL_SCREEN         全屏模式
+         * MODE_TINY_WINDOW         小屏模式
+         * @param playerState                   播放模式
+         */
+        @Override
+        public void onPlayerStateChanged(int playerState) {
+            switch (playerState) {
+                case ConstantKeys.PlayMode.MODE_NORMAL:
+                    mBack.setVisibility(GONE);
+                    mFullScreen.setSelected(false);
+                    break;
+                case ConstantKeys.PlayMode.MODE_FULL_SCREEN:
+                    mBack.setVisibility(VISIBLE);
+                    mFullScreen.setSelected(true);
+                    break;
+            }
+            //暂未实现全面屏适配逻辑,需要你自己补全
+        }
+    }
+    ```
+    - 然后该怎么使用这个自定义view呢?很简单,在之前基础上,通过控制器对象add进来即可,代码如下所示
+    ``` java
+    controller = new BasisVideoController(this);
+    AdControlView adControlView = new AdControlView(this);
+    adControlView.setListener(new AdControlView.AdControlListener() {
+        @Override
+        public void onAdClick() {
+            BaseToast.showRoundRectToast( "广告点击跳转");
+        }
+    
+        @Override
+        public void onSkipAd() {
+            playVideo();
+        }
+    });
+    controller.addControlComponent(adControlView);
+    //设置控制器
+    mVideoPlayer.setController(controller);
+    mVideoPlayer.setUrl(proxyUrl);
+    mVideoPlayer.start();
+    ```
 
 
 
 ### 09.关于优先级视图展示
+- 视频播放器为了拓展性,需要暴露view接口供外部开发者自定义视频播放器视图,通过addView的形式添加到播放器的控制器中。
+    - 这就涉及view视图的层级性。控制view视图的显示和隐藏是特别重要的,这个时候在自定义view中就需要拿到播放器的状态
+- 举一个简单的例子,基础视频播放器
+    - 添加了基础播放功能的几个播放视图。有播放完成,播放异常,播放加载,顶部标题栏,底部控制条栏,锁屏,以及手势滑动栏。如何控制它们的显示隐藏切换呢?
+    - 在addView这些视图时,大多数的view都是默认GONE隐藏的。比如当视频初始化时,先缓冲则显示缓冲view而隐藏其他视图,接着播放则显示顶部/底部视图而隐藏其他视图
+- 比如有时候需要显示两种不同的自定义视图如何处理
+    - 举个例子,播放的时候,点击一下视频,会显示顶部title视图和底部控制条视图,那么这样会同时显示两个视图。
+    - 点击顶部title视图的返回键可以关闭播放器,点击底部控制条视图的播放暂停可以控制播放条件。这个时候底部控制条视图FrameLayout的ChildView在整个视频的底部,顶部title视图FrameLayout的ChildView在整个视频的顶部,这样可以达到上下层都可以相应事件。
+- 那么FrameLayout层层重叠,如何让下层不响应事件
+    - 在最上方显示的层加上: android:clickable="true" 可以避免点击上层触发底层。或者直接给控制设置一个background颜色也可以。
 
 
 
-### 12.开发者使用方法介绍
+### 10.代码项目lib代码介绍
+![image](https://img-blog.csdnimg.cn/20201013092150588.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3NzAwMjc1,size_16,color_FFFFFF,t_70#pic_center)
+![image](https://img-blog.csdnimg.cn/2020101309293329.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3NzAwMjc1,size_16,color_FFFFFF,t_70#pic_center)
+![image](https://img-blog.csdnimg.cn/2020101321464162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3NzAwMjc1,size_16,color_FFFFFF,t_70#pic_center)
+![image](https://img-blog.csdnimg.cn/20201013094115174.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3NzAwMjc1,size_16,color_FFFFFF,t_70#pic_center)
 
 
 
 
-### 参考文章
-- https://blog.csdn.net/weixin_38753262/article/details/105445850
 
 
 

+ 79 - 8
read/03.视频播放器Api说明.md

@@ -11,7 +11,19 @@
 - 09.播放多个视频
 - 10.VideoPlayer相关Api
 - 11.Controller相关Api
-- 12.仿快手播放视频
+- 12.边播放边缓存api
+- 13.类似抖音视频预加载
+
+
+
+### 00.视频播放器通用框架
+- 基础封装视频播放器player,可以在ExoPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer可以自由切换
+- 对于视图状态切换和后期维护拓展,避免功能和业务出现耦合。比如需要支持播放器UI高度定制,而不是该lib库中UI代码
+- 针对视频播放,音频播放,播放回放,以及视频直播的功能。使用简单,代码拓展性强,封装性好,主要是和业务彻底解耦,暴露接口监听给开发者处理业务具体逻辑
+- 该播放器整体架构:播放器内核(自由切换) +  视频播放器 + 边播边缓存 + 高度定制播放器UI视图层
+- 项目地址:https://github.com/yangchong211/YCVideoPlayer
+- 关于视频播放器整体功能介绍文档:https://juejin.im/post/6883457444752654343
+
 
 
 
@@ -481,17 +493,76 @@
     controller.destroy();
     ```
 
-### 12.仿快手播放视频
-
-
-
-
-
-
 
+### 12.边播放边缓存api
+- 如下所示
+    ``` java
+    controller = new BasisVideoController(this);
+    //设置视频背景图
+    Glide.with(this).load(R.drawable.image_default).into(controller.getThumb());
+    //设置控制器
+    mVideoPlayer.setController(controller);
+    HttpProxyCacheServer server = new HttpProxyCacheServer(this);
+    String proxyVideoUrl = server.getProxyUrl(URL_AD);
+    mVideoPlayer.setUrl(proxyUrl);
+    mVideoPlayer.start();
+    ```
 
 
 
+### 13.类似抖音视频预加载
+- 如下所示,这个是针对ViewPager
+    ``` java
+    //获取PreloadManager预加载管理者对象
+    mPreloadManager = PreloadManager.getInstance(this);
+    //在播放视频的时候
+    String playUrl = mPreloadManager.getPlayUrl(url);
+    VideoLogUtils.i("startPlay: " + "position: " + position + "  url: " + playUrl);
+    mVideoPlayer.setUrl(playUrl);
+    //在页面滚动的时候
+    mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+        @Override
+        public void onPageScrollStateChanged(int state) {
+            super.onPageScrollStateChanged(state);
+            if (state == VerticalViewPager.SCROLL_STATE_IDLE) {
+                mPreloadManager.resumePreload(mCurPos, mIsReverseScroll);
+            } else {
+                mPreloadManager.pausePreload(mCurPos, mIsReverseScroll);
+            }
+        }
+    });
+    ```
+- 如下所示,这个是针对RecyclerView
+    ``` java
+    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+        /**
+         * 是否反向滑动
+         */
+        private boolean mIsReverseScroll;
+    
+        @Override
+        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+            super.onScrolled(recyclerView, dx, dy);
+            if (dy>0){
+                //表示下滑
+                mIsReverseScroll = false;
+            } else {
+                //表示上滑
+                mIsReverseScroll = true;
+            }
+        }
+    
+        @Override
+        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+            super.onScrollStateChanged(recyclerView, newState);
+            if (newState == VerticalViewPager.SCROLL_STATE_IDLE) {
+                mPreloadManager.resumePreload(mCurPos, mIsReverseScroll);
+            } else {
+                mPreloadManager.pausePreload(mCurPos, mIsReverseScroll);
+            }
+        }
+    });
+    ```
 
 
 

+ 131 - 22
read/04.视频播放器封装思路.md

@@ -7,6 +7,12 @@
 - 05.播放器UI层封装
 - 06.如何简单使用
 - 07.如何自定义播放器
+- 08.该案例的拓展性分享
+- 09.关于视频缓存方案
+- 10.如何监控视频埋点
+- 11.待实现的需求分析
+- 12.参考案例和博客记录
+
 
 
 
@@ -35,7 +41,6 @@
 
 
 ### 03.该播放器框架特点
-#### 3.1 通用视频框架特点
 - 一定要解耦合
     - 播放器内核与播放器解耦: 支持更多的播放场景、以及新的播放业务快速接入,并且不影响其他播放业务,比如后期添加阿里云播放器内核,或者腾讯播放器内核
     - 播放器player与视频UI解耦:支持添加自定义视频视图,比如支持添加自定义广告,新手引导,或者视频播放异常等视图,这个需要较强的拓展性
@@ -44,23 +49,6 @@
 
 
 
-#### 3.2 视频分层
-- 播放器内核
-    - 可以切换ExoPlayer、MediaPlayer,IjkPlayer,声网视频播放器,这里使用工厂模式Factory + AbstractVideoPlayer + 各个实现AbstractVideoPlayer抽象类的播放器类
-    - 定义抽象的播放器,主要包含视频初始化,设置,状态设置,以及播放监听。由于每个内核播放器api可能不一样,所以这里需要实现AbstractVideoPlayer抽象类的播放器类,方便后期统一调用
-    - 为了方便创建不同内核player,所以需要创建一个PlayerFactory,定义一个createPlayer创建播放器的抽象方法,然后各个内核都实现它,各自创建自己的播放器
-- VideoPlayer播放器
-    - 可以自由切换视频内核,Player+Controller。player负责播放的逻辑,Controller负责视图相关的逻辑,两者之间用接口进行通信
-    - 针对Controller,需要定义一个接口,主要负责视图UI处理逻辑,支持添加各种自定义视图View【统一实现自定义接口Control】,每个view尽量保证功能单一性,最后通过addView形式添加进来
-    - 针对Player,需要定义一个接口,主要负责视频播放处理逻辑,比如视频播放,暂停,设置播放进度,设置视频链接,切换播放模式等操作。需要注意把Controller设置到Player里面,两者之间通过接口交互
-- UI控制器视图
-    - 定义一个BaseVideoController类,这个主要是集成各种事件的处理逻辑,比如播放器状态改变,控制视图隐藏和显示,播放进度改变,锁定状态改变,设备方向监听等等操作
-    - 定义一个view的接口InterControlView,在这里类里定义绑定视图,视图隐藏和显示,播放状态,播放模式,播放进度,锁屏等操作。这个每个实现类则都可以拿到这些属性呢
-    - 在BaseVideoController中使用LinkedHashMap保存每个自定义view视图,添加则put进来后然后通过addView将视图添加到该控制器中,这样非常方便添加自定义视图
-    - 播放器切换状态需要改变Controller视图,比如视频异常则需要显示异常视图view,则它们之间的交互是通过ControlWrapper(同时实现Controller接口和Player接口)实现
-
-
-
 ### 04.播放器内核封装
 #### 4.1 视频播放器内核封装需求
 - 播放器内核难以切换
@@ -80,7 +68,11 @@
 - 提问:针对不同内核播放器,比如谷歌的ExoPlayer,B站的IjkPlayer,还有原生的MediaPlayer,有些api不一样,那使用的时候如何统一api呢?
     - 比如说,ijk和exo的视频播放listener监听api就完全不同,这个时候需要做兼容处理
     - 定义接口,然后各个不同内核播放器实现接口,重写抽象方法。调用的时候,获取接口对象调用api,这样就可以统一Api
-- 定义一个接口,这个接口有什么呢?这个接口定义通用视频播放器方法,比如常见的有:视频初始化,设置url,加载,以及播放状态,简单来说可以分为三个部分。
+- 播放器内核
+    - 可以切换ExoPlayer、MediaPlayer,IjkPlayer,声网视频播放器,这里使用工厂模式Factory + AbstractVideoPlayer + 各个实现AbstractVideoPlayer抽象类的播放器类
+    - 定义抽象的播放器,主要包含视频初始化,设置,状态设置,以及播放监听。由于每个内核播放器api可能不一样,所以这里需要实现AbstractVideoPlayer抽象类的播放器类,方便后期统一调用
+    - 为了方便创建不同内核player,所以需要创建一个PlayerFactory,定义一个createPlayer创建播放器的抽象方法,然后各个内核都实现它,各自创建自己的播放器
+- 关于AbstractVideoPlayer接口详细说明。这个接口定义通用视频播放器方法,比如常见的有:视频初始化,设置url,加载,以及播放状态,简单来说可以分为三个部分。
     - 第一部分:视频初始化实例对象方法,主要包括:initPlayer初始化视频,setDataSource设置视频播放器地址,setSurface设置视频播放器渲染view,prepareAsync开始准备播放操作
     - 第二部分:视频播放器状态方法,主要包括:播放,暂停,恢复,重制,设置进度,释放资源,获取进度,设置速度,设置音量
     - 第三部分:player绑定view后,需要监听播放状态,比如播放异常,播放完成,播放准备,播放size变化,还有播放准备
@@ -88,18 +80,92 @@
 
 
 ### 05.播放器UI层封装
+#### 5.1 实际开发遇到问题
+- 发展中遇到的问题
+    - 播放器可支持多种场景下的播放,多个产品会用到同一个播放器,这样就会带来一个问题,一个播放业务播放器状态发生变化,其他播放业务必须同步更新播放状态,各个播放业务之间互相交叉,随着播放业务的增多,开发和维护成本会急剧增加, 导致后续开发不可持续。 
+- 不太好适合多种业务场景
+    - 比如适合播放单个视频,多个视频,以及列表视频,或者类似抖音那种一个页面一个视频,还有小窗口播放视频。也就是适合大多数业务场景,视频通用性需要尽可能完善
+
+
+
+#### 5.2 如何分离播放和UI分离
+- VideoPlayer播放器
+    - 可以自由切换视频内核,Player+Controller。player负责播放的逻辑,Controller负责视图相关的逻辑,两者之间用接口进行通信
+    - 针对Controller,需要定义一个接口,主要负责视图UI处理逻辑,支持添加各种自定义视图View【统一实现自定义接口Control】,每个view尽量保证功能单一性,最后通过addView形式添加进来
+    - 针对Player,需要定义一个接口,主要负责视频播放处理逻辑,比如视频播放,暂停,设置播放进度,设置视频链接,切换播放模式等操作。需要注意把Controller设置到Player里面,两者之间通过接口交互
+- UI控制器视图
+    - 定义一个BaseVideoController类,这个主要是集成各种事件的处理逻辑,比如播放器状态改变,控制视图隐藏和显示,播放进度改变,锁定状态改变,设备方向监听等等操作
+    - 定义一个view的接口InterControlView,在这里类里定义绑定视图,视图隐藏和显示,播放状态,播放模式,播放进度,锁屏等操作。这个每个实现类则都可以拿到这些属性呢
+    - 在BaseVideoController中使用LinkedHashMap保存每个自定义view视图,添加则put进来后然后通过addView将视图添加到该控制器中,这样非常方便添加自定义视图
+    - 播放器切换状态需要改变Controller视图,比如视频异常则需要显示异常视图view,则它们之间的交互是通过ControlWrapper(同时实现Controller接口和Player接口)实现
+
+
+
+
+#### 5.3 关于优先级视图展示
+- 视频播放器为了拓展性,需要暴露view接口供外部开发者自定义视频播放器视图,通过addView的形式添加到播放器的控制器中。
+    - 这就涉及view视图的层级性。控制view视图的显示和隐藏是特别重要的,这个时候在自定义view中就需要拿到播放器的状态
+- 举一个简单的例子,基础视频播放器
+    - 添加了基础播放功能的几个播放视图。有播放完成,播放异常,播放加载,顶部标题栏,底部控制条栏,锁屏,以及手势滑动栏。如何控制它们的显示隐藏切换呢?
+    - 在addView这些视图时,大多数的view都是默认GONE隐藏的。比如当视频初始化时,先缓冲则显示缓冲view而隐藏其他视图,接着播放则显示顶部/底部视图而隐藏其他视图
+- 比如有时候需要显示两种不同的自定义视图如何处理
+    - 举个例子,播放的时候,点击一下视频,会显示顶部title视图和底部控制条视图,那么这样会同时显示两个视图。
+    - 点击顶部title视图的返回键可以关闭播放器,点击底部控制条视图的播放暂停可以控制播放条件。这个时候底部控制条视图FrameLayout的ChildView在整个视频的底部,顶部title视图FrameLayout的ChildView在整个视频的顶部,这样可以达到上下层都可以相应事件。
+- 那么FrameLayout层层重叠,如何让下层不响应事件
+    - 在最上方显示的层加上: android:clickable="true" 可以避免点击上层触发底层。或者直接给控制设置一个background颜色也可以。
+
+
+
+### 5.4 视频播放器重力感应监听
+- 区别视频几种不同的播放模式
+    - 正常播放时,设置检查系统是否开启自动旋转,打开监听
+    - 全屏模式播放视频的时候,强制监听设备方向
+    - 在小窗口模式播放视频的时候,取消重力感应监听
+    - 注意一点。关于是否开启自动旋转的重力感应监听,可以给外部开发者暴露一个方法设置的开关。让用户选择是否开启该功能
+- 具体怎么操作
+    - 写一个类,然后继承OrientationEventListener类,注意视频播放器重力感应监听不要那么频繁。表示500毫秒才检测一次……
+    - mOrientationHelper.enable();表示检查系统是否开启自动旋转。mOrientationHelper.disable();表示取消监听
+    - 具体可以看这篇博客:[06.播放器UI抽取封装](https://github.com/yangchong211/YCVideoPlayer/blob/master/read/06.%E6%92%AD%E6%94%BE%E5%99%A8UI%E6%8A%BD%E5%8F%96%E5%B0%81%E8%A3%85.md)
 
 
 
 ### 06.如何简单使用
-- 播放单个视频
+### 5.1 播放单个视频
+- 必须需要的四步骤代码如下所示
+    ``` java
+    //创建基础视频播放器,一般播放器的功能
+    BasisVideoController controller = new BasisVideoController(this);
+    //设置控制器
+    mVideoPlayer.setVideoController(controller);
+    //设置视频播放链接地址
+    mVideoPlayer.setUrl(url);
+    //开始播放
+    mVideoPlayer.start();
+    ```
+- 只需要四步操作即可,非常简单。这样就可以满足一个基础的视频播放器
+    - 具体逻辑可以看:BasisVideoController
+- 如何添加只定义视图,非常方便。AdControlView需要实现InterControlView接口才可以
+    ``` java
+    AdControlView adControlView = new AdControlView(this);
+    controller.addControlComponent(adControlView);
+    ```
+- 要是一个页面播放多个视频怎么办
+    - 直接创建两个VideoPlayer,实现代码和播放单个视频一样,只是需要注意:不要开启音频焦点监听。
+    - 如果是开启的音频焦点改变监听,那么播放该视频的时候,就会停止其他音视频的播放操作。类似,你听音乐,这个时候去看视频,那么音乐就暂停呢
 
 
-- 播放多个视频
+#### 5.2 列表播放视频
+- 关于列表播放视频,该案例支持
+    - 列表页面有多个item
+        - 第一种:点击item播放,当item滑动到不可见时暂停播放;点击其他可见item播放视频,则会暂停其他正在播放的视频,也就是说一次只能播放一个视频
+        - 第二种:滑动播放视频,
+    - 列表页面是一个页面一个item
+        - 第一种:
+- 如何保证在列表中只播放一个视频
+
 
 
 
-- 列表播放视频
 
 
 
@@ -108,12 +174,55 @@
 
 
 
+### 08.该案例的拓展性分享
+
+
+
+### 09.关于视频缓存方案
+- 网络上比较好的项目:https://github.com/danikula/AndroidVideoCache
+    - 网络用的HttpURLConnection,文件缓存处理,文件最大限度策略,回调监听处理,断点续传,代理服务等。
+- 但是存在一些问题,比如如下所示
+    - 文件的缓存超过限制后没有按照lru算法删除,
+    - 处理返回给播放器的http响应头消息,响应头消息的获取处理改为head请求(需服务器支持)
+    - 替换网络库为okHttp(因为大部分的项目都是以okHttp为网络请求库的),但是这个改动性比较大
+- 然后看一下怎么使用,超级简单。传入视频url链接,返回一个代理链接,然后就可以呢
+    ```
+    HttpProxyCacheServer server = new HttpProxyCacheServer(this);
+    String proxyVideoUrl = server.getProxyUrl(URL_AD);
+    ```
+- 大概的原理
+    - 原始的方式是直接塞播放地址给播放器,它就可以直接播放。现在我们要在中间加一层本地代理,播放器播放的时候(获取数据)是通过我们的本地代理的地址来播放的,这样我们就可以很好的在中间层(本地代理层)做一些处理,比如:文件缓存,预缓存(秒开处理),监控等。
+- 原理详细一点来说
+    - 1.采用了本地代理服务的方式,通过原始url给播放器返回一个本地代理的一个url ,代理URL类似:http://127.0.0.1:port/视频url;(port端口为系统随机分配的有效端口,真实url是为了真正的下载),然后播放器播放的时候请求到了你本地的代理上了。
+    - 2.本地代理采用ServerSocket监听127.0.0.1的有效端口,这个时候手机就是一个服务器了,客户端就是socket,也就是播放器。
+    - 3.读取客户端就是socket来读取数据(http协议请求)解析http协议。
+    - 4.根据url检查视频文件是否存在,读取文件数据给播放器,也就是往socket里写入数据(socket通信)。同时如果没有下载完成会进行断点下载,当然弱网的话数据需要生产消费同步处理。
+- 如何实现预加载
+    - 其实预加载的思路很简单,在进行一个播放视频后,再返回接下来需要预加载的视频url,启用线程去请求下载数据
+    - 开启一个线程去请求并预加载一部分的数据,可能需要预加载的数据大于>1,利用队列先进入的先进行加载,因此可以采用LinkedHashMap保存正在预加载的task。
+    - 在开始预加载的时候,判断该播放地址是否已经预加载,如果不是那么创建一个线程task,并且把它放到map集合中。然后执行预加载逻辑,也就是执行HttpURLConnection请求
+    - 提供取消对应url加载的任务,因为有可能该url不需要再进行预加载了,比如参考抖音,当用户瞬间下滑几个视频,那么很多视频就需要跳过了不需要再进行预加载。这个后期在做
+- 缓存满了该怎么处理
+
 
 
+### 10.如何监控视频埋点
 
 
+### 11.待实现的需求分析
 
 
+### 12.参考案例和博客记录
+- exo播放器
+    - https://github.com/google/ExoPlayer
+- ijk播放器
+    - https://github.com/bilibili/ijkplayer
+- 阿里云播放器
+    - https://help.aliyun.com/document_detail/51992.html?spm=a2c4g.11186623.2.24.37131bc7j1PoVK#topic2415
+- GSY播放器
+    - https://github.com/CarGuo/GSYVideoPlayer
+- 饺子播放器
+    - https://github.com/lipangit/JiaoZiVideoPlayer
 
 
 

+ 425 - 8
read/05.播放器内核切换封装.md

@@ -4,8 +4,10 @@
 - 02.播放器内核架构图
 - 03.如何兼容不同内核播放器
 - 04.看一下ijk的内核实现类
-- 05.如何创建不同内核播放器
-- 06.看一下工厂类实现代码
+- 05.看一下exo的内核实现类
+- 06.如何创建不同内核播放器
+- 07.看一下工厂类实现代码
+- 08.后期如何添加新的内核
 
 
 
@@ -35,7 +37,7 @@
 
 
 ### 04.看一下ijk的内核实现类
-- 代码如下所示
+- ijk的内核实现类代码如下所示
     ```java
     public class IjkVideoPlayer extends AbstractVideoPlayer {
     
@@ -430,7 +432,406 @@
     ```
 
 
-### 05.如何创建不同内核播放器
+### 05.看一下exo的内核实现类
+- exo的内核实现类代码如下所示,和ijk的api有些区别
+    ```java
+    public class ExoMediaPlayer extends AbstractVideoPlayer implements VideoListener, Player.EventListener {
+    
+        protected Context mAppContext;
+        protected SimpleExoPlayer mInternalPlayer;
+        protected MediaSource mMediaSource;
+        protected ExoMediaSourceHelper mMediaSourceHelper;
+        private PlaybackParameters mSpeedPlaybackParameters;
+        private int mLastReportedPlaybackState = Player.STATE_IDLE;
+        private boolean mLastReportedPlayWhenReady = false;
+        private boolean mIsPreparing;
+        private boolean mIsBuffering;
+    
+        private LoadControl mLoadControl;
+        private RenderersFactory mRenderersFactory;
+        private TrackSelector mTrackSelector;
+    
+        public ExoMediaPlayer(Context context) {
+            if (context instanceof Application){
+                mAppContext = context;
+            } else {
+                mAppContext = context.getApplicationContext();
+            }
+            mMediaSourceHelper = ExoMediaSourceHelper.getInstance(context);
+        }
+    
+        @Override
+        public void initPlayer() {
+            //创建exo播放器
+            mInternalPlayer = new SimpleExoPlayer.Builder(
+                    mAppContext,
+                    mRenderersFactory == null ? mRenderersFactory = new DefaultRenderersFactory(mAppContext) : mRenderersFactory,
+                    mTrackSelector == null ? mTrackSelector = new DefaultTrackSelector(mAppContext) : mTrackSelector,
+                    mLoadControl == null ? mLoadControl = new DefaultLoadControl() : mLoadControl,
+                    DefaultBandwidthMeter.getSingletonInstance(mAppContext),
+                    Util.getLooper(),
+                    new AnalyticsCollector(Clock.DEFAULT),
+                    /* useLazyPreparation= */ true,
+                    Clock.DEFAULT)
+                    .build();
+            setOptions();
+    
+            //播放器日志
+            if (VideoLogUtils.isIsLog() && mTrackSelector instanceof MappingTrackSelector) {
+                mInternalPlayer.addAnalyticsListener(new EventLogger((MappingTrackSelector) mTrackSelector, "ExoPlayer"));
+            }
+            initListener();
+        }
+    
+        /**
+         * exo视频播放器监听listener
+         */
+        private void initListener() {
+            mInternalPlayer.addListener(this);
+            mInternalPlayer.addVideoListener(this);
+        }
+    
+        public void setTrackSelector(TrackSelector trackSelector) {
+            mTrackSelector = trackSelector;
+        }
+    
+        public void setRenderersFactory(RenderersFactory renderersFactory) {
+            mRenderersFactory = renderersFactory;
+        }
+    
+        public void setLoadControl(LoadControl loadControl) {
+            mLoadControl = loadControl;
+        }
+    
+        /**
+         * 设置播放地址
+         *
+         * @param path    播放地址
+         * @param headers 播放地址请求头
+         */
+        @Override
+        public void setDataSource(String path, Map<String, String> headers) {
+            // 设置dataSource
+            if(path==null || path.length()==0){
+                if (mPlayerEventListener!=null){
+                    mPlayerEventListener.onInfo(PlayerConstant.MEDIA_INFO_URL_NULL, 0);
+                }
+                return;
+            }
+            mMediaSource = mMediaSourceHelper.getMediaSource(path, headers);
+        }
+    
+        @Override
+        public void setDataSource(AssetFileDescriptor fd) {
+            //no support
+        }
+    
+        /**
+         * 准备开始播放(异步)
+         */
+        @Override
+        public void prepareAsync() {
+            if (mInternalPlayer == null){
+                return;
+            }
+            if (mMediaSource == null){
+                return;
+            }
+            if (mSpeedPlaybackParameters != null) {
+                mInternalPlayer.setPlaybackParameters(mSpeedPlaybackParameters);
+            }
+            mIsPreparing = true;
+            mMediaSource.addEventListener(new Handler(), mMediaSourceEventListener);
+            //准备播放
+            mInternalPlayer.prepare(mMediaSource);
+        }
+    
+        /**
+         * 播放
+         */
+        @Override
+        public void start() {
+            if (mInternalPlayer == null){
+                return;
+            }
+            mInternalPlayer.setPlayWhenReady(true);
+        }
+    
+        /**
+         * 暂停
+         */
+        @Override
+        public void pause() {
+            if (mInternalPlayer == null){
+                return;
+            }
+            mInternalPlayer.setPlayWhenReady(false);
+        }
+    
+        /**
+         * 停止
+         */
+        @Override
+        public void stop() {
+            if (mInternalPlayer == null){
+                return;
+            }
+            mInternalPlayer.stop();
+        }
+    
+        private MediaSourceEventListener mMediaSourceEventListener = new MediaSourceEventListener() {
+            @Override
+            public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
+                if (mPlayerEventListener != null && mIsPreparing) {
+                    mPlayerEventListener.onPrepared();
+                }
+            }
+        };
+    
+        /**
+         * 重置播放器
+         */
+        @Override
+        public void reset() {
+            if (mInternalPlayer != null) {
+                mInternalPlayer.stop(true);
+                mInternalPlayer.setVideoSurface(null);
+                mIsPreparing = false;
+                mIsBuffering = false;
+                mLastReportedPlaybackState = Player.STATE_IDLE;
+                mLastReportedPlayWhenReady = false;
+            }
+        }
+    
+        /**
+         * 是否正在播放
+         */
+        @Override
+        public boolean isPlaying() {
+            if (mInternalPlayer == null){
+                return false;
+            }
+            int state = mInternalPlayer.getPlaybackState();
+            switch (state) {
+                case Player.STATE_BUFFERING:
+                case Player.STATE_READY:
+                    return mInternalPlayer.getPlayWhenReady();
+                case Player.STATE_IDLE:
+                case Player.STATE_ENDED:
+                default:
+                    return false;
+            }
+        }
+    
+        /**
+         * 调整进度
+         */
+        @Override
+        public void seekTo(long time) {
+            if (mInternalPlayer == null){
+                return;
+            }
+            mInternalPlayer.seekTo(time);
+        }
+    
+        /**
+         * 释放播放器
+         */
+        @Override
+        public void release() {
+            if (mInternalPlayer != null) {
+                mInternalPlayer.removeListener(this);
+                mInternalPlayer.removeVideoListener(this);
+                final SimpleExoPlayer player = mInternalPlayer;
+                mInternalPlayer = null;
+                new Thread() {
+                    @Override
+                    public void run() {
+                        //异步释放,防止卡顿
+                        player.release();
+                    }
+                }.start();
+            }
+    
+            mIsPreparing = false;
+            mIsBuffering = false;
+            mLastReportedPlaybackState = Player.STATE_IDLE;
+            mLastReportedPlayWhenReady = false;
+            mSpeedPlaybackParameters = null;
+        }
+    
+        /**
+         * 获取当前播放的位置
+         */
+        @Override
+        public long getCurrentPosition() {
+            if (mInternalPlayer == null){
+                return 0;
+            }
+            return mInternalPlayer.getCurrentPosition();
+        }
+    
+        /**
+         * 获取视频总时长
+         */
+        @Override
+        public long getDuration() {
+            if (mInternalPlayer == null){
+                return 0;
+            }
+            return mInternalPlayer.getDuration();
+        }
+    
+        /**
+         * 获取缓冲百分比
+         */
+        @Override
+        public int getBufferedPercentage() {
+            return mInternalPlayer == null ? 0 : mInternalPlayer.getBufferedPercentage();
+        }
+    
+        /**
+         * 设置渲染视频的View,主要用于SurfaceView
+         */
+        @Override
+        public void setSurface(Surface surface) {
+            if (mInternalPlayer != null) {
+                mInternalPlayer.setVideoSurface(surface);
+            }
+        }
+    
+        @Override
+        public void setDisplay(SurfaceHolder holder) {
+            if (holder == null){
+                setSurface(null);
+            } else{
+                setSurface(holder.getSurface());
+            }
+        }
+    
+        /**
+         * 设置音量
+         */
+        @Override
+        public void setVolume(float leftVolume, float rightVolume) {
+            if (mInternalPlayer != null){
+                mInternalPlayer.setVolume((leftVolume + rightVolume) / 2);
+            }
+        }
+    
+        /**
+         * 设置是否循环播放
+         */
+        @Override
+        public void setLooping(boolean isLooping) {
+            if (mInternalPlayer != null){
+                mInternalPlayer.setRepeatMode(isLooping ? Player.REPEAT_MODE_ALL : Player.REPEAT_MODE_OFF);
+            }
+        }
+    
+        @Override
+        public void setOptions() {
+            //准备好就开始播放
+            mInternalPlayer.setPlayWhenReady(true);
+        }
+    
+        /**
+         * 设置播放速度
+         */
+        @Override
+        public void setSpeed(float speed) {
+            PlaybackParameters playbackParameters = new PlaybackParameters(speed);
+            mSpeedPlaybackParameters = playbackParameters;
+            if (mInternalPlayer != null) {
+                mInternalPlayer.setPlaybackParameters(playbackParameters);
+            }
+        }
+    
+        /**
+         * 获取播放速度
+         */
+        @Override
+        public float getSpeed() {
+            if (mSpeedPlaybackParameters != null) {
+                return mSpeedPlaybackParameters.speed;
+            }
+            return 1f;
+        }
+    
+        /**
+         * 获取当前缓冲的网速
+         */
+        @Override
+        public long getTcpSpeed() {
+            // no support
+            return 0;
+        }
+    
+        @Override
+        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+            if (mPlayerEventListener == null){
+                return;
+            }
+            if (mIsPreparing){
+                return;
+            }
+            if (mLastReportedPlayWhenReady != playWhenReady || mLastReportedPlaybackState != playbackState) {
+                switch (playbackState) {
+                    //最开始调用的状态
+                    case Player.STATE_IDLE:
+                        break;
+                    //开始缓充
+                    case Player.STATE_BUFFERING:
+                        mPlayerEventListener.onInfo(PlayerConstant.MEDIA_INFO_BUFFERING_START, getBufferedPercentage());
+                        mIsBuffering = true;
+                        break;
+                    //开始播放
+                    case Player.STATE_READY:
+                        if (mIsBuffering) {
+                            mPlayerEventListener.onInfo(PlayerConstant.MEDIA_INFO_BUFFERING_END, getBufferedPercentage());
+                            mIsBuffering = false;
+                        }
+                        break;
+                    //播放器已经播放完了媒体
+                    case Player.STATE_ENDED:
+                        mPlayerEventListener.onCompletion();
+                        break;
+                    default:
+                        break;
+                }
+                mLastReportedPlaybackState = playbackState;
+                mLastReportedPlayWhenReady = playWhenReady;
+            }
+        }
+    
+        @Override
+        public void onPlayerError(ExoPlaybackException error) {
+            if (mPlayerEventListener != null) {
+                mPlayerEventListener.onError();
+            }
+        }
+    
+        @Override
+        public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
+            if (mPlayerEventListener != null) {
+                mPlayerEventListener.onVideoSizeChanged(width, height);
+                if (unappliedRotationDegrees > 0) {
+                    mPlayerEventListener.onInfo(PlayerConstant.MEDIA_INFO_VIDEO_ROTATION_CHANGED, unappliedRotationDegrees);
+                }
+            }
+        }
+    
+        @Override
+        public void onRenderedFirstFrame() {
+            if (mPlayerEventListener != null && mIsPreparing) {
+                mPlayerEventListener.onInfo(PlayerConstant.MEDIA_INFO_VIDEO_RENDERING_START, 0);
+                mIsPreparing = false;
+            }
+        }
+    }
+    ```
+
+### 06.如何创建不同内核播放器
 - 先来看一下创建不同内核播放器的代码,只需要开发者传入一个类型参数,即可创建不同类的实例对象。代码如下所示
     ```java
     /**
@@ -468,7 +869,7 @@
     - 简而言之,创建对象的时候只需要传递类型type,而不需要对应的工厂,即可创建具体的产品对象
 
 
-### 06.看一下工厂类实现代码
+### 07.看一下工厂类实现代码
 - 抽象工厂类,代码如下所示
     ```java
     public abstract class PlayerFactory<T extends AbstractVideoPlayer> {
@@ -495,9 +896,25 @@
 
 
 
-
-
-
+### 08.后期如何添加新的内核
+- 比如后期想要添加一个腾讯视频内核的播放器。代码如下所示,这个是简化的
+    ```java
+    public class TxPlayerFactory extends PlayerFactory<TxMediaPlayer> {
+    
+        public static TxPlayerFactory create() {
+            return new TxPlayerFactory();
+        }
+    
+        @Override
+        public TxMediaPlayer createPlayer(Context context) {
+            return new TxMediaPlayer(context);
+        }
+    }
+    
+    public class TxMediaPlayer extends AbstractVideoPlayer {
+        //省略接口的实现方法代码
+    }
+    ```
 
 
 

+ 127 - 8
read/06.播放器UI抽取封装.md

@@ -7,7 +7,8 @@
 - 05.VideoController实现
 - 06.播放Player和UI通信
 - 07.如何添加自定义播放视图
-
+- 08.关于播放器视图层级
+- 09.视频播放器重力感应监听
 
 
 ### 01.视频播放器UI封装需求
@@ -53,6 +54,7 @@
     - 在BaseVideoController中的状态监听中,通过InterControlView接口对象就可以把播放器的状态传递到子类中
 
 
+
 ### 04.VideoPlayer如何实现
 
 
@@ -154,15 +156,132 @@
     ```
 
 
+### 08.关于播放器视图层级
+- 视频播放器为了拓展性,需要暴露view接口供外部开发者自定义视频播放器视图,通过addView的形式添加到播放器的控制器中。
+    - 这就涉及view视图的层级性。控制view视图的显示和隐藏是特别重要的,这个时候在自定义view中就需要拿到播放器的状态
+- 举一个简单的例子,基础视频播放器
+    - 添加了基础播放功能的几个播放视图。有播放完成,播放异常,播放加载,顶部标题栏,底部控制条栏,锁屏,以及手势滑动栏。如何控制它们的显示隐藏切换呢?
+    - 在addView这些视图时,大多数的view都是默认GONE隐藏的。比如当视频初始化时,先缓冲则显示缓冲view而隐藏其他视图,接着播放则显示顶部/底部视图而隐藏其他视图
+- 比如有时候需要显示两种不同的自定义视图如何处理
+    - 举个例子,播放的时候,点击一下视频,会显示顶部title视图和底部控制条视图,那么这样会同时显示两个视图。
+    - 点击顶部title视图的返回键可以关闭播放器,点击底部控制条视图的播放暂停可以控制播放条件。这个时候底部控制条视图FrameLayout的ChildView在整个视频的底部,顶部title视图FrameLayout的ChildView在整个视频的顶部,这样可以达到上下层都可以相应事件。
+- 那么FrameLayout层层重叠,如何让下层不响应事件
+    - 在最上方显示的层加上: android:clickable="true" 可以避免点击上层触发底层。或者直接给控制设置一个background颜色也可以。
+- 比如基础播放器的视图层级是这样的
+    ``` java
+    //添加自动完成播放界面view
+    CustomCompleteView completeView = new CustomCompleteView(mContext);
+    completeView.setVisibility(GONE);
+    this.addControlComponent(completeView);
+    
+    //添加错误界面view
+    CustomErrorView errorView = new CustomErrorView(mContext);
+    errorView.setVisibility(GONE);
+    this.addControlComponent(errorView);
+    
+    //添加与加载视图界面view,准备播放界面
+    CustomPrepareView prepareView = new CustomPrepareView(mContext);
+    thumb = prepareView.getThumb();
+    prepareView.setClickStart();
+    this.addControlComponent(prepareView);
+    
+    //添加标题栏
+    titleView = new CustomTitleView(mContext);
+    titleView.setTitle(title);
+    titleView.setVisibility(VISIBLE);
+    this.addControlComponent(titleView);
+    
+    if (isLive) {
+        //添加底部播放控制条
+        CustomLiveControlView liveControlView = new CustomLiveControlView(mContext);
+        this.addControlComponent(liveControlView);
+    } else {
+        //添加底部播放控制条
+        CustomBottomView vodControlView = new CustomBottomView(mContext);
+        //是否显示底部进度条。默认显示
+        vodControlView.showBottomProgress(true);
+        this.addControlComponent(vodControlView);
+    }
+    //添加滑动控制视图
+    CustomGestureView gestureControlView = new CustomGestureView(mContext);
+    this.addControlComponent(gestureControlView);
+    ```
 
 
-
-
-
-
-
-
-
+### 09.视频播放器重力感应监听
+- 区别视频几种不同的播放模式
+    - 正常播放时,设置检查系统是否开启自动旋转,打开监听
+    - 全屏模式播放视频的时候,强制监听设备方向
+    - 在小窗口模式播放视频的时候,取消重力感应监听
+    - 注意一点。关于是否开启自动旋转的重力感应监听,可以给外部开发者暴露一个方法设置的开关。让用户选择是否开启该功能
+- 首先写一个类,然后继承OrientationEventListener类,注意视频播放器重力感应监听不要那么频繁。表示500毫秒才检测一次……
+    ``` java
+    public class OrientationHelper extends OrientationEventListener {
+    
+        private long mLastTime;
+    
+        private OnOrientationChangeListener mOnOrientationChangeListener;
+    
+        public OrientationHelper(Context context) {
+            super(context);
+        }
+    
+        @Override
+        public void onOrientationChanged(int orientation) {
+            long currentTime = System.currentTimeMillis();
+            if (currentTime - mLastTime < 500) {
+                return;
+            }
+            //500毫秒检测一次
+            if (mOnOrientationChangeListener != null) {
+                mOnOrientationChangeListener.onOrientationChanged(orientation);
+            }
+            mLastTime = currentTime;
+        }
+    
+    
+        public interface OnOrientationChangeListener {
+            void onOrientationChanged(int orientation);
+        }
+    
+        public void setOnOrientationChangeListener(OnOrientationChangeListener onOrientationChangeListener) {
+            mOnOrientationChangeListener = onOrientationChangeListener;
+        }
+    }
+    ```
+- 关于播放器播放模式状态发生变化时,需要更新是开启重力感应监听,还是关闭重力感应监听。代码如下所示 
+    ``` java
+    /**
+     * 子类重写此方法并在其中更新控制器在不同播放器状态下的ui
+     * 普通模式,小窗口模式,正常模式三种其中一种
+     * MODE_NORMAL              普通模式
+     * MODE_FULL_SCREEN         全屏模式
+     * MODE_TINY_WINDOW         小屏模式
+     */
+    @CallSuper
+    protected void onPlayerStateChanged(@ConstantKeys.PlayMode int playerState) {
+        switch (playerState) {
+            case ConstantKeys.PlayMode.MODE_NORMAL:
+                //视频正常播放是设置监听
+                if (mEnableOrientation) {
+                    //检查系统是否开启自动旋转
+                    mOrientationHelper.enable();
+                } else {
+                    //取消监听
+                    mOrientationHelper.disable();
+                }
+                break;
+            case ConstantKeys.PlayMode.MODE_FULL_SCREEN:
+                //在全屏时强制监听设备方向
+                mOrientationHelper.enable();
+                break;
+            case ConstantKeys.PlayMode.MODE_TINY_WINDOW:
+                //小窗口取消重力感应监听
+                mOrientationHelper.disable();
+                break;
+        }
+    }
+    ```
 
 
 

+ 10 - 1
read/12.视频边播边缓存分析.md

@@ -4,6 +4,7 @@
 - 02.致敬优秀开源项目
 - 03.边播边缓存原理
 - 04.如何实现预加载
+- 06.缓存满了怎么处理
 - 14.视频缓冲后拖动问题
 
 
@@ -48,6 +49,11 @@
 ### 03.边播边缓存原理
 - 大概的原理
     - 原始的方式是直接塞播放地址给播放器,它就可以直接播放。现在我们要在中间加一层本地代理,播放器播放的时候(获取数据)是通过我们的本地代理的地址来播放的,这样我们就可以很好的在中间层(本地代理层)做一些处理,比如:文件缓存,预缓存(秒开处理),监控等。
+- 原理详细一点来说
+    - 1.采用了本地代理服务的方式,通过原始url给播放器返回一个本地代理的一个url ,代理URL类似:http://127.0.0.1:port/视频url;(port端口为系统随机分配的有效端口,真实url是为了真正的下载),然后播放器播放的时候请求到了你本地的代理上了。
+    - 2.本地代理采用ServerSocket监听127.0.0.1的有效端口,这个时候手机就是一个服务器了,客户端就是socket,也就是播放器。
+    - 3.读取客户端就是socket来读取数据(http协议请求)解析http协议。
+    - 4.根据url检查视频文件是否存在,读取文件数据给播放器,也就是往socket里写入数据(socket通信)。同时如果没有下载完成会进行断点下载,当然弱网的话数据需要生产消费同步处理。
 
 
 
@@ -58,10 +64,13 @@
     - 提供取消对应url加载的任务,因为有可能该url不需要再进行预加载了,比如参考抖音,当用户瞬间下滑几个视频,那么很多视频就需要跳过了不需要再进行预加载
 
 
+
+### 06.缓存满了怎么处理
+
+
 ### 14.视频缓冲后拖动问题
 - 问题描述
     - 在播放视频的时候,你看到缓冲buffer缓冲到了5分钟,但这个时候你把网络一关闭。然后拖动到3分钟,发现缓冲的视频有时候无法播放,这个是为什么呢?
-    - 
 
 
 

+ 10 - 0
read/30.参考项目和博客说明.md

@@ -5,6 +5,16 @@
 
 
 
+- exo播放器
+    - https://github.com/google/ExoPlayer
+- ijk播放器
+    - https://github.com/bilibili/ijkplayer
+- 阿里云播放器
+    - https://help.aliyun.com/document_detail/51992.html?spm=a2c4g.11186623.2.24.37131bc7j1PoVK#topic2415
+- GSY播放器
+    - https://github.com/CarGuo/GSYVideoPlayer
+- 饺子播放器
+    - https://github.com/lipangit/JiaoZiVideoPlayer