接入bilibili直播开放平台

来源: 腾讯云 时间:2022-12-30 17:11:18

在前几天,无意间看到了b站直播互动平台开放了开发者接入的功能,所以继接入qq和baidu登录授权功能后决定研究一下b站的直播互动平台接入有哪些不同。对于这篇文章酝酿了好些天,因为实在是有些不大好下手,不知道怎样写才能讲的更加清晰、易懂。


(资料图片)

回顾开发的总结

先来对这些天开发的内容进行一个总结和比较

qq互联

baidu网盘开放平台

bilibili直播开放平台

接口功能

登录与基本信息

登录与基本信息

直播弹幕、礼物、舰队时时信息

审核

严格,需要网站提前准备好各项功能、开放授权即能使用,否则不予通过。并且众多审核都需要一一进行。

宽松,只要申请就能通过,没有特殊条件,只要实名认证可以进行任何操作,不过一般用户似乎无法调用上传文件的功能

较为宽松,申请接入后第二天就会发携带key和secret的邮件。上架应用需要审核,测试无需审核

授权方式

Oauth2.0

Oauth2.0

请求头携带authorization获得长链信息,直播长链使用长链信息授权建立链接

文档清晰

清晰

清晰

不清晰,很多接口表述有歧义。文档冗余,排版杂乱

网址

https://connect.qq.com/manage.html#/

https://pan.baidu.com/union/home

https://open-live.bilibili.com/document/

Oauth2.0方式流程

对于oauth2.0方式的授权方式,当运用被创建后,即可获得基本的clientId和clientSecret以及appid。

之后我们需要设置callback地址,该地址必须填写公网地址/域名,而不能是内网地址。在自行测试阶段,为了方便,可以修改本地host文件的127.0.0.1为自己设置的公网地址/域名(因为host文件在开发过程中经常使用,所以建议设置指令快捷启动该文件)。

前端通过发送官方提供的授权码code获取请求,将会进入其授权页面,用户授权通过后,授权页面将会跳转到我们的回调地址,并且url携带了授权码code。利用code再次向获取token的url发起请求,即可获得token,其余数据获取携带该token即可成功得到。

bilibili的接入流程

回归到正题,我们这节重点总结的是阿b的直播接入方式,因为其中包含的很多知识都是之前未接触的,或者说没有成体系的解决方案。

先来上一个官方提供的流程图,总体来说分为两个部分,第一部分是获取长链信息,第二部分是进行长链接入并且保活。

对于第一步

请求头携带authorization获得长链信息的流程,在之前接入图床doge云时已经有了类似的实现思路,不过阿b的规则和doge云的方式有所不同。

doge云基本校验过程是先将 REQUEST_URI+"\n"+HTTP_BODY 拼接得到签名字符串,与密钥通过hmacSHA1加密签名sign,最后:

Authorization = "Token" + (accessKey + ":" + sign) 即可获得授权串。

bilibili则是将指定规则的几个header按字典序不带空白字符串拼接、并且与secret进行Hmac-SHA256加密即可获得authorization串。下面来详细讲一下阿b的授权码获取细节。

首先是header部分的设置,因为header的一系列要求,很自然的就让我想用Map进行暂存,之后可以迭代map来对请求头进行设置。不过对于部分header请求头,还要参与签名的流程,而签名要求是请求头按字典排序(不然签名肯定不是唯一的,毕竟一般的HashMap等都是散列存储的,如果不止一对kv,是无法保证按加入顺序取出的),所以自然又会想到有没有一种map可以有序存储呢,当然备选的有TreeMap和LinkedHashMap两种,其中TreeMap采用的是红黑树实现,LinkedHashMap则是有一个链表连接,如果按顺序存储,都能达到效果,而且数据量没有多大,只有6条,性能差距不大,但是方便起见,用链式的大致还是快一点点的。所以header设置和签名串获取如下代码实现:

