引入m3u8

This commit is contained in:
CN-JS-HuiBai
2026-04-04 01:02:24 +08:00
parent f3733ef8ef
commit b3f5053482
3 changed files with 219 additions and 109 deletions

View File

@@ -126,6 +126,7 @@
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script src="/js/main.js"></script>
</body>
</html>

View File

@@ -61,6 +61,7 @@ document.addEventListener('DOMContentLoaded', () => {
const seekTotalTime = document.getElementById('seek-total-time');
const seekingOverlay = document.getElementById('seeking-overlay');
let hlsInstance = null;
let currentPollInterval = null;
let selectedBucket = null;
let selectedKey = null;
@@ -141,14 +142,10 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
const buildStreamUrl = (targetSeconds = null) => {
const buildHlsPlaylistUrl = () => {
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)}`;
if (typeof targetSeconds === 'number' && Number.isFinite(targetSeconds) && targetSeconds > 0) {
streamUrl += `&ss=${targetSeconds}`;
}
let streamUrl = `/api/hls/playlist.m3u8?bucket=${encodeURIComponent(selectedBucket)}&key=${encodeURIComponent(selectedKey)}&decoder=${encodeURIComponent(decoder)}&encoder=${encodeURIComponent(encoder)}`;
if (s3Username) streamUrl += `&username=${encodeURIComponent(s3Username)}`;
if (s3Password) streamUrl += `&password=${encodeURIComponent(s3Password)}`;
return streamUrl;
@@ -429,25 +426,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Browsers usually cannot seek inside a live fragmented MP4 stream.
// When the user drags the native video controls, remap that action to our server-side seek flow.
videoPlayer.addEventListener('seeking', () => {
if (internalSeeking || !isStreamActive || videoDuration <= 0) return;
const requestedAbsoluteTime = Math.max(0, Math.min(seekOffset + (videoPlayer.currentTime || 0), videoDuration - 0.5));
const drift = Math.abs(requestedAbsoluteTime - lastAbsolutePlaybackTime);
if (drift < 1) {
return;
}
internalSeeking = true;
setPlaybackStatus('Seeking', 'seeking');
seekToTime(requestedAbsoluteTime);
setTimeout(() => {
internalSeeking = false;
}, 500);
});
// Seek bar interaction: click or drag
const getSeekRatio = (e) => {
@@ -549,49 +528,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
const seekToTime = (targetSeconds) => {
if (!selectedKey || !selectedBucket || videoDuration <= 0) return;
targetSeconds = Math.max(0, Math.min(targetSeconds, videoDuration - 0.5));
seekOffset = targetSeconds;
isStreamActive = false;
lastAbsolutePlaybackTime = targetSeconds;
updateSeekBarPosition(targetSeconds);
// Show seeking indicator
if (seekingOverlay) seekingOverlay.classList.remove('hidden');
setPlaybackStatus('Seeking', 'seeking');
revealPlaybackChrome();
if (pendingSeekTimeout) {
clearTimeout(pendingSeekTimeout);
pendingSeekTimeout = null;
}
// Build new stream URL with ss parameter
const streamUrl = buildStreamUrl(targetSeconds);
// Changing src automatically aborts the previous HTTP request,
// which triggers res.on('close') on the server, killing the old ffmpeg process
videoPlayer.src = streamUrl;
videoPlayer.load();
const onCanPlay = () => {
if (seekingOverlay) seekingOverlay.classList.add('hidden');
isStreamActive = true;
lastAbsolutePlaybackTime = targetSeconds;
videoPlayer.play().catch(() => {});
updatePlayControls();
schedulePlaybackChromeHide();
videoPlayer.removeEventListener('canplay', onCanPlay);
};
videoPlayer.addEventListener('canplay', onCanPlay, { once: true });
// Timeout fallback: hide seeking overlay after 8s even if canplay doesn't fire
pendingSeekTimeout = setTimeout(() => {
if (seekingOverlay) seekingOverlay.classList.add('hidden');
isStreamActive = true;
pendingSeekTimeout = null;
}, 8000);
};
// --- End custom seek bar ---
@@ -875,6 +812,10 @@ document.addEventListener('DOMContentLoaded', () => {
videoPlayer.classList.add('hidden');
videoPlayer.pause();
videoPlayer.removeAttribute('src');
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
}
videoPlayer.load();
isStreamActive = false;
videoDuration = 0;
@@ -932,22 +873,57 @@ document.addEventListener('DOMContentLoaded', () => {
try {
if (!selectedBucket) throw new Error('No bucket selected');
const streamUrl = buildStreamUrl();
videoPlayer.src = streamUrl;
videoPlayer.load();
videoPlayer.addEventListener('loadedmetadata', () => {
transcodingOverlay.classList.add('hidden');
videoPlayer.classList.remove('hidden');
isStreamActive = true;
lastAbsolutePlaybackTime = seekOffset;
showSeekBar();
showCustomControls();
updateSeekBarPosition(seekOffset);
updatePlayControls();
updateVolumeControls();
updateFullscreenControls();
schedulePlaybackChromeHide();
}, { once: true });
const streamUrl = buildHlsPlaylistUrl();
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
}
if (window.Hls && Hls.isSupported()) {
hlsInstance = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60 });
hlsInstance.loadSource(streamUrl);
hlsInstance.attachMedia(videoPlayer);
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
transcodingOverlay.classList.add('hidden');
videoPlayer.classList.remove('hidden');
isStreamActive = true;
videoPlayer.play().catch(()=>{});
showSeekBar();
showCustomControls();
updatePlayControls();
updateVolumeControls();
updateFullscreenControls();
schedulePlaybackChromeHide();
});
hlsInstance.on(Hls.Events.ERROR, function (event, data) {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
hlsInstance.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
hlsInstance.recoverMediaError();
break;
default:
hlsInstance.destroy();
break;
}
}
});
} else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {
videoPlayer.src = streamUrl;
videoPlayer.addEventListener('loadedmetadata', () => {
transcodingOverlay.classList.add('hidden');
videoPlayer.classList.remove('hidden');
isStreamActive = true;
videoPlayer.play().catch(()=>{});
showSeekBar();
showCustomControls();
updatePlayControls();
updateVolumeControls();
updateFullscreenControls();
schedulePlaybackChromeHide();
}, { once: true });
}
} catch (err) {
console.error(err);
transcodingOverlay.innerHTML = `<p style="color: #ef4444;">Transcode Failed: ${err.message}</p>`;
@@ -969,6 +945,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (!res.ok) throw new Error(data.error || 'Failed to stop transcode');
handleProgress({ status: 'cancelled', percent: 0, details: 'Transcode stopped' });
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
}
isStreamActive = false;
videoPlayer.pause();
videoPlayer.removeAttribute('src');
@@ -1091,34 +1071,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('fullscreenchange', updateFullscreenControls);
document.addEventListener('keydown', (event) => {
if (!isStreamActive) return;
const tagName = event.target?.tagName;
if (tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA' || tagName === 'BUTTON') return;
if (event.code === 'Space') {
event.preventDefault();
if (videoPlayer.paused) {
videoPlayer.play().catch(() => {});
} else {
videoPlayer.pause();
}
revealPlaybackChrome();
schedulePlaybackChromeHide();
return;
}
if (event.code === 'ArrowRight') {
event.preventDefault();
seekToTime((seekOffset + (videoPlayer.currentTime || 0)) + 5);
return;
}
if (event.code === 'ArrowLeft') {
event.preventDefault();
seekToTime((seekOffset + (videoPlayer.currentTime || 0)) - 5);
}
});
updatePlayControls();
updateVolumeControls();