Kingkong直播弹幕姬分析(一)

科技是第一生产力

Posted by 恋 on January 23, 2019

Kingkong直播

开局

emmmm… 是这样的,金刚直播的弹幕系统是分区的。大陆内一区,海外(包括台湾)一区。而黎明杀机在大陆内被禁播了,所以挺多人在金刚播 - 但是国内的弹幕和海外的不互通。这就让人很难受了,因为在这里看直播的可能有80%以上是国内的 - 而我看不到他们的弹幕。

这种事情让人很难受,由于不知道为什么的原因,我特别想把弹幕和QQ群的机器人互通起来… 或者只是单项转发也行。把弹幕转发到群看起来会简单一点 - 至少在我眼里是的。

那么开始了。

Stage 1

根据习俗,先去搜了一下。我找到了一个“开源”的金刚非官方播放器,而它的代码是用Aardio写的。恕我直言我根本没听过这种语言,而且那代码看起来非常像伪代码… 官方提供的库看起来功能多得一批… 我目测了好久都没看出来到底怎么用。

DETAIL@ph

Aardio语言的开源金刚播放器 点我展开
import win.ui;
var winform = win.form(text="aardio form";right=935;bottom=599)
winform.add(
btnFullScreen={cls="button";text="全屏";left=600;top=552;right=704;bottom=584;db=1;dl=1;z=5};
btnView={cls="button";text="查看";left=456;top=552;right=560;bottom=584;db=1;dl=1;z=1};
custom={cls="custom";text="custom";left=16;top=16;right=921;bottom=525;ah=1;aw=1;z=3};
edUrl={cls="edit";left=104;top=552;right=448;bottom=584;db=1;dl=1;edge=1;multiline=1;z=2};
static={cls="static";text="直播间地址";left=24;top=560;right=104;bottom=584;db=1;dl=1;transparent=1;z=4}
)

import thread.command
import hpsocket.ssl.httpClient;
import string.xml;
import string.html;
import web.json;
import web.kit;
import mpvPlayer;
import inet.http

var http = inet.http()

var envJs = http.get( "https://www.kingkong.com.tw/static/lib/jsrsasign-jwths-min.js" )
if( !envJs ){
    winform.msgboxErr("连接king kong直播平台失败,请重试")
    return ; 
}

var addJs = /****

/*
 Version: v3.6.1
 The MIT License: Copyright (c) 2010-2017 LiosK.
*/
var UUID;
UUID=function(f){function a(){}a.generate=function(){var b=a._getRandomInt,c=a._hexAligner;return c(b(32),8)+"-"+c(b(16),4)+"-"+c(16384|b(12),4)+"-"+c(32768|b(14),4)+"-"+c(b(48),12)};a._getRandomInt=function(b){if(0>b||53<b)return NaN;var c=0|1073741824*Math.random();return 30<b?c+1073741824*(0|Math.random()*(1<<b-30)):c>>>30-b};a._hexAligner=function(b,c){for(var a=b.toString(16),d=c-a.length,e="0";0<d;d>>>=1,e+=e)d&1&&(a=e+a);return a};a.overwrittenUUID=f;"undefined"!==typeof module&&module&&module.exports&&(module.exports=a);return a}(UUID);
var _getPFID = function() {
    var e = UUID.generate().toString().replace(/\-/g, "");
    return e = e.substring(0, e.length)
}
var _getName = function(e) {
    var t;
    return e && (t = "訪客" + e.toString().substring(e.length - 5, e.length)),t
}
var getToken = function( param ) {
    var e = {}

    e.live_id = param.liveId
    e.pfid = param.pfid || _getPFID()
    e.name = param.name || _getName(e.pfid)
    e.lv = param.lv  || 1 
    e.from = param.from || 1
    e.from_seq = param.fromSeq || 1
    e.channel_id = param.channelId || 1
    e.client_type = "web"  
    
    return KJUR.jws.JWS.sign(null, JSON.stringify({
        alg: "HS256",
        typ: "JWT"
    }), JSON.stringify(e), param.liveKey)
}

****/

var wke = web.kit.view()
wke.doScript( envJs )
wke.doScript( addJs )

var httpClient = hpsocket.ssl.httpClient()
var mpv = mpvPlayer( winform.custom )

var mpvOption = {
    ["osd-font-size"] = 35;// 字体大小
    ["osd-spacing"] = 2;// 字体间距
    ["osd-align-x"] = "center";// 显示位置 
    ["osd-align-y"] = "bottom";
    ["osd-back-color"] = "#171111";// 背景颜色
}

for(k,v in mpvOption){
    mpv.setOption(k,v)
}


var thCmd = thread.command()
thCmd.onChat = function(name, msg){
    mpv.setOption("osd-msg1", name ++ "" ++ msg );
}

var tmId;
var keepLive = function(){
    if( tmId ){
        winform.killtimer(tmId)
    }
    tmId = winform.addtimer( 
        30000,
        function(hwnd,msg,id,tick){
            httpClient.sendWsMessage("2")
        } 
    )   
}

