diff --git a/frontend/src/components/StreamTab.vue b/frontend/src/components/StreamTab.vue index ea25595..0144b97 100644 --- a/frontend/src/components/StreamTab.vue +++ b/frontend/src/components/StreamTab.vue @@ -11,8 +11,50 @@ const video = ref(null) const zustand = ref('lade') const fehler = ref('') +// Stumm starten, sonst verweigert der Browser die automatische Wiedergabe. +// Den Ton schaltet der Zuschauer selbst ein — das gilt als Geste. +const stumm = ref(true) +const lautstaerke = ref(1) +const vollbild = ref(false) + let hls = null +// Was der Server sendet, läuft. Wer anhält, fällt aus dem Gleichlauf und +// müsste den Rückstand hinterher mit erhöhtem Tempo aufholen. Also lassen +// wir das Anhalten gar nicht erst zu. +function weiterlaufen() { + const v = video.value + if (!v || zustand.value !== 'laeuft') return + v.play().catch(() => { + // Nur wenn der Browser sich quer stellt, fragen wir den Zuschauer + zustand.value = 'bereit' + }) +} + +function tonAn() { + stumm.value = false + video.value.muted = false + video.value.volume = lautstaerke.value + weiterlaufen() +} + +function lautstaerkeSetzen(wert) { + lautstaerke.value = wert + video.value.volume = wert + if (wert > 0 && stumm.value) tonAn() +} + +function vollbildUmschalten() { + const halter = video.value?.parentElement + if (document.fullscreenElement) document.exitFullscreen() + else halter?.requestFullscreen?.() +} + +// Die Leertaste hält sonst das Video an, sobald es den Fokus hat +function tasteAbfangen(e) { + if (e.code === 'Space' && e.target === video.value) e.preventDefault() +} + function verbinden() { const quelle = '/stream/filmabend/index.m3u8' @@ -74,13 +116,23 @@ function verbinden() { } function starten() { - video.value?.play() zustand.value = 'laeuft' + video.value?.play() } -onMounted(verbinden) +function vollbildGewechselt() { + vollbild.value = !!document.fullscreenElement +} + +onMounted(() => { + verbinden() + document.addEventListener('fullscreenchange', vollbildGewechselt) + window.addEventListener('keydown', tasteAbfangen) +}) onUnmounted(() => { + document.removeEventListener('fullscreenchange', vollbildGewechselt) + window.removeEventListener('keydown', tasteAbfangen) hls?.destroy() hls = null }) @@ -92,14 +144,45 @@ onUnmounted(() => {
- +
+ + +
+ + + live + +
+

@@ -111,6 +194,10 @@ onUnmounted(() => { Wiedergabe starten + +

{{ fehler }}

@@ -127,10 +214,84 @@ onUnmounted(() => { min-width: 0; } +.rahmen { + position: relative; + /* Sonst ragt das Bild unter den Fensterrand und die Leiste mit ihm */ + max-height: calc(100vh - 5.5rem); + aspect-ratio: 16 / 9; + margin: 0 auto; + background: #000; + border-radius: 12px; +} + +/* Im Vollbild füllt der Rahmen den Schirm, das Video bleibt im Format */ +.rahmen:fullscreen { + max-height: none; + width: 100vw; + height: 100vh; + border-radius: 0; +} + +.rahmen:fullscreen .player { + border: 0; + border-radius: 0; +} + +.leiste { + position: absolute; + right: 0; + bottom: 0; + left: 0; + display: flex; + gap: 0.6rem; + align-items: center; + padding: 0.5rem 0.8rem; + opacity: 0; + transition: opacity 0.15s; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.75)); + border-radius: 0 0 10px 10px; +} + +.rahmen:hover .leiste, +.leiste:focus-within { + opacity: 1; +} + +.knopf { + padding: 0.1rem 0.3rem; + font-size: 1.05rem; + line-height: 1; +} + +.regler { + width: 7rem; + accent-color: var(--accent); +} + +.live { + margin-left: auto; + color: #fff; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.live::before { + display: inline-block; + width: 0.5rem; + height: 0.5rem; + margin-right: 0.4rem; + background: var(--accent); + border-radius: 50%; + content: ''; +} + .player { display: block; width: 100%; - aspect-ratio: 16 / 9; + height: 100%; + object-fit: contain; background: #000; border: 1px solid var(--line); border-radius: 12px; diff --git a/scripts/ton.sh b/scripts/ton.sh index f8a47d9..68d2438 100755 --- a/scripts/ton.sh +++ b/scripts/ton.sh @@ -24,6 +24,10 @@ for o in json.load(sys.stdin): " "$1" } +lautstaerke_von() { + wpctl get-volume "$1" 2>/dev/null | awk '{print $2}' +} + name_des_standards() { wpctl inspect @DEFAULT_AUDIO_SINK@ 2>/dev/null | sed -n 's/.*node\.name = "\([^"]*\)".*/\1/p' | head -1 @@ -52,15 +56,29 @@ auf) sleep 1 fi - # Damit du selbst weiter über dein gewohntes Gerät hörst + # Damit du selbst weiter über dein gewohntes Gerät hörst. + # setsid, sonst nimmt der Prozess das Ende dieser Shell mit und du + # hörst mitten im Film nichts mehr. if ! pgrep -f "node.name=filmabend-mithoeren" > /dev/null; then - nohup pw-loopback -m '[FL FR]' \ + setsid pw-loopback -m '[FL FR]' \ --capture-props="stream.capture.sink=true target.object=$SENKE node.name=filmabend-mithoeren" \ --playback-props="target.object=$ZURUECK node.name=filmabend-ausgabe" \ - > /dev/null 2>&1 & + > /dev/null 2>&1 < /dev/null & sleep 1 fi + # Dein Lautstärkeregler fasst ab jetzt die neue Senke an, nicht mehr das + # Ausgabegerät. Stünde das weiter auf halber Kraft, wäre bei "voll + # aufgedreht" Schluss bei der Hälfte. Also: Gerät auf Anschlag, und die + # neue Senke übernimmt den Pegel, den du gewohnt bist. + ZIEL_ID="$(id_von "$ZURUECK")" + if [ -n "$ZIEL_ID" ]; then + ALTER_PEGEL="$(lautstaerke_von "$ZIEL_ID")" + [ -n "$ALTER_PEGEL" ] && echo "$ALTER_PEGEL" > "$MERKER.pegel" + wpctl set-volume "$ZIEL_ID" 1.0 + [ -n "$ALTER_PEGEL" ] && wpctl set-volume "$(id_von "$SENKE")" "$ALTER_PEGEL" + fi + # Ab jetzt landet aller Ton in der Senke — und von dort bei dir und im Stream wpctl set-default "$(id_von "$SENKE")" echo "Ton läuft über die Senke Filmabend, du hörst weiter über $ZURUECK." @@ -70,8 +88,13 @@ ab) ZURUECK="$(cat "$MERKER" 2>/dev/null || true)" if [ -n "$ZURUECK" ]; then ID="$(id_von "$ZURUECK")" - [ -n "$ID" ] && wpctl set-default "$ID" + if [ -n "$ID" ]; then + wpctl set-default "$ID" + PEGEL="$(cat "$MERKER.pegel" 2>/dev/null || true)" + [ -n "$PEGEL" ] && wpctl set-volume "$ID" "$PEGEL" + fi fi + rm -f "$MERKER.pegel" pkill -f "node.name=filmabend-mithoeren" 2>/dev/null || true ID="$(id_von "$SENKE")" [ -n "$ID" ] && pw-cli destroy "$ID" > /dev/null 2>&1 || true