Visual overview
Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.
- Request needed permissions
- Configure media session
- Capture or play media
- Handle interruptions
- Boundary 1Media source
- Boundary 2AVFoundation pipeline
- Boundary 3Playback or capture UI
Connected responsibilities, not a required class hierarchy or an execution trace.
AVPlayer and AVPlayerViewController
import AVKit
import AVFoundation
// Simple video player in SwiftUI
struct VideoPlayerView: View {
let url: URL
@State private var player: AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear {
player = AVPlayer(url: url)
player?.play()
}
.onDisappear {
player?.pause()
player = nil
}
}
}
// Full-featured player with AVPlayerViewController (UIKit wrapper)
struct FullScreenVideoPlayer: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let player = AVPlayer(url: url)
controller.player = player
controller.allowsPictureInPicturePlayback = true
controller.canStartPictureInPictureAutomaticallyFromInline = true
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {}
}
// Observing player state
@Observable
class VideoPlayerModel {
var player: AVPlayer?
var isPlaying = false
var currentTime: TimeInterval = 0
var duration: TimeInterval = 0
private var timeObserver: Any?
func loadVideo(url: URL) {
let item = AVPlayerItem(url: url)
player = AVPlayer(playerItem: item)
// Observe playback time
timeObserver = player?.addPeriodicTimeObserver(
forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
queue: .main
) { [weak self] time in
self?.currentTime = time.seconds
}
// Observe duration when ready
Task {
if let duration = try? await item.asset.load(.duration) {
self.duration = duration.seconds
}
}
}
func togglePlayback() {
if isPlaying {
player?.pause()
} else {
player?.play()
}
isPlaying.toggle()
}
func seek(to time: TimeInterval) {
player?.seek(to: CMTime(seconds: time, preferredTimescale: 600))
}
deinit {
if let observer = timeObserver {
player?.removeTimeObserver(observer)
}
}
}
AVAudioPlayer and AVAudioRecorder
import AVFoundation
@Observable
class AudioManager {
var isPlaying = false
var isRecording = false
var recordingURL: URL?
private var audioPlayer: AVAudioPlayer?
private var audioRecorder: AVAudioRecorder?
// Play audio file
func play(url: URL) throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .default)
try session.setActive(true)
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer?.prepareToPlay()
audioPlayer?.play()
isPlaying = true
}
func stopPlayback() {
audioPlayer?.stop()
isPlaying = false
}
// Record audio
func startRecording() throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.record, mode: .default)
try session.setActive(true)
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let audioFilename = documentsPath.appendingPathComponent("\(UUID().uuidString).m4a")
recordingURL = audioFilename
let settings: [String: Any] = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 44100,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
]
audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
audioRecorder?.isMeteringEnabled = true
audioRecorder?.record()
isRecording = true
}
func stopRecording() -> URL? {
audioRecorder?.stop()
isRecording = false
return recordingURL
}
// Get audio levels for visualization
func currentLevel() -> Float {
audioRecorder?.updateMeters()
return audioRecorder?.averagePower(forChannel: 0) ?? -160
}
}
AVCaptureSession — Camera and Video Capture
import AVFoundation
import UIKit
class CameraManager: NSObject {
let captureSession = AVCaptureSession()
private var photoOutput = AVCapturePhotoOutput()
private var videoOutput = AVCaptureMovieFileOutput()
private var currentCamera: AVCaptureDevice.Position = .back
var photoCaptureCompletion: ((UIImage?) -> Void)?
func setupSession() {
captureSession.beginConfiguration()
captureSession.sessionPreset = .photo
// Add camera input
guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
let input = try? AVCaptureDeviceInput(device: camera),
captureSession.canAddInput(input)
else { return }
captureSession.addInput(input)
// Add photo output
guard captureSession.canAddOutput(photoOutput) else { return }
captureSession.addOutput(photoOutput)
photoOutput.isHighResolutionCaptureEnabled = true
captureSession.commitConfiguration()
}
func startSession() {
guard !captureSession.isRunning else { return }
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.captureSession.startRunning()
}
}
func stopSession() {
guard captureSession.isRunning else { return }
captureSession.stopRunning()
}
func capturePhoto() {
let settings = AVCapturePhotoSettings()
settings.flashMode = .auto
photoOutput.capturePhoto(with: settings, delegate: self)
}
func switchCamera() {
captureSession.beginConfiguration()
// Remove current input
if let currentInput = captureSession.inputs.first as? AVCaptureDeviceInput {
captureSession.removeInput(currentInput)
}
// Add new camera
currentCamera = currentCamera == .back ? .front : .back
guard let newCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: currentCamera),
let newInput = try? AVCaptureDeviceInput(device: newCamera),
captureSession.canAddInput(newInput)
else { return }
captureSession.addInput(newInput)
captureSession.commitConfiguration()
}
// Check camera permission
static func requestPermission() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
return true
case .notDetermined:
return await AVCaptureDevice.requestAccess(for: .video)
default:
return false
}
}
}
extension CameraManager: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
guard let data = photo.fileDataRepresentation(),
let image = UIImage(data: data)
else {
photoCaptureCompletion?(nil)
return
}
photoCaptureCompletion?(image)
}
}
// Camera preview layer for UIKit
class CameraPreviewView: UIView {
override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
var previewLayer: AVCaptureVideoPreviewLayer {
layer as! AVCaptureVideoPreviewLayer
}
func configure(session: AVCaptureSession) {
previewLayer.session = session
previewLayer.videoGravity = .resizeAspectFill
}
}
AVAsset and AVAssetExportSession
// Get video metadata
func getVideoInfo(url: URL) async throws -> (duration: TimeInterval, size: CGSize) {
let asset = AVURLAsset(url: url)
let duration = try await asset.load(.duration)
let tracks = try await asset.loadTracks(withMediaType: .video)
let size = try await tracks.first?.load(.naturalSize) ?? .zero
return (duration.seconds, size)
}
// Export/compress video
func compressVideo(inputURL: URL, outputURL: URL) async throws {
let asset = AVURLAsset(url: inputURL)
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetMediumQuality) else {
throw ExportError.sessionCreationFailed
}
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
await exportSession.export()
if let error = exportSession.error {
throw error
}
}
// Trim video
func trimVideo(inputURL: URL, outputURL: URL, startTime: TimeInterval, endTime: TimeInterval) async throws {
let asset = AVURLAsset(url: inputURL)
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
throw ExportError.sessionCreationFailed
}
let start = CMTime(seconds: startTime, preferredTimescale: 600)
let end = CMTime(seconds: endTime, preferredTimescale: 600)
exportSession.timeRange = CMTimeRange(start: start, end: end)
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
await exportSession.export()
}
// Generate thumbnail
func generateThumbnail(url: URL, at time: TimeInterval) async throws -> UIImage {
let asset = AVURLAsset(url: url)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
generator.maximumSize = CGSize(width: 320, height: 320)
let cmTime = CMTime(seconds: time, preferredTimescale: 600)
let (image, _) = try await generator.image(at: cmTime)
return UIImage(cgImage: image)
}
enum ExportError: Error {
case sessionCreationFailed
}
Audio Session Configuration
import AVFoundation
class AudioSessionManager {
// Playback only (music, podcasts)
static func configureForPlayback() throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .default, options: [])
try session.setActive(true)
}
// Recording
static func configureForRecording() throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.record, mode: .default, options: [])
try session.setActive(true)
}
// Play and record simultaneously (voice chat)
static func configureForVoiceChat() throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .voiceChat, options: [
.defaultToSpeaker,
.allowBluetooth,
])
try session.setActive(true)
}
// Handle interruptions (phone calls)
static func observeInterruptions(handler: @escaping (Bool) -> Void) {
NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: AVAudioSession.sharedInstance(),
queue: .main
) { notification in
guard let typeValue = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
handler(type == .began)
}
}
}
Now Playing Info and Remote Commands
import MediaPlayer
class NowPlayingManager {
static let shared = NowPlayingManager()
func setupRemoteCommands(
onPlay: @escaping () -> Void,
onPause: @escaping () -> Void,
onNext: @escaping () -> Void,
onPrevious: @escaping () -> Void
) {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { _ in
onPlay()
return .success
}
commandCenter.pauseCommand.addTarget { _ in
onPause()
return .success
}
commandCenter.nextTrackCommand.addTarget { _ in
onNext()
return .success
}
commandCenter.previousTrackCommand.addTarget { _ in
onPrevious()
return .success
}
}
func updateNowPlaying(title: String, artist: String, duration: TimeInterval, currentTime: TimeInterval, artwork: UIImage?) {
var info = [String: Any]()
info[MPMediaItemPropertyTitle] = title
info[MPMediaItemPropertyArtist] = artist
info[MPMediaItemPropertyPlaybackDuration] = duration
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
if let image = artwork {
info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
}