---
title: Publishing remote media sessions
framework: nowplaying
role: article
role_heading: Article
path: nowplaying/publishing-remote-media-sessions
---

# Publishing remote media sessions

Show media from an external device on the Lock Screen and Control Center.

## Overview

Overview A remote session represents media playback happening on an external device such as a speaker, streaming stick, or smart TV. When you publish a remote session, the framework makes the external device’s playback available for display on the Lock Screen and Control Center. Your app extension handles any commands the system routes back to the device. Remote sessions use an app extension architecture. An app extension conforming to RemoteMediaSessionExtension provides the session content and handles commands. Send push notifications from your server to start and update remote sessions, so the framework makes remote playback available for display even when your app isn’t running. In your main app, use RemoteMediaSession to start, fetch, and update sessions. Define session attributes Create a Codable type that captures the playback state of the external device. Your app and extension share this type to communicate session state. Conform to RemoteMediaSessionAttributes and include only the fields your app needs to represent the external device’s playback. struct MySessionAttributes: RemoteMediaSessionAttributes {     var id: String     var isPlaying: Bool     var elapsedTime: TimeInterval     var timestamp: Date     var currentTrack: TrackInfo?     var devices: [DeviceInfo]     var queueCount: Int     var currentTrackIndex: Int } Declare the extension Add an app extension target to your project. In the extension’s Info pane in Xcode, add an EXAppExtensionAttributes dictionary with an EXExtensionPointIdentifier of com.apple.nowplaying.remote-media. Xcode writes the following entries to the extension’s Info.plist file: <key>EXAppExtensionAttributes</key> <dict>     <key>EXExtensionPointIdentifier</key>     <string>com.apple.nowplaying.remote-media</string> </dict> Create the app extension Define a type that conforms to RemoteMediaSessionExtension and annotate it with @main. The system calls session(_:) when it needs your extension to produce a session for a given set of attributes. From the configuration property, return a RemoteMediaSessionExtensionConfiguration initialized with your extension instance. import ExtensionFoundation import NowPlaying

@main struct MyRemotePlaybackExtension: RemoteMediaSessionExtension {     var configuration: RemoteMediaSessionExtensionConfiguration<Self> {         RemoteMediaSessionExtensionConfiguration(extension: self)     }

func session(_ attributes: MySessionAttributes) async throws -> MyRemoteSession {         MyRemoteSession(attributes: attributes)     } } The framework caches the sessions your extension returns and routes subsequent updates and commands to the same instance. If your extension connects to a backend or device, establish that connection once and reuse it across session(_:) calls. Implement the remote session representable Create a type that conforms to RemoteMediaSessionRepresentable to provide content, playback state, commands, and device information. Mark it with Observable to detect changes automatically. import NowPlaying

@Observable final class MyRemoteSession: @MainActor RemoteMediaSessionRepresentable {     let id: String     var attributes: MySessionAttributes     let apiClient: MyAPIClient

init(attributes: MySessionAttributes) {         self.id = attributes.id         self.attributes = attributes         self.apiClient = MyAPIClient()     }

func update(_ attributes: MySessionAttributes) {         self.attributes = attributes     }

var playbackSnapshot: MediaPlaybackSnapshot? {         MediaPlaybackSnapshot(             state: attributes.isPlaying ? .playing() : .paused,             elapsedTime: attributes.elapsedTime,             timestamp: attributes.timestamp         )     } } The system delivers incoming attribute changes to update(_:). Save them to your stored attributes so the Now Playing interface reflects the external device’s current state. Provide content and commands Implement content and commands on your type to describe what’s playing and which commands the remote session supports. Forward each command to the external device. extension MyRemoteSession {     var content: (any MediaContentRepresentable)? {         guard let track = attributes.currentTrack else { return nil }         var content = 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)             }         )         content.isExplicit = track.isExplicit         return content     }

var commands: [MediaCommand] {         [             .play { try await self.apiClient.sendCommand(.play) },             .pause { try await self.apiClient.sendCommand(.pause) },             .next { try await self.apiClient.sendCommand(.next) }                 .enabled(attributes.currentTrackIndex < attributes.queueCount - 1),             .previous { try await self.apiClient.sendCommand(.previous) }                 .enabled(attributes.currentTrackIndex > 0),             .seekToPosition { time in                 try await self.apiClient.sendCommand(.seek(to: time))             },         ]     } } Provide devices and control capabilities Return the devices playing as part of the session from the devices property. Each MediaDevice declares the operations the system can perform on it through an array of MediaDevice.Capability values. extension MyRemoteSession {     var devices: [MediaDevice] {         attributes.devices.map { device in             MediaDevice(                 id: device.id,                 name: device.name,                 type: .speaker,                 capabilities: [                     .absoluteVolume(device.volume) { newLevel in                         try await self.apiClient.setVolume(newLevel, forDevice: device.id)                     }                 ]             )         }     } } Use absoluteVolume(_:onChange:) when the device exposes a volume level in the range 0.0 to 1.0. The system calls the closure with the requested level when someone adjusts the volume slider. For devices that only support stepwise volume changes, use relativeVolume(onIncrement:onDecrement:) instead. Start and update sessions from your app In your main app, use RemoteMediaSession to start a session and send updates when the external device’s state changes: let remoteSession = try await RemoteMediaSession.start(attributes: attributes) When the external device’s state changes, update the session with new attributes. The id on the attributes must match the id used to start the session: try await remoteSession.update(updatedAttributes) To have the system show this session on the Lock Screen and Control Center, call requestToBecomeSystemPrimary(): try await remoteSession.requestToBecomeSystemPrimary() note: Your app must be in the foreground when you call requestToBecomeSystemPrimary(). If your app is in the background, the request has no effect. When playback ends, call end() to remove the session: try await remoteSession.end() Start and update sessions from push notifications Remote sessions support push notifications. Your server can start, update, or end sessions when your app isn’t running. Use this approach when playback starts from a remote device without someone opening your app. Observe the push-to-start token The push-to-start token lets your server start new remote sessions with a push notification. Observe pushToStartTokenUpdates to receive the token whenever it changes, and read pushToStartToken to get the current value at any time. Send the latest token to your server so it can send push notifications to the right device. // Observe token updates. Task {     for await token in RemoteMediaSession<MySessionAttributes>.pushToStartTokenUpdates {         let tokenString = token.map { String(format: "%02x", $0) }.joined()         try await apiClient.registerStartToken(tokenString)     } }

