velody/packages/apple/VelodyPlayback/Sources/VelodyPlayback/PlaybackEngine.swift
2026-05-28 14:05:15 +02:00

102 lines
2.7 KiB
Swift

import AVFoundation
import Foundation
public enum PlaybackEngineEvent: Hashable, Sendable {
case finishedPlaying
}
@MainActor
public protocol PlaybackEngine: AnyObject {
var onEvent: (@MainActor @Sendable (PlaybackEngineEvent) -> Void)? { get set }
var currentTime: Double { get }
var duration: Double { get }
var isPlaying: Bool { get }
func loadTrack(at fileURL: URL, startTime: Double) throws
func play() throws
func pause()
func stop()
func seek(to time: Double) throws
}
@MainActor
public final class AVFoundationPlaybackEngine: NSObject, PlaybackEngine, AVAudioPlayerDelegate {
public var onEvent: (@MainActor @Sendable (PlaybackEngineEvent) -> Void)?
private var audioPlayer: AVAudioPlayer?
private let fileManager: FileManager
public init(fileManager: FileManager = .default) {
self.fileManager = fileManager
super.init()
}
public var currentTime: Double {
audioPlayer?.currentTime ?? 0
}
public var duration: Double {
audioPlayer?.duration ?? 0
}
public var isPlaying: Bool {
audioPlayer?.isPlaying ?? false
}
public func loadTrack(at fileURL: URL, startTime: Double) throws {
guard fileManager.fileExists(atPath: fileURL.path) else {
throw PlaybackError.missingLocalFile(path: fileURL.path)
}
do {
let audioPlayer = try AVAudioPlayer(contentsOf: fileURL)
audioPlayer.delegate = self
audioPlayer.prepareToPlay()
audioPlayer.currentTime = min(max(startTime, 0), audioPlayer.duration)
self.audioPlayer = audioPlayer
} catch let error as PlaybackError {
throw error
} catch {
throw PlaybackError.failedToLoadTrack(path: fileURL.path)
}
}
public func play() throws {
guard let audioPlayer else {
throw PlaybackError.noTrackLoaded
}
guard audioPlayer.play() else {
throw PlaybackError.failedToStartPlayback
}
}
public func pause() {
audioPlayer?.pause()
}
public func stop() {
audioPlayer?.stop()
audioPlayer?.currentTime = 0
}
public func seek(to time: Double) throws {
guard let audioPlayer else {
throw PlaybackError.seekUnavailable
}
audioPlayer.currentTime = min(max(time, 0), audioPlayer.duration)
}
nonisolated public func audioPlayerDidFinishPlaying(
_ player: AVAudioPlayer,
successfully flag: Bool
) {
if flag {
Task { @MainActor [weak self] in
self?.onEvent?(.finishedPlaying)
}
}
}
}