Skip to content

Vertical (9:16) output for non-Twitch destinations + VOD/EB convenience + dashboard clarity#30

Merged
Soulhackzlol merged 33 commits into
mainfrom
dual-format-verify-and-vod-eb-fix
Jul 1, 2026
Merged

Vertical (9:16) output for non-Twitch destinations + VOD/EB convenience + dashboard clarity#30
Soulhackzlol merged 33 commits into
mainfrom
dual-format-verify-and-vod-eb-fix

Conversation

@Soulhackzlol

@Soulhackzlol Soulhackzlol commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Vertical (9:16) output for non-Twitch destinations + VOD/EB convenience + dashboard clarity

Continues the Dual Format work after #28. Adds per-destination vertical
output, VOD-audio/Enhanced-Broadcasting quality-of-life, a dashboard pass
for the vertical flow, and a small status-path perf cleanup.

Vertical output (YouTube Shorts / Kick mobile / TikTok)

  • Reuses the 9:16 canvas Twitch Dual Format already produces (Enhanced
    Broadcasting + Aitum Vertical) and flattens it to a standard single-track
    RTMP feed non-Twitch platforms accept natively. No second scene, no extra
    encoder.
  • The vertical canvas is identified by decoding each track's SPS for
    orientation
    (portrait = height > width), not by guessing track IDs. The
    SPS parser is fully bounds-checked and fuzzed - never panics on partial or
    hostile bytes.
  • Vertical AVC is forwarded as legacy AVC framing (YouTube's vertical
    ingest is flaky with Enhanced-RTMP avc1, aborting on a ~11 s cycle), and a
    vertical destination leads with its own keyframe after every (re)connect
    or cut - no mid-GOP glitch.
  • A vertical destination whose canvas isn't on the wire yet doesn't even
    connect
    (avoids idle-connect reconnect churn); it waits and self-heals
    within a supervisor tick when the canvas appears. Twitch and horizontal
    destinations stream untouched.

Dashboard clarity for the vertical / Dual Format flow

  • The Stream format picker disables "Vertical" when no enabled Twitch
    source exists, with an inline reason + link to Twitch's guide (never a
    silent dead-end). A dest already set to vertical is never force-flipped.
  • A waiting vertical dest now says why: Needs Twitch EB, No vertical canvas, or Waiting for Dual Format. Copy makes clear Dual Format is
    Enhanced Broadcasting + Aitum Vertical (per Twitch's guide), not a single
    OBS toggle
    .
  • Per-destination resolution + codec readout and a Dual Format header
    pill that lights up when a vertical canvas is live.

VOD audio + Enhanced Broadcasting convenience

  • One-click "Set up VOD + EB" (writes OBS's VOD-track flag, launches OBS
    with --config-url, re-verifies, red-to-green checklist, partial-success
    aware).
  • "Create desktop shortcut" + tray "Launch OBS (VOD + EB)" via the new
    --launch-eb flag.

Performance

  • The per-destination status endpoint parses each cached video sequence header
    once per poll instead of once per destination (was a per-dest mutex lock
    • Exp-Golomb parse on a frequently-polled path).

CI

Testing

  • cargo test: 210 pass / 0 failed. cargo clippy --all-targets: clean.
  • cargo build --release: clean; binary 1,353,728 bytes (1.29 MB, matches the
    1.3 MB stated in the READMEs).
  • Dashboard inline JS syntax-checked (0 errors).

Related

…venience (v0.1.9)

Reuse Twitch Dual Format's vertical canvas to simulcast 9:16 to YouTube
Shorts / Kick mobile / TikTok with zero extra encoding.

- config: per-destination `stream_format` (horizontal default | vertical),
  persisted lean, with a `wants_vertical()` guard (Twitch forced horizontal).
- h264: bounds-checked SPS dimension parser + `detect_vertical_primary_track`
  (orientation from the wire, not Twitch's private session JSON); generalize
  `select_video_bytes` to `VideoEgress { Passthrough, Track(u8) }` so a vertical
  destination forwards only the portrait canvas, flattened, and drops the
  horizontal primary.
- controller/main/buffer: per-dest `egress_vertical` + `vertical_primary_track`
  (0xFF = unresolved -> waits, others unaffected); recomputed every supervisor
  tick so Dual Format on/off self-heals. Seq-header replay is format-aware.
- web UI: Stream format dropdown (hidden for Twitch, with a "Twitch does both"
  note), status JSON `stream_format` + `vertical_ready`, a card format icon with
  hover tooltip, a distinct "Waiting for Dual Format" status, and tour coverage.
- VOD+EB: one-click "Set up VOD + EB" with a red-to-green checklist, a
  `--launch-eb` cold-start flag, a desktop-shortcut generator (.lnk, .cmd
  fallback), and a tray "Launch OBS (VOD + EB)" item.
- docs: README EN/ES + CHANGELOG; test count 200 -> 205 (5 new h264 tests).

YouTube/Kick have no Twitch-style single-feed multitrack; vertical maps to
their normal separate-key vertical ingest, so there is no conflict.
…smooth/stable sparklines, dynamic tour target
… cards (full-bleed signal strip, hover actions)
… toggle switch, colored edge); encoder-neutral connection copy
…o opacity pop); stabilize header width so toggling doesn't shift it
…rd actions slide reveal keeps blur, smooth both ways
… glance, encoder-neutral copy, crossfade tip
…anic on malformed input) + huge-crop/fuzz tests
…s (avoids idle-connect reconnect churn when Dual Format is off)
… into the toggle); scope resets, refine layout/colors
…warning, restart banner, cancel button; stabilize rate-pill width to stop header shift
…der pill; extract+test stream_format rule; test count -> 209
…e font, no screen overflow, critical rows fixed, scroll fallback
…r (re)seed - clean reconnects, no P-frame glitch on cut/reconnect
YouTube's vertical ingest is viewable but flaky with Enhanced-RTMP avc1 -
it aborts the socket on a ~11s cycle (os error 10053) while the horizontal
primary, which reaches YouTube as legacy AVC, stays rock-solid. Rewrite a
non-Twitch vertical AVC track (OneTrack seq-header / coded frames) down to
legacy AVC (0x17/0x27) so it gets the exact framing YouTube already ingests
happily. Non-AVC codecs and ManyTracks bundles keep the E-RTMP flatten;
avc1 metadata/colour packets are dropped rather than mixed into a legacy
stream. Adds select_video_bytes_vertical_avc_rewrites_to_legacy (210 tests).
…ta.is_idr