// Read the current token. if let token = RemoteMediaSession<MySessionAttributes>.pushToStartToken {     let tokenString = token.map { String(format: "%02x", $0) }.joined()     try await apiClient.registerStartToken(tokenString) } Observe session update tokens Each active session has its own push token for receiving updates and end events. Observe pushTokenUpdates from inside your extension’s session, and query the latest token using pushToken: extension MyRemoteSession {     func startObservingPushToken() {         if let token = pushToken {             let tokenString = token.map { String(format: "%02x", $0) }.joined()             Task { try? await apiClient.registerUpdateToken(tokenString, forSessionID: id) }         }

Task { [weak self] in             guard let self else { return }             for await token in pushTokenUpdates {                 let tokenString = token.map { String(format: "%02x", $0) }.joined()                 try? await apiClient.registerUpdateToken(tokenString, forSessionID: id)             }         }     } } Send push notifications from your server Your server sends APNs push notifications to start, update, or end remote sessions. Use the nowplaying push type for each push and target the topic <your-bundle-id>.push-type.nowplaying. Set the following headers on your APNs request: apns-topic: com.example.myapp.push-type.nowplaying apns-push-type: nowplaying Start a session To start a new remote session, send a push to the push-to-start token. The event field must be "start", and the attributes field must contain a JSON-encoded representation of your RemoteMediaSessionAttributes: {     "aps": {         "event": "start",         "timestamp": 1773907200,         "attributes": {             "id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",             "devices": [                 {                     "id": "living-room-speaker",                     "name": "Living Room Speaker",                     "type": "speaker",                     "volume": 0.65                 }             ],             "isPlaying": true,             "currentTrackIndex": 0,             "queueCount": 5,             "elapsedTime": 0,             "timestamp": "2026-03-16T12:00:00Z",             "currentTrack": {                 "id": "track-1",                 "title": "Song Title",                 "artist": "Name of Artist",                 "album": "Title of the Album",                 "duration": 237.0,                 "artworkURL": "https://example.com/artwork/track-1"             }         }     } } When the system receives this push, it wakes your app extension and calls session(_:) with the decoded attributes. Return a RemoteMediaSessionRepresentable object from this method. Update a session To update an existing session, like when someone skips to the next track on the remote device, send a push notification to the session’s update token with the event field set to "update": {     "aps": {         "event": "update",         "timestamp": 1773907444,         "attributes": {             "id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",             "devices": [                 {                     "id": "living-room-speaker",                     "name": "Living Room Speaker",                     "type": "speaker",                     "volume": 0.65                 }             ],             "isPlaying": true,             "currentTrackIndex": 1,             "queueCount": 5,             "elapsedTime": 0,             "timestamp": "2026-06-08T12:00:00Z",             "currentTrack": {                 "id": "track-2",                 "title": "Song Title",                 "artist": "Name of Artist",                 "album": "Title of the Album",                 "duration": 312.0,                 "artworkURL": "https://example.com/artwork/track-2"             }         }     } } The system delivers the updated attributes to your extension’s update(_:) method, and the Now Playing interface updates to reflect the new content. End a session To end a session when playback stops on the remote device, send a push to the session’s update token with the event field set to "end". Only the session id is required in the attributes: {     "aps": {         "event": "end",         "timestamp": 1773907847,         "attributes": {             "id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"         }     } }

## See Also

### Remote sessions

- [RemoteMediaSessionRepresentable](nowplaying/remotemediasessionrepresentable.md)
- [RemoteMediaSession](nowplaying/remotemediasession.md)
- [RemoteMediaSessionExtension](nowplaying/remotemediasessionextension.md)
- [RemoteMediaSessionExtensionConfiguration](nowplaying/remotemediasessionextensionconfiguration.md)
- [RemoteMediaSessionAttributes](nowplaying/remotemediasessionattributes.md)
- [RemoteMediaSessionError](nowplaying/remotemediasessionerror.md)
- [MediaDevice](nowplaying/mediadevice.md)
