feat: playroom sync finished

This commit is contained in:
2026-04-04 17:48:34 +08:00
parent 970aae1c5f
commit 122971200f
4 changed files with 973 additions and 655 deletions

View File

@@ -11,6 +11,8 @@ const videoRef = ref(null);
const player = ref(null); const player = ref(null);
const hlsInstance = ref(null); const hlsInstance = ref(null);
const emit = defineEmits(['canplay', 'play', 'pause', 'remote-play-failed']);
const props = defineProps({ const props = defineProps({
autoplay: { type: Boolean, default: false }, autoplay: { type: Boolean, default: false },
videoUrl: { type: String, required: true }, videoUrl: { type: String, required: true },
@@ -121,10 +123,12 @@ onMounted(() => {
player.value.on('play', () => { player.value.on('play', () => {
console.log('播放:', props.videoUrl); console.log('播放:', props.videoUrl);
emit('play', { time: getCurrentTime() });
}); });
player.value.on('pause', () => { player.value.on('pause', () => {
console.log('暂停'); console.log('暂停');
emit('pause', { time: getCurrentTime() });
}); });
player.value.on('ended', () => { player.value.on('ended', () => {
@@ -140,13 +144,82 @@ onMounted(() => {
}); });
player.value.on('canplay', () => { player.value.on('canplay', () => {
console.log('视频可以播放'); // 缓冲、seek 后常会多次触发;不在此刷屏打印,由父组件用 pending* 决定是否执行同步
emit('canplay');
}); });
} catch (error) { } catch (error) {
console.error('初始化播放器失败:', 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) => { watch(() => props.videoUrl, (newUrl) => {
if (player.value && newUrl) { if (player.value && newUrl) {
console.log('切换视频到:', newUrl); console.log('切换视频到:', newUrl);

View File

@@ -59,6 +59,7 @@ export const PlayroomStore = defineStore("PlayroomStore",
return { return {
currentPlayroom, currentPlayroom,
currentUrl, currentUrl,
getCurrentId,
setCurrentPlayroom, setCurrentPlayroom,
clearPlayroom, clearPlayroom,
addmember, addmember,

File diff suppressed because it is too large Load Diff

View File

@@ -1,162 +1,191 @@
import { ref } from "vue"; import { ref } from "vue";
import { userInfoStore } from "@/store/user"; import { userInfoStore } from "@/store/user";
export const ROOM_SOCKET_VIDEO_SYNC_EVENT = "room:video_sync";
// userinfo 实例 export const ROOM_SOCKET_VIDEO_PLAY_EVENT = "room:video_play";
const userinfo = userInfoStore(); export const ROOM_SOCKET_VIDEO_PAUSE_EVENT = "room:video_pause";
const roomid = ref<number>(0); // userinfo 实例
const userinfo = userInfoStore();
// WebSocket 实例 const roomid = ref<number>(0);
const socket = ref<WebSocket | null>(null);
const isManualClose = ref<boolean>(false); // WebSocket 实例
const socket = ref<WebSocket | null>(null);
const reconnectScheduled = ref<boolean>(false);
const isManualClose = ref<boolean>(false);
const retryCount = ref<number>(0);
const reconnectScheduled = ref<boolean>(false);
const getRetryCount = () => {
return retryCount.value; const retryCount = ref<number>(0);
};
const getRetryCount = () => {
const addRetryCount = () => { return retryCount.value;
retryCount.value = retryCount.value + 1; };
};
const addRetryCount = () => {
const resetRetryCount = () => { retryCount.value = retryCount.value + 1;
retryCount.value = 0; };
};
const setReconnectScheduled = (value: boolean) => { const resetRetryCount = () => {
reconnectScheduled.value = value; retryCount.value = 0;
}; };
const setReconnectScheduled = (value: boolean) => {
const getReconnectScheduled = () => { reconnectScheduled.value = value;
return reconnectScheduled.value; };
};
const getReconnectScheduled = () => {
export const setIsManualClose = (value: boolean) => { return reconnectScheduled.value;
isManualClose.value = value; };
};
export const setIsManualClose = (value: boolean) => {
const getIsManualClose = () => { isManualClose.value = value;
return isManualClose.value; };
};
const getIsManualClose = () => {
// 连接WebSocket return isManualClose.value;
export const connectWebSocket = (r_id: number) => { };
roomid.value = r_id;
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://"; // 连接WebSocket
const host = window.location.host; export const connectWebSocket = (r_id: number) => {
const socketUrl = `${protocol}${host}/ws/playroom?r_id=${r_id}`; roomid.value = r_id;
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
// const socketUrl = `ws://localhost:8080/online?u_id=${userinfo.user.u_id}&u_name=${userinfo.user.u_name}`; const host = window.location.host;
if (socket.value && socket.value.readyState !== WebSocket.CLOSED) { const socketUrl = `${protocol}${host}/ws/playroom?r_id=${r_id}`;
console.log("还在重连中...");
return; // 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) {
const retrytime = getRetryCount(); console.log("还在重连中...");
if (retrytime >= 10) { return;
console.log("重连失败,请稍后再试"); }
return; const retrytime = getRetryCount();
} if (retrytime >= 10) {
console.log(retrytime); console.log("重连失败,请稍后再试");
socket.value = new WebSocket(socketUrl, "token-"+ userinfo.token); return;
}
socket.value.onopen = (event: any) => { console.log(retrytime);
console.log("WebSocket for video 连接已建立", event); // 调试:房间 WS 不依赖 user.idURL 只有 r_id鉴权在子协议 tokenid 只影响你主动发的消息里的 from
setReconnectScheduled(false); console.log("[playroom-debug][connectWebSocket]", {
setIsManualClose(false); r_id,
resetRetryCount(); "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),
socket.value.onmessage = (event) => { socketUrl,
console.log("从服务器收到消息:", event.data); });
try{ socket.value = new WebSocket(socketUrl, "token-"+ userinfo.token);
const MessageData = JSON.parse(event.data);
const cmd = MessageData.cmd; socket.value.onopen = (event: any) => {
switch(cmd){ console.log("[playroom-debug][ws open] 连接已建立,此时 user.id =", userinfo.user?.id, "u_id =", userinfo.user?.u_id);
case "PING": console.log("WebSocket for video 连接已建立", event);
console.log("收到PING消息"); setReconnectScheduled(false);
const msg = { setIsManualClose(false);
cmd: "PONG", resetRetryCount();
from: MessageData.to, };
// 可扩展字段
time: new Date().toLocaleString() //处理消息逻辑
} socket.value.onmessage = (event) => {
sendMessage(msg); console.log("从服务器收到消息:", event.data);
break; try{
case "VIDEO_SYNC": const MessageData = JSON.parse(event.data);
// console.log("视频同步消息", MessageData); const cmd = MessageData.cmd;
break; switch(cmd){
} case "PING":
}catch(error){ console.log("收到PING消息");
console.error("解析 JSON 失败:", error); const msg = {
} cmd: "PONG",
from: MessageData.to,
}; // 可扩展字段
time: new Date().toLocaleString()
socket.value.onerror = (error) => { }
console.error("WebSocket for video 发生错误:", error); sendMessage(msg);
// console.log(error); break;
setReconnectScheduled(true); case "VIDEO_SYNC":
socket.value.close(); console.log("视频同步消息", MessageData);
}; // 通过事件分发给页面(避免 websocket 层直接依赖具体视图)
window.dispatchEvent(
socket.value.onclose = (event) => { new CustomEvent(ROOM_SOCKET_VIDEO_SYNC_EVENT, { detail: MessageData })
if (!getIsManualClose()) { );
if (getReconnectScheduled()) { break;
socket.value = null; case "VIDEO_PLAY":
addRetryCount(); console.log("视频播放");
setTimeout(reConnectWebSocket, 5000); window.dispatchEvent(
setReconnectScheduled(false); new CustomEvent(ROOM_SOCKET_VIDEO_PLAY_EVENT, { detail: MessageData })
} else { );
// console.log("websocket因为浏览器省电设置断开"); break;
console.log("WebSocket for video 连接已关闭", event); case "VIDEO_PAUSE":
} console.log("视频暂停");
} window.dispatchEvent(
}; new CustomEvent(ROOM_SOCKET_VIDEO_PAUSE_EVENT, { detail: MessageData })
}; );
break;
// 断开WebSocket连接 }
export const disconnectWebSocket = () => { }catch(error){
if (socket.value && socket.value.readyState === WebSocket.OPEN) { console.error("解析 JSON 失败:", error);
roomid.value = 0; }
socket.value.close();
} };
};
socket.value.onerror = (error) => {
// 重连机制 console.error("WebSocket for video 发生错误:", error);
export const reConnectWebSocket = () => { // console.log(error);
connectWebSocket(roomid.value); setReconnectScheduled(true);
}; socket.value.close();
};
// 发送消息
export const sendMessage = (message: any) => { socket.value.onclose = (event) => {
try{ if (!getIsManualClose()) {
const jsonmessage = JSON.stringify(message); if (getReconnectScheduled()) {
if (socket.value && socket.value.readyState === WebSocket.OPEN) { socket.value = null;
socket.value.send(jsonmessage); addRetryCount();
} else { setTimeout(reConnectWebSocket, 5000);
console.warn("WebSocket for video 未连接,无法发送消息"); setReconnectScheduled(false);
} } else {
} // console.log("websocket因为浏览器省电设置断开");
catch(error){ console.log("WebSocket for video 连接已关闭", event);
console.error("Failed to stringify message:", error); }
} }
}; };
};
//没有错误的重连,只是浏览器在后台断开了连接
document.addEventListener("visibilitychange", () => { // 断开WebSocket连接
if (document.hidden) { export const disconnectWebSocket = () => {
} else { if (socket.value && socket.value.readyState === WebSocket.OPEN) {
if (!getIsManualClose() && socket.value.readyState === WebSocket.CLOSED) { roomid.value = 0;
if (getReconnectScheduled()) { socket.value.close();
return; }
} };
reConnectWebSocket();
} // 重连机制
} 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();
}
}
});