Digitaler Bilderrahmen mit Allsky-Bildern: Update mit Timelapse-Videos auf dem Raspberry Pi

Nachdem ich die Basis-Version meines Raspberry-Pi-Setups als digitalen Bilderrahmen einige Wochen im Alltag genutzt habe, war klar: Das System läuft stabil – aber das Dashboard selbst lässt sich deutlich verbessern.

Die Grundlage habe ich hier beschrieben:
Raspberry Pi als digitaler Bilderrahmen im Kiosk-Modus – Hardware, Setup & Troubleshooting

Dieses Update baut exakt darauf auf und beschreibt den finalen, bereinigten Stand, der inzwischen dauerhaft und unbeaufsichtigt läuft.

Ziel des Dashboard-Updates

Das neue Dashboard sollte:

  • mehrere Allsky-Ausgaben direkt am Display nutzbar machen
  • Touch-fähig sein
  • Videos stabil abspielen
  • keine Sonderlogik oder Workarounds enthalten
  • im Fehlerfall definierte Zustände haben

Kurz: weniger clever, mehr robust.

Dashboard-Aufbau (final)

Die Oberfläche besteht aus drei Bereichen:

  • Aktuelles Bild (groß, klickbar → Vollbild)
  • Keogramm „letzte Nacht“ mit Verlinkung zum Zeitraffer-Video im Vollbild
  • Keogramm „letzter Tag“ mit Verlinkung zum Zeitraffer-Video im Vollbild
  • Startrail ohne Verlinkung

Kiosk

Dashboard: index.html

Das Dashboard selbst ist eine einfache HTML-Datei mit CSS Grid und periodischem Reload der Bilder.

 <img id="latestImage" src="https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=INIT" onclick="window.location.href='fullscreen.html'">

Die Keogramme verlinken direkt auf die Timelapse-Ansicht:

 <div class="thumb" onclick="window.location.href='timelapse.html?night=1'"> <img src="https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=1"> </div>

Die Bilder werden regelmäßig aktualisiert:

 setInterval(() => { latestImage.src = 'https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=' + Date.now(); }, 25000);

index.html – Dashboard (komplett, final)

<!DOCTYPE html>
<html lang="de">

<head>
  <meta charset="UTF-8">

  <meta name="viewport"
        content="width=1280, height=800, initial-scale=1.0, user-scalable=no">

  <title> </title>

  <style>
    html, body {
      margin: 0;
      padding: 0;
      width: 1280px;
      height: 800px;
      background: #000;
      color: #fff;
      font-family: Arial, Helvetica, sans-serif;
      overflow: hidden;
    }

    .page {
      width: 100%;
      height: 100%;
      opacity: 0;
      transition: opacity 0.8s ease;
    }

    .page.visible {
      opacity: 1;
    }

    .grid {
      display: grid;
      grid-template-columns: 2fr 1fr;
      gap: 20px;
      height: 100%;
      padding: 20px;
      box-sizing: border-box;
    }

    h1 {
      font-size: 28px;
      margin: 0 0 12px 0;
      font-weight: normal;
    }

    h2 {
      font-size: 22px;
      margin: 0 0 10px 0;
      font-weight: normal;
    }

    .main img {
      width: 100%;
      height: 100%;
      object-fit: contain;
      cursor: pointer;
    }

    .sidebar {
      display: flex;
      flex-direction: column;
      gap: 30px;
    }

    .thumb img {
      width: 100%;
      border-radius: 4px;
      border: 1px solid #333;
    }

    .thumb p {
      margin: 6px 0 0 0;
      font-size: 16px;
      color: #ccc;
    }
  </style>
</head>

<body>

<div class="page" id="page">

  <div class="grid">

    <div class="main">
      <h1>Aktuelles Bild</h1>

      <img id="latestImage"
           src="https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=INIT"
           onclick="window.location.href='fullscreen.html'">
    </div>

    <div class="sidebar">

      <div>
        <h2>Zeitraffer</h2>

        <div class="thumb"
             onclick="window.location.href='timelapse.html?night=1'">

          <img id="keogramNight"
               src="https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=1&ts=INIT">

          <p>Letzte Nacht</p>
        </div>

        <div class="thumb"
             onclick="window.location.href='timelapse.html?night=0'">

          <img id="keogramDay"
               src="https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=0&ts=INIT">

          <p>Letzter Tag</p>
        </div>
      </div>

      <div>
        <h2>Startrail</h2>

        <div class="thumb">
          <img id="startrail"
               src="https://access.allsky-rodgau.de/indi-allsky/lateststartrail?ts=INIT">

          <p>Letzter Startrail</p>
        </div>
      </div>

    </div>
  </div>
</div>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    document.getElementById('page').classList.add('visible');
  });

  setInterval(() => {
    latestImage.src =
      'https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=' + Date.now();
  }, 25000);

  setInterval(() => {
    keogramNight.src =
      'https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=1&ts=' + Date.now();

    keogramDay.src =
      'https://access.allsky-rodgau.de/indi-allsky/latestkeogram?night=0&ts=' + Date.now();

    startrail.src =
      'https://access.allsky-rodgau.de/indi-allsky/lateststartrail?ts=' + Date.now();
  }, 3600000);
</script>

</body>
</html>

Timelapse-Videos per Redirect Views ziehen

