diff --git a/gulpfile.ts b/gulpfile.ts index 2bd31da..39f1f02 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -1,5 +1,6 @@ import gulp from "gulp"; import ts from "gulp-typescript"; +// @ts-ignore import clean from "gulp-clean"; import fs from "fs"; diff --git a/package.json b/package.json index 0f59b41..06a6520 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "i18next-fs-backend": "^2.6.0", "i18next-icu": "^2.3.0", "image-size": "^1.1.1", + "memory-cache": "^0.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.1.2", @@ -49,6 +50,7 @@ "@iconify-icon/react": "^2.1.0", "@types/better-sqlite3": "^7.6.12", "@types/gulp": "^4.0.17", + "@types/memory-cache": "^0.2.6", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/screenshot-desktop": "^1.12.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94f0b55..a01479d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: image-size: specifier: ^1.1.1 version: 1.1.1 + memory-cache: + specifier: ^0.2.0 + version: 0.2.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -99,6 +102,9 @@ importers: '@types/gulp': specifier: ^4.0.17 version: 4.0.17 + '@types/memory-cache': + specifier: ^0.2.6 + version: 0.2.6 '@types/react': specifier: ^18.3.12 version: 18.3.14 @@ -777,6 +783,9 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/memory-cache@0.2.6': + resolution: {integrity: sha512-G+9tuwWqss2hvX+T/RNY9CY8qSvuCx8uwnl+tXQWwXtBelXUGcn4j6zknDR+2EfdrgBsMZik0jCKNlDEHIBONQ==} + '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} @@ -2656,6 +2665,9 @@ packages: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} + memory-cache@0.2.0: + resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4663,6 +4675,8 @@ snapshots: dependencies: '@types/node': 22.10.1 + '@types/memory-cache@0.2.6': {} + '@types/ms@0.7.34': {} '@types/node@20.17.9': @@ -6865,6 +6879,8 @@ snapshots: escape-string-regexp: 4.0.0 optional: true + memory-cache@0.2.0: {} + merge2@1.4.1: {} micromatch@4.0.8: diff --git a/src/electron/backend/encoding.ts b/src/electron/backend/encoding.ts index fde5d82..dc811c5 100644 --- a/src/electron/backend/encoding.ts +++ b/src/electron/backend/encoding.ts @@ -1,14 +1,15 @@ -import { Database } from 'better-sqlite3'; -import { exec } from 'child_process'; -import fs from 'fs'; +import { Database } from "better-sqlite3"; +import { exec, spawnSync } from "child_process"; +import fs from "fs"; import path, { join } from "path"; import type { EncodingTask, Frame } from "./schema"; import sizeOf from "image-size"; -import { getScreenshotsDir } from "../utils/backend.js"; +import { getEncodingTempDir, getRecordingsDir, getScreenshotsDir } from "../utils/backend.js"; +import cache from "memory-cache"; const ENCODING_INTERVAL = 10000; // 10 sec const CHECK_TASK_INTERVAL = 5000; // 5 sec -const MIN_FRAMES_TO_ENCODE = 300; // At least 10 mins (0.5fps) +const MIN_FRAMES_TO_ENCODE = 60; // At least 10 mins (0.5fps) const CONCURRENCY = 1; // Number of concurrent encoding tasks // Detect and insert encoding tasks @@ -59,9 +60,25 @@ export function checkFramesForEncoding(db: Database) { } } -// TODO: Fix this function +export async function deleteEncodedScreenshots(db: Database) { + const stmt = db.prepare(` + SELECT * FROM frame WHERE encodeStatus = 2 AND imgFilename IS NOT NULL; + `); + const frames = stmt.all() as Frame[]; + for (const frame of frames) { + fs.unlinkSync(path.join(getScreenshotsDir(), frame.imgFilename)); + const updateStmt = db.prepare(` + UPDATE frame SET imgFilename = NULL WHERE id = ?; + `); + updateStmt.run(frame.id); + } +} + // Check and process encoding task -function processEncodingTasks(db: Database) { +export function processEncodingTasks(db: Database) { + const tasksPerforming = cache.get("tasksPerforming") as string[] || []; + if (tasksPerforming.length >= CONCURRENCY) return; + const stmt = db.prepare(` SELECT id, status FROM encoding_task @@ -69,10 +86,12 @@ function processEncodingTasks(db: Database) { LIMIT ? `); - const tasks = stmt.all(CONCURRENCY) as EncodingTask[]; + const tasks = stmt.all(CONCURRENCY - tasksPerforming.length) as EncodingTask[]; for (const task of tasks) { const taskId = task.id; + // Create transaction + db.prepare(`BEGIN TRANSACTION;`).run(); // Update task status as processing (1) const updateStmt = db.prepare(` @@ -81,37 +100,45 @@ function processEncodingTasks(db: Database) { updateStmt.run(taskId); const framesStmt = db.prepare(` - SELECT frame.imgFilename + SELECT frame.imgFilename, frame.id FROM encoding_task_data JOIN frame ON encoding_task_data.frame = frame.id WHERE encoding_task_data.encodingTaskID = ? - ORDER BY frame.createAt ASC + ORDER BY frame.createdAt ASC `); const frames = framesStmt.all(taskId) as Frame[]; - const metaFilePath = path.join(__dirname, `${taskId}_meta.txt`); - const metaContent = frames.map(frame => `file '${frame.imgFilename}'`).join('\n'); + const metaFilePath = path.join(getEncodingTempDir(), `${taskId}_meta.txt`); + const metaContent = frames.map(frame => `file '${path.join(getScreenshotsDir(), frame.imgFilename)}'\nduration 0.03333`).join("\n"); fs.writeFileSync(metaFilePath, metaContent); + cache.put("tasksPerforming", [...tasksPerforming, taskId.toString()]); - const videoName = `video_${taskId}.mp4`; - const ffmpegCommand = `ffmpeg -f concat -safe 0 -i ${metaFilePath} -c:v libx264 -r 30 ${videoName}`; + const videoPath = path.join(getRecordingsDir(), `${taskId}.mp4`); + const ffmpegCommand = `ffmpeg -f concat -safe 0 -i "${metaFilePath}" -c:v libx264 -r 30 "${videoPath}"`; + console.log("FFMPEG", ffmpegCommand); exec(ffmpegCommand, (error, stdout, stderr) => { if (error) { console.error(`FFmpeg error: ${error.message}`); - // Set task status to unprocessed (0) - const failStmt = db.prepare(` - UPDATE encoding_task SET status = 0 WHERE id = ? - `); - failStmt.run(taskId); + // Roll back transaction + db.prepare(`ROLLBACK;`).run(); } else { - console.log(`Video ${videoName} created successfully`); + console.log(`Video ${videoPath} created successfully`); // Update task status to complete (2) const completeStmt = db.prepare(` 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 = ? + `); + updateFrameStmt.run(`${taskId}.mp4`, frameIndex, frame.id); + } + db.prepare(`COMMIT;`).run(); + } + cache.put("tasksPerforming", tasksPerforming.filter(id => id !== taskId.toString())); fs.unlinkSync(metaFilePath); }); } diff --git a/src/electron/index.ts b/src/electron/index.ts index a733bf6..a165845 100644 --- a/src/electron/index.ts +++ b/src/electron/index.ts @@ -8,7 +8,7 @@ import { Database } from "better-sqlite3"; import { startScreenshotLoop } from "./backend/screenshot.js"; import { __dirname } from "./dirname.js"; import { hideDock } from "./utils/electron.js"; -import { checkFramesForEncoding } from "./backend/encoding.js"; +import { checkFramesForEncoding, deleteEncodedScreenshots, processEncodingTasks } from "./backend/encoding.js"; const i18n = initI18n(); @@ -76,7 +76,9 @@ app.on("ready", () => { createTray(); initDatabase().then((db) => { screenshotInterval = startScreenshotLoop(db); - setInterval(checkFramesForEncoding, 10000, db); + setInterval(checkFramesForEncoding, 5000, db); + setInterval(processEncodingTasks, 10000, db); + setInterval(deleteEncodedScreenshots, 5000, db) dbConnection = db; }); mainWindow = createMainWindow(port, () => (mainWindow = null)); diff --git a/src/electron/utils/backend.ts b/src/electron/utils/backend.ts index feb531a..990f544 100644 --- a/src/electron/utils/backend.ts +++ b/src/electron/utils/backend.ts @@ -1,6 +1,7 @@ import path from "path"; import os from "os"; import fs from "fs"; +import { __dirname } from "../dirname.js"; export function getUserDataDir() { switch (process.platform) { @@ -47,6 +48,10 @@ export function getScreenshotsDir() { export function getRecordingsDir() { const dataDir = createDataDir(); + const recordingsDir = path.join(dataDir, "recordings"); + if (!fs.existsSync(recordingsDir)) { + fs.mkdirSync(recordingsDir, { recursive: true }); + } return path.join(dataDir, "recordings"); } @@ -58,3 +63,16 @@ export function getEncodingTempDir() { } return encodingTempDir; } + +export function getFFmpegPath() { + switch (process.platform) { + case "win32": + return path.join(__dirname, "bin", process.platform, "ffmpeg.exe"); + case "darwin": + return path.join(__dirname, "bin", process.platform, "ffmpeg"); + case "linux": + return path.join(__dirname, "bin", process.platform, "ffmpeg"); + default: + throw new Error("Unsupported platform"); + } +}