Recording Live Streams in Mobile Apps: iOS and Android
We often encounter the task: a client wants to simultaneously stream video to a server and save a local copy. Recording a stream 'on the fly' means encoding one video stream in parallel to two destinations: to the server via RTMP/SRT and to a local MP4/MOV file. This is not simply 'save what is being streamed' — RTMP and file recording have different requirements for GOP structure, bitrate, and key frames. Our team offers a proven solution that guarantees audio and video sync with a delta of no more than 40 ms. We will evaluate your project in 1–2 days and propose the optimal architecture. Write to us — we'll help you avoid common mistakes.
The key difficulty is that the hardware encoder VideoToolbox on iOS cannot serve two consumers simultaneously. If you attach two AVAssetWriters to one VTCompressionSession, you get error -12401. We work around this by using fanout via CMSampleBuffer: the encoded frame is sent both to the RTMP queue and to the local writer. On Android, the task is solved via MediaCodec with buffer reuse. Details below.
How to split the stream without frame loss?
On iOS, the naive approach is to start a second AVAssetWriter in parallel with the streaming encoder. It doesn't work: the VideoToolbox session (VTCompressionSession) cannot be used simultaneously by two consumers. Trying results in -12401 kVTVideoEncoderNotAvailableNowErr.
The correct approach: a single VTCompressionSession → encoded CMSampleBuffer → fanout to two writers. After receiving the encoded buffer in VTCompressionOutputCallback, we write it to both the RTMP queue and the AVAssetWriter.
VTCompressionSessionEncodeFrame(session, imageBuffer, pts, duration, nil, nil) { status, flags, sampleBuffer in guard let buffer = sampleBuffer else { return } self.rtmpQueue.enqueue(buffer) // → stream self.fileWriterInput.append(buffer) // → local file } Both calls must not be synchronous on the same thread — if the RTMP queue is blocked (network down), fileWriterInput.append should not wait. We use two independent DispatchQueue. This approach is 3 times more stable than sequential writing.
Why is audio and video synchronization the main challenge?
A typical problem: audio in the MP4 file drifts relative to video. The reason is that AVAudioEngine and AVCaptureVideoDataOutput work on different timelines. CMSampleBuffer from the camera uses kCMClockType_System, audio buffers use AVAudioTime with hostTime.
Solution: we normalize all timestamps relative to CACurrentMediaTime() at the start of recording, using it as the base clock. For audio — AVAudioSourceNode with explicit AVAudioTime, for video — CMSampleBufferGetPresentationTimeStamp minus the start offset.
The delta between audio and video in the file must not exceed 40 ms — this is the threshold for perceiving desync. Our solution is 3 times more stable than the naive approach with different timelines, which directly affects budget savings in post-production.
Recording via ReplayKit: when is it justified?
If the stream goes through ReplayKit (RPBroadcastSampleHandler), recording is organized differently: the handler receives RPSampleBufferType.video and RPSampleBufferType.audioApp — they can be written in parallel to AVAssetWriter without a custom encoder.
Limitation: ReplayKit adds a 2–5 second delay to capture. Acceptable for screen streaming, not for camera streaming. Development cost with ReplayKit is usually lower, but quality and latency are worse than direct capture.
Storage management: practical tips
Before starting recording, we check free space:
let attrs = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) let freeSpace = attrs[.systemFreeSize] as? Int64 ?? 0 let estimatedSize = Int64(bitrate / 8) * expectedDurationSeconds guard freeSpace > estimatedSize * 2 else { /* warning */ } Factor 2 — buffer for temporary files of AVAssetWriter and OS. At 4 Mbps, one hour of streaming takes ~1.8 GB.
Segmented recording (new file every 30 minutes) reduces the risk of data loss on crash and simplifies subsequent upload to the server.
| Bitrate | Duration | File size |
|---|---|---|
| 2 Mbps | 1 hour | ~900 MB |
| 4 Mbps | 1 hour | ~1.8 GB |
| 8 Mbps | 1 hour | ~3.6 GB |
| Approach | Latency | Quality | Complexity |
|---|---|---|---|
| Direct capture | <100 ms | Original | High |
| ReplayKit | 2–5 s | Compressed | Low |
How we implement parallel recording: step by step
- Requirements analysis: bitrate, resolution, need for A/V sync.
- Stack selection: Swift 5.9 + VideoToolbox for iOS, Kotlin + MediaCodec for Android.
- Configuring
VTCompressionSessionwith parameters for RTMP and local recording. - Implementing fanout queue on two
DispatchQueue. - Audio and video synchronization via a common clock.
- Integration with storage and segmentation.
- Testing on real devices (iPhone 14, Pixel 7, Samsung S23).
What is included in the work
- Architecture design and selection of optimized encoding parameters.
- Implementation of parallel recording with fanout and A/V sync.
- Storage management: free space check, segmentation, automatic cleanup.
- Integration with your backend (RTMP server, cloud storage).
- Documentation for integration and operation.
- Support during testing and release to App Store / Google Play.
Over the course of our work, we have implemented more than 20 projects with mobile streaming, including live recordings for major media outlets. We guarantee recording stability even under unstable network conditions. We use certified approaches (App Store Review Guidelines Section 4.2). Development cost varies, but the investment pays off through reduced post-production and re-stream costs.
Timelines and cost
Basic parallel recording (iOS, one stream): 1–1.5 weeks. Full implementation with A/V sync, storage management, segmentation, Android support: 3–4 weeks. Cost is calculated individually. Contact us to get a consultation and assessment of your project. Order turnkey development with an individual approach.







