|
|
|
|
@ -1,25 +1,15 @@
|
|
|
|
|
import '@vidstack/react/player/styles/base.css'
|
|
|
|
|
import {
|
|
|
|
|
MediaPlayer,
|
|
|
|
|
MediaProvider,
|
|
|
|
|
MediaPlayerInstance,
|
|
|
|
|
isHLSProvider,
|
|
|
|
|
MediaProviderAdapter,
|
|
|
|
|
MediaProviderChangeEvent,
|
|
|
|
|
MediaPlayerState,
|
|
|
|
|
} from '@vidstack/react'
|
|
|
|
|
import { VideoLayout } from './video-control'
|
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
|
|
|
import { useCallback, useEffect, useRef } from 'react'
|
|
|
|
|
import { socket } from './socket'
|
|
|
|
|
import { ClientMessage, ClientUserStatus, RoomState, RoomStateChangedMessage, SetTimeMessage } from '@/lib/types/message'
|
|
|
|
|
import { $playerState, $userInfo, $userStatus } from '@/store/player'
|
|
|
|
|
import { useStore as useNanoStore } from '@nanostores/react'
|
|
|
|
|
import HLS from 'hls.js'
|
|
|
|
|
import { getUserPlayTime, getUserPlayTimeSeconds } from '@/lib/utils'
|
|
|
|
|
import { getUserPlayTimeSeconds, unixTimestampWithOffset } from '@/lib/utils'
|
|
|
|
|
|
|
|
|
|
const TIME_SYNC_ALLOWANCE = 5 // 如果播放时间与服务器时间差小于此值,则认为是同步的
|
|
|
|
|
|
|
|
|
|
export const Player = ({ roomName }: { roomName: string }) => {
|
|
|
|
|
const player = useRef<HTMLVideoElement | null>(null)
|
|
|
|
|
|
|
|
|
|
const userInfo = useNanoStore($userInfo)
|
|
|
|
|
const prevUserState = useRef<ClientUserStatus>({
|
|
|
|
|
playTime: 0,
|
|
|
|
|
@ -27,75 +17,111 @@ export const Player = ({ roomName }: { roomName: string }) => {
|
|
|
|
|
playing: false,
|
|
|
|
|
})
|
|
|
|
|
const prevRoomState = useRef<RoomState>({})
|
|
|
|
|
let player = useRef<MediaPlayerInstance>(null)
|
|
|
|
|
const prevPlayerState = useRef<MediaPlayerState | null>(null)
|
|
|
|
|
const isUserStartPlay = useRef(false)
|
|
|
|
|
const playerState = useNanoStore($playerState)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
/** 与服务器时间之间的时差 */
|
|
|
|
|
const serverTimeAdjust = useRef<number>(0)
|
|
|
|
|
|
|
|
|
|
const handleUserStateUpdated = useCallback((force: boolean = false) => {
|
|
|
|
|
if (!player.current) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subscribe to state updates.
|
|
|
|
|
return player.current.subscribe((state) => {
|
|
|
|
|
if (prevPlayerState.current?.seeking && !state.seeking && userInfo?.isAdmin) {
|
|
|
|
|
console.log('seeking, update time')
|
|
|
|
|
socket.emit("setTime", {
|
|
|
|
|
username: userInfo?.username,
|
|
|
|
|
playTime: Math.floor(state.currentTime),
|
|
|
|
|
room: roomName
|
|
|
|
|
} as ClientMessage)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!prevPlayerState.current?.playing && state.playing) {
|
|
|
|
|
console.log('跳转到最新播放时间')
|
|
|
|
|
// 开始播放时,跳转到最新的播放时间
|
|
|
|
|
if (player.current &&
|
|
|
|
|
($userInfo.value?.isAdmin && !isUserStartPlay.current) && // 如果是管理员且用户没有开始播放,则跳转到最新的播放时间
|
|
|
|
|
prevRoomState.current.playing &&
|
|
|
|
|
prevRoomState.current.playTime !== undefined &&
|
|
|
|
|
prevRoomState.current.playTimeUpdateTime !== undefined) {
|
|
|
|
|
const deltaTime = (Date.now() / 1000) - prevRoomState.current.playTimeUpdateTime
|
|
|
|
|
const computedTime = getUserPlayTimeSeconds(prevUserState.current, serverTimeAdjust.current)
|
|
|
|
|
const playerTime = Math.floor(player.current?.currentTime ?? 0)
|
|
|
|
|
const isPlaying = !player.current?.paused
|
|
|
|
|
|
|
|
|
|
player.current.currentTime = prevRoomState.current.playTime + deltaTime
|
|
|
|
|
}
|
|
|
|
|
// 标记用户开始播放
|
|
|
|
|
isUserStartPlay.current = true;
|
|
|
|
|
if (force || isPlaying !== prevUserState.current.playing ||
|
|
|
|
|
(isPlaying && Math.abs(playerTime - computedTime) > TIME_SYNC_ALLOWANCE)) {
|
|
|
|
|
prevUserState.current = {
|
|
|
|
|
playTime: playerTime,
|
|
|
|
|
updateTime: unixTimestampWithOffset(serverTimeAdjust.current),
|
|
|
|
|
playing: isPlaying
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prevPlayerState.current = {...state}
|
|
|
|
|
})
|
|
|
|
|
}, [userInfo, roomName, playerState?.url])
|
|
|
|
|
socket.emit("updateUserState", {
|
|
|
|
|
username: $userInfo.value?.username,
|
|
|
|
|
room: roomName,
|
|
|
|
|
playTime: playerTime,
|
|
|
|
|
playing: isPlaying,
|
|
|
|
|
} as ClientMessage)
|
|
|
|
|
}
|
|
|
|
|
}, [roomName])
|
|
|
|
|
|
|
|
|
|
const handleSeeked = useCallback(() => {
|
|
|
|
|
if (!player.current) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const currentTime = Math.floor(player.current.currentTime)
|
|
|
|
|
console.log('seeked to', currentTime)
|
|
|
|
|
if ($userInfo.value?.isAdmin) {
|
|
|
|
|
socket.emit("setTime", {
|
|
|
|
|
username: $userInfo.value.username,
|
|
|
|
|
playTime: currentTime,
|
|
|
|
|
room: roomName
|
|
|
|
|
} as ClientMessage)
|
|
|
|
|
}
|
|
|
|
|
}, [roomName])
|
|
|
|
|
|
|
|
|
|
const handlePlay = useCallback(() => {
|
|
|
|
|
if (!player.current) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleUserStateUpdated(true)
|
|
|
|
|
|
|
|
|
|
// 开始播放时,跳转到最新的播放时间
|
|
|
|
|
if (player.current &&
|
|
|
|
|
($userInfo.value?.isAdmin && !isUserStartPlay.current) && // 如果是管理员且用户没有开始播放,则跳转到最新的播放时间
|
|
|
|
|
prevRoomState.current.playing &&
|
|
|
|
|
prevRoomState.current.playTime !== undefined &&
|
|
|
|
|
prevRoomState.current.playTimeUpdateTime !== undefined) {
|
|
|
|
|
const deltaTime = unixTimestampWithOffset(serverTimeAdjust.current) - prevRoomState.current.playTimeUpdateTime
|
|
|
|
|
|
|
|
|
|
player.current.currentTime = prevRoomState.current.playTime + deltaTime
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果是管理员,则发送播放事件
|
|
|
|
|
if ($userInfo.value?.isAdmin) {
|
|
|
|
|
socket.emit("play", {
|
|
|
|
|
username: $userInfo.value.username,
|
|
|
|
|
room: roomName
|
|
|
|
|
} as ClientMessage)
|
|
|
|
|
|
|
|
|
|
socket.emit("setTime", {
|
|
|
|
|
playTime: Math.floor(player.current.currentTime),
|
|
|
|
|
room: roomName,
|
|
|
|
|
username: $userInfo.value.username,
|
|
|
|
|
} as ClientMessage)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 标记用户开始播放
|
|
|
|
|
isUserStartPlay.current = true
|
|
|
|
|
}, [roomName, handleUserStateUpdated])
|
|
|
|
|
|
|
|
|
|
const handlePause = useCallback(() => {
|
|
|
|
|
if (!player.current) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
console.log('paused')
|
|
|
|
|
|
|
|
|
|
if ($userInfo.value?.isAdmin) {
|
|
|
|
|
socket.emit("pause", {
|
|
|
|
|
username: $userInfo.value.username,
|
|
|
|
|
room: roomName
|
|
|
|
|
} as ClientMessage)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleUserStateUpdated(true)
|
|
|
|
|
}, [roomName, handleUserStateUpdated])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
|
if (!player.current) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const computedTime = getUserPlayTimeSeconds(prevUserState.current)
|
|
|
|
|
const playerTime = Math.floor(player.current?.currentTime ?? 0)
|
|
|
|
|
const isPlaying = !player.current?.paused
|
|
|
|
|
|
|
|
|
|
if (isPlaying !== prevUserState.current.playing ||
|
|
|
|
|
(isPlaying && Math.abs(playerTime - computedTime) > TIME_SYNC_ALLOWANCE)) {
|
|
|
|
|
prevUserState.current = {
|
|
|
|
|
playTime: playerTime,
|
|
|
|
|
updateTime: Math.floor(Date.now() / 1000),
|
|
|
|
|
playing: isPlaying
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.emit("updateUserState", {
|
|
|
|
|
username: userInfo?.username,
|
|
|
|
|
room: roomName,
|
|
|
|
|
playTime: playerTime,
|
|
|
|
|
playing: isPlaying,
|
|
|
|
|
} as ClientMessage)
|
|
|
|
|
}
|
|
|
|
|
handleUserStateUpdated()
|
|
|
|
|
}, 1000)
|
|
|
|
|
return () => clearInterval(interval)
|
|
|
|
|
}, [userInfo?.username, roomName, prevUserState])
|
|
|
|
|
}, [handleUserStateUpdated])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
function onDisconnect(e: any, d: any) {
|
|
|
|
|
@ -109,10 +135,12 @@ export const Player = ({ roomName }: { roomName: string }) => {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serverTimeAdjust.current = msg.serverTime ? msg.serverTime - (Date.now() / 1000) : 0
|
|
|
|
|
|
|
|
|
|
prevRoomState.current = {
|
|
|
|
|
url: msg.url,
|
|
|
|
|
playTime: msg.playTime,
|
|
|
|
|
playTimeUpdateTime: Date.now() / 1000,
|
|
|
|
|
playTimeUpdateTime: unixTimestampWithOffset(serverTimeAdjust.current),
|
|
|
|
|
playing: msg.playing,
|
|
|
|
|
userStatus: msg.userStatus,
|
|
|
|
|
}
|
|
|
|
|
@ -129,34 +157,51 @@ export const Player = ({ roomName }: { roomName: string }) => {
|
|
|
|
|
|
|
|
|
|
if (msg.playTime !== undefined &&
|
|
|
|
|
!$userInfo.value?.isAdmin && // 如果是管理员,则不需要更新播放时间
|
|
|
|
|
player.current?.state.canPlay && !player.current?.paused) {
|
|
|
|
|
player.current &&
|
|
|
|
|
player.current.readyState >= 2 && !player.current.paused) {
|
|
|
|
|
console.log('update play time to', msg.playTime)
|
|
|
|
|
// 检测当前播放时间
|
|
|
|
|
const localTime = Math.floor(player.current.currentTime)
|
|
|
|
|
const serverTime = msg.playTime
|
|
|
|
|
if (Math.abs(localTime - serverTime) > TIME_SYNC_ALLOWANCE) {
|
|
|
|
|
// 如果本地时间与服务器时间差异过大,则重置播放时间
|
|
|
|
|
player.current.currentTime = serverTime
|
|
|
|
|
$playerState.set({
|
|
|
|
|
...$playerState.value,
|
|
|
|
|
playTime: serverTime,
|
|
|
|
|
})
|
|
|
|
|
try {
|
|
|
|
|
player.current.currentTime = serverTime
|
|
|
|
|
$playerState.set({
|
|
|
|
|
...$playerState.value,
|
|
|
|
|
playTime: serverTime,
|
|
|
|
|
})
|
|
|
|
|
handleUserStateUpdated(true)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to set video currentTime:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onPause() {
|
|
|
|
|
console.log('pause')
|
|
|
|
|
if (player.current) {
|
|
|
|
|
if (!$userInfo.value?.isAdmin && player.current) {
|
|
|
|
|
player.current.pause()
|
|
|
|
|
handleUserStateUpdated(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onPlay() {
|
|
|
|
|
console.log('play')
|
|
|
|
|
if (isUserStartPlay.current && // 仅在用户已经开始播放的情况下触发
|
|
|
|
|
player.current && player.current?.state.canPlay) {
|
|
|
|
|
player.current.play()
|
|
|
|
|
console.log('play', { isAdmin: $userInfo.value?.isAdmin, isUserStartPlay: isUserStartPlay.current, playerReady: player.current?.readyState})
|
|
|
|
|
if (!$userInfo.value?.isAdmin &&
|
|
|
|
|
isUserStartPlay.current && // 仅在用户已经开始播放的情况下触发
|
|
|
|
|
player.current && player.current.readyState >= 2) {
|
|
|
|
|
console.log('play video')
|
|
|
|
|
try {
|
|
|
|
|
player.current.play().then(() => {
|
|
|
|
|
handleUserStateUpdated(true);
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
console.warn('Failed to play video:', error)
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to call play():', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -168,7 +213,11 @@ export const Player = ({ roomName }: { roomName: string }) => {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
player.current.currentTime = msg.playTime
|
|
|
|
|
try {
|
|
|
|
|
player.current.currentTime = msg.playTime
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to set video currentTime:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.on('disconnect', onDisconnect)
|
|
|
|
|
@ -184,33 +233,25 @@ export const Player = ({ roomName }: { roomName: string }) => {
|
|
|
|
|
socket.off('play', onPlay)
|
|
|
|
|
socket.off('setTime', onSetTime)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
function onProviderChange(
|
|
|
|
|
provider: MediaProviderAdapter | null,
|
|
|
|
|
nativeEvent: MediaProviderChangeEvent,
|
|
|
|
|
) {
|
|
|
|
|
if (isHLSProvider(provider)) {
|
|
|
|
|
provider.library = HLS
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [roomName, handleUserStateUpdated])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
{playerState?.url && <MediaPlayer
|
|
|
|
|
key={playerState.url}
|
|
|
|
|
<video
|
|
|
|
|
className="w-full aspect-video bg-slate-900 text-white font-sans overflow-hidden rounded-md ring-media-focus data-[focus]:ring-4"
|
|
|
|
|
title="test"
|
|
|
|
|
src={playerState.url}
|
|
|
|
|
crossorigin
|
|
|
|
|
playsinline
|
|
|
|
|
onProviderChange={onProviderChange}
|
|
|
|
|
title="影片"
|
|
|
|
|
src={playerState?.url}
|
|
|
|
|
crossOrigin="anonymous"
|
|
|
|
|
playsInline
|
|
|
|
|
controls
|
|
|
|
|
controlsList="nodownload,noseek"
|
|
|
|
|
onSeeked={handleSeeked}
|
|
|
|
|
onPlay={handlePlay}
|
|
|
|
|
onPause={handlePause}
|
|
|
|
|
onEnded={handlePause}
|
|
|
|
|
ref={player}
|
|
|
|
|
>
|
|
|
|
|
<MediaProvider>
|
|
|
|
|
</MediaProvider>
|
|
|
|
|
<VideoLayout roomName={roomName} />
|
|
|
|
|
</MediaPlayer>}
|
|
|
|
|
</video>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|