feat: 直播弹幕

This commit is contained in:
guozhigq
2024-08-18 23:30:51 +08:00
parent 0803444d74
commit 91856b5c21
9 changed files with 876 additions and 20 deletions

View File

@ -29,6 +29,7 @@ class Request {
late String systemProxyPort;
static final RegExp spmPrefixExp =
RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
static late String buvid;
/// 设置cookie
static setCookie() async {
@ -70,6 +71,8 @@ class Request {
final String cookieString = cookie
.map((Cookie cookie) => '${cookie.name}=${cookie.value}')
.join('; ');
buvid = cookie.firstWhere((e) => e.name == 'buvid3').value;
dio.options.headers['cookie'] = cookieString;
}

View File

@ -65,4 +65,23 @@ class LiveHttp {
};
}
}
// 获取弹幕信息
static Future liveDanmakuInfo({roomId}) async {
var res = await Request().get(Api.getDanmuInfo, data: {
'id': roomId,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -0,0 +1,101 @@
class LiveMessageModel {
// 消息类型
final LiveMessageType type;
// 用户名
final String userName;
// 信息
final String? message;
// 数据
final dynamic data;
final String? face;
final int? uid;
final Map<String, dynamic>? emots;
// 颜色
final LiveMessageColor color;
LiveMessageModel({
required this.type,
required this.userName,
required this.message,
required this.color,
this.data,
this.face,
this.uid,
this.emots,
});
}
class LiveSuperChatMessage {
final String backgroundBottomColor;
final String backgroundColor;
final DateTime endTime;
final String face;
final String message;
final String price;
final DateTime startTime;
final String userName;
LiveSuperChatMessage({
required this.backgroundBottomColor,
required this.backgroundColor,
required this.endTime,
required this.face,
required this.message,
required this.price,
required this.startTime,
required this.userName,
});
}
enum LiveMessageType {
// 普通留言
chat,
// 醒目留言
superChat,
//
online,
// 加入
join,
// 关注
follow,
}
class LiveMessageColor {
final int r, g, b;
LiveMessageColor(this.r, this.g, this.b);
static LiveMessageColor get white => LiveMessageColor(255, 255, 255);
static LiveMessageColor numberToColor(int intColor) {
var obj = intColor.toRadixString(16);
LiveMessageColor color = LiveMessageColor.white;
if (obj.length == 4) {
obj = "00$obj";
}
if (obj.length == 6) {
var R = int.parse(obj.substring(0, 2), radix: 16);
var G = int.parse(obj.substring(2, 4), radix: 16);
var B = int.parse(obj.substring(4, 6), radix: 16);
color = LiveMessageColor(R, G, B);
}
if (obj.length == 8) {
var R = int.parse(obj.substring(2, 4), radix: 16);
var G = int.parse(obj.substring(4, 6), radix: 16);
var B = int.parse(obj.substring(6, 8), radix: 16);
//var A = int.parse(obj.substring(0, 2), radix: 16);
color = LiveMessageColor(R, G, B);
}
return color;
}
@override
String toString() {
return "#${r.toRadixString(16).padLeft(2, '0')}${g.toRadixString(16).padLeft(2, '0')}${b.toRadixString(16).padLeft(2, '0')}";
}
}

View File

@ -1,10 +1,16 @@
import 'dart:convert';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/message.dart';
import 'package:pilipala/models/live/quality.dart';
import 'package:pilipala/models/live/room_info.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_socket/index.dart';
import 'package:pilipala/utils/live.dart';
import '../../models/live/room_info_h5.dart';
import '../../utils/storage.dart';
import '../../utils/video_utils.dart';
@ -24,6 +30,13 @@ class LiveRoomController extends GetxController {
int? tempCurrentQn;
late List<Map<String, dynamic>> acceptQnList;
RxString currentQnDesc = ''.obs;
Box userInfoCache = GStrorage.userInfo;
int userId = 0;
PlSocket? plSocket;
List<String> danmuHostList = [];
String token = '';
// 弹幕消息列表
RxList<LiveMessageModel> messageList = <LiveMessageModel>[].obs;
@override
void onInit() {
@ -43,6 +56,11 @@ class LiveRoomController extends GetxController {
}
// CDN优化
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);
final userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) {
userId = userInfo.mid;
}
liveDanmakuInfo().then((value) => initSocket());
}
playerInit(source) async {
@ -127,4 +145,64 @@ class LiveRoomController extends GetxController {
.description;
await queryLiveInfo();
}
Future liveDanmakuInfo() async {
var res = await LiveHttp.liveDanmakuInfo(roomId: roomId);
if (res['status']) {
danmuHostList = (res["data"]["host_list"] as List)
.map<String>((e) => '${e["host"]}:${e['wss_port']}')
.toList();
token = res["data"]["token"];
return res;
}
}
// 建立socket
void initSocket() async {
final wsUrl = danmuHostList.isNotEmpty
? danmuHostList.first
: "broadcastlv.chat.bilibili.com";
plSocket = PlSocket(
url: 'wss://$wsUrl/sub',
heartTime: 30,
onReadyCb: () {
joinRoom();
},
onMessageCb: (message) {
final List<LiveMessageModel>? liveMsg =
LiveUtils.decodeMessage(message);
if (liveMsg != null) {
messageList.addAll(liveMsg
.where((msg) => msg.type == LiveMessageType.chat)
.toList());
}
},
onErrorCb: (e) {
print('error: $e');
},
);
await plSocket?.connect();
}
void joinRoom() async {
var joinData = LiveUtils.encodeData(
json.encode({
"uid": userId,
"roomid": roomId,
"protover": 3,
"buvid": Request.buvid,
"platform": "web",
"type": 2,
"key": token,
}),
7,
);
plSocket?.sendMessage(joinData);
}
@override
void onClose() {
plSocket?.onClose();
super.onClose();
}
}

View File

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/live/message.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'controller.dart';
@ -16,7 +19,8 @@ class LiveRoomPage extends StatefulWidget {
State<LiveRoomPage> createState() => _LiveRoomPageState();
}
class _LiveRoomPageState extends State<LiveRoomPage> {
class _LiveRoomPageState extends State<LiveRoomPage>
with TickerProviderStateMixin {
final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
PlPlayerController? plPlayerController;
late Future? _futureBuilder;
@ -25,6 +29,9 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
bool isShowCover = true;
bool isPlay = true;
Floating? floating;
final ScrollController _scrollController = ScrollController();
late AnimationController fabAnimationCtr;
bool _shouldAutoScroll = true;
@override
void initState() {
@ -34,6 +41,13 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
}
videoSourceInit();
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
// 监听滚动事件
_scrollController.addListener(_onScroll);
fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
value: 0.0,
);
}
Future<void> videoSourceInit() async {
@ -41,12 +55,52 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
plPlayerController = _liveRoomController.plPlayerController;
}
void _onScroll() {
// 反向时,展示按钮
if (_scrollController.position.userScrollDirection ==
ScrollDirection.forward) {
_shouldAutoScroll = false;
fabAnimationCtr.forward();
} else {
_shouldAutoScroll = true;
fabAnimationCtr.reverse();
}
}
// 监听messageList的变化自动滚动到底部
@override
void didChangeDependencies() {
super.didChangeDependencies();
_liveRoomController.messageList.listen((_) {
if (_shouldAutoScroll) {
_scrollToBottom();
}
});
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController
.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
)
.then((value) {
_shouldAutoScroll = true;
// fabAnimationCtr.forward();
});
}
}
@override
void dispose() {
plPlayerController!.dispose();
if (floating != null) {
floating!.dispose();
}
_scrollController.dispose();
fabAnimationCtr.dispose();
super.dispose();
}
@ -80,20 +134,6 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
backgroundColor: Colors.black,
body: Stack(
children: [
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: 0.8,
child: Image.asset(
'assets/images/live/default_bg.webp',
fit: BoxFit.cover,
// width: Get.width,
// height: Get.height,
),
),
),
Obx(
() => Positioned(
left: 0,
@ -106,7 +146,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
.roomInfoH5.value.roomInfo?.appBackground !=
null
? Opacity(
opacity: 0.8,
opacity: 0.6,
child: NetworkImgLayer(
width: Get.width,
height: Get.height,
@ -116,7 +156,15 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
'',
),
)
: const SizedBox(),
: Opacity(
opacity: 0.6,
child: Image.asset(
'assets/images/live/default_bg.webp',
fit: BoxFit.cover,
// width: Get.width,
// height: Get.height,
),
),
),
),
Column(
@ -198,8 +246,45 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
child: videoPlayerPanel,
),
),
const SizedBox(height: 20),
// 显示消息的列表
buildMessageListUI(
context,
_liveRoomController,
_scrollController,
),
// 底部安全距离
SizedBox(
height: MediaQuery.of(context).padding.bottom + 20,
)
],
),
// 定位 快速滑动到底部
Positioned(
right: 20,
bottom: MediaQuery.of(context).padding.bottom + 20,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: ElevatedButton.icon(
onPressed: () {
_scrollToBottom();
},
icon: const Icon(Icons.keyboard_arrow_down), // 图标
label: const Text('新消息'), // 文字
style: ElevatedButton.styleFrom(
// primary: Colors.blue, // 按钮背景颜色
// onPrimary: Colors.white, // 按钮文字颜色
padding: const EdgeInsets.fromLTRB(14, 12, 20, 12), // 按钮内边距
),
),
),
),
],
),
);
@ -214,3 +299,153 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
}
}
}
Widget buildMessageListUI(
BuildContext context,
LiveRoomController liveRoomController,
ScrollController scrollController,
) {
return Expanded(
child: Obx(
() => MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
child: ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.black,
],
stops: [0.0, 0.1, 1.0],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: ListView.builder(
controller: scrollController,
itemCount: liveRoomController.messageList.length,
itemBuilder: (context, index) {
final LiveMessageModel liveMsgItem =
liveRoomController.messageList[index];
return Padding(
padding: EdgeInsets.only(
top: index == 0 ? 40.0 : 4.0,
bottom: 4.0,
left: 20.0,
right: 20.0,
),
child: Text.rich(
TextSpan(
style: const TextStyle(color: Colors.white),
children: [
TextSpan(
text: '${liveMsgItem.userName}: ',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 处理点击事件
print('Text clicked');
},
),
TextSpan(
children: [
...buildMessageTextSpan(context, liveMsgItem)
],
// text: liveMsgItem.message,
),
],
),
),
);
},
),
),
),
),
);
}
List<InlineSpan> buildMessageTextSpan(
BuildContext context,
LiveMessageModel liveMsgItem,
) {
final List<InlineSpan> inlineSpanList = [];
// 是否包含表情包
if (liveMsgItem.emots == null) {
// 没有表情包的消息
inlineSpanList.add(
TextSpan(
text: liveMsgItem.message ?? '',
style: const TextStyle(
shadows: [
Shadow(
offset: Offset(2.0, 2.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(-1.0, -1.0),
blurRadius: 3.0,
color: Colors.black,
),
],
),
),
);
} else {
// 有表情包的消息 使用正则匹配 表情包用图片渲染
final List<String> emotsKeys = liveMsgItem.emots!.keys.toList();
final RegExp pattern = RegExp(emotsKeys.map(RegExp.escape).join('|'));
liveMsgItem.message?.splitMapJoin(
pattern,
onMatch: (Match match) {
final emoteItem = liveMsgItem.emots![match.group(0)];
if (emoteItem != null) {
inlineSpanList.add(
WidgetSpan(
child: NetworkImgLayer(
width: emoteItem['width'].toDouble(),
height: emoteItem['height'].toDouble(),
type: 'emote',
src: emoteItem['url'],
),
),
);
}
return '';
},
onNonMatch: (String nonMatch) {
inlineSpanList.add(
TextSpan(
text: nonMatch,
style: const TextStyle(
shadows: [
Shadow(
offset: Offset(2.0, 2.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(-1.0, -1.0),
blurRadius: 3.0,
color: Colors.black,
),
],
),
),
);
return nonMatch;
},
);
}
return inlineSpanList;
}

View File

@ -0,0 +1,107 @@
import 'dart:async';
import 'package:pilipala/utils/live.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
enum SocketStatus {
connected,
failed,
closed,
}
class PlSocket {
SocketStatus status = SocketStatus.closed;
// 链接
final String url;
// 心跳时间
final int heartTime;
// 监听初始化完成
final Function? onReadyCb;
// 监听关闭
final Function? onCloseCb;
// 监听异常
final Function? onErrorCb;
// 监听消息
final Function? onMessageCb;
// 请求头
final Map<String, dynamic>? headers;
PlSocket({
required this.url,
required this.heartTime,
this.onReadyCb,
this.onCloseCb,
this.onErrorCb,
this.onMessageCb,
this.headers,
});
WebSocketChannel? channel;
StreamSubscription<dynamic>? channelStreamSub;
// 建立连接
Future connect() async {
// 连接之前关闭上次连接
onClose();
try {
channel = IOWebSocketChannel.connect(
url,
connectTimeout: const Duration(seconds: 15),
headers: null,
);
await channel?.ready;
onReady();
} catch (err) {
connect();
onError(err);
}
}
// 初始化完成
void onReady() {
status = SocketStatus.connected;
onReadyCb?.call();
channelStreamSub = channel?.stream.listen((message) {
onMessageCb?.call(message);
}, onDone: () {
// 流被关闭
print('结束了');
}, onError: (err) {
onError(err);
});
// 每30s发送心跳
Timer.periodic(Duration(seconds: heartTime), (timer) {
if (status == SocketStatus.connected) {
sendMessage(LiveUtils.encodeData(
"",
2,
));
} else {
timer.cancel();
}
});
}
// 连接关闭
void onClose() {
status = SocketStatus.closed;
onCloseCb?.call();
channelStreamSub?.cancel();
channel?.sink.close();
}
// 连接异常
void onError(err) {
onErrorCb?.call(err);
}
// 接收消息
void onMessage() {}
void sendMessage(dynamic message) {
if (status == SocketStatus.connected) {
channel?.sink.add(message);
}
}
}

View File

@ -0,0 +1,117 @@
import 'dart:typed_data';
class BinaryWriter {
List<int> buffer;
int position = 0;
BinaryWriter(this.buffer);
int get length => buffer.length;
void writeBytes(List<int> list) {
buffer.addAll(list);
position += list.length;
}
void writeInt(int value, int len, {Endian endian = Endian.big}) {
var bytes = _createByteData(len);
switch (len) {
case 1:
bytes.setUint8(0, value.toUnsigned(8));
break;
case 2:
bytes.setInt16(0, value, endian);
break;
case 4:
bytes.setInt32(0, value, endian);
break;
case 8:
bytes.setInt64(0, value, endian);
break;
default:
throw ArgumentError('Invalid length for writeInt: $len');
}
_addBytesToBuffer(bytes, len);
}
void writeDouble(double value, int len, {Endian endian = Endian.big}) {
var bytes = _createByteData(len);
switch (len) {
case 4:
bytes.setFloat32(0, value, endian);
break;
case 8:
bytes.setFloat64(0, value, endian);
break;
default:
throw ArgumentError('Invalid length for writeDouble: $len');
}
_addBytesToBuffer(bytes, len);
}
ByteData _createByteData(int len) {
var b = Uint8List(len).buffer;
return ByteData.view(b);
}
void _addBytesToBuffer(ByteData bytes, int len) {
buffer.addAll(bytes.buffer.asUint8List());
position += len;
}
}
class BinaryReader {
Uint8List buffer;
int position = 0;
BinaryReader(this.buffer);
int get length => buffer.length;
int read() {
return buffer[position++];
}
int readInt(int len, {Endian endian = Endian.big}) {
var bytes = _getBytes(len);
var data = ByteData.view(bytes.buffer);
switch (len) {
case 1:
return data.getUint8(0);
case 2:
return data.getInt16(0, endian);
case 4:
return data.getInt32(0, endian);
case 8:
return data.getInt64(0, endian);
default:
throw ArgumentError('Invalid length for readInt: $len');
}
}
int readByte({Endian endian = Endian.big}) => readInt(1, endian: endian);
int readShort({Endian endian = Endian.big}) => readInt(2, endian: endian);
int readInt32({Endian endian = Endian.big}) => readInt(4, endian: endian);
int readLong({Endian endian = Endian.big}) => readInt(8, endian: endian);
Uint8List readBytes(int len) {
var bytes = _getBytes(len);
return bytes;
}
double readFloat(int len, {Endian endian = Endian.big}) {
var bytes = _getBytes(len);
var data = ByteData.view(bytes.buffer);
switch (len) {
case 4:
return data.getFloat32(0, endian);
case 8:
return data.getFloat64(0, endian);
default:
throw ArgumentError('Invalid length for readFloat: $len');
}
}
Uint8List _getBytes(int len) {
var bytes =
Uint8List.fromList(buffer.getRange(position, position + len).toList());
position += len;
return bytes;
}
}

196
lib/utils/live.dart Normal file
View File

@ -0,0 +1,196 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:brotli/brotli.dart';
import 'package:pilipala/models/live/message.dart';
import 'package:pilipala/utils/binary_writer.dart';
class LiveUtils {
static List<int> encodeData(String msg, int action) {
var data = utf8.encode(msg);
//头部长度固定16
var length = data.length + 16;
var buffer = Uint8List(length);
var writer = BinaryWriter([]);
//数据包长度
writer.writeInt(buffer.length, 4);
//数据包头部长度,固定16
writer.writeInt(16, 2);
//协议版本0=JSON,1=Int32,2=Buffer
writer.writeInt(0, 2);
//操作类型
writer.writeInt(action, 4);
//数据包头部长度,固定1
writer.writeInt(1, 4);
writer.writeBytes(data);
return writer.buffer;
}
static List<LiveMessageModel>? decodeMessage(List<int> data) {
try {
//操作类型。3=心跳回应内容为房间人气值5=通知弹幕、广播等全部信息8=进房回应,空
int operation = readInt(data, 8, 4);
//内容
var body = data.skip(16).toList();
if (operation == 3) {
var online = readInt(body, 0, 4);
final LiveMessageModel liveMsg = LiveMessageModel(
type: LiveMessageType.online,
userName: '',
message: '',
color: LiveMessageColor.white,
data: online,
);
return [liveMsg];
} else if (operation == 5) {
//协议版本。0为JSON可以直接解析1为房间人气值,Body为4位Int322为压缩过Buffer需要解压再处理
int protocolVersion = readInt(data, 6, 2);
if (protocolVersion == 2) {
body = zlib.decode(body);
} else if (protocolVersion == 3) {
body = brotli.decode(body);
}
var text = utf8.decode(body, allowMalformed: true);
var group =
text.split(RegExp(r"[\x00-\x1f]+", unicode: true, multiLine: true));
List<LiveMessageModel> messages = [];
for (var item
in group.where((x) => x.length > 2 && x.startsWith('{'))) {
if (parseMessage(item) is LiveMessageModel) {
messages.add(parseMessage(item)!);
}
}
return messages;
}
} catch (e) {
print(e);
}
return null;
}
static LiveMessageModel? parseMessage(String jsonMessage) {
try {
var obj = json.decode(jsonMessage);
var cmd = obj["cmd"].toString();
if (cmd.contains("DANMU_MSG")) {
if (obj["info"] != null && obj["info"].length != 0) {
var message = obj["info"][1].toString();
var color = asT<int?>(obj["info"][0][3]) ?? 0;
if (obj["info"][2] != null && obj["info"][2].length != 0) {
var extra = obj["info"][0][15]['extra'];
var user = obj["info"][0][15]['user']['base'];
Map<String, dynamic> extraMap = jsonDecode(extra);
final int userId = obj["info"][2][0];
final LiveMessageModel liveMsg = LiveMessageModel(
type: LiveMessageType.chat,
userName: user['name'],
message: message,
color: color == 0
? LiveMessageColor.white
: LiveMessageColor.numberToColor(color),
face: user['face'],
uid: userId,
emots: extraMap['emots'],
);
return liveMsg;
}
}
} else if (cmd == "SUPER_CHAT_MESSAGE") {
if (obj["data"] == null) {
return null;
}
final data = obj["data"];
final userInfo = data["user_info"];
final String backgroundBottomColor =
data["background_bottom_color"].toString();
final String backgroundColor = data["background_color"].toString();
final DateTime endTime =
DateTime.fromMillisecondsSinceEpoch(data["end_time"] * 1000);
final String face = "${userInfo["face"]}@200w.jpg";
final String message = data["message"].toString();
final String price = data["price"];
final DateTime startTime =
DateTime.fromMillisecondsSinceEpoch(data["start_time"] * 1000);
final String userName = userInfo["uname"].toString();
final LiveMessageModel liveMsg = LiveMessageModel(
type: LiveMessageType.superChat,
userName: "SUPER_CHAT_MESSAGE",
message: "SUPER_CHAT_MESSAGE",
color: LiveMessageColor.white,
data: {
"backgroundBottomColor": backgroundBottomColor,
"backgroundColor": backgroundColor,
"endTime": endTime,
"face": face,
"message": message,
"price": price,
"startTime": startTime,
"userName": userName,
},
);
return liveMsg;
} else if (cmd == 'INTERACT_WORD') {
if (obj["data"] == null) {
return null;
}
final data = obj["data"];
final String userName = data['uname'];
final int msgType = data['msg_type'];
final LiveMessageModel liveMsg = LiveMessageModel(
type: msgType == 1 ? LiveMessageType.join : LiveMessageType.follow,
userName: userName,
message: msgType == 1 ? '进入直播间' : '关注了主播',
color: LiveMessageColor.white,
);
return liveMsg;
}
} catch (e) {
print(e);
}
return null;
}
static T? asT<T>(dynamic value) {
if (value is T) {
return value;
}
return null;
}
static int readInt(List<int> buffer, int start, int len) {
var data = _getByteData(buffer, start, len);
return _readIntFromByteData(data, len);
}
static ByteData _getByteData(List<int> buffer, int start, int len) {
var bytes =
Uint8List.fromList(buffer.getRange(start, start + len).toList());
return ByteData.view(bytes.buffer);
}
static int _readIntFromByteData(ByteData data, int len) {
switch (len) {
case 1:
return data.getUint8(0);
case 2:
return data.getInt16(0, Endian.big);
case 4:
return data.getInt32(0, Endian.big);
case 8:
return data.getInt64(0, Endian.big);
default:
throw ArgumentError('Invalid length: $len');
}
}
}