thCmd.keepLive = function(){
    keepLive()
}


httpClient.onUpgrade = function(hpHttpClient,connId,upgradeType){
    if(upgradeType!=1) return -1;   
    if( ! hpHttpClient.checkWsUpgradeResponse() )
        return -1;      
}

httpClient.onWsMessageHeader = function(hpHttpClient,connId,final,reserved,opCode,mask,bodyLen){ 
    hpHttpClient.reallocString(bodyLen);
}

httpClient.onWsMessageBody = function(hpHttpClient,connId,pData,len){ 
    hpHttpClient.appendString(pData,len);
}

httpClient.onWsMessageComplete = function(hpHttpClient){
    import thread.command;
    import web.json;
    
    var data = hpHttpClient.getString(); 
    if( data ){
        var msgType, msg = string.match( data, "(.+?)\,(.+?)$" )
        if( msgType === "42/chat_nsp"  ){
            var ret = web.json.parse(msg)
            if( ret[1] == "msg" ){
                thread.command.post("onChat", ret[2].name, ret[2].msg)
            }
        }
        elseif( string.indexOf(data,"pingInterval") ){
            thread.command.post("keepLive")
        }
    }
}
httpClient.onHandShake = function(hpHttpClient,connId){
        // 必须先从内存取出绑定的认证数据,因为 sendWsUpgradeRequest 会刷新内存
        var authData = hpHttpClient.getString()
        
        // 发送升级包
        hpHttpClient.sendWsUpgradeRequest("/chat_nsp/?EIO=3&transport=websocket");
        hpHttpClient.sendWsMessage("40/chat_nsp,")
        

        // 发送认证
        hpHttpClient.sendWsMessage(authData)    
}

httpClient.onClose = function(hpHttpClient,connId,enOperation,errCode){
    hpHttpClient.reallocString(0);
}



var getRooomInfo = function(url){
    import inet.http
    import string.xml;
    import string.html;
    import web.json;    
    
    var http = inet.http()    
    
    var url = string.match(url,"(<@https://www.kingkong.com.tw/@>\d+)")
    if( !url ){
        return null, "不是金刚直播的url"; 
    }

    var html = http.get( url )
    if !html 
        return null, "网络出错请重试"; 
    
    var xml = string.xml(html)
    var ele = xml.queryEle( ["class"]="view view-live")
    if !ele 
        return null, "找不到ele"; 
        
    var roomInfo = ele["data-roomInfo"] 
    if !roomInfo
        return null, "找不到roomInfo"; 
    
    var json = string.html.toText(roomInfo)
    var ret = web.json.parse(json)
    return ret, json; 
}

var getToken = function(json){
    return wke.eval( `getToken( JSON.parse( '` ++ json ++ `') )` )
}

var view = function( url ){ 
    if( !#url ){
        return null, "直播间地址不能为空"; 
    }   
    
    var ret, json = win.invoke(getRooomInfo,url)
    if( !ret ){
        return null, json; 
    }

    var token = getToken(json)
    if( !token ){
        return null, "数据解析出错,网站可能已经更新"; 
    }
    
    httpClient.stop()
    httpClient.sslCleanup()
    if( !httpClient.sslSetupContext(0) ){
        return null, "初始化SSL环境失败";          
    }
    if( !httpClient.start("cht.ws.kingkong.com.tw",443,false) ){
        return null, "连接弹幕服务器失败";       
    }
    
    
    // 绑定认证数据
    var data = "42/chat_nsp," ++ web.json.stringify({"authentication";{
            anchor_pfid = ret.pfid;
            client_type = "web";
            live_id = ret.liveId;
            token = token;}})       
    
    httpClient.reallocString(1)
    httpClient.appendString( data ) 

    winform.text = ret.name ++ "" ++ ret.roomTitle
    
    mpv.command( "loadfile", ret.videos[1].url )    
    return true; 
}


winform.btnView.oncommand = function(id,event){
    owner.disabled = true
    var url = winform.edUrl.text;
    var ok, err = view(url)
    if( !ok ){
        winform.msgboxErr(err )
    }
    owner.disabled = false
}

winform.btnFullScreen.oncommand = function(id,event){
    winform.custom.fullscreen(true)
}

import win.ui.accelerator
win.ui.accelerator({
    { vkey =0x1B/*_VK_ESC*/;oncommand = function(id,event) winform.custom.fullscreen(false) }  
    { vkey =0x7A/*_VK_F11*/; oncommand = function(id,event) winform.custom.fullscreen() }  
},winform);

winform.show() 
win.loopMessage();

