diff --git a/index.php b/index.php index 66eea26..e95ec64 100644 --- a/index.php +++ b/index.php @@ -116,7 +116,7 @@ class WebcamManager { } else { - } + } @@ -126,10 +126,23 @@ class WebcamManager { unlink($outputFile); exit; } - public function getImageFiles() { - $imageFiles = glob("image/*.{jpg,jpeg,png,gif}", GLOB_BRACE); - return json_encode($imageFiles); - } +public function getImageFiles() { + // Nur JPG-Dateien aus uploads/, KEINE MP4-Dateien + $imageFiles = glob("uploads/*.{jpg,jpeg,png,gif}", GLOB_BRACE); + + // Filtere unerwünschte Dateien aus + $imageFiles = array_filter($imageFiles, function($file) { + $basename = basename($file); + // Blockiere sequence_*.mp4 und andere unerwünschte Dateien + return pathinfo($file, PATHINFO_EXTENSION) !== 'mp4' && + strpos($basename, 'sequence_') !== 0; + }); + + return json_encode(array_values($imageFiles)); +} + + + public function captureVideoSequence($duration = 10) { $outputFile = 'sequence_' . date('YmdHis') . '.mp4'; @@ -163,34 +176,300 @@ class WebcamManager { unlink($outputFile); exit; } + public function getJavaScript() { - return " - document.addEventListener('DOMContentLoaded', function () { - var video = document.getElementById('webcam-player'); - var videoSrc = '{$this->videoSrc}'; - if (Hls.isSupported()) { - var hls = new Hls(); - hls.loadSource(videoSrc); - hls.attachMedia(video); - hls.on(Hls.Events.MANIFEST_PARSED, function () { - video.play(); + return " + document.addEventListener('DOMContentLoaded', function () { + var video = document.getElementById('webcam-player'); + var videoSrc = '{$this->videoSrc}'; + + if (Hls.isSupported()) { + var hls = new Hls({ + // WICHTIG: Live-Stream-Einstellungen + liveSyncDurationCount: 3, // Halte 3 Segmente Abstand zum Live-Edge + liveMaxLatencyDurationCount: 10, // Max 10 Segmente hinter Live + liveDurationInfinity: true, // Stream hat kein Ende + enableWorker: true, + lowLatencyMode: false, // Stabilität vor Latenz + backBufferLength: 90, // 90 Sekunden Back-Buffer + maxBufferLength: 60, // 60 Sekunden Forward-Buffer + maxMaxBufferLength: 120, // Max 120 Sekunden + maxBufferSize: 120*1000*1000, // 120MB Buffer + + // Starlink-Optimierungen + manifestLoadingTimeOut: 10000, + manifestLoadingMaxRetry: 5, + levelLoadingTimeOut: 10000, + levelLoadingMaxRetry: 5, + fragLoadingTimeOut: 10000, + fragLoadingMaxRetry: 5 + }); + + hls.loadSource(videoSrc); + hls.attachMedia(video); + + hls.on(Hls.Events.MANIFEST_PARSED, function () { + console.log('Stream geladen'); + + // WICHTIG: Springe zum Live-Edge minus 60 Sekunden + if (hls.liveSyncPosition !== null) { + var targetPosition = hls.liveSyncPosition - 60; + console.log('Setze Position auf: ' + targetPosition + ' (60s vor Live)'); + video.currentTime = targetPosition; + } + + video.play().catch(function(e) { + console.log('Autoplay blockiert'); }); - } - else if (video.canPlayType('application/vnd.apple.mpegurl')) { - video.src = videoSrc; - video.addEventListener('loadedmetadata', function () { - video.play(); - }); - } - }); - "; - } + }); + + // Überwache den Live-Sync + hls.on(Hls.Events.LEVEL_UPDATED, function(event, data) { + console.log('Stream aktualisiert, neue Segmente verfügbar'); + + // Wenn wir zu weit zurückfallen, springe vor + if (video.currentTime < hls.liveSyncPosition - 120) { + console.log('Zu weit zurück, springe näher zum Live-Edge'); + video.currentTime = hls.liveSyncPosition - 60; + } + }); + + // Fehlerbehandlung + hls.on(Hls.Events.ERROR, function(event, data) { + if (data.fatal) { + switch(data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + console.log('Netzwerkfehler - versuche erneut...'); + setTimeout(function() { + hls.startLoad(); + }, 3000); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + console.log('Media-Fehler - Recovery...'); + hls.recoverMediaError(); + break; + default: + console.log('Kritischer Fehler - Neustart...'); + hls.destroy(); + location.reload(); + break; + } + } + }); + + // Automatisches Nachladen erzwingen + setInterval(function() { + if (!video.paused && !video.seeking) { + // Prüfe ob neue Segmente verfügbar sind + if (hls.levels && hls.levels.length > 0) { + var level = hls.levels[hls.currentLevel]; + if (level && level.details && level.details.live) { + console.log('Live-Stream läuft, Edge bei: ' + level.details.edge); + + // Wenn wir am Ende sind, lade neue Segmente + var bufferEnd = 0; + if (video.buffered.length > 0) { + bufferEnd = video.buffered.end(video.buffered.length - 1); + } + + if (bufferEnd - video.currentTime < 10) { + console.log('Buffer niedrig, lade neue Segmente...'); + hls.startLoad(); + } + } + } + } + }, 5000); // Alle 5 Sekunden prüfen + + } else if (video.canPlayType('application/vnd.apple.mpegurl')) { + // Safari/iOS Fallback + video.src = videoSrc; + video.addEventListener('loadedmetadata', function () { + // Für Safari: Setze currentTime zurück für Pseudo-DVR + video.currentTime = Math.max(0, video.duration - 60); + video.play(); + }); + } + }); + "; +} + + + + public function setVideoSrc($src) { $this->videoSrc = $src; } } + + + + + + + + + + +class VisualCalendarManager { + private $videoDir; + private $monthNames; + + public function __construct($videoDir = './image/') { + $this->videoDir = $videoDir; + $this->monthNames = [ + 1 => ['de' => 'Januar', 'en' => 'January', 'it' => 'Gennaio', 'fr' => 'Janvier', 'zh' => '一月'], + 2 => ['de' => 'Februar', 'en' => 'February', 'it' => 'Febbraio', 'fr' => 'Février', 'zh' => '二月'], + 3 => ['de' => 'März', 'en' => 'March', 'it' => 'Marzo', 'fr' => 'Mars', 'zh' => '三月'], + 4 => ['de' => 'April', 'en' => 'April', 'it' => 'Aprile', 'fr' => 'Avril', 'zh' => '四月'], + 5 => ['de' => 'Mai', 'en' => 'May', 'it' => 'Maggio', 'fr' => 'Mai', 'zh' => '五月'], + 6 => ['de' => 'Juni', 'en' => 'June', 'it' => 'Giugno', 'fr' => 'Juin', 'zh' => '六月'], + 7 => ['de' => 'Juli', 'en' => 'July', 'it' => 'Luglio', 'fr' => 'Juillet', 'zh' => '七月'], + 8 => ['de' => 'August', 'en' => 'August', 'it' => 'Agosto', 'fr' => 'Août', 'zh' => '八月'], + 9 => ['de' => 'September', 'en' => 'September', 'it' => 'Settembre', 'fr' => 'Septembre', 'zh' => '九月'], + 10 => ['de' => 'Oktober', 'en' => 'October', 'it' => 'Ottobre', 'fr' => 'Octobre', 'zh' => '十月'], + 11 => ['de' => 'November', 'en' => 'November', 'it' => 'Novembre', 'fr' => 'Novembre', 'zh' => '十一月'], + 12 => ['de' => 'Dezember', 'en' => 'December', 'it' => 'Dicembre', 'fr' => 'Décembre', 'zh' => '十二月'] + ]; + } + + public function getVideosForDate($year, $month, $day) { + $videos = []; + $dateStr = sprintf('%04d%02d%02d', $year, $month, $day); + + foreach (glob($this->videoDir . "daily_video_{$dateStr}_*.mp4") as $video) { + $videos[] = [ + 'path' => $video, + 'filename' => basename($video), + 'filesize' => filesize($video), + 'time' => date('H:i', filemtime($video)) + ]; + } + + return $videos; + } + + public function hasVideosForDate($year, $month, $day) { + $dateStr = sprintf('%04d%02d%02d', $year, $month, $day); + $pattern = $this->videoDir . "daily_video_{$dateStr}_*.mp4"; + return count(glob($pattern)) > 0; + } + + public function displayVisualCalendar() { + $currentYear = isset($_GET['cal_year']) ? intval($_GET['cal_year']) : date('Y'); + $currentMonth = isset($_GET['cal_month']) ? intval($_GET['cal_month']) : date('n'); + $selectedDay = isset($_GET['cal_day']) ? intval($_GET['cal_day']) : null; + + $output = '
'; + + // Navigation + $output .= '
'; + $output .= ''; + $output .= '

' . $this->monthNames[$currentMonth]['de'] . ' ' . $currentYear . '

'; + $output .= ''; + $output .= '
'; + + // Kalender-Grid + $output .= '
'; + + // Wochentage Header + $weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; + foreach ($weekdays as $day) { + $output .= '
' . $day . '
'; + } + + // Tage des Monats + $firstDay = mktime(0, 0, 0, $currentMonth, 1, $currentYear); + $daysInMonth = date('t', $firstDay); + $dayOfWeek = date('N', $firstDay) - 1; // 0 = Montag + + // Leere Zellen vor dem ersten Tag + for ($i = 0; $i < $dayOfWeek; $i++) { + $output .= '
'; + } + + // Tage mit Videos markieren + for ($day = 1; $day <= $daysInMonth; $day++) { + $hasVideos = $this->hasVideosForDate($currentYear, $currentMonth, $day); + $isSelected = ($selectedDay == $day); + $isToday = ($currentYear == date('Y') && $currentMonth == date('n') && $day == date('j')); + + $classes = 'calendar-day'; + if ($hasVideos) $classes .= ' has-video'; + if ($isSelected) $classes .= ' selected'; + if ($isToday) $classes .= ' today'; + + $output .= '
'; + $output .= '' . $day . ''; + if ($hasVideos) { + $output .= '📹'; + } + $output .= '
'; + } + + $output .= '
'; // calendar-grid + + // Video-Liste für ausgewählten Tag + if ($selectedDay) { + $videos = $this->getVideosForDate($currentYear, $currentMonth, $selectedDay); + if (!empty($videos)) { + $output .= '
'; + $output .= '

Videos vom ' . sprintf('%02d.%02d.%04d', $selectedDay, $currentMonth, $currentYear) . '

'; + $output .= ''; + $output .= '
'; + } else { + $output .= '
Keine Videos für diesen Tag verfügbar.
'; + } + } + + $output .= '
'; // visual-calendar-container + + return $output; + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + class GuestbookManager { private $entries = []; @@ -450,15 +729,24 @@ class AdminManager { } public function displayGalleryImages() { - $output = ''; + return $output; +} + + + + public function handleSocialMediaUpdate($platform, $url) { $socialLinks = json_decode(file_get_contents('social_links.json') ?: '{}', true); $socialLinks[$platform] = $url; @@ -468,6 +756,12 @@ class AdminManager { +// Weather Bingo Manager +//require_once 'weather_bingo.php'; +//$weatherBingo = new WeatherBingo(); + + + @@ -759,6 +1053,229 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {