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'; class LiveRoomController extends GetxController { String cover = ''; late int roomId; dynamic liveItem; late String heroTag; double volume = 0.0; // 静音状态 RxBool volumeOff = false.obs; PlPlayerController plPlayerController = PlPlayerController(videoType: 'live'); Rx roomInfoH5 = RoomInfoH5Model().obs; late bool enableCDN; late int currentQn; int? tempCurrentQn; late List> acceptQnList; RxString currentQnDesc = ''.obs; Box userInfoCache = GStorage.userInfo; int userId = 0; PlSocket? plSocket; List danmuHostList = []; String token = ''; // 弹幕消息列表 RxList messageList = [].obs; DanmakuController? danmakuController; // 输入控制器 TextEditingController inputController = TextEditingController(); // 加入直播间提示 RxMap joinRoomTip = {'userName': '', 'message': ''}.obs; // 直播间弹幕开关 默认打开 RxBool danmakuSwitch = true.obs; late String buvid; RxBool isPortrait = false.obs; @override void onInit() { super.onInit(); currentQn = setting.get(SettingBoxKey.defaultLiveQa, defaultValue: LiveQuality.values.last.code); roomId = int.parse(Get.parameters['roomid']!); if (Get.arguments != null) { liveItem = Get.arguments['liveItem']; heroTag = Get.arguments['heroTag'] ?? ''; if (liveItem != null) { cover = (liveItem.pic != null && liveItem.pic != '') ? liveItem.pic : (liveItem.cover != null && liveItem.cover != '') ? liveItem.cover : ''; } Request.getBuvid().then((value) => buvid = value); } // CDN优化 enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: false); final userInfo = userInfoCache.get('userInfoCache'); if (userInfo != null && userInfo.mid != null) { userId = userInfo.mid; } liveDanmakuInfo().then((value) => initSocket()); danmakuSwitch.listen((p0) { plPlayerController.isOpenDanmu.value = p0; }); } playerInit(source) async { await plPlayerController.setDataSource( DataSource( videoSource: source, audioSource: null, type: DataSourceType.network, httpHeaders: { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15', 'referer': HttpString.baseUrl }, ), // 硬解 enableHA: true, autoplay: true, ); plPlayerController.isOpenDanmu.value = danmakuSwitch.value; heartBeat(); } Future queryLiveInfo() async { var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: currentQn); if (res['status']) { isPortrait.value = res['data'].isPortrait; List codec = res['data'].playurlInfo.playurl.stream.first.format.first.codec; CodecItem item = codec.first; // 以服务端返回的码率为准 currentQn = item.currentQn!; if (tempCurrentQn != null && tempCurrentQn == currentQn) { SmartDialog.showToast('画质切换失败,请检查登录状态'); } List acceptQn = item.acceptQn!; acceptQnList = acceptQn.map((e) { return { 'code': e, 'desc': LiveQuality.values .firstWhere((element) => element.code == e) .description, }; }).toList(); currentQnDesc.value = LiveQuality.values .firstWhere((element) => element.code == currentQn) .description; String videoUrl = enableCDN ? VideoUtils.getCdnUrl(item) : (item.urlInfo?.first.host)! + item.baseUrl! + item.urlInfo!.first.extra!; await playerInit(videoUrl); return res; } } void setVolumn(value) { if (value == 0) { // 设置音量 volumeOff.value = false; } else { // 取消音量 volume = value; volumeOff.value = true; } } Future queryLiveInfoH5() async { var res = await LiveHttp.liveRoomInfoH5(roomId: roomId); if (res['status']) { roomInfoH5.value = res['data']; } return res; } // 修改画质 void changeQn(int qn) async { tempCurrentQn = currentQn; if (currentQn == qn) { return; } currentQn = qn; currentQnDesc.value = LiveQuality.values .firstWhere((element) => element.code == currentQn) .description; await queryLiveInfo(); } Future liveDanmakuInfo() async { var res = await LiveHttp.liveDanmakuInfo(roomId: roomId); if (res['status']) { danmuHostList = (res["data"]["host_list"] as List) .map((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? liveMsg = LiveUtils.decodeMessage(message); if (liveMsg != null && liveMsg.isNotEmpty) { if (liveMsg.first.type == LiveMessageType.online) { print('当前直播间人气:${liveMsg.first.data}'); } else if (liveMsg.first.type == LiveMessageType.join || liveMsg.first.type == LiveMessageType.follow) { // 每隔一秒依次liveMsg中的每一项赋给activeUserName int index = 0; Timer.periodic(const Duration(seconds: 2), (timer) { if (index < liveMsg.length) { if (liveMsg[index].type == LiveMessageType.join || liveMsg[index].type == LiveMessageType.follow) { joinRoomTip.value = { 'userName': liveMsg[index].userName, 'message': liveMsg[index].message!, }; } index++; } else { timer.cancel(); } }); return; } // 过滤出聊天消息 var chatMessages = liveMsg.where((msg) => msg.type == LiveMessageType.chat).toList(); // 添加到 messageList messageList.addAll(chatMessages); // 将 chatMessages 转换为 danmakuItems 列表 List danmakuItems = chatMessages.map((e) { return DanmakuItem( e.message ?? '', color: Color.fromARGB( 255, e.color.r, e.color.g, e.color.b, ), ); }).toList(); // 添加到 danmakuController if (danmakuSwitch.value) { danmakuController?.addItems(danmakuItems); } } }, onErrorCb: (e) { print('error: $e'); }, ); await plSocket?.connect(); } void joinRoom() async { var joinData = LiveUtils.encodeData( json.encode({ "uid": userId, "roomid": roomId, "protover": 3, "buvid": buvid, "platform": "web", "type": 2, "key": token, }), 7, ); plSocket?.sendMessage(joinData); } // 发送弹幕 void sendMsg() async { final msg = inputController.text; if (msg.isEmpty) { return; } final res = await LiveHttp.sendDanmaku( roomId: roomId, msg: msg, ); if (res['status']) { inputController.clear(); } else { SmartDialog.showToast(res['msg']); } } // 历史记录 void heartBeat() { LiveHttp.liveRoomEntry(roomId: roomId); } String encodeToBase64(String input) { List bytes = utf8.encode(input); String base64Str = base64.encode(bytes); return base64Str; } @override void onClose() { heartBeat(); plSocket?.onClose(); super.onClose(); } }