Skip to content

feat: redesign cast UX with unified Connect drawer#14357

Merged
raymondjacobson merged 9 commits into
mainfrom
claude/stupefied-varahamihira-b469b0
May 20, 2026
Merged

feat: redesign cast UX with unified Connect drawer#14357
raymondjacobson merged 9 commits into
mainfrom
claude/stupefied-varahamihira-b469b0

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

@raymondjacobson raymondjacobson commented May 19, 2026

Summary

Replaces the AirPlay/Chromecast settings toggle and the grayed-out cast icon with a Spotify-style "Connect" picker behind a single cast button on both mobile and web.

Mobile (now-playing drawer)

  • New IconCast button opens a ConnectDrawer listing:
    1. This Device — ends any active chromecast session
    2. AirPlay & Bluetooth on iOS (openAirplayDialog) / Bluetooth on Android (Linking.sendIntent('android.settings.BLUETOOTH_SETTINGS') with openSettings fallback)
    3. Each chromecast device discovered via useDevices() — tap starts a session via SessionManager.startSession(deviceId)
  • Cast button is grayed/disabled only when offline AND no chromecast devices discovered
  • The active device gets a primary tint + checkmark; the same goes for "This Device" when nothing is casting

Web (desktop play bar)

  • New cast button placed next to the queue button
  • "Connect" popup with This web browser + Google Cast devices rows
  • "Google Cast devices" calls audio.remote.prompt() to surface Chrome's built-in cast picker — no Cast Web Sender receiver needed
  • Hidden entirely when RemotePlayback isn't supported (non-Chromium browsers)

Settings

  • Removed CastSettingsRow (the AirPlay/Chromecast segmented control). The user no longer chooses a method up-front — they pick a target each time from the drawer.

Harmony

  • New IconCast (Spotify-style laptop + speaker) and IconCastSpeaker (standalone speaker for device rows). Existing IconCastAirplay / IconCastChromecast left untouched.

Redux (@audius/common/store/cast)

  • Dropped method, updateMethod, CastMethod type, CAST_METHOD storage key, and both persistence sagas
  • Kept isCasting and added an optional deviceName so the drawer can mark the active device. GoogleCast.tsx resolves castSession.getCastDevice().friendlyName and Airplay.tsx passes the audio route's portName

Test plan

  • Mobile iOS: tap cast icon on the now-playing drawer → drawer shows This Device + AirPlay & Bluetooth + chromecast devices. AirPlay row opens the system picker. Selecting a chromecast device starts a session and the icon turns active.
  • Mobile Android: same flow but second row is labeled "Bluetooth" and opens android.settings.BLUETOOTH_SETTINGS.
  • Mobile offline: cast icon is grayed/disabled.
  • Mobile: "This Device" row ends an active chromecast session and the audio resumes locally.
  • Web (Chrome): cast icon appears between volume and queue. Popup shows This web browser + Google Cast devices. Clicking the cast-devices row opens Chrome's native picker.
  • Web (Safari/Firefox): cast button is hidden (no RemotePlayback).
  • Settings: the AirPlay/Chromecast segmented control is gone from the iOS settings screen.

Notes

  • No native iOS / Android code changes — existing Bonjour entries (Info.plist) and the cast receiver 222B31C8 (AndroidManifest.xml) are untouched.
  • Mobile typecheck/lint baseline is broken in the worktree environment (missing @react-native/typescript-config/tsconfig.json in node_modules; ~13k pre-existing errors), so a clean baseline-vs-after comparison wasn't possible locally. The one real type issue surfaced — narrowing isCasting && ... in the drawer — is fixed with Boolean(...).
  • Web cast-button/ + PlayBar.tsx pass ESLint cleanly.
  • Web dev server couldn't be started in the worktree (packages/web/env/ missing), so no browser screenshot of the new button.

🤖 Generated with Claude Code

Replaces the AirPlay/Chromecast settings toggle and grayed-out icon
behavior with a Spotify-style "Connect" picker shown on a single cast
button.

Mobile (now-playing drawer):
- New IconCast button opens a ConnectDrawer listing "This Device",
  "AirPlay & Bluetooth" (iOS) / "Bluetooth" (Android), and every
  Chromecast device discovered via useDevices()
- Tapping a device calls SessionManager.startSession(deviceId);
  "This Device" calls endCurrentSession()
- Removes CastSettingsRow and the Android-forced-chromecast hack

