I'm working for weeks to get the duration of a video file. I want to use direct api calls. I know, that I can use external command line tools to get it. But I want to avoid the dependencies with these tools. So I wrote a lot of code, but it does not show the duration. The problem may be the sync/async situation. It always return 0. Any idea? BTW: The code should be up to date. The is a function which was depreciated with macOS 13. So I like to use the actual OSX 15. And it should be a lazy var to only do this time consuming task when really needed.
import Foundation
import AVFoundation
// Helper for associated objects
private class AssociatedObjectKey {
let key: String
init(_ key: String) { self.key = key }
}
// Error types defined at file level
public enum DurationError: LocalizedError {
case notMediaFile
case noSourceURL
case failedToLoadDuration(Error?)
case loadingCancelled
case stillLoading
case unknownStatus
case timeout
case invalidDuration
public var errorDescription: String? {
switch self {
case .notMediaFile:
return "File is not a video or audio file"
case .noSourceURL:
return "No source URL available for duration loading"
case .failedToLoadDuration(let underlyingError):
return underlyingError?.localizedDescription ?? "Failed to load duration from media file"
case .loadingCancelled:
return "Duration loading was cancelled"
case .stillLoading:
return "Duration is still loading"
case .unknownStatus:
return "Unknown loading status"
case .timeout:
return "Duration loading timed out"
case .invalidDuration:
return "Invalid duration value"
}
}
}
// Separate class to handle duration storage and loading
private class DurationManager {
private var _duration: TimeInterval?
private var _sourceURL: URL?
var duration: TimeInterval? {
_duration
}
var sourceURL: URL? {
_sourceURL
}
func setSourceURL(_ url: URL) {
_sourceURL = url
}
func setDuration(_ duration: TimeInterval) {
_duration = duration
}
// Synchronous duration loading
// Quick Fix für Ihren bestehenden Code
func loadDurationSynchronously() throws -> TimeInterval {
guard let url = _sourceURL else {
print("❌ Kein Source URL verfügbar")
throw DurationError.noSourceURL
}
print("🔍 Lade Duration für: \(url.lastPathComponent)")
// Prüfe ob Datei existiert
guard FileManager.default.fileExists(atPath: url.path) else {
print("❌ Datei existiert nicht: \(url.path)")
throw DurationError.failedToLoadDuration(NSError(domain: "FileNotFound", code: 404))
}
let asset = AVURLAsset(url: url)
var durationError: Error?
var result: TimeInterval?
var finished = false
let startTime = Date()
asset.loadValuesAsynchronously(forKeys: ["duration"]) {
var error: NSError?
let status = asset.statusOfValue(forKey: "duration", error: &error)
print("📊 Duration Status: \(status.rawValue)")
if let err = error {
print("❌ AVAsset Error: \(err.localizedDescription)")
}
switch status {
case .loaded:
let duration = asset.duration
if duration.isValid && !duration.isIndefinite {
let seconds = CMTimeGetSeconds(duration)
if seconds.isFinite && seconds > 0 {
result = seconds
print("✅ Duration geladen: \(seconds) Sekunden")
} else {
print("❌ Ungültiger Duration-Wert: \(seconds)")
durationError = DurationError.invalidDuration
}
} else {
print("❌ Duration nicht gültig oder unendlich")
durationError = DurationError.invalidDuration
}
case .failed:
print("❌ Duration loading fehlgeschlagen: \(error?.localizedDescription ?? "Unbekannter Fehler")")
durationError = error ?? DurationError.failedToLoadDuration(nil)
case .cancelled:
print("⏹️ Duration loading abgebrochen")
durationError = DurationError.loadingCancelled
default:
print("❓ Unbekannter Status: \(status.rawValue)")
durationError = DurationError.unknownStatus
}
finished = true
}
// Wait mit Timeout (30 Sekunden)
while !finished {
let elapsed = Date().timeIntervalSince(startTime)
if elapsed > 30.0 {
print("⏰ Timeout nach 30 Sekunden")
throw DurationError.timeout
}
RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 0.1)) // Langsamere Polling-Rate
}
if let error = durationError {
throw error
}
guard let duration = result else {
print("❌ Keine Duration erhalten")
throw DurationError.invalidDuration
}
_duration = duration
return duration
}
}
extension EXIFData {
// MARK: - Duration Management
private var durationManager: DurationManager {
get {
let key = AssociatedObjectKey("durationManager")
if let existing = objc_getAssociatedObject(self, Unmanaged.passUnretained(key).toOpaque()) as? DurationManager {
return existing
}
let newManager = DurationManager()
objc_setAssociatedObject(self, Unmanaged.passUnretained(key).toOpaque(), newManager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return newManager
}
}
// Public computed property for duration (SYNCHRONOUS access)
public var duration: TimeInterval? {
get {
// Return cached duration if available
if let existingDuration = durationManager.duration {
return existingDuration
}
// Only load duration for media files
guard isVideoFile || isAudioFile else {
return nil
}
// Ensure we have a source URL
guard durationManager.sourceURL != nil else {
print("Warning: No source URL set for duration loading")
return nil
}
do {
let duration = try durationManager.loadDurationSynchronously()
return duration
} catch {
print("Error loading duration: \(error)")
return nil
}
}
}
// MARK: - URL Management
/// Associates a source URL with the EXIFData instance for duration loading
public func setSourceURL(_ url: URL) {
durationManager.setSourceURL(url)
}
/// Retrieves the associated source URL
public var sourceURL: URL? {
durationManager.sourceURL
}
/// Manually load duration with completion handler
public func loadDuration(completion: @escaping (TimeInterval?, Error?) -> Void) {
guard isVideoFile || isAudioFile else {
completion(nil, DurationError.notMediaFile)
return
}
guard durationManager.sourceURL != nil else {
completion(nil, DurationError.noSourceURL)
return
}
DispatchQueue.global().async {
do {
let duration = try self.durationManager.loadDurationSynchronously()
DispatchQueue.main.async {
completion(duration, nil)
}
} catch {
DispatchQueue.main.async {
completion(nil, error)
}
}
}
}
}
Update:
struct MediaFileEntry: Comparable {
let file: FileEntry
let exif: EXIFData?
private let _duration: Double?
var duration: Double? {
return _duration
}
init(file: FileEntry, exif: EXIFData?, directoryPath: String) {
self.file = file
self.exif = exif
// Construct full path properly
let fullPath = "\(directoryPath)/\(file.filename)"
if let exif = exif, (exif.isVideoFile || exif.isAudioFile),
let url = URL(string: fullPath) {
self._duration = Self.loadDurationSync(from: url)
} else {
self._duration = nil
}
}
private static func loadDurationSync(from url: URL) -> Double? {
let asset = AVURLAsset(url: url)
var result: Double?
let semaphore = DispatchSemaphore(value: 0)
Task {
do {
let duration = try await asset.load(.duration)
result = Double(duration.value) / Double(duration.timescale)
} catch {
result = nil
}
semaphore.signal()
}
semaphore.wait()
return result
}
I have this code now, but the value is still 0.
durationproperty. Embrace asynchrony and dotry await asset.load(.duration).func loadDurationSync(from url: URL) async -> Double?, and remove theTaskand semaphore. You should not do this synchronously in the first place.durationproperty. The act of "getting the duration synchronously" is being deprecated. If you want it synchronously, just keep using the deprecated property. Doing it synchronously but avoiding the deprecated property is like covering your own ears then thinking no one else can hear you.