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 @@