Skip to content

fix(mux,audio,json): a failed finish is final - #8210

Merged
kixelated merged 4 commits into
mainfrom
quest/m1/explicit-state/finish-final
Nov 8, 2026
Merged

kixelated merged 4 commits into
mainfrom
quest/m1/explicit-state/finish-final

Conversation

@kixelated

@kixelated kixelated commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Completes quest/m1/explicit-state/finish-final.md (deleted here) from the explicit-state epic.

Summary

A finish that fails now leaves the object terminally failed, with every track ended, and reports the cause to later calls. Every container importer refuses input after finish the same way.

  • Importers (mkv::Import, fmp4::Import, flv::Import, ts::Import, ts::Programs, ts::Passthrough) hold one crate-private container::Input { Open, Refused(Error), Failed(Error), Finished } instead of a finished flag beside a failed: Option.
    • Refused: a decode was refused. Later input gets that error, and finish still ends the tracks with what was accepted, as before.
    • Failed: finish failed. Every track has ended, either finished or aborted with the cause. Every later call returns the cause.
    • Finished: later input gets Error::Finished, and finish again does nothing.
  • TS import: finish used to set finished before its fallible steps, so a section or private PES left without a media timestamp left its streams open, and Drop never retried. Now a failure aborts every stream, section, and SI track and releases the reservations. decode checks the state first. Programs::finish does the same for every program.
  • fMP4/MKV/FLV: ending the tracks no longer stops at the first error. Each track is ended, keeping the first error. MKV's initial_reservation == None now means only "catalog published": the old TracksAfterFinish is renamed LateTracks, and finish is tracked by Input.
  • container::Producer: close() resets the group's state before its fallible steps and aborts the group if closing fails. A finish whose last cut fails aborts the track instead of leaving it open. The fMP4 passthrough track does the same.
  • Audio encode (moq-audio): the codec now lives in enum Codec { Live(Encoder), Finished, Failed(Error) }. A failed finish fails the track, so subscribers see an error after the published packets. After that, write and finish return the cause instead of sending input-rate PCM to the codec or republishing terminal packets.
  • JSON window and snapshot (moq-json): both now record the producer's own outcome in enum State { Open, Finished, Aborted(Error) }, alongside the openness each reads from the track. Another handle may end the track first, so the track alone can't say what this producer did.
    • A failed finish aborts the track and a retry reports the failure.
    • After abort, edits report the cause instead of Closed.
    • Finishing always closes the producer's own group, and after it the producer refuses edits even below an end another handle declared further on.
    • The window encoder is reset everywhere the group is cleared (abort and finish now do this too).
  • moq-transcode rung: a pending cut is cleared only after it is applied, and it is set before the old encoder drains.

Choices made without asking

  • finish(self) not adopted. The quest offered a consuming finish as an m2 option. These types keep &mut self on purpose: ts::Programs::finish documents holding the importer (with Linger) to keep finished tracks servable, and the audio and JSON producers document that abort or reads still work after finish. With the explicit state, &mut self can no longer reach a half-finished object, so the breaking move would buy little. I dropped it and deleted the quest instead of moving it to m2.
  • A refused decode still finishes cleanly. The fMP4 and MKV docs and tests require finish after a refused decode to end the accepted media and return Ok. I kept that contract with the separate Refused variant rather than making finish return the refusal.
  • One error for input after finish: moq_mux::Error::Finished. It replaces fmp4::Error::MoovAfterFinish and MKV's finish meaning of TracksAfterFinish. Neither variant is on release.
  • TS failure cause: TS reports through anyhow, which isn't Clone. The first failing call returns the original error. Later calls return a moq_mux::Error::Other carrying the full {:#} chain. Subscribers see StreamError::Internal, as Error::reason gives for non-transport errors.
  • Audio's failed finish fails the track with StreamError::Internal, so the packets already published stay readable.

Impact

