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

教程

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

代码

1、引入相关库

dependencies:crypto: ^3.0.3uuid: ^4.1.0dio: ^5.3.3archive: ^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'];//弹幕服务地址//打开websocketopenApiWs(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) => {//根据弹幕服务信息 打开WebsocketopenApiWsByStartResp(response)});
}
参数获取
Access_key 和 Access_Secret去B站直播开放平台注册申请个人开发者
应用ID成为个人开发者后,在直播开放平台创建应用后,就能获得应用ID
身份码登录B站直播间找到幻星-互动玩法,在里面就能找到身份码

注意:
项目没有上架前,调用BiliOpenApi的start方法不能获得场次ID,所以调用BiliOpenApi的heartbeatend方法都会报错
但这没有关系,start方法能正常获得弹幕服务信息,就可以获得个人直播间弹幕了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/101066.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Centos中清除因程序异常终止,导致的残留的Cache/buff_drop_caches命令---linux工作笔记063

我这里因为nifi程序背压设置的不合理,导致,内存和CPU消耗过高,系统崩溃,但是重启NIFI以后,发现 对应的执行top命令,看到,系统的buff/cache 依然没有减少,说明内存被浪费了,残留在这里没有被回收. 用这个办法执行这个命令; linux会自动触发清理,但是只有在内存不够用的时候才会…

SQLite Autoincrement特性

摘要 AUTOINCREMENT会使用额外的CPU、内存和磁盘空间&#xff0c;如不是特别需要尽量不要使用。类型为INTEGER PRIMARY KEY的列会被作为rowid&#xff08;WITHOUT ROWID表除外&#xff09;&#xff0c;并且是一个8字节有符号整数。执行INSERT操作时如果未提供rowid&#xff0c…

【AN-Animate教程——熟悉工作区】

【AN-Animate教程——熟悉工作区】 初始页面创建舞台主舞台界面其他常用板块 本篇内容&#xff1a;Animate用途 重点内容&#xff1a;熟悉工作区&#xff0c;以及基本操作 工 具&#xff1a;Adobe Animate 2022 初始页面 在初始页面当中&#xff0c;我们可以看到一个忍者和一个…

Mainflux IoT:Go语言轻量级开源物联网平台,支持HTTP、MQTT、WebSocket、CoAP协议

Mainflux是一个由法国的创业公司开发并维护的安全、可扩展的开源物联网平台&#xff0c;使用 Go语言开发、采用微服务的框架。Mainflux支持多种接入设备&#xff0c;包括设备、用户、APP&#xff1b;支持多种协议&#xff0c;包括HTTP、MQTT、WebSocket、CoAP&#xff0c;并支持…

Redis根据中心点坐标和半径筛选符合的数据

目录 1.启动Redis​编辑 2.导入maven依赖 3.添加redis配置 4.编写RedisService 5.使用 6.验证 1.启动Redis 2.导入maven依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifac…

uniapp搭建项目

使用Vue3/Vite版 创建以 javascript 开发的工程&#xff08;如命令行创建失败&#xff0c;请直接访问 gitee 下载模板&#xff09; npx degit dcloudio/uni-preset-vue#vite my-vue3-project复制代码 npx degit dcloudio/uni-preset-vue#vite-alpha my-vue3-project复制代码创…

pnpm、npm、yarn 包管理工具『优劣对比』及『环境迁移』

前言 博主在开发前端网站的时候&#xff0c;发现随着开发的项目的逐渐增多&#xff0c;安装的依赖包越来越臃肿&#xff0c;依赖包的安装速度也是非常越来越慢&#xff0c;多项目开发管理也是比较麻烦。之前我就了解过 pnpm&#xff0c;但是当时担心更换包管理环境可能会出现的…

[modern c++] 函数式编程与 std::ref

参考&#xff1a; std::ref, std::cref - cppreference.comhttps://en.cppreference.com/w/cpp/utility/functional/ref 正文&#xff1a; 如果不涉及函数式编程&#xff0c;那么基本上不需要使用到 std::ref &#xff0c; 这个功能式是用来解决函数式编程时入参只能进行值传…

openlayers图层数据覆盖

openlayers图层数据覆盖 openlayer 有两个图层A和B 图层A 覆盖 图层B 在 OpenLayers 中&#xff0c;可以通过添加不同的图层来实现图层的覆盖效果。要使图层A覆盖图层B&#xff0c;你可以按照以下步骤进行操作&#xff1a; 1 创建地图容器&#xff1a; var map new ol.Map…

