引入m3u8
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user