+ + Willkommen bei Aurora Wetter Livecam +
+
+ + Erleben Sie faszinierende Ausblicke der Züricher Region - in Echtzeit! +
+diff --git a/aurora-livecam/index.php b/aurora-livecam/index.php new file mode 100644 index 0000000..567eba6 --- /dev/null +++ b/aurora-livecam/index.php @@ -0,0 +1,2485 @@ +handleAjax(); + +if (isset($_GET['download_video'])) { + $videoDir = './videos/'; + $latestVideo = null; + $latestTime = 0; + + // Finde das neueste Video + foreach (glob($videoDir . '*.mp4') as $video) { + $mtime = filemtime($video); + if ($mtime > $latestTime) { + $latestTime = $mtime; + $latestVideo = $video; + } + } + + if ($latestVideo) { + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="'.basename($latestVideo).'"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + header('Content-Length: ' . filesize($latestVideo)); + readfile($latestVideo); + exit; + } else { + echo "Kein Video zum Herunterladen gefunden."; + exit; + } +} + + + + +// Funktion zur sicheren Umleitung +function safeRedirect($url) { + if (!headers_sent()) { + header("HTTP/1.1 301 Moved Permanently"); + header("Location: " . $url); + } else { + echo ''; + } + exit(); +} + +// Hauptlogik +$oldDomains = [ + 'www.aurora-wetter-lifecam.ch', + 'www.aurora-wetter-livecam.ch' +]; +$newDomain = 'www.aurora-weather-livecam.com'; + +if (in_array($_SERVER['HTTP_HOST'], $oldDomains)) { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; + $newUrl = $protocol . '://' . $newDomain . $_SERVER['REQUEST_URI']; + + // Logging für Debugging + error_log("Umleitung von {$_SERVER['HTTP_HOST']} nach $newUrl"); + + if (!headers_sent()) { + header("HTTP/1.1 301 Moved Permanently"); + header("Location: " . $newUrl); + } else { + echo ''; + } + exit(); +} + + + + + +session_start(); +error_reporting(E_ALL); +ini_set('display_errors', 1); +$imageDir = "./image"; // Angepasst an das Ausgabeverzeichnis des Bash-Skripts +$imageFiles = glob("$imageDir/screenshot_*.jpg"); +rsort($imageFiles); // Sortiert die Dateien in umgekehrter Reihenfolge (neueste zuerst) +$imageFilesJson = json_encode($imageFiles); + + +class ViewerCounter { + private $file = 'active_viewers.json'; + private $timeout = 30; // Zeit in Sekunden, bis ein User als "offline" gilt + + public function handleHeartbeat() { + $ip = md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']); // Anonymisierte ID + $now = time(); + + $viewers = []; + + // 1. Datei lesen (mit Lock für Sicherheit bei vielen Zugriffen) + if (file_exists($this->file)) { + $content = file_get_contents($this->file); + $viewers = json_decode($content, true) ?? []; + } + + // 2. Aktuellen User updaten + $viewers[$ip] = $now; + + // 3. Alte User entfernen & Zählen + $activeCount = 0; + $newViewers = []; + + foreach ($viewers as $userIp => $lastSeen) { + if ($now - $lastSeen < $this->timeout) { + $newViewers[$userIp] = $lastSeen; + $activeCount++; + } + } + + // 4. Speichern + file_put_contents($this->file, json_encode($newViewers)); + + // 5. Ergebnis zurückgeben + header('Content-Type: application/json'); + echo json_encode(['count' => $activeCount]); + exit; + } + + public function getInitialCount() { + if (file_exists($this->file)) { + $viewers = json_decode(file_get_contents($this->file), true) ?? []; + // Nur grob zählen, genaues Update macht das JS sofort nach Laden + return count($viewers); + } + return 1; // Zumindest man selbst ist da + } +} + +// Instanz erstellen +$viewerCounter = new ViewerCounter(); + + + +class WebcamManager { + private $videoSrc = 'test_video.m3u8'; + private $logoPath = 'logo.png'; + + // Zeigt NUR das Video ohne Schnickschnack + public function displayWebcam() { + return ' + '; + } + + // Das ist die neue Anzeige für unten links + public function displayStreamStats() { + return ' +
'; + } + + public function captureSnapshot() { + $outputFile = 'snapshot_' . date('YmdHis') . '.jpg'; + $src = escapeshellarg($this->videoSrc); + $logo = escapeshellarg($this->logoPath); + $command = "ffmpeg -i {$src} -i {$logo} -filter_complex 'overlay=main_w-overlay_w-10:10' -vframes 1 -q:v 2 {$outputFile}"; + exec($command, $output, $returnVar); + if ($returnVar !== 0) return "Fehler"; + + $uploadDir = "uploads/"; + if (!file_exists($uploadDir)) mkdir($uploadDir, 0777, true); + $uploadFile = $uploadDir . $outputFile; + copy($outputFile, $uploadFile); + + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $outputFile . '"'); + readfile($outputFile); + unlink($outputFile); + exit; + } + + public function getImageFiles() { + $imageFiles = glob("image/screenshot_*.jpg"); + if ($imageFiles) rsort($imageFiles); else $imageFiles = []; + return json_encode($imageFiles); + } + + public function captureVideoSequence($duration = 10) { + $outputFile = 'sequence_' . date('YmdHis') . '.mp4'; + $src = escapeshellarg($this->videoSrc); + $logo = escapeshellarg($this->logoPath); + $dur = intval($duration); + $command = "ffmpeg -i {$src} -i {$logo} -filter_complex 'overlay=10:10' -t {$dur} -c:v libx264 -preset fast -crf 23 {$outputFile}"; + exec($command, $output, $returnVar); + if ($returnVar !== 0) return "Fehler"; + + $uploadDir = "uploads/"; + if (!file_exists($uploadDir)) mkdir($uploadDir, 0777, true); + $uploadFile = $uploadDir . $outputFile; + copy($outputFile, $uploadFile); + + header('Content-Type: video/mp4'); + header('Content-Disposition: attachment; filename="' . $outputFile . '"'); + readfile($outputFile); + unlink($outputFile); + exit; + } + + public function getJavaScript() { + return " + document.addEventListener('DOMContentLoaded', function () { + var video = document.getElementById('webcam-player'); + var videoSrc = '{$this->videoSrc}'; + var bitrateBadge = document.getElementById('bitrate-display'); + var bitrateValue = document.getElementById('bitrate-value'); + var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); + + if(video) { + video.controls = false; + if (isIOS) { + video.src = videoSrc; + video.setAttribute('playsinline', ''); + video.setAttribute('webkit-playsinline', ''); + video.muted = true; + if(bitrateBadge) bitrateBadge.style.display = 'none'; + video.addEventListener('loadedmetadata', function() { video.play().catch(console.log); }); + } else if (Hls.isSupported()) { + var hls = new Hls({ enableWorker: !isMobile, lowLatencyMode: false }); + hls.loadSource(videoSrc); + hls.attachMedia(video); + hls.on(Hls.Events.MANIFEST_PARSED, function () { + if (isMobile) video.muted = true; + video.play().catch(console.log); + if(bitrateBadge) bitrateBadge.style.display = 'inline-flex'; + }); + hls.on(Hls.Events.FRAG_LOADED, function(event, data) { + var bandwidth = hls.bandwidthEstimate; + if (bandwidth && !isNaN(bandwidth) && bitrateValue) { + var mbs = bandwidth / 8 / 1024 / 1024; + if (mbs > 0) bitrateValue.textContent = mbs.toFixed(2); + } + }); + } + } + }); + "; + } + + public function setVideoSrc($src) { $this->videoSrc = $src; } +} + + + +class VisualCalendarManager { + private $videoDir; + private $aiDir; + private $monthNames; + private $settingsManager; + + // AI-Kategorien mit Icons und Farben + private $aiCategories = [ + 'sunny' => ['icon' => '☀️', 'name' => 'Sonnig', 'color' => '#FFD700'], + 'rainy' => ['icon' => '🌧️', 'name' => 'Regen', 'color' => '#4682B4'], + 'snowy' => ['icon' => '❄️', 'name' => 'Schnee', 'color' => '#E0FFFF'], + 'planes' => ['icon' => '✈️', 'name' => 'Flugzeuge', 'color' => '#87CEEB'], + 'birds' => ['icon' => '🐦', 'name' => 'Vögel', 'color' => '#98FB98'], + 'sunset' => ['icon' => '🌅', 'name' => 'Sonnenuntergang', 'color' => '#FF6347'], + 'sunrise' => ['icon' => '🌄', 'name' => 'Sonnenaufgang', 'color' => '#FFA07A'], + 'rainbow' => ['icon' => '🌈', 'name' => 'Regenbogen', 'color' => '#FF69B4'], + ]; + + public function __construct($videoDir = './videos/', $aiDir = './ai/', $settingsManager = null) { + $this->videoDir = $videoDir; + $this->aiDir = $aiDir; + $this->settingsManager = $settingsManager; + $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' => '十二月'] + ]; + } + + /** + * Holt AI-Events für ein bestimmtes Datum + */ + public function getAiEventsForDate($year, $month, $day) { + $events = []; + $dateStr = sprintf('%04d%02d%02d', $year, $month, $day); + + foreach ($this->aiCategories as $category => $info) { + $categoryDir = $this->aiDir . $category . '/'; + if (!is_dir($categoryDir)) continue; + + // Suche nach Videos für dieses Datum + $pattern = $categoryDir . "{$category}_{$dateStr}*.mp4"; + $videos = glob($pattern); + + if (!empty($videos)) { + $events[$category] = [ + 'icon' => $info['icon'], + 'name' => $info['name'], + 'color' => $info['color'], + 'videos' => $videos, + 'count' => count($videos) + ]; + } + } + + return $events; + } + + /** + * Prüft ob AI-Events für ein Datum existieren + */ + public function hasAiEventsForDate($year, $month, $day) { + $dateStr = sprintf('%04d%02d%02d', $year, $month, $day); + + foreach (array_keys($this->aiCategories) as $category) { + $pattern = $this->aiDir . $category . "/{$category}_{$dateStr}*.mp4"; + if (count(glob($pattern)) > 0) { + return true; + } + } + return false; + } + + /** + * Holt kurze Icon-Liste für Kalender-Anzeige + */ + public function getAiIconsForDate($year, $month, $day) { + $icons = []; + $events = $this->getAiEventsForDate($year, $month, $day); + + foreach ($events as $category => $info) { + $icons[] = $info['icon']; + } + + return $icons; + } + + 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; + + // Settings für Video-Modus holen + $playInPlayer = $this->settingsManager ? $this->settingsManager->get('video_mode.play_in_player') : true; + $allowDownload = $this->settingsManager ? $this->settingsManager->get('video_mode.allow_download') : true; + + $output = '📭 Keine Videos oder AI-Ereignisse für diesen Tag verfügbar.
'; + $output .= '{$entry['message']}
+ {$entry['date']}"; + if ($isAdmin) { + $output .= ""; + } + + } + $output .= 'Aurora Weather Livecam
✅ Bild erfolgreich hochgeladen.
"; + return true; + } else { + echo "Upload Fehler."; + return false; + } + } + + public function displayLoginForm() { + return ' + '; + } + + public function displayAdminContent() { + global $settingsManager; + + $feedbacks = json_decode(file_get_contents('feedbacks.json') ?: '[]', true); + + // NEUES SETTINGS PANEL + $output = 'Keine Nachrichten vorhanden.
'; + } else { + $output .= '{$feedback['message']}
"; + $output .= "📅 {$feedback['date']} | IP: {$ip}"; + $output .= "Keine Bilder in der Galerie.
'; + } + $output .= '
+
+
+ + Erleben Sie faszinierende Ausblicke der Züricher Region - in Echtzeit! +
+
+ ⛰️ Höhe: ca. 616 m ü.M.
+🧭 Blickrichtung: Südwest (SW)
+📍 Region: Zürich Oberland
++ Folge uns und kopiere den Code und sende es deinen Freunden +
+ + ++ Klicke auf den QR-Code, um die URL zu kopieren +
++ Haben Sie Fragen, Anregungen oder möchten uns unterstützen? Wir freuen uns auf Ihre Nachricht! +
+ displayForm(); ?> ++ Aurora Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten. Wir möchten Ihnen die Schönheit der Natur und Faszination des Wetters näher bringen. +
+Dazu betreiben wir seit 2010 rund um die Uhr hochauflösende Webcams. Besonders stolz sind wir auf einzigartige Einblicke, wie z.B. die Trainingsflüge der Patrouille Suisse jeden Montagmorgen.
+Aurora Wetter Livecam
+M. Kessler
+Dürnten, Schweiz
+Anfragen per Kontaktformular
+