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 = '