将视频字幕嵌入
This commit is contained in:
@@ -647,6 +647,50 @@ header p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.now-playing-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.subtitle-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--panel-border);
|
||||
padding: 0.5rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.subtitle-panel label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.subtitle-panel select {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.subtitle-panel select option {
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.now-playing p {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -140,6 +140,12 @@
|
||||
<h3>Now Playing</h3>
|
||||
<p id="current-video-title">video.mp4</p>
|
||||
<div class="now-playing-actions">
|
||||
<div id="subtitle-panel" class="subtitle-panel hidden">
|
||||
<label for="subtitle-selector">字幕:</label>
|
||||
<select id="subtitle-selector">
|
||||
<option value="-1">无字幕</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="transcode-btn" class="play-btn hidden">开始播放</button>
|
||||
<button id="stop-transcode-btn" class="play-btn stop-btn hidden">停止播放</button>
|
||||
<button id="clear-playing-download-cache-btn" class="play-btn stop-btn hidden">清空下载缓存</button>
|
||||
|
||||
@@ -23,6 +23,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeSelector = document.getElementById('theme-selector');
|
||||
const videoListHeader = document.getElementById('video-list-header');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const subtitlePanel = document.getElementById('subtitle-panel');
|
||||
const subtitleSelector = document.getElementById('subtitle-selector');
|
||||
const topBannerTitle = document.getElementById('top-banner-title');
|
||||
const playBtn = document.getElementById('play-btn');
|
||||
const topBanner = document.getElementById('top-banner');
|
||||
@@ -143,6 +145,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const decoder = 'auto';
|
||||
const encoder = encoderSelect?.value || 'h264_rkmpp';
|
||||
let streamUrl = `/api/hls/playlist.m3u8?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&decoder=${encodeURIComponent(decoder)}&encoder=${encodeURIComponent(encoder)}`;
|
||||
|
||||
const subIndex = subtitleSelector?.value;
|
||||
if (subIndex && subIndex !== '-1') {
|
||||
streamUrl += `&subtitleIndex=${encodeURIComponent(subIndex)}`;
|
||||
}
|
||||
|
||||
const sessionId = localStorage.getItem('sessionId');
|
||||
if (sessionId) {
|
||||
streamUrl += `&sessionId=${encodeURIComponent(sessionId)}`;
|
||||
@@ -265,16 +273,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
const subscribeToKey = (key) => {
|
||||
subscribedKey = key;
|
||||
let subscriptionKey = key;
|
||||
const subIndex = subtitleSelector?.value;
|
||||
if (subIndex && subIndex !== '-1') {
|
||||
subscriptionKey = `${key}-sub${subIndex}`;
|
||||
}
|
||||
|
||||
subscribedKey = subscriptionKey;
|
||||
if (wsConnected) {
|
||||
sendWsMessage({ type: 'subscribe', key });
|
||||
sendWsMessage({ type: 'subscribe', key: subscriptionKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleWsMessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.key !== currentVideoKey) return;
|
||||
if (message.key !== subscribedKey) return;
|
||||
|
||||
if (message.type === 'duration' && message.duration) {
|
||||
videoDuration = message.duration;
|
||||
@@ -891,6 +905,51 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
nowPlaying.classList.remove('hidden');
|
||||
currentVideoTitle.textContent = key.split('/').pop();
|
||||
|
||||
// Fetch subtitle metadata
|
||||
fetchVideoMetadata(selectedBucket, key);
|
||||
};
|
||||
|
||||
const handleSubtitleChange = () => {
|
||||
if (!selectedKey) return;
|
||||
subscribeToKey(selectedKey);
|
||||
};
|
||||
if (subtitleSelector) {
|
||||
subtitleSelector.addEventListener('change', handleSubtitleChange);
|
||||
}
|
||||
|
||||
const fetchVideoMetadata = async (bucket, key) => {
|
||||
if (!subtitlePanel || !subtitleSelector) return;
|
||||
|
||||
subtitlePanel.classList.add('hidden');
|
||||
subtitleSelector.innerHTML = '<option value="-1">正在加载字幕...</option>';
|
||||
subtitleSelector.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/video-metadata?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(key)}`, { headers: s3AuthHeaders });
|
||||
if (!res.ok) throw new Error('Failed to fetch metadata');
|
||||
const data = await res.json();
|
||||
|
||||
subtitleSelector.innerHTML = '<option value="-1">无字幕</option>';
|
||||
if (data.subtitleStreams && data.subtitleStreams.length > 0) {
|
||||
data.subtitleStreams.forEach(sub => {
|
||||
const option = document.createElement('option');
|
||||
option.value = sub.subIndex;
|
||||
option.textContent = `[${sub.language}] ${sub.title}`;
|
||||
subtitleSelector.appendChild(option);
|
||||
});
|
||||
subtitlePanel.classList.remove('hidden');
|
||||
} else {
|
||||
subtitleSelector.innerHTML = '<option value="-1">无嵌入字幕</option>';
|
||||
subtitlePanel.classList.remove('hidden');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fetch metadata failed:', err);
|
||||
subtitleSelector.innerHTML = '<option value="-1">无法加载字幕</option>';
|
||||
subtitlePanel.classList.remove('hidden');
|
||||
} finally {
|
||||
subtitleSelector.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startTranscode = async () => {
|
||||
|
||||
Reference in New Issue
Block a user