# xa-tts **Repository Path**: liaoyiqing/xa-tts ## Basic Information - **Project Name**: xa-tts - **Description**: 小爱同学TTS - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-09-30 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # xa-tts #### 前言 一个游戏主播朋友,因在直播打游戏的时候无法实时关注弹幕信息,于是便有了这个解决方案。 最早的思路是,通过Fiddler抓包模拟各个直播平台的websocket数据,但是由于现在各大平台监管比较严格,模拟抓包的时候,一旦检测到有使用代理,有的app就会开启自残模式,禁止所有http请求。因此便放弃了APP的数据抓取,改从browser端的直播平台下手。这里的工程以B站为例。 #### 思路 实时获取B站直播间聊天信息,并通过小爱同学实时播报。实时数据采集需要通过chrome浏览器控制台,找到B站websocket代码,稍作修改,使之能将聊天信息传入后台,后台接收到数据以后,让小爱tts按序播放聊天信息。 #### 运行环境 1. 基于node搭建websocket服务 2. 使用chrome控制台(Console)篡改网页中代码 3. 通过xiaoai-tts包合成语音 #### 安装步骤 1. 安装所需依赖 `npm install` 2. 启动服务 `node app.js` 3. 使用chrome篡改B站直播间代码 随便打开一个B站的直播间,F12打开控制台,在Sources中找到player-loader-x.x.x.min.js 4. 再找到该js中的以下代码`e.prototype._messageReply = function(t) {...}`,并再该函数中间打上断点,等待直播间有弹幕信息后,进入该断点,在Console输入以下代码 ``` javascript 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) } } ``` #### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request #### 特技 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 2. Gitee 官方博客 [gitee.com/liaoyiqing](https://gitee.com/liaoyiqing) 3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) 6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)