From f4e2a3d065550ff058de9b7731d9f6c0d0042749 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Date: Tue, 17 Jun 2025 21:47:46 -0700 Subject: [PATCH 1/6] Fix path issues for hls files for production --- docker-compose.yml | 1 + static/hls.html | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index fdbcaeb..eaf1517 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: - --videopath=/tmp/videos - --rtmp-stream-url=rtmp://nms:1935/live/mystream - --cache-state-file=/tmp/videos/state.json + - --hls-file-path=/tmp/hls volumes: - ./server.py:/app/server.py - ./static/:/app/static/ diff --git a/static/hls.html b/static/hls.html index e4a44e0..0d0d014 100644 --- a/static/hls.html +++ b/static/hls.html @@ -19,7 +19,8 @@ if (Hls.isSupported()) { var video = document.getElementById('video'); var hls = new Hls(); - hls.loadSource('/hls/tv.m3u8'); + const url = new URL('hls/tv.m3u8', window.location.href); + hls.loadSource(url.href); hls.attachMedia(video); // Error handling From 9664188d5840c46efc875aed3ee9e777e12a0ef7 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Date: Fri, 20 Jun 2025 20:44:21 -0700 Subject: [PATCH 2/6] Refactor HLS stream to add Semaphore --- server.py | 87 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/server.py b/server.py index 8b83645..51920e9 100644 --- a/server.py +++ b/server.py @@ -60,6 +60,8 @@ class UrlType(enum.Enum): interlude_lock = threading.Lock() +hls_sem = threading.Semaphore() + args = get_args() # Create a cache object to store video files, initializing it with the file path specified in the command-line arguments or configuration settings. This instance is used to cache downloaded videos. @@ -289,47 +291,54 @@ def handle_cache_play(): def run_hls_stream(): - logging.info("Starting ffmpeg command for HLS stream.") - - playlist_path = Path(args.hls_file_path).resolve() - # Ensure the directory that will contain the playlist exists + playlist_path = Path(args.hls_file_path) playlist_path.parent.mkdir(parents=True, exist_ok=True) - ffmpegcommand = [ - "ffmpeg", - "-i", - args.rtmp_stream_url, - "-c:v", "copy", - "-c:a", "copy", - "-f", "hls", - "-hls_time", "4", - "-hls_list_size", "5", - "-hls_flags", "delete_segments", - f"{args.hls_file_path}/tv.m3u8" - ] - #Delay the command to allow the monitor thread to start - command = [ - "sh", "-c", - f"sleep 2 && {' '.join(ffmpegcommand)}", - ] - logging.info(f"Running command: {' '.join(command)}") - process = subprocess.Popen( - command, - stdout=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, - stderr=subprocess.PIPE, - ) - - logging.info(f"HLS stream started with PID {process.pid}") - - def _monitor_hls_process(p): - logging.info(f"Monitoring HLS process with PID {p.pid}") - exit_code = p.wait() - if exit_code != 0: - error_output = p.stderr.read().decode(errors="replace") - logging.error(f"HLS ffmpeg process exited with code {exit_code}. Error output:\n{error_output}") - - threading.Thread(target=_monitor_hls_process, args=(process,), daemon=True).start() + while True: + ffmpeg_cmd = [ + "ffmpeg", + "-i", args.rtmp_stream_url, + "-c:v", "copy", + "-c:a", "copy", + "-f", "hls", + "-hls_time", "4", + "-hls_list_size", "5", + "-hls_flags", "delete_segments", + f"{args.hls_file_path}/tv.m3u8", + ] + proc = subprocess.Popen( + ffmpeg_cmd, + stdout=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + logging.info(f"HLS worker: started FFmpeg PID {proc.pid}") + # start the logging thread (non-blocking) + threading.Thread( + target=_monitor_ffmpeg, args=(proc,), daemon=True + ).start() + # wait until a playback thread ends + hls_sem.acquire() + # rotate: kill, clean, loop + logging.info("HLS worker: rotate signal, killing FFmpeg & cleaning dir") + kill_child_processes(proc.pid) + _clean_hls_dir() + +def _monitor_ffmpeg(proc: subprocess.Popen): + """Block until proc dies and log exit / stderr.""" + exit_code = proc.wait() + if exit_code == 0: + logging.info(f"HLS ffmpeg exited cleanly (code 0)") + else: + err = proc.stderr.read().decode(errors="replace") + logging.error( + f"HLS ffmpeg exited with code {exit_code}\n---- STDERR ----\n{err}" + ) +def _clean_hls_dir(): + hls_dir = Path(args.hls_file_path) + for f in hls_dir.glob("*.ts"): + f.unlink(missing_ok=True) + (hls_dir / "tv.m3u8").unlink(missing_ok=True) @app.get("/state") async def state(): From c327efe39a231e00f1abb55778f8e4f2762d7c8f Mon Sep 17 00:00:00 2001 From: Matthew Tran Date: Fri, 20 Jun 2025 21:25:48 -0700 Subject: [PATCH 3/6] keep the tv.m3u8 file on cleanup --- server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 51920e9..f9701c2 100644 --- a/server.py +++ b/server.py @@ -146,7 +146,7 @@ def create_ffmpeg_stream( if exit_code == 0 and play_interlude_after and args.interlude: interlude_lock.release() - + hls_sem.release() return exit_code @@ -338,7 +338,7 @@ def _clean_hls_dir(): hls_dir = Path(args.hls_file_path) for f in hls_dir.glob("*.ts"): f.unlink(missing_ok=True) - (hls_dir / "tv.m3u8").unlink(missing_ok=True) + #(hls_dir / "tv.m3u8").unlink(missing_ok=True) @app.get("/state") async def state(): From e1033ca13525baa71682e9a74359151a318bb9b3 Mon Sep 17 00:00:00 2001 From: Matthew Tran Date: Sun, 22 Jun 2025 13:48:09 -0700 Subject: [PATCH 4/6] HLS stream interlude transition fix --- docker-compose.dev.yml | 2 +- server.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index b4c66a8..1d79f0f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -17,7 +17,7 @@ services: # interlude.mp4 must exist in this project in the `videos` folder. # there is an unresolved bug where the server doesn't reload # while an interlude is playing. - # - --interlude=/tmp/videos/interlude.mp4 + - --interlude=/tmp/videos/interlude.mp4 ports: - 5001:5001 volumes: diff --git a/server.py b/server.py index f9701c2..0b2e249 100644 --- a/server.py +++ b/server.py @@ -90,7 +90,7 @@ def create_ffmpeg_stream( loop=False, title=None, thumbnail=None, - play_interlude_after=False, + play_interlude_after=True, ): if video_path is None: logging.info("video_path is None. ffmpeg_stream cancelled.") @@ -135,8 +135,9 @@ def create_ffmpeg_stream( MetricsHandler.streams_count.labels(video_type=video_type.value).inc(amount=1) # the below function returns 0 if the video ended on its own # 137, 1 + logging.info(f"process {process.pid} started for {video_type.value} video: {video_path}") exit_code = process.wait() - logging.info(f"process {process.pid} exited with code {exit_code}") + MetricsHandler.subprocess_count.labels( exit_code=exit_code, ).inc() @@ -144,9 +145,10 @@ def create_ffmpeg_stream( process_dict.pop(video_type) current_video_dict.clear() - if exit_code == 0 and play_interlude_after and args.interlude: + if (exit_code == 0 or video_type == State.PLAYING) and play_interlude_after and args.interlude: interlude_lock.release() hls_sem.release() + logging.info(f"exiting create_ffmpeg_stream with exit code {exit_code}") return exit_code @@ -338,7 +340,6 @@ def _clean_hls_dir(): hls_dir = Path(args.hls_file_path) for f in hls_dir.glob("*.ts"): f.unlink(missing_ok=True) - #(hls_dir / "tv.m3u8").unlink(missing_ok=True) @app.get("/state") async def state(): @@ -383,11 +384,11 @@ async def play_file(file_path: str = "cache", title: str = None, thumbnail: str except Exception as e: logging.exception(e) raise HTTPException(status_code=500, detail="check logs") - finally: - # Start streaming video - # Once video is finished playing (or stopped early), restart interlude - if args.interlude: - interlude_lock.release() + # finally: + # # Start streaming video + # # Once video is finished playing (or stopped early), restart interlude + # if args.interlude: + # interlude_lock.release() @app.post("/play") @@ -474,6 +475,7 @@ async def stop(): # Check if there is a video playing to stop if State.PLAYING in process_dict: # Stop the video playing subprocess + hls_sem.release() stop_video_by_type(State.PLAYING) From 6dc18ccf9d21c7b67bdf794c3b878f2d3071149f Mon Sep 17 00:00:00 2001 From: Viet Nguyen Date: Wed, 25 Jun 2025 22:30:30 -0700 Subject: [PATCH 5/6] Replace semaphore with lock for HLS synchronization and fix log ordering --- docker-compose.dev.yml | 2 +- server.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1d79f0f..b4c66a8 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -17,7 +17,7 @@ services: # interlude.mp4 must exist in this project in the `videos` folder. # there is an unresolved bug where the server doesn't reload # while an interlude is playing. - - --interlude=/tmp/videos/interlude.mp4 + # - --interlude=/tmp/videos/interlude.mp4 ports: - 5001:5001 volumes: diff --git a/server.py b/server.py index 0b2e249..6223c0e 100644 --- a/server.py +++ b/server.py @@ -60,7 +60,7 @@ class UrlType(enum.Enum): interlude_lock = threading.Lock() -hls_sem = threading.Semaphore() +hls_lock = threading.Lock() args = get_args() @@ -135,8 +135,8 @@ def create_ffmpeg_stream( MetricsHandler.streams_count.labels(video_type=video_type.value).inc(amount=1) # the below function returns 0 if the video ended on its own # 137, 1 - logging.info(f"process {process.pid} started for {video_type.value} video: {video_path}") exit_code = process.wait() + logging.info(f"process {process.pid} started for {video_type.value} video: {video_path}") MetricsHandler.subprocess_count.labels( exit_code=exit_code, @@ -147,7 +147,7 @@ def create_ffmpeg_stream( if (exit_code == 0 or video_type == State.PLAYING) and play_interlude_after and args.interlude: interlude_lock.release() - hls_sem.release() + hls_lock.release() logging.info(f"exiting create_ffmpeg_stream with exit code {exit_code}") return exit_code @@ -319,7 +319,7 @@ def run_hls_stream(): target=_monitor_ffmpeg, args=(proc,), daemon=True ).start() # wait until a playback thread ends - hls_sem.acquire() + hls_lock.acquire() # rotate: kill, clean, loop logging.info("HLS worker: rotate signal, killing FFmpeg & cleaning dir") kill_child_processes(proc.pid) @@ -475,7 +475,7 @@ async def stop(): # Check if there is a video playing to stop if State.PLAYING in process_dict: # Stop the video playing subprocess - hls_sem.release() + hls_lock.release() stop_video_by_type(State.PLAYING) From e9c9e59d8f93053160f5d73d4b898b237deffbcd Mon Sep 17 00:00:00 2001 From: NicholasLe04 Date: Mon, 16 Mar 2026 15:45:32 -0700 Subject: [PATCH 6/6] Update README title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf2334d..7f87da5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# burger +# SCE TV (previously burger) SCE TV