- Remove the orphaned pass_through_multitrack doc block left by the
  VideoEgress refactor (it was misdocumenting seq_header_track_id).
- Add an accurate doc comment to select_video_bytes describing the
  Passthrough / Track(n) / legacy-AVC behaviour.
- Keyframe-lead gate reuses the precomputed meta.is_idr (any-track
  classification) instead of re-parsing the tag on the hot path.
…eq-headers once per status poll

Dashboard clarity for the vertical / Twitch Dual Format flow:

- Disable the "Vertical" stream-format choice when no enabled Twitch
  destination exists (inline reason + link to Twitch's guide). Vertical
  has no source without Twitch Enhanced Broadcasting, so the choice is no
  longer a silent dead-end. A dest already set to vertical is never
  force-flipped - the option stays in case its source returns.

- A waiting vertical dest now says why it isn't live: "Needs Twitch EB"
  (no Twitch source), "No vertical canvas" (EB live but no 9:16 on the
  wire), or "Waiting for Dual Format" (nothing streaming). Copy makes
  clear Dual Format is Enhanced Broadcasting + Aitum Vertical (per
  Twitch's guide), not a single OBS toggle, so the setup path is honest.

- The Twitch editor already hides the format control (native dual-canvas
  passthrough) - unchanged.

Perf: the per-destination status endpoint parses each cached video
sequence header once per poll instead of once per destination (was a
per-dest mutex lock + Exp-Golomb parse on a frequently-polled path).

Docs: CHANGELOG entry; fix ES README binary badge 1.2 -> 1.3 MB (matches
the measured 1.29 MB release binary and the table).
Folds in Dependabot's #29 so the checkout v7 bump ships with this branch
instead of racing it. Covers ci.yml, codeql.yml, and release.yml (all 5
usages).
@Soulhackzlol
Soulhackzlol force-pushed the dual-format-verify-and-vod-eb-fix branch from 6eae4a2 to 36755e9 Compare July 1, 2026 20:34
@Soulhackzlol Soulhackzlol self-assigned this Jul 1, 2026
@Soulhackzlol Soulhackzlol added enhancement New feature or request documentation Improvements or additions to documentation github-actions GitHub Actions workflow dependencies dependencies Dependency updates labels Jul 1, 2026
The other e2e scenarios all run with delay=0 (pure passthrough), so the
app's core feature - arm a delay, fill the ring, activate, cut on an IDR -
was never exercised end-to-end, only unit-tested via compute_delay_cut.

Scenario E arms a 2 s delay against a live ffmpeg stream, waits for the
ring to fill (phase 'ready'), then activates and asserts:
- the delay engages (phase 'active', current_delay_ms > 0),
- the sink keeps receiving IDR windows past the cut (stream didn't stall),
- exactly one publish accept - a cut that shipped a referenceless P-frame
  would drop the downstream and reconnect, which this catches.

Adds two helpers: Wait-State (poll /state for a predicate) and a
non-blocking Start-FfmpegSource so a scenario can arm/activate mid-stream.
Verified locally: all 5 scenarios pass.
@Soulhackzlol
Soulhackzlol merged commit 8699606 into main Jul 1, 2026
4 checks passed
@Soulhackzlol
Soulhackzlol deleted the dual-format-verify-and-vod-eb-fix branch July 1, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency updates documentation Improvements or additions to documentation enhancement New feature or request github-actions GitHub Actions workflow dependencies

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vertical (9:16) output for non-Twitch destinations

1 participant