303 lines
7.6 KiB
Vue
303 lines
7.6 KiB
Vue
<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> |