@laplace.live/ws 6.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/package.json +57 -0
- package/src/browser.ts +20 -0
- package/src/buffer.ts +82 -0
- package/src/common.ts +216 -0
- package/src/extra.ts +33 -0
- package/src/index.test.ts +191 -0
- package/src/index.ts +35 -0
- package/src/inflate/brotli.ts +2285 -0
- package/src/inflate/browser.ts +9 -0
- package/src/inflate/node.ts +11 -0
- package/src/tcp.ts +48 -0
- package/src/ws.ts +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 simon3000
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# bilibili-live-ws [](https://www.npmjs.com/package/bilibili-live-ws) 
|
|
2
|
+
|
|
3
|
+
Bilibili 直播 WebSocket/TCP API
|
|
4
|
+
|
|
5
|
+
LiveWS/KeepLiveWS 支持浏览器 *(实验性)*
|
|
6
|
+
|
|
7
|
+
应该支持 bilibili直播开放平台 <https://open-live.bilibili.com>
|
|
8
|
+
|
|
9
|
+
注:如果在浏览器环境遇到问题,可以尝试手动指定引入 `bilibili-live-ws/browser`
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const { LiveWS, LiveTCP, KeepLiveWS, KeepLiveTCP } = require('bilibili-live-ws')
|
|
15
|
+
const live = new LiveWS(roomid)
|
|
16
|
+
// 或
|
|
17
|
+
const live = new LiveTCP(roomid)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
举个栗子:
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
const live = new LiveWS(14275133)
|
|
24
|
+
|
|
25
|
+
live.on('open', () => console.log('Connection is established'))
|
|
26
|
+
// Connection is established
|
|
27
|
+
live.on('live', () => {
|
|
28
|
+
live.on('heartbeat', console.log)
|
|
29
|
+
// 74185
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
或者用TCP (新功能):
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
const live = new LiveTCP(26283)
|
|
37
|
+
|
|
38
|
+
live.on('open', () => console.log('Connection is established'))
|
|
39
|
+
// Connection is established
|
|
40
|
+
live.on('live', () => {
|
|
41
|
+
live.on('heartbeat', console.log)
|
|
42
|
+
// 13928
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> 晚上做梦梦到一个白胡子的老爷爷和我说TCP更节约内存哦!
|
|
47
|
+
|
|
48
|
+
## Class: LiveWS / LiveTCP / KeepLiveWS / KeepLiveTCP
|
|
49
|
+
|
|
50
|
+
(Keep)LiveWS 和 (Keep)LiveTCP 的大部分API基本上一样, 区别在本文末尾有注明
|
|
51
|
+
|
|
52
|
+
### new LiveWS(roomid [, { address, protover, key, authBody, uid, buvid }])
|
|
53
|
+
|
|
54
|
+
### new LiveTCP(roomid [, { host, port, protover, key, authBody, uid, buvid }])
|
|
55
|
+
|
|
56
|
+
- `roomid` 房间号
|
|
57
|
+
|
|
58
|
+
比如 https://live.bilibili.com/14327465 中的 `14327465`
|
|
59
|
+
|
|
60
|
+
- `address` 可选, WebSocket连接的地址
|
|
61
|
+
|
|
62
|
+
默认 `'wss://broadcastlv.chat.bilibili.com/sub'`
|
|
63
|
+
|
|
64
|
+
- `host` 可选, TCP连接的地址
|
|
65
|
+
|
|
66
|
+
默认 `'broadcastlv.chat.bilibili.com'`
|
|
67
|
+
|
|
68
|
+
- `port` 可选, TCP连接的端口
|
|
69
|
+
|
|
70
|
+
默认 `2243`
|
|
71
|
+
|
|
72
|
+
- `protover` 可选 (1, 2, 3)
|
|
73
|
+
|
|
74
|
+
默认 `2`
|
|
75
|
+
|
|
76
|
+
* 1 (见 [#17](https://github.com/simon300000/bilibili-live-ws/issues/17))
|
|
77
|
+
* 2 (zlib.inflate)
|
|
78
|
+
* 3 (brotliDecompress)
|
|
79
|
+
|
|
80
|
+
- `uid` 可选, WS握手的 uid [#397](https://github.com/simon300000/bilibili-live-ws/issues/397)
|
|
81
|
+
|
|
82
|
+
- `key` 可选, WS握手的 Token [#397](https://github.com/simon300000/bilibili-live-ws/issues/397)
|
|
83
|
+
|
|
84
|
+
- `buvid` 可选, WS握手的 Token [#397](https://github.com/simon300000/bilibili-live-ws/issues/397)
|
|
85
|
+
|
|
86
|
+
- `authBody` 可选, 可以和 <https://open-live.bilibili.com/document/> 配合使用, 会覆盖掉 `protover` `roomid` `key` `uid` `buvid`. 如果是 `object` 会按照握手包编码,如果是 `string`/`Buffer` 会直接发送
|
|
87
|
+
|
|
88
|
+
#### live.on('open')
|
|
89
|
+
|
|
90
|
+
WebSocket连接上了
|
|
91
|
+
|
|
92
|
+
#### live.on('live')
|
|
93
|
+
|
|
94
|
+
成功登入房间
|
|
95
|
+
|
|
96
|
+
#### live.on('heartbeat', (online) => ...)
|
|
97
|
+
|
|
98
|
+
收到服务器心跳包,会在30秒之后自动发送心跳包。
|
|
99
|
+
|
|
100
|
+
- `online` 当前人气值
|
|
101
|
+
|
|
102
|
+
#### live.on('msg', (data) => ...)
|
|
103
|
+
|
|
104
|
+
- `data` 收到信息/弹幕/广播等
|
|
105
|
+
|
|
106
|
+
data的例子: (我simon3000送了一个辣条)
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
{
|
|
110
|
+
cmd: 'SEND_GIFT',
|
|
111
|
+
data: {
|
|
112
|
+
giftName: '辣条',
|
|
113
|
+
num: 1,
|
|
114
|
+
uname: 'simon3000',
|
|
115
|
+
face: 'http://i1.hdslb.com/bfs/face/c26b9f670b10599ad105e2a7fea4b5f21c0f0bcf.jpg',
|
|
116
|
+
guard_level: 0,
|
|
117
|
+
rcost: 2318827,
|
|
118
|
+
uid: 3499295,
|
|
119
|
+
top_list: [],
|
|
120
|
+
timestamp: 1555690631,
|
|
121
|
+
giftId: 1,
|
|
122
|
+
giftType: 0,
|
|
123
|
+
action: '喂食',
|
|
124
|
+
super: 0,
|
|
125
|
+
super_gift_num: 0,
|
|
126
|
+
price: 100,
|
|
127
|
+
rnd: '1555690616',
|
|
128
|
+
newMedal: 0,
|
|
129
|
+
newTitle: 0,
|
|
130
|
+
medal: [],
|
|
131
|
+
title: '',
|
|
132
|
+
beatId: '0',
|
|
133
|
+
biz_source: 'live',
|
|
134
|
+
metadata: '',
|
|
135
|
+
remain: 6,
|
|
136
|
+
gold: 0,
|
|
137
|
+
silver: 0,
|
|
138
|
+
eventScore: 0,
|
|
139
|
+
eventNum: 0,
|
|
140
|
+
smalltv_msg: [],
|
|
141
|
+
specialGift: null,
|
|
142
|
+
notice_msg: [],
|
|
143
|
+
capsule: null,
|
|
144
|
+
addFollow: 0,
|
|
145
|
+
effect_block: 1,
|
|
146
|
+
coin_type: 'silver',
|
|
147
|
+
total_coin: 100,
|
|
148
|
+
effect: 0,
|
|
149
|
+
tag_image: '',
|
|
150
|
+
user_count: 0
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### live.on(cmd, (data) => ...)
|
|
156
|
+
|
|
157
|
+
用法如上,只不过只会收到特定cmd的Event。
|
|
158
|
+
|
|
159
|
+
比如 `live.on('DANMU_MSG')` 只会收到弹幕Event,一个弹幕Event的Data例子如下: (我simon3000发了个`233`)
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
{
|
|
163
|
+
cmd: 'DANMU_MSG',
|
|
164
|
+
info: [
|
|
165
|
+
[0, 1, 25, 16777215, 1555692037, 1555690616, 0, 'c5c630b1', 0, 0, 0],
|
|
166
|
+
'233',
|
|
167
|
+
[3499295, 'simon3000', 0, 0, 0, 10000, 1, ''],
|
|
168
|
+
[5, '財布', '神楽めあOfficial', 12235923, 5805790, ''],
|
|
169
|
+
[11, 0, 6406234, '>50000'],
|
|
170
|
+
['title-58-1', 'title-58-1'],
|
|
171
|
+
0,
|
|
172
|
+
0,
|
|
173
|
+
null,
|
|
174
|
+
{ ts: 1555692037, ct: '2277D56A' }
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
#### live.on('close')
|
|
182
|
+
|
|
183
|
+
连接关闭事件
|
|
184
|
+
|
|
185
|
+
#### live.on('error', (e) => ...)
|
|
186
|
+
|
|
187
|
+
连接 error 事件,连接同时也会关闭
|
|
188
|
+
|
|
189
|
+
#### live.heartbeat()
|
|
190
|
+
|
|
191
|
+
无视30秒时间,直接发送一个心跳包。
|
|
192
|
+
|
|
193
|
+
如果连接正常,会收到来自服务器的心跳包,也就是 `live.on('heartbeat')`,可以用于更新人气值。
|
|
194
|
+
|
|
195
|
+
#### live.close()
|
|
196
|
+
|
|
197
|
+
关闭WebSocket/TCP Socket连接。
|
|
198
|
+
|
|
199
|
+
#### live.getOnline()
|
|
200
|
+
|
|
201
|
+
立即调用 `live.heartbeat()` 刷新人气数值,并且返回 Promise,resolve 人气刷新后数值
|
|
202
|
+
|
|
203
|
+
#### live.on('message')
|
|
204
|
+
|
|
205
|
+
WebSocket/TCP收到的Raw Buffer(不推荐直接使用)
|
|
206
|
+
|
|
207
|
+
#### live.send(buffer)
|
|
208
|
+
|
|
209
|
+
使用 WebSocket 或者 TCP 发送信息
|
|
210
|
+
|
|
211
|
+
### getConf(roomid)
|
|
212
|
+
|
|
213
|
+
选一个cdn,Resolve host, address 和 key, 可以直接放进(Keep)LiveWS/TCP设置
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const { getConf } = require('bilibili-live-ws')
|
|
217
|
+
|
|
218
|
+
getConf(roomid)
|
|
219
|
+
// Return
|
|
220
|
+
Promise<{
|
|
221
|
+
key: string;
|
|
222
|
+
host: string;
|
|
223
|
+
address: string;
|
|
224
|
+
}>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### getRoomid(short)
|
|
228
|
+
|
|
229
|
+
通过短房间号获取长房间号
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const { getRoomid } = require('bilibili-live-ws')
|
|
233
|
+
|
|
234
|
+
getRoomid(255)
|
|
235
|
+
// Return Promise<number>: 48743
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
<hr>
|
|
239
|
+
|
|
240
|
+
### Keep和无Keep的区别
|
|
241
|
+
|
|
242
|
+
KeepLiveWS 和 KeepLiveTCP 有断线重新连接的功能
|
|
243
|
+
|
|
244
|
+
#### new KeepLiveWS(roomid [, { address, protover, key }])
|
|
245
|
+
|
|
246
|
+
#### new KeepLiveTCP(roomid [, { host, port, protover, key }])
|
|
247
|
+
|
|
248
|
+
所有上方的API都是一样的, 只不过会有以下微小的区别
|
|
249
|
+
|
|
250
|
+
* 收到error或者close事件的时候并不代表连接关闭, 必须要手动调用`live.close()`来关闭连接
|
|
251
|
+
* 内部的连接对象(`LiveWS`/`LiveTCP`)在`live.connection`中
|
|
252
|
+
* 内部的连接关闭了之后, 如果不是因为`live.close()`关闭, 会在100毫秒之后自动打开一个新的连接。
|
|
253
|
+
* 这个100毫秒可以通过改变`live.interval`来设置
|
|
254
|
+
* 内部的boolean, `live.closed`, 用于判断是否是因为`live.close()`关闭。
|
|
255
|
+
|
|
256
|
+
<hr>
|
|
257
|
+
|
|
258
|
+
### LiveWS 和 LiveTCP 的区别
|
|
259
|
+
|
|
260
|
+
#### LiveWS.ws
|
|
261
|
+
|
|
262
|
+
LiveWS 内部 WebSocket 对象,需要时可以直接操作
|
|
263
|
+
|
|
264
|
+
Doc: <https://github.com/websockets/ws/blob/master/doc/ws.md#class-websocket>
|
|
265
|
+
|
|
266
|
+
#### LiveTCP.socket
|
|
267
|
+
|
|
268
|
+
LiveTCP 内部 TCP Socket 对象,需要时可以直接操作
|
|
269
|
+
|
|
270
|
+
Doc: <https://nodejs.org/api/net.html#net_class_net_socket>
|
|
271
|
+
|
|
272
|
+
#### LiveTCP.buffer
|
|
273
|
+
|
|
274
|
+
LiveTCP内部TCP流的Buffer缓冲区,有不完整的包
|
|
275
|
+
|
|
276
|
+
#### LiveTCP.splitBuffer()
|
|
277
|
+
|
|
278
|
+
处理LiveTCP内部TCP流的Buffer缓冲区,把其中完整的包交给decoder处理
|
|
279
|
+
|
|
280
|
+
### BenchMark 简单对比
|
|
281
|
+
|
|
282
|
+
在连接了640个直播间后过了一分钟左右(@2.0.0)
|
|
283
|
+
|
|
284
|
+
| | LiveWS (wss) | LiveWS (ws) | LiveTCP |
|
|
285
|
+
| -------- | ------------ | ----------- | ------- |
|
|
286
|
+
| 内存占用 | 63 MB | 26 MB | 18 MB |
|
|
287
|
+
|
|
288
|
+
<hr>
|
|
289
|
+
|
|
290
|
+
参考资料: <https://github.com/lovelyyoshino/Bilibili-Live-API/blob/master/API.WebSocket.md>
|
|
291
|
+
|
|
292
|
+
# 贡献
|
|
293
|
+
|
|
294
|
+
欢迎Issue和Pull Request!
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@laplace.live/ws",
|
|
3
|
+
"version": "6.3.1",
|
|
4
|
+
"description": "Bilibili Live WebSocket/TCP API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"browser": "./src/browser.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"browser": "./src/browser.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./browser": "./src/browser.ts"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"check": "tsc --noEmit",
|
|
17
|
+
"test": "bun test --timeout 5000"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/laplace-live/ws.git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"bilibili",
|
|
25
|
+
"api",
|
|
26
|
+
"websocket",
|
|
27
|
+
"live",
|
|
28
|
+
"ws",
|
|
29
|
+
"tcp"
|
|
30
|
+
],
|
|
31
|
+
"author": "simon3000 <simon3000@163.com>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/laplace-live/ws/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/laplace-live/ws#readme",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"provenance": true
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"isomorphic-ws": "^5.0.0",
|
|
43
|
+
"ws": "^8.19.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@biomejs/biome": "^2.4.6",
|
|
47
|
+
"@types/bun": "^1.3.10",
|
|
48
|
+
"@types/pako": "^2.0.4",
|
|
49
|
+
"@types/ws": "^8.18.1",
|
|
50
|
+
"typescript": "^5.9.3"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"buffer": "^6.0.3",
|
|
54
|
+
"events": "^3.3.0",
|
|
55
|
+
"pako": "^2.1.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { KeepLive } from './common'
|
|
2
|
+
import { inflates } from './inflate/browser'
|
|
3
|
+
import { LiveWSBase, type WSOptions } from './ws'
|
|
4
|
+
|
|
5
|
+
export type { LiveOptions } from './common'
|
|
6
|
+
export type { WSOptions } from './ws'
|
|
7
|
+
|
|
8
|
+
export { relayEvent } from './common'
|
|
9
|
+
|
|
10
|
+
export class LiveWS extends LiveWSBase {
|
|
11
|
+
constructor(roomid: number, opts?: WSOptions) {
|
|
12
|
+
super(inflates, roomid, opts)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class KeepLiveWS extends KeepLive<typeof LiveWSBase> {
|
|
17
|
+
constructor(roomid: number, opts?: WSOptions) {
|
|
18
|
+
super(LiveWSBase, inflates, roomid, opts)
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/buffer.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type { Buffer } from 'buffer'
|
|
2
|
+
export type Inflates = { inflateAsync: (b: Buffer) => Buffer | Promise<Buffer>, brotliDecompressAsync: (b: Buffer) => Buffer | Promise<Buffer>, Buffer: typeof Buffer }
|
|
3
|
+
|
|
4
|
+
// https://github.com/lovelyyoshino/Bilibili-Live-API/blob/master/API.WebSocket.md
|
|
5
|
+
|
|
6
|
+
const cutBuffer = (buffer: Buffer) => {
|
|
7
|
+
const bufferPacks: Buffer[] = []
|
|
8
|
+
let size: number
|
|
9
|
+
for (let i = 0; i < buffer.length; i += size) {
|
|
10
|
+
size = buffer.readInt32BE(i)
|
|
11
|
+
bufferPacks.push(buffer.slice(i, i + size))
|
|
12
|
+
}
|
|
13
|
+
return bufferPacks
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const makeDecoder = ({ inflateAsync, brotliDecompressAsync }: Inflates) => {
|
|
17
|
+
const decoder = async (buffer: Buffer) => {
|
|
18
|
+
const packs = await Promise.all(cutBuffer(buffer)
|
|
19
|
+
.map(async buf => {
|
|
20
|
+
const body = buf.slice(16)
|
|
21
|
+
const protocol = buf.readInt16BE(6)
|
|
22
|
+
const operation = buf.readInt32BE(8)
|
|
23
|
+
|
|
24
|
+
let type = 'unknow'
|
|
25
|
+
if (operation === 3) {
|
|
26
|
+
type = 'heartbeat'
|
|
27
|
+
} else if (operation === 5) {
|
|
28
|
+
type = 'message'
|
|
29
|
+
} else if (operation === 8) {
|
|
30
|
+
type = 'welcome'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let data: any
|
|
34
|
+
if (protocol === 0) {
|
|
35
|
+
data = JSON.parse(String(body))
|
|
36
|
+
}
|
|
37
|
+
if (protocol === 1 && body.length === 4) {
|
|
38
|
+
data = body.readUIntBE(0, 4)
|
|
39
|
+
}
|
|
40
|
+
if (protocol === 2) {
|
|
41
|
+
data = await decoder(await inflateAsync(body))
|
|
42
|
+
}
|
|
43
|
+
if (protocol === 3) {
|
|
44
|
+
data = await decoder(await brotliDecompressAsync(body))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { buf, type, protocol, data }
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
return packs.flatMap(pack => {
|
|
51
|
+
if (pack.protocol === 2 || pack.protocol === 3) {
|
|
52
|
+
return pack.data as typeof packs
|
|
53
|
+
}
|
|
54
|
+
return pack
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return decoder
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type EncodeType = 'heartbeat' | 'join'
|
|
62
|
+
|
|
63
|
+
export const encoder = (type: EncodeType, { Buffer }: Inflates, body: any = '') => {
|
|
64
|
+
const blank = Buffer.alloc(16)
|
|
65
|
+
if (typeof body !== 'string') {
|
|
66
|
+
body = JSON.stringify(body)
|
|
67
|
+
}
|
|
68
|
+
const head = Buffer.from(blank)
|
|
69
|
+
const buffer = Buffer.from(body)
|
|
70
|
+
|
|
71
|
+
head.writeInt32BE(buffer.length + head.length, 0)
|
|
72
|
+
head.writeInt16BE(16, 4)
|
|
73
|
+
head.writeInt16BE(1, 6)
|
|
74
|
+
if (type === 'heartbeat') {
|
|
75
|
+
head.writeInt32BE(2, 8)
|
|
76
|
+
}
|
|
77
|
+
if (type === 'join') {
|
|
78
|
+
head.writeInt32BE(7, 8)
|
|
79
|
+
}
|
|
80
|
+
head.writeInt32BE(1, 12)
|
|
81
|
+
return Buffer.concat([head, buffer])
|
|
82
|
+
}
|
package/src/common.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
|
|
3
|
+
import { encoder, type Inflates, makeDecoder } from './buffer'
|
|
4
|
+
|
|
5
|
+
export type LiveOptions = { protover?: 1 | 2 | 3; key?: string; authBody?: any; uid?: number; buvid?: string }
|
|
6
|
+
|
|
7
|
+
export const relayEvent = Symbol('relay')
|
|
8
|
+
|
|
9
|
+
class NiceEventEmitter extends EventEmitter {
|
|
10
|
+
emit(eventName: string | symbol, ...params: any[]) {
|
|
11
|
+
super.emit(eventName, ...params)
|
|
12
|
+
super.emit(relayEvent, eventName, ...params)
|
|
13
|
+
return true
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Live extends NiceEventEmitter {
|
|
18
|
+
roomid: number
|
|
19
|
+
online: number
|
|
20
|
+
live: boolean
|
|
21
|
+
closed: boolean
|
|
22
|
+
timeout: ReturnType<typeof setTimeout>
|
|
23
|
+
|
|
24
|
+
inflates: Inflates
|
|
25
|
+
|
|
26
|
+
send: (data: Buffer) => void
|
|
27
|
+
close: () => void
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
inflates: Inflates,
|
|
31
|
+
roomid: number,
|
|
32
|
+
{
|
|
33
|
+
send,
|
|
34
|
+
close,
|
|
35
|
+
protover = 3,
|
|
36
|
+
key,
|
|
37
|
+
authBody,
|
|
38
|
+
uid = 0,
|
|
39
|
+
buvid,
|
|
40
|
+
}: { send: (data: Buffer) => void; close: () => void } & LiveOptions
|
|
41
|
+
) {
|
|
42
|
+
if (typeof roomid !== 'number' || Number.isNaN(roomid)) {
|
|
43
|
+
throw new Error(`roomid ${roomid} must be Number not NaN`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
super()
|
|
47
|
+
this.inflates = inflates
|
|
48
|
+
this.roomid = roomid
|
|
49
|
+
this.online = 0
|
|
50
|
+
this.live = false
|
|
51
|
+
this.closed = false
|
|
52
|
+
this.timeout = setTimeout(() => {}, 0)
|
|
53
|
+
|
|
54
|
+
this.send = send
|
|
55
|
+
this.close = () => {
|
|
56
|
+
this.closed = true
|
|
57
|
+
close()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.on('message', async buffer => {
|
|
61
|
+
const packs = await makeDecoder(inflates)(buffer)
|
|
62
|
+
packs.forEach(({ type, data }) => {
|
|
63
|
+
if (type === 'welcome') {
|
|
64
|
+
this.live = true
|
|
65
|
+
this.emit('live')
|
|
66
|
+
this.send(encoder('heartbeat', inflates))
|
|
67
|
+
}
|
|
68
|
+
if (type === 'heartbeat') {
|
|
69
|
+
this.online = data
|
|
70
|
+
clearTimeout(this.timeout)
|
|
71
|
+
this.timeout = setTimeout(() => this.heartbeat(), 1000 * 30)
|
|
72
|
+
this.emit('heartbeat', this.online)
|
|
73
|
+
}
|
|
74
|
+
if (type === 'message') {
|
|
75
|
+
this.emit('msg', data)
|
|
76
|
+
const cmd = data.cmd || (data.msg && data.msg.cmd)
|
|
77
|
+
if (cmd) {
|
|
78
|
+
if (cmd.includes('DANMU_MSG')) {
|
|
79
|
+
this.emit('DANMU_MSG', data)
|
|
80
|
+
} else {
|
|
81
|
+
this.emit(cmd, data)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
this.on('open', () => {
|
|
89
|
+
if (authBody) {
|
|
90
|
+
if (typeof authBody === 'object') {
|
|
91
|
+
authBody = encoder('join', inflates, authBody)
|
|
92
|
+
}
|
|
93
|
+
this.send(authBody)
|
|
94
|
+
} else {
|
|
95
|
+
const hi: {
|
|
96
|
+
uid: number
|
|
97
|
+
roomid: number
|
|
98
|
+
protover: number
|
|
99
|
+
platform: string
|
|
100
|
+
type: number
|
|
101
|
+
key?: string
|
|
102
|
+
buvid?: string
|
|
103
|
+
} = { uid: uid, roomid, protover, platform: 'web', type: 2 }
|
|
104
|
+
if (key) {
|
|
105
|
+
hi.key = key
|
|
106
|
+
}
|
|
107
|
+
if (buvid) {
|
|
108
|
+
hi.buvid = buvid
|
|
109
|
+
}
|
|
110
|
+
const buf = encoder('join', inflates, hi)
|
|
111
|
+
this.send(buf)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
this.on('close', () => {
|
|
116
|
+
clearTimeout(this.timeout)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
this.on('_error', error => {
|
|
120
|
+
this.close()
|
|
121
|
+
this.emit('error', error)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
heartbeat() {
|
|
126
|
+
this.send(encoder('heartbeat', this.inflates))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getOnline() {
|
|
130
|
+
this.heartbeat()
|
|
131
|
+
return new Promise<number>(resolve => this.once('heartbeat', resolve))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class KeepLive<Base extends typeof Live> extends EventEmitter {
|
|
136
|
+
params: ConstructorParameters<Base>
|
|
137
|
+
closed: boolean
|
|
138
|
+
interval: number
|
|
139
|
+
timeout: number
|
|
140
|
+
connection: InstanceType<Base>
|
|
141
|
+
Base: Base
|
|
142
|
+
|
|
143
|
+
constructor(Base: Base, ...params: ConstructorParameters<Base>) {
|
|
144
|
+
super()
|
|
145
|
+
this.params = params
|
|
146
|
+
this.closed = false
|
|
147
|
+
this.interval = 100
|
|
148
|
+
this.timeout = 45 * 1000
|
|
149
|
+
this.connection = new (Base as any)(...this.params)
|
|
150
|
+
this.Base = Base
|
|
151
|
+
this.connect(false)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
connect(reconnect = true) {
|
|
155
|
+
if (reconnect) {
|
|
156
|
+
this.connection.close()
|
|
157
|
+
this.connection = new (this.Base as any)(...this.params)
|
|
158
|
+
}
|
|
159
|
+
const connection = this.connection
|
|
160
|
+
|
|
161
|
+
let timeout = setTimeout(() => {
|
|
162
|
+
connection.close()
|
|
163
|
+
connection.emit('timeout')
|
|
164
|
+
}, this.timeout)
|
|
165
|
+
|
|
166
|
+
connection.on(relayEvent, (eventName: string, ...params: any[]) => {
|
|
167
|
+
if (eventName !== 'error') {
|
|
168
|
+
this.emit(eventName, ...params)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
connection.on('error', e => this.emit('e', e))
|
|
173
|
+
connection.on('close', () => {
|
|
174
|
+
if (!this.closed) {
|
|
175
|
+
setTimeout(() => this.connect(), this.interval)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
connection.on('heartbeat', () => {
|
|
180
|
+
clearTimeout(timeout)
|
|
181
|
+
timeout = setTimeout(() => {
|
|
182
|
+
connection.close()
|
|
183
|
+
connection.emit('timeout')
|
|
184
|
+
}, this.timeout)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
connection.on('close', () => {
|
|
188
|
+
clearTimeout(timeout)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get online() {
|
|
193
|
+
return this.connection.online
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get roomid() {
|
|
197
|
+
return this.connection.roomid
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
close() {
|
|
201
|
+
this.closed = true
|
|
202
|
+
this.connection.close()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
heartbeat() {
|
|
206
|
+
return this.connection.heartbeat()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getOnline() {
|
|
210
|
+
return this.connection.getOnline()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
send(data: Buffer) {
|
|
214
|
+
return this.connection.send(data)
|
|
215
|
+
}
|
|
216
|
+
}
|