5

Use Case

I'm using iOS 11 Replaykit framework to try to record frames from the screen, and audio from both the app and the microphone.

Problem

Randomly, when I call my .append(sampleBuffer) get AVAssetWriterStatus.failed with the AssetWriter.Error showing

Error Domain=AVFoundationErrorDomain Code=-11823 "Cannot Save" UserInfo={NSLocalizedRecoverySuggestion=Try saving again., NSLocalizedDescription=Cannot Save, NSUnderlyingError=0x1c044c360 {Error Domain=NSOSStatusErrorDomain Code=-12412 "(null)"}}

Side issue: I play a repeating sound when the app is recording to try to verify the audio is recorded, but the sound stops when I start recording, even where I the video and external audio mic is working.

If you require more info, I can upload the other code to GitHub too.

Ideas

Since sometimes the recording saves (I can export to Photos app and replay the video) I think it must be async issues where I'm loading things out of order. Please let me know if you see any!

One I idea I will be trying is saving to my own folder in /Documents instead of directly to /Documents in case of weird permissions errors. Although I believe this would be causing consistent errors, instead of only sometimes breaking.

My Code

func startRecording() {
    guard let firstDocumentDirectoryPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else { return }

    let directoryContents = try! FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: firstDocumentDirectoryPath), includingPropertiesForKeys: nil, options: [])
    print(directoryContents)

    videoURL = URL(fileURLWithPath: firstDocumentDirectoryPath.appending("/\(arc4random()).mp4"))

    print(videoURL.absoluteString)

    assetWriter = try! AVAssetWriter(url: videoURL, fileType: AVFileType.mp4)

    let compressionProperties:[String:Any] = [...]
    let videoSettings:[String:Any] = [...]
    let audioSettings:[String:Any] = [...]

    videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
    audioMicInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings)
    audioAppInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings)

    guard let assetWriter = assetWriter else { return }
    guard let videoInput = videoInput else { return }
    guard let audioAppInput = audioAppInput else { return }
    guard let audioMicInput = audioMicInput else { return }

    videoInput.mediaTimeScale = 60
    videoInput.expectsMediaDataInRealTime = true
    audioMicInput.expectsMediaDataInRealTime = true
    audioAppInput.expectsMediaDataInRealTime = true

    if assetWriter.canAdd(videoInput) {
        assetWriter.add(videoInput)
    }

    if assetWriter.canAdd(audioAppInput) {
        assetWriter.add(audioAppInput)
    }

    if assetWriter.canAdd(audioMicInput) {
        assetWriter.add(audioMicInput)
    }

    assetWriter.movieTimeScale = 60

    RPScreenRecorder.shared().startCapture(handler: recordingHandler(sampleBuffer:sampleBufferType:error:)) { (error:Error?) in
        if error != nil {
            print("RPScreenRecorder.shared().startCapture: \(error.debugDescription)")
        } else {
            print("start capture complete")
        }
    }
}

func recordingHandler (sampleBuffer:CMSampleBuffer, sampleBufferType:RPSampleBufferType, error:Error?){
    if error != nil {
        print("recordingHandler: \(error.debugDescription)")
    }

    if CMSampleBufferDataIsReady(sampleBuffer) {
        guard let assetWriter = assetWriter else { return }
        guard let videoInput = videoInput else { return }
        guard let audioAppInput = audioAppInput else { return }
        guard let audioMicInput = audioMicInput else { return }

        if assetWriter.status == AVAssetWriterStatus.unknown {
            print("AVAssetWriterStatus.unknown")
            if !assetWriter.startWriting() {
                return
            }
            assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
        }

        if assetWriter.status == AVAssetWriterStatus.failed {
            print("AVAssetWriterStatus.failed")
            print("assetWriter.error: \(assetWriter.error.debugDescription)")
            return
        }

        if sampleBufferType == RPSampleBufferType.video {
            if videoInput.isReadyForMoreMediaData {
                print("=appending video data")
                videoInput.append(sampleBuffer)
            }
        }

        if sampleBufferType == RPSampleBufferType.audioApp {
            if audioAppInput.isReadyForMoreMediaData {
                print("==appending app audio data")
                audioAppInput.append(sampleBuffer)
            }
        }

        if sampleBufferType == RPSampleBufferType.audioMic {
            if audioMicInput.isReadyForMoreMediaData {
                print("===appending mic audio data")
                audioMicInput.append(sampleBuffer)
            }
        }
    }
}

