fix: library + caller-only recorders + live signal indicator #2

Closed
zgaetano wants to merge 0 commits from fix/library-and-signal-indicator into main
Owner

Three problems from last night's testing pass.

1. Library always rendered empty

GET /api/v1/assets returns { assets, total } but index.html (and capture.html's recent-captures grid) assumed r.data was an array. r.data.forEach() silently threw, the grid stayed empty, the count badge showed undefined assets. Uploads were succeeding — the user just never saw them.

Fix lives in api.js: getAssets() and getRecentCaptures() now unwrap r.data.assets centrally and stash the count on r.total. All existing callers keep working.

2. Caller-mode SRT/RTMP recorded audio only

When the recorder pulled an MPEG-TS stream from a remote SRT/RTMP source, ffmpeg connected fast enough to read the audio PMT but missed the H.264 SPS/PPS. The video stream was logged as pix_fmt=none and silently dropped from the stream map, so the resulting ProRes hires had only pcm_s24le and the thumbnail step exploded because the proxy had no frames.

Stream #0:0[0x100]: Video: h264 (...), none, 90k tbr, 90k tbn
Stream #0:1[0x101]: Audio: aac (LC), 44100 Hz, ...
Stream mapping:
  Stream #0:1 -> #0:0 (aac (native) -> pcm_s24le (native))   <-- video dropped

Fix in capture-manager.js: add -probesize 32M -analyzeduration 10M -fflags +genpts for all network inputs and force -map 0:v:0? -map 0:a:0? so each track lives or dies on its own. After the patch the same SRT/RTMP source maps both tracks every time.

3. Hitting Record gave no feedback

You couldn't tell whether ffmpeg actually connected, what was wrong if it didn't, or whether the upstream was sending anything. The UI just said "Recording 00:00:01…" forever.

  • capture-manager.js now parses ffmpeg's progress lines (frame=N fps=X) and tracks framesReceived, currentFps, lastFrameAt, plus the most recent error string. getStatus() derives a signal enum: connecting | receiving | lost (frames stopped for 5s+) | error | stopped.
  • Each recorder container now gets a stable network alias recorder-<id> so mam-api can reach it.
  • GET /api/v1/recorders/:id/status proxies the live capture status through to surface signal, framesReceived, currentFps, lastError to the client.
  • recorders.html polls that endpoint every 2 seconds while a recorder is recording and renders a badge under each active card: "Connecting…", "Receiving • 217 fr • 30 fps", "No signal — stream dropped", or the actual ffmpeg error message.

Bonus: caller-only UI

Per request, dropped listener mode from the create-recorder UI entirely. The MAM is no longer offered as an RTMP/SRT server — every new recorder calls out to an external source URL. Existing listener-mode records still render but the listener mode toggle is gone.

Verified

End-to-end QA with all three protocols against a bluenviron/mediamtx test publisher on the same docker network:

2026-05-17T11:31:30 ready qa-srt-v2_20260517_113128     proxy=True thumb=True   (SRT caller)
2026-05-17T11:32:14 ready qa-rtmp-v2_20260517_113214    proxy=True thumb=True   (RTMP caller)
2026-05-17T11:37:48 ready qa-signal_20260517_113748     proxy=True thumb=True   (with signal polling)

Failure path also verified — pointing a recorder at srt://nowhere.invalid:9999 surfaces "Connection error" with the actual ffmpeg error string visible on hover, no orphan container leaked.

Three problems from last night's testing pass. ## 1. Library always rendered empty `GET /api/v1/assets` returns `{ assets, total }` but `index.html` (and `capture.html`'s recent-captures grid) assumed `r.data` was an array. `r.data.forEach()` silently threw, the grid stayed empty, the count badge showed `undefined assets`. Uploads were succeeding — the user just never saw them. Fix lives in `api.js`: `getAssets()` and `getRecentCaptures()` now unwrap `r.data.assets` centrally and stash the count on `r.total`. All existing callers keep working. ## 2. Caller-mode SRT/RTMP recorded audio only When the recorder pulled an MPEG-TS stream from a remote SRT/RTMP source, ffmpeg connected fast enough to read the audio PMT but missed the H.264 SPS/PPS. The video stream was logged as `pix_fmt=none` and silently dropped from the stream map, so the resulting ProRes hires had only `pcm_s24le` and the thumbnail step exploded because the proxy had no frames. ``` Stream #0:0[0x100]: Video: h264 (...), none, 90k tbr, 90k tbn Stream #0:1[0x101]: Audio: aac (LC), 44100 Hz, ... Stream mapping: Stream #0:1 -> #0:0 (aac (native) -> pcm_s24le (native)) <-- video dropped ``` Fix in `capture-manager.js`: add `-probesize 32M -analyzeduration 10M -fflags +genpts` for all network inputs and force `-map 0:v:0? -map 0:a:0?` so each track lives or dies on its own. After the patch the same SRT/RTMP source maps both tracks every time. ## 3. Hitting Record gave no feedback You couldn't tell whether ffmpeg actually connected, what was wrong if it didn't, or whether the upstream was sending anything. The UI just said "Recording 00:00:01…" forever. * `capture-manager.js` now parses ffmpeg's progress lines (`frame=N fps=X`) and tracks `framesReceived`, `currentFps`, `lastFrameAt`, plus the most recent error string. `getStatus()` derives a `signal` enum: `connecting` | `receiving` | `lost` (frames stopped for 5s+) | `error` | `stopped`. * Each recorder container now gets a stable network alias `recorder-<id>` so mam-api can reach it. * `GET /api/v1/recorders/:id/status` proxies the live capture status through to surface `signal`, `framesReceived`, `currentFps`, `lastError` to the client. * `recorders.html` polls that endpoint every 2 seconds while a recorder is recording and renders a badge under each active card: "Connecting…", "Receiving • 217 fr • 30 fps", "No signal — stream dropped", or the actual ffmpeg error message. ## Bonus: caller-only UI Per request, dropped listener mode from the create-recorder UI entirely. The MAM is no longer offered as an RTMP/SRT server — every new recorder calls out to an external source URL. Existing listener-mode records still render but the listener mode toggle is gone. ## Verified End-to-end QA with all three protocols against a `bluenviron/mediamtx` test publisher on the same docker network: ``` 2026-05-17T11:31:30 ready qa-srt-v2_20260517_113128 proxy=True thumb=True (SRT caller) 2026-05-17T11:32:14 ready qa-rtmp-v2_20260517_113214 proxy=True thumb=True (RTMP caller) 2026-05-17T11:37:48 ready qa-signal_20260517_113748 proxy=True thumb=True (with signal polling) ``` Failure path also verified — pointing a recorder at `srt://nowhere.invalid:9999` surfaces "Connection error" with the actual ffmpeg error string visible on hover, no orphan container leaked.
zgaetano added 1 commit 2026-05-17 07:40:36 -04:00
Three problems blocked the end-to-end flow:

1) Library always rendered empty because /assets returns {assets,total} but
   index.html (and capture.html) assumed r.data was an array. Fixed in
   api.js by unwrapping r.data.assets centrally; total is kept on r.total.

2) SRT/RTMP caller mode pulled audio only. ffmpeg opened the network input
   before the H264 SPS arrived, marked the video stream as pix_fmt=none,
   and silently dropped it from the stream map. Added -probesize 32M
   -analyzeduration 10M -fflags +genpts and explicit -map 0✌️0?/0🅰️0? so
   each track survives independently of when it appears.

3) Hitting Record gave no feedback about whether a stream was actually
   arriving. capture-manager now parses ffmpeg progress lines (frame=...
   fps=...) and tracks framesReceived, currentFps, lastFrameAt, lastError.
   getStatus() returns a derived signal enum (connecting | receiving |
   lost | error | stopped). The recorder controller gives each spawned
   container a stable network alias `recorder-<id>` and the GET
   /recorders/:id/status endpoint proxies the live capture status through.
   recorders.html polls that every 2s and renders the badge under each
   active card with the running frame/fps counter or the ffmpeg error.

Also:
* recorders.html: dropped the listener-mode UI entirely. All new recorders
  are caller-mode (pull). The MAM is no longer offered as an RTMP/SRT
  server. Legacy listener records still render but read-only.
zgaetano added 1 commit 2026-05-17 08:31:02 -04:00
The api.js library-list fix from the previous commit never reached the browser because nginx served all .js with `Cache-Control: public, immutable; max-age=31536000`. The HTML referenced api.js with no version query, so the browser kept its year-cached buggy copy.

* nginx.conf: drop .js from the immutable long-cache block, add a no-cache must-revalidate block so future redeploys are picked up immediately.
* All HTML files: tag api.js refs with ?v=4 so already-running browsers fetch the new version on next page load.
zgaetano added 1 commit 2026-05-17 08:55:16 -04:00
Click any asset card to open a modal with the H.264 proxy playing inline (or audio/image, per media_type). Esc or click outside closes. Sidebar shows status/codec/resolution/fps/duration/size/created plus tags and notes.

Plugin install side: added install-windows.ps1 that copies the CEP panel to %APPDATA%\Adobe\CEP\extensions, flips PlayerDebugMode=1 across the CSXS.8-13 hives, and prints the next steps. Plugin already wired against the current API.

* services/web-ui/public/js/preview.js: standalone IIFE that lazy-injects the modal markup + CSS on first use. Renders <video controls> (or <audio>, <img>) sourced from /api/v1/assets/:id/stream, with sidebar from /api/v1/assets/:id. Falls back to a clear empty state when proxy is still processing.
* services/web-ui/public/index.html: loads preview.js, wires asset-card click to window.openAssetPreview(asset.id), guards against delete-button clicks bubbling.
* services/premiere-plugin/install-windows.ps1: one-shot Windows installer for the CEP extension.
zgaetano added 1 commit 2026-05-17 12:55:56 -04:00
Trash icon in the library was firing PATCH /assets/:id with {status:"deleted"}. The PATCH route only accepts display_name/tags/notes so it returned "No fields to update" and the asset stayed put.

* api.js: add deleteAsset(id, {hard}) helper hitting the real DELETE route.
* index.html: deleteAssetPrompt now calls deleteAsset (soft archive). Confirm dialog reworded to match.
* mam-api/routes/assets.js: list endpoint hides status=archived by default. Pass ?include_archived=true to see them in a future restore-from-trash view. Filtering by ?status=archived still works for power users.
* All HTML: bump api.js cache-buster v=4 -> v=5 so the new helper is fetched.
zgaetano added 1 commit 2026-05-17 13:10:50 -04:00
Adds the BMG-branded "AMPP Safe" hardhat photo as the visual identity for the auth + first-load surfaces.

* services/web-ui/public/img/ampp-safe.jpg (52 KB, 1200w optimized JPEG)
* services/web-ui/public/login.html: full redesign as a two-column hero + sign-in panel. Hero shows the hardhat photo full-bleed with a subtle AMPP Safe pill badge and broadcast-safe caption. Login + first-run admin setup forms unchanged functionally.
* services/web-ui/public/index.html: brief first-visit splash overlay (~1.4s) using the same image. Dismisses to the library and uses sessionStorage so it only shows once per session.
zgaetano added 1 commit 2026-05-17 14:48:37 -04:00
* Library cards now show a checkbox on hover (and persistent when selected). Click checkbox = toggle, shift-click = range. Plain click on a card with an active selection extends/shrinks the selection instead of opening preview. Floating pill at the bottom shows count + Move / Copy / Delete / Clear. Move + Copy open a tiny bin picker (current project, default to current bin).

* mam-api/routes/assets.js: PATCH /:id now also accepts bin_id (null = move out of bin). New POST /:id/copy makes a reference-copy of the asset row (same S3 keys, new id) into the target bin/project.

* api.js: moveAsset(id, binId) and copyAsset(id, {binId, projectId}) helpers.

* All accent tokens swapped from the amber oklch(76% 0.178 52) to the Wild Dragon signature blue oklch(55% 0.20 266) = #1f3ad0 ish. Login splash + first-load splash + signal-receiving + button primary all picked it up automatically through common.css.

* Loading indicator across the app uses the AMPP Safe hardhat photo gently pulsing with a tiny blue dot underneath. .ampp-loading component lives in common.css with --sm / --xs / --inline variants. Replaces the plain "Loading assets…" empty state in index.html.
zgaetano added 2 commits 2026-05-17 18:39:22 -04:00
The source image had a black border baked in. Knocked out the dark pixels into an alpha channel so the figure now floats on whatever surface is behind it — the dark gradient on the splash, the panel surface on the loading indicator, anywhere.

Pipeline: source -> resize 1200w -> python/PIL alpha-from-luminance with soft 22-55 luminance ramp -> 8-bit RGBA PNG (267KB).
Two things that together stop bogus URLs from masquerading as a recording:

PROBE BUTTON in the New Recorder panel. Before you commit to record, hit Probe Source - the capture container runs ffprobe with a 10s timeout against the URL and returns the parsed streams. UI shows green Signal Detected with codec/resolution/fps/audio, or red No Signal Detected with the actual ffprobe error message. For SDI it lists DeckLink devices. Listener-mode sources cannot be probed standalone (would block waiting for a publisher) and the UI says so.

MAIN STATUS LABEL ON THE RECORDING CARD now mirrors the live signal instead of hardcoding Recording. So a recorder pointed at a dead URL goes Connecting... -> Connection error (red) instead of looking like everything is fine. When frames actually start arriving the label flips to Recording (blue) and the dot turns blue. If a previously-good stream drops the label switches to Signal lost (red).

API:
* capture: POST /capture/probe runs ffprobe and returns { ok, streams, format, error? }
* mam-api: POST /api/v1/recorders/probe proxies through to the capture sidecar with a 15s outer timeout
zgaetano added 1 commit 2026-05-17 19:05:25 -04:00
Confirmed shape brief: ops-console direction, balanced density, blue committed accent, readability emphasized.

Design system: surface scale to 5 steps tinted toward hue 266; text contrast lifted (secondary 62->72%, tertiary 44->52, added text-disabled); borders gained faint variant; status tokens renamed semantically (signal-good/warn/bad/idle); typography upgraded (Inter 400/500/600, JetBrains Mono for numerics, new 2xl/3xl/tc scale steps).

New components: tc-display timecode classes with blue glow; tally-word with good/warn/bad/rec variants; signal-strip flutter bar with modifiers; chip dense monospaced pill; manifest table styles.

Topbar status strip: new js/topbar-strip.js auto-injects a 28px strip on every page with wall clock, page name, live API latency, and system-pulse dot.

Per-page: recorders gain live signal-strip above fps line; capture timecode bumped 38->64px with blue glow; ingest drop zone slimmed 200->88px side-by-side layout so manifest gets the real estate.
zgaetano added 1 commit 2026-05-17 19:10:22 -04:00
A thumbnail job from earlier stayed 'active' for 6+ hours: worker was restarted at 70% progress, BullMQ left it in the active set, and there was no stall reaper because the worker was created with only the default options.

Worker now passes stalledInterval: 30000, lockDuration: 60000, lockRenewTime: 15000, maxStalledCount: 1 to the Worker constructor. If a run dies, BullMQ reclaims the job back to waiting within 30s and a 'stalled' event is logged. Otherwise the lock is renewed mid-job.

Jobs UI gains a 'Kill' button per row next to Details. Calls DELETE /api/v1/jobs/:id which already removes the job from Redis. Use it on any row that looks stuck.
zgaetano added 1 commit 2026-05-17 21:44:38 -04:00
Vendored Augani/openreel-video (MIT) into services/editor and wired it to the MAM. Editor runs as its own container on port 47435. Library assets pull in via ?asset=<uuid>; render exports route back via POST /api/v1/upload/simple. Sidebar Editor link on every page; Edit button on every preview modal. See services/editor/INTEGRATION.md for the patch map.
zgaetano added 1 commit 2026-05-17 22:20:50 -04:00
Three problems were blocking the round-trip. Each fixed.

