civil-and-structural-engineering
Creating a Custom Video Recording Interface in Ios Using Avfoundation
Table of Contents
Building a Fully Custom Video Recording Interface with AVFoundation
Apple’s AVFoundation framework gives iOS developers complete control over audio and video capture. While the system camera app works for basic needs, many applications require a unique recording experience – think custom filters, branded overlays, manual controls, or non‑standard aspect ratios. This article walks through the complete process of building a production‑ready custom video recording interface from scratch, covering session configuration, live preview, custom UI elements, recording logic, permissions handling, and post‑capture workflows.
Understanding the Core Components of AVFoundation
Before writing any code, it helps to understand the key classes you’ll work with:
- AVCaptureSession – The central orchestrator that routes media data from inputs to outputs. You configure its preset (e.g.,
.high,.1920x1080) to define capture quality. - AVCaptureDevice – Represents a physical camera or microphone. You query for devices (back, front, wide‑angle, telephoto) and configure properties like focus and exposure.
- AVCaptureDeviceInput – Wraps an
AVCaptureDeviceso it can be added to the session. - AVCaptureVideoPreviewLayer – A
CALayersubclass that renders the live camera feed in your UI. It must be added to your view’s layer hierarchy and configured with the session. - AVCaptureMovieFileOutput – The simplest way to write video to a file. For more control (e.g., custom audio tracks), you can use
AVCaptureVideoDataOutputandAVCaptureAudioDataOutput in combination withAVAssetWriter.
Apple’s official AVFoundation documentation provides exhaustive detail, but for this guide we focus on practical implementation.
Setting Up the Capture Session
Creating the session is the first step. You’ll add a camera input and a microphone input, then add a video output. Here’s a structured setup:
import AVFoundation
class CameraController {
private var session: AVCaptureSession?
private var previewLayer: AVCaptureVideoPreviewLayer?
private var movieOutput: AVCaptureMovieFileOutput?
func configureSession() {
session = AVCaptureSession()
session?.sessionPreset = .high
guard let session = session else { return }
// Add camera input (back camera)
guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
let cameraInput = try? AVCaptureDeviceInput(device: camera),
session.canAddInput(cameraInput) else {
print("Unable to add camera input")
return
}
session.addInput(cameraInput)
// Add microphone input
guard let microphone = AVCaptureDevice.default(for: .audio),
let audioInput = try? AVCaptureDeviceInput(device: microphone),
session.canAddInput(audioInput) else {
print("Unable to add audio input")
return
}
session.addInput(audioInput)
// Add movie file output
movieOutput = AVCaptureMovieFileOutput()
guard let movieOutput = movieOutput, session.canAddOutput(movieOutput) else {
print("Unable to add movie output")
return
}
session.addOutput(movieOutput)
// Configure preview layer
previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer?.videoGravity = .resizeAspectFill
session.startRunning()
}
}
Always call startRunning() on a background queue to avoid blocking the main thread. You can use DispatchQueue.global(qos: .userInitiated).async.
Selecting the Right Session Preset
The sessionPreset defines the output resolution and quality. Options include .low, .medium, .high, .hd1920x1080, .hd4K3840x2160, and more. Note that the preset must be compatible with the added inputs. If you need custom resolutions (e.g., square or ultra‑wide), you can use AVCaptureDevice.Format to override the preset.
Displaying a Live Camera Preview
To show the camera feed, you add the AVCaptureVideoPreviewLayer to your view’s layer. In UIKit, this typically goes inside a container view:
class PreviewView: UIView {
override class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
// Usage in your view controller
let previewView = PreviewView(frame: view.bounds)
view.addSubview(previewView)
previewView.videoPreviewLayer.session = session
previewView.videoPreviewLayer.videoGravity = .resizeAspectFill
The preview automatically updates as the session runs. You can change the videoGravity to .resizeAspect (letterboxed) or .resize (stretched).
Designing a Custom User Interface
With the live feed on screen, you can layer custom controls on top. Typical elements include:
- Record / Stop button – A large circular button that changes color (red when recording, white when idle).
- Camera switch button – Toggles between front and back cameras.
- Flash toggle – Turns the LED flash on/off (for still photos or video recording).
- Recording timer – A label that updates every second to show elapsed time.
- Resolution selector – A picker or segmented control to change the session preset.
- Focus / exposure indicator – A visual square that appears where the user taps to set focus and exposure.
Example: Record Button with State Handling
class RecordButton: UIButton {
var isRecording = false {
didSet {
backgroundColor = isRecording ? .systemRed : .white
setTitle(isRecording ? "Stop" : "Record", for: .normal)
setTitleColor(isRecording ? .white : .systemRed, for: .normal)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = frame.width / 2
clipsToBounds = true
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
Layout these controls using Auto Layout or a stack view, ensuring they don’t interfere with the preview layer’shitTest. You may also want to add a semi‑transparent gradient view at the bottom of the screen for better readability of the controls.
Recording Video
When the user taps the record button, you start/stop the AVCaptureMovieFileOutput. The output writes to a temporary file URL. After recording finishes, you can move the file to a permanent location or present it in a player.
func toggleRecording() {
guard let movieOutput = movieOutput else { return }
guard let session = session, session.isRunning else { return }
if movieOutput.isRecording {
movieOutput.stopRecording()
recordButton.isRecording = false
timer.invalidate()
} else {
let outputPath = NSTemporaryDirectory() + "\(UUID().uuidString).mov"
let outputURL = URL(fileURLWithPath: outputPath)
movieOutput.startRecording(to: outputURL, recordingDelegate: self)
recordButton.isRecording = true
startTimer()
}
}
Implement AVCaptureFileOutputRecordingDelegate to handle completion and errors:
extension CameraController: AVCaptureFileOutputRecordingDelegate {
func fileOutput(_ output: AVCaptureFileOutput,
didFinishRecordingTo outputFileURL: URL,
from connections: [AVCaptureConnection],
error: Error?) {
if let error = error {
// Handle recording failure (e.g., disk full)
print("Recording error: \(error.localizedDescription)")
return
}
// Success – save to Photos library or present the video
DispatchQueue.main.async {
self.presentVideo(at: outputFileURL)
}
}
}
Managing the Recording Timer
Use a Timer fired every second to update the label. Format elapsed seconds into MM:SS:
var elapsedSeconds = 0
func startTimer() {
elapsedSeconds = 0
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.elapsedSeconds += 1
let minutes = self.elapsedSeconds / 60
let seconds = self.elapsedSeconds % 60
self.timerLabel.text = String(format: "%02d:%02d", minutes, seconds)
}
}
Handling Permissions and User Privacy
Before using camera or microphone, you must request authorization. Handling denial gracefully is critical for a good user experience.
- Camera –
AVCaptureDevice.requestAccess(for: .video) - Microphone –
AVCaptureDevice.requestAccess(for: .audio)
func requestPermissions() {
let group = DispatchGroup()
group.enter()
AVCaptureDevice.requestAccess(for: .video) { granted in
if !granted { print("Camera access denied") }
group.leave()
}
group.enter()
AVCaptureDevice.requestAccess(for: .audio) { granted in
if !granted { print("Microphone access denied") }
group.leave()
}
group.notify(queue: .main) {
self.configureSession()
}
}
If the user denies permission, consider showing a tailored message and a link to Settings using UIApplication.openSettingsURLString. Apple tightly enforces privacy, so never attempt to bypass the permission dialog.
Handling Interruptions (Phone Calls, Alerts)
AVFoundation sessions can be interrupted by phone calls, alarms, or other apps. You need to register for AVCaptureSessionWasInterruptedNotification and AVCaptureSessionInterruptionEndedNotification. When an interruption starts, stop the recording gracefully (if in progress) and update the UI. After interruption ends, restart the session if desired.
NotificationCenter.default.addObserver(
self,
selector: #selector(sessionWasInterrupted(_:)),
name: .AVCaptureSessionWasInterrupted,
object: session
)
@objc func sessionWasInterrupted(_ notification: Notification) {
if movieOutput?.isRecording == true {
movieOutput?.stopRecording()
recordButton.isRecording = false
}
// Show a message: "Recording paused"
}
Adding Manual Focus and Exposure
Professional video apps allow users to tap to set focus and exposure. AVFoundation supports this through AVCaptureDevice methods. First, convert the tap point from view coordinates to preview layer coordinates:
@IBAction func handleTap(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: previewView)
guard let device = currentCameraInput?.device,
let previewLayer = previewLayer else { return }
let pointOfInterest = previewLayer.captureDevicePointConverted(
fromLayerPoint: location
)
do {
try device.lockForConfiguration()
if device.isFocusPointOfInterestSupported {
device.focusPointOfInterest = pointOfInterest
device.focusMode = .autoFocus
}
if device.isExposurePointOfInterestSupported {
device.exposurePointOfInterest = pointOfInterest
device.exposureMode = .autoExpose
}
device.unlockForConfiguration()
} catch {
print("Could not lock device for configuration: \(error)")
}
}
For a polished look, animate a small focus square at the tap location that fades after a second.
Saving Recorded Video to the Photo Library
After a successful recording, you may want to save the video to the user’s Camera Roll. Use the PHPhotoLibrary class from Photos framework. First, request write authorization:
import Photos
func saveVideoToLibrary(_ videoURL: URL) {
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else { return }
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
}) { success, error in
DispatchQueue.main.async {
if success {
// Show success alert
} else if let error = error {
print("Save failed: \(error.localizedDescription)")
}
}
}
}
}
Remember that writing to the Photos library requires the NSPhotoLibraryAddUsageDescription key in Info.plist.
Switching Between Front and Back Cameras
To toggle cameras, you remove the existing camera input and add the new one. Keep in mind that the session must be reconfigured while it’s not running, or you can use beginConfiguration() / commitConfiguration() to change inputs without stopping the session.
func switchCamera() {
guard let session = session else { return }
session.beginConfiguration()
// Remove existing camera input
guard let currentCameraInput = session.inputs.first(where: { input in
guard let input = input as? AVCaptureDeviceInput else { return false }
return input.device.hasMediaType(.video)
}) else { return }
session.removeInput(currentCameraInput)
// Determine new position
let newPosition: AVCaptureDevice.Position = currentCameraInput.device.position == .back ? .front : .back
guard let newCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: newPosition),
let newInput = try? AVCaptureDeviceInput(device: newCamera),
session.canAddInput(newInput) else {
// Fall back to old input
session.addInput(currentCameraInput)
session.commitConfiguration()
return
}
session.addInput(newInput)
session.commitConfiguration()
}
When switching to the front camera, you may also want to mirror the preview layer for a more natural selfie experience. Set videoPreviewLayer.connection?.automaticallyAdjustsVideoMirroring = true.
Advanced Considerations: Custom Video Data Output and Asset Writer
For apps that need real‑time video processing (filters, augmented reality, custom compression), use AVCaptureVideoDataOutput and AVCaptureAudioDataOutput instead of AVCaptureMovieFileOutput. You feed the sample buffers into an AVAssetWriter. This approach gives you full control over the encoding process but adds complexity. A typical pipeline:
- Add
AVCaptureVideoDataOutputand set its delegate to receive sample buffer callbacks. - Add
AVCaptureAudioDataOutputsimilarly. - Create an
AVAssetWriterwith a.movoutput file. - Add an
AVAssetWriterInputfor video and audio with appropriate settings. - In the delegate callbacks, append sample buffers to the writer inputs.
This pattern is essential for integrating Core Image filters or applying custom metadata. For a detailed walkthrough, refer to Ray Wenderlich’s AVFoundation tutorial.
Performance and Battery Optimization
Video capture is resource‑intensive. To keep your app smooth:
- Use the lowest resolution preset that meets your quality requirements.
- Stop the session when the view controller disappears (override
viewWillDisappear). - Prefer
DispatchQueue.global(qos: .background)for sample buffer processing if using data outputs. - Avoid excessive UI updates during recording – batch timer updates or use a custom drawing layer.
- Test on real devices; the simulator does not provide accurate performance metrics for camera work.
Testing Your Implementation
Before shipping, verify these scenarios:
- Permission flow – First launch, denied, allowed then later revoked in Settings.
- Interruptions – Incoming call, alarm, or app switching while recording.
- Save to library – Ensure the video is correctly written and plays back in the Photos app.
- Camera switch – Verify focus and exposure still work after switching.
- Memory leaks – Use Instruments to check that the session and outputs are deallocated.
Conclusion
Building a custom video recording interface with AVFoundation unlocks a world of possibilities beyond Apple’s default camera UI. By mastering session configuration, preview layers, custom controls, recording logic, and permissions handling, you can create a feature‑rich recording experience tailored exactly to your app’s use case. Whether you’re building a social video app, a security camera viewer, or a content creation tool, the techniques covered here form the foundation. Start small, test on real devices, and gradually add polish with focus indicators, flash controls, and advanced processing. For more details, Apple’s AVFoundation Programming Guide and Hacking with Swift offer excellent supplementary reading.