preloader

Aus der Reiseplanung wird ein Video

blog-image

Die Reise ist vorbei, die Route liegt in der Datenbank. Was macht man damit? 🤔

In OurSpots steckt nach jeder Tour eine fertig berechnete Strecke mit allen Etappen. Bisher konnte man die als GPX exportieren und sich auf der Karte ansehen. Beides nützlich, aber nichts, was man jemandem zeigt.

Jetzt macht die App ein Video daraus. Die Route wächst mit, ein Wohnmobil fährt sie ab, und an jeder Station taucht ein Schild mit dem Ortsnamen und dem Kilometerstand auf.

Der gefahrene Teil kräftig, der noch offene blass gestrichelt - und das Fahrzeug dazwischen

Der gefahrene Teil kräftig, der noch offene blass gestrichelt - und das Fahrzeug dazwischen

Geht das überhaupt im Browser?

Das war die erste Frage. Die Antwort: ja, aber nicht so, wie man zuerst denkt.

Naheliegend wäre, die Animation im Browser abzufilmen – canvas.captureStream() gibt es, MediaRecorder auch. Nur hat das zwei Haken. Erstens läuft die Aufnahme in Echtzeit mit: Wenn die Karte gerade Kacheln nachlädt, ruckelt es, und genau dieses Ruckeln landet im Video. Zweitens – und das ist der eigentliche Killer – liegen Wohnmobil und Schilder gar nicht im Canvas. Es sind HTML-Elemente darüber. Ein Canvas-Mitschnitt hätte die Karte, aber kein Fahrzeug. 🤦‍♂️

Also rendert ein eigener Container auf dem Server. Ein Chromium öffnet dieselbe Seite, die auch die Vorschau füttert, und wird Bild für Bild weitergeschaltet statt in Echtzeit abgefilmt. Jedes Bild wird erst fotografiert, wenn die Karte fertig geladen hat; ffmpeg macht daraus ein MP4. Fotografiert wird die ganze Seite – damit sind die HTML-Elemente von selbst dabei.

Der angenehme Nebeneffekt: Es kann kein Bild verloren gehen. Wenn ein Kachelserver mal langsam ist, wartet der Renderer eben, statt eine halb geladene Karte ins Video zu schreiben.

Eine Regel, die ich mir selbst auferlegt habe

Die Animation liegt in einem eigenen Verzeichnis, das weder React noch die Kartenbibliothek kennt. Vorschau und Renderer treiben dieselbe Zeichnung an – die eine mit der Uhr des Browsers, der andere Bild für Bild.

Das klingt nach Prinzipienreiterei, hat aber einen praktischen Grund. Ich habe mir ein ähnliches Werkzeug angeschaut, bei dem Vorschau und Renderer getrennt gerechnet haben, und genau da liefen sie auseinander: Was man einstellte, war nicht das, was herauskam. Gäbe es die Zeichnung zweimal, passiert das früher oder später zwangsläufig.

3D, Gelände und weniger Dorfnamen

Geneigte Kamera mit überhöhtem Gelände - Mittelgebirge brauchen etwa das Dreifache, sonst sieht man aus der Luft gar nichts

Geneigte Kamera mit überhöhtem Gelände - Mittelgebirge brauchen etwa das Dreifache, sonst sieht man aus der Luft gar nichts

Die Karte kann geneigt stehen, mit Höhenmodell und Schattierung. Die echten Höhen wirken aus der Flughöhe allerdings beschämend flach, deshalb gibt es einen Regler für die Überhöhung.

Dazu eine Kleinigkeit, die mehr ausmacht als erwartet: Bei geneigter Kamera reicht der Blick bis zum Horizont, und dann stehen plötzlich dutzende Ortsnamen gleichzeitig im Bild. Ein Schalter dünnt Dörfer, Straßennamen und Punkte am Wegesrand aus. Städte, Länder und Gewässer bleiben – die tragen die Orientierung.

Kamera und Tempo als Punkte entlang der Strecke

Das ist der Teil, an dem ich am längsten gesessen habe.

Drei Punkte, je ein Regler für Höhe, Neigung und Tempo - dazwischen rechnet die App

Drei Punkte, je ein Regler für Höhe, Neigung und Tempo - dazwischen rechnet die App

Man schiebt die Vorschau an eine Stelle, setzt dort einen Punkt und stellt Kamerahöhe, Neigung und Tempo ein. Dazwischen wird überblendet. Über den Alpen also langsam und tief, über die Autobahn schnell und hoch.

Zwei Entscheidungen stecken darin, und beide haben gedauert:

Die Punkte hängen am Streckenanteil, nicht an der Videozeit. Wer die Sekunden je 100 km nachträglich ändert, verschiebt damit die ganze Zeitachse – an Sekunden festgemacht wären die Punkte über die Landschaft gewandert. An der Strecke bleiben sie, wo man sie hingesetzt hat.

Dazwischen liegt eine monoton-kubische Kurve. Eine Gerade trifft die Punkte zwar, aber die Kamera wechselt dort schlagartig ihr Tempo – im Video sieht man an jedem Punkt einen Knick. Die weiche Alternative (Catmull-Rom) läuft sauber hindurch, schießt aber über die gesetzten Werte hinaus: Zwischen 8 und 10 bekäme man zwischendurch 10,4 zu sehen. Bei einer Höhe, die man bewusst eingestellt hat, ist das schlicht falsch. Die monotone Variante kann beides. 🤓

Beim Tempo hat das eine Folge, die ich erst beim Bauen gemerkt habe: Die Fahrzeit ist dann nicht mehr Strecke durch Faktor, sondern das Integral über den Kehrwert des Tempos – und das gibt es nicht als Formel, weil die Tempokurve stückweise kubisch ist. Wird also einmal numerisch aufsummiert und als Tabelle behalten.

Was es kostet

Ehrlichkeitshalber: eine Minute Video braucht ein bis drei Stunden. Jedes Bild ist ein eigener Screenshot – rund 4 Sekunden bei 1080p, 6,6 bei 4K. Der Auftrag läuft im Hintergrund, man darf die Seite verlassen und später wiederkommen.

Dafür ist 4K nur anderthalbmal so teuer wie 1080p und nicht viermal. Der Engpass sind nämlich nicht die Pixel: Die CPU steht während eines Laufs immer wieder bei null, weil Chromium auf Kartenkacheln wartet.

Übrigens zeigen alle drei Auflösungen denselben Kartenausschnitt. Bei einer Vektorkarte ist Auflösung eine Frage der Pixeldichte, nicht der Fenstergröße – ein breiteres Fenster würde bei gleicher Zoomstufe mehr Landschaft zeigen statt dieselbe schärfer. Aus demselben Grund ergibt Hochskalieren eines fertigen Videos hier nie Sinn: Die Vektordaten sind schon da, man muss sie nur richtig rendern lassen.

Der Container

Debian statt Alpine, weil Chromium für glibc ausgeliefert wird. Dazu ffmpeg und – nicht vergessen! – die Schriftpakete: Ohne die zeichnet Chromium jeden Umlaut als leeres Kästchen, und die Ortsnamen einer Reise durch Deutschland bestehen zu einem guten Teil daraus. 🇩🇪

Die fertigen Videos liegen in einem eigenen Volume, getrennt von den Fotos, und sind bewusst nicht Teil der nächtlichen Sicherung: Sie sind jederzeit aus der Reise neu erzeugbar und wären mit ein paar hundert Megabyte das Größte im Archiv.


Der Code ist offen: