import AVFoundation final class VideoReader { var sampleRead: ((CMSampleBuffer) -> Void)? private let assetReader: AVAssetReader private let output: AVAssetReaderTrackOutput private let nominalFrameRate: Float private var timer: Timer? init?() { guard let url = Bundle.main.url(forResource: "2", withExtension: "MOV") else { return nil } let urlAsset = AVURLAsset(url: url, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true]) guard let track = urlAsset.tracks(withMediaType: .video).first else { return nil } nominalFrameRate = 1 / track.nominalFrameRate do { guard let asset = track.asset else { return nil } assetReader = try AVAssetReader(asset: asset) } catch { print(error) return nil } let settings: [String: Any] = [ String(kCVPixelBufferPixelFormatTypeKey): kCVPixelFormatType_32BGRA, String(kCVPixelBufferMetalCompatibilityKey): true ] output = AVAssetReaderTrackOutput(track: track, outputSettings: settings) output.alwaysCopiesSampleData = false guard assetReader.canAdd(output) else { return nil } assetReader.add(output) } func readSamples() { timer = Timer.scheduledTimer(withTimeInterval: Double(nominalFrameRate), repeats: true, block: { [weak self] currentTimer in guard let self = self else { currentTimer.invalidate() return } guard self.assetReader.status == .reading else { return } guard let sample = self.output.copyNextSampleBuffer() else { print("output.copyNextSampleBuffer() is nil") currentTimer.invalidate() self.timer?.invalidate() self.timer = nil return } self.sampleRead?(sample) }) if !assetReader.startReading() { timer?.invalidate() timer = nil } } private func printAssetReaderStatus() { switch self.assetReader.status { case .reading: print("reading") case .cancelled: print("canceled") case .completed: print("completed") case .failed: print("failed") case .unknown: print("unknown") @unknown default: print("@unknown default") } } }