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:
- Erstelle eine Datei namens
index.phpund füge den Code aus dem ersten Fenster ein. - Erstelle eine zweite Datei namens
cam_proxy.phpim selben Ordner mit dem Code aus dem zweiten Fenster. - 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
]);
}
?>