11import { memo , useEffect , useRef , useState } from 'react' ;
22import { useParams } from 'react-router' ;
3- import { Button , Card , Empty , Space , Spin , Tag , Tooltip , Typography , message } from 'antd' ;
3+ import { Button , Empty , Space , Spin , Tag , Tooltip , Typography , message } from 'antd' ;
44import {
55 CheckCircleFilled ,
6- ClockCircleOutlined ,
7- CopyOutlined ,
86 DisconnectOutlined ,
9- FileTextOutlined ,
107 LinkOutlined ,
11- PlayCircleOutlined ,
12- ReloadOutlined ,
138} from '@ant-design/icons' ;
149import Artplayer from 'artplayer' ;
1510import { videoRoomsApi , type VideoRoomInfo , type VideoRoomState } from '../api/videoRooms' ;
@@ -20,6 +15,16 @@ const { Text, Title } = Typography;
2015
2116const SYNC_THRESHOLD = 1.2 ;
2217
18+ function getSyncedTime ( state : VideoRoomState ) {
19+ const baseTime = Math . max ( 0 , Number ( state . current_time ) || 0 ) ;
20+ if ( state . paused || ! state . updated_at ) return baseTime ;
21+
22+ const updatedAt = Date . parse ( state . updated_at ) ;
23+ if ( Number . isNaN ( updatedAt ) ) return baseTime ;
24+
25+ return baseTime + Math . max ( 0 , ( Date . now ( ) - updatedAt ) / 1000 ) ;
26+ }
27+
2328function formatTime ( seconds : number ) {
2429 const safeSeconds = Math . max ( 0 , Math . floor ( seconds || 0 ) ) ;
2530 const hours = Math . floor ( safeSeconds / 3600 ) ;
@@ -43,6 +48,7 @@ const VideoRoomPage = memo(function VideoRoomPage() {
4348 const wsRef = useRef < WebSocket | null > ( null ) ;
4449 const applyingRemoteRef = useRef ( false ) ;
4550 const sendTimerRef = useRef < number | null > ( null ) ;
51+ const liveStateRef = useRef < VideoRoomState | null > ( null ) ;
4652 const [ room , setRoom ] = useState < VideoRoomInfo | null > ( null ) ;
4753 const [ liveState , setLiveState ] = useState < VideoRoomState | null > ( null ) ;
4854 const [ loading , setLoading ] = useState ( true ) ;
@@ -58,6 +64,7 @@ const VideoRoomPage = memo(function VideoRoomPage() {
5864 if ( ! mounted ) return ;
5965 setRoom ( data ) ;
6066 setLiveState ( data . state ) ;
67+ liveStateRef . current = data . state ;
6168 } )
6269 . catch ( ( e : any ) => {
6370 if ( ! mounted ) return ;
@@ -100,10 +107,11 @@ const VideoRoomPage = memo(function VideoRoomPage() {
100107
101108 const applyState = ( state : VideoRoomState ) => {
102109 const art = artInstance . current ;
110+ liveStateRef . current = state ;
103111 setLiveState ( state ) ;
104112 if ( ! art ) return ;
105113 const video = art . video ;
106- const targetTime = Math . max ( 0 , Number ( state . current_time ) || 0 ) ;
114+ const targetTime = getSyncedTime ( state ) ;
107115 applyingRemoteRef . current = true ;
108116 if ( Math . abs ( ( video . currentTime || 0 ) - targetTime ) > SYNC_THRESHOLD ) {
109117 video . currentTime = targetTime ;
@@ -119,6 +127,10 @@ const VideoRoomPage = memo(function VideoRoomPage() {
119127 } , 250 ) ;
120128 } ;
121129
130+ const applyLatestState = ( ) => {
131+ applyState ( liveStateRef . current || room . state ) ;
132+ } ;
133+
122134 const art = new Artplayer ( {
123135 container : artRef . current ,
124136 url : videoRoomsApi . streamUrl ( token ) ,
@@ -131,7 +143,9 @@ const VideoRoomPage = memo(function VideoRoomPage() {
131143 } ) ;
132144 artInstance . current = art ;
133145
134- art . on ( 'ready' , ( ) => applyState ( room . state ) ) ;
146+ art . on ( 'ready' , applyLatestState ) ;
147+ art . video . addEventListener ( 'loadedmetadata' , applyLatestState ) ;
148+ art . video . addEventListener ( 'canplay' , applyLatestState ) ;
135149 art . on ( 'play' , sendStateSoon ) ;
136150 art . on ( 'pause' , sendStateSoon ) ;
137151 art . on ( 'seek' , sendStateSoon ) ;
@@ -157,6 +171,8 @@ const VideoRoomPage = memo(function VideoRoomPage() {
157171 window . clearTimeout ( sendTimerRef . current ) ;
158172 sendTimerRef . current = null ;
159173 }
174+ art . video . removeEventListener ( 'loadedmetadata' , applyLatestState ) ;
175+ art . video . removeEventListener ( 'canplay' , applyLatestState ) ;
160176 ws . close ( ) ;
161177 art . destroy ( ) ;
162178 wsRef . current = null ;
@@ -169,18 +185,6 @@ const VideoRoomPage = memo(function VideoRoomPage() {
169185 message . success ( t ( 'Copied to clipboard' ) ) ;
170186 } ;
171187
172- const handleResync = ( ) => {
173- if ( ! liveState ) return ;
174- const art = artInstance . current ;
175- if ( ! art ) return ;
176- art . video . currentTime = Math . max ( 0 , Number ( liveState . current_time ) || 0 ) ;
177- if ( liveState . paused ) {
178- void art . video . pause ( ) ;
179- } else {
180- void art . video . play ( ) . catch ( ( ) => undefined ) ;
181- }
182- } ;
183-
184188 if ( loading ) {
185189 return (
186190 < div className = "video-room-page video-room-page--center" >
@@ -238,50 +242,6 @@ const VideoRoomPage = memo(function VideoRoomPage() {
238242 < span > { t ( 'Playback state is shared in this room' ) } </ span >
239243 </ div >
240244 </ section >
241-
242- < aside className = "video-room-side" >
243- < Card
244- className = "video-room-panel"
245- title = { t ( 'Room status' ) }
246- extra = { < span className = { connected ? 'video-room-dot is-connected' : 'video-room-dot' } /> }
247- >
248- < div className = "video-room-status-list" >
249- < div className = "video-room-status-item" >
250- < PlayCircleOutlined />
251- < div >
252- < span > { t ( 'Playback' ) } </ span >
253- < strong > { state . paused ? t ( 'Paused' ) : t ( 'Playing' ) } </ strong >
254- </ div >
255- </ div >
256- < div className = "video-room-status-item" >
257- < ClockCircleOutlined />
258- < div >
259- < span > { t ( 'Current position' ) } </ span >
260- < strong > { formatTime ( state . current_time ) } </ strong >
261- </ div >
262- </ div >
263- < div className = "video-room-status-item" >
264- < FileTextOutlined />
265- < div >
266- < span > { t ( 'File' ) } </ span >
267- < strong title = { room . path } > { fileName } </ strong >
268- </ div >
269- </ div >
270- </ div >
271- < Button block className = "video-room-primary-action" icon = { < ReloadOutlined /> } onClick = { handleResync } >
272- { t ( 'Resync playback' ) }
273- </ Button >
274- </ Card >
275-
276- < Card className = "video-room-panel" title = { t ( 'Room link' ) } >
277- < p className = "video-room-panel__text" >
278- { t ( 'Share this room link with friends' ) }
279- </ p >
280- < Button block icon = { < CopyOutlined /> } onClick = { handleCopy } >
281- { t ( 'Copy Link' ) }
282- </ Button >
283- </ Card >
284- </ aside >
285245 </ main >
286246 </ div >
287247 ) ;
0 commit comments