请注意,var addJs = /****这一段是定义跨行的字符串… (真是个悲伤的故事.jpg

DETAILEND@ph

好的,这个看起来非常像各种语言而且看起来库功能乱七八糟的多的玩意儿… 我试着直接看着这个写C#,但是并不怎么靠谱,因为完全没办法测试啊

所以我下载了Aardio(噗(/≧▽≦)/

Stage 2

这个Aardio是个国人做的小玩具,5.9M 挺有意思的(虽然当我搜的时候,出来的全是作者和他们的人在知乎和V2EX上撕逼说他们受到了知乎管理员的删帖迫害)

经过方便快捷的编译运行后,我发现,这段代码根本跑不了啊(′д` )…彡…彡

提示说是数据解析出错,网站可能已经更新(很正常,这段代码是2018年初的,过了一年更新了没什么问题)。于是我就想,先升级一下这玩意儿,再翻译成C#。首先要找到出错的地方(废话)。

数据解析出错的来源是getToken(json),但是我发现这玩意儿根本没有断点,于是只好msgbox了。json的值我不记得了(喂喂喂)。那暂时假设问题不是getToken有问题!于是往上翻,看到getToken的代码是eval js… 我的心好痛,我怎么调试一个连断点都没有的语言里的js引擎?我干脆在eval之前加了点代码,发现msgbox roomInfo是null…

我的内心一段woc,开局就错了怎么不报错呢,这是什么神仙操作… 放弃,这语言看起来太不靠谱了

Stage 3

于是我点开了我的2019,下载了直播页面来具体分析。看到了上面的html分析代码,我们要找一个.view-live里面的data-roomInfo。然后我发现,这玩意儿确实存在的(@Aardio 那你为什么要弹窗说null?没有类型转换还是不会提示?)…

不管了,继续。

这个data-roomInfo是个非常值得吐槽的玩意儿,因为他们,在html里,藏了个json。请注意,真的是藏了个,因为这个json是作为某个div的attribute出现的 - 并且在加载完后会自动删掉。真傻吊啊.jpg

不管怎么说,我们还是获得了这个json。

{
   "avatar":"https://img.kingkong.com.tw/public/user/主播号/主播号",
   "category":"黎明殺機",
   "categoryId":"Deadbydaylight",
   "fansNum":1029,
   "heat":62022,
   "liveStatus":1,
   "anchorId":主播号,
   "liveId":"主播号+随机串",
   "liveKey":"一个密钥",
   "name":"主播名",
   "roomId":"主播号",
   "roomTitle":"喵?",
   "pfid":主播号,
   "prettyId":"主播号",
   "lv":24,
   "videos":[  
      {  
         "id":4,
         "name":"最佳",
         "url":"https://video-wwsflv.langlive.com/obslive/主播号+随机串.flv"
      },
      {  
         "id":3,
         "name":"720P",
         "url":"https://video-wwsflv.langlive.com/obslive/主播号+随机串_s3.flv"
      },
      {  
         "id":2,
         "name":"480P",
         "url":"https://video-wwsflv.langlive.com/obslive/主播号+随机串_s2.flv"
      },
      {  
         "id":1,
         "name":"360P",
         "url":"https://video-wwsflv.langlive.com/obslive/主播号+随机串_s1.flv"
      }
   ],
   "defaultVideo":{  
      "name":"最佳",
      "url":"https://video-wwsflv.langlive.com/obslive/主播号+随机串.flv"
   },
   "data":{  
      "uid":主播号,
      "pretty_id":"主播号",
      "describe":"描述",
      "room_id":"主播号",
      "avatar":"https://img.kingkong.com.tw/public/user/主播号/主播号",
      "nickname":"主播名",
      "fans_num":1029,
      "verified":0,
      "anchor_lvl":24,
      "stream_items":[  
         {  
            "id":1,
            "title":"360P",
            "video":"https://video-wwsflv.langlive.com/obslive/主播号+随机串_s1.flv"
         },
         {  
            "id":2,
            "title":"480P",
            "video":"https://video-wwsflv.langlive.com/obslive/主播号+随机串_s2.flv"
         },
         {  
            "id":3,
            "title":"720P",
            "video":"https://video-wwsflv.langlive.com/obslive/主播号+随机串_s3.flv"
         },
         {  
            "id":4,
            "title":"最佳",
            "video":"https://video-wwsflv.langlive.com/obslive/主播号+随机串.flv"
         }
      ],
      "stream_id":4,
      "room_title":"喵?",
      "heat":62022,
      "video":"https://video-wwsflv.langlive.com/obslive/主播号+随机串_s2.flv",
      "live_status":1,
      "category_id":"Deadbydaylight",
      "category":"黎明殺機",
      "live_id":"主播号+随机串",
      "live_key":"一个密钥",
      "forbid_18":1,
      "user_total_p":427,
      "user_cnt_p":54,
      "transcode":1
   },
   "theme":"default",
   "themeInfo":null,
   "luckyDraw":null
}

嗯,就是这样。可以看得到,这里面的一些关键数据被我打了码(等等,有什么好打的)

(我去掉了这个的高亮,因为这里的json会变成一行,我也不知道发生了什么

未完待续(我先去吃晚餐&直播姬还没做好