Skip to content

feat(tv): keep several Remote Control controllers connected to one TV - #653

Merged
Quick104 merged 13 commits into
mainfrom
feat/silocast-multi-controller-tv
Oct 21, 2026
Merged

Quick104 merged 13 commits into
mainfrom
feat/silocast-multi-controller-tv

Conversation

@Quick104

@Quick104 Quick104 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Related issue: N/A
Validation tasks: changes #331 C1–C3 (not yet run)

An Android TV keeps one Remote Control controller. Every new connection closes the phone that was using it, even a port probe. Two people on the couch, or a phone and a Home Assistant integration, keep knocking each other off. A phone on one profile can also replace a title another profile is watching without anyone being asked.

This lets several devices control one Android TV, under the same rules as the Apple TV in Silo-Server/silo-apple#856. A handoff that would replace another profile's title asks first.

Approach

TvSiloCastReceiver now holds a set of controllers instead of one session. Each controller has its own heartbeat, authorization and command budget (20 per second, burst 40, then rate_limited). The player, the borrowed profile, the one in-flight handoff and the standby screen stay shared. The pure rules live in TvSiloCastReceiverPolicy, ported from silo-apple's SiloControlReceiverPolicy with the same tests:

  • Up to 8 controllers. At the cap the TV closes, in order, a connection that never said hello, an idle cross-server controller, then the oldest passive one. It never closes one that is handing off or in use; it refuses the newcomer with too_many_controllers instead.
  • A new connection from the same device replaces that device's older sockets once the TV keeps the new one.
  • 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. Reusing that profile from a new connection needs the original connection or the resume token issued to it, because device IDs are self-reported.
  • A handoff that would replace a title another profile is watching gets replace_confirmation_required with what is playing, when the controller can ask. The check runs again just before playback stops.
  • Passive controllers never put up the standby screen. Standby names the newest waiting controller and counts the rest ("… and 1 other device"). Disconnect on the TV closes only non-passive controllers, with tv_disconnected.
  • After the borrowed profile changes, every controller is re-checked. One that loses authorization gets server_mismatch, unless its own profile simply ended.
  • A borrowed profile whose refresh the server refuses now ends. Controllers get temporary_session_expired, playback under it stops, and the TV returns to its own sign-in. TokenManager reports this through a new temporarySessionExpired flow, which emits nothing by default.
  • The TV advertises serverIdentity in hello and in its mDNS record, and matches controllers on it as well as on the server ID.

Stacked on #652 (phone and wire), which is stacked on #651 (debug hooks).

The TV keeps its current timing for borrowed profiles: kept while the lending phone is connected, a 2-second hand-back once it leaves while idle, and a 10-minute idle limit. The lender is now the connection that completed the handoff, or a holder of its token, not a device ID.

