# 小程序直播间
## 基础架构和功能优化
###### 小溪里
###### 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)