Files
myplayer-vue/src/components/videoPlayer.vue
2026-04-04 17:48:34 +08:00

303 lines
7.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div ref="videoRef" class="dplayer-container"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import DPlayer from 'dplayer';
import Hls from 'hls.js';
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 },
danmaku: { type: Object, default: () => ({}) }
});
// 判断是否为 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: {},
danmaku: props.danmaku
};
if (isHls && Hls.isSupported()) {
// HLS 格式使用 customHls
options.video = {
url: proxyUrl,
type: 'customHls',
customType: {
customHls: function (video, player) {
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);
});
}
});
}
}
};
} else {
// 普通视频格式MP4等
options.video = {
url: proxyUrl,
type: 'auto' // 让 DPlayer 自动检测类型
};
}
return options;
};
onMounted(() => {
try {
const playerOptions = createPlayerOptions(props.videoUrl);
player.value = new DPlayer(playerOptions);
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', () => {
console.log('播放结束');
});
player.value.on('error', (error) => {
console.error('播放器错误:', error);
});
player.value.on('loadstart', () => {
console.log('开始加载视频');
});
player.value.on('canplay', () => {
// 缓冲、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);
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.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);
}
}
});
// 销毁时清理播放器和 HLS 实例
onBeforeUnmount(() => {
cleanupHls();
if (player.value) {
try {
player.value.destroy();
} catch (e) {
console.warn('销毁播放器时出错:', e);
}
player.value = null;
}
});
</script>
<style scoped>
.dplayer-container {
width: 100%;
height: 100%;
}
</style>