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