修复流式传输的错误

This commit is contained in:
CN-JS-HuiBai
2026-04-02 21:52:51 +08:00
parent c2a8cf79c8
commit fd199fdde7
4 changed files with 248 additions and 41 deletions

View File

@@ -636,6 +636,92 @@ header p {
transition: width 0.25s ease;
}
.progress-fill.transcode-fill {
background: linear-gradient(90deg, #8b5cf6, #6d28d9);
}
/* Phase containers */
.phase-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
width: 85%;
max-width: 420px;
animation: phaseIn 0.4s ease;
}
@keyframes phaseIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.phase-container h3 {
font-size: 1.1rem;
font-weight: 700;
margin: 0;
}
.phase-container .phase-detail {
color: var(--text-secondary);
font-size: 0.9rem;
margin: 0;
}
.phase-icon {
width: 64px;
height: 64px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.25rem;
}
.phase-icon.download-icon {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.15), rgba(59, 130, 246, 0.15));
color: #0891b2;
animation: pulse-download 2s ease-in-out infinite;
}
@keyframes pulse-download {
0%, 100% { box-shadow: 0 0 0 0 rgba(6, 182, 212, 0.3); }
50% { box-shadow: 0 0 0 12px rgba(6, 182, 212, 0); }
}
.phase-icon.transcode-icon {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(109, 40, 217, 0.15));
color: #7c3aed;
animation: pulse-transcode 2s ease-in-out infinite;
}
@keyframes pulse-transcode {
0%, 100% { box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.3); }
50% { box-shadow: 0 0 0 12px rgba(139, 92, 246, 0); }
}
/* Transcode stats */
.transcode-stats {
display: flex;
gap: 0.75rem;
justify-content: center;
flex-wrap: wrap;
margin-top: 0.25rem;
}
.transcode-stats span {
font-size: 0.8rem;
color: var(--text-secondary);
background: rgba(148, 163, 184, 0.12);
padding: 0.25rem 0.6rem;
border-radius: 8px;
font-weight: 500;
}
.transcode-stats span:empty {
display: none;
}
.play-btn {
margin-top: 1rem;
padding: 0.85rem 1.25rem;

View File

@@ -85,13 +85,37 @@
<h3>Select a video to start transcoding</h3>
</div>
<div id="transcoding-overlay" class="player-overlay hidden">
<div class="spinner"></div>
<h3>Transcoding via FFmpeg...</h3>
<p>Generating MP4 file, please wait.</p>
<div id="progress-info" class="progress-info hidden">
<div id="progress-text" class="progress-text">Initializing...</div>
<!-- Download Phase -->
<div id="download-phase" class="phase-container">
<div class="phase-icon download-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</div>
<h3>正在从 S3 下载源文件...</h3>
<p id="download-size-text" class="phase-detail">准备下载...</p>
<div class="progress-info">
<div id="download-progress-text" class="progress-text">0%</div>
<div class="progress-bar">
<div id="progress-fill" class="progress-fill"></div>
<div id="download-progress-fill" class="progress-fill"></div>
</div>
</div>
</div>
<!-- Transcode Phase -->
<div id="transcode-phase" class="phase-container hidden">
<div class="phase-icon transcode-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
</div>
<h3>正在转码并流式传输...</h3>
<p id="transcode-detail-text" class="phase-detail">FFmpeg 转码中...</p>
<div class="progress-info">
<div id="transcode-progress-text" class="progress-text">0%</div>
<div class="progress-bar">
<div id="transcode-progress-fill" class="progress-fill transcode-fill"></div>
</div>
<div id="transcode-stats" class="transcode-stats hidden">
<span id="stat-fps"></span>
<span id="stat-bitrate"></span>
<span id="stat-time"></span>
</div>
</div>
</div>
</div>

View File

@@ -20,11 +20,24 @@ document.addEventListener('DOMContentLoaded', () => {
const transcodeBtn = document.getElementById('transcode-btn');
const stopTranscodeBtn = document.getElementById('stop-transcode-btn');
const playBtn = document.getElementById('play-btn');
const progressInfo = document.getElementById('progress-info');
const progressText = document.getElementById('progress-text');
const progressFill = document.getElementById('progress-fill');
const topBanner = document.getElementById('top-banner');
// Download phase elements
const downloadPhase = document.getElementById('download-phase');
const downloadSizeText = document.getElementById('download-size-text');
const downloadProgressText = document.getElementById('download-progress-text');
const downloadProgressFill = document.getElementById('download-progress-fill');
// Transcode phase elements
const transcodePhase = document.getElementById('transcode-phase');
const transcodeDetailText = document.getElementById('transcode-detail-text');
const transcodeProgressText = document.getElementById('transcode-progress-text');
const transcodeProgressFill = document.getElementById('transcode-progress-fill');
const transcodeStats = document.getElementById('transcode-stats');
const statFps = document.getElementById('stat-fps');
const statBitrate = document.getElementById('stat-bitrate');
const statTime = document.getElementById('stat-time');
let currentPollInterval = null;
let selectedBucket = null;
let selectedKey = null;
@@ -32,8 +45,18 @@ document.addEventListener('DOMContentLoaded', () => {
let wsConnected = false;
let subscribedKey = null;
let currentVideoKey = null;
let s3Username = '';
let s3Password = '';
let s3AuthHeaders = {};
const formatBytes = (bytes) => {
if (!bytes || bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(i > 0 ? 2 : 0) + ' ' + units[i];
};
const sendWsMessage = (message) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
@@ -51,10 +74,10 @@ document.addEventListener('DOMContentLoaded', () => {
try {
const message = JSON.parse(event.data);
if (message.type === 'progress' && message.key === currentVideoKey) {
setProgress(message.progress);
handleProgress(message.progress);
}
if (message.type === 'ready' && message.key === currentVideoKey) {
setProgress({ status: 'Ready to play', percent: 100, details: 'Ready to play' });
handleProgress({ status: 'finished', percent: 100, details: 'Ready to play' });
playMp4Stream(message.mp4Url);
}
} catch (error) {
@@ -62,6 +85,95 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
const handleProgress = (progress) => {
if (!progress) return;
const status = progress.status;
if (status === 'downloading') {
showDownloadPhase();
const percent = Math.min(Math.max(Math.round(progress.percent || 0), 0), 100);
const downloaded = formatBytes(progress.downloadedBytes || 0);
const total = formatBytes(progress.totalBytes || 0);
downloadSizeText.textContent = `${downloaded} / ${total}`;
downloadProgressText.textContent = `${percent}%`;
downloadProgressFill.style.width = `${percent}%`;
} else if (status === 'downloaded') {
showDownloadPhase();
const downloaded = formatBytes(progress.downloadedBytes || progress.totalBytes || 0);
const total = formatBytes(progress.totalBytes || 0);
downloadSizeText.textContent = `${downloaded} / ${total} — 下载完成`;
downloadProgressText.textContent = '100%';
downloadProgressFill.style.width = '100%';
// Brief pause then transition to transcode phase
setTimeout(() => {
showTranscodePhase();
}, 600);
} else if (status === 'transcoding') {
showTranscodePhase();
const percent = Math.min(Math.max(Math.round(progress.percent || 0), 0), 100);
transcodeProgressText.textContent = `${percent}%`;
transcodeProgressFill.style.width = `${percent}%`;
transcodeDetailText.textContent = progress.details || 'FFmpeg 转码中...';
// Show stats
if (progress.fps || progress.bitrate || progress.timemark) {
transcodeStats.classList.remove('hidden');
statFps.textContent = progress.fps ? `${progress.fps} fps` : '';
statBitrate.textContent = progress.bitrate ? `${progress.bitrate} kbps` : '';
statTime.textContent = progress.timemark ? `${progress.timemark}` : '';
}
} else if (status === 'finished') {
showTranscodePhase();
transcodeProgressText.textContent = '100%';
transcodeProgressFill.style.width = '100%';
transcodeDetailText.textContent = '转码完成';
} else if (status === 'failed') {
transcodeDetailText.textContent = `失败: ${progress.details || '未知错误'}`;
transcodeProgressFill.style.background = 'linear-gradient(90deg, #dc2626, #b91c1c)';
} else if (status === 'cancelled') {
transcodeDetailText.textContent = '已取消';
transcodeProgressFill.style.width = '0%';
}
};
const showDownloadPhase = () => {
if (downloadPhase) downloadPhase.classList.remove('hidden');
if (transcodePhase) transcodePhase.classList.add('hidden');
};
const showTranscodePhase = () => {
if (downloadPhase) downloadPhase.classList.add('hidden');
if (transcodePhase) transcodePhase.classList.remove('hidden');
};
const resetPhases = () => {
// Reset download phase
if (downloadPhase) downloadPhase.classList.remove('hidden');
if (downloadSizeText) downloadSizeText.textContent = '准备下载...';
if (downloadProgressText) downloadProgressText.textContent = '0%';
if (downloadProgressFill) downloadProgressFill.style.width = '0%';
// Reset transcode phase
if (transcodePhase) transcodePhase.classList.add('hidden');
if (transcodeDetailText) transcodeDetailText.textContent = 'FFmpeg 转码中...';
if (transcodeProgressText) transcodeProgressText.textContent = '0%';
if (transcodeProgressFill) {
transcodeProgressFill.style.width = '0%';
transcodeProgressFill.style.background = '';
}
if (transcodeStats) transcodeStats.classList.add('hidden');
if (statFps) statFps.textContent = '';
if (statBitrate) statBitrate.textContent = '';
if (statTime) statTime.textContent = '';
// Reset buttons
if (stopTranscodeBtn) {
stopTranscodeBtn.classList.add('hidden');
stopTranscodeBtn.disabled = false;
stopTranscodeBtn.textContent = 'Stop Transcode';
}
};
const loadConfig = async () => {
if (!topBanner) return;
try {
@@ -101,30 +213,12 @@ document.addEventListener('DOMContentLoaded', () => {
});
};
const setProgress = (progress) => {
if (!progressInfo || !progressText || !progressFill) return;
const percent = Math.min(Math.max(Math.round(progress?.percent || 0), 0), 100);
progressInfo.classList.remove('hidden');
progressText.textContent = `${progress?.details || progress?.status || 'Transcoding...'} ${percent ? `${percent}%` : ''}`.trim();
progressFill.style.width = `${percent}%`;
};
const resetProgress = () => {
if (!progressInfo || !progressText || !progressFill) return;
progressInfo.classList.add('hidden');
progressText.textContent = '';
progressFill.style.width = '0%';
if (stopTranscodeBtn) {
stopTranscodeBtn.classList.add('hidden');
stopTranscodeBtn.disabled = false;
stopTranscodeBtn.textContent = 'Stop Transcode';
}
};
const setAuthHeaders = (username, password) => {
s3Username = typeof username === 'string' ? username : '';
s3Password = typeof password === 'string' ? password : '';
s3AuthHeaders = {};
if (username) s3AuthHeaders['X-S3-Username'] = username;
if (password) s3AuthHeaders['X-S3-Password'] = password;
if (s3Username) s3AuthHeaders['X-S3-Username'] = s3Username;
if (s3Password) s3AuthHeaders['X-S3-Password'] = s3Password;
};
const showLogin = () => {
@@ -375,7 +469,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (playBtn) {
playBtn.classList.add('hidden');
}
resetProgress();
resetPhases();
selectedKey = key;
currentVideoKey = key;
@@ -397,14 +491,16 @@ document.addEventListener('DOMContentLoaded', () => {
stopTranscodeBtn.textContent = 'Stop Transcode';
}
stopPolling();
resetPhases();
transcodingOverlay.classList.remove('hidden');
setProgress({ status: 'Starting stream...', percent: 0, details: 'Starting stream...' });
try {
const codec = codecSelect?.value || 'h264';
const encoder = encoderSelect?.value || 'software';
if (!selectedBucket) throw new Error('No bucket selected');
const streamUrl = `/api/stream?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&codec=${encodeURIComponent(codec)}&encoder=${encodeURIComponent(encoder)}`;
let streamUrl = `/api/stream?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&codec=${encodeURIComponent(codec)}&encoder=${encodeURIComponent(encoder)}`;
if (s3Username) streamUrl += `&username=${encodeURIComponent(s3Username)}`;
if (s3Password) streamUrl += `&password=${encodeURIComponent(s3Password)}`;
videoPlayer.src = streamUrl;
videoPlayer.load();
videoPlayer.addEventListener('loadedmetadata', () => {
@@ -436,7 +532,7 @@ document.addEventListener('DOMContentLoaded', () => {
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to stop transcode');
setProgress({ status: 'cancelled', percent: 0, details: 'Transcode stopped' });
handleProgress({ status: 'cancelled', percent: 0, details: 'Transcode stopped' });
if (transcodeBtn) {
transcodeBtn.disabled = false;
transcodeBtn.textContent = 'Start Transcode';
@@ -469,7 +565,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (stopTranscodeBtn) {
stopTranscodeBtn.classList.add('hidden');
}
resetProgress();
resetPhases();
videoPlayer.src = url;
videoPlayer.load();

View File

@@ -112,8 +112,9 @@ const shouldRetryWithSoftware = (message) => {
};
const extractS3Credentials = (req) => {
const username = req.headers['x-s3-username'] || req.body?.username || '';
const password = req.headers['x-s3-password'] || req.body?.password || '';
const query = req.query || {};
const username = req.headers['x-s3-username'] || req.body?.username || query.username || query.accessKeyId || '';
const password = req.headers['x-s3-password'] || req.body?.password || query.password || query.secretAccessKey || '';
return {
username: typeof username === 'string' ? username.trim() : '',
password: typeof password === 'string' ? password : ''