前言
一个游戏主播朋友,因在直播打游戏的时候无法实时关注弹幕信息,于是便有了这个解决方案。 最早的思路是,通过Fiddler抓包模拟各个直播平台的websocket数据,但是由于现在各大平台监管比较严格,模拟抓包的时候,一旦检测到有使用代理,有的app就会开启自残模式,禁止所有http请求。因此便放弃了APP的数据抓取,改从browser端的直播平台下手。这里的工程以B站为例。该项目我已经开源到GitEE,有需要的小伙伴自行Fork~
思路
实时获取B站直播间聊天信息,并通过小爱同学实时播报。实时数据采集需要通过chrome浏览器控制台,找到B站websocket代码,稍作修改,使之能将聊天信息传入后台,后台接收到数据以后,让小爱tts按序播放聊天信息。
运行环境
基于node搭建websocket服务
使用chrome控制台(Console)篡改网页中代码
通过xiaoai-tts包合成语音
安装步骤
安装所需依赖
npm install
启动服务
node app.js
使用chrome篡改B站直播间代码 随便打开一个B站的直播间,F12打开控制台,在Sources中找到player-loader-x.x.x.min.js
再找到该js中的以下代码e.prototype._messageReply = function(t) {…},并再该函数中间打上断点,等待直播间有弹幕信息后,进入该断点,在Console输入以下代码
e.ws = new WebSocket('ws://127.0.0.1:3001');
e.prototype._messageReply = function(t) {
var i = this
, n = this._player
, o = n.config
, a = n.state
, r = o.rnd.toString()
, s = !0;
if (t instanceof Array)
t.forEach((function(e) {
i._messageReply(e)
}
));
else if (t && t instanceof Object) {
t.msg;
if (o.useType === l.default.PLAYER_HOME && -1 === e.HOME_PLAYER_MSG.indexOf(t.cmd))
return;
var h = this.checkCmdPermission(t.cmd)
, f = h.type
, _ = h.shouldIgnore;
switch (h.hasCheckNum && (t.cmd = f),
t.cmd) {
case l.STATE_STRING.WS_MESSAGE_DANMAKU:
var m = t.info
, g = {
mode: m[0][1],
size: m[0][2],
color: m[0][3],
dmid: m[0][5],
text: m[1],
ignore: parseInt(m[2][0], 10) === o.uid && m[0][5].toString() === r,
type: parseInt(m[0][9], 10) || 0
}
, y = {
stime: -1,
mode: g.mode,
size: g.size,
color: g.color,
date: c.default.getTimeNow(!0),
uid: m[2][0],
dmid: g.dmid,
text: g.text,
uname: m[2][1],
user: {
level: m[4][0],
rank: m[2][5],
verify: !!m[2][6]
},
checkInfo: {
ts: m[9].ts,
ct: m[9].ct
},
type: g.type
};
console.log(y.uname+"说到:"+y.text);
//$.ajax({url:'http://localhost:3000/say?wd='+y.uname+'说:'+y.text});
e.ws.send(y.uname+'说到:'+y.text);
if (s = !1,
_.danmaku) {
(s = !_.callback) && (s = !n.checkDanmakuBlockStatus(y));
break
}
g.ignore || (s = !n.checkDanmakuBlockStatus(y)) && n.addDanmaku(y),
_.callback && (s = !1),
m = null,
g = null,
y = null;
break;
case l.STATE_STRING.WS_MESSAGE_SEND_GIFT:
this._player.state.onMini && (s = !1);
var b = t.data
, v = [];
if (v[l.default.DANMAKU_GIFT_666] = "666",
v[l.default.DANMAKU_GIFT_233] = "233",
v[l.default.DANMAKU_GIFT_FFF] = "FFF",
o.uid === b.uid && r === b.rnd.toString() && (s = !1),
1 === Number(b.effect) && s) {
var E = d.default.getSendGiftHtml(parseInt(b.giftId, 10), parseInt(b.num, 10));
E && n.addDanmaku({
html: E,
stime: -1,
mode: 1,
size: 25,
color: 16777215,
date: c.default.getTimeNow(!0),
uid: b.uid,
text: v[b.giftId]
})
}
break;
case l.STATE_STRING.WS_MESSAGE_LIVE:
if (!a.isStopPlayback) {
if (n.getLiveStatus() === l.default.L_STATUS_LIVE)
return;
n.resetStartTime(),
n.state.lastOpState = l.default.LAST_OP_STATE_WS_LIVE,
n.event.emit(l.EVENT.ON_LIVE),
n.playingStatusSync()
}
break;
case l.STATE_STRING.WS_MESSAGE_ROUND:
a.isStopPlayback || n.event.emit(l.EVENT.ON_ROUND);
break;
case l.STATE_STRING.HOT_ROOM_NOTIFY:
if (t.data && t.data.random_delay_req && "[object Array]" === Object.prototype.toString.apply(t.data.random_delay_req)) {
var T = Date.now() + 1e3 * parseInt(t.data.ttl, 10);
if (isNaN(T)) {
u.default.log("ttl error: " + t.data.ttl, 2);
break
}
t.data.random_delay_req.forEach((function(e) {
var t = {
time: Math.floor(Math.random() * e.delay),
expires: T
};
switch (e.path) {
case p.default.PATH.roundPlayUrl:
n.state.delay.roundPlayUrl = t;
break;
case p.default.PATH.recommend:
n.state.delay.recommend = t
}
}
))
}
break;
case l.STATE_STRING.WS_MESSAGE_PREPARING:
a.isStopPlayback || (n.changeUrlGuid(),
n.refreshPausedTime(!1),
n.refreshPlayingTime(!0),
n.destoryVideoMaskImgae(),
t.round ? n.event.emit(l.EVENT.ON_PRE_ROUND, 300) : n.event.emit(l.EVENT.ON_PREPARING));
break;
case l.STATE_STRING.WS_MESSAGE_END:
a.isStopPlayback || (n.setLiveStatus(l.default.L_STATUS_PREPARING),
n.end());
break;
case l.STATE_STRING.WS_MESSAGE_CLOSE:
a.isStopPlayback || (n.setLiveStatus(l.default.L_CLOSE),
n.end());
break;
case l.STATE_STRING.WS_MESSAGE_BLOCK:
n.setLiveStatus(l.default.L_BLOCK),
n.end();
break;
case l.STATE_STRING.WS_MESSAGE_REFRESH:
a.isStopPlayback || (n.state.lastOpState = l.default.LAST_OP_STATE_WS_REFRESH,
n.reload(l.default.LIVE_URL, 4, !0));
break;
case l.STATE_STRING.WS_ROOM_LIMIT:
setTimeout((function() {
n && n.requestRoomInit((function(e) {
e.code === A.default.ROOM_AREA_LIMIT_CODE && n.forbidden(!1, e.msg)
}
))
}
), 1e3 * (t.delay_range || 60));
break;
case l.STATE_STRING.WS_PK_PRE:
case l.STATE_STRING.WS_PK_END:
case l.STATE_STRING.WS_PK_SETTLE:
n.state.forceBufferTimeout = 2e3;
break;
case l.STATE_STRING.WS_PK_MIC_END:
n.state.forceBufferTimeout = 2e3,
setTimeout((function() {
n.state.forceBufferTimeout = 0
}
), 2e4)
}
t.cmd !== l.STATE_STRING.WS_MESSAGE_DANMAKU && t.cmd !== l.STATE_STRING.WS_MESSAGE_SEND_GIFT && (t.cmd,
l.STATE_STRING.WS_MESSAGE_WELCOME),
t.cmd === l.STATE_STRING.WS_MESSAGE_SEND_GIFT && (s = !0),
s && this._addServerCallbackSmoothly(t)
}
}
参与贡献
Fork 本仓库
新建 Feat_xxx 分支
提交代码
新建 Pull Request
特技
使用 Readme_XXX.md 来支持不同的语言,例如 Readme_en.md, Readme_zh.md
Gitee 官方博客 gitee.com/liaoyiqing
你可以 https://gitee.com/explore 这个地址来了解 Gitee 上的优秀开源项目
GVP 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
Gitee 官方提供的使用手册 https://gitee.com/help
Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 https://gitee.com/gitee-stars/