home
erste Version am 22.03.2025
letzte Änderung am 22.03.2025

DJI-Mini-4K Videos und Fotos samt Audio abspielen


Über Weihnachten habe ich mit der Anschaffung einer Drone geliebäugelt. Eine DJI Mini 4K gibts bereits für 300€. Trotzdem zu viel für einen Spontan-Kauf.
Also habe ich noch ein bischen gewartet... und als ich eigentlich eine bestellen wollte, gab es nur noch das "Fly more"-Pack für ca. 450€. Das war mir zu viel.
Etwas Zeit verging und ich habe spaßeshalber nochmal geschaut.... und welch freudige Überraschung -> an dem Tag bzw. am 27.01.2025 gab es sie gerade für
Geliefert wurde sie am 29.01.2025. Leider war das Wetter so gar nicht einladend.
Außerdem brauchte ich auch erstmal eine Haftpflicht-Versicherung, bei der die Drone inklusive ist.
Die bisherige Versicherung hatte sie nicht mit drin und hat sowieso viel zu viel gekostet.
Die hatte ich im Jahr 1988 abgeschlossen, habe sie nie in Anspruch genommen und gekostet hat sie 174,38€ pro Jahr.
Ein Anruf bei deren Hotline hat ergeben, dass sie mir netterweise einen neuen Preis machen können: 13,63 pro Monat - also 164,16€ pro Jahr.
Ich habe direkt gekündigt und bei einer anderen Gesellschaft eine neue Familien-Haftpflicht für 79.97€ pro Jahr abgeschlossen. Inklusive Drone mit bis zu 5 kg.
Und weil das Wetter immer noch kacke war, habe ich stattdessen viel Theorie gemacht.
Meint: ich habe viele YT-Videos geschaut und final dann sogar noch einen A1/A3-Dronenführerschein (im ersten Anlauf erfolgreich) absolviert... auch wenn ich ihn für diese Drone eigentlich nicht brauche.

Ein paar Flüge und Aufnahmen habe ich zwischen Ende Januar und gestern zwar schon gemacht, aber aufbewahrungswürdig waren die alle nicht. Das Wetter war zu ungünstig bzw. war Fernsicht einfach nicht gegeben.
Immerhin habe ich dabei die Erkenntnis erlangt, dass die Videos von der Drone ohne Audiospur irgendwie unvollständig sind.
Gerade solange man noch lernt, wären eigene Audio-Kommentare nützlich. Also sowas wie aktuelle Höhe oder Entfernung möchte man vielleicht einsprechen.
Oder auch eine Art Audio-Logbuch: wann habe ich was gemacht bzw. wann welche Funktion gewählt... falls bei der Sichtung zuhause irgendwas nicht passt.
Und schließlich habe ich ja garantiert immer ein Device dabei, mit dem ich Audio aufnehmen kann. Zwar steckt das Smartphone in der Fernsteuerung und läuft mit der DJI Fly App, aber nichts desto trotz kann man ja parallel noch eine Audio-Recording-App nutzen.
Ich fand die Dolby On-App gut brauchbar - speziell, weil sie ohne Werbeeinblendungen daher kommt.



Heute sah es bzgl. Fernsicht etwas besser aus. Den viel zu starken Wind (Böen über 60km/h) habe ich erst realisiert, als ich am geplanten Abhebe-Punkt angekommen war.
Daher habe ich nur vier Fotos von unserem "Dorf von oben" geschossen + ein Abhebe- und ein Lande-Video.
Zuhause angekommen, hatte ich dann das hier auf Platte:
-rw-r--r-- 1 dede users  10001397 22. Mär 11:57 20250322-115319.m4a
-rwxr-xr-x 1 dede users 375493227 22. Mär 11:27 DJI_0002.MP4
-rwxr-xr-x 1 dede users   4241722 22. Mär 11:27 DJI_0003.JPG
-rwxr-xr-x 1 dede users   4136838 22. Mär 11:27 DJI_0004.JPG
-rwxr-xr-x 1 dede users   4780946 22. Mär 11:27 DJI_0005.JPG
-rwxr-xr-x 1 dede users   4677588 22. Mär 11:27 DJI_0006.JPG
-rwxr-xr-x 1 dede users 244409158 22. Mär 11:28 DJI_0007.MP4

