diff --git a/aurora-livecam/onboarding/branding.php b/aurora-livecam/onboarding/branding.php
new file mode 100644
index 0000000..830d9ab
--- /dev/null
+++ b/aurora-livecam/onboarding/branding.php
@@ -0,0 +1,253 @@
+isLoggedIn()) {
+ header('Location: /onboarding/register.php');
+ exit;
+}
+
+$user = $auth->getUser();
+$tenantId = $user['tenant_id'] ?? 0;
+
+$error = '';
+$branding = [
+ 'site_name' => $user['tenant_name'] ?? '',
+ 'tagline' => '',
+ 'primary_color' => '#667eea',
+ 'secondary_color' => '#764ba2',
+];
+
+// Formular verarbeiten
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $branding = [
+ 'site_name' => trim($_POST['site_name'] ?? ''),
+ 'site_name_full' => trim($_POST['site_name'] ?? ''),
+ 'tagline' => trim($_POST['tagline'] ?? ''),
+ 'primary_color' => $_POST['primary_color'] ?? '#667eea',
+ 'secondary_color' => $_POST['secondary_color'] ?? '#764ba2',
+ ];
+
+ try {
+ $onboarding = new OnboardingManager();
+ $result = $onboarding->saveBranding($tenantId, $branding);
+
+ if ($result['success']) {
+ header('Location: /onboarding/complete.php');
+ exit;
+ } else {
+ $error = $result['error'] ?? 'Fehler beim Speichern';
+ }
+ } catch (\Exception $e) {
+ $error = 'Fehler: ' . $e->getMessage();
+ }
+}
+
+// Skip
+if (isset($_GET['skip'])) {
+ header('Location: /onboarding/complete.php');
+ exit;
+}
+?>
+
+
+
+
+
+ Branding - Aurora Livecam
+
+
+
+
+
+
+
+
+
diff --git a/aurora-livecam/onboarding/complete.php b/aurora-livecam/onboarding/complete.php
new file mode 100644
index 0000000..3696d4d
--- /dev/null
+++ b/aurora-livecam/onboarding/complete.php
@@ -0,0 +1,237 @@
+isLoggedIn()) {
+ header('Location: /onboarding/register.php');
+ exit;
+}
+
+$user = $auth->getUser();
+$tenantId = $user['tenant_id'] ?? 0;
+
+// Onboarding abschliessen
+try {
+ $onboarding = new OnboardingManager();
+ $onboarding->complete($tenantId);
+} catch (\Exception $e) {
+ // Ignorieren wenn DB nicht verfügbar
+}
+
+// Tenant-Info laden
+$tenantSlug = 'demo';
+$subdomain = '';
+
+try {
+ $db = Database::getInstance();
+ $tenant = $db->fetchOne("SELECT slug FROM tenants WHERE id = ?", [$tenantId]);
+ if ($tenant) {
+ $tenantSlug = $tenant['slug'];
+ $subdomain = $tenantSlug . '.aurora-livecam.com';
+ }
+} catch (\Exception $e) {
+ // Fallback
+}
+?>
+
+
+
+
+
+ Fertig! - Aurora Livecam
+
+
+
+
+
+
+
+
+
🎉
+
Herzlichen Glückwunsch!
+
Ihre Livecam ist jetzt eingerichtet und bereit.
+
+
+
+
+
https://
+
+
+
+
+
+
+
Nächste Schritte
+
+ - Stream-URL im Dashboard anpassen (falls noch nicht geschehen)
+ - Logo und Farben im Branding-Bereich hochladen
+ - Wetter-Widget konfigurieren
+ - Eigene Domain verbinden (optional)
+ isBillingEnabled()): ?>
+ - Abo auswählen für mehr Funktionen
+
+
+
+
+
+
+
+
+
diff --git a/aurora-livecam/onboarding/register.php b/aurora-livecam/onboarding/register.php
new file mode 100644
index 0000000..3649dae
--- /dev/null
+++ b/aurora-livecam/onboarding/register.php
@@ -0,0 +1,265 @@
+isSelfRegistrationEnabled()) {
+ header('Location: /');
+ exit;
+}
+
+$auth = new AuthManager();
+
+// Bereits eingeloggt?
+if ($auth->isLoggedIn()) {
+ header('Location: /dashboard/');
+ exit;
+}
+
+$errors = [];
+$formData = [];
+$success = false;
+
+// Formular verarbeiten
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $formData = [
+ 'name' => trim($_POST['name'] ?? ''),
+ 'company_name' => trim($_POST['company_name'] ?? ''),
+ 'email' => trim($_POST['email'] ?? ''),
+ 'password' => $_POST['password'] ?? '',
+ 'password_confirm' => $_POST['password_confirm'] ?? '',
+ 'stream_url' => trim($_POST['stream_url'] ?? ''),
+ 'accept_terms' => isset($_POST['accept_terms']),
+ ];
+
+ try {
+ $onboarding = new OnboardingManager();
+ $result = $onboarding->register($formData);
+
+ if ($result['success']) {
+ // Session starten und User einloggen
+ $auth->login($formData['email'], $formData['password']);
+
+ // Zur nächsten Seite weiterleiten
+ if ($onboarding->requiresEmailVerification()) {
+ // Token für Demo-Zwecke in Session speichern
+ $_SESSION['verification_token'] = $result['verification_token'];
+ header('Location: /onboarding/verify.php');
+ } else {
+ header('Location: /onboarding/stream.php');
+ }
+ exit;
+ } else {
+ $errors = $result['errors'];
+ }
+ } catch (\Exception $e) {
+ $errors['general'] = 'Registrierung fehlgeschlagen: ' . $e->getMessage();
+ }
+}
+
+$trialDays = $settingsManager->getTrialDays();
+?>
+
+
+
+
+
+ Registrierung - Aurora Livecam
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bereits registriert?
+ Anmelden
+
+
+
+
+
diff --git a/aurora-livecam/onboarding/stream.php b/aurora-livecam/onboarding/stream.php
new file mode 100644
index 0000000..0a4899a
--- /dev/null
+++ b/aurora-livecam/onboarding/stream.php
@@ -0,0 +1,265 @@
+isLoggedIn()) {
+ header('Location: /onboarding/register.php');
+ exit;
+}
+
+$user = $auth->getUser();
+$tenantId = $user['tenant_id'] ?? 0;
+
+$error = '';
+$streamUrl = '';
+$streamType = 'hls';
+$validationResult = null;
+
+// Formular verarbeiten
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $streamUrl = trim($_POST['stream_url'] ?? '');
+ $streamType = $_POST['stream_type'] ?? 'hls';
+
+ if (empty($streamUrl)) {
+ $error = 'Bitte geben Sie eine Stream-URL ein';
+ } else {
+ try {
+ // Stream validieren
+ $validator = new StreamValidator();
+ $validationResult = $validator->validate($streamUrl);
+
+ if ($validationResult['valid']) {
+ // Speichern
+ $onboarding = new OnboardingManager();
+ $result = $onboarding->saveStream($tenantId, $streamUrl, $streamType);
+
+ if ($result['success']) {
+ header('Location: /onboarding/branding.php');
+ exit;
+ } else {
+ $error = $result['error'];
+ }
+ } else {
+ $error = $validationResult['error'] ?? 'Stream-URL konnte nicht validiert werden';
+ }
+ } catch (\Exception $e) {
+ $error = 'Fehler: ' . $e->getMessage();
+ }
+ }
+}
+
+// Skip erlauben
+if (isset($_GET['skip'])) {
+ header('Location: /onboarding/branding.php');
+ exit;
+}
+?>
+
+
+
+
+
+ Stream einrichten - Aurora Livecam
+
+
+
+
+
+
+
+
+
diff --git a/aurora-livecam/onboarding/verify.php b/aurora-livecam/onboarding/verify.php
new file mode 100644
index 0000000..080be0e
--- /dev/null
+++ b/aurora-livecam/onboarding/verify.php
@@ -0,0 +1,214 @@
+isLoggedIn()) {
+ header('Location: /onboarding/register.php');
+ exit;
+}
+
+$user = $auth->getUser();
+$message = '';
+$error = '';
+$verified = false;
+
+// Token aus URL verarbeiten
+if (isset($_GET['token'])) {
+ try {
+ $onboarding = new OnboardingManager();
+ $result = $onboarding->verifyEmail($_GET['token']);
+
+ if ($result['success']) {
+ $verified = true;
+ $message = 'E-Mail erfolgreich verifiziert!';
+ } else {
+ $error = $result['error'];
+ }
+ } catch (\Exception $e) {
+ $error = 'Verifikation fehlgeschlagen';
+ }
+}
+
+// E-Mail erneut senden
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['resend'])) {
+ try {
+ $onboarding = new OnboardingManager();
+ $result = $onboarding->resendVerification($user['id']);
+
+ if ($result['success']) {
+ $_SESSION['verification_token'] = $result['token'];
+ $message = 'Verifikations-E-Mail wurde erneut gesendet!';
+ } else {
+ $error = $result['error'];
+ }
+ } catch (\Exception $e) {
+ $error = 'Fehler beim Senden';
+ }
+}
+
+// Demo: Token anzeigen (in Produktion würde eine E-Mail gesendet)
+$demoToken = $_SESSION['verification_token'] ?? null;
+?>
+
+
+
+
+
+ E-Mail verifizieren - Aurora Livecam
+
+
+
+
+
+
+
+
+
+
✅
+
E-Mail verifiziert!
+
Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
+
+ Weiter zur Stream-Konfiguration
+
+
+
📧
+
E-Mail bestätigen
+
+ Wir haben eine Bestätigungs-E-Mail an
+
+ gesendet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keine E-Mail erhalten?
+
+
+
+
+
+
+
+ Abmelden
+
+
+
+
+
+
diff --git a/aurora-livecam/src/Onboarding/OnboardingManager.php b/aurora-livecam/src/Onboarding/OnboardingManager.php
new file mode 100644
index 0000000..98bd38c
--- /dev/null
+++ b/aurora-livecam/src/Onboarding/OnboardingManager.php
@@ -0,0 +1,366 @@
+db = $db ?? Database::getInstance();
+ $this->tenantManager = new TenantManager($this->db);
+ $this->streamValidator = new StreamValidator();
+ }
+
+ /**
+ * Startet den Onboarding-Prozess (Registrierung)
+ */
+ public function register(array $data): array
+ {
+ $errors = $this->validateRegistration($data);
+
+ if (!empty($errors)) {
+ return ['success' => false, 'errors' => $errors];
+ }
+
+ try {
+ $this->db->beginTransaction();
+
+ // Tenant erstellen
+ $tenantId = $this->tenantManager->create([
+ 'name' => $data['company_name'] ?? $data['name'],
+ 'email' => $data['email'],
+ 'subdomain' => $this->generateSubdomain($data['company_name'] ?? $data['name']),
+ 'stream_url' => $data['stream_url'] ?? '',
+ 'stream_type' => $data['stream_type'] ?? 'hls',
+ ]);
+
+ // Admin-User für den Tenant erstellen
+ $auth = new AuthManager($this->db);
+ $userId = $auth->register([
+ 'tenant_id' => $tenantId,
+ 'email' => $data['email'],
+ 'password' => $data['password'],
+ 'name' => $data['name'],
+ 'role' => 'tenant_admin',
+ ]);
+
+ // Verification-Token generieren
+ $verificationToken = $this->generateVerificationToken($userId);
+
+ $this->db->commit();
+
+ return [
+ 'success' => true,
+ 'tenant_id' => $tenantId,
+ 'user_id' => $userId,
+ 'verification_token' => $verificationToken,
+ 'next_step' => self::STEP_VERIFY_EMAIL,
+ ];
+
+ } catch (\Exception $e) {
+ $this->db->rollback();
+ return ['success' => false, 'errors' => ['general' => $e->getMessage()]];
+ }
+ }
+
+ /**
+ * Validiert Registrierungsdaten
+ */
+ private function validateRegistration(array $data): array
+ {
+ $errors = [];
+
+ // Name
+ if (empty($data['name'])) {
+ $errors['name'] = 'Name ist erforderlich';
+ }
+
+ // Company/Site Name
+ if (empty($data['company_name'])) {
+ $errors['company_name'] = 'Firmen-/Site-Name ist erforderlich';
+ }
+
+ // Email
+ if (empty($data['email'])) {
+ $errors['email'] = 'E-Mail ist erforderlich';
+ } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
+ $errors['email'] = 'Ungültige E-Mail-Adresse';
+ } else {
+ // Prüfe ob Email bereits existiert
+ $existing = $this->db->fetchOne("SELECT id FROM users WHERE email = ?", [strtolower($data['email'])]);
+ if ($existing) {
+ $errors['email'] = 'Diese E-Mail-Adresse ist bereits registriert';
+ }
+ }
+
+ // Password
+ if (empty($data['password'])) {
+ $errors['password'] = 'Passwort ist erforderlich';
+ } elseif (strlen($data['password']) < 8) {
+ $errors['password'] = 'Passwort muss mindestens 8 Zeichen lang sein';
+ }
+
+ // Password Confirmation
+ if (($data['password'] ?? '') !== ($data['password_confirm'] ?? '')) {
+ $errors['password_confirm'] = 'Passwörter stimmen nicht überein';
+ }
+
+ // Stream URL (optional, aber wenn angegeben, validieren)
+ if (!empty($data['stream_url'])) {
+ $validation = $this->streamValidator->validate($data['stream_url']);
+ if (!$validation['valid']) {
+ $errors['stream_url'] = $validation['error'] ?? 'Stream-URL ungültig';
+ }
+ }
+
+ // Terms
+ if (empty($data['accept_terms'])) {
+ $errors['accept_terms'] = 'Sie müssen die AGB akzeptieren';
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Generiert eine Subdomain aus dem Firmennamen
+ */
+ private function generateSubdomain(string $name): string
+ {
+ // Umlaute ersetzen
+ $replacements = ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss'];
+ $slug = str_replace(array_keys($replacements), array_values($replacements), strtolower($name));
+
+ // Nur alphanumerische Zeichen und Bindestriche
+ $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
+ $slug = trim($slug, '-');
+
+ // Max 30 Zeichen
+ $slug = substr($slug, 0, 30);
+
+ // Eindeutigkeit prüfen
+ $baseSlug = $slug;
+ $counter = 1;
+ while (!$this->tenantManager->isDomainAvailable($slug . '.aurora-livecam.com')) {
+ $slug = $baseSlug . '-' . $counter;
+ $counter++;
+ }
+
+ return $slug;
+ }
+
+ /**
+ * Generiert einen E-Mail-Verification-Token
+ */
+ private function generateVerificationToken(int $userId): string
+ {
+ $token = bin2hex(random_bytes(32));
+
+ // Token in einer separaten Tabelle speichern (oder im User)
+ // Vereinfacht: Wir nutzen remember_token temporär
+ $this->db->update('users', ['remember_token' => hash('sha256', $token)], 'id = ?', [$userId]);
+
+ return $token;
+ }
+
+ /**
+ * Verifiziert E-Mail-Adresse
+ */
+ public function verifyEmail(string $token): array
+ {
+ $hashedToken = hash('sha256', $token);
+
+ $user = $this->db->fetchOne(
+ "SELECT id, tenant_id FROM users WHERE remember_token = ? AND email_verified_at IS NULL",
+ [$hashedToken]
+ );
+
+ if (!$user) {
+ return ['success' => false, 'error' => 'Ungültiger oder abgelaufener Token'];
+ }
+
+ $this->db->update('users', [
+ 'email_verified_at' => date('Y-m-d H:i:s'),
+ 'remember_token' => null,
+ ], 'id = ?', [$user['id']]);
+
+ // Onboarding-Status aktualisieren
+ $this->updateOnboardingStep($user['tenant_id'], self::STEP_STREAM);
+
+ return [
+ 'success' => true,
+ 'user_id' => $user['id'],
+ 'tenant_id' => $user['tenant_id'],
+ 'next_step' => self::STEP_STREAM,
+ ];
+ }
+
+ /**
+ * Speichert Stream-Konfiguration
+ */
+ public function saveStream(int $tenantId, string $url, string $type = 'hls'): array
+ {
+ // Validieren
+ $validation = $this->streamValidator->validate($url);
+
+ if (!$validation['valid']) {
+ return ['success' => false, 'error' => $validation['error']];
+ }
+
+ // Speichern
+ $existing = $this->db->fetchOne(
+ "SELECT id FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1",
+ [$tenantId]
+ );
+
+ if ($existing) {
+ $this->db->update('tenant_streams', [
+ 'stream_url' => $url,
+ 'stream_type' => $validation['type'] ?? $type,
+ 'last_status' => 'online',
+ 'last_check_at' => date('Y-m-d H:i:s'),
+ ], 'id = ?', [$existing['id']]);
+ } else {
+ $this->db->insert('tenant_streams', [
+ 'tenant_id' => $tenantId,
+ 'stream_url' => $url,
+ 'stream_type' => $validation['type'] ?? $type,
+ 'is_primary' => 1,
+ 'last_status' => 'online',
+ 'last_check_at' => date('Y-m-d H:i:s'),
+ ]);
+ }
+
+ // Onboarding-Schritt aktualisieren
+ $this->updateOnboardingStep($tenantId, self::STEP_BRANDING, ['stream_verified' => 1]);
+
+ return [
+ 'success' => true,
+ 'stream_type' => $validation['type'],
+ 'next_step' => self::STEP_BRANDING,
+ ];
+ }
+
+ /**
+ * Speichert Basis-Branding
+ */
+ public function saveBranding(int $tenantId, array $branding): array
+ {
+ $this->tenantManager->updateBranding($tenantId, $branding);
+
+ // Onboarding-Schritt aktualisieren
+ $this->updateOnboardingStep($tenantId, self::STEP_COMPLETE, ['branding_configured' => 1]);
+
+ return [
+ 'success' => true,
+ 'next_step' => self::STEP_COMPLETE,
+ ];
+ }
+
+ /**
+ * Schliesst das Onboarding ab
+ */
+ public function complete(int $tenantId): array
+ {
+ $this->db->update('tenant_onboarding', [
+ 'current_step' => self::STEP_COMPLETE,
+ 'completed_at' => date('Y-m-d H:i:s'),
+ ], 'tenant_id = ?', [$tenantId]);
+
+ // Tenant aktivieren
+ $this->tenantManager->activate($tenantId);
+
+ return ['success' => true, 'completed' => true];
+ }
+
+ /**
+ * Aktualisiert den Onboarding-Schritt
+ */
+ private function updateOnboardingStep(int $tenantId, int $step, array $extra = []): void
+ {
+ $data = array_merge(['current_step' => $step], $extra);
+ $this->db->update('tenant_onboarding', $data, 'tenant_id = ?', [$tenantId]);
+ }
+
+ /**
+ * Gibt den aktuellen Onboarding-Status zurück
+ */
+ public function getStatus(int $tenantId): array
+ {
+ $onboarding = $this->db->fetchOne(
+ "SELECT * FROM tenant_onboarding WHERE tenant_id = ?",
+ [$tenantId]
+ );
+
+ if (!$onboarding) {
+ return [
+ 'current_step' => self::STEP_REGISTER,
+ 'completed' => false,
+ ];
+ }
+
+ return [
+ 'current_step' => (int)$onboarding['current_step'],
+ 'stream_verified' => (bool)$onboarding['stream_verified'],
+ 'branding_configured' => (bool)$onboarding['branding_configured'],
+ 'payment_configured' => (bool)$onboarding['payment_configured'],
+ 'completed' => $onboarding['completed_at'] !== null,
+ 'completed_at' => $onboarding['completed_at'],
+ ];
+ }
+
+ /**
+ * Prüft ob E-Mail-Verification erforderlich ist
+ */
+ public function requiresEmailVerification(): bool
+ {
+ // Aus Settings laden
+ $settingsFile = dirname(__DIR__, 2) . '/SettingsManager.php';
+ if (file_exists($settingsFile)) {
+ require_once $settingsFile;
+ $settings = new \SettingsManager();
+ return $settings->get('saas_features.email_verification_required') ?? true;
+ }
+ return true;
+ }
+
+ /**
+ * Sendet Verification-E-Mail erneut
+ */
+ public function resendVerification(int $userId): array
+ {
+ $user = $this->db->fetchOne("SELECT email, email_verified_at FROM users WHERE id = ?", [$userId]);
+
+ if (!$user) {
+ return ['success' => false, 'error' => 'Benutzer nicht gefunden'];
+ }
+
+ if ($user['email_verified_at']) {
+ return ['success' => false, 'error' => 'E-Mail bereits verifiziert'];
+ }
+
+ $token = $this->generateVerificationToken($userId);
+
+ return [
+ 'success' => true,
+ 'token' => $token,
+ 'email' => $user['email'],
+ ];
+ }
+}
diff --git a/aurora-livecam/src/Onboarding/StreamValidator.php b/aurora-livecam/src/Onboarding/StreamValidator.php
new file mode 100644
index 0000000..786de30
--- /dev/null
+++ b/aurora-livecam/src/Onboarding/StreamValidator.php
@@ -0,0 +1,263 @@
+ false,
+ 'type' => null,
+ 'error' => null,
+ 'details' => [],
+ ];
+
+ // URL-Format prüfen
+ if (!filter_var($url, FILTER_VALIDATE_URL)) {
+ $result['error'] = 'Ungültiges URL-Format';
+ return $result;
+ }
+
+ // Stream-Typ erkennen
+ $type = $this->detectStreamType($url);
+ $result['type'] = $type;
+ $result['details']['detected_type'] = $type;
+
+ // Je nach Typ validieren
+ switch ($type) {
+ case 'hls':
+ return $this->validateHls($url, $result);
+ case 'rtmp':
+ return $this->validateRtmp($url, $result);
+ case 'iframe':
+ return $this->validateIframe($url, $result);
+ default:
+ // Generische HTTP-Prüfung
+ return $this->validateHttp($url, $result);
+ }
+ }
+
+ /**
+ * Erkennt den Stream-Typ anhand der URL
+ */
+ public function detectStreamType(string $url): string
+ {
+ $url = strtolower($url);
+
+ if (str_contains($url, '.m3u8')) {
+ return 'hls';
+ }
+
+ if (str_starts_with($url, 'rtmp://') || str_starts_with($url, 'rtmps://')) {
+ return 'rtmp';
+ }
+
+ if (str_contains($url, 'youtube.com') || str_contains($url, 'youtu.be') ||
+ str_contains($url, 'vimeo.com') || str_contains($url, 'twitch.tv')) {
+ return 'iframe';
+ }
+
+ if (str_contains($url, '.mp4') || str_contains($url, '.webm')) {
+ return 'video';
+ }
+
+ return 'unknown';
+ }
+
+ /**
+ * Validiert HLS-Stream
+ */
+ private function validateHls(string $url, array $result): array
+ {
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => $this->timeout,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_SSL_VERIFYPEER => false,
+ CURLOPT_HTTPHEADER => [
+ 'User-Agent: Mozilla/5.0 (compatible; StreamValidator/1.0)'
+ ],
+ ]);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ $result['details']['http_code'] = $httpCode;
+ $result['details']['content_type'] = $contentType;
+
+ if ($error) {
+ $result['error'] = 'Verbindungsfehler: ' . $error;
+ return $result;
+ }
+
+ if ($httpCode !== 200) {
+ $result['error'] = "HTTP-Fehler: $httpCode";
+ return $result;
+ }
+
+ // Prüfe ob es ein gültiges M3U8 ist
+ if (!str_contains($response, '#EXTM3U')) {
+ $result['error'] = 'Keine gültige HLS-Playlist gefunden';
+ return $result;
+ }
+
+ $result['valid'] = true;
+ $result['details']['is_master'] = str_contains($response, '#EXT-X-STREAM-INF');
+ $result['details']['segments'] = substr_count($response, '#EXTINF');
+
+ return $result;
+ }
+
+ /**
+ * Validiert RTMP-Stream (nur Format-Check)
+ */
+ private function validateRtmp(string $url, array $result): array
+ {
+ // RTMP kann nicht einfach per HTTP geprüft werden
+ // Wir prüfen nur das Format
+
+ $parsed = parse_url($url);
+
+ if (!isset($parsed['host']) || empty($parsed['host'])) {
+ $result['error'] = 'RTMP-URL enthält keinen gültigen Host';
+ return $result;
+ }
+
+ // DNS-Check
+ $ip = gethostbyname($parsed['host']);
+ if ($ip === $parsed['host']) {
+ $result['error'] = 'RTMP-Host nicht erreichbar (DNS-Fehler)';
+ return $result;
+ }
+
+ $result['valid'] = true;
+ $result['details']['host'] = $parsed['host'];
+ $result['details']['note'] = 'RTMP-Streams können erst zur Laufzeit vollständig validiert werden';
+
+ return $result;
+ }
+
+ /**
+ * Validiert iFrame-Embed URL
+ */
+ private function validateIframe(string $url, array $result): array
+ {
+ // Bekannte Embed-Plattformen
+ $embedPatterns = [
+ 'youtube' => '/(?:youtube\.com\/(?:embed|watch)|youtu\.be)/i',
+ 'vimeo' => '/vimeo\.com/i',
+ 'twitch' => '/(?:twitch\.tv|player\.twitch\.tv)/i',
+ 'dailymotion' => '/dailymotion\.com/i',
+ ];
+
+ $platform = 'unknown';
+ foreach ($embedPatterns as $name => $pattern) {
+ if (preg_match($pattern, $url)) {
+ $platform = $name;
+ break;
+ }
+ }
+
+ $result['details']['platform'] = $platform;
+
+ // HTTP-Check
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => $this->timeout,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_NOBODY => true, // HEAD request
+ CURLOPT_SSL_VERIFYPEER => false,
+ ]);
+
+ curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ $result['details']['http_code'] = $httpCode;
+
+ if ($httpCode >= 200 && $httpCode < 400) {
+ $result['valid'] = true;
+ } else {
+ $result['error'] = "URL nicht erreichbar (HTTP $httpCode)";
+ }
+
+ return $result;
+ }
+
+ /**
+ * Generische HTTP-Validierung
+ */
+ private function validateHttp(string $url, array $result): array
+ {
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => $this->timeout,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_NOBODY => true,
+ CURLOPT_SSL_VERIFYPEER => false,
+ ]);
+
+ curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ $result['details']['http_code'] = $httpCode;
+ $result['details']['content_type'] = $contentType;
+
+ if ($error) {
+ $result['error'] = 'Verbindungsfehler: ' . $error;
+ return $result;
+ }
+
+ if ($httpCode >= 200 && $httpCode < 400) {
+ $result['valid'] = true;
+ } else {
+ $result['error'] = "URL nicht erreichbar (HTTP $httpCode)";
+ }
+
+ return $result;
+ }
+
+ /**
+ * Schnelle Erreichbarkeitsprüfung
+ */
+ public function isReachable(string $url): bool
+ {
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 5,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_NOBODY => true,
+ CURLOPT_SSL_VERIFYPEER => false,
+ ]);
+
+ curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ return $httpCode >= 200 && $httpCode < 400;
+ }
+}