修复流式传输的错误
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
<div class="progress-bar">
|
||||
<div id="progress-fill" class="progress-fill"></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="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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 : ''
|
||||
|
||||
Reference in New Issue
Block a user