225 lines
6.5 KiB
Dart
225 lines
6.5 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:video_player/video_player.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<PlayerPage> {
|
|
late VideoPlayerController _controller;
|
|
final ChatService _chatService = ChatService();
|
|
final TextEditingController _msgController = TextEditingController();
|
|
final List<ChatMessage> _messages = [];
|
|
final List<Widget> _danmakus = []; // 为简单起见,这里存储弹幕 Widget
|
|
|
|
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<SettingsProvider>();
|
|
_chatService.connect(settings.baseUrl, widget.roomId, "User_${widget.roomId}"); // 暂定用户名
|
|
|
|
_chatService.messages.listen((msg) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_messages.insert(0, msg);
|
|
if (msg.type == "chat" || msg.type == "danmaku") {
|
|
_addDanmaku(msg.content);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
void _addDanmaku(String text) {
|
|
final id = DateTime.now().millisecondsSinceEpoch;
|
|
final top = 20.0 + (id % 5) * 30.0; // 简单的多轨道显示
|
|
|
|
final danmaku = _DanmakuItem(
|
|
key: ValueKey(id),
|
|
text: text,
|
|
top: top,
|
|
onFinished: () {
|
|
if (mounted) setState(() => _danmakus.removeWhere((w) => w.key == ValueKey(id)));
|
|
},
|
|
);
|
|
|
|
setState(() => _danmakus.add(danmaku));
|
|
}
|
|
|
|
void _sendMsg() {
|
|
if (_msgController.text.isNotEmpty) {
|
|
_chatService.sendMessage(_msgController.text, "Me", widget.roomId);
|
|
_msgController.clear();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_chatService.dispose();
|
|
_msgController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(widget.title)),
|
|
body: Column(
|
|
children: [
|
|
// 视频播放器 + 弹幕层
|
|
Container(
|
|
color: Colors.black,
|
|
width: double.infinity,
|
|
height: 250,
|
|
child: 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(),
|
|
),
|
|
// 弹幕层
|
|
..._danmakus,
|
|
],
|
|
),
|
|
),
|
|
// 评论区标题
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
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,
|
|
itemCount: _messages.length,
|
|
itemBuilder: (context, index) {
|
|
final m = _messages[index];
|
|
return ListTile(
|
|
dense: true,
|
|
title: Text(
|
|
"${m.username}: ${m.content}",
|
|
style: TextStyle(color: m.type == "system" ? Colors.blue : null),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
// 输入框
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _msgController,
|
|
decoration: InputDecoration(
|
|
hintText: "Say something...",
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(20)),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
|
),
|
|
onSubmitted: (_) => _sendMsg(),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
IconButton(icon: Icon(Icons.send, color: Colors.blue), 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<Offset> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(duration: const Duration(seconds: 8), vsync: this);
|
|
_animation = Tween<Offset>(begin: Offset(1.5, 0), end: Offset(-1.5, 0)).animate(_animationController);
|
|
|
|
_animationController.forward().then((_) => widget.onFinished());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Positioned(
|
|
top: widget.top,
|
|
width: 300,
|
|
child: SlideTransition(
|
|
position: _animation,
|
|
child: Text(
|
|
widget.text,
|
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, shadows: [Shadow(blurRadius: 2, color: Colors.black)]),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|