效果展示
先看效果,如视频所示可以采集到弹幕、礼物、点赞等等信息。
特别鸣谢
本项目在douyin-live的基础上移植到Unity
中实现相应内容,特别感谢该项目作者。
项目说明
本项目使用Unity开发,添加了部分插件、实现方式容易理解,可放心食用。
项目实例源码:点这里跳转
事前准备
先准备好protobuf
的转换工具,在项目源码中可以查看,也可以点击这里下载。
连接部分
- 首先把
douyin-live
项目拷贝到本地,再建立一个新的Unity工程。 - 模拟
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
}));
请求成功后,收集
Cookie
和ttwid
的内容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);
- 继续匹配,将回复回来的
html
网页中获取房间Id··
(不是url上面那个房间号)和其它信息(例如是否开播状态等等)。
//正则匹配
var match = Regex.Match(jsonData, @"roomId\\"":\\""(\d+)\\"",");
//获取直播房间的Id
_liveRoomId = match.Groups[1].ToString();
- 拥有
cookie
、ttwid
和房间id
后,就可以进行socket
的连接。直接在电脑端打开浏览器按F12进入开发者模式,刷新直播间页面即可获得ws的url,将其中的wss_push_room_id
和room_id
替换为上面提取到的房间id;设置socket
的标头,填入跟上面一样的浏览器信息,还有cookie
、ttwid
即可开始尝试连接。
//远程服务器链接
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(() =>
{
//无需回调
}));
- 至此,连接部分大功告成,已经可以接收到一些乱码信息。
解码部分
- 首先需要实现
Socket
的OnOpen
、OnMessage
、OnClose
、OnError
方法,在OnMessage
中进行消息分发拆解,在这里不多赘述。 - 将
Protobuf
工具中编译好的dll放到项目的Plugin
文件夹下。将dy.proto
文件放到protobuf
工具文件夹下,编辑protoGen.bat
生成对应的cs文件,将cs文件放到工程中。 - 先转化为
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();
}
}
}
- 接收事件的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;
}
- 如此,就算大功告成了!
5 条评论
示例项目跑不通了呀
编辑器中运行没有问题,但是打包出来是黑屏怎么回事啊
更新了,删除了热更新部分,按流程打包即可
他的获取礼物的信息重复获取很多次咋办
理论上不会重复获取多次才对,可能是礼物连击了,需要甄别一下