Ravensburg.cam Live & Pi Tech

Webcam Portal

Live Dashboard mit Wetter-Sensoren, Response-Time & FTP-Proxy

Besucher heute (IP): 1

Anleitung & Quellcode zum Kopieren

Binde dein eigenes Webcam-Dashboard inklusive PHP-Proxy mit diesem Code direkt ein.

Einrichtungsanleitung in 3 Schritten:

  1. Erstelle eine Datei namens index.php und füge den Code aus dem ersten Fenster ein.
  2. Erstelle eine zweite Datei namens cam_proxy.php im selben Ordner mit dem Code aus dem zweiten Fenster.
  3. Stelle sicher, dass PHP cURL auf deinem Server aktiviert ist, damit die Bilder sicher per Proxy abgerufen werden können.
1. Frontend: index.php
<?php
// IP-Zähler (1x pro Tag per ips.txt)
$ipFile = __DIR__ . '/ips.txt';
$today = date('Y-m-d');
if (file_exists($ipFile) && date('Y-m-d', filemtime($ipFile)) !== $today) {
    file_put_contents($ipFile, '');
}
$userIp = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$savedIps = file_exists($ipFile) ? file($ipFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];
if (!in_array($userIp, $savedIps)) {
    file_put_contents($ipFile, $userIp . PHP_EOL, FILE_APPEND | LOCK_EX);
    $savedIps[] = $userIp;
}
$dailyVisitors = count($savedIps);
?>
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Webcam Portal – Raspberry Pi Edition</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        :root { --rpi-red: #c7053d; --rpi-red-dark: #a00430; --rpi-red-light: #fff1f4; --rpi-gray-bg: #f3f4f6; --rpi-card-bg: #ffffff; --rpi-border: #e5e7eb; }
        body { background-color: var(--rpi-gray-bg); font-family: 'Inter', sans-serif; color: #1f2937; margin: 0; display: flex; flex-direction: column; min-height: 100vh; }
        .btn-rpi-secondary { background-color: #ffffff; color: #1f2937; font-weight: 700; border: 1px solid var(--rpi-border); border-radius: 9999px; transition: all 0.2s ease; }
        .btn-rpi-secondary:hover { background-color: #f9fafb; border-color: #d1d5db; transform: translateY(-1px); }
        .card-rpi { background-color: var(--rpi-card-bg); border: 1px solid var(--rpi-border); border-radius: 1.5rem; transition: all 0.25s ease; }
        .card-rpi:hover { border-color: #d1d5db; box-shadow: 0 12px 24px -10px rgba(0, 0, 0, 0.08); }
        .nav-link-rpi { color: #4b5563; font-weight: 600; font-size: 0.875rem; border-radius: 9999px; padding: 0.5rem 1rem; transition: all 0.2s ease; }
        .nav-link-rpi:hover { color: var(--rpi-red); background-color: var(--rpi-red-light); }
        .cam-img { width: 100%; height: 380px; object-fit: cover; background: #0e1017; border-radius: 0.75rem; }
        .badge-online { background-color: #10b981; color: #ffffff; }
        .badge-offline { background-color: var(--rpi-red); color: #ffffff; }
    </style>
</head>
<body class="antialiased">

<header class="bg-white border-b border-gray-200 sticky top-0 z-50">
    <div class="max-w-7xl mx-auto px-4 h-20 flex items-center justify-between">
        <a href="index.php" class="flex items-center gap-3">
            <div class="w-10 h-10 rounded-xl bg-[#c7053d] flex items-center justify-center text-white text-xl"><i class="bi bi-cpu-fill"></i></div>
            <span class="text-xl font-extrabold text-gray-900">Ravensburg<span class="text-[#c7053d]">.cam</span></span>
        </a>
        <nav class="hidden md:flex items-center gap-1">
            <a href="index.php" class="nav-link-rpi"><i class="bi bi-house-door"></i> Start</a>
            <a href="land/index.php" class="nav-link-rpi"><i class="bi bi-hourglass-split text-[#c7053d]"></i> Zeitraffer</a>
            <a href="webcam.php" class="nav-link-rpi"><i class="bi bi-camera-video text-[#c7053d]"></i> Webcam.php</a>
            <a href="stats.php" class="nav-link-rpi"><i class="bi bi-broadcast text-[#c7053d]"></i> Livestream</a>
            <a href="unwetter.php" class="nav-link-rpi"><i class="bi bi-exclamation-triangle text-[#c7053d]"></i> Unwetter</a>
            <a href="bilder/" class="nav-link-rpi"><i class="bi bi-phone"></i> Handycam</a>
            <a href="impressum.php" class="nav-link-rpi"><i class="bi bi-info-circle"></i> Info</a>
        </nav>
    </div>
</header>

<main class="max-w-7xl mx-auto px-4 py-8 flex-grow">
    <div class="mb-8 flex justify-between items-center">
        <h1 class="text-3xl font-extrabold flex items-center gap-2"><i class="bi bi-webcam text-[#c7053d]"></i> Webcam Portal</h1>
        <span class="bg-white px-4 py-2 rounded-full border text-sm font-medium">Besucher heute: <strong class="text-[#c7053d]"><?php echo $dailyVisitors; ?></strong></span>
    </div>
    <div class="grid grid-cols-1 lg:grid-cols-2 gap-8" id="camGrid"></div>
</main>

<script>
const cams = [
    { name: "Dach Ravensburg", url: "https://www.ravensburg-ipwebcam.de/bilder/bilder/snap.jpg", temp: "19.2°C", humidity: "58%", wind: "14 km/h", condition: "Leicht bewölkt" },
    { name: "Garten", url: "https://ravensburg-ipwebcam.de/home.jpg?", temp: "18.5°C", humidity: "64%", wind: "9 km/h", condition: "Sonnig" }
];

function createCard(cam, index) {
    const col = document.createElement("div");
    col.innerHTML = `
        <div class="card-rpi p-6 h-full flex flex-col justify-between">
            <div>
                <div class="flex justify-between items-center mb-4">
                    <h3 class="font-bold text-lg flex items-center gap-2"><i class="bi bi-camera-video-fill text-[#c7053d]"></i> \${cam.name}</h3>
                    <span id="status-\${index}" class="text-xs font-bold px-3 py-1 rounded-full badge-offline">OFFLINE</span>
                </div>
                <div class="relative overflow-hidden rounded-xl mb-4 border shadow-inner">
                    <img id="img-\${index}" class="cam-img" alt="\${cam.name}">
                </div>
                <div class="grid grid-cols-2 sm:grid-cols-4 gap-2 bg-gray-50 p-3 rounded-xl border mb-4 text-xs">
                    <div class="flex items-center gap-2"><i class="bi bi-thermometer-half text-[#c7053d] text-lg"></i><div><span class="text-gray-400 block text-[10px] font-semibold">TEMP</span><span class="font-bold text-gray-800">\${cam.temp || '--'}</span></div></div>
                    <div class="flex items-center gap-2"><i class="bi bi-droplet-half text-blue-500 text-lg"></i><div><span class="text-gray-400 block text-[10px] font-semibold">FEUCHTE</span><span class="font-bold text-gray-800">\${cam.humidity || '--'}</span></div></div>
                    <div class="flex items-center gap-2"><i class="bi bi-wind text-teal-500 text-lg"></i><div><span class="text-gray-400 block text-[10px] font-semibold">WIND</span><span class="font-bold text-gray-800">\${cam.wind || '--'}</span></div></div>
                    <div class="flex items-center gap-2"><i class="bi bi-cloud-sun text-amber-500 text-lg"></i><div><span class="text-gray-400 block text-[10px] font-semibold">WETTER</span><span class="font-bold text-gray-800 truncate">\${cam.condition || '--'}</span></div></div>
                </div>
                <p id="info-\${index}" class="text-xs text-gray-500 mb-4 flex items-center gap-1"><i class="bi bi-hourglass-split"></i> Warte auf Daten…</p>
            </div>
            <button class="btn-rpi-secondary py-2.5 px-4 text-xs w-full flex items-center justify-center gap-2" onclick="loadCam(\${index})">
                <i class="bi bi-arrow-clockwise text-base"></i> Neu laden
            </button>
        </div>
    `;
    return col;
}

function init() {
    const grid = document.getElementById("camGrid");
    if (grid) {
        cams.forEach((cam, i) => { grid.appendChild(createCard(cam, i)); loadCam(i); });
        setInterval(() => cams.forEach((_, i) => loadCam(i)), 10000);
    }
}

function loadCam(index) {
    const cam = cams[index];
    const status = document.getElementById(`status-\${index}`);
    const img = document.getElementById(`img-\${index}`);
    const info = document.getElementById(`info-\${index}`);

    fetch(`cam_proxy.php?cam=\${encodeURIComponent(cam.url)}`)
        .then(r => r.json())
        .then(data => {
            if (data.status === "online") {
                status.textContent = "ONLINE";
                status.className = "text-xs font-bold px-3 py-1 rounded-full badge-online";
                img.src = data.image;
                info.innerHTML = `<i class="bi bi-speedometer2 text-[#c7053d]"></i> \${data.response_time} ms | <i class="bi bi-clock ml-1"></i> \${new Date(data.timestamp * 1000).toLocaleTimeString()}`;
            } else {
                status.textContent = "OFFLINE";
                status.className = "text-xs font-bold px-3 py-1 rounded-full badge-offline";
                img.src = "";
                info.innerHTML = `<i class="bi bi-exclamation-triangle text-[#c7053d]"></i> Keine Verbindung | \${data.response_time} ms`;
            }
        });
}
document.addEventListener("DOMContentLoaded", init);
</script>
</body>
</html>
2. Backend: cam_proxy.php
<?php
header('Content-Type: application/json');

$camUrl = $_GET['cam'] ?? '';

if (empty($camUrl) || !filter_var($camUrl, FILTER_VALIDATE_URL)) {
    echo json_encode(['status' => 'offline', 'response_time' => 0]);
    exit;
}

$startTime = microtime(true);

$ch = curl_init($camUrl);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 5,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_USERAGENT      => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
]);

$imageData = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);

$responseTime = round((microtime(true) - $startTime) * 1000);

if ($httpCode === 200 && $imageData && !$error) {
    $base64 = 'data:image/jpeg;base64,' . base64_encode($imageData);
    echo json_encode([
        'status'        => 'online',
        'image'         => $base64,
        'response_time' => $responseTime,
        'timestamp'     => time()
    ]);
} else {
    echo json_encode([
        'status'        => 'offline',
        'response_time' => $responseTime
    ]);
}
?>