Digital picture frame with Allsky images: Update with timelapse videos on the Raspberry Pi

After using the basic version of my Raspberry Pi setup as a digital picture frame for a few weeks in everyday life, it was clear that the system was stable – but the dashboard itself could be significantly improved.

I have described the basis here:
Raspberry Pi as a digital picture frame in kiosk mode – hardware, setup & troubleshooting

This update builds exactly on this and describes the final, corrected version, which is now running permanently and unattended.

Aim of the dashboard update

The new dashboard should:

  • make several Allsky outputs directly usable on the display
  • Be touch-enabled
  • Play videos stably
  • contain no special logic or workarounds
  • have defined states in the event of an error

In short: less clever, more robust.

Dashboard structure (final)

The interface consists of three areas:

  • Current image (large, clickable → full screen)
  • “Last night” keogram with link to time-lapse video in full screen
  • Keogram “last day” with link to time-lapse video in full screen
  • Startrail without link

Kiosk

Dashboard: index.html

The dashboard itself is a simple HTML file with CSS grid and periodic reload of the images.

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

The keograms link directly to the timelapse view:

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

The images are updated regularly:

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

index.html – Dashboard (complete, final)

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

<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>Current image</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>Time lapse</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>Last night</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>Last day</p>
        </div>
      </div>

      <div>
        <h2>Start trail</h2>

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

          <p>Last 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>

Pull timelapse videos via redirect views

A central point of the update was video playback. The most important finding: no fetch, no pre-check, no redirect logic. The video element receives the URL directly – the browser does the rest.

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

Chromium follows the redirect automatically and plays the MP4.

Catching errors cleanly

If no video exists, there is no “trial and error”, but a clear status is displayed.

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

This prevents black screens and makes it immediately clear what is going on.

The entire logic is deliberately limited to a few events:

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

Navigation back to the dashboard is always via the same ✕ symbol:

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

timelapse.html (complete, final)

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

<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">
    No time lapse available
  </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>

Full screen view for the live image: fullscreen.html

The current Allsky image can be displayed in full screen – without browser UI, without navigation elements.

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

Automatic update:

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

fullscreen.html (complete, final)

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

<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>

Done, running!

Enjoyed this post?

You can support allsky-rodgau.de with a small coffee on BuyMeACoffee.

Buy me a coffee!