Skip to content

Add CTE LL-DASH ingest publisher - #15

Merged
mondain merged 19 commits into
mainfrom
feature/ctedash
Jul 20, 2026
Merged

Add CTE LL-DASH ingest publisher#15
mondain merged 19 commits into
mainfrom
feature/ctedash

Conversation

@mondain

@mondain mondain commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add a CTE LL-DASH live ingest server for HTTP/1.1 chunked PUT/POST CMAF feeds.
  • Support multiple concurrent ingest paths from day one and publish them through the existing live object API.
  • Add CLI flags, tests, implementation plan, and usage docs.

Test Plan

  • cmake --build build
  • ctest --test-dir build --output-on-failure
  • git diff --check

@mondain
mondain requested review from TilsonJoji and peterchave June 11, 2026 19:21
Comment thread src/live_dash_ingest.cpp
#endif
}

void LiveDashIngestServer::stop() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Noticed that stop() shuts down the listen socket and then joins all worker threads. In a scenario (e.g. a stalled encoder, interrupted network connection etc. ) where worker threads may still be blocked in recv() on already accepted client sockets, this could cause shutdown to hang if a client connects but does not finish sending the request/body.

One possible consideration could be to track active client sockets and shut them down during stop(), or use receive timeouts / non-blocking IO so worker threads can exit cleanly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in dcb2a4b. LiveDashIngestServer::stop() now tracks active accepted client sockets and shuts them down before joining worker threads, so a stalled chunked request blocked in recv() no longer hangs shutdown. Added a regression test that opens a partial chunked PUT, calls stop(), and verifies it returns without waiting for the client to finish the body.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Follow-up in 6c08575: this shutdown path is hardened further against two fd-reuse races that remained after dcb2a4b.

  • close_client() now erases the client fd from active_client_fds before ::close(). Previously it closed first, so the accept loop could reuse the same fd number for a new client, insert it into the set, and then have the finishing worker's erase() drop that new client's registration — leaving stop() unable to ::shutdown() it and hanging the join (the exact stalled-recv hang you described).
  • stop() now keeps the listen fd valid (shutdown, not closed) until after the accept thread joins, then closes it. Previously it closed the listen fd before joining, so the number could be reused while accept_loop was still calling ::accept() on it.

The stalled-encoder case you flagged is covered by the stalled client stop case in openmoq-publisher-live-dash-tests, which shuts down active client sockets in stop() to unblock the blocked recv().