Web (desktop play bar):
- New cast button next to the queue button opens a Connect popup with
  "This web browser" + "Google Cast devices"
- Selecting Google Cast devices calls audio.remote.prompt() to surface
  Chrome's built-in cast picker — no Cast Web Sender receiver needed
- Hidden when RemotePlayback isn't supported

Other:
- Adds IconCast + IconCastSpeaker to harmony
- Simplifies cast Redux slice: drops `method`/persistence sagas, keeps
  isCasting and adds optional deviceName so the drawer can mark the
  active device

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 19, 2026

⚠️ No Changeset found

Latest commit: 7122edb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

🌐 Web preview ready

Preview URL: https://audius-web-preview-pr-14357.audius.workers.dev

Unique preview for this PR (deployed from this branch).
Workflow run

raymondjacobson and others added 7 commits May 19, 2026 12:15
- Replaces the laptop+speaker Cast.svg with a music-note + cast-waves
  glyph (per design)
- Drops IconCastSpeaker — both the mobile ConnectDrawer and web
  ConnectPopup now use the existing IconSpeaker for each device row

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drive the active highlight off the local RemotePlayback state from
  useRemotePlayback instead of Redux. The Redux roundtrip wasn't
  reliable through Chrome's audio.remote events, so the Google Cast
  Devices row stayed inactive while casting.
- Drop the hardcoded popup height so the popup sizes to its content,
  removing the extra empty space between the "Connect" header and the
  divider.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AudioContext routing means audio.remote.state never reaches
  'connected' on this app, so the active row never flipped to Google
  Cast Devices. Add an optimistic local "user picked cast" flag set
  when audio.remote.prompt() resolves, reset when the user picks
  "This web browser".
- Remove the new-feature dot/pulse and the queued-items indicator on
  the queue button — render a plain IconButton instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Standard RemotePlayback has no programmatic disconnect, so we re-open
the browser's native cast picker when the user picks "This web
browser" while a session is active. Chrome surfaces a "Stop casting"
option for the current device, mirroring Spotify's flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Use the built-in DrawerHeader (via NativeDrawer's title + titleIcon
props) so the Connect drawer matches the Share Track drawer: bold,
uppercase, centered title with a leading icon. Removes the custom
header row + IconClose button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Safari's audio.remote.prompt() opens the AirPlay picker rather than
Chrome's cast picker, and Audius already disables WebAudio on Safari
so the audio plays straight through the <audio> element — AirPlay
should route correctly. Surface this in the popup:

- Safari: row labeled "AirPlay devices" with IconCastAirplay; tooltip
  reads "AirPlay"
- Chromium: unchanged ("Google Cast devices", IconSpeaker, "Cast")

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…clobbering

Re-introduce a `method: 'airplay' | 'chromecast' | null` field on the
cast slice and make `setIsCasting({ isCasting: false, method })` a
no-op when `method` doesn't match the currently-active one. The
AirPlay route-change listener fires on any iOS audio route change —
including the one Chromecast triggers when it takes over — so it was
clearing chromecast state right after GoogleCast.tsx set it. Both
listeners now tag their off-dispatch with their own method, so they
can't clobber each other.

Drawer UX fixes that depend on knowing the method:
- AirPlay & Bluetooth row highlights when method='airplay'
- "This Device" routes back via the AirPlay picker when AirPlaying
  (iOS has no programmatic AirPlay disconnect) and ends the chromecast
  session when chromecasting
- Pre-mute TrackPlayer.setVolume(0) before startSession so AirPlay
  doesn't briefly bleed audio while the chromecast session connects
- Add divider after AirPlay & Bluetooth and start discovery on iOS
  when the drawer opens

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Safari's `audio.remote.prompt()` doesn't reliably open the AirPlay
picker for <audio> elements. The webkit-prefixed API on
HTMLMediaElement is what actually drives Safari AirPlay:

- `webkitShowPlaybackTargetPicker()` to open the picker (must be
  called from a user gesture; no promise to await)
- `webkitCurrentPlaybackTargetIsWireless` for the active state
- `webkitcurrentplaybacktargetiswirelesschanged` event for changes

Detect Safari and route through these instead of `RemotePlayback`.
Chromium keeps using the standard API for Chrome's cast picker.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@raymondjacobson raymondjacobson merged commit ee1362d into main May 20, 2026
13 checks passed
@raymondjacobson raymondjacobson deleted the claude/stupefied-varahamihira-b469b0 branch May 20, 2026 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant