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("GPS
--") self.alt_label = QLabel("Höhe
--") self.speed_label = QLabel("Geschw.
--") self.dist_label = QLabel("Distanz
--") 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("GPS
--") self.alt_label.setText("Höhe
--") self.speed_label.setText("Geschw.
--") self.dist_label.setText("Distanz
--") 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"GPS
{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"Distanz
{dist_km*1000:.1f} m") if isinstance(alt, float): self.alt_label.setText(f"Höhe
{alt:.1f} m") speed_kmh = speed_ms * 3.6 self.speed_label.setText(f"Geschw.
{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_())