feat: dplayer

This commit is contained in:
merlin
2025-12-29 15:40:42 +08:00
parent 27c4a247d3
commit b40691a4a3
10 changed files with 394 additions and 168 deletions

View File

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

View File

@@ -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}`);
});
});

67
src/api/playroom.ts Normal file
View File

@@ -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);
}
}

View File

@@ -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 []
}
}

View File

@@ -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;
}
});
</script>

38
src/store/playroom.ts Normal file
View File

@@ -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<PlayroomState>();
const currentUrl = ref<string>("");
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,
}
})

View File

@@ -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:{
}
})

View File

@@ -16,7 +16,7 @@
<el-table-column label="简介" prop="r_introduction"></el-table-column>
<el-table-column label="">
<template #default="scope">
<el-button @click="goToRoom(scope.row.r_id)">进入房间</el-button>
<el-button @click="goToRoom(scope.row)">进入房间</el-button>
</template>
</el-table-column>
</el-table>
@@ -24,7 +24,7 @@
<el-dialog v-model="createWindow" title="创建房间" style="width: 400px;height: 300px;">
<el-form ref="form" :model="formData" label-width="80px" @submit.prevent="createRoom">
<el-form ref="form" :model="formData" label-width="80px" @submit.prevent="createroom">
<el-form-item label="房间名" prop="roomName">
<el-input v-model="formData.roomName" placeholder="请输入房间名" clearable></el-input>
</el-form-item>
@@ -39,14 +39,11 @@
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { roomStore } from '@/store/room';
import axios from 'axios';
import { userInfoStore } from '@/store/store';
import { PlayroomStore } from '@/store/playroom';
import { ElMessage } from 'element-plus';
import { getPlayrooms, createPlayroom } from '@/api/playroom';
const userinfo = userInfoStore()
const roominfo = roomStore()
const roominfo = PlayroomStore()
const createWindow = ref(false)
const formData = reactive({
roomName: ''
@@ -54,65 +51,45 @@ const formData = reactive({
const rooms = ref([])
const goToRoom = (r_id) => {
console.log(r_id)
roominfo.r_id = r_id
const goToRoom = (r) => {
roominfo.setCurrentPlayroom(r);
// 跳转到房间页面
const baseUrl = window.location.origin;
const targetUrl = `${baseUrl}/room`; // 替换为你的目标路由
window.open(targetUrl, "room");
};
const createRoom = async () => {
const createroom = async () => {
if (!formData.roomName || formData.roomName.trim() === '') {
ElMessage.error('房间名不能为空')
return
}
try {
const response = await axios.post('/api/room/create',{
r_name: formData.roomName
},{
headers: {
'Content-Type': 'application/json',
'Authorization': userinfo.token
},
})
if (response.data.code === 200) {
console.log(formData.roomName + '创建成功')
} else {
console.log(response.data.msg)
if(await createPlayroom(formData.roomName)){
ElMessage.success('创建成功')
}else{
ElMessage.error('创建失败')
}
formData.roomName = ''
createWindow.value = false
getRooms()
rooms.value = await getPlayrooms()
}
catch (error) {
console.log(error)
}
}
const getRooms = async () => {
const getrooms = async () => {
try {
const response = await axios.get('/api/room/getrooms',{
headers: {
'Content-Type': 'application/json',
'Authorization': userinfo.token
},
})
if (response.data.code === 200) {
console.log(response.data.data)
rooms.value = response.data.data
} else {
console.log(response.data.msg)
}
rooms.value = await getPlayrooms()
}
catch (error) {
console.log(error)
ElMessage.error('获取房间列表失败' + error)
}
}
onMounted(() => {
getRooms()
getrooms()
})
</script>

View File

@@ -50,7 +50,7 @@
<el-table-column prop="r_introduction" label="个性签名" width="200"></el-table-column>
<el-table-column>
<template #default="scope">
<el-button @click="joinRoom(scope.row.r_id)">加入房间</el-button>
<el-button @click="joinroom(scope.row.r_id)">加入房间</el-button>
</template>
</el-table-column>
</el-table>
@@ -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)
}
}
</script>

View File

@@ -37,7 +37,7 @@
</el-row>
</el-col>
<el-col :span="16">
<video-player :autoplay="false" :videoUrl="videoUrl" />
<video-player :autoplay="false" :videoUrl="currentURL" />
</el-col>
<el-col :span="4">
<el-row style="height: 50%">
@@ -140,7 +140,7 @@
<el-dialog title="设置/替换视频流" v-model="dialogVisibleVideo">
<el-row>
<el-col :span="20">
<el-input v-model="curruentRoomInfo.currentURL" placeholder="请输入视频流地址"></el-input>
<el-input v-model="changingVideoUrl" placeholder="请输入视频流地址"></el-input>
</el-col>
<el-col :span="4">
<el-button>测试</el-button>
@@ -159,12 +159,13 @@
<script setup>
import { onMounted, ref, watchEffect } from 'vue';
import { ElMessage } from 'element-plus';
import { userInfoStore } from '@/store/store';
import { userInfoStore } from '@/store/user';
import videoPlayer from '@/components/videoPlayer.vue';
import { roomStore } from '@/store/room';
import { PlayroomStore } from '@/store/playroom';
//import audioPlayer from '@/components/audioPlayer.vue'; // 假设你有一个音频播放组件
const curruentRoomInfo = roomStore();
const curruentRoomInfo = PlayroomStore();
const userinfo = userInfoStore();
const dialogVisibleCode = ref(false)
@@ -175,6 +176,8 @@ const drawer = ref(false);
const role = ref(null);
const invitingCode = ref('666666')
const changingVideoUrl = ref('')
const currentURL = ref('https://www.5dm.link/api/dd.php?vid=ccccxhndnys1&cid=ccccxhndnys1&xid=0&pid=55293&tid=1742788904&t=616d5131b6ade51a0e20814466b13515&ext=.mp4')
const avatar = ref(null)
const avatarPreview = ref('')
@@ -333,7 +336,7 @@ const getInvitingCode = () => {
}
const replaceURL = () => {
currentURL.value = curruentRoomInfo.currentURL;
currentURL.value = changingVideoUrl.value;
dialogVisibleVideo.value = false;
}
@@ -365,7 +368,7 @@ onMounted(() => {
//测试代码
curruentRoomInfo.r_id = '123456';
curruentRoomInfo.r_name = '弹幕聊天室';
curruentRoomInfo.currentURL = 'https://www.5dm.link/api/dd.php?vid=ccccxhndnys1&cid=ccccxhndnys1&xid=0&pid=55293&tid=1742788904&t=616d5131b6ade51a0e20814466b13515&ext=.mp4';
curruentRoomInfo.setCurrentUrl('https://www.5dm.link/api/dd.php?vid=ccccxhndnys1&cid=ccccxhndnys1&xid=0&pid=55293&tid=1742788904&t=616d5131b6ade51a0e20814466b13515&ext=.mp4')
curruentRoomInfo.r_avatar = 'https://merlin.xin/avatars/avatar';
role.value = 0;
avatarPreview.value = curruentRoomInfo.r_avatar;