diff --git a/Sources/VideoIO/MultitrackMovieRecorder.swift b/Sources/VideoIO/MultitrackMovieRecorder.swift index e673403..beeebc8 100644 --- a/Sources/VideoIO/MultitrackMovieRecorder.swift +++ b/Sources/VideoIO/MultitrackMovieRecorder.swift @@ -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