update
This commit is contained in:
180
frontend/src/components/StreamTab.vue
Normal file
180
frontend/src/components/StreamTab.vue
Normal 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>
|
||||
Reference in New Issue
Block a user