Repository navigation
Android TV Remote support for Mi TVs without MiLink - #5
Merged
Merged
Conversation
Some Mi TVs (e.g. MiTV-MOOR2 on Android TV 11, #4) don't run Xiaomi's MiLink service on 6091 but do run Google's Android TV Remote service. The mobile apps now speak its v2 protocol as a fallback: - Try MiLink first; if nothing listens on 6091, connect to 6466 over TLS with a client certificate, and remember the protocol per TV. - If the TV drops the TLS session before configuring, run the one-time pairing on 6467: the TV shows a 6-character code, the app checks its checksum, then sends the SHA-256 secret over both public keys. - Native module: TLS sockets, a persistent RSA client key (Keychain on iOS, app storage on Android), signing, and the TV's certificate. The self-signed X.509 certificate is built in TypeScript since iOS has no API for it. - Keys use RemoteKeyInject; text goes through RemoteImeBatchEdit when the TV reports a focused field, else per-key. - New pairing screen. Protocol encoders, pairing secret and certificate are unit tested against bytes from androidtvremote2; the client's probe/pair/connect flow is tested with a mocked native socket. CI: a manual run can publish a release; a suffixed version such as 1.2.0-alpha.1 becomes a mobile-only pre-release for testing branches. Refs #4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
Brings the mobile apps' Android TV Remote fallback to the Mac app, so Mi TVs without MiLink (#4) work there too: - ATVProtocol.swift: Swift port of src/atv (protobuf framing, pairing and remote messages, pairing secret, self-signed certificate DER). - ClientIdentity.swift: RSA client key and certificate in the login keychain, combined into the TLS identity. - RemoteClient (was MiLinkClient): tries MiLink, falls back to TLS 6466, pairs on 6467 when the TV doesn't know the certificate, and remembers the protocol per TV. - Pairing sheet in the popover for the code shown on the TV. - XCTest suite against the same androidtvremote2 fixtures as mobile; CI runs it before building. Pre-releases now include the macOS build again. Refs #4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
Network.framework often reports a TV aborting the TLS handshake (our certificate isn't paired yet) as a plain POSIX reset rather than a TLS error. The macOS app and the iOS module only counted TLS errors as "something listens on 6466", so they treated the TV as unreachable, went back to probing MiLink, and never reached pairing (#4). Any error other than refused / timed out / no route now counts as reachable. The macOS status line also names the port that failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
The Mac app still never reached pairing on a MOOR2 (#4). Two causes: - Apple clients offered TLS 1.3. The TV's remote service (Netty) is a TLS 1.2 server, and native clients that work with Xiaomi TVs over Network.framework pin TLS 1.2 (e.g. M1KE-27/xiaomi-remote). Pin min and max to 1.2 on macOS and iOS (and Android, for consistency). - A stalled or rejected TLS handshake can surface as a timeout or reset, which read as "nothing on 6466", so the app went back to MiLink and looped. Now a plain TCP connection is made first, like the Android module does: once TCP gets through, any TLS failure means the service is there and pairing starts. Also: TLS connections get a 10 s timeout (the keychain may prompt before the key can sign), a pairing failure keeps the sheet open with the error and port instead of silently starting over, and the sheet shows the raw error. Adds an end-to-end test: CI starts Tests/fake_tv.py, a stand-in Android TV (TLS 1.2 only, client certificates required, unpaired ones rejected, real polo pairing with a code), and drives the Mac client through MiLink fallback, pairing, connect, key presses and reconnect. The fake TV is checked against androidtvremote2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
The end-to-end test showed the Mac app connecting without a client
certificate ("peer did not return a certificate"), so the TV refused
both pairing and the remote session. Looking the certificate and
identity up by label in the login keychain came back empty.
Now the certificate lives in Application Support and the identity is
built with SecIdentityCreateWithCertificate, which pairs it with the
private key in the keychain. The key is tagged as well as labeled. A
missing identity is reported in the UI instead of silently connecting
without one. Adds a step-by-step keychain test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
While macOS Android TV Remote support is being tested (#4), alpha and beta builds skip Android and iOS. Manual runs can also pass macos_only. The release job runs as long as nothing failed, and the notes only list the platforms that were built. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
"Type on TV" did nothing on the MOOR2 (#4). Text was only sent as an IME batch edit after the TV had reported IME counters; otherwise it fell back to letter key events, which the Android TV Remote service ignores. Now text always goes as an IME edit, counters defaulting to 0 and following the TV's batch edits, IME show requests and focused-field reports. The cursor sits after the text, as kud/androidtv-remote and googletv-tui do. The fake TV records IME edits, and the macOS end-to-end test types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
Over Android TV Remote the MOOR2 ignores KEYCODE_TV_INPUT (178) and KEYCODE_APP_SWITCH (187), like most key codes outside the service's supported set, and nothing documents what Xiaomi TVs respond to instead. On TVs using that protocol, Input source and Recent apps now open a panel of candidates (TV, HDMI 1-4, Guide, Settings, F1-F12; hold or double-press Home, All apps, Menu, hold OK) so a tester can report which one works (#4). MiLink TVs keep the original keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
Release prep for the Android TV Remote work (#4): - The test panel is gone. Testing on a MiTV-MOOR2 showed the Android TV Remote service ignores every input-source and recent-apps key we tried. On those TVs Input source now opens Settings (where the inputs are) and Recent apps is greyed out; MiLink TVs are unchanged. Same in the macOS and mobile apps. - Mobile parity with the macOS fixes: a failed pairing stays on the pairing screen with the error and port instead of silently starting over; iOS keeps the client certificate as a file too, re-adds it to the keychain if the identity can't be formed, and refuses to connect without a certificate rather than failing obscurely. - CI: every release builds all three apps again; macos_only stays as an opt-in for quick Mac test builds. - README: MOOR2 tested, Android TV Remote limitations, keychain note, TLS 1.2 / TCP probe / certificate notes, end-to-end test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YS6HGULeQFiqj9SZfZ7Gvv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds support for Mi TVs that don't run Xiaomi's MiLink service, through Google's Android TV Remote protocol (the one the Google TV app uses), in all three apps. Fixes #4.
What changes
macos_onlyskips the mobile builds for quick Mac test builds.Bugs found along the way (macOS/iOS)
SecIdentityCreateWithCertificate. iOS got the same hardening.Testing
swift test, in CI): the same fixtures, a keychain identity test, and an end-to-end test againstapps/macos/Tests/fake_tv.py. That's a stand-in TV: TLS 1.2 only, client certificates required, unpaired ones rejected, real pairing. The test covers MiLink fallback, pairing, keys, typing and reconnecting. The fake TV itself was checked against androidtvremote2.Generated by Claude Code