0

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.

4
  • 5
    You should not load the duration synchronously. That's the whole point of deprecating the old duration property. Embrace asynchrony and do try await asset.load(.duration). Commented Sep 15 at 11:40
  • I did it, but was not successful! Commented Sep 16 at 7:58
  • 1
    The function signature should be func loadDurationSync(from url: URL) async -> Double?, and remove the Task and semaphore. You should not do this synchronously in the first place. Commented Sep 16 at 8:01
  • 2
    You need to realise that what is being deprecated isn't just that particular duration property. 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. Commented Sep 16 at 8:06

1 Answer 1

0

You need to stop and read up on async/await, and learn to use it. It sounds like your problem is that the way you have written your code using tasks, the call to

asset.load(.duration)

has not completed. If you await that call, the results should be available.

As Sweeper said in their comment, you need to write your function loadDuration as an asynchronous function. Its signature should look like this:

func loadDuration(from url: URL) async -> Double?

It should not use a task or semaphores internally. It should simply call try await asset.load(duration)

Then you should call your function with an asynchronous call:

let path = "~/path/to/file.mov"
let imageURL = URL(fileURLWithPath: path)

if let result = await loadDuration(from: imageURL) {
    print(result)
} else {
    print("Unable to load info about file \(path)")
}

I tested it and that approach works, and returns a duration value for a video file I had lying around.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.