diff --git a/web/src/pages/VideoRoomPage.css b/web/src/pages/VideoRoomPage.css index 51532b1..cf1bcad 100644 --- a/web/src/pages/VideoRoomPage.css +++ b/web/src/pages/VideoRoomPage.css @@ -61,9 +61,6 @@ } .video-room-shell { - display: grid; - grid-template-columns: minmax(0, 1fr) 360px; - gap: 18px; max-width: 1720px; margin: 0 auto; } @@ -100,113 +97,6 @@ color: var(--ant-color-primary, #1677ff); } -.video-room-side { - display: flex; - flex-direction: column; - gap: 14px; - min-width: 0; -} - -.video-room-panel.ant-card { - border-color: rgba(255, 255, 255, 0.08); - background: #14191d; -} - -.video-room-panel .ant-card-head { - min-height: 48px; - border-bottom-color: rgba(255, 255, 255, 0.08); -} - -.video-room-panel .ant-card-head-title { - color: rgba(255, 255, 255, 0.9); - font-weight: 600; -} - -.video-room-panel .ant-card-body { - padding: 16px; -} - -.video-room-dot { - display: block; - width: 8px; - height: 8px; - border-radius: 999px; - background: var(--ant-color-error, #ff4d4f); - box-shadow: 0 0 0 4px rgba(255, 77, 79, 0.14); -} - -.video-room-dot.is-connected { - background: var(--ant-color-success, #52c41a); - box-shadow: 0 0 0 4px rgba(82, 196, 26, 0.14); -} - -.video-room-status-list { - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 16px; -} - -.video-room-status-item { - display: flex; - align-items: center; - gap: 12px; - min-width: 0; - padding: 12px; - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 8px; - background: rgba(255, 255, 255, 0.03); -} - -.video-room-status-item > .anticon { - flex: none; - color: var(--ant-color-primary, #1677ff); - font-size: 18px; -} - -.video-room-status-item div { - min-width: 0; -} - -.video-room-status-item span { - display: block; - margin-bottom: 2px; - color: rgba(255, 255, 255, 0.45); - font-size: 12px; -} - -.video-room-status-item strong { - display: block; - overflow: hidden; - color: rgba(255, 255, 255, 0.88); - font-size: 14px; - font-weight: 600; - text-overflow: ellipsis; - white-space: nowrap; -} - -.video-room-primary-action.ant-btn { - height: 40px; -} - -.video-room-panel__text { - margin: 0 0 14px; - color: rgba(255, 255, 255, 0.55); - font-size: 14px; - line-height: 1.6; -} - -@media (max-width: 1180px) { - .video-room-shell { - grid-template-columns: 1fr; - } - - .video-room-side { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - @media (max-width: 760px) { .video-room-page { padding: 14px; @@ -231,8 +121,4 @@ height: 56vh; min-height: 280px; } - - .video-room-side { - display: flex; - } } diff --git a/web/src/pages/VideoRoomPage.tsx b/web/src/pages/VideoRoomPage.tsx index 852c41b..fdb6750 100644 --- a/web/src/pages/VideoRoomPage.tsx +++ b/web/src/pages/VideoRoomPage.tsx @@ -1,15 +1,10 @@ import { memo, useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router'; -import { Button, Card, Empty, Space, Spin, Tag, Tooltip, Typography, message } from 'antd'; +import { Button, Empty, Space, Spin, Tag, Tooltip, Typography, message } from 'antd'; import { CheckCircleFilled, - ClockCircleOutlined, - CopyOutlined, DisconnectOutlined, - FileTextOutlined, LinkOutlined, - PlayCircleOutlined, - ReloadOutlined, } from '@ant-design/icons'; import Artplayer from 'artplayer'; import { videoRoomsApi, type VideoRoomInfo, type VideoRoomState } from '../api/videoRooms'; @@ -20,6 +15,16 @@ const { Text, Title } = Typography; const SYNC_THRESHOLD = 1.2; +function getSyncedTime(state: VideoRoomState) { + const baseTime = Math.max(0, Number(state.current_time) || 0); + if (state.paused || !state.updated_at) return baseTime; + + const updatedAt = Date.parse(state.updated_at); + if (Number.isNaN(updatedAt)) return baseTime; + + return baseTime + Math.max(0, (Date.now() - updatedAt) / 1000); +} + function formatTime(seconds: number) { const safeSeconds = Math.max(0, Math.floor(seconds || 0)); const hours = Math.floor(safeSeconds / 3600); @@ -43,6 +48,7 @@ const VideoRoomPage = memo(function VideoRoomPage() { const wsRef = useRef(null); const applyingRemoteRef = useRef(false); const sendTimerRef = useRef(null); + const liveStateRef = useRef(null); const [room, setRoom] = useState(null); const [liveState, setLiveState] = useState(null); const [loading, setLoading] = useState(true); @@ -58,6 +64,7 @@ const VideoRoomPage = memo(function VideoRoomPage() { if (!mounted) return; setRoom(data); setLiveState(data.state); + liveStateRef.current = data.state; }) .catch((e: any) => { if (!mounted) return; @@ -100,10 +107,11 @@ const VideoRoomPage = memo(function VideoRoomPage() { const applyState = (state: VideoRoomState) => { const art = artInstance.current; + liveStateRef.current = state; setLiveState(state); if (!art) return; const video = art.video; - const targetTime = Math.max(0, Number(state.current_time) || 0); + const targetTime = getSyncedTime(state); applyingRemoteRef.current = true; if (Math.abs((video.currentTime || 0) - targetTime) > SYNC_THRESHOLD) { video.currentTime = targetTime; @@ -119,6 +127,10 @@ const VideoRoomPage = memo(function VideoRoomPage() { }, 250); }; + const applyLatestState = () => { + applyState(liveStateRef.current || room.state); + }; + const art = new Artplayer({ container: artRef.current, url: videoRoomsApi.streamUrl(token), @@ -131,7 +143,9 @@ const VideoRoomPage = memo(function VideoRoomPage() { }); artInstance.current = art; - art.on('ready', () => applyState(room.state)); + art.on('ready', applyLatestState); + art.video.addEventListener('loadedmetadata', applyLatestState); + art.video.addEventListener('canplay', applyLatestState); art.on('play', sendStateSoon); art.on('pause', sendStateSoon); art.on('seek', sendStateSoon); @@ -157,6 +171,8 @@ const VideoRoomPage = memo(function VideoRoomPage() { window.clearTimeout(sendTimerRef.current); sendTimerRef.current = null; } + art.video.removeEventListener('loadedmetadata', applyLatestState); + art.video.removeEventListener('canplay', applyLatestState); ws.close(); art.destroy(); wsRef.current = null; @@ -169,18 +185,6 @@ const VideoRoomPage = memo(function VideoRoomPage() { message.success(t('Copied to clipboard')); }; - const handleResync = () => { - if (!liveState) return; - const art = artInstance.current; - if (!art) return; - art.video.currentTime = Math.max(0, Number(liveState.current_time) || 0); - if (liveState.paused) { - void art.video.pause(); - } else { - void art.video.play().catch(() => undefined); - } - }; - if (loading) { return (
@@ -238,50 +242,6 @@ const VideoRoomPage = memo(function VideoRoomPage() { {t('Playback state is shared in this room')}
- - );