- 更新协议版本,使用较新协议(webcast100) - 增加登录相关 Cookie 配置入口 - 目前需登录才可正常接收礼物信息 - 未登录只能获取其它信息,无法获取礼物信息 - 配置操作可查看界面说明 => ? - 更新 emoji 列表,支持更多 emoji - 对界面做一些其它优化
345 lines
11 KiB
Markdown
345 lines
11 KiB
Markdown
# 抖音弹幕姬
|
|
|
|
<p align=center>
|
|
<a href="https://github.com/skmcj/dycast">
|
|
<img src="https://gcore.jsdelivr.net/gh/skmcj/pic-bed/common/dydm-bg-logo.png" alt="抖音弹幕姬" style="width: 200px">
|
|
</a>
|
|
</p>
|
|
|
|
<p align=center style="font-weight: bold;">
|
|
抖音弹幕姬
|
|
</p>
|
|
|
|
## 简介
|
|
|
|
一个用于获取抖音直播间弹幕的小作品
|
|
|
|
用户只需要输入直播间的房间号,程序就能实时获取对应直播间的弹幕,并将其解析展示出来,用户还可通过`ws/wss`地址将获取的弹幕信息转发到自己的后端以作它用(如:弹幕互动游戏、数据分析等)
|
|
|
|
### 实现功能
|
|
|
|
- 获取直播间连接信息
|
|
- 连接直播间,获取直播间弹幕
|
|
- 实现重连机制,能一定程度保证连接的稳定性
|
|
- 转发直播间弹幕
|
|
- 主要为解析提取后的弹幕,通过序列化`json`的格式
|
|
- 分类展示直播间弹幕
|
|
- 聊天弹幕(包含文本、普通表情、会员表情、合并表情等)
|
|
- 礼物弹幕(需登录 cookie)
|
|
- 关注弹幕
|
|
- 点赞弹幕(包含点赞数量)
|
|
- 进入弹幕
|
|
- 其它信息(如连接过程的一些提示)
|
|
- 展示直播间信息,如人数等
|
|
|
|
## 实现原理
|
|
|
|
主要需解决两个难点,分别为计算抖音弹幕的`wss`链接和解析接收到的二进制弹幕数据
|
|
|
|
- 计算抖音弹幕的`wss`链接
|
|
|
|
- 进入抖音直播间,打开浏览器的网络请求面板,可看到有一个`ws`链接,其则为抖音直播间实时弹幕通信链接
|
|
- 观察它的协议地址,主要包含一个最重要的参数`signature`,其需通过`roomId`与`uniqueId`计算得出,通过断点跟随,执行一些逆向工程,即可知道其大致的计算原理
|
|
- 本项目将对应计算函数封装在`src/core/signature`文件内
|
|
- 计算出`signature`参数后,再将其与前面的`roomId`与`uniqueId`整合,即可得到完整的`wss`链接
|
|
- 之后建立链接,接收数据即可
|
|
|
|
- 解析弹幕数据
|
|
|
|
- 成功建立链接后,会发现接收到的数据为二进制串
|
|
|
|
- 在网上查阅资料可知,其运用技术为`protobuf`协议传输,要解析,需对应的`proto`文件
|
|
|
|
- 相应的`proto`文件可通过进入直播间,通过一些逆向工程,模仿其背后解析的对象结构,整合出相应的`proto`文件
|
|
|
|
- 具体可自行尝试,如没有思路,可通过一些关键词搜寻,如`PushFrame`、`WebcastChatMessage`等
|
|
|
|
- 得出弹幕数据的`proto`文件后,使用`protoc`或其它一些工具将其编译为各种语言的文件
|
|
|
|
- 本项目主要是编译为`ts`文件,通过`protobufjs`进行编译,并对产物进行魔改,将`Long`改为字符串
|
|
|
|
- ```sh
|
|
# 参考命令
|
|
pbjs --ts model.ts model.proto
|
|
```
|
|
|
|
- 生成的`[model.ts]`即可在项目引入使用
|
|
|
|
- 有了解析文件后,即可使用其解析弹幕数据。将`ws`获取到的一帧数据解析为`PushFrame`,其中的`payload`依旧为一段二进制数据,且经过了`gzip`压缩,对其进行解压后,解析为`Response`,其中的`messages`即为对应的消息数据,结构为`Message`类型,其中的`payload`解析后即为具体的弹幕消息体,主要解析类型有`ChatMessage`、`MemberMessage`、`LikeMessage`等。
|
|
|
|
- 以上的`PushFrame`、`··· ···`均为弹幕数据的`proto`结构,具体可自行了解
|
|
|
|
## 数据结构
|
|
|
|
包装传给后台的数据
|
|
|
|
```typescript
|
|
/** 最后的整理转发的弹幕消息结构 */
|
|
export interface DyMessage {
|
|
// 弹幕 ID
|
|
id?: string;
|
|
// 弹幕类型
|
|
method?: CastMethod;
|
|
// 用户信息
|
|
user?: CastUser;
|
|
// 送给用户
|
|
toUser?: CastUser;
|
|
// 礼物信息(当类型为礼物弹幕时有值)
|
|
gift?: CastGift;
|
|
// 弹幕文本
|
|
content?: string;
|
|
// 富文本信息
|
|
rtfContent?: CastRtfContent[];
|
|
// 房间相关信息
|
|
room?: LiveRoom;
|
|
// 礼物排行榜信息
|
|
rank?: LiveRankItem[];
|
|
}
|
|
|
|
/** 直播间信息 */
|
|
export interface LiveRoom {
|
|
/**
|
|
* 在线观众数
|
|
*/
|
|
audienceCount?: number | string;
|
|
/**
|
|
* 本场点赞数
|
|
*/
|
|
likeCount?: number | string;
|
|
/**
|
|
* 主播粉丝数
|
|
*/
|
|
followCount?: number | string;
|
|
/**
|
|
* 累计观看人数
|
|
*/
|
|
totalUserCount?: number | string;
|
|
/** 房间状态 */
|
|
status?: number;
|
|
}
|
|
/**
|
|
* 送礼点赞榜
|
|
*/
|
|
export interface LiveRankItem {
|
|
nickname: string;
|
|
avatar: string;
|
|
rank: number | string;
|
|
}
|
|
|
|
export interface CastUser {
|
|
// user.sec_uid | user.id_str
|
|
id?: string;
|
|
// user.nickname
|
|
name?: string;
|
|
// user.avatar_thumb.url_list.0
|
|
avatar?: string;
|
|
// 性别(猜测) 0 | 1 | 2 => 未知 | 男 | 女
|
|
gender?: number;
|
|
}
|
|
|
|
export interface CastGift {
|
|
id?: string;
|
|
name?: string;
|
|
// 价值抖音币 diamond_count
|
|
price?: number;
|
|
type?: number;
|
|
// 描述
|
|
desc?: string;
|
|
// 图片
|
|
icon?: string;
|
|
// 数量 repeat_count | combo_count
|
|
count?: number | string;
|
|
// 礼物消息可能重复发送,0 表示第一次,未重复
|
|
repeatEnd?: number;
|
|
}
|
|
|
|
/**
|
|
* 富文本类型
|
|
* 1 - 普通文本
|
|
* 2 - 合并表情
|
|
*/
|
|
export enum CastRtfContentType {
|
|
TEXT = 1,
|
|
EMOJI = 2
|
|
}
|
|
|
|
// 富文本
|
|
export interface CastRtfContent {
|
|
type?: CastRtfContentType;
|
|
text?: string;
|
|
url?: string;
|
|
}
|
|
// 弹幕类型
|
|
export enum CastMethod {
|
|
CHAT = 'WebcastChatMessage',
|
|
GIFT = 'WebcastGiftMessage',
|
|
LIKE = 'WebcastLikeMessage',
|
|
MEMBER = 'WebcastMemberMessage',
|
|
SOCIAL = 'WebcastSocialMessage',
|
|
ROOM_USER_SEQ = 'WebcastRoomUserSeqMessage',
|
|
CONTROL = 'WebcastControlMessage',
|
|
ROOM_RANK = 'WebcastRoomRankMessage',
|
|
ROOM_STATS = 'WebcastRoomStatsMessage',
|
|
EMOJI_CHAT = 'WebcastEmojiChatMessage',
|
|
FANSCLUB = 'WebcastFansclubMessage',
|
|
ROOM_DATA_SYNC = 'WebcastRoomDataSyncMessage',
|
|
/** 自定义消息 */
|
|
CUSTOM = 'CustomMessage'
|
|
}
|
|
```
|
|
|
|
**注意:** 理论上,接收的原始弹幕数据包含抖音弹幕该有的全部数据,但传递给后台的目前只提取包装了以上较为重要的数据,如需其它数据,可自行研究包装修改,目标文件为`src/core/dycast.ts`
|
|
|
|
## 项目预览
|
|
|
|
完整项目演示,请移步[哔哩哔哩](https://www.bilibili.com/video/BV1Vj411c7FF/)
|
|
|
|
- 项目运行后,具体界面展示如下
|
|
|
|

|
|
|
|
- 整体界面为三栏布局:左侧为直播间信息及连接状态展示;中间为主要弹幕展示;右侧为输入及其它信息展示
|
|
- 右侧主要包含两个输入框,第一个为房间号输入框,第二个为转发地址输入框;输入带有格式验证,格式不正确无法连接
|
|
- 弹幕展示列表右侧的一排图标按钮表示当前列表所展示的弹幕类型,点击可控制其显隐
|
|
|
|
- 在右侧房间号输入框输入房间号后,点击**连接**,等待几秒后,会在左下方状态信息展示连接结果,有时可能出现网络拥堵情况,稍后再连接即可,正常连接成功/失败均会有相应的消息通知提示,也可以看控制台输出。连接成功后,大致展示如下:
|
|
|
|

|
|
|
|
- 此时,用户可在转发信息框填入自己的`WebSocket`服务端地址,点击**转发**,即可建立连接,将弹幕信息实时传送到所设置后端
|
|
|
|
## 部署步骤
|
|
|
|
- 项目依赖安装
|
|
|
|
```sh
|
|
npm install
|
|
```
|
|
|
|
- 项目运行
|
|
|
|
```sh
|
|
npm run dev
|
|
```
|
|
|
|
- 项目打包
|
|
|
|
```sh
|
|
npm run build
|
|
```
|
|
|
|
- 项目部署到`nginx`
|
|
|
|
```nginx
|
|
# 配置网络监听
|
|
server {
|
|
# 监听端口号,如:1234
|
|
listen 1234;
|
|
# 监听地址,可以是域名或ip地址,可正则书写
|
|
server_name localhost;
|
|
|
|
location / {
|
|
add_header Access-Control-Allow-Origin *;
|
|
# 根目录,即项目打包内容位置(···/dist),可以是项目的本地路径
|
|
root /var/dycast;
|
|
# 配置默认主页文件
|
|
index index.html index.htm;
|
|
# 配置单页面应用刷新问题,默认返回主页
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
# 配置接口跨域
|
|
location /dylive {
|
|
# proxy_pass 你要跨域的的接口地址
|
|
proxy_pass https://live.douyin.com/;
|
|
|
|
# 响应头大小
|
|
proxy_buffer_size 64k;
|
|
# 响应体大小 = 数量 * size
|
|
proxy_buffers 32 64k;
|
|
# 处于busy状态的buffer大小,一般为 proxy_buffer_size * 2
|
|
proxy_busy_buffers_size 128k;
|
|
|
|
# 修改请求头
|
|
proxy_set_header Host live.douyin.com;
|
|
proxy_set_header Referer 'https://live.douyin.com/';
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
# 如果需要配置移动端打开也能用
|
|
# 需设置请求头 User-Agent,伪装 PC 端 UA,防止移动端重定向
|
|
set $ua $http_user_agent;
|
|
if ($http_user_agent ~* "(iphone|ipad|android|mobile)") {
|
|
set $ua "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0";
|
|
}
|
|
proxy_set_header User-Agent $ua;
|
|
|
|
# 处理响应 Set-Cookie
|
|
# 确保 Set-Cookie 能正常设置到当前域下
|
|
# 清空 Domain
|
|
proxy_cookie_domain ~.* $host;
|
|
# 统一 Path
|
|
proxy_cookie_path / /;
|
|
|
|
# 清除 SameSite / Secure
|
|
# 不一定都需要设置,某些浏览器需要
|
|
# 可借助 ngx_headers_more 模块实现
|
|
|
|
# 确保 Set-Cookie 被转发到客户端
|
|
proxy_pass_header Set-Cookie;
|
|
|
|
|
|
# 重写路径 - 移除/dylive前缀
|
|
rewrite ^/dylive/(.*) /$1 break;
|
|
}
|
|
|
|
location /socket {
|
|
# Nginx 不区分 ws / wss 协议
|
|
# WebSocket 实际上是通过 HTTP 升级实现的
|
|
# 故使用 https:// 非 wss://
|
|
proxy_pass https://webcast100-ws-web-lq.douyin.com/;
|
|
|
|
# WebSocket 关键配置
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
|
|
# 跨域相关头
|
|
proxy_set_header Origin https://live.douyin.com;
|
|
proxy_set_header Host webcast5-ws-web-lf.douyin.com;
|
|
|
|
# 可选:保留 Cookie 头,用于认证
|
|
proxy_set_header Cookie $http_cookie;
|
|
|
|
set $ua $http_user_agent;
|
|
if ($http_user_agent ~* "(iphone|ipad|android|mobile)") {
|
|
set $ua "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0";
|
|
}
|
|
proxy_set_header User-Agent $ua;
|
|
|
|
# 重写路径 - 移除/socket
|
|
rewrite ^/socket/(.*) /$1 break;
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
|
|
## Star History
|
|
|
|

|
|
|
|
## 打赏
|
|
|
|
<p align=center>
|
|
<img src="https://static.ltgcm.top/md/20250428191027.png" alt="打赏" style="width: 350px">
|
|
</p>
|
|
|
|
<p align=center style="color: #68945c;">
|
|
如果想支持本项目的持续维护,可以投喂UP (`・ω・´)ゞ敬礼っ
|
|
</p>
|
|
|
|
|
|
|
|
## 免责声明
|
|
|
|
本项目仅用于学习交流使用,禁止一切非法滥用,否则后果自负 |