Skip to content

feat(cast): show only subtitle languages in the phone remote - #654

Merged
Quick104 merged 1 commit into
feat/silocast-multi-controller-tvfrom
feat/remote-subtitles-language-only
Oct 21, 2026
Merged

Quick104 merged 1 commit into
feat/silocast-multi-controller-tvfrom
feat/remote-subtitles-language-only

Conversation

@Quick104

@Quick104 Quick104 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Related issue: N/A
Validation tasks: none

The phone remote's Subtitles menu listed the tracks, eleven delay offsets and three positions. The iPhone showed the same items in a different order, so the two phone apps disagreed, and the languages were buried among timing options.

The Subtitles menu now lists Off and the subtitle tracks, the same as silo-apple.

Approach

  • Subtitles shows Off and the tracks. Delay and position stay in the TV's own player menu, where they're adjusted while watching. The TV still accepts both commands, so other controllers can keep sending them.
  • The remote's dropdown no longer needs section headers, so that support is removed.

The matching Apple change is Silo-Server/silo-apple#860. Stacked on #653.

Validation

  • :androidApp:testDebugUnitTest, lintDebug and lintVitalRelease: pass.
  • Android phone emulator on an Android TV emulator: the old build lists Track, 11 delays and 3 positions. The new build lists Off and English, and choosing English turned subtitles on on the TV.

Evidence

Evidence: https://evidence.siloserver.org/r/silo-android/pr-654/

Surface and build: Android phone emulator, before on #653 (12ff75145), after on this branch. The tablet and foldable sizes are the same signed-in emulator resized with wm size and wm density. The Android TV app doesn't change.

Risks

None identified. Subtitle delay and position remain available on the TV.

Checklist

  • I read and can explain the complete diff.
  • This pull request addresses one concern.
  • The Evidence section shows every change a user can see, or says there is none.

AI Disclosure

  • Harness: Claude Code in T3 Code
  • Tool(s): Claude Code, GitHub CLI, Gradle, Android emulator and adb
  • Model(s): claude-opus-5-5
  • Involvement: Fully AI-generated; human verification of the diff is pending
  • Adversarial review: n/a, a menu reduction requested by the maintainer

🤖 Generated with Claude Code

Note

Show Subtitles chip in cast remote only when subtitle tracks exist

Simplifies the phone remote subtitle menu in SiloCastRemoteScreen.kt to show only subtitle language choices.

  • The menu now contains an Off entry plus each available subtitle track; subtitle delay and subtitle position can no longer be changed from this menu.
  • The Subtitles chip is shown only when subtitle tracks are present; the previous checks for subtitle-delay and subtitle-position support were removed.
  • RemoteChipMenu no longer renders section headers; every entry is a selectable dropdown item. The MenuEntry.isSectionHeader field and the subtitleDelayLabel helper were deleted.
  • Behavioral Change: subtitle sync delay and position must now be adjusted elsewhere; check subtitleMenuEntries and RemoteSecondaryControls for the removed callbacks.

Macroscope summarized ff88c5b.

The remote's Subtitles menu listed tracks, eleven delay offsets and three
positions. It now lists Off and the subtitle tracks only, the same as
silo-apple. Delay and position stay in the TV's own player menu, which is
where they are adjusted while watching. The dropdown no longer needs
section headers.

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

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a06d8d56-21de-4959-b6ab-fc17bfe2a9ab

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • 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.

@silo-triage silo-triage Bot added size/M 50–199 added + deleted lines (all files); change volume, not priority. enhancement New feature or request impact: polish Annoyance, cosmetic, or nice-to-have priority: P2 Limited scope, workaround exists, or polish stacked Stacked on another open PR or branch labels Oct 21, 2026
@Quick104
Quick104 merged commit ac5195a into feat/silocast-multi-controller-tv Oct 21, 2026
10 checks passed
@Quick104
Quick104 deleted the feat/remote-subtitles-language-only branch October 21, 2026 01:36
Quick104 added a commit that referenced this pull request Oct 21, 2026
…#653)

