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
4 changes: 4 additions & 0 deletions TimeLapze.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
3EE5FE11296232C800768BF7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EE5FE10296232C800768BF7 /* Assets.xcassets */; };
3EE5FE14296232C800768BF7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EE5FE13296232C800768BF7 /* Preview Assets.xcassets */; };
3EEFFBB929A9B662002D0040 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EEFFBB829A9B662002D0040 /* Utils.swift */; };
3ET1AMP52CF0000100000001 /* TimestampOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ET1AMP52CF0000100000002 /* TimestampOverlay.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -82,6 +83,7 @@
3EE5FE13296232C800768BF7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
3EE5FE1A296232C800768BF7 /* TimeLapze.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TimeLapze.entitlements; sourceTree = "<group>"; };
3EEFFBB829A9B662002D0040 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
3ET1AMP52CF0000100000002 /* TimestampOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampOverlay.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -162,6 +164,7 @@
3E87A931299EBDFF00714AFC /* RecorderViewModel.swift */,
3E87A933299EBE0D00714AFC /* Recordable.swift */,
3EEFFBB829A9B662002D0040 /* Utils.swift */,
3ET1AMP52CF0000100000002 /* TimestampOverlay.swift */,
3E4D87C629AD741800D3EC7B /* Screen.swift */,
3E4D87C829AD744C00D3EC7B /* Camera.swift */,
3E61935A2A2EC12200333918 /* VideoConfiguration.swift */,
Expand Down Expand Up @@ -355,6 +358,7 @@
3E0DCD742BAB2A5800DB969A /* ReviewManager.swift in Sources */,
3E75E34C2AED6EC90016EB52 /* CameraRecorder.swift in Sources */,
3EEFFBB929A9B662002D0040 /* Utils.swift in Sources */,
3ET1AMP52CF0000100000001 /* TimestampOverlay.swift in Sources */,
3E87A934299EBE0D00714AFC /* Recordable.swift in Sources */,
3EAB1AFD2BA8A33300BEC597 /* OnboardingViewModel.swift in Sources */,
3E9894D12A2ED44600C25059 /* Logger.swift in Sources */,
Expand Down
83 changes: 78 additions & 5 deletions TimeLapze/Camera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class Camera: NSObject, Recordable {

var lastAppendedFrame: CMTime = .zero
var tmpFrameBuffer: CMSampleBuffer?
var sessionStartDate: Date?

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

override var description: String {
if inputDevice.manufacturer.isEmpty {
Expand Down Expand Up @@ -74,7 +78,21 @@ class Camera: NSObject, Recordable {

let writer = try AVAssetWriter(outputURL: url, fileType: fileType)

let input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
let input: AVAssetWriterInput
if TimestampOverlay.shouldUseOverlayFormat(for: .camera),
let formatHint = TimestampOverlay.formatDescription32BGRA(
width: Int(dimensions.width),
height: Int(dimensions.height)
)
{
input = AVAssetWriterInput(
mediaType: .video,
outputSettings: settings,
sourceFormatHint: formatHint
)
} else {
input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
}
input.expectsMediaDataInRealTime = true

guard writer.canAdd(input) else {
Expand All @@ -93,27 +111,62 @@ 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
sessionStartDate = 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 +176,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 +207,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 All @@ -166,11 +232,18 @@ class Camera: NSObject, Recordable {

if writer.status == .unknown {
self.offset = buffer.presentationTimeStamp
sessionStartDate = Date()

writer.startWriting()
writer.startSession(atSourceTime: self.offset)

input.append(buffer)
if let start = sessionStartDate,
let overlaid = TimestampOverlay.apply(to: buffer, displayTime: start, source: .camera)
{
input.append(overlaid)
} else {
input.append(buffer)
}
return
}

Expand Down
41 changes: 41 additions & 0 deletions TimeLapze/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"sourceLanguage" : "en",
"strings" : {
"%@" : {
"extractionState" : "stale",
"localizations" : {
"ar" : {
"stringUnit" : {
Expand Down Expand Up @@ -246,6 +247,7 @@
}
},
"%@x faster" : {
"extractionState" : "stale",
"localizations" : {
"ar" : {
"stringUnit" : {
Expand Down Expand Up @@ -1946,6 +1948,9 @@
}
}
}
},
"Camera timestamp" : {

},
"Cameras" : {
"localizations" : {
Expand Down Expand Up @@ -2678,6 +2683,18 @@
}
}
}
},
"Color" : {

},
"Copy path" : {

},
"Copy the folder path to the clipboard" : {

},
"Date and time appear in the top-right corner of recordings when enabled." : {

},
"Define Your Time Multiple" : {
"localizations" : {
Expand Down Expand Up @@ -4374,6 +4391,9 @@
}
}
}
},
"Font" : {

},
"Format" : {
"localizations" : {
Expand Down Expand Up @@ -7061,6 +7081,12 @@
}
}
}
},
"Open in Finder" : {

},
"Open the output folder in Finder" : {

},
"Output FPS" : {
"localizations" : {
Expand Down Expand Up @@ -9499,6 +9525,9 @@
}
}
}
},
"Screen timestamp" : {

},
"Screens" : {
"localizations" : {
Expand Down Expand Up @@ -10719,6 +10748,12 @@
}
}
}
},
"Show on camera" : {

},
"Show on screen recording" : {

},
"Show video after saving" : {
"localizations" : {
Expand Down Expand Up @@ -10963,6 +10998,9 @@
}
}
}
},
"Size: %lld pt" : {

},
"Skip" : {
"localizations" : {
Expand Down Expand Up @@ -13891,6 +13929,9 @@
}
}
}
},
"x faster" : {

},
"Your time multiple is how much faster your output video is than real life" : {
"localizations" : {
Expand Down
Loading