Live Activities fix
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
import UIKit
|
||||
|
||||
private let logger = Logger(subsystem: "com.musicassistant.mobile", category: "PlayerManager")
|
||||
|
||||
@@ -21,6 +22,7 @@ final class MAPlayerManager {
|
||||
|
||||
private weak var service: MAService?
|
||||
private var eventTask: Task<Void, Never>?
|
||||
let liveActivityManager = MALiveActivityManager()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@@ -84,6 +86,7 @@ final class MAPlayerManager {
|
||||
await MainActor.run {
|
||||
players[player.playerId] = player
|
||||
logger.debug("Updated player: \(player.name) state=\(player.state.rawValue) item=\(player.currentItem?.name ?? "nil")")
|
||||
updateLiveActivity()
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to decode player_updated event: \(error)")
|
||||
@@ -98,6 +101,7 @@ final class MAPlayerManager {
|
||||
await MainActor.run {
|
||||
playerQueues[queue.queueId] = queue
|
||||
logger.debug("Updated queue state for player \(queue.queueId), current: \(queue.currentItem?.name ?? "nil")")
|
||||
updateLiveActivity()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -140,6 +144,119 @@ final class MAPlayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Live Activity
|
||||
|
||||
/// Finds the best currently-playing player and pushes its state to the Live Activity.
|
||||
/// Spawns a Task to fetch artwork with auth before updating.
|
||||
private func updateLiveActivity() {
|
||||
let playing = players.values
|
||||
.filter { $0.state == .playing }
|
||||
.first { $0.currentItem != nil || playerQueues[$0.playerId]?.currentItem != nil }
|
||||
?? players.values.first { $0.state == .playing }
|
||||
|
||||
guard let player = playing else {
|
||||
liveActivityManager.end()
|
||||
return
|
||||
}
|
||||
|
||||
guard let item = playerQueues[player.playerId]?.currentItem ?? player.currentItem else {
|
||||
liveActivityManager.end()
|
||||
return
|
||||
}
|
||||
|
||||
let media = item.mediaItem
|
||||
let trackTitle = item.name.isEmpty ? (media?.name ?? "Unknown Track") : item.name
|
||||
let artistName = media?.artists?.first?.name ?? ""
|
||||
let isPlaying = player.state == .playing
|
||||
let playerName = player.name
|
||||
let imagePath = media?.imageUrl
|
||||
let imageProvider = media?.imageProvider
|
||||
|
||||
logger.debug("updateLiveActivity: track='\(trackTitle)' imagePath=\(imagePath ?? "nil")")
|
||||
|
||||
// Update immediately so the live activity appears without waiting for artwork.
|
||||
liveActivityManager.update(
|
||||
trackTitle: trackTitle,
|
||||
artistName: artistName,
|
||||
artworkData: nil,
|
||||
isPlaying: isPlaying,
|
||||
playerName: playerName
|
||||
)
|
||||
|
||||
// Then fetch artwork in background and refresh.
|
||||
let capturedService = service
|
||||
Task {
|
||||
let artworkData = await Self.fetchArtworkData(path: imagePath, provider: imageProvider, service: capturedService)
|
||||
logger.debug("fetchArtworkData result: \(artworkData != nil ? "\(artworkData!.count) bytes" : "nil")")
|
||||
guard let artworkData else { return }
|
||||
liveActivityManager.update(
|
||||
trackTitle: trackTitle,
|
||||
artistName: artistName,
|
||||
artworkData: artworkData,
|
||||
isPlaying: isPlaying,
|
||||
playerName: playerName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches artwork as small JPEG data for the Live Activity.
|
||||
/// Checks the app's ImageCache at sizes the app normally loads (512, 64) before
|
||||
/// falling back to a fresh network download at size 128 with auth.
|
||||
private static func fetchArtworkData(path: String?, provider: String?, service: MAService?) async -> Data? {
|
||||
guard let path, !path.isEmpty else {
|
||||
logger.debug("fetchArtworkData: no image path")
|
||||
return nil
|
||||
}
|
||||
guard let service else {
|
||||
logger.debug("fetchArtworkData: service is nil")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check cache at sizes the app commonly loads
|
||||
for size in [512, 64] {
|
||||
guard let url = service.imageProxyURL(path: path, provider: provider, size: size) else { continue }
|
||||
let key = ImageCache.shared.cacheKey(for: url)
|
||||
if let img = ImageCache.shared.memoryImage(for: key) {
|
||||
logger.debug("fetchArtworkData: memory cache hit at size \(size)")
|
||||
return resizeAndEncode(img)
|
||||
}
|
||||
if let img = await Task.detached(priority: .userInitiated) { ImageCache.shared.diskImage(for: key) }.value {
|
||||
logger.debug("fetchArtworkData: disk cache hit at size \(size)")
|
||||
return resizeAndEncode(img)
|
||||
}
|
||||
}
|
||||
|
||||
// Not cached — download fresh at compact size with auth header
|
||||
guard let downloadURL = service.imageProxyURL(path: path, provider: provider, size: 128) else { return nil }
|
||||
logger.debug("fetchArtworkData: downloading from \(downloadURL)")
|
||||
var request = URLRequest(url: downloadURL)
|
||||
if let token = service.authManager.currentToken {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
} else {
|
||||
logger.warning("fetchArtworkData: no auth token available")
|
||||
}
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||
logger.debug("fetchArtworkData: HTTP \(status), \(data.count) bytes")
|
||||
guard status == 200, let img = UIImage(data: data) else { return nil }
|
||||
ImageCache.shared.store(img, data: data, for: ImageCache.shared.cacheKey(for: downloadURL))
|
||||
return resizeAndEncode(img)
|
||||
} catch {
|
||||
logger.error("fetchArtworkData: network error: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func resizeAndEncode(_ image: UIImage) -> Data? {
|
||||
// ActivityKit ContentState limit is 4 KB total (Data fields are base64 in the payload).
|
||||
// 40×40 JPEG at 0.3 quality ≈ 400–700 bytes, well within limits.
|
||||
let size = CGSize(width: 40, height: 40)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let scaled = renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: size)) }
|
||||
return scaled.jpegData(compressionQuality: 0.3)
|
||||
}
|
||||
|
||||
// MARK: - Data Loading
|
||||
|
||||
/// Load all players and their queue states
|
||||
@@ -175,6 +292,7 @@ final class MAPlayerManager {
|
||||
playerQueues[pid] = queue
|
||||
}
|
||||
logger.info("Loaded queue states for \(queueResults.count) players")
|
||||
updateLiveActivity()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user