diff --git a/aurora-livecam/index.php b/aurora-livecam/index.php
index 78a91c6..975c3b3 100644
--- a/aurora-livecam/index.php
+++ b/aurora-livecam/index.php
@@ -1553,10 +1553,41 @@ nav ul li a:hover { color: #4CAF50; }
}
.zoom-value {
font-weight: 700;
- min-width: 60px;
- text-align: right;
+ min-width: 50px;
+ text-align: center;
color: #333;
}
+.zoom-btn {
+ width: 36px;
+ height: 36px;
+ border: none;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ font-size: 18px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: transform 0.2s, box-shadow 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.zoom-btn:hover {
+ transform: scale(1.1);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+.zoom-btn:active {
+ transform: scale(0.95);
+}
+.video-container {
+ cursor: default;
+}
+.video-container.zoomed {
+ cursor: grab;
+}
+.video-container.zoomed:active {
+ cursor: grabbing;
+}
.tech-stat { justify-self: start; font-family: monospace; color: #555; }
.bitrate-icon { color: #4CAF50; }
@@ -2031,9 +2062,11 @@ body.theme-neo footer {
-
-
- 1x
+
+
+ 1.0x
+
+
@@ -2260,7 +2293,7 @@ document.getElementById('qrcode')?.addEventListener('click', function() {
window.zoomConfig = {
enabled: true,
minZoom: 1,
- maxZoom: 100,
+ maxZoom: 4,
defaultZoom: 1
};
diff --git a/aurora-livecam/indexmiau.php b/aurora-livecam/indexmiau.php
new file mode 100644
index 0000000..04668ba
--- /dev/null
+++ b/aurora-livecam/indexmiau.php
@@ -0,0 +1,721 @@
+ true, 'settings' => $settingsManager->get()]);
+ exit;
+
+ case 'update':
+ $key = $_POST['key'] ?? null;
+ $value = $_POST['value'] ?? null;
+
+ if ($value === 'true') $value = true;
+ if ($value === 'false') $value = false;
+ if (is_numeric($value)) $value = intval($value);
+
+ if ($key && $settingsManager->set($key, $value)) {
+ echo json_encode(['success' => true, 'message' => 'Gespeichert']);
+ } else {
+ echo json_encode(['success' => false, 'message' => 'Fehler']);
+ }
+ exit;
+ }
+}
+
+if (isset($_GET['download_video'])) {
+ $videoDir = './videos/';
+ $latestVideo = null;
+ $latestTime = 0;
+ foreach (glob($videoDir . '*.mp4') as $video) {
+ $mtime = filemtime($video);
+ if ($mtime > $latestTime) { $latestTime = $mtime; $latestVideo = $video; }
+ }
+ if ($latestVideo) {
+ header('Content-Type: application/octet-stream');
+ header('Content-Disposition: attachment; filename="'.basename($latestVideo).'"');
+ header('Content-Length: ' . filesize($latestVideo));
+ readfile($latestVideo);
+ exit;
+ }
+ echo "Kein Video gefunden.";
+ exit;
+}
+
+$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';
+ header("HTTP/1.1 301 Moved Permanently");
+ header("Location: " . $protocol . '://' . $newDomain . $_SERVER['REQUEST_URI']);
+ exit;
+}
+
+session_start();
+error_reporting(E_ALL);
+ini_set('display_errors', 0);
+
+$imageDir = "./image";
+$imageFiles = glob("$imageDir/screenshot_*.jpg");
+if ($imageFiles) rsort($imageFiles);
+$imageFilesJson = json_encode($imageFiles ?: []);
+
+class ViewerCounter {
+ private $file = 'active_viewers.json';
+ private $timeout = 30;
+
+ public function handleHeartbeat() {
+ $ip = md5($_SERVER['REMOTE_ADDR'] . ($_SERVER['HTTP_USER_AGENT'] ?? ''));
+ $now = time();
+ $viewers = file_exists($this->file) ? json_decode(file_get_contents($this->file), true) ?? [] : [];
+ $viewers[$ip] = $now;
+ $active = [];
+ foreach ($viewers as $u => $t) { if ($now - $t < $this->timeout) $active[$u] = $t; }
+ file_put_contents($this->file, json_encode($active));
+ header('Content-Type: application/json');
+ echo json_encode(['count' => count($active)]);
+ exit;
+ }
+
+ public function getInitialCount() {
+ if (file_exists($this->file)) {
+ return max(1, count(json_decode(file_get_contents($this->file), true) ?? []));
+ }
+ return 1;
+ }
+}
+
+$viewerCounter = new ViewerCounter();
+
+class WebcamManager {
+ private $videoSrc = 'test_video.m3u8';
+
+ public function displayWebcam() {
+ return '';
+ }
+
+ public function displayStreamStats() {
+ return '
+ 0.00 MBit/s
+
';
+ }
+
+ public function getImageFiles() {
+ $f = glob("image/screenshot_*.jpg");
+ if ($f) rsort($f);
+ return json_encode($f ?: []);
+ }
+
+ public function getJavaScript() {
+ return "
+ document.addEventListener('DOMContentLoaded', function () {
+ var video = document.getElementById('webcam-player');
+ var videoSrc = '{$this->videoSrc}';
+ if(video && typeof Hls !== 'undefined' && Hls.isSupported()) {
+ var hls = new Hls();
+ hls.loadSource(videoSrc);
+ hls.attachMedia(video);
+ hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play().catch(()=>{}); });
+ } else if (video) {
+ video.src = videoSrc;
+ video.play().catch(()=>{});
+ }
+ });";
+ }
+}
+
+class VisualCalendarManager {
+ private $videoDir, $settingsManager;
+ private $months = [1=>'Jan',2=>'Feb',3=>'Mär',4=>'Apr',5=>'Mai',6=>'Jun',7=>'Jul',8=>'Aug',9=>'Sep',10=>'Okt',11=>'Nov',12=>'Dez'];
+
+ public function __construct($videoDir = './videos/', $sm = null) {
+ $this->videoDir = $videoDir;
+ $this->settingsManager = $sm;
+ }
+
+ public function hasVideosForDate($y, $m, $d) {
+ return count(glob($this->videoDir . sprintf("daily_video_%04d%02d%02d_*.mp4", $y, $m, $d))) > 0;
+ }
+
+ public function getVideosForDate($y, $m, $d) {
+ $vids = [];
+ foreach (glob($this->videoDir . sprintf("daily_video_%04d%02d%02d_*.mp4", $y, $m, $d)) as $v) {
+ $vids[] = ['path' => $v, 'name' => basename($v), 'size' => filesize($v), 'time' => date('H:i', filemtime($v))];
+ }
+ return $vids;
+ }
+
+ public function displayVisualCalendar() {
+ $cy = isset($_GET['cal_year']) ? intval($_GET['cal_year']) : date('Y');
+ $cm = isset($_GET['cal_month']) ? intval($_GET['cal_month']) : date('n');
+ $sd = isset($_GET['cal_day']) ? intval($_GET['cal_day']) : null;
+ $pip = $this->settingsManager ? $this->settingsManager->get('video_mode.play_in_player') : true;
+ $dl = $this->settingsManager ? $this->settingsManager->get('video_mode.allow_download') : true;
+
+ $o = '';
+ $o .= '
'.$this->months[$cm].' '.$cy.'
';
+ $o .= '
';
+ foreach(['Mo','Di','Mi','Do','Fr','Sa','So'] as $wd) $o .= '
'.$wd.'
';
+
+ $fd = mktime(0,0,0,$cm,1,$cy);
+ $dim = date('t', $fd);
+ $dow = date('N', $fd) - 1;
+ for ($i=0; $i<$dow; $i++) $o .= '
';
+
+ for ($d=1; $d<=$dim; $d++) {
+ $hv = $this->hasVideosForDate($cy,$cm,$d);
+ $sel = $sd==$d;
+ $td = ($cy==date('Y') && $cm==date('n') && $d==date('j'));
+ $cls = 'cal-day' . ($hv?' has-vid':'') . ($sel?' sel':'') . ($td?' today':'');
+ $o .= '
'.$d.''.($hv?'📹':'').'
';
+ }
+ $o .= '
';
+
+ if ($sd) {
+ $vids = $this->getVideosForDate($cy,$cm,$sd);
+ $o .= '
📅 '.sprintf('%02d.%02d.%04d',$sd,$cm,$cy).'
';
+ if ($vids) {
+ $o .= '
';
+ foreach ($vids as $v) {
+ $sz = round($v['size']/1024/1024,1);
+ $tk = hash_hmac('sha256', $v['path'], session_id());
+ $o .= '- 🕐 '.$v['time'].''.$sz.' MB';
+ if ($pip) $o .= '▶️';
+ if ($dl) $o .= '⬇️';
+ $o .= '
';
+ }
+ $o .= '
';
+ } else {
+ $o .= '
Keine Videos.
';
+ }
+ $o .= '
';
+ }
+ $o .= '
';
+ return $o;
+ }
+}
+
+class GuestbookManager {
+ private $entries = [], $file = 'guestbook.json';
+ public function __construct() { if (file_exists($this->file)) $this->entries = json_decode(file_get_contents($this->file), true) ?? []; }
+ public function handleFormSubmission() {
+ if (isset($_POST['guestbook'],$_POST['guest-name'],$_POST['guest-message'])) {
+ $this->entries[] = ['name'=>htmlspecialchars($_POST['guest-name']),'message'=>htmlspecialchars($_POST['guest-message']),'date'=>date('Y-m-d H:i:s')];
+ file_put_contents($this->file, json_encode($this->entries));
+ }
+ }
+ public function deleteEntry($i) { if (isset($this->entries[$i])) { unset($this->entries[$i]); $this->entries = array_values($this->entries); file_put_contents($this->file, json_encode($this->entries)); return true; } return false; }
+ public function displayForm() { return ''; }
+ public function displayEntries($admin=false) {
+ $o = '';
+ foreach ($this->entries as $i=>$e) {
+ $o .= '
'.$e['name'].'
'.$e['message'].'
'.$e['date'].'';
+ if ($admin) $o .= '
';
+ $o .= '
';
+ }
+ return $o.'
';
+ }
+}
+
+class ContactManager {
+ private $file = 'feedbacks.json';
+ public function displayForm() { return ''; }
+ public function handleSubmission($n,$e,$m) {
+ if (!$n||!$e||!$m) return ['success'=>false,'message'=>'Alle Felder ausfüllen'];
+ $fb = ['name'=>htmlspecialchars($n),'email'=>filter_var($e,FILTER_SANITIZE_EMAIL),'message'=>htmlspecialchars($m),'date'=>date('Y-m-d H:i:s'),'ip'=>$_SERVER['REMOTE_ADDR']??''];
+ $all = file_exists($this->file) ? json_decode(file_get_contents($this->file),true) : [];
+ $all[] = $fb;
+ file_put_contents($this->file, json_encode($all, JSON_PRETTY_PRINT));
+ return ['success'=>true,'message'=>'Nachricht gesendet!'];
+ }
+ public function deleteFeedback($i) { $all = json_decode(file_get_contents($this->file),true); if (isset($all[$i])) { unset($all[$i]); file_put_contents($this->file, json_encode(array_values($all),JSON_PRETTY_PRINT)); return true; } return false; }
+}
+
+class AdminManager {
+ public function isAdmin() { return isset($_SESSION['admin']) && $_SESSION['admin'] === true; }
+ public function handleLogin($u,$p) { if ($u==='admin' && $p==='sonne4000$$$$Q') { $_SESSION['admin']=true; return true; } return false; }
+ public function displayLoginForm() { return ''; }
+ public function displayAdminContent() {
+ global $settingsManager;
+ $o = '';
+ $o .= '📩 Nachrichten
';
+ $msgs = file_exists('feedbacks.json') ? json_decode(file_get_contents('feedbacks.json'),true) : [];
+ foreach ($msgs as $i=>$m) {
+ $o .= '
'.$m['name'].' ('.$m['email'].')
'.$m['message'].'
'.$m['date'].'';
+ $o .= '
';
+ }
+ if (!$msgs) $o .= '
Keine Nachrichten.
';
+ $o .= '
';
+ return $o;
+ }
+ public function displayGalleryImages() {
+ $o = '';
+ foreach (glob("uploads/*.{jpg,jpeg,png,gif}",GLOB_BRACE) as $f) $o .= '

';
+ return $o.'
';
+ }
+}
+
+class VideoArchiveManager {
+ private $dir;
+ public function __construct($d='./videos/') { $this->dir = $d; }
+ public function handleSpecificVideoDownload() {
+ if (isset($_GET['download_specific_video'],$_GET['token'])) {
+ $p = $_GET['download_specific_video'];
+ if (!hash_equals(hash_hmac('sha256',$p,session_id()), $_GET['token'])) { echo "Invalid"; exit; }
+ $rp = realpath($p);
+ $rd = realpath($this->dir);
+ if ($rp && strpos($rp,$rd)===0 && file_exists($rp)) {
+ header('Content-Type: video/mp4');
+ header('Content-Disposition: attachment; filename="'.basename($rp).'"');
+ header('Content-Length: '.filesize($rp));
+ readfile($rp);
+ exit;
+ }
+ echo "Not found"; exit;
+ }
+ }
+}
+
+$webcamManager = new WebcamManager();
+$guestbookManager = new GuestbookManager();
+$contactManager = new ContactManager();
+$adminManager = new AdminManager();
+$videoArchiveManager = new VideoArchiveManager('./videos/');
+$videoArchiveManager->handleSpecificVideoDownload();
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (isset($_POST['action']) && $_POST['action'] === 'viewer_heartbeat') $viewerCounter->handleHeartbeat();
+ if (isset($_POST['guestbook'])) { $guestbookManager->handleFormSubmission(); header("Location: ".$_SERVER['PHP_SELF']."#guestbook"); exit; }
+ if (isset($_POST['contact'])) {
+ $r = $contactManager->handleSubmission($_POST['name'],$_POST['email'],$_POST['message']);
+ if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { header('Content-Type: application/json'); echo json_encode($r); exit; }
+ header('Location: '.$_SERVER['PHP_SELF'].'#kontakt'); exit;
+ }
+ if (isset($_POST['admin-login'])) { $adminManager->handleLogin($_POST['username'],$_POST['password']); header('Location: '.$_SERVER['PHP_SELF'].'#admin'); exit; }
+ if ($adminManager->isAdmin()) {
+ if (isset($_POST['action']) && $_POST['action']==='delete_guestbook') { $guestbookManager->deleteEntry(intval($_POST['delete_entry'])); header("Location: ".$_SERVER['PHP_SELF']."#guestbook"); exit; }
+ if (isset($_POST['action']) && $_POST['action']==='delete_feedback') { $contactManager->deleteFeedback(intval($_POST['delete_index'])); header("Location: ".$_SERVER['PHP_SELF']."#admin"); exit; }
+ }
+}
+
+$vc = $viewerCounter->getInitialCount();
+$sv = $settingsManager->get('viewer_display.enabled') && $vc >= $settingsManager->get('viewer_display.min_viewers');
+$mv = $settingsManager->get('viewer_display.min_viewers');
+?>
+
+
+
+
+Aurora Livecam
+
+
+
+
+
+
+
+
+
+Aurora Wetter Livecam
+Faszinierende Ausblicke aus dem Zürcher Oberland
+
+
+
+
+
+
+displayWebcam(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+--:--:--
+
+
+
+
+
+
+
+
+displayStreamStats(); ?>
+
Zuschauer
+
+
+
+
+
+
+
+
+
📅 Videoarchiv
+displayVisualCalendar(); ?>
+
+
+
+
+
+
Gästebuch
+displayForm(); echo $guestbookManager->displayEntries($adminManager->isAdmin()); ?>
+
+
+
+
+
+
Kontakt
+displayForm(); ?>
+
+
+
+
+
+
Galerie
+displayGalleryImages(); ?>
+
+
+
+isAdmin()): ?>
+
+
+
⚙️ Admin
+displayAdminContent(); ?>
+
+
+
+
+
+
Admin Login
+displayLoginForm(); ?>
+
+
+
+
+
+
+
+
×
+
![]()
+
+
+
+
+
diff --git a/aurora-livecam/js/video-zoom.js b/aurora-livecam/js/video-zoom.js
index e641760..1994c98 100644
--- a/aurora-livecam/js/video-zoom.js
+++ b/aurora-livecam/js/video-zoom.js
@@ -1,37 +1,207 @@
+/**
+ * Video Zoom & Pan Controller
+ * - Zoom für alle Video-Modi (Live, Timelapse, Tagesvideo)
+ * - Pan-Funktion: Mit Maus den gezoomten Bereich verschieben
+ */
(() => {
const config = window.zoomConfig || {};
if (!config.enabled) return;
- const slider = document.getElementById('zoom-range');
- const valueEl = document.getElementById('zoom-value');
- if (!slider || !valueEl) return;
+ let currentZoom = 1;
+ let panX = 0;
+ let panY = 0;
+ let isDragging = false;
+ let startX = 0;
+ let startY = 0;
const minZoom = Number(config.minZoom || 1);
- const maxZoom = Number(config.maxZoom || 100);
+ const maxZoom = Number(config.maxZoom || 4);
const defaultZoom = Number(config.defaultZoom || 1);
- slider.min = minZoom;
- slider.max = maxZoom;
- slider.value = defaultZoom;
+ const slider = document.getElementById('zoom-range');
+ const valueEl = document.getElementById('zoom-value');
- const targets = [
- document.getElementById('webcam-player'),
- document.getElementById('timelapse-image'),
- document.getElementById('daily-video')
- ].filter(Boolean);
+ // Finde das aktuell aktive Video-Element
+ function getActiveTarget() {
+ const webcam = document.getElementById('webcam-player');
+ const timelapse = document.getElementById('timelapse-image');
+ const daily = document.getElementById('daily-video');
+ const timelapseViewer = document.getElementById('timelapse-viewer');
+ const dailyPlayer = document.getElementById('daily-video-player');
- const applyZoom = (zoomValue) => {
- const zoom = Math.max(minZoom, Math.min(maxZoom, zoomValue));
- valueEl.textContent = `${zoom}x`;
- targets.forEach((el) => {
- el.style.transform = `scale(${zoom})`;
- el.style.transformOrigin = 'center center';
+ // Prüfe welches Element sichtbar ist
+ if (dailyPlayer && dailyPlayer.style.display !== 'none' && daily) {
+ return daily;
+ }
+ if (timelapseViewer && timelapseViewer.style.display !== 'none' && timelapse) {
+ return timelapse;
+ }
+ if (webcam) {
+ return webcam;
+ }
+ return null;
+ }
+
+ // Wende Zoom und Pan auf das aktive Element an
+ function applyTransform() {
+ const target = getActiveTarget();
+ if (!target) return;
+
+ // Bei Zoom 1x: Kein Pan erlaubt
+ if (currentZoom <= 1) {
+ panX = 0;
+ panY = 0;
+ }
+
+ // Begrenzen der Pan-Werte basierend auf Zoom
+ const maxPan = (currentZoom - 1) * 50; // Prozent
+ panX = Math.max(-maxPan, Math.min(maxPan, panX));
+ panY = Math.max(-maxPan, Math.min(maxPan, panY));
+
+ target.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`;
+ target.style.transformOrigin = 'center center';
+ target.style.transition = isDragging ? 'none' : 'transform 0.2s ease';
+
+ // Update UI
+ if (valueEl) valueEl.textContent = `${currentZoom.toFixed(1)}x`;
+ if (slider) slider.value = currentZoom;
+ }
+
+ // Zoom setzen
+ function setZoom(value) {
+ currentZoom = Math.max(minZoom, Math.min(maxZoom, value));
+ applyTransform();
+ }
+
+ // Zoom anpassen
+ function adjustZoom(delta) {
+ setZoom(currentZoom + delta);
+ }
+
+ // Zoom zurücksetzen
+ function resetZoom() {
+ currentZoom = 1;
+ panX = 0;
+ panY = 0;
+ applyTransform();
+ }
+
+ // Mouse Events für Pan
+ function setupPanEvents() {
+ const container = document.querySelector('.video-container');
+ if (!container) return;
+
+ container.addEventListener('mousedown', (e) => {
+ if (currentZoom <= 1) return;
+ isDragging = true;
+ startX = e.clientX;
+ startY = e.clientY;
+ container.style.cursor = 'grabbing';
+ e.preventDefault();
});
- };
- applyZoom(defaultZoom);
+ document.addEventListener('mousemove', (e) => {
+ if (!isDragging) return;
- slider.addEventListener('input', (event) => {
- applyZoom(Number(event.target.value));
+ const dx = (e.clientX - startX) / 5; // Sensitivität anpassen
+ const dy = (e.clientY - startY) / 5;
+
+ panX += dx / currentZoom;
+ panY += dy / currentZoom;
+
+ startX = e.clientX;
+ startY = e.clientY;
+
+ applyTransform();
+ });
+
+ document.addEventListener('mouseup', () => {
+ if (isDragging) {
+ isDragging = false;
+ const container = document.querySelector('.video-container');
+ if (container) container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
+ }
+ });
+
+ // Touch Events für Mobile
+ container.addEventListener('touchstart', (e) => {
+ if (currentZoom <= 1 || e.touches.length !== 1) return;
+ isDragging = true;
+ startX = e.touches[0].clientX;
+ startY = e.touches[0].clientY;
+ }, { passive: true });
+
+ container.addEventListener('touchmove', (e) => {
+ if (!isDragging || e.touches.length !== 1) return;
+
+ const dx = (e.touches[0].clientX - startX) / 5;
+ const dy = (e.touches[0].clientY - startY) / 5;
+
+ panX += dx / currentZoom;
+ panY += dy / currentZoom;
+
+ startX = e.touches[0].clientX;
+ startY = e.touches[0].clientY;
+
+ applyTransform();
+ }, { passive: true });
+
+ container.addEventListener('touchend', () => {
+ isDragging = false;
+ });
+
+ // Cursor anpassen bei Zoom
+ container.style.cursor = 'default';
+ }
+
+ // Slider Events
+ function setupSlider() {
+ if (!slider) return;
+
+ slider.min = minZoom;
+ slider.max = maxZoom;
+ slider.step = 0.5;
+ slider.value = defaultZoom;
+
+ slider.addEventListener('input', (e) => {
+ setZoom(Number(e.target.value));
+ });
+ }
+
+ // Globale Funktionen für Buttons
+ window.adjustZoom = adjustZoom;
+ window.resetZoom = resetZoom;
+ window.setZoom = setZoom;
+
+ // Initialisierung
+ document.addEventListener('DOMContentLoaded', () => {
+ setupSlider();
+ setupPanEvents();
+ currentZoom = defaultZoom;
+
+ // Warte kurz, damit Video-Elemente geladen sind
+ setTimeout(() => {
+ applyTransform();
+ }, 500);
+
+ // Update Cursor bei Zoom-Änderung
+ const container = document.querySelector('.video-container');
+ if (container) {
+ const observer = new MutationObserver(() => {
+ container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
+ });
+ }
+ });
+
+ // Bei Moduswechsel Pan zurücksetzen
+ window.addEventListener('click', (e) => {
+ if (e.target.id === 'timelapse-button' ||
+ e.target.closest('#timelapse-button') ||
+ e.target.id === 'dvp-back-live' ||
+ e.target.closest('.play-link')) {
+ panX = 0;
+ panY = 0;
+ setTimeout(applyTransform, 100);
+ }
});
})();