Skip to content

Audio: per-TV Opus limits, and formats the player learns to avoid - #1034

Merged
GLinnik21 merged 15 commits into
GLinnik21:mainfrom
tiper:native-audio-learning
Oct 26, 2026
Merged

GLinnik21 merged 15 commits into
GLinnik21:mainfrom
tiper:native-audio-learning

Conversation

@tiper

@tiper tiper commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #992. Its stereo cap in Auto is unchanged.

On my LG C1 (webOS 6.5.3), 5.1 Opus stalls silently once packets go over 1500 bytes, and Vorbis is refused. This adds:

  • A small per-firmware list of what’s known to break, which Force Direct Play respects too.
  • Learning: if the TV refuses a format or stalls on it, the app remembers that for this firmware and retries once with the server converting. A firmware update clears it.

FLAC stays on #950’s PCM route. Only measured on a C1 so far.

Dr-Amp and others added 7 commits October 24, 2026 13:43
The device table already lists OPUS, but DP_AUDIO_CODECS never offered it, so
every Opus track went to the server for conversion and Force direct play failed.

- devcaps: opus is a pipeline-only audio codec, appended only when the device
  table was actually read; the assumed fallback still omits it, and Force
  direct play applies the same table gate (forced_feeds_audio).
- engine: the Load payload carries codec.audio "OPUS" plus contents.opusInfo
  {channels, sampleRate 48, streamHeader = base64 OpusHead} (family 0 for 1-2
  channels, Vorbis mapping family 1 for 3-8). An unknown width refuses the Load.
- transcoder: opus joins the copy-only lanes, last, so it is never the encode head.

Verified on an LG C5 (webOS 10.3): AV1 + Opus 5.1 MKV direct play, seeks and
next-episode autoplay. Known gap: a native switch between two Opus tracks of
different widths keeps the first track's channel count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 26bf886)
Signed-off-by: Dr-Amp <dr_amp@users.noreply.github.com>
…he server

