chore: sync pending changes

This commit is contained in:
Peter Steinberger
2025-12-30 00:59:30 +01:00
parent 37f85bb2d1
commit 7aabe73521

View File

@@ -68,114 +68,119 @@ final class ScreenRecordService: @unchecked Sendable {
try? FileManager.default.removeItem(at: outURL) try? FileManager.default.removeItem(at: outURL)
let state = CaptureState() let state = CaptureState()
let recordQueue = DispatchQueue(label: "com.steipete.clawdis.screenrecord")
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let handler: @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void = { sample, type, error in let handler: @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void = { sample, type, error in
if let error { // ReplayKit can call the capture handler on a background queue.
state.withLock { state in // Serialize writes to avoid queue asserts.
if state.handlerError == nil { state.handlerError = error } recordQueue.async {
} if let error {
return state.withLock { state in
} if state.handlerError == nil { state.handlerError = error }
guard CMSampleBufferDataIsReady(sample) else { return }
switch type {
case .video:
let pts = CMSampleBufferGetPresentationTimeStamp(sample)
let shouldSkip = state.withLock { state in
if let lastVideoTime = state.lastVideoTime {
let delta = CMTimeSubtract(pts, lastVideoTime)
return delta.seconds < (1.0 / fpsValue)
} }
return false return
} }
if shouldSkip { return } guard CMSampleBufferDataIsReady(sample) else { return }
if state.withLock({ $0.writer == nil }) { switch type {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sample) else { case .video:
state.withLock { state in let pts = CMSampleBufferGetPresentationTimeStamp(sample)
if state.handlerError == nil { let shouldSkip = state.withLock { state in
state.handlerError = ScreenRecordError.captureFailed("Missing image buffer") if let lastVideoTime = state.lastVideoTime {
} let delta = CMTimeSubtract(pts, lastVideoTime)
return delta.seconds < (1.0 / fpsValue)
} }
return return false
} }
let width = CVPixelBufferGetWidth(imageBuffer) if shouldSkip { return }
let height = CVPixelBufferGetHeight(imageBuffer)
do {
let w = try AVAssetWriter(outputURL: outURL, fileType: .mp4)
let settings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: width,
AVVideoHeightKey: height,
]
let vInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
vInput.expectsMediaDataInRealTime = true
guard w.canAdd(vInput) else {
throw ScreenRecordError.writeFailed("Cannot add video input")
}
w.add(vInput)
if includeAudio { if state.withLock({ $0.writer == nil }) {
let aInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil) guard let imageBuffer = CMSampleBufferGetImageBuffer(sample) else {
aInput.expectsMediaDataInRealTime = true
if w.canAdd(aInput) {
w.add(aInput)
state.withLock { state in
state.audioInput = aInput
}
}
}
guard w.startWriting() else {
throw ScreenRecordError
.writeFailed(w.error?.localizedDescription ?? "Failed to start writer")
}
w.startSession(atSourceTime: pts)
state.withLock { state in
state.writer = w
state.videoInput = vInput
state.started = true
}
} catch {
state.withLock { state in
if state.handlerError == nil { state.handlerError = error }
}
return
}
}
let vInput = state.withLock { $0.videoInput }
let isStarted = state.withLock { $0.started }
guard let vInput, isStarted else { return }
if vInput.isReadyForMoreMediaData {
if vInput.append(sample) {
state.withLock { state in
state.sawVideo = true
state.lastVideoTime = pts
}
} else {
let err = state.withLock { $0.writer?.error }
if let err {
state.withLock { state in state.withLock { state in
if state.handlerError == nil { if state.handlerError == nil {
state.handlerError = ScreenRecordError.writeFailed(err.localizedDescription) state.handlerError = ScreenRecordError.captureFailed("Missing image buffer")
}
}
return
}
let width = CVPixelBufferGetWidth(imageBuffer)
let height = CVPixelBufferGetHeight(imageBuffer)
do {
let w = try AVAssetWriter(outputURL: outURL, fileType: .mp4)
let settings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: width,
AVVideoHeightKey: height,
]
let vInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
vInput.expectsMediaDataInRealTime = true
guard w.canAdd(vInput) else {
throw ScreenRecordError.writeFailed("Cannot add video input")
}
w.add(vInput)
if includeAudio {
let aInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
aInput.expectsMediaDataInRealTime = true
if w.canAdd(aInput) {
w.add(aInput)
state.withLock { state in
state.audioInput = aInput
}
}
}
guard w.startWriting() else {
throw ScreenRecordError
.writeFailed(w.error?.localizedDescription ?? "Failed to start writer")
}
w.startSession(atSourceTime: pts)
state.withLock { state in
state.writer = w
state.videoInput = vInput
state.started = true
}
} catch {
state.withLock { state in
if state.handlerError == nil { state.handlerError = error }
}
return
}
}
let vInput = state.withLock { $0.videoInput }
let isStarted = state.withLock { $0.started }
guard let vInput, isStarted else { return }
if vInput.isReadyForMoreMediaData {
if vInput.append(sample) {
state.withLock { state in
state.sawVideo = true
state.lastVideoTime = pts
}
} else {
let err = state.withLock { $0.writer?.error }
if let err {
state.withLock { state in
if state.handlerError == nil {
state.handlerError = ScreenRecordError.writeFailed(err.localizedDescription)
}
} }
} }
} }
} }
}
case .audioApp, .audioMic: case .audioApp, .audioMic:
let aInput = state.withLock { $0.audioInput } let aInput = state.withLock { $0.audioInput }
let isStarted = state.withLock { $0.started } let isStarted = state.withLock { $0.started }
guard includeAudio, let aInput, isStarted else { return } guard includeAudio, let aInput, isStarted else { return }
if aInput.isReadyForMoreMediaData { if aInput.isReadyForMoreMediaData {
_ = aInput.append(sample) _ = aInput.append(sample)
} }
@unknown default: @unknown default:
break break
}
} }
} }