Skip to content

feat(ios): add iosNextPreviousCommandsSkip option for AirPods seek support#2584

Open
anojht wants to merge 1 commit intodoublesymmetry:mainfrom
anojht:feat/ios-airpods-skip-forward-backward
Open

feat(ios): add iosNextPreviousCommandsSkip option for AirPods seek support#2584
anojht wants to merge 1 commit intodoublesymmetry:mainfrom
anojht:feat/ios-airpods-skip-forward-backward

Conversation

@anojht
Copy link

@anojht anojht commented Feb 18, 2026

Summary

Adds a new iosNextPreviousCommandsSkip option to updateOptions() that enables AirPods double-press (seek forward) and triple-press (seek backward) for podcast and audiobook apps, without changing the lock screen / Control Center button layout.

Problem

Podcast and audiobook apps typically configure their lock screen controls with Capability.JumpForward and Capability.JumpBackward to show skip-interval buttons (e.g., "15s back", "30s forward") instead of next/previous track arrows. However, this means AirPods double-press and triple-press do nothing because:

  1. Capability.Skip is a NOOP on iOS — it maps to "NOOP" in the constants dictionary and registers no commands
  2. Capability.SkipToNext / Capability.SkipToPrevious register nextTrackCommand / previousTrackCommand, which changes the lock screen UI to show next/previous track arrows, replacing the skip-interval buttons

There is currently no way to:

  • Keep the skip-interval buttons on the lock screen (via JumpForward / JumpBackward)
  • AND have AirPods double/triple-press trigger seek operations

This is a common requirement for podcast apps — Apple's own Podcasts app supports this exact behavior.

Solution

A new iosNextPreviousCommandsSkip boolean option in updateOptions() that, when true:

  1. Directly registers nextTrackCommand and previousTrackCommand handlers on MPRemoteCommandCenter.shared() outside of the RNTP capabilities system
  2. Routes these handlers to player.seek() using the configured forwardJumpInterval / backwardJumpInterval
  3. Does not add these commands to player.remoteCommands, so they don't affect the lock screen button layout

Why register outside the capabilities system?

The RNTP capabilities array maps 1:1 to player.remoteCommands, which SwiftAudioEx uses to determine both the command handlers AND the lock screen UI. By registering directly on MPRemoteCommandCenter.shared(), we decouple the AirPods command handling from the lock screen layout.

How AirPods commands work on iOS

AirPods button presses map to MPRemoteCommandCenter commands:

  • Single presstogglePlayPauseCommand
  • Double pressnextTrackCommand
  • Triple presspreviousTrackCommand

These are hardware-level mappings that cannot be changed. The only way to make double-press seek forward is to register a nextTrackCommand handler that performs a seek.

Usage

await TrackPlayer.updateOptions({
  capabilities: [
    Capability.Play,
    Capability.Pause,
    Capability.JumpForward,
    Capability.JumpBackward,
    Capability.SeekTo,
  ],
  forwardJumpInterval: 30,
  backwardJumpInterval: 15,
  // Enable AirPods double/triple-press to seek forward/backward
  iosNextPreviousCommandsSkip: true,
});

Lock screen result: Shows skip-interval buttons (⟲15 advancement advancement advancement ▶ ⟳30) — same as without the option.

AirPods result:

  • Double-press → seeks forward by forwardJumpInterval (30s)
  • Triple-press → seeks backward by backwardJumpInterval (15s)

Changes

src/interfaces/UpdateOptions.ts

  • Added iosNextPreviousCommandsSkip?: boolean with JSDoc documentation

ios/TrackPlayer.swift

  • Added nextPreviousCommandTargets instance property to track registered command targets for cleanup
  • Added configureNextPreviousAsSkip(enabled:) private method that:
    • Cleans up any previously registered targets (safe to call multiple times)
    • When enabled: registers nextTrackCommand and previousTrackCommand on MPRemoteCommandCenter.shared() with seek handlers
    • When disabled: removes targets and returns (commands stay managed by RNTP's capabilities system)
  • Called from update(options:resolve:reject:) after player.remoteCommands is set

Backward compatibility

  • Default value is false — no behavior change for existing users
  • The option is iOS-only and ignored on Android
  • Does not interfere with Capability.SkipToNext / Capability.SkipToPrevious if they are also in the capabilities array
  • Properly cleans up on subsequent updateOptions() calls

Test plan

  • Set iosNextPreviousCommandsSkip: true with JumpForward/JumpBackward in capabilities
    • Verify lock screen shows skip-interval buttons (not next/prev track arrows)
    • Verify AirPods double-press seeks forward by forwardJumpInterval
    • Verify AirPods triple-press seeks backward by backwardJumpInterval
  • Set iosNextPreviousCommandsSkip: false (or omit)
    • Verify AirPods double-press has no effect (current default behavior)
    • Verify lock screen is unaffected
  • Call updateOptions() multiple times toggling iosNextPreviousCommandsSkip
    • Verify no duplicate handlers or memory leaks
  • Use with SkipToNext/SkipToPrevious in capabilities simultaneously
    • Verify no crashes or conflicts
  • Test with different forwardJumpInterval/backwardJumpInterval values
  • Test with AirPods Pro (stem press) and AirPods (tap gesture)

🤖 Generated with Claude Code

…pport

Add a new `iosNextPreviousCommandsSkip` option to `updateOptions()` that
registers `nextTrackCommand` and `previousTrackCommand` on
`MPRemoteCommandCenter` with handlers that seek by the configured jump
intervals instead of skipping tracks.

This enables AirPods double-press (seek forward) and triple-press (seek
backward) without affecting the lock screen / Control Center UI, which
continues to show the skip-interval buttons when `JumpForward` and
`JumpBackward` are in the capabilities array.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@anojht anojht requested a review from dcvz as a code owner February 18, 2026 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant