Skip to content
All essays
iOSMarch 27, 202512 min

Swift AVFoundation: Video Processing

Process videos with AVFoundation. Video playback, editing, and advanced video features.

Ü
Ümit Uz
Mobile & Full Stack Developer

AVFoundation provides comprehensive video processing capabilities. Learn to play, edit, and process videos.

AVPlayer Setup

Basic Video Player

swift
import AVFoundation
import AVKit
import SwiftUI

struct VideoPlayerView: View {
    @StateObject private var player = VideoPlayer()

    var body: some View {
        VStack {
            VideoPlayer(player: player.player)
                .onAppear {
                    player.play()
                }
                .onDisappear {
                    player.pause()
                }
        }
    }
}

class VideoPlayer: NSObject, ObservableObject {
    @Published var isPlaying = false
    @Published var currentTime: Double = 0
    @Published var duration: Double = 0

    var player: AVPlayer

    override init() {
        super.init()

        guard let url = URL(string: "https://example.com/video.mp4") else {
            fatalError("Invalid URL")
        }

        player = AVPlayer(url: url)

        // Add periodic observer
        let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
            guard let self = self else { return }

            self?.currentTime = CMTimeGetSeconds(time)
        }

        // Observe player status
        player.addObserver(self, forKeyPath: "timeControlStatus", options: .new)

        // Observe duration
        let asset = AVAsset(url: url)
        asset.loadValuesAsynchronously(forKeys: ["duration"], completionHandler: { loaded, error in
            guard loaded, error == nil else { return }

            let duration = try? asset.duration
            self?.duration = CMTimeGetSeconds(duration ?? CMTime())
        })
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "timeControlStatus" {
            DispatchQueue.main.async { [weak self] in
                self?.updatePlayingStatus()
            }
        }
    }

    func play() {
        player.play()
        isPlaying = true
    }

    func pause() {
        player.pause()
        isPlaying = false
    }

    func stop() {
        player.pause()
        player.replaceCurrentItem(with: nil)
        isPlaying = false
    }

    func seek(to seconds: Double) {
        let time = CMTime(seconds: seconds, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        player.seek(to: time)
    }

    private func updatePlayingStatus() {
        isPlaying = player.timeControlStatus == .playing
    }

    deinit {
        player.removeTimeObserver(self)
        player.removeObserver(self, forKeyPath: "timeControlStatus")
    }
}

Video Composition

AVFoundation Editing

swift
import AVFoundation

class VideoEditor {
    var asset: AVAsset
    var composition: AVMutableComposition?

    init(asset: AVAsset) {
        self.asset = asset
    }

    func createComposition() -> AVMutableComposition {
        let composition = AVMutableComposition()
        let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
        let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)

        do {
            try videoTrack.insertTimeRange(
                CMTimeRange(start: .zero, duration: asset.duration),
                of: asset.tracks(withMediaType: .video)[0],
                at: .zero
            )

            try audioTrack.insertTimeRange(
                CMTimeRange(start: .zero, duration: asset.duration),
                of: asset.tracks(withMediaType: .audio)[0],
                at: .zero
            )
        } catch {
            print("Error creating composition: \(error)")
        }

