一个基于 WebSocket 的抖音直播弹幕抓取工具。
项目边界说明,请先阅读
本项目仅用于研究和记录抖音直播 WebSocket 链接的逆向获取、连接方式及基础数据接收流程。
本项目不承诺、也不负责保证任何具体业务消息一定能够收到或完整解析。包括但不限于:礼物消息收不到、某类消息缺失、字段无法解析、消息结构变化、个别直播间数据不完整等问题,均不在本项目维护范围内。
请不要提交“没有礼物消息”“某类消息解析不了”“为什么收不到某条消息”等相关 Issue。此类问题不会作为 Bug 处理,也不作为本项目后续适配目标。
它做的事很简单:
- 连接抖音直播间消息流
- 解析弹幕 / 礼物 / 点赞 / 进场等消息
- 再通过你本地启动的 WebSocket 服务把消息转发给你的客户端
适合两种用法:
- 直接当成一个本地 WebSocket 服务用
- 当成 Go 库集成到你自己的项目里
- 实时接收直播间消息
- 支持单进程监听多个直播间
- 支持弹幕、礼物、点赞、进场、关注等常见消息
- 支持可选 Cookie,适配部分需要登录态的场景
- 可作为独立服务运行,也可作为 Go 库使用
- 内置断线重连和基础保活逻辑
这个项目主要是直播间消息抓取 / 转发,不是录播工具。
它不负责:
- 下载 flv / m3u8 视频流
- 录制直播画面
- 保存回放
如果你要的是录播,应该看录制类项目;如果你要的是实时弹幕、礼物、互动消息,这个项目更合适。
- 打开 Releases
- 下载对应平台的程序
- 运行程序
发布包名称会带上版本号和构建 commit,格式类似:
douyinLive-v2.0.3-abcdef123456-linux-amd64.tar.gz
douyinLive-v2.0.3-abcdef123456-windows-amd64.zip
压缩包里的可执行文件名仍然固定为 douyinLive,所以脚本和 Docker 启动命令不需要因为 hash 变化而每次修改。
./douyinLive程序启动后会在本地启动一个 WebSocket 服务,默认端口是 1088。
然后你的客户端连接:
ws://127.0.0.1:1088/ws/直播间标识
例如:
ws://127.0.0.1:1088/ws/516466932480
git clone https://github.com/jwwsjlm/douyinLive.git
cd douyinLive
go build -o douyinLive ./cmd/main
./douyinLive查看当前二进制的构建信息:
./douyinLive --version输出示例:
tag=v2.0.3 commit=abcdef123456 buildDate=2026-05-24T00:00:00Z source=github-actions/release#123.1
docker run --rm -p 1088:1088 ghcr.io/jwwsjlm/douyinlive:latest程序启动后,对外提供的 WebSocket 地址仍然是:
ws://127.0.0.1:1088/ws/直播间标识
如果你需要固定版本,也可以直接拉指定 tag:
docker run --rm -p 1088:1088 ghcr.io/jwwsjlm/douyinlive:v2.0.3Docker 镜像也支持查看构建信息:
docker run --rm ghcr.io/jwwsjlm/douyinlive:v2.0.3 --version如果你希望加载自定义配置,先在宿主机准备一个 config.yaml,再把它挂载到容器中的 /app/config.yaml,并通过 --config 显式传入:
docker run --rm -p 1088:1088 \
-v $(pwd)/config.yaml:/app/config.yaml:ro \
ghcr.io/jwwsjlm/douyinlive:latest --config /app/config.yaml说明:
-v $(pwd)/config.yaml:/app/config.yaml:ro:把宿主机当前目录下的config.yaml挂载到容器内:ro:只读挂载,避免容器误改宿主机配置--config /app/config.yaml:显式指定程序读取这个配置文件
如果你希望容器长期后台运行,不要使用 --rm,建议改成:
docker run -d \
--name douyinlive \
--restart unless-stopped \
-p 1088:1088 \
-v $(pwd)/config.yaml:/app/config.yaml:ro \
ghcr.io/jwwsjlm/douyinlive:latest --config /app/config.yaml这样即使容器被删除或重建,宿主机上的 config.yaml 仍然保留,达到配置持久化的效果。
如果你后续不只想挂一个配置文件,也可以直接挂整个目录:
mkdir -p ./data
cp config.example.yaml ./data/config.yaml
docker run -d \
--name douyinlive \
--restart unless-stopped \
-p 1088:1088 \
-v $(pwd)/data:/app/data \
ghcr.io/jwwsjlm/douyinlive:latest --config /app/data/config.yaml这种方式更适合统一管理容器运行时使用到的文件。
项目已自带两个 compose 示例文件:
compose.yaml:挂载单个config.yamlcompose.data.yaml:挂载整个data目录
先准备配置文件:
cp config.example.yaml config.yaml然后直接启动:
docker compose up -d
docker compose logs -f
docker compose downcompose.yaml 内容如下:
services:
douyinlive:
image: ghcr.io/jwwsjlm/douyinlive:latest
container_name: douyinlive
restart: unless-stopped
ports:
- "1088:1088"
volumes:
- ./config.yaml:/app/config.yaml:ro
command: ["--config", "/app/config.yaml"]如果你想把配置统一收纳到目录里,先执行:
mkdir -p data
cp config.example.yaml data/config.yaml然后用下面命令启动:
docker compose -f compose.data.yaml up -d
docker compose -f compose.data.yaml logs -f
docker compose -f compose.data.yaml downcompose.data.yaml 内容如下:
services:
douyinlive:
image: ghcr.io/jwwsjlm/douyinlive:latest
container_name: douyinlive
restart: unless-stopped
ports:
- "1088:1088"
volumes:
- ./data:/app/data
command: ["--config", "/app/data/config.yaml"]此时你只需要保证宿主机存在:
./data/config.yaml
docker logs -f douyinlive
docker ps
docker stop douyinlive
docker rm -f douyinlive很多人第一次用会卡在这里。
这个程序启动时不需要在命令行传直播间号。
直播间标识是通过 WebSocket 路径传进去的:
ws://127.0.0.1:1088/ws/直播间标识
也就是说:
- 程序只负责启动本地服务
- 你连接哪个房间,是由
/ws/后面的内容决定的
一般就是你访问下面这个地址时,后面的那段:
https://live.douyin.com/xxxxx
这里的 xxxxx 就是你应该传给 /ws/ 的内容。
例如:
https://live.douyin.com/516466932480- 则连接:
ws://127.0.0.1:1088/ws/516466932480
- 则连接:
如果你传的是无效标识,服务端会关闭这个连接。
如果直播间暂时未开播:
- 本地 WebSocket 连接会保留
- 服务端会先返回一条“直播间未开播”的状态通知
- 然后按配置的时间间隔持续推送未开播状态
- 一旦检测到开播,就自动切回正常消息流
douyinLive 启动后是一个本地 WebSocket 服务。直播间标识不是 CLI 启动参数,而是客户端连接 WebSocket 时写在 URL 里。
cp config.example.yaml config.yaml
./douyinLive --config ./config.yaml --port 1088 --log-level info然后让你的客户端连接:
ws://127.0.0.1:1088/ws/516466932480
Copy-Item .\config.example.yaml .\config.yaml
.\douyinLive.exe --config .\config.yaml --port 1088 --log-level info然后让你的客户端连接:
ws://127.0.0.1:1088/ws/516466932480
如果不需要配置文件,也可以直接启动:
./douyinLiveWindows:
.\douyinLive.exe默认行为:
- 读取同目录下的
config.yaml(如果存在) - 如果没有配置文件,就使用默认值
- 默认端口:
1088 - 默认日志级别:
info
./douyinLive --port 1088Windows:
.\douyinLive.exe --port 1088./douyinLive --config ./config.yamlWindows:
.\douyinLive.exe --config .\config.yaml./douyinLive --unknownWindows:
.\douyinLive.exe --unknown./douyinLive --log-level debugWindows:
.\douyinLive.exe --log-level debug支持 debug、info、warn、error,默认是 info。也可以写进配置文件:
log:
level: "debug"日志使用 Go slog 文本格式,会带上 level、time 以及 room_id、live_id、err 等字段,方便长时间挂机时排查连接和重连状态。
./douyinLive --versionWindows:
.\douyinLive.exe --version输出会包含:
tag:本次构建对应的 tag,本地手动构建默认为devcommit:构建时注入的短 commit hashbuildDate:构建时间source:构建来源,例如 GitHub Actions 或本地构建
--config string 指定配置文件路径,例如 ./config.yaml
--port string 本地 WebSocket 服务端口,默认 1088
--unknown 输出未知 protobuf 消息类型,调试用
--log-level string 日志级别:debug、info、warn、error
--version 输出版本和构建来源
你可以创建一个 config.yaml 放在程序同目录下。
示例:
port: "1088"
unknown: false
log:
level: "info"
monitor:
poll_interval: "15s"
notify_interval: "30s"
cookie:
douyin: ""
rooms:
# "516466932480": "ttwid=...; sessionid=..."项目里也自带了一个示例文件:
config.example.yaml
本地 WebSocket 服务端口。
默认值:
port: "1088"是否打印未知消息类型。
默认值:
unknown: false日志级别。默认输出 info 及以上级别,排查连接、心跳、重连问题时可以临时调整为 debug。
默认值:
log:
level: "info"未开播时,服务端检查“是否已经开播”的时间间隔。
默认值:
monitor:
poll_interval: "15s"未开播时,服务端向本地 WebSocket 客户端重复推送状态通知的时间间隔。
默认值:
monitor:
notify_interval: "30s"客户端会收到类似:
{"type":"system","event":"live_status","live":false,"room_id":"516466932480","message":"直播间未开播","retry_interval_seconds":30}抖音默认 Cookie,可选。
没有单独配置某个直播间的 Cookie 时,会优先回退到这里。再往后才是自动获取的逻辑。
cookie:
douyin: "ttwid=...; sessionid=..."按直播间 ID 单独配置 Cookie,可选。
如果你要同时监听多个直播间,而且它们对应不同账号、不同登录态,就可以在这里分别配置。没有配置到的直播间,会自动回退使用 cookie.douyin。
cookie:
douyin: "默认 Cookie"
rooms:
"516466932480": "直播间 516466932480 专用 Cookie"
"123456789": "直播间 123456789 专用 Cookie"
"888888888": "直播间 888888888 专用 Cookie"一个更完整的例子:
port: "1088"
unknown: false
log:
level: "info"
monitor:
poll_interval: "15s"
notify_interval: "30s"
cookie:
douyin: "默认 Cookie"
rooms:
"516466932480": "room A 的 Cookie"
"123456789": "room B 的 Cookie"Cookie 优先级:
WebSocket 临时 Cookie > 直播间 Cookie(cookie.rooms) > 默认 Cookie(cookie.douyin) > 自动获取
WebSocket 临时 Cookie 仅建议临时调试使用:
ws://127.0.0.1:1088/ws/直播间ID?cookie_b64=BASE64URL_COOKIE
也支持直接传 URL 编码后的 Cookie:
ws://127.0.0.1:1088/ws/直播间ID?cookie=URL_ENCODED_COOKIE
不是所有场景都必须填 Cookie。
你可以先不填,直接跑。
如果出现下面这些情况,再考虑补 Cookie:
- 某些直播间拿不到消息
- 请求被限制
- 页面返回结果异常
- 需要更稳定的登录态
- 浏览器打开:
https://live.douyin.com - 登录抖音
- 按
F12 - 打开
Network - 随便点一个请求
- 复制请求头里的
Cookie
然后填到:
cookie:
douyin: "你的完整 Cookie"你也可以直接把 douyinLive 作为 Go 库集成到你自己的项目中。
go get github.com/jwwsjlm/douyinLive/v2新版本推荐使用 LiveMessage 相关订阅接口:
SubscribeMessage(handler):订阅所有抖音消息SubscribeMethod(method, handler):只订阅一个消息类型SubscribeMethods(methods, handler):订阅多个消息类型
消息类型由抖音 WebSocket 下发的 method 字段决定,例如 WebcastChatMessage、WebcastGiftMessage、WebcastLikeMessage。也就是说,订阅分发不是靠结构体类型猜测,而是先看 method 字符串,再把匹配到的消息交给对应 handler。
LiveMessage 会同时带上原始消息、已解析消息和直播间元信息:
type LiveMessage struct {
LiveID string
RoomID string
LiveName string
Title string
AvatarThumb string
Raw *new_douyin.Webcast_Im_Message
Parsed proto.Message
ReceivedAt time.Time
}常用方法:
msg.GetMethod():获取消息类型msg.GetPayload():获取 protobuf 原始 payload
如果你的项目使用 log/slog,可以直接用 NewDouyinLiveWithSlog 创建实例,日志会保留结构化级别和字段:
dl, err := douyinlive.NewDouyinLiveWithSlog(roomID, slog.Default(), cookie)package main
import (
"log"
douyinlive "github.com/jwwsjlm/douyinLive/v2"
)
func main() {
// 直播间ID,从 https://live.douyin.com/xxxx 获取
roomID := "516466932480"
// 可选 Cookie,如果需要登录态可以传入,留空表示不使用
cookie := ""
// 创建实例
dl, err := douyinlive.NewDouyinLive(roomID, log.Default(), cookie)
if err != nil {
log.Fatalf("创建失败: %v", err)
return
}
// 订阅所有抖音消息
dl.SubscribeMessage(func(msg *douyinlive.LiveMessage) {
log.Printf("收到消息 method=%s payload_len=%d live=%s\n",
msg.GetMethod(),
len(msg.GetPayload()),
msg.LiveName,
)
})
// 启动监听,会阻塞直到连接关闭
dl.Start()
}package main
import (
"log"
douyinlive "github.com/jwwsjlm/douyinLive/v2"
"github.com/jwwsjlm/douyinLive/v2/generated/new_douyin"
"google.golang.org/protobuf/proto"
)
func main() {
roomID := "516466932480"
dl, err := douyinlive.NewDouyinLive(roomID, log.Default(), "")
if err != nil {
log.Fatal(err)
}
dl.SubscribeMethod(douyinlive.WebcastChatMessage, func(msg *douyinlive.LiveMessage) {
chat := &new_douyin.Webcast_Im_ChatMessage{}
if err := proto.Unmarshal(msg.GetPayload(), chat); err != nil {
log.Println(err)
return
}
if chat.GetContent() != "" && chat.GetUser() != nil {
log.Printf("弹幕 [%s]: %s\n", chat.GetUser().GetNickname(), chat.GetContent())
}
})
dl.SubscribeMethods([]string{
douyinlive.WebcastGiftMessage,
douyinlive.WebcastLikeMessage,
}, func(msg *douyinlive.LiveMessage) {
switch msg.GetMethod() {
case douyinlive.WebcastGiftMessage:
gift := &new_douyin.Webcast_Im_GiftMessage{}
if err := proto.Unmarshal(msg.GetPayload(), gift); err != nil {
log.Println(err)
return
}
if gift.GetUser() != nil && gift.GetGift() != nil {
log.Printf("礼物: %s 赠送了 %s x%d\n",
gift.GetUser().GetNickname(),
gift.GetGift().GetName(),
gift.GetCount(),
)
}
case douyinlive.WebcastLikeMessage:
like := &new_douyin.Webcast_Im_LikeMessage{}
if err := proto.Unmarshal(msg.GetPayload(), like); err != nil {
log.Println(err)
return
}
if like.GetUser() != nil {
log.Printf("%s 点赞了直播间\n", like.GetUser().GetNickname())
}
}
})
dl.Start()
}更多消息类型可以参考 generated/new_douyin 包下的 protobuf 生成代码。
旧的 Subscribe(func(raw, parsed)) 接口仍然保留,方便已有代码兼容;新代码建议优先使用 SubscribeMessage / SubscribeMethod / SubscribeMethods。
如果你直接运行独立服务,你的客户端只需要连本地 WebSocket 服务即可。
const ws = new WebSocket('ws://127.0.0.1:1088/ws/516466932480');
ws.onopen = () => {
console.log('已连接');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到消息:', data);
if (data.event === 'live_status') {
if (data.live) {
console.log('状态通知: 直播间已开播');
} else if (data.ended) {
console.log(`状态通知: ${data.message},后续会继续按 ${data.retry_interval_seconds} 秒轮询`);
} else {
console.log(`状态通知: ${data.message},${data.retry_interval_seconds} 秒后重试`);
}
return;
}
switch (data.method) {
case 'WebcastChatMessage':
console.log(`弹幕: ${data.user.nickname} - ${data.content}`);
break;
case 'WebcastGiftMessage':
console.log(`礼物: ${data.user.nickname} 赠送了 ${data.gift.name}`);
break;
case 'WebcastLikeMessage':
console.log(`${data.user.nickname} 点赞了直播间`);
break;
default:
break;
}
};
ws.onclose = () => {
console.log('连接关闭');
};
ws.onerror = (err) => {
console.error('WebSocket 错误:', err);
};
// 可选:给本地服务发 ping,服务会回 pong
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send('ping');
}
}, 30000);服务端会把解析后的 protobuf 消息转成 JSON 文本发给你。
不同消息类型字段不完全一样,但都会包含对应消息内容。
另外会额外补一个字段:
livename:直播间名称method:抖音消息类型,例如WebcastChatMessagetitle:直播间标题avatarThumb:主播头像缩略图地址
如果直播间还没开播,则会返回系统状态消息,例如:
{"type":"system","event":"live_status","live":false,"room_id":"516466932480","message":"直播间未开播","retry_interval_seconds":30}检测到开播时,也会先返回一条状态消息:
{"type":"system","event":"live_status","live":true,"room_id":"516466932480","message":"直播间已开播"}如果直播过程中下播,也会先返回一条状态消息:
{"type":"system","event":"live_status","live":false,"room_id":"516466932480","message":"直播间已下播","ended":true,"retry_interval_seconds":30}douyinLive/
├── cmd/main/ # 可执行程序入口
│ ├── main.go # 主程序
│ ├── app.go # HTTP / WebSocket 服务
│ ├── room.go # 房间与客户端管理
│ ├── config.go # 配置读取
│ └── WsHandler.go # WebSocket 事件处理
├── douyin.go # 核心抓取逻辑,对外库接口
├── sign/ # 签名与 Cookie 相关逻辑
├── jsScript/ # 签名脚本
├── protobuf/ # protobuf 定义
├── generated/ # 生成后的 protobuf 代码
├── utils/ # 工具函数
├── config.example.yaml # 配置示例
└── README.md
如果你需要:
- 获取抖音直播间实时弹幕
- 做自己的弹幕大屏
- 做直播互动统计
- 做礼物 / 点赞 / 关注监听
- 把抖音消息接进自己的系统
这个项目就比较合适。
本项目参考过这些项目和资料:
感谢原作者们的公开分享。
如果这个项目对你有帮助,欢迎点个 Star。