Skip to content

feat(ffi): finish a broadcast cleanly in every binding - #6157

Merged
kixelated merged 17 commits into
mainfrom
quest/m1/linger-ffi
Oct 22, 2026
Merged

kixelated merged 17 commits into
mainfrom
quest/m1/linger-ffi

Conversation

@kixelated

@kixelated kixelated commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Quest: quest/m1/linger-ffi.md

A binding app whose input ended cleanly can now finish its broadcast. Subscribers still catching up can drain the finished tracks until the media is stale. The binding is a thin layer over moq-net's broadcast::Producer::finish and broadcast::Finish, which are unchanged. close still ends a broadcast abruptly.

API shape

Binding Finish Linger Wait Interrupt
moq-ffi (UniFFI) MoqBroadcastProducer::finish() -> MoqBroadcastFinish linger_us() async wait() close(); a dropped wait future closes too
Python BroadcastProducer.finish() -> BroadcastFinish linger: timedelta await wait() cancel the task, or close()
Go (*BroadcastProducer).Finish() (*BroadcastFinish, error) Linger() time.Duration Wait(ctx) error end ctx, or Close()
Kotlin BroadcastProducer.finish(): BroadcastFinish linger: Duration suspend wait() cancel the coroutine, or end()
Swift BroadcastProducer.finish() throws -> BroadcastFinish lingerUs wait() async throws cancel the task, or close()
Dart BroadcastProducer.finish() -> BroadcastFinish linger: Duration Future wait() close() (futures can't be cancelled)
C++ (generated) BroadcastProducer::finish() linger_us() wait() future close()
C (moq-c) moq_publish_finish(broadcast, &linger_us) -> finish id written to linger_us none: hold for the linger on your own clock moq_publish_finish_close(finish)

finish() consumes the producer: every later call on it fails Closed, as after close(). A wait() after close(), or a second wait(), also fails Closed.

Changes

  • moq-ffi: adds MoqBroadcastFinish. A per-broadcast stash keeps finished producers listed until the broadcast ends. This covers media, audio and video importers, which used to drop their producer at finish(), and raw tracks, whose handle a GC may release early. Without it, the finish held nothing. The linger counts from finish() on the moq-clock, not from the first poll of wait(), so a mocked clock and a C caller's own clock agree on when it ends.
  • moq-c: adds moq_publish_finish and moq_publish_finish_close. The same stash keeps finished tracks, media and containers. pair_mut becomes an internal Target struct.
  • Wrappers: py, go, kt, swift and dart get hand-written wrappers. C++ is generated, so it only gets docs and a probe check. Every binding gets a smoke test, and the doc/lib pages for py, swift, kt, go, dart, c and cpp document the new calls.

Public API and wire impact

  • Additive: the finish calls in every binding listed above.
  • Breaking (behavior):
    • A finished track now stays listed, with its catalog rendition and demand, until its broadcast closes or its finish lets go.
    • Until then, its name stays taken.
    • Its demand watch, or in C its terminal demand callback, ends with the broadcast instead of at the finish.
    • MIGRATE.md entries are in rs/moq-ffi, rs/moq-c, py, go, kt, swift, dart and cpp.
  • Wire: no change.

Decisions

Settled by the maintainer (2026-10-21): every recommendation below is accepted.

  1. When a finished track is retired. Recommended: keep it until the broadcast ends (this PR), because otherwise the linger has nothing to hold.
    • Cost: an app that finishes a rendition mid-broadcast to replace it now keeps the old rendition in the catalog until the broadcast ends. A broadcast that churns tracks also holds each finished one until it ends.
    • Alternative: broadcast.finish() finishes the still-open importers and tracks itself, and an explicit track finish() keeps retiring at once. This preserves mid-broadcast swaps, but a broadcast needs a registry of its open producers, and apps must not finish tracks first.
  2. The FFI linger counts from finish(), not from the first wait() poll as Rust's Finish::wait does. Recommended as is: the C API has no wait to poll, and a late wait() shouldn't extend the linger. Rust could match this in a follow-up.
  3. C shape. moq_publish_finish(broadcast, uint64_t *linger_us) returns a finish handle, and moq_publish_finish_close releases it. Recommended as is. A NULL linger_us is refused before the broadcast is consumed.
  4. Kotlin renames the finish handle's close to end, matching BroadcastProducer.end(), because AutoCloseable close() collides. Recommended as is.
  5. JSON and flate tracks are not held. moq-mux json/flate finish(self) consumes and drops the producer, so a finished JSON track unlists. Recommended: a follow-up in moq-mux to keep the producer after finishing.
  6. OBS does not finish on a clean stop. Recommended: leave cpp/obs closing at once. A live OBS stop expects the output to end immediately, the session drain already delivers the tail, and lingering would keep the session up after the user pressed Stop.

Review fixes

After merging origin/main (MIGRATE.md and export-list conflicts, regenerated Dart bindings):

  • A finish whose driver check fails now closes the broadcast, in moq-ffi and moq-c, so a retained dynamic handle can't keep it announced.
  • A repeated raw track finish() in moq-ffi keeps the track once instead of appending a clone per call.
  • A delivered C track request carries the broadcast's finished stash, so a raw accept still works after the broadcast closed or finished.
  • The moq-c finish test no longer races a cancel against its finished track's end.
  • Go's BroadcastFinish.Wait on an already-done context, and a Python wait cancelled before its first step, now close the finish as cancelling promises.
  • The generated packages' own MIGRATE.md files (py/moq-ffi, go/ffi, dart/moq_ffi, kt/moq-ffi, swift/ffi) note the finish change.
  • Declined: holding finished JSON/flate producers (decision 5, a moq-mux follow-up), and finishing cleanly on an OBS stop (decision 6).

Checks

  • just check (moq-c, moq-ffi, py, kt, go, dart and docs selected): passed on 8261731; the head adds only MIGRATE.md entries.
  • just go check and just py check passed with their fixes; each new Go and Python test fails without its fix.
  • cargo test -p moq-c -p moq-ffi: 129 and 247 passed. Each new regression test fails without its fix.
  • just test interop --all: passed after the merge.
  • Earlier on 912cf45: just go check, just dart check, just cpp check (probe at C++17/23) passed.
  • Not run: just swift check, which needs macOS, so the new Swift smoke test is unverified locally.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 9 commits October 20, 2026 21:35
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A binding's garbage collector may release a finished track handle before
the broadcast finishes, which unlisted the track so the linger held nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each binding exposes `BroadcastProducer.finish()` and its finish handle,
mapping a cancelled wait to its own idiom: a done context in Go, a
cancelled coroutine in Kotlin, a cancelled task in Swift. Dart and C++
futures can't be cancelled, so closing the handle is the interrupt there.
Kotlin names the handle's close `end`, like the broadcast's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	doc/lib/dart/index.md
#	doc/lib/kt/index.md
#	doc/lib/py/index.md
#	doc/lib/swift/index.md
#	go/wrapper/MIGRATE.md
#	rs/moq-c/MIGRATE.md
#	rs/moq-ffi/MIGRATE.md
…r merge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 21, 2026 16:19
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 94524bf9-a69e-4d52-8855-ca65219bdb92
📥 Commits

Reviewing files that changed from the base of the PR and between 7dc0441 and 4a7c803.

📒 Files selected for processing: 48
  • cpp/moq/MIGRATE.md
  • cpp/moq/test/probe.cpp
  • dart/moq/MIGRATE.md
  • dart/moq/lib/src/aliases.dart
  • dart/moq/lib/src/durations.dart
  • dart/moq/test/moq_test.dart
  • dart/moq_ffi/MIGRATE.md
  • dart/moq_ffi/lib/src/moq.dart
  • doc/lib/c/index.md
  • doc/lib/cpp/index.md
  • doc/lib/dart/index.md
  • doc/lib/go/index.md
  • doc/lib/kt/index.md
  • doc/lib/py/index.md
  • doc/lib/swift/index.md
  • go/ffi/MIGRATE.md
  • go/wrapper/MIGRATE.md
  • go/wrapper/moq_test.go
  • go/wrapper/publish.go
  • kt/moq-ffi/MIGRATE.md
  • kt/moq/MIGRATE.md
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Durations.kt
  • kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt
  • py/moq-ffi/MIGRATE.md
  • py/moq-rs/MIGRATE.md
  • py/moq-rs/moq/__init__.py
  • py/moq-rs/moq/publish.py
  • py/moq-rs/tests/test_local.py
  • py/moq-rs/tests/test_testing_clock.py
  • rs/moq-c/MIGRATE.md
  • rs/moq-c/src/api.rs
  • rs/moq-c/src/audio.rs
  • rs/moq-c/src/publish.rs
  • rs/moq-c/src/test.rs
  • rs/moq-c/src/video.rs
  • rs/moq-ffi/MIGRATE.md
  • rs/moq-ffi/src/audio.rs
  • rs/moq-ffi/src/media.rs
  • rs/moq-ffi/src/origin.rs
  • rs/moq-ffi/src/producer.rs
  • rs/moq-ffi/src/test.rs
  • rs/moq-ffi/src/video.rs
  • rs/moq-ffi/uniffi.toml
  • swift/MIGRATE.md
  • swift/Sources/Moq/Broadcast.swift
  • swift/Tests/MoqTests/SmokeTests.swift
  • swift/ffi/MIGRATE.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T19:25:40.060366Z 4a7c803 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 912cf45338

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +124 to +130
impl Finished {
/// Keep a finished producer until the broadcast ends, or drop it now if it already has.
pub(crate) fn keep(&self, producer: impl kio::MaybeSend + 'static) {
let producer: Box<dyn kio::MaybeSend> = Box::new(producer);
let mut kept = self.0.lock().unwrap();
if let Some(kept) = kept.as_mut() {
kept.push(producer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retain finished JSON and binary producers

Extend this registry to the adopted JSON and binary producers too. MoqJsonSnapshotProducer::finish, MoqJsonStreamProducer::finish, and both flate finish methods still consume and drop their sole producer, so the track, catalog entry, and demand watch disappear before MoqBroadcastProducer::finish() can hold them; the analogous C Data<T> slabs have the same omission. Late subscribers therefore cannot drain these finished tracks despite the new documented guarantee. Carry Finished through adopt/data_track and retain these producers just like raw and media producers.

AGENTS.md reference: AGENTS.md:L105-L109

Useful? React with 👍 / 👎.

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.

Declining for this PR. This is decision 5 in the body, which the maintainer accepted: moq-mux's json/flate finish(self) consumes and drops the sole producer, so holding it here would mean reaching around moq-mux. The fix belongs in moq-mux (keep the producer after finishing), landed as a follow-up, after which these tracks get held by the same stash with no FFI-specific plumbing.

(Written by Claude Opus 5.5)

Comment thread rs/moq-ffi/src/producer.rs Outdated
Comment on lines +352 to +356
if let Err(err) = state.catalog.finish() {
state.broadcast.close();
return Err(err.into());
}
state.driver.check()?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close the broadcast when the driver check fails

If the background driver has panicked, this ? returns after state has already been removed but before a Finish is created or the broadcast is explicitly closed. A retained dynamic handle can then keep the broadcast and its announcement alive while the caller has permanently lost the producer handle needed to close it. Close the broadcast on this error path, as the preceding catalog-failure branch does; rs/moq-c/src/publish.rs::Publish::finish has the same ordering.

Useful? React with 👍 / 👎.

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 c96b19c: a failed driver check now closes the broadcast on the same path as a failed catalog finish, in both MoqBroadcastProducer::finish and moq-c's Publish::finish. Regression tests a_finish_after_a_driver_panic_closes_the_broadcast (moq-ffi and moq-c) hold a dynamic handle, panic the driver, and assert the broadcast is closed after the failed finish; both fail without the fix.

(Written by Claude Opus 5.5)

Comment thread rs/moq-ffi/src/producer.rs Outdated
Comment on lines +749 to +752
if track.end().is_none() {
track.finish()?;
}
self.finished.keep(track.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain the raw track only on its first finish

When finish() is called repeatedly on the same raw track, the existing end().is_none() guard skips the terminal operation but this unconditional call still appends another clone to the broadcast's retained vector. Since the method remains callable after a successful finish, repeated calls can grow memory without bound until the broadcast ends. Add the retained clone only when the track transitions to finished.

Useful? React with 👍 / 👎.

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 c96b19c: the track handle's keep is now taken by the first finish(), so repeating it (or a finish() after finish_at) keeps the track exactly once. Regression test repeated_finishes_keep_the_track_once fails without the fix. moq-c is unaffected: moq_publish_track_finish releases the handle, so it can't repeat.

(Written by Claude Opus 5.5)

kixelated and others added 3 commits October 21, 2026 10:43
# Conflicts:
#	cpp/moq/MIGRATE.md
#	dart/moq/MIGRATE.md
#	dart/moq_ffi/lib/src/moq.dart
#	go/wrapper/MIGRATE.md
#	kt/moq/MIGRATE.md
#	py/moq-rs/MIGRATE.md
#	py/moq-rs/moq/__init__.py
#	rs/moq-c/MIGRATE.md
#	rs/moq-ffi/MIGRATE.md
#	rs/moq-ffi/src/media.rs
#	swift/MIGRATE.md
…once

A finish that failed its driver check had already consumed the producer, so a
retained dynamic handle kept the broadcast announced with nothing able to close
it. Close it on that path too, in moq-ffi and moq-c.

A repeated raw track finish appended another clone to the broadcast's kept
producers each time; the first finish now takes the keep.

The moq-c finish test no longer cancels a subscription its finished track may
already have ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Merged origin/main (MIGRATE.md and binding-list conflicts, regenerated Dart bindings) and fixed both P2 findings with regression tests; the P1 is declined per decision 5.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c96b19c811

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-c/src/publish.rs Outdated
Comment on lines +759 to +760
let TrackRequest { broadcast, request } = self.track_request.remove(request).ok_or(Error::NotFound)?;
let finished = self.finished(broadcast)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve raw requests across broadcast teardown

When a dynamic raw-track request has already been delivered and the application concurrently closes or finishes its broadcast, that operation removes the broadcast from self.broadcasts; this new lookup therefore returns BroadcastNotFound after the request has already been removed from track_request, rejecting a request that previously could still be accepted through its own producer handle. This also diverges from the FFI implementation, which stores the Finished handle directly on MoqTrackRequest. Carry the registry handle with the queued C TrackRequest rather than resolving it from the now-invalid broadcast ID.

Useful? React with 👍 / 👎.

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 e7b432d: the queued C TrackRequest now carries the broadcast's finished stash, captured when moq_publish_dynamic spawns the handler, so a raw accept no longer resolves the broadcast ID and works after the broadcast closed or finished. Regression test track_request_accepts_after_the_broadcast_closes fails with -24 (BroadcastNotFound) without it. Audio and video accepts still need the broadcast's catalog, as before this PR.

(Written by Claude Opus 5.5)

A raw track request looked up its broadcast's finished stash at accept, so a
broadcast closed or finished after the request was delivered failed the accept
with BroadcastNotFound. The request now carries the stash, as moq-ffi's does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Fixed the C track request finding in e7b432d with a regression test.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: e7b432d50f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Conflicts:
#	dart/moq_ffi/lib/src/moq.dart
#	rs/moq-ffi/src/audio.rs
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Merged origin/main again (#6091 conflicted on the MoqAudioProducer::finish doc and the generated Dart checksums, regenerated with just dart generate). No other changes since the clean review of e7b432d.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 85f5020635

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/wrapper/publish.go
// Wait keeps the broadcast up for its linger, or until [BroadcastFinish.Close],
// then closes it. Cancelling ctx closes the broadcast at once.
func (f *BroadcastFinish) Wait(ctx context.Context) error {
return bridge.CallErr(ctx, f.inner.Close, f.inner.Wait)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close finishes for already-cancelled contexts

When ctx is already cancelled before Wait is called, bridge.CallErr returns from internal/bridge/call.go lines 38-40 without invoking its cancellation callback, so this path never calls f.inner.Close(). A caller that discards the finish after receiving context.Canceled therefore leaves the broadcast announced until the handle is eventually destroyed, contrary to Wait's cancellation contract; explicitly close the finish when entering with an already-done context.

Useful? React with 👍 / 👎.

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 f77e634: BroadcastFinish.Wait now closes the finish itself when ctx is already done, since the bridge starts no call (and so runs no cancel) on a done context. Regression test TestBroadcastFinishWaitOnDoneContextCloses waits on a cancelled context, then expects a second wait to fail ErrClosed at once; without the fix it lingers until its deadline.

(Written by Claude Opus 5.5)

The context bridge starts no native call on an already-done context, so it
never reached its cancel and the broadcast stayed announced, against Wait's
contract that cancelling closes it at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Fixed the Go done-context finding in f77e634 with a regression test.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f77e634e6a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// the handle for [`MoqBroadcastFinish::linger_us`] on your own clock and then close it.
///
/// Every later call on this producer fails with `Closed`, as after [`close`](Self::close).
pub fn finish(&self) -> Result<Arc<MoqBroadcastFinish>, MoqError> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the clean finish path when OBS stops

When the OBS publisher stops, cpp/obs/src/moq-output.cpp::Reset finishes its video and audio tracks but still calls broadcast->close() at lines 421-442, immediately retracting the broadcast and preventing late viewers from draining the retained tail for its max-age linger. Update that workflow to call finish() and retain or drive its wait alongside the existing session drain; the required cross-package synchronization also calls for updating the OBS documentation.

AGENTS.md reference: AGENTS.md:L107-L110

Useful? React with 👍 / 👎.

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.

Declining: this is decision 6 in the body, which the maintainer accepted. OBS keeps closing at once on a stop. A user pressing Stop expects the output to end immediately, the session drain already delivers the tail to connected subscribers, and lingering would keep the session up after Stop. Since OBS behavior is unchanged, doc/bin/obs.md needs no update.

(Written by Claude Opus 5.5)

Comment thread py/moq-rs/moq/publish.py Outdated
Comment on lines +378 to +381
try:
await self._inner.wait()
except asyncio.CancelledError:
self._inner.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close a wait cancelled before its first poll

When a caller runs task = asyncio.create_task(finish.wait()) and immediately cancels it before the event loop first polls the task, Python never enters this coroutine, so the except block does not call _inner.close(). The broadcast therefore remains announced for its full linger, or until the handle is later released, despite the public docstring promising that cancelling the wait closes it at once; return a cancellation-aware future whose cancel() performs the close rather than relying solely on coroutine-body cleanup.

AGENTS.md reference: py/AGENTS.md:L16-L16

Useful? React with 👍 / 👎.

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 8261731: BroadcastFinish.wait() now returns a coroutine object that closes the finish when a cancellation is thrown into it. That covers a task cancelled before its first step, which never runs a coroutine body, while await, create_task and ensure_future all still work. Regression test test_finish_wait_cancelled_before_it_starts_closes times out waiting for the announcement to end without the fix.

(Written by Claude Opus 5.5)

A task cancelled before its first step never runs its coroutine body, so the
except that closed the finish was skipped and the broadcast lingered. The wait
is now a coroutine object that closes on the cancellation thrown into it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Fixed the Python cancel-before-first-step finding in 8261731 with a regression test; declined the OBS finding per decision 6.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82617312f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +11266 to +11269
abstract class MoqBroadcastFinishInterface {
void close();
int lingerUs();
Future<void> wait();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update the generated packages' migration guides

The new low-level finish API and changed track-finish behavior affect the generated py/moq-ffi, go/ffi, and dart/moq_ffi packages, but each package's current MIGRATE.md still says that method signatures and behavior are unchanged. Add an Unreleased migration entry to those package-specific guides so users are not told that this behavioral and API change is metadata-only.

AGENTS.md reference: AGENTS.md:L62-L64

Useful? React with 👍 / 👎.

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 4a7c803: py/moq-ffi, go/ffi, dart/moq_ffi, kt/moq-ffi and swift/ffi each get an Unreleased entry for the finished-track behavior change and the new MoqBroadcastProducer finish, above the existing regenerate note, which only covers #6091's doc checksums.

(Written by Claude Opus 5.5)

…n guides

The generated py, go, dart, kt and swift packages got their own MIGRATE.md on
main; they now carry the finished-track behavior change and the new finish API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Added the generated packages' migration entries in 4a7c803.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 4a7c8033e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kixelated
kixelated enabled auto-merge October 21, 2026 19:26
@kixelated
kixelated added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 150111b Oct 22, 2026
10 checks passed
@kixelated
kixelated deleted the quest/m1/linger-ffi branch October 22, 2026 00:31
kixelated added a commit that referenced this pull request Oct 22, 2026
Adapt fills to main's error-carrying drops (#6143): a fill's local drop
records its error, a cursor's given-up misses are named NOT_FOUND without
overwriting a known reason, and a held legacy fill rejects the group it
waited on with the error it was given up with. FetchServeRun takes the
track info (#6107), and the C dynamic handler keeps main's finished
broadcasts (#6157).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 22, 2026
Drop the JS and binding linger requirements from interop-finish, since
both landed, and remove binding linger from the generated C epic's list:
#6157 shipped the C half, and the generated package inherits
the moq-ffi finish handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant