Contents

Publishing media sessions

Show your app’s media on the Lock Screen and Control Center.

Overview

A local session publishes media that’s playing on the current device. When you create one, the system shows your content’s metadata and playback controls across the Lock Screen, Control Center, and connected accessories, including CarPlay and AirPlay-capable devices.

To publish a local session, create a type that conforms to MediaSessionRepresentable, then register it with the system using MediaSession. The framework observes your model with Observable and syncs updates to the system automatically.

Create a session representable

To publish your media to the system, conform to MediaSessionRepresentable and mark your type with the Observable macro. The framework reads four properties from your type: id to uniquely identify the session, content to describe what’s playing, playbackSnapshot to capture playback state, and commands to declare the controls your app supports.

import NowPlaying

@Observable
@MainActor
final class AudioPlayer: MediaSessionRepresentable {
    let id: String = UUID().uuidString

    var currentTrack: Track?
    var isPlaying: Bool = false
    var elapsedTime: TimeInterval = 0
    var timestamp: Date = .now

    var content: (any MediaContentRepresentable)? {
        guard let track = currentTrack else { return nil }
        return MusicContent(
            id: track.id,
            songTitle: track.title,
            artistName: track.artist,
            albumName: track.album,
            type: .audio,
            duration: .finite(track.duration),
            artwork: nil
        )
    }

    var playbackSnapshot: MediaPlaybackSnapshot? {
        MediaPlaybackSnapshot(
            state: isPlaying ? .playing() : .paused,
            elapsedTime: elapsedTime,
            timestamp: timestamp
        )
    }

    var commands: [MediaCommand] {
        [
            .play { self.play() },
            .pause { self.pause() },
        ]
    }

    func play() { /* ... */ }
    func pause() { /* ... */ }
}

The content property returns a content type that matches your media. The framework provides MusicContent, PodcastContent, MovieContent, TVShowContent, BookContent, RadioContent, HomeMediaContent, and GenericContent.

The playbackSnapshot property returns a MediaPlaybackSnapshot that captures the current playback state and progress. Provide the elapsed time and a timestamp so the system extrapolates the playback position between updates.

Add artwork

Provide artwork by adding an Artwork structure for your content. The framework requests artwork from your process asynchronously at the size the system requires.

var content: (any MediaContentRepresentable)? {
    guard let track = currentTrack else { return nil }
    return MusicContent(
        id: track.id,
        songTitle: track.title,
        artistName: track.artist,
        albumName: track.album,
        type: .audio,
        duration: .finite(track.duration),
        artwork: Artwork(id: track.artworkID) { size in
            let (data, _) = try await URLSession.shared.data(from: track.artworkURL)
            return try ArtworkRepresentation(data: data)
        }
    )
}

Manage the session life cycle

Create a MediaSession from your representable and call requestToBecomeApplicationPrimary() to begin publishing. When playback ends, set your session reference to nil to remove it from the system.

@Observable
@MainActor
final class AudioPlayer: MediaSessionRepresentable {
    var session: MediaSession<AudioPlayer>?

    func activate() async throws {
        let session = MediaSession(self)
        self.session = session
        try await session.requestToBecomeApplicationPrimary()
    }

    func deactivate() {
        session = nil
    }

    // ...
}

If your app plays audio (such as music, a podcast, or an audiobook), configure the audio session before calling requestToBecomeApplicationPrimary():

let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default)
try audioSession.setActive(true)

When you’re done with playback, call setActive(false) and pass notifyOthersOnDeactivation in the options so other apps can resume:

try audioSession.setActive(false, options: .notifyOthersOnDeactivation)

Coordinate multiple sessions

Apps can have more than one local session. For example, your app can be both a music player and a podcast player. Each session observes its own representable independently. Use requestToBecomeApplicationPrimary() to switch which session represents your app to the system.

To display a session on the Lock Screen and Control Center, call requestToBecomeSystemPrimary(). This makes the session both your app’s primary session and the system’s active session.

try await session.requestToBecomeSystemPrimary()

Observe isApplicationPrimary and isSystemPrimary to reflect the current state in your UI.

Respond to commands

Declare the commands your app supports in the commands property. Each entry is a MediaCommand created with a static factory method that takes an action closure. The system calls the closure when someone interacts with the corresponding control.

var commands: [MediaCommand] {
    [
        .play { self.play() },
        .pause { self.pause() },
        .next { self.next() }.enabled(hasNextTrack),
        .previous { self.previous() }.enabled(hasPreviousTrack),
        .seekToPosition { time in
            self.seek(to: time)
        },
        .feedback(status: currentFeedback) { newStatus in
            self.setFeedback(newStatus)
        },
    ]
}

Use enabled(_:) to conditionally make a command available or unavailable. For example, make next(_:) unavailable when there are no more tracks in the queue.

For the full set of commands you can offer, including seek, skip, playback rate, repeat, shuffle, and feedback, see Playback commands.

See Also

Local sessions