前言

一个游戏主播朋友,因在直播打游戏的时候无法实时关注弹幕信息,于是便有了这个解决方案。 最早的思路是,通过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/