import 'dart:async'; import 'dart:io'; import 'package:PiliPlus/http/live.dart'; import 'package:PiliPlus/pages/live_room/widgets/chat.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/plugin/pl_player/index.dart'; import 'package:screen_brightness/screen_brightness.dart'; import '../../utils/storage.dart'; import 'controller.dart'; import 'widgets/bottom_control.dart'; class LiveRoomPage extends StatefulWidget { const LiveRoomPage({super.key}); @override State createState() => _LiveRoomPageState(); } class _LiveRoomPageState extends State with WidgetsBindingObserver { late final int _roomId; late final LiveRoomController _liveRoomController; late final PlPlayerController plPlayerController; late Future? _futureBuilder; late Future? _futureBuilderFuture; bool isShowCover = true; bool isPlay = true; Floating? floating; late final _isLogin = GStorage.userInfo.get('userInfoCache') != null; late final _node = FocusNode(); late final _ctr = TextEditingController(); StreamSubscription? _listener; int latestAddedPosition = -1; bool? _isFullScreen; bool? _isPipMode; void playCallBack() { plPlayerController.play(); } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _roomId = int.parse(Get.parameters['roomid'] ?? '-1'); _liveRoomController = Get.put( LiveRoomController(), tag: Utils.makeHeroTag(_roomId), ); PlPlayerController.setPlayCallBack(playCallBack); if (Platform.isAndroid) { floating = Floating(); } videoSourceInit(); _futureBuilderFuture = _liveRoomController.queryLiveInfo(); plPlayerController.autoEnterFullscreen(); _liveRoomController.liveMsg(); _listener = plPlayerController.isFullScreen.listen((isFullScreen) { if (isFullScreen != _isFullScreen) { _isFullScreen = isFullScreen; _updateFontSize(); } }); WidgetsBinding.instance.addPostFrameCallback((_) { if (context.orientation == Orientation.landscape) { plPlayerController.triggerFullScreen(status: true); } }); } void _updateFontSize() async { if (Platform.isAndroid) { _isPipMode = await const MethodChannel("floating").invokeMethod('inPipAlready'); } if (_liveRoomController.controller != null) { _liveRoomController.controller!.updateOption( _liveRoomController.controller!.option.copyWith( fontSize: _getFontSize(plPlayerController.isFullScreen.value), ), ); } } double _getFontSize(isFullScreen) { return isFullScreen == false || _isPipMode == true ? 15 * plPlayerController.fontSizeVal : 15 * plPlayerController.fontSizeFSVal; } Future videoSourceInit() async { _futureBuilder = _liveRoomController.queryLiveInfoH5(); plPlayerController = _liveRoomController.plPlayerController; } @override void dispose() { _listener?.cancel(); WidgetsBinding.instance.removeObserver(this); ScreenBrightness().resetApplicationScreenBrightness(); PlPlayerController.setPlayCallBack(null); _liveRoomController.msgStream?.close(); // floating?.dispose(); _node.dispose(); plPlayerController.dispose(); _ctr.dispose(); _liveRoomController.scrollController.removeListener(() {}); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _liveRoomController.showDanmaku = true; } else if (state == AppLifecycleState.paused) { _liveRoomController.showDanmaku = false; plPlayerController.danmakuController?.clear(); } } Widget get videoPlayerPanel { return FutureBuilder( future: _futureBuilderFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData && snapshot.data['status']) { return PLVideoPlayer( plPlayerController: plPlayerController, bottomControl: BottomControl( controller: plPlayerController, liveRoomCtr: _liveRoomController, floating: floating, ), danmuWidget: Obx( () => AnimatedOpacity( opacity: plPlayerController.isOpenDanmu.value ? 1 : 0, duration: const Duration(milliseconds: 100), child: DanmakuScreen( createdController: (DanmakuController e) { plPlayerController.danmakuController = _liveRoomController.controller = e; }, option: DanmakuOption( fontSize: _getFontSize(plPlayerController.isFullScreen.value), fontWeight: plPlayerController.fontWeight, area: plPlayerController.showArea, opacity: plPlayerController.opacityVal, hideTop: plPlayerController.blockTypes.contains(5), hideScroll: plPlayerController.blockTypes.contains(2), hideBottom: plPlayerController.blockTypes.contains(4), duration: plPlayerController.danmakuDurationVal ~/ plPlayerController.playbackSpeed, strokeWidth: plPlayerController.strokeWidth, lineHeight: plPlayerController.danmakuLineHeight, ), ), ), ), ); } else { return const SizedBox(); } }, ); } Widget get childWhenDisabled { return Scaffold( primary: true, backgroundColor: Colors.black, body: Stack( children: [ Positioned( left: 0, top: 0, right: 0, bottom: 0, child: Opacity( opacity: 0.6, child: Image.asset( 'assets/images/live/default_bg.webp', fit: BoxFit.cover, ), ), ), Obx( () => Positioned( left: 0, top: 0, right: 0, bottom: 0, child: _liveRoomController .roomInfoH5.value.roomInfo?.appBackground != '' && _liveRoomController .roomInfoH5.value.roomInfo?.appBackground != null ? Opacity( opacity: 0.6, child: NetworkImgLayer( width: Get.width, height: Get.height, type: 'bg', src: _liveRoomController .roomInfoH5.value.roomInfo?.appBackground ?? '', ), ) : const SizedBox(), ), ), Column( children: [ AppBar( backgroundColor: Colors.transparent, foregroundColor: Colors.white, titleTextStyle: TextStyle(color: Colors.white), toolbarHeight: MediaQuery.of(context).orientation == Orientation.portrait ? 56 : 0, title: FutureBuilder( future: _futureBuilder, builder: (context, snapshot) { if (snapshot.data == null) { return const SizedBox(); } Map data = snapshot.data as Map; if (data['status']) { return Obx( () => Row( children: [ GestureDetector( onTap: () { _node.unfocus(); dynamic uid = _liveRoomController .roomInfoH5.value.roomInfo?.uid; Get.toNamed( '/member?mid=$uid', arguments: { 'heroTag': Utils.makeHeroTag(uid), }, ); }, child: NetworkImgLayer( width: 34, height: 34, type: 'avatar', src: _liveRoomController.roomInfoH5.value .anchorInfo!.baseInfo!.face, ), ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _liveRoomController.roomInfoH5.value .anchorInfo!.baseInfo!.uname!, style: const TextStyle(fontSize: 14), ), const SizedBox(height: 1), if (_liveRoomController .roomInfoH5.value.watchedShow != null) Text( _liveRoomController.roomInfoH5.value .watchedShow!['text_large'] ?? '', style: const TextStyle(fontSize: 12), ), ], ), const Spacer(), //刷新 IconButton( tooltip: '刷新', onPressed: () { _futureBuilderFuture = _liveRoomController.queryLiveInfo(); // videoSourceInit(); }, icon: const Icon(Icons.refresh), ), //内置浏览器打开 IconButton( tooltip: '浏览器打开', onPressed: () { Utils.handleWebview( 'https://live.bilibili.com/h5/${_liveRoomController.roomId}', off: true, ); }, icon: const Icon(Icons.open_in_browser)), ], ), ); } else { return const SizedBox(); } }, ), ), PopScope( canPop: plPlayerController.isFullScreen.value != true, onPopInvokedWithResult: (bool didPop, Object? result) { if (plPlayerController.isFullScreen.value == true) { plPlayerController.triggerFullScreen(status: false); // if (MediaQuery.of(context).orientation == // Orientation.landscape) { // verticalScreenForTwoSeconds(); // } } }, child: Listener( onPointerDown: (_) { _node.unfocus(); }, child: SizedBox( width: Get.size.width, height: MediaQuery.of(context).orientation == Orientation.landscape ? Get.size.height : Get.size.width * 9 / 16, child: videoPlayerPanel, ), ), ), Expanded( child: Listener( onPointerDown: (_) { _node.unfocus(); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: LiveRoomChat( roomId: _roomId, liveRoomController: _liveRoomController, ), ), ), ), Container( padding: EdgeInsets.only( left: 10, top: 10, right: 10, bottom: 25 + MediaQuery.of(context).padding.bottom, ), decoration: BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), border: Border( top: BorderSide(color: Color(0x1AFFFFFF)), ), color: Color(0x1AFFFFFF), ), child: Row( children: [ Obx( () => IconButton( onPressed: () { plPlayerController.isOpenDanmu.value = !plPlayerController.isOpenDanmu.value; GStorage.setting.put(SettingBoxKey.enableShowDanmaku, plPlayerController.isOpenDanmu.value); }, icon: Icon( plPlayerController.isOpenDanmu.value ? Icons.subtitles_outlined : Icons.subtitles_off_outlined, color: _color, ), ), ), Expanded( child: TextField( focusNode: _node, controller: _ctr, textInputAction: TextInputAction.send, cursorColor: _color, style: TextStyle(color: _color), onSubmitted: (value) { if (value.isNotEmpty) { _onSendMsg(value); } }, decoration: InputDecoration( border: InputBorder.none, hintText: '发送弹幕', hintStyle: TextStyle( color: Colors.white.withOpacity(0.6), ), ), ), ), IconButton( onPressed: () { if (_ctr.text.isNotEmpty) { _onSendMsg(_ctr.text); } }, icon: Icon( Icons.send, color: _color, ), ), ], ), ), ], ), ], ), ); } @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) { _updateFontSize(); }); if (Platform.isAndroid) { return PiPSwitcher( getChildWhenDisabled: () => childWhenDisabled, getChildWhenEnabled: () => videoPlayerPanel, floating: floating, ); } else { return childWhenDisabled; } } Color get _color => Color(0xFFEEEEEE); void _onSendMsg(msg) async { if (!_isLogin) { SmartDialog.showToast('未登录'); return; } dynamic res = await LiveHttp.sendLiveMsg( roomId: _liveRoomController.roomId, msg: msg); if (res['status']) { _ctr.clear(); SmartDialog.showToast('发送成功'); } else { SmartDialog.showToast(res['msg']); } } }