Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d3b0245
note on how to fix NSCameraUsageDescription requirement
giomurru Jun 3, 2022
ba2c558
fix wrong color profile while recording on some devices
giomurru Jun 3, 2022
0404b22
move description of NSCameraUsageDescription fix to README
giomurru Jun 3, 2022
5e81cf9
modify instructions on how to fix NSCameraUsageDescription
giomurru Jun 3, 2022
5de693b
fix typo and bold for known issue:
giomurru Jun 3, 2022
c1ed960
change priority of queue to userInteractive to solve lag problem on s…
giomurru Jun 7, 2022
8535f93
Merge branch 'master' into spherualizer
giomurru Jul 26, 2022
963e935
revert disable videoColorProperties
giomurru Jul 26, 2022
e63b55a
disable videoColorProperties for iOS 12 and lower
giomurru Jul 27, 2022
80dd0e5
Merge remote-tracking branch 'upstream/master' into spherualizer
giomurru Jun 16, 2023
f21c225
reverse changes from original repository
giomurru Jun 27, 2023
43c346e
restore wrongly deleted line
giomurru Jun 27, 2023
12aea14
Merge branch 'master' into spherualizer
giomurru Nov 30, 2023
0eab640
align branch with stable v2.8.1
giomurru Dec 8, 2023
81d1d64
Increase priority of queues to userInteractive.
giomurru Dec 11, 2023
7215150
Add option to include metadata in VideoSettings
giomurru Dec 21, 2023
ee15d88
Merge commit by polycamnick on original repo named `obs->scnobs name …
giomurru Sep 17, 2024
c5bd152
Audio is recording! But audio can't be heard while recording.
giomurru Nov 20, 2024
54333b4
Fix issue that prevented the audio to be heard while recording.
giomurru Nov 20, 2024
3fb9188
Refactoring the class. Change name of the class and rename CMTimeRang…
giomurru Nov 20, 2024
bdb9acb
Remove the reference to the playerItem.
giomurru Nov 20, 2024
28eba16
Pause video before finish recording to avoid black frames at the end …
giomurru Mar 25, 2025
ac90833
Just add some comments explaining what the swizzled_nextDrawable meth…
giomurru Jun 10, 2025
3a44264
Merge branch 'master' into spherualizer
giomurru Jul 2, 2025
35269fb
Merge branch 'AudioEngine.VideoPlayer.2' into spherualizer
giomurru Jul 2, 2025
b101540
Adjustments to align to master branch v2.9.0
giomurru Jul 2, 2025
ba9edbe
Remove the unsafe pause that was introduced to solve the black frame …
giomurru Jul 3, 2025
7e8009e
End the session before finishWriting to avoid black frames at the end…
giomurru Jul 3, 2025
cd7257b
Remove use of Unmanaged in MTAudioProcessingTap to fix MTAudioProcess…
giomurru Oct 1, 2025
21e3c4b
Update minimum iOS version to 13
HassanTaleb90 Jan 29, 2026
c755e5a
Fix AVPlayer audio tap crash and improve AudioEngine processing forma…
HassanTaleb90 Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PackageDescription

let package = Package(
name: "SCNRecorder",
platforms: [ .iOS(.v12) ],
platforms: [ .iOS(.v13) ],
products: [
.library(
name: "SCNRecorder",
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ override func viewDidLoad() {
}
```

**Known issue:** Capturing audio requires that you provide a valid NSCameraUsageDescription key in the InfoPlist when you submit your app to appstoreconnect. If you are not using the audio recorder, you can exclude the `AVCaptureSession+BaseRecorder.swift` extension from the Target Membership. Doing that fixes appstoreconnect asking for NSCameraUsageDescription key.

### Music Overlay

Instead of capturing audio using microphone you can play music and add it to video at the same time.
Expand Down
1 change: 1 addition & 0 deletions SCNRecorder.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Pod::Spec.new do |s|
s.module_name = 'SCNRecorder'
s.swift_version = '5.0'
s.source_files = 'Sources/**/*.{swift}'
s.exclude_files = 'Sources/Extensions/AVCaptureSession+BaseRecorder.swift'
s.dependency 'MTDMulticastDelegate'

s.app_spec 'Example' do |app_spec|
Expand Down
232 changes: 232 additions & 0 deletions Sources/AudioEngine/AudioEngine.AVPlayerTapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import Foundation
import AVFoundation