func stopRecording() {
    RPScreenRecorder.shared().stopCapture { (error) in
        guard let assetWriter = self.assetWriter else { return }
        guard let videoInput = self.videoInput else { return }
        guard let audioAppInput = self.audioAppInput else { return }
        guard let audioMicInput = self.audioMicInput else { return }

        if error != nil {
            print("recordingHandler: \(error.debugDescription)")
        } else {
            videoInput.markAsFinished()
            audioMicInput.markAsFinished()
            audioAppInput.markAsFinished()

            assetWriter.finishWriting(completionHandler: {
                print(self.videoURL)
                self.saveToCameraRoll(URL: self.videoURL)
            })
        }
    }
}
picciano
  • 22,341
  • 9
  • 69
  • 82
stanley
  • 1,113
  • 1
  • 12
  • 26
  • Did you discover a solution that worked for you? – Brandon A Apr 12 '18 at 18:36
  • Did you find a solution to this? I'm having the exact same issue. WLee's suggestion did not work for me. – Geoff H Apr 16 '18 at 20:47
  • Unfortunately not :( Maybe see if someone filed a radar for it or file a new one – stanley Apr 16 '18 at 20:51
  • Thanks for the quick reply. Will do. My issuse is only different in that my video records perfectly every time the user chooses "Record Screen Only" but when "Record Screen & Microphone" is selected it always fails on the first attempt then randomly succeeds on attempts after that. I think you may be onto something with the async idea. I'm using multithreading in my app. – Geoff H Apr 16 '18 at 21:03
  • @stanley video recorded successfully but audio record not working. please suggest. – Nilesh Parmar Jun 01 '20 at 08:09
  • @stanley when I record video. audio record not working. please suggest – Nilesh Parmar Jun 01 '20 at 08:12

2 Answers2

3

I got it to work. I believe it was indeed an async issue. The problem, for some reason is you must make sure

assetWriter.startWriting()
assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))

happen strictly serially.

Change your code from this:

if assetWriter.status == AVAssetWriterStatus.unknown {
    print("AVAssetWriterStatus.unknown")
    if !assetWriter.startWriting() {
        return
    }
    assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
}

to this:

DispatchQueue.main.async { [weak self] in
    if self?.assetWriter.status == AVAssetWriterStatus.unknown {
        print("AVAssetWriterStatus.unknown")
        if !self?.assetWriter.startWriting() {
            return
        }
        self?.assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
    }
}

Or even better, the whole block inside CMSampleBufferDataIsReady ie.

if CMSampleBufferDataIsReady(sampleBuffer) {
    DispatchQueue.main.async { [weak self] in
        ...
        ...
    }
}

Let me know if it works!

Geoff H
  • 3,107
  • 1
  • 28
  • 53
0

I had similar issue. I fixed it by first check whether videoURL file is already existed. If so, remove it first then the error will go away.

WLee
  • 1
  • Since I save the result of the replay video in a temporary directory, before creating a new video, check whether the file already existed. If so, remove it. – WLee Mar 21 '18 at 15:42
  • Since I save the result of the replay video in a temporary directory, before creating a new video, check whether the file already existed. If so, remove it. if FileManager().fileExists(atPath: Constants.TempPath) {try! FileManager().removeItem(atPath: Constants.TempPath)} Also, make sure to check whether the video can be added before adding: if assetWriter.canAdd(videoInput){assetWriter.add(videoInput)} – WLee Mar 21 '18 at 15:47