Zuerst das Audio-File, dann das Abhebe-Video, danach vier Fotos und schließlich das Lande-Video.
Das will ich nun alles im Stück ansehen können - samt Audio.



Bei diesem Nachmittags-Projekt ist also das Audio-File der Master (der die Zeit vorgibt).
Zuerst müssen per Audacity die Zeitpunkte ermittelt werden, wann welche Foto- oder Video-Datei in das jeweilige Darstellungsprogramm geladen werden sollen.
Für das Abspielen von Videos bietet sich der mplayer an (weil ich den eh schon fernsteuern kann) und für die Fotos fand ich remmina passend.

Das hier ist das Ergebnis von drei bis vier Stunden Bastelei:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# play_on_audio.py
#
# Detlev Ahlgrimm 22.03.2025

import os
import sys
import time
import datetime
import threading
import subprocess
import queue

audio_file = "/10TB2/DJI/2025-03-22/20250322-115319.m4a"
seq = {
"00:00:00.0": ("mplayer:pause", "DJI_0002.MP4"),
"00:01:02.3": ("mplayer:unpause", "..."),
"00:02:21.5": ("ristretto", "DJI_0003.JPG"),
"00:02:26.9": ("ristretto", "DJI_0004.JPG"),
"00:02:30.3": ("ristretto", "DJI_0005.JPG"),
"00:02:33.5": ("ristretto", "DJI_0006.JPG"),
"00:02:42.0": ("mplayer:start", "DJI_0007.MP4"),
"00:03:34.6": None,
}

# ------------------------------------------------------------------
# Wandelt z.B. "00:01:02.3" nach 62.3.
def time_to_secs(t):
h, m, s = t.split(":")
return int(h)*3600 + int(m)+60 + float(s)


# ------------------------------------------------------------------
# Wandelt z.B. 62.3 nach "00:01:02.3".
def secs_to_time(s):
h = int(s / 3600)
m = int((s - h*3600) / 60)
s = s - h*3600 - m*60
if s < 10:
return f"{h:02}:{m:02}:0{s:3.1f}"
return f"{h:02}:{m:02}:{s:4.1f}"


# ------------------------------------------------------------------
# Läuft als Thread.
# Liest aus der stdout-pipe vom mplayer und schreibt Positions-
# Antworten in die audio_pos_queue.
def pipe_print(pipe):
global audio_is_up, audio_length
for line in iter(pipe.readline, b''):
ln = line.decode("utf-8").strip()
#print(">>>>", ln)
if ln.startswith("ANS_LENGTH"):
_, lng = ln.split("=")
audio_length = float(lng)
if ln.startswith("ANS_TIME_POSITION"):
_, pos = ln.split("=")
audio_pos_queue.put(float(pos))
elif ln.startswith("Exiting... (Quit)"):
audio_is_up = False
print("mplayer geschlossen")
elif ln.startswith("Exiting... (End of file)"):
audio_is_up = False
print("mplayer am Ende der Playlist")
elif "Starting playback" in ln:
audio_is_up = True
time.sleep(0.1)


# ----------------------------------------------------------------------
# Öffnet ristretto mit "fn" und liefert die ProzessID zurück.
def open_ristretto(fn):
cmd = ["/usr/bin/ristretto", "-f", fn]
return subprocess.Popen(cmd)


# ----------------------------------------------------------------------
# Öffnet mplayer mit "fn", pausiert das Video sofort und liefert die
# ProzessID zurück.
def open_mplayer_paused(fn):
cmd = ["/usr/bin/mplayer", "-slave", "-quiet", "-fs", fn]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
p.stdin.write(b"pause\n")
p.stdin.flush()
return p


