feature: encoding (compress) screenshots
This commit is contained in:
parent
04cf5a8ab8
commit
09b9cb220e
@ -1,5 +1,6 @@
|
||||
import gulp from "gulp";
|
||||
import ts from "gulp-typescript";
|
||||
// @ts-ignore
|
||||
import clean from "gulp-clean";
|
||||
import fs from "fs";
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user