diff --git a/lib/http/api.dart b/lib/http/api.dart
index d9286e47..93226946 100644
--- a/lib/http/api.dart
+++ b/lib/http/api.dart
@@ -558,4 +558,11 @@ class Api {
/// 系统通知标记已读
static const String systemMarkRead =
'${HttpString.messageBaseUrl}/x/sys-msg/update_cursor';
+
+ /// 直播间弹幕信息
+ static const String getDanmuInfo =
+ '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo';
+
+ /// 直播间发送弹幕
+ static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send';
}
diff --git a/lib/http/init.dart b/lib/http/init.dart
index cb9d6f39..faa57dd5 100644
--- a/lib/http/init.dart
+++ b/lib/http/init.dart
@@ -29,6 +29,7 @@ class Request {
late String systemProxyPort;
static final RegExp spmPrefixExp =
RegExp(r'');
+ 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;
}
diff --git a/lib/http/live.dart b/lib/http/live.dart
index e624120e..f6fc4ea4 100644
--- a/lib/http/live.dart
+++ b/lib/http/live.dart
@@ -65,4 +65,56 @@ 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'],
+ };
+ }
+ }
+
+ // 发送弹幕
+ static Future sendDanmaku({roomId, msg}) async {
+ var res = await Request().post(Api.sendLiveMsg, queryParameters: {
+ 'bubble': 0,
+ 'msg': msg,
+ 'color': 16777215, // 颜色
+ 'mode': 1, // 模式
+ 'room_type': 0,
+ 'jumpfrom': 71001, // 直播间来源
+ 'reply_mid': 0,
+ 'reply_attr': 0,
+ 'replay_dmid': '',
+ 'statistics': {"appId": 100, "platform": 5},
+ 'fontsize': 25, // 字体大小
+ 'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳
+ 'roomid': roomId,
+ 'csrf': await Request.getCsrf(),
+ 'csrf_token': await Request.getCsrf(),
+ });
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': res.data['data'],
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
}
diff --git a/lib/models/live/message.dart b/lib/models/live/message.dart
new file mode 100644
index 00000000..cd0f4b75
--- /dev/null
+++ b/lib/models/live/message.dart
@@ -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? 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')}";
+ }
+}
diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart
index 52c423d7..ed259b96 100644
--- a/lib/pages/danmaku/controller.dart
+++ b/lib/pages/danmaku/controller.dart
@@ -2,8 +2,9 @@ import 'package:pilipala/http/danmaku.dart';
import 'package:pilipala/models/danmaku/dm.pb.dart';
class PlDanmakuController {
- PlDanmakuController(this.cid);
+ PlDanmakuController(this.cid, this.type);
final int cid;
+ final String type;
Map> dmSegMap = {};
// 已请求的段落标记
List requestedSeg = [];
diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart
index 109f0206..3cf1ed8a 100644
--- a/lib/pages/danmaku/view.dart
+++ b/lib/pages/danmaku/view.dart
@@ -12,11 +12,15 @@ import 'package:pilipala/utils/storage.dart';
class PlDanmaku extends StatefulWidget {
final int cid;
final PlPlayerController playerController;
+ final String type;
+ final Function(DanmakuController)? createdController;
const PlDanmaku({
super.key,
required this.cid,
required this.playerController,
+ this.type = 'video',
+ this.createdController,
});
@override
@@ -43,9 +47,9 @@ class _PlDanmakuState extends State {
super.initState();
enableShowDanmaku =
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
- _plDanmakuController = PlDanmakuController(widget.cid);
- if (mounted) {
- playerController = widget.playerController;
+ _plDanmakuController = PlDanmakuController(widget.cid, widget.type);
+ playerController = widget.playerController;
+ if (mounted && widget.type == 'video') {
if (enableShowDanmaku || playerController.isOpenDanmu.value) {
_plDanmakuController.initiate(
playerController.duration.value.inMilliseconds,
@@ -55,13 +59,15 @@ class _PlDanmakuState extends State {
..addStatusLister(playerListener)
..addPositionListener(videoPositionListen);
}
- playerController.isOpenDanmu.listen((p0) {
- if (p0 && !_plDanmakuController.initiated) {
- _plDanmakuController.initiate(
- playerController.duration.value.inMilliseconds,
- playerController.position.value.inMilliseconds);
- }
- });
+ if (widget.type == 'video') {
+ playerController.isOpenDanmu.listen((p0) {
+ if (p0 && !_plDanmakuController.initiated) {
+ _plDanmakuController.initiate(
+ playerController.duration.value.inMilliseconds,
+ playerController.position.value.inMilliseconds);
+ }
+ });
+ }
blockTypes = playerController.blockTypes;
showArea = playerController.showArea;
opacityVal = playerController.opacityVal;
@@ -128,6 +134,7 @@ class _PlDanmakuState extends State {
child: DanmakuView(
createdController: (DanmakuController e) async {
playerController.danmakuController = _controller = e;
+ widget.createdController?.call(e);
},
option: DanmakuOption(
fontSize: 15 * fontSizeVal,
@@ -136,8 +143,7 @@ class _PlDanmakuState extends State {
hideTop: blockTypes.contains(5),
hideScroll: blockTypes.contains(2),
hideBottom: blockTypes.contains(4),
- duration:
- danmakuDurationVal / playerController.playbackSpeed,
+ duration: danmakuDurationVal / playerController.playbackSpeed,
strokeWidth: strokeWidth,
// initDuration /
// (danmakuSpeedVal * widget.playerController.playbackSpeed),
diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart
index d21a89bc..3d393277 100644
--- a/lib/pages/follow/widgets/follow_item.dart
+++ b/lib/pages/follow/widgets/follow_item.dart
@@ -1,3 +1,4 @@
+import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@@ -47,9 +48,21 @@ class FollowItem extends StatelessWidget {
height: 34,
child: TextButton(
onPressed: () async {
- await Get.bottomSheet(
- GroupPanel(mid: item.mid!),
- isScrollControlled: true,
+ await showFlexibleBottomSheet(
+ bottomSheetColor: Colors.transparent,
+ minHeight: 1,
+ initHeight: 1,
+ maxHeight: 1,
+ context: Get.context!,
+ builder: (BuildContext context,
+ ScrollController scrollController, double offset) {
+ return GroupPanel(
+ mid: item.mid!,
+ scrollController: scrollController,
+ );
+ },
+ anchors: [1],
+ isSafeArea: true,
);
},
style: TextButton.styleFrom(
diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart
index 4e67fa2c..99025dce 100644
--- a/lib/pages/live_room/controller.dart
+++ b/lib/pages/live_room/controller.dart
@@ -1,10 +1,19 @@
+import 'dart:async';
+import 'dart:convert';
+import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
+import 'package:hive/hive.dart';
+import 'package:ns_danmaku/ns_danmaku.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 +33,20 @@ class LiveRoomController extends GetxController {
int? tempCurrentQn;
late List