# ----------------------------------------------------------------------
# Setzt das Video des pausierten mplayer-Prozesses fort.
def unpause_mplayer(proc):
proc.stdin.write(b"pause\n")
proc.stdin.flush()


# ----------------------------------------------------------------------
# Öffnet mplayer mit "fn" und liefert die ProzessID zurück.
def start_mplayer(fn):
cmd = ["/usr/bin/mplayer", "-slave", "-quiet", "-fs", fn]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
return p


# ----------------------------------------------------------------------
# Verteilt seq auf die jeweils zuständigen Funktionen.
def exec_cmd(seq, proc=None):
if seq is None:
return
prog, filename = seq
if prog.startswith("mplayer"):
_, cmd = prog.split(":")
if cmd == "pause":
proc2 = open_mplayer_paused(filename)
time.sleep(.3)
if proc is not None:
proc.kill()
proc = proc2
elif cmd == "unpause":
unpause_mplayer(proc)
elif cmd == "start":
proc2 = start_mplayer(filename)
time.sleep(.3)
if proc is not None:
proc.kill()
proc = proc2
elif prog == "ristretto":
proc2 = open_ristretto(filename)
time.sleep(.3)
if proc is not None:
proc.kill()
proc = proc2
return proc


# ----------------------------------------------------------------------
#
if __name__=="__main__":
"""
p = open_mplayer_paused("DJI_0002.MP4")
time.sleep(10)
unpause_mplayer(p)
sys.exit()

p = open_ristretto("DJI_0003.JPG")
time.sleep(3)
p.kill()
p = open_ristretto("DJI_0004.JPG")
time.sleep(3)
p.kill()
sys.exit()
"""
cmd = ["/usr/bin/mplayer", "-slave", "-quiet", audio_file]
audio_pos_queue = queue.Queue()
audio_is_up = False

audio_process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
worker = threading.Thread(target=pipe_print, name="pipe-print", args=(audio_process.stdout,))
worker.setDaemon(True)
worker.start()

while not audio_is_up:
time.sleep(0.1)
print("running...")
audio_process.stdin.write(b"get_time_length\n")
audio_process.stdin.flush()

pos, audio_length = 0, 10
proc = None
while pos < audio_length:
#print(pos, "/", audio_length, flush=True)
try:
pos = audio_pos_queue.get(timeout=.1)
except queue.Empty:
pass
t = secs_to_time(pos)
if t in seq:
print(pos, t, seq[t])
proc = exec_cmd(seq[t], proc)
time.sleep(0.1) # Sorgt dafür, dass t nur einmal in seq gefunden wird.

if audio_process:
try:
audio_process.stdin.write(b"get_time_pos\n")
audio_process.stdin.flush()
except BrokenPipeError:
break
time.sleep(0.01)

print("killing audio-player")
if audio_process:
audio_process.kill()


Und hier dann das Screenrecoder-Video mit dem eigentlichen Ergebnis:


Ab 03:30 folgt nur noch mein Gebrabbel, während ich die Drone zusammengeklappt, die beiden Propeller-Schutz-Teile aufgesetzt und die Audio-Aufzeichnung beendet habe.

Das, was derzeit als statisches Dictionary oben im Script steht, könnte natürlich noch leicht in eine JSON-Datei verschoben werden - so dass man das Script mit einem Verzeichnis-Namen starten könnte und alle variablen Daten aus eben diesem Ordner kämen.

Mir war im vornherhein bewusst, dass das Onboard-Micro des Smartphones für diesen Einsatzzweck ungeeignet ist. Deswegen habe ich mir auch genau dafür bereits ein DJI Mic Mini angeschafft.
Nur leider .... stirbt die Bluetooth-Verbindung zwischen Mic und Smartphone regelmäßig nach ungefähr einer Minute. Vielleicht taugt die dafür genutzte App nix oder es liegt daran, dass das Smartphone zeitweilig ebenfalls per Bluetooth (mit der Drone) kommuniziert ... und dabei die andere Verbindung kappt.
Möglicherweise wäre ein zweites Smartphone (extra für das Mic) eine Lösung. Wir werden sehen....