improve: better logic for composing FFmpeg command
add: hardware encoding when available
This commit is contained in:
parent
bf7be530a1
commit
bc0483cdc8
@ -1,28 +1,37 @@
|
|||||||
import js from '@eslint/js'
|
import js from "@eslint/js";
|
||||||
import globals from 'globals'
|
import globals from "globals";
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ['dist'] },
|
{ ignores: ["dist"] },
|
||||||
{
|
{
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ["**/*.{ts,tsx}"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
'react-hooks': reactHooks,
|
"react-hooks": reactHooks,
|
||||||
'react-refresh': reactRefresh,
|
"react-refresh": reactRefresh
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
'react-refresh/only-export-components': [
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
'warn',
|
"@typescript-eslint/no-unused-vars": [
|
||||||
{ allowConstantExport: true },
|
"error",
|
||||||
],
|
{
|
||||||
},
|
args: "all",
|
||||||
},
|
argsIgnorePattern: "^_",
|
||||||
)
|
caughtErrors: "all",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
destructuredArrayIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
8338
pnpm-lock.yaml
8338
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,12 @@ import fs from "fs";
|
|||||||
import path, { join } from "path";
|
import path, { join } from "path";
|
||||||
import type { EncodingTask, Frame } from "./schema";
|
import type { EncodingTask, Frame } from "./schema";
|
||||||
import sizeOf from "image-size";
|
import sizeOf from "image-size";
|
||||||
import { getEncodingTempDir, getRecordingsDir, getScreenshotsDir } from "../utils/backend.js";
|
import {
|
||||||
|
getEncodingTempDir,
|
||||||
|
getFFmpegCommand,
|
||||||
|
getRecordingsDir,
|
||||||
|
getScreenshotsDir
|
||||||
|
} from "../utils/backend.js";
|
||||||
import cache from "memory-cache";
|
import cache from "memory-cache";
|
||||||
|
|
||||||
const FRAME_RATE = 0.5;
|
const FRAME_RATE = 0.5;
|
||||||
@ -32,8 +37,8 @@ export function checkFramesForEncoding(db: Database) {
|
|||||||
const currentFrameSize = sizeOf(join(getScreenshotsDir(), frame.imgFilename));
|
const currentFrameSize = sizeOf(join(getScreenshotsDir(), frame.imgFilename));
|
||||||
const lastFrameSize = sizeOf(join(getScreenshotsDir(), lastFrame.imgFilename));
|
const lastFrameSize = sizeOf(join(getScreenshotsDir(), lastFrame.imgFilename));
|
||||||
const twoFramesHaveSameSize =
|
const twoFramesHaveSameSize =
|
||||||
currentFrameSize.width === lastFrameSize.width
|
currentFrameSize.width === lastFrameSize.width &&
|
||||||
&& currentFrameSize.height === lastFrameSize.height;
|
currentFrameSize.height === lastFrameSize.height;
|
||||||
const bufferIsBigEnough = buffer.length >= MIN_FRAMES_TO_ENCODE;
|
const bufferIsBigEnough = buffer.length >= MIN_FRAMES_TO_ENCODE;
|
||||||
const chunkConditionSatisfied = !twoFramesHaveSameSize || bufferIsBigEnough;
|
const chunkConditionSatisfied = !twoFramesHaveSameSize || bufferIsBigEnough;
|
||||||
buffer.push(lastFrame);
|
buffer.push(lastFrame);
|
||||||
@ -50,9 +55,11 @@ export function checkFramesForEncoding(db: Database) {
|
|||||||
`);
|
`);
|
||||||
for (const frame of buffer) {
|
for (const frame of buffer) {
|
||||||
insertStmt.run(taskId, frame.id);
|
insertStmt.run(taskId, frame.id);
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
UPDATE frame SET encodeStatus = 1 WHERE id = ?;
|
UPDATE frame SET encodeStatus = 1 WHERE id = ?;
|
||||||
`).run(frame.id);
|
`
|
||||||
|
).run(frame.id);
|
||||||
}
|
}
|
||||||
console.log(`Created encoding task ${taskId} with ${buffer.length} frames`);
|
console.log(`Created encoding task ${taskId} with ${buffer.length} frames`);
|
||||||
buffer.length = 0;
|
buffer.length = 0;
|
||||||
@ -60,7 +67,7 @@ export function checkFramesForEncoding(db: Database) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEncodedScreenshots(db: Database) {
|
function deleteEncodedScreenshots(db: Database) {
|
||||||
const stmt = db.prepare(`
|
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;
|
||||||
`);
|
`);
|
||||||
@ -74,9 +81,44 @@ export async function deleteEncodedScreenshots(db: Database) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
`);
|
||||||
|
const dbFiles = dbStmt.all() as { imgFilename: string }[];
|
||||||
|
const dbFileSet = new Set(dbFiles.map((f) => f.imgFilename));
|
||||||
|
|
||||||
|
for (const filename of filesInDir) {
|
||||||
|
if (!dbFileSet.has(filename)) {
|
||||||
|
fs.unlinkSync(path.join(screenshotDir, filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUnnecessaryScreenshots(db: Database) {
|
||||||
|
deleteEncodedScreenshots(db);
|
||||||
|
deleteNonExistentScreenshots(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTasksPerforming() {
|
||||||
|
return (cache.get("backend:encodingTasksPerforming") as string[]) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMetaFile(frames: Frame[]) {
|
||||||
|
return frames
|
||||||
|
.map(
|
||||||
|
(frame) =>
|
||||||
|
`file '${path.join(getScreenshotsDir(), frame.imgFilename)}'\nduration 0.03333`
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
// Check and process encoding task
|
// Check and process encoding task
|
||||||
export function processEncodingTasks(db: Database) {
|
export function processEncodingTasks(db: Database) {
|
||||||
const tasksPerforming = cache.get("backend:encodingTasksPerforming") as string[] || [];
|
let tasksPerforming = getTasksPerforming();
|
||||||
if (tasksPerforming.length >= CONCURRENCY) return;
|
if (tasksPerforming.length >= CONCURRENCY) return;
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
@ -109,12 +151,12 @@ export function processEncodingTasks(db: Database) {
|
|||||||
const frames = framesStmt.all(taskId) as Frame[];
|
const frames = framesStmt.all(taskId) as Frame[];
|
||||||
|
|
||||||
const metaFilePath = path.join(getEncodingTempDir(), `${taskId}_meta.txt`);
|
const metaFilePath = path.join(getEncodingTempDir(), `${taskId}_meta.txt`);
|
||||||
const metaContent = frames.map(frame => `file '${path.join(getScreenshotsDir(), frame.imgFilename)}'\nduration 0.03333`).join("\n");
|
const metaContent = createMetaFile(frames);
|
||||||
fs.writeFileSync(metaFilePath, metaContent);
|
fs.writeFileSync(metaFilePath, metaContent);
|
||||||
cache.put("backend:encodingTasksPerforming", [...tasksPerforming, taskId.toString()]);
|
cache.put("backend:encodingTasksPerforming", [...tasksPerforming, taskId.toString()]);
|
||||||
|
|
||||||
const videoPath = path.join(getRecordingsDir(), `${taskId}.mp4`);
|
const videoPath = path.join(getRecordingsDir(), `${taskId}.mp4`);
|
||||||
const ffmpegCommand = `ffmpeg -f concat -safe 0 -i "${metaFilePath}" -c:v libx264 -r 30 -threads 1 "${videoPath}"`;
|
const ffmpegCommand = getFFmpegCommand(metaFilePath, videoPath);
|
||||||
console.log("FFMPEG", ffmpegCommand);
|
console.log("FFMPEG", ffmpegCommand);
|
||||||
exec(ffmpegCommand, (error, _stdout, _stderr) => {
|
exec(ffmpegCommand, (error, _stdout, _stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -136,9 +178,12 @@ export function processEncodingTasks(db: Database) {
|
|||||||
updateFrameStmt.run(`${taskId}.mp4`, frameIndex, frame.id);
|
updateFrameStmt.run(`${taskId}.mp4`, frameIndex, frame.id);
|
||||||
}
|
}
|
||||||
db.prepare(`COMMIT;`).run();
|
db.prepare(`COMMIT;`).run();
|
||||||
|
|
||||||
}
|
}
|
||||||
cache.put("backend:encodingTasksPerforming", tasksPerforming.filter(id => id !== taskId.toString()));
|
tasksPerforming = getTasksPerforming();
|
||||||
|
cache.put(
|
||||||
|
"backend:encodingTasksPerforming",
|
||||||
|
tasksPerforming.filter((id) => id !== taskId.toString())
|
||||||
|
);
|
||||||
fs.unlinkSync(metaFilePath);
|
fs.unlinkSync(metaFilePath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { Database } from "better-sqlite3";
|
|||||||
import { startScreenshotLoop } from "./backend/screenshot.js";
|
import { startScreenshotLoop } from "./backend/screenshot.js";
|
||||||
import { __dirname } from "./dirname.js";
|
import { __dirname } from "./dirname.js";
|
||||||
import { hideDock } from "./utils/electron.js";
|
import { hideDock } from "./utils/electron.js";
|
||||||
import { checkFramesForEncoding, deleteEncodedScreenshots, processEncodingTasks } from "./backend/encoding.js";
|
import { checkFramesForEncoding, deleteUnnecessaryScreenshots, processEncodingTasks } from "./backend/encoding.js";
|
||||||
import honoApp from "./server/index.js";
|
import honoApp from "./server/index.js";
|
||||||
import { serve } from "@hono/node-server";
|
import { serve } from "@hono/node-server";
|
||||||
import { findAvailablePort } from "./utils/server.js";
|
import { findAvailablePort } from "./utils/server.js";
|
||||||
@ -94,7 +94,7 @@ app.on("ready", () => {
|
|||||||
screenshotInterval = startScreenshotLoop(db);
|
screenshotInterval = startScreenshotLoop(db);
|
||||||
setInterval(checkFramesForEncoding, 5000, db);
|
setInterval(checkFramesForEncoding, 5000, db);
|
||||||
setInterval(processEncodingTasks, 10000, db);
|
setInterval(processEncodingTasks, 10000, db);
|
||||||
setInterval(deleteEncodedScreenshots, 5000, db);
|
setInterval(deleteUnnecessaryScreenshots, 20000, db);
|
||||||
dbConnection = db;
|
dbConnection = db;
|
||||||
cache.put("server:dbConnection", dbConnection);
|
cache.put("server:dbConnection", dbConnection);
|
||||||
});
|
});
|
||||||
|
@ -2,13 +2,21 @@ import path from "path";
|
|||||||
import os from "os";
|
import os from "os";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { __dirname } from "../dirname.js";
|
import { __dirname } from "../dirname.js";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import cache from "memory-cache";
|
||||||
|
|
||||||
export function getUserDataDir() {
|
export function getUserDataDir() {
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case "win32":
|
case "win32":
|
||||||
return path.join(process.env.APPDATA!, "OpenRewind", "Record Data");
|
return path.join(process.env.APPDATA!, "OpenRewind", "Record Data");
|
||||||
case "darwin":
|
case "darwin":
|
||||||
return path.join(os.homedir(), "Library", "Application Support", "OpenRewind", "Record Data");
|
return path.join(
|
||||||
|
os.homedir(),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"OpenRewind",
|
||||||
|
"Record Data"
|
||||||
|
);
|
||||||
case "linux":
|
case "linux":
|
||||||
return path.join(os.homedir(), ".config", "OpenRewind", "Record Data");
|
return path.join(os.homedir(), ".config", "OpenRewind", "Record Data");
|
||||||
default:
|
default:
|
||||||
@ -76,3 +84,24 @@ export function getFFmpegPath() {
|
|||||||
throw new Error("Unsupported platform");
|
throw new Error("Unsupported platform");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBestCodec() {
|
||||||
|
const cachedCodec = cache.get("backend:bestCodec");
|
||||||
|
if (cachedCodec) {
|
||||||
|
return cachedCodec;
|
||||||
|
}
|
||||||
|
const codecs = execSync(`${getFFmpegPath()} -codecs`).toString("utf-8");
|
||||||
|
let codec = "";
|
||||||
|
if (codecs.includes("h264_videotoolbox")) {
|
||||||
|
codec = "h264_videotoolbox";
|
||||||
|
} else {
|
||||||
|
codec = "libx264";
|
||||||
|
}
|
||||||
|
cache.put("backend:bestCodec", codec);
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFFmpegCommand(metaFilePath: string, videoPath: string) {
|
||||||
|
const codec = getBestCodec();
|
||||||
|
return `${getFFmpegPath()} -f concat -safe 0 -i "${metaFilePath}" -c:v ${codec} -r 30 -y -threads 1 "${videoPath}"`;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user