Generating MP4 file, please wait.
-
Initializing...
-
-
+
+
+
+
正在从 S3 下载源文件...
+
准备下载...
+
+
+
+
+
+
正在转码并流式传输...
+
FFmpeg 转码中...
+
diff --git a/public/js/main.js b/public/js/main.js
index dcc6a47..fe837a2 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -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();
diff --git a/server.js b/server.js
index 1a876f5..f9b4386 100644
--- a/server.js
+++ b/server.js
@@ -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 : ''