diff --git a/src/components/videoPlayer.vue b/src/components/videoPlayer.vue index e93e029..f814e55 100644 --- a/src/components/videoPlayer.vue +++ b/src/components/videoPlayer.vue @@ -11,6 +11,8 @@ const videoRef = ref(null); const player = ref(null); const hlsInstance = ref(null); +const emit = defineEmits(['canplay', 'play', 'pause', 'remote-play-failed']); + const props = defineProps({ autoplay: { type: Boolean, default: false }, videoUrl: { type: String, required: true }, @@ -121,10 +123,12 @@ onMounted(() => { player.value.on('play', () => { console.log('播放:', props.videoUrl); + emit('play', { time: getCurrentTime() }); }); player.value.on('pause', () => { console.log('暂停'); + emit('pause', { time: getCurrentTime() }); }); player.value.on('ended', () => { @@ -140,13 +144,82 @@ onMounted(() => { }); player.value.on('canplay', () => { - console.log('视频可以播放'); + // 缓冲、seek 后常会多次触发;不在此刷屏打印,由父组件用 pending* 决定是否执行同步 + emit('canplay'); }); } catch (error) { console.error('初始化播放器失败:', error); } }); +const getCurrentTime = () => { + const video = player.value?.video; + if (!video || typeof video.currentTime !== 'number' || Number.isNaN(video.currentTime)) { + return 0; + } + return video.currentTime; +}; + +const seekTo = (timeSec) => { + const t = Number(timeSec); + if (!Number.isFinite(t) || t < 0) return; + if (player.value?.seek) { + try { + player.value.seek(t); + return; + } catch (e) {} + } + const video = player.value?.video; + if (video && typeof video.currentTime === 'number') { + try { + video.currentTime = t; + } catch (e) {} + } +}; + +/** + * @param {{ remote?: boolean }} options + * remote=true:来自 WebSocket 同步,无用户手势;需静音才能通过多数浏览器的自动播放策略 + */ +const play = async (options = {}) => { + const remote = options?.remote === true; + const video = player.value?.video; + if (!video?.play) return false; + if (remote) { + try { + video.muted = true; + video.setAttribute?.('playsinline', 'true'); + } catch (e) {} + } + try { + await video.play(); + return true; + } catch (e) { + console.warn('[videoPlayer] play() 被拒绝(多为自动播放策略):', e?.name, e?.message); + if (remote) { + emit('remote-play-failed', { error: e }); + } + return false; + } +}; + +const pause = () => { + if (player.value?.pause) { + try { + player.value.pause(); + return; + } catch (e) {} + } + const video = player.value?.video; + if (video?.pause) { + try { + video.pause(); + } catch (e) {} + } +}; + +defineExpose({ getCurrentTime, seekTo, play, pause }); + watch(() => props.videoUrl, (newUrl) => { if (player.value && newUrl) { console.log('切换视频到:', newUrl); diff --git a/src/store/playroom.ts b/src/store/playroom.ts index 81a0fa4..d74dd33 100644 --- a/src/store/playroom.ts +++ b/src/store/playroom.ts @@ -59,6 +59,7 @@ export const PlayroomStore = defineStore("PlayroomStore", return { currentPlayroom, currentUrl, + getCurrentId, setCurrentPlayroom, clearPlayroom, addmember, diff --git a/src/views/room/index.vue b/src/views/room/index.vue index fbfa8a1..c5e3b25 100644 --- a/src/views/room/index.vue +++ b/src/views/room/index.vue @@ -1,493 +1,708 @@ - - - - - \ No newline at end of file diff --git a/src/websocket/roomSocket.ts b/src/websocket/roomSocket.ts index e3fdd83..abb7957 100644 --- a/src/websocket/roomSocket.ts +++ b/src/websocket/roomSocket.ts @@ -1,162 +1,191 @@ -import { ref } from "vue"; -import { userInfoStore } from "@/store/user"; - - -// userinfo 实例 -const userinfo = userInfoStore(); - -const roomid = ref(0); - - -// WebSocket 实例 -const socket = ref(null); - -const isManualClose = ref(false); - -const reconnectScheduled = ref(false); - -const retryCount = ref(0); - -const getRetryCount = () => { - return retryCount.value; -}; - -const addRetryCount = () => { - retryCount.value = retryCount.value + 1; -}; - -const resetRetryCount = () => { - retryCount.value = 0; -}; -const setReconnectScheduled = (value: boolean) => { - reconnectScheduled.value = value; -}; - -const getReconnectScheduled = () => { - return reconnectScheduled.value; -}; - -export const setIsManualClose = (value: boolean) => { - isManualClose.value = value; -}; - -const getIsManualClose = () => { - return isManualClose.value; -}; - -// 连接WebSocket -export const connectWebSocket = (r_id: number) => { - roomid.value = r_id; - const protocol = window.location.protocol === "https:" ? "wss://" : "ws://"; - const host = window.location.host; - const socketUrl = `${protocol}${host}/ws/playroom?r_id=${r_id}`; - - // const socketUrl = `ws://localhost:8080/online?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`; - if (socket.value && socket.value.readyState !== WebSocket.CLOSED) { - console.log("还在重连中..."); - return; - } - const retrytime = getRetryCount(); - if (retrytime >= 10) { - console.log("重连失败,请稍后再试"); - return; - } - console.log(retrytime); - socket.value = new WebSocket(socketUrl, "token-"+ userinfo.token); - - socket.value.onopen = (event: any) => { - console.log("WebSocket for video 连接已建立", event); - setReconnectScheduled(false); - setIsManualClose(false); - resetRetryCount(); - }; - - //处理消息逻辑 - socket.value.onmessage = (event) => { - console.log("从服务器收到消息:", event.data); - try{ - const MessageData = JSON.parse(event.data); - const cmd = MessageData.cmd; - switch(cmd){ - case "PING": - console.log("收到PING消息"); - const msg = { - cmd: "PONG", - from: MessageData.to, - // 可扩展字段 - time: new Date().toLocaleString() - } - sendMessage(msg); - break; - case "VIDEO_SYNC": - // console.log("视频同步消息", MessageData); - break; - } - }catch(error){ - console.error("解析 JSON 失败:", error); - } - - }; - - socket.value.onerror = (error) => { - console.error("WebSocket for video 发生错误:", error); - // console.log(error); - setReconnectScheduled(true); - socket.value.close(); - }; - - socket.value.onclose = (event) => { - if (!getIsManualClose()) { - if (getReconnectScheduled()) { - socket.value = null; - addRetryCount(); - setTimeout(reConnectWebSocket, 5000); - setReconnectScheduled(false); - } else { - // console.log("websocket因为浏览器省电设置断开"); - console.log("WebSocket for video 连接已关闭", event); - } - } - }; -}; - -// 断开WebSocket连接 -export const disconnectWebSocket = () => { - if (socket.value && socket.value.readyState === WebSocket.OPEN) { - roomid.value = 0; - socket.value.close(); - } -}; - -// 重连机制 -export const reConnectWebSocket = () => { - connectWebSocket(roomid.value); -}; - -// 发送消息 -export const sendMessage = (message: any) => { - try{ - const jsonmessage = JSON.stringify(message); - if (socket.value && socket.value.readyState === WebSocket.OPEN) { - socket.value.send(jsonmessage); - } else { - console.warn("WebSocket for video 未连接,无法发送消息"); - } - } - catch(error){ - console.error("Failed to stringify message:", error); - } -}; - -//没有错误的重连,只是浏览器在后台断开了连接 -document.addEventListener("visibilitychange", () => { - if (document.hidden) { - } else { - if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) { - if (getReconnectScheduled()) { - return; - } - reConnectWebSocket(); - } - } -}); +import { ref } from "vue"; +import { userInfoStore } from "@/store/user"; + +export const ROOM_SOCKET_VIDEO_SYNC_EVENT = "room:video_sync"; +export const ROOM_SOCKET_VIDEO_PLAY_EVENT = "room:video_play"; +export const ROOM_SOCKET_VIDEO_PAUSE_EVENT = "room:video_pause"; + +// userinfo 实例 +const userinfo = userInfoStore(); + +const roomid = ref(0); + + +// WebSocket 实例 +const socket = ref(null); + +const isManualClose = ref(false); + +const reconnectScheduled = ref(false); + +const retryCount = ref(0); + +const getRetryCount = () => { + return retryCount.value; +}; + +const addRetryCount = () => { + retryCount.value = retryCount.value + 1; +}; + +const resetRetryCount = () => { + retryCount.value = 0; +}; +const setReconnectScheduled = (value: boolean) => { + reconnectScheduled.value = value; +}; + +const getReconnectScheduled = () => { + return reconnectScheduled.value; +}; + +export const setIsManualClose = (value: boolean) => { + isManualClose.value = value; +}; + +const getIsManualClose = () => { + return isManualClose.value; +}; + +// 连接WebSocket +export const connectWebSocket = (r_id: number) => { + roomid.value = r_id; + const protocol = window.location.protocol === "https:" ? "wss://" : "ws://"; + const host = window.location.host; + const socketUrl = `${protocol}${host}/ws/playroom?r_id=${r_id}`; + + // const socketUrl = `ws://localhost:8080/online?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`; + if (socket.value && socket.value.readyState !== WebSocket.CLOSED) { + console.log("还在重连中..."); + return; + } + const retrytime = getRetryCount(); + if (retrytime >= 10) { + console.log("重连失败,请稍后再试"); + return; + } + console.log(retrytime); + // 调试:房间 WS 不依赖 user.id(URL 只有 r_id,鉴权在子协议 token);id 只影响你主动发的消息里的 from + console.log("[playroom-debug][connectWebSocket]", { + r_id, + "user.id": userinfo.user?.id, + "user.id > 0": typeof userinfo.user?.id === "number" && userinfo.user.id > 0, + "user.u_id": userinfo.user?.u_id, + hasToken: Boolean(userinfo.token), + socketUrl, + }); + socket.value = new WebSocket(socketUrl, "token-"+ userinfo.token); + + socket.value.onopen = (event: any) => { + console.log("[playroom-debug][ws open] 连接已建立,此时 user.id =", userinfo.user?.id, "u_id =", userinfo.user?.u_id); + console.log("WebSocket for video 连接已建立", event); + setReconnectScheduled(false); + setIsManualClose(false); + resetRetryCount(); + }; + + //处理消息逻辑 + socket.value.onmessage = (event) => { + console.log("从服务器收到消息:", event.data); + try{ + const MessageData = JSON.parse(event.data); + const cmd = MessageData.cmd; + switch(cmd){ + case "PING": + console.log("收到PING消息"); + const msg = { + cmd: "PONG", + from: MessageData.to, + // 可扩展字段 + time: new Date().toLocaleString() + } + sendMessage(msg); + break; + case "VIDEO_SYNC": + console.log("视频同步消息", MessageData); + // 通过事件分发给页面(避免 websocket 层直接依赖具体视图) + window.dispatchEvent( + new CustomEvent(ROOM_SOCKET_VIDEO_SYNC_EVENT, { detail: MessageData }) + ); + break; + case "VIDEO_PLAY": + console.log("视频播放"); + window.dispatchEvent( + new CustomEvent(ROOM_SOCKET_VIDEO_PLAY_EVENT, { detail: MessageData }) + ); + break; + case "VIDEO_PAUSE": + console.log("视频暂停"); + window.dispatchEvent( + new CustomEvent(ROOM_SOCKET_VIDEO_PAUSE_EVENT, { detail: MessageData }) + ); + break; + } + }catch(error){ + console.error("解析 JSON 失败:", error); + } + + }; + + socket.value.onerror = (error) => { + console.error("WebSocket for video 发生错误:", error); + // console.log(error); + setReconnectScheduled(true); + socket.value.close(); + }; + + socket.value.onclose = (event) => { + if (!getIsManualClose()) { + if (getReconnectScheduled()) { + socket.value = null; + addRetryCount(); + setTimeout(reConnectWebSocket, 5000); + setReconnectScheduled(false); + } else { + // console.log("websocket因为浏览器省电设置断开"); + console.log("WebSocket for video 连接已关闭", event); + } + } + }; +}; + +// 断开WebSocket连接 +export const disconnectWebSocket = () => { + if (socket.value && socket.value.readyState === WebSocket.OPEN) { + roomid.value = 0; + socket.value.close(); + } +}; + +// 重连机制 +export const reConnectWebSocket = () => { + connectWebSocket(roomid.value); +}; + +// 发送消息 +export const sendMessage = (message: any) => { + try{ + const jsonmessage = JSON.stringify(message); + if (socket.value && socket.value.readyState === WebSocket.OPEN) { + socket.value.send(jsonmessage); + } else { + console.warn("WebSocket for video 未连接,无法发送消息"); + } + } + catch(error){ + console.error("Failed to stringify message:", error); + } +}; + +//没有错误的重连,只是浏览器在后台断开了连接 +document.addEventListener("visibilitychange", () => { + if (document.hidden) { + } else { + if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) { + if (getReconnectScheduled()) { + return; + } + reConnectWebSocket(); + } + } +});