2026 Audio & Video
WWDC26 · 13 min · Audio & Video
Meet the Now Playing framework
Get a first look at Now Playing — a Swift framework that connects your app’s media playback to system surfaces like the Lock Screen, Control Center, Dynamic Island, and CarPlay. Discover how to publish playback state and respond to commands using its observable API. Explore remote playback sessions, a new capability that lets your app represent media playing on external devices and bring full playback controls to those same system surfaces.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 6 snippets
Existing PlayerModel implementation
import Observation
final class PlayerModel {
let player: SoundPlayer
var sound: Sound { player.currentSound }
init(player: SoundPlayer) {
self.player = player
}
} Adopt MediaSessionRepresentable
import NowPlaying
extension PlayerModel: MediaSessionRepresentable {
var id: String { "ambient-sound-session" }
var content: (any MediaContentRepresentable)? {
return GenericContent(
id: sound.id,
title: sound.name,
subtitle: sound.description,
type: .audio,
duration: .live,
artwork: Artwork(id: sound.id) { size in
let data = try await self.artworkData(size: size)
return try ArtworkRepresentation(data: data)
}
)
}
var playbackSnapshot: MediaPlaybackSnapshot? {
MediaPlaybackSnapshot(
state: player.isPlaying ? .playing() : .paused
)
}
var commands: [MediaCommand] {[
.play { self.player.play() },
.pause { self.player.pause() },
.previous { self.player.previous() },
.next { self.player.next() }
]}
} MediaSession initialization
import NowPlaying
struct PlayerController {
let player: SoundPlayer
let model: PlayerModel
let session: MediaSession<PlayerModel>
init() {
self.player = SoundPlayer()
self.model = PlayerModel(player: player)
self.session = MediaSession(model)
}
} App extension entry point
import ExtensionFoundation
import NowPlaying
@main
final class SampleAppExtension: @MainActor RemoteMediaSessionExtension {
var configuration: some AppExtensionConfiguration {
RemoteMediaSessionExtensionConfiguration(extension: self)
}
var extensionPoint: AppExtensionPoint {
AppExtensionPoint.Identifier(host: "com.apple.nowplaying", name: "remote-media")
}
func session(_ state: RemotePlayerState) async throws -> RemotePlayerModel {
RemotePlayerModel(state: state)
}
} Existing RemotePlayerModel implementation
import Observation
final class RemotePlayerModel {
let client: ServerClient
var state: RemotePlayerState
init(state: RemotePlayerState) {
self.client = ServerClient(sessionID: state.sessionID)
self.state = state
}
} Adopt RemoteMediaSessionRepresentable in app extension
import NowPlaying
extension RemotePlayerModel: @MainActor RemoteMediaSessionRepresentable {
var id: String { state.sessionID }
var content: (any MediaContentRepresentable)? {
GenericContent(
id: state.sound.id,
title: state.sound.name,
subtitle: state.sound.description,
type: .audio,
duration: .live,
artwork: Artwork(id: state.sound.id) { size in
let data = try await self.artworkData(size: size)
return try ArtworkRepresentation(data: data)
}
)
}
var playbackSnapshot: MediaPlaybackSnapshot? {
MediaPlaybackSnapshot(
state: state.isPlaying ? .playing() : .paused
)
}
var commands: [MediaCommand] {[
.play { try await self.client.send(.play) },
.pause { try await self.client.send(.pause) },
.previous { try await self.client.send(.previous) },
.next { try await self.client.send(.next) }
]}
var devices: [MediaDevice] {
state.devices.map { device in
MediaDevice(
id: device.id,
name: device.name,
type: .speaker,
capabilities: [
.absoluteVolume(device.volume) { volume in
// send volume change to server
}
]
)
}
}
func update(_ state: RemotePlayerState) {
self.state = state
}
}