gorastudio-git / SCNRecorder

The best way to record your AR experience!
MIT License
201 stars 51 forks source link

The recording cannot resume #61

Closed asam139 closed 1 year ago

asam139 commented 1 year ago

When the recording is resumed, it throws the error

Error Domain=AVFoundationErrorDomain Code=-11862 "Cannot append media data after ending session" UserInfo={NSUnderlyingError=0x2808ebf00 {Error Domain=NSOSStatusErrorDomain Code=-12763 "(null)"}, NSLocalizedFailureReason=The application encountered a programming error., NSDebugDescription=Cannot append media data after ending session, NSLocalizedDescription=The operation is not allowed}

The pause method of the VideoOutput finishes the session which does not allow to resume.

func pause(_ videoOutput: VideoOutput) -> Self {
    switch  self {
    case .starting:
      return .starting
    case .ready,
         .preparing:
      return .ready
    case .recording(let seconds):
      // HERE
      videoOutput.endSession(at: seconds)
      return .paused
    case .paused,
         .canceled,
         .finished,
         .failed:
      return self
    }
  }
v-grigoriev commented 1 year ago

True, the limitation comes from AVAssetWriter endSession. Unfortunately it does not support multiple sample-writing sessions.

I have an unfinished implementation of the pause by manual subtraction of paused intervals from the presentation time.

asam139 commented 1 year ago

I'll try to create a PR with my solution. My idea is to add a computed property in the VideoOuputState to know when it is recording so the session does not need to be ended.

var isRecording: Bool {
    guard case .recording = self else {
        return false
    }
    return true
  }

Then to check this isRecording property together isReadyForMoreMediaData to control if the pixelBuffer can be added

func append(pixelBuffer: CVPixelBuffer, withPresentationTime time: CMTime) throws {
    guard
        pixelBufferAdaptor.assetWriterInput.isReadyForMoreMediaData,
        state.isRecording // HERE
    else { return }
    guard pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: time) else {
      if assetWriter.status == .failed { throw assetWriter.error ?? Error.unknown }
      return
    }

    let seconds = time.seconds
    duration += seconds - lastSeconds
    lastSeconds = seconds
  }

  func appendVideo(sampleBuffer: CMSampleBuffer) throws {
    guard
        videoInput.isReadyForMoreMediaData,
        state.isRecording // HERE
    else { return }
    guard videoInput.append(sampleBuffer) else {
      if assetWriter.status == .failed { throw assetWriter.error ?? Error.unknown }
      return
    }

    let timeStamp: CMTime
    let duration: CMTime

    if #available(iOS 13.0, *) {
      timeStamp = sampleBuffer.presentationTimeStamp
      duration = sampleBuffer.duration
    } else {
      timeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
      duration = CMSampleBufferGetDuration(sampleBuffer)
    }

    self.duration += duration.seconds
    lastSeconds = (timeStamp + duration).seconds
  }

I did a quick test, and it seems to work fine 🥹

v-grigoriev commented 1 year ago

It's already done in https://github.com/gorastudio-ceo/SCNRecorder/blob/e4e1b7c579eff8feb4a93f2c9c31940a54cdcd07/Sources/Outputs/VideoOutput/VideoOutput.State.swift#L249

If your idea works, you can just comment out videoOutput.endSession(at: seconds) in the pause method. And, probably make some changes to the resume method of the state.

But I suppose it will lead to a gap in the video.

v-grigoriev commented 1 year ago

Basically any changes to the VideoOutput logic should be done in the VideoOutput.State. VideoOutput is state machine managed.

asam139 commented 1 year ago

@v-grigoriev I created the PR with my solution. I tested a while, and it seems to work really well.

Please take a look and give me your feedback ;)