|
1 | 1 | # ordinaryroad-bilibili-live |
2 | 2 |
|
| 3 | +  |
| 4 | + |
3 | 5 | 使用Netty来连接B站直播间的弹幕信息流Websocket接口 |
4 | 6 |
|
5 | 7 | - Feature 0: Netty |
6 | 8 | - Feature 1: 消息中的未知属性统一放到单独的MAP中 |
7 | 9 | - Feature 2: 支持房间短id |
8 | 10 |
|
9 | | -example请看`BilibiliBinaryFrameHandlerTest`测试类 |
| 11 | +### 1. 引入依赖 |
| 12 | + |
| 13 | +```xml |
| 14 | + |
| 15 | +<dependency> |
| 16 | + <groupId>tech.ordinaryroad.bilibili.live</groupId> |
| 17 | + <artifactId>ordinaryroad-bilibili-live</artifactId> |
| 18 | + <!-- 参考github release版本,不需要前缀`v` --> |
| 19 | + <version>${ordinaryroad-bilibili-live.version}</version> |
| 20 | +</dependency> |
| 21 | +``` |
| 22 | + |
| 23 | +### 2. 开始使用 |
| 24 | + |
| 25 | +> 参考`BilibiliBinaryFrameHandlerTest`测试类 |
| 26 | +
|
| 27 | +重写`IBilibiliSendSmsReplyMsgListener`中的方法,进行处理业务逻辑(耗时操作可能需要异步) |
| 28 | + |
| 29 | +### BilibiliBinaryFrameHandlerTest |
10 | 30 |
|
11 | 31 | 修改创建认证包方法的参数后,运行查看效果 |
12 | 32 |
|
13 | 33 | > 创建发送认证包 |
14 | 34 |  |
| 35 | + |
15 | 36 | > 控制台输出示例 |
16 | 37 |  |
| 38 | + |
17 | 39 | > 注:目前protover仅支持2(普通包正文使用zlib压缩) |
18 | 40 | > CmdEnum可能不全,需要根据控制台信息手动补(不影响运行) |
19 | 41 |
|
20 | | -### Bilibili协议编解码工具类`BilibiliCodecUtil` |
21 | | - |
22 | | -```java |
23 | | - |
24 | | -@Slf4j |
25 | | -public class BilibiliCodecUtil { |
26 | | - |
27 | | - public static final short FRAME_HEADER_LENGTH = 16; |
28 | | - |
29 | | - public static ByteBuf encode(BaseBilibiliMsg msg) { |
30 | | - ByteBuf out = Unpooled.buffer(100); |
31 | | - String bodyJsonString = msg.toString(); |
32 | | - // HeartbeatMsg不需要正文,如果序列化后得到`{}`,则替换为空字符串 |
33 | | - if ("{}".equals(bodyJsonString)) { |
34 | | - bodyJsonString = ""; |
35 | | - } |
36 | | - byte[] bodyBytes = bodyJsonString.getBytes(StandardCharsets.UTF_8); |
37 | | - int length = bodyBytes.length + FRAME_HEADER_LENGTH; |
38 | | - out.writeInt(length); |
39 | | - out.writeShort(FRAME_HEADER_LENGTH); |
40 | | - out.writeShort(msg.getProtoverEnum().getCode()); |
41 | | - out.writeInt(msg.getOperationEnum().getCode()); |
42 | | - out.writeInt(BaseBilibiliMsg.sequence++); |
43 | | - out.writeBytes(bodyBytes); |
44 | | - return out; |
45 | | - } |
46 | | - |
47 | | - public static List<BaseBilibiliMsg> decode(ByteBuf in) { |
48 | | - List<BaseBilibiliMsg> msgList = new ArrayList<>(); |
49 | | - Queue<ByteBuf> pendingByteBuf = new LinkedList<>(); |
50 | | - |
51 | | - do { |
52 | | - Optional<BaseBilibiliMsg> msg = doDecode(in, pendingByteBuf); |
53 | | - msg.ifPresent(msgList::add); |
54 | | - in = pendingByteBuf.poll(); |
55 | | - } while (in != null); |
56 | | - |
57 | | - return msgList; |
58 | | - } |
59 | | - |
60 | | - /** |
61 | | - * 执行解码操作,有压缩则先解压,解压后可能得到多条消息 |
62 | | - * |
63 | | - * @param in handler收到的一条消息 |
64 | | - * @param pendingByteBuf 用于存放未读取完的ByteBuf |
65 | | - * @return Optional<BaseBilibiliMsg> 何时为空值:不支持的{@link OperationEnum},不支持的{@link ProtoverEnum},{@link #parse(OperationEnum, String)}反序列化失败 |
66 | | - * @see OperationEnum |
67 | | - * @see ProtoverEnum |
68 | | - */ |
69 | | - private static Optional<BaseBilibiliMsg> doDecode(ByteBuf in, Queue<ByteBuf> pendingByteBuf) { |
70 | | - int length = in.readInt(); |
71 | | - short frameHeaderLength = in.readShort(); |
72 | | - short protoverCode = in.readShort(); |
73 | | - int operationCode = in.readInt(); |
74 | | - int sequence = in.readInt(); |
75 | | - int contentLength = length - frameHeaderLength; |
76 | | - byte[] inputBytes = new byte[contentLength]; |
77 | | - in.readBytes(inputBytes); |
78 | | - if (in.readableBytes() != 0) { |
79 | | - pendingByteBuf.offer(in); |
80 | | - } |
81 | | - |
82 | | - OperationEnum operationEnum = OperationEnum.getByCode(operationCode); |
83 | | - if (protoverCode == ProtoverEnum.NORMAL_ZLIB.getCode()) { |
84 | | - switch (operationEnum) { |
85 | | - case SEND_SMS_REPLY -> { |
86 | | - // Decompress the bytes |
87 | | - Inflater inflater = new Inflater(); |
88 | | - inflater.reset(); |
89 | | - inflater.setInput(inputBytes); |
90 | | - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(contentLength); |
91 | | - try { |
92 | | - byte[] bytes = new byte[1024]; |
93 | | - while (!inflater.finished()) { |
94 | | - int count = inflater.inflate(bytes); |
95 | | - byteArrayOutputStream.write(bytes, 0, count); |
96 | | - } |
97 | | - } catch (DataFormatException e) { |
98 | | - throw new RuntimeException(e); |
99 | | - } |
100 | | - inflater.end(); |
101 | | - |
102 | | - return doDecode(Unpooled.wrappedBuffer(byteArrayOutputStream.toByteArray()), pendingByteBuf); |
103 | | - } |
104 | | - case HEARTBEAT_REPLY -> { |
105 | | - BigInteger bigInteger = new BigInteger(inputBytes, 0, 4); |
106 | | - return parse(operationEnum, "{\"popularity\":%d}".formatted(bigInteger)); |
107 | | - } |
108 | | - default -> { |
109 | | - System.out.println("operationCode = " + operationCode); |
110 | | - String s = new String(inputBytes, StandardCharsets.UTF_8); |
111 | | - return parse(operationEnum, s); |
112 | | - } |
113 | | - } |
114 | | - } else if (protoverCode == ProtoverEnum.NORMAL_NO_COMPRESSION.getCode()) { |
115 | | - String s = new String(inputBytes, StandardCharsets.UTF_8); |
116 | | - return parse(operationEnum, s); |
117 | | - } else { |
118 | | - log.warn("暂不支持的版本:{}", protoverCode); |
119 | | - return Optional.empty(); |
120 | | - } |
121 | | - } |
122 | | - |
123 | | - public static Optional<BaseBilibiliMsg> parse(OperationEnum operation, String jsonString) { |
124 | | - switch (operation) { |
125 | | - case SEND_SMS_REPLY -> { |
126 | | - try { |
127 | | - return Optional.ofNullable(BaseBilibiliMsg.OBJECT_MAPPER.readValue(jsonString, SendSmsReplyMsg.class)); |
128 | | - } catch (JsonProcessingException e) { |
129 | | - throw new RuntimeException(e); |
130 | | - } |
131 | | - } |
132 | | - case AUTH_REPLY -> { |
133 | | - try { |
134 | | - return Optional.ofNullable(BaseBilibiliMsg.OBJECT_MAPPER.readValue(jsonString, AuthReplyMsg.class)); |
135 | | - } catch (JsonProcessingException e) { |
136 | | - throw new RuntimeException(e); |
137 | | - } |
138 | | - } |
139 | | - case HEARTBEAT_REPLY -> { |
140 | | - try { |
141 | | - return Optional.ofNullable(BaseBilibiliMsg.OBJECT_MAPPER.readValue(jsonString, HeartbeatReplyMsg.class)); |
142 | | - } catch (JsonProcessingException e) { |
143 | | - throw new RuntimeException(e); |
144 | | - } |
145 | | - } |
146 | | - default -> { |
147 | | - log.warn("暂不支持 {}", operation); |
148 | | - return Optional.empty(); |
149 | | - } |
150 | | - } |
151 | | - } |
152 | | - |
153 | | -} |
154 | | -``` |
155 | | - |
156 | | -### Bilibili信息流回掉接口`IBilibiliSendSmsReplyMsgListener` |
157 | | - |
158 | | -```java |
159 | | -public interface IBilibiliSendSmsReplyMsgListener { |
160 | | - |
161 | | - /** |
162 | | - * 收到弹幕 |
163 | | - * |
164 | | - * @param msg SendSmsReplyMsg |
165 | | - */ |
166 | | - void onDanmuMsg(SendSmsReplyMsg msg); |
167 | | - |
168 | | - /** |
169 | | - * 收到礼物 |
170 | | - * |
171 | | - * @param msg SendSmsReplyMsg |
172 | | - */ |
173 | | - void onSendGift(SendSmsReplyMsg msg); |
174 | | - |
175 | | - /** |
176 | | - * 普通用户进入直播间 |
177 | | - * |
178 | | - * @param msg SendSmsReplyMsg |
179 | | - */ |
180 | | - void onEnterRoom(SendSmsReplyMsg msg); |
181 | | - |
182 | | - /** |
183 | | - * 入场效果(高能用户) |
184 | | - * |
185 | | - * @param sendSmsReplyMsg SendSmsReplyMsg |
186 | | - */ |
187 | | - void onEntryEffect(SendSmsReplyMsg sendSmsReplyMsg); |
188 | | - |
189 | | - /** |
190 | | - * 观看人数变化 |
191 | | - * |
192 | | - * @param msg SendSmsReplyMsg |
193 | | - */ |
194 | | - void onWatchedChange(SendSmsReplyMsg msg); |
195 | | - |
196 | | - /** |
197 | | - * 为主播点赞 |
198 | | - * |
199 | | - * @param msg SendSmsReplyMsg |
200 | | - */ |
201 | | - void onClickLike(SendSmsReplyMsg msg); |
202 | | - |
203 | | - /** |
204 | | - * 点赞数更新 |
205 | | - * |
206 | | - * @param msg SendSmsReplyMsg |
207 | | - */ |
208 | | - void onClickUpdate(SendSmsReplyMsg msg); |
209 | | - |
210 | | - /** |
211 | | - * 其他消息 |
212 | | - * |
213 | | - * @param cmd CmdEnum |
214 | | - * @param msg SendSmsReplyMsg |
215 | | - */ |
216 | | - default void onOtherSendSmsReplyMsg(CmdEnum cmd, SendSmsReplyMsg msg) { |
217 | | - // ignore |
218 | | - } |
219 | | - |
220 | | - /** |
221 | | - * 未知cmd |
222 | | - * |
223 | | - * @param cmdString 实际收到的cmd字符串 |
224 | | - * @param msg SendSmsReplyMsg |
225 | | - */ |
226 | | - default void onUnknownCmd(String cmdString, SendSmsReplyMsg msg) { |
227 | | - // ignore |
228 | | - } |
229 | | -} |
230 | | - |
231 | | -``` |
232 | | - |
233 | 42 | ### 相关链接 |
234 | 43 |
|
235 | 44 | - [B站直播数据包分析连载(2018-12-11更新)_weixin_34009794的博客-CSDN博客](https://blog.csdn.net/weixin_34009794/article/details/88689474) |
|
0 commit comments