Skip to content

iOS Control Center Not Showing Audio Player When App is Closed #2555

@faraz-bukhari-n

Description

@faraz-bukhari-n

iOS Control Center Not Showing Audio Player When App is Closed

Description

When I play an audio track using react-native-track-player and the track is playing, if I close the app (swipe up to close), the audio continues playing in the background (which is expected), but the audio player controls do not appear in iOS Control Center. The audio is playing but there's no way to control it from Control Center.

Environment

  • React Native: 0.81.4
  • react-native-track-player: 4.1.2
  • Expo: 54.0.23 (bare build)
  • iOS: Tested on iOS device
  • Platform: iOS

Expected Behavior

When the app is closed/swiped away while audio is playing, the audio player should appear in iOS Control Center with play/pause controls and track metadata.

Actual Behavior

Audio continues playing in the background, but no player controls appear in iOS Control Center. The audio is playing but there's no visual indication or control available.

Steps to Reproduce

  1. Initialize TrackPlayer with audio session configuration
  2. Add a track to the queue and start playback
  3. While audio is playing, swipe up to close the app (fully close it, not just background)
  4. Audio continues playing, but Control Center does not show the player

Code Sample

Track Player Setup (services/audioPlayer.ts)

import TrackPlayer, {
  Capability,
  State,
  Track,
  Event,
  useTrackPlayerEvents,
  IOSCategory,
  IOSCategoryMode,
  IOSCategoryOptions,
} from 'react-native-track-player';
import { MUSIC_PLAYER_METADATA } from '@/utils/constants';

// Initialize the track player with iOS-specific capabilities
export async function setupAudioPlayer() {
  try {
    await TrackPlayer.setupPlayer({
      minBuffer: 10,
      // iOS audio session category - 'playback' is required for Control Center
      iosCategory: IOSCategory.Playback,
      // Default mode for music playback
      iosCategoryMode: IOSCategoryMode.Default,
      // Allow AirPlay
      iosCategoryOptions: [IOSCategoryOptions.AllowAirPlay],
      // Automatically update Now Playing metadata (critical for Control Center)
      autoUpdateMetadata: true,
    });

    console.log('✅ Audio player initialized successfully');
  } catch (error: any) {
    const errorMessage = error?.message || '';
    const isAlreadySetup =
      errorMessage.includes('already') ||
      errorMessage.includes('initialized') ||
      errorMessage.includes('setup');

    if (isAlreadySetup) {
      console.log('ℹ️ Audio player already initialized, skipping setup');
    } else {
      console.error('❌ Error setting up audio player:', error);
      throw error;
    }
  }

  // Configure capabilities for iOS lock screen and control center
  try {
    await TrackPlayer.updateOptions({
      capabilities: [
        Capability.Play,
        Capability.Pause,
        Capability.Stop,
        Capability.SkipToNext,
        Capability.SkipToPrevious,
        Capability.SeekTo,
      ],
      compactCapabilities: [
        Capability.Play,
        Capability.Pause,
        Capability.SkipToNext,
        Capability.SkipToPrevious,
      ],
      notificationCapabilities: [
        Capability.Play,
        Capability.Pause,
        Capability.Stop,
        Capability.SkipToNext,
        Capability.SkipToPrevious,
      ],
      progressUpdateEventInterval: 1,
    });
  } catch (error) {
    console.warn('⚠️ Could not update player options:', error);
  }
}

// Add a track to the queue
export async function addTrackToPlaylist(
  url: string,
  title?: string,
  artist?: string,
  artwork?: string
) {
  try {
    const track: Track = {
      url,
      title: title || 'Audio Track',
      artist: artist || 'XYZ',
      album: 'XYZ Music',
      id: `${Date.now()}-${Math.random()}`,
      artwork: artwork || undefined,
      duration: 0,
    };

    const queue = await TrackPlayer.getQueue();
    const exists = queue.some((t) => t.url === url);
    if (exists) {
      console.log('Track already in queue, skipping:', url);
      return;
    }

    await TrackPlayer.add(track);
    console.log('✅ Track added to playlist:', url);

    const state = (await TrackPlayer.getPlaybackState()).state;
    if (
      state === State.None ||
      state === State.Ready ||
      state === State.Stopped
    ) {
      // Update metadata BEFORE playing
      try {
        await TrackPlayer.updateNowPlayingMetadata({
          ...MUSIC_PLAYER_METADATA,
        });
        console.log('✅ Now Playing metadata set before playback');
      } catch (error) {
        console.warn('⚠️ Could not update Now Playing metadata:', error);
      }

      await TrackPlayer.play();
      console.log('✅ Started playing track');

      // Wait for track to start
      await new Promise((resolve) => setTimeout(resolve, 1000));

      // Update metadata again after playback starts
      try {
        const activeTrack = await TrackPlayer.getActiveTrack();
        if (activeTrack) {
          await TrackPlayer.updateNowPlayingMetadata({
            ...MUSIC_PLAYER_METADATA,
          });
          console.log('✅ Now Playing metadata updated after playback start');
        }
      } catch (error) {
        console.warn('⚠️ Could not update Now Playing metadata:', error);
      }
    }

    return track;
  } catch (error) {
    console.error('❌ Error adding track to playlist:', error);
    throw error;
  }
}

Track Player Service (services/trackPlayerService.js)

import { MUSIC_PLAYER_METADATA } from '@/utils/constants';
import TrackPlayer, { Event } from 'react-native-track-player';

module.exports = async function trackPlayerService() {
  // Handle remote control events from Control Center and Lock Screen
  TrackPlayer.addEventListener(Event.RemotePlay, async () => {
    console.log('Remote play event received');
    await TrackPlayer.play();
  });

  TrackPlayer.addEventListener(Event.RemotePause, async () => {
    console.log('Remote pause event received');
    await TrackPlayer.pause();
  });

  TrackPlayer.addEventListener(Event.RemoteStop, async () => {
    console.log('Remote stop event received');
    await TrackPlayer.stop();
  });

  TrackPlayer.addEventListener(Event.RemoteNext, async () => {
    console.log('Remote next event received');
    await TrackPlayer.skipToNext();
  });

  TrackPlayer.addEventListener(Event.RemotePrevious, async () => {
    console.log('Remote previous event received');
    await TrackPlayer.skipToPrevious();
  });

  TrackPlayer.addEventListener(Event.RemoteSeek, async ({ position }) => {
    console.log('Remote seek event received:', position);
    await TrackPlayer.seekTo(position);
  });

  // Handle when track changes - ensure metadata is visible in Control Center
  TrackPlayer.addEventListener(
    Event.PlaybackActiveTrackChanged,
    async ({ nextTrack }) => {
      if (nextTrack) {
        console.log('Track changed - Now Playing:', {
          title: nextTrack.title,
          artist: nextTrack.artist,
          url: nextTrack.url,
        });

        await new Promise((resolve) => setTimeout(resolve, 300));

        try {
          await TrackPlayer.updateNowPlayingMetadata({
            ...MUSIC_PLAYER_METADATA,
          });
          console.log('✅ Now Playing metadata updated in Control Center');
        } catch (error) {
          console.warn('⚠️ Could not update Now Playing metadata:', error);
        }
      }
    }
  );
};

Service Registration (index.js)

import TrackPlayer from 'react-native-track-player';

// Register the track player service
TrackPlayer.registerPlaybackService(() =>
  require('./services/trackPlayerService')
);

// Export the app entry point
export { default } from 'expo-router/entry';

Constants (utils/constants.ts)

export const MUSIC_PLAYER_METADATA = {
  title: 'ABC',
  artist: 'XYZ',
  album: 'XYZ Album',
  artwork: require('@/assets/images/splash-icon.png'),
};

Xcode Settings

Info.plist Configuration

The UIBackgroundModes key is set in Info.plist:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
  <string>remote-notification</string>
  <string>fetch</string>
  <string>processing</string>
</array>

app.json Configuration

{
  "expo": {
    "ios": {
      "infoPlist": {
        "UIBackgroundModes": [
          "audio",
          "remote-notification",
          "fetch",
          "processing"
        ]
      }
    }
  }
}

Additional Notes

  • I'm using React Native inside Expo with a bare build (not managed workflow)
  • The audio session category is set to IOSCategory.Playback
  • Background audio mode is enabled in Info.plist
  • Remote control event handlers are properly set up
  • Metadata is being updated multiple times to ensure it's set
  • The issue occurs specifically when the app is fully closed (not just backgrounded)

What I've Tried

  1. Setting iosCategory to IOSCategory.Playback
  2. Setting autoUpdateMetadata: true in setupPlayer
  3. Manually calling updateNowPlayingMetadata at various points
  4. Ensuring UIBackgroundModes includes "audio" in Info.plist
  5. Setting up remote control event handlers
  6. Updating metadata when app goes to background

The audio continues playing correctly, but the Control Center integration is not working when the app is fully closed.

Questions

  1. Is there an additional configuration needed for Control Center to appear when the app is fully closed?
  2. Should the audio session be configured differently?
  3. Are there any known issues with Control Center integration in Expo bare builds?
  4. Is there a timing issue with metadata updates that needs to be addressed?

Thank you for your help!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions