From 561110d37b2dfc60225bc4071c2871e802b75297 Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sat, 17 Aug 2024 13:45:24 +0200 Subject: [PATCH 01/10] video review gui: various improvements - clicking on a yet-to-be-downloaded video moves it to the top of the queue so it is downloaded next - make sure the download thread sleeps when it's done (d'oh!) - add a "status" column, later changes will download by default in low res and offer the option to use higher res --- examples/video_review_gui.py | 62 ++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index bff9fd1..49719b1 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -6,7 +6,6 @@ import sys import re import datetime -import queue import subprocess from configparser import RawConfigParser from datetime import datetime as dt, timedelta @@ -14,8 +13,9 @@ from PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QWidget, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QFileDialog, QHeaderView, QStyle, QSlider, QStyleOptionSlider, QSplitter from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput from PyQt6.QtMultimediaWidgets import QVideoWidget -from PyQt6.QtCore import Qt, QUrl, QTimer, QThread, pyqtSignal, QMutex -from PyQt6.QtGui import QColor, QBrush, QFont +from PyQt6.QtCore import Qt, QUrl, QTimer, QThread, pyqtSignal, QMutex, QWaitCondition +from PyQt6.QtGui import QColor, QBrush, QFont, QIcon +from collections import deque import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -109,12 +109,21 @@ def __init__(self, download_queue, cam): self.download_queue = download_queue self.cam = cam self.mutex = QMutex() + self.wait_condition = QWaitCondition() self.is_running = True def run(self): while self.is_running: + self.mutex.lock() + if len(self.download_queue) == 0: + self.wait_condition.wait(self.mutex) + self.mutex.unlock() + + if not self.is_running: + break + try: - fname, output_path = self.download_queue.get(timeout=1) + fname, output_path = self.download_queue.popleft() output_path = os.path.join(video_storage_dir, output_path) if os.path.isfile(output_path): print(f"File already exists: {output_path}") @@ -128,13 +137,20 @@ def run(self): self.download_complete.emit(output_path) else: print(f"Download failed: {fname}") - except queue.Empty: + except IndexError: pass def stop(self): self.mutex.lock() self.is_running = False self.mutex.unlock() + self.wait_condition.wakeAll() + + def add_to_queue(self, fname, output_path): + self.mutex.lock() + self.download_queue.append((fname, output_path)) + self.wait_condition.wakeOne() + self.mutex.unlock() class VideoPlayer(QWidget): @@ -144,7 +160,7 @@ def __init__(self, video_files): super().__init__() self.setWindowTitle("Reolink Video Review GUI") self.cam = cam - self.download_queue = queue.Queue() + self.download_queue = deque() self.download_thread = DownloadThread(self.download_queue, self.cam) self.download_thread.download_start.connect(self.on_download_start) self.download_thread.download_complete.connect(self.on_download_complete) @@ -161,8 +177,8 @@ def __init__(self, video_files): # Create table widget to display video files self.video_table = QTableWidget() - self.video_table.setColumnCount(9) - self.video_table.setHorizontalHeaderLabels(["Video Path", "Start Datetime", "End Time", "Channel", "Person", "Vehicle", "Pet", "Motion", "Timer" ]) + self.video_table.setColumnCount(10) + self.video_table.setHorizontalHeaderLabels(["Video Path", "Start Datetime", "End Time", "Channel", "Person", "Vehicle", "Pet", "Motion", "Timer", "Status" ]) self.video_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.video_table.setSortingEnabled(True) self.video_table.cellClicked.connect(self.play_video) @@ -289,6 +305,9 @@ def add_video(self, video_path): font.setItalic(True) file_name_item.setFont(font) + status_item = QTableWidgetItem() + status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.video_table.setItem(row_position, 0, file_name_item) self.video_table.setItem(row_position, 1, start_datetime_item) self.video_table.setItem(row_position, 2, QTableWidgetItem(parsed_data['end_time'])) @@ -300,6 +319,7 @@ def add_video(self, video_path): self.video_table.setItem(row_position, 6, QTableWidgetItem("✓" if parsed_data['triggers']['ai_ad'] else "")) self.video_table.setItem(row_position, 7, QTableWidgetItem("✓" if parsed_data['triggers']['is_motion_record'] else "")) self.video_table.setItem(row_position, 8, QTableWidgetItem("✓" if parsed_data['triggers']['is_schedule_record'] else "")) + self.video_table.setItem(row_position, 9, status_item) if parsed_data['triggers']['ai_other']: print(f"File {file_path} has ai_other flag!") @@ -311,10 +331,12 @@ def add_video(self, video_path): output_path = os.path.join(video_storage_dir, base_file_name) if os.path.isfile(output_path): + status_item.setText("4K") self.file_exists_signal.emit(output_path) else: # Add to download queue - self.download_queue.put((video_path, base_file_name)) + status_item.setIcon(QIcon("queued.png")) + self.download_thread.add_to_queue(video_path, base_file_name) else: print(f"Could not parse file {video_path}") @@ -327,6 +349,8 @@ def on_download_complete(self, video_path): font.setItalic(False) font.setBold(False) file_name_item.setFont(font) + status_item = self.video_table.item(row, 9) + status_item.setText("4K") break def on_download_start(self, video_path): @@ -338,6 +362,8 @@ def on_download_start(self, video_path): font = QFont() font.setBold(True) file_name_item.setFont(font) + status_item = self.video_table.item(row, 9) + status_item.setIcon(QIcon("spinner.gif")) break def play_video(self, row, column): @@ -345,7 +371,19 @@ def play_video(self, row, column): video_path = os.path.join(video_storage_dir, file_name_item.text()) if file_name_item.font().italic() or file_name_item.foreground().color().lightness() >= 200: - print(f"Video {video_path} is not yet downloaded. Please wait.") + print(f"Video {video_path} is not yet downloaded. Moving it to top of queue. Please wait for download.") + # Find the item in the download_queue that matches the base file name + found_item = None + for item in list(self.download_queue): + if item[1] == file_name_item.text(): + found_item = item + break + + if found_item: + # Remove the item from its current position in the queue + self.download_queue.remove(found_item) + # Add the item to the end of the queue + self.download_thread.add_to_queue(*found_item) return print(f"Playing video: {video_path}") @@ -430,6 +468,7 @@ def read_config(props_path: str) -> dict: def signal_handler(sig, frame): print("Exiting the application...") sys.exit(0) + QApplication.quit() @@ -458,6 +497,9 @@ def signal_handler(sig, frame): processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=1) + if len(processed_motions) == 0: + print("Camera did not return any video?!") + video_files = [] for i, motion in enumerate(processed_motions): fname = motion['filename'] From 95abc4471ccd54a7f07392c255d6c3be20c5fd00 Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sat, 17 Aug 2024 14:57:10 +0200 Subject: [PATCH 02/10] video review gui: add status column Add status column with icons to indicate download status --- examples/video_review_gui.py | 67 +++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index 49719b1..a568a03 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -178,21 +178,26 @@ def __init__(self, video_files): # Create table widget to display video files self.video_table = QTableWidget() self.video_table.setColumnCount(10) - self.video_table.setHorizontalHeaderLabels(["Video Path", "Start Datetime", "End Time", "Channel", "Person", "Vehicle", "Pet", "Motion", "Timer", "Status" ]) + self.video_table.setHorizontalHeaderLabels(["Status", "Video Path", "Start Datetime", "End Time", "Channel", "Person", "Vehicle", "Pet", "Motion", "Timer" ]) self.video_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.video_table.setSortingEnabled(True) self.video_table.cellClicked.connect(self.play_video) # Set smaller default column widths - self.video_table.setColumnWidth(0, 120) # Video Path - self.video_table.setColumnWidth(1, 130) # Start Datetime - self.video_table.setColumnWidth(2, 80) # End Time - self.video_table.setColumnWidth(3, 35) # Channel - self.video_table.setColumnWidth(4, 35) # Person - self.video_table.setColumnWidth(5, 35) # Vehicle - self.video_table.setColumnWidth(6, 35) # Pet - self.video_table.setColumnWidth(7, 35) # Motion - self.video_table.setColumnWidth(8, 30) # Timer + self.video_table.setColumnWidth(0, 35) # Status + self.video_table.setColumnWidth(1, 120) # Video Path + self.video_table.setColumnWidth(2, 130) # Start Datetime + self.video_table.setColumnWidth(3, 80) # End Time + self.video_table.setColumnWidth(4, 35) # Channel + self.video_table.setColumnWidth(5, 35) # Person + self.video_table.setColumnWidth(6, 35) # Vehicle + self.video_table.setColumnWidth(7, 35) # Pet + self.video_table.setColumnWidth(8, 35) # Motion + self.video_table.setColumnWidth(9, 30) # Timer + + self.video_table.verticalHeader().setVisible(False) + + QIcon.setThemeName("Adwaita") # Create open button to select video files self.open_button = QPushButton("Open Videos") @@ -308,18 +313,18 @@ def add_video(self, video_path): status_item = QTableWidgetItem() status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) - self.video_table.setItem(row_position, 0, file_name_item) - self.video_table.setItem(row_position, 1, start_datetime_item) - self.video_table.setItem(row_position, 2, QTableWidgetItem(parsed_data['end_time'])) - self.video_table.setItem(row_position, 3, QTableWidgetItem(f"{parsed_data['channel']}")) + self.video_table.setItem(row_position, 0, status_item) + self.video_table.setItem(row_position, 1, file_name_item) + self.video_table.setItem(row_position, 2, start_datetime_item) + self.video_table.setItem(row_position, 3, QTableWidgetItem(parsed_data['end_time'])) + self.video_table.setItem(row_position, 4, QTableWidgetItem(f"{parsed_data['channel']}")) # Set individual trigger flags - self.video_table.setItem(row_position, 4, QTableWidgetItem("✓" if parsed_data['triggers']['ai_pd'] else "")) - self.video_table.setItem(row_position, 5, QTableWidgetItem("✓" if parsed_data['triggers']['ai_vd'] else "")) - self.video_table.setItem(row_position, 6, QTableWidgetItem("✓" if parsed_data['triggers']['ai_ad'] else "")) - self.video_table.setItem(row_position, 7, QTableWidgetItem("✓" if parsed_data['triggers']['is_motion_record'] else "")) - self.video_table.setItem(row_position, 8, QTableWidgetItem("✓" if parsed_data['triggers']['is_schedule_record'] else "")) - self.video_table.setItem(row_position, 9, status_item) + self.video_table.setItem(row_position, 5, QTableWidgetItem("✓" if parsed_data['triggers']['ai_pd'] else "")) + self.video_table.setItem(row_position, 6, QTableWidgetItem("✓" if parsed_data['triggers']['ai_vd'] else "")) + self.video_table.setItem(row_position, 7, QTableWidgetItem("✓" if parsed_data['triggers']['ai_ad'] else "")) + self.video_table.setItem(row_position, 8, QTableWidgetItem("✓" if parsed_data['triggers']['is_motion_record'] else "")) + self.video_table.setItem(row_position, 9, QTableWidgetItem("✓" if parsed_data['triggers']['is_schedule_record'] else "")) if parsed_data['triggers']['ai_other']: print(f"File {file_path} has ai_other flag!") @@ -331,43 +336,43 @@ def add_video(self, video_path): output_path = os.path.join(video_storage_dir, base_file_name) if os.path.isfile(output_path): - status_item.setText("4K") + status_item.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) self.file_exists_signal.emit(output_path) else: # Add to download queue - status_item.setIcon(QIcon("queued.png")) + status_item.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_CommandLink)) self.download_thread.add_to_queue(video_path, base_file_name) else: print(f"Could not parse file {video_path}") def on_download_complete(self, video_path): for row in range(self.video_table.rowCount()): - if self.video_table.item(row, 0).text() == os.path.basename(video_path): - file_name_item = self.video_table.item(row, 0) + if self.video_table.item(row, 1).text() == os.path.basename(video_path): + file_name_item = self.video_table.item(row, 1) file_name_item.setForeground(QBrush(QColor(0, 0, 0))) # Black color for normal text font = QFont() font.setItalic(False) font.setBold(False) file_name_item.setFont(font) - status_item = self.video_table.item(row, 9) - status_item.setText("4K") + status_item = self.video_table.item(row, 0) + status_item.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) break def on_download_start(self, video_path): for row in range(self.video_table.rowCount()): - if self.video_table.item(row, 0).text() == os.path.basename(video_path): - file_name_item = self.video_table.item(row, 0) + if self.video_table.item(row, 1).text() == os.path.basename(video_path): + file_name_item = self.video_table.item(row, 1) grey_color = QColor(200, 200, 200) # Light grey file_name_item.setForeground(QBrush(grey_color)) font = QFont() font.setBold(True) file_name_item.setFont(font) - status_item = self.video_table.item(row, 9) - status_item.setIcon(QIcon("spinner.gif")) + status_item = self.video_table.item(row, 0) + status_item.setIcon(QIcon.fromTheme("emblem-synchronizing")) break def play_video(self, row, column): - file_name_item = self.video_table.item(row, 0) + file_name_item = self.video_table.item(row, 1) video_path = os.path.join(video_storage_dir, file_name_item.text()) if file_name_item.font().italic() or file_name_item.foreground().color().lightness() >= 200: From 8c3ed52f82f31d3fe8cee9fd39118b1e9bd4f3a6 Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sat, 17 Aug 2024 15:35:11 +0200 Subject: [PATCH 03/10] assorted updates --- examples/video_review_gui.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index a568a03..b6a5294 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -146,9 +146,12 @@ def stop(self): self.mutex.unlock() self.wait_condition.wakeAll() - def add_to_queue(self, fname, output_path): + def add_to_queue(self, fname, output_path, left=False): self.mutex.lock() - self.download_queue.append((fname, output_path)) + if left: + self.download_queue.appendleft((fname, output_path)) + else: + self.download_queue.append((fname, output_path)) self.wait_condition.wakeOne() self.mutex.unlock() @@ -388,7 +391,7 @@ def play_video(self, row, column): # Remove the item from its current position in the queue self.download_queue.remove(found_item) # Add the item to the end of the queue - self.download_thread.add_to_queue(*found_item) + self.download_thread.add_to_queue(*found_item, left=True) return print(f"Playing video: {video_path}") @@ -493,13 +496,13 @@ def signal_handler(sig, frame): start = dt.combine(dt.now(), dt.min.time()) end = dt.now() - processed_motions = cam.get_motion_files(start=start, end=end, streamtype='main', channel=0) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=1) + processed_motions = cam.get_motion_files(start=start, end=end, streamtype='sub', channel=0) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype='sub', channel=1) start = dt.now() - timedelta(days=1) end = dt.combine(start, dt.max.time()) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=0) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=1) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype='sub', channel=0) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype='sub', channel=1) if len(processed_motions) == 0: From 7e43bbb1309f8514fbd14e778d7edd819ccf9456 Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sat, 17 Aug 2024 15:41:51 +0200 Subject: [PATCH 04/10] upd --- examples/video_review_gui.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index b6a5294..14a3ec1 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -275,7 +275,7 @@ def __init__(self, video_files): def add_initial_videos(self, video_files): for video_path in video_files: self.add_video(video_path) - self.video_table.sortItems(1, Qt.SortOrder.DescendingOrder) + self.video_table.sortItems(2, Qt.SortOrder.DescendingOrder) def open_videos(self): file_dialog = QFileDialog(self) @@ -286,7 +286,7 @@ def open_videos(self): for file in file_dialog.selectedFiles(): self.add_video(os.path.basename(file)) self.video_table.setSortingEnabled(True) - self.video_table.sortItems(1, Qt.SortOrder.DescendingOrder) + self.video_table.sortItems(2, Qt.SortOrder.DescendingOrder) def add_video(self, video_path): # We are passed the camera file name, e.g. Mp4Record/2024-08-12/RecM13_DST20240812_214255_214348_1F1E828_4DDA4D.mp4 @@ -299,7 +299,10 @@ def add_video(self, video_path): start_datetime_str = parsed_data['start_datetime'].strftime("%Y-%m-%d %H:%M:%S") start_datetime_item = QTableWidgetItem(start_datetime_str) start_datetime_item.setData(Qt.ItemDataRole.UserRole, parsed_data['start_datetime']) - + + end_time = datetime.datetime.strptime(parsed_data['end_time'], "%H%M%S") + end_time_str = end_time.strftime("%H:%M:%S") + # Create the item for the first column with the base file name file_name_item = QTableWidgetItem(base_file_name) @@ -319,7 +322,7 @@ def add_video(self, video_path): self.video_table.setItem(row_position, 0, status_item) self.video_table.setItem(row_position, 1, file_name_item) self.video_table.setItem(row_position, 2, start_datetime_item) - self.video_table.setItem(row_position, 3, QTableWidgetItem(parsed_data['end_time'])) + self.video_table.setItem(row_position, 3, QTableWidgetItem(end_time_str)) self.video_table.setItem(row_position, 4, QTableWidgetItem(f"{parsed_data['channel']}")) # Set individual trigger flags From eefdfc53ca14c35993ca3bf60bf74063ca51e388 Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sat, 17 Aug 2024 16:50:49 +0200 Subject: [PATCH 05/10] upd --- examples/video_review_gui.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index 14a3ec1..9b94e5a 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -342,7 +342,6 @@ def add_video(self, video_path): output_path = os.path.join(video_storage_dir, base_file_name) if os.path.isfile(output_path): - status_item.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) self.file_exists_signal.emit(output_path) else: # Add to download queue @@ -353,15 +352,24 @@ def add_video(self, video_path): def on_download_complete(self, video_path): for row in range(self.video_table.rowCount()): - if self.video_table.item(row, 1).text() == os.path.basename(video_path): - file_name_item = self.video_table.item(row, 1) + file_name_item = self.video_table.item(row, 1) + if file_name_item.text() == os.path.basename(video_path): file_name_item.setForeground(QBrush(QColor(0, 0, 0))) # Black color for normal text font = QFont() font.setItalic(False) font.setBold(False) file_name_item.setFont(font) status_item = self.video_table.item(row, 0) - status_item.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) + if "RecS" in file_name_item.text(): + # One day (hopefully) offer the option to download the + # high-res version This is not trivial because we have to + # re-do a camera search for the relevant time period and + # match based on start and end dates (+/- one second in my + # experience) + # For now simply display that this is low-res. + status_item.setText("lowres") + else: + status_item.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) break def on_download_start(self, video_path): @@ -499,13 +507,13 @@ def signal_handler(sig, frame): start = dt.combine(dt.now(), dt.min.time()) end = dt.now() - processed_motions = cam.get_motion_files(start=start, end=end, streamtype='sub', channel=0) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='sub', channel=1) + processed_motions = cam.get_motion_files(start=start, end=end, streamtype='main', channel=0) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=1) start = dt.now() - timedelta(days=1) end = dt.combine(start, dt.max.time()) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='sub', channel=0) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='sub', channel=1) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=0) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=1) if len(processed_motions) == 0: From 24e1ba88bc842d10cf1fd7449d1f1916b735bd4c Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sat, 17 Aug 2024 22:27:39 +0200 Subject: [PATCH 06/10] video_review_gui: more updates - re-download file if it exists but filesize doesn't match expected value - use a QTreeWidget instead of a QTableWidget in order to present channel 1 as a child of channel 0 (you never want to watch channel 1 without having established that channel 0 had something interesting) - add CLI --sub to search the sub stream (faster downloads = faster review) - the new logic also presents sub & main in the same tree root (conceptually, one start datetime = tree root under which all video related to that moment are listed) - add icons for statuses - exit after 1second timeout instead of being blocked until end of download --- examples/video_review_gui.py | 320 ++++++++++++++++++++--------------- 1 file changed, 181 insertions(+), 139 deletions(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index 9b94e5a..7f4618e 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -7,10 +7,11 @@ import re import datetime import subprocess +import argparse from configparser import RawConfigParser from datetime import datetime as dt, timedelta from reolinkapi import Camera -from PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QWidget, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QFileDialog, QHeaderView, QStyle, QSlider, QStyleOptionSlider, QSplitter +from PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QWidget, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QFileDialog, QHeaderView, QStyle, QSlider, QStyleOptionSlider, QSplitter, QTreeWidget, QTreeWidgetItem, QTreeWidgetItemIterator from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput from PyQt6.QtMultimediaWidgets import QVideoWidget from PyQt6.QtCore import Qt, QUrl, QTimer, QThread, pyqtSignal, QMutex, QWaitCondition @@ -62,7 +63,7 @@ def parse_filename(file_name): start_time = match.group(4) # HHMMSS end_time = match.group(5) # HHMMSS flags_hex = match.group(6) # flags hex - file_size = match.group(7) # second hexadecimal + file_size = int(match.group(7), 16) # Combine date and start time into a datetime object start_datetime = datetime.datetime.strptime(f"{start_date} {start_time}", "%Y%m%d %H%M%S") @@ -101,7 +102,7 @@ def pixelPosToRangeValue(self, pos): return QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), pos - sliderMin, sliderMax - sliderMin, opt.upsideDown) class DownloadThread(QThread): - download_complete = pyqtSignal(str) + download_complete = pyqtSignal(str, bool) download_start = pyqtSignal(str) def __init__(self, download_queue, cam): @@ -125,18 +126,15 @@ def run(self): try: fname, output_path = self.download_queue.popleft() output_path = os.path.join(video_storage_dir, output_path) - if os.path.isfile(output_path): - print(f"File already exists: {output_path}") - self.download_complete.emit(output_path) + print(f"Downloading: {fname}") + self.download_start.emit(output_path) + resp = self.cam.get_file(fname, output_path=output_path) + if resp: + print(f"Download complete: {output_path}") + self.download_complete.emit(output_path, True) else: - print(f"Downloading: {fname}") - self.download_start.emit(output_path) - resp = self.cam.get_file(fname, output_path=output_path) - if resp: - print(f"Download complete: {output_path}") - self.download_complete.emit(output_path) - else: - print(f"Download failed: {fname}") + print(f"Download failed: {fname}") + self.download_complete.emit(output_path, False) except IndexError: pass @@ -157,7 +155,7 @@ def add_to_queue(self, fname, output_path, left=False): class VideoPlayer(QWidget): - file_exists_signal = pyqtSignal(str) + file_exists_signal = pyqtSignal(str, bool) def __init__(self, video_files): super().__init__() @@ -179,26 +177,25 @@ def __init__(self, video_files): self.media_player.setPlaybackRate(1.5) # Create table widget to display video files - self.video_table = QTableWidget() - self.video_table.setColumnCount(10) - self.video_table.setHorizontalHeaderLabels(["Status", "Video Path", "Start Datetime", "End Time", "Channel", "Person", "Vehicle", "Pet", "Motion", "Timer" ]) - self.video_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) - self.video_table.setSortingEnabled(True) - self.video_table.cellClicked.connect(self.play_video) + self.video_tree = QTreeWidget() + self.video_tree.setColumnCount(10) + self.video_tree.setHeaderLabels(["Status", "Video Path", "Start Datetime", "End Time", "Channel", "Person", "Vehicle", "Pet", "Motion", "Timer"]) + self.video_tree.setSortingEnabled(True) + self.video_tree.itemClicked.connect(self.play_video) # Set smaller default column widths - self.video_table.setColumnWidth(0, 35) # Status - self.video_table.setColumnWidth(1, 120) # Video Path - self.video_table.setColumnWidth(2, 130) # Start Datetime - self.video_table.setColumnWidth(3, 80) # End Time - self.video_table.setColumnWidth(4, 35) # Channel - self.video_table.setColumnWidth(5, 35) # Person - self.video_table.setColumnWidth(6, 35) # Vehicle - self.video_table.setColumnWidth(7, 35) # Pet - self.video_table.setColumnWidth(8, 35) # Motion - self.video_table.setColumnWidth(9, 30) # Timer - - self.video_table.verticalHeader().setVisible(False) + self.video_tree.setColumnWidth(0, 35) # Status + self.video_tree.setColumnWidth(1, 120) # Video Path + self.video_tree.setColumnWidth(2, 130) # Start Datetime + self.video_tree.setColumnWidth(3, 80) # End Time + self.video_tree.setColumnWidth(4, 35) # Channel + self.video_tree.setColumnWidth(5, 35) # Person + self.video_tree.setColumnWidth(6, 35) # Vehicle + self.video_tree.setColumnWidth(7, 35) # Pet + self.video_tree.setColumnWidth(8, 35) # Motion + self.video_tree.setColumnWidth(9, 30) # Timer + + self.video_tree.setIndentation(10) QIcon.setThemeName("Adwaita") @@ -237,7 +234,7 @@ def __init__(self, video_files): # Left side (table and open button) left_widget = QWidget() left_layout = QVBoxLayout(left_widget) - left_layout.addWidget(self.video_table) + left_layout.addWidget(self.video_tree) left_layout.addWidget(self.open_button) splitter.addWidget(left_widget) @@ -275,126 +272,162 @@ def __init__(self, video_files): def add_initial_videos(self, video_files): for video_path in video_files: self.add_video(video_path) - self.video_table.sortItems(2, Qt.SortOrder.DescendingOrder) + self.video_tree.sortItems(2, Qt.SortOrder.DescendingOrder) +# self.video_tree.expandAll() def open_videos(self): file_dialog = QFileDialog(self) file_dialog.setNameFilters(["Videos (*.mp4 *.avi *.mov)"]) file_dialog.setFileMode(QFileDialog.FileMode.ExistingFiles) if file_dialog.exec(): - self.video_table.setSortingEnabled(False) + self.video_tree.setSortingEnabled(False) for file in file_dialog.selectedFiles(): self.add_video(os.path.basename(file)) - self.video_table.setSortingEnabled(True) - self.video_table.sortItems(2, Qt.SortOrder.DescendingOrder) + self.video_tree.setSortingEnabled(True) + self.video_tree.sortItems(2, Qt.SortOrder.DescendingOrder) +# self.video_tree.expandAll() def add_video(self, video_path): # We are passed the camera file name, e.g. Mp4Record/2024-08-12/RecM13_DST20240812_214255_214348_1F1E828_4DDA4D.mp4 file_path = path_name_from_camera_path(video_path) base_file_name = file_path parsed_data = parse_filename(file_path) - if parsed_data: - row_position = self.video_table.rowCount() - self.video_table.insertRow(row_position) - start_datetime_str = parsed_data['start_datetime'].strftime("%Y-%m-%d %H:%M:%S") - start_datetime_item = QTableWidgetItem(start_datetime_str) - start_datetime_item.setData(Qt.ItemDataRole.UserRole, parsed_data['start_datetime']) - - end_time = datetime.datetime.strptime(parsed_data['end_time'], "%H%M%S") - end_time_str = end_time.strftime("%H:%M:%S") - - # Create the item for the first column with the base file name - file_name_item = QTableWidgetItem(base_file_name) - - # Set the full path as tooltip - file_name_item.setToolTip(base_file_name) - - # Set the style for queued status - grey_color = QColor(200, 200, 200) # Light grey - file_name_item.setForeground(QBrush(grey_color)) - font = QFont() - font.setItalic(True) - file_name_item.setFont(font) - - status_item = QTableWidgetItem() - status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) - - self.video_table.setItem(row_position, 0, status_item) - self.video_table.setItem(row_position, 1, file_name_item) - self.video_table.setItem(row_position, 2, start_datetime_item) - self.video_table.setItem(row_position, 3, QTableWidgetItem(end_time_str)) - self.video_table.setItem(row_position, 4, QTableWidgetItem(f"{parsed_data['channel']}")) - - # Set individual trigger flags - self.video_table.setItem(row_position, 5, QTableWidgetItem("✓" if parsed_data['triggers']['ai_pd'] else "")) - self.video_table.setItem(row_position, 6, QTableWidgetItem("✓" if parsed_data['triggers']['ai_vd'] else "")) - self.video_table.setItem(row_position, 7, QTableWidgetItem("✓" if parsed_data['triggers']['ai_ad'] else "")) - self.video_table.setItem(row_position, 8, QTableWidgetItem("✓" if parsed_data['triggers']['is_motion_record'] else "")) - self.video_table.setItem(row_position, 9, QTableWidgetItem("✓" if parsed_data['triggers']['is_schedule_record'] else "")) - - if parsed_data['triggers']['ai_other']: - print(f"File {file_path} has ai_other flag!") - - # Make the fields non-editable - for column in range(self.video_table.columnCount()): - item = self.video_table.item(row_position, column) - item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) - - output_path = os.path.join(video_storage_dir, base_file_name) - if os.path.isfile(output_path): - self.file_exists_signal.emit(output_path) + if not parsed_data: + print(f"Could not parse file {video_path}") + return + + start_datetime = parsed_data['start_datetime'] + channel = parsed_data['channel'] + + end_time = datetime.datetime.strptime(parsed_data['end_time'], "%H%M%S") + end_time_str = end_time.strftime("%H:%M:%S") + + video_item = QTreeWidgetItem() + video_item.setText(0, "") # Status + video_item.setTextAlignment(0, Qt.AlignmentFlag.AlignCenter) + video_item.setText(1, base_file_name) + video_item.setText(2, start_datetime.strftime("%Y-%m-%d %H:%M:%S")) + video_item.setData(2, Qt.ItemDataRole.UserRole, parsed_data['start_datetime']) + video_item.setText(3, end_time_str) + video_item.setText(4, str(channel)) + video_item.setText(5, "✓" if parsed_data['triggers']['ai_pd'] else "") + video_item.setText(6, "✓" if parsed_data['triggers']['ai_vd'] else "") + video_item.setText(7, "✓" if parsed_data['triggers']['ai_ad'] else "") + video_item.setText(8, "✓" if parsed_data['triggers']['is_motion_record'] else "") + video_item.setText(9, "✓" if parsed_data['triggers']['is_schedule_record'] else "") + + if parsed_data['triggers']['ai_other']: + print(f"File {file_path} has ai_other flag!") + + video_item.setToolTip(1, base_file_name) + # Set the style for queued status + grey_color = QColor(200, 200, 200) # Light grey + video_item.setForeground(1, QBrush(grey_color)) + font = QFont() + font.setItalic(True) + video_item.setFont(1, font) + + # Make the fields non-editable + iterator = QTreeWidgetItemIterator(self.video_tree) + while iterator.value(): + item = iterator.value() + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + iterator += 1 + + # Find a potentially pre-existing channel0 item for this datetime, if so, add as a child + # This lets channel1 appear as a child, but also main & sub videos appear in the same group + channel_0_item = self.find_channel_0_item(start_datetime) + if channel_0_item: + channel_0_item.addChild(video_item) + else: + self.video_tree.addTopLevelItem(video_item) + + output_path = os.path.join(video_storage_dir, base_file_name) + expected_size = parsed_data['file_size'] + + need_download = True + if os.path.isfile(output_path): + actual_size = os.path.getsize(output_path) + if actual_size == expected_size: + need_download = False + self.file_exists_signal.emit(output_path, True) + else: + print(f"File size mismatch for {output_path}. Expected: {expected_size}, Actual: {actual_size}. Downloading again") + + if need_download: + video_item.setIcon(1, self.style().standardIcon(QStyle.StandardPixmap.SP_CommandLink)) + self.download_thread.add_to_queue(video_path, base_file_name) + + def find_channel_0_item(self, datetime_obj): + # Truncate seconds to nearest 10 + truncated_seconds = datetime_obj.second - (datetime_obj.second % 10) + truncated_datetime = datetime_obj.replace(second=truncated_seconds) + + for i in range(self.video_tree.topLevelItemCount()): + item = self.video_tree.topLevelItem(i) + item_datetime = item.data(2, Qt.ItemDataRole.UserRole) + item_truncated = item_datetime.replace(second=item_datetime.second - (item_datetime.second % 10)) + if item_truncated == truncated_datetime: + return item + return None + + def find_item_by_path(self, path): + iterator = QTreeWidgetItemIterator(self.video_tree) + while iterator.value(): + item = iterator.value() + text = item.text(1) + if text == path: + return item + iterator += 1 + print(f"Could not find item by path {path}") + return None + + def on_download_complete(self, video_path, success): + print(f"on_download_complete {video_path} success={success}") + item = self.find_item_by_path(os.path.basename(video_path)) + if not item: + print(f"on_download_complete {video_path} did not find item?!") + item.setForeground(1, QBrush(QColor(0, 0, 0))) # Black color for normal text + font = item.font(1) + font.setItalic(False) + font.setBold(False) + item.setFont(1, font) + if success: + if "RecS" in item.text(1): + # One day (hopefully) offer the option to download the + # high-res version This is not trivial because we have to + # re-do a camera search for the relevant time period and + # match based on start and end dates (+/- one second in my + # experience) + # For now simply display that this is low-res. + item.setText(0, "sub") else: - # Add to download queue - status_item.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_CommandLink)) - self.download_thread.add_to_queue(video_path, base_file_name) + item.setIcon(1, QIcon()) else: - print(f"Could not parse file {video_path}") - - def on_download_complete(self, video_path): - for row in range(self.video_table.rowCount()): - file_name_item = self.video_table.item(row, 1) - if file_name_item.text() == os.path.basename(video_path): - file_name_item.setForeground(QBrush(QColor(0, 0, 0))) # Black color for normal text - font = QFont() - font.setItalic(False) - font.setBold(False) - file_name_item.setFont(font) - status_item = self.video_table.item(row, 0) - if "RecS" in file_name_item.text(): - # One day (hopefully) offer the option to download the - # high-res version This is not trivial because we have to - # re-do a camera search for the relevant time period and - # match based on start and end dates (+/- one second in my - # experience) - # For now simply display that this is low-res. - status_item.setText("lowres") - else: - status_item.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) - break - + item.setIcon(1, self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxCritical)) + + def on_download_start(self, video_path): - for row in range(self.video_table.rowCount()): - if self.video_table.item(row, 1).text() == os.path.basename(video_path): - file_name_item = self.video_table.item(row, 1) - grey_color = QColor(200, 200, 200) # Light grey - file_name_item.setForeground(QBrush(grey_color)) - font = QFont() - font.setBold(True) - file_name_item.setFont(font) - status_item = self.video_table.item(row, 0) - status_item.setIcon(QIcon.fromTheme("emblem-synchronizing")) - break + item = self.find_item_by_path(os.path.basename(video_path)) + if item: + grey_color = QColor(200, 200, 200) # Light grey + item.setForeground(1, QBrush(grey_color)) + font = item.font(1) + font.setBold(True) + item.setFont(1, font) + item.setIcon(1, QIcon.fromTheme("emblem-synchronizing")) + else: + print(f"Cannot find item for {video_path}") - def play_video(self, row, column): - file_name_item = self.video_table.item(row, 1) - video_path = os.path.join(video_storage_dir, file_name_item.text()) + def play_video(self, file_name_item, column): + video_path = os.path.join(video_storage_dir, file_name_item.text(1)) - if file_name_item.font().italic() or file_name_item.foreground().color().lightness() >= 200: + if file_name_item.font(1).italic() or file_name_item.foreground(1).color().lightness() >= 200: print(f"Video {video_path} is not yet downloaded. Moving it to top of queue. Please wait for download.") # Find the item in the download_queue that matches the base file name found_item = None for item in list(self.download_queue): - if item[1] == file_name_item.text(): + if item[1] == file_name_item.text(1): found_item = item break @@ -465,7 +498,9 @@ def open_in_mpv(self): def closeEvent(self, event): self.download_thread.stop() - self.download_thread.wait() + self.download_thread.wait(1000) + self.download_thread.terminate() + self.cam.logout() super().closeEvent(event) def read_config(props_path: str) -> dict: @@ -487,14 +522,19 @@ def read_config(props_path: str) -> dict: def signal_handler(sig, frame): print("Exiting the application...") sys.exit(0) + cam.logout() QApplication.quit() if __name__ == '__main__': signal.signal(signal.SIGINT, signal_handler) -# Read in your ip, username, & password -# (NB! you'll likely have to create this file. See tests/test_camera.py for details on structure) + + parser = argparse.ArgumentParser(description="Reolink Video Review GUI") + parser.add_argument('--sub', action='store_true', help="Search for sub channel instead of main channel") + parser.add_argument('files', nargs='*', help="Optional video file names to process") + args = parser.parse_args() + config = read_config('camera.cfg') ip = config.get('camera', 'ip') @@ -507,13 +547,15 @@ def signal_handler(sig, frame): start = dt.combine(dt.now(), dt.min.time()) end = dt.now() - processed_motions = cam.get_motion_files(start=start, end=end, streamtype='main', channel=0) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=1) + + streamtype = 'sub' if args.sub else 'main' + processed_motions = cam.get_motion_files(start=start, end=end, streamtype=streamtype, channel=0) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype=streamtype, channel=1) start = dt.now() - timedelta(days=1) end = dt.combine(start, dt.max.time()) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=0) - processed_motions += cam.get_motion_files(start=start, end=end, streamtype='main', channel=1) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype=streamtype, channel=0) + processed_motions += cam.get_motion_files(start=start, end=end, streamtype=streamtype, channel=1) if len(processed_motions) == 0: @@ -525,7 +567,7 @@ def signal_handler(sig, frame): print("Processing %s" % (fname)) video_files.append(fname) - video_files.extend(sys.argv[1:]) + video_files.extend([os.path.basename(file) for file in args.files]) app = QApplication(sys.argv) player = VideoPlayer(video_files) player.resize(1900, 1000) From 08c8d0a216d1d38529c657b09e080c15ab5b213b Mon Sep 17 00:00:00 2001 From: Sven337 Date: Wed, 21 Aug 2024 08:51:02 +0200 Subject: [PATCH 07/10] video_review: fix icons with sub stream --- examples/video_review_gui.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index 7f4618e..42811e9 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -187,7 +187,7 @@ def __init__(self, video_files): self.video_tree.setColumnWidth(0, 35) # Status self.video_tree.setColumnWidth(1, 120) # Video Path self.video_tree.setColumnWidth(2, 130) # Start Datetime - self.video_tree.setColumnWidth(3, 80) # End Time + self.video_tree.setColumnWidth(3, 70) # End Time self.video_tree.setColumnWidth(4, 35) # Channel self.video_tree.setColumnWidth(5, 35) # Person self.video_tree.setColumnWidth(6, 35) # Vehicle @@ -401,8 +401,7 @@ def on_download_complete(self, video_path, success): # experience) # For now simply display that this is low-res. item.setText(0, "sub") - else: - item.setIcon(1, QIcon()) + item.setIcon(1, QIcon()) else: item.setIcon(1, self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxCritical)) From e1b3ad9232230f2e86c7de979d8d812d2516ef6e Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sun, 8 Sep 2024 09:09:57 +0200 Subject: [PATCH 08/10] bump to width 2400 --- examples/stream_gui.py | 2 +- examples/video_review_gui.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/stream_gui.py b/examples/stream_gui.py index 45607b3..c174bf9 100644 --- a/examples/stream_gui.py +++ b/examples/stream_gui.py @@ -32,7 +32,7 @@ class CameraPlayer(QWidget): def __init__(self, rtsp_url_wide, rtsp_url_telephoto, camera: Camera): super().__init__() self.setWindowTitle("Reolink PTZ Streamer") - self.setGeometry(10, 10, 1900, 600) + self.setGeometry(10, 10, 2400, 900) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.camera = camera diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index 42811e9..be88d14 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -383,7 +383,6 @@ def find_item_by_path(self, path): return None def on_download_complete(self, video_path, success): - print(f"on_download_complete {video_path} success={success}") item = self.find_item_by_path(os.path.basename(video_path)) if not item: print(f"on_download_complete {video_path} did not find item?!") @@ -569,6 +568,6 @@ def signal_handler(sig, frame): video_files.extend([os.path.basename(file) for file in args.files]) app = QApplication(sys.argv) player = VideoPlayer(video_files) - player.resize(1900, 1000) + player.resize(2400, 1000) player.show() sys.exit(app.exec()) From 6d152aa8dc86690f04c5d7659a9b0ff6cd70105e Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sun, 8 Sep 2024 10:01:22 +0200 Subject: [PATCH 09/10] support new version 9 of filenames --- examples/video_review_gui.py | 55 ++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index be88d14..602548a 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -52,28 +52,61 @@ def decode_hex_to_flags(hex_value): def parse_filename(file_name): # Mp4Record_2024-08-12_RecM13_DST20240812_214255_214348_1F1E828_4DDA4D.mp4 + # Mp4Record_2024-09-13-RecS09_DST20240907_084519_084612_0_55289080000000_307BC0.mp4 # https://github.com/sven337/ReolinkLinux/wiki/Figuring-out-the-file-names#file-name-structure - pattern = r'.*?Mp4Record_(\d{4}-\d{2}-\d{2})_Rec[MS](\d)\d_DST(\d{8})_(\d{6})_(\d{6})_(\w{4,8})_(\w{4,8})\.mp4' + pattern = r'.*?Mp4Record_(\d{4}-\d{2}-\d{2})_Rec[MS](\d)(\d)_(DST)?(\d{8})_(\d{6})_(\d{6})' + v3_suffix = r'.*_(\w{4,8})_(\w{4,8})\.mp4' + v9_suffix = r'.*_(\d)_(\w{7})(\w{7})_(\w{4,8})\.mp4' match = re.match(pattern, file_name) + out = {} + version = 0 + if match: date = match.group(1) # YYYY-MM-DD - channel = int(match.group(2)) # Mx as integer - start_date = match.group(3) # YYYYMMDD - start_time = match.group(4) # HHMMSS - end_time = match.group(5) # HHMMSS - flags_hex = match.group(6) # flags hex - file_size = int(match.group(7), 16) - + channel = int(match.group(2)) + version = int(match.group(3)) # version + start_date = match.group(5) # YYYYMMDD + start_time = match.group(6) # HHMMSS + end_time = match.group(7) # HHMMSS + # Combine date and start time into a datetime object start_datetime = datetime.datetime.strptime(f"{start_date} {start_time}", "%Y%m%d %H%M%S") - triggers = decode_hex_to_flags(flags_hex) - - return {'start_datetime': start_datetime, 'channel': channel, 'end_time': end_time, 'triggers': triggers, 'file_size': file_size} + out = {'start_datetime': start_datetime, 'channel': channel, 'end_time': end_time } else: print("parse error") return None + + if version == 9: + match = re.match(v9_suffix, file_name) + if not match: + print(f"v9 parse error for {file_name}") + return None + + animal_type = match.group(1) + flags_hex1 = match.group(2) + flags_hex2 = match.group(3) + file_size = int(match.group(4), 16) + + triggers = decode_hex_to_flags(flags_hex1) + + out.update({'animal_type' : animal_type, 'file_size' : file_size, 'triggers' : triggers }) + + elif version == 2 or version == 3: + match = re.match(v3_suffix, file_name) + if not match: + print(f"v3 parse error for {file_name}") + return None + + flags_hex = match.group(1) + file_size = int(match.group(2), 16) + + triggers = decode_hex_to_flags(flags_hex) + + out.update({'file_size' : file_size, 'triggers' : triggers }) + + return out class ClickableSlider(QSlider): def mousePressEvent(self, event): From 5549877787d6fc58b484a720ef375592787b5d20 Mon Sep 17 00:00:00 2001 From: Sven337 Date: Sun, 8 Sep 2024 22:16:37 +0200 Subject: [PATCH 10/10] add GetHighRes button to download from main stream when running in sub mode --- examples/video_review_gui.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/examples/video_review_gui.py b/examples/video_review_gui.py index 602548a..011d8eb 100644 --- a/examples/video_review_gui.py +++ b/examples/video_review_gui.py @@ -242,7 +242,11 @@ def __init__(self, video_files): self.mpv_button = QPushButton("MPV") self.mpv_button.clicked.connect(self.open_in_mpv) - + + self.get_highres_button = QPushButton("GetHighRes") + self.get_highres_button.clicked.connect(self.get_highres_stream_for_file) + self.get_highres_button.setEnabled(False) # Disable by default + # Create seek slider self.seek_slider = ClickableSlider(Qt.Orientation.Horizontal) self.seek_slider.setRange(0, 0) @@ -283,6 +287,7 @@ def __init__(self, video_files): control_layout.addWidget(self.play_button) control_layout.addWidget(self.seek_slider) control_layout.addWidget(self.mpv_button) + control_layout.addWidget(self.get_highres_button) controls_layout.addLayout(control_layout) speed_layout = QHBoxLayout() @@ -469,6 +474,9 @@ def play_video(self, file_name_item, column): self.download_thread.add_to_queue(*found_item, left=True) return + # Enable/disable GetHighRes button based on whether it's a sub stream + self.get_highres_button.setEnabled("RecS" in file_name_item.text(1)) + print(f"Playing video: {video_path}") url = QUrl.fromLocalFile(video_path) self.media_player.setSource(url) @@ -483,6 +491,28 @@ def start_playback(): # Timer needed to be able to play at seek offset in the video, otherwise setPosition seems ignored QTimer.singleShot(20, start_playback) + def get_highres_stream_for_file(self): + current_item = self.video_tree.currentItem() + if not current_item or "RecS" not in current_item.text(1): + return + + parsed_data = parse_filename(current_item.text(1)) + if not parsed_data: + print(f"Could not parse file {current_item.text(1)}") + return + + start_time = parsed_data['start_datetime'] - timedelta(seconds=1) + end_time = datetime.datetime.strptime(f"{parsed_data['start_datetime'].strftime('%Y%m%d')} {parsed_data['end_time']}", "%Y%m%d %H%M%S") + timedelta(seconds=1) + + main_files = self.cam.get_motion_files(start=start_time, end=end_time, streamtype='main', channel=parsed_data['channel']) + + if main_files: + for main_file in main_files: + self.add_video(main_file['filename']) + self.video_tree.sortItems(2, Qt.SortOrder.DescendingOrder) + else: + print(f"No main stream file found for {current_item.text(1)}") + def play_pause(self): if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState: self.media_player.pause()