iOS Video with AVFoundation: HLS, Covers and Compression

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Baseline date: 2026-10-07. iOS plays HLS natively, which is why the packaging in video streaming: HLS, DASH and CMAF is aimed at it first. Capturing video, the photo library and the VideoPlayer basics are in Camera, Media and Photos. This page adds the media-pipeline pieces. API availability changes with the iOS version, so check Apple’s documentation for the deployment target of your app.

Playing HLS

import AVKit
import SwiftUI

struct VideoScreen: View {
    @State private var player = AVPlayer(url: URL(string: "https://cdn.example.com/videos/42/master.m3u8")!)

    var body: some View {
        VideoPlayer(player: player)
            .aspectRatio(16.0 / 9.0, contentMode: .fit)
            .onDisappear { player.pause() }
    }
}

AVPlayer chooses the rendition from the multivariant playlist and switches by segment. You can bound the choice on the AVPlayerItem with preferredPeakBitRate, preferredPeakBitRateForExpensiveNetworks (iOS 15 and later, for example on cellular) and preferredMaximumResolution. Private video works with signed cookies in HTTPCookieStorage.shared, or a token in a custom AVURLAsset header through the AVURLAssetHTTPHeaderFieldsKey option. The requirements Apple states for streams (keyframes every two seconds, six second segments) are in video encoding fundamentals. Reference: AVPlayer and HTTP Live Streaming.

Cover frames with AVAssetImageGenerator

import AVFoundation
import UIKit

func coverImage(for url: URL, at seconds: Double) async throws -> UIImage {
    let asset = AVURLAsset(url: url)
    let generator = AVAssetImageGenerator(asset: asset)
    generator.appliesPreferredTrackTransform = true            // honour the rotation of the recording
    generator.maximumSize = CGSize(width: 1280, height: 1280)  // never decode a full 4K frame
    let time = CMTime(seconds: seconds, preferredTimescale: 600)
    let (cgImage, _) = try await generator.image(at: time)     // async API from iOS 16
    return UIImage(cgImage: cgImage)
}

appliesPreferredTrackTransform matters: iPhones record landscape pixels with a rotation flag, so without it the cover is sideways. By default the generator picks the nearest frame with a tolerance, and you can set requestedTimeToleranceBefore/After to .zero for an exact frame, which is slower. Compress the result to JPEG or HEIC with UIImage.jpegData(compressionQuality:) and upload it as the poster. Reference: AVAssetImageGenerator and video covers and thumbnails.

Compressing with AVAssetExportSession

func compress(_ source: URL, to destination: URL) async throws {
    let asset = AVURLAsset(url: source)
    guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset1280x720) else {
        throw CocoaError(.fileWriteUnknown)
    }
    session.outputURL = destination
    session.outputFileType = .mp4
    session.shouldOptimizeForNetworkUse = true                  // moov atom first, like faststart
    await session.export()                                      // iOS 18 offers export(to:as:) with progress states
    if session.status != .completed { throw session.error ?? CocoaError(.fileWriteUnknown) }
}

Presets such as AVAssetExportPreset1280x720 and AVAssetExportPresetMediumQuality pick the size and bit rate for you, and AVAssetExportPresetHEVCHighestQuality produces HEVC. Check AVAssetExportSession.exportPresets(compatibleWith:) for what a given asset supports. For full control of codec and bit rate, use AVAssetReader and AVAssetWriter. HEVC from the camera is small but not playable in every browser, which is why the server transcodes anyway, see device-side compression and uploads. Reference: AVAssetExportSession.

Background uploads with URLSession

An upload must survive the app being suspended. A background URLSession hands the transfer to the system:

final class Uploader: NSObject, URLSessionTaskDelegate {
    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(withIdentifier: "com.example.uploads")
        config.isDiscretionary = false
        config.sessionSendsLaunchEvents = true
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    func upload(file: URL, to presignedURL: URL, contentType: String) {
        var request = URLRequest(url: presignedURL)
        request.httpMethod = "PUT"
        request.setValue(contentType, forHTTPHeaderField: "Content-Type")
        session.uploadTask(with: request, fromFile: file).resume()    // background sessions require a file, not Data
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        // tell the API the upload is complete, or retry; see the pipeline page
    }
}

The URL is the pre-signed PUT URL from the backend, see upload pipelines. Implement application(_:handleEventsForBackgroundURLSession:completionHandler:) to be woken when transfers finish in the background. Reference: URL loading system, background transfers.