├── .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)
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 | }
--------------------------------------------------------------------------------