Public API (moq-mux, moq-audio, moq-json). None of the removed or renamed items is on the release branch, so this is not breaking:

  • Added moq_mux::Error::Finished.
  • Removed fmp4::Error::MoovAfterFinish, which is now moq_mux::Error::Finished.
  • Renamed mkv::Error::TracksAfterFinish to LateTracks. It now only means a TrackEntry after the catalog was published. Input after finish is moq_mux::Error::Finished.
  • Behavior:
    • fMP4 cut/seek/discontinuity, MKV and FLV seek, and FLV/TS/Programs/passthrough decode now refuse after finish.
    • MKV decode(&[]) after finish now errors.
    • ts::Programs::finish called again returns the same broadcasts.
    • The JSON snapshot's second finish returns Ok.
    • Every failed finish now ends its tracks with an error instead of leaving them open.

Wire: none.

Caused by

Validation

  • Each bug is reproduced in a test that fails on main:
    • ts::import::test::a_failed_finish_aborts_every_track: the section track used to stay open, and a retried finish returned Ok.
    • ts::import::test::input_after_finish_is_refused
    • mkv::import::tests::a_failed_finish_reports_its_cause
    • encode::producer::tests::a_failed_finish_refuses_later_calls: a write after a failed finish used to return Ok.
    • window::test::a_failed_finish_is_final
    • window::test::edits_after_an_abort_report_its_cause
  • Review-round tests:
    • flv::import_test::input_after_finish_is_refused
    • ts::passthrough_test::input_after_finish_is_refused
    • window::test::finish_closes_the_group_on_a_track_already_ended
    • snapshot::test::a_finished_producer_refuses_edits_below_a_declared_end
  • just check passes on the head: 5470 tests, plus clippy and fmt.

Suggested follow-ups

  • finish(self) across producers and importers (m2): recommend dropping it. The explicit state already prevents half-finished objects, and holding a finished importer or producer is a documented use (see Choices).
  • catalog::Producer::finish marks closed before its fallible track finishes (rs/moq-mux/src/catalog/producer.rs), which is shape 2 outside this quest's list. Recommend planning it under the explicit-state epic.

Models

Size: M
Implementer: Claude Code, claude-opus-5-5, high effort

🤖 Generated with Claude Code

(Written by Claude Opus 5.5 with high effort)

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, xhigh effort
  • Session: 01a1b857-ffc8-7141-ada7-e7773c9c2ac1, round 3
  • Head: 4cdc050
  • Base: origin/main at 09b4d2a
  • Score: 10/10, The remaining lifecycle finding is fixed, with no additional issues found in the incremental source review; builds and tests were not run as instructed.
  • Findings: none
  • The last round's findings, at f9a7c18:
    • Fixed: medium, rs/moq-json/src/snapshot/producer.rs:408: Enforce the snapshot producer's terminal state before updating; rs/moq-json/src/snapshot/producer.rs:358 now calls open() before encoding or writing, so the producer's Finished state rejects updates even below a future track boundary.

kixelated and others added 2 commits November 7, 2026 14:38
Importers (MKV, fMP4, TS, TS Programs) hold one `Input` state instead of a
`finished` flag and a separate failure slot. A finish that fails ends every
track and reports its cause to every later call; input after a successful
finish is refused with `Error::Finished`.

The audio encode producer keeps its codec in a `Codec` enum, so a failed
finish refuses later writes with its cause instead of feeding input-rate PCM
to the codec. moq-json's window and snapshot producers abort the track when
finishing fails and report an abort's cause, matching snapshot's design.

container::Producer resets a group's state before the fallible close and
aborts the group or track when closing fails; transcode keeps a pending cut
until it applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title A failed finish is final fix(mux,audio,json): a failed finish is final Nov 7, 2026
kixelated and others added 2 commits November 7, 2026 15:40
… finish owns its outcome

FLV and TS passthrough imports take the shared Input state. moq-json's
window and snapshot producers record their own outcome in a State enum,
and finishing closes their group even when another handle already ended
the track.

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 November 7, 2026 23:48
@coderabbitai

coderabbitai Bot commented Nov 7, 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 23 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

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ba4954a2-98e8-4457-a9c5-85e29f92f8ee
Commits

Reviewing files that changed from the base of the PR and between d389cfb and 4cdc050.

Files selected for processing: 26

  • quest/m1/explicit-state/README.md
  • quest/m1/explicit-state/finish-final.md
  • rs/moq-audio/src/encode/producer.rs
  • rs/moq-json/src/lib.rs
  • rs/moq-json/src/snapshot/mod.rs
  • rs/moq-json/src/snapshot/producer.rs
  • rs/moq-json/src/state.rs
  • rs/moq-json/src/window/mod.rs
  • rs/moq-json/src/window/producer.rs
  • rs/moq-mux/src/container/flv/import.rs
  • rs/moq-mux/src/container/flv/import_test.rs
  • rs/moq-mux/src/container/fmp4/import.rs
  • rs/moq-mux/src/container/fmp4/import_test.rs
  • rs/moq-mux/src/container/fmp4/mod.rs
  • rs/moq-mux/src/container/input.rs
  • rs/moq-mux/src/container/mkv/import.rs
  • rs/moq-mux/src/container/mkv/import_test.rs
  • rs/moq-mux/src/container/mkv/mod.rs
  • rs/moq-mux/src/container/mod.rs
  • rs/moq-mux/src/container/producer.rs
  • rs/moq-mux/src/container/ts/import.rs
  • rs/moq-mux/src/container/ts/passthrough.rs
  • rs/moq-mux/src/container/ts/passthrough_test.rs
  • rs/moq-mux/src/container/ts/programs.rs
  • rs/moq-mux/src/error.rs
  • rs/moq-transcode/src/rung.rs
  • 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

@kixelated kixelated left a comment

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.

Automated review by review (OpenAI)

Reviewed head 4cdc050.

No actionable findings. Recording terminal outcomes after fallible teardown, ending the remaining importer tracks, and preserving the first failure is a sound direction. The separate Refused state retains the existing contract that accepted media can still be finished after a decode refusal. The JSON producers now distinguish their own terminal state from an end declared by another track handle, and the audio producer cannot reuse its codec after a failed drain. Keeping a pending transcode cut until it succeeds also preserves the owed boundary.

Verification limits: GitHub-only static review of all 26 changed files and surrounding importer, group-close, audio-drain and JSON lifecycle code. No builds, tests, media playback, or failure-injection runs performed; reported runtime validation remains author-provided. Rechecked open/non-draft state, full head/base and published reviews immediately before posting.

@pullfrog pullfrog Bot 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.

✅ No new issues found.

Reviewed changes

The full diff at 4cdc05069, including all three fix commits and the two prior review rounds' fixes, plus the changed files in context. Ran the moq-json, moq-audio, and moq-transcode unit suites and the moq-mux library suite: everything passes except 22 moq-mux tests that shell out to ffmpeg/ffprobe, which are absent from this environment (recovery_test.rs:220, the export ffprobe helpers). Both old error variants are confirmed absent from release, as the Impact section claims.

  • Shared importer state — container::Input { Open, Refused, Failed, Finished } replaces the finished flag plus failed: Option in MKV, fMP4, FLV, TS import, TS Programs, and TS passthrough; check() runs before every decode/seek/cut entry point, and Failed is terminal across all six.
  • Failed finishes end their tracks — fMP4/MKV/FLV end every track with result.and(track.end()) and keep the first error; TS routes a failed end() through fail(), aborting streams, sections, SI, and releasing reservations; TS Programs does the same per program.
  • container::Producer — group state is taken before the fallible close, a close failure aborts the group and clears the buffer, and a failed last cut aborts the track rather than leaving it open; fMP4's passthrough track mirrors this.
  • Audio and JSON producers — the codec/state now records the terminal outcome (Codec::Failed, State::Aborted), so a failed finish fails the track and every later write/finish returns its cause instead of feeding the codec or republishing; a finished producer's own state now gates edits even below an end another handle declared.
  • Transcode rung — pending_cut is owed before the old sink drains and cleared only once applied, so a failed drain or cut is retried.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

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