fix: avoid screen recorder data races
This commit is contained in:
@@ -50,11 +50,6 @@ final class ScreenRecordService {
|
|||||||
}()
|
}()
|
||||||
try? FileManager.default.removeItem(at: outURL)
|
try? FileManager.default.removeItem(at: outURL)
|
||||||
|
|
||||||
let recorder = RPScreenRecorder.shared()
|
|
||||||
await MainActor.run {
|
|
||||||
recorder.isMicrophoneEnabled = includeAudio
|
|
||||||
}
|
|
||||||
|
|
||||||
var writer: AVAssetWriter?
|
var writer: AVAssetWriter?
|
||||||
var videoInput: AVAssetWriterInput?
|
var videoInput: AVAssetWriterInput?
|
||||||
var audioInput: AVAssetWriterInput?
|
var audioInput: AVAssetWriterInput?
|
||||||
@@ -77,104 +72,110 @@ final class ScreenRecordService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||||||
Task { @MainActor in
|
let handler: @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void = { sample, type, error in
|
||||||
recorder.startCapture(handler: { sample, type, error in
|
if let error {
|
||||||
if let error {
|
setHandlerError(error)
|
||||||
setHandlerError(error)
|
return
|
||||||
return
|
}
|
||||||
}
|
guard CMSampleBufferDataIsReady(sample) else { return }
|
||||||
guard CMSampleBufferDataIsReady(sample) else { return }
|
|
||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
case .video:
|
case .video:
|
||||||
let pts = CMSampleBufferGetPresentationTimeStamp(sample)
|
let pts = CMSampleBufferGetPresentationTimeStamp(sample)
|
||||||
let shouldSkip = withStateLock {
|
let shouldSkip = withStateLock {
|
||||||
if let lastVideoTime {
|
if let lastVideoTime {
|
||||||
let delta = CMTimeSubtract(pts, lastVideoTime)
|
let delta = CMTimeSubtract(pts, lastVideoTime)
|
||||||
return delta.seconds < (1.0 / fpsValue)
|
return delta.seconds < (1.0 / fpsValue)
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
if shouldSkip { return }
|
return false
|
||||||
|
}
|
||||||
|
if shouldSkip { return }
|
||||||
|
|
||||||
if withStateLock({ writer == nil }) {
|
if withStateLock({ writer == nil }) {
|
||||||
guard let imageBuffer = CMSampleBufferGetImageBuffer(sample) else {
|
guard let imageBuffer = CMSampleBufferGetImageBuffer(sample) else {
|
||||||
setHandlerError(ScreenRecordError.captureFailed("Missing image buffer"))
|
setHandlerError(ScreenRecordError.captureFailed("Missing image buffer"))
|
||||||
return
|
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")
|
||||||
}
|
}
|
||||||
let width = CVPixelBufferGetWidth(imageBuffer)
|
w.add(vInput)
|
||||||
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 includeAudio {
|
||||||
let aInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
|
let aInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
|
||||||
aInput.expectsMediaDataInRealTime = true
|
aInput.expectsMediaDataInRealTime = true
|
||||||
if w.canAdd(aInput) {
|
if w.canAdd(aInput) {
|
||||||
w.add(aInput)
|
w.add(aInput)
|
||||||
withStateLock {
|
withStateLock {
|
||||||
audioInput = aInput
|
audioInput = aInput
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard w.startWriting() else {
|
|
||||||
throw ScreenRecordError
|
|
||||||
.writeFailed(w.error?.localizedDescription ?? "Failed to start writer")
|
|
||||||
}
|
|
||||||
w.startSession(atSourceTime: pts)
|
|
||||||
withStateLock {
|
|
||||||
writer = w
|
|
||||||
videoInput = vInput
|
|
||||||
started = true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setHandlerError(error)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let vInput = withStateLock { videoInput }
|
guard w.startWriting() else {
|
||||||
let isStarted = withStateLock { started }
|
throw ScreenRecordError
|
||||||
guard let vInput, isStarted else { return }
|
.writeFailed(w.error?.localizedDescription ?? "Failed to start writer")
|
||||||
if vInput.isReadyForMoreMediaData {
|
|
||||||
if vInput.append(sample) {
|
|
||||||
withStateLock {
|
|
||||||
sawVideo = true
|
|
||||||
lastVideoTime = pts
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let err = withStateLock({ writer?.error }) {
|
|
||||||
setHandlerError(ScreenRecordError.writeFailed(err.localizedDescription))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
w.startSession(atSourceTime: pts)
|
||||||
|
withStateLock {
|
||||||
|
writer = w
|
||||||
|
videoInput = vInput
|
||||||
|
started = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setHandlerError(error)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
case .audioApp, .audioMic:
|
|
||||||
let aInput = withStateLock { audioInput }
|
|
||||||
let isStarted = withStateLock { started }
|
|
||||||
guard includeAudio, let aInput, isStarted else { return }
|
|
||||||
if aInput.isReadyForMoreMediaData {
|
|
||||||
_ = aInput.append(sample)
|
|
||||||
}
|
|
||||||
|
|
||||||
@unknown default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}, completionHandler: { error in
|
|
||||||
if let error { cont.resume(throwing: error) } else { cont.resume() }
|
let vInput = withStateLock { videoInput }
|
||||||
})
|
let isStarted = withStateLock { started }
|
||||||
|
guard let vInput, isStarted else { return }
|
||||||
|
if vInput.isReadyForMoreMediaData {
|
||||||
|
if vInput.append(sample) {
|
||||||
|
withStateLock {
|
||||||
|
sawVideo = true
|
||||||
|
lastVideoTime = pts
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let err = withStateLock({ writer?.error }) {
|
||||||
|
setHandlerError(ScreenRecordError.writeFailed(err.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .audioApp, .audioMic:
|
||||||
|
let aInput = withStateLock { audioInput }
|
||||||
|
let isStarted = withStateLock { started }
|
||||||
|
guard includeAudio, let aInput, isStarted else { return }
|
||||||
|
if aInput.isReadyForMoreMediaData {
|
||||||
|
_ = aInput.append(sample)
|
||||||
|
}
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let completion: @Sendable (Error?) -> Void = { error in
|
||||||
|
if let error { cont.resume(throwing: error) } else { cont.resume() }
|
||||||
|
}
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
let recorder = RPScreenRecorder.shared()
|
||||||
|
recorder.isMicrophoneEnabled = includeAudio
|
||||||
|
recorder.startCapture(handler: handler, completionHandler: completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +183,7 @@ final class ScreenRecordService {
|
|||||||
|
|
||||||
let stopError = await withCheckedContinuation { cont in
|
let stopError = await withCheckedContinuation { cont in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
let recorder = RPScreenRecorder.shared()
|
||||||
recorder.stopCapture { error in cont.resume(returning: error) }
|
recorder.stopCapture { error in cont.resume(returning: error) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user