import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; import '../providers/auth_provider.dart'; import '../providers/settings_provider.dart'; import '../services/chat_service.dart'; class PlayerPage extends StatefulWidget { final String title; final String rtmpUrl; final String roomId; const PlayerPage({ Key? key, required this.title, required this.rtmpUrl, required this.roomId, }) : super(key: key); @override _PlayerPageState createState() => _PlayerPageState(); } class _PlayerPageState extends State { late VideoPlayerController _controller; final ChatService _chatService = ChatService(); final TextEditingController _msgController = TextEditingController(); final List _messages = []; final List _danmakus = []; bool _isError = false; String? _errorMessage; @override void initState() { super.initState(); _initializePlayer(); _initializeChat(); } void _initializePlayer() async { _controller = VideoPlayerController.networkUrl(Uri.parse(widget.rtmpUrl)); try { await _controller.initialize(); _controller.play(); if (mounted) setState(() {}); } catch (e) { if (mounted) setState(() { _isError = true; _errorMessage = e.toString(); }); } } void _initializeChat() { final settings = context.read(); final auth = context.read(); // 使用真实用户名建立连接 final currentUsername = auth.username ?? "Guest_${widget.roomId}"; _chatService.connect(settings.baseUrl, widget.roomId, currentUsername); _chatService.messages.listen((msg) { if (mounted) { setState(() { _messages.insert(0, msg); if (!msg.isHistory && (msg.type == "chat" || msg.type == "danmaku")) { _addDanmaku(msg.content); } }); } }); } void _addDanmaku(String text) { final key = UniqueKey(); final top = 20.0 + (DateTime.now().millisecondsSinceEpoch % 6) * 30.0; final danmaku = _DanmakuItem( key: key, text: text, top: top, onFinished: () { if (mounted) setState(() => _danmakus.removeWhere((w) => w.key == key)); }, ); setState(() => _danmakus.add(danmaku)); } void _sendMsg() { if (_msgController.text.isNotEmpty) { final auth = context.read(); _chatService.sendMessage(_msgController.text, auth.username ?? "Anonymous", widget.roomId); _msgController.clear(); } } @override void dispose() { _controller.dispose(); _chatService.dispose(); _msgController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { bool isWide = MediaQuery.of(context).size.width > 900; return Scaffold( appBar: AppBar(title: Text(widget.title)), body: isWide ? _buildWideLayout() : _buildMobileLayout(), ); } // 宽屏布局:左右分栏 Widget _buildWideLayout() { return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 左侧视频区 (占比 75%) Expanded( flex: 3, child: Container( color: Colors.black, child: _buildVideoWithDanmaku(), ), ), // 右侧聊天区 (占比 25%) Container( width: 350, decoration: BoxDecoration( border: Border(left: BorderSide(color: Theme.of(context).dividerColor)), ), child: _buildChatSection(), ), ], ); } // 移动端布局:上下堆叠 Widget _buildMobileLayout() { return Column( children: [ // 上方视频区 Container( color: Colors.black, width: double.infinity, height: 250, child: _buildVideoWithDanmaku(), ), // 下方聊天区 Expanded(child: _buildChatSection()), ], ); } // 抽离视频播放器与弹幕组件 Widget _buildVideoWithDanmaku() { return Stack( children: [ Center( child: _isError ? Text("Error: $_errorMessage", style: TextStyle(color: Colors.white)) : _controller.value.isInitialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), ) : CircularProgressIndicator(), ), // 弹幕层使用 ClipRect 裁剪,防止飘出视频区域 ClipRect( child: Stack(children: _danmakus), ), ], ); } // 抽离聊天区域组件 Widget _buildChatSection() { return Column( children: [ Container( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), color: Theme.of(context).colorScheme.surfaceVariant, child: Row( children: [ Icon(Icons.chat_bubble_outline, size: 16), SizedBox(width: 8), Text("Live Chat", style: TextStyle(fontWeight: FontWeight.bold)), ], ), ), Expanded( child: ListView.builder( reverse: true, padding: EdgeInsets.all(8), itemCount: _messages.length, itemBuilder: (context, index) { final m = _messages[index]; return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: RichText( text: TextSpan( style: TextStyle(color: Theme.of(context).textTheme.bodyMedium?.color), children: [ TextSpan( text: "${m.username}: ", style: TextStyle(fontWeight: FontWeight.bold, color: m.type == "system" ? Colors.blue : Theme.of(context).colorScheme.primary), ), TextSpan(text: m.content), ], ), ), ); }, ), ), Divider(height: 1), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ Expanded( child: TextField( controller: _msgController, decoration: InputDecoration( hintText: "Send a message...", border: OutlineInputBorder(borderRadius: BorderRadius.circular(20)), contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), onSubmitted: (_) => _sendMsg(), ), ), IconButton(icon: Icon(Icons.send, color: Theme.of(context).colorScheme.primary), onPressed: _sendMsg), ], ), ), ], ); } } class _DanmakuItem extends StatefulWidget { final String text; final double top; final VoidCallback onFinished; const _DanmakuItem({Key? key, required this.text, required this.top, required this.onFinished}) : super(key: key); @override __DanmakuItemState createState() => __DanmakuItemState(); } class __DanmakuItemState extends State<_DanmakuItem> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _animation; @override void initState() { super.initState(); _animationController = AnimationController(duration: const Duration(seconds: 10), vsync: this); // 使用相对位置:从右向左 _animation = Tween(begin: 1.0, end: -0.5).animate(_animationController); _animationController.forward().then((_) => widget.onFinished()); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Positioned( top: widget.top, // left 使用 MediaQuery 获取屏幕宽度进行动态计算 left: MediaQuery.of(context).size.width * _animation.value, child: Text( widget.text, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, shadows: [Shadow(blurRadius: 4, color: Colors.black, offset: Offset(1, 1))], ), ), ); }, ); } }