* MediaBridge.importFromURL went through the file-import service but not the Zustand store, so the media bin stayed empty. mam-bridge now calls window.__projectStore.getState().importMedia(file) which is what the actual UI uses. project-store.ts exposes useProjectStore on window for that hook.
* rustfs serves the proxy with content-type application/octet-stream; the editor rejects with DECODE_ERROR on that mime. Bridge now forces video/mp4 (or audio/wav, video/webm, etc.) based on the asset filename.
* The Recover Your Work modal and the Welcome tour blocked editor initialization. Bridge now auto-clicks Start Fresh and Skip Tour (alongside the format chooser), and waits 1.5s of project-id stability before calling importMedia so it does not get clobbered by the project-replacement cycle. One-shot guard prevents duplicate imports.
zgaetano added 1 commit 2026-05-17 22:44:10 -04:00
Clicking Edit on the preview modal worked, but the user only saw an empty editor for ~25s while the recovery + format-chooser cycle ran and the bridge waited for a stable project. Looked broken. Now: a centered top banner appears the moment the bridge detects ?asset=, reads Loading clip from Z-AMPP MAM, switches to Clip ready in media bin on success, or surfaces the failure. Project-stability gate tightened from 1500ms to 600ms so the import lands sooner.
zgaetano added 1 commit 2026-05-18 07:30:29 -04:00
While a recorder is running, the capture container tees an HLS
stream into /live/<assetId>/ alongside the ProRes master upload.
The asset row is pre-created at recorder start with status='live'
so the clip appears in the library immediately. /api/v1/assets/:id/stream
returns the HLS playlist URL until recording stops, then proxy.

* docker-compose: shared wild-dragon-live mount on api/capture/web-ui
* migration 001-add-live-status: idempotent ALTER TYPE for asset_status
* mam-api: runMigrations() on boot; recorders.js pre-creates live asset
  + passes ASSET_ID; assets.js POST upserts on existing live row instead
  of inserting a duplicate, and stream route returns HLS for live assets
* capture: parallel HLS ffmpeg into /live/<assetId>/; ASSET_ID env
* web-ui: nginx serves /live/, preview.js loads hls.js, LIVE badge added
zgaetano added 1 commit 2026-05-18 07:46:09 -04:00
zgaetano added 1 commit 2026-05-18 07:57:50 -04:00
zgaetano added 1 commit 2026-05-18 09:25:57 -04:00
zgaetano added 1 commit 2026-05-18 09:28:50 -04:00
zgaetano added 1 commit 2026-05-18 09:40:44 -04:00
zgaetano added 1 commit 2026-05-18 09:47:05 -04:00
zgaetano added 1 commit 2026-05-18 09:56:22 -04:00
zgaetano added 1 commit 2026-05-18 09:58:36 -04:00
zgaetano added 1 commit 2026-05-18 09:59:55 -04:00
zgaetano added 1 commit 2026-05-18 10:01:38 -04:00
zgaetano added 1 commit 2026-05-18 10:03:33 -04:00
zgaetano added 1 commit 2026-05-18 10:07:54 -04:00
zgaetano added 1 commit 2026-05-18 10:11:32 -04:00
zgaetano added 1 commit 2026-05-18 10:13:09 -04:00
zgaetano added 1 commit 2026-05-18 10:16:14 -04:00
zgaetano added 1 commit 2026-05-18 10:17:33 -04:00
zgaetano added 1 commit 2026-05-18 10:18:16 -04:00
zgaetano added 1 commit 2026-05-18 10:25:56 -04:00
zgaetano added 1 commit 2026-05-18 10:49:47 -04:00
zgaetano added 1 commit 2026-05-18 10:56:56 -04:00
zgaetano added 1 commit 2026-05-18 11:05:32 -04:00
zgaetano added 2 commits 2026-05-18 23:01:03 -04:00
- Remove @import Google Fonts from common.css (was blocking CSS on LAN)
- Update Editor nav link on all pages to dynamically resolve to :47435
  (OpenReel SPA) using inline script so it works on any hostname
- Fix page titles from Wild Dragon -> Z-AMPP across all pages
- Resolver: <a href="#" id="editor-nav-link"> + IIFE sets href at load
zgaetano added 1 commit 2026-05-18 23:02:59 -04:00
zgaetano closed this pull request 2026-05-24 12:03:24 -04:00

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: WildDragonLLC/dragonflight#2
No description provided.