URL url = new URL(DOMAIN_NAME + path); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Accept","application/json"); connection.setRequestProperty("Content-Type","application/json");// SET HEADERS Map xBili = new LinkedHashMap<>(6); xBili.put("x-bili-accesskeyid",ACCESS_KEY_ID); xBili.put("x-bili-content-md5",DigestUtils.md5DigestAsHex(content.getBytes())); xBili.put("x-bili-signature-method",SIGNATURE_METHOD); xBili.put("x-bili-signature-nonce", UUID.randomUUID().toString()); xBili.put("x-bili-signature-version","1.0"); xBili.put("x-bili-timestamp",String.valueOf(System.currentTimeMillis()/1000)); // load x-bili-* to a [String] & load into [headers] StringBuilder xBiliStr = new StringBuilder(); for (Map.Entry next : xBili.entrySet()) {      connection.setRequestProperty(next.getKey(), next.getValue());      xBiliStr.append(next.getKey()).append(":").append(next.getValue()).append("\n"); } xBiliStr.deleteCharAt(xBiliStr.length()-1);

接下来是对获取到的签名进行hmacSHA256加密,其中需要注意字节数组转为要转为HEX十六进制形式,只有一位要补0。对于java的Mac(message authorization code)需要三部实现:Mac.getInstance("HmacSHA256")获取实例、mac.init(SecretKeySpec(secret,"HmacSHA256"))对实例初始化密钥、mac.doFinal(signStr)加密。即可实现获取byte数组,最后通过简单的转换算法即可获得sign签名。

// AuthorizationString hmacSha56 = "HmacSHA256";Mac hmac = Mac.getInstance(hmacSha56);SecretKeySpec secretKey = new SecretKeySpec(ACCESS_KEY_SECRET.getBytes(StandardCharsets.UTF_8),hmacSha56);hmac.init(secretKey);byte[] arr = hmac.doFinal(xBiliStr.toString().getBytes(StandardCharsets.UTF_8));StringBuilder sb = new StringBuilder();for (byte item : arr) {sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));}connection.setRequestProperty("Authorization",sb.toString().toLowerCase());

最后,设置输出流获取输入流,第一部分就算完成了。后续可以定义一些常用的接口接入的方法入口进行快捷操作。

// SET CONTENTconnection.setDoOutput(true);connection.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8));// GET RESULTBufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));// only one line, so write this wayreturn br.readLine();

对于bilibili直播接入,首先我们需要获得websocket_info长链信息,可以通过接口 /v2/app/start 获得,需要code(自己直播间的身份码)和appid即可获得包含该信息的json数据。websocket_info包含了auth_body和wss_link两个个部分。我们可以将其封装进一个实体类方便后续调用:

