Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# burger
# SCE TV (previously burger)

SCE TV

Expand Down
107 changes: 59 additions & 48 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class UrlType(enum.Enum):

interlude_lock = threading.Lock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] The HLS stream rotation logic in run_hls_stream has a critical flaw regarding the hls_lock initialization and usage. hls_lock is initialized as unlocked. When run_hls_stream starts, it immediately starts an FFmpeg process for HLS, then acquires the hls_lock (which succeeds immediately as it's unlocked), kills the FFmpeg process it just started, cleans the directory, starts another FFmpeg process, and then blocks indefinitely on the second hls_lock.acquire() call because no hls_lock.release() has occurred yet. This leads to an immediate, unnecessary rotation of the HLS stream on startup, followed by indefinite blocking of the HLS rotation mechanism until the first external hls_lock.release() signal is received from another thread (e.g., when a video starts or stops).

hls_lock = threading.Lock()

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.
Expand Down Expand Up @@ -88,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.")
Expand Down Expand Up @@ -134,17 +136,19 @@ def create_ffmpeg_stream(
# the below function returns 0 if the video ended on its own
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW] The logging message logging.info(f"process {process.pid} started for {video_type.value} video: {video_path}") is placed after process.wait(). At this point, the FFmpeg process has already exited, not 'started'. For accurate logging, this message should be moved to immediately after subprocess.Popen to reflect when the process actually begins execution.

# 137, 1
exit_code = process.wait()
logging.info(f"process {process.pid} exited with code {exit_code}")
logging.info(f"process {process.pid} started for {video_type.value} video: {video_path}")

MetricsHandler.subprocess_count.labels(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] The condition for releasing interlude_lock has been expanded to include video_type == State.PLAYING. This means an interlude will now play after a video process exits if it was of type State.PLAYING, regardless of its exit code (e.g., even if it was forcibly stopped). This is a change in behavior for interlude playback.

exit_code=exit_code,
).inc()
if video_type in process_dict:
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_lock.release()
logging.info(f"exiting create_ffmpeg_stream with exit code {exit_code}")
return exit_code


Expand Down Expand Up @@ -289,47 +293,53 @@ 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_lock.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)

@app.get("/state")
async def state():
Expand Down Expand Up @@ -374,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")
Expand Down Expand Up @@ -465,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_lock.release()
stop_video_by_type(State.PLAYING)


Expand Down