Live Activities fix

This commit is contained in:
2026-04-19 16:57:57 +02:00
parent 053c743c41
commit c41b58d837
24 changed files with 1079 additions and 7 deletions
@@ -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 400700 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()
}
}