Note: TODO items are AI generated and can include incorrect or incomplete details
-
Create separate
ArtworkRequestConfigtype instead of reusingMediaRequestConfigCurrentMediaRequestConfigis overloaded with artwork-specific fields (imageQueryParams)- The
transformcallback has different semantics for media vs artwork (artwork getsImageContext)
-
Simplify by removing
resolveand passingtracktotransforminstead- Current: two callbacks (
resolve+transform) with overlapping responsibilities - Proposed: single
transformcallback that receives everything
interface ArtworkTransformParams { request: RequestConfig // Merged base config (track.artwork as default path) track: Track // The track being processed context?: ImageContext // Size hints from AA/CarPlay (undefined at browse-time) } interface ArtworkRequestConfig extends RequestConfig { transform?: ( params: ArtworkTransformParams ) => Promise<RequestConfig | null> imageQueryParams?: ImageQueryParams }
- Return
nullfrom transform to skip artwork for a track - Covers all use cases: URL construction, signing, size params, conditional artwork
- Current: two callbacks (
-
Apply same pattern to
MediaRequestConfigfor audio streamsinterface MediaTransformParams { request: RequestConfig track: Track } interface MediaRequestConfig extends RequestConfig { transform?: (params: MediaTransformParams) => Promise<RequestConfig> }
- Enables per-track URL signing for audio streams too
-
By default there should be no icon in the tabs on CarPlay
- Currently tabs may show a default icon
- Tabs should only show icons if explicitly configured
-
Consider adding a configurable loading message for CarPlay cold start
- Currently shows blank screen while waiting for
configureBrowser()to be called - Could add
carPlayLoadingMessage?: stringto browser config - Or just use a generic "Loading..." - but that's not localized
- Currently shows blank screen while waiting for
-
CarPlay lazy tab loading improvements
- Loading indicator: When a tab is selected for the first time, there's no visual feedback while content loads. The tab appears empty until
loadContentcompletes. Consider showing a loading item in the section. - Race condition: If user rapidly switches tabs, multiple
loadContentcalls could be in flight. Current code doesn't track in-flight loads, soloadContentIfNeededcould be called multiple times before the first load completes. Consider adding aSet<String>to track loading paths. - Error handling:
loadContent(for:into:)logs errors but doesn't provide user feedback. A failed lazy load leaves an empty tab with no indication of what went wrong. Consider showing an error item with retry option.
- Loading indicator: When a tab is selected for the first time, there's no visual feedback while content loads. The tab appears empty until
-
CarPlay list item spinner for async operations
- When a list item is selected, the handler receives a completion block
- If async work is initiated without immediately calling completion, CarPlay displays a spinner
- Call completion block when ready to tell CarPlay to remove the spinner
- Currently we may be calling completion too early or not leveraging this for loading states
-
Configure CPNowPlayingTemplate immediately on CarPlay connect
- Per WWDC: "When your app connects to the CarPlay scene, that's a great time to set up the shared nowPlayingTemplate"
- System may launch app just to show Now Playing - template should be ready immediately
- Configure playback rate button, Up Next button, and observers right away
- Can always update the shared template later
- Currently we may be configuring it too late in the lifecycle
-
Album/Artist button on Now Playing screen
- Enable
CPNowPlayingTemplate.shared.isAlbumArtistButtonEnabled - Implement
nowPlayingTemplateAlbumArtistButtonTapped(stub exists inNowPlayingObserver)
- Enable
-
CarPlay/Siri voice search (iOS) - library-friendly approach
- SiriKit requires app-level setup (entitlements, Intent Extension target, Info.plist)
- Library cannot add these - must be done by host app
- Library provides:
handlePlayMediaIntent(searchTerm: string)method- Searches via browser's search callback
- Loads results into queue and starts playback
- Returns success/failure for intent response
- App provides: IntentHandler that calls the library method
class IntentHandler: INExtension, INPlayMediaIntentHandling { func handle(intent: INPlayMediaIntent, completion: ...) { let searchTerm = intent.mediaSearch?.mediaName ?? "" AudioBrowser.shared.handlePlayMediaIntent(searchTerm) { success in completion(INPlayMediaIntentResponse(code: success ? .success : .failure, ...)) } } }
- Documentation needed: Setup guide for Intent Extension, entitlements, Info.plist
- Mirrors Android's
playFromSearch()forMEDIA_PLAY_FROM_SEARCH - Note:
CPSearchTemplateis for navigation apps, not audio apps - Note:
CPAssistantCellConfigurationrequires SiriKit - crashes without it
-
Move
carPlayUpNextButtonandcarPlayNowPlayingButtonstoupdateOptions- Currently these are in
BrowserConfigurationbut they're really player options - Should be moved to
IOSUpdateOptionsalongside other player configuration - This matches the pattern where browser config is about content/navigation, updateOptions is about player behavior
- Would make them updatable at runtime like other player options
- Currently these are in
-
Investigate renaming
carPlayNowPlayingButtonstoiosNowPlayingButtons- Currently only used for CarPlay Now Playing screen
- Check if regular iOS Now Playing (lock screen, Control Center) supports these buttons too
- If so, rename to
iosNowPlayingButtonsfor clarity - Requires testing on a physical device (simulator may not show full Now Playing UI)
-
Make playback rate options configurable
Currently hardcoded in CarPlayController:[0.5, 1.0, 1.5, 2.0]Add→carPlayPlaybackRates?: number[]to BrowserConfigurationiosPlaybackRatesinupdateOptions()- Default:
[0.5, 1.0, 1.5, 2.0] - Android Auto: No equivalent UI (no
CPNowPlayingPlaybackRateButton)
-
Move
iosPlaybackRatesintoIOSUpdateOptions- Currently a top-level option in
UpdateOptions - Should be nested under
ios: { playbackRates: [...] }for consistency
- Currently a top-level option in
-
Add manual testing steps for Now Playing screen updating when
updateOptions()is called- Verify shuffle/repeat/rate button states update when capabilities change
- Test adding/removing capabilities at runtime
- Ensure CarPlay Now Playing reflects changes immediately
-
Investigate where MPFeedbackCommand
localizedTitle/localizedShortTitleare displayed- iOS
likeCommanduses these properties but unclear where they're visible to users - Possibilities: VoiceOver/accessibility, Siri, CarPlay, or not displayed at all
- Currently hardcoded to "Favorite" in
Capability+RemoteCommand.swift - If visible somewhere useful, consider making configurable via
iosFavoriteTitleinupdateOptions() - Apple docs: "a localized string used to describe the context of a command"
- See: https://developer.apple.com/documentation/mediaplayer/mpfeedbackcommand/1622905-localizedtitle
- iOS
-
Remove like/dislike/bookmark capabilities and callbacks until proper implementation
- Currently half-implemented: capabilities exist but no clear user-facing functionality
likemaps to iOS dislike command (confusing mapping)bookmarkanddislikeexist but unclear what they should do- Should be removed until we design a proper rating/feedback system
- Affects: PlayerCapabilities interface, iOS buildRemoteCommands, Android MediaSessionCommandManager
- Keep
favoritesince it has clear semantics and implementation
-
Refactor capabilities/notification buttons/now playing buttons for better alignment
- Current system is inconsistent and confusing:
- PlayerCapabilities:
skip?: boolean, jump?: boolean(unified capabilities) - NotificationButton:
'skip-to-previous' | 'skip-to-next' | 'jump-backward' | 'jump-forward'(separate buttons) - NowPlayingButtons:
nextPrevious: 'skip' | 'jump'(iOS constraint forcing choice)
- PlayerCapabilities:
- Problems: Different naming, different granularity, platform leakage
- Goal: Consistent naming and concepts across all three systems
- Consider platform-specific capabilities approach or unified capability->button mapping
- See controls-plan.md for proposed redesign
- Current system is inconsistent and confusing:
-
Change
capabilities: Capability[]tocapabilities: PlayerCapabilitiesinterface- Use an interface with optional boolean properties instead of an array
- All capabilities enabled by default (
undefinedortrue= enabled,false= disabled) - More ergonomic: only specify what you want to disable
interface PlayerCapabilities { play?: boolean pause?: boolean stop?: boolean seekTo?: boolean skipToNext?: boolean skipToPrevious?: boolean jumpForward?: boolean jumpBackward?: boolean favorite?: boolean bookmark?: boolean shuffleMode?: boolean repeatMode?: boolean playbackRate?: boolean } // Usage: disable only specific capabilities updateOptions({ capabilities: { shuffleMode: false, repeatMode: false } })
- Breaking change - updated iOS and Android to check for
!= falseinstead of array membership - Removed
playFromId,playFromSearch, andskip- they didn't gate any functionality - Renamed
ButtonCapabilitytoNotificationButtonfor clarity - Added JSDoc comments explaining what each capability enables/disables
- Persist and restore player settings on iOS
- Playback rate - save/restore
rateproperty - Repeat mode - save/restore
repeatMode - Shuffle mode - save/restore
shuffleEnabled - Android already does this via
PlaybackStateStore - Use UserDefaults or similar for iOS persistence
- Restore settings in
setupPlayer()or when player initializes
- Playback rate - save/restore
-
Pass platform limits to callbacks and API endpoints
- CarPlay has runtime limits:
CPTabBarTemplate.maximumTabCount,CPListTemplate.maximumSectionCount,CPListTemplate.maximumItemCount - Android Auto has similar constraints
- Currently: CarPlay truncates data after fetching full content
- Proposed: Pass limits in
BrowserSourceCallbackParamso data sources can optimize
interface BrowserSourceCallbackParam { path: string params: Record<string, string> limits?: { maxSections?: number maxItems?: number } }
- Requires: CarPlay/Android Auto to make separate requests rather than sharing
onContentChanged
- CarPlay has runtime limits:
-
Allow sync browser callbacks (currently all callbacks return
Promise<Promise<T>>)- Some callbacks could be synchronous for simpler use cases
- Would simplify routes like:
// Current (async required) '/favorites'() { return Promise.resolve({ url: '/favorites', title: 'Favorites', children: favorites }) } // Desired (sync) '/favorites': () => ({ url: '/favorites', title: 'Favorites', children: favorites })
- Would require Nitro spec changes and native implementation updates
-
Move initial navigation logic from Nitro layer to BrowserManager on both platforms
- Currently:
AudioBrowser.ktandHybridAudioBrowser.swifthandle initial navigation inconfigurationsetter - Proposed:
BrowserManagershould auto-navigate whenconfigis set, with anonNavigationErrorcallback - Benefits: Cleaner separation, self-contained BrowserManager, less duplication between platforms
- Currently:
- Handle duplicate tracks in queue source optimization
- Problem: Current optimization uses
firstIndex(where: { $0.src == trackId })/indexOfFirst { it.src == trackId } - If a playlist contains the same track twice, selecting the second instance jumps to the first
- Options:
- Encode index in contextual URL: Change
__trackId={src}to__trackId={src}&__index={n}- Preserves position from browse time
- Requires changes to BrowserPathHelper on both platforms
- Add unique instance ID to Track: New
instanceId: string?field generated client-side- Cleaner architecturally, useful for other features (reordering, removing specific instances)
- Would change contextual URL to use instanceId instead of src
- ID only stable for queue lifetime (fine for this optimization, resumption re-expands anyway)
- Requires Nitro type changes and codegen
- Accept limitation: Document that optimization only works for playlists without duplicates
- Simplest, but edge case will behave incorrectly
- Encode index in contextual URL: Change
- Decision pending - need to weigh complexity vs correctness
- Problem: Current optimization uses
- Add
useVolume()hook for reactive volume state - Investigate what volume is doing - probably a multiplier on top of system volume (0.0-1.0 range?)
- Restore volume in PlaybackStateStore (persist and restore volume level across sessions)
- Silent failure in voice search (Service.kt:92-106):
- Issue: The boolean return from
playFromSearch()is ignored - user gets no feedback if search fails - Recommendation: Log the result or show notification/toast when search fails (e.g., "No results found for 'query'")
- Issue: The boolean return from
- Test with voice commands like "play michael jackson billie jean"
- Replace current example audio tracks with something else
- Need to update the example app's audio content
- Potential sources for freely usable and self-hostable audio files:
- Free Music Archive (FMA): CC-licensed music tracks
- ccMixter: Community music remixes under Creative Commons
- Incompetech: Royalty-free music by Kevin MacLeod
- Freesound: Short audio clips and sound effects (CC licenses)
- YouTube Audio Library: Free music and sound effects
- Internet Archive's Audio Collection: Public domain recordings
- Musopen: Classical music recordings in public domain
- Sample Focus: Free samples and loops for music production
- NASA Audio Collection: Space-related sounds and recordings (public domain)
- BBC Sound Effects: 33,000+ sound effects under RemArc license