[JAVA版本] Websocket获取B站直播弹幕

您所在的位置:网站首页 b站直播间不能发弹幕怎么办呢 [JAVA版本] Websocket获取B站直播弹幕

[JAVA版本] Websocket获取B站直播弹幕

2024-07-17 09:28| 来源: 网络整理| 查看: 265

教程

B站直播间弹幕Websocket获取 — 哔哩哔哩直播开放平台 基于B站直播开放平台开放且未上架时,只能个人使用。

代码实现 1、相关依赖

fastjson2用于解析JSON字符串,可自行替换成别的框架。 hutool-core用于解压zip数据,可自行替换成别的框架。

com.alibaba.fastjson2 fastjson2 2.0.40 cn.hutool hutool-core 5.8.21 1、新建ProjectRequest.java

用于发送项目start、end、heartbeat请求。 注意: 没有上架的项目,start返回结果没有场次ID,导致end、heartbeat请求不能正常执行。 但是没有关系,start能获得弹幕服务信息就行。

import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import jakarta.annotation.Nonnull; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; public class ProjectRequest { /** * 项目ID */ private long appId; /** * 身份验证Key */ private String accessKey; /** * 身份验证密钥 */ private String accessSecret; public ProjectRequest(long appId, String accessKey, String accessSecret) { this.appId = appId; this.accessKey = accessKey; this.accessSecret = accessSecret; } public final static String START_URL = "https://live-open.biliapi.com/v2/app/start"; public final static String END_URL = "https://live-open.biliapi.com/v2/app/end"; public final static String HEART_BEAT_URL = "https://live-open.biliapi.com/v2/app/heartbeat"; public final static String BATCH_HEART_BEAT_URL = "https://live-open.biliapi.com/v2/app/batchHeartbeat"; /** * 接口描述:开启项目第一步,平台会根据入参进行鉴权校验。鉴权通过后,返回长连信息、场次信息和主播信息。开发者拿到长连和心跳信息后,需要参照[长连说明]和[项目心跳],与平台保持健康的 * @param code 必填 string [主播身份码] * param appId 必填 integer(13位长度的数值,注意不要用普通int,会溢出的) 项目ID */ public String start(String code) throws IOException, NoSuchAlgorithmException, InvalidKeyException { Map params = new HashMap(); params.put("code", code); params.put("app_id", appId); return post(START_URL, params); } /** * 接口描述:项目关闭时需要主动调用此接口,使用对应项目Id及项目开启时返回的game_id作为唯一标识,调用后会同步下线互动道具等内容,项目关闭后才能进行下一场次互动。 * param appId 必填 integer(13位长度的数值,注意不要用普通int,会溢出的) 项目ID * param gameId 必填 场次id */ public String end(String gameId) throws IOException, NoSuchAlgorithmException, InvalidKeyException { Map params = new HashMap(); params.put("game_id", gameId); params.put("app_id", appId); return post(END_URL, params); } /** * 接口描述:项目开启后,需要持续间隔20秒调用一次该接口。平台超过60s未收到项目心跳,会自动关闭当前场次(game_id),同时将道具相关功能下线,以确保下一场次项目正常运行。 * 接口地址:/v2/app/heartbeat * 方法:POST * param gameId 必填 场次id */ public String heartbeat(String gameId) throws IOException, NoSuchAlgorithmException, InvalidKeyException { Map params = new HashMap(); params.put("game_id", gameId); return post(HEART_BEAT_URL, params); } /** * 项目批量心跳 * 接口地址:/v2/app/batchHeartbeat * 方法:POST * @param gameIds 必填 []string 场次id * */ public String batchHeartbeat(@Nonnull List gameIds) throws IOException, NoSuchAlgorithmException, InvalidKeyException { Map params = new HashMap(); params.put("game_ids", JSONArray.toJSONString(gameIds)); return post(HEART_BEAT_URL, params); } /** * 自定义post请求 * @param url * @param dataMap * @throws IOException * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ private String post(String url, Map dataMap) throws IOException, NoSuchAlgorithmException, InvalidKeyException { String bodyStr = JSONObject.toJSONString(dataMap); HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); con.setRequestMethod("POST"); // 设置请求头 setHeader(con, bodyStr); // 发送 POST 请求 con.setDoOutput(true); try(DataOutputStream wr = new DataOutputStream(con.getOutputStream())) { wr.writeBytes(bodyStr); wr.flush(); } // 获取响应结果 try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))){ // 返回响应结果 return bufferedReader.lines().collect(Collectors.joining("\n")); } } public static String KEY_CONTENT_MD5 = "x-bili-content-md5"; public static String KEY_TIMESTAMP = "x-bili-timestamp"; public static String KEY_SIGNATURE_NONCE = "x-bili-signature-nonce"; /** * 设置请求头 * @param con * @param bodyStr 请求体 * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ private void setHeader(HttpURLConnection con,String bodyStr) throws NoSuchAlgorithmException, InvalidKeyException { con.setRequestProperty("User-Agent", "Mozilla/5.0"); /**----------------------------------------------------------------------------**/ //必填:接受的返回结果的类型。目前只支持JSON类型,取值:application/json。 con.setRequestProperty("Accept", "application/json"); //必填:当前请求体(Request Body)的数据类型。目前只支持JSON类型,取值:application/json。 con.setRequestProperty("Content-Type", "application/json"); //必填:请求体的编码值,根据请求体计算所得。算法说明:将请求体内容当作字符串进行MD5编码。 con.setRequestProperty(KEY_CONTENT_MD5, getContentMd5(bodyStr)); //必填:unix时间戳,单位是秒。请求时间戳不能超过当前时间10分钟,否则请求会被丢弃。 con.setRequestProperty(KEY_TIMESTAMP, String.valueOf(System.currentTimeMillis()/1000)); //必填: 版本1.0 con.setRequestProperty("x-bili-signature-version", "1.0"); //必填:签名唯一随机数。用于防止网络重放攻击,建议您每一次请求都使用不同的随机数 con.setRequestProperty(KEY_SIGNATURE_NONCE, UUID.randomUUID().toString()); //必填:加密算法 con.setRequestProperty("x-bili-signature-method", "HMAC-SHA256"); //必填: accesskey id con.setRequestProperty("x-bili-accesskeyid", accessKey); //必填:请求签名(注意生成的签名是小写的)。关于请求签名的计算方法,请参见签名机制 con.setRequestProperty("Authorization", generateSignature(con)); } /** * MD5计算 */ private String getContentMd5(String content) throws NoSuchAlgorithmException { MessageDigest md5 = MessageDigest.getInstance("MD5"); return byte2Hex( md5.digest(content.getBytes(StandardCharsets.UTF_8)) ); } /** * 签名 HmacSHA256计算 */ public String generateSignature(HttpURLConnection con) throws NoSuchAlgorithmException, InvalidKeyException { StringBuilder s = new StringBuilder(); s.append("x-bili-accesskeyid:").append(accessKey).append("\n"); s.append("x-bili-content-md5:").append(con.getRequestProperty(KEY_CONTENT_MD5)).append("\n"); s.append("x-bili-signature-method:").append("HMAC-SHA256").append("\n"); s.append("x-bili-signature-nonce:").append(con.getRequestProperty(KEY_SIGNATURE_NONCE)).append("\n"); s.append("x-bili-signature-version:").append("1.0").append("\n"); s.append("x-bili-timestamp:").append(con.getRequestProperty(KEY_TIMESTAMP)); byte[] headerByte = s.toString().getBytes(StandardCharsets.UTF_8); byte[] secretByte = accessSecret.getBytes(StandardCharsets.UTF_8); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secretByte, "HmacSHA256")); byte[] bytes = mac.doFinal(headerByte); return byte2Hex(bytes); } /** * 字节数组转16进制字符串 * @param bytes * @return */ private static String byte2Hex(byte[] bytes){ StringBuffer stringBuffer = new StringBuffer(); String temp = null; for (int i=0;i { try { ByteBuffer heartBeatPack = ByteBuffer.wrap(generateHeartBeatPack()); remote.sendBinary(heartBeatPack); } catch (IOException e) { throw new RuntimeException(e); } }, 0, 30, TimeUnit.SECONDS); } @OnMessage public void onMessage(ByteBuffer byteBuffer) { //解包 unpack(byteBuffer); } @OnClose public void onClose(Session session, CloseReason closeReason) { System.out.println("关闭Websocket服务: " + closeReason); } @OnError public void onError(Session session, Throwable t) { System.out.println("Websocket服务异常: " + t.getMessage()); } public interface Opt{ short HEARTBEAT = 2;// 客户端发送的心跳包(30秒发送一次) short HEARTBEAT_REPLY = 3;// 服务器收到心跳包的回复 人气值,数据不是JSON,是4字节整数 short SEND_SMS_REPLY = 5;// 服务器推送的弹幕消息包 short AUTH = 7;//客户端发送的鉴权包(客户端发送的第一个包) short AUTH_REPLY = 8;//服务器收到鉴权包后的回复 } public interface Version{ short NORMAL = 0;//Body实际发送的数据——普通JSON数据 short ZIP = 2; //Body中是经过压缩后的数据,请使用zlib解压,然后按照Proto协议去解析。 } /** * 封包 * @param jsonStr 数据 * @param code 协议包类型 * @return * @throws IOException */ public static byte[] pack(String jsonStr, short code) throws IOException { byte[] contentBytes = new byte[0]; if(Opt.AUTH == code){ contentBytes = jsonStr.getBytes(); } try(ByteArrayOutputStream data = new ByteArrayOutputStream(); DataOutputStream stream = new DataOutputStream(data)){ stream.writeInt(contentBytes.length + 16);//封包总大小 stream.writeShort(16);//头部长度 header的长度,固定为16 stream.writeShort(Version.NORMAL); stream.writeInt(code);//操作码(封包类型) stream.writeInt(1);//保留字段,可以忽略。 if(Opt.AUTH == code){ stream.writeBytes(jsonStr); } return data.toByteArray(); } } /** * 生成认证包 * @return */ public static byte[] generateAuthPack(String jsonStr) throws IOException { return pack(jsonStr, Opt.AUTH); } /** * 生成心跳包 * @return */ public static byte[] generateHeartBeatPack() throws IOException { return pack(null, Opt.HEARTBEAT); } /** * 解包 * @param byteBuffer * @return */ public static void unpack(ByteBuffer byteBuffer){ int packageLen = byteBuffer.getInt(); short headLength = byteBuffer.getShort(); short protVer = byteBuffer.getShort(); int optCode = byteBuffer.getInt(); int sequence = byteBuffer.getInt(); if(Opt.HEARTBEAT_REPLY == optCode){ System.out.println("这是服务器心跳回复"); } byte[] contentBytes = new byte[packageLen - headLength]; byteBuffer.get(contentBytes); //如果是zip包就进行解包 if(Version.ZIP == protVer){ unpack(ByteBuffer.wrap(ZipUtil.unZlib(contentBytes))); return; } String content = new String(contentBytes, StandardCharsets.UTF_8); if(Opt.AUTH_REPLY == optCode){ //返回{"code":0}表示成功 System.out.println("这是鉴权回复:"+content); } //真正的弹幕消息 if(Opt.SEND_SMS_REPLY == optCode){ System.out.println("真正的弹幕消息:"+content); // todo 自定义处理 } //只存在ZIP包解压时才有的情况 //如果byteBuffer游标 小于 byteBuffer大小,那就证明还有数据 if(byteBuffer.position()


【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3