Comment thread src/live_dash_ingest.cpp Outdated
close_fd(client_fd);
break;
}
worker_threads.emplace_back([this, client_fd]() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Noticed that each accepted connection appends a new worker thread, and completed workers seem to be joined only during stop(). In a long-running ingest server with many reconnects/requests, it could cause worker_threads to keep growing for the lifetime of the process

One possible consideration could be to periodically reap completed workers.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in dcb2a4b. Completed workers now mark themselves done, and the accept loop reaps/join-removes completed worker records before adding another accepted connection. stop() still joins any remaining active workers after shutting down the listen socket and active clients.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The opportunistic reaping added in dcb2a4b (reap_finished_workers_locked() on each accept) is unchanged by 6c08575, so the worker_threads vector no longer grows unbounded across reconnects. 6c08575 only touches the per-client fd bookkeeping and the listen-fd close ordering in stop(), not the reaping loop.

@piersoh

piersoh commented Jun 22, 2026

Copy link
Copy Markdown

I've run it:
./build/openmoq-publisher --live-source dash --dash-listen 0.0.0.0:8088 --dash-path /ingest --endpoint https://relay.moq:9668/moq --namespace live --draft 16 --publish-catalog --forward 0
And then pushed a test ingest feed like so:
ffmpeg -re -f lavfi -i "testsrc2=size=1280x720:rate=25" -f lavfi -i "anullsrc=r=48000:cl=stereo" -filter_complex "[0:v]split=2[v1][v2];[v1]scale=1280:720[v720];[v2]scale=640:360[v360]" -map "[v720]" -c:v:0 libx264 -b:v:0 1500k -g 50 -keyint_min 50 -sc_threshold 0 -map "[v360]" -c:v:1 libx264 -b:v:1 500k -g 50 -keyint_min 50 -sc_threshold 0 -map 1:a -c:a aac -b:a 128k -f dash -seg_duration 2 -use_template 1 -use_timeline 0 -init_seg_name 'video$RepresentationID$' -media_seg_name 'video$RepresentationID$' -adaptation_sets "id=0,streams=v id=1,streams=a" -multiple_requests 1 -streaming 1 -remove_at_exit 0 -window_size 20 -extra_window_size 20 http://localhost:8088/ingest/
Once ffmpeg starts pushing the media segments the publisher logs:
connection_id=1301d0605748f2ec
But it fails to go into await-subscribe mode and I'm unable to subscribe to the stream on the relay.

mondain commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up for @piersoh's FFmpeg usage: dcb2a4b adds a live DASH ingest regression test based on that shape. The test uses FFmpeg-style representation paths under the ingest prefix (/ingest/video0, /ingest/video1, /ingest/video2) with separate init requests followed by media requests to the same representation paths, then verifies the server exposes catalog plus representation tracks and emits media objects.

I also probed the minimized FFmpeg command locally and confirmed that the DASH muxer writes video0, video1, and video2 for that naming pattern.

mondain added 3 commits June 25, 2026 05:28
Resolve the confirmed findings from the PR #15 review of the CTE LL-DASH
ingest publisher:

- Guard the ingest worker thread so a malformed fragment (e.g. a moof
  referencing an unknown track) is dropped instead of escaping the thread
  and calling std::terminate.
- Embed each track's base64 CMAF init segment in the catalog via a reusable
  track_init_data_base64 helper so subscribers can initialize decoders.
- Freeze the announced track set when source() is taken; paths whose init
  segments arrive afterwards are ignored rather than enqueued against an
  unknown track (which aborted the whole publish session).
- Add a bounded poll plus an is_finished predicate on LiveObjectSource so
  the publisher services control messages during media gaps and ends only
  on close, instead of blocking indefinitely.
- Give each catalog emission a monotonic group id so a re-published catalog
  no longer collides with an already-delivered object id.
- Erase a client fd from the active set before closing it, and defer closing
  the listen fd until after the accept thread joins, to close two fd-reuse
  races on shutdown.
- Reject trailing garbage in --dash-listen/--dash-queue-depth with clear
  messages and require --dash-path/--dash-queue-depth to accompany
  --live-source dash.

Add regression tests for crash-safety, catalog initData, late-path
rejection, and the CLI validation.
@mondain

mondain commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up hardening in 6c08575, from a correctness/reuse pass over the DASH ingest path. Beyond the shutdown fd-reuse races (replied inline above), it resolves:

  • Crash safety — a malformed fragment (e.g. a moof whose tfhd track_id isn't in the path's init segment) made build_live_fragment throw on the worker thread with no handler up the stack, calling std::terminate and killing the whole publisher on client-controlled input. The fragment is now caught and dropped, and the ingest call is guarded at the thread boundary.
  • Catalog initData — the hand-rolled catalog omitted the per-track base64 CMAF init segment that build_live_catalog emits, so subscribers had no init data to bootstrap decoders. Now embedded via a reusable track_init_data_base64 helper.
  • Late representations — the publisher's track alias table is fixed from the source() snapshot, but the session kept registering paths that connected after the settle window and enqueuing their media, which aborted the whole publish with "live object references unknown track". The announced track set is now frozen at snapshot time and late paths are ignored.
  • Control starvation / hangnext_object_blocking() waited on the condvar with no timeout, so during media gaps the publish loop couldn't service SUBSCRIBE/control messages (and hung forever if the encoder died). Added a bounded poll plus an optional is_finished predicate on LiveObjectSource so gaps keep polling and only close() ends the stream.
  • Duplicate catalog object IDs — catalog re-emission reused group=0/object=0, colliding with an already-delivered catalog on the wire. Each emission now uses a monotonic group id.
  • CLI validation--dash-listen/--dash-queue-depth used bare std::stoi (accepted 80abc as 80, reported stoi on bad input) and --dash-path/--dash-queue-depth weren't tied to --live-source dash. Now strict parsing with clear messages and cross-validation.

Regression tests added for crash-safety, catalog initData, late-path rejection, and the CLI validation. Full suite (10 targets) passes and git diff --check is clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new live ingest mode for the publisher: a CTE LL-DASH / CMAF HTTP/1.1 chunked PUT/POST ingest listener that can accept multiple concurrent ingest paths and publish the resulting live objects via the existing live object API.

Changes:

  • Added LiveDashIngestServer / LiveDashIngestSession (with an incremental chunked-transfer decoder) to ingest CMAF and emit LiveObjects + a catalog.
  • Extended CLI/main wiring to support --live-source dash with --dash-listen, --dash-path, and --dash-queue-depth.
  • Updated live publishing loop to support “transient gaps” via a new LiveObjectSource::is_finished predicate, plus added docs/tests/CMake integration.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/live_dash_ingest_test.cpp New tests for chunked decoding, session behavior, concurrent ingest paths, and server stop behavior.
tests/cli_options_test.cpp New CLI parsing/validation tests for DASH flags and error messages.
src/transport/moqt_session.cpp Adds support for live sources that can report “not finished yet” during temporary object gaps.
src/main.cpp Wires --live-source dash to start the ingest server and publish from its LiveObjectSource.
src/live_dash_ingest.cpp Implements HTTP/1.1 chunked ingest server, decoder, per-path MP4 parsing, and live object/catalog production.
src/cmsf_packager.cpp Exposes track_init_data_base64() helper for embedding per-track init segments in catalogs.
src/cli_options.cpp Adds DASH live-source parsing, strict integer parsing, host:port parsing, and validations for DASH flags.
README.md Documents the new live DASH ingest mode with curl/FFmpeg examples and platform notes.
include/openmoq/publisher/live_object.h Extends LiveObjectSource with is_finished predicate to distinguish gaps vs EOF.
include/openmoq/publisher/live_dash_ingest.h New public header for DASH ingest config/session/server and chunked decoder.
include/openmoq/publisher/cmsf_packager.h Declares track_init_data_base64() for reuse by live sources assembling catalogs.
include/openmoq/publisher/cli_options.h Adds LiveSourceKind::kDash and new DASH-related CLI option fields.
docs/quickstart.md Adds a quickstart section for DASH ingest usage and operational guidance.
docs/ffmpeg.md Adds FFmpeg guidance for pushing live DASH/CMAF over HTTP chunked ingest.
docs/ctedash-implementation-plan.md Adds implementation plan document for the feature.
CMakeLists.txt Builds src/live_dash_ingest.cpp and registers new DASH test executable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/transport/moqt_session.cpp
Comment thread src/live_dash_ingest.cpp
Comment thread src/live_dash_ingest.cpp
mondain and others added 9 commits July 2, 2026 12:48
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
shutdown() on a listening socket does not wake a blocked accept() on
macOS/BSD (it fails with ENOTCONN), so once stop() started keeping the
listener fd open until the accept thread joins, macOS CI hung in the
first server stop and the live-dash test binary hit the ctest timeout.

Gate accept() behind a 100ms poll() that re-checks stop_requested,
matching the client workers' receive cadence, and make the listener
non-blocking so a connection aborted between poll() and accept() cannot
park the loop. Accepted sockets inherit O_NONBLOCK on BSD-derived
systems, so handle_client clears it before applying SO_RCVTIMEO.
publish_live parked its stdin reader thread in istream::read on std::cin,
which blocks until a full 16 KiB chunk or EOF. Every error-path join then
depended on the feeder closing stdin: a relay drop with a live-but-idle
feeder hung the publisher - the same unwakeable-blocking-call shape as the
DASH accept() hang fixed in 65c5a4f.

Route all stdin consumption in publish_live (both the ftyp+moov discovery
phase and the reader thread) through one helper that, for real stdin on
POSIX, reads fd 0 directly behind a bounded 100ms poll and re-checks a stop
flag that joins now set first. Reading the fd raw in only one phase would
lose bytes read ahead into cin's stdio buffer, so both phases share the
helper and the istream is never used for cin. Non-cin istreams (unit tests)
and Windows keep the original blocking read.
Correct setup option parsing, request-stream polling and framing, and draft-17/18 SUBSCRIBE_OK encoding. Add regressions and archive the draft-19 text for later review.
@mondain
mondain merged commit a417f69 into main Jul 20, 2026
3 checks passed
@mondain
mondain deleted the feature/ctedash branch July 20, 2026 22:24
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.

5 participants