From f7b8910bd8c3608589843686656d7c27eebbb101 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 18 Dec 2025 19:46:06 +0000 Subject: [PATCH] ADD file system event handling to monitor & allow for timestamp-based seeking --- rewind/daemon.py | 35 ++++++++++++++--------------- rewind/video.py | 57 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/rewind/daemon.py b/rewind/daemon.py index 644aa16..4bf3ed3 100755 --- a/rewind/daemon.py +++ b/rewind/daemon.py @@ -8,6 +8,8 @@ import subprocess from rewind.video import get_duration from rewind.paths import load_state, write_state, load_config +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler INTERVAL = 10 MAX_AGE_SECONDS = 60 * 60 * 1 @@ -49,14 +51,13 @@ def add_file_to_state(file_path: str) -> None: state = load_state() files = state.get("files", []) - # Update duration of last file if exists if files and len(files) > 0: last_file = files[-1] - last_file["duration"] = get_duration(last_file["path"]) + last_file["e_timestamp"] = datetime.datetime.now().timestamp() files.append({ "path": file_path, - "duration": 0.0 + "timestamp": datetime.datetime.now().timestamp(), }) state["files"] = files @@ -66,6 +67,12 @@ def handle_shutdown(signum, frame): global running running = False +class Handler(FileSystemEventHandler): + def on_created(self, event): + if not event.is_directory and event.src_path.endswith(".ts"): + add_file_to_state(event.src_path) + print(f"Added new file to state: {event.src_path}") + def main() -> None: signal.signal(signal.SIGINT, handle_shutdown) signal.signal(signal.SIGTERM, handle_shutdown) @@ -75,29 +82,19 @@ def main() -> None: config = load_config() con = open_obs_connection(config["obs"]["host"], config["obs"]["port"], config["obs"]["password"]) - if con is None: - return + recording_dir = con.get_record_directory().record_directory start_recording(con) - create_state_file() - current_files = os.listdir(recording_dir) - try: + event_handler = Handler() + observer = Observer() + observer.schedule(event_handler, path=recording_dir, recursive=False) + observer.start() + while running: cleanup_old_files(recording_dir, MAX_AGE_SECONDS) - - new_files = os.listdir(recording_dir) - added_files = set(new_files) - set(current_files) - - # Add new files to state - for filename in added_files: - file_path = os.path.join(recording_dir, filename) - add_file_to_state(file_path) - print(f"Added new file to state: {file_path}") - - current_files = new_files time.sleep(INTERVAL) finally: stop_recording(con) diff --git a/rewind/video.py b/rewind/video.py index 6961dad..61e2440 100644 --- a/rewind/video.py +++ b/rewind/video.py @@ -4,25 +4,53 @@ import datetime import subprocess from rewind.paths import load_state +""" +Retrieves .ts files recorded between the specified timestamps. +Returns a list of file paths and extra start and end offsets if needed. +get_duration() is used as little as possible since it is slow. +end_timestamp of a file is the start time of the next file. +""" +def get_ts_files(start_timestamp: float, end_timestamp: float) -> tuple[list[str], float, float]: + ts_files = load_state()["files"] + selected_files = [] + start_offset = 0.0 + end_offset = 0.0 + + for i, file_info in enumerate(ts_files): + file_start = file_info["timestamp"] + file_end = ts_files[i + 1]["timestamp"] if i + 1 < len(ts_files) else get_duration(file_info["path"]) + file_start + + if file_end <= start_timestamp: + continue + if file_start >= end_timestamp: + break + + selected_files.append(file_info["path"]) + + if file_start <= start_timestamp < file_end: + start_offset = start_timestamp - file_start + if file_start < end_timestamp <= file_end: + end_offset = file_end - end_timestamp + + return selected_files, start_offset, end_offset + + def combine_last_x_ts_files(seconds: float, output_file: str) -> None: - ts_files = load_state().get("files", []) - ts_files[-1]["duration"] = get_duration(ts_files[-1]["path"]) + ts_files = load_state()["files"] - total_duration = 0.0 - files_to_include = [] - - while ts_files and total_duration < seconds: - ts_file = ts_files.pop() - files_to_include.append(ts_file["path"]) - total_duration += ts_file["duration"] + files, start_offset, end_offset = get_ts_files( + datetime.datetime.now().timestamp() - seconds, + datetime.datetime.now().timestamp() + ) + + print(f"Combining files: {files} with start offset {start_offset} and end offset {end_offset}") - files_to_include.reverse() with open("file_list.txt", "w") as f: - for file_path in files_to_include: + for file_path in files: f.write(f"file '{file_path}'\n") subprocess.run(["ffmpeg", "-y", - "-ss", str(max(0, total_duration - seconds)), + "-ss", str(start_offset), "-f", "concat", "-safe", "0", "-i", "file_list.txt", "-c", "copy", @@ -43,4 +71,9 @@ def get_duration(file_path: str) -> float: stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) + + # error checking + if result.returncode != 0: + raise RuntimeError(f"ffprobe failed for file {file_path}") + return float(result.stdout) \ No newline at end of file