Video-System Self-Hosting
Systemübersicht und Architektur
\nDieses Dokument richtet sich an Entwickler und Administratoren mit Web- und Server-Grundlagen. Es beschreibt Architektur, Implementierung und Betrieb des Video-Self-Hosting-Systems.
\nDas Video-Self-Hosting-System reduziert die Abhängigkeit von Drittanbietern wie Vimeo oder YouTube. Speicherung, Routing und Auslieferung erfolgen auf eigener Infrastruktur. Videos werden außerhalb des DocumentRoot gespeichert und über einen dedizierten Controller kontrolliert ausgeliefert.
\nKomponenten
\nDas System besteht aus sechs PHP-Services, einem Controller, einem JavaScript-Modul und einer CSS-Datei:
\nPHP-Services (unter src/Services/Video/):
- \n
- VideoComponentService -- Rendering-Engine, erzeugt das gesamte Player-HTML \n
- HlsAbrService -- Multi-Rendition-Encoding und Master-Playlist-Generierung \n
- ChapterService -- Kapitelmarken laden und rendern \n
- SubtitleService -- WebVTT-Untertitel verwalten und streamen \n
- TranscriptService -- Transkript laden, parsen und rendern \n
- ThumbnailService -- Sprite-Sheet-Generierung und -Auslieferung \n
Controller (VideoController):
- \n
- Streaming von MP4, HLS-Playlists, Segmenten, Untertiteln und Thumbnails \n
Frontend (public/assets/):
- \n
js/video-player.js-- Tastatursteuerung, Speed-Control, Fullscreen, Quality-Selector, Chapters, Transcript, Thumbnails, Lazy Loading \ncss/video.css-- Responsive Styles für alle Player-Elemente \n
Datenfluss
\nDer Weg vom Placeholder zum gestreamten Video verläuft in fünf Schritten:
\n- \n
- Template enthält
{{video:slug|params}}Placeholder \n - PlaceholderParser oder ArtefaktRenderer erkennt das Pattern \n
- VideoComponentService lädt Video-Metadaten aus der Datenbank \n
- HTML wird generiert: Video-Element, Data-Attribute, CSS, JS, HLS-Init-Script \n
- Client initialisiert je nach Browser-Unterstützung HLS.js oder native HLS-Wiedergabe und streamt Segmente über den VideoController \n
Fallback-Kette
\nDie Wiedergabe folgt einer dreistufigen Fallback-Kette:
\n- \n
- HLS.js -- JavaScript-basierter HLS-Player (alle modernen Browser) \n
- Native HLS -- Safari und iOS unterstützen HLS nativ über das
<video>-Element \n - MP4 Direct -- Direkter Download der Quelldatei als letzter Fallback \n
Sicherheitskonzept
\nVideos liegen im Verzeichnis /var/www/videos/ -- außerhalb des DocumentRoot /var/www/dev.karlkratz.de/public/. Der vorgesehene Zugriffspfad führt über den VideoController, der Slugs validiert, Dateipfade auflöst und mittels realpath() Path-Traversal-Angriffe verhindert. Die tatsächliche Auslieferung erfolgt über Apache X-Sendfile, das die Dateiübertragung an den Webserver delegiert.
Verzeichnisstruktur und Dateispeicherung
Alle Videodateien werden außerhalb des DocumentRoot unter /var/www/videos/ gespeichert. Pro Video existiert ein Unterverzeichnis mit einer definierten Ordnerstruktur.
Namenskonvention
Verzeichnisse folgen dem Schema {datum}_{beschreibung}/:
/var/www/videos/
2026-02-04_fallbesprechung-ki-protokollierung-artefakte/
Vollständige Verzeichnisstruktur
/var/www/videos/{verzeichnis}/
video{id}.mp4 Quelldatei (Original-Upload)
poster.jpg Vorschaubild (Standard-Poster)
hls/ HLS-Streaming-Verzeichnis
master.m3u8 ABR Master-Playlist (referenziert alle Renditions)
playlist.m3u8 Single-Rendition Playlist (Abwärtskompatibilität)
segment_0000.ts Single-Rendition Segmente
segment_0001.ts
...
1080p/ Rendition: Full HD
playlist.m3u8
segment_0000.ts ... segment_NNNN.ts
720p/ Rendition: HD
playlist.m3u8
segment_0000.ts ... segment_NNNN.ts
480p/ Rendition: SD
playlist.m3u8
segment_0000.ts ... segment_NNNN.ts
360p/ Rendition: Mobil
playlist.m3u8
segment_0000.ts ... segment_NNNN.ts
thumbnails/ Thumbnail-Sprite-Verzeichnis
sprite.webp Zusammengesetztes Thumbnail-Grid
sprite.json Metadata (Größe, Spalten, Intervall)
subtitles/ Untertitel-Verzeichnis
de.vtt Deutsche Untertitel (WebVTT)
en.vtt Englische Untertitel
Speichervolumen
Ein typisches Video (85 Minuten, 2144x1080) erzeugt folgendes Datenvolumen:
- Quelldatei: ~478 MB (MP4, H.264 Main)
- Single-Rendition HLS: ~480 MB (~510 Segmente à 10 Sekunden)
- ABR 4 Renditions: ~1.5 GB zusätzlich (1080p + 720p + 480p + 360p)
- Thumbnail-Sprite: ~200 KB (160x90px, alle 10 Sekunden)
- Gesamt: ~2.5 GB pro Video mit vollem Feature-Set
Die tatsächlichen Dateigrößen hängen von Inhaltskomplexität und Encoding-Parametern ab. Die Bitrate-Vorgaben (-b:v, -maxrate) sind Obergrenzen -- bei wenig Bewegung im Bild können die Segmente deutlich kleiner ausfallen.
Pfad-Konventionen
| Element | Pfad relativ zum Video-Verzeichnis |
|---|---|
| Quelldatei | *.mp4 (beliebiger Dateiname) |
| Poster | poster.jpg |
| HLS-Master | hls/master.m3u8 |
| HLS-Single | hls/playlist.m3u8 |
| Rendition | hls/{qualität}/playlist.m3u8 |
| Sprite | thumbnails/sprite.webp |
| Sprite-Meta | thumbnails/sprite.json |
| Untertitel | subtitles/{lang}.vtt |
Diese einheitliche Namenskonvention ermöglicht es dem VideoController, alle zugehörigen Dateien eines Videos anhand des Slug-Verzeichnisses automatisch aufzulösen -- ohne zusätzliche Konfiguration pro Video.
Datenbank: Artefakte und Abhängigkeiten
Videos werden in der artefakte-Tabelle der Datenbank karlkratz_de verwaltet. Jedes Video ist ein Artefakt vom Typ video, dessen content-Feld den absoluten Pfad zur Videodatei enthält.
Video-Artefakt
INSERT INTO artefakte (slug, type, content, meta_title, meta_description, status)
VALUES (
'fallbesprechung-ki-protokoll-video',
'video',
'/var/www/videos/2026-02-04_fallbesprechung-ki-protokollierung-artefakte/video1350955604.mp4',
'Fallbesprechung: KI-Protokollierung',
'Video der Fallbesprechung zum Thema KI-Protokollierung und Artefakte',
'dev'
);
Relevante Spalten:
| Spalte | Inhalt |
|---|---|
slug |
Eindeutiger Bezeichner (z.B. fallbesprechung-ki-protokoll-video) |
type |
Immer video |
content |
Absoluter Dateipfad zur MP4-Datei |
meta_title |
Titel des Videos |
meta_description |
Beschreibung |
status |
dev oder prod |
deleted_at |
Soft-Delete (NULL = aktiv) |
Poster-Konvention
Poster werden als separates Artefakt mit dem Slug {video-slug}-poster gespeichert:
INSERT INTO artefakte (slug, type, content)
VALUES (
'fallbesprechung-ki-protokoll-video-poster',
'video',
'/var/www/videos/2026-02-04_.../poster.jpg'
);
Der VideoComponentService prüft automatisch, ob ein Artefakt mit dem Suffix -poster existiert.
Kapitel-Artefakt
Kapitelmarken werden als eigenes Artefakt gespeichert. Das content-Feld enthält JSON:
{
"slug": "fallbesprechung-kapitel",
"type": "content",
"content": "[{\"time\": 0, \"title\": \"Einleitung\"}, {\"time\": 120, \"title\": \"Thema 1\"}, {\"time\": 600, \"title\": \"Thema 2\"}]"
}
Transkript-Artefakt
Transkripte können als JSON-Artefakt oder als WebVTT-Datei vorliegen:
[
{"start": 0.0, "end": 5.2, "text": "Willkommen zum Video."},
{"start": 5.2, "end": 10.1, "text": "Heute besprechen wir..."}
]
Abhängigkeiten (artefakt_dependencies)
Wenn ein Seiten-Artefakt ein Video einbettet, wird die Abhängigkeit in artefakt_dependencies gespeichert:
page_slug: 'fallbesprechung-ki-protokoll-artefakt'
depends_on_slug: 'fallbesprechung-ki-protokoll-video'
dependency_type: 'video'
Dies ermöglicht die automatische Cache-Invalidierung: Wenn das Video-Artefakt aktualisiert wird, wird der Cache der einbettenden Seite ungültig.
Abfrage-Muster
Der VideoComponentService verwendet folgendes Query-Muster:
SELECT slug, content, meta_title, meta_description
FROM artefakte
WHERE slug = ? AND type = 'video' AND deleted_at IS NULL
LIMIT 1
Nur aktive (nicht gelöschte) Artefakte werden ausgeliefert. Die Spalte deleted_at dient als Soft-Delete-Mechanismus.
Das Placeholder-System
Videos werden über das Placeholder-System in Seiten eingebettet. Die Syntax folgt dem bestehenden Artefakt-Placeholder-Muster.
Syntax
{{video:slug}}
{{video:slug|param1=wert1|param2=wert2}}
Beispiele:
{{video:fallbesprechung-ki-protokoll-video}}
{{video:fallbesprechung-ki-protokoll-video|lazy=true|autoplay=false}}
{{video:fallbesprechung-ki-protokoll-video|chapters=kapitel-slug|captions=de,en}}
{{video:fallbesprechung-ki-protokoll-video|transcript=transkript-slug|thumbnails=true}}
{{video:fallbesprechung-ki-protokoll-video|poster=/assets/img/custom-poster.jpg}}
{{video:123456789}}
Verfügbare Parameter
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
width |
String | 100% |
Breite des Containers |
lazy |
Boolean | true |
Lazy Loading mit Poster-Placeholder |
autoplay |
Boolean | false |
Automatische Wiedergabe |
muted |
Boolean | false |
Stummschaltung |
loop |
Boolean | false |
Endloswiedergabe |
controls |
Boolean | true |
Browser-native Steuerelemente |
playsinline |
Boolean | true |
Inline-Wiedergabe auf Mobilgeräten |
preload |
String | metadata |
Preload-Strategie (none, metadata, auto) |
poster |
String | -- | URL oder Artefakt-Slug für Vorschaubild |
chapters |
String | -- | Artefakt-Slug für Kapitelmarken (JSON) |
captions |
String | -- | Sprachcodes für Untertitel (z.B. de,en) |
transcript |
String | -- | Artefakt-Slug für Transkript-Daten |
thumbnails |
Boolean | false |
Thumbnail-Sprite-Preview aktivieren |
Erkennung: Vimeo vs. Lokal
Numerische Slugs (z.B. 123456789) werden als Vimeo-Video-ID interpretiert und als iframe eingebettet. Text-Slugs verweisen auf lokale Videos. Voraussetzung: Lokale Video-Slugs dürfen nicht rein numerisch sein, da sie sonst als Vimeo-ID behandelt würden. Dies ist eine bewusste Design-Entscheidung zugunsten einfacher Syntax. Eine robustere Alternative wäre ein expliziter Präfix (z.B. vimeo:123456). Details zur Vimeo-Integration und den verfügbaren Parametern beschreibt das Kapitel Vimeo-Embedding.
Verarbeitungskette
Die Placeholder-Verarbeitung erfolgt an zwei Stellen:
- PlaceholderParser (
src/Services/PlaceholderParser.php) -- für klassische Seiten - ArtefaktRenderer (
src/Services/ArtefaktRenderer.php) -- für Artefakt-basierte Seiten
Beide verwenden dasselbe Regex-Pattern:
/\{\{video:([a-zA-Z0-9_-]+)(?:\|([^}]+))?\}\}/i
Und delegieren an VideoComponentService::render().
Parsedown-Interaktion (ArtefaktRenderer)
Im ArtefaktRenderer durchläuft Artefakt-Content Parsedown für Markdown-Rendering. Standalone-{{video:...}}-Placeholder würden von Parsedown in <p>-Tags eingeschlossen, was zu invalidem HTML führt (<p><div class="video-container">...</div></p>).
Lösung: Zwei Schutzmaßnahmen in resolveArticle():
-
Div-Wrapping: Vor Parsedown wird
inVideo nicht gefunden
<div>gewrappt. Parsedown behandelt Inhalte innerhalb von HTML-Block-Elementen als HTML-Block und fügt kein</div>Video nicht gefunden
<p>hinzu. -
Script-Protection: Verschachtelte Artikel (`
) werden VOR Parsedown aufgelöst. Wenn ein Kind-Artikel ein Video enthält, produziertVideoComponentServiceInline-<code><script></code>-Tags. Diese würden vom Parsedown des Eltern-Artikels als <code><pre><code></code> interpretiert. Lösung: <code><script></code>-Blöcke werden vor Parsedown als`-Token extrahiert und danach wiederhergestellt.
Die eigentliche Video-Auflösung ({{video:...}} → HTML) erfolgt NACH Parsedown in resolveNonArticlePlaceholders().
Fehlerbehandlung
Bei nicht gefundenen Videos oder Fehlern wird ein Fehler-Element ausgegeben:
<div class="video-error">
<p>Video 'slug' nicht gefunden</p>
</div>
Die Fehlermeldung erscheint direkt an der Stelle, an der das Video eingebettet wäre. So bleibt das Seitenlayout intakt, und der Fehler ist sofort sichtbar.
Vimeo-Embedding (Fallback)
Für externe Videos oder als Übergangslösung unterstützt das Placeholder-System auch Vimeo-Embeds. Die Erkennung erfolgt automatisch anhand des Slug-Formats.
Erkennung
Ein rein numerischer Slug wird als Vimeo-Video-ID interpretiert:
{{video:123456789}} Vimeo-Embed
{{video:mein-lokales-video}} Lokales Video
Die Unterscheidung erfolgt in VideoComponentService::render():
if (ctype_digit($slugOrId)) {
return $this->renderVimeo($slugOrId, $params);
}
return $this->renderLocalVideo($slugOrId, $params);
Generiertes HTML
<div class="video-container video-vimeo">
<div style="padding:56.25% 0 0 0;position:relative;">
<iframe src="https://player.vimeo.com/video/123456789?badge=0&autopause=0"
frameborder="0"
allow="autoplay; fullscreen; picture-in-picture"
style="position:absolute;top:0;left:0;width:100%;height:100%;"
loading="lazy">
</iframe>
</div>
<script src="https://player.vimeo.com/api/player.js"></script>
</div>
Responsive Container
Der Padding-Trick (padding: 56.25% 0 0 0) erzeugt ein 16:9-Seitenverhältnis. Das iframe wird absolut positioniert und füllt den Container vollständig aus. So bleibt das Video bei jeder Bildschirmgröße proportional.
Verfügbare Parameter
| Parameter | Vimeo-Wert | Standard |
|---|---|---|
autoplay |
autoplay=1/0 |
0 |
muted |
muted=1/0 |
0 |
loop |
loop=1/0 |
0 |
width |
CSS-Breite des Containers | 100% |
Einschränkungen gegenüber lokalen Videos
Bei Vimeo-Embeds sind folgende Features nicht verfügbar:
- Tastatursteuerung (wird von Vimeo's eigenem Player behandelt)
- Geschwindigkeitssteuerung (nur über Vimeo-Player)
- Kapitelmarken
- Transkript
- Thumbnail-Preview
- Quality-Selector
- Untertitel (nur über Vimeo-eigene Untertitel)
Vimeo-Videos erhalten die CSS-Klasse .video-vimeo statt .video-local. Das JavaScript-Modul initialisiert nur .video-local-Container.
VideoComponentService: Die Rendering-Engine
Der VideoComponentService ist das Herzstück des Video-Systems. Er koordiniert alle Services, generiert das vollständige HTML und stellt sicher, dass Assets nur einmal pro Seite geladen werden.
Pfad
src/Services/Video/VideoComponentService.php
Statische Flags
Drei statische Flags verhindern die mehrfache Einbindung von Assets bei mehreren Videos auf einer Seite:
private static bool $hlsJsIncluded = false;
private static bool $cssIncluded = false;
private static bool $jsIncluded = false;
Das erste Video auf der Seite bindet CSS, JS und HLS.js ein. Alle weiteren Videos nutzen die bereits geladenen Assets.
Rendering-Ablauf (renderLocalVideo)
- Datenbank-Lookup:
loadVideoArtefakt($slug)-- lädt Video-Pfad, Titel, Beschreibung - Datei-Prüfung: Existiert die MP4-Datei unter dem gespeicherten Pfad?
- Options-Aufbau:
buildOptions()-- mergt Parameter mit DEFAULT_OPTIONS - HLS-Erkennung:
getHlsInfo()-- prüft obmaster.m3u8(ABR) oderplaylist.m3u8existiert - Poster-Auflösung:
getPosterUrl()-- dreistufige Kaskade (Parameter → Artefakt → Datei) - Feature-Ladung (je nach Parameter):
- ChapterService → Kapitel-Array
- SubtitleService → Track-Array
- TranscriptService → Segment-Array
- ThumbnailService → Sprite-Metadata
- HTML-Generierung:
renderPlayer()-- baut das vollständige Player-HTML
Data-Attribute am Video-Element
Der Renderer schreibt Feature-Daten als Data-Attribute auf das <video>-Element. JSON-Werte werden dabei kontextgerecht für HTML-Attribute escaped (htmlspecialchars mit ENT_QUOTES), da Kapitel, Transkripte und Sprite-Metadaten aus externen Artefakten stammen können. Das JavaScript liest die Attribute beim Initialisieren:
| Attribut | Inhalt | Beispiel |
|---|---|---|
data-video-slug |
Video-Slug | fallbesprechung-ki-protokoll-video |
data-hls-src |
HLS-Playlist URL | /hls/slug/master.m3u8 |
data-mp4-src |
MP4-Fallback URL | /video/slug |
data-chapters |
Kapitel als JSON | [{"time":0,"title":"Einleitung"}] |
data-sprite |
Sprite-Metadata JSON | {"url":"/video/slug/thumbnails/sprite.webp","columns":10,...} |
data-renditions |
Verfügbare Qualitäten | ["1080p","480p"] |
HLS-Initialisierungsskript
Für jedes Video mit HLS wird ein Inline-Script generiert, das:
- HLS.js lokal lädt (einmalig pro Seite)
- Die HLS-Instanz erstellt und an das Video-Element bindet
- Die Instanz als
video._hlsInstancespeichert (für den Quality-Selector) - Bei Klick auf den Lazy-Placeholder das Video initialisiert
- Bei HLS-Fehlern automatisch auf MP4 zurückfällt
Poster-Auflösungskaskade
Das Poster wird über eine dreistufige Kaskade aufgelöst: expliziter Parameter, Artefakt-Konvention ({slug}-poster) und Datei-Konvention (poster.jpg). Die vollständige Logik mit Code-Beispielen beschreibt das Kapitel Poster und Vorschaubilder.
Generiertes HTML (vereinfacht)
<link rel="stylesheet" href="/assets/css/video.css">
<div class="video-container video-local" id="video-a1b2c3d4">
<div class="video-lazy-wrapper" data-video-lazy="true">
<img src="/video/slug-poster" class="video-poster-placeholder">
<button class="video-play-button">▶</button>
</div>
<video id="player-video-a1b2c3d4" class="video-player"
controls playsinline preload="metadata"
poster="/video/slug-poster"
data-video-slug="slug"
data-hls-src="/hls/slug/master.m3u8"
data-mp4-src="/video/slug"
data-chapters='[...]'
data-renditions='["1080p","720p"]'
style="display:none;">
<source src="/video/slug" type="video/mp4">
<track kind="subtitles" src="/video/slug/subtitles/de.vtt" srclang="de" label="Deutsch" default>
</video>
</div>
<ol class="video-chapters-list">...</ol>
<div class="video-transcript">...</div>
<script src="/assets/js/hls.min.js"></script>
<script>/* HLS-Init */</script>
<script src="/assets/js/video-player.js"></script>
Der VideoComponentService erzeugt das gesamte Player-Markup in einem einzigen Durchlauf. CSS, JavaScript und HLS.js werden dabei nur einmal pro Seite eingebunden, auch wenn mehrere Videos auf einer Seite erscheinen.
Poster und Vorschaubilder
Poster-Bilder werden als Vorschau vor dem Abspielen angezeigt. Sie dienen sowohl als Lazy-Loading-Placeholder als auch als visueller Hinweis auf den Videoinhalt.
Dreistufige Auflösungskaskade
Der VideoComponentService sucht das Poster in drei Stufen:
Stufe 1: Parameter
{{video:mein-video|poster=/assets/img/custom-poster.jpg}}
{{video:mein-video|poster=mein-video-poster-slug}}
URLs (beginnend mit / oder http) werden direkt verwendet. Alle anderen Werte werden als Video-Artefakt-Slug interpretiert und über /video/{slug} gestreamt.
Stufe 2: Artefakt-Konvention
Automatische Suche nach einem Artefakt mit dem Slug {video-slug}-poster:
SELECT slug FROM artefakte
WHERE slug = 'mein-video-poster' AND type = 'video' AND deleted_at IS NULL
Stufe 3: Datei-Konvention
Prüfung auf eine Datei poster.jpg im Video-Verzeichnis:
/var/www/videos/{verzeichnis}/poster.jpg
Verwendung im HTML
Das Poster wird an zwei Stellen eingesetzt:
- HTML5 poster-Attribut:
<video poster="/video/slug-poster"> - Lazy-Wrapper:
<img src="/video/slug-poster" class="video-poster-placeholder">
Der Lazy-Wrapper zeigt das Poster großflächig mit einem überlagerten Play-Button. Das poster-Attribut des Video-Elements dient als Fallback, falls das Video ohne Lazy-Loading eingebunden wird.
Poster-Streaming
Poster-Bilder werden über denselben VideoController-Endpunkt wie Videos ausgeliefert:
// URL: /video/{slug}-poster
// Der Controller erkennt die Dateiendung (.jpg) und setzt:
Content-Type: image/jpeg
Cache-Control: public, max-age=86400
Das Poster-Artefakt zeigt in seinem content-Feld auf den absoluten Pfad der Bilddatei, z.B. /var/www/videos/{verzeichnis}/poster.jpg.
Empfohlenes Format
- Format: JPEG (gute Kompression für Fotos)
- Seitenverhältnis: 16:9 (passend zum Video-Container)
- Auflösung: Mindestens 960x540 (wird per
object-fit: coverskaliert)
Ein gut gewähltes Poster-Bild gibt dem Zuschauer vorab einen visuellen Eindruck des Videoinhalts und motiviert zum Abspielen.
HTTP-Routing: .htaccess und index.php
Vier URL-Patterns leiten Video-Anfragen an den VideoController weiter. Die Verarbeitung erfolgt in zwei Stufen: Apache .htaccess schreibt die URL auf index.php um, und dort wird die Route an die passende Controller-Methode delegiert.
.htaccess RewriteRules
# Thumbnail Sprite Route: /video/{slug}/thumbnails/sprite.webp
RewriteRule ^video/([a-zA-Z0-9_-]+)/thumbnails/sprite\.jpg$ index.php?x=video/$1/thumbnails/sprite.webp [QSA,L]
# Subtitle Route: /video/{slug}/subtitles/{lang}.vtt
RewriteRule ^video/([a-zA-Z0-9_-]+)/subtitles/([a-z]{2})\.vtt$ index.php?x=video/$1/subtitles/$2.vtt [QSA,L]
# Video Streaming Route: /video/{slug}
RewriteRule ^video/([a-zA-Z0-9_-]+)/?$ index.php?x=video/$1 [QSA,L]
# HLS Streaming Route: /hls/{slug}/{datei}
RewriteRule ^hls/([a-zA-Z0-9_-]+)/(.+)$ index.php?x=hls/$1/$2 [QSA,L]
Die Reihenfolge ist entscheidend: Spezifischere Regeln (Thumbnails, Subtitles) stehen vor der allgemeinen Video-Route, da /video/{slug}/thumbnails/sprite.webp sonst als Video-Slug {slug}/thumbnails/sprite.webp interpretiert würde.
index.php Routing-Logik
Die index.php extrahiert den Slug aus $_GET['x'] und prüft in dieser Reihenfolge:
$slug = $_GET['x'] ?? 'index';
// 1. Thumbnail Sprite
if (preg_match('#^video/([a-zA-Z0-9_-]+)/thumbnails/sprite\.jpg$#', $slug, $m)) {
$videoController->thumbnailAction($m[1]);
exit;
}
// 2. Subtitle
if (preg_match('#^video/([a-zA-Z0-9_-]+)/subtitles/([a-z]{2})\.vtt$#', $slug, $m)) {
$videoController->subtitleAction($m[1], $m[2]);
exit;
}
// 3. Video Stream (MP4)
if (str_starts_with($slug, 'video/')) {
$videoSlug = substr($slug, 6);
$videoController->streamAction($videoSlug);
exit;
}
// 4. HLS Stream
if (str_starts_with($slug, 'hls/')) {
$hlsPath = substr($slug, 4);
if (preg_match('#^([a-zA-Z0-9_-]+)/(.+)$#', $hlsPath, $matches)) {
$videoController->hlsAction($matches[1], $matches[2]);
exit;
}
}
URL-Schema (Zusammenfassung)
| URL | Controller-Methode | Zweck |
|---|---|---|
/video/{slug} |
streamAction($slug) |
MP4-Video streamen |
/video/{slug}/thumbnails/sprite.webp |
thumbnailAction($slug) |
Sprite-Sheet ausliefern |
/video/{slug}/subtitles/{lang}.vtt |
subtitleAction($slug, $lang) |
WebVTT-Untertitel |
/hls/{slug}/master.m3u8 |
hlsAction($slug, 'master.m3u8') |
ABR Master-Playlist |
/hls/{slug}/playlist.m3u8 |
hlsAction($slug, 'playlist.m3u8') |
Single-Rendition Playlist |
/hls/{slug}/segment_0001.ts |
hlsAction($slug, 'segment_0001.ts') |
HLS-Segment |
/hls/{slug}/720p/playlist.m3u8 |
hlsAction($slug, '720p/playlist.m3u8') |
Rendition-Playlist |
/hls/{slug}/720p/segment_0001.ts |
hlsAction($slug, '720p/segment_0001.ts') |
Rendition-Segment |
Alle Video-URLs folgen einem konsistenten Schema mit dem Slug als zentralem Identifikator. Der Router leitet jeden Request an die passende Controller-Methode weiter, die ihrerseits den Dateipfad auflöst und die Datei via X-Sendfile ausliefert.
VideoController: Streaming und Auslieferung
Der VideoController ist der zentrale HTTP-Handler für alle Video-bezogenen Requests. Er validiert Eingaben, löst Dateipfade auf und liefert Dateien über Apache X-Sendfile aus.
Pfad
src/Controllers/VideoController.php
Konstanten
private const VIDEO_BASE_PATH = '/var/www/videos';
private const MIME_TYPES = [
'mp4' => 'video/mp4',
'webm' => 'video/webm',
'ogg' => 'video/ogg',
'm3u8' => 'application/vnd.apple.mpegurl',
'ts' => 'video/mp2t',
'jpg' => 'image/jpeg',
'png' => 'image/png',
];
streamAction: MP4-Streaming
Streamt die Original-Videodatei mit HTTP Range-Request-Support. Dies ermöglicht das Springen im Video und die Fortschrittsanzeige im Browser.
Ablauf:
- Slug-Validierung:
/^[a-zA-Z0-9_-]+$/ - Pfad-Auflösung:
resolveVideoPath($slug) - Range-Header parsen (falls vorhanden)
- Response-Header setzen: Content-Type, Content-Length, Accept-Ranges, Cache-Control
- X-Sendfile-Header für Apache
- PHP-Fallback: Chunk-basiertes Streaming (8 KB Buffer)
Range-Request (HTTP 206):
HTTP/1.1 206 Partial Content
Content-Range: bytes 1000-2000/50000
Content-Length: 1001
Content-Type: video/mp4
Accept-Ranges: bytes
hlsAction: HLS-Streaming
Validiert und streamt HLS-Playlists und Segmente. Unterstützt sowohl Single-Rendition als auch Multi-Rendition (ABR):
# Erlaubte Datei-Patterns (Regex):
^(?:(?:\d{3,4}p)/)?(?:master\.m3u8|playlist\.m3u8|segment_\d{4}\.ts)$
Dies erlaubt:
master.m3u8-- ABR Master-Playlistplaylist.m3u8-- Single-Rendition Playlistsegment_0042.ts-- Segment-Dateien720p/playlist.m3u8-- Rendition-Playlist720p/segment_0042.ts-- Rendition-Segment
resolveVideoPath: Pfad-Auflösung
Die Pfad-Auflösung erfolgt in zwei Stufen:
- Datenbank-Lookup:
SELECT content FROM artefakte WHERE slug = ? AND type = 'video' - Glob-Fallback:
glob('/var/www/videos/*{slug}*/*.mp4')als Rückfall (nicht deterministisch bei mehreren Treffern -- nur für die initiale Migration vorgesehen, nicht für den Produktivbetrieb)
In beiden Fällen wird der aufgelöste Pfad mit realpath() verifiziert und gegen VIDEO_BASE_PATH geprüft.
X-Sendfile
Apache X-Sendfile ist ein Modul, das die Dateiauslieferung vom PHP-Prozess an den Apache-Webserver delegiert. PHP setzt nur den Header X-Sendfile: /absoluter/pfad/zur/datei und Apache übernimmt die Dateiübertragung. Ob dabei tatsächlich Kernel-Level Sendfile (Zero-Copy) zum Einsatz kommt, hängt von TLS-Konfiguration und Betriebssystem ab.
Range-Support bei X-Sendfile ist abhängig von der Apache-Konfiguration und muss nach Änderungen am Server explizit getestet werden.
Vorteile:
- Kein PHP-Speicherverbrauch für große Dateien
- Kernel-Level Sendfile für maximale Performance
- PHP-Prozess wird sofort freigegeben
Durch die Kombination aus sicherer Pfadauflösung und X-Sendfile vereint der VideoController Sicherheit mit Performance -- PHP validiert den Zugriff, Apache übernimmt die effiziente Auslieferung.
HLS-Streaming und Segmentierung
HTTP Live Streaming (HLS) ist ein von Apple entwickeltes Streaming-Protokoll. Das Video wird in kleine Segmente aufgeteilt, die einzeln heruntergeladen werden. Eine Playlist-Datei (.m3u8) beschreibt die verfügbaren Segmente.
Funktionsprinzip
- Das Quellvideo wird mit ffmpeg in 10-Sekunden-Segmente (
.ts) zerlegt - Eine Playlist-Datei (
playlist.m3u8) listet alle Segmente mit ihren Dauern - Der Browser lädt die Playlist und dann einzelne Segmente nach Bedarf
- Während der Wiedergabe werden nachfolgende Segmente im Voraus geladen (Buffering)
Playlist-Format
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:12
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:11.360000,
segment_0000.ts
#EXTINF:9.720000,
segment_0001.ts
#EXTINF:10.000000,
segment_0002.ts
...
Jeder #EXTINF-Eintrag gibt die Dauer des folgenden Segments in Sekunden an. EXT-X-PLAYLIST-TYPE:VOD signalisiert, dass es sich um ein abgeschlossenes Video handelt (Video on Demand).
Encoding-Befehl (Single-Rendition)
ffmpeg -i input.mp4 \
-c:v libx264 -preset medium -profile:v main -level 4.0 \
-c:a aac -b:a 128k -ar 44100 \
-f hls -hls_time 10 -hls_list_size 0 \
-hls_segment_filename 'segment_%04d.ts' \
-hls_playlist_type vod \
playlist.m3u8
Parameter-Erläuterung:
| Parameter | Bedeutung |
|---|---|
-c:v libx264 |
H.264 Video-Codec |
-preset medium |
Encoding-Geschwindigkeit vs. Qualität |
-profile:v main |
Kompatibilitätsprofil (breite Geräteunterstützung) |
-c:a aac |
AAC Audio-Codec |
-hls_time 10 |
Segment-Dauer in Sekunden |
-hls_list_size 0 |
Alle Segmente in der Playlist (kein Sliding Window) |
-hls_playlist_type vod |
Markiert als Video on Demand |
Segment-Namenskonvention
Segmente werden mit 4-stelliger Nummerierung benannt: segment_0000.ts, segment_0001.ts, usw. Das .ts-Format (MPEG Transport Stream) ist das Standardformat für HLS-Segmente.
Vorteile gegenüber progressivem Download
- Sofortiger Start: Nur das erste Segment muss geladen werden
- Adaptiv: Verschiedene Qualitäten je nach Bandbreite (ABR)
- Effizientes Seeking: Nur betroffene Segmente werden geladen
- Geringerer Speicherverbrauch: Kein vollständiger Download nötig
HLS löst damit die zentralen Probleme des progressiven Downloads und bildet die Grundlage für ein bandbreitenschonendes Streaming-Erlebnis.
Adaptive Bitrate (ABR): Multi-Rendition Encoding
Adaptive Bitrate Streaming erzeugt mehrere Qualitätsstufen desselben Videos. Eine Master-Playlist referenziert alle verfügbaren Renditions. HLS.js wählt automatisch die optimale Qualität basierend auf der aktuellen Bandbreite.
HlsAbrService
src/Services/Video/HlsAbrService.php
Rendition-Presets
| Qualität | Video-Bitrate | Audio-Bitrate | Max-Rate | Buffer |
|---|---|---|---|---|
| 1080p | 4.500 kbps | 192 kbps | 5.000 kbps | 7.500 kbps |
| 480p | 1.000 kbps | 96 kbps | 1.200 kbps | 1.800 kbps |
Automatische Rendition-Auswahl
Nur Qualitätsstufen, die kleiner oder gleich der Quellauflösung sind, werden erzeugt. Ein 720p-Quellvideo erzeugt nur 480p -- nicht 1080p.
Die Breite jeder Rendition wird aus dem Seitenverhältnis des Quellvideos berechnet und auf gerade Pixelzahlen gerundet (Anforderung der H.264-Spezifikation).
Master-Playlist
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=4692000,RESOLUTION=1920x1080,NAME="1080p"
1080p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1096000,RESOLUTION=854x480,NAME="480p"
480p/playlist.m3u8
Jede Zeile EXT-X-STREAM-INF enthält:
- BANDWIDTH: Gesamtbitrate (Video + Audio) in Bits pro Sekunde
- RESOLUTION: Breite x Höhe der Rendition
- NAME: Menschenlesbarer Label
Encoding-Ablauf
Der generateAbr()-Prozess:
- Video-Info ermitteln (Auflösung, Dauer via ffprobe)
- Anwendbare Renditions bestimmen (basierend auf Quell-Höhe)
- Pro Rendition: ffmpeg-Encoding mit spezifischen Parametern
- Master-Playlist generieren
Jede Rendition wird mit folgendem Befehl erzeugt:
ffmpeg -y -i /pfad/video.mp4 \
-vf "scale=854:480" \
-c:v libx264 -preset medium -profile:v main -level 4.0 \
-b:v 1000k -maxrate 1200k -bufsize 1800k \
-c:a aac -b:a 96k -ar 44100 \
-f hls -hls_time 10 -hls_list_size 0 \
-hls_segment_filename '/pfad/hls/480p/segment_%04d.ts' \
-hls_playlist_type vod \
/pfad/hls/480p/playlist.m3u8
Abwärtskompatibilität
Die bestehende hls/playlist.m3u8 (Single-Rendition) bleibt parallel zur master.m3u8 erhalten. Alte Einbettungen, die keine ABR-Unterstützung haben, funktionieren weiterhin.
Bandbreiten-Steuerung
HLS.js verwendet startLevel: -1 (Auto-Modus). Es beginnt mit einer mittleren Qualität und passt dann basierend auf:
- Download-Geschwindigkeit der letzten Segmente
- Verfügbarer Buffer-Länge
- Tatsächlicher Netzwerk-Bandbreite
Die automatische Qualitätsanpassung sorgt dafür, dass Zuschauer stets die bestmögliche Qualität erhalten, ohne manuell eingreifen zu müssen. Wer dennoch eine feste Qualität bevorzugt, kann diese über den Quality Selector wählen.
HLS.js: Client-seitige Wiedergabe
HLS.js ist eine JavaScript-Bibliothek, die HLS-Streaming in allen modernen Browsern ermöglicht. Safari unterstützt HLS nativ, andere Browser benötigen HLS.js als Polyfill.
Einbindung
<script src="/assets/js/hls.min.js"></script>
Die Bibliothek wird lokal geladen. Der VideoComponentService stellt sicher, dass der Script-Tag nur einmal pro Seite eingefügt wird (statisches Flag $hlsJsIncluded).
Initialisierung
Für jedes lokale Video generiert der VideoComponentService ein Inline-Script:
var video = document.getElementById('player-video-a1b2c3d4');
if (Hls && Hls.isSupported()) {
var hls = new Hls({
maxBufferLength: 30,
maxMaxBufferLength: 60,
startLevel: -1
});
video._hlsInstance = hls;
hls.loadSource('/hls/slug/master.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play().catch(function() {});
});
}
Konfiguration
| Parameter | Wert | Bedeutung |
|---|---|---|
maxBufferLength |
30 Sek. | Maximaler Buffer bei normaler Wiedergabe |
maxMaxBufferLength |
60 Sek. | Absolutes Buffer-Maximum |
startLevel |
-1 | Auto-Modus (HLS.js wählt die Startqualität) |
Instanz-Speicherung
Die HLS-Instanz wird als video._hlsInstance am Video-Element gespeichert. Dies ermöglicht dem Quality-Selector später den Zugriff:
var hls = video._hlsInstance;
hls.currentLevel = 2; // Manueller Level-Wechsel
hls.currentLevel = -1; // Zurück zu Auto
Fallback-Kette im Detail
function initPlayer() {
// 1. HLS.js verfügbar?
if (Hls && Hls.isSupported()) {
// JavaScript-basiertes HLS
var hls = new Hls({...});
hls.loadSource(hlsUrl);
hls.attachMedia(video);
}
// 2. Natives HLS (Safari)?
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = hlsUrl;
}
// 3. Fallback auf MP4
else {
video.src = mp4Url;
}
}
Auf Safari wird HLS nativ vom Browser gehandhabt -- inklusive ABR. Auf allen anderen Browsern übernimmt HLS.js die Playlist-Verarbeitung, Segment-Downloads und Qualitätswechsel.
Error-Handling
Bei fatalen HLS-Fehlern (z.B. Netzwerkprobleme, defekte Segmente) wird automatisch auf die MP4-Quelle umgeschaltet:
hls.on(Hls.Events.ERROR, function(event, data) {
if (data.fatal) {
console.warn('HLS error, falling back to MP4');
video.src = mp4Url;
video.play().catch(function() {});
}
});
Dieser Fallback-Mechanismus versucht, bei fatalen HLS-Fehlern auf die MP4-Quelle umzuschalten -- der Zuschauer bemerkt idealerweise nur eine kurze Unterbrechung beim Wechsel zur MP4-Quelle.
Lazy Loading und Preload-Strategie
Lazy Loading verzögert das Laden des Videos, bis der Benutzer aktiv auf den Play-Button klickt. Die Preload-Strategie passt das Lade-Verhalten an die Verbindungsqualität an.
Lazy Loading: Poster-Placeholder
Wenn lazy=true (Standard), wird anstelle des Videos ein Poster-Bild mit einem Play-Button angezeigt:
<div class="video-lazy-wrapper" data-video-lazy="true">
<img src="/video/slug-poster" class="video-poster-placeholder" loading="lazy">
<button class="video-play-button" aria-label="Video abspielen">
<svg>▶</svg>
</button>
</div>
<video style="display:none;">...</video>
Das Video-Element ist initial versteckt (display:none). Erst beim Klick auf den Play-Button wird:
- Der Lazy-Wrapper ausgeblendet
- Das Video-Element sichtbar gemacht
- HLS.js initialisiert
- Die Wiedergabe gestartet
IntersectionObserver
Zusätzlich überwacht ein IntersectionObserver das Video-Container-Element:
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
container.classList.add('video-visible');
observer.unobserve(container);
}
});
}, { threshold: 0.5 });
Sobald 50% des Containers im Viewport sichtbar sind, wird die CSS-Klasse .video-visible hinzugefügt. Dies kann für Animationen oder Vorab-Aktionen genutzt werden.
Preload-Strategie
Die Preload-Strategie wertet die Network Information API aus:
var conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (conn) {
// Langsame Verbindung: Kein Preload
if (conn.effectiveType === 'slow-2g' || conn.effectiveType === '2g') {
video.preload = 'none';
}
// Data-Saver aktiv: Kein Preload
if (conn.saveData) {
video.preload = 'none';
}
}
Preload-Werte:
| Wert | Verhalten |
|---|---|
none |
Nichts vorladen (nur bei Interaktion) |
metadata |
Nur Metadaten laden (Dauer, Auflösung) -- Standard |
auto |
Browser entscheidet (oft vollständiges Buffering) |
Auf langsamen Verbindungen (2G) und bei aktivem Data-Saver wird preload auf none gesetzt, um Datenvolumen zu sparen.
Zusammenspiel
- Seitenlade: Poster wird angezeigt, Video ist versteckt
- IntersectionObserver: Erkennt Sichtbarkeit (
.video-visible) - Preload-Strategie: Passt
preload-Attribut an Verbindung an - Klick auf Play: Video wird initialisiert und gestartet
Dieses Zusammenspiel stellt sicher, dass Videos keine Bandbreite verbrauchen, solange sie nicht sichtbar sind, und beim Abspielen dennoch sofort starten.
CSS-Architektur und Responsive Design
Alle Video-Styles sind in einer einzigen CSS-Datei organisiert, die modulare Sektionen für jedes Feature enthält.
Pfad
public/assets/css/video.css
Design-Tokens
Die CSS-Datei verwendet CSS Custom Properties als Design-Tokens mit Fallback-Werten:
background: var(--color-bg-dark, #1a1a1a);
border-radius: var(--radius-md, 8px);
color: var(--color-primary, #4a90d9);
border: 1px solid var(--color-border-light, #ddd);
background: var(--color-mark, #fff3cd);
Dies ermöglicht die Anpassung an das bestehende Design-System ohne Änderung der Video-CSS.
Container und Layout
.video-container {
position: relative;
width: 100%;
margin: 1.5rem 0;
background: var(--color-bg-dark, #1a1a1a);
border-radius: var(--radius-md, 8px);
overflow: hidden;
}
.video-container.video-local {
aspect-ratio: 16 / 9;
}
Der Container nutzt aspect-ratio: 16/9 für konsistente Proportionen. overflow: hidden verhindert, dass überlagerte Elemente über den abgerundeten Rand hinausragen.
Overlay Controls (Speed, Fullscreen, Quality)
Alle Overlay-Buttons (Speed, Fullscreen, Quality) sind in einem Flex-Wrapper gruppiert:
.video-overlay-controls {
position: absolute;
top: 12px;
right: 12px;
z-index: 8;
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
transition: opacity 0.2s ease;
}
.video-container:hover .video-overlay-controls,
.video-container:focus-within .video-overlay-controls {
opacity: 1;
}
Der Flex-Wrapper positioniert sich oben rechts. Die Buttons darin teilen sich einen einheitlichen Stil ohne individuelle absolute Positionierung. Buttons sind im Ruhezustand unsichtbar und erscheinen bei Hover oder Fokus.
Sektionen in der CSS-Datei
- Container und Video-Element
- Lazy Loading Wrapper und Play-Button
- Vimeo Container
- Error State
- Chapter List
- Transcript (Header, Body, Segments, Search)
- Keyboard Feedback Overlay 8-10. Overlay Controls (Flex Wrapper, Buttons, Quality Menu)
- Thumbnail Preview
- Fullscreen State
- Responsive Breakpoints
Responsive Design
@media (max-width: 768px) {
.video-play-button { width: 60px; height: 60px; }
.video-overlay-controls { opacity: 1; }
}
Auf Mobilgeräten (unter 768px):
- Play-Button wird kleiner (60px statt 80px)
- Overlay-Controls-Wrapper ist dauerhaft sichtbar (kein Hover auf Touch-Geräten)
Durch diese Anpassungen bleibt der Player auf allen Bildschirmgrößen vollständig bedienbar -- ohne separate mobile Stylesheets oder JavaScript-Unterscheidungen.
JavaScript-Architektur und Initialisierung
Das gesamte JavaScript ist in einer einzigen Datei organisiert, die als IIFE (Immediately Invoked Function Expression) ausgeführt wird.
Pfad
public/assets/js/video-player.js
IIFE-Pattern
(function() {
'use strict';
// Alle Funktionen sind lokal (kein Scope-Leak)
function initKeyboard(container) { ... }
function initSpeedControl(container) { ... }
// ...
// Öffentliche API
window.VideoPlayer = { init: initVideoPlayer, initAll: initAll };
})();
Alle internen Funktionen sind innerhalb des IIFE gekapselt. Nur window.VideoPlayer ist von außen erreichbar.
Auto-Initialisierung
function initAll() {
var containers = document.querySelectorAll('.video-container.video-local');
for (var i = 0; i < containers.length; i++) {
initVideoPlayer(containers[i]);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
Bei Seitenladung werden automatisch alle .video-container.video-local-Elemente initialisiert.
Re-Initialisierungsschutz
function initVideoPlayer(container) {
if (container.dataset.videoInit) return;
container.dataset.videoInit = 'true';
// ...
}
Das data-video-init-Attribut verhindert, dass ein Container mehrfach initialisiert wird (z.B. bei dynamisch nachgeladenem Content).
Initialisierungsreihenfolge
initKeyboard(container); // Tastatursteuerung
initSpeedControl(container); // Geschwindigkeitsbutton
initFullscreen(container); // Vollbild-Button
initQualitySelector(container); // Qualitätswahl
initOverlayControls(container); // Flex-Wrapper für Overlay-Buttons
initChapters(container); // Kapitelmarken
initTranscript(container); // Transkript
initThumbnailPreview(container); // Thumbnail-Hover
initLazyLoading(container); // IntersectionObserver
initPreloadStrategy(container); // Verbindungsabhängiger Preload
Die Reihenfolge ist relevant: Keyboard und Buttons werden zuerst eingerichtet, Features die DOM-Siblings benötigen (Chapters, Transcript) danach, und Performance-Features (Lazy, Preload) zuletzt.
Öffentliche API für dynamischen Content
// Einzelnen Container initialisieren
var container = document.getElementById('video-neues-video');
window.VideoPlayer.init(container);
// Alle neuen Container finden und initialisieren
window.VideoPlayer.initAll();
Dies ist nützlich, wenn Videos per AJAX oder nach einem Artefakt-Update dynamisch in die Seite eingefügt werden.
Feature-Sektionen
| Sektion | Zeilen | Funktion |
|---|---|---|
| Keyboard Navigation | 15-194 | Tastenbelegung und Feedback |
| Speed Control | 197-256 | Presets, Persistenz, Button |
| Fullscreen | 259-301 | Fullscreen API, Icon-Wechsel |
| Captions | 304-331 | Untertitel-Toggle |
| Chapter Markers | 334-391 | Click-to-Seek, Aktiv-Highlighting |
| Thumbnail Preview | 394-450 | Sprite-Hover-Logik |
| Quality Selector | 456-599 | ABR-Dropdown, HLS.js-Steuerung |
| Transcript | 605-692 | Suche, Click-to-Seek, Highlighting |
| Lazy Loading | 698-714 | IntersectionObserver |
| Preload Strategy | 720-737 | Network Information API |
| Overlay Controls | 747-762 | Flex-Wrapper für Speed/Fullscreen/Quality |
| Initialization | 765-795 | Auto-Init, API-Export |
Die Initialisierung erfolgt automatisch für alle .video-local-Container auf der Seite. Die API wird als window.VideoPlayer exportiert, sodass externe Scripts bei Bedarf auf Player-Instanzen zugreifen können.
Tastatursteuerung
Das Video-Player-Modul bietet eine vollständige Tastatursteuerung. Alle Aktionen werden durch ein visuelles Feedback-Overlay bestätigt.
Tastenbelegung
| Taste(n) | Aktion | Feedback |
|---|---|---|
| Leertaste / K | Play/Pause umschalten | ▶ oder ⏸ |
| Pfeil links | 5 Sekunden zurück | ◀ 5s |
| Pfeil rechts | 5 Sekunden vor | 5s ▶ |
| J | 10 Sekunden zurück | ◀◀ 10s |
| L | 10 Sekunden vor | 10s ▶▶ |
| Pfeil hoch | Lautstärke +5% | 🔊 80% |
| Pfeil runter | Lautstärke -5% | 🔉 60% |
| M | Stummschalten umschalten | 🔇 Stumm / 🔊 Ton an |
| F | Vollbild umschalten | -- |
| C | Untertitel durchschalten | UT: Deutsch / UT: Aus |
| < (Komma) | Geschwindigkeit -0.25x | 0.75x |
| > (Punkt) | Geschwindigkeit +0.25x | 1.25x |
| 0-9 | Zu 0-90% der Dauer springen | 50% -- MM:SS |
| Home | Zum Anfang springen | Anfang |
| End | Zum Ende springen | Ende |
Konfigurationskonstanten
var SEEK_SHORT = 5; // Pfeiltasten: 5 Sekunden
var SEEK_LONG = 10; // J/L: 10 Sekunden
var VOLUME_STEP = 0.05; // Lautstärke: 5% Schritte
Visuelles Feedback
Jede Tastenaktion zeigt kurz ein Overlay in der Video-Mitte:
function showFeedback(container, text) {
var el = document.createElement('div');
el.className = 'video-feedback';
el.textContent = text;
container.appendChild(el);
setTimeout(function() { el.classList.add('video-feedback--fade'); }, 10);
setTimeout(function() { el.remove(); }, 800);
}
Das Overlay erscheint sofort, beginnt nach 10ms zu verblassen (CSS-Transition 400ms) und wird nach 800ms entfernt.
Fokus-Management
Der Video-Container wird als fokussierbares Element konfiguriert:
container.setAttribute('tabindex', '0');
container.setAttribute('role', 'application');
container.setAttribute('aria-label', 'Video Player - Tastatursteuerung aktiv');
Ein Klick auf das Video fokussiert automatisch den Container, sodass Tastatureingaben erkannt werden.
Schutzmechanismen
- Input/Textarea: Tastatureingaben in Formularfeldern werden ignoriert
- Verstecktes Video: Keine Aktionen wenn
video.style.display === 'none'(Lazy-Zustand) - Event-Handling:
preventDefault()undstopPropagation()verhindern unerwünschtes Browser-Verhalten (z.B. Seiten-Scroll bei Leertaste)
Diese Schutzmechanismen stellen sicher, dass Tastaturkürzel nur dann greifen, wenn der Video-Player tatsächlich fokussiert und aktiv ist.
Geschwindigkeitssteuerung
Die Wiedergabegeschwindigkeit kann über einen Button oder per Tastatur gesteuert werden. Die Einstellung wird im Browser gespeichert und bei zukünftigen Besuchen wiederhergestellt.
Geschwindigkeits-Presets
var SPEED_PRESETS = [0.5, 0.75, 1, 1.25, 1.5, 2];
Steuerung
Button (oben rechts im Video):
- Zeigt die aktuelle Geschwindigkeit (z.B. "1.5x")
- Klick schaltet durch die Presets: 0.5x → 0.75x → 1x → 1.25x → 1.5x → 2x → 0.5x → ...
Tastatur:
<oder,-- Geschwindigkeit -0.25x>oder.-- Geschwindigkeit +0.25x
Snap-to-Preset
Beim Ändern per Tastatur wird auf den nächsten Preset gerundet, wenn die Differenz unter 0.13x liegt:
var closest = SPEED_PRESETS.reduce(function(prev, curr) {
return Math.abs(curr - newSpeed) < Math.abs(prev - newSpeed) ? curr : prev;
});
if (Math.abs(closest - newSpeed) < 0.13) newSpeed = closest;
Dies verhindert unübliche Geschwindigkeiten wie 1.03x oder 0.97x.
Persistenz
Die gewählte Geschwindigkeit wird im localStorage gespeichert:
// Speichern
localStorage.setItem('video-speed-preference', String(newSpeed));
// Wiederherstellen beim Laden
var saved = localStorage.getItem('video-speed-preference');
if (saved) video.playbackRate = parseFloat(saved);
Die Geschwindigkeit bleibt über Seitenbesuche und Browser-Neustarts hinweg erhalten. Alle Videos auf der Seite verwenden dieselbe Einstellung.
CSS
Der Speed-Button wird nur bei Hover oder Fokus sichtbar (opacity-Transition). Auf Mobilgeräten ist er dauerhaft sichtbar.
.video-speed-btn {
position: absolute;
top: 12px;
right: 56px;
opacity: 0;
transition: opacity 0.2s ease;
}
.video-container:hover .video-speed-btn {
opacity: 1;
}
Der Speed-Button erscheint nur bei Hover über den Video-Container. Auf Touch-Geräten ist er dauerhaft sichtbar, da Hover-Events dort nicht verfügbar sind.
Vollbild-Modus
Der Vollbild-Modus verwendet die Standard Fullscreen API mit webkit-Fallback für ältere Safari-Versionen. Der gesamte Video-Container (nicht nur das Video-Element) wird in den Vollbild-Modus versetzt.
Aktivierung
- Button: Oben rechts im Video (SVG-Icon)
- Tastatur: F-Taste
Implementierung
function toggleFullscreen(container) {
if (document.fullscreenElement || document.webkitFullscreenElement) {
if (document.exitFullscreen) document.exitFullscreen();
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
} else {
if (container.requestFullscreen) container.requestFullscreen();
else if (container.webkitRequestFullscreen) container.webkitRequestFullscreen();
}
}
Dynamischer Icon-Wechsel
Das Button-Icon wechselt zwischen Expand (vier Pfeile nach außen) und Collapse (vier Pfeile nach innen):
document.addEventListener('fullscreenchange', function() {
var isFs = document.fullscreenElement === container;
container.classList.toggle('video-fullscreen', isFs);
// Icon aktualisieren
});
CSS im Vollbild
.video-container.video-fullscreen {
border-radius: 0;
}
.video-container.video-fullscreen .video-player {
width: 100vw;
height: 100vh;
object-fit: contain;
}
object-fit: contain stellt sicher, dass das Video seine Proportionen behält und schwarze Balken bei abweichendem Seitenverhältnis angezeigt werden.
Container-basierter Vollbild-Modus
Im Gegensatz zu video.requestFullscreen() wird der gesamte Container in den Vollbild-Modus versetzt. Dies hat den Vorteil, dass alle überlagerten Elemente (Speed-Button, Fullscreen-Button, Quality-Selector, Feedback-Overlay) weiterhin sichtbar sind.
Qualitätswahl (Quality Selector)
Der Quality Selector ermöglicht die manuelle Wahl der Videoqualität. Im Auto-Modus passt HLS.js die Qualität automatisch an die Bandbreite an.
Voraussetzung
Der Quality Selector erscheint nur, wenn ABR-Renditions vorhanden sind. Das Video-Element muss das Attribut data-renditions enthalten:
<video data-renditions='["1080p","720p","480p","360p"]'>
Dropdown-Menü
Der Selector besteht aus einem Button und einem Dropdown-Menü:
- Button: Zeigt die aktuelle Qualität (z.B. "Auto" oder "720p")
- Menü: Öffnet nach oben, zeigt alle verfügbaren Qualitäten
- Aktive Option: Hervorgehoben mit Punkt-Symbol (●)
Die Optionen werden absteigend sortiert: 1080p → 720p → 480p → 360p, mit "Auto" als erste Option.
HLS.js-Integration
Die Qualitätswahl kommuniziert direkt mit der HLS.js-Instanz:
var hls = video._hlsInstance;
// Auto-Modus (HLS.js entscheidet)
hls.currentLevel = -1;
// Manuelle Qualität (z.B. 720p)
for (var i = 0; i < hls.levels.length; i++) {
if (hls.levels[i].height === 720) {
hls.currentLevel = i;
break;
}
}
Live-Update im Auto-Modus
Im Auto-Modus aktualisiert sich der Button-Text bei jedem Qualitätswechsel:
hls.on(Hls.Events.LEVEL_SWITCHED, function(event, data) {
if (autoMode && hls.levels[data.level]) {
var h = hls.levels[data.level].height;
btn.textContent = 'Auto (' + h + 'p)';
}
});
So sieht der Benutzer jederzeit, in welcher Qualität das Video gerade abgespielt wird, auch wenn HLS.js die Entscheidung trifft.
CSS
.video-quality-wrapper {
position: absolute;
top: 12px;
right: 100px;
}
.video-quality-menu {
position: absolute;
bottom: calc(100% + 4px);
background: rgba(20, 20, 20, 0.95);
border-radius: 4px;
}
.video-quality-active::before {
content: '● ';
}
Das Menü positioniert sich oberhalb des Buttons und zeigt die aktive Qualitätsstufe mit einem Punkt-Marker an. Die halbtransparente Hintergrundfarbe fügt sich in das Gesamtdesign des Players ein.
Kapitelmarken (ChapterService)
Kapitelmarken ermöglichen die Navigation innerhalb eines Videos über klickbare Zeitmarken. Die Kapitel werden als JSON in einem separaten Artefakt gespeichert.
ChapterService
src/Services/Video/ChapterService.php
Datenformat
Kapitel werden als JSON-Array im content-Feld eines Artefakts gespeichert:
[
{"time": 0, "title": "Einleitung"},
{"time": 120, "title": "Grundlagen"},
{"time": 600, "title": "Praxisbeispiel"},
{"time": 1200, "title": "Zusammenfassung"}
]
Jedes Objekt hat zwei Felder:
time-- Startzeit in Sekunden (Integer)title-- Angezeigter Kapitelname
Einbindung
{{video:mein-video|chapters=mein-video-kapitel}}
Der Wert von chapters= ist der Slug des Artefakts, das die Kapitel-JSON enthält.
HTML-Ausgabe
Der ChapterService generiert eine klickbare Liste unterhalb des Videos:
<div class="video-chapters">
<h3 class="video-chapters-title">Kapitel</h3>
<ol class="video-chapters-list">
<li class="video-chapter-item" data-time="0">
<button class="video-chapter-btn">
<span class="video-chapter-time">0:00</span>
<span class="video-chapter-label">Einleitung</span>
</button>
</li>
...
</ol>
</div>
JavaScript-Interaktion
Klick auf Kapitel:
btn.addEventListener('click', function() {
var time = parseInt(item.dataset.time, 10);
video.currentTime = time;
if (video.paused) video.play();
container.focus();
});
Aktiv-Markierung:
Während der Wiedergabe wird das aktuelle Kapitel hervorgehoben. Das timeupdate-Event prüft bei jeder Aktualisierung, welches Kapitel aktiv ist:
video.addEventListener('timeupdate', function() {
var current = video.currentTime;
for (var i = chapters.length - 1; i >= 0; i--) {
if (current >= chapters[i].time) {
// Kapitel i ist aktiv
break;
}
}
});
Die Suche beginnt beim letzten Kapitel und läuft rückwärts -- das erste Kapitel, dessen Startzeit kleiner oder gleich der aktuellen Position ist, ist das aktive.
Untertitel: WebVTT und SubtitleService
Untertitel werden im WebVTT-Format gespeichert und über den VideoController ausgeliefert. Der SubtitleService verwaltet die verfügbaren Spuren und generiert die HTML5 Track-Elemente.
SubtitleService
src/Services/Video/SubtitleService.php
Speicherort
/var/www/videos/{verzeichnis}/subtitles/
de.vtt Deutsche Untertitel
en.vtt Englische Untertitel
fr.vtt Französische Untertitel
WebVTT-Format
WEBVTT
00:00:00.000 --> 00:00:05.200
Willkommen zum Video.
00:00:05.200 --> 00:00:10.100
Heute besprechen wir das Thema
KI-Protokollierung.
00:00:10.100 --> 00:00:15.500
Beginnen wir mit den Grundlagen.
Unterstützte Sprachen
| Code | Label |
|---|---|
de |
Deutsch |
en |
English |
fr |
Français |
es |
Español |
it |
Italiano |
Einbindung
{{video:mein-video|captions=de}}
{{video:mein-video|captions=de,en}}
{{video:mein-video|captions=auto}}
- Einzelne Sprache:
captions=de - Mehrere Sprachen:
captions=de,en - Auto-Discovery:
captions=auto(alle vorhandenen .vtt-Dateien)
HTML5 Track-Elemente
Der SubtitleService generiert <track>-Elemente innerhalb des <video>-Tags:
<track kind="subtitles" src="/video/slug/subtitles/de.vtt"
srclang="de" label="Deutsch" default>
<track kind="subtitles" src="/video/slug/subtitles/en.vtt"
srclang="en" label="English">
Die erste Spur mit srclang="de" erhält das default-Attribut.
Tastatur-Toggle
Die C-Taste schaltet durch die verfügbaren Untertitelspuren:
// Zyklus: Aus → Spur 1 → Spur 2 → ... → Aus
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].mode === 'showing') { activeIdx = i; break; }
}
// Aktuelle deaktivieren, nächste aktivieren
tracks[activeIdx].mode = 'hidden';
tracks[nextIdx].mode = 'showing';
Streaming
Untertitel werden über den VideoController gestreamt:
// URL: /video/{slug}/subtitles/{lang}.vtt
// Headers:
Content-Type: text/vtt; charset=UTF-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400
Der CORS-Header ist nur notwendig, wenn WebVTT-Dateien von einer anderen Origin geladen werden. Bei Same-Origin-Zugriff kann er entfallen.
Transkript (TranscriptService)
Das Transkript zeigt den gesprochenen Text des Videos als durchsuchbare, interaktive Liste unterhalb des Players an.
TranscriptService
src/Services/Video/TranscriptService.php
Datenquellen
Das Transkript kann aus zwei Quellen geladen werden:
- JSON-Artefakt (primär): Artefakt mit Segment-Array
- WebVTT-Datei (Fallback): Automatisches Parsen aus vorhandener
.vtt-Datei
Segment-Format (JSON)
[
{"start": 0.0, "end": 5.2, "text": "Willkommen zum Video."},
{"start": 5.2, "end": 10.1, "text": "Heute besprechen wir das Thema KI-Protokollierung."},
{"start": 10.1, "end": 15.5, "text": "Beginnen wir mit den Grundlagen."}
]
Einbindung
{{video:mein-video|transcript=transkript-slug}}
HTML-Ausgabe
<div class="video-transcript">
<div class="video-transcript-header">
<h3 class="video-transcript-title">Transkript</h3>
<input class="video-transcript-search" placeholder="Suchen...">
</div>
<div class="video-transcript-body">
<div class="video-transcript-segment" data-start="0" data-end="5.2">
<button class="video-transcript-time">0:00</button>
<span class="video-transcript-text">Willkommen zum Video.</span>
</div>
...
</div>
</div>
Interaktive Features
Klick-to-Seek: Ein Klick auf den Zeitstempel springt zur entsprechenden Position im Video und startet die Wiedergabe.
Aktiv-Highlighting: Das aktuell gesprochene Segment wird hervorgehoben und automatisch in den sichtbaren Bereich gescrollt:
video.addEventListener('timeupdate', function() {
var t = video.currentTime;
for (var i = segments.length - 1; i >= 0; i--) {
var start = parseFloat(segments[i].dataset.start);
var end = parseFloat(segments[i].dataset.end);
if (t >= start && t < end) {
active = segments[i];
break;
}
}
if (active) {
active.classList.add('video-transcript-active');
active.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
});
Volltextsuche: Das Suchfeld filtert Segmente in Echtzeit:
- Nicht passende Segmente werden ausgeblendet (
display: none) - Treffer werden mit
<mark>hervorgehoben - Die Suche ist case-insensitive
WebVTT-Parser
Falls kein JSON-Artefakt vorhanden ist, parst der TranscriptService eine vorhandene WebVTT-Datei:
// Regex für VTT-Zeitstempel:
/(\d{2}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}\.\d{3})/
Der folgende Text wird als Segment-Text extrahiert.
Thumbnail-Preview (Sprite-Sheet)
Die Thumbnail-Preview zeigt beim Hovern über das Video ein Vorschaubild der entsprechenden Stelle. Die Thumbnails werden als Sprite-Sheet (ein großes Bild mit allen Frames in einem Grid) gespeichert.
ThumbnailService
src/Services/Video/ThumbnailService.php
Konfiguration
private const THUMB_WIDTH = 160; // Breite pro Thumbnail in Pixel
private const THUMB_HEIGHT = 90; // Höhe pro Thumbnail in Pixel
private const COLUMNS = 10; // Spalten im Grid
private const INTERVAL = 10; // Ein Frame alle 10 Sekunden
Generierung (CLI)
php scripts/generate-video-thumbnails.php fallbesprechung-ki-protokoll-video
Schritte:
- Video-Dauer ermitteln (ffprobe)
- Frames extrahieren:
ffmpeg -vf "fps=1/10,scale=160:90"erzeugt ein JPEG alle 10 Sekunden - Sprite zusammensetzen:
montage frame_*.jpg -tile 10x{rows} -geometry 160x90+0+0 -quality 60 sprite.webp - Metadata schreiben:
sprite.json - Temporäre Einzelframes löschen
Sprite-Metadata (sprite.json)
{
"width": 1600,
"height": 540,
"thumbWidth": 160,
"thumbHeight": 90,
"columns": 10,
"rows": 6,
"count": 52,
"interval": 10,
"duration": 5113
}
Einbindung
{{video:mein-video|thumbnails=true}}
Dies fügt dem Video-Element das Attribut data-sprite mit der JSON-Metadata hinzu.
JavaScript-Hover-Logik
video.addEventListener('mousemove', function(e) {
var rect = video.getBoundingClientRect();
var pct = (e.clientX - rect.left) / rect.width; // 0.0 - 1.0
var time = pct * video.duration; // Zeit in Sekunden
var frameIdx = Math.floor(time / sprite.interval); // Index im Grid
var col = frameIdx % sprite.columns; // Spalte
var row = Math.floor(frameIdx / sprite.columns); // Zeile
// CSS Background-Position berechnen
preview.style.backgroundPosition =
'-' + (col * sprite.thumbWidth) + 'px ' +
'-' + (row * sprite.thumbHeight) + 'px';
});
Das Sprite-Bild wird einmal geladen und als CSS-Background verwendet. Durch Verschieben der background-position wird der richtige Ausschnitt angezeigt.
Streaming
Das Sprite-Bild wird über den VideoController ausgeliefert:
// URL: /video/{slug}/thumbnails/sprite.webp
// Headers:
Content-Type: image/webp
Cache-Control: public, max-age=604800 // 1 Woche
Das Sprite-Bild wird mit einer Woche Cache-Dauer ausgeliefert, da sich die Thumbnails eines veröffentlichten Videos nicht mehr ändern.
CLI-Tools: Encoding und Generierung
Zwei CLI-Scripts automatisieren die Vorbereitung von Videos für das Self-Hosting-System. Beide benötigen ffmpeg und werden über die Kommandozeile ausgeführt.
ABR-Encoding: generate-video-hls-abr.php
php scripts/generate-video-hls-abr.php <video-slug> [--info] [--force]
Optionen:
| Flag | Beschreibung |
|---|---|
| (ohne) | Encoding starten (überspringe wenn master.m3u8 existiert) |
--info |
Nur Videoinfo und anwendbare Renditions anzeigen (Dry-Run) |
--force |
Vorhandene ABR-Renditions überschreiben |
Beispiel-Ausgabe (--info):
Video: /var/www/videos/.../video1350955604.mp4
Resolution: 2144x1080
Duration: 01:25:13
Bitrate: 747 kbps
Applicable renditions (4):
1080p: 2144x1080 @ 4500k video + 192k audio
720p: 1430x720 @ 2500k video + 128k audio
480p: 952x480 @ 1000k video + 96k audio
360p: 714x360 @ 500k video + 64k audio
Beispiel-Ausgabe (Encoding):
Starting ABR encoding...
------------------------------------------------------------
[14:23:01] 1080p: encoding
[14:35:42] 1080p: done
[14:35:42] 720p: encoding
[14:42:18] 720p: done
[14:42:18] 480p: encoding
[14:45:01] 480p: done
[14:45:01] 360p: encoding
[14:46:30] 360p: done
------------------------------------------------------------
Elapsed: 00:23:29
Generated files:
Master: /var/www/videos/.../hls/master.m3u8
1080p: 512 segments (1.8 GB)
720p: 512 segments (920 MB)
480p: 512 segments (380 MB)
360p: 512 segments (195 MB)
Thumbnail-Generierung: generate-video-thumbnails.php
php scripts/generate-video-thumbnails.php <video-slug>
Beispiel-Ausgabe:
Generating thumbnail sprite for: fallbesprechung-ki-protokoll-video
Video: /var/www/videos/.../video1350955604.mp4
Done! Sprite generated.
Frames: 52
Size: 1600x540
Interval: 10s
Voraussetzungen
Beide Scripts benötigen:
- ffmpeg: Video-Encoding und Frame-Extraktion
- ffprobe: Video-Analyse (Auflösung, Dauer)
- ImageMagick (montage): Sprite-Sheet-Zusammensetzung (nur für Thumbnails)
Ablauf: Neues Video einrichten
# 1. Video-Datei in Verzeichnis ablegen
# /var/www/videos/2026-02-05_mein-thema/video.mp4
# 2. Artefakt in Datenbank anlegen (manuell oder via Admin)
# 3. HLS-Encoding starten
php scripts/generate-video-hls-abr.php mein-video-slug
# 4. Thumbnails generieren
php scripts/generate-video-thumbnails.php mein-video-slug
# 5. Optional: Untertitel-Dateien ablegen
# /var/www/videos/2026-02-05_mein-thema/subtitles/de.vtt
# 6. Optional: Kapitel- und Transkript-Artefakte anlegen
# 7. Video in Seite einbetten
# {{video:mein-video-slug|chapters=...|captions=de|thumbnails=true}}
Nach Abschluss dieser Schritte ist das Video über seinen Slug erreichbar und wird beim nächsten Seitenaufruf mit allen konfigurierten Features gerendert.
Sicherheit, Validierung und Performance
Das Video-System implementiert mehrere Sicherheitsschichten und Caching-Strategien für eine sichere und performante Auslieferung.
Slug-Validierung
Alle eingehenden Slugs werden gegen ein striktes Pattern validiert:
/^[a-zA-Z0-9_-]+$/
Nur alphanumerische Zeichen, Bindestriche und Unterstriche sind erlaubt. Dies verhindert Path-Traversal-Angriffe (z.B. ../../etc/passwd).
HLS-Datei-Whitelist
Die hlsAction validiert den Dateinamen mit einer Whitelist:
/^(?:(?:\d{3,4}p)/)?(?:master\.m3u8|playlist\.m3u8|segment_\d{4}\.ts)$/
Nur folgende Dateitypen sind erlaubt:
master.m3u8undplaylist.m3u8(Playlists)segment_NNNN.ts(Segmente mit 4-stelliger Nummerierung)- Optional mit Rendition-Präfix (z.B.
720p/)
realpath()-Prüfung
Jeder aufgelöste Dateipfad wird mit realpath() verifiziert:
$realPath = realpath($hlsFile);
if (!$realPath || !str_starts_with($realPath, '/var/www/videos')) {
$this->sendError(403, 'Zugriff verweigert');
return;
}
realpath() löst symbolische Links und ..-Segmente auf. Die anschließende Prüfung verifiziert, dass der tatsächliche Pfad innerhalb des Video-Verzeichnisses liegt.
Untertitel-Sprach-Validierung
Sprachcodes für Untertitel werden auf genau zwei Kleinbuchstaben beschränkt:
/^[a-z]{2}$/
X-Sendfile: Sichere Dateiauslieferung
Die eigentliche Dateiauslieferung übernimmt Apache X-Sendfile auf Kernel-Level. PHP setzt nur den Header und wird sofort freigegeben. Details zur Funktionsweise beschreibt das Kapitel VideoController.
Zugriffskontrolle
Das System implementiert Slug-Validierung, Pfadprüfung und Soft-Delete, jedoch keine Autorisierung auf Benutzerebene. Alle Videos sind über ihre Slugs öffentlich erreichbar. Falls eingeschränkter Zugriff erforderlich ist, muss eine Session- oder Token-Prüfung im VideoController vor dem X-Sendfile-Header ergänzt werden.
Soft-Delete
Die Abfragen filtern auf deleted_at IS NULL, sodass gelöschte Videos über die Anwendung nicht mehr gefunden werden. Bestehende Browser- oder Proxy-Caches können Inhalte bis zum Ablauf ihrer Cache-Dauer jedoch weiterhin ausliefern.
Caching-Strategie
| Asset | Cache-Dauer | Header |
|---|---|---|
| MP4-Video | 1 Tag | Cache-Control: public, max-age=86400 |
| HLS-Playlists | 1 Tag | Cache-Control: public, max-age=86400 |
| HLS-Segmente | 1 Tag | Cache-Control: public, max-age=86400 |
| Thumbnail-Sprite | 1 Woche | Cache-Control: public, max-age=604800 |
| Untertitel | 1 Tag | Cache-Control: public, max-age=86400 |
Bei Re-Encoding oder Änderungen an Renditions muss die Cache-Dauer der Playlists angepasst oder der Cache explizit invalidiert werden.
| CSS/JS (Dev) | Kein Cache | Cache-Control: no-store, no-cache |
| CSS/JS (Prod) | 1 Jahr | Cache-Control: public, max-age=31536000, immutable |
Performance-Optimierungen
Server-seitig:
- X-Sendfile: PHP delegiert die Dateiübertragung an Apache (kein PHP-Memory für große Dateien)
- HLS-Segmentierung: Nur benötigte Teile werden geladen
- ABR: Automatische Anpassung an Bandbreite
- Sprite-Sheet: Eine HTTP-Request für alle Thumbnails
Client-seitig:
- Lazy Loading: Videos werden erst bei Interaktion geladen
- IntersectionObserver: Erkennung der Sichtbarkeit im Viewport
- Preload-Strategie: Kein Vorladen auf langsamen Verbindungen
- localStorage: Geschwindigkeitseinstellung ohne Server-Request
- HLS.js Buffer: Bis zu 60 Sekunden Vorab-Pufferung
- Einmalige Asset-Einbindung: CSS, JS und HLS.js nur einmal pro Seite
Serverseitige und clientseitige Optimierungen greifen ineinander: Der Server liefert Dateien effizient aus, der Client lädt nur, was tatsächlich benötigt wird. Das System ist auf Performance ausgelegt. Ob Engpässe auftreten, hängt vom Lastprofil, der Infrastruktur und der Anzahl paralleler Streams ab.