Token导航 LogoToken导航TokenDH.com
开发需要联网github未标认证来源可访问clear审计通过

axiom-now-playing公理正在播放

Agent Skill

axiom-now-playing 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要围绕仓库状态、代码变更或协作事项进行整理时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

4,143

周安装

166

GitHub Stars

868

下载量

1,341
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

3

许可证

MIT

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:axiom-now-playing(公理正在播放)
来源仓库:https://github.com/charleswiltgen/axiom
仓库路径:skills/axiom-now-playing
安装命令:
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-now-playing
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-now-playing

简介

解决 iOS 18+ 下 Now Playing 信息显示异常、远程控制失效、封面图错误或状态不同步四大问题。

  • 基于 AVAudioSession 激活、远程命令处理器与元数据发布三要素协同工作机制设计。
  • 适用于音频播放器、播客应用或支持锁屏/控制中心交互的音乐类 App 开发。
  • 安装前需确认项目使用 Swift 6.0+ 与 Xcode 16+,并检查音频会话配置顺序。
  • 建议先启用 Metal Validation 等调试手段,再按清单逐项核对关键组件完整性。

SKILL.md

Now Playing Integration Guide

Purpose: Prevent the 4 most common Now Playing issues on iOS 18+: info not appearing, commands not working, artwork problems, and state sync issues

Swift Version: Swift 6.0+ iOS Version: iOS 18+ Xcode: Xcode 16+

Core Philosophy

"Now Playing eligibility requires THREE things working together: AVAudioSession activation, remote command handlers, and metadata publishing. Missing ANY of these silently breaks the entire system. 90% of Now Playing issues stem from incorrect activation order or missing command handlers, not API bugs."

Key Insight from WWDC 2022/110338: Apps must meet two system heuristics:

  1. Register handlers for at least one remote command
  2. Configure AVAudioSession with a non-mixable category

When to Use This Skill

Use this skill when:

  • Now Playing info doesn't appear on Lock Screen or Control Center
  • Play/pause/skip buttons are grayed out or don't respond
  • Album artwork is missing, wrong, or flickers between images
  • Control Center shows "Playing" when app is paused, or vice versa
  • Apple Music or other apps "steal" Now Playing status
  • Implementing Now Playing for the first time
  • Debugging Now Playing issues in existing implementation
  • Integrating CarPlay Now Playing (covered in Pattern 6)
  • Working with MusicKit/Apple Music content (covered in Pattern 7)

iOS 26 Note

iOS 26 introduces Liquid Glass visual design for Lock Screen and Control Center Now Playing widgets. This is automatic system behavior — no code changes required. The patterns in this skill remain valid for iOS 26.

Do NOT use this skill for:

  • Background audio configuration details (see AVFoundation skill)

Related Skills

  • swift-concurrency - For @MainActor patterns, weak self in closures, async artwork loading
  • memory-debugging - For retain cycles in command handlers
  • avfoundation-ref - For AVAudioSession configuration details

Red Flags / Anti-Patterns

If you see ANY of these, suspect Now Playing misconfiguration:

  • Info appears briefly then disappears (AVAudioSession deactivated)
  • Commands work in simulator but not on device (simulator has different audio stack)
  • Artwork shows placeholder then updates (race condition, not necessarily wrong)
  • Artwork never appears (format/size issue or MPMediaItemArtwork block returning nil)
  • Play/pause state incorrect after backgrounding (not updating on playback rate changes)
  • Another app "steals" Now Playing (didn't meet eligibility requirements)
  • playbackState property doesn't update (iOS doesn't have playbackState, macOS only!)

FORBIDDEN Assumptions:

  • "Just set nowPlayingInfo and it works" - Must have AVAudioSession + command handlers
  • "playbackState controls Control Center" - iOS ignores playbackState, uses playbackRate
  • "Artwork just needs an image" - Needs proper MPMediaItemArtwork with size handler
  • "Commands enable themselves" - Must add target AND set isEnabled = true
  • "Update elapsed time every second" - System infers from rate, causes jitter

Mandatory First Steps (Pre-Diagnosis)

Run this code to understand current state before debugging:

// 1. Verify AVAudioSession configuration
let session = AVAudioSession.sharedInstance()
print("Category: \(session.category.rawValue)")
print("Mode: \(session.mode.rawValue)")
print("Options: \(session.categoryOptions)")
print("Is active: \(try? session.setActive(true))")
// Must be: .playback category, NOT .mixWithOthers option

// 2. Verify background mode
// Info.plist must have: UIBackgroundModes = ["audio"]

// 3. Check command handlers are registered
let commandCenter = MPRemoteCommandCenter.shared()
print("Play enabled: \(commandCenter.playCommand.isEnabled)")
print("Pause enabled: \(commandCenter.pauseCommand.isEnabled)")
// Must have at least one command with target AND isEnabled = true

// 4. Check nowPlayingInfo dictionary
if let info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
    print("Title: \(info[MPMediaItemPropertyTitle] ?? "nil")")
    print("Artwork: \(info[MPMediaItemPropertyArtwork] != nil)")
    print("Duration: \(info[MPMediaItemPropertyPlaybackDuration] ?? "nil")")
    print("Elapsed: \(info[MPNowPlayingInfoPropertyElapsedPlaybackTime] ?? "nil")")
    print("Rate: \(info[MPNowPlayingInfoPropertyPlaybackRate] ?? "nil")")
} else {
    print("No nowPlayingInfo set!")
}

What this tells you:

ObservationDiagnosisPattern
Category is.ambient or has.mixWithOthersWon't become Now Playing appPattern 1
No commands have targetsSystem ignores appPattern 2
Commands have targets but isEnabled = falseUI grayed outPattern 2
Artwork is nilMPMediaItemArtwork block returning nilPattern 3
playbackRate is 0.0 when playingControl Center shows pausedPattern 4
Background mode "audio" not in Info.plistInfo disappears on lockPattern 1

Decision Tree

Now Playing not working?
├─ Info never appears at all?
│  ├─ AVAudioSession category .ambient or .mixWithOthers?
│  │  └─ Pattern 1a (Wrong Category)
│  ├─ No remote command handlers registered?
│  │  └─ Pattern 2a (Missing Handlers)
│  ├─ Background mode "audio" not in Info.plist?
│  │  └─ Pattern 1b (Background Mode)
│  └─ AVAudioSession.setActive(true) never called?
│     └─ Pattern 1c (Not Activated)
│
├─ Info appears briefly, then disappears?
│  ├─ On lock screen specifically?
│  │  ├─ AVAudioSession deactivated too early?
│  │  │  └─ Pattern 1d (Early Deactivation)
│  │  └─ App suspended (no background mode)?
│  │     └─ Pattern 1b (Background Mode)
│  └─ When switching apps?
│     └─ Another app claiming Now Playing → Pattern 5
│
├─ Commands not responding?
│  ├─ Buttons grayed out (disabled)?
│  │  └─ command.isEnabled = false → Pattern 2b
│  ├─ Buttons visible but no response?
│  │  ├─ Handler not returning .success?
│  │  │  └─ Pattern 2c (Handler Return)
│  │  └─ Using wrong command center (session vs shared)?
│  │     └─ Pattern 2d (Command Center)
│  └─ Skip forward/backward not showing?
│     └─ preferredIntervals not set → Pattern 2e
│
├─ Artwork problems?
│  ├─ Never appears?
│  │  ├─ MPMediaItemArtwork block returning nil?
│  │  │  └─ Pattern 3a (Artwork Block)
│  │  └─ Image format/size invalid?
│  │     └─ Pattern 3b (Image Format)
│  ├─ Wrong artwork showing?
│  │  └─ Race condition between sources → Pattern 3c
│  └─ Artwork flickering?
│     └─ Multiple updates in rapid succession → Pattern 3d
│
├─ State sync issues?
│  ├─ Shows "Playing" when paused?
│  │  └─ playbackRate not updated → Pattern 4a
│  ├─ Progress bar stuck or jumping?
│  │  └─ elapsedTime not updated at right moments → Pattern 4b
│  └─ Duration wrong?
│     └─ Not setting playbackDuration → Pattern 4c
│
├─ CarPlay specific issues?
│  ├─ App doesn't appear in CarPlay at all?
│  │  └─ Missing entitlement → Pattern 6 (Add com.apple.developer.carplay-audio)
│  ├─ Now Playing blank in CarPlay but works on iOS?
│  │  └─ Same root cause as iOS → Check Patterns 1-4
│  ├─ Custom buttons don't appear in CarPlay?
│  │  └─ Wrong configuration timing → Pattern 6 (Configure at templateApplicationScene)
│  └─ Works on device but not CarPlay simulator?
│     └─ Debugger interference → Pattern 6 (Run without debugger)
│
└─ Using MusicKit (ApplicationMusicPlayer)?
   ├─ Now Playing shows wrong info?
   │  └─ Overwriting automatic data → Pattern 7 (Don't set nowPlayingInfo manually)
   └─ Mixing MusicKit + own content?
      └─ Hybrid approach needed → Pattern 7 (Switch between players)

Pattern 1: AVAudioSession Configuration (Info Not Appearing)

Time cost: 10-15 minutes

Symptom

  • Now Playing info never appears on Lock Screen
  • Info appears briefly then disappears on lock
  • Works in foreground, disappears in background

BAD Code

// ❌ WRONG — Category allows mixing, won't become Now Playing app
class PlayerService {
    func setupAudioSession() throws {
        try AVAudioSession.sharedInstance().setCategory(
            .playback,
            options: .mixWithOthers  // ❌ Mixable = not eligible for Now Playing
        )
        // Never called setActive()  // ❌ Session not activated
    }

    func play() {
        player.play()
        updateNowPlaying()  // ❌ Won't appear - session not active
    }
}

GOOD Code

// ✅ CORRECT — Non-mixable category, activated before playback
class PlayerService {
    func setupAudioSession() throws {
        try AVAudioSession.sharedInstance().setCategory(
            .playback,
            mode: .default,
            options: []  // ✅ No .mixWithOthers = eligible for Now Playing
        )
    }

    func play() async throws {
        // ✅ Activate BEFORE starting playback
        try AVAudioSession.sharedInstance().setActive(true)

        player.play()
        updateNowPlaying()  // ✅ Now appears correctly
    }

    func stop() async throws {
        player.pause()

        // ✅ Deactivate AFTER stopping, with notify option
        try AVAudioSession.sharedInstance().setActive(
            false,
            options: .notifyOthersOnDeactivation
        )
    }
}

Info.plist Requirement

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

Verification

  • Lock screen shows Now Playing controls
  • Info persists when app backgrounded
  • Survives app switch (unless another app plays)

Pattern 2: Remote Command Registration (Commands Not Working)

Time cost: 15-20 minutes

Symptom

  • Play/pause buttons grayed out
  • Buttons visible but tapping does nothing
  • Skip buttons don't appear
  • Commands work once then stop

BAD Code

// ❌ WRONG — Missing targets and isEnabled
class PlayerService {
    func setupCommands() {
        let commandCenter = MPRemoteCommandCenter.shared()

        // ❌ Added target but forgot isEnabled
        commandCenter.playCommand.addTarget { _ in
            self.player.play()
            return .success
        }
        // playCommand.isEnabled defaults to false!

        // ❌ Never added pause handler

        // ❌ skipForward without preferredIntervals
        commandCenter.skipForwardCommand.addTarget { _ in
            return .success
        }
    }
}

GOOD Code

// ✅ CORRECT — Targets registered, enabled, with proper configuration
@MainActor
class PlayerService {
    private var commandTargets: [Any] = []  // Keep strong references

    func setupCommands() {
        let commandCenter = MPRemoteCommandCenter.shared()

        // ✅ Play command - add target AND enable
        let playTarget = commandCenter.playCommand.addTarget { [weak self] _ in
            self?.player.play()
            self?.updateNowPlayingPlaybackState(isPlaying: true)
            return .success
        }
        commandCenter.playCommand.isEnabled = true
        commandTargets.append(playTarget)

        // ✅ Pause command
        let pauseTarget = commandCenter.pauseCommand.addTarget { [weak self] _ in
            self?.player.pause()
            self?.updateNowPlayingPlaybackState(isPlaying: false)
            return .success
        }
        commandCenter.pauseCommand.isEnabled = true
        commandTargets.append(pauseTarget)

        // ✅ Skip forward - set preferredIntervals BEFORE adding target
        commandCenter.skipForwardCommand.preferredIntervals = [15.0]
        let skipForwardTarget = commandCenter.skipForwardCommand.addTarget { [weak self] event in
            guard let skipEvent = event as? MPSkipIntervalCommandEvent else {
                return .commandFailed
            }
            self?.skip(by: skipEvent.interval)
            return .success
        }
        commandCenter.skipForwardCommand.isEnabled = true
        commandTargets.append(skipForwardTarget)

        // ✅ Skip backward
        commandCenter.skipBackwardCommand.preferredIntervals = [15.0]
        let skipBackwardTarget = commandCenter.skipBackwardCommand.addTarget { [weak self] event in
            guard let skipEvent = event as? MPSkipIntervalCommandEvent else {
                return .commandFailed
            }
            self?.skip(by: -skipEvent.interval)
            return .success
        }
        commandCenter.skipBackwardCommand.isEnabled = true
        commandTargets.append(skipBackwardTarget)
    }

    func teardownCommands() {
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.playCommand.removeTarget(nil)
        commandCenter.pauseCommand.removeTarget(nil)
        commandCenter.skipForwardCommand.removeTarget(nil)
        commandCenter.skipBackwardCommand.removeTarget(nil)
        commandTargets.removeAll()
    }

    deinit {
        teardownCommands()
    }
}

Verification

  • Buttons not grayed out in Control Center
  • Tapping play/pause actually plays/pauses
  • Skip buttons show with correct interval (15s)

Pattern 3: Artwork Configuration (Artwork Problems)

Time cost: 15-25 minutes

Symptom

  • Artwork never appears (generic placeholder)
  • Wrong artwork for current track
  • Artwork flickers between images
  • Artwork appears then disappears

BAD Code

// ❌ WRONG — MPMediaItemArtwork block can return nil, no size handling
func updateNowPlaying() {
    var nowPlayingInfo = [String: Any]()
    nowPlayingInfo[MPMediaItemPropertyTitle] = track.title

    // ❌ Storing UIImage directly (doesn't work)
    nowPlayingInfo[MPMediaItemPropertyArtwork] = image

    // ❌ Or: Block that ignores requested size
    let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in
        return self.cachedImage  // ❌ May be nil, ignores requested size
    }

    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}

// ❌ WRONG — Multiple rapid updates cause flickering
func loadArtwork(from url: URL) {
    // Request 1
    loadImage(url) { image in
        self.updateNowPlayingArtwork(image)  // Update 1
    }
    // Request 2 (cached) returns faster
    loadCachedImage(url) { image in
        self.updateNowPlayingArtwork(image)  // Update 2 - flicker!
    }
}

GOOD Code

// ✅ CORRECT — Proper MPMediaItemArtwork with value capture (Swift 6 compliant)
@MainActor
class NowPlayingService {
    private var currentArtworkURL: URL?

    func updateNowPlayingArtwork(_ image: UIImage, for trackURL: URL) {
        // ✅ Prevent race conditions - only update if still current track
        guard trackURL == currentArtworkURL else { return }

        // ✅ Create MPMediaItemArtwork with VALUE CAPTURE (not stored property)
        // This is Swift 6 strict concurrency compliant — UIImage is immutable
        // and safe to capture across isolation domains
        let artwork = MPMediaItemArtwork(boundsSize: image.size) { [image] requestedSize in
            // ✅ System calls this block from any thread
            // Captured value avoids "Main actor-isolated property" error
            return image
        }

        // ✅ Update only artwork key, preserve other values
        var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
        nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

    // ✅ Single entry point with priority: embedded > cached > remote
    func loadArtwork(for track: Track) async {
        currentArtworkURL = track.artworkURL

        // Priority 1: Embedded in file (immediate, no flicker)
        if let embedded = await extractEmbeddedArtwork(track.fileURL) {
            updateNowPlayingArtwork(embedded, for: track.artworkURL)
            return
        }

        // Priority 2: Already cached (fast)
        if let cached = await loadFromCache(track.artworkURL) {
            updateNowPlayingArtwork(cached, for: track.artworkURL)
            return
        }

        // Priority 3: Remote (slow, but don't flicker)
        // ✅ Set placeholder first, then update once with real image
        if let remote = await downloadImage(track.artworkURL) {
            updateNowPlayingArtwork(remote, for: track.artworkURL)
        }
    }
}

Why value capture, not nonisolated(unsafe): The closure passed to MPMediaItemArtwork may be called by the system from any thread. Under Swift 6 strict concurrency, accessing @MainActor-isolated stored properties from this closure would cause a compile error. Capturing the image value directly is cleaner than using nonisolated(unsafe) because UIImage is immutable and thread-safe for reads.

Artwork Size Guidelines

  • Lock Screen: 300x300 points (600x600 @2x, 900x900 @3x)
  • Control Center: Various sizes
  • Best practice: Provide image at least 600x600 pixels

Verification

  • Artwork appears on Lock Screen
  • Correct artwork for current track
  • No flickering when track changes
  • Artwork persists after backgrounding

Pattern 4: Playback State Synchronization (State Sync Issues)

Time cost: 10-20 minutes

Symptom

  • Control Center shows "Playing" when actually paused
  • Progress bar doesn't move or jumps unexpectedly
  • Duration shows wrong value
  • Scrubbing doesn't work correctly

BAD Code

// ❌ WRONG — Using playbackState (macOS only, ignored on iOS)
func updatePlaybackState(isPlaying: Bool) {
    MPNowPlayingInfoCenter.default().playbackState = isPlaying ? .playing : .paused
    // ❌ iOS ignores this property! Only macOS uses it.
}

// ❌ WRONG — Updating elapsed time on a timer (causes drift)
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
    var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
    info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.player.currentTime().seconds
    MPNowPlayingInfoCenter.default().nowPlayingInfo = info
    // ❌ Every second creates jitter, system already infers from timestamp
}

// ❌ WRONG — Partial dictionary updates cause race conditions
func updateTitle() {
    var info = [String: Any]()
    info[MPMediaItemPropertyTitle] = track.title
    MPNowPlayingInfoCenter.default().nowPlayingInfo = info
    // ❌ Cleared all other values (artwork, duration, etc.)!
}

GOOD Code

// ✅ CORRECT — Use playbackRate for iOS, update at key moments only
@MainActor
class NowPlayingService {

    // ✅ Update when playback STARTS
    func playbackStarted(track: Track, player: AVPlayer) {
        var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]

        // ✅ Core metadata
        nowPlayingInfo[MPMediaItemPropertyTitle] = track.title
        nowPlayingInfo[MPMediaItemPropertyArtist] = track.artist
        nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = track.album
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player.currentItem?.duration.seconds ?? 0

        // ✅ Playback state via RATE (not playbackState property)
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime().seconds
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0  // Playing

        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

    // ✅ Update when playback PAUSES
    func playbackPaused(player: AVPlayer) {
        var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]

        // ✅ Update elapsed time AND rate together
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime().seconds
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0  // Paused

        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

    // ✅ Update when user SEEKS
    func userSeeked(to time: CMTime, player: AVPlayer) {
        var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]

        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time.seconds
        // ✅ Keep current rate (don't change playing/paused state)

        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

    // ✅ Update when track CHANGES
    func trackChanged(to newTrack: Track, player: AVPlayer) {
        // ✅ Full refresh of all metadata
        var nowPlayingInfo = [String: Any]()

        nowPlayingInfo[MPMediaItemPropertyTitle] = newTrack.title
        nowPlayingInfo[MPMediaItemPropertyArtist] = newTrack.artist
        nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = newTrack.album
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player.currentItem?.duration.seconds ?? 0
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = 0.0
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate

        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo

        // Then load artwork asynchronously
        Task {
            await loadArtwork(for: newTrack)
        }
    }
}

When to Update Now Playing Info

EventWhat to Update
Playback startsAll metadata + elapsed=current + rate=1.0
Playback pauseselapsed=current + rate=0.0
User seekselapsed=newPosition (keep rate)
Track changesAll metadata (new track)
Playback rate changes (2x, 0.5x)rate=newRate

DO NOT Update

  • On a timer (system infers from elapsed + rate + timestamp)
  • Elapsed time continuously (causes jitter)
  • Partial dictionaries (loses other values)

Pattern 5: MPNowPlayingSession (iOS 16+ Recommended Approach)

Time cost: 20-30 minutes

When to Use MPNowPlayingSession

  • iOS 16+ (available since iOS 16, previously tvOS only)
  • Using AVPlayer for playback
  • Want automatic publishing of playback state
  • Multiple players (Picture-in-Picture scenarios)

BAD Code (Manual Approach - More Error-Prone)

// ❌ Manual updates are error-prone, easy to miss state changes
class OldStylePlayer {
    func play() {
        player.play()
        // Must remember to:
        updateNowPlayingElapsed()
        updateNowPlayingRate()
        // Easy to forget one...
    }
}

GOOD Code (MPNowPlayingSession)

// ✅ CORRECT — MPNowPlayingSession handles automatic publishing
@MainActor
class ModernPlayerService {
    private var player: AVPlayer
    private var session: MPNowPlayingSession?

    init() {
        player = AVPlayer()
        setupSession()
    }

    func setupSession() {
        // ✅ Create session with player
        session = MPNowPlayingSession(players: [player])

        // ✅ Enable automatic publishing of:
        // - Duration
        // - Elapsed time
        // - Playback state (rate)
        // - Playback progress
        session?.automaticallyPublishNowPlayingInfo = true

        // ✅ Register commands on SESSION's command center (not shared)
        session?.remoteCommandCenter.playCommand.addTarget { [weak self] _ in
            self?.player.play()
            return .success
        }
        session?.remoteCommandCenter.playCommand.isEnabled = true

        session?.remoteCommandCenter.pauseCommand.addTarget { [weak self] _ in
            self?.player.pause()
            return .success
        }
        session?.remoteCommandCenter.pauseCommand.isEnabled = true

        // ✅ Try to become active Now Playing session
        session?.becomeActiveIfPossible { success in
            print("Became active Now Playing: \(success)")
        }
    }

    func play(track: Track) async {
        let item = AVPlayerItem(url: track.url)

        // ✅ Set static metadata on player item (title, artwork)
        item.nowPlayingInfo = [
            MPMediaItemPropertyTitle: track.title,
            MPMediaItemPropertyArtist: track.artist,
            MPMediaItemPropertyArtwork: await createArtwork(for: track)
        ]

        player.replaceCurrentItem(with: item)
        player.play()
        // ✅ No need to manually update elapsed time, rate, duration
        // MPNowPlayingSession publishes automatically!
    }
}

Multiple Sessions (Picture-in-Picture)

class MultiPlayerService {
    var mainSession: MPNowPlayingSession
    var pipSession: MPNowPlayingSession

    func pipDidExpand() {
        // ✅ Promote PiP session when it expands to full screen
        pipSession.becomeActiveIfPossible { success in
            // PiP now controls Lock Screen, Control Center
        }
    }

    func pipDidMinimize() {
        // ✅ Demote back to main session
        mainSession.becomeActiveIfPossible { success in
            // Main player now controls Lock Screen, Control Center
        }
    }
}

Critical Gotcha

When using MPNowPlayingSession: Use session.remoteCommandCenter, NOT MPRemoteCommandCenter.shared()

// ❌ WRONG
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { _ in }

// ✅ CORRECT
session.remoteCommandCenter.playCommand.addTarget { _ in }

Pattern 6: CarPlay Integration

For CarPlay-specific integration patterns, invoke /skill axiom-now-playing-carplay.

Key insight: CarPlay uses the SAME MPNowPlayingInfoCenter and MPRemoteCommandCenter as iOS. If your Now Playing works on iOS, it works in CarPlay with zero additional code.


Pattern 7: MusicKit Integration (Apple Music)

For MusicKit-specific integration patterns and hybrid app examples, invoke /skill axiom-now-playing-musickit.

Key insight: MusicKit's ApplicationMusicPlayer automatically publishes to MPNowPlayingInfoCenter. You don't need to manually update Now Playing info when playing Apple Music content.


Pressure Scenarios

Scenario 1: Apple Music Keeps Taking Over (24-Hour Launch Deadline)

Situation

  • App launching tomorrow
  • QA reports: "Now Playing works, but when user opens Apple Music then returns to our app, our controls disappear"
  • Product manager: "This is a blocker, users will think our app is broken"
  • You're 2 hours from code freeze

Rationalization Traps (DO NOT)

  1. *"Just tell users not to use Apple Music"* - Unacceptable UX, will get 1-star reviews
  2. *"Force our app to always be Now Playing"* - Impossible, system controls eligibility
  3. *"File a bug with Apple"* - Won't help before launch

Root Cause

Your app loses eligibility because:

  • Using .mixWithOthers option (allows other apps to play simultaneously)
  • Not calling becomeActiveIfPossible() when returning to foreground
  • AVAudioSession deactivated when backgrounded

Systematic Fix (30 minutes)

// 1. Remove mixWithOthers
try AVAudioSession.sharedInstance().setCategory(.playback, options: [])

// 2. Reactivate when returning to foreground
NotificationCenter.default.addObserver(
    forName: UIApplication.willEnterForegroundNotification,
    object: nil,
    queue: .main
) { [weak self] _ in
    guard self?.isPlaying == true else { return }

    do {
        try AVAudioSession.sharedInstance().setActive(true)
        self?.session?.becomeActiveIfPossible { _ in }
    } catch {
        print("Failed to reactivate audio session: \(error)")
    }
}

// 3. Handle interruptions (phone call, Siri)
NotificationCenter.default.addObserver(
    forName: AVAudioSession.interruptionNotification,
    object: nil,
    queue: .main
) { [weak self] notification in
    guard let info = notification.userInfo,
          let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
          let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
        return
    }

    if type == .ended {
        // ✅ Reactivate after interruption
        try? AVAudioSession.sharedInstance().setActive(true)
        self?.session?.becomeActiveIfPossible { _ in }
    }
}

Communication Template

To PM: Found root cause - our audio session config allowed Apple Music to take over.
Fix implemented: 3 changes to audio session handling.
Testing: Verified fix with Apple Music, Spotify, phone calls.
ETA: 20 more minutes for full regression test.

To QA: Please test this flow:
1. Play audio in our app
2. Open Apple Music, play a song
3. Return to our app, tap play
4. Lock screen should show OUR controls

Time Saved

  • 2-3 hours of debugging speculation
  • Launch delay avoided
  • QA confidence restored

Scenario 2: Artwork Flickers Every Track Change

Situation

  • User feedback: "Album art keeps flashing when songs change"
  • Analytics show 3-4 artwork updates per track change
  • Designer: "This looks unprofessional"

Root Cause

Multiple artwork sources racing:

  1. Cache check (async)
  2. Remote URL fetch (async)
  3. Embedded artwork extraction (async)

All three complete at different times, each updating Now Playing

Fix (20 minutes)

// ✅ Single-source-of-truth with cancellation
private var artworkTask: Task<Void, Never>?

func loadArtwork(for track: Track) {
    // Cancel previous artwork load
    artworkTask?.cancel()

    artworkTask = Task { @MainActor in
        // Clear previous artwork immediately (optional)
        // updateNowPlayingArtwork(nil)

        // Wait for best available artwork
        let artwork = await loadBestArtwork(for: track)

        // Check if still current track
        guard !Task.isCancelled else { return }

        // Single update
        updateNowPlayingArtwork(artwork, for: track.artworkURL)
    }
}

private func loadBestArtwork(for track: Track) async -> UIImage? {
    // Priority order: embedded > cached > remote
    if let embedded = await extractEmbeddedArtwork(track) {
        return embedded
    }
    if let cached = await loadFromCache(track.artworkURL) {
        return cached
    }
    return await downloadImage(track.artworkURL)
}

Communication Template

To Designer: Fixed artwork flicker - reduced from 3-4 updates to 1 per track.
Root cause: Multiple async sources racing to update artwork.
Solution: Task cancellation + priority order (embedded > cached > remote).
Testing: Verified with 10 track changes, zero flicker.

Time Saved

  • 1-2 hours investigating image caching
  • Designer approval unblocked
  • Professional UX restored

Common Gotchas

SymptomCauseSolutionTime to Fix
Info never appearsMissing background modeAdd audio to UIBackgroundModes in Info.plist2 min
Info never appearsAVAudioSession not activatedCall setActive(true) before playback5 min
Info never appearsNo command handlersAdd target to at least one command10 min
Info never appearsUsing .mixWithOthersRemove.mixWithOthers option5 min
Commands grayed outisEnabled = falseSet command.isEnabled = true after adding target5 min
Commands don't respondHandler returns wrong statusReturn .success from handler5 min
Commands don't respondUsing shared command center with MPNowPlayingSessionUse session.remoteCommandCenter instead10 min
Skip buttons missingNo preferredIntervalsSet skipCommand.preferredIntervals = [15.0]5 min
Artwork never appearsMPMediaItemArtwork block returns nilEnsure image is loaded before creating artwork15 min
Artwork flickersMultiple rapid updatesSingle source of truth with cancellation20 min
Wrong play/pause stateUsing playbackState propertyUse playbackRate (1.0 = playing, 0.0 = paused)10 min
Progress bar stuckNot updating on seekUpdate elapsedPlaybackTime after seek completes10 min
Progress bar jumpsUpdating elapsed on timerDon't update on timer; system infers from rate10 min
Loses Now Playing to other appsSession not reactivated on foregroundCall becomeActiveIfPossible() on foreground15 min
playbackState doesn't workiOS-only appplaybackState is macOS only; use playbackRate on iOS10 min
Siri skip ignores preferredIntervalsHardcoded interval in handlerUse event.interval from MPSkipIntervalCommandEvent5 min
CarPlay: App doesn't appearMissing entitlementAdd com.apple.developer.carplay-audio to entitlements5 min
CarPlay: Custom buttons don't appearConfigured at wrong timeConfigure at templateApplicationScene(_:didConnect:)5 min
CarPlay: Works on device, not simulatorDebugger attachedRun without debugger for reliable testing1 min
MusicKit: Now Playing wrongOverwriting automatic dataDon't set nowPlayingInfo when using ApplicationMusicPlayer5 min

Expert Checklist

Before Implementing Now Playing

  • Added audio to UIBackgroundModes in Info.plist
  • AVAudioSession category is .playback without .mixWithOthers
  • Decided: Manual (MPNowPlayingInfoCenter) or Automatic (MPNowPlayingSession)?

AVAudioSession Setup

  • setCategory(.playback) called at app launch
  • setActive(true) called before playback starts
  • setActive(false, options:.notifyOthersOnDeactivation) on stop
  • Interruption notification handled (reactivate after phone call)
  • Foreground notification handled (reactivate after background)

Remote Commands

  • At least one command has target registered
  • All registered commands have isEnabled = true
  • Skip commands have preferredIntervals set
  • Handlers return .success on success
  • Using correct command center (session's vs shared)
  • Command targets stored to prevent deallocation
  • Commands removed in deinit

Now Playing Info

  • Title set (MPMediaItemPropertyTitle)
  • Duration set (MPMediaItemPropertyPlaybackDuration)
  • Elapsed time set at play/pause/seek (MPNowPlayingInfoPropertyElapsedPlaybackTime)
  • Playback rate set (MPNowPlayingInfoPropertyPlaybackRate: 1.0 = playing, 0.0 = paused)
  • Artwork created with MPMediaItemArtwork(boundsSize:requestHandler:)
  • NOT using playbackState property (macOS only)
  • NOT updating elapsed time on a timer

Artwork

  • Image at least 600x600 pixels
  • MPMediaItemArtwork block never returns nil (return placeholder if needed)
  • Single source of truth prevents flickering
  • Previous artwork load cancelled on track change

Testing

  • Lock screen shows correct info
  • Control Center shows correct info
  • Play/pause buttons respond
  • Skip buttons show and respond
  • Progress bar moves correctly
  • Survives app background/foreground
  • Survives phone call interruption
  • Survives other app playing audio
  • Tested with Apple Music conflict
  • Tested with Spotify conflict

CarPlay (if applicable)

  • Added com.apple.developer.carplay-audio entitlement
  • CPNowPlayingTemplate configured at templateApplicationScene(_:didConnect:)
  • Custom buttons (if any) configured with CPNowPlayingButton
  • Tested on CarPlay simulator (I/O → External Displays → CarPlay)
  • Tested in real vehicle (if available)
  • Tested both with and without debugger attached

Resources

WWDC: 2022-110338, 2017-251, 2019-501

Docs: /mediaplayer/mpnowplayinginfocenter, /mediaplayer/mpremotecommandcenter, /mediaplayer/mpnowplayingsession

Skills: axiom-avfoundation-ref, axiom-now-playing-carplay, axiom-now-playing-musickit


Last Updated: 2026-01-04 Status: iOS 18+ discipline skill covering Now Playing, CarPlay, and MusicKit integration Tested: Based on WWDC 2019-501, WWDC 2022-110338 patterns

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

04

需要参考平台分布和安装热度时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Claude Code

30.01%
按下载量换算402

Codex

22.14%
按下载量换算297

OpenCode

16.82%
按下载量换算226

Antigravity

13.76%
按下载量换算185

Cursor

7.23%
按下载量换算97

windsurf

3.3%
按下载量换算44

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。

来源信息

继续浏览同类 Skills