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
{
///
/// 哔哩哔哩弹幕抓取器
/// 协议格式: 自定义16字节Big-Endian帧头 + JSON body (body可能为zlib/brotli压缩)
///
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";
/// 弹幕消息
public event EventHandler> OnChatMessage;
/// 点赞消息
public event EventHandler> OnLikeMessage;
/// 礼物消息
public event EventHandler> OnGiftMessage;
/// 进入直播间/关注消息
public event EventHandler> OnInteractMessage;
/// 房间统计消息
public event EventHandler> OnRoomStatsMessage;
/// 分享消息 (INTERACT_WORD中msg_type=3)
public event EventHandler> OnShareMessage;
public BiliBarrageGrab()
{
}
///
/// 当前认证的房间ID (从AUTH包解析,供消息处理器读取)
///
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);
}
///
/// 处理代理收到的B站WS数据
///
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++;
}
}
}
///
/// 处理客户端发送的B站WS数据 (解析AUTH认证包提取roomid)
/// B站客户端连接WS后会发送AUTH包: {uid,roomid,protover,platform,type,key}
///
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().ToString()
: roomIdToken.Value() ?? "";
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;
///
/// 递归解析缓冲区中可能包含的多个B站数据包
///
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(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);
}
}
///
/// 将JSON字节解析为B站消息并触发对应事件
///
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();
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(processName, msg));
}
break;
}
case BiliCmd.SEND_GIFT:
{
var msg = jobj["data"]?.ToObject();
if (msg != null)
{
OnGiftMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
}
break;
}
case BiliCmd.INTERACT_WORD:
{
var msg = jobj["data"]?.ToObject();
if (msg != null)
{
// msg_type: 1=进入 2=关注 3=分享
if (msg.MsgType == 3)
{
OnShareMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
}
else
{
OnInteractMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
}
}
break;
}
case BiliCmd.LIKE_INFO_V3_CLICK:
{
var msg = jobj["data"]?.ToObject();
if (msg != null)
{
OnLikeMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
}
break;
}
case BiliCmd.ONLINE_RANK_COUNT:
{
var count = jobj["data"]?["count"]?.Value() ?? 0;
var stats = new BiliRoomStats { Online = count, Watched = 0 };
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats));
break;
}
case BiliCmd.WATCHED_CHANGE:
{
var num = jobj["data"]?["num"]?.Value() ?? 0;
var stats = new BiliRoomStats { Online = 0, Watched = num };
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats));
break;
}
case BiliCmd.ROOM_REAL_TIME_MESSAGE_UPDATE:
{
// 同时包含在线人数和看过人数
var online = jobj["data"]?["online"]?.Value() ?? 0;
var watched = jobj["data"]?["watched_show"]?["num"]?.Value() ?? 0;
var stats = new BiliRoomStats { Online = online, Watched = watched };
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats));
break;
}
default:
// 未知消息类型忽略
break;
}
}
///
/// 解析DANMU_MSG消息: info[1]=内容, info[2]=[uid,uname,isAdmin,rank,?anchor]
///
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() ?? "";
var userInfo = info[2] as JArray;
if (userInfo == null || userInfo.Count < 2) return null;
var msg = new BiliDanmuMsg
{
Content = content,
Uid = userInfo[0]?.Value() ?? 0,
Uname = userInfo[1]?.Value() ?? "",
IsAdmin = userInfo.Count > 2 && userInfo[2]?.Value() == 1,
IsAnchor = userInfo.Count > 4 && userInfo[4] != null && userInfo[4].Type != JTokenType.Null
};
return msg;
}
catch
{
return null;
}
}
/// Zlib解压
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;
}
}
/// Brotli解压 (使用 BrotliSharpLib)
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;
}
}
}
/// B站消息事件参数
public class BiliRoomMessageEventArgs : 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;
}
}
}