I am recording the screen and I want to combine the mic audio and the sounds from the app's audio into a video with ONE stero audio track. With the AVAssetWriter
setup I have, it creates a video file with TWO separate audio tracks; one stereo track for device audio and one mono track for the mic audio. This is no good.
I've also tried taking the resulting video file & reconstructing a NEW video file with the separate audio AVAssetTrack
s merged into one, using AVMutableCompositionTrack
s insertTimeRange(
function as you will see below. But this does NOT merge the tracks, no matter what I try, it just concatenates them (in sequence, not overlayed over oneanother).
Please can someone tell me how I can either record the tracks merged in the first place with AVAssetWriter. Or how to merge them over eachother later. There is nothing online that discusses this and gets it done. Many articles refer to the use of insertTimeRange(
but this function CONCATENATES the tracks. Please help.
The code I'm using so far:
func startRecording(withFileName fileName: String, recordingHandler: @escaping (Error?) -> Void) {
let sharedRecorder = RPScreenRecorder.shared()
currentlyRecordingURL = URL(fileURLWithPath: CaptureArchiver.filePath(fileName))
guard currentlyRecordingURL != nil else { return }
desiredMicEnabled = RPScreenRecorder.shared().isMicrophoneEnabled
assetWriter = try! AVAssetWriter(outputURL: currentlyRecordingURL!, fileType: AVFileType.mp4)
let appAudioOutputSettings = [
AVFormatIDKey : kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey : 2,
AVSampleRateKey : 44100.0,
AVEncoderBitRateKey: 192000
] as [String : Any]
let micAudioOutputSettings = [
AVFormatIDKey : kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey : 1,
AVSampleRateKey : 44100.0,
AVEncoderBitRateKey: 192000
] as [String : Any]
let adjustedWidth = ceil(UIScreen.main.bounds.size.width/4)*4
let videoOutputSettings: Dictionary<String, Any> = [
AVVideoCodecKey : AVVideoCodecType.h264,
AVVideoWidthKey : adjustedWidth,
AVVideoHeightKey : UIScreen.main.bounds.size.height
]
let audioInput_app = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: appAudioOutputSettings)
audioInput_app.expectsMediaDataInRealTime = true
if assetWriter.canAdd(audioInput_app) { assetWriter.add(audioInput_app) }
self.audioInput_app = audioInput_app
let audioInput_mic = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: micAudioOutputSettings)
audioInput_mic.expectsMediaDataInRealTime = true
if assetWriter.canAdd(audioInput_mic) { assetWriter.add(audioInput_mic) }
self.audioInput_mic = audioInput_mic
let videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoOutputSettings)
videoInput.expectsMediaDataInRealTime = true
if assetWriter.canAdd(videoInput) { assetWriter.add(videoInput) }
self.videoInput = videoInput
RPScreenRecorder.shared().startCapture(handler: { [unowned self] (sample, bufferType, error) in
if CMSampleBufferDataIsReady(sample) {
DispatchQueue.main.async { [unowned self] in
if self.assetWriter.status == AVAssetWriter.Status.unknown {
self.assetWriter.startWriting()
#if DEBUG
let status = self.assetWriter.status
log(self, message: "LAUNCH assetWriter.status[\(status.rawValue)]:\(String(describing: self.readable(status)))")
#endif
self.assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sample))
} else if self.assetWriter.status == AVAssetWriter.Status.failed {
recordingHandler(error)
return
} else {
switch bufferType {
case .audioApp:
if let audioInput_app = self.audioInput_app {
if audioInput_app.isReadyForMoreMediaData { audioInput_app.append(sample) }
}
case .audioMic:
if let audioInput_mic = self.audioInput_mic {
if audioInput_mic.isReadyForMoreMediaData { audioInput_mic.append(sample) }
}
case .video:
if let videoInput = self.videoInput {
if videoInput.isReadyForMoreMediaData { videoInput.append(sample) }
}
@unknown default:
fatalError("Unknown RPSampleBufferType:\(bufferType)")
}
}
}
}
}) { [unowned self] (error) in
recordingHandler(error)
if error == nil && self.desiredMicEnabled == true && RPScreenRecorder.shared().isMicrophoneEnabled == false {
self.viewController.mic_cap_denied = true
} else {
self.viewController.mic_cap_denied = false
}
}
}
func mergeAudioTracksInVideo(_ videoURL: URL, completion: @escaping ((Bool) -> Void)) {
let sourceAsset = AVURLAsset(url: videoURL)
let sourceVideoTrack: AVAssetTrack = sourceAsset.tracks(withMediaType: AVMediaType.video)[0]
let sourceAudioTrackApp: AVAssetTrack = sourceAsset.tracks(withMediaType: AVMediaType.audio)[0]
let sourceAudioTrackMic: AVAssetTrack = sourceAsset.tracks(withMediaType: AVMediaType.audio)[1]
let comp: AVMutableComposition = AVMutableComposition()
guard let newVideoTrack: AVMutableCompositionTrack = comp.addMutableTrack(withMediaType: AVMediaType.video,
preferredTrackID: kCMPersistentTrackID_Invalid) else {
completion(false)
return
}
newVideoTrack.preferredTransform = sourceVideoTrack.preferredTransform
guard let newAudioTrack: AVMutableCompositionTrack = comp.addMutableTrack(withMediaType: AVMediaType.audio,
preferredTrackID: kCMPersistentTrackID_Invalid) else {
completion(false)
return
}
//THE MIXING //THIS STILL RESULTS IN TWO SEPARATE AUDIO TRACKS //LOOKS LIKE THIS IS MORE ABOUT VOLUME LEVELS
let mix = AVMutableAudioMix()
let audioMixInputParamsMic = AVMutableAudioMixInputParameters()
audioMixInputParamsMic.trackID = sourceAudioTrackMic.trackID
audioMixInputParamsMic.setVolume(1.0, at: CMTime.zero)
let audioMixInputParamsApp = AVMutableAudioMixInputParameters()
audioMixInputParamsApp.trackID = sourceAudioTrackApp.trackID
audioMixInputParamsApp.setVolume(1.0, at: CMTime.zero)
mix.inputParameters.append(audioMixInputParamsMic)
mix.inputParameters.append(audioMixInputParamsApp)
///////
let timeRange: CMTimeRange = CMTimeRangeMake(start: CMTime.zero, duration: sourceAsset.duration)
do {
try newVideoTrack.insertTimeRange(timeRange, of: sourceVideoTrack, at: CMTime.zero)
try newAudioTrack.insertTimeRange(timeRange, of: sourceAudioTrackMic, at: CMTime.zero)
try newAudioTrack.insertTimeRange(timeRange, of: sourceAudioTrackApp, at: CMTime.zero)
} catch {
completion(false)
return
}
let exporter: AVAssetExportSession = AVAssetExportSession(asset: comp, presetName: AVAssetExportPresetHighestQuality)!
exporter.audioMix = mix
exporter.outputFileType = AVFileType.mp4
exporter.outputURL = videoURL
removeFileAtURLIfExists(url: videoURL)
exporter.exportAsynchronously(completionHandler: {
switch exporter.status {
case AVAssetExportSession.Status.failed:
#if DEBUG
log(self, message: "1000000000failed \(String(describing: exporter.error))")
#endif
case AVAssetExportSession.Status.cancelled:
#if DEBUG
log(self, message: "1000000000cancelled \(String(describing: exporter.error))")
#endif
case AVAssetExportSession.Status.unknown:
#if DEBUG
log(self, message: "1000000000unknown\(String(describing: exporter.error))")
#endif
case AVAssetExportSession.Status.waiting:
#if DEBUG
log(self, message: "1000000000waiting\(String(describing: exporter.error))")
#endif
case AVAssetExportSession.Status.exporting:
#if DEBUG
log(self, message: "1000000000exporting\(String(describing: exporter.error))")
#endif
default:
#if DEBUG
log(self, message: "1000000000-----Mutable video exportation complete.")
#endif
}
completion(true)
})
}