├── .github └── workflows │ └── build.yml ├── .gitignore ├── .idea └── .idea.BLiveAPI │ └── .idea │ ├── .gitignore │ ├── encodings.xml │ ├── indexLayout.xml │ └── vcs.xml ├── BLiveAPI.cs ├── BLiveAPI.csproj ├── BLiveAPI.sln ├── BLiveEvents.cs ├── BLiveExceptions.cs ├── LICENSE ├── Properties └── AssemblyInfo.cs ├── README.md └── TargetCmdAttribute.cs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build .NET Framework 4.7.2 Library 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build: 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup MSBuild 19 | uses: microsoft/setup-msbuild@v1.3.1 20 | 21 | - name: Restore NuGet packages 22 | run: nuget restore 23 | 24 | - name: Build 25 | run: msbuild /p:Configuration=Release 26 | 27 | - name: Upload Release 28 | uses: actions/upload-artifact@v3 29 | with: 30 | name: BLiveAPI 31 | path: bin -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ -------------------------------------------------------------------------------- /.idea/.idea.BLiveAPI/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # Rider 忽略的文件 5 | /contentModel.xml 6 | /projectSettingsUpdater.xml 7 | /.idea.BLiveAPI.iml 8 | /modules.xml 9 | # 基于编辑器的 HTTP 客户端请求 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/.idea.BLiveAPI/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.BLiveAPI/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.BLiveAPI/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BLiveAPI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Net.WebSockets; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using BrotliSharpLib; 12 | using Newtonsoft.Json; 13 | using Newtonsoft.Json.Linq; 14 | 15 | namespace BLiveAPI; 16 | 17 | /// 18 | /// B站直播间弹幕接口 19 | /// 20 | public class BLiveApi : BLiveEvents 21 | { 22 | private const string WsHost = "wss://broadcastlv.chat.bilibili.com/sub"; 23 | private ClientWebSocket _clientWebSocket; 24 | private ulong? _roomId; 25 | private CancellationTokenSource _webSocketCancelToken; 26 | 27 | 28 | private static int BytesToInt(byte[] bytes) 29 | { 30 | if (BitConverter.IsLittleEndian) Array.Reverse(bytes); 31 | return bytes.Length switch 32 | { 33 | 2 => BitConverter.ToInt16(bytes, 0), 34 | 4 => BitConverter.ToInt32(bytes, 0), 35 | _ => throw new InvalidBytesLengthException() 36 | }; 37 | } 38 | 39 | private void DecodeMessage(ServerOperation operation, byte[] messageData) 40 | { 41 | switch (operation) 42 | { 43 | case ServerOperation.OpAuthReply: 44 | OnOpAuthReply((JObject)JsonConvert.DeserializeObject(Encoding.UTF8.GetString(messageData)), _roomId, messageData); 45 | break; 46 | case ServerOperation.OpHeartbeatReply: 47 | OnOpHeartbeatReply(BytesToInt(new ArraySegment(messageData, 0, messageData.Length).ToArray()), messageData); 48 | break; 49 | case ServerOperation.OpSendSmsReply: 50 | OnOpSendSmsReply((JObject)JsonConvert.DeserializeObject(Encoding.UTF8.GetString(messageData)), messageData); 51 | break; 52 | default: 53 | throw new UnknownServerOperationException(operation); 54 | } 55 | } 56 | 57 | /// 58 | /// 暴露DecodePacket方法 59 | /// 60 | /// 需要解包的数据 61 | /// 是否抛出异常 62 | public void DecodePacket(byte[] packetData, bool throwError) 63 | { 64 | try 65 | { 66 | DecodePacket(packetData); 67 | } 68 | catch (Exception) 69 | { 70 | if (throwError) throw; 71 | } 72 | } 73 | 74 | private void DecodePacket(byte[] packetData) 75 | { 76 | while (true) 77 | { 78 | var header = new ArraySegment(packetData, 0, 16).ToArray(); 79 | var body = new ArraySegment(packetData, 16, packetData.Length - 16).ToArray(); 80 | var version = BytesToInt(new ArraySegment(header, 6, 2).ToArray()); 81 | switch (version) 82 | { 83 | case 0: 84 | case 1: 85 | var firstPacketLength = BytesToInt(new ArraySegment(header, 0, 4).ToArray()); 86 | var operation = (ServerOperation)BytesToInt(new ArraySegment(header, 8, 4).ToArray()); 87 | DecodeMessage(operation, new ArraySegment(body, 0, firstPacketLength - 16).ToArray()); 88 | if (packetData.Length > firstPacketLength) 89 | { 90 | packetData = new ArraySegment(packetData, firstPacketLength, packetData.Length - firstPacketLength).ToArray(); 91 | continue; 92 | } 93 | 94 | break; 95 | case 2: 96 | using (var resultStream = new MemoryStream()) 97 | using (var packetStream = new MemoryStream(body, 2, body.Length - 2)) 98 | using (var deflateStream = new DeflateStream(packetStream, CompressionMode.Decompress)) 99 | { 100 | deflateStream.CopyTo(resultStream); 101 | packetData = resultStream.ToArray(); 102 | continue; 103 | } 104 | 105 | case 3: 106 | packetData = Brotli.DecompressBuffer(body, 0, body.Length); 107 | continue; 108 | default: 109 | throw new UnknownVersionException(version); 110 | } 111 | 112 | break; 113 | } 114 | } 115 | 116 | private async Task ReceiveMessage() 117 | { 118 | var buffer = new List(); 119 | while (_clientWebSocket.State == WebSocketState.Open) 120 | { 121 | var tempBuffer = new byte[1024]; 122 | var result = await _clientWebSocket.ReceiveAsync(new ArraySegment(tempBuffer), _webSocketCancelToken.Token); 123 | buffer.AddRange(new ArraySegment(tempBuffer, 0, result.Count)); 124 | if (!result.EndOfMessage) continue; 125 | try 126 | { 127 | DecodePacket(buffer.ToArray()); 128 | } 129 | catch (Exception e) 130 | { 131 | OnDecodeError(e.Message, e); 132 | _webSocketCancelToken?.Cancel(); 133 | throw; 134 | } 135 | finally 136 | { 137 | buffer.Clear(); 138 | } 139 | } 140 | 141 | throw new OperationCanceledException(); 142 | } 143 | 144 | private async Task SendHeartbeat(ArraySegment heartPacket) 145 | { 146 | while (_clientWebSocket.State == WebSocketState.Open) 147 | { 148 | await _clientWebSocket.SendAsync(heartPacket, WebSocketMessageType.Binary, true, _webSocketCancelToken.Token); 149 | await Task.Delay(TimeSpan.FromSeconds(20), _webSocketCancelToken.Token); 150 | } 151 | 152 | throw new OperationCanceledException(); 153 | } 154 | 155 | private static byte[] ToBigEndianBytes(int value) 156 | { 157 | var bytes = BitConverter.GetBytes(value); 158 | if (BitConverter.IsLittleEndian) Array.Reverse(bytes); 159 | return bytes; 160 | } 161 | 162 | private static byte[] ToBigEndianBytes(short value) 163 | { 164 | var bytes = BitConverter.GetBytes(value); 165 | if (BitConverter.IsLittleEndian) Array.Reverse(bytes); 166 | return bytes; 167 | } 168 | 169 | private static ArraySegment CreateWsPacket(ClientOperation operation, byte[] body) 170 | { 171 | var packetLength = 16 + body.Length; 172 | var result = new byte[packetLength]; 173 | Buffer.BlockCopy(ToBigEndianBytes(packetLength), 0, result, 0, 4); 174 | Buffer.BlockCopy(ToBigEndianBytes((short)16), 0, result, 4, 2); 175 | Buffer.BlockCopy(ToBigEndianBytes((short)1), 0, result, 6, 2); 176 | Buffer.BlockCopy(ToBigEndianBytes((int)operation), 0, result, 8, 4); 177 | Buffer.BlockCopy(ToBigEndianBytes(1), 0, result, 12, 4); 178 | Buffer.BlockCopy(body, 0, result, 16, body.Length); 179 | return new ArraySegment(result); 180 | } 181 | 182 | private static ulong? GetRoomId(ulong shortRoomId) 183 | { 184 | try 185 | { 186 | var url = $"https://api.live.bilibili.com/xlive/web-room/v1/index/getRoomBaseInfo?room_ids={shortRoomId}&req_biz=web/"; 187 | var result = new HttpClient().GetStringAsync(url).Result; 188 | var jsonResult = (JObject)JsonConvert.DeserializeObject(result); 189 | var roomInfo = (JObject)jsonResult?["data"]?["by_room_ids"]?.Values().FirstOrDefault(); 190 | var roomId = (ulong?)roomInfo?.GetValue("room_id"); 191 | if (roomId is null) throw new InvalidRoomIdException(); 192 | return roomId; 193 | } 194 | catch (InvalidRoomIdException) 195 | { 196 | throw; 197 | } 198 | catch (ArgumentException) 199 | { 200 | throw new DomainNameEncodingException(); 201 | } 202 | catch 203 | { 204 | throw new NetworkException(); 205 | } 206 | } 207 | 208 | private static string GetBuVid() 209 | { 210 | try 211 | { 212 | var result = new HttpClient().GetAsync("https://data.bilibili.com/v/").Result; 213 | return result.Headers.GetValues("Set-Cookie").First().Split(';').First().Split('=').Last(); 214 | } 215 | catch (ArgumentException) 216 | { 217 | throw new DomainNameEncodingException(); 218 | } 219 | catch 220 | { 221 | throw new NetworkException(); 222 | } 223 | } 224 | 225 | private static (ulong, string) GetUidAndKey(ulong? roomId, string sessdata) 226 | { 227 | try 228 | { 229 | var client = new HttpClient(new HttpClientHandler { UseCookies = false }); 230 | client.DefaultRequestHeaders.Add("Cookie", $"SESSDATA={sessdata}"); 231 | var userInfoResult = client.GetStringAsync("https://api.bilibili.com/x/space/v2/myinfo").Result; 232 | var userInfoJsonResult = (JObject)JsonConvert.DeserializeObject(userInfoResult); 233 | var uid = (ulong?)userInfoJsonResult?["data"]?["profile"]?["mid"] ?? 0; 234 | if (uid == 0) client.DefaultRequestHeaders.Remove("Cookie"); 235 | var danmuInfoResult = client.GetStringAsync($"https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id={roomId}&type=0").Result; 236 | var danmuInfoJsonResult = (JObject)JsonConvert.DeserializeObject(danmuInfoResult); 237 | return (uid, (string)danmuInfoJsonResult?["data"]?["token"]); 238 | } 239 | catch (ArgumentException) 240 | { 241 | throw new DomainNameEncodingException(); 242 | } 243 | catch 244 | { 245 | throw new NetworkException(); 246 | } 247 | } 248 | 249 | /// 250 | /// 关闭当前对象中的WebSocket 251 | /// 252 | public async Task Close() 253 | { 254 | _webSocketCancelToken?.Cancel(); 255 | if (_clientWebSocket is not null && _clientWebSocket.State == WebSocketState.Open) 256 | await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close", CancellationToken.None); 257 | } 258 | 259 | /// 260 | /// 连接指定的直播间 261 | /// 262 | /// 直播间id,可以是短位id 263 | /// 压缩类型2:zlib,3:brotli
unity中请使用zlib,使用brotli会导致unity闪退假死等问题!!!! 264 | /// 使用者的B站Cookie中的SESSDATA 265 | public async Task Connect(ulong roomId, int protoVer, string sessdata = null) 266 | { 267 | if (_webSocketCancelToken is not null) throw new ConnectAlreadyRunningException(); 268 | if (protoVer is not (2 or 3)) throw new InvalidProtoVerException(); 269 | try 270 | { 271 | _webSocketCancelToken = new CancellationTokenSource(); 272 | _roomId = GetRoomId(roomId); 273 | var (uid, key) = GetUidAndKey(_roomId, sessdata); 274 | if (uid == 0 && sessdata != null) throw new SessdataExpireException(); 275 | _clientWebSocket = new ClientWebSocket(); 276 | var authBody = new { uid, roomid = _roomId, protover = protoVer, buvid = GetBuVid(), platform = "web", type = 2, key }; 277 | var authPacket = CreateWsPacket(ClientOperation.OpAuth, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(authBody))); 278 | var heartPacket = CreateWsPacket(ClientOperation.OpHeartbeat, Array.Empty()); 279 | await _clientWebSocket.ConnectAsync(new Uri(WsHost), _webSocketCancelToken.Token); 280 | await _clientWebSocket.SendAsync(authPacket, WebSocketMessageType.Binary, true, _webSocketCancelToken.Token); 281 | await Task.WhenAll(ReceiveMessage(), SendHeartbeat(heartPacket)); 282 | } 283 | catch (OperationCanceledException) 284 | { 285 | OnWebSocketClose("WebSocket主动关闭", 0); 286 | throw new WebSocketCloseException(); 287 | } 288 | catch (WebSocketException) 289 | { 290 | OnWebSocketError("WebSocket异常关闭", -1); 291 | _webSocketCancelToken?.Cancel(); 292 | throw new WebSocketErrorException(); 293 | } 294 | finally 295 | { 296 | _roomId = null; 297 | _clientWebSocket = null; 298 | _webSocketCancelToken = null; 299 | } 300 | } 301 | 302 | private enum ClientOperation 303 | { 304 | OpHeartbeat = 2, 305 | OpAuth = 7 306 | } 307 | 308 | private enum ServerOperation 309 | { 310 | OpHeartbeatReply = 3, 311 | OpSendSmsReply = 5, 312 | OpAuthReply = 8 313 | } 314 | } -------------------------------------------------------------------------------- /BLiveAPI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {861DCA84-ED11-4F61-8CB8-AD7366E610BB} 8 | Library 9 | Properties 10 | BLiveAPI 11 | BLiveAPI 12 | v4.7.2 13 | 512 14 | latest 15 | 16 | 17 | AnyCPU 18 | true 19 | portable 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | bin\Debug\BLiveAPI.xml 26 | 27 | 28 | AnyCPU 29 | portable 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | bin\Release\BLiveAPI.xml 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Always 56 | 57 | 58 | Always 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /BLiveAPI.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BLiveAPI", "BLiveAPI.csproj", "{861DCA84-ED11-4F61-8CB8-AD7366E610BB}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {861DCA84-ED11-4F61-8CB8-AD7366E610BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {861DCA84-ED11-4F61-8CB8-AD7366E610BB}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {861DCA84-ED11-4F61-8CB8-AD7366E610BB}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {861DCA84-ED11-4F61-8CB8-AD7366E610BB}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /BLiveEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Text; 5 | using Google.Protobuf; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace BLiveAPI; 9 | 10 | /// 11 | /// BLiveAPI的各种事件 12 | /// 13 | public abstract class BLiveEvents 14 | { 15 | /// 16 | public delegate void BLiveEventHandler(object sender, TEventArgs e); 17 | 18 | /// 19 | protected BLiveEvents() 20 | { 21 | SendSmsReply += OnDanmuMsg; 22 | SendSmsReply += OnInteractWord; 23 | SendSmsReply += OnSendGift; 24 | SendSmsReply += OnSuperChatMessage; 25 | SendSmsReply += OnUserToastMsg; 26 | } 27 | 28 | /// 29 | /// 服务器回复的认证消息 30 | /// 31 | public event BLiveEventHandler<(JObject authReply, ulong? roomId, byte[] rawData)> OpAuthReply; 32 | 33 | /// 34 | protected void OnOpAuthReply(JObject authReply, ulong? roomId, byte[] rawData) 35 | { 36 | OpAuthReply?.Invoke(this, (authReply, roomId, rawData)); 37 | } 38 | 39 | /// 40 | /// 服务器回复的心跳消息 41 | /// 42 | public event BLiveEventHandler<(int heartbeatReply, byte[] rawData)> OpHeartbeatReply; 43 | 44 | /// 45 | protected void OnOpHeartbeatReply(int heartbeatReply, byte[] rawData) 46 | { 47 | OpHeartbeatReply?.Invoke(this, (heartbeatReply, rawData)); 48 | } 49 | 50 | /// 51 | /// 服务器发送的SMS消息 52 | /// 53 | public event BLiveEventHandler<(string cmd, string hitCmd, JObject jsonRawData, byte[] rawData)> OpSendSmsReply; 54 | 55 | private void InvokeOpSendSmsReply(JObject jsonRawData, bool hit, byte[] rawData) 56 | { 57 | if (OpSendSmsReply is null) return; 58 | var waitInvokeList = OpSendSmsReply.GetInvocationList().ToList(); 59 | var cmd = (string)jsonRawData["cmd"]; 60 | foreach (var invocation in OpSendSmsReply.GetInvocationList()) 61 | { 62 | var targetCmdAttribute = invocation.Method.GetCustomAttributes().FirstOrDefault(); 63 | if (targetCmdAttribute is null) 64 | { 65 | invocation.DynamicInvoke(this, (cmd, "ALL", jsonRawData, rawData)); 66 | waitInvokeList.Remove(invocation); 67 | } 68 | else if (targetCmdAttribute.HasCmd(cmd)) 69 | { 70 | invocation.DynamicInvoke(this, (cmd, cmd, jsonRawData, rawData)); 71 | waitInvokeList.Remove(invocation); 72 | hit = true; 73 | } 74 | else if (targetCmdAttribute.HasCmd("ALL")) 75 | { 76 | invocation.DynamicInvoke(this, (cmd, "ALL", jsonRawData, rawData)); 77 | waitInvokeList.Remove(invocation); 78 | } 79 | else if (!targetCmdAttribute.HasCmd("OTHERS")) 80 | { 81 | waitInvokeList.Remove(invocation); 82 | } 83 | } 84 | 85 | if (hit) return; 86 | foreach (var invocation in waitInvokeList) invocation.DynamicInvoke(this, (cmd, "OTHERS", jsonRawData, rawData)); 87 | } 88 | 89 | private event BLiveSmsEventHandler SendSmsReply; 90 | 91 | private bool InvokeSendSmsReply(JObject jsonRawData, byte[] rawData) 92 | { 93 | if (SendSmsReply is null) return false; 94 | var cmd = (string)jsonRawData["cmd"]; 95 | return (from invocation in SendSmsReply.GetInvocationList() 96 | let targetCmdAttribute = invocation.Method.GetCustomAttributes().FirstOrDefault() 97 | where targetCmdAttribute != null && targetCmdAttribute.HasCmd(cmd) 98 | select invocation).Aggregate(false, (current, invocation) => (bool)invocation.DynamicInvoke(jsonRawData, rawData.ToArray()) || current); 99 | } 100 | 101 | /// 102 | protected void OnOpSendSmsReply(JObject jsonRawData, byte[] rawData) 103 | { 104 | InvokeOpSendSmsReply(jsonRawData, InvokeSendSmsReply(jsonRawData, rawData), rawData); 105 | } 106 | 107 | /// 108 | /// 弹幕消息,guardLevel 0:普通观众 1:总督 2:提督 3:舰长 109 | /// 110 | public event BLiveEventHandler<(string msg, ulong userId, string userName, int guardLevel, string face, JObject jsonRawData, byte[] rawData)> DanmuMsg; 111 | 112 | private static byte[] GetChildFromProtoData(byte[] protoData, int target) 113 | { 114 | using (var input = new CodedInputStream(protoData)) 115 | { 116 | while (!input.IsAtEnd) 117 | { 118 | var tag = input.ReadTag(); 119 | var tagId = WireFormat.GetTagFieldNumber(tag); 120 | if (tagId == target) return input.ReadBytes().ToByteArray(); 121 | input.SkipLastField(); 122 | } 123 | } 124 | 125 | return Array.Empty(); 126 | } 127 | 128 | [TargetCmd("DANMU_MSG")] 129 | private bool OnDanmuMsg(JObject jsonRawData, byte[] rawData) 130 | { 131 | var msg = (string)jsonRawData["info"][1]; 132 | var userId = (ulong)jsonRawData["info"][2]?[0]; 133 | var userName = (string)jsonRawData["info"][2]?[1]; 134 | var guardLevel = (int)jsonRawData["info"][7]; 135 | var protoData = Convert.FromBase64String(jsonRawData["dm_v2"].ToString()); 136 | var face = Encoding.UTF8.GetString(GetChildFromProtoData(GetChildFromProtoData(protoData, 20), 4)); 137 | DanmuMsg?.Invoke(this, (msg, userId, userName, guardLevel, face, jsonRawData, rawData)); 138 | return DanmuMsg is not null; 139 | } 140 | 141 | /// 142 | /// 观众进房消息,privilegeType 0:普通观众 1:总督 2:提督 3:舰长 143 | /// 144 | public event BLiveEventHandler<(int privilegeType, ulong userId, string userName, JObject jsonRawData, byte[] rawData)> InteractWord; 145 | 146 | [TargetCmd("INTERACT_WORD")] 147 | private bool OnInteractWord(JObject jsonRawData, byte[] rawData) 148 | { 149 | var privilegeType = (int)jsonRawData["data"]["privilege_type"]; 150 | var userId = (ulong)jsonRawData["data"]["uid"]; 151 | var userName = (string)jsonRawData["data"]["uname"]; 152 | InteractWord?.Invoke(this, (privilegeType, userId, userName, jsonRawData, rawData)); 153 | return InteractWord is not null; 154 | } 155 | 156 | /// 157 | /// 投喂礼物事件 giftInfo:礼物信息 blindInfo:盲盒礼物信息,如果此礼物不是盲盒爆出则为null coinType:区别是金瓜子礼物还是银瓜子礼物 guardLevel 0:普通观众 1:总督 2:提督 3:舰长 158 | /// 159 | public event BLiveEventHandler<( JObject giftInfo, JObject blindInfo, string coinType, ulong userId, string userName, int guardLevel, string face, JObject jsonRawData, byte[] rawData)> SendGift; 160 | 161 | [TargetCmd("SEND_GIFT")] 162 | private bool OnSendGift(JObject jsonRawData, byte[] rawData) 163 | { 164 | var data = jsonRawData["data"]; 165 | var blind = data["blind_gift"]; 166 | var userId = (ulong)data["uid"]; 167 | var userName = (string)data["uname"]; 168 | var guardLevel = (int)data["guard_level"]; 169 | var face = (string)data["face"]; 170 | var coinType = (string)data["coin_type"]; 171 | var giftInfo = JObject.FromObject(new { action = data["action"], giftId = data["giftId"], giftName = data["giftName"], price = data["price"] }); 172 | var blindInfo = blind?.Type is JTokenType.Null 173 | ? null 174 | : JObject.FromObject(new 175 | { 176 | action = blind?.SelectToken("gift_action"), 177 | giftId = blind?.SelectToken("original_gift_id"), 178 | giftName = blind?.SelectToken("original_gift_name"), 179 | price = blind?.SelectToken("original_gift_price") 180 | }); 181 | SendGift?.Invoke(this, (giftInfo, blindInfo, coinType, userId, userName, guardLevel, face, jsonRawData, rawData)); 182 | return SendGift is not null; 183 | } 184 | 185 | /// 186 | /// SC消息事件 guardLevel 0:普通观众 1:总督 2:提督 3:舰长 187 | /// 188 | public event BLiveEventHandler<(string message, ulong id, int price, ulong userId, string userName, int guardLevel, string face, JObject jsonRawData, byte[] rawData)> SuperChatMessage; 189 | 190 | [TargetCmd("SUPER_CHAT_MESSAGE")] 191 | private bool OnSuperChatMessage(JObject jsonRawData, byte[] rawData) 192 | { 193 | var message = (string)jsonRawData["data"]["message"]; 194 | var price = (int)jsonRawData["data"]["price"]; 195 | var id = (ulong)jsonRawData["data"]["id"]; 196 | var userId = (ulong)jsonRawData["data"]["uid"]; 197 | var face = (string)jsonRawData["data"]["user_info"]?["face"]; 198 | var userName = (string)jsonRawData["data"]["user_info"]?["uname"]; 199 | var guardLevel = (int)jsonRawData["data"]["user_info"]?["guard_level"]; 200 | SuperChatMessage?.Invoke(this, (message, id, price, userId, userName, guardLevel, face, jsonRawData, rawData)); 201 | return SuperChatMessage is not null; 202 | } 203 | 204 | /// 205 | /// 上舰消息事件 price的单位是金瓜子 206 | /// 207 | public event BLiveEventHandler<(string roleName, int giftId, int guardLevel, int price, int num, string unit, ulong userId, string userName, JObject jsonRawData, byte[] rawData)> UserToastMsg; 208 | 209 | [TargetCmd("USER_TOAST_MSG")] 210 | private bool OnUserToastMsg(JObject jsonRawData, byte[] rawData) 211 | { 212 | var roleName = (string)jsonRawData["data"]["role_name"]; 213 | var giftId = (int)jsonRawData["data"]["gift_id"]; 214 | var guardLevel = (int)jsonRawData["data"]["guard_level"]; 215 | var price = (int)jsonRawData["data"]["price"]; 216 | var num = (int)jsonRawData["data"]["num"]; 217 | var unit = (string)jsonRawData["data"]["unit"]; 218 | var userId = (ulong)jsonRawData["data"]["uid"]; 219 | var userName = (string)jsonRawData["data"]["username"]; 220 | UserToastMsg?.Invoke(this, (roleName, giftId, guardLevel, price, num, unit, userId, userName, jsonRawData, rawData)); 221 | return UserToastMsg is not null; 222 | } 223 | 224 | /// 225 | /// WebSocket异常关闭 226 | /// 227 | public event BLiveEventHandler<(string message, int code)> WebSocketError; 228 | 229 | /// 230 | protected void OnWebSocketError(string message, int code) 231 | { 232 | WebSocketError?.Invoke(this, (message, code)); 233 | } 234 | 235 | /// 236 | /// WebSocket主动关闭 237 | /// 238 | public event BLiveEventHandler<(string message, int code)> WebSocketClose; 239 | 240 | /// 241 | protected void OnWebSocketClose(string message, int code) 242 | { 243 | WebSocketClose?.Invoke(this, (message, code)); 244 | } 245 | 246 | /// 247 | /// 解析消息过程出现的错误,不影响WebSocket正常运行,所以不抛出异常(当前版本暂时会抛出) 248 | /// 249 | public event BLiveEventHandler<(string message, Exception e)> DecodeError; 250 | 251 | /// 252 | protected void OnDecodeError(string message, Exception e) 253 | { 254 | DecodeError?.Invoke(this, (message, e)); 255 | } 256 | 257 | private delegate bool BLiveSmsEventHandler(JObject jsonRawData, byte[] rawData); 258 | } -------------------------------------------------------------------------------- /BLiveExceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BLiveAPI; 4 | 5 | /// 6 | /// 同一对象的Connect方法重复运行的异常 7 | /// 8 | public class ConnectAlreadyRunningException : Exception 9 | { 10 | /// 11 | public ConnectAlreadyRunningException() : base("该对象的Connect方法已经在运行中,禁止重复运行") 12 | { 13 | } 14 | } 15 | 16 | /// 17 | /// 房间号无效的异常 18 | /// 19 | public class InvalidRoomIdException : Exception 20 | { 21 | /// 22 | public InvalidRoomIdException() : base("无效的房间号") 23 | { 24 | } 25 | } 26 | 27 | /// 28 | /// protoVer无效,只能是2或3 29 | /// 30 | public class InvalidProtoVerException : Exception 31 | { 32 | /// 33 | public InvalidProtoVerException() : base("protoVer无效,只能是2或3") 34 | { 35 | } 36 | } 37 | 38 | /// 39 | /// 未知的ServerOperation异常 40 | /// 41 | public class UnknownServerOperationException : Exception 42 | { 43 | /// 44 | public UnknownServerOperationException(object value) : base($"未知的ServerOperation:{value}") 45 | { 46 | } 47 | } 48 | 49 | /// 50 | /// 未知的Version异常 51 | /// 52 | public class UnknownVersionException : Exception 53 | { 54 | /// 55 | public UnknownVersionException(object value) : base($"未知的Version:{value}") 56 | { 57 | } 58 | } 59 | 60 | /// 61 | /// 网络异常 62 | /// 63 | public class NetworkException : Exception 64 | { 65 | /// 66 | public NetworkException() : base("网络异常") 67 | { 68 | } 69 | } 70 | 71 | /// 72 | /// 主机用户名编码异常 73 | /// 74 | public class DomainNameEncodingException : Exception 75 | { 76 | /// 77 | public DomainNameEncodingException() : base("主机用户名编码异常,请检查主机用户名中是否有非ASCII字符") 78 | { 79 | } 80 | } 81 | 82 | /// 83 | /// 字节集长度错误 84 | /// 85 | public class InvalidBytesLengthException : Exception 86 | { 87 | /// 88 | public InvalidBytesLengthException() : base("字节集长度错误") 89 | { 90 | } 91 | } 92 | 93 | /// 94 | /// WebSocket主动关闭 95 | /// 96 | public class WebSocketCloseException : Exception 97 | { 98 | /// 99 | public WebSocketCloseException() : base("WebSocket主动关闭") 100 | { 101 | } 102 | } 103 | 104 | /// 105 | /// WebSocket异常关闭 106 | /// 107 | public class WebSocketErrorException : Exception 108 | { 109 | /// 110 | public WebSocketErrorException() : base("WebSocket异常关闭") 111 | { 112 | } 113 | } 114 | 115 | /// 116 | /// SESSDATA过期 117 | /// 118 | public class SessdataExpireException : Exception 119 | { 120 | /// 121 | public SessdataExpireException() : base("SESSDATA过期") 122 | { 123 | } 124 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 skyatgit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("BLiveAPI")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("BLiveAPI")] 12 | [assembly: AssemblyCopyright("Copyright © 2023")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("861DCA84-ED11-4F61-8CB8-AD7366E610BB")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BLiveAPI 2 | 3 | B站直播间弹幕野生接口。 4 | 5 | [![](https://github.com/skyatgit/BLiveAPI/actions/workflows/build.yml/badge.svg)](https://github.com/skyatgit/BLiveAPI/actions/workflows/build.yml) 6 | 7 | ```c# 8 | //创建一个BLiveApi对象 9 | var api = new BLiveApi(); 10 | //用于接收认证消息的方法(没啥用,可以不写) 11 | private void OpAuthReplyEvent(object sender, (JObject authReply, byte[] rawData) e) 12 | { 13 | } 14 | //用于接收心跳消息的方法 15 | private void OpHeartbeatReplyEvent(object sender, (int heartbeatReply, byte[] rawData) e) 16 | { 17 | } 18 | //用于接收主动关闭消息的方法(使用者主动调用api.Close()时),同时会触发异常 19 | private void WebSocketCloseEvent(object sender, (string message, int code) e) 20 | { 21 | } 22 | //用于接收被动关闭消息的方法(一般是网络错误等原因),同时会触发异常 23 | private void WebSocketErrorEvent(object sender, (string message, int code) e) 24 | { 25 | } 26 | //用于接收API内部解码错误,一般情况下不会触发,除非B站改逻辑或其他特殊情况,此消息触发时不会引起异常 27 | //目前发现在不同的C#版本引入库时会出现不同的问题,所以暂时将此异常抛出并终止与直播间的连接 28 | //Unity使用本库时Brotli库不可用,在使用API的Connect方法时请将第二个参数设置为2 29 | //.NET项目使用本库时需要自己在NuGet安装 Newtonsoft.Json 30 | //.NET Framework项目目前使用无问题 31 | private void DecodeErrorEvent(object sender, (string message, Exception e) e) 32 | { 33 | } 34 | //用于接收API内部提供的一个简单处理过后的弹幕消息的方法 35 | //此方法订阅DanmuMsg事件时会和使用者创建并绑定了携带[TargetCmd("DANMU_MSG")]的方法一样屏蔽掉携带OTHERS的方法(可看下面的示例) 36 | private void DanmuMsgEvent(object sender, (string msg, ulong userId, string userName, int guardLevel,string face, JObject jsonRawData, byte[] rawData) e) 37 | { 38 | } 39 | //当方法与OpSendSmsReply绑定时需要使用[TargetCmd("cmd1","cmd2"...)]设置方法想要接收的命令,建议每个方法只设置1个命令 40 | //此方法是使用者自定义的用于接收OpSendSmsReply事件中SEND_GIFT命令对应的事件的方法 41 | [TargetCmd("SEND_GIFT")] 42 | private void OnSendGiftEvent(object sender, (string cmd, string hitCmd, JObject jsonRawData, byte[] rawData) e) 43 | { 44 | } 45 | //TargetCmd支持填入ALL和OTHERS 46 | //当携带ALL或者没有标注[TargetCmd("cmd1","cmd2"...)]时,该方法会无差别的接收所有SMS消息,但会首先命中TargetCmd参数列表中的其他命令 47 | //当cmd只命中ALL或此方法未携带[TargetCmd("cmd1","cmd2"...)]时,不视作命令被命中,携带OTHERS的方法仍然会被Invoke 48 | [TargetCmd("ALL")] 49 | private void OnAllEvent(object sender, (string cmd, string hitCmd, JObject jsonRawData, byte[] rawData) e) 50 | { 51 | } 52 | //当携带OTHERS时,该方法会接收未被其他方法命中的SMS消息,但TargetCmd参数列表中的其他命令被命中时不会被再次Invoke 53 | [TargetCmd("OTHERS")] 54 | private void OtherMessagesEvent(object sender, (string cmd, string hitCmd, JObject jsonRawData, byte[] rawData) e) 55 | { 56 | } 57 | 58 | //绑定事件 59 | api.OpAuthReply += OpAuthReplyEvent; 60 | api.OpHeartbeatReply += OpHeartbeatReplyEvent; 61 | api.WebSocketClose += WebSocketCloseEvent; 62 | api.WebSocketError += WebSocketErrorEvent; 63 | api.DecodeError += DecodeErrorEvent; 64 | api.DanmuMsg += DanmuMsgEvent; 65 | api.OpSendSmsReply += OnSendGiftEvent; 66 | api.OpSendSmsReply += OnAllEvent; 67 | api.OpSendSmsReply += OtherMessagesEvent; 68 | 69 | //连接到某个直播间,Connect内有可能会抛出一些回事WebSocket断开连接的异常,需要监听并处理 70 | try 71 | { 72 | //Connect的第一个参数代表房间号,支持短位房间号 73 | //第二个参数代表数据压缩协议的版本,只支持输入2或3,2代表zlib方式,3代表brotli方式 74 | //因为使用的Brotli库会在unity中报错的原因,建议在unity中使用时填入2 75 | //sessdata为使用者B站Cookie中的SESSDATA,如果不传则以游客身份连接弹幕服务器,如果传入无效的SESSDATA,则会抛出SessdataExpireException 76 | await api.Connect(1234,3,sessdata); 77 | } 78 | catch (Exception e) 79 | { 80 | Console.WriteLine(e); 81 | } 82 | //可以通过Close方法主动关闭WebSocket连接 83 | await api.Close(); 84 | 85 | //可能出现的异常 86 | ConnectAlreadyRunningException//Connect后没有主动或被动结束,一个api对象同时只能连接到一个房间 87 | InvalidRoomIdException//输入的roomId有误,大概率是B站没有这个房间号对应的直播间 88 | UnknownServerOperationException//未知的消息类型,正常情况下不会出现,可在DecodeError事件中接收到 89 | UnknownVersionException//未知的压缩类型,正常情况下不会出现,可在DecodeError事件中接收到 90 | NetworkException//网络错误,一般会在连接房间但本机无网络的情况下出现 91 | InvalidBytesLengthException//API内部解码过程中出现的问题,正常情况下不会出现,可在DecodeError事件中接收到 92 | WebSocketCloseException//使用者主动关闭了该房间的连接 93 | WebSocketErrorException//被动关闭了该房间的连接,一般出现在正常连接后突然断网一段时间的情况下 94 | InvalidProtoVerException//输入的protoVer有误,只能是2或3 95 | DomainNameEncodingException//主机用户名编码异常,请检查主机用户名中是否有非ASCII字符 96 | SessdataExpireException//SESSDATA过期 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /TargetCmdAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BLiveAPI; 5 | 6 | /// 7 | /// 用来标记某个方法想要绑定哪些cmd对应的SMS消息事件 8 | /// 9 | [AttributeUsage(AttributeTargets.Method)] 10 | public class TargetCmdAttribute : Attribute 11 | { 12 | private readonly HashSet _targetCmdArray; 13 | 14 | /// 15 | public TargetCmdAttribute(params string[] cmdArray) 16 | { 17 | _targetCmdArray = new HashSet(cmdArray); 18 | } 19 | 20 | internal bool HasCmd(string cmd) 21 | { 22 | return _targetCmdArray.Contains(cmd); 23 | } 24 | } --------------------------------------------------------------------------------