        self.composition = composition
        return composition
    }

    func trimVideo(from startTime: Double, to endTime: Double) -> AVAsset? {
        guard let composition = composition else { return nil }

        let start = CMTime(seconds: startTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        let end = CMTime(seconds: endTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        let range = CMTimeRange(start: start, end: end)

        let asset = try? AVAsset(url: composition.url!)

        return asset
    }
}

Video Export

Export Video

swift
class VideoExporter {
    func exportComposition(_ composition: AVMutableComposition, to url: URL) async throws {
        let exportSession = AVAssetExportSession(
            asset: composition,
            presetName: AVAssetExportPresetHighestQuality
        )

        exportSession.outputURL = url
        exportSession.outputFileType = .mp4

        exportSession.shouldOptimizeForNetworkUse = true

        await exportSession.export()

        switch exportSession.status {
        case .completed:
            print("Export completed")
        case .failed:
            throw exportSession.error ?? ExportError.exportFailed
        case .cancelled:
            throw ExportError.exportCancelled
        default:
            break
        }
    }

    enum ExportError: Error {
        case exportFailed
        case exportCancelled
    }
}

Video Effects

Add Effects

swift
class VideoEffectsProcessor {
    func addWatermark(to video: URL, watermarkImage: UIImage, outputURL: URL) async throws {
        let asset = AVAsset(url: video)
        let composition = AVMutableComposition()
        let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)

        try? videoTrack.insertTimeRange(
            CMTimeRange(start: .zero, duration: asset.duration),
            of: asset.tracks(withMediaType: .video)[0],
            at: .zero
        )

        // Create watermark layer
        let watermarkLayer = CALayer()
        let watermarkImage = watermarkImage.cgImage.flatMap { UIImage(cgImage: $0) }
        let watermarkContent = watermarkLayer.contents(image: watermarkImage)
        watermarkLayer.frame = CGRect(x: 20, y: 20, width: 100, height: 50)

        // Apply watermark
        let videoComposition = AVMutableVideoComposition(asset: asset)
        videoComposition.layerInstructions = [AVMutableVideoCompositionLayerInstruction(assetVideoTrack: videoTrack.tracks.first!)]

        // Export
        let exporter = VideoExporter()
        try await exporter.exportComposition(composition, to: outputURL)
    }

    func applyFilter(to video: URL, filter: CIFilter, outputURL: URL) async throws {
        let asset = AVAsset(url: video)
        let item = AVPlayerItem(asset: asset)

        // Apply filter using AVVideoComposition
        let videoComposition = AVMutableVideoComposition(asset: asset)

        let composition = AVMutableComposition()
        let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)

        try? videoTrack.insertTimeRange(
            CMTimeRange(start: .zero, duration: asset.duration),
            of: asset.tracks(withMediaType: .video)[0],
            at: .zero
        )

        // Apply filter
        let compositionWithFilter = composition
        let exportSession = AVAssetExportSession(asset: compositionWithFilter, presetName: AVAssetExportPresetHighestQuality)

        exportSession.outputURL = outputURL
        exportSession.outputFileType = .mp4

        await exportSession.export()
    }
}

Video Capture

Camera Recording

swift
import AVFoundation

class VideoCaptureManager: NSObject, AVCaptureFileOutputRecordingDelegate {
    private var captureSession: AVCaptureSession?
    private var videoOutput: AVCaptureMovieFileOutput?
    private var captureDevice: AVCaptureDevice?

    @Published var isRecording = false

    func setupCaptureSession() {
        let session = AVCaptureSession()
        session.sessionPreset = .high

        guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
            return
        }

        captureDevice = camera

        do {
            let input = try AVCaptureDeviceInput(device: camera)

            if session.canAddInput(input) {
                session.addInput(input)
            }

            videoOutput = AVCaptureMovieFileOutput()

            if session.canAddOutput(videoOutput) {
                session.addOutput(videoOutput)
            }

            session.startRunning()

        } catch {
            print("Error setting up capture: \(error)")
        }
    }

    func startRecording(to url: URL) {
        guard let output = videoOutput else { return }

        output.startRecording(to: url, recordingDelegate: self)
        isRecording = true
    }

    func stopRecording() {
        videoOutput?.stopRecording()
        isRecording = false
    }

    func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
        if let error = error {
            print("Recording error: \(error)")
        } else {
            print("Recording saved to: \(outputFileURL)")
        }
    }
}

Thumbnails

Generate Thumbnails

swift
class ThumbnailGenerator {
    func generateThumbnail(from videoURL: URL, at time: CMTime, size: CGSize) -> UIImage? {
        let asset = AVAsset(url: videoURL)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
        imageGenerator.appliesPreferredTrackTransform = true
        imageGenerator.maximumSize = size

        var thumbnailImage: UIImage?

        DispatchQueue.main.sync {
            thumbnailImage = imageGenerator.copyCGImage(at: time, actualTime: nil)
        }

        if let cgImage = thumbnailImage {
            return UIImage(cgImage: cgImage)
        }

        return nil
    }

    func generateAllThumbnails(from videoURL: URL, interval: CMTime, size: CGSize) -> [UIImage] {
        let asset = AVAsset(url: videoURL)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
        imageGenerator.appliesPreferredTrackTransform = true
        imageGenerator.maximumSize = size

        var thumbnails: [UIImage] = []
        var currentTime = CMTime.zero

        let duration = asset.duration

        while currentTime < duration {
            autoreleasepool {
                if let cgImage = try? imageGenerator.copyCGImage(at: currentTime, actualTime: nil) {
                    let image = UIImage(cgImage: cgImage)
                    thumbnails.append(image)
                }

                currentTime = CMTimeAdd(currentTime, interval)
            }
        }

        return thumbnails
    }
}

Best Practices

  1. 1Memory: Manage video memory carefully
  2. 2Performance: Optimize for smooth playback
  3. 3Export: Use appropriate export presets
  4. 4Storage: Handle large video files
  5. 5Testing: Test on various devices
  6. 6Quality: Balance quality and performance
  7. 7Compression: Use appropriate compression
  8. 8UX: Provide smooth user experience

AVFoundation provides comprehensive video processing capabilities!

Related essays

Next essay
Swift WatchKit: Apple Watch Development