新增转码进度显示功能,优化视频标题显示,添加编码器选择面板。
This commit is contained in:
@@ -401,6 +401,35 @@ header p {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
width: 0%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.play-btn {
|
.play-btn {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 0.85rem 1.25rem;
|
padding: 0.85rem 1.25rem;
|
||||||
|
|||||||
@@ -65,6 +65,12 @@
|
|||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<h3>Transcoding via FFmpeg...</h3>
|
<h3>Transcoding via FFmpeg...</h3>
|
||||||
<p>Generating HLS segments, please wait.</p>
|
<p>Generating HLS segments, 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<video id="video-player" controls preload="auto" class="hidden"></video>
|
<video id="video-player" controls preload="auto" class="hidden"></video>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,9 +10,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const nowPlaying = document.getElementById('now-playing');
|
const nowPlaying = document.getElementById('now-playing');
|
||||||
const currentVideoTitle = document.getElementById('current-video-title');
|
const currentVideoTitle = document.getElementById('current-video-title');
|
||||||
const playBtn = document.getElementById('play-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');
|
||||||
|
|
||||||
let currentPollInterval = null;
|
let currentPollInterval = null;
|
||||||
|
|
||||||
|
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 (playBtn) {
|
if (playBtn) {
|
||||||
playBtn.addEventListener('click', () => {
|
playBtn.addEventListener('click', () => {
|
||||||
videoPlayer.play().catch(e => console.log('Play blocked:', e));
|
videoPlayer.play().catch(e => console.log('Play blocked:', e));
|
||||||
@@ -144,11 +162,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
videoPlayer.classList.add('hidden');
|
videoPlayer.classList.add('hidden');
|
||||||
videoPlayer.pause();
|
videoPlayer.pause();
|
||||||
if (playBtn) playBtn.classList.add('hidden');
|
if (playBtn) playBtn.classList.add('hidden');
|
||||||
|
resetProgress();
|
||||||
|
|
||||||
nowPlaying.classList.remove('hidden');
|
nowPlaying.classList.remove('hidden');
|
||||||
currentVideoTitle.textContent = key.split('/').pop();
|
currentVideoTitle.textContent = key.split('/').pop();
|
||||||
|
|
||||||
transcodingOverlay.classList.remove('hidden');
|
transcodingOverlay.classList.remove('hidden');
|
||||||
|
setProgress({ status: 'Starting transcoding...', percent: 0, details: 'Starting transcoding...' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const codec = codecSelect?.value || 'h264';
|
const codec = codecSelect?.value || 'h264';
|
||||||
@@ -181,8 +201,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const res = await fetch(`/api/status?key=${encodeURIComponent(key)}`);
|
const res = await fetch(`/api/status?key=${encodeURIComponent(key)}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.ready) {
|
if (data.progress) {
|
||||||
|
setProgress(data.progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.ready) {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
setProgress({ status: 'Ready to play', percent: 100, details: 'Ready to play' });
|
||||||
playHlsStream(data.hlsUrl);
|
playHlsStream(data.hlsUrl);
|
||||||
} else if (attempts >= maxAttempts) {
|
} else if (attempts >= maxAttempts) {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
@@ -206,6 +231,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
transcodingOverlay.classList.add('hidden');
|
transcodingOverlay.classList.add('hidden');
|
||||||
videoPlayer.classList.remove('hidden');
|
videoPlayer.classList.remove('hidden');
|
||||||
playBtn.classList.add('hidden');
|
playBtn.classList.add('hidden');
|
||||||
|
resetProgress();
|
||||||
|
|
||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
const hls = new Hls();
|
const hls = new Hls();
|
||||||
|
|||||||
25
server.js
25
server.js
@@ -28,6 +28,7 @@ const s3Client = new S3Client({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
|
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
|
||||||
|
const progressMap = {};
|
||||||
|
|
||||||
// Endpoint to list videos in the bucket
|
// Endpoint to list videos in the bucket
|
||||||
app.get('/api/videos', async (req, res) => {
|
app.get('/api/videos', async (req, res) => {
|
||||||
@@ -82,9 +83,12 @@ app.post('/api/transcode', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
||||||
|
const progressKey = safeKeySegments.join('/');
|
||||||
const outputDir = path.join(__dirname, 'public', 'hls', ...safeKeySegments);
|
const outputDir = path.join(__dirname, 'public', 'hls', ...safeKeySegments);
|
||||||
const m3u8Path = path.join(outputDir, 'index.m3u8');
|
const m3u8Path = path.join(outputDir, 'index.m3u8');
|
||||||
const hlsUrl = `/hls/${safeKeySegments.join('/')}/index.m3u8`;
|
const hlsUrl = `/hls/${progressKey}/index.m3u8`;
|
||||||
|
|
||||||
|
progressMap[progressKey] = { status: 'pending', percent: 0, details: 'Waiting for ffmpeg to start' };
|
||||||
|
|
||||||
// If it already exists, just return the URL
|
// If it already exists, just return the URL
|
||||||
if (fs.existsSync(m3u8Path)) {
|
if (fs.existsSync(m3u8Path)) {
|
||||||
@@ -119,11 +123,24 @@ app.post('/api/transcode', async (req, res) => {
|
|||||||
'-f hls'
|
'-f hls'
|
||||||
])
|
])
|
||||||
.output(m3u8Path)
|
.output(m3u8Path)
|
||||||
|
.on('progress', (progress) => {
|
||||||
|
progressMap[progressKey] = {
|
||||||
|
status: 'transcoding',
|
||||||
|
percent: Math.min(Math.max(Math.round(progress.percent || 0), 0), 100),
|
||||||
|
frame: progress.frames || null,
|
||||||
|
fps: progress.currentFps || null,
|
||||||
|
bitrate: progress.currentKbps || null,
|
||||||
|
timemark: progress.timemark || null,
|
||||||
|
details: `Transcoding... ${Math.min(Math.max(Math.round(progress.percent || 0), 0), 100)}%`
|
||||||
|
};
|
||||||
|
})
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
console.log(`Finished transcoding ${key} to HLS`);
|
console.log(`Finished transcoding ${key} to HLS`);
|
||||||
|
progressMap[progressKey] = { status: 'finished', percent: 100, details: 'Transcoding complete' };
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
console.error(`Error transcoding ${key}:`, err);
|
console.error(`Error transcoding ${key}:`, err);
|
||||||
|
progressMap[progressKey] = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, details: err.message || 'Transcoding failed' };
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
@@ -142,13 +159,15 @@ app.get('/api/status', (req, res) => {
|
|||||||
if (!key) return res.status(400).json({ error: 'Key is required' });
|
if (!key) return res.status(400).json({ error: 'Key is required' });
|
||||||
|
|
||||||
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
||||||
|
const progressKey = safeKeySegments.join('/');
|
||||||
const m3u8Path = path.join(__dirname, 'public', 'hls', ...safeKeySegments, 'index.m3u8');
|
const m3u8Path = path.join(__dirname, 'public', 'hls', ...safeKeySegments, 'index.m3u8');
|
||||||
|
const progress = progressMap[progressKey] || null;
|
||||||
|
|
||||||
// Check if the playlist file exists
|
// Check if the playlist file exists
|
||||||
if (fs.existsSync(m3u8Path)) {
|
if (fs.existsSync(m3u8Path)) {
|
||||||
res.json({ ready: true, hlsUrl: `/hls/${safeKeySegments.join('/')}/index.m3u8` });
|
res.json({ ready: true, hlsUrl: `/hls/${safeKeySegments.join('/')}/index.m3u8`, progress });
|
||||||
} else {
|
} else {
|
||||||
res.json({ ready: false });
|
res.json({ ready: false, progress });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user