mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
Pause native screen capture across platforms
This commit is contained in:
@@ -28,7 +28,13 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
private var firstPrimaryAudioSampleTime: CMTime?
|
||||
private var firstMicrophoneSampleTime: CMTime?
|
||||
private var lastSampleBuffer: CMSampleBuffer?
|
||||
private var lastVideoPresentationTime: CMTime = .zero
|
||||
private var lastVideoDuration: CMTime = .zero
|
||||
private var isRecording = false
|
||||
private var isPaused = false
|
||||
private var pauseStartedHostTime: CMTime?
|
||||
private var pendingResumeAdjustment = false
|
||||
private var accumulatedPausedDuration: CMTime = .zero
|
||||
private var sessionStarted = false
|
||||
private var frameCount = 0
|
||||
private var outputURL: URL?
|
||||
@@ -230,8 +236,14 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
assetWriter.startSession(atSourceTime: .zero)
|
||||
sessionStarted = true
|
||||
isRecording = true
|
||||
isPaused = false
|
||||
pauseStartedHostTime = nil
|
||||
pendingResumeAdjustment = false
|
||||
accumulatedPausedDuration = .zero
|
||||
frameCount = 0
|
||||
firstSampleTime = .zero
|
||||
lastVideoPresentationTime = .zero
|
||||
lastVideoDuration = .zero
|
||||
startWindowValidationIfNeeded()
|
||||
print("Recording started")
|
||||
fflush(stdout)
|
||||
@@ -245,8 +257,22 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
return try await finishCapture()
|
||||
}
|
||||
|
||||
func pauseCapture() {
|
||||
guard isRecording, !isPaused else { return }
|
||||
isPaused = true
|
||||
pauseStartedHostTime = CMClockGetTime(CMClockGetHostTimeClock())
|
||||
pendingResumeAdjustment = false
|
||||
}
|
||||
|
||||
func resumeCapture() {
|
||||
guard isRecording, isPaused else { return }
|
||||
isPaused = false
|
||||
pendingResumeAdjustment = true
|
||||
}
|
||||
|
||||
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) {
|
||||
guard sessionStarted, sampleBuffer.isValid, isRecording else { return }
|
||||
guard let presentationTime = adjustedPresentationTime(for: sampleBuffer, outputType: outputType) else { return }
|
||||
|
||||
if outputType == .screen {
|
||||
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]],
|
||||
@@ -263,11 +289,12 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
firstSampleTime = sampleBuffer.presentationTimeStamp
|
||||
}
|
||||
|
||||
let presentationTime = sampleBuffer.presentationTimeStamp - firstSampleTime
|
||||
lastSampleBuffer = sampleBuffer
|
||||
let timing = CMSampleTimingInfo(duration: sampleBuffer.duration, presentationTimeStamp: presentationTime, decodeTimeStamp: sampleBuffer.decodeTimeStamp)
|
||||
if let retimedSampleBuffer = try? CMSampleBuffer(copying: sampleBuffer, withNewTiming: [timing]) {
|
||||
videoInput.append(retimedSampleBuffer)
|
||||
lastVideoPresentationTime = presentationTime
|
||||
lastVideoDuration = sampleBuffer.duration
|
||||
frameCount += 1
|
||||
}
|
||||
return
|
||||
@@ -275,15 +302,15 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
|
||||
if outputType == .audio {
|
||||
guard primaryAudioSource == .system, let audioInput else { return }
|
||||
appendAudioSampleBuffer(sampleBuffer, to: audioInput, firstSampleTime: &firstPrimaryAudioSampleTime)
|
||||
appendAudioSampleBuffer(sampleBuffer, to: audioInput, firstSampleTime: &firstPrimaryAudioSampleTime, presentationTime: presentationTime)
|
||||
return
|
||||
}
|
||||
|
||||
if outputType.rawValue == microphoneOutputTypeRawValue {
|
||||
if writesMicrophoneToSeparateTrack, let microphoneOnlyInput {
|
||||
appendAudioSampleBuffer(sampleBuffer, to: microphoneOnlyInput, firstSampleTime: &firstMicrophoneSampleTime)
|
||||
appendAudioSampleBuffer(sampleBuffer, to: microphoneOnlyInput, firstSampleTime: &firstMicrophoneSampleTime, presentationTime: presentationTime)
|
||||
} else if primaryAudioSource == .microphone, let audioInput {
|
||||
appendAudioSampleBuffer(sampleBuffer, to: audioInput, firstSampleTime: &firstPrimaryAudioSampleTime)
|
||||
appendAudioSampleBuffer(sampleBuffer, to: audioInput, firstSampleTime: &firstPrimaryAudioSampleTime, presentationTime: presentationTime)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -312,7 +339,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
stream = nil
|
||||
|
||||
if let originalBuffer = lastSampleBuffer, let videoInput = videoInput {
|
||||
let additionalTime = CMTime(seconds: ProcessInfo.processInfo.systemUptime, preferredTimescale: 600) - firstSampleTime
|
||||
let additionalTime = lastVideoPresentationTime + frameDuration(for: originalBuffer)
|
||||
let timing = CMSampleTimingInfo(duration: originalBuffer.duration, presentationTimeStamp: additionalTime, decodeTimeStamp: originalBuffer.decodeTimeStamp)
|
||||
if let additionalSampleBuffer = try? CMSampleBuffer(copying: originalBuffer, withNewTiming: [timing]) {
|
||||
videoInput.append(additionalSampleBuffer)
|
||||
@@ -340,7 +367,13 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
firstPrimaryAudioSampleTime = nil
|
||||
firstMicrophoneSampleTime = nil
|
||||
lastSampleBuffer = nil
|
||||
lastVideoPresentationTime = .zero
|
||||
lastVideoDuration = .zero
|
||||
frameCount = 0
|
||||
isPaused = false
|
||||
pauseStartedHostTime = nil
|
||||
pendingResumeAdjustment = false
|
||||
accumulatedPausedDuration = .zero
|
||||
capturesSystemAudio = false
|
||||
capturesMicrophone = false
|
||||
writesMicrophoneToSeparateTrack = false
|
||||
@@ -348,16 +381,53 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
return path
|
||||
}
|
||||
|
||||
private func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer, to input: AVAssetWriterInput, firstSampleTime: inout CMTime?) {
|
||||
private func adjustedPresentationTime(for sampleBuffer: CMSampleBuffer, outputType: SCStreamOutputType) -> CMTime? {
|
||||
if isPaused {
|
||||
return nil
|
||||
}
|
||||
|
||||
let sampleTime = sampleBuffer.presentationTimeStamp
|
||||
if pendingResumeAdjustment, let pauseStartedHostTime {
|
||||
let pauseGap = sampleTime - pauseStartedHostTime
|
||||
if pauseGap > .zero {
|
||||
accumulatedPausedDuration = accumulatedPausedDuration + pauseGap
|
||||
}
|
||||
self.pauseStartedHostTime = nil
|
||||
pendingResumeAdjustment = false
|
||||
}
|
||||
|
||||
if outputType == .screen {
|
||||
if firstSampleTime == .zero {
|
||||
firstSampleTime = sampleTime
|
||||
}
|
||||
return max(.zero, sampleTime - firstSampleTime - accumulatedPausedDuration)
|
||||
}
|
||||
|
||||
return sampleTime - accumulatedPausedDuration
|
||||
}
|
||||
|
||||
private func frameDuration(for sampleBuffer: CMSampleBuffer) -> CMTime {
|
||||
if sampleBuffer.duration.isValid && sampleBuffer.duration > .zero {
|
||||
return sampleBuffer.duration
|
||||
}
|
||||
|
||||
if lastVideoDuration.isValid && lastVideoDuration > .zero {
|
||||
return lastVideoDuration
|
||||
}
|
||||
|
||||
return CMTime(value: 1, timescale: CMTimeScale(targetCaptureFPS))
|
||||
}
|
||||
|
||||
private func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer, to input: AVAssetWriterInput, firstSampleTime: inout CMTime?, presentationTime: CMTime) {
|
||||
guard input.isReadyForMoreMediaData else { return }
|
||||
|
||||
if firstSampleTime == nil {
|
||||
firstSampleTime = sampleBuffer.presentationTimeStamp
|
||||
firstSampleTime = presentationTime
|
||||
}
|
||||
|
||||
guard let firstSampleTime else { return }
|
||||
let presentationTime = sampleBuffer.presentationTimeStamp - firstSampleTime
|
||||
let timing = CMSampleTimingInfo(duration: sampleBuffer.duration, presentationTimeStamp: presentationTime, decodeTimeStamp: sampleBuffer.decodeTimeStamp)
|
||||
let relativePresentationTime = max(.zero, presentationTime - firstSampleTime)
|
||||
let timing = CMSampleTimingInfo(duration: sampleBuffer.duration, presentationTimeStamp: relativePresentationTime, decodeTimeStamp: sampleBuffer.decodeTimeStamp)
|
||||
if let retimedSampleBuffer = try? CMSampleBuffer(copying: sampleBuffer, withNewTiming: [timing]) {
|
||||
input.append(retimedSampleBuffer)
|
||||
}
|
||||
@@ -457,6 +527,18 @@ final class RecorderService {
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
queue.async {
|
||||
self.recorder.pauseCapture()
|
||||
}
|
||||
}
|
||||
|
||||
func resume() {
|
||||
queue.async {
|
||||
self.recorder.resumeCapture()
|
||||
}
|
||||
}
|
||||
|
||||
func waitUntilFinished() {
|
||||
completionGroup.wait()
|
||||
}
|
||||
@@ -478,6 +560,16 @@ service.start(configJSON: CommandLine.arguments[1])
|
||||
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
while let input = readLine(strippingNewline: true)?.lowercased() {
|
||||
if input == "pause" {
|
||||
service.pause()
|
||||
continue
|
||||
}
|
||||
|
||||
if input == "resume" {
|
||||
service.resume()
|
||||
continue
|
||||
}
|
||||
|
||||
if input == "stop" {
|
||||
service.stop()
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user