feat(mac,ios,playback,store): initial macos implementation
This commit is contained in:
1 parent
e5bb559d7f
commit
d2d7166cf0
72 files changed
+4452
-318
No files matched your search
@@ -3,7 +3,7 @@
|
||||
import Domain
|
||||
import Foundation
|
||||
|
||||
public actor AVFoundationEngine: OutputEngine {
|
||||
public actor AVFoundationEngine: OutputEngine, PlaybackTuning {
|
||||
public typealias Resolve = @Sendable (Track) async throws -> StreamRequest
|
||||
|
||||
private struct Slot {
|
||||
@@ -22,6 +22,7 @@
|
||||
private var prepared: Slot?
|
||||
private var adoptable = false
|
||||
private var wantsPlaying = false
|
||||
public private(set) var matchesSampleRate = false
|
||||
private var sessionActive = false
|
||||
private var loadGeneration = 0
|
||||
private var prepareGeneration = 0
|
||||
@@ -58,7 +59,10 @@
|
||||
current = prepared
|
||||
self.prepared = nil
|
||||
} else {
|
||||
guard let slot = await makeSlot(item), generation == loadGeneration else { return }
|
||||
guard let request = await resolveStream(item), generation == loadGeneration else { return }
|
||||
await switchRate(for: item, request: request)
|
||||
guard generation == loadGeneration else { return }
|
||||
let slot = makeSlot(item, request)
|
||||
player.removeAllItems()
|
||||
prepared = nil
|
||||
player.insert(slot.playerItem, after: nil)
|
||||
@@ -80,11 +84,20 @@
|
||||
self.prepared = nil
|
||||
}
|
||||
guard let item, current != nil else { return }
|
||||
guard let slot = await makeSlot(item), generation == prepareGeneration, current != nil else { return }
|
||||
guard let request = await resolveStream(item), generation == prepareGeneration, current != nil else { return }
|
||||
guard rateSwitch(for: item, request: request) == nil else { return }
|
||||
let slot = makeSlot(item, request)
|
||||
player.insert(slot.playerItem, after: nil)
|
||||
prepared = slot
|
||||
}
|
||||
|
||||
public func setMatchesSampleRate(_ isOn: Bool) {
|
||||
matchesSampleRate = isOn
|
||||
guard isOn, let prepared, rateSwitch(for: prepared.item, request: nil) != nil else { return }
|
||||
player.remove(prepared.playerItem)
|
||||
self.prepared = nil
|
||||
}
|
||||
|
||||
public func play() async {
|
||||
wantsPlaying = true
|
||||
if !sessionActive {
|
||||
@@ -104,6 +117,10 @@
|
||||
reportAnchor()
|
||||
}
|
||||
|
||||
public func setVolume(_ volume: Double) {
|
||||
player.volume = Float(min(max(volume, 0), 1))
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
wantsPlaying = false
|
||||
player.pause()
|
||||
@@ -113,22 +130,44 @@
|
||||
adoptable = false
|
||||
}
|
||||
|
||||
private func makeSlot(_ item: QueueItem) async -> Slot? {
|
||||
private func resolveStream(_ item: QueueItem) async -> StreamRequest? {
|
||||
do {
|
||||
let request = try await resolve(item.track)
|
||||
let playerItem = makePlayerItem(request)
|
||||
let observation = playerItem.observe(\.status, options: [.new]) { @Sendable [weak self] playerItem, _ in
|
||||
guard playerItem.status == .failed else { return }
|
||||
let id = ObjectIdentifier(playerItem)
|
||||
Task { await self?.itemFailed(id) }
|
||||
}
|
||||
return Slot(item: item, playerItem: playerItem, statusObservation: observation)
|
||||
return try await resolve(item.track)
|
||||
} catch {
|
||||
continuation.yield(.failed(item.id))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func makeSlot(_ item: QueueItem, _ request: StreamRequest) -> Slot {
|
||||
let playerItem = makePlayerItem(request)
|
||||
let observation = playerItem.observe(\.status, options: [.new]) { @Sendable [weak self] playerItem, _ in
|
||||
guard playerItem.status == .failed else { return }
|
||||
let id = ObjectIdentifier(playerItem)
|
||||
Task { await self?.itemFailed(id) }
|
||||
}
|
||||
return Slot(item: item, playerItem: playerItem, statusObservation: observation)
|
||||
}
|
||||
|
||||
private func rateSwitch(for item: QueueItem, request: StreamRequest?) -> Double? {
|
||||
#if os(macOS)
|
||||
let format = request?.format ?? item.track.formats.first
|
||||
return SampleRateMatcher(isEnabled: matchesSampleRate).targetRate(for: format, on: OutputDevice.current())
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
private func switchRate(for item: QueueItem, request: StreamRequest) async {
|
||||
#if os(macOS)
|
||||
guard let rate = rateSwitch(for: item, request: request) else { return }
|
||||
player.pause()
|
||||
player.removeAllItems()
|
||||
prepared = nil
|
||||
try? await OutputDevice.setNominalRate(rate)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func makePlayerItem(_ request: StreamRequest) -> AVPlayerItem {
|
||||
var options: [String: any Sendable] = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
|
||||
if !request.resource.headers.isEmpty {
|
||||
|
||||
Reference in new issue
Block a user