Skip to content
Open
Changes from all commits
Commits
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
60 changes: 44 additions & 16 deletions Sources/VideoIO/MultitrackMovieRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,32 +300,60 @@ public final class MultitrackMovieRecorder {
audioSettings = [AVFormatIDKey : kAudioFormatMPEG4AAC]
}

// Populate sample rate and channel count from the format description.
// Guard against zero values that can occur when an external USB audio
// device is connected before app launch, causing AVCaptureSession to
// initialise audio in a degraded state with an incomplete ASBD.
if let currentASBD = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) {
audioSettings[AVSampleRateKey] = currentASBD.pointee.mSampleRate
audioSettings[AVNumberOfChannelsKey] = currentASBD.pointee.mChannelsPerFrame
let sampleRate = currentASBD.pointee.mSampleRate
let channelCount = currentASBD.pointee.mChannelsPerFrame
audioSettings[AVSampleRateKey] = sampleRate > 0 ? sampleRate : Double(44100)
audioSettings[AVNumberOfChannelsKey] = channelCount > 0 ? channelCount : UInt32(1)
} else {
// ASBD unavailable — fall back to safe mono defaults.
audioSettings[AVSampleRateKey] = Double(44100)
audioSettings[AVNumberOfChannelsKey] = UInt32(1)
}


// Only set AVChannelLayoutKey when real layout data is present.
// Setting it to Data() (empty) causes canApply(outputSettings:forMediaType:audio)
// to return false, which throws cannotSetupAudioInputs. This happens when
// CMAudioFormatDescriptionGetChannelLayout returns nil — e.g. when an external
// USB audio device was connected before launch and AVCaptureSession initialised
// audio without a channel layout. Per Apple docs, omitting AVChannelLayoutKey
// is valid for ≤2 channels; AVAssetWriter infers mono/stereo from
// AVNumberOfChannelsKey.
var aclSize: Int = 0
let currentChannelLayout = CMAudioFormatDescriptionGetChannelLayout(formatDescription, sizeOut: &aclSize)
let currentChannelLayoutData: Data
if let currentChannelLayout = currentChannelLayout, aclSize > 0 {
currentChannelLayoutData = Data(bytes: currentChannelLayout, count: aclSize)
} else {
currentChannelLayoutData = Data()
audioSettings[AVChannelLayoutKey] = Data(bytes: currentChannelLayout, count: aclSize)
}
audioSettings[AVChannelLayoutKey] = currentChannelLayoutData

if self.assetWriter.canApply(outputSettings: audioSettings, forMediaType: .audio) {
let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings, sourceFormatHint: formatDescription)

func makeAndAddAudioInput(settings: [String: Any], hint: CMFormatDescription?) throws {
let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: settings, sourceFormatHint: hint)
audioInput.expectsMediaDataInRealTime = true
if self.assetWriter.canAdd(audioInput) {
self.assetWriter.add(audioInput)
audioInputs.append(audioInput)
} else {
guard self.assetWriter.canAdd(audioInput) else {
throw RecorderError.cannotSetupAudioInputs
}
self.assetWriter.add(audioInput)
audioInputs.append(audioInput)
}

if self.assetWriter.canApply(outputSettings: audioSettings, forMediaType: .audio) {
try makeAndAddAudioInput(settings: audioSettings, hint: formatDescription)
} else {
throw RecorderError.cannotSetupAudioInputs
// Last resort: minimal known-good settings that AVAssetWriter always accepts
// for AAC output, used only if the format-description-derived values are
// still rejected (e.g. unsupported sample rate from a degraded device).
let safeSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: Double(44100),
AVNumberOfChannelsKey: UInt32(1)
]
guard self.assetWriter.canApply(outputSettings: safeSettings, forMediaType: .audio) else {
throw RecorderError.cannotSetupAudioInputs
}
try makeAndAddAudioInput(settings: safeSettings, hint: nil)
}
}
self.audioInputs = audioInputs
Expand Down