feature: encoding (compress) screenshots

This commit is contained in:
alikia2x (寒寒) 2024-12-23 02:08:18 +08:00
parent 04cf5a8ab8
commit 09b9cb220e
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
6 changed files with 89 additions and 23 deletions

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";

View File

@ -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",

View File

@ -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:

View File

@ -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);
});
}

View File

@ -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));

View File

@ -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");
}
}