Skip to content

Make streaming actually work end-to-end#2

Open
Cfomodz wants to merge 1 commit into
mainfrom
claude/e2e-functionality-hgkmki
Open

Make streaming actually work end-to-end#2
Cfomodz wants to merge 1 commit into
mainfrom
claude/e2e-functionality-hgkmki

Conversation

@Cfomodz

@Cfomodz Cfomodz commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

The core pipeline previously streamed an FFmpeg test pattern instead of real content, and starting any stream crashed before FFmpeg even launched. This PR makes the whole flow work: login → create stream → start → real page/game frames arrive at the RTMP endpoint — verified end-to-end against a local RTMP sink for the HTML, pygame, and test-pattern paths.

What was broken

  • It never streamed your content. The headless path had TODO stubs pushing a testsrc test pattern instead of the page/game. headless_streamer.py tried to screenshot Chrome via a DevTools HTTP endpoint that doesn't exist (screenshots require a WebSocket CDP session), so it produced zero frames, and --virtual-time-budget froze pages after 5s.
  • Starting a stream crashed: JSON columns (audio_config, multi_stream_targets) came out of SQLite as strings and got .get() called on them; display/port numbers came from int(uuid[-1]), which raises ValueError for hex letters.
  • The X11 path captured a black screen — Chrome ran with --headless under Xvfb, so nothing rendered to the display x11grab recorded.
  • ~10 API routes called StreamDatabase methods that didn't exist (templates, platform configs, project streams) → instant AttributeError.
  • Editing/deleting a running stream deadlocked the whole app (non-reentrant lock re-acquired on the same thread), and every automatic recovery failed because start_streaming() refused to run while status was still "live".
  • Slow 24/7 killers: subprocess stderr PIPEs nobody drained (Chrome/FFmpeg block once the 64KB buffer fills), stale live statuses surviving restarts, multi-target tee muxer missing its required -map args, and a committed test streams.db.

What this PR does

Real frame capture (new):

  • capture_html.py — attaches to headless Chrome via the DevTools screencast API over WebSocket and pipes JPEG frames to FFmpeg at a fixed frame rate. Chrome now launches with --remote-allow-origins and without --disable-images.
  • capture_pygame.py — runs user pygame scripts under SDL's dummy driver, intercepts display.flip()/update(), and emits frames on a fixed-rate schedule (duplicating the last frame while the game idles) so the stream timeline tracks wall-clock.
  • Pipe-fed FFmpeg normalizes frames to the preset resolution, adds the silent AAC track platforms require, and uses -shortest so it exits when capture ends (instead of streaming silence forever).
  • X11 path uses kiosk-mode (non-headless) Chrome so x11grab actually captures content; headless_streamer.py rewritten around the same capture helpers.

Crash/correctness fixes in stream_manager.py:

  • Decode JSON columns when loading streams; hash-derived display/debug-port numbers; reentrant stream_lock; _full_restart resets status first so recovery can succeed; fallback to test pattern cleans up partial pipelines (orphaned FFmpeg previously kept fighting for the RTMP endpoint); _restart_ffmpeg only bounces FFmpeg in isolation on the X11 path.
  • Added the missing DB methods (template CRUD, create_stream_from_template, platform config CRUD, get_project_streams).
  • Persist orientation from the create form; widen update_stream allowed fields; audio update writes to the real column; test-pattern drawtext str - int TypeError fixed, unsafe characters escaped, lavfi input paced with -re.
  • Stale live/error statuses reset at startup; Flask session secret persisted so restarts don't log users out; subprocess output to DEVNULL/log files instead of unread PIPEs.

Misc: browser binary auto-detection (STREAMDROP_BROWSER override), monitor.sh fixes (syntax error, hardcoded /home/toor paths, query against a nonexistent table), fonts added to setup.sh (page rendering + drawtext need them), websocket-client added / unused selenium/opencv/numpy dropped, committed test streams.db removed.

How it was verified

