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.