Vidstack.io for REDAXO
Eine PHP-Klasse, die Videos auf Websites einbindet - mit Style! YouTube, Vimeo oder eigene Videos? Alles kein Problem. Und das Beste? Es ist so einfach zu benutzen, dass selbst ein Kater es könnte (wenn er Daumen hätte).
Klar, natürlich über den REDAXO-Installer oder als GitHub Release. Aber das war's noch nicht ganz:
Jetzt kommt der interessante Teil - wir müssen noch ein paar Dateien in unser Frontend einbinden, damit der ganze Zauber funktioniert. Hier ist, was du brauchst:
// In deinem Template oder an einer anderen passenden Stelle:
// CSS einbinden
echo '<link rel="stylesheet" href="' . rex_url::addonAssets('vidstack', 'vidstack.css') . '">';
echo '<link rel="stylesheet" href="' . rex_url::addonAssets('vidstack', 'vidstack_helper.css') . '">';
// JavaScript einbinden
echo '<script src="' . rex_url::addonAssets('vidstack', 'vidstack.js') . '"></script>';
echo '<script src="' . rex_url::addonAssets('vidstack', 'vidstack_helper.js') . '"></script>';
Was passiert hier? Wir benutzen rex_url::addonAssets()
, um die richtigen URLs für unsere Assets zu generieren. Das ist wie ein Zauberstab, der immer auf die korrekten Dateien in deinem REDAXO-Setup zeigt, egal wo sie sich versteckt haben.
Die vidstack.css
und vidstack.js
sind die Hauptdarsteller - sie bringen den Video-Player zum Laufen. Die *_helper
-Dateien sind wie die fleißigen Backstage-Helfer. Sie kümmern sich um Extras wie die DSGVO-Abfrage und andere nützliche Funktionen.
Übrigens: Wenn du nur die generate()
-Methode verwendest und auf den ganzen Schnickschnack wie Consent-Abfragen verzichten möchtest, kannst du die Helper-Dateien weglassen. Aber für das volle Programm mit generateFull()
braucht man alle vier Dateien.
So, jetzt aber! Dein REDAXO ist jetzt bereit, Videos mit Style zu servieren. 🎬🍿
<?php
use FriendsOfRedaxo\VidStack\Video;
// YouTube-Video
$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'Never Gonna Give You Up');
echo $video->generateFull();
// Vimeo-Video
$vimeoVideo = new Video('https://vimeo.com/148751763', 'Vimeo-Beispiel');
echo $vimeoVideo->generateFull();
// Lokales Video
$localVideo = new Video('video.mp4', 'Eigenes Video');
echo $localVideo->generate();
// Externes Video
$externalVideo = new Video('https://somedomain.tld/video.mp4', 'Eigenes Video');
echo $externalVideo->generate();
<?php
use FriendsOfRedaxo\VidStack\Video;
// Video aus dem Medienpool mit Poster-Bild
$video = new Video('mein_video.mp4', 'Mein tolles Video mit Vorschaubild');
$video->setPoster('vorschaubild.jpg', 'Beschreibung des Vorschaubilds');
echo $video->generate();
<?php
use FriendsOfRedaxo\VidStack\Video;
// Video mit mehrsprachigen Untertiteln
$video = new Video('erklaervideo.mp4', 'Erklärvideo mit Untertiteln');
$video->addSubtitle('untertitel_de.vtt', 'captions', 'Deutsch', 'de', true); // Standard-Untertitel
$video->addSubtitle('untertitel_en.vtt', 'captions', 'Englisch', 'en');
echo $video->generate();
<?php
use FriendsOfRedaxo\VidStack\Video;
// Barrierefreies Video mit zusätzlichen Informationen
$video = new Video('tutorial.mp4', 'Tutorial: REDAXO Installation');
// Ausführliche Beschreibung für Screenreader hinzufügen
$video->setA11yContent(
'Das Video zeigt Schritt für Schritt, wie REDAXO installiert wird. Beginnend mit dem Download bis zur ersten Anmeldung im Backend.',
'https://beispiel.de/redaxo-installation-text.html' // Alternative Text-Version
);
// Kapitelmarken hinzufügen
$video->addSubtitle('chapters.vtt', 'chapters', 'Kapitel', 'de');
echo $video->generateFull();
<?php
use FriendsOfRedaxo\VidStack\Video;
// YouTube-Video mit Datenschutzhinweis
$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'DSGVO-konformes YouTube-Video');
// WICHTIG: Für die Consent-Funktionalität müssen die helper-Dateien im Frontend eingebunden sein!
// Siehe Installation -> Für das Frontend
// generateFull() erzeugt automatisch den DSGVO-konformen Platzhalter für YouTube und Vimeo
echo $video->generateFull();
<?php
use FriendsOfRedaxo\VidStack\Video;
// Video mit Thumbnail-Vorschau beim Hover über die Zeitleiste
$video = new Video('produktvideo.mp4', 'Produktvideo mit Thumbnail-Vorschau');
// VTT-Datei mit Zeitstempeln und Bildpfaden
$video->setThumbnails('thumbnails.vtt');
// Beispiel für eine thumbnails.vtt Datei:
// WEBVTT
//
// 00:00:00.000 --> 00:00:05.000
// thumbnails/img1.jpg
//
// 00:00:05.000 --> 00:00:10.000
// thumbnails/img2.jpg
echo $video->generate();
<?php
use FriendsOfRedaxo\VidStack\Video;
// Audio-Datei einbinden
$audio = new Video('podcast.mp3', 'Podcast Episode #42');
// Audioplayer bekommt automatisch das richtige Layout
echo $audio->generate();
__construct($source, $title = '', $lang = 'de'): void
$source
: URL oder Pfad zum Video (Pflicht)$title
: Titel des Videos (Optional)$lang
: Sprachcode (Optional, Standard: 'de')
setAttributes(array $attributes): void
: Zusätzliche Player-AttributesetA11yContent($description, $alternativeUrl = ''): void
: Barrierefreiheits-InfossetThumbnails($thumbnailsUrl): void
: Thumbnail-Vorschaubilder (VTT-Format)setPoster($posterSrc, $posterAlt): void
: Poster-Bild für das Video setzenaddSubtitle($src, $kind, $label, $lang, $default = false): void
: Untertitel hinzufügengenerateFull(): string
: Vollständiger HTML-Code mit allen Schikanengenerate(): string
: Einfacher Video-Player ohne SchnickschnackisMedia($url): bool
: Prüft, ob es sich um eine Mediendatei handeltisAudio($url): bool
: Prüft, ob es sich um eine Audiodatei handeltvideoOembedHelper(): void
: Registriert einen Output-Filter für oEmbed-TagsparseOembedTags(string $content): string
: Parst oEmbed-Tags im Inhaltshow_sidebar(\rex_extension_point $ep): ?string
: Generiert Medienvorschau für die Sidebar im MedienpoolgetSourceUrl(): string
: Gibt die URL der Videoquelle zurückgetAlternativeUrl(): string
: Gibt eine alternative URL für das Video zurückgetVideoInfo($source): array
: Gibt Informationen über das Video zurück (Plattform und ID) [Statische Methode]generateAttributesString(): string
: Generiert einen String mit allen gesetzten AttributengenerateConsentPlaceholder(string $consentText, string $platform, string $videoId): string
: Generiert einen Platzhalter für die Consent-Abfrage
$source
beim Erstellen des Video-Objekts
$title
beim Erstellen des Video-Objekts$lang
beim Erstellen des Video-Objekts- Alle Attribute in
setAttributes()
- Beschreibung und alternativer URL in
setA11yContent()
- Thumbnail-URL in
setThumbnails()
- Poster-Bild in
setPoster()
- Untertitel-Informationen in
addSubtitle()
Der Video-Player spricht mehr Sprachen als ein UNO-Dolmetscher! Aktuell im Repertoire:
- Deutsch (de)
- Englisch (en)
- Spanisch (es)
- Slowenisch (si)
- Französisch (fr)
Sprachänderung leicht gemacht:
$videoES = new Video('https://www.youtube.com/watch?v=example', 'Mi Video', 'es');
$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'Never Gonna Give You Up', 'en');
$video->setAttributes(['autoplay' => true, 'muted' => true]);
$video->setA11yContent('This is a music video by Rick Astley');
$video->setThumbnails('/pfad/zu/thumbnails.vtt');
$video->setPoster('/pfad/zu/poster.jpg', 'Rick Astley dancing');
$video->addSubtitle('/untertitel/deutsch.vtt', 'captions', 'Deutsch', 'de', true);
$video->addSubtitle('/untertitel/english.vtt', 'captions', 'English', 'en');
echo $video->generateFull();
$video = new Video('/pfad/zu/katzen_spielen_schach.mp4', 'Schachgenies');
echo $video->generate();
$video = new Video('https://vimeo.com/148751763', 'Vimeo-Meisterwerk', 'fr');
$video->setThumbnails('/vimeo_thumbs.vtt');
$video->setPoster('/vimeo_poster.jpg', 'Video thumbnail');
$video->addSubtitle('/sous-titres.vtt', 'captions', 'Français', 'fr', true);
echo $video->generateFull();
Aufwendig und zu teuer Hier kommt der Königsklasse-Einsatz - alle Funktionen auf einmal:
<?php
use FriendsOfRedaxo\VidStack\Video;
// Initialisierung des Video-Objekts
$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'Ultimate Rickroll Experience', 'en');
// Setzen aller möglichen Player-Attribute
$video->setAttributes([
'autoplay' => false,
'muted' => false,
'loop' => true,
'playsinline' => true,
'crossorigin' => 'anonymous',
'preload' => 'metadata',
'controlsList' => 'nodownload',
'class' => 'my-custom-video-class',
'data-custom' => 'some-value'
]);
// Hinzufügen von ausführlichen Barrierefreiheits-Inhalten
$video->setA11yContent(
'This legendary music video features Rick Astley performing "Never Gonna Give You Up". The video begins with Rick, dressed in a black leather jacket, dancing in various locations. The catchy synth-pop tune and Rick\'s distinctive baritone voice have made this song an internet phenomenon.',
'https://example.com/detailed-audio-description'
);
// Setzen von Thumbnail-Vorschaubildern für den Player-Fortschritt
$video->setThumbnails('/pfad/zu/detailed-thumbnails.vtt');
// Setzen des Poster-Bildes
$video->setPoster('/pfad/zu/rickroll_poster.jpg', 'Rick Astley in his iconic pose');
// Hinzufügen von Untertiteln in mehreren Sprachen
$video->addSubtitle('/untertitel/english.vtt', 'captions', 'English', 'en', true);
$video->addSubtitle('/untertitel/deutsch.vtt', 'captions', 'Deutsch', 'de');
$video->addSubtitle('/untertitel/francais.vtt', 'captions', 'Français', 'fr');
$video->addSubtitle('/untertitel/espanol.vtt', 'captions', 'Español', 'es');
$video->addSubtitle('/untertitel/slovenscina.vtt', 'captions', 'Slovenščina', 'si');
// Hinzufügen von Audiodeskription
$video->addSubtitle('/audio/description.vtt', 'descriptions', 'Audio Description', 'en');
// Hinzufügen von Kapitelmarkierungen
$video->addSubtitle('/chapters/rickroll.vtt', 'chapters', 'Chapters', 'en');
// Generieren des vollständigen Video-Player-Codes
$fullPlayerCode = $video->generateFull();
// Ausgabe des generierten Codes
echo $fullPlayerCode;
Dieses Beispiel zeigt die Hauptfunktionalität des Players mit allen verfügbaren Optionen. In den meisten Fällen wird das bereits alles sein, was Sie brauchen.
Die folgenden erweiterten Methoden sind für spezielle Anwendungsfälle gedacht, wenn Sie mehr Kontrolle über den Player benötigen oder eigene Implementierungen erstellen möchten.
<?php
use FriendsOfRedaxo\VidStack\Video;
// Video-Objekt erstellen
$video = new Video('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'Datenschutzkonformes YouTube-Video');
// Plattform und Video-ID ermitteln
$videoInfo = Video::getVideoInfo($video->getSourceUrl());
// Nur wenn es ein YouTube oder Vimeo Video ist, DSGVO-Abfrage anzeigen
if ($videoInfo['platform'] !== 'default') {
// Angepassten Consent-Text erstellen
$consentText = "Um dieses {$videoInfo['platform']}-Video anzusehen, klicken Sie bitte auf 'Video laden'. " .
"Dadurch werden Daten an {$videoInfo['platform']} übermittelt. " .
"Weitere Informationen finden Sie in unserer Datenschutzerklärung.";
// Container mit eigener Klasse für Styling erstellen
echo '<div class="custom-video-consent">';
// Vorschaubild mit Platzhalter anzeigen (nutzt die Video-Klassen-Methode)
echo $video->generateConsentPlaceholder($consentText, $videoInfo['platform'], $videoInfo['id']);
// Informationstext anzeigen
echo '<div class="consent-info">';
echo '<p>Video-Quelle: ' . htmlspecialchars($video->getSourceUrl()) . '</p>';
echo '</div>';
echo '</div>';
} else {
// Bei lokalen Videos direkt anzeigen
echo $video->generate();
}
<?php
use FriendsOfRedaxo\VidStack\Video;
function createTrackedVideo($source, $title = '') {
// Video erstellen
$video = new Video($source, $title);
// Video-Informationen für Analytics-Tracking
$videoInfo = Video::getVideoInfo($video->getSourceUrl());
$platform = $videoInfo['platform'];
$videoId = $videoInfo['id'];
// Standard HTML für den Player generieren
$playerHtml = $video->generate();
// Attribute für das Analytics-Tracking hinzufügen
$trackingAttributes = ' data-tracking="true" data-platform="' . htmlspecialchars($platform) .
'" data-video-id="' . htmlspecialchars($videoId) . '"';
// HTML-Code mit Tracking-Attributen ergänzen
$trackedHtml = str_replace('<media-player', '<media-player' . $trackingAttributes, $playerHtml);
// JavaScript für das Tracking hinzufügen
$trackedHtml .= <<<EOT
<script>
document.addEventListener('DOMContentLoaded', function() {
const player = document.querySelector('media-player[data-tracking="true"]');
if (player) {
player.addEventListener('play', function() {
// Hier Tracking-Code einfügen
console.log('Video gestartet:', player.getAttribute('data-platform'), player.getAttribute('data-video-id'));
});
player.addEventListener('ended', function() {
// Video wurde vollständig angesehen
console.log('Video beendet:', player.getAttribute('data-platform'), player.getAttribute('data-video-id'));
});
}
});
</script>
EOT;
return $trackedHtml;
}
// Verwendung
echo createTrackedVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'Tracking-Demo');
<?php
use FriendsOfRedaxo\VidStack\Video;
function createCustomLayoutVideo($source, $title, $showInfo = true) {
$video = new Video($source, $title);
// Video-Info ermitteln
$videoInfo = $video->getVideoInfo();
$isYouTube = $videoInfo['platform'] === 'youtube';
// Custom Container erstellen
$output = '<div class="custom-video-player">';
// Titel und Info anzeigen, wenn gewünscht
if ($showInfo) {
$output .= '<div class="video-header">';
$output .= '<h3>' . htmlspecialchars($title) . '</h3>';
if ($isYouTube) {
$output .= '<div class="platform-info">Quelle: YouTube</div>';
}
$output .= '</div>';
}
// Player-Container
$output .= '<div class="video-container">';
// Für YouTube wird der Consent-Platzhalter verwendet
if ($isYouTube) {
$consentText = "YouTube-Videos werden erst nach Zustimmung geladen, um Ihre Privatsphäre zu schützen.";
$output .= $video->generateConsentPlaceholder($consentText, 'youtube', $videoInfo['id']);
} else {
// Für lokale Videos normalen Player anzeigen
$output .= $video->generate();
}
$output .= '</div>';
// Custom Controls oder zusätzliche Informationen
if ($showInfo) {
$output .= '<div class="video-footer">';
$output .= '<div class="video-source">Video-URL: ' . htmlspecialchars($video->getSourceUrl()) . '</div>';
$output .= '</div>';
}
$output .= '</div>';
return $output;
}
// Verwendung
echo createCustomLayoutVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'Custom Layout Demo');
<?php
use FriendsOfRedaxo\VidStack\Video;
function createResponsiveVideo($source, $title = '', $isMobile = false) {
$video = new Video($source, $title);
// Auf mobilen Geräten andere Attribute setzen
if ($isMobile) {
$video->setAttributes([
'playsinline' => true,
'preload' => 'none', // Bandbreite sparen
'controlsList' => 'nodownload',
'disablePictureInPicture' => true,
'class' => 'mobile-optimized'
]);
// Einfache Version für mobile Geräte
return $video->generate();
} else {
// Auf Desktop volle Funktionalität
$video->setAttributes([
'class' => 'desktop-enhanced',
'preload' => 'metadata'
]);
// Poster und Untertitel für Desktop hinzufügen
$video->setPoster('/pfad/zu/hq-poster.jpg', 'Video-Vorschau');
$video->addSubtitle('/untertitel/deutsch.vtt', 'captions', 'Deutsch', 'de', true);
return $video->generateFull();
}
}
// Einfache Geräteerkennung (in der Praxis würden Sie hier eine richtige Erkennung verwenden)
$isMobile = strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') !== false;
// Verwendung
echo createResponsiveVideo('https://example.com/video.mp4', 'Responsives Video', $isMobile);
<?php
use FriendsOfRedaxo\VidStack\Video;
// Angenommen, wir haben eine REX_MEDIA-Variable mit einem Video
$mediaName = REX_MEDIA[1];
if ($mediaName) {
$video = new Video($mediaName, 'Video aus dem Medienpool');
// Prüfen, ob es sich um eine Audiodatei handelt
if (Video::isAudio($mediaName)) {
echo '<div class="audio-player-wrapper">';
echo '<h4>Audio-Player</h4>';
echo $video->generate();
echo '</div>';
} else {
// Video mit Standardeinstellungen anzeigen
$video->setAttributes([
'controls' => true,
'playsinline' => true
]);
// Wenn ein Poster-Bild ausgewählt wurde
if (REX_MEDIA[2]) {
$video->setPoster(rex_url::media(REX_MEDIA[2]), 'Vorschaubild');
}
echo $video->generateFull();
}
}
Durch diese praktischen Beispiele wird deutlich, wie die erweiterten Methoden der Video-Klasse sinnvoll in verschiedenen Szenarien eingesetzt werden können, anstatt sie nur isoliert zu demonstrieren.
Wer faul clever ist, baut sich eine Hilfsfunktion für Standardeinstellungen:
function createDefaultVideo($source, $title = '', $a11yContent = null) {
$current_lang = rex_clang::getCurrent();
$lang_code = $current_lang->getCode();
$video = new Video($source, $title, $lang_code);
$video->setAttributes([
'autoplay' => false,
'muted' => true,
'playsinline' => true
]);
if ($a11yContent !== null) {
$video->setA11yContent($a11yContent);
}
$video->setPoster('/pfad/zu/default_poster.jpg', 'Default video poster');
return $video;
}
// Verwendung
$easyVideo = createDefaultVideo('https://youtube.com/watch?v=abcdefg', 'Einfach Genial', 'Ein Video über etwas Interessantes');
echo $easyVideo->generateFull();
Das Addon unterstützt auch die Einbindung von Audio-Dateien. Genauso wie für Videos:
$audio = new Video('audio.mp3', 'Mein Lieblingssong');
echo $audio->generate();
Hier muss man nichts machen - außer Videos schauen.
Leider muss es ja sein.
Hiermit kann man in einem Consent-Manager oder auch so mal zwischendurch die Erlaubnis für Vimeo oder Youtube setzen. Wer keine Cookies erlaubt bekommt halt Local-Storage 😉.
<script>
// YouTube
(()=>{let v=JSON.parse(localStorage.getItem('video_consent')||'{}');v.youtube=true;localStorage.setItem('video_consent',JSON.stringify(v));document.cookie='youtube_consent=true; path=/; max-age=2592000; SameSite=Lax; Secure';})();
// Vimeo
(()=>{let v=JSON.parse(localStorage.getItem('video_consent')||'{}');v.vimeo=true;localStorage.setItem('video_consent',JSON.stringify(v));document.cookie='vimeo_consent=true; path=/; max-age=2592000; SameSite=Lax; Secure';})();
</script>
oder für beide
<script>
// Consent für alle unterstützten Video-Plattformen automatisch setzen
(function() {
// Vorhandene Einstellungen auslesen
let videoConsent = JSON.parse(localStorage.getItem('video_consent') || '{}');
// Consent für alle Plattformen setzen
videoConsent.youtube = true;
videoConsent.vimeo = true;
// Speichern in localStorage
localStorage.setItem('video_consent', JSON.stringify(videoConsent));
// Cookies ebenfalls setzen
document.cookie = 'youtube_consent=true; path=/; max-age=2592000; SameSite=Lax; Secure';
document.cookie = 'vimeo_consent=true; path=/; max-age=2592000; SameSite=Lax; Secure';
// Optional: Event auslösen, um vorhandene Player zu aktualisieren
document.dispatchEvent(new Event('vsrun'));
})();
</script>
(das Plyr-AddOn lässt grüßen)
CKE5 kann ja bekanntlich Videos einbinden, aber liefert nichts für die Ausgabe im Frontend mit. 👋 Hier ist die Lösung:
Einfach im String suchen und umwanden:
echo Video::parseOembedTags($content);
und schon sind die Videos da 😀
…oder in der boot.php vom Project-AddOn (gerne auch im eigenen AddOn) den Outputfilter nutzen.
if (rex::isFrontend()) {
Video::videoOembedHelper();
}
Es soll ja nicht nur vorne schön sein. ❤️ Hier muss man dafür sorgen, dass es ggf. in den Blocks nicht ausgeführt wird.
if (rex::isBackend() && rex_be_controller::getCurrentPagePart(1) == 'content' && !in_array(rex_request::get('function', 'string'), ['add', 'edit'])) {
Video::videoOembedHelper();
}
Jetzt bist du ein Video-Einbettungs-Ninja! Geh raus und mache das Internet zu einem besseren Ort - ein Video nach dem anderen. Und denk dran: Mit großer Macht kommt große Verantwortung (und coole Videos)!
Viel Spaß beim Coden! 🚀👩💻👨💻
Ihr wollt uns sicher mal bei der Weiterentwicklung helfen. Das geht so:
Im Ordner build ist alles drin was man braucht.
- Also forken, lokal runterladen.
- npm install ausführen
- npm npm run build ausführen
- Im Assets-Ordner die Dateien des Dist-Ordners austauschen (Ihr habt richtig gesehen, es gibt auch die reine JS-Variante 😉)
PR erstellen 😀
…fliegt hier so im Repo rum, einfach mal reinschauen. 👀
flowchart TD
A[Start] --> B[Erstelle Video-Objekt mit Dateipfad]
B --> C{Ist es eine gültige Datei?}
C -->|Nein| D[Fehler: Ungültige Datei]
C -->|Ja| E{Ist es ein unterstütztes Format?}
E -->|Nein| F[Fehler: Nicht unterstütztes Format]
E -->|Ja| G[Setze grundlegende Attribute]
G --> H{Ist es ein Video?}
H -->|Ja| I[Setze Video-spezifische Attribute]
H -->|Nein| J[Setze Audio-spezifische Attribute]
I --> K{Poster-Bild angegeben?}
K -->|Ja| L{Ist Poster-Datei gültig?}
L -->|Nein| M[Warnung: Ungültiges Poster]
L -->|Ja| N[Setze Poster-Bild]
K -->|Nein| O[Verwende Standard-Poster]
J --> P[Prüfe auf Untertitel]
N --> P
O --> P
M --> P
P --> Q{Untertitel vorhanden?}
Q -->|Ja| R{Sind Untertitel-Dateien gültig?}
R -->|Nein| S[Warnung: Ungültige Untertitel]
R -->|Ja| T[Füge Untertitel hinzu]
Q -->|Nein| U[Keine Untertitel]
S --> V[Generiere Player-HTML]
T --> V
U --> V
V --> W{HTML erfolgreich generiert?}
W -->|Nein| X[Fehler: HTML-Generierung fehlgeschlagen]
W -->|Ja| Y[Zeige Video/Audio-Player]
Y --> Z[Ende]
D --> Z
F --> Z
X --> Z
Friends Of REDAXO
- http://www.redaxo.org
- https://github.com/FriendsOfREDAXO
- Ein bisschen KI 😎
Projektleitung
Thanks to Vidstack.io