Ran the real user flow through the web API against a local RTMP sink (ffmpeg -listen 1):

  • HTML stream: extracted frames show the actual page — live JS clock advancing, CSS animation moving — at exactly 1280×720/30fps H.264+AAC. Not a test pattern.
  • Pygame stream: example_game.py's bouncing balls captured headlessly, pillar-boxed to 1280×720.
  • Test-pattern fallback delivers to RTMP; login accepts/rejects correctly; all previously-crashing CRUD endpoints return sane responses; stop kills every pipeline process; statuses reset on restart; the monitor auto-recovers a killed pipeline; editing a live stream no longer deadlocks.

Known limitation

Per-stream display/debug-port numbers are hash-derived (sha1(id) % 500), so two streams have a ~1-in-500 chance of colliding. Fine for the typical few-streams-per-droplet setup; proper port allocation can follow if needed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VEoYpbVmPMBeLkoWRtdva7


Generated by Claude Code

The core pipeline previously streamed an FFmpeg test pattern instead of
real content (TODOs in _start_headless_ffmpeg_stream), and starting any
stream crashed on unparsed JSON columns and UUID-derived display/port
numbers. This makes the whole flow work: login -> create stream ->
start -> real page/game frames arrive at the RTMP endpoint (verified
against a local RTMP sink for HTML, pygame, and test-pattern paths).

Real frame capture:
- New capture_html.py: attaches to headless Chrome via the DevTools
  screencast API (websocket) and pipes JPEG frames to FFmpeg at a fixed
  frame rate. Chrome now launched with --remote-allow-origins and
  without --disable-images.
- New capture_pygame.py: runs user pygame scripts under SDL's dummy
  driver, intercepts display.flip()/update(), and emits frames at a
  fixed rate (duplicating the last frame when the game idles) so the
  stream timeline tracks wall-clock.
- Pipe-fed FFmpeg normalizes frames to the preset resolution and adds a
  silent AAC track with -shortest so it exits when capture ends.
- X11 path: Chrome no longer launched with --headless under Xvfb (it
  rendered nothing for x11grab); kiosk mode instead.
- headless_streamer.py rewritten around the same capture helpers; its
  old HTTP screenshot endpoint doesn't exist in CDP and produced zero
  frames, and --virtual-time-budget froze pages.

Crash and correctness fixes in stream_manager.py:
- Parse audio_config/multi_stream_targets/schedule_config JSON columns
  when loading streams (previously str.get() AttributeError on start).
- Derive X display / debug port numbers from a hash of the stream ID
  (int(uuid[-1]) raised ValueError for hex letters).
- Add the ~10 StreamDatabase methods that routes referenced but never
  existed (templates get/update/delete, create_stream_from_template,
  platform config CRUD, get_project_streams).
- stream_lock is now reentrant: updating or deleting a running stream
  deadlocked the whole app (update_stream held the lock and called
  stop_stream which re-acquired it).
- _full_restart reset status first (start_streaming refused to run
  while status was "live", so every automatic recovery failed).
- _restart_ffmpeg only restarts FFmpeg in isolation on the X11 path;
  pipe-fed and test-pattern pipelines restart whole, and fallback to
  test pattern cleans up partial pipelines (orphaned FFmpeg processes
  kept fighting for the RTMP endpoint).
- Test pattern: fix str-int TypeError in drawtext y position, escape
  unsafe characters in the overlay text, pace lavfi input with -re.
- Persist orientation (the create form sent it; it was dropped), widen
  update_stream allowed fields, fix audio update writing to a
  nonexistent column.
- Reset stale live/error statuses at startup; persist the Flask session
  secret so restarts don't log users out; send subprocess output to
  DEVNULL/log files (unread PIPEs filled up and blocked Chrome/FFmpeg
  after a while, killing 24/7 streams).
- Multi-target tee muxer now includes the required -map arguments.

Misc:
- Detect chromium/chrome binary instead of hardcoding chromium-browser
  (override with STREAMDROP_BROWSER).
- monitor.sh: fix syntax error in process count, hardcoded /home/toor
  paths, and a query against a nonexistent table.
- setup.sh installs fonts (drawtext and page rendering need them).
- requirements: add websocket-client, drop unused selenium/opencv/numpy.
- Remove committed test streams.db (already gitignored).
- main.py: initialize ffmpeg_process (cleanup crashed on failed start);
  smart_streamer passes credentials to the HTMLStreamer fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VEoYpbVmPMBeLkoWRtdva7
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants