[JAVA版本] Websocket获取B站直播弹幕——基于直播开放平台

这篇具有很好参考价值的文章主要介绍了[JAVA版本] Websocket获取B站直播弹幕——基于直播开放平台。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

教程

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

代码实现

1、相关依赖

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

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.40</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-core -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
    <version>5.8.21</version>
</dependency>

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<String,Object> 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<String,Object> 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<String,Object> 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<String> gameIds) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        Map<String,Object> 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<String,Object> 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<bytes.length;i++){
            temp = Integer.toHexString(bytes[i] & 0xFF);
            if (temp.length()==1){
                //1得到一位的进行补0操作
                stringBuffer.append("0");
            }
            stringBuffer.append(temp);
        }
        return stringBuffer.toString();
    }
}

3、新建 WebsocketListener.java

用于监听接收到的数据。

import jakarta.websocket.*;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import cn.hutool.core.util.ZipUtil;

@ClientEndpoint
public class WebsocketListener {
	private Session session;
    private String authBody;

    public WebsocketListener(String authBody) {
        this.authBody = authBody;
    }
    @OnOpen
    public void onOpen(Session session) throws IOException {
        System.out.println("已连接服务...");
        this.session = session;
        RemoteEndpoint.Async remote = session.getAsyncRemote();
        //鉴权协议包
        ByteBuffer authPack = ByteBuffer.wrap(generateAuthPack(authBody));
        remote.sendBinary(authPack);
        //每30秒发送心跳包
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        executorService.scheduleAtFixedRate(() -> {
            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() < byteBuffer.limit()){
            unpack(byteBuffer);
        }
    }
}

4、使用

public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeyException, URISyntaxException, DeploymentException {
	ProjectRequest p = new ProjectRequest(你的应用ID, 你的Access_key, 你的 Access_Secret);
	//获取弹幕服务信息
	String result = p.start(你的身份码);
	JSONObject data = JSONObject.parseObject(result).getJSONObject("data");
	//个人信息
	JSONObject anchorInfo = data.getJSONObject("anchor_info");
	//弹幕服务器信息
	JSONObject websocketInfo = data.getJSONObject("websocket_info");
	//弹幕服务器地址
	JSONArray wssLinks = websocketInfo.getJSONArray("wss_link");
	//websocket鉴权信息
	String authBody = websocketInfo.getString("auth_body");
	//选一个服务器节点
	String uri = wssLinks.getString(0);
	WebSocketContainer container = ContainerProvider.getWebSocketContainer();
	// 连接到WebSocket服务器
	container.connectToServer(new WebsocketListener(authBody), new URI(uri)); 
}
参数获取
Access_key 和 Access_Secret 去B站直播开放平台注册申请个人开发者后就能获得
应用ID 成为个人开发者后,在直播开放平台创建应用后,就能获得应用ID
身份码 登录B站直播间找到幻星-互动玩法,在里面就能找到身份码

其他版本

【flutter / dart 版本】Websocket获取B站直播间弹幕教程——基于B站直播开发平台文章来源地址https://www.toymoban.com/news/detail-736712.html

到了这里,关于[JAVA版本] Websocket获取B站直播弹幕——基于直播开放平台的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 抖音直播间websocket礼物和弹幕消息推送可能出现重复的情况,解决办法

    在抖音直播间里,通过websocket收到的礼物消息数据格式如下: 根据字段名称可以看到送礼物的人和送的礼物是什么,并且这个礼物的traceId是唯一的,所以可以通过这个traceId进行去重。 判断这个礼物是否在监控列表中并且是否已经在全局id中: 消息和礼物等数据也有可能会出

    2024年02月01日
    浏览(236)
  • 极简式 Unity 获取 bilibili 直播弹幕、SC、上舰、礼物等 插件

    极简式 Unity 获取 bilibili 直播弹幕、SC、上舰、礼物等 1. 声明 下载链接 软件均仅用于学习交流,请勿用于任何商业用途! 2. 介绍 该项目为Unity实时爬取B站直播弹幕。 项目介绍:通过传入B站直播间账号,实现监控B站直播弹幕、SC、上舰、礼物等。 运行方式:下载后将文件夹

    2024年02月05日
    浏览(36)
  • 视频号直播弹幕采集

    训练地址:https://www.qiulianmao.com websocket逆向 http拦截 websocket拦截 视频号直播弹幕采集 实战一:Http轮询弹幕拦截 更新中

    2024年02月06日
    浏览(49)
  • 抖音直播间弹幕rpc学习

    目标url 随便找个直播间即可。 https://live.douyin.com/198986091107 接口分析 首先并没有在xhr下找到对应的接口 因为采用了websocket来传输信息。切换到ws即可看到 消息下,可以看到16进制的数据在源源不断地增加。 那么我们只要找到反序列化后的数据,再发送到本地的socket服务,就

    2023年04月22日
    浏览(79)
  • 最简单vue获取当前地区天气--高德开放平台实现

    目录 前言 一、注册成为高德平台开发者 二、注册天气key 1.点击首页右上角打开控制台  2.创建新应用 三、vue项目使用 1.打开vue项目找到public下的index.html,如果是vue3的话直接在主目录打开index.html文件就行,主要就是打开出口文件 ​编辑 2.根据高德开放文档获取当前城市信息

    2024年02月10日
    浏览(49)
  • 「GPT虚拟直播」实战篇|GPT接入虚拟人实现直播间弹幕回复

    ChatGPT和元宇宙都是当前数字化领域中非常热门的技术和应用。结合两者的优势和特点,可以探索出更多的应用场景和商业模式。例如,在元宇宙中使用ChatGPT进行自然语言交互,可以为用户提供更加智能化、个性化的服务和支持;在ChatGPT中使用元宇宙进行虚拟现实体验,可以

    2024年02月06日
    浏览(55)
  • 基于websocket协议的某音直播间数据采集

    目录 声明  本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 本文章未经许可禁止转载,禁止任何修改后二次传播,擅

    2024年02月13日
    浏览(43)
  • 直播弹幕系统(二)- 整合RabbitMQ进行消息广播和异步处理

    上一篇文章 SpringCloud网关对WebSocket链接进行负载均衡 中把主要的架子搭建好了,这一篇文章就要开始写业务逻辑了。在分布式系统下,如何达到SpringBoot - WebSocket的使用和聊天室练习的效果。 我们页面上,通过 WebSocket 发送弹幕信息的时候,后端通过 @OnMessage 注解修饰的函数

    2023年04月08日
    浏览(39)
  • 植物大战僵尸小游戏抖音快手直播搭建弹幕插件教程

    植物大战弹幕插件功能介绍 该插件由梦歌技术部团队支持开发,本插件软件通过监测抖音弹幕信息,获取礼物数据触发脚本插件对应的功能; 功能目前基本上已经完善,后期功能会陆续上线支持更新,全新的脚本监测稳定方便实用! 1.打开植物大战僵尸游戏 2.打开弹幕插件

    2024年02月02日
    浏览(351)
  • Python制作一个自动发送弹幕的工具,让你看直播不冷场

    前言 嗨喽,大家好呀~这里是爱看美女的茜茜呐 让我们先看看效果: 名字我就打码了,当然名字不是关键,我直接截图展示算了,GIF的话,太麻烦了。 环境使用: Python 3.8 / 编译器 Pycharm 2021.2版本 / 编辑器 素材准备 接下来我们要准备好你想发送的弹幕内容 这个我都是随便打

    2023年04月27日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包