From 1f9b8ea124e08960e43fe0ed0ae0579d9350955e Mon Sep 17 00:00:00 2001 From: alikia2x Date: Mon, 9 Dec 2024 00:23:05 +0800 Subject: [PATCH] update: function to add frames into encoding queue add: scripts to migrate to V3 schema TODO: fix processEncodingTasks() in `encoding.ts` TODO: write docs for V3 schema --- package.json | 7 +- pnpm-lock.yaml | 132 ++++++++------ src/electron/backend/encoding.ts | 118 ++++++++++++ src/electron/backend/init.ts | 58 +++++- src/electron/backend/migrate/index.ts | 26 +++ src/electron/backend/migrate/migrateToV2.ts | 2 +- src/electron/backend/migrate/migrateToV3.ts | 191 ++++++++++++++++++++ src/electron/backend/schema.d.ts | 22 +++ src/electron/backend/screenshot.ts | 4 +- src/electron/index.ts | 8 +- src/electron/utils/backend.ts | 11 +- 11 files changed, 503 insertions(+), 76 deletions(-) create mode 100644 src/electron/backend/encoding.ts create mode 100644 src/electron/backend/migrate/migrateToV3.ts create mode 100644 src/electron/backend/schema.d.ts diff --git a/package.json b/package.json index b3e225a..c3f1e01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openrewind", - "version": "0.4.0", + "version": "0.5.0", "type": "module", "description": "Your second brain, superpowered.", "main": "dist/electron/index.js", @@ -32,15 +32,16 @@ "i18next-electron-fs-backend": "^3.0.2", "i18next-fs-backend": "^2.6.0", "i18next-icu": "^2.3.0", + "image-size": "^1.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.1.2", "react-router": "^7.0.1", "react-router-dom": "^7.0.1", + "screenshot-desktop": "^1.15.0", "sqlite3": "^5.1.7", "sqlstring": "^2.3.3", - "vite-tsconfig-paths": "^5.1.3", - "screenshot-desktop": "^1.15.0" + "vite-tsconfig-paths": "^5.1.3" }, "devDependencies": { "@electron/rebuild": "^3.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de17652..94f0b55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 2.0.3 better-sqlite3: specifier: ^11.6.0 - version: 11.6.0 + version: 11.7.0 electron-context-menu: specifier: ^4.0.4 version: 4.0.4 @@ -37,13 +37,13 @@ importers: version: 5.0.3 execa: specifier: ^9.5.1 - version: 9.5.1 + version: 9.5.2 i18next: specifier: ^24.0.2 version: 24.0.5(typescript@5.6.3) i18next-browser-languagedetector: specifier: ^8.0.0 - version: 8.0.0 + version: 8.0.1 i18next-electron-fs-backend: specifier: ^3.0.2 version: 3.0.2 @@ -52,7 +52,10 @@ importers: version: 2.6.0 i18next-icu: specifier: ^2.3.0 - version: 2.3.0(intl-messageformat@10.7.7) + version: 2.3.0(intl-messageformat@10.7.8) + image-size: + specifier: ^1.1.1 + version: 1.1.1 react: specifier: ^18.3.1 version: 18.3.1 @@ -131,7 +134,7 @@ importers: version: 0.0.3 electron-builder: specifier: ^25.1.8 - version: 25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + version: 25.1.8(electron-builder-squirrel-windows@25.1.8) eslint: specifier: ^9.13.0 version: 9.16.0(jiti@1.21.6) @@ -487,17 +490,17 @@ packages: resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@formatjs/ecma402-abstract@2.2.4': - resolution: {integrity: sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==} + '@formatjs/ecma402-abstract@2.2.5': + resolution: {integrity: sha512-ep/5vGkyZvMSi6s8nQG8k7vTcKjuXs402fgGIWixj0AWRgKbeaZeLuYc32NIPXexgBjWepMeZGgHLuZXkuD2Gg==} - '@formatjs/fast-memoize@2.2.3': - resolution: {integrity: sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==} + '@formatjs/fast-memoize@2.2.4': + resolution: {integrity: sha512-8SzI0cBADgbLOYsoQW/IqVHljCH964CrOdESFQ07wMkRLP90+MfV7k6gZPiGD88ubqET9igJV5c292rT28B7xQ==} - '@formatjs/icu-messageformat-parser@2.9.4': - resolution: {integrity: sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==} + '@formatjs/icu-messageformat-parser@2.9.5': + resolution: {integrity: sha512-mHauC9wuVXtnshAIoAYjlNrh6+OFOT6cC4fpK+AG+DHkVWwIPFVQE28hLQ/KptuvQ8VMfG/zYx6rRjtaeFPkSQ==} - '@formatjs/icu-skeleton-parser@1.8.8': - resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==} + '@formatjs/icu-skeleton-parser@1.8.9': + resolution: {integrity: sha512-1KSSlU7ywsU5E5v7xr6VTlBzLGszMi3GOu7EVINjkfA501GN5OkeNSbd5q6ie1wIknZJGBlqkvXPYYdp3YXjpw==} '@formatjs/intl-localematcher@0.5.8': resolution: {integrity: sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==} @@ -929,8 +932,8 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - agent-base@7.1.2: - resolution: {integrity: sha512-JVzqkCNRT+VfqzzgPWDPnwvDheSAUdiMUn3NoLXpDJF5lRqeJqyC9iGsAxIOAW+mzIdq+uP1TvcX6bMtrH0agg==} + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} agentkeepalive@4.5.0: @@ -1173,8 +1176,8 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - better-sqlite3@11.6.0: - resolution: {integrity: sha512-2J6k/eVxcFYY2SsTxsXrj6XylzHWPxveCn4fKPKZFv/Vqn/Cd7lOuX4d7rGQXT5zL+97MkNL3nSbCrIoe3LkgA==} + better-sqlite3@11.7.0: + resolution: {integrity: sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -1407,8 +1410,8 @@ packages: engines: {node: '>=18'} hasBin: true - conf@13.0.1: - resolution: {integrity: sha512-l9Uwc9eOnz39oADzGO2cSBDi7siv8lwO+31ocQ2nOJijnDiW3pxqm9VV10DPYUO28wW83DjABoUqY1nfHRR2hQ==} + conf@13.1.0: + resolution: {integrity: sha512-Bi6v586cy1CoTFViVO4lGTtx780lfF96fUmS1lSX6wpZf6330NvHUu6fReVuDP1de8Mg0nkZb01c8tAQdz1o3w==} engines: {node: '>=18'} config-file-ts@0.2.8-rc1: @@ -1766,8 +1769,8 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - execa@9.5.1: - resolution: {integrity: sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==} + execa@9.5.2: + resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} engines: {node: ^18.19.0 || >=20.5.0} expand-template@2.0.3: @@ -2208,8 +2211,8 @@ packages: i18next-browser-languagedetector@4.0.1: resolution: {integrity: sha512-RxSoX6mB8cab0CTIQ+klCS764vYRj+Jk621cnFVsINvcdlb/cdi3vQFyrPwmnowB7ReUadjHovgZX+RPIzHVQQ==} - i18next-browser-languagedetector@8.0.0: - resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==} + i18next-browser-languagedetector@8.0.1: + resolution: {integrity: sha512-z9ZuWA7qxbww+cPtdJTgV0O2H9+qlLpQnb37RpnwfsWnUmrO+q92gbVKVtfBL7jRvxfmVMOUKxKGg6VBqO49Pg==} i18next-electron-fs-backend@3.0.2: resolution: {integrity: sha512-KRP+4ORx0WG31qHvMNUpI4CytEQFAkFtXnrZ9/NBXH6k/DcKU3IdB573Zl+L+lR4GA6PCKyBX89mqrUhStoItA==} @@ -2252,6 +2255,11 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + image-size@1.1.1: + resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==} + engines: {node: '>=16.x'} + hasBin: true + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2287,8 +2295,8 @@ packages: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} - intl-messageformat@10.7.7: - resolution: {integrity: sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==} + intl-messageformat@10.7.8: + resolution: {integrity: sha512-XnFFzJnTfdaDqeiF/ZAUjpkoKEM8UKwHijQXuqpLiM42kuJCawytP/rYAMDYNNaWww/PTaI0rIoG4oUjRrRlnA==} ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} @@ -3167,6 +3175,9 @@ packages: queue-tick@1.0.1: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -4382,25 +4393,25 @@ snapshots: dependencies: levn: 0.4.1 - '@formatjs/ecma402-abstract@2.2.4': + '@formatjs/ecma402-abstract@2.2.5': dependencies: - '@formatjs/fast-memoize': 2.2.3 + '@formatjs/fast-memoize': 2.2.4 '@formatjs/intl-localematcher': 0.5.8 tslib: 2.8.1 - '@formatjs/fast-memoize@2.2.3': + '@formatjs/fast-memoize@2.2.4': dependencies: tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@2.9.4': + '@formatjs/icu-messageformat-parser@2.9.5': dependencies: - '@formatjs/ecma402-abstract': 2.2.4 - '@formatjs/icu-skeleton-parser': 1.8.8 + '@formatjs/ecma402-abstract': 2.2.5 + '@formatjs/icu-skeleton-parser': 1.8.9 tslib: 2.8.1 - '@formatjs/icu-skeleton-parser@1.8.8': + '@formatjs/icu-skeleton-parser@1.8.9': dependencies: - '@formatjs/ecma402-abstract': 2.2.4 + '@formatjs/ecma402-abstract': 2.2.5 tslib: 2.8.1 '@formatjs/intl-localematcher@0.5.8': @@ -4847,11 +4858,7 @@ snapshots: transitivePeerDependencies: - supports-color - agent-base@7.1.2: - dependencies: - debug: 4.4.0 - transitivePeerDependencies: - - supports-color + agent-base@7.1.3: {} agentkeepalive@4.5.0: dependencies: @@ -4927,7 +4934,7 @@ snapshots: app-builder-bin@5.0.0-alpha.10: {} - app-builder-lib@25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)): + app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8): dependencies: '@develar/schema-utils': 2.6.5 '@electron/notarize': 2.5.0 @@ -5119,7 +5126,7 @@ snapshots: dependencies: tweetnacl: 0.14.5 - better-sqlite3@11.6.0: + better-sqlite3@11.7.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.2 @@ -5431,7 +5438,7 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 - conf@13.0.1: + conf@13.1.0: dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) @@ -5568,7 +5575,7 @@ snapshots: dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) builder-util: 25.1.7 builder-util-runtime: 9.2.10 fs-extra: 10.1.0 @@ -5646,7 +5653,7 @@ snapshots: electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) archiver: 5.3.2 builder-util: 25.1.7 fs-extra: 10.1.0 @@ -5655,9 +5662,9 @@ snapshots: - dmg-builder - supports-color - electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)): + electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) builder-util: 25.1.7 builder-util-runtime: 9.2.10 chalk: 4.1.2 @@ -5718,7 +5725,7 @@ snapshots: electron-store@10.0.0: dependencies: - conf: 13.0.1 + conf: 13.1.0 type-fest: 4.30.0 electron-to-chromium@1.5.71: {} @@ -5878,7 +5885,7 @@ snapshots: esutils@2.0.3: {} - execa@9.5.1: + execa@9.5.2: dependencies: '@sindresorhus/merge-streams': 4.0.0 cross-spawn: 7.0.6 @@ -5933,7 +5940,8 @@ snapshots: extsprintf@1.3.0: {} - extsprintf@1.4.1: {} + extsprintf@1.4.1: + optional: true fancy-log@1.3.3: dependencies: @@ -6404,7 +6412,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: - agent-base: 7.1.2 + agent-base: 7.1.3 debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -6429,7 +6437,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: - agent-base: 7.1.2 + agent-base: 7.1.3 debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -6444,7 +6452,7 @@ snapshots: dependencies: '@babel/runtime': 7.26.0 - i18next-browser-languagedetector@8.0.0: + i18next-browser-languagedetector@8.0.1: dependencies: '@babel/runtime': 7.26.0 @@ -6455,9 +6463,9 @@ snapshots: i18next-fs-backend@2.6.0: {} - i18next-icu@2.3.0(intl-messageformat@10.7.7): + i18next-icu@2.3.0(intl-messageformat@10.7.8): dependencies: - intl-messageformat: 10.7.7 + intl-messageformat: 10.7.8 i18next@19.0.2: dependencies: @@ -6487,6 +6495,10 @@ snapshots: ignore@5.3.2: {} + image-size@1.1.1: + dependencies: + queue: 6.0.2 + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -6513,11 +6525,11 @@ snapshots: interpret@3.1.1: {} - intl-messageformat@10.7.7: + intl-messageformat@10.7.8: dependencies: - '@formatjs/ecma402-abstract': 2.2.4 - '@formatjs/fast-memoize': 2.2.3 - '@formatjs/icu-messageformat-parser': 2.9.4 + '@formatjs/ecma402-abstract': 2.2.5 + '@formatjs/fast-memoize': 2.2.4 + '@formatjs/icu-messageformat-parser': 2.9.5 tslib: 2.8.1 ip-address@9.0.5: @@ -7335,6 +7347,10 @@ snapshots: queue-tick@1.0.1: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + quick-lru@5.1.1: {} rc@1.2.8: @@ -8104,7 +8120,7 @@ snapshots: dependencies: assert-plus: 1.0.0 core-util-is: 1.0.2 - extsprintf: 1.4.1 + extsprintf: 1.3.0 verror@1.10.1: dependencies: diff --git a/src/electron/backend/encoding.ts b/src/electron/backend/encoding.ts new file mode 100644 index 0000000..fde5d82 --- /dev/null +++ b/src/electron/backend/encoding.ts @@ -0,0 +1,118 @@ +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 { getScreenshotsDir } from "../utils/backend.js"; + +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 CONCURRENCY = 1; // Number of concurrent encoding tasks + +// Detect and insert encoding tasks +export function checkFramesForEncoding(db: Database) { + const stmt = db.prepare(` + SELECT id, imgFilename, createdAt + FROM frame + WHERE encodeStatus = 0 + ORDER BY createdAt ASC; + `); + const frames = stmt.all() as Frame[]; + + const buffer: Frame[] = []; + + if (frames.length < MIN_FRAMES_TO_ENCODE) return; + + for (let i = 1; i < frames.length; i++) { + const frame = frames[i]; + const lastFrame = frames[i - 1]; + const currentFrameSize = sizeOf(join(getScreenshotsDir(), frame.imgFilename)); + const lastFrameSize = sizeOf(join(getScreenshotsDir(), lastFrame.imgFilename)); + const twoFramesHaveSameSize = + currentFrameSize.width === lastFrameSize.width + && currentFrameSize.height === lastFrameSize.height; + const bufferIsBigEnough = buffer.length >= MIN_FRAMES_TO_ENCODE; + const chunkConditionSatisfied = !twoFramesHaveSameSize || bufferIsBigEnough; + buffer.push(lastFrame); + if (chunkConditionSatisfied) { + // Create new encoding task + const taskStmt = db.prepare(` + 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 (?, ?); + `); + for (const frame of buffer) { + insertStmt.run(taskId, frame.id); + db.prepare(` + UPDATE frame SET encodeStatus = 1 WHERE id = ?; + `).run(frame.id); + } + console.log(`Created encoding task ${taskId} with ${buffer.length} frames`); + buffer.length = 0; + } + } +} + +// TODO: Fix this function +// Check and process encoding task +function processEncodingTasks(db: Database) { + const stmt = db.prepare(` + SELECT id, status + FROM encoding_task + WHERE status = 0 + LIMIT ? + `); + + const tasks = stmt.all(CONCURRENCY) as EncodingTask[]; + + for (const task of tasks) { + const taskId = task.id; + + // Update task status as processing (1) + const updateStmt = db.prepare(` + UPDATE encoding_task SET status = 1 WHERE id = ? + `); + updateStmt.run(taskId); + + const framesStmt = db.prepare(` + SELECT frame.imgFilename + FROM encoding_task_data + JOIN frame ON encoding_task_data.frame = frame.id + WHERE encoding_task_data.encodingTaskID = ? + ORDER BY frame.createAt 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'); + fs.writeFileSync(metaFilePath, metaContent); + + const videoName = `video_${taskId}.mp4`; + const ffmpegCommand = `ffmpeg -f concat -safe 0 -i ${metaFilePath} -c:v libx264 -r 30 ${videoName}`; + 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); + } else { + console.log(`Video ${videoName} created successfully`); + // Update task status to complete (2) + const completeStmt = db.prepare(` + UPDATE encoding_task SET status = 2 WHERE id = ? + `); + completeStmt.run(taskId); + } + + fs.unlinkSync(metaFilePath); + }); + } +} \ No newline at end of file diff --git a/src/electron/backend/init.ts b/src/electron/backend/init.ts index 38ba13e..601eabd 100644 --- a/src/electron/backend/init.ts +++ b/src/electron/backend/init.ts @@ -4,7 +4,6 @@ import DB from "better-sqlite3"; import { __dirname } from "../dirname.js"; import { getDatabaseDir } from "../utils/backend.js"; import { migrate } from "./migrate/index.js"; -import { initSchemaInV2 } from "./migrate/migrateToV2"; function getLibSimpleExtensionPath() { switch (process.platform) { @@ -28,14 +27,14 @@ function init(db: Database) { db.exec(` CREATE TABLE IF NOT EXISTS frame ( id INTEGER PRIMARY KEY AUTOINCREMENT, - createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + createdAt REAL, imgFilename TEXT, segmentID INTEGER NULL, videoPath TEXT NULL, videoFrameIndex INTEGER NULL, collectionID INTEGER NULL, - encoded BOOLEAN DEFAULT 0, - FOREIGN KEY (segmentID) REFERENCES segements (id) + encodeStatus INTEGER DEFAULT 0, + FOREIGN KEY (segmentID) REFERENCES segments (id) ); `); @@ -50,10 +49,10 @@ function init(db: Database) { `); db.exec(` - CREATE TABLE IF NOT EXISTS segements( + CREATE TABLE IF NOT EXISTS segments( id INTEGER PRIMARY KEY AUTOINCREMENT, - startAt TIMESTAMP, - endAt TIMESTAMP, + startedAt REAL, + endedAt REAL, title TEXT, appName TEXT, appPath TEXT, @@ -97,10 +96,49 @@ function init(db: Database) { END; `); - initSchemaInV2(db); + db.exec(` + CREATE TABLE config ( + key TEXT PRIMARY KEY, + value TEXT + ); + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS encoding_task ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + createdAt REAL, + status INT DEFAULT 0 + ); + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS encoding_task_data ( + encodingTaskID INTEGER, + frame ID INTEGER PRIMARY KEY, + FOREIGN KEY (encodingTaskID) REFERENCES encoding_task(id), + FOREIGN KEY (frame) REFERENCES frame(id) + ); + `); + + db.exec(` + CREATE TRIGGER IF NOT EXISTS delete_encoding_task + AFTER UPDATE OF status + ON encoding_task + BEGIN + DELETE FROM encoding_task_data + WHERE encodingTaskID = OLD.id AND NEW.status = 2; + + DELETE FROM encoding_task + WHERE id = OLD.id AND NEW.status = 2; + END; + `); + + db.exec(` + INSERT INTO config (key, value) VALUES ('version', '3'); + `); } -export function initDatabase() { +export async function initDatabase() { const dbPath = getDatabaseDir(); const db = new DB(dbPath, { verbose: console.log }); const libSimpleExtensionPath = getLibSimpleExtensionPath(); @@ -114,5 +152,7 @@ export function initDatabase() { migrate(db); } + db.exec("PRAGMA journal_mode=WAL;"); + return db; } diff --git a/src/electron/backend/migrate/index.ts b/src/electron/backend/migrate/index.ts index dab9024..c33da6c 100644 --- a/src/electron/backend/migrate/index.ts +++ b/src/electron/backend/migrate/index.ts @@ -1,5 +1,16 @@ import { Database } from "better-sqlite3"; import { migrateToV2 } from "./migrateToV2.js"; +import { migrateToV3 } from "./migrateToV3.js"; + +const CURRENT_VERSION = 3; + +function migrateTo(version: number, db: Database) { + switch (version) { + case 2: + migrateToV3(db); + break; + } +} export function migrate(db: Database) { const configTableExists = @@ -8,4 +19,19 @@ export function migrate(db: Database) { if (!configTableExists) { migrateToV2(db); } + let databaseVersion = parseInt( + ( + db.prepare(`SELECT value FROM config WHERE key = 'version';`).get() as + { value: any } + ).value + ); + while (databaseVersion < CURRENT_VERSION) { + migrateTo(databaseVersion, db); + databaseVersion = parseInt( + ( + db.prepare(`SELECT value FROM config WHERE key = 'version';`).get() as + { value: any } + ).value + ); + } } \ No newline at end of file diff --git a/src/electron/backend/migrate/migrateToV2.ts b/src/electron/backend/migrate/migrateToV2.ts index e9157c1..629a612 100644 --- a/src/electron/backend/migrate/migrateToV2.ts +++ b/src/electron/backend/migrate/migrateToV2.ts @@ -11,7 +11,7 @@ interface OldFrame { encoded: number; } -export function initSchemaInV2(db: Database) { +function initSchemaInV2(db: Database) { db.exec(` CREATE TABLE config ( key TEXT PRIMARY KEY, diff --git a/src/electron/backend/migrate/migrateToV3.ts b/src/electron/backend/migrate/migrateToV3.ts new file mode 100644 index 0000000..fb74864 --- /dev/null +++ b/src/electron/backend/migrate/migrateToV3.ts @@ -0,0 +1,191 @@ +import { Database } from "better-sqlite3"; + +function convertTimestampToUnix(timestamp: string): number { + const date = new Date(timestamp); + const now = new Date(); + const offsetInMinutes = now.getTimezoneOffset(); + const offsetInSeconds = offsetInMinutes * 60; + return date.getTime() / 1000 - offsetInSeconds; +} + +function transformEncodingTask(db: Database) { + const createTableSql = ` + CREATE TABLE IF NOT EXISTS encoding_task_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status INT DEFAULT 0 + ); + + INSERT INTO encoding_task_new (id, createdAt, status) + SELECT id, createdAt, status FROM encoding_task; + DROP TABLE encoding_task; + ALTER TABLE encoding_task_new RENAME TO encoding_task; + ALTER TABLE encoding_task ADD COLUMN createdAt_new REAL; + `; + db.exec(createTableSql); + + const rows = db.prepare(`SELECT id, createdAt FROM encoding_task`).all() as { [x: string]: unknown; id: unknown; }[]; + const updateStmt = db.prepare(`UPDATE encoding_task SET createdAt_new = ? WHERE id = ?`); + rows.forEach((row) => { + const unixTimestamp = convertTimestampToUnix(row.createdAt as string); + updateStmt.run(unixTimestamp, row.id); + }); + + db.exec(` + ALTER TABLE encoding_task DROP COLUMN createdAt; + ALTER TABLE encoding_task RENAME COLUMN createdAt_new TO createdAt; + `); +} + +function transformFrame(db: Database) { + const createTableSql = ` + CREATE TABLE frame_new( + id INTEGER PRIMARY KEY AUTOINCREMENT, + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + imgFilename TEXT, + segmentID INTEGER NULL, + videoPath TEXT NULL, + videoFrameIndex INTEGER NULL, + collectionID INTEGER NULL, + encodeStatus INT DEFAULT 0, + FOREIGN KEY (segmentID) REFERENCES segments (id) + ); + INSERT INTO frame_new (id, createdAt, imgFilename, segmentID, videoPath, videoFrameIndex, collectionID, encodeStatus) + SELECT id, createdAt, imgFilename, segmentID, videoPath, videoFrameIndex, collectionID, encodeStatus FROM frame; + DROP TABLE frame; + ALTER TABLE frame_new RENAME TO frame; + ALTER TABLE frame ADD COLUMN createdAt_new REAL; + ` + db.exec(createTableSql); + + const rows = db.prepare(`SELECT id, createdAt FROM frame`).all() as { [x: string]: unknown; id: unknown; }[]; + const updateStmt = db.prepare(`UPDATE frame SET createdAt_new = ? WHERE id = ?`); + rows.forEach((row) => { + const unixTimestamp = convertTimestampToUnix(row.createdAt as string); + updateStmt.run(unixTimestamp, row.id); + }); + + db.exec(` + ALTER TABLE frame DROP COLUMN createdAt; + ALTER TABLE frame RENAME COLUMN createdAt_new TO createdAt; + `); +} + +function transformSegments(db: Database) { + db.exec(` + CREATE TABLE IF NOT EXISTS segments_new( + id INTEGER PRIMARY KEY AUTOINCREMENT, + startedAt REAL, + endedAt REAL, + title TEXT, + appName TEXT, + appPath TEXT, + text TEXT, + type TEXT, + appBundleID TEXT NULL, + url TEXT NULL + ); + INSERT INTO segments_new (id, startedAt, endedAt, title, appName, appPath, text, type, appBundleID, url) + SELECT id, startedAt, endedAt, title, appName, appPath, text, type, appBundleID, url FROM segments; + DROP TABLE segments; + ALTER TABLE segments_new RENAME TO segments; + ALTER TABLE segments ADD COLUMN startedAt_new REAL; + ALTER TABLE segments ADD COLUMN endedAt_new REAL; + `); + const rows = db.prepare(`SELECT id, startedAt, endedAt FROM segments`).all() as { [x: string]: unknown; id: unknown; }[]; + const updateStart = db.prepare(`UPDATE segments SET startedAt_new = ? WHERE id = ?`); + const updateEnd = db.prepare(`UPDATE segments SET endedAt_new = ? WHERE id = ?`); + rows.forEach((row) => { + updateStart.run(convertTimestampToUnix(row.startedAt as string), row.id); + updateEnd.run(convertTimestampToUnix(row.endedAt as string), row.id); + }); + + db.exec(` + ALTER TABLE segments DROP COLUMN startedAt; + ALTER TABLE segments DROP COLUMN endedAt; + ALTER TABLE segments RENAME COLUMN startedAt_new TO startedAt; + ALTER TABLE segments RENAME COLUMN endedAt_new TO endedAt; + `); +} + +function renameColumn(tableName: string, oldColumnName: string, newColumnName: string, db: Database) { + if (db.prepare(`SELECT 1 FROM pragma_table_info(?) WHERE name=?`).get([tableName, oldColumnName])) { + db.prepare(`ALTER TABLE ? RENAME COLUMN ? TO ?`).run([tableName, oldColumnName, newColumnName]); + } +} + +export function migrateToV3(db: Database) { + db.prepare(`ALTER TABLE segements RENAME TO segments`).run(); + db.exec(` + PRAGMA foreign_keys = OFF; + CREATE TABLE frame_new( + id INTEGER PRIMARY KEY AUTOINCREMENT, + createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + imgFilename TEXT, + segmentID INTEGER NULL, + videoPath TEXT NULL, + videoFrameIndex INTEGER NULL, + collectionID INTEGER NULL, + encodeStatus INT DEFAULT 0, + FOREIGN KEY (segmentID) REFERENCES segments (id) + ); + INSERT INTO frame_new (id, createAt, imgFilename, segmentID, videoPath, videoFrameIndex, collectionID, encodeStatus) + SELECT id, createAt, imgFilename, segmentID, videoPath, videoFrameIndex, collectionID, encodeStatus FROM frame; + DROP TABLE frame; + ALTER TABLE frame_new RENAME TO frame; + + CREATE TABLE encoding_task_data_new ( + encodingTaskID INTEGER, + frame ID INTEGER PRIMARY KEY, + FOREIGN KEY (encodingTaskID) REFERENCES encoding_task(id), + FOREIGN KEY (frame) REFERENCES frame(id) + ); + + INSERT INTO encoding_task_data SELECT * FROM encoding_task_data_new; + DROP TRIGGER delete_encoding_task; + DROP TABLE encoding_task_data; + + ALTER TABLE encoding_task_data_new RENAME TO encoding_task_data; + + CREATE TRIGGER IF NOT EXISTS delete_encoding_task + AFTER UPDATE OF status + ON encoding_task + BEGIN + DELETE FROM encoding_task_data + WHERE encodingTaskID = OLD.id AND NEW.status = 2; + + DELETE FROM encoding_task + WHERE id = OLD.id AND NEW.status = 2; + END; + + CREATE TABLE recognition_data_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + frameID INTEGER, + data TEXT, + text TEXT, + FOREIGN KEY (frameID) REFERENCES frame (id) + ); + + INSERT INTO recognition_data SELECT * FROM recognition_data_new; + DROP TABLE recognition_data; + ALTER TABLE recognition_data_new RENAME TO recognition_data; + + PRAGMA foreign_keys = ON; + `); + + renameColumn('encoding_task', 'createAt', 'createdAt', db); + renameColumn('frame', 'createAt', 'createdAt', db); + renameColumn('segments', 'startAt', 'startedAt', db); + renameColumn('segments', 'endAt', 'endedAt', db); + if (db.prepare(`SELECT 1 FROM pragma_table_info('frame') WHERE name='encoded'`).get()) { + db.prepare(`ALTER TABLE frame DROP COLUMN encoded`).run(); + } + + transformSegments(db); + transformFrame(db); + transformEncodingTask(db); + + db.exec(` + UPDATE config SET value = '3' WHERE key = 'version'; + `); +} \ No newline at end of file diff --git a/src/electron/backend/schema.d.ts b/src/electron/backend/schema.d.ts new file mode 100644 index 0000000..7f2371e --- /dev/null +++ b/src/electron/backend/schema.d.ts @@ -0,0 +1,22 @@ +export interface Frame { + id: number; + createdAt: number; + imgFilename: string; + segmentID: number | null; + videoPath: string | null; + videoFrameIndex: number | null; + collectionID: number | null; + encodeStatus: number; +} + + +export interface EncodingTask { + id: number; + createdAt: number; + status: number; +} + +export interface EncodingTaskData { + encodingTaskID: number; + frame: number; +} \ No newline at end of file diff --git a/src/electron/backend/screenshot.ts b/src/electron/backend/screenshot.ts index b7127d6..4af640b 100644 --- a/src/electron/backend/screenshot.ts +++ b/src/electron/backend/screenshot.ts @@ -12,8 +12,8 @@ export function startScreenshotLoop(db: Database) { const screenshotPath = join(screenshotDir, filename); screenshot({filename: screenshotPath, format: "png"}).then((absolutePath) => { const SQL = SqlString.format( - "INSERT INTO frame (imgFilename) VALUES (?)", - [filename] + "INSERT INTO frame (imgFilename, createdAt) VALUES (?, ?)", + [filename, new Date().getTime() / 1000] ); db.exec(SQL); }).catch((err) => { diff --git a/src/electron/index.ts b/src/electron/index.ts index 6a1840e..a733bf6 100644 --- a/src/electron/index.ts +++ b/src/electron/index.ts @@ -8,6 +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"; const i18n = initI18n(); @@ -73,8 +74,11 @@ app.on("activate", () => {}); app.on("ready", () => { createTray(); - dbConnection = initDatabase(); - screenshotInterval = startScreenshotLoop(dbConnection); + initDatabase().then((db) => { + screenshotInterval = startScreenshotLoop(db); + setInterval(checkFramesForEncoding, 10000, db); + dbConnection = db; + }); mainWindow = createMainWindow(port, () => (mainWindow = null)); settingsWindow = createSettingsWindow(port, () => (settingsWindow = null)); globalShortcut.register("Escape", () => { diff --git a/src/electron/utils/backend.ts b/src/electron/utils/backend.ts index 8033132..feb531a 100644 --- a/src/electron/utils/backend.ts +++ b/src/electron/utils/backend.ts @@ -48,4 +48,13 @@ export function getScreenshotsDir() { export function getRecordingsDir() { const dataDir = createDataDir(); return path.join(dataDir, "recordings"); -} \ No newline at end of file +} + +export function getEncodingTempDir() { + const tempDir = createTempDir(); + const encodingTempDir = path.join(tempDir, "encoding"); + if (!fs.existsSync(encodingTempDir)) { + fs.mkdirSync(encodingTempDir, { recursive: true }); + } + return encodingTempDir; +}