A C1 (webOS 6) stalls on 5.1 Opus once packets pass ~1500 bytes, with nothing in the log
(GLinnik21#917, GLinnik21#918). Auto now caps Opus at two channels whatever a table row claims, so the profile's
audio.channels limitation hands surround Opus to the server and the planner refuses it locally.
Force Direct Play keeps no ceiling, as for every codec; a 5.1 Opus film played through on a C5
(webOS 10.3), which is what a later per-model allowance would rest on.

Signed-off-by: Dr-Amp <dr_amp@users.noreply.github.com>
Signed-off-by: Dr-Amp <dr_amp@users.noreply.github.com>
Brings upstream main (e87ca65: FLAC decoded in the app and fed as PCM, GLinnik21#950) into
the Opus branch. Conflicts, each resolved by keeping both sides:

- Load audio code: upstream's FLAC PCM already takes 5, so Opus moves to 6
  (audio_payload_codec, load_a_str, and the Opus Load test).
- devcaps: forced_feeds_audio admits pipeline-only Opus and app-decoded FLAC on a
  read table. The audio list is DP_AUDIO_CODECS's subset, then Opus, then flac.
  OPUS_AUTO_MAX_CHANNELS and PCM_MAX_CHANNELS are both classified in ci/list-caps.ini.
- engine: the opusInfo helpers and upstream's pcmInfo helpers are both kept.

Signed-off-by: Dr-Amp <dr_amp@users.noreply.github.com>
… learns

Builds on GLinnik21#992 (Opus direct play, stereo in Auto). Auto's stereo ceiling for Opus is unchanged.

What a set announces for audio the pipeline decodes itself is now layered, in this order and
logged at boot ("devcaps: ... audio layers:"): the device codec table, minus the codecs
/etc/gst/gstcool.conf shows no decoder for, minus per-firmware exclusions (devcaps::EXCLUSIONS,
keyed on the firmware codename, each row carrying its evidence), minus the refusals this install
has learned.

- LG C1 (webOS 6.5.3, "kisscurl-koli"), measured: stereo Opus plays; 6-channel Opus stalls the
  pipeline once packets pass 1500 bytes (1500 B CBR plays; 1536 B CBR and VBR noise stall after
  ~190 AUs, with no event); Vorbis is refused (type=18 num=200). The Opus row limits it to
  stereo, and Force Direct Play now holds a measured ceiling too
  (Caps::forced_feeds_audio_track), where it used to apply none.
- A native Load that ends in type=18 num=200 before any picture, or a native surround feed that
  stalls (Load returned, no picture, both AU queues holding data, nothing accepted for 4 s), is
  learned as a refusal for that firmware, codec and channel count, and the play is retried once
  through the Retry path, which now has the server convert that audio. A known combination never
  retries, so it cannot loop; stereo is never learned from a stall.
- A learned refusal records the firmware build (nyx core_os_release); at boot, entries of another
  build, and those an older record never tagged, are forgotten, so a firmware update gets a fresh
  native try.
- Debug switches (debug builds): nativeaudiowide lifts the exclusions and learned entries,
  nativeaudiolearn keeps learning under it, nativeaudioforget clears the learned list at start.

FLAC stays on the app-decoded PCM route (GLinnik21#950); native Vorbis is not fed (libpf's Vorbis element
needs a caps streamheader the payload has no key for), and the exclusion row records the C1's
refusal.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Tiago Pereira <1698241+tiper@users.noreply.github.com>
…caps merge

Behaviour is unchanged.

- engine.rs: drop the local `base64` helper; `opusInfo.streamHeader` is encoded with the
  existing `plx_base::b64::encode` (the keymanager and SPKI pin code already share it). The
  RFC 4648 vector test moves to `base/src/b64.rs`, next to the code it covers.
- engine.rs: remove a stray blank line the PR added before an unrelated test.
- devcaps.rs: the Opus Auto ceiling uses `BTreeMap::entry` instead of a get/insert pair, and the
  `table_gated` closure (used once negated, once as a filter) becomes a direct
  `PIPELINE_ONLY_AUDIO.contains` test on each side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GLinnik21 and others added 5 commits October 26, 2026 02:24
Resolves the Preferences descriptor imports and count: GLinnik21#992's
InstantDpadSeeking row (17 Setting rows) next to GLinnik21#1034's
NativeAudioRefusal list row (5 list rows).

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…his tree

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…21#937's shared step

GLinnik21#937 made fail_engine_for_good (fail the Engine, enter Error, arm the pipeline release)
the one place an Engine ends for good. The two sites this branch adds, the learned
Load refusal and the learned native-audio stall, open-coded the old fail-and-Error pair
and so skipped arming the release; they call the shared step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GLinnik21 and others added 3 commits October 26, 2026 03:15
… learned refusals

The refused-native-audio retry no longer restarts the play from the app loop.
A type=18 num=200 refusal before the first frame, or the stall watchdog firing,
is learned and then recovered on the pre-picture recovery path every failed open
already uses: open_failure_action picks ConvertNativeAudio,
route::dispatch_native_audio_conversion flies a ClaimWork::Encode under a
FlightOwner::Recovery (today's rebuild, planned against the new capability
answer), and land_recovery_flight reloads onto it. The viewer keeps the
connecting spinner and never sees the error read-out. Removed:
native_audio_retry, take_native_audio_retry, retry_after_native_audio_refusal.

The install preference prefs.native_audio_refused is now the one list of learned
refusals, changed through one method on the session
(with_native_audio_refusal, which also retires another build's rows).
devcaps keeps no list: it holds the Arc<Caps> answer derived from it and folds
refusals into it monotonically (they only ever remove native audio), from a
fresh refusal and from the stored record. The per-rebuild Box::leak snapshots
and the second FORGOTTEN global are gone. The projection that runs under the
session cache lock is now pure; the queued write it used to make re-entered
that lock.

Tests: a hostsim pump test that a native-audio refusal before the first frame
ends in a recovery flight and not PlaybackState::Error (red before this change),
one that a refused conversion fails once without looping, and devcaps/session
tests for the fold and the single owner.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A direct play with native surround audio rebuilds as the plain Original remux
(the server copies the video, converts only the audio), the same contract an
undecodable audio-track pick asks for; only a subtitle the server must burn
makes it a re-encode.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…y attempt (review fix)

The refused-audio conversion's landing reopens the engine after a failed open but
never advanced the attempt ledger, so a failure of the converted route was reported
as the first fresh open instead of the app's own recovery. The other recovery flights
(the rollback rebase, the auto-HLS install) record the reopen; the Retranscode arm of
install_recovery_landing now does too, once the start transaction is prepared and
before the install verdict.

The flight test asserts the origin and ordinal after the landing. FlightRig gets
begin_attempt so a rig route belongs to a report attempt (a ledger note from trace
generation 0 changes nothing); the drop puts the generation and latches back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GLinnik21
GLinnik21 merged commit 48b3040 into GLinnik21:main Oct 26, 2026
13 checks passed
@GLinnik21

Copy link
Copy Markdown
Owner

Thanks @tiper, the packet-size measurement on the C1 is exactly the evidence this needed, and the per-firmware table with its evidence strings is a nice shape. Merged.

What I changed before merging:

Kept as you wrote them: the measurements, the exclusion table, the stall watchdog and packet-size evidence, the stored preference type with its stale-build handling, and your tests. Nothing here was run on a TV, so a C1 check of the converted 5.1 Opus route would be welcome.

GLinnik21 added a commit that referenced this pull request Oct 26, 2026
…1063)

Tests pin that Force Direct Play refuses audio the install has learned the TV refuses before any Load, naming the codec and offering "Switch to Auto and play". No production change: the Forced gate reads the attempt's start value (#1054, #1062), and main has refused this case since #1034.

- `forced_direct_play_refuses_a_learned_refusal_before_the_load`: Forced plus a stored `dca 6` row gives `ForcedFailure::Audio { codec: "DTS" }` and no part URL.
- `forced_direct_play_plays_the_ac3_sibling_of_a_learned_dts`: with an AC3 sibling, Forced plays the sibling.
- `the_play_start_value_lacks_an_applied_refusal`: the first test of the app's `start_caps()`.
- Red observed by mutation (the wrapper returning the boot value; the Forced pick reading the global; the Forced arm using the codec-level check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tiper added a commit to tiper/fin-native that referenced this pull request Oct 26, 2026
…f alone

Upstream's GLinnik21#992 and GLinnik21#1034 now feed stereo Opus to the television's own decoder, with the
per-firmware exclusions and the learned refusals, and GLinnik21#950 feeds FLAC as PCM. That replaces
FinNative's own native FLAC, Opus and Vorbis feed, which this commit used to carry: on the C1
its native FLAC was stereo only, where the PCM route already plays it, and Vorbis was refused at
every channel count.

What remains is the one rule the conversion needs beside them: with Convert audio on the TV, a
track the TV's table decodes at its channel count is fed as it is, not converted to AC3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tiper added a commit to tiper/fin-native that referenced this pull request Oct 26, 2026
The maintainer pushed "Simplify:" commits onto every PR of ours he merged (GLinnik21#839, GLinnik21#937, GLinnik21#1018,
GLinnik21#1034), behaviour unchanged each time. The skill writes down what he changed (reuse the existing
path, one owner per state, values over globals, one named step for a repeated sequence, no
redundant guards, docs kept true) and the steps to take before opening an upstream PR so he does
not have to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@tiper

tiper commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@GLinnik21
Tested on my C1 (webOS 6.5.3) with your build of main, against a mock server and a 5.1 Opus clip that stalls the C1’s decoder:

Auto: converted by the server, plays with no error screen.
Force Direct Play: refused up front by the C1 limit.
With the limit lifted (nativeaudiowide), the stall is caught after ~4 s and the background recovery plays it converted, no error screen.

Thanks for the rework, it’s cleaner than mine 🙂

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.

2 participants