Compare commits

..

No commits in common. "main" and "0.8.0" have entirely different histories.
main ... 0.8.0

20 changed files with 120 additions and 467 deletions

View File

@ -2,38 +2,37 @@
OpenRewind is an open-source alternative to [rewind.ai](https://rewind.ai), forked from [OpenRecall](https://github.com/openrecall/openrecall).
We want to create an open source app that provides similar core functionality
We wanted to create an open source app that provides similar core functionality
to rewind.ai, and that app is **OpenRewind**.
## Alpha Release: 0.8.0
Latest works: There is an Alpha version available! We currently only support Apple Silicon Macs.
(Of course, thanks to building on Electron, there will definitely be support for multiple platforms in the beta/stable release)
### ✨ Features
- GUI app. No terminal windows, no need to install any dependencies
- Take a screenshot of your screen every 2 seconds
- Encode screenshots to video at regular intervals
- A full screen "rewind" page similar to Rewind, with scrolling to view captured screenshots
- Screenshots can be taken excluding the "rewind" window
## To-dos
### OCR optimized for the specific platform
### Update the OCR Engine
We will use the OCR API provided by the OS for macOS and Windows.
OpenRecall currently uses docTR as its OCR engine, but it performs inadequately.
On my MacBook Air M2 (2022), processing a screenshot takes around 20 seconds, with CPU usage peaking at over 400%.
During this time, screenshots cannot be captured, and the engine appears to recognize only Latin characters.
Reference projects:
- [ocrit](https://github.com/insidegui/ocrit/)
> We [forked](https://github.com/alikia2x/ocrit) this project to suit our needs
- [Windows.Media.Ocr.Cli](https://github.com/zh-h/Windows.Media.Ocr.Cli)
To address this, we plan to replace the OCR with a more efficient alternative that supports multiple writing systems.
We are working on [RapidOCR ONNX](https://github.com/alikia2x/RapidOCR-ONNX), a fork of a project which has same name,
developed by RapidAI.
RapidOCR ONNX uses [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR) as its model architecture, and
runs on the [ONNX Runtime](https://github.com/microsoft/onnxruntime/).
### Big-little architecture optimizations for Apple Silicon
### Implement a Task Queue/Scheduler
We wrote a small Swift program that allows a given program to run at a selected QoS class. On ARM Mac, this means we can put some work (such as video encoding) to Efficient cores, reducing peak CPU usage and power consumption.
Currently, OpenRecall's OCR recognition and database operations are synchronous (blocking).
This results in increased screenshot frequency, as described in the previous section.
> See: [Prioritize Work with Quality of Service Classes](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html)
Our next goal is to introduce a task queue to handle high-load tasks (such as OCR, indexing, and archiving) asynchronously. This will ensure that time-sensitive tasks (like capturing screenshots) are prioritized.
### Improve the Frontend
The current frontend of OpenRecall is quite basic. Given my expertise in web development,
I will build a more elegant frontend from scratch.
We are now switched to Electron in order to deliver a native experience,
aiming to match the functionality of [rewind.ai](https://rewind.ai).
### Add More Features

BIN
bun.lockb

Binary file not shown.

View File

@ -2,7 +2,8 @@
"appId": "com.alikia2x.openrewind",
"mac": {
"category": "public.app-category.productivity",
"target": "dmg"
"target": "dmg",
"files": ["bin/macos"]
},
"productName": "OpenRewind",
"directories": {

View File

@ -1,5 +1,6 @@
import gulp from "gulp";
import ts from "gulp-typescript";
// @ts-ignore
import clean from "gulp-clean";
import fs from "fs";
@ -29,7 +30,7 @@ gulp.task("assets", () => {
});
gulp.task("binary", () => {
return gulp.src(`bin/${process.platform}-${process.arch}/**/*`, { encoding: false }).pipe(gulp.dest("dist/electron/bin"));
return gulp.src("bin/**/*", { encoding: false }).pipe(gulp.dest("dist/electron/bin"));
});
gulp.task("locales", () => {

View File

@ -1,6 +1,6 @@
{
"name": "openrewind",
"version": "0.10.0",
"version": "0.8.0",
"type": "module",
"description": "Your second brain, superpowered.",
"main": "dist/electron/index.js",
@ -21,7 +21,6 @@
"@alikia/random-key": "npm:@jsr/alikia__random-key",
"@electron/remote": "^2.1.2",
"@hono/node-server": "^1.13.7",
"@types/node-os-utils": "^1.3.4",
"@unly/universal-language-detector": "^2.0.3",
"better-sqlite3": "^11.6.0",
"dayjs": "^1.11.13",
@ -42,8 +41,6 @@
"image-size": "^1.1.1",
"jotai": "^2.11.0",
"memory-cache": "^0.2.0",
"node-os-utils": "^1.3.7",
"pino": "^9.6.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.1.2",

View File

@ -34,7 +34,7 @@ function Image({ src }: { src: string }) {
<img
src={src}
alt="Current frame"
className="w-full h-full object-contain absolute inset-0"
className="w-full h-full object-cover absolute inset-0"
/>
);
}
@ -181,6 +181,8 @@ export default function RewindPage() {
const newIndex = Math.min(Math.max(currentIndex - delta, 0), timeline.length - 1);
const newFrameId = timeline[newIndex].id;
console.log(currentFrameId, lastAvaliableFrameId);
if (newFrameId !== currentFrameId) {
setCurrentFrameId(newFrameId);
// Preload adjacent images
@ -210,19 +212,9 @@ export default function RewindPage() {
return (
<div
ref={containerRef}
className="w-screen h-screen relative dark:text-white overflow-hidden bg-black"
className="w-screen h-screen relative dark:text-white overflow-hidden"
onWheel={handleScroll}
>
<img
src={currentFrameId
? images[currentFrameId] ||
(lastAvaliableFrameId.current ? images[lastAvaliableFrameId.current] : "")
: ""}
alt="background"
className="w-full h-full object-cover absolute inset-0 blur-lg"
/>
{/* Current image */}
<Image
src={
@ -233,8 +225,6 @@ export default function RewindPage() {
}
/>
{/* Time capsule */}
<div
className="absolute bottom-8 left-8 bg-zinc-800 text-white bg-opacity-80 backdrop-blur-lg

View File

@ -1,28 +1,26 @@
import { Database } from "better-sqlite3";
import { exec } from "child_process";
import fs from "fs";
import path, { join } from "path";
import type { EncodingTask, Frame } from "./schema";
import sizeOf from "image-size";
import { getEncodeCommand } from "../utils/index.js";
import { getRecordingsDir, getEncodingTempDir, getScreenshotsDir } from "../utils/index.js";
import { getEncodeCommand } from "../utils/video/index.js";
import { getRecordingsDir, getEncodingTempDir, getScreenshotsDir } from "../utils/fs/index.js";
import cache from "memory-cache";
import { ENCODING_FRAME_INTERVAL, RECORD_FRAME_RATE as FRAME_RATE } from "./consts.js";
import { getDatabase } from "../utils/index.js";
const THREE_MINUTES = 180;
const MIN_FRAMES_TO_ENCODE = THREE_MINUTES * FRAME_RATE;
const CONCURRENCY = 1;
// Detect and insert encoding tasks
export function checkFramesForEncoding() {
const db = getDatabase();
export function checkFramesForEncoding(db: Database) {
const stmt = db.prepare(`
SELECT id, imgFilename, createdAt
FROM frame
WHERE encodeStatus = 0
AND imgFilename IS NOT NULL
ORDER BY createdAt;
`);
WHERE encodeStatus = 0 AND imgFilename IS NOT NULL
ORDER BY createdAt ASC;
`);
const frames = stmt.all() as Frame[];
const buffer: Frame[] = [];
@ -34,12 +32,12 @@ export function checkFramesForEncoding() {
const lastFramePath = join(getScreenshotsDir(), lastFrame.imgFilename!);
if (!fs.existsSync(framePath)) {
console.warn("File not exist:", frame.imgFilename);
deleteFrameFromDB(frame.id);
deleteFrameFromDB(db, frame.id);
continue;
}
if (!fs.existsSync(lastFramePath)) {
console.warn("File not exist:", lastFrame.imgFilename);
deleteFrameFromDB(lastFrame.id);
deleteFrameFromDB(db, lastFrame.id);
continue;
}
const currentFrameSize = sizeOf(framePath);
@ -53,24 +51,20 @@ export function checkFramesForEncoding() {
if (chunkConditionSatisfied) {
// Create new encoding task
const taskStmt = db.prepare(`
INSERT INTO encoding_task (status)
VALUES (0);
INSERT INTO encoding_task (status) VALUES (0);
`);
const taskId = taskStmt.run().lastInsertRowid;
// Insert frames into encoding_task_data
const insertStmt = db.prepare(`
INSERT INTO encoding_task_data (encodingTaskID, frame)
VALUES (?, ?);
INSERT INTO encoding_task_data (encodingTaskID, frame) VALUES (?, ?);
`);
for (const frame of buffer) {
insertStmt.run(taskId, frame.id);
db.prepare(
`
UPDATE frame
SET encodeStatus = 1
WHERE id = ?;
`
UPDATE frame SET encodeStatus = 1 WHERE id = ?;
`
).run(frame.id);
}
console.log(`Created encoding task ${taskId} with ${buffer.length} frames`);
@ -79,39 +73,28 @@ export function checkFramesForEncoding() {
}
}
function deleteEncodedScreenshots() {
const db = getDatabase();
function deleteEncodedScreenshots(db: Database) {
// TODO: double-check that the frame was really encoded into the video
const stmt = db.prepare(`
SELECT *
FROM frame
WHERE encodeStatus = 2
AND imgFilename IS NOT NULL;
SELECT * FROM frame WHERE encodeStatus = 2 AND imgFilename IS NOT NULL;
`);
const frames = stmt.all() as Frame[];
for (const frame of frames) {
const imgPath = path.join(getScreenshotsDir(), frame.imgFilename!);
if (fs.existsSync(imgPath)) {
fs.unlinkSync(imgPath);
}
if (!frame.imgFilename) continue;
fs.unlinkSync(path.join(getScreenshotsDir(), frame.imgFilename));
const updateStmt = db.prepare(`
UPDATE frame
SET imgFilename = NULL
WHERE id = ?;
UPDATE frame SET imgFilename = NULL WHERE id = ?;
`);
updateStmt.run(frame.id);
}
}
function _deleteNonExistentScreenshots() {
const db = getDatabase();
function _deleteNonExistentScreenshots(db: Database) {
const screenshotDir = getScreenshotsDir();
const filesInDir = new Set(fs.readdirSync(screenshotDir));
const dbStmt = db.prepare(`
SELECT imgFilename
FROM frame
WHERE imgFilename IS NOT NULL;
SELECT imgFilename FROM frame WHERE imgFilename IS NOT NULL;
`);
const dbFiles = dbStmt.all() as { imgFilename: string }[];
const dbFileSet = new Set(dbFiles.map((f) => f.imgFilename));
@ -124,17 +107,14 @@ function _deleteNonExistentScreenshots() {
}
}
export async function deleteUnnecessaryScreenshots() {
deleteEncodedScreenshots();
//deleteNonExistentScreenshots();
export async function deleteUnnecessaryScreenshots(db: Database) {
deleteEncodedScreenshots(db);
//deleteNonExistentScreenshots(db);
}
export function deleteFrameFromDB(id: number) {
const db = getDatabase();
export function deleteFrameFromDB(db: Database, id: number) {
const deleteStmt = db.prepare(`
DELETE
FROM frame
WHERE id = ?;
DELETE FROM frame WHERE id = ?;
`);
deleteStmt.run(id);
console.log(`Deleted frame ${id} from database`);
@ -156,16 +136,16 @@ function createMetaFile(frames: Frame[]) {
}
// Check and process encoding task
export function processEncodingTasks() {
const db = getDatabase();
export function processEncodingTasks(db: Database) {
let tasksPerforming = getTasksPerforming();
if (tasksPerforming.length >= CONCURRENCY) return;
const stmt = db.prepare(`
SELECT id, status
FROM encoding_task
WHERE status = 0 LIMIT ?
`);
SELECT id, status
FROM encoding_task
WHERE status = 0
LIMIT ?
`);
const tasks = stmt.all(CONCURRENCY - tasksPerforming.length) as EncodingTask[];
@ -176,19 +156,17 @@ export function processEncodingTasks() {
// Update task status as processing (1)
const updateStmt = db.prepare(`
UPDATE encoding_task
SET status = 1
WHERE id = ?
`);
UPDATE encoding_task SET status = 1 WHERE id = ?
`);
updateStmt.run(taskId);
const framesStmt = db.prepare(`
SELECT frame.imgFilename, frame.id
FROM encoding_task_data
JOIN frame ON encoding_task_data.frame = frame.id
JOIN frame ON encoding_task_data.frame = frame.id
WHERE encoding_task_data.encodingTaskID = ?
ORDER BY frame.createdAt
`);
ORDER BY frame.createdAt ASC
`);
const frames = framesStmt.all(taskId) as Frame[];
const metaFilePath = path.join(getEncodingTempDir(), `${taskId}_meta.txt`);
@ -208,19 +186,13 @@ export function processEncodingTasks() {
console.log(`Video ${videoPath} created successfully`);
// Update task status to complete (2)
const completeStmt = db.prepare(`
UPDATE encoding_task
SET status = 2
WHERE id = ?
`);
UPDATE encoding_task SET status = 2 WHERE id = ?
`);
completeStmt.run(taskId);
for (let frameIndex = 0; frameIndex < frames.length; frameIndex++) {
const frame = frames[frameIndex];
const updateFrameStmt = db.prepare(`
UPDATE frame
SET videoPath = ?,
videoFrameIndex = ?,
encodeStatus = 2
WHERE id = ?
UPDATE frame SET videoPath = ?, videoFrameIndex = ?, encodeStatus = 2 WHERE id = ?
`);
updateFrameStmt.run(`${taskId}.mp4`, frameIndex, frame.id);
}

View File

@ -2,17 +2,17 @@ import * as path from "path";
import { Database } from "better-sqlite3";
import DB from "better-sqlite3";
import { __dirname } from "../dirname.js";
import { getBinDir, getDatabaseDir } from "../utils/index.js";
import { getDatabaseDir } from "../utils/fs/index.js";
import { migrate } from "./migrate/index.js";
function getLibSimpleExtensionPath() {
switch (process.platform) {
case "win32":
return path.join(getBinDir(), "libsimple", "simple.dll");
return path.join(__dirname, "bin", process.platform, "libsimple", "simple.dll");
case "darwin":
return path.join(getBinDir(), "libsimple", "libsimple.dylib");
return path.join(__dirname, "bin", process.platform, "libsimple", "libsimple.dylib");
case "linux":
return path.join(getBinDir(), "libsimple", "libsimple.so");
return path.join(__dirname, "bin", process.platform, "libsimple", "libsimple.so");
default:
throw new Error("Unsupported platform");
}
@ -142,7 +142,7 @@ function init(db: Database) {
export async function initDatabase() {
const dbPath = getDatabaseDir();
const db = new DB(dbPath);
const db = new DB(dbPath, { verbose: console.log });
const libSimpleExtensionPath = getLibSimpleExtensionPath();
db.loadExtension(libSimpleExtensionPath);

View File

@ -1,13 +0,0 @@
type RecognitionResult = RecognitionLine[];
type Pixels = number;
type OriginX = Pixels;
type OriginY = Pixels;
type Width = Pixels;
type Height = Pixels;
type Coordinates = [OriginX, OriginY, Width, Height];
interface RecognitionLine {
text: string;
confidence?: number | number[];
position?: Coordinates;
}

View File

@ -1,253 +0,0 @@
import osu from "node-os-utils";
type TaskId = string;
type TaskFunction = () => void;
interface Task {
id: TaskId;
func: TaskFunction;
interval?: number;
lastRun?: number;
nextRun?: number;
isPaused: boolean;
delayUntil?: number;
requiredSystemState: SystemState;
}
export interface TaskStatus {
status: "NOT_FOUND" | "PAUSED" | "DELAYED" | "SCHEDULED" | "IDLE";
until?: string;
nextRun?: string;
}
type SystemState = "ANY" | "LOW_POWER" | "IDLE";
export class Scheduler {
private tasks: Map<TaskId, Task> = new Map();
private timer: NodeJS.Timeout | null = null;
private monitorTimer: NodeJS.Timeout | null = null;
private cpuUsage: number = 0;
constructor(private readonly minTickInterval: number = 500) {
this.start();
}
private start(): void {
this.scheduleNextTick();
this.monitorTimer = setInterval(() => this.monitor(), 1000);
}
private monitor(): void {
osu.cpu.usage().then((cpuPercentage) => {
this.cpuUsage = cpuPercentage / 100;
})
}
private scheduleNextTick(): void {
if (this.timer) {
clearTimeout(this.timer);
}
const now = Date.now();
let nextTick = now + this.minTickInterval;
for (const task of this.tasks.values()) {
const isTaskPaused = task.isPaused;
const isTaskDelayed = task.delayUntil && now < task.delayUntil;
if (isTaskPaused || isTaskDelayed) {
continue;
}
const nextTaskEarlierThanNextTick = task.nextRun && task.nextRun < nextTick;
if (nextTaskEarlierThanNextTick) {
nextTick = task.nextRun!;
}
}
const delay = Math.max(0, nextTick - now);
this.timer = setTimeout(() => this.tick(), delay);
}
private tickSingleTask(
task: Task,
getNextTick: () => number,
updateNextTick: (nextTick: number) => void
): void {
const now = Date.now();
const isTaskPaused = task.isPaused;
const isTaskDelayed = task.delayUntil && now < task.delayUntil;
if (isTaskPaused || isTaskDelayed) {
return;
}
const taskRequiredLowPower = task.requiredSystemState === "LOW_POWER";
const cpuUsage = this.cpuUsage;
const isSystemLowPower = cpuUsage < 0.75;
const isTaskReadyForLowPowerRun = taskRequiredLowPower ? isSystemLowPower : true;
const reachedTaskNextRun = task.interval && task.nextRun && now >= task.nextRun;
const isTaskReadyForIntervalRun = reachedTaskNextRun && isTaskReadyForLowPowerRun;
if (!isTaskReadyForLowPowerRun) {
this.delayTask(task.id, 1000)
}
if (isTaskReadyForIntervalRun) {
task.func();
task.lastRun = now;
task.nextRun = now + task.interval!;
}
const isTaskNextRunEarlierThanNextTick = task.nextRun && task.nextRun < getNextTick();
if (isTaskNextRunEarlierThanNextTick) {
updateNextTick(task.nextRun!);
}
}
private tick(): void {
const now = Date.now();
let nextTick = now + this.minTickInterval;
for (const task of this.tasks.values()) {
this.tickSingleTask(
task,
() => nextTick,
(v) => (nextTick = v)
);
}
this.scheduleNextTick();
}
/**
* Add a new task to the scheduler.
*
* @param id A unique string identifier for the task.
* @param func The function to be executed by the task.
* @param interval The interval (in milliseconds) between task executions.
* @param requiredSystemState The required system state for the task to run.
*/
addTask(id: TaskId, func: TaskFunction, interval?: number, requiredSystemState: SystemState = "ANY"): void {
this.tasks.set(id, {
id,
func,
interval,
isPaused: false,
lastRun: undefined,
nextRun: interval ? Date.now() + interval : undefined,
requiredSystemState: requiredSystemState,
});
this.scheduleNextTick();
}
/**
* Trigger a task to execute immediately, regardless of its current state.
*
* If the task is paused or delayed, it will not be executed.
*
* @param id The unique string identifier for the task.
*/
triggerTask(id: TaskId): void {
const task = this.tasks.get(id);
if (task && !task.isPaused && (!task.delayUntil || Date.now() >= task.delayUntil)) {
task.func();
task.lastRun = Date.now();
if (task.interval) {
task.nextRun = Date.now() + task.interval;
}
}
this.scheduleNextTick();
}
/**
* Pause a task, so that it will not be executed until it is resumed.
*
* @param id The unique string identifier for the task.
*/
pauseTask(id: TaskId): void {
const task = this.tasks.get(id);
if (task) {
task.isPaused = true;
}
this.scheduleNextTick();
}
/**
* Resume a paused task, so that it can be executed according to its interval.
*
* @param id The unique string identifier for the task.
*/
resumeTask(id: TaskId): void {
const task = this.tasks.get(id);
if (task) {
task.isPaused = false;
}
this.scheduleNextTick();
}
/**
* Delay a task from being executed for a specified amount of time.
*
* @param id The unique string identifier for the task.
* @param delayMs The amount of time in milliseconds to delay the task's execution.
*/
delayTask(id: TaskId, delayMs: number): void {
const task = this.tasks.get(id);
if (task) {
task.delayUntil = Date.now() + delayMs;
if (task.nextRun) {
task.nextRun += delayMs;
}
}
this.scheduleNextTick();
}
setTaskInterval(id: TaskId, interval: number): void {
const task = this.tasks.get(id);
if (task) {
task.interval = interval;
task.nextRun = Date.now() + interval;
}
this.scheduleNextTick();
}
getTaskStatus(id: TaskId): TaskStatus {
const task = this.tasks.get(id);
if (!task) {
return { status: "NOT_FOUND" };
}
if (task.isPaused) {
return { status: "PAUSED" };
}
if (task.delayUntil && Date.now() < task.delayUntil) {
return {
status: "DELAYED",
until: new Date(task.delayUntil).toLocaleString()
};
}
if (task.nextRun) {
return {
status: "SCHEDULED",
nextRun: new Date(task.nextRun).toLocaleString()
};
}
return { status: "IDLE" };
}
stop(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.monitorTimer) {
clearTimeout(this.monitorTimer);
this.monitorTimer = null;
}
}
}

View File

@ -1,23 +1,25 @@
import screenshot from "screenshot-desktop";
import { getDatabase, getScreenshotsDir } from "../utils/index.js";
import { getScreenshotsDir } from "../utils/fs/index.js";
import { join } from "path";
import { Database } from "better-sqlite3";
import SqlString from "sqlstring";
export function takeScreenshot() {
const db = getDatabase();
const timestamp = new Date().getTime();
const screenshotDir = getScreenshotsDir();
const filename = `${timestamp}.png`;
const screenshotPath = join(screenshotDir, filename);
screenshot({ filename: screenshotPath, format: "png" })
.then(() => {
const SQL = SqlString.format(
"INSERT INTO frame (imgFilename, createdAt) VALUES (?, ?)",
[filename, new Date().getTime() / 1000]
);
db.exec(SQL);
})
.catch((err) => {
console.error(err);
});
export function startScreenshotLoop(db: Database) {
return setInterval(() => {
const timestamp = new Date().getTime();
const screenshotDir = getScreenshotsDir();
const filename = `${timestamp}.png`;
const screenshotPath = join(screenshotDir, filename);
screenshot({ filename: screenshotPath, format: "png" })
.then(() => {
const SQL = SqlString.format(
"INSERT INTO frame (imgFilename, createdAt) VALUES (?, ?)",
[filename, new Date().getTime() / 1000]
);
db.exec(SQL);
})
.catch((err) => {
console.error(err);
});
}, 2000);
}

View File

@ -2,7 +2,7 @@ import { app, BrowserWindow, screen } from "electron";
import { join } from "path";
import { __dirname } from "./dirname.js";
import windowStateManager from "electron-window-state";
import { hideDock, showDock } from "./utils/index.js";
import { hideDock, showDock } from "./utils/platform/index.js";
function loadURL(window: BrowserWindow, path = "", vitePort: string) {
const dev = !app.isPackaged;

View File

@ -14,9 +14,9 @@ import initI18n from "./i18n.js";
import { createMainWindow, createSettingsWindow } from "./createWindow.js";
import { initDatabase } from "./backend/init.js";
import { Database } from "better-sqlite3";
import { takeScreenshot } from "./backend/screenshot.js";
import { startScreenshotLoop } from "./backend/screenshot.js";
import { __dirname } from "./dirname.js";
import { hideDock } from "./utils/index.js";
import { hideDock } from "./utils/platform/index.js";
import {
checkFramesForEncoding,
deleteUnnecessaryScreenshots,
@ -24,20 +24,19 @@ import {
} from "./backend/encoding.js";
import honoApp from "./server/index.js";
import { serve } from "@hono/node-server";
import { findAvailablePort } from "./utils/index.js";
import { findAvailablePort } from "./utils/network/index.js";
import cache from "memory-cache";
import { generate as generateAPIKey } from "@alikia/random-key";
import { Scheduler } from "./backend/scheduler.js";
const i18n = initI18n();
const t = i18n.t.bind(i18n);
const port = process.env.PORT || "5173";
const dev = !app.isPackaged;
const scheduler = new Scheduler();
let tray: null | Tray = null;
let dbConnection: null | Database = null;
let screenshotInterval: null | NodeJS.Timeout = null;
let mainWindow: BrowserWindow | null;
let settingsWindow: BrowserWindow | null;
@ -110,10 +109,10 @@ app.on("ready", () => {
});
});
initDatabase().then((db) => {
scheduler.addTask("screenshot", takeScreenshot, 2000);
scheduler.addTask("check-encoding", checkFramesForEncoding, 5000);
scheduler.addTask("process-encoding", processEncodingTasks, 10000, "LOW_POWER");
scheduler.addTask("delete-screenshots", deleteUnnecessaryScreenshots, 20000);
screenshotInterval = startScreenshotLoop(db);
setInterval(checkFramesForEncoding, 5000, db);
setInterval(processEncodingTasks, 10000, db);
setInterval(deleteUnnecessaryScreenshots, 20000, db);
dbConnection = db;
cache.put("server:dbConnection", dbConnection);
});
@ -127,7 +126,7 @@ app.on("ready", () => {
app.on("will-quit", () => {
dbConnection?.close();
scheduler.stop();
if (screenshotInterval) clearInterval(screenshotInterval);
});
ipcMain.on("close-settings", () => {
@ -135,8 +134,8 @@ ipcMain.on("close-settings", () => {
});
ipcMain.handle("request-api-info", () => {
return {
port: cache.get("server:port"),
apiKey: cache.get("server:APIKey")
};
return {
port: cache.get("server:port"),
apiKey: cache.get("server:APIKey")
};
});

View File

@ -10,8 +10,8 @@ import {
getRecordingsDir,
getScreenshotsDir,
waitForFileExists
} from "../utils/index.js";
import { immediatelyExtractFrameFromVideo } from "../utils/index.js";
} from "../utils/fs/index.js";
import { immediatelyExtractFrameFromVideo } from "../utils/video/index.js";
import { existsSync } from "fs";
const app = new Hono();

View File

@ -1,6 +0,0 @@
import { Database } from "better-sqlite3";
import cache from "memory-cache";
export function getDatabase(): Database {
return cache.get("server:dbConnection");
}

View File

@ -1,7 +1,6 @@
import path from "path";
import fs from "fs";
import { getUserDataDir } from "../platform/index.js";
import { __dirname } from "../../dirname.js";
export function createDataDir() {
const dataDir = getUserDataDir();
@ -60,19 +59,6 @@ export function getDecodingTempDir() {
return decodingTempDir;
}
export function getLogDir() {
const dataDir = createDataDir();
const logDir = path.join(dataDir, "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
return logDir;
}
export function getBinDir() {
return path.join(__dirname, "bin");
}
export async function waitForFileExists(filePath: string, timeout: number = 10000): Promise<void> {
return new Promise((resolve, reject) => {
fs.access(filePath, fs.constants.F_OK, (err) => {

View File

@ -2,5 +2,3 @@ export * from "./fs/index.js";
export * from "./platform/index.js";
export * from "./video/index.js";
export * from "./network/index.js";
export * from "./logging/index.js";
export * from "./backend/index.js";

View File

@ -1,10 +0,0 @@
import pino from "pino";
import { join } from "path";
import { getLogDir } from "../fs/index.js";
const logPath = join(getLogDir(), "log.json");
const dest = pino.destination(logPath);
const logger = pino(dest);
export { logger };

View File

@ -1,14 +1,14 @@
import { join } from "path";
import path from "path";
import os from "os";
import { app } from "electron";
import { getBinDir, logger } from "../index.js";
import { __dirname } from "../../dirname.js";
export function getUserDataDir() {
switch (process.platform) {
case "win32":
return join(process.env.APPDATA!, "OpenRewind", "Record Data");
return path.join(process.env.APPDATA!, "OpenRewind", "Record Data");
case "darwin":
return join(
return path.join(
os.homedir(),
"Library",
"Application Support",
@ -16,7 +16,7 @@ export function getUserDataDir() {
"Record Data"
);
case "linux":
return join(os.homedir(), ".config", "OpenRewind", "Record Data");
return path.join(os.homedir(), ".config", "OpenRewind", "Record Data");
default:
throw new Error("Unsupported platform");
}
@ -37,24 +37,14 @@ export function showDock() {
}
export function getFFmpegPath() {
let path = "";
switch (process.platform) {
case "win32":
path = join(getBinDir(), "ffmpeg.exe");
break;
return path.join(__dirname, "bin", process.platform, "ffmpeg.exe");
case "darwin":
path = join(getBinDir(), "ffmpeg");
break;
return path.join(__dirname, "bin", process.platform, "ffmpeg");
case "linux":
path = join(getBinDir(), "ffmpeg");
break;
return path.join(__dirname, "bin", process.platform, "ffmpeg");
default:
throw new Error("Unsupported platform");
}
logger.info("FFmpeg path: %s", path);
return path;
}
export function getOCRitPath() {
const path = join(getBinDir(), "ocrit");
}

View File

@ -44,7 +44,7 @@ export function immediatelyExtractFrameFromVideo(
"1",
`${outputPathArg}`
];
const ffmpeg = spawn(getFFmpegPath(), args);
const ffmpeg = spawn("ffmpeg", args);
ffmpeg.stdout.on("data", (data) => {
console.log(data.toString());
});