├── .gitignore ├── LICENSE ├── PS4broadcast now.drawio.png ├── README.md ├── coverUU.py ├── douyu ├── commander │ ├── CHANGELOG.md │ ├── LICENSE │ ├── Readme.md │ ├── index.js │ └── package.json ├── douyudm │ ├── LICENSE │ ├── README.md │ ├── build │ │ ├── config │ │ │ ├── dev.js │ │ │ ├── index.js │ │ │ └── prod.js │ │ ├── index.js │ │ └── package.json │ ├── dist │ │ ├── douyudanmaku.js │ │ ├── douyudanmaku.js.map │ │ └── douyudanmaku.min.js │ ├── example │ │ ├── README.md │ │ ├── cli.js │ │ ├── stt.js │ │ └── web │ │ │ ├── gift.json │ │ │ ├── host.json │ │ │ └── index.html │ ├── package.json │ ├── src │ │ ├── client.js │ │ ├── clientEvent.js │ │ ├── cmd.js │ │ ├── config.js │ │ ├── index.js │ │ ├── logger.js │ │ ├── loggerNode.js │ │ ├── messageEvent.js │ │ ├── packet.js │ │ ├── stt.js │ │ ├── util.js │ │ └── websocket.js │ └── test │ │ ├── old │ │ └── BufferCoder.js │ │ ├── packet.test.js │ │ └── stt.test.js ├── fast-text-encoding │ ├── .travis.yml │ ├── LICENSE │ ├── README.md │ ├── compile.sh │ ├── externs.js │ ├── package.json │ ├── text.js │ ├── text.min.js │ └── text.min.js.map └── ws │ ├── LICENSE │ ├── README.md │ ├── browser.js │ ├── index.js │ ├── lib │ ├── buffer-util.js │ ├── constants.js │ ├── event-target.js │ ├── extension.js │ ├── limiter.js │ ├── permessage-deflate.js │ ├── receiver.js │ ├── sender.js │ ├── stream.js │ ├── validation.js │ ├── websocket-server.js │ └── websocket.js │ └── package.json ├── extends ├── bootstrap.min.css ├── bootstrap.min.js ├── jquery-3.2.1.min.js └── pst.js ├── help.png ├── index.classic.html ├── index.html ├── index.html.save ├── install.sh ├── modules ├── README.md ├── _realroll.js ├── room_bilibili.js └── room_douyu.js ├── package-lock.json ├── package.json ├── start-web.sh ├── start.js └── version /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | *.data 5 | log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | EIMIT License 2 | 3 | Copyright (c) 2021 Tilerphy 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 | 1. Use the code without any CPU made by © Intel Corporation. 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /PS4broadcast now.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilerphy/ps4broadcast/6c22756eff912d6f08b2b1f59d404d959fe4ef73/PS4broadcast now.drawio.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://img.shields.io/gitter/room/tilerphy/ps4broadcast.svg)](https://gitter.im/ghost-studio/ps4broadcast) 2 | # 增加对openwrt中uu插件的支持。 3 | 4 | 环境: 5 | 6 | 运行ps4broadcast的机器, 7 | 8 | 一个openwrt路由器运行了uu插件, 9 | 10 | ps4\ps5 的网关设置为openwrt,要求openwrt路由器能通过ip访问到ps4broadcast,方式不限。 11 | 12 | 13 | 设置方法: 14 | 1. 为openwrt安装python3 15 | 3. 将coverUU.py 复制到openwrt的某一个路径下,我们假设为/app/coverUU.py 16 | 4. 修改coverUU.py中的192.168.8.11到你运行ps4broadcast的机器的ip 17 | 5. 修改openwrt的/etc/rc.local ,在exit 0之前增加一行语句: /usr/bin/python /app/coverUU.py & 18 | 6. 别省略上一行的 & 19 | 7. 重启openwrt 20 | 8. 这之后,就可以在uu同时使用ps4broadcast进行直播了。 21 | 22 | 目前的流程如下: 23 | ![](https://raw.githubusercontent.com/Tilerphy/ps4broadcast/master/PS4broadcast%20now.drawio.png) 24 | 25 | # 我们是索狗qq群 26 | 27 | 如果希望讨论游戏,我们有一个qq群: 855457801 28 | 当然,不论xbox、ps、switch、pc或者其他游戏平台,都可以进群聊啊,游戏才是我们共同的、永久的话题。 29 | 30 | # 确切测试 31 | PS5、XBOX Series X依然支持rtmp方式直播,所以ps4broadcast也支持PS5和XBOX Series X(XBOX无弹幕支持)。 32 | 33 | PS5上使用ps4broadcast的效果非常好,自带录像功能也支持了1080p@60fps,并且PS5与PS4的弹幕重定向功能是相同协议,所以弹幕也支持。 34 | 35 | 36 | # 前言 37 | 鉴于PS4暂时没有直播到国内直播平台的功能,所以用了一些手段拦截twitch的RTMP内容与IRC聊天室内容,并且转发到斗鱼(将来可能会增加其他的国内直播平台。) 38 | 另外,这只是个折中手段,没准过几天PS4官方支持国内的直播平台推流了,起码国行机器是有这个可能的,这个谁说的准呢?所以这个方案只是个临时的,没有办法的办法。 39 | 40 | # 为什么开源 41 | 分享,是网络的基础。 42 | 43 | # 为什么是Linux? 44 | 1. Windows操作系统做网络转发太不直接了,而且要形成脚本还挺不好写。而且作者只会Linux方式的iptables配置。 45 | 46 | 2. 再有,作者自己用的是树莓派3(类似单片机的超小型电脑),所以Windows也不好用。 47 | 48 | 3. 再再有,如果有大拿愿意移植到openwrt,让路由器实现这个功能,用我这个方案也是可以的。 49 | 50 | 51 | # 需要会什么 52 | 53 | 一些Linux的基础知识,一些shell脚本以及目录知识。 54 | 55 | 如果不会这些,请直接使用镜像教程: https://www.psnine.com/topic/32642 56 | 57 | 最新的镜像release: https://github.com/Tilerphy/ps4broadcast/releases 58 | 59 | 如果Twitch绑定不上需要科学上网,请去qq群383701265找我,我能提供一个临时的代理。 60 | 61 | # 准备工作 62 | 63 | Debian 8 或者 Raspbian(树莓派操作系统,debian的ARM编译)的电脑或者树莓派或者虚拟机,这台电脑的ip假设是`192.168.0.8` 64 | 一台其他的电脑或者虚拟机的运行机器 65 | 66 | Nodejs 6+ 建议直接到Nodejs v8.7.0 67 | 68 | # 局域网真实IP,网卡设备号 69 | 70 | 执行ifconfig获取到当前的网卡地址,一般是eth0也有一些是eth1,或者enp0s3什么的, 类似 71 | ``` 72 | eth0: flags=4163 mtu 1500 73 | inet 192.168.0.8 netmask 255.255.0.0 broadcast 192.168.255.255 74 | inet6 fe80::2e0:66ff:fee7:e31b prefixlen 64 scopeid 0x20 75 | ether 00:e0:86:f7:e3:1b txqueuelen 1000 (Ethernet) 76 | RX packets 20558634 bytes 16007006847 (14.9 GiB) 77 | RX errors 0 dropped 0 overruns 0 frame 0 78 | TX packets 17867778 bytes 15731185654 (14.6 GiB) 79 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 80 | 81 | ``` 82 | `eth0`就是网卡的设备号 83 | `192.168.0.8` 就是这台机器的真实IP 84 | 85 | # 着手准备 86 | 1. 执行`apt-get install -y git sudo tar libnet-ifconfig-wrapper-perl xz-utils`安装工具,sudo的用处是以管理员身份来执行命令。 87 | 2. 为了方便起见在`/`目录下创建一个目录 `mkdir /ps4broadcast` 88 | 3. 进入这个目录`cd /ps4broadcast` 89 | 4. `git clone https://github.com/Tilerphy/ps4broadcast.git` 90 | 5. 下载最新的nodejs源代码,并且编译安装 91 | 92 | ``` 93 | wget https://nodejs.org/dist/v8.7.0/node-v8.7.0.tar.gz 94 | tar -xvf node-v8.7.0.tar.gz 95 | cd node-v8.7.0 96 | sudo ./configure 97 | sudo make && sudo make install 98 | ``` 99 | 100 | 执行 `node -v` 得到的结果应该是`v8.7.0`或者其他版本号。 101 | 102 | # 准备完成,检查 103 | 此时目录结构应该是 104 | ``` 105 | /ps4broadcast 106 | /ps4broadcast/node-v8.7.0 107 | /ps4broadcast/ps4broadcast 108 | ``` 109 | 110 | # 继续 111 | 更改脚本的执行权限 112 | ``` 113 | cd /ps4broadcast/ps4broadcast 114 | sudo chmod 777 install.sh 115 | sudo chmod 777 start-web.sh 116 | 117 | ``` 118 | 119 | 执行安装脚本 120 | ``` 121 | sudo ./install.sh 122 | sudo npm install 123 | ``` 124 | 125 | # 开始直播? 126 | 127 | 0. Twitch 128 | 129 | 在什么都没做的情况下,在PS4上开一个游戏,按手柄的share键,选择“播放游玩画面”,选择twitch,如果之前没有进行过twitch直播,此时会要求你注册一个twitch账号。请根据提示一步步注册一个账号,并且`记住你的twitchid` 130 | 131 | 1. 斗鱼 132 | 133 | 首先打开要去直播的网站,目前只有斗鱼,进入自己的直播间,在自己的直播屏幕上方有一个“直播开关”,点一下on。 134 | 然后界面会刷新,然后再自己的直播间屏幕左上方会找到一个“直播码”的按钮,按一下,会弹出一个框。 135 | 136 | 其中rtmp地址我们称之为 `url` 137 | 138 | 直播码我们称之为 `code` 139 | 140 | 2. 我们的服务器 141 | 142 | 这里需要用到设备号,假设之前获取到的设备号是`eth0` 143 | 144 | ``` 145 | cd /ps4broadcast/ps4broadcast 146 | 147 | sudo ./start-web.sh eth0 148 | 149 | ``` 150 | 151 | 3. Control Panel 152 | 153 | 打开http://192.168.0.8:26666/ 154 | 155 | 填入`Twitch ID`(Twitch的登录名),与 `Douyu Room Id`(斗鱼直播间号) 156 | 157 | 将斗鱼那边得到的 `url` 和 `code` 填入后,点击`reset live` 158 | 159 | 如果得到`LIVING STATUS: true`说明一切运行正常(更新的版本显示的是`n channels.`) 160 | 161 | ![](https://github.com/Tilerphy/ps4broadcast/blob/master/help.png) 162 | 163 | 4. PS4 164 | 165 | 设置->网络->LAN或者无线->自定 166 | 167 | 按照这样设置: 168 | ``` 169 | IP: 192.168.200.45 170 | 掩码(Netmask): 255.255.255.0 171 | 网关(Gateway): 192.168.200.1 172 | Primary DNS: 114.114.114.114 173 | ``` 174 | 然后一路下一步。 175 | 176 | 找到个游戏,按手柄的share键,选择“播放游玩画面”,选择twitch,一路下一步。 177 | 178 | 如果斗鱼那头没有效果,可能是斗鱼的直播码过期,毕竟只有5分钟有效期,过期不用就作废,所以请重新申请`url`和`code`,填入Control Panel,点击`reset live`,然后重新再PS4上开始直播。 179 | 180 | 181 | --- 182 | # 特别鸣谢 183 | 184 | ### PSNID: jwq-1416 感谢其对Bilibili弹幕功能测试做出的杰出贡献。 185 | 186 | --- 187 | 188 | # 欢迎发pull request 189 | # 跪求大神移植到openwrt 190 | -------------------------------------------------------------------------------- /coverUU.py: -------------------------------------------------------------------------------- 1 | # 在openwrt中使用/etc/rc.local 设置跟随系统启动本脚本。 2 | # 建议不要使用22版本的openwrt,官方删除了iptables命令,因此uu也无法使用。 我们测试是在openwrt 21 中。 3 | import os 4 | import time 5 | while True: 6 | # 程序启动后每两秒进行一次检查 7 | time.sleep(2) 8 | # 首先把iptables的nat规则导出到一个文件中 9 | os.system("iptables -t nat -L > latest_iptables") 10 | # 读取iptables内容 11 | with open("latest_iptables", "r") as rules: 12 | lines = rules.readlines() 13 | # 我们规定iptables的第三行是 1935 端口的重定向 (rtmp端口) 14 | firstRule = lines[2].strip() 15 | # 我们规定iptables的第四行是 6667 端口的重定向 (twitch 弹幕端口) 16 | secondRule = lines[3].strip() 17 | # 一旦有任意规则不符合,则删掉我们自己定义的规则,并重新添加到第三行第四行。 18 | if not firstRule.endswith("1935") or not secondRule.endswith("6667"): 19 | os.system("iptables -t nat -D PREROUTING -p tcp --dport 1935 -j DNAT --to-destination 192.168.8.11:1935") 20 | os.system("iptables -t nat -D PREROUTING -p tcp --dport 6667 -j DNAT --to-destination 192.168.8.11:6667") 21 | os.system("iptables -t nat -I PREROUTING 1 -p tcp --dport 1935 -j DNAT --to-destination 192.168.8.11:1935") 22 | os.system("iptables -t nat -I PREROUTING 2 -p tcp --dport 6667 -j DNAT --to-destination 192.168.8.11:6667") 23 | 24 | -------------------------------------------------------------------------------- /douyu/commander/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 TJ Holowaychuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /douyu/commander/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "commander@^4.1.0", 3 | "_id": "commander@4.1.1", 4 | "_inBundle": false, 5 | "_integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 6 | "_location": "/commander", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "commander@^4.1.0", 12 | "name": "commander", 13 | "escapedName": "commander", 14 | "rawSpec": "^4.1.0", 15 | "saveSpec": null, 16 | "fetchSpec": "^4.1.0" 17 | }, 18 | "_requiredBy": [ 19 | "/douyudm" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 22 | "_shasum": "9fd602bd936294e9e9ef46a3f4d6964044b18068", 23 | "_spec": "commander@^4.1.0", 24 | "_where": "/main/douyudanmu/node_modules/douyudm", 25 | "author": { 26 | "name": "TJ Holowaychuk", 27 | "email": "tj@vision-media.ca" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/tj/commander.js/issues" 31 | }, 32 | "bundleDependencies": false, 33 | "dependencies": {}, 34 | "deprecated": false, 35 | "description": "the complete solution for node.js command-line programs", 36 | "devDependencies": { 37 | "@types/jest": "^24.0.23", 38 | "@types/node": "^12.12.11", 39 | "eslint": "^6.7.0", 40 | "eslint-plugin-jest": "^22.21.0", 41 | "jest": "^24.8.0", 42 | "standard": "^14.3.1", 43 | "typescript": "^3.7.2" 44 | }, 45 | "engines": { 46 | "node": ">= 6" 47 | }, 48 | "files": [ 49 | "index.js", 50 | "typings/index.d.ts" 51 | ], 52 | "homepage": "https://github.com/tj/commander.js#readme", 53 | "keywords": [ 54 | "commander", 55 | "command", 56 | "option", 57 | "parser" 58 | ], 59 | "license": "MIT", 60 | "main": "index", 61 | "name": "commander", 62 | "repository": { 63 | "type": "git", 64 | "url": "git+https://github.com/tj/commander.js.git" 65 | }, 66 | "scripts": { 67 | "lint": "eslint index.js \"tests/**/*.js\"", 68 | "test": "jest && npm run test-typings", 69 | "test-typings": "tsc -p tsconfig.json" 70 | }, 71 | "typings": "typings/index.d.ts", 72 | "version": "4.1.1" 73 | } 74 | -------------------------------------------------------------------------------- /douyu/douyudm/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 flxxyz 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. 22 | -------------------------------------------------------------------------------- /douyu/douyudm/README.md: -------------------------------------------------------------------------------- 1 | # douyudanmu 2 | 实时获取斗鱼弹幕 3 | 4 | 5 | Downloads 6 | Version 7 | License 8 | 9 | > \>= 1.1.0 支持所有消息输出到文件 10 | 11 | ## 安装(命令行) 12 | ```shell 13 | npm i -g douyudm 14 | or 15 | yarn global add douyudm 16 | ``` 17 | 18 | ## 使用(命令行) 19 | 通过命令行监听,默认显示,`--debug`开启输出到文件,默认保存当前运行目录 20 | ```shell 21 | douyudm -i 房间号 22 | ``` 23 | 更多命令查看 `douyudm --help` 24 | 25 | ## 安装(WEB) 26 | 建议在html顶部位置引入js文件 27 | ``` 28 | 29 | ``` 30 | 31 | ## 使用(WEB) 32 | 挂载在window下,可通过 `douyudanmaku`, `danmaku`调用,开启 **debug** 可直接调用 `danmaku.logger.export()` 导出弹幕 33 | ``` 34 | 63 | ``` 64 | 65 | ## 安装(API) 66 | ```shell 67 | npm i douyudm 68 | or 69 | yarn add douyudm 70 | ``` 71 | 72 | ## 使用(API) 73 | 通过库调用自行封装 74 | ```javascript 75 | //引入类库 76 | const douyu = require('douyudm') 77 | 78 | //设置房间号,初始化 79 | const roomId = 102965 80 | const opts = { 81 | debug: false, 82 | logfile: `/自定义路径/${roomId}.log`, //默认保存到当前运行目录,格式: 房间号.log 83 | } 84 | const room = new douyu(roomId, opts) 85 | 86 | //系统事件 87 | room.on('connect', function () { 88 | console.log('[connect] roomId=%s', this.roomId) 89 | }) 90 | room.on('disconnect', function () { 91 | console.log('[disconnect] roomId=%s', this.roomId) 92 | }) 93 | room.on('error', function(err) { 94 | console.log('[error] roomId=%s', this.roomId) 95 | }) 96 | 97 | //消息事件 98 | room.on('chatmsg', function(res) { 99 | console.log('[chatmsg]', ` [${res.nn}] ${res.txt}`) 100 | }) 101 | room.on('loginres', function(res) { 102 | console.log('[loginres]', '登录成功') 103 | }) 104 | room.on('uenter', function(res) { 105 | console.log('[uenter]', `${res.nn}进入房间`) 106 | }) 107 | 108 | //开始监听 109 | room.run() 110 | ``` 111 | 112 | ## 事件列表 113 | | 系统事件 | 描述 | 114 | |:----------:|:--------:| 115 | | connect | 连接 | 116 | | disconnect | 断开 | 117 | | error | 错误监听 | 118 | 119 | - - - 120 | 121 | | 消息事件 | 描述 | 122 | |:--------------:|:-------------------:| 123 | | loginres | 登入 | 124 | | chatmsg | 弹幕消息 | 125 | | uenter | 进入房间 | 126 | | upgrade | 用户等级提升 | 127 | | rss | 房间开播提醒 | 128 | | bc_buy_deserve | 赠送酬勤通知 | 129 | | ssd | 超级弹幕 | 130 | | spbc | 房间内礼物广播 | 131 | | dgb | 赠送礼物 | 132 | | onlinegift | 领取在线鱼丸 | 133 | | ggbb | 房间用户抢红包 | 134 | | rankup | 房间内top10变化消息 | 135 | | ranklist | 广播排行榜消息 | 136 | | mrkl | 心跳 | 137 | | erquizisn | 鱼丸预言 | 138 | | blab | 粉丝等级升级 | 139 | | rri | 未知的消息事件 | 140 | | synexp | 未知的消息事件 | 141 | | noble_num_info | 未知的消息事件 | 142 | | gbroadcast | 未知的消息事件 | 143 | | qausrespond | 未知的消息事件 | 144 | | wiru | 未知的消息事件 | 145 | | wirt | 未知的消息事件 | 146 | | mcspeacsite | 未知的消息事件 | 147 | | rank_change | 未知的消息事件 | 148 | | srres | 未知的消息事件 | 149 | | anbc | 未知的消息事件 | 150 | | frank | 未知的消息事件 | 151 | 152 | ## 斗鱼STT序列化反序列化库 153 | 154 | STT序列化规定如下: 155 | 156 | 1. 键key和值value直接采用`@=`分割 157 | 2. 数组采用`/`分割 158 | 3. 如果key或者value中含有字符`/`, 则使用`@S`转义 159 | 4. 如果key或者value中含有字符`@`, 则使用`@A`转义 160 | 161 | ```javascript 162 | //引入类库 163 | const stt = require('douyudm').stt 164 | 165 | //序列化测试数据 166 | const obj = { 167 | type: 'chatmsg', 168 | nn: '河马( ̄。。 ̄)', 169 | ic: 'avatar_v3/201912/b99d77251eb643b5a88bb81863afea4e', 170 | cst: '1592152272402', 171 | brid: '0', 172 | lk: '', 173 | list: [{ 174 | lev: '1', 175 | num: '2' 176 | }, { 177 | lev: '7', 178 | num: '3' 179 | }] 180 | } 181 | 182 | //反序列化测试数据 183 | const str = 'type@=chatmsg/nn@=河马( ̄。。 ̄)/ic@=avatar_v3@S201912@Sb99d77251eb643b5a88bb81863afea4e/cst@=1592152272402/brid@=0/lk@=/list@=lev@AA=1@ASnum@AA=2@AS@Slev@AA=7@ASnum@AA=3@AS@S/' 184 | 185 | // 1.序列化 186 | console.log(stt.serialize(obj)) 187 | // 输出: type@=chatmsg/nn@=河马( ̄。。 ̄)/ic@=avatar_v3@S201912@Sb99d77251eb643b5a88bb81863afea4e/cst@=1592152272402/brid@=0/lk@=/list@=lev@AA=1@ASnum@AA=2@AS@Slev@AA=7@ASnum@AA=3@AS@S/ 188 | 189 | // 2.反序列化 190 | console.log(stt.deserialize(str)) 191 | // 输出: 192 | // { 193 | // type: 'chatmsg', 194 | // nn: '河马( ̄。。 ̄)', 195 | // ic: 'avatar_v3/201912/b99d77251eb643b5a88bb81863afea4e', 196 | // cst: '1592152272402', 197 | // brid: '0', 198 | // lk: '', 199 | // list: [ { lev: '1', num: '2' }, { lev: '7', num: '3' } ] 200 | // } 201 | ``` 202 | 203 | ## 后话 204 | 坑太多了,github上的库大部分都是不能使用的,如果近期更新的可以判断使用的新接口,review了几乎所有相关的库,都是依据斗鱼自己官方平台的方法发起tcp连接?但根本连不上,一直拒绝... 205 | 206 | 看了下能使用的库,都是通过websocket建立的连接,立马修改,不出片刻撸完,发现发送数据的格式有点难搞,虽说示意图挺清楚的,但是用Buffer传输死活没有相应的消息,调试太磨人心性了,玛德,直接去把斗鱼网页上的方法扒下来。 207 | 208 | 通过webpack打包混淆代码乍一眼看去很混乱,其实仔细观察还是有规律寻找的。 209 | 210 | 文档中编码的几个固定参数均为数字,在webpack中数字的混淆我还没见过,按这个思路精准的找到这段代码。经过我十几分钟的理解,提取出 [**bufferCoder.js**](src/bufferCoder.js) 211 | 212 | 斗鱼自有的序列化,反序列化方法可以查看 [**stt.js**](src/stt.js) 213 | -------------------------------------------------------------------------------- /douyu/douyudm/build/config/dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | devtool: 'source-maps', 6 | output: { 7 | path: path.resolve(__dirname, '..', '..', 'dist'), 8 | filename: '[name].js' 9 | }, 10 | } -------------------------------------------------------------------------------- /douyu/douyudm/build/config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const config = { 4 | entry: { 5 | douyudanmaku: path.resolve(__dirname, '..', 'index.js'), 6 | }, 7 | module: { 8 | rules: [{ 9 | test: /\.m?js$/, 10 | exclude: /node_modules/, 11 | use: { 12 | loader: 'babel-loader', 13 | options: { 14 | plugins: [ 15 | "@babel/plugin-transform-async-to-generator", 16 | "@babel/plugin-proposal-object-rest-spread", 17 | "@babel/plugin-transform-exponentiation-operator", 18 | "@babel/plugin-transform-parameters", 19 | "@babel/plugin-transform-for-of", 20 | "@babel/plugin-transform-property-literals", 21 | "@babel/plugin-proposal-class-properties", 22 | "babel-plugin-transform-remove-console", 23 | "minify-simplify", 24 | "minify-constant-folding", 25 | "minify-dead-code-elimination", 26 | "minify-guarded-expressions", 27 | "minify-mangle-names", 28 | "minify-numeric-literals", 29 | "minify-type-constructors", 30 | ], 31 | presets: [ 32 | [ 33 | "@babel/preset-env", 34 | { 35 | targets: { 36 | esmodules: true 37 | } 38 | } 39 | ] 40 | ], 41 | }, 42 | }, 43 | }] 44 | }, 45 | node: { 46 | fs: 'empty', 47 | }, 48 | } 49 | 50 | 51 | 52 | module.exports = function (argv) { 53 | if (argv === 'dev') { 54 | return Object.assign({}, config, require('./dev')) 55 | } else { 56 | return Object.assign({}, config, require('./prod')) 57 | } 58 | } -------------------------------------------------------------------------------- /douyu/douyudm/build/config/prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'production', 5 | // devtool: 'eval', 6 | output: { 7 | path: path.resolve(__dirname, '..', '..', 'dist'), 8 | filename: '[name].min.js' 9 | }, 10 | } -------------------------------------------------------------------------------- /douyu/douyudm/build/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import douyudanmaku from '../src/index' 3 | 4 | window.douyudanmaku = douyudanmaku 5 | window.danmaku = douyudanmaku 6 | -------------------------------------------------------------------------------- /douyu/douyudm/build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npx webpack --env prod --mode production --config ./config/index.js --colors --progress", 4 | "dev": "npx webpack --env dev --mode development --config ./config/index.js --colors --progress --display-reasons --display-modules" 5 | }, 6 | "devDependencies": { 7 | "@babel/core": "^7.8.7", 8 | "@babel/plugin-proposal-class-properties": "^7.10.1", 9 | "@babel/plugin-proposal-object-rest-spread": "^7.9.6", 10 | "@babel/plugin-transform-async-to-generator": "^7.8.3", 11 | "@babel/plugin-transform-exponentiation-operator": "^7.8.3", 12 | "@babel/plugin-transform-for-of": "^7.9.0", 13 | "@babel/plugin-transform-parameters": "^7.9.5", 14 | "@babel/plugin-transform-property-literals": "^7.8.3", 15 | "@babel/preset-env": "^7.8.7", 16 | "babel-loader": "^8.0.6", 17 | "babel-plugin-minify-constant-folding": "^0.5.0", 18 | "babel-plugin-minify-dead-code-elimination": "^0.5.1", 19 | "babel-plugin-minify-guarded-expressions": "^0.4.4", 20 | "babel-plugin-minify-mangle-names": "^0.5.0", 21 | "babel-plugin-minify-numeric-literals": "^0.4.3", 22 | "babel-plugin-minify-simplify": "^0.5.1", 23 | "babel-plugin-minify-type-constructors": "^0.4.3", 24 | "babel-plugin-transform-remove-console": "^6.9.4", 25 | "webpack": "^4.42.0", 26 | "webpack-cli": "^3.3.11" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /douyu/douyudm/example/README.md: -------------------------------------------------------------------------------- 1 | - `stt.js` 演示斗鱼stt序列化,反序列化 2 | - `cli.js` 演示通过命令行的方式展现弹幕 3 | - `web/index.js` 演示通过网页的方式展现弹幕 -------------------------------------------------------------------------------- /douyu/douyudm/example/cli.js: -------------------------------------------------------------------------------- 1 | //引入类库 2 | const danmaku = require('../src/index') 3 | 4 | //设置房间号,初始化 5 | const roomId = 102965 6 | const room = new danmaku(roomId) 7 | 8 | //系统事件 9 | room.on('connect', function () { 10 | console.log('[connect] roomId=%s', this.roomId) 11 | }) 12 | room.on('disconnect', function () { 13 | console.log('[disconnect] roomId=%s', this.roomId) 14 | }) 15 | room.on('error', function (err) { 16 | console.log('[error] roomId=%s', this.roomId) 17 | }) 18 | 19 | //消息事件 20 | room.on('chatmsg', function (res) { 21 | console.log('[chatmsg]', ` [${res.nn}] ${res.txt}`) 22 | }) 23 | room.on('loginres', function (res) { 24 | console.log('[loginres]', '登录成功') 25 | }) 26 | room.on('uenter', function (res) { 27 | console.log('[uenter]', `${res.nn}进入房间`) 28 | }) 29 | 30 | //开始监听 31 | room.run() -------------------------------------------------------------------------------- /douyu/douyudm/example/stt.js: -------------------------------------------------------------------------------- 1 | //引入类库 2 | const stt = require('../src/index').stt 3 | 4 | //序列化测试数据 5 | const obj = { 6 | type: 'chatmsg', 7 | nn: '河马( ̄。。 ̄)', 8 | ic: 'avatar_v3/201912/b99d77251eb643b5a88bb81863afea4e', 9 | cst: '1592152272402', 10 | brid: '0', 11 | lk: '', 12 | list: [{ 13 | lev: '1', 14 | num: '2' 15 | }, { 16 | lev: '7', 17 | num: '3' 18 | }] 19 | } 20 | 21 | //反序列化测试数据 22 | const str = 'type@=chatmsg/nn@=河马( ̄。。 ̄)/ic@=avatar_v3@S201912@Sb99d77251eb643b5a88bb81863afea4e/cst@=1592152272402/brid@=0/lk@=/list@=lev@AA=1@ASnum@AA=2@AS@Slev@AA=7@ASnum@AA=3@AS@S/' 23 | 24 | console.log('1.序列化') 25 | console.log('原始: ') 26 | console.log(obj) 27 | console.log('输出: ') 28 | console.log(stt.serialize(obj)) 29 | 30 | console.log('\n-------------------------------------------') 31 | 32 | console.log('2.反序列化') 33 | console.log('原始: ') 34 | console.log(str) 35 | console.log('输出: ') 36 | console.log(stt.deserialize(str)) 37 | 38 | //一个例外 39 | // const a = `type@=frank/fc@=4395/bnn@=拒绝R/list@=uid@AA=197290249@ASnn@AA=宋宋宋555@ASic@AA=avatar_v3@AAS201809@AASde94cadbaa975630940499fdbafbf258@ASfim@AA=74078400@ASbl@AA=28@ASlev@AA=52@ASpg@AA=1@ASrg@AA=4@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=216851315@ASnn@AA=滴滴么么儿@ASic@AA=avatar_v3@AAS202006@AASf4f65dbd49cf44da944f657fd934c094@ASfim@AA=22429300@ASbl@AA=24@ASlev@AA=41@ASpg@AA=1@ASrg@AA=4@ASnl@AA=7@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=177076498@ASnn@AA=橘井仁心@ASic@AA=avanew@AASface@AAS201711@AAS13@AAS19@AAS0b33a9f10246b83ca9527fb8efdc132f@ASfim@AA=16192900@ASbl@AA=22@ASlev@AA=38@ASpg@AA=1@ASrg@AA=4@ASnl@AA=7@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=163415865@ASnn@AA=HArianaC@ASic@AA=avatar_v3@AAS202005@AASd21cfac310a04a2fb76ed17a8d80040d@ASfim@AA=14609100@ASbl@AA=22@ASlev@AA=39@ASpg@AA=1@ASrg@AA=4@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=172172693@ASnn@AA=不要昵称12@ASic@AA=avanew@AASface@AAS201710@AAS13@AAS15@AASd1f39c4b83e2eb4cc55c09a174cb1744@ASfim@AA=12552160@ASbl@AA=21@ASlev@AA=39@ASpg@AA=1@ASrg@AA=4@ASnl@AA=7@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=193517041@ASnn@AA=风暴中的汉堡@ASic@AA=avatar_v3@AAS201807@AASb047ee52079c53aedf0760177ade0c16@ASfim@AA=10228360@ASbl@AA=20@ASlev@AA=40@ASpg@AA=1@ASrg@AA=4@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=25190947@ASnn@AA=无畏孤独狼@ASic@AA=avatar_v3@AAS201809@AAS992cd2a66f66952c8b8afa4267d2f160@ASfim@AA=9686340@ASbl@AA=20@ASlev@AA=38@ASpg@AA=1@ASrg@AA=4@ASnl@AA=7@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=39270085@ASnn@AA=看见你是我的荣耀@ASic@AA=avatar@AAS039@AAS27@AAS00@AAS85_avatar@ASfim@AA=9559080@ASbl@AA=20@ASlev@AA=37@ASpg@AA=1@ASrg@AA=4@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=37862064@ASnn@AA=JC小猫@ASic@AA=avatar_v3@AAS201906@AASd94deeed8c8d43c7b09afc673e733daa@ASfim@AA=9284120@ASbl@AA=20@ASlev@AA=40@ASpg@AA=1@ASrg@AA=4@ASnl@AA=1@ASsahf@AA=0@ASnewrg@AA=0@AS@Suid@AA=227676692@ASnn@AA=董旺成@ASic@AA=avatar_v3@AAS202003@AAS5548b1db5ee1481fa8d8828d5fc2ee80@ASfim@AA=8120680@ASbl@AA=19@ASlev@AA=39@ASpg@AA=1@ASrg@AA=4@ASnl@AA=7@ASsahf@AA=0@ASnewrg@AA=0@AS@S/ver@=156739/ci@=r6ddzwf2uy/rid@=102965/` 40 | // console.log(stt.deserialize(a)) -------------------------------------------------------------------------------- /douyu/douyudm/example/web/gift.json: -------------------------------------------------------------------------------- 1 | { 2 | "192": "赞(系统)", 3 | "193": "弱鸡(系统)", 4 | "519": "呵呵(系统)", 5 | "520": "稳(系统)", 6 | "712": "棒棒哒(系统)", 7 | "714": "怂(系统)", 8 | "824": "荧光棒", 9 | "1859": "小飞碟", 10 | "20000": "鱼丸", 11 | "20001": "弱鸡", 12 | "20002": "办卡", 13 | "20003": "飞机", 14 | "20004": "火箭", 15 | "20005": "超级火箭", 16 | "20006": "赞", 17 | "20008": "超大丸星", 18 | "20234": "爱心飞机", 19 | "20387": "心动火箭", 20 | "20417": "福袋", 21 | "20541": "大气", 22 | "20542": "666", 23 | "20618": "魔法戒指", 24 | "20624": "魔法皇冠", 25 | "20626": "幸福券", 26 | "20642": "能量电池", 27 | "20643": "能量水晶", 28 | "20644": "能量戒指", 29 | "20709": "壁咚", 30 | "20710": "金鲨鱼", 31 | "20725": "宠爱卡", 32 | "20726": "挚爱超火", 33 | "20727": "乖乖戴口罩", 34 | "20728": "勤洗手", 35 | "20758": "打弱鸡", 36 | "20760": "风暴超火", 37 | "20761": "风暴火箭", 38 | "20885": "告白书", 39 | "20886": "告白卡", 40 | "20887": "告白飞机", 41 | "20888": "告白火箭", 42 | "20889": "告白超火", 43 | "20900": "小厨娘", 44 | "20914": "开黑券", 45 | "20940": "狂欢卡", 46 | "20941": "狂欢火箭", 47 | "20951": "狂欢趴", 48 | "20952": "狂欢飞机", 49 | "20953": "狂欢超火" 50 | } -------------------------------------------------------------------------------- /douyu/douyudm/example/web/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "gift": "https://a-sh-cn.machine.flxxyz.com/gift.php", 3 | "douyu": "https://mirror.flxxyz.com/douyu/" 4 | } -------------------------------------------------------------------------------- /douyu/douyudm/example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 演示页面 8 | 9 | 10 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{ tips }} 31 |
32 |
33 |
    34 | 35 |
