From 80d4f692e082a50974703fe7980620f7087db18c Mon Sep 17 00:00:00 2001 From: CGH0S7 <776459475@qq.com> Date: Thu, 25 Jun 2026 10:50:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E7=9B=B4=E6=92=AD?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E9=80=9A=E7=9F=A5=E4=B8=8E=20WebSocket=20?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/chat/hub.go | 19 ++++- backend/internal/stream/server.go | 2 +- frontend/lib/l10n/app_en.arb | 2 + frontend/lib/l10n/app_ja.arb | 2 + frontend/lib/l10n/app_localizations.dart | 12 ++++ frontend/lib/l10n/app_localizations_en.dart | 6 ++ frontend/lib/l10n/app_localizations_ja.dart | 6 ++ frontend/lib/l10n/app_localizations_zh.dart | 12 ++++ frontend/lib/l10n/app_zh.arb | 2 + frontend/lib/l10n/app_zh_Hant.arb | 2 + frontend/lib/pages/player_page.dart | 78 ++++++++++++++++++--- frontend/lib/services/chat_service.dart | 1 - 12 files changed, 130 insertions(+), 14 deletions(-) diff --git a/backend/internal/chat/hub.go b/backend/internal/chat/hub.go index 37fdbd4..ef087f1 100644 --- a/backend/internal/chat/hub.go +++ b/backend/internal/chat/hub.go @@ -18,7 +18,7 @@ const ( ) type Message struct { - Type string `json:"type"` // "chat", "system", "danmaku" + Type string `json:"type"` // "chat", "system", "danmaku", "stream_end" Username string `json:"username"` Content string `json:"content"` RoomID string `json:"room_id"` @@ -190,7 +190,15 @@ func (r *roomHub) handleBroadcast(message Message) { delete(r.clients, client) } } + shouldDeleteIfIdle := message.Type == "stream_end" + if shouldDeleteIfIdle { + r.history = nil + } r.mutex.Unlock() + + if shouldDeleteIfIdle { + r.manager.deleteRoomIfIdle(r) + } } func (r *roomHub) handleClearHistory() { @@ -223,6 +231,15 @@ func (h *Hub) BroadcastToRoom(msg Message) { h.getOrCreateRoom(msg.RoomID).broadcast <- msg } +func (h *Hub) NotifyStreamEnded(roomID string) { + h.BroadcastToRoom(Message{ + Type: "stream_end", + Username: "System", + Content: "The host has ended the live stream.", + RoomID: roomID, + }) +} + func (c *Client) ReadPump() { defer func() { c.Hub.UnregisterClient(c) diff --git a/backend/internal/stream/server.go b/backend/internal/stream/server.go index 9d60c30..c40400e 100644 --- a/backend/internal/stream/server.go +++ b/backend/internal/stream/server.go @@ -172,7 +172,7 @@ func NewRTMPServer(rtmpPort string) *RTMPServer { monitor.Warnf("Failed to mark room inactive room_id=%s: %v", roomID, err) } } - chat.MainHub.ClearRoomHistory(roomID) + chat.MainHub.NotifyStreamEnded(roomID) monitor.Infof("Publishing ended for room_id=%s", roomID) } else { monitor.Infof("Variant publishing ended for room_id=%s path=%s", roomID, channelPath) diff --git a/frontend/lib/l10n/app_en.arb b/frontend/lib/l10n/app_en.arb index 7a8e2e7..27fb782 100644 --- a/frontend/lib/l10n/app_en.arb +++ b/frontend/lib/l10n/app_en.arb @@ -50,6 +50,8 @@ "availableNow": "Available now", "waitingForTranscoding": "Waiting for backend transcoding output", "sendMessage": "Send a message...", + "liveStreamEnded": "The host has ended the live stream.", + "liveStreamEndedShort": "Live stream ended", "liveChat": "Live Chat", "refresh": "Refresh", "volume": "Volume", diff --git a/frontend/lib/l10n/app_ja.arb b/frontend/lib/l10n/app_ja.arb index b7e41b3..fdb859a 100644 --- a/frontend/lib/l10n/app_ja.arb +++ b/frontend/lib/l10n/app_ja.arb @@ -50,6 +50,8 @@ "availableNow": "利用可能", "waitingForTranscoding": "バックエンドのトランスコード出力を待機中", "sendMessage": "メッセージを送信...", + "liveStreamEnded": "配信者が退出したため、ライブ配信は終了しました。", + "liveStreamEndedShort": "ライブ配信は終了しました", "liveChat": "ライブチャット", "refresh": "更新", "volume": "音量", diff --git a/frontend/lib/l10n/app_localizations.dart b/frontend/lib/l10n/app_localizations.dart index 5305fbf..158a627 100644 --- a/frontend/lib/l10n/app_localizations.dart +++ b/frontend/lib/l10n/app_localizations.dart @@ -401,6 +401,18 @@ abstract class AppLocalizations { /// **'Send a message...'** String get sendMessage; + /// No description provided for @liveStreamEnded. + /// + /// In en, this message translates to: + /// **'The host has ended the live stream.'** + String get liveStreamEnded; + + /// No description provided for @liveStreamEndedShort. + /// + /// In en, this message translates to: + /// **'Live stream ended'** + String get liveStreamEndedShort; + /// No description provided for @liveChat. /// /// In en, this message translates to: diff --git a/frontend/lib/l10n/app_localizations_en.dart b/frontend/lib/l10n/app_localizations_en.dart index dbea0cf..b255cea 100644 --- a/frontend/lib/l10n/app_localizations_en.dart +++ b/frontend/lib/l10n/app_localizations_en.dart @@ -159,6 +159,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sendMessage => 'Send a message...'; + @override + String get liveStreamEnded => 'The host has ended the live stream.'; + + @override + String get liveStreamEndedShort => 'Live stream ended'; + @override String get liveChat => 'Live Chat'; diff --git a/frontend/lib/l10n/app_localizations_ja.dart b/frontend/lib/l10n/app_localizations_ja.dart index 7a266b6..3c27dd1 100644 --- a/frontend/lib/l10n/app_localizations_ja.dart +++ b/frontend/lib/l10n/app_localizations_ja.dart @@ -159,6 +159,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get sendMessage => 'メッセージを送信...'; + @override + String get liveStreamEnded => '配信者が退出したため、ライブ配信は終了しました。'; + + @override + String get liveStreamEndedShort => 'ライブ配信は終了しました'; + @override String get liveChat => 'ライブチャット'; diff --git a/frontend/lib/l10n/app_localizations_zh.dart b/frontend/lib/l10n/app_localizations_zh.dart index 01dee5e..1559879 100644 --- a/frontend/lib/l10n/app_localizations_zh.dart +++ b/frontend/lib/l10n/app_localizations_zh.dart @@ -158,6 +158,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get sendMessage => '发送消息...'; + @override + String get liveStreamEnded => '主播已退出,直播已结束。'; + + @override + String get liveStreamEndedShort => '直播已结束'; + @override String get liveChat => '实时聊天'; @@ -364,6 +370,12 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String get sendMessage => '發送訊息...'; + @override + String get liveStreamEnded => '主播已退出,直播已結束。'; + + @override + String get liveStreamEndedShort => '直播已結束'; + @override String get liveChat => '即時聊天'; diff --git a/frontend/lib/l10n/app_zh.arb b/frontend/lib/l10n/app_zh.arb index ebd1710..776fbb2 100644 --- a/frontend/lib/l10n/app_zh.arb +++ b/frontend/lib/l10n/app_zh.arb @@ -50,6 +50,8 @@ "availableNow": "当前可用", "waitingForTranscoding": "正在等待后端转码输出", "sendMessage": "发送消息...", + "liveStreamEnded": "主播已退出,直播已结束。", + "liveStreamEndedShort": "直播已结束", "liveChat": "实时聊天", "refresh": "刷新", "volume": "音量", diff --git a/frontend/lib/l10n/app_zh_Hant.arb b/frontend/lib/l10n/app_zh_Hant.arb index b7809fb..ecb2578 100644 --- a/frontend/lib/l10n/app_zh_Hant.arb +++ b/frontend/lib/l10n/app_zh_Hant.arb @@ -50,6 +50,8 @@ "availableNow": "目前可用", "waitingForTranscoding": "正在等待後端轉碼輸出", "sendMessage": "發送訊息...", + "liveStreamEnded": "主播已退出,直播已結束。", + "liveStreamEndedShort": "直播已結束", "liveChat": "即時聊天", "refresh": "重新整理", "volume": "音量", diff --git a/frontend/lib/pages/player_page.dart b/frontend/lib/pages/player_page.dart index 3702731..6778769 100644 --- a/frontend/lib/pages/player_page.dart +++ b/frontend/lib/pages/player_page.dart @@ -43,6 +43,7 @@ class _PlayerPageState extends State { bool _isRefreshing = false; bool _isFullscreen = false; bool _controlsVisible = true; + bool _streamEnded = false; double _volume = kIsWeb ? 0.0 : 1.0; int _playerVersion = 0; String _selectedResolution = 'Source'; @@ -146,6 +147,10 @@ class _PlayerPageState extends State { _chatService.messages.listen((msg) { if (mounted) { + if (msg.type == "stream_end") { + _handleStreamEnded(msg.content); + return; + } setState(() { _messages.insert(0, msg); if (!msg.isHistory && (msg.type == "chat" || msg.type == "danmaku")) { @@ -158,6 +163,44 @@ class _PlayerPageState extends State { }); } + void _handleStreamEnded(String message) { + if (_streamEnded) { + return; + } + + final l10n = AppLocalizations.of(context)!; + final streamEndedMessage = l10n.liveStreamEnded; + + setState(() { + _streamEnded = true; + _isRefreshing = false; + _danmakus.clear(); + _messages.insert( + 0, + ChatMessage( + type: "system", + username: "System", + content: streamEndedMessage, + roomId: widget.roomId, + ), + ); + if (!kIsWeb) { + _isError = true; + _errorMessage = streamEndedMessage; + } + }); + + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(streamEndedMessage))); + + Future.delayed(const Duration(seconds: 2), () { + if (mounted && Navigator.canPop(context)) { + Navigator.pop(context); + } + }); + } + void _addDanmaku(String text) { final key = UniqueKey(); final lane = DateTime.now().millisecondsSinceEpoch % 8; @@ -180,7 +223,7 @@ class _PlayerPageState extends State { } void _sendMsg() { - if (_msgController.text.isNotEmpty) { + if (!_streamEnded && _msgController.text.isNotEmpty) { final auth = context.read(); _chatService.sendMessage( _msgController.text, @@ -195,6 +238,9 @@ class _PlayerPageState extends State { if (_isRefreshing) { return; } + if (_streamEnded) { + return; + } await _loadPlaybackOptions(); @@ -625,7 +671,7 @@ class _PlayerPageState extends State { _buildControlButton( icon: Icons.refresh, label: l10n.refresh, - onPressed: _refreshPlayer, + onPressed: _streamEnded ? null : _refreshPlayer, ), _buildControlButton( icon: _volume == 0 @@ -645,7 +691,9 @@ class _PlayerPageState extends State { icon: _isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, - label: _isFullscreen ? l10n.exitFullscreen : l10n.fullscreen, + label: _isFullscreen + ? l10n.exitFullscreen + : l10n.fullscreen, onPressed: _toggleFullscreen, ), _buildControlButton( @@ -665,13 +713,15 @@ class _PlayerPageState extends State { Widget _buildControlButton({ required IconData icon, required String label, - required FutureOr Function() onPressed, + required FutureOr Function()? onPressed, }) { return FilledButton.tonalIcon( - onPressed: () async { - _showControls(); - await onPressed(); - }, + onPressed: onPressed == null + ? null + : () async { + _showControls(); + await onPressed(); + }, icon: Icon(icon, size: 18), label: Text(label), style: FilledButton.styleFrom( @@ -693,7 +743,10 @@ class _PlayerPageState extends State { children: [ const Icon(Icons.chat_bubble_outline, size: 16), const SizedBox(width: 8), - Text(l10n.liveChat, style: const TextStyle(fontWeight: FontWeight.bold)), + Text( + l10n.liveChat, + style: const TextStyle(fontWeight: FontWeight.bold), + ), ], ), ), @@ -716,8 +769,11 @@ class _PlayerPageState extends State { Expanded( child: TextField( controller: _msgController, + enabled: !_streamEnded, decoration: InputDecoration( - hintText: l10n.sendMessage, + hintText: _streamEnded + ? l10n.liveStreamEndedShort + : l10n.sendMessage, border: OutlineInputBorder( borderRadius: BorderRadius.circular(20), ), @@ -734,7 +790,7 @@ class _PlayerPageState extends State { Icons.send, color: Theme.of(context).colorScheme.primary, ), - onPressed: _sendMsg, + onPressed: _streamEnded ? null : _sendMsg, ), ], ), diff --git a/frontend/lib/services/chat_service.dart b/frontend/lib/services/chat_service.dart index e279163..0b97df9 100644 --- a/frontend/lib/services/chat_service.dart +++ b/frontend/lib/services/chat_service.dart @@ -46,7 +46,6 @@ class ChatService { void connect(String baseUrl, String roomId, String username) { final wsUri = _webSocketUri(baseUrl).replace( - scheme: 'ws', path: '/api/ws/room/$roomId', queryParameters: {'username': username}, );