This commit is contained in:
team3
2026-08-31 11:54:20 +02:00
parent f29a4106bb
commit e7216138c7
21 changed files with 1517 additions and 87 deletions

View File

@@ -0,0 +1,180 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import Hls from 'hls.js'
// Alle Zuschauer regeln auf dieselbe Distanz zur Live-Kante. Damit laufen
// sie synchron, ohne dass ein Server sie taktet. hls.js passt dafür die
// Abspielgeschwindigkeit an (maxLiveSyncPlaybackRate).
const ZIEL_LATENZ = 4
const video = ref(null)
const zustand = ref('lade')
const fehler = ref('')
let hls = null
function verbinden() {
const quelle = '/stream/filmabend/index.m3u8'
if (!Hls.isSupported()) {
// Safari spielt HLS selbst ab, dann übernimmt der Browser die Regelung
if (video.value.canPlayType('application/vnd.apple.mpegurl')) {
video.value.src = quelle
zustand.value = 'laeuft'
return
}
fehler.value = 'Dieser Browser kann den Stream nicht abspielen.'
zustand.value = 'fehler'
return
}
hls = new Hls({
lowLatencyMode: true,
liveSyncDuration: ZIEL_LATENZ,
// Ab hier springt hls.js selbst auf die Zielposition zurück, statt
// ewig mit 1,5-fachem Tempo aufzuholen. Zu dicht an ZIEL_LATENZ
// führt laut Doku zu Stockern.
liveMaxLatencyDuration: ZIEL_LATENZ + 3,
// Über 1 erlaubt hls.js, die Geschwindigkeit anzuheben, um die
// Ziel-Latenz zu halten. Standard ist 1, also aus.
maxLiveSyncPlaybackRate: 1.5,
backBufferLength: 30,
})
hls.on(Hls.Events.ERROR, (_, daten) => {
if (!daten.fatal) return
if (daten.type === Hls.ErrorTypes.NETWORK_ERROR) {
zustand.value = 'wartet'
// Nicht sofort neu anfragen — sonst hämmert der Browser den Server
window.setTimeout(() => hls?.startLoad(), 3000)
} else if (daten.type === Hls.ErrorTypes.MEDIA_ERROR) {
hls.recoverMediaError()
} else {
fehler.value = 'Stream nicht erreichbar.'
zustand.value = 'fehler'
}
})
hls.on(Hls.Events.MANIFEST_PARSED, () => {
zustand.value = 'laeuft'
video.value.play().catch(() => {
// Autoplay ohne Ton ist erlaubt, mit Ton nicht — dann wartet es
// auf einen Klick
zustand.value = 'bereit'
})
})
hls.loadSource(quelle)
hls.attachMedia(video.value)
// Für Diagnose im Betrieb: Latenzwerte in der Konsole nachsehen
window.__stream = hls
}
function starten() {
video.value?.play()
zustand.value = 'laeuft'
}
onMounted(verbinden)
onUnmounted(() => {
hls?.destroy()
hls = null
})
</script>
<template>
<div class="stream-tab">
<div class="buehne">
<!-- Ohne Stream zeigt der Browser nur seinen eigenen Ladekringel und
verrät nicht, warum nichts kommt -->
<video
v-show="zustand === 'laeuft' || zustand === 'bereit'"
ref="video"
class="player"
controls
playsinline
muted
></video>
<div v-if="zustand === 'wartet' || zustand === 'lade'" class="platzhalter">
<p class="gross">
{{ zustand === 'lade' ? 'Verbinde…' : 'Es wird gerade nichts gesendet.' }}
</p>
</div>
<button v-if="zustand === 'bereit'" class="start" type="button" @click="starten">
Wiedergabe starten
</button>
<p v-if="fehler" class="note error">{{ fehler }}</p>
</div>
</div>
</template>
<style scoped>
.stream-tab {
min-width: 0;
}
.buehne {
position: relative;
min-width: 0;
}
.player {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
border: 1px solid var(--line);
border-radius: 12px;
}
.platzhalter {
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: center;
align-items: center;
aspect-ratio: 16 / 9;
padding: 2rem;
text-align: center;
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: 12px;
}
.gross {
color: var(--text);
font-size: 1.05rem;
}
.start {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 0.8rem 1.8rem;
color: #fff;
font-size: 1rem;
font-weight: 600;
background: var(--accent);
border-radius: 8px;
}
.note {
padding: 1rem 0;
color: var(--muted);
text-align: center;
}
.note.error {
color: var(--accent);
}
</style>