# 小程序直播间 ## 基础架构和功能优化 ###### 小溪里 ###### 2020-07-08
## 知识点 * 小程序业务介绍 * `socket` 基础知识 * `socket-service` 的连接机制 * 断网重连的bug * 播放器的设计 * 消息监听
## 小程序功能简介
### 我的课程列表 * 账号登录 * 购买后的课程列表 ###### ![](https://image-hosting.xiaoxili.com/img/20200713100311.png)
### 网师主页 内嵌 web 端网师主页 需要隐藏 Web端价格标签,如价格、优惠活动等 ###### ![](https://image-hosting.xiaoxili.com/img/20200713100337.png)
### 课时学习页 包含 Web 端的群 intro 页和 三合一页面主体功能 * 视频播放、课时切换 * 群模块:网师、评价、推荐 ###### ![](https://image-hosting.xiaoxili.com/img/20200713100353.png)
### 直播间 * 在线直播播放 * 聊天消息列表 ###### ![](https://image-hosting.xiaoxili.com/img/20200713100408.gif)
# Socket连接及
Socket-service 服务
### Socket 连接原理 * HTTP 连接:API 请求 * 长轮询:订单状态查询 * WebSocket 连接:聊天室 ###### ![](https://image-hosting.xiaoxili.com/img/20200713100456.png)
### 主要连接顺序 1. 创建 `socket` 连接,以 `wss` 协议 2. 验证 `service-token` 信息 3. 请求指令、回应指令 * 获取上麦人数、是否禁言、在线人数等 * 监听多种消息类型,如普通消息、上麦、送鲜花等 ###### ![](https://image-hosting.xiaoxili.com/img/20200713100516.png)
### 连接库 基于 `socket.io` 实现,有 `socket.io-server` 服务端和 `socket.io-client` 客户端版本 Web端使用 `socket.io-client` 1.4.6 版 小程序端用的 `wxapp-socket-io` 1.0.0 版,特点为编译后未压缩版本大小为 60 k,使用单socket连接,其核心逻辑为 `engine.js` 中有 `wx.connectSocket` 和 `wx.closeSocket` 的单任务连接机制
#### 连接库的疑问 为何不使用 `wxapp-socket-io` 的最新版呢? 最新版实现的原理是基于`socket.io-client` 的 2.3.6版本,将编译后未压缩版本大小为300k左右,且在分析namespace时会多一个 “,”, ###### ![](https://image-hosting.xiaoxili.com/img/20200713100545.png) ```javascript [3] // 错误 /group123456, /group123456,,invalid namespace ```
### 连接初始代码 1、io 连接 `wss.cctalk.com:8000/group123456` 其中 `wss.cctalk.com:8000` 为socket连接地址,`group123456` 为命名空间(群聊房间) ```javascript // 调用时的配置 const socket = new Socket({ store, userId: userInfo.userId, actions: liveActions, videoId: videoInfo.videoId, groupId: videoInfo.groupId }) ```
2、获取 `service-token` ,校验token,此时才会认为 `socket` 登录成功 ```javascript const data = await this.actions.user.getSocketToken() // 开始验证 token,当后续验证 token 通过时,才算登录成功 this.sendCMD(__code.SCMD_LOGIN_VERIFY_TOKEN_REQ, { content: data.token })  ``` ```json { "blackReason": "", "nickName": "xiaoxili22", "releaseTime": 0, "token": "6680462445615xxxxxx", "userId": 65875530, "userName": "xiaoxili22" } ``` > 直播间socket连接的机制,命名空间、token校验、多次重连机制、10秒心跳重连
### 断网重连bug bug表现:在用户断网后再次重连后,用户会被从参与列表中自动移除
#### 怀疑表现1 微信开发者工具没有问题,但小程序上就有问题 CCtalk web端直播间使用的每隔10秒请求一次群成员数量以此来判断是否连接成功 沪江网校的方案为每隔10秒会关闭Socket连接,再重新连接一次 **但经过多次尝试,依然无效** > 客户端断开用户连接的时间为2分钟,2分钟内重连成功后不会将用户从用户列表移除。
#### 怀疑表现2 但通过测试,iPhone手机的 Safari 浏览器也有类似的bug,怀疑为 Server 问题,(电脑端没有这个问题) 表现为: * 直接断开网络后,参与成员在80秒左右会自动离开 * 25秒断开网络后,参与成员在80秒左右会自动离开
Server同学在解决问题后,给出一下分析: * 存在多个wss的情况下,会出现不能保证顺序执行用户离开再进入,会变成先进入再离开,所以断线后就连不上了。 * 对单次登录做了唯一uuid记录,离开的时候需要匹配uuid,否则直接不处理此次离开(认为非法) * 这样先收到进入(新的uuid),再收到离开(老的uuid)。因为老的uuid跟新的uuid不匹配,所以做丢弃处理
## 播放器
### 视频 RTMP 格式 **小程序端** `live-player` 的 src 支持 rtmp 格式, 首次使用 `videoInfo` 里面的 rtmp 地址,但也支持通过 `Socket` 指令来更换 rtmp 地址 **Web端** HLS的 m3u8 方式,简单来说就是每次传递视频的一小块。
### 同层渲染 LivePlayer 在小程序基础库v2.9.1 起支持同层渲染。 > CCtalk 课程小程序的最低基础库限制为 V2.8.0 但想覆盖住全屏后的 `LivePlayer`,就需要使用 `CoverView` ###### ![](https://image-hosting.xiaoxili.com/img/20200713100608.png)
## 状态监听
### 退出直播间 * 进入页面,从直播被变为回顾,自动返回课程学习页,并刷新视频播放区 * 老师主动结束直播,播放区提示 * 移除群成员,弹窗提示 | 老师主动结束 | 移除群成员 | | --- | --- | | ![](https://image-hosting.xiaoxili.com/img/20200713100844.png) | ![](https://image-hosting.xiaoxili.com/img/20200713101002.png) |
### 自动播放 * Wifi 自动开始播放 * 4G 提示消耗流量,再同意后才进行播放 ###### ![](https://image-hosting.xiaoxili.com/img/20200713101058.png)
#### Wifi下自动播放 ```javascript [7-13] onFirstPlay = () => { const { playStatus } = this.state const { networkInfo } = this.props const { networkType } = networkInfo if (playStatus === PLAY_STATUS.INIT) { if (networkType === 'wifi') { this.onLivePlay() } else { this.setState({ playStatus: PLAY_STATUS.LOADED }) } } } ```
#### 4G 提示流量消耗 ```javascript [7-10] onToPlayBtn = () => { ... code if (!this.toPlayed && networkType !== 'wifi') { this.toPlayed = true Taro.showModal({ title: '提示', content: '当前没有WI-FI,直播将消耗流量', cancelText: '退出直播', confirmText: '继续', success: (res) => { if (res.confirm) { this.onLivePlay() } else if (res.cancel) { this.onLivePause() } } }) return } this.onLivePlay() } ```
## 消息发送
### 消息合并动图 | app | 小程序 | | --- | --- | | ![](https://image-hosting.xiaoxili.com/img/20200713101753.gif) | ![](https://image-hosting.xiaoxili.com/img/20200713101854.gif) |
### 消息合并原理 > * 移动端app和pc客户端都需要将本地消息与网络消息进行合并 > * 小程序直播间:只有网络消息,只记录从进入、离开聊天室这一段时间 原理:在最新的消息基础上增加一条时间类型消息 * 第一条 * 最新消息与次新消息间隔5分钟
### 新消息提醒 | 自动滚动到底部 | 新消息提示 | | --- | --- | | ![](https://image-hosting.xiaoxili.com/img/20200713101142.gif) | ![](https://image-hosting.xiaoxili.com/img/20200713101214.gif) |
#### 业务需求 * 新消息来时,自动定位到最下方 * 用户输入文字时,自动定位到最下方 * 若用户主动滚动聊天列表区域,则会显示“新消息”提示,点击后会自动滚动到最底部 **即核心为判断聊天消息区域是否滚动到底部。**
#### Web端实现方案 通过监听 `Scroll` 事件获取Dom来模拟计算出是否到达底部 ```js [10-16] isScrollToBottom = () => { const scrollWrapper = this.scrollRef const sHeight = scrollWrapper.scrollHeight const sTop = scrollWrapper.scrollTop const refHeight = parseInt(getStyle(scrollWrapper, 'height'), 10) return sTop + refHeight >= sHeight - 20 } onScrollHandler = () => { const bubble = this.bubbleRef // 是否滚动到最底部 this.isAutoToBottom = this.isScrollToBottom() bubble.style.display = 'none' } ```
#### 小程序端方案一:监听 onScroll 事件 ```js [4-8] onScroll = () => { this.isAutoToBottom = false } onScrollToLower = () => { this.isAutoToBottom = true this.setState({ isShowBubble: false }) } ``` 存在的问题:`onScrollToLower` 方法默认距离底部 `50px` 触发,但之后 `onScroll` 依旧会触发,也就是说无法实现判断滚动到底部
那为何不像Web端那样监听 DOM高度来计算是否滚动到底部呢?因为小程序上获取 DOM 的Top的方法是异步的,在 `onScroll` 事件内频繁触发,很耗费性能。 ```js [5-9] // 获取dom的top值 export function getDomRect(nodeRef) { return new Promise(resolve => { if (nodeRef) { nodeRef.boundingClientRect(res => { if (res) { resolve(res) } }).exec() } }) } ```
### 小程序端方案二:监听高 `1px`的元素是否在视图在内 ###### ![](https://image-hosting.xiaoxili.com/img/20200713101214.gif)
### 自动滚动到最底部的实现方法
#### 方案一 scrollTop `scrollTop` 的方案可以继续用,但需要每次更换 `scrollTop` 才能触发 state 更新 ```js this.toBottomT = setTimeout(() => this.setState({ listScrollTop: 9999999 + parseInt(Math.random() * 10000)}), 500) ```
#### 方案二 `scrollToView` 快速跳转位置 > `scroll-into-view` 的优先级高于 `scroll-top` ```js [6, 9-12] toBottomHandler = () => { this.isAutoToBottom = true // 滚动到底部 this.setState({ inView: 'bottom-line', isShowBubble: false, }, () => { // 滚动完成后,需要清空 inView 的值,这样才可以在下次更新 inView 值触发 state 更新 this.setState({ inView: '', }) }) } ```
### 输入框与输入法遮挡的高度 ###### ![](https://image-hosting.xiaoxili.com/img/20200713101414.gif)
#### 方案一:上推页面 `Input` 的 `adjust-position` 属性支持在输入法弹出时,自动将页面上推 存在的问题:导致整个页面上推,即使是 `postion: fixed` 的直播播放区 ###### ![](https://image-hosting.xiaoxili.com/img/20200713101524.gif)
#### 方案二:聊天列表区域收缩 CCtalk APP在输入法弹出时会改变聊天消息区域高度 ###### ![](https://image-hosting.xiaoxili.com/img/20200713101614.gif) 影响了对聊天列表区域高度,从而对相应的监听产生影响
#### 方案三:分离式 具体来说:小程序端采取输入框跟随键盘位置,但聊天消息区域高度不变 并加入了css延迟动画,以模拟贴合输入法高度变化的情况。 与CCtalk app上的交互类似,当input 聚焦及全屏操作时,认为离开了聊天消息区域,此时需要将聊天消息区域自动滚动到最底部。 ###### ![](https://image-hosting.xiaoxili.com/img/20200713101711.gif)
### 输入框跟随输入法键盘 | app | 小程序 | | --- | --- | | CCtalk APP在输入法弹出时会改变聊天消息区域高度 | 小程序端采取输入框跟随键盘位置,但聊天消息区域高度不变,并加入了css延迟动画,以模拟贴合输入法高度变化的情况 | | ![](https://image-hosting.xiaoxili.com/img/20200713104857.gif) | ![](https://image-hosting.xiaoxili.com/img/20200713104927.gif) |
### 大小表情 表情map表,在直播间、三合一回顾中出现 `bhzh` 对应的是 `https://cc.hjfile.cn/cc/face/pc/big_hh/1.gif` ```json { "bhzh": "pc/big_hh/1" } ``` 小表情支持图文混合,大表情为单独的一条 #### ![](https://image-hosting.xiaoxili.com/img/20200713101642.gif)
# 谢谢 ##### QA时间