DJI-HeadsUp/main.py
2025-06-09 13:53:19 +00:00

372 lines
14 KiB
Python

import sys
import os
import subprocess
import tempfile
import re
import srt
import folium
import vlc
from math import radians, sin, cos, sqrt, atan2
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout,
QWidget, QFileDialog, QHBoxLayout, QSlider, QSizePolicy, QFrame
)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
def haversine(lat1, lon1, lat2, lon2):
R = 6371.0
lat1_rad = radians(lat1)
lon1_rad = radians(lon1)
lat2_rad = radians(lat2)
lon2_rad = radians(lon2)
dlon = lon2_rad - lon1_rad
dlat = lat2_rad - lat1_rad
a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
c = 2 * atan2(sqrt(a), sqrt(1 - a))
distance = R * c
return distance
def extract_srt_from_mp4(mp4_path):
temp_srt = tempfile.NamedTemporaryFile(suffix=".srt", delete=False)
temp_srt.close()
cmd = [
"ffmpeg", "-y", "-i", mp4_path, "-map", "0:s:0", "-c:s", "srt", temp_srt.name
]
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return temp_srt.name
except subprocess.CalledProcessError:
os.unlink(temp_srt.name)
return None
def parse_telemetry_srt(srt_path):
with open(srt_path, encoding="utf-8") as f:
srt_content = f.read()
subs = list(srt.parse(srt_content))
telemetry = []
for sub in subs:
data = {
"start": sub.start.total_seconds(),
"end": sub.end.total_seconds(),
}
text = sub.content.replace('\n', ' ')
m = re.search(r'GPS \(([-\d.]+),\s*([-\d.]+),\s*(\d+)\)', text)
if m:
data["lat"] = float(m.group(2))
data["lon"] = float(m.group(1))
m = re.search(r'H\s*([-\d.]+)m', text)
if m:
data["height"] = float(m.group(1))
m = re.search(r'H\.S\s*([-\d.]+)m/s', text)
if m:
data["h_speed"] = float(m.group(1))
telemetry.append(data)
return telemetry
class MapWidget(QWebEngineView):
def __init__(self, lat, lon):
super().__init__()
self.map = folium.Map(location=[lat, lon], zoom_start=16)
self.start_marker = folium.Marker([lat, lon], popup="Start", icon=folium.Icon(color='green'))
self.start_marker.add_to(self.map)
self.current_marker = folium.Marker([lat, lon], icon=folium.Icon(color='red'))
self.current_marker.add_to(self.map)
self.path_line = folium.PolyLine([[lat, lon]], color="blue", weight=3, opacity=0.8)
self.path_line.add_to(self.map)
self.map_name = self.map.get_name()
self.marker_name = self.current_marker.get_name()
self.line_name = self.path_line.get_name()
html_content = self.map.get_root().render()
self.setHtml(html_content)
def update_marker(self, lat, lon):
js_code = f"""
var new_latlng = L.latLng({lat}, {lon});
{self.marker_name}.setLatLng(new_latlng);
{self.line_name}.addLatLng(new_latlng);
{self.map_name}.panTo(new_latlng);
"""
self.page().runJavaScript(js_code)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("DJI Drone Telemetry Viewer")
self.setGeometry(100, 100, 1280, 720)
self.setStyleSheet("""
QMainWindow { background-color: #23272b; }
QLabel { color: #fff; }
QPushButton { background: #3c4148; border: 1px solid #555; padding: 8px 16px; border-radius: 4px; color: white; }
QPushButton:hover { background: #4a5058; }
QPushButton:pressed { background: #2c3138; }
QSlider::groove:horizontal { height: 8px; background: #333; border-radius: 4px; }
QSlider::handle:horizontal { background: #4f8dcb; width: 16px; margin: -4px 0; border-radius: 8px; }
QFrame { background: #111; }
""")
self.video_frame = QFrame()
self.video_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.video_frame.setStyleSheet("background: #111; border-radius: 8px;")
right_panel = QVBoxLayout()
self.map_placeholder = QLabel("Karte wird geladen...")
self.map_placeholder.setAlignment(Qt.AlignCenter)
self.map_placeholder.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.map_placeholder.setStyleSheet("background: #222; border-radius: 8px; color: #888;")
self.map_widget = None
right_panel.addWidget(self.map_placeholder, stretch=3)
self.gps_label = QLabel("<b>GPS</b><br>--")
self.alt_label = QLabel("<b>Höhe</b><br>--")
self.speed_label = QLabel("<b>Geschw.</b><br>--")
self.dist_label = QLabel("<b>Distanz</b><br>--")
hud_font = QFont("Arial", 11)
for label in [self.gps_label, self.alt_label, self.speed_label, self.dist_label]:
label.setFont(hud_font)
label.setStyleSheet("margin-top: 10px; color: #fff;")
right_panel.addWidget(label)
right_panel.addStretch(1)
self.play_btn = QPushButton("▶ Play")
self.pause_btn = QPushButton("⏸ Pause")
self.unload_btn = QPushButton("Unload")
self.slider = QSlider(Qt.Horizontal)
self.slider.setRange(0, 1000)
self.time_label = QLabel("00:00 / 00:00")
self.open_btn = QPushButton("Öffnen")
main_layout = QHBoxLayout()
main_layout.addWidget(self.video_frame, 3)
right_panel_widget = QWidget()
right_panel_widget.setLayout(right_panel)
right_panel_widget.setMinimumWidth(280)
right_panel_widget.setMaximumWidth(400)
right_panel_widget.setStyleSheet("background: #181b1f; border-radius: 10px; padding: 10px;")
main_layout.addWidget(right_panel_widget, 1)
player_layout = QHBoxLayout()
player_layout.addWidget(self.play_btn)
player_layout.addWidget(self.pause_btn)
player_layout.addWidget(self.slider)
player_layout.addWidget(self.time_label)
player_layout.addWidget(self.open_btn)
player_layout.addWidget(self.unload_btn)
vbox = QVBoxLayout()
vbox.addLayout(main_layout)
vbox.addLayout(player_layout)
container = QWidget()
container.setLayout(vbox)
self.setCentralWidget(container)
self.video_path = None
self.telemetry = []
self.start_coords = None
self.telemetry_idx = 0
self.slider_is_pressed = False
self.vlc_instance = vlc.Instance("--no-xlib")
self.media_player = self.vlc_instance.media_player_new()
self.telemetry_timer = QTimer(self)
self.telemetry_timer.setInterval(33)
self.telemetry_timer.timeout.connect(self.update_telemetry_and_progress)
self.open_btn.clicked.connect(self.open_video)
self.play_btn.clicked.connect(self.play_video)
self.pause_btn.clicked.connect(self.pause_video)
self.unload_btn.clicked.connect(self.unload_video)
self.slider.sliderPressed.connect(self.slider_pressed)
self.slider.sliderReleased.connect(self.slider_released)
self.play_btn.setEnabled(False)
self.pause_btn.setEnabled(False)
self.unload_btn.setEnabled(False)
self.slider.setEnabled(False)
def open_video(self):
fname, _ = QFileDialog.getOpenFileName(self, "Video auswählen", "", "MP4 Files (*.mp4)")
if not fname:
return
self.unload_video()
self.video_path = fname
srt_path = extract_srt_from_mp4(fname)
if not srt_path or not os.path.exists(srt_path):
self.gps_label.setText("Keine Telemetriedaten (SRT) gefunden.")
return
self.telemetry = parse_telemetry_srt(srt_path)
os.unlink(srt_path)
if not self.telemetry or "lat" not in self.telemetry[0]:
self.gps_label.setText("Keine GPS-Daten in der Telemetrie.")
return
self.start_coords = (self.telemetry[0]["lat"], self.telemetry[0]["lon"])
if self.map_widget:
self.map_widget.deleteLater()
self.map_widget = MapWidget(self.start_coords[0], self.start_coords[1])
right_layout = self.centralWidget().layout().itemAt(0).layout().itemAt(1).widget().layout()
right_layout.replaceWidget(self.map_placeholder, self.map_widget)
self.map_placeholder.hide()
self.map_widget.show()
media = self.vlc_instance.media_new(self.video_path)
self.media_player.set_media(media)
self.media_player.set_hwnd(self.video_frame.winId())
self.play_btn.setEnabled(True)
self.pause_btn.setEnabled(True)
self.unload_btn.setEnabled(True)
self.slider.setEnabled(True)
self.play_video()
def play_video(self):
if self.media_player.get_media():
self.media_player.play()
self.telemetry_timer.start()
self.play_btn.setEnabled(False)
self.pause_btn.setEnabled(True)
def pause_video(self):
if self.media_player.is_playing():
self.media_player.pause()
self.telemetry_timer.stop()
self.play_btn.setEnabled(True)
self.pause_btn.setEnabled(False)
def unload_video(self):
self.media_player.stop()
self.telemetry_timer.stop()
self.video_path = None
self.telemetry = []
self.start_coords = None
self.telemetry_idx = 0
if self.map_widget:
self.map_widget.hide()
right_layout = self.centralWidget().layout().itemAt(0).layout().itemAt(1).widget().layout()
right_layout.replaceWidget(self.map_widget, self.map_placeholder)
self.map_widget.deleteLater()
self.map_widget = None
self.map_placeholder.show()
self.gps_label.setText("<b>GPS</b><br>--")
self.alt_label.setText("<b>Höhe</b><br>--")
self.speed_label.setText("<b>Geschw.</b><br>--")
self.dist_label.setText("<b>Distanz</b><br>--")
self.time_label.setText("00:00 / 00:00")
self.slider.setValue(0)
self.play_btn.setEnabled(False)
self.pause_btn.setEnabled(False)
self.unload_btn.setEnabled(False)
self.slider.setEnabled(False)
def update_telemetry_and_progress(self):
duration_ms = self.media_player.get_length()
pos_ms = self.media_player.get_time()
if duration_ms <= 0:
return
pos_sec = pos_ms / 1000.0
if not self.slider_is_pressed:
self.slider.setValue(int((pos_ms / duration_ms) * 1000))
self.time_label.setText(f"{self.format_time(pos_sec)} / {self.format_time(duration_ms / 1000.0)}")
while (self.telemetry_idx + 1 < len(self.telemetry) and
self.telemetry[self.telemetry_idx + 1]["start"] <= pos_sec):
self.telemetry_idx += 1
data = self.telemetry[self.telemetry_idx] if self.telemetry_idx < len(self.telemetry) else {}
self.update_hud(data)
lat = data.get("lat")
lon = data.get("lon")
if lat is not None and lon is not None and self.map_widget:
self.map_widget.update_marker(lat, lon)
if self.media_player.get_state() == vlc.State.Ended:
self.video_finished()
def update_hud(self, data):
lat = data.get('lat')
lon = data.get('lon')
alt = data.get('height', '--')
speed_ms = data.get('h_speed', 0.0)
if lat is not None and lon is not None:
self.gps_label.setText(f"<b>GPS</b><br>{lat:.5f}°, {lon:.5f}°")
if self.start_coords:
dist_km = haversine(self.start_coords[0], self.start_coords[1], lat, lon)
self.dist_label.setText(f"<b>Distanz</b><br>{dist_km*1000:.1f} m")
if isinstance(alt, float):
self.alt_label.setText(f"<b>Höhe</b><br>{alt:.1f} m")
speed_kmh = speed_ms * 3.6
self.speed_label.setText(f"<b>Geschw.</b><br>{speed_kmh:.1f} km/h")
def slider_pressed(self):
if self.media_player.is_playing():
self.slider_is_pressed = True
self.media_player.pause()
self.telemetry_timer.stop()
def slider_released(self):
if self.slider_is_pressed:
seek_ratio = self.slider.value() / 1000.0
self.media_player.set_position(seek_ratio)
new_time_sec = (self.media_player.get_length() / 1000.0) * seek_ratio
self.telemetry_idx = 0
while (self.telemetry_idx + 1 < len(self.telemetry) and
self.telemetry[self.telemetry_idx + 1]["start"] <= new_time_sec):
self.telemetry_idx += 1
self.media_player.play()
self.telemetry_timer.start()
self.slider_is_pressed = False
self.play_btn.setEnabled(False)
self.pause_btn.setEnabled(True)
def video_finished(self):
self.telemetry_timer.stop()
self.play_btn.setEnabled(True)
self.pause_btn.setEnabled(False)
self.slider.setValue(1000)
def format_time(self, sec):
return f"{int(sec)//60:02}:{int(sec)%60:02}"
def closeEvent(self, event):
self.media_player.stop()
self.media_player.release()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
try:
QWebEngineSettings.globalSettings().setAttribute(QWebEngineSettings.WebAttribute.WebGLEnabled, False)
except Exception:
pass
window = MainWindow()
window.show()
sys.exit(app.exec_())