Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
repos:
- repo: local
hooks:
- id: swift-format
name: swift format
entry: swift format --in-place --configuration .swift-format
language: system
types: [swift]
- id: swiftlint
name: swiftlint
entry: swiftlint lint --strict
language: system
types: [swift]
56 changes: 53 additions & 3 deletions TimeLapze/Camera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class Camera: NSObject, Recordable {
var lastAppendedFrame: CMTime = .zero
var tmpFrameBuffer: CMSampleBuffer?

// Flag to prevent starting new recording while previous is finalizing
private var isFinalizingRecording = false

override var description: String {
if inputDevice.manufacturer.isEmpty {
return "\(self.inputDevice.localizedName)"
Expand Down Expand Up @@ -93,27 +96,61 @@ class Camera: NSObject, Recordable {
func startRecording() {
guard self.enabled else { return }
guard self.state != .recording else { return }
guard !isFinalizingRecording else {
logger.warning("Cannot start recording while previous recording is being finalized")
return
}
logger.log("\(self.description) Recording")

// Reset state for new recording
resetRecordingState()

self.state = .recording

setup(path: getFilename())
}

/// Resets all state variables for a new recording session
private func resetRecordingState() {
// Clean up old objects
if let oldRecordVideo = recordVideo, oldRecordVideo.isRecording() {
oldRecordVideo.stopSession()
}
recordVideo = nil
writer = nil
input = nil

// Reset time synchronization
offset = CMTime(seconds: 0.0, preferredTimescale: 60)
frameCount = 0
frameChanged = true
lastAppendedFrame = .zero
tmpFrameBuffer = nil
}

func saveRecording() {
guard self.enabled else { return }

self.state = .stopped
self.isFinalizingRecording = true

logger.log("Camera - saved recording")

if let recorder = recordVideo, recorder.isRecording() {
recorder.stopSession()
logger.error("Stopped running")
logger.log("Stopped capture session")
}

guard let input = input, let writer = writer else {
logger.log("Either the input or the writer is null")
self.isFinalizingRecording = false
return
}

// Check if writer is in a valid state to finish
guard writer.status == .writing else {
logger.error("Writer is not in writing state, status: \(writer.status.rawValue)")
self.isFinalizingRecording = false
return
}

Expand All @@ -123,8 +160,13 @@ class Camera: NSObject, Recordable {
sleep(1) // sleeping for a second
}

input.markAsFinished() // this is good
input.markAsFinished()
writer.finishWriting { [self] in
defer {
// Always reset the flag when finalization is complete
self.isFinalizingRecording = false
}

if writer.status == .completed {
// Asset writing completed successfully
if UserDefaults.standard.bool(forKey: "showAfterSave")
Expand All @@ -149,8 +191,16 @@ class Camera: NSObject, Recordable {

// MARK: Streaming
func handleVideo(buffer: CMSampleBuffer) {
// Ignore frames during finalization or when not recording
guard state == .recording, !isFinalizingRecording else {
return
}

guard let input = self.input, let writer = self.writer else {
logger.error("Not video writer present")
// Only log if we're actually supposed to be recording
if state == .recording {
logger.error("Not video writer present")
}
return
}

Expand Down
16 changes: 14 additions & 2 deletions TimeLapze/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,22 @@ struct PreferencesView: View {
)

HStack {
Text("\(String(format: "%.1f", preferencesViewModel.timeMultiple))x faster")
Slider(value: $preferencesViewModel.timeMultiple, in: .init(uncheckedBounds: (1.0, 240.0)))
TextField(
"",
value: Binding(
get: { preferencesViewModel.timeMultiple },
set: { preferencesViewModel.timeMultiple = min(max($0, 1.0), 240.0) }
),
format: .number.precision(.fractionLength(1))
)
.textFieldStyle(.roundedBorder)
.frame(width: 55)
.multilineTextAlignment(.trailing)
Text("x faster")
}

Slider(value: $preferencesViewModel.timeMultiple, in: 1.0...240.0)

if #available(macOS 14.0, *) {
Picker("Output FPS", selection: $preferencesViewModel.fpsDropdown) {
ForEach(0..<preferencesViewModel.validFPS.count) { index in
Expand Down
49 changes: 46 additions & 3 deletions TimeLapze/Screen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class Screen: NSObject, SCStreamOutput, Recordable {

var height: Int?
var width: Int?

// Flag to prevent starting new recording while previous is finalizing
private var isFinalizingRecording = false

override var description: String {
if height == nil || width == nil {
Expand All @@ -54,13 +57,36 @@ class Screen: NSObject, SCStreamOutput, Recordable {
func startRecording(excluding: [SCRunningApplication], showCursor: Bool) {
guard self.enabled else { return }
guard self.state != .recording else { return }
guard !isFinalizingRecording else {
logger.warning("Cannot start recording while previous recording is being finalized")
return
}

self.showCursor = showCursor

// Reset state for new recording
resetRecordingState()

self.state = .recording

setup(path: getFilename(), excluding: excluding)
}

/// Resets all state variables for a new recording session
private func resetRecordingState() {
// Clean up old stream
stream = nil
writer = nil
input = nil

// Reset time synchronization
offset = CMTime(seconds: 0.0, preferredTimescale: 60)
frameCount = 0
frameChanged = true
lastAppendedFrame = .zero
tmpFrameBuffer = nil
lastSavedFrame = nil
}

func pauseRecording() {
self.state = .paused
Expand All @@ -77,12 +103,20 @@ class Screen: NSObject, SCStreamOutput, Recordable {
guard let writer = writer, let input = input else { return }

self.state = .stopped
self.isFinalizingRecording = true

logger.log("Screen -- saved recording")

if let stream = stream {
stream.stopCapture()
}

// Check if writer is in a valid state to finish
guard writer.status == .writing else {
logger.error("Writer is not in writing state, status: \(writer.status.rawValue)")
self.isFinalizingRecording = false
return
}

while !input.isReadyForMoreMediaData {
logger.log("Not able to mark the stream as finished")
Expand All @@ -91,6 +125,11 @@ class Screen: NSObject, SCStreamOutput, Recordable {

input.markAsFinished()
writer.finishWriting { [self] in
defer {
// Always reset the flag when finalization is complete
self.isFinalizingRecording = false
}

if writer.status == .completed {
// Asset writing completed successfully

Expand Down Expand Up @@ -255,7 +294,8 @@ class Screen: NSObject, SCStreamOutput, Recordable {

/// Saves each `CMSampleBuffer` from the screen
func stream(_ stream: SCStream, didOutputSampleBuffer: CMSampleBuffer, of: SCStreamOutputType) {
guard self.state == .recording else { return }
// Ignore frames during finalization or when not recording
guard self.state == .recording, !isFinalizingRecording else { return }

switch of {
case .screen:
Expand All @@ -269,8 +309,11 @@ class Screen: NSObject, SCStreamOutput, Recordable {

/// Receives a list of `CMSampleBuffers` and uses `appendBuffer` to save them
func handleVideo(buffer: CMSampleBuffer) {
guard self.input != nil else { // both
logger.error("No AVAssetWriter with the name `input` is present")
guard self.input != nil else {
// Only log if we're actually supposed to be recording
if state == .recording {
logger.error("No AVAssetWriter with the name `input` is present")
}
return
}

Expand Down
Loading