Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix chunk indexing #90

Merged
merged 7 commits into from
Apr 29, 2024
Merged
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
70 changes: 66 additions & 4 deletions rem/DB.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ class DatabaseManager {
private let uniqueAppNames = Table("unique_application_names")

let allText = VirtualTable("allText")
let chunksFramesView = View("chunks_frames_view")

private let id = Expression<Int64>("id")
private let offsetIndex = Expression<Int64>("offsetIndex")
private let chunkId = Expression<Int64>("chunkId")
private let chunksFramesIndex = Expression<Int64>("chunksFramesIndex")
private let timestamp = Expression<Date>("timestamp")
private let filePath = Expression<String>("filePath")
private let activeApplicationName = Expression<String?>("activeApplicationName")
Expand All @@ -43,6 +45,7 @@ class DatabaseManager {
private var currentChunkId: Int64 = 0 // Initialize with a default value
private var lastFrameId: Int64 = 0
private var currentFrameOffset: Int64 = 0
private var lastChunksFramesIndex: Int64 = 0

init() {
if let savedir = RemFileManager.shared.getSaveDir() {
Expand All @@ -54,6 +57,7 @@ class DatabaseManager {
createTables()
currentChunkId = getCurrentChunkId()
lastFrameId = getLastFrameId()
lastChunksFramesIndex = getLastChunksFramesIndex()
}

func purge() {
Expand Down Expand Up @@ -116,6 +120,26 @@ class DatabaseManager {

// Text search
try! db.run(allText.create(.FTS4(config), ifNotExists: true))

// Create chunksFramesView (ensures all frames have associated chunks)
let viewSQL = """
CREATE VIEW IF NOT EXISTS chunks_frames_view AS
SELECT
ROW_NUMBER() OVER (ORDER BY vc.id, f.id) as chunksFramesIndex,
vc.id as chunkId,
vc.filePath,
f.id as frameId,
f.timestamp,
f.activeApplicationName,
f.offsetIndex
FROM
video_chunks vc
JOIN
frames f ON vc.id = f.chunkId
ORDER BY
vc.id, f.id;
"""
try! db.run(viewSQL)
}

private func createIndices() {
Expand All @@ -124,6 +148,8 @@ class DatabaseManager {
try db.run(frames.createIndex(chunkId, id, unique: false, ifNotExists: true))
try db.run(frames.createIndex(timestamp, ifNotExists: true))

// For speeding up chunksFramesView
try db.run(videoChunks.createIndex(id, unique: true, ifNotExists: true))
// Additional indices can be added here as needed
} catch {
print("Failed to create indices: \(error)")
Expand All @@ -132,8 +158,8 @@ class DatabaseManager {

private func getCurrentChunkId() -> Int64 {
do {
if let lastChunk = try db.pluck(videoChunks.order(id.desc)) {
return lastChunk[id] + 1
if let lastFrame = try db.pluck(frames.order(id.desc)) {
return lastFrame[chunkId] + 1
}
} catch {
print("Error fetching last chunk ID: \(error)")
Expand All @@ -147,17 +173,29 @@ class DatabaseManager {
return lastFrame[id]
}
} catch {
print("Error fetching last chunk ID: \(error)")
print("Error fetching last frame ID: \(error)")
}
return 0
}

private func getLastChunksFramesIndex() -> Int64 {
do {
if let lastFrame = try db.pluck(chunksFramesView.order(chunksFramesIndex.desc)) {
return lastFrame[chunksFramesIndex]
}
} catch {
print("Error fetching last chunks frames Index: \(error)")
}
return 0
}

// Insert a new video chunk and return its ID
func startNewVideoChunk(filePath: String) -> Int64 {
let insert = videoChunks.insert(self.filePath <- filePath)
let insert = videoChunks.insert(self.id <- currentChunkId, self.filePath <- filePath)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could remove auto increment but I don't see the harm in keeping the requirements of increasing and unique

let id = try! db.run(insert)
currentChunkId = id + 1
currentFrameOffset = 0
lastChunksFramesIndex = getLastChunksFramesIndex()
return id
}

Expand Down Expand Up @@ -208,6 +246,19 @@ class DatabaseManager {
if let frame = try db.pluck(query) {
return (frame[offsetIndex], frame[filePath])
}
} catch {
return nil
}

return nil
}

func getFrameByChunksFramesIndex(forIndex index: Int64) -> (offsetIndex: Int64, filePath: String)? {
do {
let query = chunksFramesView.filter(chunksFramesIndex == index).limit(1)
if let frame = try db.pluck(query) {
return (frame[offsetIndex], frame[filePath])
}

// let justFrameQuery = frames.filter(frames[id] === index).limit(1)
// try! db.run(justFrameQuery.delete())
Expand Down Expand Up @@ -283,6 +334,10 @@ class DatabaseManager {
return lastFrameId
}

func getMaxChunksFramesIndex() -> Int64 {
return lastChunksFramesIndex
}

func getLastAccessibleFrame() -> Int64 {
do {
let query = frames.join(videoChunks, on: chunkId == videoChunks[id]).select(frames[id]).order(frames[id].desc).limit(1)
Expand Down Expand Up @@ -387,6 +442,13 @@ class DatabaseManager {
return extractFrame(from: videoURL, frameOffset: frameData.offsetIndex, maxSize: maxSize)
}

func getImageByChunksFramesIndex(index: Int64, maxSize: CGSize? = nil) -> CGImage? {
guard let frameData = DatabaseManager.shared.getFrameByChunksFramesIndex(forIndex: index) else { return nil }

let videoURL = URL(fileURLWithPath: frameData.filePath)
return extractFrame(from: videoURL, frameOffset: frameData.offsetIndex, maxSize: maxSize)
}

func getImages(filePath: String, frameOffsets: [Int64], maxSize: CGSize? = nil) -> AVAssetImageGenerator.Images {
let videoURL = URL(fileURLWithPath: filePath)
return extractFrames(from: videoURL, frameOffsets: frameOffsets, maxSize: maxSize)
Expand Down
12 changes: 6 additions & 6 deletions rem/TimelineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct TimelineView: View {
var body: some View {
ZStack {
let frame = NSScreen.main?.frame ?? NSRect.zero
let image = DatabaseManager.shared.getImage(index: viewModel.currentFrameIndex)
let image = DatabaseManager.shared.getImageByChunksFramesIndex(index: viewModel.currentFrameIndex)
let nsImage = image.flatMap { NSImage(cgImage: $0, size: NSSize(width: $0.width, height: $0.height)) }

CustomHostingControllerRepresentable(
Expand Down Expand Up @@ -74,7 +74,7 @@ struct TimelineView: View {

private func analyzeImage(index: Int64) {
Task {
if let image = DatabaseManager.shared.getImage(index: index) {
if let image = DatabaseManager.shared.getImageByChunksFramesIndex(index: index) {
let configuration = ImageAnalyzer.Configuration([.text])
do {
let analysis = try await imageAnalyzer.analyze(image, orientation: CGImagePropertyOrientation.up, configuration: configuration)
Expand Down Expand Up @@ -290,7 +290,7 @@ class TimelineViewModel: ObservableObject {
private var indexUpdateThrottle = Throttler(delay: 0.05)

init() {
let maxFrame = DatabaseManager.shared.getMaxFrame()
let maxFrame = DatabaseManager.shared.getMaxChunksFramesIndex()
currentFrameIndex = maxFrame
currentFrameContinuous = Double(maxFrame)
}
Expand All @@ -299,21 +299,21 @@ class TimelineViewModel: ObservableObject {
// Logic to update the index based on the delta
// This method will be called from AppDelegate
let nextValue = currentFrameContinuous - delta * speedFactor
let maxValue = Double(DatabaseManager.shared.getMaxFrame())
let maxValue = Double(DatabaseManager.shared.getMaxChunksFramesIndex())
let clampedValue = min(max(1, nextValue), maxValue)
self.currentFrameContinuous = clampedValue
self.updateIndexSafely()
}

func updateIndex(withIndex: Int64) {
let maxValue = Double(DatabaseManager.shared.getMaxFrame())
let maxValue = Double(DatabaseManager.shared.getMaxChunksFramesIndex())
let clampedValue = min(max(1, Double(withIndex)), maxValue)
self.currentFrameContinuous = clampedValue
self.updateIndexSafely()
}

func setIndexToLatest() {
let maxFrame = DatabaseManager.shared.getMaxFrame()
let maxFrame = DatabaseManager.shared.getMaxChunksFramesIndex()
DispatchQueue.main.async {
self.currentFrameContinuous = Double(maxFrame)
self.currentFrameIndex = maxFrame
Expand Down
34 changes: 27 additions & 7 deletions rem/remApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {
}
let menu = NSMenu()
let recordingTitle = self.isCapturing == .recording ? "Stop Remembering" : "Start Remembering"
let recordingSelector = self.isCapturing == .recording ? #selector(self.disableRecording) : #selector(self.enableRecording)
let recordingSelector = self.isCapturing == .recording ? #selector(self.userDisableRecording) : #selector(self.enableRecording)
menu.addItem(NSMenuItem(title: recordingTitle, action: recordingSelector, keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Toggle Timeline", action: #selector(self.toggleTimeline), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Search", action: #selector(self.showSearchView), keyEquivalent: ""))
Expand Down Expand Up @@ -225,7 +225,7 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {
if isTimelineOpen() {
closeTimelineView()
} else {
let frame = DatabaseManager.shared.getMaxFrame()
let frame = DatabaseManager.shared.getMaxChunksFramesIndex()
showTimelineView(with: frame)
}
}
Expand Down Expand Up @@ -278,7 +278,7 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {

if event.scrollingDeltaY < 0 && !isTimelineOpen() { // Check if scroll up
DispatchQueue.main.async { [weak self] in
self?.showTimelineView(with: DatabaseManager.shared.getMaxFrame())
self?.showTimelineView(with: DatabaseManager.shared.getMaxChunksFramesIndex())
}
}
}
Expand Down Expand Up @@ -483,8 +483,6 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {
if let savedir = RemFileManager.shared.getSaveDir() {
let outputPath = savedir.appendingPathComponent("output-\(Date().timeIntervalSince1970).mp4").path

let _ = DatabaseManager.shared.startNewVideoChunk(filePath: outputPath)

// Setup the FFmpeg process for the chunk
let ffmpegProcess = Process()
let bundleURL = Bundle.main.bundleURL
Expand Down Expand Up @@ -531,6 +529,17 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {

// Close the pipe and handle the process completion
ffmpegInputPipe.fileHandleForWriting.closeFile()
ffmpegProcess.waitUntilExit()

// Check if FFmpeg process completed successfully
if ffmpegProcess.terminationStatus == 0 {
// Start new video chunk in database only if FFmpeg succeeds
let _ = DatabaseManager.shared.startNewVideoChunk(filePath: outputPath)
logger.info("Video successfully saved and registered.")
} else {
logger.error("FFmpeg failed to process video chunk.")
}


// Read FFmpeg's output and error
let outputData = ffmpegOutputPipe.fileHandleForReading.readDataToEndOfFile()
Expand All @@ -548,6 +557,9 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {
}

@objc func enableRecording() {
if isCapturing == .recording {
return
}
isCapturing = .recording

Task {
Expand All @@ -560,6 +572,12 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {
logger.info("Screen capture paused")
}

@objc func userDisableRecording() {
wasRecordingBeforeSearchView = false
wasRecordingBeforeTimelineView = false
disableRecording()
}

@objc func disableRecording() {
if isCapturing != .recording {
return
Expand Down Expand Up @@ -645,8 +663,9 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {
}

@objc func showTimelineView(with index: Int64) {
wasRecordingBeforeTimelineView = (isCapturing == .recording)
wasRecordingBeforeTimelineView = (isCapturing == .recording) || wasRecordingBeforeSearchView // handle going from search to TL
disableRecording()
wasRecordingBeforeSearchView = false
closeSearchView()
if timelineViewWindow == nil {
let screenRect = NSScreen.main?.frame ?? NSRect.zero
Expand Down Expand Up @@ -710,8 +729,9 @@ func drawStatusBarIcon(rect: CGRect) -> Bool {
}

@objc func showSearchView() {
wasRecordingBeforeSearchView = (isCapturing == .recording)
wasRecordingBeforeSearchView = (isCapturing == .recording) || wasRecordingBeforeTimelineView
disableRecording()
wasRecordingBeforeTimelineView = false
closeTimelineView()
// Ensure that the search view window is created and shown
if searchViewWindow == nil {
Expand Down