* test(cast): add adb hooks to inspect and inject Remote Control targets

Emulators sit behind NAT that mDNS can't cross, so a phone emulator can't
discover a TV emulator or an Apple TV simulator, and there was no way to see
what a TV advertises. Debug builds get two more PlaybackDebugReceiver
actions:

- CAST_STATUS (TV): the live Remote Control port, name and TXT record.
- CAST_TARGET (phone): adds or replaces a TV target from a host, port,
  name and TXT string resolved on the host; `--ez clear true` removes
  them. Injected targets merge with real discovery, survive a restart of
  it, and win over an mDNS record with the same device id.

The browser's TXT parsing moves into SiloCastNsdBrowser.targetFromRecord
so the hook and discovery read records the same way, and the advertiser
keeps the record it registered so the hook reports exactly that. Behavior
of release builds is unchanged.

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

* feat(cast): let the phone share a TV with other controllers

Apple TVs now keep several Remote Control controllers connected and say
why they close a session; Android phones treated every close as "someone
took over", forgot the TV, and replaced other profiles' titles without
asking. This brings the phone and the shared wire format level with
silo-apple's SiloControl contract. Every addition is optional and
feature-gated, so older peers see today's behavior.

Wire (shared):
- hello: serverIdentity, features, passive, clientKind; `presence`
  message; optional `close` reason, decoded leniently as Apple does.
- handoff_offer: serverIdentity, resumeToken, replaceConfirmed,
  replacingContentId. handoff_ready: resumeToken. handoff_cancel:
  nowPlaying.
- Feature, close-reason, cancel-reason and client-kind names, matching
  Apple's strings.
- TVs advertising serverIdentity match an active server on the same
  deployment at another address.

Phone:
- Lists close_reason, resume_token and replace_confirmation; a silent
  auto-resume connects passive and sends presence once the user engages
  (remote opened, a command, or a title sent).
- Close policy: an older TV's close still forgets it. A multi-controller
  TV's timeout reconnects; tv_disconnected forgets the TV; other reasons
  end the session but keep the target, and the remote keeps the TV's
  error.
- Sends its profile name, its verified server identity, and the resume
  token the TV issued it; the token lives in memory per TV, server and
  profile.
- "Replace what's playing?" on the remote, for a TV's
  replace_confirmation_required and before sending a different title to a
  TV that is playing one. The consent names the title the user saw.
- Play and pause buttons send explicit play/pause for what they show,
  never play_pause. A rate_limited reply no longer fails a launch.

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

* feat(tv): keep several Remote Control controllers connected to one TV

An Android TV kept one controller: every new TCP connection, even a port
probe, closed the phone that was using it, so two people on a couch, or a
phone and a Home Assistant integration, kept knocking each other off.
Apple TVs now keep several controllers; this brings the Android TV
receiver to the same contract (silo-apple SiloControlReceiverPolicy),
with the pure rules in TvSiloCastReceiverPolicy.

- Up to 8 controllers, each with its own heartbeat, authorization and
  command budget (20/s, burst 40, `rate_limited`). At the cap the TV
  closes, in order, a connection that never said hello, an idle
  cross-server controller, then the oldest passive one, never one that is
  handing off or in use; otherwise it refuses the newcomer with
  `too_many_controllers`.
- A new connection from the same device replaces its older sockets.
- One state tick goes to every authorized controller, through a
  latest-only slot per controller so a slow peer holds up no one; errors
  go only to the controller that caused them.
- One handoff at a time: a newer offer from another controller cancels
  the pending one with `superseded`. A launch plays under a borrowed
  profile only for the controller whose handoff produced it, and reusing
  that profile needs the same connection or the resume token issued to
  it (device IDs are self-reported). Keeping it alive while its phone
  reconnects still follows that phone.