Validation

  • testDebugUnitTest: 6,120 tests pass (last run on 12ff751), including 14 new receiver-policy tests and one new identity-reuse test. lintDebug passes for all three modules, lintVitalRelease for both apps, and the supply-chain checks pass.
  • A scripted controller ran the same protocol checks against this Android TV and the silo-apple#856 Apple TV, with identical results:
    • A second socket from the same device replaces the first with replaced.
    • Two active controllers both stay connected, and standby counts the other.
    • At the cap of 8, a ninth active controller gets too_many_controllers, while a passive controller makes room for a newcomer.
    • A 41-command burst gets one rate_limited.
    • An unknown message type is ignored and the connection stays.
  • iPhone simulator (silo-apple#856) and Android phone emulator (feat(cast): let the phone share a TV with other controllers and ask before replacing #652), both on this TV:
    • Both stay connected and follow each other's pause and play.
    • Sending a different title asks once, on the sending phone.
    • The same title under another profile gets the TV's prompt naming the other viewer. Cancel leaves it playing; Replace switches profiles, and both phones follow.
  • Before captures use main with test(cast): add adb hooks to inspect and inject Remote Control targets #651's hooks: the iPhone joining knocks the Android phone off, and standby names only the iPhone.
  • A 70-second recording on the TV and the Android phone covers the TV's prompt, Cancel (the title keeps playing), the prompt again, and Replace (the TV closes the other profile's playback and resumes at the new profile's position).
  • Not run: a temporary_session_expired end to end, which needs the server to refuse a refresh. Unit tests cover it.

Evidence

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

Surface and build: Android TV emulator (1080p) on this stack, and main plus #651's hooks for the before captures. The phone captures show the TV's prompt and the other controller's view. Their tablet and foldable layouts are in #652's evidence.

Risks

  • fix(tv): hand control back to the TV when Back leaves a phone-controlled title #567 (hand control back to the TV on Back) changes TvSiloCastReceiver around the single activeSession that this PR replaces. Whichever lands second needs a port: with several controllers, "return control to the TV" should close the non-passive controllers with tv_disconnected, as Disconnect does here.
  • Older phones can't confirm a replacement, so the TV keeps newest-launch-wins for them. After a reconnect they take a full handoff instead of reusing the profile.
  • The pre-shared-key trust model is unchanged. Any LAN device that speaks the protocol can connect, now alongside the phone instead of replacing it.

Follow-ups:

  • Stop while a launched title is still loading returns player_not_ready; the Apple TV cancels the load.
  • The audiobook player doesn't register with the receiver.
  • remoteStopRequests has no replay, so a Stop right after registration can be lost.
  • Revoke an approved temporary session when a handoff stops just before activation (the same follow-up as silo-apple#856).

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, Xcode simulators
  • Model(s): claude-opus-5-5
  • Involvement: Fully AI-generated; human verification of the diff is pending
  • Adversarial review: an independent Claude Code agent (claude-opus-5-5) reviewed the whole stack against main and silo-apple's receiver, without running it. It reported eight findings, all in the receiver:
    • Fixed in this PR (1–6):
      1. A refusal sent to a dropped socket, or a throw in the shared ticker, expiry collector or identity-end job, could crash the app. These now run guarded.
      2. A failed handoff_ready left other controllers de-authorized and could end another controller's profile.
      3. A handoff could end without a reply.
      4. The identity-end job was swapped outside the lock.
      5. Standby didn't come back after a launch that never opened a player.
      6. The superseded notice was sent from the new controller's read loop.
    • Left as a follow-up (7): Stop during a pending launch.
    • Kept by design (8): older phones take a full handoff after a reconnect.

🤖 Generated with Claude Code

Note

Support multiple simultaneously connected Remote Control controllers on one TV

  • Replaces the single active-controller slot in TvSiloCastReceiver with a controller map that admits up to 8 concurrent connections and evicts idle controllers (oldest silent, then unauthorized, then passive) when full. Adds pure decision helpers in TvSiloCastReceiverPolicy.kt
  • Restricts borrowed-identity reuse to the owning connection or a controller presenting an issued 32-byte hex resume token; RemotePlaybackIdentityManager.prepare now takes an allowReuse flag because device IDs are self-reported
  • Adds one in-flight handoff at a time, replacement confirmation before replacing another profile's on-screen title, and per-controller command rate limiting (40-command burst, 20/sec refill)
  • Publishes the deployment identity in NSD TXT records and the TV hello, and reconciles per-controller authorization when the effective server changes
  • Adds temporarySessionExpired flow to TokenManager so borrowed-profile session expiry stops playback and notifies authorized controllers
  • Simplifies the phone remote's Subtitles menu to track selection only (Off + tracks); delay and position controls removed
  • Risk: existing controller clients that relied on automatic single-controller replacement on connect will now be refused or rate-limited; check handleOffer reuse rules and prepare callers updated for the new allowReuse parameter

Macroscope summarized ac5195a.

Quick104 and others added 3 commits October 20, 2026 16:31
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>
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>
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>
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4de472fc-9ac4-49f7-9e18-881749bbfec7
📥 Commits

Reviewing files that changed from the base of the PR and between 5d73a26 and 8a5b80b.

📒 Files selected for processing: 10
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/cast/SiloCastNsdAdvertiser.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/cast/RemotePlaybackIdentityManager.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/cast/TvSiloCastReceiver.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/cast/TvSiloCastReceiverPolicy.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/di/AndroidTvModule.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/cast/RemotePlaybackHandoffOrderTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/cast/RemotePlaybackIdentityClaimTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/cast/TvSiloCastReceiverPolicyTest.kt
  • shared/src/androidMain/kotlin/org/siloserver/silo/network/EncryptedTokenManagerImpl.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/network/TokenManager.kt
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Don't program by coincidence. Rely only on reliable things. Beware of accidental complexity, and don't confuse a happy coincidence with a purposeful plan. |
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • 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 the size/XXL 1000–2999 added + deleted lines (all files); change volume, not priority. label Oct 20, 2026
@silo-triage silo-triage Bot added enhancement New feature or request priority: P2 Limited scope, workaround exists, or polish stacked Stacked on another open PR or branch labels Oct 20, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 20, 2026

Copy link
Copy Markdown

Temporary borrowed-session expiry is a visible flow without an end-to-end recording: playback stops and the TV exits the player, while the Android Remote Control shows an expiry or server-mismatch error. The description says this flow was not run because it requires a refused refresh. Please add a recording covering Android TV and the Android phone layouts (phone, tablet, and foldable folded and open); the private evidence page should include those affected devices.

Automated check: Macroscope check run agent (gpt-6-luna). Evidence was not reviewed for correctness.

Posted via Macroscope — Visible change evidence

Quick104 and others added 3 commits October 20, 2026 18:41
…shed 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>
@macroscopeapp

macroscopeapp Bot commented Oct 20, 2026

Copy link
Copy Markdown

Two visible paths are not fully evidenced:

  • SiloCastNsdAdvertiser.kt adds serverIdentity so the phone can discover a TV for the same deployment at a different address. Please add before-and-after views of the same target-picker data on Android phone, tablet, and foldable folded/open; the Evidence section does not describe this case.
  • The cross-profile replacement flow is multi-step. The Evidence text describes captures and the Cancel/Replace outcomes but does not identify a recording. Please include a short recording of the prompt, choice, and resulting playback on the controller and Android TV, with the phone layouts represented across phone, tablet, and foldable folded/open.

Automated check: Macroscope check run agent (gpt-6-luna). Evidence was not reviewed for correctness.

Posted via Macroscope — Visible change evidence

Quick104 and others added 2 commits October 20, 2026 19:09
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>
@Quick104

Copy link
Copy Markdown
Contributor Author

Evidence updated (same link):

  • Cross-profile replace: added a 70-second recording on the Android TV and the Android phone. It shows the TV's prompt, Cancel (the title keeps playing), the prompt again, and Replace (the TV closes the other profile's playback and resumes at the new profile's position). The phone's dialog at tablet and foldable sizes is on feat(cast): let the phone share a TV with other controllers and ask before replacing #652's evidence page.
  • serverIdentity in the TV's mDNS record: added before and after picker captures with a TV advertising another address of the same deployment. The other sizes are on feat(cast): let the phone share a TV with other controllers and ask before replacing #652's page.
  • Expiry of a borrowed session: not recorded. It needs the server to refuse the TV's refresh of a temporary session, which can't be triggered from the clients without changing shared dev. Unit tests cover it, and the PR lists it as not run end to end.

@Quick104
Quick104 changed the base branch from feat/silocast-multi-controller-phone to main October 21, 2026 00:58
…ontroller-tv

# Conflicts:
#	android-shared/src/androidMain/kotlin/org/siloserver/silo/common/cast/SiloCastNsdAdvertiser.kt
#	androidApp/src/androidMain/kotlin/org/siloserver/silo/android/cast/SiloCastController.kt
@silo-kody

silo-kody Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

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>
@silo-kody

silo-kody Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌
⚠️ 4 Kody Rule(s) were not evaluated

These rules declare that they need repository context beyond this diff, and the review could not retrieve it, so they were not judged on this pull request:

  • Validate download ownership before publishing subtitle bytes
  • Bind playback markers to the actual playing file
  • Share route-aware audio decisions and preserve original-format decoder fallback
  • Apply playback route eligibility consistently at admission and media loading

… 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>
@silo-kody

silo-kody Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌
⚠️ 4 Kody Rule(s) were not evaluated

These rules declare that they need repository context beyond this diff, and the review could not retrieve it, so they were not judged on this pull request:

  • Validate download ownership before publishing subtitle bytes
  • Bind playback markers to the actual playing file
  • Share route-aware audio decisions and preserve original-format decoder fallback
  • Apply playback route eligibility consistently at admission and media loading

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>
@silo-kody

silo-kody Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌
⚠️ 4 Kody Rule(s) were not evaluated

These rules declare that they need repository context beyond this diff, and the review could not retrieve it, so they were not judged on this pull request:

  • Validate download ownership before publishing subtitle bytes
  • Bind playback markers to the actual playing file
  • Share route-aware audio decisions and preserve original-format decoder fallback
  • Apply playback route eligibility consistently at admission and media loading

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>
@silo-kody

silo-kody Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌
⚠️ 4 Kody Rule(s) were not evaluated

These rules declare that they need repository context beyond this diff, and the review could not retrieve it, so they were not judged on this pull request:

  • Validate download ownership before publishing subtitle bytes
  • Bind playback markers to the actual playing file
  • Share route-aware audio decisions and preserve original-format decoder fallback
  • Apply playback route eligibility consistently at admission and media loading

@Quick104
Quick104 merged commit 7d158d5 into main Oct 21, 2026
3 checks passed
@Quick104
Quick104 deleted the feat/silocast-multi-controller-tv branch October 21, 2026 01:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority: P2 Limited scope, workaround exists, or polish size/XXL 1000–2999 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