fix: 完善直播结束通知与 WebSocket 连接

This commit is contained in:
2026-06-25 10:50:35 +08:00
parent 2281c98b1b
commit 80d4f692e0
12 changed files with 130 additions and 14 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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",

View File

@@ -50,6 +50,8 @@
"availableNow": "利用可能",
"waitingForTranscoding": "バックエンドのトランスコード出力を待機中",
"sendMessage": "メッセージを送信...",
"liveStreamEnded": "配信者が退出したため、ライブ配信は終了しました。",
"liveStreamEndedShort": "ライブ配信は終了しました",
"liveChat": "ライブチャット",
"refresh": "更新",
"volume": "音量",

View File

@@ -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:

View File

@@ -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';

View File

@@ -159,6 +159,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get sendMessage => 'メッセージを送信...';
@override
String get liveStreamEnded => '配信者が退出したため、ライブ配信は終了しました。';
@override
String get liveStreamEndedShort => 'ライブ配信は終了しました';
@override
String get liveChat => 'ライブチャット';

View File

@@ -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 => '即時聊天';

View File

@@ -50,6 +50,8 @@
"availableNow": "当前可用",
"waitingForTranscoding": "正在等待后端转码输出",
"sendMessage": "发送消息...",
"liveStreamEnded": "主播已退出,直播已结束。",
"liveStreamEndedShort": "直播已结束",
"liveChat": "实时聊天",
"refresh": "刷新",
"volume": "音量",

View File

@@ -50,6 +50,8 @@
"availableNow": "目前可用",
"waitingForTranscoding": "正在等待後端轉碼輸出",
"sendMessage": "發送訊息...",
"liveStreamEnded": "主播已退出,直播已結束。",
"liveStreamEndedShort": "直播已結束",
"liveChat": "即時聊天",
"refresh": "重新整理",
"volume": "音量",

View File

@@ -43,6 +43,7 @@ class _PlayerPageState extends State<PlayerPage> {
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<PlayerPage> {
_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<PlayerPage> {
});
}
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<void>.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<PlayerPage> {
}
void _sendMsg() {
if (_msgController.text.isNotEmpty) {
if (!_streamEnded && _msgController.text.isNotEmpty) {
final auth = context.read<AuthProvider>();
_chatService.sendMessage(
_msgController.text,
@@ -195,6 +238,9 @@ class _PlayerPageState extends State<PlayerPage> {
if (_isRefreshing) {
return;
}
if (_streamEnded) {
return;
}
await _loadPlaybackOptions();
@@ -625,7 +671,7 @@ class _PlayerPageState extends State<PlayerPage> {
_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<PlayerPage> {
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<PlayerPage> {
Widget _buildControlButton({
required IconData icon,
required String label,
required FutureOr<void> Function() onPressed,
required FutureOr<void> 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<PlayerPage> {
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<PlayerPage> {
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<PlayerPage> {
Icons.send,
color: Theme.of(context).colorScheme.primary,
),
onPressed: _sendMsg,
onPressed: _streamEnded ? null : _sendMsg,
),
],
),

View File

@@ -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},
);