Files
Reisaye/EyeVue/src/views/HomeView.vue
2025-08-08 16:07:49 +08:00

421 lines
10 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 class="stream-player">
<div class="controls">
<input
v-model="streamId"
placeholder="输入流ID"
class="stream-id-input"
/>
<input
v-model="authToken"
placeholder="输入认证Token"
class="auth-token-input"
/>
<button @click="startPlay" class="start-btn">开始播放</button>
<button @click="stopPlay" class="stop-btn" :disabled="!isPlaying">停止播放</button>
</div>
<div class="status" v-if="statusMessage" :class="statusClass">{{ statusMessage }}</div>
<!-- 显式显示当前加载的TS文件名称 -->
<div class="ts-info" v-if="currentTsFile || loadedTsFiles.length">
<div class="current-ts">当前加载: <strong>{{ currentTsFile || '无' }}</strong></div>
<div class="loaded-ts-list">
已加载列表:
<span
v-for="(ts, index) in loadedTsFiles"
:key="index"
:class="{ 'current': ts === currentTsFile }"
>
{{ ts }}
</span>
</div>
</div>
<div class="video-container">
<video
id="streamVideo"
class="video-js vjs-big-play-centered"
controls
autoplay
playsinline
></video>
</div>
</div>
</template>
<script>
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
import '@videojs/http-streaming';
export default {
name: 'StreamPlayer',
data() {
return {
streamId: '',
authToken: '',
player: null,
isPlaying: false,
statusMessage: '',
requestCounter: 0,
manifestRefreshTimer: null,
lastSegmentIndex: -1,
currentTsFile: '', // 当前正在加载的TS文件
loadedTsFiles: [] // 已加载的TS文件列表
};
},
computed: {
statusClass() {
if (!this.statusMessage) return '';
if (this.statusMessage.includes('错误')) return 'status-error';
if (this.statusMessage.includes('播放中')) return 'status-playing';
return 'status-info';
}
},
methods: {
// 提取TS文件名从URL中
extractTsFileName(url) {
// 匹配URL中的xxx.ts格式文件名
const match = url.match(/([^\/]+\.ts)(\?|$)/);
return match ? match[1] : null;
},
startPlay() {
// 输入验证
if (!this.streamId.trim()) {
this.statusMessage = '错误: 请输入流ID';
return;
}
this.requestCounter++;
const timestamp = new Date().getTime();
const cacheBuster = `${timestamp}-${this.requestCounter}-${Math.random().toString(36).substr(2, 8)}`;
const hlsUrl = `/api/hls/${this.streamId}/stream.m3u8?cache-buster=${cacheBuster}`;
this.statusMessage = '正在连接流...';
this.currentTsFile = '';
this.loadedTsFiles = []; // 重置已加载列表
// 销毁现有播放器
if (this.player) {
this.stopPlay();
}
// 初始化播放器
this.player = videojs('streamVideo', {
autoplay: true,
controls: true,
responsive: true,
fluid: true,
preload: 'none',
sources: [{
src: hlsUrl,
type: 'application/x-mpegURL'
}],
html5: {
vhs: {
overrideNative: true,
enableLowInitialLatency: true,
cacheDuration: 0,
maxBufferLength: 8,
maxMaxBufferLength: 15
},
hls: {
xhrSetup: (xhr, url) => {
// 添加认证Token
if (this.authToken) {
xhr.setRequestHeader('Authorization', `Bearer ${this.authToken}`);
}
// 缓存控制
xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
xhr.setRequestHeader('Pragma', 'no-cache');
xhr.setRequestHeader('Expires', '0');
// 检测TS文件请求并记录文件名
if (url.includes('.ts')) {
const tsFileName = this.extractTsFileName(url);
if (tsFileName) {
// 更新当前加载的TS文件
this.currentTsFile = tsFileName;
// 添加到已加载列表(去重)
if (!this.loadedTsFiles.includes(tsFileName)) {
this.loadedTsFiles.push(tsFileName);
// 只保留最近10个记录避免列表过长
if (this.loadedTsFiles.length > 10) {
this.loadedTsFiles.shift();
}
console.log(`[TS加载] ${tsFileName}`);
}
}
}
},
lowLatencyMode: true,
onError: (error) => {
console.error('HLS错误:', error);
if (this.player && this.player.hls) {
this.player.hls.startLoad();
}
}
}
}
});
// 监听播放器事件
this.player.on('loadedmetadata', () => {
this.isPlaying = true;
this.statusMessage = '播放中...';
this.startManifestRefresh();
});
this.player.on('error', () => {
const error = this.player.error();
const errorMsg = this.getErrorMessage(error);
this.statusMessage = `播放错误: ${errorMsg}`;
this.isPlaying = false;
console.error('播放器错误详情:', error);
// 解码或加载错误时尝试重连
if (error.code === 3 || error.code === 4) {
setTimeout(() => {
if (!this.isPlaying) {
this.statusMessage = '尝试重新连接...';
this.startPlay();
}
}, 3000);
}
});
this.player.on('ended', () => {
this.statusMessage = '播放结束';
this.isPlaying = false;
this.clearManifestRefresh();
});
// 监听片段加载完成事件
this.player.on('hlsSegmentLoaded', (event, data) => {
const tsFileName = this.extractTsFileName(data.segment.uri);
if (tsFileName) {
console.log(`[TS加载完成] ${tsFileName}`);
}
});
},
stopPlay() {
if (this.player) {
this.player.pause();
this.player.dispose();
this.player = null;
}
this.isPlaying = false;
this.statusMessage = '已停止播放';
this.clearManifestRefresh();
this.currentTsFile = '';
},
// 启动M3U8定期刷新
startManifestRefresh() {
this.clearManifestRefresh();
this.manifestRefreshTimer = setInterval(() => {
if (this.player && this.player.hls) {
try {
const playlist = this.player.hls.playlists.selectedVideoPlaylist;
if (playlist && playlist.segments) {
const currentSegments = playlist.segments.length;
// 检测是否有新片段
if (currentSegments > this.lastSegmentIndex + 1) {
this.lastSegmentIndex = currentSegments - 1;
const newTsFile = this.extractTsFileName(playlist.segments[this.lastSegmentIndex].uri);
console.log(`[检测到新片段] ${newTsFile || '未知片段'}`);
this.player.hls.startLoad();
}
}
} catch (e) {
console.error('刷新播放列表时出错:', e);
}
}
}, 3000);
},
// 清除刷新定时器
clearManifestRefresh() {
if (this.manifestRefreshTimer) {
clearInterval(this.manifestRefreshTimer);
this.manifestRefreshTimer = null;
}
},
getErrorMessage(error) {
switch (error.code) {
case 1: return '加载视频时发生错误';
case 2: return '视频格式不支持请确认使用H.264/AAC编码';
case 3: return '视频无法解码(可能是文件损坏或编码问题)';
case 4: return '媒体源无法加载(检查网络或服务器)';
case 5: return '未授权访问Token无效或已过期';
default: return `未知错误 (${error.code}): ${error.message || ''}`;
}
}
},
beforeDestroy() {
this.clearManifestRefresh();
if (this.player) {
this.player.dispose();
}
}
};
</script>
<style scoped>
.stream-player {
max-width: 1280px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: center;
}
.stream-id-input, .auth-token-input {
flex: 1;
min-width: 200px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.start-btn, .stop-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: opacity 0.2s;
}
.start-btn {
background-color: #4CAF50;
color: white;
}
.start-btn:hover {
background-color: #45a049;
}
.stop-btn {
background-color: #f44336;
color: white;
opacity: 0.7;
}
.stop-btn:not(:disabled) {
opacity: 1;
}
.stop-btn:not(:disabled):hover {
background-color: #d32f2f;
}
.stop-btn:disabled {
cursor: not-allowed;
}
.status {
margin-bottom: 15px;
padding: 10px 15px;
border-radius: 4px;
background-color: #f5f5f5;
font-size: 14px;
}
.status-error {
background-color: #ffebee;
color: #b71c1c;
border: 1px solid #ef9a9a;
}
.status-playing {
background-color: #e8f5e9;
color: #2e7d32;
border: 1px solid #a5d6a7;
}
.status-info {
background-color: #e3f2fd;
color: #0d47a1;
border: 1px solid #bbdefb;
}
/* TS文件信息样式 */
.ts-info {
margin: 15px 0;
padding: 10px 15px;
background-color: #f9f9f9;
border-radius: 4px;
font-size: 14px;
}
.current-ts {
margin-bottom: 8px;
color: #333;
}
.loaded-ts-list {
color: #666;
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 5px;
}
.loaded-ts-list span {
padding: 3px 8px;
background-color: #e0e0e0;
border-radius: 3px;
font-size: 12px;
}
.loaded-ts-list span.current {
background-color: #4CAF50;
color: white;
}
.video-container {
width: 100%;
background-color: #000;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
:deep(.video-js) {
width: 100%;
height: auto;
min-height: 400px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.controls {
flex-direction: column;
}
.stream-id-input, .auth-token-input {
width: 100%;
min-width: unset;
}
.start-btn, .stop-btn {
width: 100%;
}
:deep(.video-js) {
min-height: 250px;
}
}
</style>