36 |
37 | 38 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /douyu/douyudm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "douyudm", 3 | "_id": "douyudm@1.3.2-beta.3", 4 | "_inBundle": false, 5 | "_integrity": "sha512-x2MluqyRFpskAVcGVYbTIgEpChZuKmOj01GdMl67DxS0AQM1jQoUwk5yHaODio3Hb8OYPXNWvWkQps7F2xwWAw==", 6 | "_location": "/douyudm", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "tag", 10 | "registry": true, 11 | "raw": "douyudm", 12 | "name": "douyudm", 13 | "escapedName": "douyudm", 14 | "rawSpec": "", 15 | "saveSpec": null, 16 | "fetchSpec": "latest" 17 | }, 18 | "_requiredBy": [ 19 | "#USER", 20 | "/" 21 | ], 22 | "_resolved": "https://registry.npmjs.org/douyudm/-/douyudm-1.3.2-beta.3.tgz", 23 | "_shasum": "42a057979b4f7d765e7309dcde6b1d2430b0d237", 24 | "_spec": "douyudm", 25 | "_where": "/main/douyudanmu", 26 | "author": { 27 | "name": "flxxyz", 28 | "email": "i@mdzz.name" 29 | }, 30 | "bin": { 31 | "douyudm": "src/cmd.js" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/flxxyz/douyudm/issues" 35 | }, 36 | "bundleDependencies": false, 37 | "dependencies": { 38 | "commander": "^4.1.0", 39 | "fast-text-encoding": "^1.0.0", 40 | "ws": "^7.2.1" 41 | }, 42 | "deprecated": false, 43 | "description": "实时获取斗鱼弹幕", 44 | "devDependencies": { 45 | "mocha": "^8.0.1" 46 | }, 47 | "homepage": "https://github.com/flxxyz/douyudm#readme", 48 | "keywords": [ 49 | "douyu", 50 | "danmu", 51 | "斗鱼", 52 | "弹幕", 53 | "stt", 54 | "serialize", 55 | "deserialize", 56 | "序列化" 57 | ], 58 | "license": "MIT", 59 | "main": "src/index.js", 60 | "name": "douyudm", 61 | "publishConfig": { 62 | "registry": "https://registry.npmjs.org/", 63 | "access": "public" 64 | }, 65 | "repository": { 66 | "type": "git", 67 | "url": "git+https://github.com/flxxyz/douyudm.git" 68 | }, 69 | "scripts": { 70 | "test": "mocha test/" 71 | }, 72 | "version": "1.3.2-beta.3" 73 | } 74 | -------------------------------------------------------------------------------- /douyu/douyudm/src/client.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | const event = require('./clientEvent') 3 | const stt = require('./stt') 4 | const util = require('./util') 5 | const packet = require('./packet') 6 | const messageEvent = require('./messageEvent') 7 | 8 | class Client { 9 | constructor(roomId, opts) { 10 | this.roomId = roomId 11 | this.ws = null 12 | this.logger = null 13 | this.heartbeatTask = null 14 | this.messageEvent = messageEvent 15 | this.ignore = [] 16 | this.options = this.setOptions(opts || {}) 17 | this.clientEvent = { 18 | connect: { 19 | name: 'open', 20 | listener: event.open 21 | }, 22 | disconnect: { 23 | name: 'close', 24 | listener: event.close 25 | }, 26 | error: { 27 | name: 'error', 28 | listener: event.error 29 | }, 30 | } 31 | } 32 | 33 | initSocket(ws) { 34 | this.ws = new ws(config.URL) 35 | this.ws.on('open', this.clientEvent.connect.listener.bind(this)) 36 | this.ws.on('error', this.clientEvent.error.listener.bind(this)) 37 | this.ws.on('close', this.clientEvent.disconnect.listener.bind(this)) 38 | this.ws.on('message', event.message.bind(this)) 39 | } 40 | 41 | send(message) { 42 | this.ws.send(packet.Encode(stt.serialize(message))) 43 | } 44 | 45 | login() { 46 | this.send({ 47 | type: 'loginreq', 48 | roomid: this.roomId, 49 | }) 50 | } 51 | 52 | joinGroup() { 53 | this.send({ 54 | type: 'joingroup', 55 | rid: this.roomId, 56 | gid: 0 57 | }) 58 | } 59 | 60 | heartbeat() { 61 | this.heartbeatTask = setInterval(() => { 62 | this.send({ 63 | type: 'mrkl' 64 | }) 65 | }, config.HEARBEAT_INTERVAL * 1000) 66 | } 67 | 68 | logout() { 69 | this.send({ 70 | type: 'logout', 71 | }) 72 | 73 | clearInterval(this.heartbeatTask) 74 | } 75 | 76 | run(websocket, logger) { 77 | const ws = websocket || require('./websocket') 78 | const Logger = logger || require('./logger') 79 | this.logger = new Logger() 80 | this.initSocket(ws) 81 | } 82 | 83 | setIgnore(key, value) { 84 | if (util.isObject(key)) { 85 | for (let i in key) { 86 | if (key[i]) { 87 | this.ignore.push(i) 88 | } 89 | } 90 | } else { 91 | if (value) { 92 | this.ignore.push(key) 93 | } 94 | } 95 | 96 | return this 97 | } 98 | 99 | setOptions(opts) { 100 | const defOpts = { 101 | debug: false, 102 | logfile: `${this.roomId}.log`, 103 | } 104 | const options = {} 105 | 106 | if (!util.isObject(opts)) { 107 | return defOpts 108 | } 109 | 110 | if (opts.hasOwnProperty('debug') && util.isBoolean(opts.debug)) { 111 | options.debug = opts.debug 112 | } 113 | 114 | if (opts.hasOwnProperty('logfile') && util.isString(opts.logfile)) { 115 | options.logfile = opts.logfile 116 | } 117 | 118 | return Object.assign(defOpts, options) 119 | } 120 | 121 | messageHandle(data) { 122 | packet.Decode(data, m => { 123 | const r = stt.deserialize(m) 124 | 125 | if (this.options.debug) { 126 | this.logger.init(util.isBrowser() ? this.roomId : this.options.logfile) 127 | this.logger.echo(r) 128 | } 129 | 130 | if (Object.keys(this.messageEvent).filter(v => { 131 | return !this.ignore.includes(v) 132 | }).includes(r.type)) { 133 | this.messageEvent[r.type](r) 134 | } 135 | }) 136 | } 137 | 138 | on(method, callback) { 139 | const clientEventName = Object.keys(this.clientEvent).find(clientEvent => clientEvent === method.toLocaleLowerCase()) 140 | if (clientEventName) { 141 | //在创建连接是触发connect事件时,发送登入,加入组,监听心跳消息 142 | if (clientEventName === 'connect') { 143 | let cb = callback 144 | callback = function (res) { 145 | this.login() 146 | this.joinGroup() 147 | this.heartbeat() 148 | cb.bind(this)(res) 149 | } 150 | } 151 | this.clientEvent[method].listener = callback.bind(this) 152 | } 153 | 154 | const messageEventName = Object.keys(this.messageEvent).find(messageEvent => messageEvent === method.toLocaleLowerCase()) 155 | if (messageEventName) { 156 | this.messageEvent[method] = callback.bind(this) 157 | } 158 | } 159 | } 160 | 161 | module.exports = Client -------------------------------------------------------------------------------- /douyu/douyudm/src/clientEvent.js: -------------------------------------------------------------------------------- 1 | function open() { 2 | this.login() //登入 3 | this.joinGroup() //加入组 4 | this.heartbeat() //发送心跳,强制45秒 5 | } 6 | 7 | function error(err) { 8 | console.error(err) 9 | } 10 | 11 | function close() { 12 | this.logout() 13 | } 14 | 15 | function message(data) { 16 | if (typeof MessageEvent !== 'undefined') { 17 | //无MessageEvent类型判断为node环境,转换数据为arraybuffer类型 18 | const reader = new FileReader() 19 | reader.onload = e => this.messageHandle(e.target.result) 20 | reader.readAsArrayBuffer(data.data) 21 | } else { 22 | this.messageHandle(data) 23 | } 24 | } 25 | 26 | module.exports = { 27 | open, 28 | error, 29 | close, 30 | message, 31 | } -------------------------------------------------------------------------------- /douyu/douyudm/src/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander') 4 | const package = require('../package.json') 5 | const client = require('../src/client') 6 | 7 | program 8 | .requiredOption('-i, --id ', '输入房间id') 9 | .option('-j, --uenter', '忽略用户进入房间', false) 10 | .option('-u, --upgrade', '忽略用户等级提升', false) 11 | .option('-r, --rss', '忽略房间开播提醒', false) 12 | .option('-b, --bc_buy_deserve', '忽略赠送酬勤通知', false) 13 | .option('-s, --ssd', '忽略超级弹幕', false) 14 | .option('-p, --spbc', '忽略房间内礼物广播', false) 15 | .option('-d, --dgb', '忽略赠送礼物', false) 16 | .option('-o, --onlinegift', '忽略领取在线鱼丸', false) 17 | .option('-g, --ggbb', '忽略房间用户抢红包', false) 18 | .option('-r, --rankup', '忽略房间内top10变化消息', false) 19 | .option('-l, --ranklist', '忽略广播排行榜消息', false) 20 | .option('--debug', '开启debug模式,输出消息内容保存到文件', false) 21 | .version(package.version) 22 | .parse(process.argv) 23 | 24 | const c = new client(program.id, { 25 | debug: program.debug, 26 | }) 27 | c.setIgnore({ 28 | uenter: program.uenter, 29 | upgrade: program.upgrade, 30 | rss: program.rss, 31 | bc_buy_deserve: program.bc_buy_deserve, 32 | ssd: program.ssd, 33 | spbc: program.spbc, 34 | dgb: program.dgb, 35 | onlinegift: program.onlinegift, 36 | ggbb: program.ggbb, 37 | rankup: program.rankup, 38 | ranklist: program.ranklist, 39 | }).run() -------------------------------------------------------------------------------- /douyu/douyudm/src/config.js: -------------------------------------------------------------------------------- 1 | const util = require('./util') 2 | 3 | //目前已知的弹幕服务器 4 | const port = 8500 + util.random(1, 6) 5 | const URL = `wss://danmuproxy.douyu.com:${port}/` 6 | 7 | const HEARBEAT_INTERVAL = 45 8 | 9 | const MSG_LIVE_ON = '主播正在直播' 10 | const MSG_LIVE_OFF = '主播没有直播' 11 | const MSG_ROOM_RSS = '房间开播提醒' 12 | const MSG_BC_BUY_DESERVE = '赠送酬勤通知' 13 | const MSG_SSD = '超级弹幕' 14 | const MSG_ROOM_SPBC = '房间内礼物广播' 15 | 16 | module.exports = { 17 | URL, 18 | HEARBEAT_INTERVAL, 19 | MSG_LIVE_ON, 20 | MSG_LIVE_OFF, 21 | MSG_ROOM_RSS, 22 | MSG_BC_BUY_DESERVE, 23 | MSG_SSD, 24 | MSG_ROOM_SPBC, 25 | } -------------------------------------------------------------------------------- /douyu/douyudm/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const danmaku = require('./client') 4 | danmaku.stt = require('./stt') 5 | danmaku.util = require('./util') 6 | danmaku.logger = require('./logger') 7 | danmaku.Websocket = require('./websocket') 8 | danmaku.packet = require('./packet') 9 | 10 | module.exports = danmaku -------------------------------------------------------------------------------- /douyu/douyudm/src/logger.js: -------------------------------------------------------------------------------- 1 | const util = require('./util') 2 | 3 | class Logger { 4 | constructor() { 5 | this.name = 'unknown' 6 | this.inited = false 7 | this.db = null 8 | this.version = 2 9 | } 10 | 11 | init(name) { 12 | if (!this.inited) { 13 | this.name = name 14 | 15 | const IndexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB 16 | this.DB = IndexedDB.open('danmaku', this.version) 17 | this.DB.addEventListener('success', e => { 18 | console.log('连接数据库') 19 | this.db = e.target.result 20 | }) 21 | 22 | this.DB.addEventListener('upgradeneeded', e => { 23 | console.log('升级数据库') 24 | this.db = e.target.result 25 | const store = this.db.createObjectStore('douyu', { 26 | keyPath: 'id', 27 | autoIncrement: true, 28 | }) 29 | store.createIndex('idx_room_id', 'room_id', { 30 | unique: false 31 | }) 32 | }) 33 | 34 | this.DB.addEventListener('error', e => { 35 | console.log('连接数据库出错 Error:', e) 36 | }) 37 | 38 | this.inited = true 39 | } 40 | } 41 | 42 | echo(data) { 43 | if (this.db !== null) { 44 | const tx = this.db.transaction('douyu', 'readwrite') 45 | const store = tx.objectStore('douyu') 46 | store.add({ 47 | room_id: this.name, 48 | timestamp: new Date().getTime(), 49 | frame: data 50 | }) 51 | } 52 | } 53 | 54 | all(roomId) { 55 | if (this.db !== null) { 56 | const tx = this.db.transaction('douyu', 'readonly') 57 | const store = tx.objectStore('douyu') 58 | const req = (roomId ? store.index('idx_room_id').getAll(roomId) : store.getAll()) 59 | return new Promise(function (resolve, reject) { 60 | req.addEventListener('success', function (e) { 61 | resolve(e.target.result) 62 | }) 63 | req.addEventListener('error', function (e) { 64 | reject(false) 65 | }) 66 | }) 67 | } 68 | } 69 | 70 | count(roomId) { 71 | if (this.db !== null) { 72 | const tx = this.db.transaction('douyu', 'readonly') 73 | const store = tx.objectStore('douyu') 74 | const req = (roomId ? store.index('idx_room_id').count(roomId) : store.count()) 75 | return new Promise(function (resolve, reject) { 76 | req.addEventListener('success', function (e) { 77 | resolve(req.result) 78 | }) 79 | req.addEventListener('error', function (e) { 80 | reject(false) 81 | }) 82 | }) 83 | } 84 | } 85 | 86 | async export (roomId) { 87 | if (this.db !== null) { 88 | const r = await this.all(roomId) 89 | const text = r.reduce((arr, row) => { 90 | const v = { 91 | timestamp: row.timestamp, 92 | frame: row.frame, 93 | } 94 | if (!roomId) v.roomId = this.name 95 | arr.push(JSON.stringify(v)) 96 | return arr 97 | }, []) 98 | util.download(this.name, text.join('\n')) 99 | return text 100 | } 101 | } 102 | 103 | clear(roomId) { 104 | if (this.db !== null) { 105 | const tx = this.db.transaction('douyu', 'readwrite') 106 | const store = tx.objectStore('douyu') 107 | if (roomId) { 108 | const index = store.index('idx_room_id') 109 | const req = index.openCursor(IDBKeyRange.only(roomId)) 110 | req.addEventListener('success', function () { 111 | const cursor = req.result 112 | if (cursor) { 113 | cursor.delete() 114 | cursor.continue() 115 | } 116 | }) 117 | } else { 118 | store.clear() 119 | } 120 | } 121 | } 122 | } 123 | 124 | module.exports = util.isBrowser() ? Logger : require('./loggerNode') -------------------------------------------------------------------------------- /douyu/douyudm/src/loggerNode.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | module.exports = class Logger { 4 | constructor() { 5 | this.name = 'unknown' 6 | this.inited = false 7 | } 8 | 9 | init(name) { 10 | if (!this.inited) { 11 | this.name = name 12 | this.inited = true 13 | } 14 | } 15 | 16 | echo(data) { 17 | const content = `${JSON.stringify({timestamp: new Date().getTime(),frame: data})}\n` 18 | fs.appendFile( 19 | this.name, 20 | content, 21 | function (err) { 22 | if (err) { 23 | console.error('日志保存出错, Error:', err) 24 | } 25 | }) 26 | } 27 | 28 | all() { 29 | return new Promise((resolve, reject) => { 30 | fs.readFile(this.name, 'utf8', function (err, str) { 31 | if (err) { 32 | reject(err) 33 | } else { 34 | resolve(str) 35 | } 36 | }) 37 | }) 38 | } 39 | 40 | count() { 41 | return new Promise(async (resolve, reject) => { 42 | const content = await this.all() 43 | resolve(content.split('\n').filter(v => v !== '').length) 44 | }) 45 | } 46 | 47 | export () { 48 | return fs.readFileSync(this.name, 'utf8') 49 | } 50 | 51 | clear() { 52 | try { 53 | return fs.writeFileSync(this.name, '', 'utf8') 54 | } catch (err) { 55 | return false 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /douyu/douyudm/src/messageEvent.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | 3 | module.exports = { 4 | loginres: function (r) { 5 | // 登录响应 6 | // 服务端返回登录响应消息,完整的数据部分应包含的字段如下: 7 | // 字段说明 8 | // type 表示为“登录”消息,固定为 loginres 9 | // userid 用户 ID 10 | // roomgroup 房间权限组 11 | // pg 平台权限组 12 | // sessionid 会话 ID 13 | // username 用户名 14 | // nickname 用户昵称 15 | // is_signed 是否已在房间签到 16 | // signed_count 日总签到次数 17 | // live_stat 直播状态 18 | // npv 是否需要手机验证 19 | // best_dlev 最高酬勤等级 20 | // cur_lev 酬勤等级 21 | }, 22 | chatmsg: function (r) { 23 | // 弹幕消息 24 | // 用户在房间发送弹幕时,服务端发此消息给客户端,完整的数据部分应包含的字 段如下: 25 | // 字段说明 26 | // type 表示为“弹幕”消息,固定为 chatmsg 27 | // gid 弹幕组 id 28 | // rid 房间 id 29 | // uid 发送者 uid 30 | // nn 发送者昵称 31 | // txt 弹幕文本内容 32 | // cid 弹幕唯一 ID 33 | // level 用户等级 34 | // gt 礼物头衔:默认值 0(表示没有头衔) 35 | // col 颜色:默认值 0(表示默认颜色弹幕) 36 | // ct 客户端类型:默认值 0(表示 web 用户) 37 | // rg 房间权限组:默认值 1(表示普通权限用户) 38 | // pg 平台权限组:默认值 1(表示普通权限用户) 39 | // dlv 酬勤等级:默认值 0(表示没有酬勤) 40 | // dc 酬勤数量:默认值 0(表示没有酬勤数量) 41 | // bdlv 最高酬勤等级:默认值 0(表示全站都没有酬勤) 42 | console.log(" [%s] %s", r.level + (r.level < 10 ? ' ' : ''), r.nn, r.txt) 43 | }, 44 | uenter: function (r) { 45 | // 进入直播间 46 | // 具有特殊属性的用户进入直播间时,服务端发送此消息至客户端。完整的数据部 分应包含的字段如下: 47 | // 字段说明 48 | // type 表示为“用户进房通知”消息,固定为 uenter 49 | // rid 房间 ID 50 | // gid 弹幕分组 ID 51 | // uid 用户 ID 52 | // nn 用户昵称 53 | // str 战斗力 54 | // level 新用户等级 55 | // gt 礼物头衔:默认值 0(表示没有头衔) 56 | // rg 房间权限组:默认值 1(表示普通权限用户) 57 | // pg 平台身份组:默认值 1(表示普通权限用户) 58 | // dlv 酬勤等级:默认值 0(表示没有酬勤) 59 | // dc 酬勤数量:默认值 0(表示没有酬勤数量) 60 | // bdlv 最高酬勤等级:默认值 0(表示全站都没有酬勤) 61 | console.log(`欢迎${r.nn}进入了直播间`) 62 | }, 63 | upgrade: function (r) { 64 | // 用户等级提升 65 | }, 66 | rss: function (r) { 67 | // 房间开播提醒 68 | // 房间开播提醒主要部分应包含的字段如下: 69 | // 字段说明 70 | // type 表示为“房间开播提醒”消息,固定为 rss 71 | // rid 房间 id 72 | // gid 弹幕分组 id 73 | // ss 直播状态,1-正在直播, 2-没有直播 74 | // code 类型 75 | // rt 开关播原因:0-主播开关播,其他值-其他原因 76 | // notify 通知类型 77 | // endtime 关播时间(仅关播时有效) 78 | console.log('[开播提醒]', r.ss == 1 ? config.MSG_LIVE_ON : config.MSG_LIVE_OFF) 79 | }, 80 | bc_buy_deserve: function (r) { 81 | // 赠送酬勤通知 82 | // 用户赠送酬勤时,服务端发送此消息至客户端。完整的数据部分应包含的字段如 下: 83 | // 字段说明 84 | // type 表示为“赠送酬勤通知”消息,固定为 bc_buy_deserve 85 | // rid 房间 ID 86 | // gid 弹幕分组 ID 87 | // level 用户等级 88 | // cnt 赠送数量 89 | // hits 赠送连击次数 90 | // lev 酬勤等级 91 | // sui 用户信息序列化字符串,详见下文。注意,此处为嵌套序列化,需注 意符号的转义变换。(转义符号参见 2.2 序列化) 92 | }, 93 | ssd: function (r) { 94 | // 超级弹幕 95 | // 超级弹幕主要部分应包含的字段如下: 96 | // 字段说明 97 | // type 表示为“超级弹幕”消息,固定为 ssd 98 | // rid 房间 id 99 | // gid 弹幕分组 id 100 | // sdid 超级弹幕 id 101 | // trid 跳转房间 id 102 | // content 超级弹幕的内容 103 | }, 104 | spbc: function (r) { 105 | // 房间内礼物广播 106 | // 房间内赠送礼物成功后效果主要部分应包含的字段如下: 107 | // 字段说明 108 | // type 表示为“房间内礼物广播”,固定为 spbc 109 | // rid 房间 id 110 | // gid 弹幕分组 id 111 | // sn 赠送者昵称 112 | // dn 受赠者昵称 113 | // gn 礼物名称 114 | // gc 礼物数量 115 | // drid 赠送房间 116 | // gs 广播样式 117 | // gb 是否有礼包(0-无礼包,1-有礼包) 118 | // es 广播展现样式(1-火箭,2-飞机) 119 | // gfid 礼物 id 120 | // eid 特效 id 121 | console.log(`------------- 感谢[${r.sn}] 赠送的 ${r.gn}x${r.gc}`) 122 | }, 123 | dgb: function (r) { 124 | // 赠送礼物 125 | // 用户在房间赠送礼物时,服务端发送此消息给客户端。完整的数据部分应包含的 字段如下: 126 | // 字段说明 127 | // type 表示为“赠送礼物”消息,固定为 dgb 128 | // rid 房间 ID 129 | // gid 弹幕分组 ID 130 | // gfid 礼物 id 131 | // gs 礼物显示样式 132 | // uid 用户 id 133 | // nn 用户昵称 134 | // str 用户战斗力 135 | // level 用户等级 136 | // dw 主播体重 137 | // gfcnt 礼物个数:默认值 1(表示 1 个礼物) 138 | // hits 礼物连击次数:默认值 1(表示 1 连击) 139 | // dlv 酬勤头衔:默认值 0(表示没有酬勤) 140 | // dc 酬勤个数:默认值 0(表示没有酬勤数量) 141 | // bdl 全站最高酬勤等级:默认值 0(表示全站都没有酬勤) 142 | // rg 房间身份组:默认值 1(表示普通权限用户) 143 | // pg 平台身份组:默认值 1(表示普通权限用户) 144 | // rpid 红包 id:默认值 0(表示没有红包) 145 | // slt 红包开启剩余时间:默认值 0(表示没有红包) 146 | // elt 红包销毁剩余时间:默认值 0(表示没有红包) 147 | }, 148 | onlinegift: function (r) { 149 | // 领取在线鱼丸 150 | // 在线领取鱼丸时,若出现暴击(鱼丸数大于等于 60)服务则发送领取暴击消息 到客户端。完整的数据部分应包含的字段如下: 151 | // 字段说明 152 | // type 表示为“领取在线鱼丸”消息,固定为 onlinegift 153 | // rid 房间 ID 154 | // uid 用户 ID 155 | // gid 弹幕分组 ID 156 | // sil 鱼丸数 157 | // if 领取鱼丸的等级 158 | // ct 客户端类型标识 159 | // nn 用户昵称 160 | console.log(`------------- [${r.nn}] 领取鱼丸x${r.sil}`) 161 | }, 162 | ggbb: function (r) { 163 | // 房间用户抢红包 164 | // 房间赠送礼物成功后效果(赠送礼物效果,连击数)主要部分应包含的字段如下: 165 | // 字段说明 166 | // type 表示“房间用户抢红包”信息,固定为 ggbb 167 | // rid 房间 id 168 | // gid 弹幕分组 id 169 | // sl 抢到的鱼丸数量 170 | // sid 礼包产生者 id 171 | // did 抢礼包者 id 172 | // snk 礼包产生者昵称 173 | // dnk 抢礼包者昵称 174 | // rpt 礼包类型 175 | }, 176 | rankup: function (r) { 177 | // 房间内top10变化消息 178 | // 房间内 top10 排行榜变化后,广播。主要部分应包含的字段如下: 179 | // 字段说明 180 | // type 表示为“房间 top10 排行榜变换”,固定为 rankup 181 | // rid 房间 id 182 | // gid 弹幕分组 id 183 | // uid 用户 id 184 | // drid 目标房间 id 185 | // rt 房间所属栏目类型 186 | // bt 广播类型:1-房间内广播,2-栏目广播,4-全站广播 187 | // sz 展示区域:1-聊天区展示,2-flash 展示,3-都显示 188 | // nk 用户昵称 189 | // rkt top10 榜的类型 1-周榜 2-总榜 4-日榜 190 | // rn 上升后的排名 191 | }, 192 | ranklist: function (r) { 193 | // 广播排行榜消息 194 | }, 195 | mrkl: function(r) { 196 | // 心跳 197 | }, 198 | erquizisn: function(r) { 199 | // 鱼丸预言,参数未知 200 | }, 201 | blab: function(r) { 202 | // 粉丝等级升级 203 | // 字段说明 204 | // type 表示为“粉丝等级升级”,固定为 blab 205 | // rid 房间 id 206 | // uid 用户 id 207 | // nn 用户昵称 208 | // bl 升级后的等级 209 | // bnn 升级的粉丝牌 210 | // lbl 未知(猜测未升级前的等级) 211 | // ba 未知 212 | }, 213 | rri: function(r) { 214 | // 未知的消息事件 215 | }, 216 | synexp: function(r) { 217 | 218 | }, 219 | noble_num_info: function(r) { 220 | // 未知的消息事件 221 | }, 222 | gbroadcast: function(r) { 223 | // 未知的消息事件 224 | }, 225 | qausrespond: function(r) { 226 | // 未知的消息事件 227 | }, 228 | wiru: function(r) { 229 | // 未知的消息事件 230 | }, 231 | wirt: function(r) { 232 | // 未知的消息事件 233 | }, 234 | mcspeacsite: function(r) { 235 | // 未知的消息事件 236 | }, 237 | rank_change: function(r) { 238 | // 未知的消息事件 239 | }, 240 | srres: function(r) { 241 | // 未知的消息事件 242 | }, 243 | anbc: function(r) { 244 | // 未知的消息事件 245 | }, 246 | frank: function(r) { 247 | // 粉丝排行榜变化 248 | }, 249 | } -------------------------------------------------------------------------------- /douyu/douyudm/src/packet.js: -------------------------------------------------------------------------------- 1 | require('fast-text-encoding') 2 | const util = require('./util') 3 | 4 | class Packet { 5 | constructor() { 6 | this.HEADER_LEN_SIZE = 4 7 | this.HEADER_LEN_TYPECODE = 2 8 | this.HEADER_LEN_ENCRYPT = 1 9 | this.HEADER_LEN_PLACEHOLDER = 1 10 | this.HEADER_LEN_TOTAL = this.HEADER_LEN_SIZE * 2 + 11 | this.HEADER_LEN_TYPECODE + 12 | this.HEADER_LEN_ENCRYPT + 13 | this.HEADER_LEN_PLACEHOLDER 14 | 15 | this.encoder = new TextEncoder() 16 | this.decoder = new TextDecoder() 17 | this.buffer = new ArrayBuffer(0) 18 | this.readLength = 0 19 | } 20 | 21 | concat() { 22 | const arr = [] 23 | for (let n = 0; n < arguments.length; n++) arr[n] = arguments[n] 24 | 25 | return arr.reduce(function (arr, buf) { 26 | const message = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf 27 | const t = new Uint8Array(arr.length + message.length) 28 | t.set(arr, 0) 29 | t.set(message, arr.length) 30 | return t 31 | }, new Uint8Array(0)) 32 | } 33 | 34 | Encode(data) { 35 | const msgBody = this.concat(this.encoder.encode(data), Uint8Array.of(0)) 36 | const msgLen = msgBody.length + this.HEADER_LEN_SIZE * 2 37 | const msgTotalLen = msgBody.length + this.HEADER_LEN_TOTAL 38 | const r = new DataView(new ArrayBuffer(msgTotalLen)) 39 | 40 | r.setUint32(0, msgLen, true) 41 | r.setUint32(4, msgLen, true) 42 | r.setInt16(8, 689, true) 43 | r.setInt16(10, 0, true) 44 | 45 | return new Uint8Array(r.buffer).set(msgBody, this.HEADER_LEN_TOTAL), r.buffer 46 | } 47 | 48 | Decode(buf, callback) { 49 | this.buffer = this.concat(this.buffer, buf).buffer 50 | while (this.buffer && this.buffer.byteLength > 0) { 51 | if (0 === this.readLength) { 52 | if (this.buffer.byteLength < 4) return; 53 | 54 | this.readLength = new DataView(this.buffer).getUint32(0, true) 55 | this.buffer = this.buffer.slice(4) 56 | } 57 | 58 | if (this.buffer.byteLength < this.readLength) return; 59 | 60 | const message = this.decoder.decode(this.buffer.slice(8, this.readLength - 1)) 61 | this.buffer = this.buffer.slice(this.readLength) 62 | this.readLength = 0 63 | callback(message) 64 | } 65 | } 66 | 67 | /** 68 | * blob转arraybuffer 69 | * @param {Blob} blob 待转换的Blob类型参数 70 | */ 71 | Blob2ab(blob) { 72 | return new Promise((resolve, reject) => { 73 | const reader = new FileReader() 74 | reader.onload = function (e) { 75 | resolve(e.target.result) 76 | } 77 | reader.readAsArrayBuffer(blob) 78 | }) 79 | } 80 | 81 | /** 82 | * arraybuffer转blob 83 | * @param {ArrayBuffer} ab 待转换的ArrayBuffer类型参数 84 | */ 85 | Ab2blob(ab) { 86 | return new Blob([ab]) 87 | } 88 | } 89 | 90 | module.exports = new Packet() -------------------------------------------------------------------------------- /douyu/douyudm/src/stt.js: -------------------------------------------------------------------------------- 1 | const util = require('./util') 2 | 3 | class STT { 4 | escape(v) { 5 | return util.isUndefined(v) ? '' : v.toString().replace(/@/g, '@A').replace(/\//g, '@S') 6 | } 7 | 8 | unescape(v) { 9 | return util.isUndefined(v) ? '' : v.toString().replace(/@A/g, '@').replace(/@S/g, '/') 10 | } 11 | 12 | serialize(raw) { 13 | if (util.isObject(raw)) { 14 | return Object.keys(raw).map(k => `${k}@=${this.escape(this.serialize(raw[k]))}/`).join('') 15 | } else if (Array.isArray(raw)) { 16 | return raw.map(v => `${this.escape(this.serialize(v))}/`).join('') 17 | } else if (util.isString(raw) || util.isNumber(raw)) { 18 | return raw.toString() 19 | } 20 | } 21 | 22 | deserialize(raw) { 23 | if (raw.includes('//')) { 24 | return raw.split('//').filter(e => e !== '').map(item => this.deserialize(item)) 25 | } 26 | 27 | if (raw.includes('@=')) { 28 | return raw.split('/').filter(e => e !== '').reduce((o, s) => { 29 | const [k, v] = s.split('@=') 30 | o[k] = this.deserialize(this.unescape(v)) 31 | return o 32 | }, {}) 33 | } else if (raw.includes('@A=')) { 34 | return this.deserialize(this.unescape(raw)) 35 | } else { 36 | return raw.toString() 37 | } 38 | } 39 | } 40 | 41 | module.exports = new STT() -------------------------------------------------------------------------------- /douyu/douyudm/src/util.js: -------------------------------------------------------------------------------- 1 | function Util() { 2 | [ 3 | 'Object', 4 | 'Array', 5 | 'String', 6 | 'Number', 7 | 'Boolean', 8 | 'Function', 9 | 'RegExp', 10 | 'Date', 11 | 'Undefined', 12 | 'Null', 13 | 'Symbol', 14 | 'Blob', 15 | 'ArrayBuffer' 16 | ].forEach(type => { 17 | this[`is${type}`] = (p) => Object.prototype.toString.call(p).slice(8, -1) === type 18 | }) 19 | } 20 | 21 | //判断浏览器环境 22 | Util.prototype.isBrowser = () => (typeof window !== 'undefined') ? true : false 23 | 24 | //随机数生成 25 | Util.prototype.random = (min, max) => Math.floor(Math.random() * (max - min + 1) + min) 26 | 27 | //web端下载文件 28 | Util.prototype.download = function(filename, text) { 29 | if (this.isBrowser()) { 30 | let element = document.createElement('a'); 31 | element.style.display = 'none' 32 | let blob = new Blob([text]) 33 | element.download = `${filename}.log` 34 | element.href = URL.createObjectURL(blob) 35 | document.body.appendChild(element) 36 | element.click() 37 | URL.revokeObjectURL(blob) 38 | document.body.removeChild(element) 39 | } 40 | } 41 | 42 | module.exports = new Util() -------------------------------------------------------------------------------- /douyu/douyudm/src/websocket.js: -------------------------------------------------------------------------------- 1 | const util = require('./util') 2 | 3 | class websocket { 4 | constructor(address) { 5 | this.listener = {} 6 | this.socket = new WebSocket(address) 7 | } 8 | 9 | send(data) { 10 | this.socket.send(data) 11 | } 12 | 13 | close(code, reason) { 14 | this.socket.close(code, reason) 15 | } 16 | 17 | on(method, callback) { 18 | let eventName = ['open', 'error', 'close', 'message'].find(event => event === method.toLocaleLowerCase()) 19 | 20 | if (eventName) { 21 | if (Object.keys(this.listener).includes(eventName)) { 22 | this.socket.removeEventListener(eventName, this.listener[eventName]) 23 | } 24 | 25 | this.listener[eventName] = callback 26 | this.socket.addEventListener(eventName, callback) 27 | } 28 | } 29 | } 30 | 31 | module.exports = util.isBrowser() ? websocket : require('ws') -------------------------------------------------------------------------------- /douyu/douyudm/test/old/BufferCoder.js: -------------------------------------------------------------------------------- 1 | require('fast-text-encoding') 2 | 3 | function BufferCoder() { 4 | this.buffer = new ArrayBuffer(0) 5 | this.decoder = new TextDecoder() 6 | this.encoder = new TextEncoder() 7 | this.littleEndian = !0 8 | this.readLength = 0 9 | } 10 | 11 | BufferCoder.prototype.concat = function () { 12 | const arr = [] 13 | for (let n = 0; n < arguments.length; n++) arr[n] = arguments[n] 14 | 15 | return arr.reduce(function (arr, buf) { 16 | const message = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf 17 | const t = new Uint8Array(arr.length + message.length) 18 | t.set(arr, 0) 19 | t.set(message, arr.length) 20 | return t 21 | }, new Uint8Array(0)) 22 | } 23 | 24 | BufferCoder.prototype.Decode = function (buf, callback, LE) { 25 | LE = LE || this.littleEndian 26 | this.buffer = this.concat(this.buffer, buf).buffer 27 | for (; this.buffer && this.buffer.byteLength > 0;) { 28 | if (0 === this.readLength) { 29 | if (this.buffer.byteLength < 4) return; 30 | 31 | this.readLength = new DataView(this.buffer).getUint32(0, LE) 32 | this.buffer = this.buffer.slice(4) 33 | } 34 | 35 | if (this.buffer.byteLength < this.readLength) return; 36 | 37 | const str = this.decoder.decode(this.buffer.slice(8, this.readLength - 1)) 38 | this.buffer = this.buffer.slice(this.readLength) 39 | this.readLength = 0 40 | callback(str) 41 | } 42 | } 43 | 44 | BufferCoder.prototype.Encode = function (str, LE) { 45 | LE = LE || this.littleEndian 46 | let message = this.concat(this.encoder.encode(str), Uint8Array.of(0)) 47 | let len = 8 + message.length 48 | let r = new DataView(new ArrayBuffer(len + 4)) 49 | let offset = 0 50 | 51 | r.setUint32(offset, len, LE) 52 | offset += 4 53 | r.setUint32(offset, len, LE) 54 | offset += 4 55 | r.setInt16(offset, 689, LE) 56 | offset += 2 57 | r.setInt8(offset, 0) 58 | offset += 1 59 | r.setInt8(offset, 0) 60 | offset += 1 61 | 62 | return new Uint8Array(r.buffer).set(message, offset), r.buffer 63 | } 64 | 65 | /** 66 | * blob转arraybuffer 67 | * @param {Blob} blob 待转换的Blob类型参数 68 | */ 69 | BufferCoder.prototype.blob2ab = function (blob) { 70 | return new Promise((resolve, reject) => { 71 | const reader = new FileReader() 72 | reader.onload = function (e) { 73 | resolve(e.target.result) 74 | } 75 | reader.readAsArrayBuffer(blob) 76 | }) 77 | } 78 | 79 | /** 80 | * arraybuffer转blob 81 | * @param {ArrayBuffer} ab 待转换的ArrayBuffer类型参数 82 | */ 83 | BufferCoder.prototype.ab2blob = function (ab) { 84 | return new Blob([ab]) 85 | } 86 | 87 | module.exports = new BufferCoder() -------------------------------------------------------------------------------- /douyu/douyudm/test/packet.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const danmaku = require('../src/index') 3 | const packet = danmaku.packet 4 | const stt = danmaku.stt 5 | const oldPacket = require('./old/BufferCoder') 6 | 7 | describe('packet.js 消息编码', function () { 8 | it('发送构建的消息', function () { 9 | const msg = stt.serialize({ 10 | type: 'chatmsg', 11 | nn: '河马( ̄。。 ̄)', 12 | ic: 'avatar_v3/201912/b99d77251eb643b5a88bb81863afea4e', 13 | cst: '1592152272402', 14 | brid: '0', 15 | lk: '', 16 | list: [{ 17 | lev: '1', 18 | num: '2' 19 | }, { 20 | lev: '7', 21 | num: '3' 22 | }] 23 | }) 24 | const oldMsg = oldPacket.Encode(msg) 25 | 26 | assert.deepEqual(packet.Encode(msg), oldMsg) 27 | }) 28 | }) 29 | 30 | describe('packet.js 消息解码', function () { 31 | it('多条消息响应', function () { 32 | const loginres = 'type@=loginres/userid@=0/roomgroup@=0/pg@=0/sessionid@=0/username@=/nickname@=/live_stat@=0/is_illegal@=0/ill_ct@=/ill_ts@=0/now@=0/ps@=0/es@=0/it@=0/its@=0/npv@=0/best_dlev@=0/cur_lev@=0/nrc@=1958850976/ih@=0/sid@=72193/sahf@=0/sceneid@=0/newrg@=0/regts@=0/ip@=27.17.93.42/' 33 | const pingreq = 'type@=pingreq/tick@=1592335920516/' 34 | const loginresBuf = oldPacket.Encode(loginres) 35 | const pingreqBuf = oldPacket.Encode(pingreq) 36 | const loginMsg = oldPacket.concat(loginresBuf, pingreqBuf) 37 | 38 | packet.Decode(loginMsg, data => { 39 | const r = stt.deserialize(data) 40 | if (r.type === 'loginres') { 41 | assert.equal(loginres, data) 42 | } else { 43 | assert.equal(pingreq, data) 44 | } 45 | }) 46 | }) 47 | 48 | it('单条消息响应', function () { 49 | const uenter = 'type@=uenter/rid@=102965/uid@=2069037/nn@=colaen/level@=29/ic@=avatar@S002@S06@S90@S37_avatar/rni@=0/el@=/sahf@=0/wgei@=0/' 50 | const uenterBuf = oldPacket.Encode(uenter) 51 | 52 | packet.Decode(uenterBuf, data => { 53 | assert.equal(uenter, data) 54 | }) 55 | }) 56 | }) -------------------------------------------------------------------------------- /douyu/douyudm/test/stt.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const danmaku = require('../src/index') 3 | const stt = danmaku.stt 4 | 5 | describe('stt.js 斗鱼序列化反序列化', function () { 6 | const test = { 7 | configscreenObj: { 8 | gbtemp: '192', 9 | nrt: '0', 10 | now: '1593273574796', 11 | btype: 'qzs202006', 12 | vsrc: 'https://rpic.douyucdn.cn/asrpic/200627/3374504_2350.png/dy1', 13 | otherContent: '狂欢火箭x2、狂欢卡x1、狂欢趴x3', 14 | avatar: 'https://apic.douyucdn.cn/upload/avatar_v3/202005/a5df465fd49b4826b853ad212556dafb_big.jpg', 15 | type: 'configscreen', 16 | rid: '3374504', 17 | userName: '', 18 | anchorName: '三岁伊丶' 19 | }, 20 | configscreenStr: 'gbtemp@=192/nrt@=0/now@=1593273574796/btype@=qzs202006/vsrc@=https:@S@Srpic.douyucdn.cn@Sasrpic@S200627@S3374504_2350.png@Sdy1/otherContent@=狂欢火箭x2、狂欢卡x1、狂欢趴x3/avatar@=https:@S@Sapic.douyucdn.cn@Supload@Savatar_v3@S202005@Sa5df465fd49b4826b853ad212556dafb_big.jpg/type@=configscreen/rid@=3374504/userName@=/anchorName@=三岁伊丶/', 21 | nlkstatusObj: {}, 22 | nlkstatusStr: 'idb@=nn@A=解说拒绝R@Sresult@A=3@Ssc@A=100@Snrt@A=0@Stopnn@A=烨烨夜月@Swinnum@A=0@Stopuid@A=29969620@Stopavt@A=https:@AS@ASapic.douyucdn.cn@ASupload@ASavatar_v3@AS202006@ASe39c178fe523404e8d903308195996c7_big.jpg@Svsrc@A=https:@AS@ASrpic.douyucdn.cn@ASasrpic@AS200628@AS102965_0045.png@ASdy1@Srid@A=102965@Savt@A=https:@AS@ASapic.douyucdn.cn@ASupload@ASavatar_v3@AS202001@ASc9755268a9fe4283b552513a37f376c4_big.jpg@Stop1sc@A=100@S/rida@=nn@A=糖小九吖@Sresult@A=1@Ssc@A=200@Snrt@A=0@Stopnn@A=恭喜我中奖发财@Swinnum@A=2@Stopuid@A=275508679@Stopavt@A=https:@AS@ASapic.douyucdn.cn@ASupload@ASavatar@ASdefault@AS12_big.jpg@Svsrc@A=https:@AS@ASrpic.douyucdn.cn@ASasrpic@AS200628@AS6585782_0045.png@ASdy1@Srid@A=6585782@Savt@A=https:@AS@ASapic.douyucdn.cn@ASupload@ASavatar_v3@AS201909@AS295e0d5b1f0d4adcaf59a5ce89bed5bb_big.jpg@Stop1sc@A=200@S/pkId@=696079/is_exc@=0/show_mvp@=1/time@=1593276404/type@=nlkstatus/ts@=1593276405792/status@=3/', 23 | chatMsgObj: { 24 | type: 'chatmsg', 25 | rid: '605964', 26 | ct: '1', 27 | uid: '355384536', 28 | nn: '用户16301824', 29 | txt: '加油', 30 | cid: 'fcb83b44585a4ee00adc320100000000', 31 | ic: 'avatar/default/01', 32 | level: '2', 33 | sahf: '0', 34 | cst: '1593273451933', 35 | bnn: '', 36 | bl: '0', 37 | brid: '0', 38 | hc: '', 39 | mgt: '6', 40 | mid: '50010', 41 | mtn: '白 鲨', 42 | ml: '3', 43 | gl: '11', 44 | ms: '0', 45 | el: '', 46 | lk: '', 47 | hb: '140/', 48 | urlev: '1', 49 | dms: '6', 50 | pdg: '68', 51 | pdk: '8' 52 | }, 53 | chatmsgStr: 'type@=chatmsg/rid@=605964/ct@=1/uid@=355384536/nn@=用户16301824/txt@=加油/cid@=fcb83b44585a4ee00adc320100000000/ic@=avatar@Sdefault@S01/level@=2/sahf@=0/cst@=1593273451933/bnn@=/bl@=0/brid@=0/hc@=/mgt@=6/mid@=50010/mtn@=白 鲨/ml@=3/gl@=11/ms@=0/el@=/lk@=/hb@=140@S/urlev@=1/dms@=6/pdg@=68/pdk@=8/', 54 | nobleNumInfoObj: { 55 | type: 'noble_num_info', 56 | sum: '26', 57 | vn: '557', 58 | rid: '102965', 59 | list: [{ 60 | lev: '4', 61 | num: '1' 62 | }, { 63 | lev: '1', 64 | num: '2' 65 | }, { 66 | lev: '7', 67 | num: '23' 68 | }] 69 | }, 70 | nobleNumInfoStr: 'type@=noble_num_info/sum@=26/vn@=557/rid@=102965/list@=lev@AA=4@ASnum@AA=1@AS@Slev@AA=1@ASnum@AA=2@AS@Slev@AA=7@ASnum@AA=23@AS@S/', 71 | dgbObj: { 72 | type: 'dgb', 73 | rid: '102965', 74 | gfid: '824', 75 | gs: '0', 76 | uid: '63912680', 77 | nn: '昂一文', 78 | ic: 'avatar_v3/202001/01dd1da4833f4a398f96231dda2deb5a', 79 | eid: '0', 80 | eic: '20052', 81 | level: '16', 82 | dw: '0', 83 | gfcnt: '1', 84 | hits: '2', 85 | bcnt: '2', 86 | bst: '2', 87 | nl: '7', 88 | ct: '2', 89 | el: '', 90 | cm: '0', 91 | bnn: '拒绝R', 92 | bl: '9', 93 | brid: '102965', 94 | hc: 'd48b0bb9c375e34fb20074a424b9f0ef', 95 | sahf: '0', 96 | fc: '0', 97 | gpf: '1', 98 | pid: '268', 99 | bnid: '1', 100 | bnl: '1', 101 | receive_uid: '3195592', 102 | receive_nn: '解说拒绝R', 103 | from: '2' 104 | }, 105 | dgbStr: 'type@=dgb/rid@=102965/gfid@=824/gs@=0/uid@=63912680/nn@=昂一文/ic@=avatar_v3@S202001@S01dd1da4833f4a398f96231dda2deb5a/eid@=0/eic@=20052/level@=16/dw@=0/gfcnt@=1/hits@=2/bcnt@=2/bst@=2/nl@=7/ct@=2/el@=/cm@=0/bnn@=拒绝R/bl@=9/brid@=102965/hc@=d48b0bb9c375e34fb20074a424b9f0ef/sahf@=0/fc@=0/gpf@=1/pid@=268/bnid@=1/bnl@=1/receive_uid@=3195592/receive_nn@=解说拒绝R/from@=2/', 106 | uenterObj: { 107 | type: 'uenter', 108 | rid: '102965', 109 | uid: '208936492', 110 | nn: '水边一鸣', 111 | level: '33', 112 | rg: '4', 113 | ic: 'avatar_v3/201812/8e95f0bfa54e4feda0716c3593816131', 114 | nl: '7', 115 | rni: '0', 116 | el: '', 117 | sahf: '0', 118 | wgei: '0', 119 | fl: '19' 120 | }, 121 | uenterStr: 'type@=uenter/rid@=102965/uid@=208936492/nn@=水边一鸣/level@=33/rg@=4/ic@=avatar_v3@S201812@S8e95f0bfa54e4feda0716c3593816131/nl@=7/rni@=0/el@=/sahf@=0/wgei@=0/fl@=19/', 122 | } 123 | 124 | it('序列化', function () { 125 | console.log('[stt.serialize]', stt.serialize(test.uenterObj)) 126 | assert.equal(stt.serialize(test.uenterObj), test.uenterStr) 127 | }) 128 | 129 | it('反序列化', function () { 130 | console.log('[stt.deserialize]', stt.deserialize(test.uenterStr)) 131 | assert.deepEqual(stt.deserialize(test.uenterStr), test.uenterObj) 132 | }) 133 | }) -------------------------------------------------------------------------------- /douyu/fast-text-encoding/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | -------------------------------------------------------------------------------- /douyu/fast-text-encoding/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /douyu/fast-text-encoding/README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://api.travis-ci.org/samthor/fast-text-encoding.svg?branch=master)](https://travis-ci.org/samthor/fast-text-encoding) 2 | 3 | This is a fast polyfill for [`TextEncoder`][1] and [`TextDecoder`][2], which let you encode and decode JavaScript strings into UTF-8 bytes. 4 | 5 | It is fast partially as it does not support any encodings aside UTF-8 (and note that natively, only `TextDecoder` supports alternative encodings anyway). 6 | See [some benchmarks](https://github.com/samthor/fast-text-encoding/tree/master/bench). 7 | 8 | [1]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder 9 | [2]: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder 10 | 11 | # Usage 12 | 13 | Install as "fast-text-encoding" via your favourite package manager. 14 | 15 | You only need this polyfill if you're supporting older browsers like IE, legacy Edge, ancient Chrome and Firefox, or Node before v11. 16 | 17 | ## Browser 18 | 19 | Include the minified code inside a `script` tag or as an ES6 Module for its side effects. 20 | It will create `TextEncoder` and `TextDecoder` if the symbols are missing on `window` or `global.` 21 | 22 | ```html 23 | 24 | 29 | ``` 30 | 31 | ⚠️ You'll probably want to depend on `text.min.js`, as it's compiled to ES5 for older environments. 32 | 33 | ## Node 34 | 35 | You only need this polyfill in Node before v11. 36 | However, you can use `Buffer` to provide the same functionality (but not conforming to any spec) in versions even older than that. 37 | 38 | ```js 39 | require('fast-text-encoding'); // just require me before use 40 | 41 | const buffer = new TextEncoder().encode('Turn me into UTF-8!'); 42 | // buffer is now a Uint8Array of [84, 117, 114, 110, ...] 43 | ``` 44 | 45 | In Node v5.1 and above, this polyfill uses `Buffer` to implement `TextDecoder`. 46 | 47 | # Release 48 | 49 | Compile code with [Closure Compiler](https://closure-compiler.appspot.com/home). 50 | 51 | ``` 52 | // ==ClosureCompiler== 53 | // @compilation_level ADVANCED_OPTIMIZATIONS 54 | // @output_file_name text.min.js 55 | // ==/ClosureCompiler== 56 | 57 | // code here 58 | ``` 59 | -------------------------------------------------------------------------------- /douyu/fast-text-encoding/compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | google-closure-compiler \ 4 | --compilation_level ADVANCED \ 5 | --js_output_file text.min.js \ 6 | --language_out ECMASCRIPT5 \ 7 | --warning_level VERBOSE \ 8 | --create_source_map %outname%.map \ 9 | --externs externs.js \ 10 | text.js 11 | -------------------------------------------------------------------------------- /douyu/fast-text-encoding/externs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @constructor 4 | */ 5 | var Buffer; 6 | 7 | /** 8 | * @param {!ArrayBuffer} raw 9 | * @param {number} byteOffset 10 | * @param {number} byteLength 11 | * @return {!Buffer} 12 | */ 13 | Buffer.from = function(raw, byteOffset, byteLength) {}; 14 | 15 | /** 16 | * @this {*} 17 | * @param {string} encoding 18 | * @return {string} 19 | */ 20 | Buffer.prototype.toString = function(encoding) {}; 21 | -------------------------------------------------------------------------------- /douyu/fast-text-encoding/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "fast-text-encoding@^1.0.0", 3 | "_id": "fast-text-encoding@1.0.3", 4 | "_inBundle": false, 5 | "_integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", 6 | "_location": "/fast-text-encoding", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "fast-text-encoding@^1.0.0", 12 | "name": "fast-text-encoding", 13 | "escapedName": "fast-text-encoding", 14 | "rawSpec": "^1.0.0", 15 | "saveSpec": null, 16 | "fetchSpec": "^1.0.0" 17 | }, 18 | "_requiredBy": [ 19 | "/douyudm" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", 22 | "_shasum": "ec02ac8e01ab8a319af182dae2681213cfe9ce53", 23 | "_spec": "fast-text-encoding@^1.0.0", 24 | "_where": "/main/douyudanmu/node_modules/douyudm", 25 | "author": { 26 | "name": "Sam Thorogood", 27 | "email": "sam.thorogood@gmail.com" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/samthor/fast-text-encoding/issues" 31 | }, 32 | "bundleDependencies": false, 33 | "deprecated": false, 34 | "description": "Fast polyfill for TextEncoder and TextDecoder, only supports utf-8", 35 | "devDependencies": { 36 | "chai": "^4.2.0", 37 | "google-closure-compiler": "^20200406.0.0", 38 | "mocha": "^7.1.0" 39 | }, 40 | "homepage": "https://github.com/samthor/fast-text-encoding#readme", 41 | "license": "Apache-2.0", 42 | "main": "text.min.js", 43 | "name": "fast-text-encoding", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/samthor/fast-text-encoding.git" 47 | }, 48 | "scripts": { 49 | "compile": "./compile.sh", 50 | "test": "mocha" 51 | }, 52 | "version": "1.0.3" 53 | } 54 | -------------------------------------------------------------------------------- /douyu/fast-text-encoding/text.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Sam Thorogood. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Polyfill for TextEncoder and TextDecoder. 19 | * 20 | * You probably want `text.min.js`, and not this file directly. 21 | */ 22 | 23 | (function(scope) { 24 | 'use strict'; 25 | 26 | // fail early 27 | if (scope['TextEncoder'] && scope['TextDecoder']) { 28 | return false; 29 | } 30 | 31 | // used for FastTextDecoder 32 | const validUtfLabels = ['utf-8', 'utf8', 'unicode-1-1-utf-8']; 33 | 34 | /** 35 | * @constructor 36 | */ 37 | function FastTextEncoder() { 38 | // This does not accept an encoding, and always uses UTF-8: 39 | // https://www.w3.org/TR/encoding/#dom-textencoder 40 | } 41 | 42 | Object.defineProperty(FastTextEncoder.prototype, 'encoding', {value: 'utf-8'}); 43 | 44 | /** 45 | * @param {string} string 46 | * @param {{stream: boolean}=} options 47 | * @return {!Uint8Array} 48 | */ 49 | FastTextEncoder.prototype['encode'] = function(string, options={stream: false}) { 50 | if (options.stream) { 51 | throw new Error(`Failed to encode: the 'stream' option is unsupported.`); 52 | } 53 | 54 | let pos = 0; 55 | const len = string.length; 56 | 57 | let at = 0; // output position 58 | let tlen = Math.max(32, len + (len >>> 1) + 7); // 1.5x size 59 | let target = new Uint8Array((tlen >>> 3) << 3); // ... but at 8 byte offset 60 | 61 | while (pos < len) { 62 | let value = string.charCodeAt(pos++); 63 | if (value >= 0xd800 && value <= 0xdbff) { 64 | // high surrogate 65 | if (pos < len) { 66 | const extra = string.charCodeAt(pos); 67 | if ((extra & 0xfc00) === 0xdc00) { 68 | ++pos; 69 | value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; 70 | } 71 | } 72 | if (value >= 0xd800 && value <= 0xdbff) { 73 | continue; // drop lone surrogate 74 | } 75 | } 76 | 77 | // expand the buffer if we couldn't write 4 bytes 78 | if (at + 4 > target.length) { 79 | tlen += 8; // minimum extra 80 | tlen *= (1.0 + (pos / string.length) * 2); // take 2x the remaining 81 | tlen = (tlen >>> 3) << 3; // 8 byte offset 82 | 83 | const update = new Uint8Array(tlen); 84 | update.set(target); 85 | target = update; 86 | } 87 | 88 | if ((value & 0xffffff80) === 0) { // 1-byte 89 | target[at++] = value; // ASCII 90 | continue; 91 | } else if ((value & 0xfffff800) === 0) { // 2-byte 92 | target[at++] = ((value >>> 6) & 0x1f) | 0xc0; 93 | } else if ((value & 0xffff0000) === 0) { // 3-byte 94 | target[at++] = ((value >>> 12) & 0x0f) | 0xe0; 95 | target[at++] = ((value >>> 6) & 0x3f) | 0x80; 96 | } else if ((value & 0xffe00000) === 0) { // 4-byte 97 | target[at++] = ((value >>> 18) & 0x07) | 0xf0; 98 | target[at++] = ((value >>> 12) & 0x3f) | 0x80; 99 | target[at++] = ((value >>> 6) & 0x3f) | 0x80; 100 | } else { 101 | continue; // out of range 102 | } 103 | 104 | target[at++] = (value & 0x3f) | 0x80; 105 | } 106 | 107 | // Use subarray if slice isn't supported (IE11). This will use more memory 108 | // because the original array still exists. 109 | return target.slice ? target.slice(0, at) : target.subarray(0, at); 110 | } 111 | 112 | /** 113 | * @constructor 114 | * @param {string=} utfLabel 115 | * @param {{fatal: boolean}=} options 116 | */ 117 | function FastTextDecoder(utfLabel='utf-8', options={fatal: false}) { 118 | if (validUtfLabels.indexOf(utfLabel.toLowerCase()) === -1) { 119 | throw new RangeError( 120 | `Failed to construct 'TextDecoder': The encoding label provided ('${utfLabel}') is invalid.`); 121 | } 122 | if (options.fatal) { 123 | throw new Error(`Failed to construct 'TextDecoder': the 'fatal' option is unsupported.`); 124 | } 125 | } 126 | 127 | Object.defineProperty(FastTextDecoder.prototype, 'encoding', {value: 'utf-8'}); 128 | 129 | Object.defineProperty(FastTextDecoder.prototype, 'fatal', {value: false}); 130 | 131 | Object.defineProperty(FastTextDecoder.prototype, 'ignoreBOM', {value: false}); 132 | 133 | /** 134 | * @param {!Uint8Array} bytes 135 | * @return {string} 136 | */ 137 | function decodeBuffer(bytes) { 138 | return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('utf-8'); 139 | } 140 | 141 | /** 142 | * @param {!Uint8Array} bytes 143 | * @return {string} 144 | */ 145 | function decodeSyncXHR(bytes) { 146 | const b = new Blob([bytes], {type: 'text/plain;charset=UTF-8'}); 147 | const u = URL.createObjectURL(b); 148 | 149 | // This hack will fail in non-Edgium Edge because sync XHRs are disabled (and 150 | // possibly in other places), so ensure there's a fallback call. 151 | try { 152 | const x = new XMLHttpRequest(); 153 | x.open('GET', u, false); 154 | x.send(); 155 | return x.responseText; 156 | } catch (e) { 157 | return decodeFallback(bytes); 158 | } finally { 159 | URL.revokeObjectURL(u); 160 | } 161 | } 162 | 163 | /** 164 | * @param {!Uint8Array} bytes 165 | * @return {string} 166 | */ 167 | function decodeFallback(bytes) { 168 | let inputIndex = 0; 169 | 170 | // Create a working buffer for UTF-16 code points, but don't generate one 171 | // which is too large for small input sizes. UTF-8 to UCS-16 conversion is 172 | // going to be at most 1:1, if all code points are ASCII. The other extreme 173 | // is 4-byte UTF-8, which results in two UCS-16 points, but this is still 50% 174 | // fewer entries in the output. 175 | const pendingSize = Math.min(256 * 256, bytes.length + 1); 176 | const pending = new Uint16Array(pendingSize); 177 | const chunks = []; 178 | let pendingIndex = 0; 179 | 180 | for (;;) { 181 | const more = inputIndex < bytes.length; 182 | 183 | // If there's no more data or there'd be no room for two UTF-16 values, 184 | // create a chunk. This isn't done at the end by simply slicing the data 185 | // into equal sized chunks as we might hit a surrogate pair. 186 | if (!more || (pendingIndex >= pendingSize - 1)) { 187 | // nb. .apply and friends are *really slow*. Low-hanging fruit is to 188 | // expand this to literally pass pending[0], pending[1], ... etc, but 189 | // the output code expands pretty fast in this case. 190 | chunks.push(String.fromCharCode.apply(null, pending.subarray(0, pendingIndex))); 191 | 192 | if (!more) { 193 | return chunks.join(''); 194 | } 195 | 196 | // Move the buffer forward and create another chunk. 197 | bytes = bytes.subarray(inputIndex); 198 | inputIndex = 0; 199 | pendingIndex = 0; 200 | } 201 | 202 | // The native TextDecoder will generate "REPLACEMENT CHARACTER" where the 203 | // input data is invalid. Here, we blindly parse the data even if it's 204 | // wrong: e.g., if a 3-byte sequence doesn't have two valid continuations. 205 | 206 | const byte1 = bytes[inputIndex++]; 207 | if ((byte1 & 0x80) === 0) { // 1-byte or null 208 | pending[pendingIndex++] = byte1; 209 | } else if ((byte1 & 0xe0) === 0xc0) { // 2-byte 210 | const byte2 = bytes[inputIndex++] & 0x3f; 211 | pending[pendingIndex++] = ((byte1 & 0x1f) << 6) | byte2; 212 | } else if ((byte1 & 0xf0) === 0xe0) { // 3-byte 213 | const byte2 = bytes[inputIndex++] & 0x3f; 214 | const byte3 = bytes[inputIndex++] & 0x3f; 215 | pending[pendingIndex++] = ((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3; 216 | } else if ((byte1 & 0xf8) === 0xf0) { // 4-byte 217 | const byte2 = bytes[inputIndex++] & 0x3f; 218 | const byte3 = bytes[inputIndex++] & 0x3f; 219 | const byte4 = bytes[inputIndex++] & 0x3f; 220 | 221 | // this can be > 0xffff, so possibly generate surrogates 222 | let codepoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; 223 | if (codepoint > 0xffff) { 224 | // codepoint &= ~0x10000; 225 | codepoint -= 0x10000; 226 | pending[pendingIndex++] = (codepoint >>> 10) & 0x3ff | 0xd800; 227 | codepoint = 0xdc00 | codepoint & 0x3ff; 228 | } 229 | pending[pendingIndex++] = codepoint; 230 | } else { 231 | // invalid initial byte 232 | } 233 | } 234 | } 235 | 236 | // Decoding a string is pretty slow, but use alternative options where possible. 237 | let decodeImpl = decodeFallback; 238 | if (typeof Buffer === 'function' && Buffer.from) { 239 | // Buffer.from was added in Node v5.10.0 (2015-11-17). 240 | decodeImpl = decodeBuffer; 241 | } else if (typeof Blob === 'function' && typeof URL === 'function' && typeof URL.createObjectURL === 'function') { 242 | // Blob and URL.createObjectURL are available from IE10, Safari 6, Chrome 19 243 | // (all released in 2012), Firefox 19 (2013), ... 244 | decodeImpl = decodeSyncXHR; 245 | } 246 | 247 | /** 248 | * @param {(!ArrayBuffer|!ArrayBufferView)} buffer 249 | * @param {{stream: boolean}=} options 250 | * @return {string} 251 | */ 252 | FastTextDecoder.prototype['decode'] = function(buffer, options={stream: false}) { 253 | if (options['stream']) { 254 | throw new Error(`Failed to decode: the 'stream' option is unsupported.`); 255 | } 256 | 257 | let bytes; 258 | 259 | if (buffer instanceof Uint8Array) { 260 | // Accept Uint8Array instances as-is. 261 | bytes = buffer; 262 | } else if (buffer.buffer instanceof ArrayBuffer) { 263 | // Look for ArrayBufferView, which isn't a real type, but basically 264 | // represents all the valid TypedArray types plus DataView. They all have 265 | // ".buffer" as an instance of ArrayBuffer. 266 | bytes = new Uint8Array(buffer.buffer); 267 | } else { 268 | // The only other valid argument here is that "buffer" is an ArrayBuffer. 269 | // We also try to convert anything else passed to a Uint8Array, as this 270 | // catches anything that's array-like. Native code would throw here. 271 | bytes = new Uint8Array(buffer); 272 | } 273 | 274 | return decodeImpl(/** @type {!Uint8Array} */ (bytes)); 275 | } 276 | 277 | scope['TextEncoder'] = FastTextEncoder; 278 | scope['TextDecoder'] = FastTextDecoder; 279 | 280 | }(typeof window !== 'undefined' ? window : (typeof global !== 'undefined' ? global : this))); 281 | -------------------------------------------------------------------------------- /douyu/fast-text-encoding/text.min.js: -------------------------------------------------------------------------------- 1 | (function(l){function m(){}function k(a,c){a=void 0===a?"utf-8":a;c=void 0===c?{fatal:!1}:c;if(-1===r.indexOf(a.toLowerCase()))throw new RangeError("Failed to construct 'TextDecoder': The encoding label provided ('"+a+"') is invalid.");if(c.fatal)throw Error("Failed to construct 'TextDecoder': the 'fatal' option is unsupported.");}function t(a){return Buffer.from(a.buffer,a.byteOffset,a.byteLength).toString("utf-8")}function u(a){var c=URL.createObjectURL(new Blob([a],{type:"text/plain;charset=UTF-8"})); 2 | try{var f=new XMLHttpRequest;f.open("GET",c,!1);f.send();return f.responseText}catch(e){return q(a)}finally{URL.revokeObjectURL(c)}}function q(a){for(var c=0,f=Math.min(65536,a.length+1),e=new Uint16Array(f),h=[],d=0;;){var b=c=f-1){h.push(String.fromCharCode.apply(null,e.subarray(0,d)));if(!b)return h.join("");a=a.subarray(c);d=c=0}b=a[c++];if(0===(b&128))e[d++]=b;else if(192===(b&224)){var g=a[c++]&63;e[d++]=(b&31)<<6|g}else if(224===(b&240)){g=a[c++]&63;var n=a[c++]&63;e[d++]= 3 | (b&31)<<12|g<<6|n}else if(240===(b&248)){g=a[c++]&63;n=a[c++]&63;var v=a[c++]&63;b=(b&7)<<18|g<<12|n<<6|v;65535>>10&1023|55296,b=56320|b&1023);e[d++]=b}}}if(l.TextEncoder&&l.TextDecoder)return!1;var r=["utf-8","utf8","unicode-1-1-utf-8"];Object.defineProperty(m.prototype,"encoding",{value:"utf-8"});m.prototype.encode=function(a,c){c=void 0===c?{stream:!1}:c;if(c.stream)throw Error("Failed to encode: the 'stream' option is unsupported.");c=0;for(var f=a.length,e=0,h=Math.max(32, 4 | f+(f>>>1)+7),d=new Uint8Array(h>>>3<<3);c=b){if(c=b)continue}e+4>d.length&&(h+=8,h*=1+c/a.length*2,h=h>>>3<<3,g=new Uint8Array(h),g.set(d),d=g);if(0===(b&4294967168))d[e++]=b;else{if(0===(b&4294965248))d[e++]=b>>>6&31|192;else if(0===(b&4294901760))d[e++]=b>>>12&15|224,d[e++]=b>>>6&63|128;else if(0===(b&4292870144))d[e++]=b>>>18&7|240,d[e++]=b>>>12& 5 | 63|128,d[e++]=b>>>6&63|128;else continue;d[e++]=b&63|128}}return d.slice?d.slice(0,e):d.subarray(0,e)};Object.defineProperty(k.prototype,"encoding",{value:"utf-8"});Object.defineProperty(k.prototype,"fatal",{value:!1});Object.defineProperty(k.prototype,"ignoreBOM",{value:!1});var p=q;"function"===typeof Buffer&&Buffer.from?p=t:"function"===typeof Blob&&"function"===typeof URL&&"function"===typeof URL.createObjectURL&&(p=u);k.prototype.decode=function(a,c){c=void 0===c?{stream:!1}:c;if(c.stream)throw Error("Failed to decode: the 'stream' option is unsupported."); 6 | a=a instanceof Uint8Array?a:a.buffer instanceof ArrayBuffer?new Uint8Array(a.buffer):new Uint8Array(a);return p(a)};l.TextEncoder=m;l.TextDecoder=k})("undefined"!==typeof window?window:"undefined"!==typeof global?global:this); 7 | -------------------------------------------------------------------------------- /douyu/fast-text-encoding/text.min.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version":3, 3 | "file":"text.min.js", 4 | "lineCount":6, 5 | "mappings":"AAsBC,SAAQ,CAACA,CAAD,CAAQ,CAcjBC,QAASA,EAAe,EAAG,EAgF3BC,QAASA,EAAe,CAACC,CAAD,CAAmBC,CAAnB,CAA2C,CAA1CD,CAAA,CAAA,IAAA,EAAA,GAAAA,CAAA,CAAS,OAAT,CAAAA,CAAkBC,EAAA,CAAA,IAAA,EAAA,GAAAA,CAAA,CAAQ,CAACC,MAAO,CAAA,CAAR,CAAR,CAAAD,CACzC,IAAuD,EAAvD,GAAIE,CAAAC,QAAA,CAAuBJ,CAAAK,YAAA,EAAvB,CAAJ,CACE,KAAM,KAAIC,UAAJ,CACJ,mEADI,CACgEN,CADhE,CACJ,gBADI,CAAN,CAGF,GAAIC,CAAAC,MAAJ,CACE,KAAUK,MAAJ,CAAU,uEAAV,CAAN,CAN+D,CAoBnEC,QAASA,EAAY,CAACC,CAAD,CAAQ,CAC3B,MAAOC,OAAAC,KAAA,CAAYF,CAAAG,OAAZ,CAA0BH,CAAAI,WAA1B,CAA4CJ,CAAAK,WAA5C,CAAAC,SAAA,CAAuE,OAAvE,CADoB,CAQ7BC,QAASA,EAAa,CAACP,CAAD,CAAQ,CAE5B,IAAMQ,EAAIC,GAAAC,gBAAA,CADAC,IAAIC,IAAJD,CAAS,CAACX,CAAD,CAATW,CAAkB,CAACE,KAAM,0BAAP,CAAlBF,CACA,CAIV;GAAI,CACF,IAAMG,EAAI,IAAIC,cACdD,EAAAE,KAAA,CAAO,KAAP,CAAcR,CAAd,CAAiB,CAAA,CAAjB,CACAM,EAAAG,KAAA,EACA,OAAOH,EAAAI,aAJL,CAKF,MAAOC,CAAP,CAAU,CACV,MAAOC,EAAA,CAAepB,CAAf,CADG,CALZ,OAOU,CACRS,GAAAY,gBAAA,CAAoBb,CAApB,CADQ,CAbkB,CAsB9BY,QAASA,EAAc,CAACpB,CAAD,CAAQ,CAa7B,IAZA,IAAIsB,EAAa,CAAjB,CAOMC,EAAcC,IAAAC,IAAA,CAAS,KAAT,CAAoBzB,CAAA0B,OAApB,CAAmC,CAAnC,CAPpB,CAQMC,EAAU,IAAIC,WAAJ,CAAgBL,CAAhB,CARhB,CASMM,EAAS,EATf,CAUIC,EAAe,CAEnB,CAAA,CAAA,CAAS,CACP,IAAMC,EAAOT,CAAPS,CAAoB/B,CAAA0B,OAK1B,IAAI,CAACK,CAAL,EAAcD,CAAd,EAA8BP,CAA9B,CAA4C,CAA5C,CAAgD,CAI9CM,CAAAG,KAAA,CAAYC,MAAAC,aAAAC,MAAA,CAA0B,IAA1B,CAAgCR,CAAAS,SAAA,CAAiB,CAAjB,CAAoBN,CAApB,CAAhC,CAAZ,CAEA,IAAI,CAACC,CAAL,CACE,MAAOF,EAAAQ,KAAA,CAAY,EAAZ,CAITrC,EAAA,CAAQA,CAAAoC,SAAA,CAAed,CAAf,CAERQ,EAAA,CADAR,CACA,CADa,CAZiC,CAoB1CgB,CAAAA,CAAQtC,CAAA,CAAMsB,CAAA,EAAN,CACd,IAAuB,CAAvB,IAAKgB,CAAL,CAAa,GAAb,EACEX,CAAA,CAAQG,CAAA,EAAR,CAAA,CAA0BQ,CAD5B,KAEO,IAAuB,GAAvB,IAAKA,CAAL,CAAa,GAAb,EAA6B,CAClC,IAAMC,EAAQvC,CAAA,CAAMsB,CAAA,EAAN,CAARiB,CAA8B,EACpCZ,EAAA,CAAQG,CAAA,EAAR,CAAA,EAA4BQ,CAA5B,CAAoC,EAApC,GAA6C,CAA7C,CAAkDC,CAFhB,CAA7B,IAGA,IAAuB,GAAvB,IAAKD,CAAL,CAAa,GAAb,EAA6B,CAC5BC,CAAAA,CAAQvC,CAAA,CAAMsB,CAAA,EAAN,CAARiB,CAA8B,EACpC,KAAMC,EAAQxC,CAAA,CAAMsB,CAAA,EAAN,CAARkB,CAA8B,EACpCb,EAAA,CAAQG,CAAA,EAAR,CAAA;CAA4BQ,CAA5B,CAAoC,EAApC,GAA6C,EAA7C,CAAoDC,CAApD,EAA6D,CAA7D,CAAkEC,CAHhC,CAA7B,IAIA,IAAuB,GAAvB,IAAKF,CAAL,CAAa,GAAb,EAA6B,CAC5BC,CAAAA,CAAQvC,CAAA,CAAMsB,CAAA,EAAN,CAARiB,CAA8B,EAC9BC,EAAAA,CAAQxC,CAAA,CAAMsB,CAAA,EAAN,CAARkB,CAA8B,EACpC,KAAMC,EAAQzC,CAAA,CAAMsB,CAAA,EAAN,CAARmB,CAA8B,EAGhCC,EAAAA,EAAcJ,CAAdI,CAAsB,CAAtBA,GAA+B,EAA/BA,CAAwCH,CAAxCG,EAAiD,EAAjDA,CAA0DF,CAA1DE,EAAmE,CAAnEA,CAA2ED,CAC/D,MAAhB,CAAIC,CAAJ,GAEEA,CAEA,EAFa,KAEb,CADAf,CAAA,CAAQG,CAAA,EAAR,CACA,CAD2BY,CAC3B,GADyC,EACzC,CAD+C,IAC/C,CADuD,KACvD,CAAAA,CAAA,CAAY,KAAZ,CAAqBA,CAArB,CAAiC,IAJnC,CAMAf,EAAA,CAAQG,CAAA,EAAR,CAAA,CAA0BY,CAbQ,CApC7B,CAboB,CA5I/B,GAAItD,CAAA,YAAJ,EAA4BA,CAAA,YAA5B,CACE,MAAO,CAAA,CAIT,KAAMM,EAAiB,CAAC,OAAD,CAAU,MAAV,CAAkB,mBAAlB,CAUvBiD,OAAAC,eAAA,CAAsBvD,CAAAwD,UAAtB,CAAiD,UAAjD,CAA6D,CAACC,MAAO,OAAR,CAA7D,CAOAzD,EAAAwD,UAAA,OAAA,CAAsC,QAAQ,CAACE,CAAD,CAASvD,CAAT,CAAkC,CAAzBA,CAAA,CAAA,IAAA,EAAA,GAAAA,CAAA,CAAQ,CAACwD,OAAQ,CAAA,CAAT,CAAR,CAAAxD,CACrD,IAAIA,CAAAwD,OAAJ,CACE,KAAUlD,MAAJ,CAAU,uDAAV,CAAN,CAGEmD,CAAAA,CAAM,CAOV,KANA,IAAMC,EAAMH,CAAArB,OAAZ,CAEIyB,EAAK,CAFT,CAGIC,EAAO5B,IAAA6B,IAAA,CAAS,EAAT;AAAaH,CAAb,EAAoBA,CAApB,GAA4B,CAA5B,EAAiC,CAAjC,CAHX,CAIII,EAAS,IAAIC,UAAJ,CAAgBH,CAAhB,GAAyB,CAAzB,EAA+B,CAA/B,CAEb,CAAOH,CAAP,CAAaC,CAAb,CAAA,CAAkB,CAChB,IAAIJ,EAAQC,CAAAS,WAAA,CAAkBP,CAAA,EAAlB,CACZ,IAAa,KAAb,EAAIH,CAAJ,EAAgC,KAAhC,EAAuBA,CAAvB,CAAwC,CAEtC,GAAIG,CAAJ,CAAUC,CAAV,CAAe,CACb,IAAMO,EAAQV,CAAAS,WAAA,CAAkBP,CAAlB,CACW,MAAzB,IAAKQ,CAAL,CAAa,KAAb,IACE,EAAER,CACF,CAAAH,CAAA,GAAUA,CAAV,CAAkB,IAAlB,GAA4B,EAA5B,GAAmCW,CAAnC,CAA2C,IAA3C,EAAoD,KAFtD,CAFa,CAOf,GAAa,KAAb,EAAIX,CAAJ,EAAgC,KAAhC,EAAuBA,CAAvB,CACE,QAVoC,CAepCK,CAAJ,CAAS,CAAT,CAAaG,CAAA5B,OAAb,GACE0B,CAMA,EANQ,CAMR,CALAA,CAKA,EALS,CAKT,CALgBH,CAKhB,CALsBF,CAAArB,OAKtB,CALuC,CAKvC,CAJA0B,CAIA,CAJQA,CAIR,GAJiB,CAIjB,EAJuB,CAIvB,CAFMM,CAEN,CAFe,IAAIH,UAAJ,CAAeH,CAAf,CAEf,CADAM,CAAAC,IAAA,CAAWL,CAAX,CACA,CAAAA,CAAA,CAASI,CAPX,CAUA,IAA6B,CAA7B,IAAKZ,CAAL,CAAa,UAAb,EACEQ,CAAA,CAAOH,CAAA,EAAP,CAAA,CAAeL,CADjB,KAGO,CAAA,GAA6B,CAA7B,IAAKA,CAAL,CAAa,UAAb,EACLQ,CAAA,CAAOH,CAAA,EAAP,CAAA,CAAiBL,CAAjB,GAA4B,CAA5B,CAAiC,EAAjC,CAAyC,GADpC,KAEA,IAA6B,CAA7B,IAAKA,CAAL,CAAa,UAAb,EACLQ,CAAA,CAAOH,CAAA,EAAP,CACA,CADiBL,CACjB,GAD2B,EAC3B,CADiC,EACjC,CADyC,GACzC,CAAAQ,CAAA,CAAOH,CAAA,EAAP,CAAA,CAAiBL,CAAjB,GAA4B,CAA5B,CAAiC,EAAjC,CAAyC,GAFpC,KAGA,IAA6B,CAA7B,IAAKA,CAAL,CAAa,UAAb,EACLQ,CAAA,CAAOH,CAAA,EAAP,CAEA,CAFiBL,CAEjB,GAF2B,EAE3B,CAFiC,CAEjC,CAFyC,GAEzC,CADAQ,CAAA,CAAOH,CAAA,EAAP,CACA,CADiBL,CACjB,GAD2B,EAC3B;AADiC,EACjC,CADyC,GACzC,CAAAQ,CAAA,CAAOH,CAAA,EAAP,CAAA,CAAiBL,CAAjB,GAA4B,CAA5B,CAAiC,EAAjC,CAAyC,GAHpC,KAKL,SAGFQ,EAAA,CAAOH,CAAA,EAAP,CAAA,CAAgBL,CAAhB,CAAwB,EAAxB,CAAgC,GAbzB,CA9BS,CAgDlB,MAAOQ,EAAAM,MAAA,CAAeN,CAAAM,MAAA,CAAa,CAAb,CAAgBT,CAAhB,CAAf,CAAqCG,CAAAlB,SAAA,CAAgB,CAAhB,CAAmBe,CAAnB,CA5DkC,CA8EhFR,OAAAC,eAAA,CAAsBtD,CAAAuD,UAAtB,CAAiD,UAAjD,CAA6D,CAACC,MAAO,OAAR,CAA7D,CAEAH,OAAAC,eAAA,CAAsBtD,CAAAuD,UAAtB,CAAiD,OAAjD,CAA0D,CAACC,MAAO,CAAA,CAAR,CAA1D,CAEAH,OAAAC,eAAA,CAAsBtD,CAAAuD,UAAtB,CAAiD,WAAjD,CAA8D,CAACC,MAAO,CAAA,CAAR,CAA9D,CA0GA,KAAIe,EAAazC,CACK,WAAtB,GAAI,MAAOnB,OAAX,EAAoCA,MAAAC,KAApC,CAEE2D,CAFF,CAEe9D,CAFf,CAG2B,UAH3B,GAGW,MAAOa,KAHlB,EAGwD,UAHxD,GAGyC,MAAOH,IAHhD,EAGqG,UAHrG,GAGsE,MAAOA,IAAAC,gBAH7E,GAMEmD,CANF,CAMetD,CANf,CAcAjB,EAAAuD,UAAA,OAAA,CAAsC,QAAQ,CAAC1C,CAAD,CAASX,CAAT,CAAkC,CAAzBA,CAAA,CAAA,IAAA,EAAA,GAAAA,CAAA,CAAQ,CAACwD,OAAQ,CAAA,CAAT,CAAR,CAAAxD,CACrD,IAAIA,CAAA,OAAJ,CACE,KAAUM,MAAJ,CAAU,uDAAV,CAAN;AAOAE,CAAA,CAFEG,CAAJ,WAAsBoD,WAAtB,CAEUpD,CAFV,CAGWA,CAAAA,OAAJ,WAA6B2D,YAA7B,CAIG,IAAIP,UAAJ,CAAepD,CAAAA,OAAf,CAJH,CASG,IAAIoD,UAAJ,CAAepD,CAAf,CAGV,OAAO0D,EAAA,CAAuC7D,CAAvC,CAtBuE,CAyBhFZ,EAAA,YAAA,CAAuBC,CACvBD,EAAA,YAAA,CAAuBE,CA/PN,CAAhB,CAAA,CAiQmB,WAAlB,GAAA,MAAOyE,OAAP,CAAgCA,MAAhC,CAA4D,WAAlB,GAAA,MAAOC,OAAP,CAAgCA,MAAhC,CAAyC,IAjQpF;", 6 | "sources":["text.js"], 7 | "names":["scope","FastTextEncoder","FastTextDecoder","utfLabel","options","fatal","validUtfLabels","indexOf","toLowerCase","RangeError","Error","decodeBuffer","bytes","Buffer","from","buffer","byteOffset","byteLength","toString","decodeSyncXHR","u","URL","createObjectURL","b","Blob","type","x","XMLHttpRequest","open","send","responseText","e","decodeFallback","revokeObjectURL","inputIndex","pendingSize","Math","min","length","pending","Uint16Array","chunks","pendingIndex","more","push","String","fromCharCode","apply","subarray","join","byte1","byte2","byte3","byte4","codepoint","Object","defineProperty","prototype","value","string","stream","pos","len","at","tlen","max","target","Uint8Array","charCodeAt","extra","update","set","slice","decodeImpl","ArrayBuffer","window","global"] 8 | } 9 | -------------------------------------------------------------------------------- /douyu/ws/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Einar Otto Stangvik 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. 22 | -------------------------------------------------------------------------------- /douyu/ws/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function () { 4 | throw new Error( 5 | 'ws does not work in the browser. Browser clients must use the native ' + 6 | 'WebSocket object' 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /douyu/ws/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebSocket = require('./lib/websocket'); 4 | 5 | WebSocket.createWebSocketStream = require('./lib/stream'); 6 | WebSocket.Server = require('./lib/websocket-server'); 7 | WebSocket.Receiver = require('./lib/receiver'); 8 | WebSocket.Sender = require('./lib/sender'); 9 | 10 | module.exports = WebSocket; 11 | -------------------------------------------------------------------------------- /douyu/ws/lib/buffer-util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { EMPTY_BUFFER } = require('./constants'); 4 | 5 | /** 6 | * Merges an array of buffers into a new buffer. 7 | * 8 | * @param {Buffer[]} list The array of buffers to concat 9 | * @param {Number} totalLength The total length of buffers in the list 10 | * @return {Buffer} The resulting buffer 11 | * @public 12 | */ 13 | function concat(list, totalLength) { 14 | if (list.length === 0) return EMPTY_BUFFER; 15 | if (list.length === 1) return list[0]; 16 | 17 | const target = Buffer.allocUnsafe(totalLength); 18 | let offset = 0; 19 | 20 | for (let i = 0; i < list.length; i++) { 21 | const buf = list[i]; 22 | target.set(buf, offset); 23 | offset += buf.length; 24 | } 25 | 26 | if (offset < totalLength) return target.slice(0, offset); 27 | 28 | return target; 29 | } 30 | 31 | /** 32 | * Masks a buffer using the given mask. 33 | * 34 | * @param {Buffer} source The buffer to mask 35 | * @param {Buffer} mask The mask to use 36 | * @param {Buffer} output The buffer where to store the result 37 | * @param {Number} offset The offset at which to start writing 38 | * @param {Number} length The number of bytes to mask. 39 | * @public 40 | */ 41 | function _mask(source, mask, output, offset, length) { 42 | for (let i = 0; i < length; i++) { 43 | output[offset + i] = source[i] ^ mask[i & 3]; 44 | } 45 | } 46 | 47 | /** 48 | * Unmasks a buffer using the given mask. 49 | * 50 | * @param {Buffer} buffer The buffer to unmask 51 | * @param {Buffer} mask The mask to use 52 | * @public 53 | */ 54 | function _unmask(buffer, mask) { 55 | // Required until https://github.com/nodejs/node/issues/9006 is resolved. 56 | const length = buffer.length; 57 | for (let i = 0; i < length; i++) { 58 | buffer[i] ^= mask[i & 3]; 59 | } 60 | } 61 | 62 | /** 63 | * Converts a buffer to an `ArrayBuffer`. 64 | * 65 | * @param {Buffer} buf The buffer to convert 66 | * @return {ArrayBuffer} Converted buffer 67 | * @public 68 | */ 69 | function toArrayBuffer(buf) { 70 | if (buf.byteLength === buf.buffer.byteLength) { 71 | return buf.buffer; 72 | } 73 | 74 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); 75 | } 76 | 77 | /** 78 | * Converts `data` to a `Buffer`. 79 | * 80 | * @param {*} data The data to convert 81 | * @return {Buffer} The buffer 82 | * @throws {TypeError} 83 | * @public 84 | */ 85 | function toBuffer(data) { 86 | toBuffer.readOnly = true; 87 | 88 | if (Buffer.isBuffer(data)) return data; 89 | 90 | let buf; 91 | 92 | if (data instanceof ArrayBuffer) { 93 | buf = Buffer.from(data); 94 | } else if (ArrayBuffer.isView(data)) { 95 | buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); 96 | } else { 97 | buf = Buffer.from(data); 98 | toBuffer.readOnly = false; 99 | } 100 | 101 | return buf; 102 | } 103 | 104 | try { 105 | const bufferUtil = require('bufferutil'); 106 | const bu = bufferUtil.BufferUtil || bufferUtil; 107 | 108 | module.exports = { 109 | concat, 110 | mask(source, mask, output, offset, length) { 111 | if (length < 48) _mask(source, mask, output, offset, length); 112 | else bu.mask(source, mask, output, offset, length); 113 | }, 114 | toArrayBuffer, 115 | toBuffer, 116 | unmask(buffer, mask) { 117 | if (buffer.length < 32) _unmask(buffer, mask); 118 | else bu.unmask(buffer, mask); 119 | } 120 | }; 121 | } catch (e) /* istanbul ignore next */ { 122 | module.exports = { 123 | concat, 124 | mask: _mask, 125 | toArrayBuffer, 126 | toBuffer, 127 | unmask: _unmask 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /douyu/ws/lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], 5 | GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 6 | kStatusCode: Symbol('status-code'), 7 | kWebSocket: Symbol('websocket'), 8 | EMPTY_BUFFER: Buffer.alloc(0), 9 | NOOP: () => {} 10 | }; 11 | -------------------------------------------------------------------------------- /douyu/ws/lib/event-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Class representing an event. 5 | * 6 | * @private 7 | */ 8 | class Event { 9 | /** 10 | * Create a new `Event`. 11 | * 12 | * @param {String} type The name of the event 13 | * @param {Object} target A reference to the target to which the event was dispatched 14 | */ 15 | constructor(type, target) { 16 | this.target = target; 17 | this.type = type; 18 | } 19 | } 20 | 21 | /** 22 | * Class representing a message event. 23 | * 24 | * @extends Event 25 | * @private 26 | */ 27 | class MessageEvent extends Event { 28 | /** 29 | * Create a new `MessageEvent`. 30 | * 31 | * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data 32 | * @param {WebSocket} target A reference to the target to which the event was dispatched 33 | */ 34 | constructor(data, target) { 35 | super('message', target); 36 | 37 | this.data = data; 38 | } 39 | } 40 | 41 | /** 42 | * Class representing a close event. 43 | * 44 | * @extends Event 45 | * @private 46 | */ 47 | class CloseEvent extends Event { 48 | /** 49 | * Create a new `CloseEvent`. 50 | * 51 | * @param {Number} code The status code explaining why the connection is being closed 52 | * @param {String} reason A human-readable string explaining why the connection is closing 53 | * @param {WebSocket} target A reference to the target to which the event was dispatched 54 | */ 55 | constructor(code, reason, target) { 56 | super('close', target); 57 | 58 | this.wasClean = target._closeFrameReceived && target._closeFrameSent; 59 | this.reason = reason; 60 | this.code = code; 61 | } 62 | } 63 | 64 | /** 65 | * Class representing an open event. 66 | * 67 | * @extends Event 68 | * @private 69 | */ 70 | class OpenEvent extends Event { 71 | /** 72 | * Create a new `OpenEvent`. 73 | * 74 | * @param {WebSocket} target A reference to the target to which the event was dispatched 75 | */ 76 | constructor(target) { 77 | super('open', target); 78 | } 79 | } 80 | 81 | /** 82 | * Class representing an error event. 83 | * 84 | * @extends Event 85 | * @private 86 | */ 87 | class ErrorEvent extends Event { 88 | /** 89 | * Create a new `ErrorEvent`. 90 | * 91 | * @param {Object} error The error that generated this event 92 | * @param {WebSocket} target A reference to the target to which the event was dispatched 93 | */ 94 | constructor(error, target) { 95 | super('error', target); 96 | 97 | this.message = error.message; 98 | this.error = error; 99 | } 100 | } 101 | 102 | /** 103 | * This provides methods for emulating the `EventTarget` interface. It's not 104 | * meant to be used directly. 105 | * 106 | * @mixin 107 | */ 108 | const EventTarget = { 109 | /** 110 | * Register an event listener. 111 | * 112 | * @param {String} type A string representing the event type to listen for 113 | * @param {Function} listener The listener to add 114 | * @param {Object} options An options object specifies characteristics about 115 | * the event listener 116 | * @param {Boolean} options.once A `Boolean`` indicating that the listener 117 | * should be invoked at most once after being added. If `true`, the 118 | * listener would be automatically removed when invoked. 119 | * @public 120 | */ 121 | addEventListener(type, listener, options) { 122 | if (typeof listener !== 'function') return; 123 | 124 | function onMessage(data) { 125 | listener.call(this, new MessageEvent(data, this)); 126 | } 127 | 128 | function onClose(code, message) { 129 | listener.call(this, new CloseEvent(code, message, this)); 130 | } 131 | 132 | function onError(error) { 133 | listener.call(this, new ErrorEvent(error, this)); 134 | } 135 | 136 | function onOpen() { 137 | listener.call(this, new OpenEvent(this)); 138 | } 139 | 140 | const method = options && options.once ? 'once' : 'on'; 141 | 142 | if (type === 'message') { 143 | onMessage._listener = listener; 144 | this[method](type, onMessage); 145 | } else if (type === 'close') { 146 | onClose._listener = listener; 147 | this[method](type, onClose); 148 | } else if (type === 'error') { 149 | onError._listener = listener; 150 | this[method](type, onError); 151 | } else if (type === 'open') { 152 | onOpen._listener = listener; 153 | this[method](type, onOpen); 154 | } else { 155 | this[method](type, listener); 156 | } 157 | }, 158 | 159 | /** 160 | * Remove an event listener. 161 | * 162 | * @param {String} type A string representing the event type to remove 163 | * @param {Function} listener The listener to remove 164 | * @public 165 | */ 166 | removeEventListener(type, listener) { 167 | const listeners = this.listeners(type); 168 | 169 | for (let i = 0; i < listeners.length; i++) { 170 | if (listeners[i] === listener || listeners[i]._listener === listener) { 171 | this.removeListener(type, listeners[i]); 172 | } 173 | } 174 | } 175 | }; 176 | 177 | module.exports = EventTarget; 178 | -------------------------------------------------------------------------------- /douyu/ws/lib/extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 4 | // Allowed token characters: 5 | // 6 | // '!', '#', '$', '%', '&', ''', '*', '+', '-', 7 | // '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' 8 | // 9 | // tokenChars[32] === 0 // ' ' 10 | // tokenChars[33] === 1 // '!' 11 | // tokenChars[34] === 0 // '"' 12 | // ... 13 | // 14 | // prettier-ignore 15 | const tokenChars = [ 16 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 17 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 18 | 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 19 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 20 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 21 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 22 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 23 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 24 | ]; 25 | 26 | /** 27 | * Adds an offer to the map of extension offers or a parameter to the map of 28 | * parameters. 29 | * 30 | * @param {Object} dest The map of extension offers or parameters 31 | * @param {String} name The extension or parameter name 32 | * @param {(Object|Boolean|String)} elem The extension parameters or the 33 | * parameter value 34 | * @private 35 | */ 36 | function push(dest, name, elem) { 37 | if (dest[name] === undefined) dest[name] = [elem]; 38 | else dest[name].push(elem); 39 | } 40 | 41 | /** 42 | * Parses the `Sec-WebSocket-Extensions` header into an object. 43 | * 44 | * @param {String} header The field value of the header 45 | * @return {Object} The parsed object 46 | * @public 47 | */ 48 | function parse(header) { 49 | const offers = Object.create(null); 50 | 51 | if (header === undefined || header === '') return offers; 52 | 53 | let params = Object.create(null); 54 | let mustUnescape = false; 55 | let isEscaping = false; 56 | let inQuotes = false; 57 | let extensionName; 58 | let paramName; 59 | let start = -1; 60 | let end = -1; 61 | let i = 0; 62 | 63 | for (; i < header.length; i++) { 64 | const code = header.charCodeAt(i); 65 | 66 | if (extensionName === undefined) { 67 | if (end === -1 && tokenChars[code] === 1) { 68 | if (start === -1) start = i; 69 | } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { 70 | if (end === -1 && start !== -1) end = i; 71 | } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { 72 | if (start === -1) { 73 | throw new SyntaxError(`Unexpected character at index ${i}`); 74 | } 75 | 76 | if (end === -1) end = i; 77 | const name = header.slice(start, end); 78 | if (code === 0x2c) { 79 | push(offers, name, params); 80 | params = Object.create(null); 81 | } else { 82 | extensionName = name; 83 | } 84 | 85 | start = end = -1; 86 | } else { 87 | throw new SyntaxError(`Unexpected character at index ${i}`); 88 | } 89 | } else if (paramName === undefined) { 90 | if (end === -1 && tokenChars[code] === 1) { 91 | if (start === -1) start = i; 92 | } else if (code === 0x20 || code === 0x09) { 93 | if (end === -1 && start !== -1) end = i; 94 | } else if (code === 0x3b || code === 0x2c) { 95 | if (start === -1) { 96 | throw new SyntaxError(`Unexpected character at index ${i}`); 97 | } 98 | 99 | if (end === -1) end = i; 100 | push(params, header.slice(start, end), true); 101 | if (code === 0x2c) { 102 | push(offers, extensionName, params); 103 | params = Object.create(null); 104 | extensionName = undefined; 105 | } 106 | 107 | start = end = -1; 108 | } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) { 109 | paramName = header.slice(start, i); 110 | start = end = -1; 111 | } else { 112 | throw new SyntaxError(`Unexpected character at index ${i}`); 113 | } 114 | } else { 115 | // 116 | // The value of a quoted-string after unescaping must conform to the 117 | // token ABNF, so only token characters are valid. 118 | // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 119 | // 120 | if (isEscaping) { 121 | if (tokenChars[code] !== 1) { 122 | throw new SyntaxError(`Unexpected character at index ${i}`); 123 | } 124 | if (start === -1) start = i; 125 | else if (!mustUnescape) mustUnescape = true; 126 | isEscaping = false; 127 | } else if (inQuotes) { 128 | if (tokenChars[code] === 1) { 129 | if (start === -1) start = i; 130 | } else if (code === 0x22 /* '"' */ && start !== -1) { 131 | inQuotes = false; 132 | end = i; 133 | } else if (code === 0x5c /* '\' */) { 134 | isEscaping = true; 135 | } else { 136 | throw new SyntaxError(`Unexpected character at index ${i}`); 137 | } 138 | } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { 139 | inQuotes = true; 140 | } else if (end === -1 && tokenChars[code] === 1) { 141 | if (start === -1) start = i; 142 | } else if (start !== -1 && (code === 0x20 || code === 0x09)) { 143 | if (end === -1) end = i; 144 | } else if (code === 0x3b || code === 0x2c) { 145 | if (start === -1) { 146 | throw new SyntaxError(`Unexpected character at index ${i}`); 147 | } 148 | 149 | if (end === -1) end = i; 150 | let value = header.slice(start, end); 151 | if (mustUnescape) { 152 | value = value.replace(/\\/g, ''); 153 | mustUnescape = false; 154 | } 155 | push(params, paramName, value); 156 | if (code === 0x2c) { 157 | push(offers, extensionName, params); 158 | params = Object.create(null); 159 | extensionName = undefined; 160 | } 161 | 162 | paramName = undefined; 163 | start = end = -1; 164 | } else { 165 | throw new SyntaxError(`Unexpected character at index ${i}`); 166 | } 167 | } 168 | } 169 | 170 | if (start === -1 || inQuotes) { 171 | throw new SyntaxError('Unexpected end of input'); 172 | } 173 | 174 | if (end === -1) end = i; 175 | const token = header.slice(start, end); 176 | if (extensionName === undefined) { 177 | push(offers, token, params); 178 | } else { 179 | if (paramName === undefined) { 180 | push(params, token, true); 181 | } else if (mustUnescape) { 182 | push(params, paramName, token.replace(/\\/g, '')); 183 | } else { 184 | push(params, paramName, token); 185 | } 186 | push(offers, extensionName, params); 187 | } 188 | 189 | return offers; 190 | } 191 | 192 | /** 193 | * Builds the `Sec-WebSocket-Extensions` header field value. 194 | * 195 | * @param {Object} extensions The map of extensions and parameters to format 196 | * @return {String} A string representing the given object 197 | * @public 198 | */ 199 | function format(extensions) { 200 | return Object.keys(extensions) 201 | .map((extension) => { 202 | let configurations = extensions[extension]; 203 | if (!Array.isArray(configurations)) configurations = [configurations]; 204 | return configurations 205 | .map((params) => { 206 | return [extension] 207 | .concat( 208 | Object.keys(params).map((k) => { 209 | let values = params[k]; 210 | if (!Array.isArray(values)) values = [values]; 211 | return values 212 | .map((v) => (v === true ? k : `${k}=${v}`)) 213 | .join('; '); 214 | }) 215 | ) 216 | .join('; '); 217 | }) 218 | .join(', '); 219 | }) 220 | .join(', '); 221 | } 222 | 223 | module.exports = { format, parse }; 224 | -------------------------------------------------------------------------------- /douyu/ws/lib/limiter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const kDone = Symbol('kDone'); 4 | const kRun = Symbol('kRun'); 5 | 6 | /** 7 | * A very simple job queue with adjustable concurrency. Adapted from 8 | * https://github.com/STRML/async-limiter 9 | */ 10 | class Limiter { 11 | /** 12 | * Creates a new `Limiter`. 13 | * 14 | * @param {Number} concurrency The maximum number of jobs allowed to run 15 | * concurrently 16 | */ 17 | constructor(concurrency) { 18 | this[kDone] = () => { 19 | this.pending--; 20 | this[kRun](); 21 | }; 22 | this.concurrency = concurrency || Infinity; 23 | this.jobs = []; 24 | this.pending = 0; 25 | } 26 | 27 | /** 28 | * Adds a job to the queue. 29 | * 30 | * @public 31 | */ 32 | add(job) { 33 | this.jobs.push(job); 34 | this[kRun](); 35 | } 36 | 37 | /** 38 | * Removes a job from the queue and runs it if possible. 39 | * 40 | * @private 41 | */ 42 | [kRun]() { 43 | if (this.pending === this.concurrency) return; 44 | 45 | if (this.jobs.length) { 46 | const job = this.jobs.shift(); 47 | 48 | this.pending++; 49 | job(this[kDone]); 50 | } 51 | } 52 | } 53 | 54 | module.exports = Limiter; 55 | -------------------------------------------------------------------------------- /douyu/ws/lib/sender.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { randomFillSync } = require('crypto'); 4 | 5 | const PerMessageDeflate = require('./permessage-deflate'); 6 | const { EMPTY_BUFFER } = require('./constants'); 7 | const { isValidStatusCode } = require('./validation'); 8 | const { mask: applyMask, toBuffer } = require('./buffer-util'); 9 | 10 | const mask = Buffer.alloc(4); 11 | 12 | /** 13 | * HyBi Sender implementation. 14 | */ 15 | class Sender { 16 | /** 17 | * Creates a Sender instance. 18 | * 19 | * @param {net.Socket} socket The connection socket 20 | * @param {Object} extensions An object containing the negotiated extensions 21 | */ 22 | constructor(socket, extensions) { 23 | this._extensions = extensions || {}; 24 | this._socket = socket; 25 | 26 | this._firstFragment = true; 27 | this._compress = false; 28 | 29 | this._bufferedBytes = 0; 30 | this._deflating = false; 31 | this._queue = []; 32 | } 33 | 34 | /** 35 | * Frames a piece of data according to the HyBi WebSocket protocol. 36 | * 37 | * @param {Buffer} data The data to frame 38 | * @param {Object} options Options object 39 | * @param {Number} options.opcode The opcode 40 | * @param {Boolean} options.readOnly Specifies whether `data` can be modified 41 | * @param {Boolean} options.fin Specifies whether or not to set the FIN bit 42 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 43 | * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit 44 | * @return {Buffer[]} The framed data as a list of `Buffer` instances 45 | * @public 46 | */ 47 | static frame(data, options) { 48 | const merge = options.mask && options.readOnly; 49 | let offset = options.mask ? 6 : 2; 50 | let payloadLength = data.length; 51 | 52 | if (data.length >= 65536) { 53 | offset += 8; 54 | payloadLength = 127; 55 | } else if (data.length > 125) { 56 | offset += 2; 57 | payloadLength = 126; 58 | } 59 | 60 | const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); 61 | 62 | target[0] = options.fin ? options.opcode | 0x80 : options.opcode; 63 | if (options.rsv1) target[0] |= 0x40; 64 | 65 | target[1] = payloadLength; 66 | 67 | if (payloadLength === 126) { 68 | target.writeUInt16BE(data.length, 2); 69 | } else if (payloadLength === 127) { 70 | target.writeUInt32BE(0, 2); 71 | target.writeUInt32BE(data.length, 6); 72 | } 73 | 74 | if (!options.mask) return [target, data]; 75 | 76 | randomFillSync(mask, 0, 4); 77 | 78 | target[1] |= 0x80; 79 | target[offset - 4] = mask[0]; 80 | target[offset - 3] = mask[1]; 81 | target[offset - 2] = mask[2]; 82 | target[offset - 1] = mask[3]; 83 | 84 | if (merge) { 85 | applyMask(data, mask, target, offset, data.length); 86 | return [target]; 87 | } 88 | 89 | applyMask(data, mask, data, 0, data.length); 90 | return [target, data]; 91 | } 92 | 93 | /** 94 | * Sends a close message to the other peer. 95 | * 96 | * @param {(Number|undefined)} code The status code component of the body 97 | * @param {String} data The message component of the body 98 | * @param {Boolean} mask Specifies whether or not to mask the message 99 | * @param {Function} cb Callback 100 | * @public 101 | */ 102 | close(code, data, mask, cb) { 103 | let buf; 104 | 105 | if (code === undefined) { 106 | buf = EMPTY_BUFFER; 107 | } else if (typeof code !== 'number' || !isValidStatusCode(code)) { 108 | throw new TypeError('First argument must be a valid error code number'); 109 | } else if (data === undefined || data === '') { 110 | buf = Buffer.allocUnsafe(2); 111 | buf.writeUInt16BE(code, 0); 112 | } else { 113 | const length = Buffer.byteLength(data); 114 | 115 | if (length > 123) { 116 | throw new RangeError('The message must not be greater than 123 bytes'); 117 | } 118 | 119 | buf = Buffer.allocUnsafe(2 + length); 120 | buf.writeUInt16BE(code, 0); 121 | buf.write(data, 2); 122 | } 123 | 124 | if (this._deflating) { 125 | this.enqueue([this.doClose, buf, mask, cb]); 126 | } else { 127 | this.doClose(buf, mask, cb); 128 | } 129 | } 130 | 131 | /** 132 | * Frames and sends a close message. 133 | * 134 | * @param {Buffer} data The message to send 135 | * @param {Boolean} mask Specifies whether or not to mask `data` 136 | * @param {Function} cb Callback 137 | * @private 138 | */ 139 | doClose(data, mask, cb) { 140 | this.sendFrame( 141 | Sender.frame(data, { 142 | fin: true, 143 | rsv1: false, 144 | opcode: 0x08, 145 | mask, 146 | readOnly: false 147 | }), 148 | cb 149 | ); 150 | } 151 | 152 | /** 153 | * Sends a ping message to the other peer. 154 | * 155 | * @param {*} data The message to send 156 | * @param {Boolean} mask Specifies whether or not to mask `data` 157 | * @param {Function} cb Callback 158 | * @public 159 | */ 160 | ping(data, mask, cb) { 161 | const buf = toBuffer(data); 162 | 163 | if (buf.length > 125) { 164 | throw new RangeError('The data size must not be greater than 125 bytes'); 165 | } 166 | 167 | if (this._deflating) { 168 | this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); 169 | } else { 170 | this.doPing(buf, mask, toBuffer.readOnly, cb); 171 | } 172 | } 173 | 174 | /** 175 | * Frames and sends a ping message. 176 | * 177 | * @param {Buffer} data The message to send 178 | * @param {Boolean} mask Specifies whether or not to mask `data` 179 | * @param {Boolean} readOnly Specifies whether `data` can be modified 180 | * @param {Function} cb Callback 181 | * @private 182 | */ 183 | doPing(data, mask, readOnly, cb) { 184 | this.sendFrame( 185 | Sender.frame(data, { 186 | fin: true, 187 | rsv1: false, 188 | opcode: 0x09, 189 | mask, 190 | readOnly 191 | }), 192 | cb 193 | ); 194 | } 195 | 196 | /** 197 | * Sends a pong message to the other peer. 198 | * 199 | * @param {*} data The message to send 200 | * @param {Boolean} mask Specifies whether or not to mask `data` 201 | * @param {Function} cb Callback 202 | * @public 203 | */ 204 | pong(data, mask, cb) { 205 | const buf = toBuffer(data); 206 | 207 | if (buf.length > 125) { 208 | throw new RangeError('The data size must not be greater than 125 bytes'); 209 | } 210 | 211 | if (this._deflating) { 212 | this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); 213 | } else { 214 | this.doPong(buf, mask, toBuffer.readOnly, cb); 215 | } 216 | } 217 | 218 | /** 219 | * Frames and sends a pong message. 220 | * 221 | * @param {Buffer} data The message to send 222 | * @param {Boolean} mask Specifies whether or not to mask `data` 223 | * @param {Boolean} readOnly Specifies whether `data` can be modified 224 | * @param {Function} cb Callback 225 | * @private 226 | */ 227 | doPong(data, mask, readOnly, cb) { 228 | this.sendFrame( 229 | Sender.frame(data, { 230 | fin: true, 231 | rsv1: false, 232 | opcode: 0x0a, 233 | mask, 234 | readOnly 235 | }), 236 | cb 237 | ); 238 | } 239 | 240 | /** 241 | * Sends a data message to the other peer. 242 | * 243 | * @param {*} data The message to send 244 | * @param {Object} options Options object 245 | * @param {Boolean} options.compress Specifies whether or not to compress `data` 246 | * @param {Boolean} options.binary Specifies whether `data` is binary or text 247 | * @param {Boolean} options.fin Specifies whether the fragment is the last one 248 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 249 | * @param {Function} cb Callback 250 | * @public 251 | */ 252 | send(data, options, cb) { 253 | const buf = toBuffer(data); 254 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 255 | let opcode = options.binary ? 2 : 1; 256 | let rsv1 = options.compress; 257 | 258 | if (this._firstFragment) { 259 | this._firstFragment = false; 260 | if (rsv1 && perMessageDeflate) { 261 | rsv1 = buf.length >= perMessageDeflate._threshold; 262 | } 263 | this._compress = rsv1; 264 | } else { 265 | rsv1 = false; 266 | opcode = 0; 267 | } 268 | 269 | if (options.fin) this._firstFragment = true; 270 | 271 | if (perMessageDeflate) { 272 | const opts = { 273 | fin: options.fin, 274 | rsv1, 275 | opcode, 276 | mask: options.mask, 277 | readOnly: toBuffer.readOnly 278 | }; 279 | 280 | if (this._deflating) { 281 | this.enqueue([this.dispatch, buf, this._compress, opts, cb]); 282 | } else { 283 | this.dispatch(buf, this._compress, opts, cb); 284 | } 285 | } else { 286 | this.sendFrame( 287 | Sender.frame(buf, { 288 | fin: options.fin, 289 | rsv1: false, 290 | opcode, 291 | mask: options.mask, 292 | readOnly: toBuffer.readOnly 293 | }), 294 | cb 295 | ); 296 | } 297 | } 298 | 299 | /** 300 | * Dispatches a data message. 301 | * 302 | * @param {Buffer} data The message to send 303 | * @param {Boolean} compress Specifies whether or not to compress `data` 304 | * @param {Object} options Options object 305 | * @param {Number} options.opcode The opcode 306 | * @param {Boolean} options.readOnly Specifies whether `data` can be modified 307 | * @param {Boolean} options.fin Specifies whether or not to set the FIN bit 308 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 309 | * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit 310 | * @param {Function} cb Callback 311 | * @private 312 | */ 313 | dispatch(data, compress, options, cb) { 314 | if (!compress) { 315 | this.sendFrame(Sender.frame(data, options), cb); 316 | return; 317 | } 318 | 319 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 320 | 321 | this._bufferedBytes += data.length; 322 | this._deflating = true; 323 | perMessageDeflate.compress(data, options.fin, (_, buf) => { 324 | if (this._socket.destroyed) { 325 | const err = new Error( 326 | 'The socket was closed while data was being compressed' 327 | ); 328 | 329 | if (typeof cb === 'function') cb(err); 330 | 331 | for (let i = 0; i < this._queue.length; i++) { 332 | const callback = this._queue[i][4]; 333 | 334 | if (typeof callback === 'function') callback(err); 335 | } 336 | 337 | return; 338 | } 339 | 340 | this._bufferedBytes -= data.length; 341 | this._deflating = false; 342 | options.readOnly = false; 343 | this.sendFrame(Sender.frame(buf, options), cb); 344 | this.dequeue(); 345 | }); 346 | } 347 | 348 | /** 349 | * Executes queued send operations. 350 | * 351 | * @private 352 | */ 353 | dequeue() { 354 | while (!this._deflating && this._queue.length) { 355 | const params = this._queue.shift(); 356 | 357 | this._bufferedBytes -= params[1].length; 358 | Reflect.apply(params[0], this, params.slice(1)); 359 | } 360 | } 361 | 362 | /** 363 | * Enqueues a send operation. 364 | * 365 | * @param {Array} params Send operation parameters. 366 | * @private 367 | */ 368 | enqueue(params) { 369 | this._bufferedBytes += params[1].length; 370 | this._queue.push(params); 371 | } 372 | 373 | /** 374 | * Sends a frame. 375 | * 376 | * @param {Buffer[]} list The frame to send 377 | * @param {Function} cb Callback 378 | * @private 379 | */ 380 | sendFrame(list, cb) { 381 | if (list.length === 2) { 382 | this._socket.cork(); 383 | this._socket.write(list[0]); 384 | this._socket.write(list[1], cb); 385 | this._socket.uncork(); 386 | } else { 387 | this._socket.write(list[0], cb); 388 | } 389 | } 390 | } 391 | 392 | module.exports = Sender; 393 | -------------------------------------------------------------------------------- /douyu/ws/lib/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Duplex } = require('stream'); 4 | 5 | /** 6 | * Emits the `'close'` event on a stream. 7 | * 8 | * @param {stream.Duplex} The stream. 9 | * @private 10 | */ 11 | function emitClose(stream) { 12 | stream.emit('close'); 13 | } 14 | 15 | /** 16 | * The listener of the `'end'` event. 17 | * 18 | * @private 19 | */ 20 | function duplexOnEnd() { 21 | if (!this.destroyed && this._writableState.finished) { 22 | this.destroy(); 23 | } 24 | } 25 | 26 | /** 27 | * The listener of the `'error'` event. 28 | * 29 | * @private 30 | */ 31 | function duplexOnError(err) { 32 | this.removeListener('error', duplexOnError); 33 | this.destroy(); 34 | if (this.listenerCount('error') === 0) { 35 | // Do not suppress the throwing behavior. 36 | this.emit('error', err); 37 | } 38 | } 39 | 40 | /** 41 | * Wraps a `WebSocket` in a duplex stream. 42 | * 43 | * @param {WebSocket} ws The `WebSocket` to wrap 44 | * @param {Object} options The options for the `Duplex` constructor 45 | * @return {stream.Duplex} The duplex stream 46 | * @public 47 | */ 48 | function createWebSocketStream(ws, options) { 49 | let resumeOnReceiverDrain = true; 50 | 51 | function receiverOnDrain() { 52 | if (resumeOnReceiverDrain) ws._socket.resume(); 53 | } 54 | 55 | if (ws.readyState === ws.CONNECTING) { 56 | ws.once('open', function open() { 57 | ws._receiver.removeAllListeners('drain'); 58 | ws._receiver.on('drain', receiverOnDrain); 59 | }); 60 | } else { 61 | ws._receiver.removeAllListeners('drain'); 62 | ws._receiver.on('drain', receiverOnDrain); 63 | } 64 | 65 | const duplex = new Duplex({ 66 | ...options, 67 | autoDestroy: false, 68 | emitClose: false, 69 | objectMode: false, 70 | writableObjectMode: false 71 | }); 72 | 73 | ws.on('message', function message(msg) { 74 | if (!duplex.push(msg)) { 75 | resumeOnReceiverDrain = false; 76 | ws._socket.pause(); 77 | } 78 | }); 79 | 80 | ws.once('error', function error(err) { 81 | if (duplex.destroyed) return; 82 | 83 | duplex.destroy(err); 84 | }); 85 | 86 | ws.once('close', function close() { 87 | if (duplex.destroyed) return; 88 | 89 | duplex.push(null); 90 | }); 91 | 92 | duplex._destroy = function (err, callback) { 93 | if (ws.readyState === ws.CLOSED) { 94 | callback(err); 95 | process.nextTick(emitClose, duplex); 96 | return; 97 | } 98 | 99 | let called = false; 100 | 101 | ws.once('error', function error(err) { 102 | called = true; 103 | callback(err); 104 | }); 105 | 106 | ws.once('close', function close() { 107 | if (!called) callback(err); 108 | process.nextTick(emitClose, duplex); 109 | }); 110 | ws.terminate(); 111 | }; 112 | 113 | duplex._final = function (callback) { 114 | if (ws.readyState === ws.CONNECTING) { 115 | ws.once('open', function open() { 116 | duplex._final(callback); 117 | }); 118 | return; 119 | } 120 | 121 | // If the value of the `_socket` property is `null` it means that `ws` is a 122 | // client websocket and the handshake failed. In fact, when this happens, a 123 | // socket is never assigned to the websocket. Wait for the `'error'` event 124 | // that will be emitted by the websocket. 125 | if (ws._socket === null) return; 126 | 127 | if (ws._socket._writableState.finished) { 128 | callback(); 129 | if (duplex._readableState.endEmitted) duplex.destroy(); 130 | } else { 131 | ws._socket.once('finish', function finish() { 132 | // `duplex` is not destroyed here because the `'end'` event will be 133 | // emitted on `duplex` after this `'finish'` event. The EOF signaling 134 | // `null` chunk is, in fact, pushed when the websocket emits `'close'`. 135 | callback(); 136 | }); 137 | ws.close(); 138 | } 139 | }; 140 | 141 | duplex._read = function () { 142 | if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { 143 | resumeOnReceiverDrain = true; 144 | if (!ws._receiver._writableState.needDrain) ws._socket.resume(); 145 | } 146 | }; 147 | 148 | duplex._write = function (chunk, encoding, callback) { 149 | if (ws.readyState === ws.CONNECTING) { 150 | ws.once('open', function open() { 151 | duplex._write(chunk, encoding, callback); 152 | }); 153 | return; 154 | } 155 | 156 | ws.send(chunk, callback); 157 | }; 158 | 159 | duplex.on('end', duplexOnEnd); 160 | duplex.on('error', duplexOnError); 161 | return duplex; 162 | } 163 | 164 | module.exports = createWebSocketStream; 165 | -------------------------------------------------------------------------------- /douyu/ws/lib/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | try { 4 | const isValidUTF8 = require('utf-8-validate'); 5 | 6 | exports.isValidUTF8 = 7 | typeof isValidUTF8 === 'object' 8 | ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 9 | : isValidUTF8; 10 | } catch (e) /* istanbul ignore next */ { 11 | exports.isValidUTF8 = () => true; 12 | } 13 | 14 | /** 15 | * Checks if a status code is allowed in a close frame. 16 | * 17 | * @param {Number} code The status code 18 | * @return {Boolean} `true` if the status code is valid, else `false` 19 | * @public 20 | */ 21 | exports.isValidStatusCode = (code) => { 22 | return ( 23 | (code >= 1000 && 24 | code <= 1014 && 25 | code !== 1004 && 26 | code !== 1005 && 27 | code !== 1006) || 28 | (code >= 3000 && code <= 4999) 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /douyu/ws/lib/websocket-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | const { createHash } = require('crypto'); 5 | const { createServer, STATUS_CODES } = require('http'); 6 | 7 | const PerMessageDeflate = require('./permessage-deflate'); 8 | const WebSocket = require('./websocket'); 9 | const { format, parse } = require('./extension'); 10 | const { GUID, kWebSocket } = require('./constants'); 11 | 12 | const keyRegex = /^[+/0-9A-Za-z]{22}==$/; 13 | 14 | /** 15 | * Class representing a WebSocket server. 16 | * 17 | * @extends EventEmitter 18 | */ 19 | class WebSocketServer extends EventEmitter { 20 | /** 21 | * Create a `WebSocketServer` instance. 22 | * 23 | * @param {Object} options Configuration options 24 | * @param {Number} options.backlog The maximum length of the queue of pending 25 | * connections 26 | * @param {Boolean} options.clientTracking Specifies whether or not to track 27 | * clients 28 | * @param {Function} options.handleProtocols A hook to handle protocols 29 | * @param {String} options.host The hostname where to bind the server 30 | * @param {Number} options.maxPayload The maximum allowed message size 31 | * @param {Boolean} options.noServer Enable no server mode 32 | * @param {String} options.path Accept only connections matching this path 33 | * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable 34 | * permessage-deflate 35 | * @param {Number} options.port The port where to bind the server 36 | * @param {http.Server} options.server A pre-created HTTP/S server to use 37 | * @param {Function} options.verifyClient A hook to reject connections 38 | * @param {Function} callback A listener for the `listening` event 39 | */ 40 | constructor(options, callback) { 41 | super(); 42 | 43 | options = { 44 | maxPayload: 100 * 1024 * 1024, 45 | perMessageDeflate: false, 46 | handleProtocols: null, 47 | clientTracking: true, 48 | verifyClient: null, 49 | noServer: false, 50 | backlog: null, // use default (511 as implemented in net.js) 51 | server: null, 52 | host: null, 53 | path: null, 54 | port: null, 55 | ...options 56 | }; 57 | 58 | if (options.port == null && !options.server && !options.noServer) { 59 | throw new TypeError( 60 | 'One of the "port", "server", or "noServer" options must be specified' 61 | ); 62 | } 63 | 64 | if (options.port != null) { 65 | this._server = createServer((req, res) => { 66 | const body = STATUS_CODES[426]; 67 | 68 | res.writeHead(426, { 69 | 'Content-Length': body.length, 70 | 'Content-Type': 'text/plain' 71 | }); 72 | res.end(body); 73 | }); 74 | this._server.listen( 75 | options.port, 76 | options.host, 77 | options.backlog, 78 | callback 79 | ); 80 | } else if (options.server) { 81 | this._server = options.server; 82 | } 83 | 84 | if (this._server) { 85 | this._removeListeners = addListeners(this._server, { 86 | listening: this.emit.bind(this, 'listening'), 87 | error: this.emit.bind(this, 'error'), 88 | upgrade: (req, socket, head) => { 89 | this.handleUpgrade(req, socket, head, (ws) => { 90 | this.emit('connection', ws, req); 91 | }); 92 | } 93 | }); 94 | } 95 | 96 | if (options.perMessageDeflate === true) options.perMessageDeflate = {}; 97 | if (options.clientTracking) this.clients = new Set(); 98 | this.options = options; 99 | } 100 | 101 | /** 102 | * Returns the bound address, the address family name, and port of the server 103 | * as reported by the operating system if listening on an IP socket. 104 | * If the server is listening on a pipe or UNIX domain socket, the name is 105 | * returned as a string. 106 | * 107 | * @return {(Object|String|null)} The address of the server 108 | * @public 109 | */ 110 | address() { 111 | if (this.options.noServer) { 112 | throw new Error('The server is operating in "noServer" mode'); 113 | } 114 | 115 | if (!this._server) return null; 116 | return this._server.address(); 117 | } 118 | 119 | /** 120 | * Close the server. 121 | * 122 | * @param {Function} cb Callback 123 | * @public 124 | */ 125 | close(cb) { 126 | if (cb) this.once('close', cb); 127 | 128 | // 129 | // Terminate all associated clients. 130 | // 131 | if (this.clients) { 132 | for (const client of this.clients) client.terminate(); 133 | } 134 | 135 | const server = this._server; 136 | 137 | if (server) { 138 | this._removeListeners(); 139 | this._removeListeners = this._server = null; 140 | 141 | // 142 | // Close the http server if it was internally created. 143 | // 144 | if (this.options.port != null) { 145 | server.close(() => this.emit('close')); 146 | return; 147 | } 148 | } 149 | 150 | process.nextTick(emitClose, this); 151 | } 152 | 153 | /** 154 | * See if a given request should be handled by this server instance. 155 | * 156 | * @param {http.IncomingMessage} req Request object to inspect 157 | * @return {Boolean} `true` if the request is valid, else `false` 158 | * @public 159 | */ 160 | shouldHandle(req) { 161 | if (this.options.path) { 162 | const index = req.url.indexOf('?'); 163 | const pathname = index !== -1 ? req.url.slice(0, index) : req.url; 164 | 165 | if (pathname !== this.options.path) return false; 166 | } 167 | 168 | return true; 169 | } 170 | 171 | /** 172 | * Handle a HTTP Upgrade request. 173 | * 174 | * @param {http.IncomingMessage} req The request object 175 | * @param {net.Socket} socket The network socket between the server and client 176 | * @param {Buffer} head The first packet of the upgraded stream 177 | * @param {Function} cb Callback 178 | * @public 179 | */ 180 | handleUpgrade(req, socket, head, cb) { 181 | socket.on('error', socketOnError); 182 | 183 | const key = 184 | req.headers['sec-websocket-key'] !== undefined 185 | ? req.headers['sec-websocket-key'].trim() 186 | : false; 187 | const version = +req.headers['sec-websocket-version']; 188 | const extensions = {}; 189 | 190 | if ( 191 | req.method !== 'GET' || 192 | req.headers.upgrade.toLowerCase() !== 'websocket' || 193 | !key || 194 | !keyRegex.test(key) || 195 | (version !== 8 && version !== 13) || 196 | !this.shouldHandle(req) 197 | ) { 198 | return abortHandshake(socket, 400); 199 | } 200 | 201 | if (this.options.perMessageDeflate) { 202 | const perMessageDeflate = new PerMessageDeflate( 203 | this.options.perMessageDeflate, 204 | true, 205 | this.options.maxPayload 206 | ); 207 | 208 | try { 209 | const offers = parse(req.headers['sec-websocket-extensions']); 210 | 211 | if (offers[PerMessageDeflate.extensionName]) { 212 | perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); 213 | extensions[PerMessageDeflate.extensionName] = perMessageDeflate; 214 | } 215 | } catch (err) { 216 | return abortHandshake(socket, 400); 217 | } 218 | } 219 | 220 | // 221 | // Optionally call external client verification handler. 222 | // 223 | if (this.options.verifyClient) { 224 | const info = { 225 | origin: 226 | req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], 227 | secure: !!(req.connection.authorized || req.connection.encrypted), 228 | req 229 | }; 230 | 231 | if (this.options.verifyClient.length === 2) { 232 | this.options.verifyClient(info, (verified, code, message, headers) => { 233 | if (!verified) { 234 | return abortHandshake(socket, code || 401, message, headers); 235 | } 236 | 237 | this.completeUpgrade(key, extensions, req, socket, head, cb); 238 | }); 239 | return; 240 | } 241 | 242 | if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); 243 | } 244 | 245 | this.completeUpgrade(key, extensions, req, socket, head, cb); 246 | } 247 | 248 | /** 249 | * Upgrade the connection to WebSocket. 250 | * 251 | * @param {String} key The value of the `Sec-WebSocket-Key` header 252 | * @param {Object} extensions The accepted extensions 253 | * @param {http.IncomingMessage} req The request object 254 | * @param {net.Socket} socket The network socket between the server and client 255 | * @param {Buffer} head The first packet of the upgraded stream 256 | * @param {Function} cb Callback 257 | * @throws {Error} If called more than once with the same socket 258 | * @private 259 | */ 260 | completeUpgrade(key, extensions, req, socket, head, cb) { 261 | // 262 | // Destroy the socket if the client has already sent a FIN packet. 263 | // 264 | if (!socket.readable || !socket.writable) return socket.destroy(); 265 | 266 | if (socket[kWebSocket]) { 267 | throw new Error( 268 | 'server.handleUpgrade() was called more than once with the same ' + 269 | 'socket, possibly due to a misconfiguration' 270 | ); 271 | } 272 | 273 | const digest = createHash('sha1') 274 | .update(key + GUID) 275 | .digest('base64'); 276 | 277 | const headers = [ 278 | 'HTTP/1.1 101 Switching Protocols', 279 | 'Upgrade: websocket', 280 | 'Connection: Upgrade', 281 | `Sec-WebSocket-Accept: ${digest}` 282 | ]; 283 | 284 | const ws = new WebSocket(null); 285 | let protocol = req.headers['sec-websocket-protocol']; 286 | 287 | if (protocol) { 288 | protocol = protocol.trim().split(/ *, */); 289 | 290 | // 291 | // Optionally call external protocol selection handler. 292 | // 293 | if (this.options.handleProtocols) { 294 | protocol = this.options.handleProtocols(protocol, req); 295 | } else { 296 | protocol = protocol[0]; 297 | } 298 | 299 | if (protocol) { 300 | headers.push(`Sec-WebSocket-Protocol: ${protocol}`); 301 | ws.protocol = protocol; 302 | } 303 | } 304 | 305 | if (extensions[PerMessageDeflate.extensionName]) { 306 | const params = extensions[PerMessageDeflate.extensionName].params; 307 | const value = format({ 308 | [PerMessageDeflate.extensionName]: [params] 309 | }); 310 | headers.push(`Sec-WebSocket-Extensions: ${value}`); 311 | ws._extensions = extensions; 312 | } 313 | 314 | // 315 | // Allow external modification/inspection of handshake headers. 316 | // 317 | this.emit('headers', headers, req); 318 | 319 | socket.write(headers.concat('\r\n').join('\r\n')); 320 | socket.removeListener('error', socketOnError); 321 | 322 | ws.setSocket(socket, head, this.options.maxPayload); 323 | 324 | if (this.clients) { 325 | this.clients.add(ws); 326 | ws.on('close', () => this.clients.delete(ws)); 327 | } 328 | 329 | cb(ws); 330 | } 331 | } 332 | 333 | module.exports = WebSocketServer; 334 | 335 | /** 336 | * Add event listeners on an `EventEmitter` using a map of 337 | * pairs. 338 | * 339 | * @param {EventEmitter} server The event emitter 340 | * @param {Object.} map The listeners to add 341 | * @return {Function} A function that will remove the added listeners when called 342 | * @private 343 | */ 344 | function addListeners(server, map) { 345 | for (const event of Object.keys(map)) server.on(event, map[event]); 346 | 347 | return function removeListeners() { 348 | for (const event of Object.keys(map)) { 349 | server.removeListener(event, map[event]); 350 | } 351 | }; 352 | } 353 | 354 | /** 355 | * Emit a `'close'` event on an `EventEmitter`. 356 | * 357 | * @param {EventEmitter} server The event emitter 358 | * @private 359 | */ 360 | function emitClose(server) { 361 | server.emit('close'); 362 | } 363 | 364 | /** 365 | * Handle premature socket errors. 366 | * 367 | * @private 368 | */ 369 | function socketOnError() { 370 | this.destroy(); 371 | } 372 | 373 | /** 374 | * Close the connection when preconditions are not fulfilled. 375 | * 376 | * @param {net.Socket} socket The socket of the upgrade request 377 | * @param {Number} code The HTTP response status code 378 | * @param {String} [message] The HTTP response body 379 | * @param {Object} [headers] Additional HTTP response headers 380 | * @private 381 | */ 382 | function abortHandshake(socket, code, message, headers) { 383 | if (socket.writable) { 384 | message = message || STATUS_CODES[code]; 385 | headers = { 386 | Connection: 'close', 387 | 'Content-Type': 'text/html', 388 | 'Content-Length': Buffer.byteLength(message), 389 | ...headers 390 | }; 391 | 392 | socket.write( 393 | `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + 394 | Object.keys(headers) 395 | .map((h) => `${h}: ${headers[h]}`) 396 | .join('\r\n') + 397 | '\r\n\r\n' + 398 | message 399 | ); 400 | } 401 | 402 | socket.removeListener('error', socketOnError); 403 | socket.destroy(); 404 | } 405 | -------------------------------------------------------------------------------- /douyu/ws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "ws@^7.2.1", 3 | "_id": "ws@7.3.1", 4 | "_inBundle": false, 5 | "_integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", 6 | "_location": "/ws", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "ws@^7.2.1", 12 | "name": "ws", 13 | "escapedName": "ws", 14 | "rawSpec": "^7.2.1", 15 | "saveSpec": null, 16 | "fetchSpec": "^7.2.1" 17 | }, 18 | "_requiredBy": [ 19 | "/douyudm" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", 22 | "_shasum": "d0547bf67f7ce4f12a72dfe31262c68d7dc551c8", 23 | "_spec": "ws@^7.2.1", 24 | "_where": "/main/douyudanmu/node_modules/douyudm", 25 | "author": { 26 | "name": "Einar Otto Stangvik", 27 | "email": "einaros@gmail.com", 28 | "url": "http://2x.io" 29 | }, 30 | "browser": "browser.js", 31 | "bugs": { 32 | "url": "https://github.com/websockets/ws/issues" 33 | }, 34 | "bundleDependencies": false, 35 | "deprecated": false, 36 | "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", 37 | "devDependencies": { 38 | "benchmark": "^2.1.4", 39 | "bufferutil": "^4.0.1", 40 | "coveralls": "^3.0.3", 41 | "eslint": "^7.2.0", 42 | "eslint-config-prettier": "^6.0.0", 43 | "eslint-plugin-prettier": "^3.0.1", 44 | "mocha": "^7.0.0", 45 | "nyc": "^15.0.0", 46 | "prettier": "^2.0.5", 47 | "utf-8-validate": "^5.0.2" 48 | }, 49 | "engines": { 50 | "node": ">=8.3.0" 51 | }, 52 | "files": [ 53 | "browser.js", 54 | "index.js", 55 | "lib/*.js" 56 | ], 57 | "homepage": "https://github.com/websockets/ws", 58 | "keywords": [ 59 | "HyBi", 60 | "Push", 61 | "RFC-6455", 62 | "WebSocket", 63 | "WebSockets", 64 | "real-time" 65 | ], 66 | "license": "MIT", 67 | "main": "index.js", 68 | "name": "ws", 69 | "peerDependencies": { 70 | "bufferutil": "^4.0.1", 71 | "utf-8-validate": "^5.0.2" 72 | }, 73 | "peerDependenciesMeta": { 74 | "bufferutil": { 75 | "optional": true 76 | }, 77 | "utf-8-validate": { 78 | "optional": true 79 | } 80 | }, 81 | "repository": { 82 | "type": "git", 83 | "url": "git+https://github.com/websockets/ws.git" 84 | }, 85 | "scripts": { 86 | "integration": "mocha --throw-deprecation test/*.integration.js", 87 | "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"", 88 | "test": "nyc --reporter=html --reporter=text mocha --throw-deprecation test/*.test.js" 89 | }, 90 | "version": "7.3.1" 91 | } 92 | -------------------------------------------------------------------------------- /extends/pst.js: -------------------------------------------------------------------------------- 1 | var lastTrophies = null; 2 | function checkT(psnid){ 3 | $.ajax({ 4 | type:"get", 5 | url: "http://192.168.88.139:26666/t/"+psnid, 6 | success: function(resp){ 7 | if(lastTrophies){ 8 | var message = ""; 9 | var newTrophies = resp["trophies"]; 10 | if(newTrophies["bronze"] != lastTrophies["bronze"]){ 11 | message += ""; 12 | } 13 | if(newTrophies["gold"] != lastTrophies["gold"]){ 14 | message+= ""; 15 | } 16 | 17 | if(newTrophies["silver"] != lastTrophies["silver"]){ 18 | message += ""; 19 | } 20 | 21 | if(newTrophies["platinum"] != lastTrophies["platinum"]){ 22 | message += ""; 23 | } 24 | } 25 | lastTrophies = newTrophies; 26 | } 27 | }); 28 | } 29 | 30 | setInterval(function(){ 31 | checkT(""); 32 | }, 10000); 33 | -------------------------------------------------------------------------------- /help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tilerphy/ps4broadcast/6c22756eff912d6f08b2b1f59d404d959fe4ef73/help.png -------------------------------------------------------------------------------- /index.classic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PS4Broadcast Control Panel 8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 |
23 | 24 | 25 | 26 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PS4Broadcast Control Panel 8 | 9 | 10 | 11 |
12 |
Record Path:
13 |
Twitch Id: 14 |
15 |
16 |
17 | 18 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 | 31 | 32 | 33 | 119 | 270 | 271 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /index.html.save: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PS4Broadcast Control Panel 8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 |
16 | 17 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 | 30 | 31 | 32 | 125 | 276 | 277 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "install the required software......" 3 | apt-get update 4 | apt-get install -y git libpcre3-dev nodejs openssl libssl-dev gcc g++ automake zlib1g-dev make psmisc 5 | echo "download nginx......" 6 | wget http://nginx.org/download/nginx-1.18.0.tar.gz 7 | tar -xvf nginx-1.18.0.tar.gz 8 | cd nginx-1.18.0 9 | echo "clone nginx-rtmp-module from Github.........." 10 | git clone https://github.com/Tilerphy/nginx-rtmp-module.git 11 | echo "Complile nginx with rtmp-module....................." 12 | ./configure --prefix=/usr/local/nginx/ --add-module=./nginx-rtmp-module 13 | echo "Install nginx to /usr/local/nginx..................." 14 | make CFLAGS='-Wno-implicit-fallthrough' 15 | make install 16 | #echo "Install nodejs.." 17 | #wget https://nodejs.org/dist/v8.7.0/node-v8.7.0.tar.gz 18 | #tar -xvf node-v8.7.0.tar.gz 19 | #cd node-v8.7.0 20 | #./configure 21 | #make 22 | #make install 23 | echo "Prepare the rtmp config folder." 24 | rm -rf /usr/local/nginx/conf/rtmp.conf.d 25 | mkdir /usr/local/nginx/conf/rtmp.conf.d 26 | chmod 777 /usr/local/nginx/conf/rtmp.conf.d 27 | chmod 777 /usr/local/nginx/sbin/nginx 28 | sed -i "s/rtmp { include \/usr\/local\/nginx\/conf\/rtmp.conf.d\/\*;}//g" /usr/local/nginx/conf/nginx.conf 29 | sed -i "s/#user nobody;/rtmp { include \/usr\/local\/nginx\/conf\/rtmp.conf.d\/*;}\n#user nobody;/g" /usr/local/nginx/conf/nginx.conf 30 | #echo "Configure iptables................." 31 | #remove old 32 | #sed -i "s/iptables -N ps4broadcast -t nat//g" /etc/rc.local 33 | #sed -i "s/ifconfig $1:2 192.168.200.1 netmask 255.255.255.0//g" /etc/rc.local 34 | #sed -i "s/sysctl -w net.ipv4.ip_forward=1//g" /etc/rc.local 35 | #sed -i "s/sysctl -p//g" /etc/rc.local 36 | #sed -i "s/iptables -t nat -A PREROUTING --ipv4 -s 192.168.200.1 -j RETURN//g" /etc/rc.local 37 | #sed -i "s/iptables -t nat -A PREROUTING -p tcp -s 192.168.200.0\/24 --dport 1935 -j DNAT --to-destination 192.168.200.1:1935//g" /etc/rc.local 38 | #sed -i "s/iptables -t nat -A PREROUTING -s 192.168.200.0\/24 -j ps4broadcast//g" /etc/rc.local 39 | #sed -i "s/iptables -t nat -A PREROUTING -p tcp -s 192.168.200.0\/24 --dport 6667 -j DNAT --to-destination 192.168.200.1:6667//g" /etc/rc.local 40 | #sed -i "s/iptables -t nat -A POSTROUTING --ipv4 -j MASQUERADE//g" /etc/rc.local 41 | #add back 42 | #sed -i "s/exit 0/iptables -N ps4broadcast -t nat\nexit 0/g" /etc/rc.local 43 | #sed -i "s/exit 0/ifconfig $1:2 192.168.200.1 netmask 255.255.255.0\nexit 0/g" /etc/rc.local 44 | #sed -i "s/exit 0/sysctl -w net.ipv4.ip_forward=1\nexit 0/g" /etc/rc.local 45 | #sed -i "s/exit 0/sysctl -p\nexit 0/g" /etc/rc.local 46 | #sed -i "s/exit 0/iptables -t nat -A PREROUTING --ipv4 -s 192.168.200.1 -j RETURN\nexit 0/g" /etc/rc.local 47 | #sed -i "s/exit 0/iptables -t nat -A PREROUTING -p tcp -s 192.168.200.0\/24 --dport 1935 -j DNAT --to-destination 192.168.200.1:1935\nexit 0/g" /etc/rc.local 48 | #sed -i "s/exit 0/iptables -t nat -A PREROUTING -s 192.168.200.0\/24 -j ps4broadcast\nexit 0/g" /etc/rc.local 49 | #sed -i "s/exit 0/iptables -t nat -A PREROUTING -p tcp -s 192.168.200.0\/24 --dport 6667 -j DNAT --to-destination 192.168.200.1:6667\nexit 0/g" /etc/rc.local 50 | #sed -i "s/exit 0/iptables -t nat -A POSTROUTING --ipv4 -j MASQUERADE\nexit 0/g" /etc/rc.local 51 | #real do 52 | #iptables -N ps4broadcast -t nat 53 | #ifconfig $1:2 192.168.200.1 netmask 255.255.255.0 54 | #sysctl -w net.ipv4.ip_forward=1 55 | #sysctl -p 56 | #iptables -t nat -A PREROUTING --ipv4 -s 192.168.200.1 -j RETURN 57 | #iptables -t nat -A PREROUTING -p tcp -s 192.168.200.0/24 --dport 1935 -j DNAT --to-destination 192.168.200.1:1935 58 | #iptables -t nat -A PREROUTING -s 192.168.200.0/24 -j ps4broadcast 59 | #iptables -t nat -A PREROUTING -p tcp -s 192.168.200.0/24 --dport 6667 -j DNAT --to-destination 192.168.200.1:6667 60 | #iptables -t nat -A POSTROUTING --ipv4 -j MASQUERADE 61 | #clear 62 | echo "###############################################" 63 | echo "Completed." 64 | echo "Set on PS4/XBOX ONE/OBS" 65 | echo "IP: 192.168.200.any" 66 | echo "Gateway: 192.168.200.1" 67 | echo "Netmask: 255.255.255.0" 68 | echo "DNS: YOUR BEST DNS, mayebe 114.114.114.114" 69 | echo "run ./start-web.sh to start web-server." 70 | -------------------------------------------------------------------------------- /modules/README.md: -------------------------------------------------------------------------------- 1 | ## 有效模块 2 | 1. _开头的文件名是作为特殊模块存在的, room_开头的文件是加载弹幕拉去模块,目前支持斗鱼和bilibili 3 | 2. module.exports.invoke指向了有效的内置方法/函数。 4 | 3. 目前只有弹幕内容会作为参数发至这些模块。 5 | -------------------------------------------------------------------------------- /modules/_realroll.js: -------------------------------------------------------------------------------- 1 | var exec = require("child_process").exec; 2 | function invoke(txt){ 3 | if(txt.toLowerCase() == "roll"){ 4 | console.log("rolling!"); 5 | exec(__dirname+"/../88.e", (err, o,oe)=>{ 6 | console.log(oe); 7 | }); 8 | } 9 | } 10 | 11 | module.exports.invoke = invoke; 12 | -------------------------------------------------------------------------------- /modules/room_bilibili.js: -------------------------------------------------------------------------------- 1 | var net = require("net"); 2 | const xhttps = require('https'); 3 | const { deflate, unzip,inflate } = require('zlib'); 4 | 5 | var buvidString = ""; 6 | var buvid =""; 7 | 8 | function refreshToken(){ 9 | buvidString = ""; 10 | xhttps.get('https://api.bilibili.com/x/frontend/finger/spi', (response) => { 11 | response.on("data", (d)=>{ 12 | buvidString += d; 13 | }); 14 | 15 | response.on('end', () => { 16 | var buvidObj = JSON.parse(buvidString); 17 | buvid = buvidObj["data"]["b_3"]; 18 | console.log("buvid is "+ buvid); 19 | }); 20 | 21 | }); 22 | 23 | } 24 | 25 | setInterval(refreshToken, 10000); 26 | 27 | 28 | var init = function (rid, io, lp){ 29 | this.uid = 123456789012345 + parseInt(1000000000000000*Math.random()); 30 | this.rid = rid; 31 | this.lp = lp; 32 | this.interval = null; 33 | 34 | var sock = net.connect(80, "broadcastlv.chat.bilibili.com"); 35 | sock.on("connect", ()=>{ 36 | console.log("connected"); 37 | 38 | }); 39 | sock.on("data", (d)=>{ 40 | console.log(d); 41 | popBilibiliMsg(d, (msg)=>{ 42 | switch(msg.cmd){ 43 | case "DANMU_MSG": 44 | if(this.lp.currentTwitchClient){ 45 | this.lp.currentTwitchClient.toPS4(msg.info[2][1], msg.info[1]); 46 | } 47 | io.emit("message", '[B]'+msg.info[2][1]+": "+msg.info[1]); 48 | break; 49 | case "SEND_GIFT": 50 | if(this.lp.currentTwitchClient){ 51 | this.lp.currentTwitchClient.toPS4(msg.data.uname+"送出礼物√", msg.data.giftName); 52 | } 53 | io.emit("message",'[B]'+msg.data.uname+"送出礼物√:"+msg.data.giftName); 54 | break; 55 | case "WELCOME": 56 | if(this.lp.currentTwitchClient){ 57 | this.lp.currentTwitchClient.toPS4(msg.data.uanme, "进入直播间"); 58 | } 59 | io.emit("message", '[B]'+msg.data.uname+"进入直播间"); 60 | break; 61 | default: 62 | break; 63 | } 64 | }); 65 | }); 66 | 67 | var dataBuffer = Buffer.from("{\"roomid\":"+this.rid+",\"uid\":"+this.uid+",\"protover\":2, \"buvid\":\""+buvid+"\"}"); 68 | var headerBuffer= Buffer.from([0,0,0,dataBuffer.length+16,0,16,0,1,0,0,0,7,0,0,0,1]); 69 | var heartBeat = Buffer.from([0,0,0,16,0,16,0,1,0,0,0,2,0,0,0,1]); 70 | var packageBuffer = Buffer.alloc(dataBuffer.length+headerBuffer.length); 71 | packageBuffer = Buffer.concat([headerBuffer,dataBuffer]); 72 | var p = new Promise((resolve, reject)=>{ 73 | sock.write(packageBuffer,()=>{ 74 | this.interval = setInterval(()=>{sock.write(heartBeat); console.log("heartbeat");}, 30000); 75 | resolve(); 76 | }); 77 | }); 78 | 79 | 80 | this.close = ()=>{ 81 | clearInterval(this.interval); 82 | sock.destroy(); 83 | }; 84 | 85 | sock.on("end",()=>{ 86 | clearInterval(this.interval); 87 | sock.destroy(); 88 | }); 89 | sock.on("error",()=>{ 90 | clearInterval(this.interval); 91 | sock.destroy(); 92 | console.log("reconnect....."); 93 | init(rid,io,lp); 94 | }); 95 | 96 | Promise.all([p]); 97 | } 98 | var buf = null; 99 | function popBilibiliMsg(d, callback){ 100 | if(buf){ 101 | d = Buffer.concat([buf , d]); 102 | } 103 | var version = d[6]<<8 | d[7]; 104 | var action = d[8]|d[9]|d[10]|d[11]; 105 | console.log(action); 106 | if(callback && d[11]!= 3 && d[11]!= 8){ 107 | var length = d[0]*256*256*256+d[1]*256*256+d[2]*256+d[3]; 108 | //resolve bilibili TCP-nagle bug 109 | console.log(version); 110 | console.log("action"+action); 111 | if(version != 2){ 112 | if(d.length == length){ 113 | buf = null; 114 | callback(JSON.parse(d.slice(16).toString())); 115 | }else if (d.length > length){ 116 | buf = null; 117 | callback(JSON.parse(d.slice(16,length).toString())); 118 | popBilibiliMsg(d.slice(length), callback); 119 | }else{ 120 | buf = d; 121 | } 122 | }else{ 123 | if(action==5){ 124 | inflate(d.slice(16), (er4, newD)=>{ 125 | if(er4){ 126 | console.log(er4); 127 | }else{ 128 | _msg = newD.toString().slice(16); 129 | console.log(_msg); 130 | sta = []; 131 | tmpSentence = ""; 132 | reloop = true; 133 | for(var index=0;index<_msg.length; index++){ 134 | if (_msg[index] == '{'){ 135 | reloop = false; 136 | sta.push("{"); 137 | tmpSentence = tmpSentence+_msg[index]; 138 | } 139 | else if(_msg[index] == '}'){ 140 | sta.pop(); 141 | tmpSentence = tmpSentence+_msg[index]; 142 | } 143 | else{ 144 | if(sta.length == 0){ 145 | //ignore 146 | }else{ 147 | tmpSentence = tmpSentence+_msg[index]; 148 | } 149 | } 150 | if(sta.length == 0 && !reloop ){ 151 | //console.log("#"+tmpSentence); 152 | callback(JSON.parse(tmpSentence)); 153 | tmpSentence=""; 154 | reloop = true; 155 | } 156 | } 157 | } 158 | }); 159 | } 160 | } 161 | } 162 | } 163 | module.exports.type= "bilibili"; 164 | module.exports.init = init; 165 | -------------------------------------------------------------------------------- /modules/room_douyu.js: -------------------------------------------------------------------------------- 1 | 2 | var xhttp = require("http"); 3 | var douyu = require("douyudm"); 4 | function init(rid, io, lp, app){ 5 | this.rid = rid; 6 | this.lp= lp; 7 | this.opt= { 8 | debug:false 9 | }; 10 | this.currentRoom = new douyu(this.rid, this.opt); 11 | this.close = ()=>{ 12 | this.currentRoom.logout(); 13 | } 14 | this.currentRoom.on("error",()=>{ 15 | io.emit("message","[D]弹幕连接失败"); 16 | }); 17 | this.currentRoom.on("chatmsg", (msg)=>{ 18 | if(this.lp.currentTwitchClient){ 19 | this.lp.currentTwitchClient.toPS4(msg.nn, msg.txt); 20 | } 21 | io.emit("message",'[D]'+msg.nn + ":"+msg.txt); 22 | }); 23 | this.currentRoom.on("uenter", (msg)=>{ 24 | if(this.lp.currentTwitchClient){ 25 | this.lp.currentTwitchClient.toPS4(msg.nn, "进入直播间"); 26 | } 27 | io.emit("message",'[D]'+msg.nn +": 进入直播间"); 28 | }); 29 | this.currentRoom.on("dgb", (msg)=>{ 30 | var douyuReq= xhttp.request("http://open.douyucdn.cn/api/RoomApi/room/"+this.rid, (res)=>{ 31 | var all = ""; 32 | res.on("data",(d)=>{ 33 | all+=d.toString(); 34 | }); 35 | 36 | res.on("end", ()=>{ 37 | var gifts = JSON.parse(all).data.gift; 38 | var gift = null; 39 | if(gifts){ 40 | for(var g of gifts){ 41 | if(g.id == msg.gfid){ 42 | gift = g; 43 | break; 44 | } 45 | } 46 | } 47 | if(this.lp.currentTwitchClient){ 48 | this.lp.currentTwitchClient.toPS4(msg.nn+"送出礼物√", gift ==null?"":gift.name); 49 | } 50 | io.emit("message", '[D]'+msg.nn+"送出礼物√ "+ (gift==null?"":gift.name)); 51 | }); 52 | }); 53 | douyuReq.end(); 54 | }); 55 | try{ 56 | this.currentRoom.run(); 57 | }catch(e){ 58 | io.emit("error", e.toString()); 59 | } 60 | 61 | } 62 | 63 | module.exports.type = "douyu"; 64 | module.exports.init = init; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ps4broadcast", 3 | "version": "1.0.0", 4 | "description": "======================================================== 经网友测试,Ubuntu 16.04 16.10 17.04 17.10都无法使用,原因未知,希望大神帮助解决。 可能是iptables masquerade功能失效 或者 设置的软网卡失效 引起。", 5 | "main": "start.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Tilerphy/ps4broadcast.git" 12 | }, 13 | "author": "Tilerphy", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/Tilerphy/ps4broadcast/issues" 17 | }, 18 | "homepage": "https://github.com/Tilerphy/ps4broadcast#readme", 19 | "dependencies": { 20 | "express": "^4.16.2", 21 | "socket.io": "^2.0.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /start-web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | basepath=$(cd `dirname $0`; pwd) 3 | cd $basepath 4 | mkdir -p node_modules 5 | cp -R ./douyu/* node_modules 6 | iptables -t nat -F 7 | #ip route add local default dev lo table 100 8 | #ip rule add fwmark 1 lookup 100 9 | ifconfig $1:2 192.168.200.1 netmask 255.255.255.0 10 | sysctl -w net.ipv4.ip_forward=1 11 | sysctl -p 12 | iptables -t nat -A PREROUTING --ipv4 -s 192.168.200.1 -j RETURN 13 | iptables -t nat -A PREROUTING -p tcp -s 192.168.200.0/24 --dport 1935 -j DNAT --to-destination 192.168.200.1:1935 14 | #iptables -t nat -A PREROUTING -s 192.168.200.0/24 -j ps4broadcast 15 | iptables -t nat -A PREROUTING -p tcp -s 192.168.200.0/24 --dport 6667 -j DNAT --to-destination 192.168.200.1:6667 16 | #iptables -t nat -A PREROUTING --ipv4 -s 192.168.200.0/24 -p tcp -j DNAT --to-destination 192.168.200.1:20000 17 | #iptables -t mangle -A PREROUTING -p udp -j TPROXY --on-port 20000 --tproxy-mark 0x01/0x01 18 | iptables -t filter -A FORWARD -s 192.168.200.0/24 -j ACCEPT 19 | iptables -t filter -A FORWARD -d 192.168.200.0/24 -j ACCEPT 20 | iptables -t nat -A POSTROUTING --ipv4 -j MASQUERADE 21 | node $basepath/start.js 22 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 2.3.3.20230827 更新协议修复bilibili弹幕 2 | --------------------------------------------------------------------------------