Stabilizza live YouTube/RTMP e fix crash rotazione mobile.

Pipeline YouTube con relay, overlay e publisher sync lato backend; fix GL pauseGlDuringRotation su Android per evitare crash in rotazione durante lo streaming Flutter.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-06 15:30:55 +02:00
parent 1058c644bd
commit 854738b46d
65 changed files with 2606 additions and 579 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -36,6 +36,41 @@
let pendingAction = null;
let hls = null;
let previewStarted = false;
let wasPausedPreview = false;
function hlsUrlFresh() {
const sep = cfg.hlsUrl.includes("?") ? "&" : "?";
return `${cfg.hlsUrl}${sep}_t=${Date.now()}`;
}
function hidePreviewPlaceholder() {
if (els.previewPlaceholder) els.previewPlaceholder.hidden = true;
}
function showPreviewPlaceholder(text) {
if (!els.previewPlaceholder) return;
els.previewPlaceholder.textContent = text;
els.previewPlaceholder.hidden = false;
}
function destroyPreview() {
previewStarted = false;
if (hls) {
hls.destroy();
hls = null;
}
if (els.preview) {
els.preview.removeAttribute("src");
els.preview.load();
}
}
function refreshPreviewForCover() {
if (!els.preview || !cfg.hlsUrl || cfg.streamClosed) return;
destroyPreview();
previewStarted = false;
startPreview();
}
function toast(msg) {
els.toast.textContent = msg;
@@ -169,13 +204,23 @@
return;
}
const paused = data.paused || data.status === "paused";
if (data.on_air) {
if (!previewStarted) startPreview();
else if (els.preview && els.preview.readyState >= 2) hidePreviewPlaceholder();
if (!previewStarted) {
startPreview();
} else if (paused && !wasPausedPreview) {
refreshPreviewForCover();
} else if (paused) {
if (els.preview && els.preview.readyState >= 2) hidePreviewPlaceholder();
else els.preview?.play().catch(() => {});
} else if (els.preview && els.preview.readyState >= 2) {
hidePreviewPlaceholder();
}
} else {
destroyPreview();
showPreviewPlaceholder("Anteprima in attesa del segnale…");
}
wasPausedPreview = paused;
}
async function pollStatus() {
@@ -203,7 +248,13 @@
const url = hlsUrlFresh();
if (window.Hls && Hls.isSupported()) {
hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
hls = new Hls({
lowLatencyMode: false,
maxBufferLength: 30,
maxMaxBufferLength: 60,
liveSyncDurationCount: 5,
maxLiveSyncPlaybackRate: 1.0
});
hls.loadSource(url);
hls.attachMedia(els.preview);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
@@ -283,7 +334,42 @@
document.getElementById("btn-share")?.addEventListener("click", () => shareLink());
document.getElementById("btn-copy")?.addEventListener("click", () => copyLink());
function bindPreviewStallRecovery() {
if (!els.preview) return;
let stallAt = 0;
let lastProgressAt = Date.now();
let lastTime = 0;
const markProgress = () => {
const t = els.preview.currentTime;
if (t > lastTime + 0.05) {
lastTime = t;
lastProgressAt = Date.now();
}
};
els.preview.addEventListener("timeupdate", markProgress);
els.preview.addEventListener("playing", markProgress);
const nudge = (force) => {
if (!previewStarted || cfg.streamClosed) return;
const now = Date.now();
if (!force && now - lastProgressAt < 8000) {
els.preview.play().catch(() => {});
return;
}
if (now - stallAt < 5000) return;
stallAt = now;
lastProgressAt = now;
if (hls) {
hls.startLoad(-1);
els.preview.play().catch(() => {});
}
};
setInterval(() => {
if (previewStarted && !cfg.streamClosed && Date.now() - lastProgressAt > 12000) nudge(true);
}, 4000);
}
bindPreviewEvents();
bindPreviewStallRecovery();
if (!cfg.streamClosed && cfg.hlsUrl) {
pollStatus();
} else if (cfg.streamClosed) {