1
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.RegularExpressions;
|
||||
using BarrageGrab.Modles.BiliProtoEntity;
|
||||
using BarrageGrab.Proxy.ProxyEventArgs;
|
||||
using BrotliSharpLib;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BarrageGrab
|
||||
{
|
||||
/// <summary>
|
||||
/// 哔哩哔哩弹幕抓取器
|
||||
/// 协议格式: 自定义16字节Big-Endian帧头 + JSON body (body可能为zlib/brotli压缩)
|
||||
/// </summary>
|
||||
public class BiliBarrageGrab
|
||||
{
|
||||
// B站直播弹幕WS域名
|
||||
// broadcast-msg.chat.bilibili.com (主域名)
|
||||
// *-live-comet.chat.bilibili.com (新版comet服务器, 如 zj-cn-live-comet.chat.bilibili.com)
|
||||
// live-trace.com (备用)
|
||||
// ws.live.bilibili.com (历史域名)
|
||||
private readonly Regex biliWsReg = new Regex(@"chat\.bilibili\.com|live-trace\.com|ws\.live\.bilibili\.com", RegexOptions.IgnoreCase);
|
||||
private const string BILI_LIVE_HOST = "live.bilibili.com";
|
||||
|
||||
/// <summary>弹幕消息</summary>
|
||||
public event EventHandler<BiliRoomMessageEventArgs<BiliDanmuMsg>> OnChatMessage;
|
||||
/// <summary>点赞消息</summary>
|
||||
public event EventHandler<BiliRoomMessageEventArgs<BiliLikeData>> OnLikeMessage;
|
||||
/// <summary>礼物消息</summary>
|
||||
public event EventHandler<BiliRoomMessageEventArgs<BiliGiftData>> OnGiftMessage;
|
||||
/// <summary>进入直播间/关注消息</summary>
|
||||
public event EventHandler<BiliRoomMessageEventArgs<BiliInteractData>> OnInteractMessage;
|
||||
/// <summary>房间统计消息</summary>
|
||||
public event EventHandler<BiliRoomMessageEventArgs<BiliRoomStats>> OnRoomStatsMessage;
|
||||
/// <summary>分享消息 (INTERACT_WORD中msg_type=3)</summary>
|
||||
public event EventHandler<BiliRoomMessageEventArgs<BiliInteractData>> OnShareMessage;
|
||||
|
||||
public BiliBarrageGrab()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前认证的房间ID (从AUTH包解析,供消息处理器读取)
|
||||
/// </summary>
|
||||
public string CurrentRoomId { get; private set; } = "";
|
||||
|
||||
public bool IsBilibiliHost(string hostname)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hostname)) return false;
|
||||
hostname = hostname.Trim().ToLower();
|
||||
return biliWsReg.IsMatch(hostname) || hostname == BILI_LIVE_HOST;
|
||||
}
|
||||
|
||||
public bool IsBilibiliWebSocket(string uri)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri)) return false;
|
||||
return biliWsReg.IsMatch(uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理代理收到的B站WS数据
|
||||
/// </summary>
|
||||
public void HandleWebSocketData(WsMessageEventArgs e)
|
||||
{
|
||||
var buff = e.Payload;
|
||||
if (buff == null || buff.Length == 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 一个WS帧可能包含多个B站数据包,需要循环解析
|
||||
ProcessBuffer(buff, 0, buff.Length, e.ProcessName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (biliErrCount < 3)
|
||||
{
|
||||
Logger.LogError(ex, $"处理B站弹幕数据包时出错:{ex.Message}");
|
||||
biliErrCount++;
|
||||
}
|
||||
if (biliErrCount == 3)
|
||||
{
|
||||
Logger.LogWarn("B站弹幕解析持续失败,已抑制后续错误日志");
|
||||
biliErrCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理客户端发送的B站WS数据 (解析AUTH认证包提取roomid)
|
||||
/// B站客户端连接WS后会发送AUTH包: {uid,roomid,protover,platform,type,key}
|
||||
/// </summary>
|
||||
public void HandleSentData(WsMessageEventArgs e)
|
||||
{
|
||||
var buff = e.Payload;
|
||||
if (buff == null || buff.Length < BiliSocketFrame.HEADER_SIZE) return;
|
||||
|
||||
try
|
||||
{
|
||||
BiliSocketFrame frame;
|
||||
if (!BiliSocketFrame.TryParse(buff, 0, buff.Length, out frame)) return;
|
||||
|
||||
// 只关注认证请求包 (Operation=7)
|
||||
if (frame.Operation != BiliOperation.AUTH) return;
|
||||
|
||||
var payload = BiliSocketFrame.GetPayload(buff, 0, frame);
|
||||
if (payload == null || payload.Length == 0) return;
|
||||
|
||||
var json = System.Text.Encoding.UTF8.GetString(payload);
|
||||
var jobj = JObject.Parse(json);
|
||||
var roomIdToken = jobj["roomid"];
|
||||
if (roomIdToken == null) return;
|
||||
|
||||
// roomid可能是整数或字符串
|
||||
string roomId = roomIdToken.Type == JTokenType.Integer
|
||||
? roomIdToken.Value<long>().ToString()
|
||||
: roomIdToken.Value<string>() ?? "";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(roomId))
|
||||
{
|
||||
CurrentRoomId = roomId;
|
||||
Logger.LogInfo($"B站认证包解析成功,当前房间ID: {roomId}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarn($"解析B站AUTH认证包失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static int biliErrCount = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 递归解析缓冲区中可能包含的多个B站数据包
|
||||
/// </summary>
|
||||
private void ProcessBuffer(byte[] buffer, int offset, int count, string processName)
|
||||
{
|
||||
int processed = 0;
|
||||
while (processed + BiliSocketFrame.HEADER_SIZE <= count)
|
||||
{
|
||||
BiliSocketFrame frame;
|
||||
if (!BiliSocketFrame.TryParse(buffer, offset + processed, count - processed, out frame))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// 包长超过剩余缓冲区,说明数据不完整,等下次再处理
|
||||
if (processed + frame.PacketLength > count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var payload = BiliSocketFrame.GetPayload(buffer, offset + processed, frame);
|
||||
processed += (int)frame.PacketLength;
|
||||
|
||||
// 只处理服务器推送的消息和心跳回复
|
||||
if (frame.Operation == BiliOperation.HEARTBEAT_REPLY)
|
||||
{
|
||||
// 心跳回复: body是4字节big-endian人气值
|
||||
if (payload != null && payload.Length >= 4)
|
||||
{
|
||||
var online = (long)BiliSocketFrame.ReadUInt32BE(payload, 0);
|
||||
var stats = new BiliRoomStats { Online = online, Watched = 0 };
|
||||
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliRoomStats>(processName, stats));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frame.Operation != BiliOperation.MESSAGE) continue;
|
||||
|
||||
// 根据协议版本解压
|
||||
byte[] jsonBytes;
|
||||
switch (frame.ProtocolVersion)
|
||||
{
|
||||
case BiliProtocolVersion.PLAIN:
|
||||
case BiliProtocolVersion.PLAIN_AUTH:
|
||||
jsonBytes = payload;
|
||||
break;
|
||||
case BiliProtocolVersion.ZLIB:
|
||||
jsonBytes = ZlibDecompress(payload);
|
||||
break;
|
||||
case BiliProtocolVersion.BROTLI:
|
||||
jsonBytes = BrotliDecompress(payload);
|
||||
break;
|
||||
default:
|
||||
jsonBytes = payload;
|
||||
break;
|
||||
}
|
||||
|
||||
if (jsonBytes == null || jsonBytes.Length == 0) continue;
|
||||
|
||||
// 解压后可能仍然是多个数据包 (Zlib/Brotli压缩的通常是合并包)
|
||||
if (frame.ProtocolVersion == BiliProtocolVersion.ZLIB ||
|
||||
frame.ProtocolVersion == BiliProtocolVersion.BROTLI)
|
||||
{
|
||||
ProcessBuffer(jsonBytes, 0, jsonBytes.Length, processName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 明文JSON: 解析为消息
|
||||
DispatchJsonMessage(jsonBytes, processName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将JSON字节解析为B站消息并触发对应事件
|
||||
/// </summary>
|
||||
private void DispatchJsonMessage(byte[] jsonBytes, string processName)
|
||||
{
|
||||
string json;
|
||||
try
|
||||
{
|
||||
json = System.Text.Encoding.UTF8.GetString(jsonBytes);
|
||||
}
|
||||
catch { return; }
|
||||
|
||||
if (string.IsNullOrWhiteSpace(json)) return;
|
||||
|
||||
JObject jobj;
|
||||
try
|
||||
{
|
||||
jobj = JObject.Parse(json);
|
||||
}
|
||||
catch { return; }
|
||||
|
||||
var cmdToken = jobj["cmd"];
|
||||
if (cmdToken == null) return;
|
||||
string cmd = cmdToken.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(cmd)) return;
|
||||
|
||||
// 兼容cmd带数字后缀的版本,如 DANMU_MSG-4b1cb6f3
|
||||
int dashIdx = cmd.IndexOf('-');
|
||||
if (dashIdx > 0) cmd = cmd.Substring(0, dashIdx);
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case BiliCmd.DANMU_MSG:
|
||||
{
|
||||
var msg = ParseDanmuMsg(jobj);
|
||||
if (msg != null)
|
||||
{
|
||||
OnChatMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliDanmuMsg>(processName, msg));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BiliCmd.SEND_GIFT:
|
||||
{
|
||||
var msg = jobj["data"]?.ToObject<BiliGiftData>();
|
||||
if (msg != null)
|
||||
{
|
||||
OnGiftMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliGiftData>(processName, msg));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BiliCmd.INTERACT_WORD:
|
||||
{
|
||||
var msg = jobj["data"]?.ToObject<BiliInteractData>();
|
||||
if (msg != null)
|
||||
{
|
||||
// msg_type: 1=进入 2=关注 3=分享
|
||||
if (msg.MsgType == 3)
|
||||
{
|
||||
OnShareMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliInteractData>(processName, msg));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnInteractMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliInteractData>(processName, msg));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BiliCmd.LIKE_INFO_V3_CLICK:
|
||||
{
|
||||
var msg = jobj["data"]?.ToObject<BiliLikeData>();
|
||||
if (msg != null)
|
||||
{
|
||||
OnLikeMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliLikeData>(processName, msg));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BiliCmd.ONLINE_RANK_COUNT:
|
||||
{
|
||||
var count = jobj["data"]?["count"]?.Value<long>() ?? 0;
|
||||
var stats = new BiliRoomStats { Online = count, Watched = 0 };
|
||||
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliRoomStats>(processName, stats));
|
||||
break;
|
||||
}
|
||||
case BiliCmd.WATCHED_CHANGE:
|
||||
{
|
||||
var num = jobj["data"]?["num"]?.Value<long>() ?? 0;
|
||||
var stats = new BiliRoomStats { Online = 0, Watched = num };
|
||||
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliRoomStats>(processName, stats));
|
||||
break;
|
||||
}
|
||||
case BiliCmd.ROOM_REAL_TIME_MESSAGE_UPDATE:
|
||||
{
|
||||
// 同时包含在线人数和看过人数
|
||||
var online = jobj["data"]?["online"]?.Value<long>() ?? 0;
|
||||
var watched = jobj["data"]?["watched_show"]?["num"]?.Value<long>() ?? 0;
|
||||
var stats = new BiliRoomStats { Online = online, Watched = watched };
|
||||
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliRoomStats>(processName, stats));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// 未知消息类型忽略
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析DANMU_MSG消息: info[1]=内容, info[2]=[uid,uname,isAdmin,rank,?anchor]
|
||||
/// </summary>
|
||||
private BiliDanmuMsg ParseDanmuMsg(JObject jobj)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = jobj["info"] as JArray;
|
||||
if (info == null || info.Count < 3) return null;
|
||||
|
||||
var content = info[1]?.Value<string>() ?? "";
|
||||
var userInfo = info[2] as JArray;
|
||||
if (userInfo == null || userInfo.Count < 2) return null;
|
||||
|
||||
var msg = new BiliDanmuMsg
|
||||
{
|
||||
Content = content,
|
||||
Uid = userInfo[0]?.Value<long>() ?? 0,
|
||||
Uname = userInfo[1]?.Value<string>() ?? "",
|
||||
IsAdmin = userInfo.Count > 2 && userInfo[2]?.Value<int>() == 1,
|
||||
IsAnchor = userInfo.Count > 4 && userInfo[4] != null && userInfo[4].Type != JTokenType.Null
|
||||
};
|
||||
return msg;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Zlib解压</summary>
|
||||
private byte[] ZlibDecompress(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length == 0) return null;
|
||||
try
|
||||
{
|
||||
using (var ms = new MemoryStream(data))
|
||||
using (var zlib = new DeflateStream(ms, CompressionMode.Decompress))
|
||||
using (var outMs = new MemoryStream())
|
||||
{
|
||||
// zlib前2字节是header(0x78 0x9c等),DeflateStream需要跳过
|
||||
// 但 .NET Core 2.1+ 的 DeflateStream 可以处理 zlib header, .NET Framework 需要跳过
|
||||
if (data.Length >= 2 && data[0] == 0x78)
|
||||
{
|
||||
ms.Position = 2;
|
||||
}
|
||||
zlib.CopyTo(outMs);
|
||||
return outMs.ToArray();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarn($"B站Zlib解压失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Brotli解压 (使用 BrotliSharpLib)</summary>
|
||||
private byte[] BrotliDecompress(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length == 0) return null;
|
||||
try
|
||||
{
|
||||
using (var ms = new MemoryStream(data))
|
||||
using (var brotli = new BrotliSharpLib.BrotliStream(ms, CompressionMode.Decompress))
|
||||
using (var outMs = new MemoryStream())
|
||||
{
|
||||
brotli.CopyTo(outMs);
|
||||
return outMs.ToArray();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarn($"B站Brotli解压失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>B站消息事件参数</summary>
|
||||
public class BiliRoomMessageEventArgs<T> : EventArgs where T : class
|
||||
{
|
||||
public string Process { get; set; }
|
||||
public T Message { get; set; }
|
||||
|
||||
public BiliRoomMessageEventArgs() { }
|
||||
|
||||
public BiliRoomMessageEventArgs(string process, T data)
|
||||
{
|
||||
this.Process = process;
|
||||
this.Message = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.RegularExpressions;
|
||||
using BarrageGrab.Modles.KsProtoEntity;
|
||||
using BarrageGrab.Proxy.ProxyEventArgs;
|
||||
using ProtoBuf;
|
||||
|
||||
namespace BarrageGrab
|
||||
{
|
||||
public class KsBarrageGrab
|
||||
{
|
||||
// 2026-05 快手WS域名变更为 livejs-ws.kuaishou.cn
|
||||
private readonly Regex kuaishouWsReg = new Regex(@"livejs-ws\.kuaishou\.cn|(?:livemate|pclivemate)\.pull\.yximgs\.com", RegexOptions.IgnoreCase);
|
||||
private const string KUAISHOU_LIVE_HOST = "live.kuaishou.com";
|
||||
|
||||
public event EventHandler<KsRoomMessageEventArgs<KsWebCommentFeed>> OnChatMessage;
|
||||
public event EventHandler<KsRoomMessageEventArgs<KsWebLikeFeed>> OnLikeMessage;
|
||||
public event EventHandler<KsRoomMessageEventArgs<KsWebGiftFeed>> OnGiftMessage;
|
||||
public event EventHandler<KsRoomMessageEventArgs<KsWebShareFeed>> OnShareMessage;
|
||||
public event EventHandler<KsRoomMessageEventArgs<KsSCWebFeedPush>> OnRoomStatsMessage;
|
||||
|
||||
public KsBarrageGrab()
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsKuaishouHost(string hostname)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hostname)) return false;
|
||||
hostname = hostname.Trim().ToLower();
|
||||
return kuaishouWsReg.IsMatch(hostname) || hostname == KUAISHOU_LIVE_HOST;
|
||||
}
|
||||
|
||||
public bool IsKuaishouWebSocket(string uri)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri)) return false;
|
||||
return kuaishouWsReg.IsMatch(uri);
|
||||
}
|
||||
|
||||
public void HandleWebSocketData(WsMessageEventArgs e)
|
||||
{
|
||||
var buff = e.Payload;
|
||||
if (buff == null || buff.Length == 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
var socketMsg = Serializer.Deserialize<KsSocketMessage>(new ReadOnlyMemory<byte>(buff));
|
||||
if (socketMsg == null) return;
|
||||
|
||||
if (socketMsg.PayloadType != KsPayloadType.SC_FEED_PUSH) return;
|
||||
|
||||
byte[] payload = socketMsg.Payload;
|
||||
if (socketMsg.CompressionType == KsCompressionType.GZIP)
|
||||
{
|
||||
payload = Decompress(payload);
|
||||
}
|
||||
|
||||
var feedPush = Serializer.Deserialize<KsSCWebFeedPush>(new ReadOnlyMemory<byte>(payload));
|
||||
if (feedPush == null) return;
|
||||
|
||||
foreach (var comment in feedPush.CommentFeeds)
|
||||
{
|
||||
OnChatMessage?.Invoke(this, new KsRoomMessageEventArgs<KsWebCommentFeed>(e.ProcessName, comment));
|
||||
}
|
||||
|
||||
foreach (var like in feedPush.LikeFeeds)
|
||||
{
|
||||
OnLikeMessage?.Invoke(this, new KsRoomMessageEventArgs<KsWebLikeFeed>(e.ProcessName, like));
|
||||
}
|
||||
|
||||
foreach (var gift in feedPush.GiftFeeds)
|
||||
{
|
||||
OnGiftMessage?.Invoke(this, new KsRoomMessageEventArgs<KsWebGiftFeed>(e.ProcessName, gift));
|
||||
}
|
||||
|
||||
foreach (var share in feedPush.ShareFeeds)
|
||||
{
|
||||
OnShareMessage?.Invoke(this, new KsRoomMessageEventArgs<KsWebShareFeed>(e.ProcessName, share));
|
||||
}
|
||||
|
||||
OnRoomStatsMessage?.Invoke(this, new KsRoomMessageEventArgs<KsSCWebFeedPush>(e.ProcessName, feedPush));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, $"处理快手弹幕数据包时出错:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] Decompress(byte[] zippedData)
|
||||
{
|
||||
using (var ms = new MemoryStream(zippedData))
|
||||
using (var gzip = new GZipStream(ms, CompressionMode.Decompress))
|
||||
using (var outBuffer = new MemoryStream())
|
||||
{
|
||||
byte[] block = new byte[1024];
|
||||
while (true)
|
||||
{
|
||||
int bytesRead = gzip.Read(block, 0, block.Length);
|
||||
if (bytesRead <= 0) break;
|
||||
outBuffer.Write(block, 0, bytesRead);
|
||||
}
|
||||
return outBuffer.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class KsRoomMessageEventArgs<T> : EventArgs where T : class
|
||||
{
|
||||
public string Process { get; set; }
|
||||
public T Message { get; set; }
|
||||
|
||||
public KsRoomMessageEventArgs() { }
|
||||
|
||||
public KsRoomMessageEventArgs(string process, T data)
|
||||
{
|
||||
this.Process = process;
|
||||
this.Message = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
@@ -39,7 +39,9 @@ namespace BarrageGrab
|
||||
ConcurrentDictionary<string, Tuple<int, DateTime>> giftCountCache = new ConcurrentDictionary<string, Tuple<int, DateTime>>();//礼物计数缓存
|
||||
Timer dieout = new Timer(10000);//离线客户端清理计时器
|
||||
Timer giftCountTimer = new Timer(10000);//礼物缓存清理计时器
|
||||
WssBarrageGrab grab = new WssBarrageGrab();//弹幕解析器核心
|
||||
WssBarrageGrab grab = new WssBarrageGrab();//抖音弹幕解析器核心
|
||||
KsBarrageGrab ksGrab = new KsBarrageGrab();//快手弹幕解析器核心
|
||||
BiliBarrageGrab biliGrab = new BiliBarrageGrab();//B站弹幕解析器核心
|
||||
AppSetting Appsetting = AppSetting.Current;//全局配置文件实例
|
||||
static int printCount = 0; //控制台输出计数,用于判断清理控制台
|
||||
|
||||
@@ -92,6 +94,43 @@ namespace BarrageGrab
|
||||
this.grab.OnControlMessage += Grab_OnControlMessage;
|
||||
this.grab.Proxy.OnLiveCompanAction += Proxy_OnLiveCompanAction;
|
||||
|
||||
//快手事件订阅
|
||||
this.ksGrab.OnChatMessage += KsGrab_OnChatMessage;
|
||||
this.ksGrab.OnLikeMessage += KsGrab_OnLikeMessage;
|
||||
this.ksGrab.OnGiftMessage += KsGrab_OnGiftMessage;
|
||||
this.ksGrab.OnShareMessage += KsGrab_OnShareMessage;
|
||||
this.ksGrab.OnRoomStatsMessage += KsGrab_OnRoomStatsMessage;
|
||||
|
||||
//B站事件订阅
|
||||
this.biliGrab.OnChatMessage += BiliGrab_OnChatMessage;
|
||||
this.biliGrab.OnLikeMessage += BiliGrab_OnLikeMessage;
|
||||
this.biliGrab.OnGiftMessage += BiliGrab_OnGiftMessage;
|
||||
this.biliGrab.OnInteractMessage += BiliGrab_OnInteractMessage;
|
||||
this.biliGrab.OnShareMessage += BiliGrab_OnShareMessage;
|
||||
this.biliGrab.OnRoomStatsMessage += BiliGrab_OnRoomStatsMessage;
|
||||
|
||||
//多平台WS数据路由 - 将代理收到的WS数据按平台分发到对应解析器
|
||||
this.grab.Proxy.OnWebSocketData += (s, e) =>
|
||||
{
|
||||
if (e.Platform == LivePlatform.Kuaishou)
|
||||
{
|
||||
this.ksGrab.HandleWebSocketData(e);
|
||||
}
|
||||
else if (e.Platform == LivePlatform.Bilibili)
|
||||
{
|
||||
this.biliGrab.HandleWebSocketData(e);
|
||||
}
|
||||
};
|
||||
|
||||
//B站发送数据路由 - 解析AUTH认证包提取roomid
|
||||
this.grab.Proxy.OnWebSocketSentData += (s, e) =>
|
||||
{
|
||||
if (e.Platform == LivePlatform.Bilibili)
|
||||
{
|
||||
this.biliGrab.HandleSentData(e);
|
||||
}
|
||||
};
|
||||
|
||||
this.socketServer = socket;
|
||||
//dieout.Start();
|
||||
giftCountTimer.Start();
|
||||
@@ -633,6 +672,319 @@ namespace BarrageGrab
|
||||
}
|
||||
}
|
||||
|
||||
#region 快手消息处理
|
||||
|
||||
private MsgUser KsUserToMsgUser(Modles.KsProtoEntity.KsSimpleUserInfo user)
|
||||
{
|
||||
if (user == null) return null;
|
||||
return new MsgUser()
|
||||
{
|
||||
Id = long.TryParse(user.PrincipalId, out var pid) ? pid : 0,
|
||||
DisplayId = user.PrincipalId ?? "",
|
||||
Nickname = user.UserName ?? "用户" + user.PrincipalId,
|
||||
HeadImgUrl = user.HeadUrl ?? "",
|
||||
Gender = 0,
|
||||
Level = 0,
|
||||
PayLevel = 0,
|
||||
ShortId = 0,
|
||||
};
|
||||
}
|
||||
|
||||
private string KsRoomId;
|
||||
|
||||
private void KsGrab_OnChatMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsWebCommentFeed> e)
|
||||
{
|
||||
var feed = e.Message;
|
||||
var enty = new Msg()
|
||||
{
|
||||
MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
|
||||
User = KsUserToMsgUser(feed.User),
|
||||
Content = feed.Content ?? "",
|
||||
RoomId = KsRoomId ?? "",
|
||||
WebRoomId = KsRoomId ?? "",
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.弹幕消息;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
private void KsGrab_OnLikeMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsWebLikeFeed> e)
|
||||
{
|
||||
var feed = e.Message;
|
||||
var enty = new LikeMsg()
|
||||
{
|
||||
MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
|
||||
User = KsUserToMsgUser(feed.User),
|
||||
Content = $"{feed.User?.UserName ?? "用户"} 为主播点了赞",
|
||||
Count = 1,
|
||||
Total = 0,
|
||||
RoomId = KsRoomId ?? "",
|
||||
WebRoomId = KsRoomId ?? "",
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.点赞消息;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
private void KsGrab_OnGiftMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsWebGiftFeed> e)
|
||||
{
|
||||
var feed = e.Message;
|
||||
var enty = new GiftMsg()
|
||||
{
|
||||
MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
|
||||
User = KsUserToMsgUser(feed.User),
|
||||
GiftId = feed.GiftId,
|
||||
GiftName = KsGiftHelper.GetGiftName(feed.GiftId) ?? $"礼物#{feed.GiftId}",
|
||||
GroupId = 0,
|
||||
GiftCount = feed.ComboCount > 0 ? feed.ComboCount : 1,
|
||||
RepeatCount = feed.ComboCount > 0 ? feed.ComboCount : 1,
|
||||
DiamondCount = 0,
|
||||
Combo = feed.ComboCount > 1,
|
||||
Content = $"{feed.User?.UserName ?? "用户"} 送出 {KsGiftHelper.GetGiftName(feed.GiftId) ?? $"礼物#{feed.GiftId}"} x {feed.ComboCount}",
|
||||
RoomId = KsRoomId ?? "",
|
||||
WebRoomId = KsRoomId ?? "",
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.礼物消息;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
private void KsGrab_OnShareMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsWebShareFeed> e)
|
||||
{
|
||||
var feed = e.Message;
|
||||
var enty = new ShareMessage()
|
||||
{
|
||||
MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
|
||||
User = KsUserToMsgUser(feed.User),
|
||||
Content = $"{feed.User?.UserName ?? "用户"} 分享了直播间",
|
||||
ShareType = ShareType.未知,
|
||||
RoomId = KsRoomId ?? "",
|
||||
WebRoomId = KsRoomId ?? "",
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.直播间分享;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
private void KsGrab_OnRoomStatsMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsSCWebFeedPush> e)
|
||||
{
|
||||
var feed = e.Message;
|
||||
var enty = new UserSeqMsg()
|
||||
{
|
||||
MsgId = DateTime.Now.Ticks,
|
||||
Content = $"当前直播间人数 {feed.DisplayWatchingCount},点赞 {feed.DisplayLikeCount}",
|
||||
OnlineUserCount = long.TryParse(feed.DisplayWatchingCount, out var watching) ? watching : 0,
|
||||
TotalUserCount = 0,
|
||||
OnlineUserCountStr = feed.DisplayWatchingCount ?? "0",
|
||||
TotalUserCountStr = "0",
|
||||
RoomId = KsRoomId ?? "",
|
||||
WebRoomId = KsRoomId ?? "",
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.直播间统计;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region B站消息处理
|
||||
|
||||
private MsgUser BiliUserToMsgUser(Modles.BiliProtoEntity.BiliDanmuMsg user)
|
||||
{
|
||||
if (user == null) return null;
|
||||
return new MsgUser()
|
||||
{
|
||||
Id = user.Uid,
|
||||
DisplayId = user.Uid.ToString(),
|
||||
Nickname = user.Uname ?? "用户" + user.Uid,
|
||||
IsAdmin = user.IsAdmin,
|
||||
IsAnchor = user.IsAnchor,
|
||||
};
|
||||
}
|
||||
|
||||
private MsgUser BiliUserToMsgUser(long uid, string uname)
|
||||
{
|
||||
return new MsgUser()
|
||||
{
|
||||
Id = uid,
|
||||
DisplayId = uid.ToString(),
|
||||
Nickname = string.IsNullOrWhiteSpace(uname) ? "用户" + uid : uname,
|
||||
};
|
||||
}
|
||||
|
||||
//B站弹幕
|
||||
private void BiliGrab_OnChatMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliDanmuMsg> e)
|
||||
{
|
||||
var msg = e.Message;
|
||||
var enty = new Msg()
|
||||
{
|
||||
MsgId = DateTime.Now.Ticks,
|
||||
User = BiliUserToMsgUser(msg),
|
||||
Content = msg.Content ?? "",
|
||||
RoomId = biliGrab.CurrentRoomId,
|
||||
WebRoomId = biliGrab.CurrentRoomId,
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.弹幕消息;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
//B站点赞
|
||||
private void BiliGrab_OnLikeMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliLikeData> e)
|
||||
{
|
||||
var msg = e.Message;
|
||||
var enty = new LikeMsg()
|
||||
{
|
||||
MsgId = DateTime.Now.Ticks,
|
||||
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||
Content = $"{msg.Uname ?? "用户"} 为主播点了赞",
|
||||
Count = 1,
|
||||
Total = 0,
|
||||
RoomId = biliGrab.CurrentRoomId,
|
||||
WebRoomId = biliGrab.CurrentRoomId,
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.点赞消息;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
//B站礼物
|
||||
private void BiliGrab_OnGiftMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliGiftData> e)
|
||||
{
|
||||
var msg = e.Message;
|
||||
var count = msg.Num > 0 ? msg.Num : 1;
|
||||
var enty = new GiftMsg()
|
||||
{
|
||||
MsgId = DateTime.Now.Ticks,
|
||||
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||
GiftId = msg.GiftId,
|
||||
GiftName = msg.GiftName ?? $"礼物#{msg.GiftId}",
|
||||
GroupId = 0,
|
||||
GiftCount = count,
|
||||
RepeatCount = msg.SuperBatchNum > 0 ? msg.SuperBatchNum : count,
|
||||
DiamondCount = (int)(msg.Price * 1000),
|
||||
Combo = msg.ComboNum > 1,
|
||||
ImgUrl = msg.GifUrl ?? "",
|
||||
Content = $"{msg.Uname ?? "用户"} 送出 {msg.GiftName ?? $"礼物#{msg.GiftId}"} x {count}",
|
||||
RoomId = biliGrab.CurrentRoomId,
|
||||
WebRoomId = biliGrab.CurrentRoomId,
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.礼物消息;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
//B站进房/关注
|
||||
private void BiliGrab_OnInteractMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliInteractData> e)
|
||||
{
|
||||
var msg = e.Message;
|
||||
// msg_type: 1=进入 2=关注
|
||||
if (msg.MsgType == 2)
|
||||
{
|
||||
var enty = new Msg()
|
||||
{
|
||||
MsgId = DateTime.Now.Ticks,
|
||||
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||
Content = $"{msg.Uname ?? "用户"} 关注了主播",
|
||||
RoomId = biliGrab.CurrentRoomId,
|
||||
WebRoomId = biliGrab.CurrentRoomId,
|
||||
Appid = "0"
|
||||
};
|
||||
var msgType = PackMsgType.关注消息;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||
Broadcast(pack);
|
||||
}
|
||||
else
|
||||
{
|
||||
var enty = new Modles.JsonEntity.MemberMessage()
|
||||
{
|
||||
MsgId = DateTime.Now.Ticks,
|
||||
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||
Content = $"{msg.Uname ?? "用户"} 来了直播间",
|
||||
CurrentCount = 0,
|
||||
EnterTipType = 0,
|
||||
RoomId = biliGrab.CurrentRoomId,
|
||||
WebRoomId = biliGrab.CurrentRoomId,
|
||||
Appid = "0"
|
||||
};
|
||||
var msgType = PackMsgType.进直播间;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||
Broadcast(pack);
|
||||
}
|
||||
}
|
||||
|
||||
//B站分享
|
||||
private void BiliGrab_OnShareMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliInteractData> e)
|
||||
{
|
||||
var msg = e.Message;
|
||||
var enty = new ShareMessage()
|
||||
{
|
||||
MsgId = DateTime.Now.Ticks,
|
||||
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||
Content = $"{msg.Uname ?? "用户"} 分享了直播间",
|
||||
ShareType = ShareType.未知,
|
||||
RoomId = biliGrab.CurrentRoomId,
|
||||
WebRoomId = biliGrab.CurrentRoomId,
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.直播间分享;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
//B站房间统计
|
||||
private void BiliGrab_OnRoomStatsMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliRoomStats> e)
|
||||
{
|
||||
var msg = e.Message;
|
||||
var enty = new UserSeqMsg()
|
||||
{
|
||||
MsgId = DateTime.Now.Ticks,
|
||||
Content = $"当前直播间人数 {msg.Online},看过 {msg.Watched}",
|
||||
OnlineUserCount = msg.Online,
|
||||
TotalUserCount = msg.Watched,
|
||||
OnlineUserCountStr = msg.Online.ToString(),
|
||||
TotalUserCountStr = msg.Watched.ToString(),
|
||||
RoomId = biliGrab.CurrentRoomId,
|
||||
WebRoomId = biliGrab.CurrentRoomId,
|
||||
Appid = "0"
|
||||
};
|
||||
|
||||
var msgType = PackMsgType.直播间统计;
|
||||
PrintMsg(enty, msgType);
|
||||
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||
Broadcast(pack);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
//监听用户连接
|
||||
private void Listen(IWebSocketConnection socket)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BarrageGrab.Modles.JsonEntity;
|
||||
using BarrageGrab.Modles.ProtoEntity;
|
||||
using BarrageGrab.Proxy;
|
||||
using BarrageGrab.Proxy.ProxyEventArgs;
|
||||
@@ -24,7 +25,7 @@ namespace BarrageGrab
|
||||
/// <summary>
|
||||
/// 进入直播间
|
||||
/// </summary>
|
||||
public event EventHandler<RoomMessageEventArgs<MemberMessage>> OnMemberMessage;
|
||||
public event EventHandler<RoomMessageEventArgs<Modles.ProtoEntity.MemberMessage>> OnMemberMessage;
|
||||
|
||||
/// <summary>
|
||||
/// 关注
|
||||
@@ -103,8 +104,10 @@ namespace BarrageGrab
|
||||
}
|
||||
|
||||
//ws数据处理
|
||||
private static int douyinErrCount = 0;
|
||||
private void Proxy_OnWebSocketData(object sender, WsMessageEventArgs e)
|
||||
{
|
||||
if (e.Platform != LivePlatform.Douyin) return;
|
||||
if (!appsetting.ProcessFilter.Contains(e.ProcessName)) return;
|
||||
var buff = e.Payload;
|
||||
if (buff.Length == 0) return;
|
||||
@@ -129,7 +132,17 @@ namespace BarrageGrab
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, $"处理弹幕数据包时出错:{ex.Message}");
|
||||
// 限流:同类错误最多打印3次,防止刷屏和连锁崩溃
|
||||
if (douyinErrCount < 3)
|
||||
{
|
||||
Logger.LogError(ex, $"处理抖音弹幕数据包时出错:{ex.Message}");
|
||||
douyinErrCount++;
|
||||
}
|
||||
if (douyinErrCount == 3)
|
||||
{
|
||||
Logger.LogWarn("抖音弹幕解析持续失败,已抑制后续错误日志(不影响快手弹幕)");
|
||||
douyinErrCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +196,8 @@ namespace BarrageGrab
|
||||
//来了
|
||||
case "WebcastMemberMessage":
|
||||
{
|
||||
var arg = Serializer.Deserialize<MemberMessage>(new ReadOnlyMemory<byte>(msg.Payload));
|
||||
this.OnMemberMessage?.Invoke(this, new RoomMessageEventArgs<MemberMessage>(processName, arg));
|
||||
var arg = Serializer.Deserialize<Modles.ProtoEntity.MemberMessage>(new ReadOnlyMemory<byte>(msg.Payload));
|
||||
this.OnMemberMessage?.Invoke(this, new RoomMessageEventArgs<Modles.ProtoEntity.MemberMessage>(processName, arg));
|
||||
break;
|
||||
}
|
||||
//关注
|
||||
|
||||
Reference in New Issue
Block a user