B站直播开发平台弹幕获取教程01
dependencies:
crypto: ^3.0.3
uuid: ^4.1.0
dio: ^5.3.3
archive: ^3.3.7
import 'dart:convert';
import 'package:bili_websocket.dart';
import 'package:crypto/crypto.dart';
import 'package:uuid/uuid.dart';
import 'package:dio/dio.dart';
class BiliOpenApi{
static const Uuid myUuid = Uuid();
static Dio dio = Dio();
static const String host = "https://live-open.biliapi.com";
late int _appId;
late String _accesskey;
late String _accessSecret;
BiliOpenApi(this._appId, this._accesskey, this._accessSecret);
///开始项目
Future<Response> start(String code) async {
Map data = {
'code': code,
'app_id': _appId
};
return _post("/v2/app/start", data);
}
///项目心跳
Future<Response> heartbeat(String gameId) async {
Map data = {
'game_id': gameId
};
return _post("/v2/app/heartbeat", data);
}
///批量发送心跳
Future<Response> batchHeartbeat(List<String> gameId) async {
Map data = {
'game_ids': gameId
};
return _post("/v2/app/batchHeartbeat", data);
}
///结束项目
Future<Response> end(String gameId) async {
Map data = {
'app_id': _appId,
'game_id': gameId
};
return _post("/v2/app/end", data);
}
//自定义POST请求
Future<Response> _post(String url ,Map data) async {
String body = json.encode(data);
Options options = Options();
///请求header的配置
options.contentType="application/json";
options.headers = _headers(body);
return await dio.post('$host$url',options:options, data: body);
}
/// 添加B站请求头
Map<String,String> _headers(String body){
int timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
print(timestamp);
Map<String,String> headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'x-bili-content-md5': md5.convert(utf8.encode(body)).toString(),
'x-bili-timestamp': timestamp.toString(),
'x-bili-signature-method': 'HMAC-SHA256',
'x-bili-signature-nonce': myUuid.v4(),
'x-bili-accesskeyid': _accesskey,
'x-bili-signature-version': '1.0',
'Authorization': ''
};
headers['Authorization'] = _generateSignature(headers);
return headers;
}
/// B站请求头签名
String _generateSignature(Map<String,String> headers){
String? contentMd5 = headers['x-bili-content-md5'];
String? nonce = headers['x-bili-signature-nonce'];
String? timestamp = headers['x-bili-timestamp'];
String sginText = "x-bili-accesskeyid:$_accesskey\nx-bili-content-md5:$contentMd5\nx-bili-signature-method:HMAC-SHA256\nx-bili-signature-nonce:$nonce\nx-bili-signature-version:1.0\nx-bili-timestamp:$timestamp";
List<int> key = utf8.encode(_accessSecret);
List<int> message = utf8.encode(sginText);
return Hmac(sha256, key).convert(message).toString();
}
}
注意:解压zip数据部分,我没办法测试,比较没这么大量,请自行测试
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:archive/archive.dart';
///根据start的响应 打开websocket
Future<void> openApiWsByStartResp(Response response) async {
var data = response.data['data'];
var anchorInfo = data['anchor_info'];//个人信息
var gameInfo = data['game_info'];//项目游戏场次
var websocketInfo = data['websocket_info'];
var authBody = websocketInfo['auth_body'];//鉴权数据
var wssLink = websocketInfo['wss_link'];//弹幕服务地址
//打开websocket
openApiWs(wssLink[0], authBody);
}
///打开websocket
Future<void> openApiWs(String url,String authBody) async {
print("连接websocket地址:$url");
var socket = await WebSocket.connect(url);
if(socket.readyState == WebSocket.open){
print("发送鉴权包");
socket.add(_authPack(authBody));
// 发送心跳包
sendHeartbeatPack(socket);
//监听
socket.listen((data) {
//解包
unpack(convertUint8ArrayViewToUint8List(data));
}, onDone: () {
print('WebSocket断开');
},onError: (e){
print("服务异常:$e");
});
}
}
///将Websocket响应转为Uint8List
Uint8List convertUint8ArrayViewToUint8List(dynamic view) {
final list = Uint8List(view.length);
for (var i = 0; i < view.length; i++) {
list[i] = view[i];
}
return list;
}
///Operation:消息的类型: int32:四个字节
const int optCodeHeartbeat = 2; //客户端发送的心跳包(30秒发送一次)
const int optCodeHeartbeatReply = 3; //服务器收到心跳包的回复
const int optCodeSendSmsReply = 5; //服务器推送的弹幕消息包
const int optCodeAuth = 7; //客户端发送的鉴权包(客户端发送的第一个包)
const int optCodeAuthReply = 8; //服务器收到鉴权包后的回复
///生成鉴权包
List<int> _authPack(String authBody){
return _pack(authBody, optCodeAuth);
}
///生成心跳包
List<int> _HeartbeatPack(){
return _pack("", optCodeHeartbeat);
}
/// 定时每30秒发送心跳包
sendHeartbeatPack(WebSocket webSocket){
Timer.periodic(Duration(seconds: 30), (Timer t){
if (webSocket.readyState == WebSocket.open) {
webSocket.add(_HeartbeatPack());
print("发送心跳");
}
});
}
///封包
List<int> _pack(String body,int optCode){
List<int> result = [];
List<int> bodyBytes = [];
if(body != ""){
print("鉴权body:$body");
bodyBytes = utf8.encode(body);
}
ByteData bd = ByteData(16);
//Packet Length:整个Packet的长度,包含Header int32:四个字节
bd.setInt32(0, 16+bodyBytes.length);
// Header Length:Header的长度,固定为16。int16:两个个字节
bd.setInt16(4, 16);
//Version:int16:两个个字节
//如果Version=0,Body中就是实际发送的数据。
//如果Version=2,Body中是经过压缩后的数据,请使用zlib解压,然后按照Proto协议去解析。
bd.setInt16(6, 0);
bd.setInt32(8, optCode);
//Sequence ID:保留字段,可以忽略。int32:四个字节
bd.setInt32(12, 1);
for (int i = 0; i < bd.lengthInBytes; i++) {
result.add(bd.getUint8(i));
}
result.addAll(bodyBytes);
return result;
}
///解包
void unpack(Uint8List uint8list){
ByteData byteData = ByteData.sublistView(uint8list);
int packLength = byteData.getInt32(0);//包大小
//int headerLength = byteData.getInt16(4);//头部长度
int version = byteData.getInt16(6);//版本:判断是否压缩包. 2为压缩包
int optCode = byteData.getInt32(8);//操作码
//int sequence = byteData.getInt32(12);//Sequence ID:保留字段,可以忽略。int32:四个字节
print("包大小:$packLength,版本:$version,操作码:$optCode");
Uint8List bodyList = uint8list.sublist(16, packLength);
//如果是压缩包,就再解包
if(version == 2){
//Deflate解压没测试过,我直播间没有这么大的量
bodyList = Uint8List.fromList(Deflate(bodyList).getBytes());
unpack(bodyList);
return;
}
if(optCode == optCodeHeartbeatReply){
print('收到服务器心跳');
return;
}
String bodyStr = utf8.decode(bodyList);
Map<String, dynamic> BodyMap = json.decode(bodyStr);
if(optCode == optCodeAuthReply && BodyMap["code"] == 0){
print("鉴权成功");
}
//弹幕消息
if(optCode == optCodeSendSmsReply){
//获得真正的消息
String cmdCode = BodyMap["cmd"];
switch(cmdCode){
case "NOTICE_MSG":{
//print('通知消息:$bodyStr');
}
case "STOP_LIVE_ROOM_LIST":{
//print('离开房间ID列表:$bodyStr');
}
default:{
print('弹幕数据类型:$cmdCode');
Map<String, dynamic> dataMap = BodyMap["data"];
print('弹幕数据:$dataMap');
//todo
//你自定义处理数据方法
}
}
}
//只有压缩包,存在这种情况
if(packLength < uint8list.lengthInBytes){
unpack(uint8list.sublist(packLength));
}
}
void main(){
BiliOpenApi bp = BiliOpenApi(你的应用ID,你的Access_key,你的Access_Secret);
//调用项目start方法,获取弹幕服务信息
Future<Response> start = bp.start(你的身份码);
start.then((response) => {
//根据弹幕服务信息 打开Websocket
openApiWsByStartResp(response)
});
}
参数获取 | |
---|---|
Access_key 和 Access_Secret | 去B站直播开放平台注册申请个人开发者 |
应用ID | 成为个人开发者后,在直播开放平台创建应用后,就能获得应用ID |
身份码 | 登录B站直播间找到幻星-互动玩法,在里面就能找到身份码 |
注意:
项目没有上架前,调用BiliOpenApi的start方法不能获得场次ID,所以调用BiliOpenApi的heartbeat和end方法都会报错。
但这没有关系,start方法能正常获得弹幕服务信息,就可以获得个人直播间弹幕了。