public static BasicConnectInfo start(){String start = "{\"code\":\""+CID+"\",\"app_id\":"+PROJECT_ID+"}";String tmp = null;try {tmp = commonRequest("/v2/app/start", start);//上述的签名认证函数} catch (Exception e) {e.printStackTrace();}// handle the resultif (tmp == null){System.err.println("[start] \t read line from INPUT - NULL");return null;}JSONObject jsonObject = new JSONObject(tmp);int code = jsonObject.getInt("code");if (code != 0) {System.err.println(tmp);return null;} else {JSONObject data = jsonObject.getJSONObject("data");JSONArray jsonArray = data.getJSONObject("websocket_info").getJSONArray("wss_link");String gameID = data.getJSONObject("game_info").getString("game_id");String authBody = data.getJSONObject("websocket_info").getString("auth_body");List wssLink = new ArrayList<>();for (int i = 0; i < jsonArray.length(); i++) {wssLink.add(jsonArray.getString(i));}BasicConnectInfo info = new BasicConnectInfo();info.setGameID(gameID);info.setAuthBody(authBody);info.setWssLink(wssLink);return info;}}

对于第二步

由第一步我们获得了长链的信息,接下来我们前端需要获取该信息,并且在连接建立的时候将该信息发送给长连服务,通过鉴定成功后,每过30s还需要发送一个心跳包进行双向保活确认,否则将会被断开。这部分主要战场在前端。对于传输的协议,官方已经给出图解:

首先需要构建符合协议的pack包,方便后续其它信息的交互。

function reqPack(op,body){let buffer = new ArrayBuffer(1500)let view = new DataView(buffer,0)let len = 4view.setInt16(len,16);len += 2  //  Header Length   [default:16]view.setInt16(len, 1);len += 2  //  Version [0:raw, 2:zlib]view.setInt32(len, op);len += 4  //  Operation [OP_AUTH:7, HEART_BEAT:2]view.setInt32(len, 0);len += 4  //  Sequence ID [remain (ignore)]let arr = strUTF8Encode(body)for (let i = 0; i < arr.length; i++,len++) {view.setInt8(len, arr[i]);}view.setInt32(0, len)return buffer.slice(0, len)}

可能会注意到strUTF8Encode函数,是自定义的将字符串进行编码得到byte[]的函数,参考了网上比较高效的处理方式,即通过encodeURIComponent编码后去掉%后将后面两个十六进制字符合并转场byte即可:

function strUTF8Encode(str){let s = encodeURIComponent(str)let arr = []for (let i = 0; i < s.length; i++) {let tmp = s.charAt(i)if (tmp === "%") {let hex = s.charAt(i+1) + s.charAt(i+2)let val = parseInt(hex,16)arr.push(val)i += 2} else {arr.push(tmp.charCodeAt(0))}}return arr}

接下来就是利用js的原生websocket创建连接并且发送心跳包、接受回参的流程了。对于部分代码需要解释一下,首先对于心跳包,我采用的是setInterval函数操作,里面添加了一个mutex变量即success,如果长链的auth包正确,那么success不会改变,表明可以不断发送心跳包,否则说明建立失败,后续也不需要发送心跳包了,所以销毁该interval,并且在连接主动关闭(可能断网等情况)的时候,也需要将success设置为false,因为连接已经断开了,除非在断开时进行重连操作,不过我这里没有重连尝试了。

firstUsed也是一个锁,是当我们进行第一次auth后,后续无需再auth,那么收到的数据必然不会是认证包。(总感觉设计上有点问题,处理方式还是太杂乱了,虽然有用函数处理不同结果,看到一连串的右括号依然觉得难受!)

function wwsServiceRun(wssLink0,bodyStr0){let websocket = nulllet firstUsed = false   // if not first meetif ("WebSocket" in window){websocket = new WebSocket(wssLink0)let success = truewebsocket.onopen = function (ev) {console.log("connect")websocket.send(reqPack(7,bodyStr0))let timer = setInterval(function () {if (success) {websocket.send(reqPack(2,""))} else {this.close(timer)}},30000)}websocket.onmessage = function (ev){if (typeof ev.data == "string") console.log(ev.data)else {let blob = new Blob([ev.data.slice(16,ev.data.byteLength)],{type:"application/text"})let txt = new FileReader()txt.readAsText(blob)txt.onload = function (){if (!firstUsed){let meet = JSON.parse(txt.result)if (meet.code !== 0){success = false} else {firstUsed = true}} else {// if === 1 ,that means a heartbeatif (txt.result.charCodeAt(3) !== 1){handleData(txt.result)}}}}}websocket.onerror = function (ev){ console.log("err") }websocket.onclose = function (ev){console.log("close")success = false}window.onbeforeunload = function (){ websocket.close() }} else alert("this browser does not support websocket")}

对于阿b,当长链建立成功后,当弹幕和礼物等来袭会主动推送带CMD的json数据(二进制),我们使用switch进行分流到不同函数处理即可。

function handleData(raw){let jsonRes =JSON.parse(raw)let data = jsonRes.dataswitch (jsonRes.cmd) {case "LIVE_OPEN_PLATFORM_DM": DM(data);breakcase "LIVE_OPEN_PLATFORM_SEND_GIFT":GIFT(data);break;case "LIVE_OPEN_PLATFORM_SUPER_CHAT":break;case "LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL":break;case "LIVE_OPEN_PLATFORM_GUARD":break;default: console.log(j)}}

至于之后的业务逻辑代码,就看自己的需求进行改变了。这里不多赘述。

更多扩展

既然说到长链,而且这也是自己第一次解除长链,肯定不能只会接受这么简单,肯定也要学会如何创建一个ws服务。于是有了接下来的部分。

首先依然使用的是springboot,我们需要引入依赖 spring-boot-starter-websocket ,然后添加配置类:

@Configurationpublic class WebSocketConfig {  // 会暴露所有的端点服务    @Bean    public ServerEndpointExporter serverEndpointExporter(){        return new ServerEndpointExporter();    }  // 如果websocket中想要调用业务,如果再其内部直接autowired获取到的是null,只能主动赋值给已经定义了的static变量    @Autowired    public void setAcountService(AccountService service){        CommonWebSocket.service = service;    }}

之后编写业务逻辑(不做筛选关键代码了):

@Component@Slf4j@ServerEndpoint("/commonWebSocket")public class CommonWebSocket {    private Session session;    public static AccountService service;    private final CopyOnWriteArraySet webSockets = new CopyOnWriteArraySet<>();    @OnOpen    public void onOpen(Session session){        this.session = session;        webSockets.add(this);    }    @OnMessage    public void onMessage(ByteBuffer bb){        int pakLen = bb.getInt(0);        int headerLen = bb.getChar(4);        int len = pakLen - headerLen;        /* op            0:  user create            1:  game msg            2:  user heartbeat            3:  user info upgrade # NOT SUPPORT NOW            4:  list give         */        int op = bb.getInt(8);        String msg = new String(bb.array(),headerLen,len,StandardCharsets.UTF_8);        JSONObject json = new JSONObject(msg);//        USER CREATE        if (op == 0) {            //uid,name,avatar            long uid = json.getLong("uid");            String name = json.getString("name");            String avatar = json.getString("avatar");            if (!service.exist(uid).getData()){                service.create(new Account(uid,name,avatar,10));            }        }//        GAME PLAY        if (op == 1){            //uid,delta            long uid = json.getLong("uid");            int delta = json.getInt("delta");            service.deltaUpgrade(delta,uid);        }        sendForList();    }    private void sendForList(){//        COMMON LIST PUSH        List data = service.selectSort().getData();        JSONObject res = new JSONObject();        res.put("code",100);        res.put("data",data);        sendMessage(4, res.toString());    }    public void sendMessage(int op, String body){        for (CommonWebSocket webSocket : webSockets) {            try {                webSocket.session.getBasicRemote().sendBinary(pack(op,body));            } catch (IOException e) {                e.printStackTrace();            }        }    }    private ByteBuffer pack(int op,String body){        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);        int totalLen = 16 + bytes.length;        ByteBuffer buffer = ByteBuffer.allocate(totalLen);        int index = 0;        buffer.putInt(index, totalLen);index+=4;        buffer.putChar(index, (char) 16);index+=2;        buffer.putChar(index, (char) 1);index+=2;        buffer.putInt(index,op);index+=4;        buffer.putInt(index,0);index+=4;        for (byte tmp : bytes) {            buffer.put(index++,tmp);        }        return buffer;    }    @OnClose    public void onClose(){        webSockets.remove(this);        log.info("[websocket]\tconnect exit");    }}

总结

之前总结都是突兀的在文章后面换一行总结,现在想想那样和正文难以分清,所以总结也加一个大标题吧!

通过这次对接bilibili的接口,第一次的接触了长链的创建和使用方式,虽然在之前的tcp/ip课程中有过类似的装包操作,但是那时候包的设计可扩展性和设计的数据安全性不高,这次学习了阿b的协议设计方式,感觉确实不错(包含总包长、包头长、版本号、操作码、保留位、实体消息),估计也会沿用到我的后续设计之中。当然,我也成功的通过接入该开放平台,实现了全民弹幕互动扫雷的项目,虽然没啥人来测试(;´д`)ゞ

哦,还有,springboot切换数据库真的是非常方便,我一开始连接的mysql,配置依赖时候用的是mysql的connect,后来由于个人业务需求,不得不换sqlite,结果发现只需要改变依赖为sqlite-jdbc然后取消掉账号密码的设置即可,非常方便欸!

好了,今天的总结就到这里,溜了溜了,今晚原神版本直播还没看,现在回去补看了,拜拜,下次见!

上一篇:

下一篇:

X 关闭

热门推荐

接入bilibili直播开放平台

2022-12-30   腾讯云

实力出圈,赛诺秀捧回追光大赏双项大奖

2022-12-30   中国产业经济信息网

午饭团队_午饭团-今日最新

2022-12-30   万能网

北京成文旅融合榜单“领头羊”

2022-12-30   北京青年报官网

餐饮老字号备足腊八粥、腊八蒜迎客

2022-12-30   北京商报官方账号

前沿热点:76火线魔盒官网

2022-12-30   万能网

基于Hexo搭建静态博客

2022-12-30   腾讯云

金立v9游戏手机广告_金立v9

2022-12-30   万能网

元旦小长假,天气给力宜出行-世界通讯

2022-12-30   北京日报客户端

北京电信千兆小区覆盖率达80% 环球热议

2022-12-30   北京商报官方账号

谢谢,高龄独居老人的守护者!

2022-12-30   北京日报客户端

超级解霸2001_超级解霸2010

2022-12-30   万能网

【2022年终报道】寻趣绿野

2022-12-30   新华网

1180年什么朝代_11808:百事通

2022-12-30   万能网

京唐城际铁路今日开通运营

2022-12-30   北京青年报官网

米app成仁Tik Tok

2022-12-29   万能网

金声古筝

2022-12-29   万能网

生以啜芳华,行而沐春光

2022-12-29   中国产业经济信息网

redis总结

2022-12-29   腾讯云

报道:北京东城区北极阁路全线贯通通车

2022-12-29   北京青年报官网

护国寺小吃起源店推出二十种粥料迎腊八

2022-12-29   北京青年报官网

北京12月30日晚高峰出行集中 交通压力大

2022-12-29   北京青年报官网

请上潮汇聚国潮服饰,助力国潮焕发新生机!

2022-12-29   中国产业经济信息网

全球讯息:玲珑女_玲彩

2022-12-29   万能网

2022年我国竞技体育成绩优异

2022-12-29   人民日报

“阳”了,先别急着奔医院

2022-12-29   央广网

北京电信营业厅爱心翼站已达137家

2022-12-29   北京商报官方账号

北京买房:理清思路,购房建议696

2022-12-29   章哥说买房

中国电气装备举办青年创新创意大赛 当前速递

2022-12-29   中国产业经济信息网

天天微资讯!长安汽车发布全新用户品牌

2022-12-29   中国产业经济信息网

中国建材集团建成非洲最大玻纤基地_最新快讯

2022-12-29   中国产业经济信息网

三峡集团在鲁首个海上风电项目全容量并网

2022-12-29   中国产业经济信息网

全球快看点丨智能制造赋能云锡高质量发展

2022-12-29   中国产业经济信息网

用关爱温暖快递小哥:天天快讯

2022-12-29   中国经济网

平谷区第十六届冰雪季开幕|全球头条

2022-12-29   北京青年报官网

政策实打实 服务心贴心

2022-12-29   金台资讯

北京石景山区将新建20000平方米公租房

2022-12-28   北京青年报官网

侪辈什么意思_侪联

2022-12-28   万能网

Copyright   2015-2022 中国行业信息网版权所有  备案号:   联系邮箱:29 59 11 57 8@qq.com