main.py
This commit is contained in:
commit
048c182b57
372
main.py
Normal file
372
main.py
Normal file
@ -0,0 +1,372 @@
|
||||
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_())
|
Loading…
x
Reference in New Issue
Block a user