From 27c4a247d395402140c2f457caca2427fd3b9e2d Mon Sep 17 00:00:00 2001 From: merlin Date: Wed, 17 Dec 2025 18:03:16 +0800 Subject: [PATCH 1/8] reafactor: group basic function refactor complete --- src/api/group.ts | 93 ++++++++++ src/functions/groupHistoryMessage.js | 2 +- src/router/index.js | 2 +- src/store/group_message.ts | 76 ++++++++ src/views/home/default.vue | 3 +- src/views/home/group.vue | 233 +++++++++---------------- src/views/room/{room.vue => index.vue} | 0 src/websocket/onlineSocket.js | 11 +- src/websocket/voiceSocket.js | 2 +- 9 files changed, 267 insertions(+), 155 deletions(-) create mode 100644 src/api/group.ts create mode 100644 src/store/group_message.ts rename src/views/room/{room.vue => index.vue} (100%) diff --git a/src/api/group.ts b/src/api/group.ts new file mode 100644 index 0000000..90cbc3c --- /dev/null +++ b/src/api/group.ts @@ -0,0 +1,93 @@ +import axios from "axios"; +import { userInfoStore } from "@/store/user"; + + +const userinfo = userInfoStore(); + + +export const getGroups = async ()=> { + const response = await axios.get('/api/group/get',{ + headers:{ + 'Authorization' : `Bearer ${userinfo.token}` + } + }); + if(response.data.code === "200"){ + return response.data.data; + } else { + throw new Error(response.data.message); + } +} + + +export const createGroup = async (group_name: string) => { + const response = await axios.post('/api/group/create',{ + g_name: group_name + } + ,{ + headers:{ + 'Authorization' : `Bearer ${userinfo.token}` + } + }); + if(response.data.code === "200"){ + return true; + } else { + throw new Error(response.data.message); + } +} + +export const searchGroups = async (group_name: string) => { + const response = await axios.post('/api/group/search',{ + g_name: group_name + },{ + headers:{ + 'Authorization' : `Bearer ${userinfo.token}` + } + }) + if(response.data.code === "200"){ + return response.data.data.records; + }else{ + throw new Error(response.data.message); + } + +} + +export const joinGroup = async (group_id: number) => { + const response = await axios.get('/api/group/join/'+group_id,{ + headers:{ + 'Authorization' : `Bearer ${userinfo.token}` + }}) + if(response.data.code === "200"){ + return true; + }else{ + throw new Error(response.data.message); + } +} + +export const leaveGroup = async (group_id: number) => { + const response = await axios.get('/api/group/leave/'+group_id,{ + headers:{ + 'Authorization' : `Bearer ${userinfo.token}` + }}) + if(response.data.code === "200"){ + return true; + }else{ + throw new Error(response.data.message); + } +} + +export const getGroupMembers = async (group_id: number) => { + const response = await axios.get('/api/group/member/'+group_id,{ + headers:{ + 'Authorization' : `Bearer ${userinfo.token}` + }, + params:{ + currentPage: 1, + pageSize: 50 + } + }) + if(response.data.code === "200"){ + return response.data.data; + }else{ + throw new Error(response.data.message); + } +} \ No newline at end of file diff --git a/src/functions/groupHistoryMessage.js b/src/functions/groupHistoryMessage.js index 9cbf97a..ff86954 100644 --- a/src/functions/groupHistoryMessage.js +++ b/src/functions/groupHistoryMessage.js @@ -5,7 +5,7 @@ const STORE_NAME = 'groupHistoryMessages'; // 打开数据库 const getDB = async () => { - return openDB(DB_NAME, 3, { + return openDB(DB_NAME, 4, { upgrade(db) { if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME); // 使用 userId 作为 key diff --git a/src/router/index.js b/src/router/index.js index dd368df..3bbe662 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -79,7 +79,7 @@ const router = createRouter({ { path: '/room', name: 'room', - component: () => import('../views/room/room.vue'), + component: () => import('../views/room/index.vue'), } diff --git a/src/store/group_message.ts b/src/store/group_message.ts new file mode 100644 index 0000000..c92718d --- /dev/null +++ b/src/store/group_message.ts @@ -0,0 +1,76 @@ +import { defineStore } from "pinia"; +import { + saveGroupMessages, + loadGroupMessages, + deleteGroupMessages, +} from "@/functions/groupHistoryMessage"; + +interface Group_Message{ + cmd: string; + from: number; + to: number; + group: number; + content: string; + time: string; +} + +export const groupMessageStore = defineStore("groupMessageStore", { + state: () => ({ + g_id: "", + historymessages: [], + messages: [], + corresponding: [], + }), + actions: { + async recieveMessage(u_id: number, message: Group_Message) { + const key = `${u_id}-${message.group}` + if(this.g_id !== '' && this.g_id === message.group) this.messages.push(message); + await saveGroupMessages(key, [message]); + }, + reset(){ + this.g_id = ""; + this.historymessages = []; + this.messages = []; + this.corresponding = []; + }, + addMessage(message: Group_Message) { + this.messages.push(message); + }, + clearMessages() { + this.messages = []; + }, + + async initMessages() { + this.historymessages = [...this.historymessages, ...this.messages]; + this.messages = []; + }, + + async getHistoryMessages(u_id:number, g_id: number) { + this.g_id = g_id; + const key = `${u_id}-${g_id}`; + try { + this.historymessages = await loadGroupMessages(key); + // 确保历史消息是数组类型 + if (!Array.isArray(this.historymessages)) { + console.error("历史消息数据无效:", this.historymessages); + this.historymessages = []; // 如果数据无效,设置为空数组 + } + } catch (error) { + console.log("加载历史消息时出错" + error); + } + }, + async saveMessagesHistory(u_id: number, g_id: number) { + // const key = `${u_id}-${g_id}`; + // const messages = toRaw(this.messages); + this.messages = []; + this.historymessages = []; + // await saveGroupMessages(key, messages); + }, + async deleteMessagesHistory(u_id: number, g_id: number) { + const key = `${u_id}-${g_id}`; + this.historymessages = []; + this.messages = []; + await deleteGroupMessages(key, []); + }, + }, +}); \ No newline at end of file diff --git a/src/views/home/default.vue b/src/views/home/default.vue index 346d474..b5e702e 100644 --- a/src/views/home/default.vue +++ b/src/views/home/default.vue @@ -1,11 +1,12 @@ diff --git a/src/views/room/room.vue b/src/views/room/index.vue similarity index 100% rename from src/views/room/room.vue rename to src/views/room/index.vue diff --git a/src/websocket/onlineSocket.js b/src/websocket/onlineSocket.js index d18df7a..a36eaaf 100644 --- a/src/websocket/onlineSocket.js +++ b/src/websocket/onlineSocket.js @@ -1,15 +1,16 @@ import { ref } from "vue"; -import { userInfoStore } from "@/store/store"; +import { userInfoStore } from "@/store/user"; import { messageStore } from "@/store/message"; import { ElMessage } from "element-plus"; import { messageSignStore } from "@/store/message_sign"; +import { groupMessageStore } from "@/store/group_message"; // userinfo 实例 const userinfo = userInfoStore(); // message 实例 const message = messageStore(); // groupMessage 实例 -// const groupMessage = groupMessageStore(); +const groupMessage = groupMessageStore(); // messageSignStoregn 实例 // const messageSign = messageSignStore(); @@ -85,6 +86,7 @@ export const connectWebSocket = () => { switch(cmd){ case "MESSAGE": message.addMessage(MessageData); + // TODO:需要使用u_name或者u_id进行消息标记 // messageSign.setMessageSign(true); // ElMessage.info("您有一条新的消息"); break; @@ -92,6 +94,11 @@ export const connectWebSocket = () => { messageSign.setMessageSign(true); ElMessage.info("您有一条新的邀请消息"); break; + case "GROUP_MESSAGE": + groupMessage.recieveMessage(userinfo.user.id, MessageData); + + // TODO: 需要使用groupId进行消息标记 + break; } }catch(error){ console.error("解析 JSON 失败:", error); diff --git a/src/websocket/voiceSocket.js b/src/websocket/voiceSocket.js index b4e32ba..9277808 100644 --- a/src/websocket/voiceSocket.js +++ b/src/websocket/voiceSocket.js @@ -1,5 +1,5 @@ import { ref } from "vue"; -import { userInfoStore } from "@/store/store"; +import { userInfoStore } from "@/store/user"; import { onCallStore } from "@/store/VoiceTarget"; import { ElMessage } from "element-plus"; From b40691a4a3b002abd7a73bf528fccce340a5a151 Mon Sep 17 00:00:00 2001 From: merlin Date: Mon, 29 Dec 2025 15:40:42 +0800 Subject: [PATCH 2/8] feat: dplayer --- package.json | 3 +- proxy.js | 75 +++++++++-- src/api/playroom.ts | 67 ++++++++++ src/api/room.ts | 21 --- src/components/videoPlayer.vue | 235 ++++++++++++++++++++++++++------- src/store/playroom.ts | 38 ++++++ src/store/room.js | 15 --- src/views/home/playroom.vue | 57 +++----- src/views/home/search.vue | 34 ++--- src/views/room/index.vue | 17 ++- 10 files changed, 394 insertions(+), 168 deletions(-) create mode 100644 src/api/playroom.ts delete mode 100644 src/api/room.ts create mode 100644 src/store/playroom.ts delete mode 100644 src/store/room.js diff --git a/package.json b/package.json index c62b481..c1a8139 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "proxy": "node proxy.js" }, "dependencies": { "axios": "^1.12.0", diff --git a/proxy.js b/proxy.js index dc3cdf9..68e6424 100644 --- a/proxy.js +++ b/proxy.js @@ -8,6 +8,8 @@ const port = 3000; // 允许跨域请求 app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Range'); next(); }); @@ -18,22 +20,35 @@ app.get('/proxy', async (req, res) => { const targetUrl = req.query.url; console.log('Fetching data from:', targetUrl); if (!targetUrl) { - return res.status(400).send('URL parameter is required'); + return res.status(400).json({ error: 'URL parameter is required' }); } try { // 设置请求头 - const headers = {}; + const headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }; if (req.headers.range) { - headers['Range'] = req.headers.range; // 转发 Range 请求头 + headers['Range'] = req.headers.range; // 转发 Range 请求头,支持视频分段加载 } // 向目标 URL 发起请求 const response = await fetch(targetUrl, { headers }); + // 检查响应状态 + if (!response.ok) { + return res.status(response.status).json({ + error: `Failed to fetch: ${response.statusText}`, + status: response.status + }); + } + // 设置响应头 response.headers.forEach((value, key) => { - if (key !== 'content-encoding') { // 避免某些头信息导致问题 + // 跳过一些可能导致问题的头信息 + if (key.toLowerCase() !== 'content-encoding' && + key.toLowerCase() !== 'transfer-encoding' && + key.toLowerCase() !== 'connection') { res.setHeader(key, value); } }); @@ -44,19 +59,55 @@ app.get('/proxy', async (req, res) => { } // 将响应体直接流式传输给客户端 - response.body.pipe(res, { end: true }); + if (response.body) { + response.body.pipe(res, { end: true }); - // 错误处理 - response.body.on('error', (err) => { - console.error('Error during data transfer:', err); - res.status(500).send('Error during data transfer'); - }); + // 错误处理 + response.body.on('error', (err) => { + console.error('Error during data transfer:', err); + if (!res.headersSent) { + res.status(500).json({ error: 'Error during data transfer' }); + } + }); + } else { + res.status(500).json({ error: 'No response body' }); + } } catch (error) { console.error('Error fetching data:', error); - res.status(500).send('Error fetching data'); + + // 根据错误类型返回更具体的错误信息 + let errorMessage = 'Error fetching data'; + let statusCode = 500; + let hostname = ''; + + try { + hostname = new URL(targetUrl).hostname; + } catch (e) { + hostname = 'unknown'; + } + + if (error.code === 'ENOTFOUND') { + errorMessage = `DNS解析失败:无法解析域名 "${hostname}"。请检查URL是否正确,或网络连接是否正常。`; + statusCode = 502; + } else if (error.code === 'ECONNREFUSED') { + errorMessage = `连接被拒绝:无法连接到目标服务器 "${hostname}"。`; + statusCode = 502; + } else if (error.code === 'ETIMEDOUT') { + errorMessage = `请求超时:目标服务器 "${hostname}" 响应时间过长。`; + statusCode = 504; + } + + if (!res.headersSent) { + res.status(statusCode).json({ + error: errorMessage, + code: error.code, + targetUrl: targetUrl + }); + } } }); app.listen(port, () => { console.log(`Proxy server running at http://localhost:${port}`); -}); \ No newline at end of file +}); + diff --git a/src/api/playroom.ts b/src/api/playroom.ts new file mode 100644 index 0000000..143ffbd --- /dev/null +++ b/src/api/playroom.ts @@ -0,0 +1,67 @@ +import { userInfoStore } from "@/store/user"; +import axios from "axios"; + +const userinfo = userInfoStore(); + + +export const searchPlayRoom = async (inputValue: string) => { + const response = await axios.post('/api/playroom/search',{ + r_name: inputValue + },{ + headers:{ + 'Authorization': "Bearer " + userinfo.token + } + }) + if(response.data.code === "200"){ + return response.data.data.records + } else { + console.error(response.data.msg) + return [] + } +} + +export const createPlayroom = async (room_name: string) => { + const response = await axios.post('/api/playroom/create',{ + r_name: room_name + },{ + headers:{ + 'Authorization': "Bearer " + userinfo.token + } + }) + if(response.data.code === "200"){ + return true; + } else { + throw new Error(response.data.message); + } +} + +export const getPlayrooms = async ()=> { + const response = await axios.get('/api/playroom/get',{ + headers:{ + 'Authorization' : `Bearer ${userinfo.token}` + } + }) + if(response.data.code === "200"){ + return response.data.data; + } else { + throw new Error(response.data.message); + } +} + +export const joinPlayroom = async (r_id: number) => { + const response = await axios.post('/api/inviting/playroom',{ + inviter: userinfo.user.id, + target: userinfo.user.id, + status: 0, + room: r_id, + },{ + headers:{ + 'Authorization': "Bearer " + userinfo.token + } + }) + if(response.data.code === "200"){ + return true; + } else { + throw new Error(response.data.message); + } +} \ No newline at end of file diff --git a/src/api/room.ts b/src/api/room.ts deleted file mode 100644 index 1c5d5d0..0000000 --- a/src/api/room.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { userInfoStore } from "@/store/user"; -import axios from "axios"; - -const userinfo = userInfoStore(); - - -export const searchPlayRoom = async (inputValue: string) => { - const response = await axios.post('/api/playroom/search',{ - r_name: inputValue - },{ - headers:{ - 'Authorization': "Bearer " + userinfo.token - } - }) - if(response.data.code === "200"){ - return response.data.data - } else { - console.error(response.data.msg) - return [] - } -} \ No newline at end of file diff --git a/src/components/videoPlayer.vue b/src/components/videoPlayer.vue index e585458..e93e029 100644 --- a/src/components/videoPlayer.vue +++ b/src/components/videoPlayer.vue @@ -8,6 +8,8 @@ import DPlayer from 'dplayer'; import Hls from 'hls.js'; const videoRef = ref(null); +const player = ref(null); +const hlsInstance = ref(null); const props = defineProps({ autoplay: { type: Boolean, default: false }, @@ -15,75 +17,208 @@ const props = defineProps({ danmaku: { type: Object, default: () => ({}) } }); -onMounted(() => { - const playerOptions = { +// 判断是否为 HLS 格式 +const isHlsUrl = (url) => { + return url.includes('.m3u8') || url.includes('hls') || url.includes('application/x-mpegURL'); +}; + +// 清理 HLS 实例 +const cleanupHls = () => { + if (hlsInstance.value) { + try { + hlsInstance.value.destroy(); + } catch (e) { + console.warn('清理 HLS 实例时出错:', e); + } + hlsInstance.value = null; + } +}; + +// 创建播放器配置 +const createPlayerOptions = (url) => { + const proxyUrl = `/proxy?url=${encodeURIComponent(url)}`; + const isHls = isHlsUrl(url); + + console.log('视频 URL:', url); + console.log('是否为 HLS 格式:', isHls); + console.log('代理 URL:', proxyUrl); + + const options = { container: videoRef.value, autoplay: props.autoplay, - video: { - url: `/proxy?url=${encodeURIComponent(props.videoUrl)}`, + video: {}, + danmaku: props.danmaku + }; + + if (isHls && Hls.isSupported()) { + // HLS 格式使用 customHls + options.video = { + url: proxyUrl, type: 'customHls', customType: { customHls: function (video, player) { - if (Hls.isSupported()) { - const hls = new Hls(); - hls.loadSource(video.src); - hls.attachMedia(video); - hls.config.maxBufferLength = 60; // 设置最大缓冲时间为60秒 - hls.on(Hls.Events.MEDIA_ATTACHED, () => { - video.play(); - }); - } + cleanupHls(); // 清理旧的实例 + + const hls = new Hls({ + maxBufferLength: 20, + maxMaxBufferLength: 60, + enableWorker: true, + lowLatencyMode: false + }); + + hlsInstance.value = hls; + + hls.loadSource(video.src); + hls.attachMedia(video); + + // 错误处理 + hls.on(Hls.Events.ERROR, (event, data) => { + console.error('HLS 错误:', data); + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + console.error('网络错误,尝试恢复...'); + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + console.error('媒体错误,尝试恢复...'); + hls.recoverMediaError(); + break; + default: + console.error('致命错误,无法恢复'); + hls.destroy(); + break; + } + } + }); + + hls.on(Hls.Events.MANIFEST_PARSED, () => { + console.log('HLS 清单解析完成'); + if (props.autoplay) { + video.play().catch(err => { + console.warn('自动播放失败:', err); + }); + } + }); } } - }, - danmaku: props.danmaku + }; + } else { + // 普通视频格式(MP4等) + options.video = { + url: proxyUrl, + type: 'auto' // 让 DPlayer 自动检测类型 + }; + } + return options; +}; - }; +onMounted(() => { + try { + const playerOptions = createPlayerOptions(props.videoUrl); + player.value = new DPlayer(playerOptions); - console.log(`/proxy?url=${encodeURIComponent(props.videoUrl)}`) - const player = new DPlayer(playerOptions); + player.value.on('play', () => { + console.log('播放:', props.videoUrl); + }); + + player.value.on('pause', () => { + console.log('暂停'); + }); + + player.value.on('ended', () => { + console.log('播放结束'); + }); + + player.value.on('error', (error) => { + console.error('播放器错误:', error); + }); + + player.value.on('loadstart', () => { + console.log('开始加载视频'); + }); + + player.value.on('canplay', () => { + console.log('视频可以播放'); + }); + } catch (error) { + console.error('初始化播放器失败:', error); + } +}); - player.on('play', () => { - console.log("播放...") - }) - player.on('pause', () => { - console.log("暂停...") - }) - player.on('ended', () => { - console.log("结束...") - }) - player.on('error', () => { - console.log("出错...") - }) - - watch(() => props.videoUrl, (newUrl) => { - if (player) { - console.log("切换视频...") - player.switchVideo({ - url: `/proxy?url=${encodeURIComponent(newUrl)}`, - type: 'customHls', - customType: { - customHls: function (video, player) { - if (Hls.isSupported()) { - const hls = new Hls(); +watch(() => props.videoUrl, (newUrl) => { + if (player.value && newUrl) { + console.log('切换视频到:', newUrl); + + try { + cleanupHls(); // 清理旧的 HLS 实例 + + const isHls = isHlsUrl(newUrl); + const proxyUrl = `/proxy?url=${encodeURIComponent(newUrl)}`; + + if (isHls && Hls.isSupported()) { + // HLS 格式切换 + player.value.switchVideo({ + url: proxyUrl, + type: 'customHls', + customType: { + customHls: function (video, player) { + cleanupHls(); + + const hls = new Hls({ + maxBufferLength: 20, + maxMaxBufferLength: 60, + enableWorker: true + }); + + hlsInstance.value = hls; hls.loadSource(video.src); hls.attachMedia(video); - hls.on(Hls.Events.MEDIA_ATTACHED, () => { - video.play(); + + hls.on(Hls.Events.ERROR, (event, data) => { + console.error('HLS 错误:', data); + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + hls.recoverMediaError(); + break; + default: + hls.destroy(); + break; + } + } }); } } - } - }) + }); + } else { + // 普通视频格式切换 + player.value.switchVideo({ + url: proxyUrl, + type: 'auto' + }); + } + } catch (error) { + console.error('切换视频失败:', error); } } - ) +}); - // 销毁时清理播放器 - onBeforeUnmount(() => { - player.destroy(); - }); +// 销毁时清理播放器和 HLS 实例 +onBeforeUnmount(() => { + cleanupHls(); + if (player.value) { + try { + player.value.destroy(); + } catch (e) { + console.warn('销毁播放器时出错:', e); + } + player.value = null; + } }); diff --git a/src/store/playroom.ts b/src/store/playroom.ts new file mode 100644 index 0000000..0a64fd1 --- /dev/null +++ b/src/store/playroom.ts @@ -0,0 +1,38 @@ +import { ref } from "vue"; +import { defineStore } from "pinia"; + +interface PlayroomState { + id: number + r_id: number; + r_name: string; + r_introduction: string; + r_avatar: string; + role: number; +} + + +export const PlayroomStore = defineStore("PlayroomStore", + () =>{ + const currentPlayroom = ref(); + const currentUrl = ref(""); + + const setCurrentPlayroom = (playroom: PlayroomState) => { + currentPlayroom.value = playroom; + } + + const setCurrentUrl = (url: string) => { + currentUrl.value = url; + } + + const clearPlayroom = () => { + currentPlayroom.value = undefined; + currentUrl.value = ""; + } + + return { + currentPlayroom, + setCurrentPlayroom, + setCurrentUrl, + clearPlayroom, + } +}) \ No newline at end of file diff --git a/src/store/room.js b/src/store/room.js deleted file mode 100644 index a2ce794..0000000 --- a/src/store/room.js +++ /dev/null @@ -1,15 +0,0 @@ -import { ref } from "vue"; -import { defineStore } from "pinia"; - -export const roomStore = defineStore("room",{ - state: () => ({ - r_id: '', - r_name: '', - r_avatar: '', - inroomTag: '', - currentURL: '', - }), - actions:{ - - } -}) \ No newline at end of file diff --git a/src/views/home/playroom.vue b/src/views/home/playroom.vue index cec47f0..6427e3f 100644 --- a/src/views/home/playroom.vue +++ b/src/views/home/playroom.vue @@ -16,7 +16,7 @@ @@ -24,7 +24,7 @@ - + @@ -39,14 +39,11 @@ diff --git a/src/views/home/search.vue b/src/views/home/search.vue index 333b940..8fb4330 100644 --- a/src/views/home/search.vue +++ b/src/views/home/search.vue @@ -50,7 +50,7 @@ @@ -69,7 +69,7 @@ import { userInfoStore } from '@/store/user' import { onlineSocketStore } from '@/store/Online' import { ElMessage } from 'element-plus' import { searchFriend,addFriend } from '@/api/friend' -import { searchPlayRoom } from '@/api/room' +import { searchPlayRoom, joinPlayroom } from '@/api/playroom' const socket = onlineSocketStore() const inputValue = ref('') @@ -123,26 +123,16 @@ const addfriend = async (f_id) => { } //加入房间申请逻辑 -const joinRoom = (r_id) => { - axios({ - headers: { - 'Authorization': userinfo.token, - }, - url: '/api/inviting/sendinviting', - method: 'POST', - data: { - inviter: userinfo.user.u_id, - target: userinfo.user.u_id, - room: r_id - } - }).then((response) => { - if (response.data.code === 200) { - ElMessage.success("请求发送成功,请耐心等待审核") - } - else if (response.data.code === 500) { - ElMessage.error("请勿重复发送!") - } - }) +const joinroom = (r_id) => { + try{ + if(joinPlayroom(r_id)){ + ElMessage.success("请求发送成功,请耐心等待审核") + return + } + }catch(error){ + ElMessage.error("加入房间失败:",error) + } + } diff --git a/src/views/room/index.vue b/src/views/room/index.vue index e8f6d91..9500ce3 100644 --- a/src/views/room/index.vue +++ b/src/views/room/index.vue @@ -37,7 +37,7 @@ - + @@ -140,7 +140,7 @@ - + 测试 @@ -159,12 +159,13 @@ - - \ No newline at end of file diff --git a/src/components/phonePanel.vue b/src/components/phonePanel.vue index 62075cd..58730cc 100644 --- a/src/components/phonePanel.vue +++ b/src/components/phonePanel.vue @@ -1,153 +1,152 @@ - - - \ No newline at end of file diff --git a/src/components/userProfile.vue b/src/components/userProfile.vue index 4e4b59c..29e40f9 100644 --- a/src/components/userProfile.vue +++ b/src/components/userProfile.vue @@ -1,72 +1,72 @@ - - - - - \ No newline at end of file diff --git a/src/store/Online.js b/src/store/Online.ts similarity index 77% rename from src/store/Online.js rename to src/store/Online.ts index 96aebd3..7c9c6ec 100644 --- a/src/store/Online.js +++ b/src/store/Online.ts @@ -1,37 +1,42 @@ -import { defineStore } from 'pinia' -import { connectWebSocket, disconnectWebSocket, sendMessage, setIsManualClose } from '@/websocket/onlineSocket' -import { messageStore } from './message' - -const message = messageStore() - -export const onlineSocketStore = defineStore('onlineSocket', { - state: () => ({ - isConnected: false, - hasGotMessage: false, - id: '' - }), - - actions: { - connect(id) { - this.id = id; - if (this.isConnected === true) return - connectWebSocket(); - this.isConnected = true; - if (!this.hasGotMessage) { - message.loadMessagesHistory(this.id) - this.hasGotMessage = true - } - }, - disconnect() { - setIsManualClose(true); - disconnectWebSocket(); - this.isConnected = false; - if (this.hasGotMessage) { - message.saveMessagesHistory(this.id) - } - }, - send(message) { - sendMessage(JSON.stringify(message)); - } - } +import { defineStore } from 'pinia' +import { connectWebSocket, disconnectWebSocket, sendMessage, setIsManualClose } from '@/websocket/onlineSocket' +import { messageStore } from './message' + +const message = messageStore() + +export const onlineSocketStore = defineStore('onlineSocket', { + state: () => ({ + isConnected: false, + hasGotMessage: false, + id: '' + }), + + actions: { + connect(id: number) { + this.id = id; + if (this.isConnected === true) return + connectWebSocket(); + this.isConnected = true; + if (!this.hasGotMessage) { + message.loadMessagesHistory(this.id) + this.hasGotMessage = true + } + }, + disconnect() { + setIsManualClose(true); + disconnectWebSocket(); + this.isConnected = false; + if (this.hasGotMessage) { + message.saveMessagesHistory(this.id) + } + }, + send(message: any) { + try { + sendMessage(message); + } catch (error) { + console.error('Failed to stringify message:', error); + return; + } + } + } }) \ No newline at end of file diff --git a/src/store/Voice.js b/src/store/Voice.js deleted file mode 100644 index 7f32d70..0000000 --- a/src/store/Voice.js +++ /dev/null @@ -1,50 +0,0 @@ -import { connectVoicesocket, disconnectVoicesocket, sendMessage, hangup } from "@/websocket/voiceSocket"; -import { defineStore } from "pinia"; - -export const voiceStore = defineStore("voice", { - state: () => ({ - isConnected: false - }), - actions: { - connect() { - connectVoicesocket(); - this.isConnected = true; - }, - disconnect() { - disconnectVoicesocket(); - this.isConnected = false; - }, - startCall(from, from_name, from_avatar, to) { - if (this.isConnected) { - const message = { - type: "incomingcall", - from: from, - from_name: from_name, - from_avatar: from_avatar, - to: to - } - sendMessage(JSON.stringify(message)) - } else { - console.log("voice socket is not connected") - } - }, - pickup(from,to){ - if (this.isConnected) { - const message ={ - type: "pickup", - from: from, - to: to - } - sendMessage(JSON.stringify(message)) - } else { - console.log("voice socket is not connected") - } - }, - hangup(){ - hangup() - } - } - -} - -) \ No newline at end of file diff --git a/src/store/Voice.ts b/src/store/Voice.ts new file mode 100644 index 0000000..d8c5f34 --- /dev/null +++ b/src/store/Voice.ts @@ -0,0 +1,235 @@ +import { sendMessage } from "@/websocket/onlineSocket"; +import { onCallStore } from "@/store/VoiceTarget"; +import { ref } from "vue"; + +const iceserver = { + iceServers: [ + { + urls: process.env.STUN_URL, + }, + { + urls: process.env.TURN_URL, + username: process.env.TURN_USERNAME, + credential: process.env.TURN_CREDENTIAL, + }, + ], +}; + + +interface message { + cmd: "VOICE_ICE_CANDIDATE" | "VOICE_SDP_OFFER" | "VOICE_SDP_ANSWER", + from: number, + to: number, + content: any, +} + +const oncall = onCallStore(); + +const RTCpeerConnection = ref(null); + +const localstream = ref(null); +const remotestream = ref(null); + +// 本地候选:为了避免“绑定 onicecandidate 太晚”导致丢失,统一先缓存,等 remoteDescription 就绪后再发送 +const pendingLocalCandidates = ref([]); +// 远端候选:避免远端 candidate 早到但 remoteDescription 未 set 导致 addIceCandidate 失败 +const pendingRemoteCandidates = ref([]); + +// 当前会话对端信息(用于发送 ICE 时带上 from/to) +const currentFrom = ref(null); +const currentTo = ref(null); + +const canSendIceNow = () => { + const pc: any = RTCpeerConnection.value; + return ( + !!pc && + !!pc.localDescription && + !!pc.remoteDescription && + currentFrom.value !== null && + currentTo.value !== null + ); +}; + +const flushLocalCandidatesIfReady = () => { + if (!canSendIceNow()) return; + if (pendingLocalCandidates.value.length === 0) return; + + pendingLocalCandidates.value.forEach((candidate) => { + const message: message = { + cmd: "VOICE_ICE_CANDIDATE", + from: currentFrom.value as number, + to: currentTo.value as number, + content: candidate, + }; + sendMessage(message); + }); + pendingLocalCandidates.value = []; +}; + +const flushRemoteCandidatesIfReady = async () => { + const pc: any = RTCpeerConnection.value; + if (!pc || !pc.remoteDescription) return; + if (pendingRemoteCandidates.value.length === 0) return; + + for (const c of pendingRemoteCandidates.value) { + await pc.addIceCandidate(new RTCIceCandidate(c)); + } + pendingRemoteCandidates.value = []; +}; + +const getlocalStream = async () => { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + console.log("获取本地音频流成功"); + return stream; + // // 获取音频和视频轨道 + // const audioTrack = stream.getAudioTracks()[0]; + // // 将轨道添加到 RTCPeerConnection + // peerConnection.addTrack(audioTrack, stream); +}; + +const closeLocalStream = (stream: MediaStream) => { + stream.getTracks().forEach(track => track.stop()); +} + +export const initRTCconnection = async () => { + RTCpeerConnection.value = new RTCPeerConnection(iceserver); + localstream.value = await getlocalStream(); + RTCpeerConnection.value.addTrack(localstream.value.getAudioTracks()[0], localstream.value); + + // 必须尽早绑定,否则可能在 setLocalDescription 后就把 candidate 产完了 + RTCpeerConnection.value.onicecandidate = (event: { candidate: any }) => { + if (!event.candidate) return; + pendingLocalCandidates.value.push(event.candidate); + flushLocalCandidatesIfReady(); + }; + + // 远端音频:收到 track 后把 MediaStream 绑定到 UI 的