Ein zentraler Punkt des Updates war die Video-Wiedergabe. Wichtigste Erkenntnis: Kein Fetch, keine Vorabprüfung, keine Redirect-Logik. Das Video-Element bekommt direkt die URL – der Browser erledigt den Rest.

 video.src = 'https://access.allsky-rodgau.de/indi-allsky/latesttimelapse?night=' + night + '&ts=' + Date.now();

Chromium folgt dem Redirect automatisch und spielt das MP4 ab.

Fehlerfall sauber abfangen

Falls kein Video existiert, wird nicht „herumprobiert“, sondern ein klarer Zustand angezeigt.

 video.addEventListener('error', () => { message.style.display = 'flex'; });

Das verhindert schwarze Screens und macht sofort klar, was Sache ist.

Die komplette Logik beschränkt sich bewusst auf wenige Events:

 video.addEventListener('canplay', () => { container.classList.add('visible'); }); video.addEventListener('error', () => { message.style.display = 'flex'; });

Navigation zurück zum Dashboard erfolgt immer über das gleiche ✕-Symbol:

 function goBack() { window.location.href = 'index.html'; }

timelapse.html (komplett, final)

<!DOCTYPE html>
<html lang="de">

<head>
  <meta charset="UTF-8">

  <meta name="viewport"
        content="width=1280, height=800, initial-scale=1.0, user-scalable=no">

  <title> </title>

  <style>
    html, body {
      margin: 0;
      padding: 0;
      width: 1280px;
      height: 800px;
      background: #000;
      font-family: Arial, Helvetica, sans-serif;
      overflow: hidden;
      color: #fff;
    }

    .container {
      position: relative;
      width: 100%;
      height: 100%;
      opacity: 0;
      transition: opacity 0.8s ease;
    }

    .container.visible {
      opacity: 1;
    }

    video {
      width: 100%;
      height: 100%;
      object-fit: contain;
      background: #000;
    }

    .message {
      position: absolute;
      inset: 0;
      display: none;
      align-items: center;
      justify-content: center;
      font-size: 22px;
      color: #ccc;
      text-align: center;
    }

    .close {
      position: fixed;
      top: 20px;
      right: 20px;
      width: 48px;
      height: 48px;
      font-size: 32px;
      line-height: 48px;
      text-align: center;
      background: rgba(0,0,0,0.6);
      border-radius: 4px;
      cursor: pointer;
    }
  </style>
</head>

<body>

<div class="close" onclick="goBack()">✕</div>

<div class="container" id="container">

  <video id="video"
         autoplay
         muted
         playsinline>
  </video>

  <div class="message" id="message">
    Kein Zeitraffer verfügbar
  </div>

</div>

<script>
  const params = new URLSearchParams(window.location.search);
  const night  = params.get('night');

  const video     = document.getElementById('video');
  const container = document.getElementById('container');
  const message   = document.getElementById('message');

  video.addEventListener('canplay', () => {
    container.classList.add('visible');
  });

  video.addEventListener('error', () => {
    message.style.display = 'flex';
    container.classList.add('visible');
  });

  video.src =
    'https://access.allsky-rodgau.de/indi-allsky/latesttimelapse?night=' +
    night + '&ts=' + Date.now();

  function goBack() {
    container.classList.remove('visible');

    setTimeout(() => {
      window.location.href = 'index.html';
    }, 800);
  }
</script>

</body>
</html>

Vollbild-Ansicht für das Livebild: fullscreen.html

Das aktuelle Allsky-Bild kann im Vollbild angezeigt werden – ohne Browser-UI, ohne Navigationselemente.

 <img id="latestImage" src="https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=INIT">

Automatische Aktualisierung:

 setInterval(() => { latestImage.src = 'https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=' + Date.now(); }, 25000);

fullscreen.html (komplett, final)

<!DOCTYPE html>
<html lang="de">

<head>
  <meta charset="UTF-8">

  <meta name="viewport"
        content="width=1280, height=800, initial-scale=1.0, user-scalable=no">

  <title> </title>

  <style>
    html, body {
      margin: 0;
      padding: 0;
      width: 1280px;
      height: 800px;
      background: #000;
      overflow: hidden;
      font-family: Arial, Helvetica, sans-serif;
    }

    img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }

    .close {
      position: fixed;
      top: 20px;
      right: 20px;
      width: 48px;
      height: 48px;
      font-size: 32px;
      line-height: 48px;
      text-align: center;
      background: rgba(0,0,0,0.6);
      color: #fff;
      cursor: pointer;
      border-radius: 4px;
    }

    .dashboard {
      position: fixed;
      bottom: 30px;
      left: 50%;
      transform: translateX(-50%);
      padding: 12px 24px;
      background: rgba(0,0,0,0.6);
      color: #fff;
      border-radius: 4px;
      cursor: pointer;
      font-size: 18px;
    }
  </style>
</head>

<body>

<img id="latestImage"
     src="https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=INIT">

<div class="close" onclick="goDashboard()">✕</div>
<div class="dashboard" onclick="goDashboard()">Dashboard</div>

<script>
  setInterval(() => {
    latestImage.src =
      'https://access.allsky-rodgau.de/indi-allsky/latestimage?ts=' + Date.now();
  }, 25000);

  function goDashboard() {
    window.location.href = 'index.html';
  }
</script>

</body>
</html>

Fertig, läuft!

 

 

 

 

Hat dir dieser Beitrag gefallen?

Du kannst allsky-rodgau.de mit einem kleinen Kaffee auf BuyMeACoffee unterstützen.

Jetzt Kaffee spendieren!