【思维构造】Dominant Character—CF1605C

Dominant Character—CF1605C 思路 若字符串中存在相邻的 a 字符&#xff0c;即存在 aa 子串。输出 2 2 2。若字符串中不存在相邻的 a 字符。那么满足条件的子串的格式一定是 a xx a ... a xx a&#xff0c;其中 xx 代表由 b 和 c 组成的字符串。那么显然可以对 a 的数…

什么是广电入库

什么是广电入库 入库&#xff0c;就意味着该节目在广播电视媒体机构可随时随地被调用&#xff0c;可任意安排播出时间和播出频率。入库的节目可实现一次制作&#xff0c;无限次使用&#xff0c;大大提高节目制作效率。同时&#xff0c;入库的节目因为具有统一的标准和规范&…

使用poi-tl循环导出word报表

先看模板和导出的效果 模板 效果 根据模板循环生成表格&#xff0c;每个表格再循环填充数据&#xff0c;也就是两层循环&#xff0c;第一层循环是学生学期信息&#xff0c;第二层循环是学生的成绩数据。 第一个循环 {{?listTable}} {{/}}第二个循环 {{reportList}} 表格…

Python 何时传的是值,何时传的是引用?

在 Python 中&#xff0c;参数的传递方式可以是传值&#xff08;pass by value&#xff09;或传引用&#xff08;pass by reference&#xff09;。然而&#xff0c;实际上 Python 中的参数传递方式是通过对象的引用进行传递。简单来说&#xff0c;当传递不可变对象时&#xff0…

KWin、libdrm、DRM从上到下全过程 —— drmModeAddFBxxx(4)

接前一篇文章:KWin、libdrm、DRM从上到下全过程 —— drmModeAddFBxxx(3) 上一回讲解了drm_mode_addfb2函数中的第一步暨第一个函数drm_core_check_feature。本回继续对于drm_mode_addfb2函数往下进行解析。为了便于理解,再次贴出其代码,在drivers/gpu/drm/drm_framebuffe…

电脑图片jpeg怎么转jpg格式?jpeg和jpg的转换方法

很多平台对上传的图片格式都有严格的要求&#xff0c;当我们遇到图片格式不对的时候&#xff0c;就需要改图片格式了&#xff0c;下面以jpeg转jpg&#xff08;在线图片格式转换器&#xff08;jpg、png、gif、webp、bmp、tiff&#xff09;-压缩图&#xff09;为例子&#xff0c;…

SpringBoot项目整合

一、创建项目 IDEA中采用spring initialzer...创建&#xff0c;jdk选择8&#xff0c;maven,jar。。。springboot版本2.5.0&#xff08;稳定&#xff09; 项目依赖&#xff1a; 二、项目结构&#xff1a; 原始pom.xml文件 <?xml version"1.0" encoding"UT…

Android网络监听

1.通过注册BroadCastReceiver进行网络监听。 1) 添加网络权限 <uses-permission android:name"android.permission.INTERNET"/><uses-permission android:name"android.permission.ACCESS_NETWORK_STATE" /> 2&#xff09;定义BroadCastRe…

tcpdump(五)命令行参数讲解(四)

一 案例讲解 tcpdump官方参考文档 最全的tcpdump手册 强调&#xff1a; -nn 选项一般是must 必选 ① 现场分析并保留现场信息 tcpdump -l | tee dat 使用tee来把tcpdump的输出同时放到文件dat和标准输出中场景&#xff1a; 自己现场分析同时把现场信息保留下来 ② …

朋友圈一键转发(可修改文案),无需多个账号复制粘贴

相信很多人手上都有不止一个微信&#xff0c;每次发个朋友圈都要在多个账号切换&#xff0c;重复发送&#xff0c;好不麻烦。而一些朋友想要一键跟随转发朋友圈&#xff0c;却总是需要一个个复制粘贴&#xff0c;麻烦而且容易漏发。 那实现朋友圈一键跟随转发&#xff0c;无需多…

wifi管理软件 WiFi Signal mac中文介绍

WiFi Signal mac是一款WiFi信号强度监测工具&#xff0c;它可以帮助用户实时监测WiFi信号的强度、频率、噪声等信息&#xff0c;并提供详细的图表和统计数据。 WiFi Signal可以自动扫描附近的WiFi网络&#xff0c;并显示它们的信号强度和频率。用户可以通过WiFi Signal来找到最…