效果展示

先看效果,如视频所示可以采集到弹幕、礼物、点赞等等信息。

特别鸣谢

本项目在douyin-live的基础上移植到Unity中实现相应内容,特别感谢该项目作者。

项目说明

本项目使用Unity开发,添加了部分插件、实现方式容易理解,可放心食用。
项目实例源码:点这里跳转

事前准备

先准备好protobuf的转换工具,在项目源码中可以查看,也可以点击这里下载。

连接部分

  1. 首先把douyin-live项目拷贝到本地,再建立一个新的Unity工程。
  2. 模拟Http请求,向pc网页版O音索要数据,2023.12.26更新了ttwid获取方式。
//清空标头,添加标头
HttpClearHeader();
HttpAddHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
HttpAddHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36");
HttpAddHeader("cookie", "__ac_nonce=0638733a400869171be51");
//发送Get请求,获取ttwid
_routine = HttpGet(Txt_Url.text, new Action<string>((jsonData) =>
{
    //获取房间Id
}));

  1. 请求成功后,收集Cookiettwid的内容

        var www = _routine.GetWWW();
        //获取Cookie
        var cookie = www.GetResponseHeader("Set-Cookie");
        //正则匹配
        var ttwid = Regex.Match(cookie, @"ttwid=\S+;").Value;
        HttpAddHeader("Cookie", ttwid);
        //发送Get请求,获取房间Id
        _routine = HttpGet(Txt_Url.text + Input_Room.text, new Action<string>((jsonData) =>
        {
            //回调方法,下面补充
        }));
        Debug.Log(ttwid);
    
  2. 继续匹配,将回复回来的html网页中获取房间Id··(不是url上面那个房间号)和其它信息(例如是否开播状态等等)。
//正则匹配
var match = Regex.Match(jsonData, @"roomId\\"":\\""(\d+)\\"",");
//获取直播房间的Id
_liveRoomId = match.Groups[1].ToString();
  1. 拥有cookiettwid房间id后,就可以进行socket的连接。直接在电脑端打开浏览器按F12进入开发者模式,刷新直播间页面即可获得ws的url,将其中的wss_push_room_idroom_id替换为上面提取到的房间id;设置socket的标头,填入跟上面一样的浏览器信息,还有cookiettwid即可开始尝试连接。
//远程服务器链接
var wsUrl = $"wss://webcast3-ws-web-lq.douyin.com/webcast/im/push/v2/?app_name=douyin_web&version_code=180800&webcast_sdk_version=1.3.0&update_version_code=1.3.0&compress=gzip&internal_ext=internal_src:dim|wss_push_room_id:{_liveRoomId}|wss_push_did:7188358506633528844|dim_log_id:20230521093022204E5B327EF20D5CDFC6|fetch_time:1684632622323|seq:1|wss_info:0-1684632622323-0-0|wrds_kvs:WebcastRoomRankMessage-1684632106402346965_WebcastRoomStatsMessage-1684632616357153318&cursor=t-1684632622323_r-1_d-1_u-1_h-1&host=https://live.douyin.com&aid=6383&live_id=1&did_rule=3&debug=false&maxCacheMessageNumber=20&endpoint=live_pc&support_wrds=1&im_path=/webcast/im/fetch/&user_unique_id=7188358506633528844&device_platform=web&cookie_enabled=true&screen_width=1440&screen_height=900&browser_language=zh&browser_platform=MacIntel&browser_name=Mozilla&browser_version=5.0%20(Macintosh;%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit/537.36%20(KHTML,%20like%20Gecko)%20Chrome/113.0.0.0%20Safari/537.36&browser_online=true&tz_name=Asia/Shanghai&identity=audience&room_id={_liveRoomId}&heartbeatDuration=0&signature=00000000";

//设置校验标头,没有ttwid会导致无法连接服务器
SocketClearHeader();
SocketAddHeader("cookie", ttwid);
SocketAddHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
//创建Socket
SocketConnect(wsUrl, new Action(() =>
{
    //无需回调
}));
  1. 至此,连接部分大功告成,已经可以接收到一些乱码信息。

解码部分

  1. 首先需要实现SocketOnOpenOnMessageOnCloseOnError方法,在OnMessage中进行消息分发拆解,在这里不多赘述。
  2. Protobuf工具中编译好的dll放到项目的Plugin文件夹下。将dy.proto文件放到protobuf工具文件夹下,编辑protoGen.bat生成对应的cs文件,将cs文件放到工程中。
  3. 先转化为Protobuf生成的类,再使用Gzip解压具体的数据,再判断是否有ack心跳需求,如果有就回复,无就略过,消息解析方法在后面会详细写出来。