- A handoff that would replace a title another profile is watching asks
  first (`replace_confirmation_required` with what is playing), when the
  controller can ask, and checks again just before it stops playback.
- Passive controllers never put up standby; standby names the newest
  waiting controller and counts the rest. Disconnect on the TV closes the
  non-passive ones with `tv_disconnected`.
- After the borrowed profile changes, every controller is re-checked;
  one that loses authorization is told `server_mismatch`, unless its own
  profile simply ended, and cross-server ones stay connected to hand off
  again.
- A borrowed profile whose session the server stops refreshing now ends:
  every authorized controller gets `temporary_session_expired`, playback
  under it stops, and the TV falls back to its own sign-in. The token
  manager reports this through a new `temporarySessionExpired` flow.
- Close reasons go to controllers that list `close_reason`; the TV
  advertises `serverIdentity` in hello and Bonjour and matches
  controllers on it as well as the server ID.

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

* fix(cast): keep injected targets authoritative and report only a finished registration

Review follow-ups on the debug hooks:

- A resolved mDNS record could pass the "no injected target with this
  id" check outside the browser's lock and then overwrite an injected
  target set in between. Every write to the target list now holds the
  lock.
- Clearing an injected target dropped the discovered record it had
  replaced until discovery found it again. The browser keeps the hidden
  record and restores it.
- CAST_STATUS reported the requested advertisement before NsdManager
  confirmed it, and under the requested name. It now reports only a
  registered advertisement, under the name NsdManager registered.

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

* fix(cast): learn the server's identity when Remote Control starts

A server signed in before the identity contract has no verified
identity on the phone. The picker then can't list a TV that advertises
another address of the same deployment, and hello and the handoff offer
can't name the deployment. silo-apple learns it on the server's next
activation; the phone now learns it once when it starts looking for TVs
(the picker or a foreground auto-resume), through ServerIdentityRepository.

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

* fix(tv): fence state and handoff rollback to the right controller

Review follow-ups in the multi-controller receiver:

- broadcastState chose its recipients before reading the player state, so
  a handoff that changed the TV's profile during the read could send the
  new profile's state to a controller it had just de-authorized. It now
  chooses recipients after the read, and each controller's writer checks
  authorization again before sending a queued state.
- A failed handoff_ready rolled back the session's current readiness. A
  newer offer from the same phone could own it by then, so the older
  attempt's failure ended the newer profile. Each attempt now rolls back
  only the generation it granted.

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

* fix(tv): grant readiness only for the prepared profile, and don't let expiry wait on writes

Review follow-ups in the multi-controller receiver:

- completeHandoff granted readiness for whichever profile was active.
  It now checks, under the receiver lock, that the active profile is the
  one this offer prepared, and fails the handoff otherwise.
- When the server refused a borrowed profile's refresh, the TV notified
  every controller before stopping playback and ending the profile, so a
  controller that stopped reading could stall both. It now tears down
  first and sends the notices in the background.

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

* fix(tv): check state authorization when the write is admitted

A queued state was checked against the controller's authorization before
the writer waited for the connection's write lock, and wasn't tied to the
profile it was read under. Each state now carries the borrowed-profile
generation it was read under, and the writer checks authorization and
that generation after taking the write lock, just before writing. A state
read under another profile, or queued before the controller lost access,
is dropped; the next tick follows within half a second.

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

* feat(cast): show only subtitle languages in the phone remote (#654)

The remote's Subtitles menu listed tracks, eleven delay offsets and three
positions. It now lists Off and the subtitle tracks only, the same as
silo-apple. Delay and position stay in the TV's own player menu, which is
where they are adjusted while watching. The dropdown no longer needs
section headers.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

enhancement New feature or request impact: polish Annoyance, cosmetic, or nice-to-have priority: P2 Limited scope, workaround exists, or polish size/M 50–199 added + deleted lines (all files); change volume, not priority. stacked Stacked on another open PR or branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant