import ExpoModulesCore import MediaPlayer import MusicKit /** * Apple Music for the iOS app: * - the one-tap "Allow access to Apple Music" prompt (MusicKit authorization) * - the Music User Token the server needs to read recently played * - live now-playing from the Music app while our app is open * (iOS doesn't let apps observe the Music app in the background) */ public class AppleMusicModule: Module { private var observers: [NSObjectProtocol] = [] public func definition() -> ModuleDefinition { Name("AppleMusic") Events("onNowPlayingChange") Function("authorizationStatus") { () -> String in Self.describe(MusicAuthorization.currentStatus) } AsyncFunction("requestAuthorization") { () async -> String in Self.describe(await MusicAuthorization.request()) } /// developerToken comes from our server (signed with the MusicKit key). AsyncFunction("getUserToken") { (developerToken: String) async throws -> String in try await MusicUserTokenProvider().userToken(for: developerToken, options: .ignoreCache) } AsyncFunction("getNowPlaying") { () async -> [String: Any] in await MainActor.run { Self.snapshot() } } OnStartObserving { DispatchQueue.main.async { self.startObserving() } } OnStopObserving { DispatchQueue.main.async { self.stopObserving() } } } private func startObserving() { guard observers.isEmpty, MusicAuthorization.currentStatus == .authorized else { return } let player = MPMusicPlayerController.systemMusicPlayer player.beginGeneratingPlaybackNotifications() let center = NotificationCenter.default for name in [Notification.Name.MPMusicPlayerControllerNowPlayingItemDidChange, Notification.Name.MPMusicPlayerControllerPlaybackStateDidChange] { observers.append(center.addObserver(forName: name, object: player, queue: .main) { [weak self] _ in self?.sendEvent("onNowPlayingChange", Self.snapshot()) }) } } private func stopObserving() { observers.forEach { NotificationCenter.default.removeObserver($0) } observers.removeAll() MPMusicPlayerController.systemMusicPlayer.endGeneratingPlaybackNotifications() } /// Must be called on the main thread. private static func snapshot() -> [String: Any] { let player = MPMusicPlayerController.systemMusicPlayer guard let item = player.nowPlayingItem, let title = item.title, !title.isEmpty else { return ["state": "stopped"] } let state: String switch player.playbackState { case .playing, .seekingForward, .seekingBackward: state = "playing" case .paused, .interrupted: state = "paused" default: state = "stopped" } return [ "state": state, "title": title, "artist": item.artist ?? "", "album": item.albumTitle ?? "", "durationMs": item.playbackDuration * 1000, "storeId": item.playbackStoreID, ] } private static func describe(_ status: MusicAuthorization.Status) -> String { switch status { case .authorized: return "authorized" case .denied: return "denied" case .restricted: return "restricted" case .notDetermined: return "notDetermined" @unknown default: return "unknown" } } }