extension AudioEngine {
public final class AVPlayerTapper {
private weak var player: AVPlayer?
private var audioFormat: AVAudioFormat? {
AVAudioFormat(commonFormat: .pcmFormatFloat32,
sampleRate: 44100,
channels: 2,
interleaved: false)
}

@SCNObservable public internal(set) var error: Swift.Error?

public weak var recorder: BaseRecorder? {
didSet {
oldValue?.audioInput.audioFormat = nil
guard let recorder = recorder else {
removeAudioTap()
return
}

recorder.audioInput.audioFormat = audioFormat

guard oldValue == nil else { return }
removeAudioTap()
setupAudioTap()
}
}

deinit {
recorder = nil
}

public init(player: AVPlayer) {
self.player = player
guard player.currentItem != nil else {
fatalError("FATAL: Player item is not initialized.")
}
}

private func setupAudioTap() {
// Create an AVMutableAudioMix
let audioMix = AVMutableAudioMix()

// Get the first audio track
guard let audioTrack = player?.currentItem?.asset.tracks(withMediaType: .audio).first else {
print("ERROR: No audio track found")
return
}

// Create AVMutableAudioMixInputParameters for the track
let inputParams = AVMutableAudioMixInputParameters(track: audioTrack)

// Install tap
inputParams.setVolume(1.0, at: .zero)
inputParams.audioTapProcessor = createAudioTapProcessor()

audioMix.inputParameters = [inputParams]

// Set the audio mix to the player item
player?.currentItem?.audioMix = audioMix
}

private func removeAudioTap() {
player?.currentItem?.audioMix = nil
}

private func createAudioTapProcessor() -> MTAudioProcessingTap {
var callbacks = MTAudioProcessingTapCallbacks(
version: kMTAudioProcessingTapCallbacksVersion_0,
clientInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
init: tapInitCallback,
finalize: tapFinalizeCallback,
prepare: tapPrepareCallback,
unprepare: tapUnprepareCallback,
process: tapProcessCallback
)

var tap: MTAudioProcessingTap?
let status = MTAudioProcessingTapCreate(kCFAllocatorDefault, &callbacks, kMTAudioProcessingTapCreationFlag_PostEffects, &tap)

if status != noErr {
fatalError("FATAL: creating MTAudioProcessingTap: \(status)")
}

return tap!
}

// Define the TapContext class
class TapContext {
var processingFormat: AVAudioFormat?
weak var selfInstance: AudioEngine.AVPlayerTapper?
}

// MTAudioProcessingTap callbacks
private let tapInitCallback: MTAudioProcessingTapInitCallback = { (tap, clientInfo, tapStorageOut) in
// Initialization code
let context = TapContext()
context.selfInstance = Unmanaged<AudioEngine.AVPlayerTapper>.fromOpaque(clientInfo!).takeUnretainedValue()
tapStorageOut.pointee = Unmanaged.passRetained(context).toOpaque()
}

private let tapFinalizeCallback: MTAudioProcessingTapFinalizeCallback = { (tap) in
// Finalization code
let storage = MTAudioProcessingTapGetStorage(tap)
Unmanaged<TapContext>.fromOpaque(storage).release()
}

private let tapPrepareCallback: MTAudioProcessingTapPrepareCallback = { (tap, maxFrames, processingFormat) in
// Prepare code
let storage = MTAudioProcessingTapGetStorage(tap)
let context = Unmanaged<TapContext>.fromOpaque(storage).takeUnretainedValue()
// Save the processing format
var asbd = processingFormat.pointee
context.processingFormat = AVAudioFormat(streamDescription: &asbd)
}

private let tapUnprepareCallback: MTAudioProcessingTapUnprepareCallback = { (tap) in
// Unprepare code if needed
}

private let tapProcessCallback: MTAudioProcessingTapProcessCallback = {
(tap, numberFrames, flags, bufferListInOut, numberFramesOut, flagsOut) in

let storage = MTAudioProcessingTapGetStorage(tap)
let context = Unmanaged<TapContext>.fromOpaque(storage).takeUnretainedValue()

guard let selfInstance = context.selfInstance else {
print("ERROR: tapProcessCallback: selfInstance not available")
return
}

var status = noErr
var tapFlags: MTAudioProcessingTapFlags = 0
var numFrames = numberFrames
var timeRangeOut = CMTimeRange()

// Get source audio
status = MTAudioProcessingTapGetSourceAudio(
tap,
numberFrames,
bufferListInOut,
&tapFlags,
&timeRangeOut,
&numFrames
)

if status != noErr {
print("ERROR: tapProcessCallback: getting source audio, status: \(status)")
return
}

numberFramesOut.pointee = numFrames
flagsOut.pointee = tapFlags

let bufferListPtr = UnsafeMutableAudioBufferListPointer(bufferListInOut)

// MARK: - SAFE FORMAT HANDLING
let processingFormat: AVAudioFormat

if let format = context.processingFormat {
processingFormat = format
} else {
let channels = min(2, bufferListPtr.count)

guard let fallback = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: 44100,
channels: AVAudioChannelCount(channels),
interleaved: false
) else {
print("ERROR: cannot create fallback format")
return
}

context.processingFormat = fallback
processingFormat = fallback
}

let audioTime = AVAudioTime(hostTime: mach_absolute_time())

guard let pcmBuffer = AVAudioPCMBuffer(
pcmFormat: processingFormat,
frameCapacity: AVAudioFrameCount(numFrames)
) else {
return
}

pcmBuffer.frameLength = AVAudioFrameCount(numFrames)

// MARK: - SAFE CHANNEL COPY (FIXED CRASH HERE)
let dstChannels = Int(processingFormat.channelCount)
let srcChannels = bufferListPtr.count
let channelsToCopy = min(dstChannels, srcChannels)

for i in 0..<channelsToCopy {

let srcBuffer = bufferListPtr[i]

guard let src = srcBuffer.mData,
let dst = pcmBuffer.floatChannelData?[i] else {
continue
}

let byteSize = min(
Int(srcBuffer.mDataByteSize),
Int(pcmBuffer.frameCapacity) * MemoryLayout<Float>.size
)

memcpy(dst, src, byteSize)
}

// MARK: - SEND TO RECORDER
do {
let sampleBuffer = try AudioEngine.createAudioSampleBuffer(
from: pcmBuffer,
time: audioTime
)

selfInstance.recorder?.audioInput.audioEngine(
didOutputAudioSampleBuffer: sampleBuffer
)

} catch {
print("ERROR: tapProcessCallback: failed to create sample buffer: \(error)")
selfInstance.error = error
}
}
}
}
Loading