fix: 完善直播结束通知与 WebSocket 连接
This commit is contained in:
@@ -18,7 +18,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Type string `json:"type"` // "chat", "system", "danmaku"
|
Type string `json:"type"` // "chat", "system", "danmaku", "stream_end"
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
@@ -190,7 +190,15 @@ func (r *roomHub) handleBroadcast(message Message) {
|
|||||||
delete(r.clients, client)
|
delete(r.clients, client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
shouldDeleteIfIdle := message.Type == "stream_end"
|
||||||
|
if shouldDeleteIfIdle {
|
||||||
|
r.history = nil
|
||||||
|
}
|
||||||
r.mutex.Unlock()
|
r.mutex.Unlock()
|
||||||
|
|
||||||
|
if shouldDeleteIfIdle {
|
||||||
|
r.manager.deleteRoomIfIdle(r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roomHub) handleClearHistory() {
|
func (r *roomHub) handleClearHistory() {
|
||||||
@@ -223,6 +231,15 @@ func (h *Hub) BroadcastToRoom(msg Message) {
|
|||||||
h.getOrCreateRoom(msg.RoomID).broadcast <- msg
|
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() {
|
func (c *Client) ReadPump() {
|
||||||
defer func() {
|
defer func() {
|
||||||
c.Hub.UnregisterClient(c)
|
c.Hub.UnregisterClient(c)
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ func NewRTMPServer(rtmpPort string) *RTMPServer {
|
|||||||
monitor.Warnf("Failed to mark room inactive room_id=%s: %v", roomID, err)
|
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)
|
monitor.Infof("Publishing ended for room_id=%s", roomID)
|
||||||
} else {
|
} else {
|
||||||
monitor.Infof("Variant publishing ended for room_id=%s path=%s", roomID, channelPath)
|
monitor.Infof("Variant publishing ended for room_id=%s path=%s", roomID, channelPath)
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
"availableNow": "Available now",
|
"availableNow": "Available now",
|
||||||
"waitingForTranscoding": "Waiting for backend transcoding output",
|
"waitingForTranscoding": "Waiting for backend transcoding output",
|
||||||
"sendMessage": "Send a message...",
|
"sendMessage": "Send a message...",
|
||||||
|
"liveStreamEnded": "The host has ended the live stream.",
|
||||||
|
"liveStreamEndedShort": "Live stream ended",
|
||||||
"liveChat": "Live Chat",
|
"liveChat": "Live Chat",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"volume": "Volume",
|
"volume": "Volume",
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
"availableNow": "利用可能",
|
"availableNow": "利用可能",
|
||||||
"waitingForTranscoding": "バックエンドのトランスコード出力を待機中",
|
"waitingForTranscoding": "バックエンドのトランスコード出力を待機中",
|
||||||
"sendMessage": "メッセージを送信...",
|
"sendMessage": "メッセージを送信...",
|
||||||
|
"liveStreamEnded": "配信者が退出したため、ライブ配信は終了しました。",
|
||||||
|
"liveStreamEndedShort": "ライブ配信は終了しました",
|
||||||
"liveChat": "ライブチャット",
|
"liveChat": "ライブチャット",
|
||||||
"refresh": "更新",
|
"refresh": "更新",
|
||||||
"volume": "音量",
|
"volume": "音量",
|
||||||
|
|||||||
@@ -401,6 +401,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Send a message...'**
|
/// **'Send a message...'**
|
||||||
String get sendMessage;
|
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.
|
/// No description provided for @liveChat.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -159,6 +159,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sendMessage => 'Send a message...';
|
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
|
@override
|
||||||
String get liveChat => 'Live Chat';
|
String get liveChat => 'Live Chat';
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sendMessage => 'メッセージを送信...';
|
String get sendMessage => 'メッセージを送信...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get liveStreamEnded => '配信者が退出したため、ライブ配信は終了しました。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get liveStreamEndedShort => 'ライブ配信は終了しました';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get liveChat => 'ライブチャット';
|
String get liveChat => 'ライブチャット';
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sendMessage => '发送消息...';
|
String get sendMessage => '发送消息...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get liveStreamEnded => '主播已退出,直播已结束。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get liveStreamEndedShort => '直播已结束';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get liveChat => '实时聊天';
|
String get liveChat => '实时聊天';
|
||||||
|
|
||||||
@@ -364,6 +370,12 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get sendMessage => '發送訊息...';
|
String get sendMessage => '發送訊息...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get liveStreamEnded => '主播已退出,直播已結束。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get liveStreamEndedShort => '直播已結束';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get liveChat => '即時聊天';
|
String get liveChat => '即時聊天';
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
"availableNow": "当前可用",
|
"availableNow": "当前可用",
|
||||||
"waitingForTranscoding": "正在等待后端转码输出",
|
"waitingForTranscoding": "正在等待后端转码输出",
|
||||||
"sendMessage": "发送消息...",
|
"sendMessage": "发送消息...",
|
||||||
|
"liveStreamEnded": "主播已退出,直播已结束。",
|
||||||
|
"liveStreamEndedShort": "直播已结束",
|
||||||
"liveChat": "实时聊天",
|
"liveChat": "实时聊天",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"volume": "音量",
|
"volume": "音量",
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
"availableNow": "目前可用",
|
"availableNow": "目前可用",
|
||||||
"waitingForTranscoding": "正在等待後端轉碼輸出",
|
"waitingForTranscoding": "正在等待後端轉碼輸出",
|
||||||
"sendMessage": "發送訊息...",
|
"sendMessage": "發送訊息...",
|
||||||
|
"liveStreamEnded": "主播已退出,直播已結束。",
|
||||||
|
"liveStreamEndedShort": "直播已結束",
|
||||||
"liveChat": "即時聊天",
|
"liveChat": "即時聊天",
|
||||||
"refresh": "重新整理",
|
"refresh": "重新整理",
|
||||||
"volume": "音量",
|
"volume": "音量",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
bool _isRefreshing = false;
|
bool _isRefreshing = false;
|
||||||
bool _isFullscreen = false;
|
bool _isFullscreen = false;
|
||||||
bool _controlsVisible = true;
|
bool _controlsVisible = true;
|
||||||
|
bool _streamEnded = false;
|
||||||
double _volume = kIsWeb ? 0.0 : 1.0;
|
double _volume = kIsWeb ? 0.0 : 1.0;
|
||||||
int _playerVersion = 0;
|
int _playerVersion = 0;
|
||||||
String _selectedResolution = 'Source';
|
String _selectedResolution = 'Source';
|
||||||
@@ -146,6 +147,10 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
|
|
||||||
_chatService.messages.listen((msg) {
|
_chatService.messages.listen((msg) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
if (msg.type == "stream_end") {
|
||||||
|
_handleStreamEnded(msg.content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages.insert(0, msg);
|
_messages.insert(0, msg);
|
||||||
if (!msg.isHistory && (msg.type == "chat" || msg.type == "danmaku")) {
|
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) {
|
void _addDanmaku(String text) {
|
||||||
final key = UniqueKey();
|
final key = UniqueKey();
|
||||||
final lane = DateTime.now().millisecondsSinceEpoch % 8;
|
final lane = DateTime.now().millisecondsSinceEpoch % 8;
|
||||||
@@ -180,7 +223,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _sendMsg() {
|
void _sendMsg() {
|
||||||
if (_msgController.text.isNotEmpty) {
|
if (!_streamEnded && _msgController.text.isNotEmpty) {
|
||||||
final auth = context.read<AuthProvider>();
|
final auth = context.read<AuthProvider>();
|
||||||
_chatService.sendMessage(
|
_chatService.sendMessage(
|
||||||
_msgController.text,
|
_msgController.text,
|
||||||
@@ -195,6 +238,9 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
if (_isRefreshing) {
|
if (_isRefreshing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_streamEnded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _loadPlaybackOptions();
|
await _loadPlaybackOptions();
|
||||||
|
|
||||||
@@ -625,7 +671,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
_buildControlButton(
|
_buildControlButton(
|
||||||
icon: Icons.refresh,
|
icon: Icons.refresh,
|
||||||
label: l10n.refresh,
|
label: l10n.refresh,
|
||||||
onPressed: _refreshPlayer,
|
onPressed: _streamEnded ? null : _refreshPlayer,
|
||||||
),
|
),
|
||||||
_buildControlButton(
|
_buildControlButton(
|
||||||
icon: _volume == 0
|
icon: _volume == 0
|
||||||
@@ -645,7 +691,9 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
icon: _isFullscreen
|
icon: _isFullscreen
|
||||||
? Icons.fullscreen_exit
|
? Icons.fullscreen_exit
|
||||||
: Icons.fullscreen,
|
: Icons.fullscreen,
|
||||||
label: _isFullscreen ? l10n.exitFullscreen : l10n.fullscreen,
|
label: _isFullscreen
|
||||||
|
? l10n.exitFullscreen
|
||||||
|
: l10n.fullscreen,
|
||||||
onPressed: _toggleFullscreen,
|
onPressed: _toggleFullscreen,
|
||||||
),
|
),
|
||||||
_buildControlButton(
|
_buildControlButton(
|
||||||
@@ -665,13 +713,15 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
Widget _buildControlButton({
|
Widget _buildControlButton({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String label,
|
required String label,
|
||||||
required FutureOr<void> Function() onPressed,
|
required FutureOr<void> Function()? onPressed,
|
||||||
}) {
|
}) {
|
||||||
return FilledButton.tonalIcon(
|
return FilledButton.tonalIcon(
|
||||||
onPressed: () async {
|
onPressed: onPressed == null
|
||||||
_showControls();
|
? null
|
||||||
await onPressed();
|
: () async {
|
||||||
},
|
_showControls();
|
||||||
|
await onPressed();
|
||||||
|
},
|
||||||
icon: Icon(icon, size: 18),
|
icon: Icon(icon, size: 18),
|
||||||
label: Text(label),
|
label: Text(label),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
@@ -693,7 +743,10 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.chat_bubble_outline, size: 16),
|
const Icon(Icons.chat_bubble_outline, size: 16),
|
||||||
const SizedBox(width: 8),
|
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(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _msgController,
|
controller: _msgController,
|
||||||
|
enabled: !_streamEnded,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: l10n.sendMessage,
|
hintText: _streamEnded
|
||||||
|
? l10n.liveStreamEndedShort
|
||||||
|
: l10n.sendMessage,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
@@ -734,7 +790,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
Icons.send,
|
Icons.send,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
onPressed: _sendMsg,
|
onPressed: _streamEnded ? null : _sendMsg,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class ChatService {
|
|||||||
|
|
||||||
void connect(String baseUrl, String roomId, String username) {
|
void connect(String baseUrl, String roomId, String username) {
|
||||||
final wsUri = _webSocketUri(baseUrl).replace(
|
final wsUri = _webSocketUri(baseUrl).replace(
|
||||||
scheme: 'ws',
|
|
||||||
path: '/api/ws/room/$roomId',
|
path: '/api/ws/room/$roomId',
|
||||||
queryParameters: {'username': username},
|
queryParameters: {'username': username},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user