【flutter / dart 版本】Websocket获取B站直播间弹幕教程——基于B站直播开发平台

教程

B站直播开发平台弹幕获取教程01

代码

1、引入相关库

dependencies:
  crypto: ^3.0.3
  uuid: ^4.1.0
  dio: ^5.3.3
  archive: ^3.3.7

2、创建bili_project.dart

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();
  }
}

3、新建 bili_websocket.dart

注意:解压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));
  }
}

4、使用

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的heartbeatend方法都会报错
但这没有关系,start方法能正常获得弹幕服务信息,就可以获得个人直播间弹幕了。

你可能感兴趣的:(其他,flutter,dart,bilibili,websocket,弹幕,直播开放平台)