//1、拆收到的消息
var wssPackage = PushFrame.Parser.ParseFrom(evt.bytes);
//2、反序列化pb后 数据需要GZip解压
var unGzip = GZipDecompress(wssPackage.Payload.ToByteArray());
var payloadPackage = Response.Parser.ParseFrom(unGzip);
//3、拆Gzip解压后 如果需要Ack就发送Ack
if (payloadPackage.NeedAck)
{
    //发送ack不能ToString再GetBytes,必须直接发送byte[]
    var obj = new PushFrame();
    obj.PayloadType = "ack";
    obj.Payload = ByteString.CopyFromUtf8(payloadPackage.InternalExt);
    obj.LogId = wssPackage.LogId;
    SendMsg(obj.ToByteArray());
}
//4、消息解析输出
foreach (var msg in payloadPackage.MessagesList)
{
    GameGod.Instance.EventManager.SendEven((ushort)GameData.UIEvent.OnReadMsg, msg);
}

static byte[] GZipDecompress(byte[] bytes)
{
    using (MemoryStream ms = new MemoryStream(bytes))
    {
        using (GZipStream zs = new GZipStream(ms, CompressionMode.Decompress))
        {
            byte[] buffer = new byte[512];
            MemoryStream buf = new MemoryStream();
            for (int offset; (offset = zs.Read(buffer, 0, 512)) > 0;)
                buf.Write(buffer, 0, offset);
            return buf.ToArray();
        }
    }
}
  1. 接收事件的UI回调
switch (msg.Method)
{
    case "WebcastChatMessage":
        var chatMessage = Douyin.ChatMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = chatMessage.User.NickName + "发送消息:" + chatMessage.Content;
        Txt_Msg.color = Color.white;
        //GameGod.Instance.Log(E_Log.Custom, chatMessage.User.NickName + "发送弹幕", chatMessage.Content, "#15FFC3");
        break;

    case "WebcastMatchAgainstScoreMessage":
        //
        var matchAgainstScoreMessage = Douyin.MatchAgainstScoreMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = "matchAgainstScoreMessage消息";
        Txt_Msg.color = Color.yellow;
        //GameGod.Instance.Log(E_Log.Custom, "matchAgainstScoreMessage消息", null, "#15FFC3");
        break;

    case "WebcastLikeMessage":
        //点赞
        var likeMessage = Douyin.LikeMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = likeMessage.User.NickName + "给主播点赞";
        Txt_Msg.color = Color.blue;
        //GameGod.Instance.Log(E_Log.Custom, likeMessage.User.NickName + "给主播点赞", null, "#15FFC3");
        break;

    case "WebcastMemberMessage":
        //xx成员进入直播间消息
        var memberMessage = Douyin.MemberMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = memberMessage.User.NickName + "进入了直播间";
        Txt_Msg.color = Color.black;
        //GameGod.Instance.Log(E_Log.Custom, memberMessage.User.NickName + "进入了直播间", null, "#15FFC3");
        break;

    case "WebcastGiftMessage":
        //礼物
        var giftMessage = Douyin.GiftMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = giftMessage.Common.Describe;
        Txt_Msg.color = Color.red;
        //GameGod.Instance.Log(E_Log.Custom, giftMessage.Common.Describe, null, "#15FFC3");
        break;

    case "WebcastSocialMessage":
        //关注
        var socialMessage = Douyin.SocialMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = socialMessage.User.NickName + "关注了主播";
        Txt_Msg.color = Color.green;
        //GameGod.Instance.Log(E_Log.Custom, socialMessage.User.NickName + "关注了主播", null, "#15FFC3");
        break;

    case "WebcastRoomUserSeqMessage":
        //
        var roomUserSeqMessage = Douyin.RoomUserSeqMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = "roomUserSeqMessage消息";
        Txt_Msg.color = Color.yellow;
        //GameGod.Instance.Log(E_Log.Custom, "roomUserSeqMessage消息", null, "#15FFC3");
        break;

    case "WebcastUpdateFanTicketMessage":
        //
        var updateFanTicketMessage = Douyin.UpdateFanTicketMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = "updateFanTicketMessage消息";
        Txt_Msg.color = Color.yellow;
        //GameGod.Instance.Log(E_Log.Proto, "updateFanTicketMessage消息", null, "#15FFC3");
        break;

    case "WebcastCommonTextMessage":
        //
        var commonTextMessage = Douyin.CommonTextMessage.Parser.ParseFrom(msg.Payload);
        Txt_Msg.text = "commonTextMessage消息";
        Txt_Msg.color = Color.yellow;
        //GameGod.Instance.Log(E_Log.Custom, "commonTextMessage消息", null, "#15FFC3");
        break;

    default:
        Txt_Msg.text = msg.Method;
        Txt_Msg.color = Color.cyan;
        break;
}
  1. 如此,就算大功告成了!
最后修改:2024 年 05 月 16 日
如果觉得我的文章对你有用,请随意赞赏