iOS 27 remote now playing - #5690
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Multiple unresolved critical security, duplicate-command, and memory-safety issues block approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Sources/RemoteMediaCore/RemoteMediaSnapshotMapper.swift:91
Boolalso bridges toNSNumberin this integer path, sosupported_features: truebecomes the pause capability on-device while the server mapper rejects it. Reject booleans before converting numeric feature flags.
- Files reviewed: 100/104 changed files
- Comments generated: 11
- Review effort level: Balanced
| public static func isFetchable(_ url: URL) -> Bool { | ||
| guard url.scheme?.lowercased() == "https" else { return false } | ||
| guard url.user == nil, url.password == nil else { return false } | ||
| guard let host = url.host, !host.isEmpty else { return false } | ||
| return true |
There was a problem hiding this comment.
I don’t think rejecting all private/LAN addresses would be the best solution since local entity picture URLs can be used legitimately in some setups (if you're self-hosting music). I'm going to change it to still allow LAN hosted artwork and just prevent redirects from bypassing the checks
| // Belt and braces: `ephemeral` already holds nothing, and this says so at the request too. | ||
| request.httpShouldHandleCookies = false | ||
|
|
||
| guard let (data, response) = try? await session.data(for: request) else { |
| case is URLError: | ||
| return true |
| public static func send( | ||
| server: Server, | ||
| baseURL: URL, | ||
| method: Method = .get, |
| Current.settingsStore.remoteMediaSelection = selection | ||
| // Each Follow is its own relationship. Re-following the same player reuses the session | ||
| // identifier, so this is what lets the server retire the token the last one registered and | ||
| // know which of the two came later. | ||
| Current.settingsStore.startRemoteMediaFollowLifetime(following: selection) |
| /// One client and a cached context, refreshed when the host delivers new attributes so route | ||
| /// changes are picked up without making every command build the app's networking stack. | ||
| private let client = RemoteMediaWebhookClient() |
| // The host app is authoritative when it is running, but a reconciliation already in flight | ||
| // is answering a command the user just pressed, so it is not thrown away here. | ||
| apply(attributes.snapshot) |
| } catch { | ||
| RemoteMediaLog.logger.error( | ||
| "command \(command.rawValue, privacy: .public) failed: \(error.localizedDescription, privacy: .public)" | ||
| ) | ||
| throw error |
| switch value { | ||
| case let number as NSNumber: candidate = number.doubleValue | ||
| case let text as String: candidate = Double(text) |
| } catch { | ||
| lastError = error | ||
| guard Self.shouldTryNextCandidate(after: error) else { throw error } |
606e61c to
772be0c
Compare
772be0c to
176a9c7
Compare
Validate a followed entity id against the character set Home Assistant slugifies to, so it cannot escape the quoted literal it sits in inside the render_template query. Fall through to the next webhook route on statuses that prove a service call never arrived, which is what a deleted cloudhook returns. Clamp seek and volume through the command so the settle condition waits for the value that was sent. Block up artwork bytes before appending them, parse a space-separated timestamp with no fractional seconds, and drop some dead code. Co-authored-by: Claude <noreply@anthropic.com>
bgoncal
left a comment
There was a problem hiding this comment.
Please split this PR into smaller iterations, it's already over 8k lines added + 110 file changes.
Also coordinate with your core PR to have it ready first, core PRs can change quite a lot during review so I would advise keeping it simple around here until it's not done there.
Also, there is currently a PR open in core that implements entity state subscription reported via silent push notifications, that one could help you a lot in here
AI Policy
Select exactly one option that describes AI usage in this contribution:
Summary
Adds Remote Now Playing support on iOS 27.
A media_player can be followed from its Add to... menu, which makes it appear in the native iOS Now Playing interface on the Lock Screen and in Control Center. Only one media player can be followed at a time. The current player can be stopped either from its add to menu or from the companion app settings
The session continues receiving metadata, progress, artwork, and playback state updates while the app is not open.
Screenshots
Follow / stop following
Lock Screen / Control Center
Link to pull request in Documentation repository
Documentation: home-assistant/companion.home-assistant#1420
Any other notes
It might be in the best interest to split this PR, it's huge.
Documentation needs to be updated with generic screenshots
Needs Bump GitHub runners to Xcode 27 #5665
Work in other repos:
Core: Add Remote Now Playing session support core#181613
APNS: Now playing direct apns mobile-apps-fcm-push#363
Discussion: Native Media Player Notification for Mobile Apps feature-requests#1338
Tested on a physical iPhone running iOS 27, including metadata and artwork updates with the Companion app force-closed and playback controls from the native Now Playing UI.
The RemoteMedia-focused test suites pass 56/56. SwiftFormat, SwiftLint, and RuboCop also pass.