diff --git a/document/bullet_screen_stream_json/DANMU_MSG.json b/document/bullet_screen_stream_json/DANMU_MSG.json new file mode 100644 index 0000000..5c3f25c --- /dev/null +++ b/document/bullet_screen_stream_json/DANMU_MSG.json @@ -0,0 +1,43 @@ +{ + "info": [ + [ + 0, + 1, + 25, + 16777215, + 1510498713, + "1510498712", + 0, + "8a0f75dc", + 0 + ], + "网易云音乐库在当前直播间已停留0天0时39分41秒", + [ + 39042255, + "夏沫丶琉璃浅梦", + 0, + 1, + 0, + 10000, + 1 + ], + [ + 13, + "夏沫", + "乄夏沫丶", + "1547306", + 16746162, + "" + ], + [ + 41, + 0, + 16746162, + 6603 + ], + [], + 0, + 0 + ], + "cmd": "DANMU_MSG" +} diff --git a/document/bullet_screen_stream_json/DANMU_MSG_WITH_COMMENT.json b/document/bullet_screen_stream_json/DANMU_MSG_WITH_COMMENT.json new file mode 100644 index 0000000..9046e09 --- /dev/null +++ b/document/bullet_screen_stream_json/DANMU_MSG_WITH_COMMENT.json @@ -0,0 +1,68 @@ +{ + "info": [ + //弹幕基本属性 + [ + 0, + //pool + 1, + //mode + 25, + //fontSize + 16777215, + //color + 1510498713, + //弹幕发送时间 + "1510498712", + //用户进房时间(Android 客户端发送的弹幕, 这个值会是随机数) + 0, + "8a0f75dc", + 0 + ], + "网易云音乐库在当前直播间已停留0天0时39分41秒", + //发送者有关信息 + [ + 39042255, + //发送者 ID + "夏沫丶琉璃浅梦", + //发送者用户名 + 0, + //是否是管理员 + 1, + //是否是 VIP + 0, + //是否是 svip + 10000, + 1 + ], + //发送者的粉丝勋章有关信息(没有粉丝勋章的发送者, 这个 JsonArray 将是空的) + [ + 13, + //勋章等级 + "夏沫", + //勋章名称 + "乄夏沫丶", + //勋章主播的名字 + "1547306", + //勋章主播的直播间 + 16746162, + "" + ], + //用户经验有关信息 + [ + 41, + //发送者的观众等级 + 0, + 16746162, + 6603 + //排名 + ], + //用户头衔有关信息(里面有两个元素, 但是总是一样的, 不知道为什么) + [ + "title-131-1", + "title-131-1" + ], + 0, + 0 + ], + "cmd": "DANMU_MSG" +} diff --git a/document/bullet_screen_stream_json/LIVE.json b/document/bullet_screen_stream_json/LIVE.json new file mode 100644 index 0000000..55fe08f --- /dev/null +++ b/document/bullet_screen_stream_json/LIVE.json @@ -0,0 +1,4 @@ +{ + "cmd": "LIVE", + "roomid": 1110317 +} diff --git a/document/bullet_screen_stream_json/PREPARING.json b/document/bullet_screen_stream_json/PREPARING.json new file mode 100644 index 0000000..e5efee1 --- /dev/null +++ b/document/bullet_screen_stream_json/PREPARING.json @@ -0,0 +1,4 @@ +{ + "cmd": "PREPARING", + "roomid": 1110317 +} diff --git a/document/bullet_screen_stream_json/SEND_GIFT.json b/document/bullet_screen_stream_json/SEND_GIFT.json new file mode 100644 index 0000000..7330bc1 --- /dev/null +++ b/document/bullet_screen_stream_json/SEND_GIFT.json @@ -0,0 +1,80 @@ +{ + "cmd": "SEND_GIFT", + "data": { + "giftName": "\u4ebf\u5706", + "num": 1, + "uname": "\u30c5\u4ee3\u6211\u56de\u5bb6", + "rcost": 106855699, + "uid": 14146398, + "top_list": [ + { + "uid": 10952886, + "uname": "\u5b89\u4e36\u664b", + "coin": 498900, + "face": "http://static.hdslb.com/images/member/noface.gif", + "guard_level": 0, + "rank": 1, + "score": 498900 + }, + { + "uid": 13174983, + "uname": "-\u56db\u5b63\u8c46-", + "coin": 384300, + "face": "http://i0.hdslb.com/bfs/face/23f9f57a69378736f68b50fc4cac4f6b01683e97.jpg", + "guard_level": "3", + "rank": 2, + "score": 384300 + }, + { + "uid": 87444977, + "uname": "\u964c\u964c\u821e\u98ce", + "coin": 377700, + "face": "http://i2.hdslb.com/bfs/face/c06c835bf8ed6401535847bf21e78d4d3b89d402.jpg", + "guard_level": 0, + "rank": 3, + "score": 377700 + } + ], + "timestamp": 1510565032, + "giftId": 6, + "giftType": 0, + "action": "\u8d60\u9001", + "super": 0, + "price": 1000, + "rnd": "541907145", + "newMedal": 0, + "newTitle": 0, + "medal": [], + "title": "", + "beatId": "", + "biz_source": "live", + "metadata": "", + "remain": 0, + "gold": 0, + "silver": 0, + "eventScore": 0, + "eventNum": 0, + "smalltv_msg": [], + "specialGift": null, + "notice_msg": [], + "capsule": { + "normal": { + "coin": 6, + "change": 0, + "progress": { + "now": 4800, + "max": 10000 + } + }, + "colorful": { + "coin": 0, + "change": 0, + "progress": { + "now": 0, + "max": 5000 + } + } + }, + "addFollow": 0 + } +} diff --git a/document/bullet_screen_stream_json/SYS_GIFT.json b/document/bullet_screen_stream_json/SYS_GIFT.json new file mode 100644 index 0000000..ee2e9ac --- /dev/null +++ b/document/bullet_screen_stream_json/SYS_GIFT.json @@ -0,0 +1,11 @@ +{ + "cmd": "SYS_GIFT", + "msg": "sakamakiryoryo\u5728\u76f4\u64ad\u95f471084\u5f00\u542f\u4e86\u4e30\u6536\u796d\u5178\uff0c\u4e00\u8d77\u6765\u5206\u4eab\u6536\u83b7\u7684\u798f\u5229\u5427\uff01", + "msg_text": "sakamakiryoryo\u5728\u76f4\u64ad\u95f471084\u5f00\u542f\u4e86\u4e30\u6536\u796d\u5178\uff0c\u4e00\u8d77\u6765\u5206\u4eab\u6536\u83b7\u7684\u798f\u5229\u5427\uff01", + "tips": "sakamakiryoryo\u5728\u76f4\u64ad\u95f471084\u5f00\u542f\u4e86\u4e30\u6536\u796d\u5178\uff0c\u4e00\u8d77\u6765\u5206\u4eab\u6536\u83b7\u7684\u798f\u5229\u5427\uff01", + "url": "http:\/\/live.bilibili.com\/71084", + "roomid": 71084, + "real_roomid": 71084, + "giftId": 102, + "msgTips": 0 +} diff --git a/document/bullet_screen_stream_json/SYS_MSG.json b/document/bullet_screen_stream_json/SYS_MSG.json new file mode 100644 index 0000000..9bde5ee --- /dev/null +++ b/document/bullet_screen_stream_json/SYS_MSG.json @@ -0,0 +1,12 @@ +{ + "cmd": "SYS_MSG", + "msg": "\u3010\u5e7d\u5c0f\u591c\u5929\u5c0f\u52ab\u3011:?\u5728\u76f4\u64ad\u95f4:?\u3010392\u3011:?\u8d60\u9001 \u5c0f\u7535\u89c6\u4e00\u4e2a\uff0c\u8bf7\u524d\u5f80\u62bd\u5956", + "msg_text": "\u3010\u5e7d\u5c0f\u591c\u5929\u5c0f\u52ab\u3011:?\u5728\u76f4\u64ad\u95f4:?\u3010392\u3011:?\u8d60\u9001 \u5c0f\u7535\u89c6\u4e00\u4e2a\uff0c\u8bf7\u524d\u5f80\u62bd\u5956", + "rep": 1, + "styleType": 2, + "url": "http:\/\/live.bilibili.com\/392", + "roomid": 392, + "real_roomid": 71084, + "rnd": 44332151, + "tv_id": "29349" +} diff --git a/document/bullet_screen_stream_json/WELCOME.json b/document/bullet_screen_stream_json/WELCOME.json new file mode 100644 index 0000000..0ed3e89 --- /dev/null +++ b/document/bullet_screen_stream_json/WELCOME.json @@ -0,0 +1,10 @@ +{ + "cmd": "WELCOME", + "data": { + "uid": 18625858, + "uname": "\u662f\u767d\u8272\u70e4\u6f06", + "isadmin": 0, + "svip": 1 + }, + "roomid": 39189 +} diff --git a/document/bullet_screen_stream_json/WELCOME_GUARD.json b/document/bullet_screen_stream_json/WELCOME_GUARD.json new file mode 100644 index 0000000..473fe09 --- /dev/null +++ b/document/bullet_screen_stream_json/WELCOME_GUARD.json @@ -0,0 +1,10 @@ +{ + "cmd": "WELCOME_GUARD", + "data": { + "uid": 23598108, + "username": "lovevael", + "guard_level": 3, + "water_god": 0 + }, + "roomid": 43001 +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/entity/BulletScreenEntity.java b/src/main/java/com/hiczp/bilibili/api/live/entity/BulletScreenEntity.java index b05b9bb..7382ccd 100644 --- a/src/main/java/com/hiczp/bilibili/api/live/entity/BulletScreenEntity.java +++ b/src/main/java/com/hiczp/bilibili/api/live/entity/BulletScreenEntity.java @@ -12,6 +12,7 @@ public class BulletScreenEntity { @SerializedName("msg") private String message; + //在 web 端发送弹幕, 该字段是固定的, 为用户进入直播页面的时间的时间戳. 但是在 Android 端, 这是一个随机数 //该随机数不包括符号位有 9 位 @SerializedName("rnd") private long random = (long) (Math.random() * (999999999 - (-999999999)) + (-999999999)); diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenDispatcherRunnable.java b/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenDispatcherRunnable.java new file mode 100644 index 0000000..fcf2296 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenDispatcherRunnable.java @@ -0,0 +1,116 @@ +package com.hiczp.bilibili.api.live.socket; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.hiczp.bilibili.api.live.socket.entity.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.Arrays; +import java.util.function.Consumer; + +public class BulletScreenDispatcherRunnable implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(BulletScreenDispatcherRunnable.class); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final JsonParser JSON_PARSER = new JsonParser(); + private SocketChannel socketChannel; + private LiveClient liveClient; + + public BulletScreenDispatcherRunnable(SocketChannel socketChannel, LiveClient liveClient) { + this.socketChannel = socketChannel; + this.liveClient = liveClient; + } + + private void invokeCallback(Consumer consumer) { + Utils.invokeCallback(liveClient.getBulletScreenListeners(), consumer); + } + + @Override + public void run() { + while (true) { + try { + ByteBuffer[] byteBuffers = PackageRepository.readNextPackageSplit(socketChannel); + //如果没有回调则不解析数据包, 直接开始接收下一个数据包 + if (liveClient.getBulletScreenListeners().size() == 0) { + continue; + } + + //判断数据包类型 + byte[] packageTypeBytes = byteBuffers[3].array(); + Consumer consumer = null; + if (Arrays.equals(packageTypeBytes, PackageRepository.DATA_PACKAGE_TYPE_BYTES)) { //弹幕 + String json = new String(byteBuffers[5].array()); + JsonObject jsonObject = JSON_PARSER.parse(json).getAsJsonObject(); + String cmd = jsonObject.get("cmd").getAsString(); + //判断 cmd + switch (cmd) { + case "DANMU_MSG": { + consumer = bulletScreenListener -> bulletScreenListener.onDanMuMSGPackage(GSON.fromJson(json, DanMuMSGEntity.class)); + } + break; + case "SEND_GIFT": { + consumer = bulletScreenListener -> bulletScreenListener.onSendGiftPackage(GSON.fromJson(json, SendGiftEntity.class)); + } + break; + case "SYS_MSG": { + consumer = bulletScreenListener -> bulletScreenListener.onSysMSGPackage(GSON.fromJson(json, SysMSGEntity.class)); + } + break; + case "SYS_GIFT": { + consumer = bulletScreenListener -> bulletScreenListener.onSysGiftPackage(GSON.fromJson(json, SysGiftEntity.class)); + } + break; + case "WELCOME": { + consumer = bulletScreenListener -> bulletScreenListener.onWelcomePackage(GSON.fromJson(json, WelcomeEntity.class)); + } + break; + case "WELCOME_GUARD": { + consumer = bulletScreenListener -> bulletScreenListener.onWelcomeGuardPackage(GSON.fromJson(json, WelcomeGuardEntity.class)); + } + break; + case "LIVE": { + consumer = bulletScreenListener -> bulletScreenListener.onLivePackage(GSON.fromJson(json, LiveEntity.class)); + } + break; + case "PREPARING": { + consumer = bulletScreenListener -> bulletScreenListener.onPreparingPackage(GSON.fromJson(json, PreparingEntity.class)); + } + break; + default: { //未知的 cmd + LOGGER.error("Unknown json below: "); + GSON.toJson(jsonObject, System.out); + System.out.println(); + } + } + } else if (Arrays.equals(packageTypeBytes, PackageRepository.VIEWER_COUNT_PACKAGE_TYPE_BYTES)) { //在线人数 + consumer = bulletScreenListener -> bulletScreenListener.onViewerCountPackage(byteBuffers[5].getInt()); + } else { //未知类型 + ByteBuffer byteBuffer = ByteBuffer.allocate(Arrays.stream(byteBuffers).mapToInt(ByteBuffer::limit).sum()); + Arrays.stream(byteBuffers).forEach(byteBuffer::put); + byte[] bytes = byteBuffer.array(); + LOGGER.error("Unknown package below: "); + Utils.printBytes(bytes); + consumer = bulletScreenListener -> bulletScreenListener.onUnknownPackage(bytes); + } + + //执行回调 + invokeCallback(consumer); + } catch (IOException e) { + LOGGER.debug("Connection closed, BulletScreenDispatcherThread prepare to exit"); + try { + liveClient.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + //调用 onDisconnect 回调 + invokeCallback(BulletScreenListener::onDisconnect); + break; + } + } + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenListener.java b/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenListener.java new file mode 100644 index 0000000..8b21056 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenListener.java @@ -0,0 +1,29 @@ +package com.hiczp.bilibili.api.live.socket; + +import com.hiczp.bilibili.api.live.socket.entity.*; + +public interface BulletScreenListener { + void onConnect(); + + void onDisconnect(); + + void onViewerCountPackage(int viewerCount); + + void onDanMuMSGPackage(DanMuMSGEntity danMuMSGEntity); + + void onSendGiftPackage(SendGiftEntity sendGiftEntity); + + void onSysMSGPackage(SysMSGEntity sysMSGEntity); + + void onSysGiftPackage(SysGiftEntity sysGiftEntity); + + void onWelcomePackage(WelcomeEntity welcomeEntity); + + void onWelcomeGuardPackage(WelcomeGuardEntity welcomeGuardEntity); + + void onLivePackage(LiveEntity liveEntity); + + void onPreparingPackage(PreparingEntity preparingEntity); + + void onUnknownPackage(byte[] raw); +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenListenerAdaptor.java b/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenListenerAdaptor.java new file mode 100644 index 0000000..9ce77d0 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/BulletScreenListenerAdaptor.java @@ -0,0 +1,65 @@ +package com.hiczp.bilibili.api.live.socket; + +import com.hiczp.bilibili.api.live.socket.entity.*; + +public class BulletScreenListenerAdaptor implements BulletScreenListener { + @Override + public void onConnect() { + + } + + @Override + public void onDisconnect() { + + } + + @Override + public void onViewerCountPackage(int viewerCount) { + + } + + @Override + public void onDanMuMSGPackage(DanMuMSGEntity danMuMSGEntity) { + + } + + @Override + public void onSendGiftPackage(SendGiftEntity sendGiftEntity) { + + } + + @Override + public void onSysMSGPackage(SysMSGEntity sysMSGEntity) { + + } + + @Override + public void onSysGiftPackage(SysGiftEntity sysGiftEntity) { + + } + + @Override + public void onWelcomePackage(WelcomeEntity welcomeEntity) { + + } + + @Override + public void onWelcomeGuardPackage(WelcomeGuardEntity welcomeGuardEntity) { + + } + + @Override + public void onLivePackage(LiveEntity liveEntity) { + + } + + @Override + public void onPreparingPackage(PreparingEntity preparingEntity) { + + } + + @Override + public void onUnknownPackage(byte[] raw) { + + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/LiveClient.java b/src/main/java/com/hiczp/bilibili/api/live/socket/LiveClient.java new file mode 100644 index 0000000..48f9f0d --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/LiveClient.java @@ -0,0 +1,138 @@ +package com.hiczp.bilibili.api.live.socket; + +import com.hiczp.bilibili.api.BilibiliRESTAPI; +import com.hiczp.bilibili.api.live.LiveService; +import com.hiczp.bilibili.api.live.entity.LiveRoomInfoEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.nio.channels.SocketChannel; +import java.util.Timer; +import java.util.TimerTask; +import java.util.Vector; + +public class LiveClient implements Closeable { + private static final Logger LOGGER = LoggerFactory.getLogger(LiveClient.class); + + private int showRoomId; + private int roomId; + private int userId = BilibiliRESTAPI.getMid(); + private LiveService liveService = BilibiliRESTAPI.getLiveService(); + private Vector bulletScreenListeners = new Vector<>(); + private LiveRoomInfoEntity.LiveRoomEntity liveRoomEntity; + private SocketChannel socketChannel; + private Thread bulletScreenDispatcherThread; + private Timer heartBeatTimer; + + public LiveClient(int showRoomId) { + this.showRoomId = showRoomId; + } + + //如果不传入 userId, 将使用默认值 0 + public LiveClient(int showRoomId, int userId) { + this.showRoomId = showRoomId; + this.userId = userId; + } + + public LiveClient addListener(BulletScreenListener bulletScreenListener) { + bulletScreenListeners.add(bulletScreenListener); + return this; + } + + public LiveClient removeListener(BulletScreenListener bulletScreenListener) { + bulletScreenListeners.remove(bulletScreenListener); + return this; + } + + public LiveClient connect() throws IOException { + LOGGER.info("Entering live room {} with uid {}", showRoomId, userId); + //获取直播间信息 + LiveRoomInfoEntity liveRoomInfoEntity = liveService.getRoomInfo(showRoomId).execute().body(); + if (liveRoomInfoEntity.getCode() != 0) { + LOGGER.error("Can't get live room info"); + throw new IOException(liveRoomInfoEntity.getMessage()); + } + liveRoomEntity = liveRoomInfoEntity.getData(); + roomId = liveRoomEntity.getRoomId(); + LOGGER.debug("Real room id: {}", roomId); + + //socket 连接 + String address = liveRoomEntity.getCmt(); + int port = liveRoomEntity.getCmtPortGoim(); + LOGGER.info("Connect to {}:{}", address, port); + socketChannel = SocketChannel.open(new InetSocketAddress(address, port)); + //发送进房包 + socketChannel.write(PackageRepository.createEnterRoomPackage(roomId, userId)); + //验证下一个数据包是否是进房成功数据包 + if (PackageRepository.isNextPackageIsEnterRoomSuccessPackage(socketChannel)) { + LOGGER.info("Socket connection success"); + //调用 onConnect 回调 + Utils.invokeCallback(bulletScreenListeners, BulletScreenListener::onConnect); + } else { + LOGGER.error("Socket connection failed"); + socketChannel.close(); + throw new SocketException("Can't connection to Bullet Screen server"); + } + + //启动回调分发线程 + bulletScreenDispatcherThread = new Thread(new BulletScreenDispatcherRunnable(socketChannel, this)); + bulletScreenDispatcherThread.setName("BulletScreenDispatcherThread"); + bulletScreenDispatcherThread.start(); + LOGGER.debug("BulletScreenDispatcherThread started"); + + //启动心跳包线程 + heartBeatTimer = new Timer("LiveHeartBeatThread"); + heartBeatTimer.schedule(new TimerTask() { + private final Logger logger = LoggerFactory.getLogger(TimerTask.class); + + @Override + public void run() { + try { + socketChannel.socket().getOutputStream().write(PackageRepository.createHeartBeatPackage(roomId, userId).array()); + logger.debug("Send heart beat package success"); + } catch (IOException e) { + logger.debug("Connection closed, cancel HeartBeatTimerTask"); + cancel(); + try { + close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + }, 0, 30 * 1000); + LOGGER.debug("HeatBeatTimer started"); + + return this; + } + + @Override + public synchronized void close() throws IOException { + if (heartBeatTimer != null) { + heartBeatTimer.cancel(); + LOGGER.debug("HeartBeatTimer canceled"); + heartBeatTimer = null; + } + if (socketChannel != null && socketChannel.isConnected()) { + socketChannel.close(); + LOGGER.debug("Socket closed"); + socketChannel = null; + } + } + + public Vector getBulletScreenListeners() { + return bulletScreenListeners; + } + + public void setBulletScreenListeners(Vector bulletScreenListeners) { + this.bulletScreenListeners = bulletScreenListeners; + } + + public LiveRoomInfoEntity.LiveRoomEntity getLiveRoomEntity() { + return liveRoomEntity; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/PackageRepository.java b/src/main/java/com/hiczp/bilibili/api/live/socket/PackageRepository.java new file mode 100644 index 0000000..9d1981a --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/PackageRepository.java @@ -0,0 +1,143 @@ +package com.hiczp.bilibili.api.live.socket; + +import com.google.gson.Gson; +import com.hiczp.bilibili.api.live.socket.entity.EnterRoomEntity; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.Arrays; + +//数据包结构说明 +//00 00 00 28 00 10 00 00 00 00 00 07 00 00 00 00 +//00 00 00 28/00 10/00 00 00 00 00 07/00 00 00 00 +//1-4 字节: 数据包长度 +//5-6 字节: 协议头长度, 固定值 0x10 +//7-8 字节: 设备类型, Android 固定为 0 +//9-12 字节: 数据包类型 +//13-16 字节: 设备类型, 同 7-8 字节 +public class PackageRepository { + //数据包总长标识占用的 bytes 长度 + static final short PACKAGE_LENGTH_BYTES_LENGTH = 4; + //协议头长度标识占用的 bytes 长度 + static final short PROTOCOL_HEAD_LENGTH_BYTES_LENGTH = 2; + //设备类型短标识 bytes + static final byte[] SHORT_DEVICE_TYPE_BYTES = {0x00, 0x00}; + //数据包类型标识 bytes + static final short PACKAGE_TYPE_BYTES_LENGTH = 4; + static final byte[] HEART_BEAT_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x02}; //心跳包 + static final byte[] VIEWER_COUNT_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x03}; //观众人数 + static final byte[] DATA_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x05}; //弹幕, 礼物, 系统消息 etc + static final byte[] ENTER_ROOM_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x07}; //进入房间 + static final byte[] ENTER_ROOM_SUCCESS_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x08}; //进入房间的响应包 + //设备类型长标识 bytes + static final byte[] LONG_DEVICE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x00}; + //协议头总长度 + static final short PROTOCOL_HEAD_LENGTH = (short) (PACKAGE_LENGTH_BYTES_LENGTH + PROTOCOL_HEAD_LENGTH_BYTES_LENGTH + SHORT_DEVICE_TYPE_BYTES.length + PACKAGE_TYPE_BYTES_LENGTH + LONG_DEVICE_TYPE_BYTES.length); + //协议头长度标识 bytes + static final byte[] PROTOCOL_HEAD_LENGTH_BYTES = ByteBuffer.allocate(PROTOCOL_HEAD_LENGTH_BYTES_LENGTH).putShort(PROTOCOL_HEAD_LENGTH).array(); + private static final Gson GSON = new Gson(); + + private static ByteBuffer createPackage(byte[] packageTypeBytes, String content) { + int totalLength = PROTOCOL_HEAD_LENGTH + content.length(); + ByteBuffer byteBuffer = ByteBuffer.allocate(totalLength) + .putInt(totalLength) + .put(PROTOCOL_HEAD_LENGTH_BYTES) + .put(SHORT_DEVICE_TYPE_BYTES) + .put(packageTypeBytes) + .put(LONG_DEVICE_TYPE_BYTES) + .put(content.getBytes()); + byteBuffer.flip(); + return byteBuffer; + } + + private static void readDataFromSocketChannel(SocketChannel socketChannel, ByteBuffer byteBuffer) throws IOException { + while (byteBuffer.hasRemaining()) { + socketChannel.read(byteBuffer); + } + } + + //userId 为 0 表示用户未登录, 并不影响弹幕推送, 但是可能和用户统计有关 + public static ByteBuffer createEnterRoomPackage(int roomId, int userId) { + return createPackage(ENTER_ROOM_PACKAGE_TYPE_BYTES, GSON.toJson(new EnterRoomEntity(roomId, userId))); + } + + public static ByteBuffer createHeartBeatPackage(int roomId, int userId) { + return createPackage(HEART_BEAT_PACKAGE_TYPE_BYTES, GSON.toJson(new EnterRoomEntity(roomId, userId))); + } + + public static ByteBuffer readNextPackage(SocketChannel socketChannel) throws IOException { + //获取数据包总长度 + ByteBuffer packageLengthByteBuffer = ByteBuffer.allocate(PACKAGE_LENGTH_BYTES_LENGTH); + readDataFromSocketChannel(socketChannel, packageLengthByteBuffer); + packageLengthByteBuffer.flip(); + int packageLength = packageLengthByteBuffer.getInt(); + packageLengthByteBuffer.rewind(); + + //获取数据包剩下的部分 + ByteBuffer restPackageByteBuffer = ByteBuffer.allocate(packageLength - PACKAGE_LENGTH_BYTES_LENGTH); + readDataFromSocketChannel(socketChannel, restPackageByteBuffer); + restPackageByteBuffer.flip(); + + //合并 ByteBuffer + ByteBuffer byteBuffer = ByteBuffer.allocate(packageLengthByteBuffer.limit() + restPackageByteBuffer.limit()); + byteBuffer.put(packageLengthByteBuffer).put(restPackageByteBuffer); + byteBuffer.flip(); + return byteBuffer; + } + + public static ByteBuffer[] readNextPackageSplit(SocketChannel socketChannel) throws IOException { + //获取数据包总长度 + ByteBuffer packageLengthByteBuffer = ByteBuffer.allocate(PACKAGE_LENGTH_BYTES_LENGTH); + readDataFromSocketChannel(socketChannel, packageLengthByteBuffer); + packageLengthByteBuffer.flip(); + int packageLength = packageLengthByteBuffer.getInt(); + packageLengthByteBuffer.rewind(); + + //获取协议头长度 + ByteBuffer protocolHeadLengthByteBuffer = ByteBuffer.allocate(PROTOCOL_HEAD_LENGTH_BYTES_LENGTH); + readDataFromSocketChannel(socketChannel, protocolHeadLengthByteBuffer); + protocolHeadLengthByteBuffer.flip(); + int protocolHeadLength = protocolHeadLengthByteBuffer.getShort(); + protocolHeadLengthByteBuffer.rewind(); + + //获取剩余的协议头 + ByteBuffer restProtocolHeadByteBuffer = ByteBuffer.allocate(protocolHeadLength - PACKAGE_LENGTH_BYTES_LENGTH - PROTOCOL_HEAD_LENGTH_BYTES_LENGTH); + readDataFromSocketChannel(socketChannel, restProtocolHeadByteBuffer); + restProtocolHeadByteBuffer.flip(); + + //得到设备类型短标识 + ByteBuffer shortDeviceTypeByteBuffer = ByteBuffer.allocate(SHORT_DEVICE_TYPE_BYTES.length); + shortDeviceTypeByteBuffer.putShort(restProtocolHeadByteBuffer.getShort()); + shortDeviceTypeByteBuffer.flip(); + + //得到数据包类型 + ByteBuffer packageTypeByteBuffer = ByteBuffer.allocate(PACKAGE_TYPE_BYTES_LENGTH); + packageTypeByteBuffer.putInt(restProtocolHeadByteBuffer.getInt()); + packageTypeByteBuffer.flip(); + + //得到设备类型长标识 + ByteBuffer longDeviceTypeByteBuffer = ByteBuffer.allocate(LONG_DEVICE_TYPE_BYTES.length); + longDeviceTypeByteBuffer.putInt(restProtocolHeadByteBuffer.getInt()); + longDeviceTypeByteBuffer.flip(); + + //获取正文 + ByteBuffer contentByteBuffer = ByteBuffer.allocate(packageLength - protocolHeadLength); + readDataFromSocketChannel(socketChannel, contentByteBuffer); + contentByteBuffer.flip(); + + //组成数组 + return new ByteBuffer[]{ + packageLengthByteBuffer, //0 + protocolHeadLengthByteBuffer, //1 + shortDeviceTypeByteBuffer, //2 + packageTypeByteBuffer, //3 + longDeviceTypeByteBuffer, //4 + contentByteBuffer //5 + }; + } + + public static boolean isNextPackageIsEnterRoomSuccessPackage(SocketChannel socketChannel) throws IOException { + return Arrays.equals(readNextPackageSplit(socketChannel)[3].array(), ENTER_ROOM_SUCCESS_PACKAGE_TYPE_BYTES); + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/Utils.java b/src/main/java/com/hiczp/bilibili/api/live/socket/Utils.java new file mode 100644 index 0000000..30a2d72 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/Utils.java @@ -0,0 +1,70 @@ +package com.hiczp.bilibili.api.live.socket; + +import com.sun.istack.internal.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class Utils { + private static byte[][] splitBytes(byte[] bytes, int n) { + int lineCount = bytes.length % n == 0 ? bytes.length / n : bytes.length / n + 1; + byte[][] result = new byte[lineCount][]; + int to; + for (int line = 1; line <= lineCount; line++) { + if (line != lineCount) { + to = line * n; + } else { + to = bytes.length; + } + result[line - 1] = Arrays.copyOfRange(bytes, (line - 1) * n, to); + } + return result; + } + + static void invokeCallback(List bulletScreenListeners, @Nullable Consumer consumer) { + if (consumer != null) { + for (int i = bulletScreenListeners.size() - 1; i >= 0; i--) { + try { + consumer.accept(bulletScreenListeners.get(i)); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + public static void printBytes(byte[] bytes) { + byte[][] data = splitBytes(bytes, 16); + byte[] currentRow; + char c; + for (int i = 0; i < data.length; i++) { + System.out.printf("%08x ", i * 16); + currentRow = data[i]; + for (int j = 0; j < currentRow.length; j++) { + System.out.printf("%02x ", currentRow[j]); + if (j == 7) { + System.out.print(" "); + } + } + if (currentRow.length < 16) { + for (int k = 0; k < (48 - currentRow.length * 2 - (currentRow.length - 1)); k++) { + System.out.print(" "); + } + } + System.out.print(" "); + for (int j = 0; j < currentRow.length; j++) { + if (currentRow[j] < ' ') { + c = '.'; + } else { + c = (char) currentRow[j]; + } + System.out.print(c); + if (j == 7) { + System.out.print(" "); + } + } + System.out.println(); + } + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/DanMuMSGEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/DanMuMSGEntity.java new file mode 100644 index 0000000..8d72ff2 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/DanMuMSGEntity.java @@ -0,0 +1,154 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; + +public class DanMuMSGEntity { + private static final Gson GSON = new Gson(); + private static final Type LIST_STRING_TYPE = new TypeToken>() { + }.getType(); + + /** + * info : [[0,1,25,16777215,1510498713,"1510498712",0,"8a0f75dc",0],"网易云音乐库在当前直播间已停留0天0时39分41秒",[39042255,"夏沫丶琉璃浅梦",0,1,0,10000,1],[13,"夏沫","乄夏沫丶","1547306",16746162,""],[41,0,16746162,6603],[],0,0] + * cmd : DANMU_MSG + */ + + @SerializedName("cmd") + private String cmd; + @SerializedName("info") + private JsonArray info; + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public JsonArray getInfo() { + return info; + } + + public void setInfo(JsonArray info) { + this.info = info; + } + + //pool 发布的弹幕池 (0 普通 1 字幕 2 特殊) + public int getPool() { + return info.get(0).getAsJsonArray().get(0).getAsInt(); + } + + //mode 弹幕的模式 (1 普通 4 底端 5 顶端 6 逆向 7 特殊 9 高级) + public int getMode() { + return info.get(0).getAsJsonArray().get(1).getAsInt(); + } + + //fontSize 字体大小 + public int getFontSize() { + return info.get(0).getAsJsonArray().get(2).getAsInt(); + } + + //color 字体颜色 + public int getColor() { + return info.get(0).getAsJsonArray().get(3).getAsInt(); + } + + //弹幕发送时间(Unix 时间戳)(其实是服务器接收到弹幕的时间) + public long getSendTime() { + return info.get(0).getAsJsonArray().get(4).getAsInt(); + } + + //用户进入房间的时间(Unix 时间戳)(但是 Android 发送的弹幕, 这个值会是随机数) + public String getUserEnterTime() { + return info.get(0).getAsJsonArray().get(5).getAsString(); + } + + //得到弹幕内容 + public String getMessage() { + return info.get(1).getAsString(); + } + + //得到发送者的用户 ID + public int getUserId() { + return info.get(2).getAsJsonArray().get(0).getAsInt(); + } + + //得到发送者的用户名 + public String getUsername() { + return info.get(2).getAsJsonArray().get(1).getAsString(); + } + + //发送者是否是管理员 + public boolean isAdmin() { + return info.get(2).getAsJsonArray().get(2).getAsBoolean(); + } + + //发送者是否是 VIP + public boolean isVip() { + return info.get(2).getAsJsonArray().get(3).getAsBoolean(); + } + + //发送者是否是 SVip + public boolean isSVip() { + return info.get(2).getAsJsonArray().get(4).getAsBoolean(); + } + + //表示粉丝勋章有关信息的 JsonArray 可能是空的 + //获取粉丝勋章等级 + public Optional getFansMedalLevel() { + if (info.get(3).getAsJsonArray().size() > 0) { + return Optional.of(info.get(3).getAsJsonArray().get(0).getAsInt()); + } else { + return Optional.empty(); + } + } + + //获取粉丝勋章名称 + public Optional getFansMedalName() { + if (info.get(3).getAsJsonArray().size() > 0) { + return Optional.of(info.get(3).getAsJsonArray().get(1).getAsString()); + } else { + return Optional.empty(); + } + } + + //获取粉丝勋章对应的主播的名字 + public Optional getFansMedalOwnerName() { + if (info.get(3).getAsJsonArray().size() > 0) { + return Optional.of(info.get(3).getAsJsonArray().get(2).getAsString()); + } else { + return Optional.empty(); + } + } + + //获取粉丝勋章对应的主播的直播间 ID + public Optional getFansMedalOwnerRoomId() { + if (info.get(3).getAsJsonArray().size() > 0) { + return Optional.of(info.get(3).getAsJsonArray().get(3).getAsString()); + } else { + return Optional.empty(); + } + } + + //获得用户的观众等级 + public int getUserLevel() { + return info.get(4).getAsJsonArray().get(0).getAsInt(); + } + + //获得用户的观众等级排名 + public String getUserRank() { + return info.get(4).getAsJsonArray().get(3).getAsString(); + } + + //获得用户头衔 + public List getUserTitles() { + return GSON.fromJson(info.get(5), LIST_STRING_TYPE); + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/EnterRoomEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/EnterRoomEntity.java new file mode 100644 index 0000000..6215be6 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/EnterRoomEntity.java @@ -0,0 +1,31 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.annotations.SerializedName; + +public class EnterRoomEntity { + @SerializedName("roomid") + private int roomId; + @SerializedName("uid") + private int userId; + + public EnterRoomEntity(int roomId, int userId) { + this.roomId = roomId; + this.userId = userId; + } + + public int getRoomId() { + return roomId; + } + + public void setRoomId(int roomId) { + this.roomId = roomId; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/LiveEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/LiveEntity.java new file mode 100644 index 0000000..2126475 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/LiveEntity.java @@ -0,0 +1,31 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.annotations.SerializedName; + +public class LiveEntity { + /** + * cmd : LIVE + * roomid : 1110317 + */ + + @SerializedName("cmd") + private String cmd; + @SerializedName("roomid") + private String roomId; + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public String getRoomId() { + return roomId; + } + + public void setRoomId(String roomId) { + this.roomId = roomId; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/PreparingEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/PreparingEntity.java new file mode 100644 index 0000000..26833f9 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/PreparingEntity.java @@ -0,0 +1,31 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.annotations.SerializedName; + +public class PreparingEntity { + /** + * cmd : PREPARING + * roomid : 1110317 + */ + + @SerializedName("cmd") + private String cmd; + @SerializedName("roomid") + private String roomId; + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public String getRoomId() { + return roomId; + } + + public void setRoomId(String roomId) { + this.roomId = roomId; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SendGiftEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SendGiftEntity.java new file mode 100644 index 0000000..75ab111 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SendGiftEntity.java @@ -0,0 +1,605 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class SendGiftEntity { + /** + * cmd : SEND_GIFT + * data : {"giftName":"辣条","num":64,"uname":"明暗纠结星","rcost":66347035,"uid":12768615,"top_list":[{"uid":9256,"userName":"SSR丶尧","coin":2905900,"face":"http://i0.hdslb.com/bfs/face/eba52abb1daaf3aecd7b986b9731451872d66613.jpg","guard_level":"3","rank":1,"score":2905900},{"uid":4986301,"userName":"乄夏沫丶","coin":1386000,"face":"http://i2.hdslb.com/bfs/face/b3969027a263d2610711317addf437fe59a9b97e.jpg","guard_level":0,"rank":2,"score":1386000},{"uid":5211302,"userName":"朝雾怜","coin":805700,"face":"http://i1.hdslb.com/bfs/face/d366be69d716469514d355642aa324ceba3fa122.jpg","guard_level":0,"rank":3,"score":805700}],"timestamp":1510498496,"giftId":1,"giftType":0,"action":"喂食","super":0,"price":100,"rnd":"1510498460","newMedal":0,"newTitle":0,"medal":[],"title":"","beatId":"0","biz_source":"live","metadata":"","remain":0,"gold":0,"silver":0,"eventScore":0,"eventNum":0,"smalltv_msg":[],"notice_msg":[],"capsule":{"normal":{"coin":13,"change":1,"progress":{"now":4000,"max":10000}},"colorful":{"coin":0,"change":0,"progress":{"now":0,"max":5000}}},"addFollow":0} + */ + + @SerializedName("cmd") + private String cmd; + @SerializedName("data") + private DataEntity data; + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public DataEntity getData() { + return data; + } + + public void setData(DataEntity data) { + this.data = data; + } + + public static class DataEntity { + /** + * giftName : 辣条 + * num : 64 + * uname : 明暗纠结星 + * rcost : 66347035 + * uid : 12768615 + * top_list : [{"uid":9256,"uname":"SSR丶尧","coin":2905900,"face":"http://i0.hdslb.com/bfs/face/eba52abb1daaf3aecd7b986b9731451872d66613.jpg","guard_level":"3","rank":1,"score":2905900},{"uid":4986301,"userName":"乄夏沫丶","coin":1386000,"face":"http://i2.hdslb.com/bfs/face/b3969027a263d2610711317addf437fe59a9b97e.jpg","guard_level":0,"rank":2,"score":1386000},{"uid":5211302,"userName":"朝雾怜","coin":805700,"face":"http://i1.hdslb.com/bfs/face/d366be69d716469514d355642aa324ceba3fa122.jpg","guard_level":0,"rank":3,"score":805700}] + * timestamp : 1510498496 + * giftId : 1 + * giftType : 0 + * action : 喂食 + * super : 0 + * price : 100 + * rnd : 1510498460 + * newMedal : 0 + * newTitle : 0 + * medal : [] + * title : + * beatId : 0 + * biz_source : live + * metadata : + * remain : 0 + * gold : 0 + * silver : 0 + * eventScore : 0 + * eventNum : 0 + * smalltv_msg : [] + * notice_msg : [] + * capsule : {"normal":{"coin":13,"change":1,"progress":{"now":4000,"max":10000}},"colorful":{"coin":0,"change":0,"progress":{"now":0,"max":5000}}} + * addFollow : 0 + */ + + @SerializedName("giftName") + private String giftName; + @SerializedName("num") + private int num; + @SerializedName("uname") + private String userName; + @SerializedName("rcost") + private int rCost; + @SerializedName("uid") + private int uid; + @SerializedName("timestamp") + private int timestamp; + @SerializedName("giftId") + private int giftId; + @SerializedName("giftType") + private int giftType; + @SerializedName("action") + private String action; + @SerializedName("super") + private int superX; + @SerializedName("price") + private int price; + @SerializedName("rnd") + private String rnd; + @SerializedName("newMedal") + private int newMedal; + @SerializedName("newTitle") + private int newTitle; + @SerializedName("title") + private String title; + @SerializedName("beatId") + private String beatId; + @SerializedName("biz_source") + private String bizSource; + @SerializedName("metadata") + private String metadata; + @SerializedName("remain") + private int remain; + @SerializedName("gold") + private int gold; + @SerializedName("silver") + private int silver; + @SerializedName("eventScore") + private int eventScore; + @SerializedName("eventNum") + private int eventNum; + @SerializedName("capsule") + private CapsuleEntity capsule; + @SerializedName("addFollow") + private int addFollow; + @SerializedName("top_list") + private List topList; + @SerializedName("medal") + private JsonElement medal; + @SerializedName("smalltv_msg") + private JsonElement smallTVMsg; + @SerializedName("notice_msg") + private List noticeMsg; + + public String getGiftName() { + return giftName; + } + + public void setGiftName(String giftName) { + this.giftName = giftName; + } + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public int getrCost() { + return rCost; + } + + public void setrCost(int rCost) { + this.rCost = rCost; + } + + public int getUid() { + return uid; + } + + public void setUid(int uid) { + this.uid = uid; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public int getGiftId() { + return giftId; + } + + public void setGiftId(int giftId) { + this.giftId = giftId; + } + + public int getGiftType() { + return giftType; + } + + public void setGiftType(int giftType) { + this.giftType = giftType; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public int getSuperX() { + return superX; + } + + public void setSuperX(int superX) { + this.superX = superX; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + public String getRnd() { + return rnd; + } + + public void setRnd(String rnd) { + this.rnd = rnd; + } + + public int getNewMedal() { + return newMedal; + } + + public void setNewMedal(int newMedal) { + this.newMedal = newMedal; + } + + public int getNewTitle() { + return newTitle; + } + + public void setNewTitle(int newTitle) { + this.newTitle = newTitle; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBeatId() { + return beatId; + } + + public void setBeatId(String beatId) { + this.beatId = beatId; + } + + public String getBizSource() { + return bizSource; + } + + public void setBizSource(String bizSource) { + this.bizSource = bizSource; + } + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } + + public int getRemain() { + return remain; + } + + public void setRemain(int remain) { + this.remain = remain; + } + + public int getGold() { + return gold; + } + + public void setGold(int gold) { + this.gold = gold; + } + + public int getSilver() { + return silver; + } + + public void setSilver(int silver) { + this.silver = silver; + } + + public int getEventScore() { + return eventScore; + } + + public void setEventScore(int eventScore) { + this.eventScore = eventScore; + } + + public int getEventNum() { + return eventNum; + } + + public void setEventNum(int eventNum) { + this.eventNum = eventNum; + } + + public CapsuleEntity getCapsule() { + return capsule; + } + + public void setCapsule(CapsuleEntity capsule) { + this.capsule = capsule; + } + + public int getAddFollow() { + return addFollow; + } + + public void setAddFollow(int addFollow) { + this.addFollow = addFollow; + } + + public List getTopList() { + return topList; + } + + public void setTopList(List topList) { + this.topList = topList; + } + + public JsonElement getMedal() { + return medal; + } + + public void setMedal(JsonElement medal) { + this.medal = medal; + } + + public JsonElement getSmallTVMsg() { + return smallTVMsg; + } + + public void setSmallTVMsg(JsonObject smallTVMsg) { + this.smallTVMsg = smallTVMsg; + } + + public List getNoticeMsg() { + return noticeMsg; + } + + public void setNoticeMsg(List noticeMsg) { + this.noticeMsg = noticeMsg; + } + + public static class CapsuleEntity { + /** + * normal : {"coin":13,"change":1,"progress":{"now":4000,"max":10000}} + * colorful : {"coin":0,"change":0,"progress":{"now":0,"max":5000}} + */ + + @SerializedName("normal") + private NormalEntity normal; + @SerializedName("colorful") + private ColorfulEntity colorful; + + public NormalEntity getNormal() { + return normal; + } + + public void setNormal(NormalEntity normal) { + this.normal = normal; + } + + public ColorfulEntity getColorful() { + return colorful; + } + + public void setColorful(ColorfulEntity colorful) { + this.colorful = colorful; + } + + public static class NormalEntity { + /** + * coin : 13 + * change : 1 + * progress : {"now":4000,"max":10000} + */ + + @SerializedName("coin") + private int coin; + @SerializedName("change") + private int change; + @SerializedName("progress") + private ProgressEntity progress; + + public int getCoin() { + return coin; + } + + public void setCoin(int coin) { + this.coin = coin; + } + + public int getChange() { + return change; + } + + public void setChange(int change) { + this.change = change; + } + + public ProgressEntity getProgress() { + return progress; + } + + public void setProgress(ProgressEntity progress) { + this.progress = progress; + } + + public static class ProgressEntity { + /** + * now : 4000 + * max : 10000 + */ + + @SerializedName("now") + private int now; + @SerializedName("max") + private int max; + + public int getNow() { + return now; + } + + public void setNow(int now) { + this.now = now; + } + + public int getMax() { + return max; + } + + public void setMax(int max) { + this.max = max; + } + } + } + + public static class ColorfulEntity { + /** + * coin : 0 + * change : 0 + * progress : {"now":0,"max":5000} + */ + + @SerializedName("coin") + private int coin; + @SerializedName("change") + private int change; + @SerializedName("progress") + private ProgressEntityX progress; + + public int getCoin() { + return coin; + } + + public void setCoin(int coin) { + this.coin = coin; + } + + public int getChange() { + return change; + } + + public void setChange(int change) { + this.change = change; + } + + public ProgressEntityX getProgress() { + return progress; + } + + public void setProgress(ProgressEntityX progress) { + this.progress = progress; + } + + public static class ProgressEntityX { + /** + * now : 0 + * max : 5000 + */ + + @SerializedName("now") + private int now; + @SerializedName("max") + private int max; + + public int getNow() { + return now; + } + + public void setNow(int now) { + this.now = now; + } + + public int getMax() { + return max; + } + + public void setMax(int max) { + this.max = max; + } + } + } + } + + public static class TopListEntity { + /** + * uid : 9256 + * uname : SSR丶尧 + * coin : 2905900 + * face : http://i0.hdslb.com/bfs/face/eba52abb1daaf3aecd7b986b9731451872d66613.jpg + * guard_level : 3 + * rank : 1 + * score : 2905900 + */ + + @SerializedName("uid") + private int uid; + @SerializedName("uname") + private String userName; + @SerializedName("coin") + private int coin; + @SerializedName("face") + private String face; + @SerializedName("guard_level") + private String guardLevel; + @SerializedName("rank") + private int rank; + @SerializedName("score") + private int score; + + public int getUid() { + return uid; + } + + public void setUid(int uid) { + this.uid = uid; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public int getCoin() { + return coin; + } + + public void setCoin(int coin) { + this.coin = coin; + } + + public String getFace() { + return face; + } + + public void setFace(String face) { + this.face = face; + } + + public String getGuardLevel() { + return guardLevel; + } + + public void setGuardLevel(String guardLevel) { + this.guardLevel = guardLevel; + } + + public int getRank() { + return rank; + } + + public void setRank(int rank) { + this.rank = rank; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + } + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SysGiftEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SysGiftEntity.java new file mode 100644 index 0000000..b8098ca --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SysGiftEntity.java @@ -0,0 +1,108 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.annotations.SerializedName; + +public class SysGiftEntity { + /** + * cmd : SYS_GIFT + * msg : あさひなみよう在直播间5135178开启了丰收祭典,一起来分享收获的福利吧! + * msg_text : あさひなみよう在直播间5135178开启了丰收祭典,一起来分享收获的福利吧! + * tips : あさひなみよう在直播间5135178开启了丰收祭典,一起来分享收获的福利吧! + * url : http://live.bilibili.com/5135178 + * roomid : 5135178 + * real_roomid : 5135178 + * giftId : 103 + * msgTips : 0 + */ + + @SerializedName("cmd") + private String cmd; + @SerializedName("msg") + private String msg; + @SerializedName("msg_text") + private String msgText; + @SerializedName("tips") + private String tips; + @SerializedName("url") + private String url; + @SerializedName("roomid") + private int roomId; + @SerializedName("real_roomid") + private int realRoomId; + @SerializedName("giftId") + private int giftId; + @SerializedName("msgTips") + private int msgTips; + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public String getMsgText() { + return msgText; + } + + public void setMsgText(String msgText) { + this.msgText = msgText; + } + + public String getTips() { + return tips; + } + + public void setTips(String tips) { + this.tips = tips; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public int getRoomId() { + return roomId; + } + + public void setRoomId(int roomId) { + this.roomId = roomId; + } + + public int getRealRoomId() { + return realRoomId; + } + + public void setRealRoomId(int realRoomId) { + this.realRoomId = realRoomId; + } + + public int getGiftId() { + return giftId; + } + + public void setGiftId(int giftId) { + this.giftId = giftId; + } + + public int getMsgTips() { + return msgTips; + } + + public void setMsgTips(int msgTips) { + this.msgTips = msgTips; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SysMSGEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SysMSGEntity.java new file mode 100644 index 0000000..fa9fe7a --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/SysMSGEntity.java @@ -0,0 +1,120 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.annotations.SerializedName; + +public class SysMSGEntity { + /** + * cmd : SYS_MSG + * msg : 【瑾然-】:?在直播间:?【3939852】:?赠送 小电视一个,请前往抽奖 + * msg_text : 【瑾然-】:?在直播间:?【3939852】:?赠送 小电视一个,请前往抽奖 + * rep : 1 + * styleType : 2 + * url : http://live.bilibili.com/3939852 + * roomid : 3939852 + * real_roomid : 3939852 + * rnd : 1510499432 + * tv_id : 29318 + */ + + @SerializedName("cmd") + private String cmd; + @SerializedName("msg") + private String msg; + //B站自己的广告, msgText 可能是空的 + @SerializedName("msg_text") + private String msgText; + @SerializedName("rep") + private int rep; + @SerializedName("styleType") + private int styleType; + @SerializedName("url") + private String url; + @SerializedName("roomid") + private int roomId; + @SerializedName("real_roomid") + private int realRoomId; + @SerializedName("rnd") + private int rnd; + @SerializedName("tv_id") + private String tvId; + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public String getMsgText() { + return msgText; + } + + public void setMsgText(String msgText) { + this.msgText = msgText; + } + + public int getRep() { + return rep; + } + + public void setRep(int rep) { + this.rep = rep; + } + + public int getStyleType() { + return styleType; + } + + public void setStyleType(int styleType) { + this.styleType = styleType; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public int getRoomId() { + return roomId; + } + + public void setRoomId(int roomId) { + this.roomId = roomId; + } + + public int getRealRoomId() { + return realRoomId; + } + + public void setRealRoomId(int realRoomId) { + this.realRoomId = realRoomId; + } + + public int getRnd() { + return rnd; + } + + public void setRnd(int rnd) { + this.rnd = rnd; + } + + public String getTvId() { + return tvId; + } + + public void setTvId(String tvId) { + this.tvId = tvId; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/WelcomeEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/WelcomeEntity.java new file mode 100644 index 0000000..80e7598 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/WelcomeEntity.java @@ -0,0 +1,81 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.annotations.SerializedName; + +public class WelcomeEntity { + /** + * cmd : WELCOME + * data : {"uid":516505,"uname":"圣蝎","is_admin":false,"vip":1} + */ + + @SerializedName("cmd") + private String cmd; + @SerializedName("data") + private DataEntity data; + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public DataEntity getData() { + return data; + } + + public void setData(DataEntity data) { + this.data = data; + } + + public static class DataEntity { + /** + * uid : 516505 + * uname : 圣蝎 + * is_admin : false + * vip : 1 + */ + + @SerializedName("uid") + private int uid; + @SerializedName("uname") + private String userName; + @SerializedName("is_admin") + private boolean isAdmin; + @SerializedName("vip") + private int vip; + + public int getUid() { + return uid; + } + + public void setUid(int uid) { + this.uid = uid; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public boolean isIsAdmin() { + return isAdmin; + } + + public void setIsAdmin(boolean isAdmin) { + this.isAdmin = isAdmin; + } + + public int getVip() { + return vip; + } + + public void setVip(int vip) { + this.vip = vip; + } + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/live/socket/entity/WelcomeGuardEntity.java b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/WelcomeGuardEntity.java new file mode 100644 index 0000000..72923e3 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/live/socket/entity/WelcomeGuardEntity.java @@ -0,0 +1,92 @@ +package com.hiczp.bilibili.api.live.socket.entity; + +import com.google.gson.annotations.SerializedName; + +public class WelcomeGuardEntity { + /** + * cmd : WELCOME_GUARD + * data : {"uid":23598108,"username":"lovevael","guard_level":3,"water_god":0} + * roomid : 43001 + */ + + @SerializedName("cmd") + private String cmd; + @SerializedName("data") + private DataEntity data; + @SerializedName("roomid") + private int roomId; + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public DataEntity getData() { + return data; + } + + public void setData(DataEntity data) { + this.data = data; + } + + public int getRoomId() { + return roomId; + } + + public void setRoomId(int roomId) { + this.roomId = roomId; + } + + public static class DataEntity { + /** + * uid : 23598108 + * username : lovevael + * guard_level : 3 + * water_god : 0 + */ + + @SerializedName("uid") + private int uid; + @SerializedName("username") + private String username; + @SerializedName("guard_level") + private int guardLevel; + @SerializedName("water_god") + private int waterGod; + + public int getUid() { + return uid; + } + + public void setUid(int uid) { + this.uid = uid; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public int getGuardLevel() { + return guardLevel; + } + + public void setGuardLevel(int guardLevel) { + this.guardLevel = guardLevel; + } + + public int getWaterGod() { + return waterGod; + } + + public void setWaterGod(int waterGod) { + this.waterGod = waterGod; + } + } +} diff --git a/src/test/java/com/hiczp/bilibili/api/test/LiveRoomTest.java b/src/test/java/com/hiczp/bilibili/api/test/LiveRoomTest.java new file mode 100644 index 0000000..cb2659d --- /dev/null +++ b/src/test/java/com/hiczp/bilibili/api/test/LiveRoomTest.java @@ -0,0 +1,156 @@ +package com.hiczp.bilibili.api.test; + +import com.google.gson.Gson; +import com.hiczp.bilibili.api.BilibiliRESTAPI; +import com.hiczp.bilibili.api.live.entity.BulletScreenEntity; +import com.hiczp.bilibili.api.live.entity.LiveRoomInfoEntity; +import com.hiczp.bilibili.api.live.entity.SendBulletScreenResponseEntity; +import com.hiczp.bilibili.api.live.socket.BulletScreenListenerAdaptor; +import com.hiczp.bilibili.api.live.socket.LiveClient; +import com.hiczp.bilibili.api.live.socket.PackageRepository; +import com.hiczp.bilibili.api.live.socket.Utils; +import com.hiczp.bilibili.api.live.socket.entity.*; +import org.junit.FixMethodOrder; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; + +@Ignore +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LiveRoomTest { + private static final Logger LOGGER = LoggerFactory.getLogger(LiveRoomTest.class); + private static final Config CONFIG = Config.getInstance(); + private static final int USER_ID = BilibiliRESTAPI.getMid(); + private static final int STOP_AFTER_N_HEART_BEATS = 3; + private static final int STOP_AFTER_SECOND = 90; + + @Ignore + @Test + public void _0socketTest() throws IOException { + LOGGER.info("Start socket connection to live Bullet Screen stream server, test continue for {} heart beat", STOP_AFTER_N_HEART_BEATS); + LiveRoomInfoEntity.LiveRoomEntity liveRoomEntity = BilibiliRESTAPI.getLiveService().getRoomInfo(CONFIG.getRoomId()).execute().body().getData(); + int roomId = liveRoomEntity.getRoomId(); + + SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(liveRoomEntity.getCmt(), liveRoomEntity.getCmtPortGoim())); + socketChannel.write(PackageRepository.createEnterRoomPackage(roomId, USER_ID)); + Thread thread = new Thread(() -> { + while (true) { + try { + Utils.printBytes(PackageRepository.readNextPackage(socketChannel).array()); + System.out.println(); + } catch (IOException e) { + break; + } + } + }); + thread.start(); + + for (int i = STOP_AFTER_N_HEART_BEATS; i > 0; i--) { + socketChannel.write(PackageRepository.createHeartBeatPackage(roomId, USER_ID)); + LOGGER.debug("Send heart beat package"); + BilibiliRESTAPI.getLiveService().sendBulletScreen(new BulletScreenEntity(roomId, "send heart beat")).enqueue(new Callback() { + private Gson gson = new Gson(); + + @Override + public void onResponse(Call call, Response response) { + gson.toJson(response.body(), System.out); + System.out.println(); + } + + @Override + public void onFailure(Call call, Throwable throwable) { + throwable.printStackTrace(); + } + }); + try { + Thread.sleep(30 * 1000); + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } + } + + socketChannel.close(); + } + + @Test + public void _1liveClientTest() throws IOException { + int roomId = CONFIG.getRoomId(); + LOGGER.info("Start LiveClientTest for room {}", roomId); + LiveClient liveClient = new LiveClient(roomId, USER_ID) + .addListener(new BulletScreenListenerAdaptor() { + @Override + public void onConnect() { + LOGGER.info("Connected"); + } + + @Override + public void onDisconnect() { + LOGGER.info("Disconnected"); + } + + @Override + public void onViewerCountPackage(int viewerCount) { + LOGGER.info("Current viewers: {}", viewerCount); + } + + @Override + public void onDanMuMSGPackage(DanMuMSGEntity danMuMSGEntity) { + LOGGER.info("[{}]{}", danMuMSGEntity.getUsername(), danMuMSGEntity.getMessage()); + } + + @Override + public void onSendGiftPackage(SendGiftEntity sendGiftEntity) { + SendGiftEntity.DataEntity dataEntity = sendGiftEntity.getData(); + LOGGER.info("{} send {} * {}", dataEntity.getUserName(), dataEntity.getGiftName(), dataEntity.getNum()); + } + + @Override + public void onSysMSGPackage(SysMSGEntity sysMSGEntity) { + LOGGER.info("System message: {} {}", sysMSGEntity.getMsg(), sysMSGEntity.getUrl()); + } + + @Override + public void onSysGiftPackage(SysGiftEntity sysGiftEntity) { + LOGGER.info("System gift: {} {}", sysGiftEntity.getMsg(), sysGiftEntity.getUrl()); + } + + @Override + public void onWelcomePackage(WelcomeEntity welcomeEntity) { + LOGGER.info("Welcome {}", welcomeEntity.getData().getUserName()); + } + + @Override + public void onWelcomeGuardPackage(WelcomeGuardEntity welcomeGuardEntity) { + WelcomeGuardEntity.DataEntity dataEntity = welcomeGuardEntity.getData(); + LOGGER.info("Welcome guard [Lv{}]{}", dataEntity.getGuardLevel(), dataEntity.getUsername()); + } + + @Override + public void onLivePackage(LiveEntity liveEntity) { + LOGGER.info("Room {} start live", liveEntity.getRoomId()); + } + + @Override + public void onPreparingPackage(PreparingEntity preparingEntity) { + LOGGER.info("Room {} stop live", preparingEntity.getRoomId()); + } + }) + .connect(); + try { + Thread.sleep(STOP_AFTER_SECOND * 1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + liveClient.close(); + } +} diff --git a/src/test/java/com/hiczp/bilibili/api/test/LoginTest.java b/src/test/java/com/hiczp/bilibili/api/test/LoginTest.java index 2b06550..619836a 100644 --- a/src/test/java/com/hiczp/bilibili/api/test/LoginTest.java +++ b/src/test/java/com/hiczp/bilibili/api/test/LoginTest.java @@ -5,6 +5,7 @@ import com.google.gson.GsonBuilder; import com.hiczp.bilibili.api.BilibiliRESTAPI; import org.junit.After; import org.junit.FixMethodOrder; +import org.junit.Ignore; import org.junit.Test; import org.junit.runners.MethodSorters; import org.slf4j.Logger; @@ -34,6 +35,7 @@ public class LoginTest { ); } + @Ignore @Test public void _2refreshToken() throws Exception { LOGGER.info("Refreshing token"); diff --git a/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java b/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java index 0d926be..55374c0 100644 --- a/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java +++ b/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java @@ -14,6 +14,7 @@ import java.io.InputStreamReader; @Suite.SuiteClasses({ LoginTest.class, LiveServiceTest.class, + LiveRoomTest.class, LogoutTest.class }) public class RuleSuite {