添加内嵌字幕识别
This commit is contained in:
@@ -53,10 +53,6 @@
|
||||
<option value="" disabled selected>Choose a bucket</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="codec-panel">
|
||||
<label for="decoder-select">视频解码器:</label>
|
||||
<select id="decoder-select"></select>
|
||||
</div>
|
||||
<div class="codec-panel">
|
||||
<label for="encoder-select">硬件编码器:</label>
|
||||
<select id="encoder-select"></select>
|
||||
@@ -143,6 +139,17 @@
|
||||
<span id="seek-total-time" class="control-time">00:00:00</span>
|
||||
</div>
|
||||
<div class="controls-right">
|
||||
<div id="subtitle-control" class="control-popover speed-popover hidden">
|
||||
<button id="control-subtitle-toggle" class="control-btn" type="button" aria-label="Subtitles">CC Off</button>
|
||||
<div class="popover-panel">
|
||||
<label class="speed-control" for="subtitle-select">
|
||||
<span>Subtitles</span>
|
||||
<select id="subtitle-select" class="speed-select">
|
||||
<option value="off" selected>Off</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-popover speed-popover">
|
||||
<button id="control-speed-toggle" class="control-btn" type="button" aria-label="Playback Speed">1x</button>
|
||||
<div class="popover-panel">
|
||||
|
||||
@@ -10,9 +10,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginPasswordInput = document.getElementById('login-password');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const loginError = document.getElementById('login-error');
|
||||
const decoderSelect = document.getElementById('decoder-select');
|
||||
const encoderSelect = document.getElementById('encoder-select');
|
||||
const decoderLabel = document.querySelector('label[for="decoder-select"]');
|
||||
const encoderLabel = document.querySelector('label[for="encoder-select"]');
|
||||
const playerOverlay = document.getElementById('player-overlay');
|
||||
const transcodingOverlay = document.getElementById('transcoding-overlay');
|
||||
@@ -28,7 +26,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const controlPlayToggle = document.getElementById('control-play-toggle');
|
||||
const controlMuteToggle = document.getElementById('control-mute-toggle');
|
||||
const controlFullscreenToggle = document.getElementById('control-fullscreen-toggle');
|
||||
const controlSubtitleToggle = document.getElementById('control-subtitle-toggle');
|
||||
const controlSpeedToggle = document.getElementById('control-speed-toggle');
|
||||
const subtitleControl = document.getElementById('subtitle-control');
|
||||
const subtitleSelect = document.getElementById('subtitle-select');
|
||||
const volumeSlider = document.getElementById('volume-slider');
|
||||
const volumeValue = document.getElementById('volume-value');
|
||||
const playbackStatus = document.getElementById('playback-status');
|
||||
@@ -84,6 +85,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let controlsHideTimeout = null;
|
||||
let controlsHovered = false;
|
||||
let controlsPointerReleaseTimeout = null;
|
||||
let managedSubtitleTracks = [];
|
||||
let selectedSubtitleTrackId = 'off';
|
||||
|
||||
if (videoPlayer) {
|
||||
videoPlayer.controls = false;
|
||||
@@ -91,9 +94,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (playbackSpeed) {
|
||||
videoPlayer.playbackRate = parseFloat(playbackSpeed.value) || 1;
|
||||
}
|
||||
if (decoderLabel) {
|
||||
decoderLabel.textContent = '视频解码器:';
|
||||
}
|
||||
if (encoderLabel) {
|
||||
encoderLabel.textContent = '视频编码器:';
|
||||
}
|
||||
@@ -142,10 +142,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
const buildStreamUrl = (targetSeconds = null) => {
|
||||
const decoder = decoderSelect?.value || 'auto';
|
||||
const encoder = encoderSelect?.value || 'h264_rkmpp';
|
||||
const streamSessionId = createStreamSessionId();
|
||||
let streamUrl = `/api/stream?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&decoder=${encodeURIComponent(decoder)}&encoder=${encodeURIComponent(encoder)}&streamSessionId=${encodeURIComponent(streamSessionId)}`;
|
||||
let streamUrl = `/api/stream?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&decoder=auto&encoder=${encodeURIComponent(encoder)}&streamSessionId=${encodeURIComponent(streamSessionId)}`;
|
||||
if (typeof targetSeconds === 'number' && Number.isFinite(targetSeconds) && targetSeconds > 0) {
|
||||
streamUrl += `&ss=${targetSeconds}`;
|
||||
}
|
||||
@@ -204,6 +203,119 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateSubtitleControls = () => {
|
||||
if (!controlSubtitleToggle || !subtitleSelect) return;
|
||||
const selectedOption = subtitleSelect.selectedOptions?.[0];
|
||||
controlSubtitleToggle.textContent = selectedOption ? selectedOption.textContent : 'CC Off';
|
||||
};
|
||||
|
||||
const resetSubtitleTracks = () => {
|
||||
managedSubtitleTracks.forEach((track) => track.element.remove());
|
||||
managedSubtitleTracks = [];
|
||||
selectedSubtitleTrackId = 'off';
|
||||
|
||||
if (subtitleSelect) {
|
||||
subtitleSelect.innerHTML = '';
|
||||
const offOption = document.createElement('option');
|
||||
offOption.value = 'off';
|
||||
offOption.textContent = 'Off';
|
||||
subtitleSelect.appendChild(offOption);
|
||||
subtitleSelect.value = 'off';
|
||||
}
|
||||
|
||||
if (subtitleControl) {
|
||||
subtitleControl.classList.add('hidden');
|
||||
}
|
||||
|
||||
updateSubtitleControls();
|
||||
};
|
||||
|
||||
const applySelectedSubtitleTrack = () => {
|
||||
managedSubtitleTracks.forEach((track) => {
|
||||
if (track.element.track) {
|
||||
track.element.track.mode = track.id === selectedSubtitleTrackId ? 'showing' : 'disabled';
|
||||
}
|
||||
});
|
||||
updateSubtitleControls();
|
||||
};
|
||||
|
||||
const buildSubtitleUrl = (streamIndex, targetSeconds = 0) => {
|
||||
let subtitleUrl = `/api/subtitles?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&streamIndex=${encodeURIComponent(streamIndex)}`;
|
||||
if (typeof targetSeconds === 'number' && Number.isFinite(targetSeconds) && targetSeconds > 0) {
|
||||
subtitleUrl += `&ss=${targetSeconds}`;
|
||||
}
|
||||
if (s3Username) subtitleUrl += `&username=${encodeURIComponent(s3Username)}`;
|
||||
if (s3Password) subtitleUrl += `&password=${encodeURIComponent(s3Password)}`;
|
||||
return subtitleUrl;
|
||||
};
|
||||
|
||||
const loadSubtitleTracks = async (targetSeconds = 0) => {
|
||||
if (!selectedBucket || !selectedKey || !subtitleSelect) {
|
||||
resetSubtitleTracks();
|
||||
return;
|
||||
}
|
||||
|
||||
const previousSelection = selectedSubtitleTrackId;
|
||||
resetSubtitleTracks();
|
||||
|
||||
try {
|
||||
let subtitleListUrl = `/api/subtitles?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}`;
|
||||
if (s3Username) subtitleListUrl += `&username=${encodeURIComponent(s3Username)}`;
|
||||
if (s3Password) subtitleListUrl += `&password=${encodeURIComponent(s3Password)}`;
|
||||
|
||||
const res = await fetch(subtitleListUrl);
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to load subtitles');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const tracks = Array.isArray(data.tracks) ? data.tracks : [];
|
||||
if (tracks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracks.forEach((track) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(track.index);
|
||||
option.textContent = track.label || `Subtitle ${track.index}`;
|
||||
option.disabled = !track.supported;
|
||||
subtitleSelect.appendChild(option);
|
||||
|
||||
if (!track.supported) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackElement = document.createElement('track');
|
||||
trackElement.kind = 'subtitles';
|
||||
trackElement.label = track.label || `Subtitle ${track.index}`;
|
||||
if (track.language) {
|
||||
trackElement.srclang = track.language;
|
||||
}
|
||||
trackElement.src = buildSubtitleUrl(track.index, targetSeconds);
|
||||
trackElement.dataset.managedSubtitle = 'true';
|
||||
videoPlayer.appendChild(trackElement);
|
||||
managedSubtitleTracks.push({
|
||||
id: String(track.index),
|
||||
element: trackElement
|
||||
});
|
||||
});
|
||||
|
||||
if (subtitleControl) {
|
||||
subtitleControl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const nextSelection = managedSubtitleTracks.some((track) => track.id === previousSelection)
|
||||
? previousSelection
|
||||
: 'off';
|
||||
subtitleSelect.value = nextSelection;
|
||||
selectedSubtitleTrackId = nextSelection;
|
||||
applySelectedSubtitleTrack();
|
||||
} catch (error) {
|
||||
console.error('Subtitle load failed:', error);
|
||||
resetSubtitleTracks();
|
||||
}
|
||||
};
|
||||
|
||||
const updateTranscodeProgressBar = (percent = 0) => {
|
||||
if (!seekBarTranscode) return;
|
||||
const safePercent = Math.min(Math.max(Math.round(percent || 0), 0), 100);
|
||||
@@ -600,6 +712,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
isStreamActive = true;
|
||||
pendingSeekTimeout = null;
|
||||
}, 8000);
|
||||
|
||||
loadSubtitleTracks(targetSeconds).catch((error) => {
|
||||
console.error('Subtitle reload failed after seek:', error);
|
||||
});
|
||||
};
|
||||
|
||||
// --- End custom seek bar ---
|
||||
@@ -614,7 +730,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
topBanner.textContent = title;
|
||||
topBanner.classList.remove('hidden');
|
||||
document.title = title;
|
||||
populateSelect(decoderSelect, data.videoDecoders || [], data.defaultVideoDecoder || 'auto');
|
||||
populateSelect(encoderSelect, data.videoEncoders || [], data.defaultVideoEncoder || 'h264_rkmpp');
|
||||
} catch (err) {
|
||||
console.error('Config load failed:', err);
|
||||
@@ -909,6 +1024,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
playBtn.classList.add('hidden');
|
||||
}
|
||||
resetPhases();
|
||||
resetSubtitleTracks();
|
||||
|
||||
selectedKey = key;
|
||||
currentVideoKey = key;
|
||||
@@ -956,6 +1072,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateVolumeControls();
|
||||
updateFullscreenControls();
|
||||
schedulePlaybackChromeHide();
|
||||
loadSubtitleTracks(seekOffset).catch((error) => {
|
||||
console.error('Subtitle load failed:', error);
|
||||
});
|
||||
}, { once: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -984,6 +1103,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
videoPlayer.load();
|
||||
hideSeekBar();
|
||||
hideCustomControls();
|
||||
resetSubtitleTracks();
|
||||
setPlaybackStatus('Paused', 'paused');
|
||||
updatePlayControls();
|
||||
if (transcodeBtn) {
|
||||
@@ -1032,6 +1152,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateVolumeControls();
|
||||
updateFullscreenControls();
|
||||
schedulePlaybackChromeHide();
|
||||
loadSubtitleTracks(seekOffset).catch((error) => {
|
||||
console.error('Subtitle load failed:', error);
|
||||
});
|
||||
}, { once: true });
|
||||
};
|
||||
|
||||
@@ -1082,6 +1205,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (subtitleSelect) {
|
||||
subtitleSelect.addEventListener('change', (event) => {
|
||||
selectedSubtitleTrackId = event.target.value || 'off';
|
||||
applySelectedSubtitleTrack();
|
||||
revealPlaybackChrome();
|
||||
schedulePlaybackChromeHide();
|
||||
});
|
||||
}
|
||||
|
||||
if (controlFullscreenToggle) {
|
||||
controlFullscreenToggle.addEventListener('click', async () => {
|
||||
try {
|
||||
@@ -1133,6 +1265,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateVolumeControls();
|
||||
updateFullscreenControls();
|
||||
updateSpeedControls();
|
||||
resetSubtitleTracks();
|
||||
updateTranscodeProgressBar(0);
|
||||
|
||||
// Bind events
|
||||
|
||||
Reference in New Issue
Block a user