Rendi affidabile pausa con slate e conferma i controlli laterali.
Evita buchi HLS/YouTube in pausa (path slate ricreata, reload player, no patch recording inutili) e richiede conferma su tasti laterali app/regia. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,6 +52,18 @@ module Public
|
||||
muteError: t("regia.js.mute_error"),
|
||||
closeError: t("regia.js.close_error"),
|
||||
closeConfirm: t("regia.js.close_confirm"),
|
||||
pauseConfirmTitle: t("regia.js.pause_confirm_title"),
|
||||
pauseConfirmBody: t("regia.js.pause_confirm_body"),
|
||||
resumeConfirmTitle: t("regia.js.resume_confirm_title"),
|
||||
resumeConfirmBody: t("regia.js.resume_confirm_body"),
|
||||
muteConfirmTitle: t("regia.js.mute_confirm_title"),
|
||||
muteConfirmBody: t("regia.js.mute_confirm_body"),
|
||||
unmuteConfirmTitle: t("regia.js.unmute_confirm_title"),
|
||||
unmuteConfirmBody: t("regia.js.unmute_confirm_body"),
|
||||
advancePeriodTitle: t("regia.js.advance_period_title"),
|
||||
advancePeriodBody: t("regia.js.advance_period_body"),
|
||||
confirmAction: t("regia.modal.confirm_action"),
|
||||
stopConfirmTitle: t("regia.js.stop_confirm_title"),
|
||||
linkUnavailable: t("regia.js.link_unavailable"),
|
||||
linkCopied: t("regia.js.link_copied"),
|
||||
copyPrompt: t("regia.js.copy_prompt"),
|
||||
|
||||
@@ -74,7 +74,48 @@ module Mediamtx
|
||||
set_always_available(session, enabled: true)
|
||||
end
|
||||
|
||||
# Slate alwaysAvailable: copertina sullo stesso path quando il telefono è offline.
|
||||
# Garantisce path config + slate: se MediaMTX ha perso la config (restart → all_others),
|
||||
# in pausa non resterebbe nessuno stream verso HLS/YouTube.
|
||||
def ensure_path_with_slate!(session)
|
||||
path = session.mediamtx_path_name
|
||||
response = @conn.get("/v3/config/paths/get/#{CGI.escape(path)}")
|
||||
missing = response.status == 404 ||
|
||||
(response.body.is_a?(Hash) && response.body["error"].present?)
|
||||
|
||||
if missing
|
||||
forget_always_available(path)
|
||||
begin
|
||||
create_path(session)
|
||||
rescue Error
|
||||
# Path creato in parallelo o già presente: forza solo la slate.
|
||||
forget_always_available(path)
|
||||
set_always_available(session, enabled: true)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
conf = response.body.is_a?(Hash) ? response.body : {}
|
||||
desired = slate_file_path(session)
|
||||
if conf["alwaysAvailable"] == true && conf["alwaysAvailableFile"].to_s == desired.to_s
|
||||
remember_always_available(path, enabled: true)
|
||||
return true
|
||||
end
|
||||
|
||||
forget_always_available(path)
|
||||
set_always_available(session, enabled: true)
|
||||
end
|
||||
|
||||
# Spegnere record solo se attivo: ogni PATCH path ricarica MediaMTX e distrugge i muxer HLS.
|
||||
def disable_recording_if_active!(session)
|
||||
path = session.mediamtx_path_name
|
||||
response = @conn.get("/v3/config/paths/get/#{CGI.escape(path)}")
|
||||
return true unless response.success?
|
||||
return true unless response.body.is_a?(Hash) && response.body["record"] == true
|
||||
|
||||
set_path_recording(session, enabled: false)
|
||||
end
|
||||
|
||||
# Slate alwaysAvailable: copertina sullo stesso path quando il telefono è offline.
|
||||
# Resta accesa anche con publisher in onda (MediaMTX usa il publisher se presente).
|
||||
def set_always_available(session, enabled:)
|
||||
path = session.mediamtx_path_name
|
||||
|
||||
@@ -79,9 +79,8 @@ module Mediamtx
|
||||
end
|
||||
|
||||
def restore_slate_path!(session)
|
||||
return if session.platform == "matchlivetv"
|
||||
|
||||
Client.for_session(session).set_always_available(session, enabled: true)
|
||||
# Anche su matchlivetv: dopo restart MediaMTX la path può cadere su all_others senza slate.
|
||||
Client.for_session(session).ensure_path_with_slate!(session)
|
||||
rescue Client::Error => e
|
||||
Rails.logger.warn("[PublisherSync] enable slate session=#{session.id}: #{e.message}")
|
||||
end
|
||||
@@ -146,6 +145,14 @@ module Mediamtx
|
||||
nil
|
||||
end
|
||||
|
||||
def self.remember_recording_off!(session_id)
|
||||
redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
|
||||
redis.set("mediamtx:recording:#{session_id}", "0", ex: 48.hours.to_i)
|
||||
redis.set(format("mediamtx:recording:patched_at:%s", session_id), Time.current.to_i, ex: 48.hours.to_i)
|
||||
rescue Redis::BaseError
|
||||
nil
|
||||
end
|
||||
|
||||
def recording_state_key(session_id)
|
||||
"mediamtx:recording:#{session_id}"
|
||||
end
|
||||
|
||||
@@ -7,11 +7,13 @@ module Sessions
|
||||
def call
|
||||
cancel_timeout_job
|
||||
@session.pause! if @session.may_pause?
|
||||
Mediamtx::PublisherSync.forget_recording_state!(@session.id)
|
||||
begin
|
||||
mtx = Mediamtx::Client.for_session(@session)
|
||||
mtx.set_path_recording(@session, enabled: false)
|
||||
mtx.set_always_available(@session, enabled: true)
|
||||
# Ricrea/riallinea slate prima di spegnere l'RTMP del telefono.
|
||||
mtx.ensure_path_with_slate!(@session)
|
||||
# PATCH recording solo se era attivo (evita kill muxer HLS a ogni pausa).
|
||||
mtx.disable_recording_if_active!(@session)
|
||||
Mediamtx::PublisherSync.remember_recording_off!(@session.id)
|
||||
rescue Mediamtx::Client::Error => e
|
||||
Rails.logger.warn("[Sessions::Pause] MediaMTX: #{e.message}")
|
||||
end
|
||||
|
||||
@@ -4,10 +4,15 @@ module Streams
|
||||
ENV.fetch("MEDIAMTX_SLATES_CUSTOM_DIR", "/slates/custom")
|
||||
end
|
||||
|
||||
# ActiveStorage checksum is base64 e può contenere "/", "+", "=" — non usabile come path.
|
||||
def self.safe_digest(checksum)
|
||||
checksum.to_s.tr("+/", "-_").delete("=")
|
||||
end
|
||||
|
||||
def self.filename_for(record)
|
||||
return nil unless record.cover_slate.attached?
|
||||
|
||||
digest = record.cover_slate.blob.checksum
|
||||
digest = safe_digest(record.cover_slate.blob.checksum)
|
||||
prefix = record.class.name.underscore
|
||||
"#{prefix}-#{record.id}-#{digest}.mp4"
|
||||
end
|
||||
|
||||
@@ -31,8 +31,11 @@ module Streams
|
||||
end
|
||||
|
||||
host_path = result.slate_local_path
|
||||
filename = File.basename(host_path)
|
||||
mediamtx_path = "#{CoverSlatePaths.slates_custom_root}/#{filename}"
|
||||
filename = CoverSlatePaths.filename_for(result.record)
|
||||
raise Error, "slate filename missing" if filename.blank?
|
||||
|
||||
# Path completo (non File.basename): il checksum AS può contenere "/" e spezzerebbe il path.
|
||||
mediamtx_path = CoverSlatePaths.expected_path(result.record)
|
||||
|
||||
if cloud_node?
|
||||
push_to_agent!(filename, result.record, host_path)
|
||||
|
||||
@@ -219,7 +219,8 @@
|
||||
}
|
||||
|
||||
function onEnterPause() {
|
||||
nudgePlayback();
|
||||
// Camera → slate cambia risoluzione/codec: senza reload HLS.js resta spesso bloccato.
|
||||
hardReloadPlayer();
|
||||
}
|
||||
|
||||
function hardReloadPlayer() {
|
||||
|
||||
@@ -165,4 +165,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/regia.js?v=19"></script>
|
||||
<script src="/regia.js?v=20"></script>
|
||||
|
||||
@@ -21,7 +21,7 @@ module MatchLiveTv
|
||||
end
|
||||
|
||||
def hls_public_url
|
||||
ENV.fetch("HLS_PUBLIC_URL", "http://localhost:8888")
|
||||
ENV.fetch("HLS_PUBLIC_URL") { "#{app_public_url.chomp('/')}/hls" }
|
||||
end
|
||||
|
||||
def mediamtx_hls_url
|
||||
|
||||
@@ -57,7 +57,7 @@ de:
|
||||
username_label: Benutzername
|
||||
password_label: Passwort
|
||||
submit: Anmelden
|
||||
initial_credentials_html: "Anfangs-Zugangsdaten: <code>admin</code> / <code>admin</code>. Ändere das Passwort nach der ersten Anmeldung."
|
||||
initial_credentials_html: "Anfangs-Zugangsdaten: <code>admin</code> / <code>AdminPass123</code>. Ändere das Passwort nach der ersten Anmeldung."
|
||||
passwords:
|
||||
edit:
|
||||
title: Passwort ändern
|
||||
|
||||
@@ -57,7 +57,7 @@ en:
|
||||
username_label: Username
|
||||
password_label: Password
|
||||
submit: Log in
|
||||
initial_credentials_html: "Initial credentials: <code>admin</code> / <code>admin</code>. Change the password after the first login."
|
||||
initial_credentials_html: "Initial credentials: <code>admin</code> / <code>AdminPass123</code>. Change the password after the first login."
|
||||
passwords:
|
||||
edit:
|
||||
title: Change password
|
||||
|
||||
@@ -57,7 +57,7 @@ es:
|
||||
username_label: Usuario
|
||||
password_label: Contraseña
|
||||
submit: Acceder
|
||||
initial_credentials_html: "Credenciales iniciales: <code>admin</code> / <code>admin</code>. Cambia la contraseña después del primer acceso."
|
||||
initial_credentials_html: "Credenciales iniciales: <code>admin</code> / <code>AdminPass123</code>. Cambia la contraseña después del primer acceso."
|
||||
passwords:
|
||||
edit:
|
||||
title: Cambiar contraseña
|
||||
|
||||
@@ -57,7 +57,7 @@ fr:
|
||||
username_label: Nom d'utilisateur
|
||||
password_label: Mot de passe
|
||||
submit: Se connecter
|
||||
initial_credentials_html: "Identifiants initiaux : <code>admin</code> / <code>admin</code>. Changez le mot de passe après la première connexion."
|
||||
initial_credentials_html: "Identifiants initiaux : <code>admin</code> / <code>AdminPass123</code>. Changez le mot de passe après la première connexion."
|
||||
passwords:
|
||||
edit:
|
||||
title: Changer le mot de passe
|
||||
|
||||
@@ -61,7 +61,7 @@ it:
|
||||
username_label: Username
|
||||
password_label: Password
|
||||
submit: Accedi
|
||||
initial_credentials_html: "Credenziali iniziali: <code>admin</code> / <code>admin</code>. Cambia la password dopo il primo accesso."
|
||||
initial_credentials_html: "Credenziali iniziali: <code>admin</code> / <code>AdminPass123</code>. Cambia la password dopo il primo accesso."
|
||||
passwords:
|
||||
edit:
|
||||
title: Cambia password
|
||||
|
||||
@@ -491,6 +491,7 @@ de:
|
||||
modal:
|
||||
set_won_title: Satz gewonnen
|
||||
confirm: Satz beenden
|
||||
confirm_action: Bestätigen
|
||||
cancel: Abbrechen
|
||||
board:
|
||||
next_period: Nächstes Viertel
|
||||
@@ -530,6 +531,17 @@ de:
|
||||
mute_error: Fehler beim Stummschalten
|
||||
close_error: Fehler beim Beenden
|
||||
close_confirm: Übertragung endgültig beenden?
|
||||
pause_confirm_title: Livestream pausieren?
|
||||
pause_confirm_body: Das Cover bleibt on air, bis du fortsetzt.
|
||||
resume_confirm_title: Livestream fortsetzen?
|
||||
resume_confirm_body: Die Kamera geht wieder on air statt des Covers.
|
||||
mute_confirm_title: Audio stummschalten?
|
||||
mute_confirm_body: Das Mikrofon wird nicht mehr an den Stream gesendet, bis du es wieder aktivierst.
|
||||
unmute_confirm_title: Audio wieder aktivieren?
|
||||
unmute_confirm_body: Das Mikrofon ist wieder aktiv im Stream.
|
||||
advance_period_title: Zum nächsten Abschnitt wechseln?
|
||||
advance_period_body: Die Anzeigetafel wechselt zum nächsten Abschnitt und der aktuelle Stand wird gespeichert.
|
||||
stop_confirm_title: Livestream beenden?
|
||||
link_unavailable: Link nicht verfügbar
|
||||
link_copied: Link kopiert
|
||||
copy_prompt: "Link kopieren:"
|
||||
|
||||
@@ -494,6 +494,7 @@ en:
|
||||
modal:
|
||||
set_won_title: Set won
|
||||
confirm: Close set
|
||||
confirm_action: Confirm
|
||||
cancel: Cancel
|
||||
board:
|
||||
next_period: Next quarter
|
||||
@@ -533,6 +534,17 @@ en:
|
||||
mute_error: Error muting audio
|
||||
close_error: Error closing
|
||||
close_confirm: Permanently close the live stream?
|
||||
pause_confirm_title: Pause the live stream?
|
||||
pause_confirm_body: The cover slate will stay on air until you resume.
|
||||
resume_confirm_title: Resume the live stream?
|
||||
resume_confirm_body: The camera will go back on air instead of the cover slate.
|
||||
mute_confirm_title: Mute audio?
|
||||
mute_confirm_body: The microphone will not be sent to the stream until you unmute.
|
||||
unmute_confirm_title: Unmute audio?
|
||||
unmute_confirm_body: The microphone will be active on the stream again.
|
||||
advance_period_title: Advance to the next period?
|
||||
advance_period_body: The scoreboard will move to the next period and the current period score will be saved.
|
||||
stop_confirm_title: End the live stream?
|
||||
link_unavailable: Link not available
|
||||
link_copied: Link copied
|
||||
copy_prompt: "Copy the link:"
|
||||
|
||||
@@ -491,6 +491,7 @@ es:
|
||||
modal:
|
||||
set_won_title: Set ganado
|
||||
confirm: Cerrar set
|
||||
confirm_action: Confirmar
|
||||
cancel: Cancelar
|
||||
board:
|
||||
next_period: Siguiente cuarto
|
||||
@@ -530,6 +531,17 @@ es:
|
||||
mute_error: Error al silenciar el audio
|
||||
close_error: Error al cerrar
|
||||
close_confirm: "¿Cerrar definitivamente la retransmisión?"
|
||||
pause_confirm_title: "¿Pausar el directo?"
|
||||
pause_confirm_body: La portada seguirá en antena hasta que lo reanudes.
|
||||
resume_confirm_title: "¿Reanudar el directo?"
|
||||
resume_confirm_body: La cámara volverá a antena en lugar de la portada.
|
||||
mute_confirm_title: "¿Silenciar el audio?"
|
||||
mute_confirm_body: El micrófono no se enviará al stream hasta que lo reactives.
|
||||
unmute_confirm_title: "¿Reactivar el audio?"
|
||||
unmute_confirm_body: El micrófono volverá a estar activo en el stream.
|
||||
advance_period_title: "¿Pasar al periodo siguiente?"
|
||||
advance_period_body: El marcador pasará al siguiente periodo y se guardará el marcador del periodo actual.
|
||||
stop_confirm_title: "¿Terminar el directo?"
|
||||
link_unavailable: Enlace no disponible
|
||||
link_copied: Enlace copiado
|
||||
copy_prompt: "Copia el enlace:"
|
||||
|
||||
@@ -491,6 +491,7 @@ fr:
|
||||
modal:
|
||||
set_won_title: Set gagné
|
||||
confirm: Terminer le set
|
||||
confirm_action: Confirmer
|
||||
cancel: Annuler
|
||||
board:
|
||||
next_period: Quart-temps suivant
|
||||
@@ -530,6 +531,17 @@ fr:
|
||||
mute_error: Erreur lors de la coupure audio
|
||||
close_error: Erreur de clôture
|
||||
close_confirm: Clôturer définitivement le direct ?
|
||||
pause_confirm_title: Mettre le direct en pause ?
|
||||
pause_confirm_body: La couverture restera à l'antenne jusqu'à la reprise.
|
||||
resume_confirm_title: Reprendre le direct ?
|
||||
resume_confirm_body: La caméra reviendra à l'antenne à la place de la couverture.
|
||||
mute_confirm_title: Couper l'audio ?
|
||||
mute_confirm_body: Le micro ne sera plus envoyé au flux tant que vous ne le réactivez pas.
|
||||
unmute_confirm_title: Réactiver l'audio ?
|
||||
unmute_confirm_body: Le micro sera à nouveau actif sur le flux.
|
||||
advance_period_title: Passer à la période suivante ?
|
||||
advance_period_body: Le tableau passera à la période suivante et le score de la période en cours sera enregistré.
|
||||
stop_confirm_title: Terminer le direct ?
|
||||
link_unavailable: Lien non disponible
|
||||
link_copied: Lien copié
|
||||
copy_prompt: "Copiez le lien :"
|
||||
|
||||
@@ -515,6 +515,7 @@ it:
|
||||
modal:
|
||||
set_won_title: Set vinto
|
||||
confirm: Chiudi set
|
||||
confirm_action: Conferma
|
||||
cancel: Annulla
|
||||
board:
|
||||
next_period: Prossimo quarto
|
||||
@@ -554,6 +555,17 @@ it:
|
||||
mute_error: Errore silenziamento audio
|
||||
close_error: Errore chiusura
|
||||
close_confirm: Chiudere definitivamente la diretta?
|
||||
pause_confirm_title: Mettere in pausa?
|
||||
pause_confirm_body: La diretta passerà alla copertina finché non la riprendi.
|
||||
resume_confirm_title: Riprendere la diretta?
|
||||
resume_confirm_body: La telecamera tornerà in onda al posto della copertina.
|
||||
mute_confirm_title: Silenziare l'audio?
|
||||
mute_confirm_body: Il microfono non verrà inviato allo stream finché non lo riattivi.
|
||||
unmute_confirm_title: Riattivare l'audio?
|
||||
unmute_confirm_body: Il microfono tornerà attivo sullo stream.
|
||||
advance_period_title: Passare al periodo successivo?
|
||||
advance_period_body: Il tabellone passerà al periodo successivo e il punteggio del periodo corrente sarà salvato.
|
||||
stop_confirm_title: Terminare la diretta?
|
||||
link_unavailable: Link non disponibile
|
||||
link_copied: Link copiato
|
||||
copy_prompt: "Copia il link:"
|
||||
|
||||
+72
-13
@@ -33,6 +33,18 @@
|
||||
muteError: "Errore silenziamento audio",
|
||||
closeError: "Errore chiusura",
|
||||
closeConfirm: "Chiudere definitivamente la diretta?",
|
||||
pauseConfirmTitle: "Mettere in pausa?",
|
||||
pauseConfirmBody: "La diretta passerà alla copertina finché non la riprendi.",
|
||||
resumeConfirmTitle: "Riprendere la diretta?",
|
||||
resumeConfirmBody: "La telecamera tornerà in onda al posto della copertina.",
|
||||
muteConfirmTitle: "Silenziare l'audio?",
|
||||
muteConfirmBody: "Il microfono non verrà inviato allo stream finché non lo riattivi.",
|
||||
unmuteConfirmTitle: "Riattivare l'audio?",
|
||||
unmuteConfirmBody: "Il microfono tornerà attivo sullo stream.",
|
||||
advancePeriodTitle: "Passare al periodo successivo?",
|
||||
advancePeriodBody: "Il tabellone passerà al periodo successivo e il punteggio del periodo corrente sarà salvato.",
|
||||
confirmAction: "Conferma",
|
||||
stopConfirmTitle: "Terminare la diretta?",
|
||||
linkUnavailable: "Link non disponibile",
|
||||
linkCopied: "Link copiato",
|
||||
copyPrompt: "Copia il link:",
|
||||
@@ -389,13 +401,18 @@
|
||||
if (cfg.board === "volley" || cfg.board === "racket") {
|
||||
if (data.set_won && action !== "close_set") {
|
||||
const winner = data.winner === "home" ? cfg.homeName : cfg.awayName;
|
||||
openModal(I18N.setWonTitle, fillTemplate(I18N.setWonBodyTemplate, { winner }), () => postScore("close_set"));
|
||||
openModal(I18N.setWonTitle, fillTemplate(I18N.setWonBodyTemplate, { winner }), () => postScore("close_set"), I18N.closeSetTitle);
|
||||
}
|
||||
}
|
||||
if (data.match_won) {
|
||||
const winner = data.winner === "home" ? cfg.homeName : cfg.awayName;
|
||||
openModal(I18N.matchWonTitle, fillTemplate(I18N.matchWonBodyTemplate, { winner }), () => stopStream());
|
||||
}
|
||||
if (data.match_won) {
|
||||
const winner = data.winner === "home" ? cfg.homeName : cfg.awayName;
|
||||
openModal(
|
||||
I18N.matchWonTitle,
|
||||
fillTemplate(I18N.matchWonBodyTemplate, { winner }),
|
||||
() => performStopStream(),
|
||||
I18N.confirmAction
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (prevHome != null && els.homePoints) els.homePoints.textContent = prevHome;
|
||||
const awayEl = document.getElementById("away-points");
|
||||
@@ -406,9 +423,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(title, body, onConfirm) {
|
||||
function openModal(title, body, onConfirm, confirmLabel) {
|
||||
els.modalTitle.textContent = title;
|
||||
els.modalBody.textContent = body;
|
||||
if (els.modalConfirm) {
|
||||
els.modalConfirm.textContent = confirmLabel || I18N.confirmAction;
|
||||
}
|
||||
els.modal.classList.add("open");
|
||||
pendingAction = onConfirm;
|
||||
}
|
||||
@@ -440,7 +460,9 @@
|
||||
btn.addEventListener("click", () => {
|
||||
const action = btn.dataset.action;
|
||||
if (action === "close_set") {
|
||||
openModal(I18N.closeSetTitle, I18N.closeSetConfirm, () => postScore("close_set"));
|
||||
openModal(I18N.closeSetTitle, I18N.closeSetConfirm, () => postScore("close_set"), I18N.closeSetTitle);
|
||||
} else if (action === "advance_period") {
|
||||
openModal(I18N.advancePeriodTitle, I18N.advancePeriodBody, () => handleAction("advance_period"));
|
||||
} else {
|
||||
handleAction(action);
|
||||
}
|
||||
@@ -689,9 +711,8 @@
|
||||
toast(audioMuted ? I18N.muted : I18N.unmuted);
|
||||
}
|
||||
|
||||
async function stopStream() {
|
||||
const ok = confirm(I18N.closeConfirm);
|
||||
if (!ok) return null;
|
||||
async function performStopStream() {
|
||||
if (!cfg.stopUrl) return null;
|
||||
const res = await fetch(cfg.stopUrl, { method: "POST", headers: { Accept: "application/json" } });
|
||||
if (!res.ok) throw new Error(I18N.closeError);
|
||||
const data = await res.json();
|
||||
@@ -701,9 +722,47 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
els.btnPause?.addEventListener("click", () => togglePauseStream().catch((e) => toast(e.message)));
|
||||
els.btnAudioMute?.addEventListener("click", () => toggleMuteAudio().catch((e) => toast(e.message)));
|
||||
document.getElementById("btn-stop")?.addEventListener("click", () => stopStream().catch((e) => toast(e.message)));
|
||||
function requestStopStream() {
|
||||
openModal(I18N.stopConfirmTitle, I18N.closeConfirm, () => performStopStream(), I18N.confirmAction);
|
||||
}
|
||||
|
||||
function requestPauseToggle() {
|
||||
if (streamPaused) {
|
||||
openModal(I18N.resumeConfirmTitle, I18N.resumeConfirmBody, async () => {
|
||||
await togglePauseStream();
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
openModal(I18N.pauseConfirmTitle, I18N.pauseConfirmBody, async () => {
|
||||
await togglePauseStream();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function requestMuteToggle() {
|
||||
if (audioMuted) {
|
||||
openModal(I18N.unmuteConfirmTitle, I18N.unmuteConfirmBody, async () => {
|
||||
await toggleMuteAudio();
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
openModal(I18N.muteConfirmTitle, I18N.muteConfirmBody, async () => {
|
||||
await toggleMuteAudio();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function stopStream() {
|
||||
// Compatibilità: conferma via modal (non window.confirm).
|
||||
requestStopStream();
|
||||
return null;
|
||||
}
|
||||
|
||||
els.btnPause?.addEventListener("click", () => requestPauseToggle());
|
||||
els.btnAudioMute?.addEventListener("click", () => requestMuteToggle());
|
||||
document.getElementById("btn-stop")?.addEventListener("click", () => requestStopStream());
|
||||
|
||||
let minQualityBusy = false;
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ RSpec.describe Mediamtx::PublisherSync do
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active?).and_return(true)
|
||||
allow_any_instance_of(described_class).to receive(:redis).and_return(redis)
|
||||
allow(client).to receive(:set_path_recording)
|
||||
allow(client).to receive(:ensure_path_with_slate!)
|
||||
allow(client).to receive(:set_always_available)
|
||||
end
|
||||
|
||||
it "abilita la registrazione quando il publisher è online e la sessione è live" do
|
||||
|
||||
@@ -43,7 +43,7 @@ RSpec.describe Streams::SlateDistributor do
|
||||
|
||||
it "usa path custom su nodo home quando la slate è su disco" do
|
||||
attach_ready_cover(club)
|
||||
custom = "/slates/custom/#{File.basename(Streams::CoverSlatePaths.expected_path(club))}"
|
||||
custom = Streams::CoverSlatePaths.expected_path(club)
|
||||
expect(described_class.slate_path_for_session(session)).to eq(custom)
|
||||
end
|
||||
|
||||
@@ -72,9 +72,8 @@ RSpec.describe Streams::SlateDistributor do
|
||||
allow(http).to receive(:read_timeout=)
|
||||
allow(http).to receive(:request).and_return(response)
|
||||
|
||||
filename = File.basename(Streams::CoverSlatePaths.expected_path(club))
|
||||
path = described_class.slate_path_for_session(session)
|
||||
expect(path).to eq("/slates/custom/#{filename}")
|
||||
expect(path).to eq(Streams::CoverSlatePaths.expected_path(club))
|
||||
expect(http).to have_received(:request)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user