kopia lustrzana https://github.com/mifi/lossless-cut
upgrade to electron-vite
rodzic
8a7c1f8a17
commit
f677619039
|
@ -1,3 +1,3 @@
|
||||||
/dist
|
/dist
|
||||||
/vite-dist
|
/out
|
||||||
/ts-dist
|
/ts-dist
|
|
@ -12,7 +12,7 @@ module.exports = {
|
||||||
|
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['./src/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
|
files: ['./src/renderer/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
|
||||||
env: {
|
env: {
|
||||||
node: false,
|
node: false,
|
||||||
browser: true,
|
browser: true,
|
||||||
|
@ -22,7 +22,13 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['./script/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}', 'vite.config.js'],
|
files: ['./src/preload/**/*.{js,cjs,jsx,ts,tsx}'],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['./script/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}', 'electron.vite.config.js'],
|
||||||
rules: {
|
rules: {
|
||||||
'import/no-extraneous-dependencies': ['error', {
|
'import/no-extraneous-dependencies': ['error', {
|
||||||
devDependencies: true,
|
devDependencies: true,
|
||||||
|
|
|
@ -98,7 +98,7 @@ jobs:
|
||||||
- name: (MacOS) Upload to Mac App Store
|
- name: (MacOS) Upload to Mac App Store
|
||||||
if: startsWith(matrix.os, 'macos') && env.is_tag == 'true'
|
if: startsWith(matrix.os, 'macos') && env.is_tag == 'true'
|
||||||
run: |
|
run: |
|
||||||
node script/xcrun-wrapper.mjs dist/mas-universal/LosslessCut-mac-universal.pkg ${{ secrets.api_key_id }} ${{ secrets.api_key_issuer_id }} 1505323402 no.mifi.losslesscut-mac
|
npx tsx script/xcrun-wrapper.mts dist/mas-universal/LosslessCut-mac-universal.pkg ${{ secrets.api_key_id }} ${{ secrets.api_key_issuer_id }} 1505323402 no.mifi.losslesscut-mac
|
||||||
|
|
||||||
- name: (MacOS) Upload artifacts
|
- name: (MacOS) Upload artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|
|
@ -11,7 +11,7 @@ node_modules
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
|
||||||
/dist
|
/dist
|
||||||
/vite-dist
|
/out
|
||||||
/icon-build
|
/icon-build
|
||||||
/build-resources
|
/build-resources
|
||||||
/doc
|
/doc
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"/public/locales/**": true,
|
"/src/main/locales/**": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<br>
|
<br>
|
||||||
<p><a href="https://mifi.no/losslesscut/"><img src="src/icon.svg" width="120" alt="LosslessCut" /></a></p>
|
<p><a href="https://mifi.no/losslesscut/"><img src="src/renderer/src/icon.svg" width="120" alt="LosslessCut" /></a></p>
|
||||||
<p><b>LosslessCut</b></p>
|
<p><b>LosslessCut</b></p>
|
||||||
The swiss army knife of lossless video/audio editing
|
The swiss army knife of lossless video/audio editing
|
||||||
<br>
|
<br>
|
||||||
|
|
2
cli.md
2
cli.md
|
@ -26,7 +26,7 @@ LosslessCut file1.mp4 file2.mkv
|
||||||
```
|
```
|
||||||
|
|
||||||
## Override settings (experimental)
|
## Override settings (experimental)
|
||||||
See [available settings](https://github.com/mifi/lossless-cut/blob/master/public/configStore.js). Note that this is subject to change in newer versions. ⚠️ If you specify incorrect values it could corrupt your configuration file. You may use JSON or JSON5. Example:
|
See [available settings](https://github.com/mifi/lossless-cut/blob/master/src/main/configStore.js). Note that this is subject to change in newer versions. ⚠️ If you specify incorrect values it could corrupt your configuration file. You may use JSON or JSON5. Example:
|
||||||
```bash
|
```bash
|
||||||
LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}'
|
LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}'
|
||||||
```
|
```
|
||||||
|
|
|
@ -5,10 +5,6 @@
|
||||||
This app is built using Electron.
|
This app is built using Electron.
|
||||||
Make sure you have at least Node v16. The app uses ffmpeg from PATH when developing.
|
Make sure you have at least Node v16. The app uses ffmpeg from PATH when developing.
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/mifi/lossless-cut.git
|
git clone https://github.com/mifi/lossless-cut.git
|
||||||
cd lossless-cut
|
cd lossless-cut
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {
|
||||||
|
// https://electron-vite.org/guide/dev#dependencies-vs-devdependencies
|
||||||
|
// For the main process and preload, the best practice is to externalize dependencies and only bundle our own code.
|
||||||
|
// However, until we use ESM for electron main, we need to include ESM-only deps in the bundle: (exclude from externalize)
|
||||||
|
plugins: [externalizeDepsPlugin({ exclude: ['p-map', 'execa', 'nanoid'] })],
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
// https://electron-vite.org/guide/dev#dependencies-vs-devdependencies
|
||||||
|
plugins: [externalizeDepsPlugin({ exclude: [] })],
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 3e6,
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
https: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,8 +1,8 @@
|
||||||
// eslint-disable-line unicorn/filename-case
|
// eslint-disable-line unicorn/filename-case
|
||||||
export default {
|
export default {
|
||||||
input: ['src/**/*.{js,jsx,ts,tsx}', 'public/*.{js,ts}'],
|
input: ['src/renderer/**/*.{js,jsx,ts,tsx}', 'src/main/*.{js,ts}'],
|
||||||
|
|
||||||
output: 'public/locales/$LOCALE/$NAMESPACE.json',
|
output: 'src/main/locales/$LOCALE/$NAMESPACE.json',
|
||||||
indentation: 4,
|
indentation: 4,
|
||||||
|
|
||||||
sort: true,
|
sort: true,
|
||||||
|
@ -16,7 +16,7 @@ export default {
|
||||||
|
|
||||||
defaultValue: (lng, ns, key) => key,
|
defaultValue: (lng, ns, key) => key,
|
||||||
|
|
||||||
// Keep in sync between i18next-parser.config.js and i18n-common.js:
|
// Keep in sync between i18next-parser.config.js and i18nCommon.js:
|
||||||
keySeparator: false,
|
keySeparator: false,
|
||||||
namespaceSeparator: false,
|
namespaceSeparator: false,
|
||||||
};
|
};
|
||||||
|
|
66
package.json
66
package.json
|
@ -4,31 +4,27 @@
|
||||||
"description": "The swiss army knife of lossless video/audio editing",
|
"description": "The swiss army knife of lossless video/audio editing",
|
||||||
"copyright": "Copyright © 2021 ${author}",
|
"copyright": "Copyright © 2021 ${author}",
|
||||||
"version": "3.60.0",
|
"version": "3.60.0",
|
||||||
"main": "public/electron.js",
|
"main": "./out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -k \"npm run dev:frontend\" \"npm run dev:electron\"",
|
"clean": "rimraf dist out ts-dist build-resources icon-build",
|
||||||
"dev:frontend": "cross-env vite --port 3001",
|
"start": "electron-vite preview",
|
||||||
"dev:electron": "wait-on tcp:3001 && electron .",
|
"dev": "electron-vite dev -w",
|
||||||
"icon-gen": "mkdirp icon-build build-resources/appx && node script/icon-gen.mjs",
|
"icon-gen": "mkdirp icon-build build-resources/appx && tsx script/icon-gen.mts",
|
||||||
"download-ffmpeg-darwin-x64": "mkdirp ffmpeg/darwin-x64 && cd ffmpeg/darwin-x64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-X64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-X64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
|
"download-ffmpeg-darwin-x64": "mkdirp ffmpeg/darwin-x64 && cd ffmpeg/darwin-x64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-X64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-X64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
|
||||||
"download-ffmpeg-darwin-arm64": "mkdirp ffmpeg/darwin-arm64 && cd ffmpeg/darwin-arm64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-ARM64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-ARM64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
|
"download-ffmpeg-darwin-arm64": "mkdirp ffmpeg/darwin-arm64 && cd ffmpeg/darwin-arm64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-ARM64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-ARM64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
|
||||||
"download-ffmpeg-linux-x64": "mkdirp ffmpeg/linux-x64 && cd ffmpeg/linux-x64 && wget https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0.tar.xz -O ffmpeg-ffprobe.xz && tar -xv -f ffmpeg-ffprobe.xz && mv ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0 extracted && mkdirp lib && mv extracted/bin/ffmpeg extracted/bin/ffprobe extracted/lib/lib*.so* lib",
|
"download-ffmpeg-linux-x64": "mkdirp ffmpeg/linux-x64 && cd ffmpeg/linux-x64 && wget https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0.tar.xz -O ffmpeg-ffprobe.xz && tar -xv -f ffmpeg-ffprobe.xz && mv ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0 extracted && mkdirp lib && mv extracted/bin/ffmpeg extracted/bin/ffprobe extracted/lib/lib*.so* lib",
|
||||||
"download-ffmpeg-win32-x64": "mkdirp ffmpeg/win32-x64 && cd ffmpeg/win32-x64 && npx download-cli https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0.zip --out . --filename ffmpeg-ffprobe.zip && 7z x ffmpeg-ffprobe.zip && mkdirp lib && cd ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0/bin && npx shx mv ffmpeg.exe ffprobe.exe *.dll ../../lib",
|
"download-ffmpeg-win32-x64": "mkdirp ffmpeg/win32-x64 && cd ffmpeg/win32-x64 && npx download-cli https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0.zip --out . --filename ffmpeg-ffprobe.zip && 7z x ffmpeg-ffprobe.zip && mkdirp lib && cd ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0/bin && npx shx mv ffmpeg.exe ffprobe.exe *.dll ../../lib",
|
||||||
"build": "yarn icon-gen && vite build --outDir vite-dist",
|
"build": "yarn icon-gen && electron-vite build",
|
||||||
"tsc": "tsc --build",
|
"tsc": "tsc --build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs .",
|
"lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs,.mts .",
|
||||||
"pack-mac": "electron-builder --mac -m dmg",
|
"pack-mac": "yarn build && electron-builder --mac -m dmg",
|
||||||
"prepack-mac": "yarn build",
|
"pack-mas-dev": "yarn build && electron-builder --mac -m mas-dev -c.mas.provisioningProfile=LosslessCut_Dev.provisionprofile -c.mas.identity='Apple Development: Mikael Finstad (JH4PH8B3C8)'",
|
||||||
"pack-mas-dev": "electron-builder --mac -m mas-dev -c.mas.provisioningProfile=LosslessCut_Dev.provisionprofile -c.mas.identity='Apple Development: Mikael Finstad (JH4PH8B3C8)'",
|
"pack-win": "yarn build && electron-builder --win",
|
||||||
"prepack-mas-dev": "yarn build",
|
|
||||||
"pack-win": "electron-builder --win",
|
|
||||||
"prepack-win": "yarn build",
|
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"version": "node script/postversion.mjs && git add no.mifi.losslesscut.appdata.xml",
|
"version": "tsx script/postversion.mts && git add no.mifi.losslesscut.appdata.xml",
|
||||||
"pack-linux": "electron-builder --linux",
|
"pack-linux": "yarn build && electron-builder --linux",
|
||||||
"prepack-linux": "yarn build",
|
|
||||||
"scan-i18n": "i18next --config i18next-parser.config.mjs",
|
"scan-i18n": "i18next --config i18next-parser.config.mjs",
|
||||||
"generate-licenses": "yarn licenses generate-disclaimer > licenses.txt && echo '\n\nffmpeg is licensed under GPL v2+:\n\nhttp://www.gnu.org/licenses/old-licenses/gpl-2.0.html' >> licenses.txt"
|
"generate-licenses": "yarn licenses generate-disclaimer > licenses.txt && echo '\n\nffmpeg is licensed under GPL v2+:\n\nhttp://www.gnu.org/licenses/old-licenses/gpl-2.0.html' >> licenses.txt"
|
||||||
},
|
},
|
||||||
|
@ -44,29 +40,36 @@
|
||||||
"license": "GPL-2.0-only",
|
"license": "GPL-2.0-only",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/open-sans": "^4.5.14",
|
"@fontsource/open-sans": "^4.5.14",
|
||||||
|
"@radix-ui/colors": "^0.1.8",
|
||||||
"@radix-ui/react-switch": "^1.0.1",
|
"@radix-ui/react-switch": "^1.0.1",
|
||||||
|
"@tsconfig/node18": "^18.2.2",
|
||||||
"@tsconfig/strictest": "^2.0.2",
|
"@tsconfig/strictest": "^2.0.2",
|
||||||
"@tsconfig/vite-react": "^3.0.0",
|
"@tsconfig/vite-react": "^3.0.0",
|
||||||
"@types/color": "^3.0.6",
|
"@types/color": "^3.0.6",
|
||||||
"@types/css-modules": "^1.0.5",
|
"@types/css-modules": "^1.0.5",
|
||||||
"@types/eslint": "^8",
|
"@types/eslint": "^8",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
"@types/lodash": "^4.14.202",
|
"@types/lodash": "^4.14.202",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/node": "18",
|
"@types/node": "18",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@types/sortablejs": "^1.15.0",
|
"@types/sortablejs": "^1.15.0",
|
||||||
|
"@types/yargs-parser": "^21.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
"@typescript-eslint/parser": "^6.12.0",
|
"@typescript-eslint/parser": "^6.12.0",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"color": "^3.1.0",
|
"color": "^3.1.0",
|
||||||
"concurrently": "^6.0.0",
|
"concurrently": "^6.0.0",
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"csv-parse": "^4.15.3",
|
"csv-parse": "^4.15.3",
|
||||||
"csv-stringify": "^5.6.2",
|
"csv-stringify": "^5.6.2",
|
||||||
|
"data-uri-to-buffer": "^4.0.0",
|
||||||
"electron": "^27.0.0",
|
"electron": "^27.0.0",
|
||||||
"electron-builder": "^24.6.4",
|
"electron-builder": "^24.6.4",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
|
"electron-vite": "^2.1.0",
|
||||||
"eslint": "^8.2.0",
|
"eslint": "^8.2.0",
|
||||||
"eslint-config-mifi": "^0.0.3",
|
"eslint-config-mifi": "^0.0.3",
|
||||||
"eslint-plugin-import": "^2.25.3",
|
"eslint-plugin-import": "^2.25.3",
|
||||||
|
@ -78,7 +81,7 @@
|
||||||
"fast-xml-parser": "^4.2.5",
|
"fast-xml-parser": "^4.2.5",
|
||||||
"framer-motion": "^9.0.3",
|
"framer-motion": "^9.0.3",
|
||||||
"i18next-parser": "^7.6.0",
|
"i18next-parser": "^7.6.0",
|
||||||
"icon-gen": "^3.0.0",
|
"icon-gen": "^4.0.0",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"ky": "^0.33.1",
|
"ky": "^0.33.1",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
|
@ -97,6 +100,7 @@
|
||||||
"react-sortablejs": "^6.1.4",
|
"react-sortablejs": "^6.1.4",
|
||||||
"react-syntax-highlighter": "^15.4.3",
|
"react-syntax-highlighter": "^15.4.3",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
"scroll-into-view-if-needed": "^2.2.28",
|
"scroll-into-view-if-needed": "^2.2.28",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
|
@ -105,28 +109,25 @@
|
||||||
"sweetalert2": "^11.0.0",
|
"sweetalert2": "^11.0.0",
|
||||||
"sweetalert2-react-content": "^5.0.7",
|
"sweetalert2-react-content": "^5.0.7",
|
||||||
"tiny-invariant": "^1.3.3",
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"tsx": "^4.7.1",
|
||||||
"typescript": "~5.2.0",
|
"typescript": "~5.2.0",
|
||||||
"use-debounce": "^5.1.0",
|
"use-debounce": "^5.1.0",
|
||||||
"use-trace-update": "^1.3.0",
|
"use-trace-update": "^1.3.0",
|
||||||
"vite": "^4.5.2",
|
"vite": "^4.5.2",
|
||||||
"vitest": "^1.2.2",
|
"vitest": "^1.2.2"
|
||||||
"wait-on": "^7.0.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/remote": "^2.0.10",
|
"@electron/remote": "^2.0.10",
|
||||||
"@radix-ui/colors": "^0.1.8",
|
"@octokit/core": "5",
|
||||||
"cue-parser": "^0.3.0",
|
"cue-parser": "^0.3.0",
|
||||||
"data-uri-to-buffer": "^4.0.0",
|
|
||||||
"electron-is-dev": "^2.0.0",
|
|
||||||
"electron-store": "5.1.1",
|
"electron-store": "5.1.1",
|
||||||
"electron-unhandled": "^4.0.1",
|
"electron-unhandled": "^4.0.1",
|
||||||
"execa": "5",
|
"execa": "^8.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"file-type": "16",
|
"file-type": "16",
|
||||||
"file-url": "^3.0.0",
|
"file-url": "^3.0.0",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"github-api": "^3.2.2",
|
|
||||||
"i18next": "^22.4.10",
|
"i18next": "^22.4.10",
|
||||||
"i18next-fs-backend": "^2.1.1",
|
"i18next-fs-backend": "^2.1.1",
|
||||||
"json5": "^2.2.2",
|
"json5": "^2.2.2",
|
||||||
|
@ -134,25 +135,28 @@
|
||||||
"mime-types": "^2.1.14",
|
"mime-types": "^2.1.14",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"semver": "^7.5.2",
|
"semver": "^7.5.2",
|
||||||
"string-to-stream": "^1.1.1",
|
"string-to-stream": "^3.0.1",
|
||||||
"winston": "^3.8.1",
|
"winston": "^3.8.1",
|
||||||
"yargs-parser": "^21.0.0"
|
"yargs-parser": "^21.1.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build-resources"
|
"buildResources": "build-resources"
|
||||||
},
|
},
|
||||||
"extraMetadata": {
|
|
||||||
"main": "vite-dist/electron.js"
|
|
||||||
},
|
|
||||||
"files": [
|
"files": [
|
||||||
"vite-dist/**/*"
|
"out/**/*"
|
||||||
],
|
],
|
||||||
"asar": {
|
"asar": {
|
||||||
"smartUnpack": false
|
"smartUnpack": false
|
||||||
},
|
},
|
||||||
"appId": "no.mifi.losslesscut",
|
"appId": "no.mifi.losslesscut",
|
||||||
"artifactName": "${productName}-${os}-${arch}.${ext}",
|
"artifactName": "${productName}-${os}-${arch}.${ext}",
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "locales",
|
||||||
|
"to": "locales"
|
||||||
|
}
|
||||||
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
"hardenedRuntime": true,
|
"hardenedRuntime": true,
|
||||||
"appId": "no.mifi.losslesscut-mac",
|
"appId": "no.mifi.losslesscut-mac",
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
const homepage = 'https://mifi.no/losslesscut/';
|
|
||||||
const githubLink = 'https://github.com/mifi/lossless-cut/';
|
|
||||||
const getReleaseUrl = (version) => `https://github.com/mifi/lossless-cut/releases/tag/v${version}`;
|
|
||||||
const licensesPage = 'https://losslesscut.mifi.no/licenses.txt';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
homepage,
|
|
||||||
getReleaseUrl,
|
|
||||||
githubLink,
|
|
||||||
licensesPage,
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
const os = require('os');
|
|
||||||
|
|
||||||
const frontendBuildDir = 'vite-dist';
|
|
||||||
|
|
||||||
const platform = os.platform();
|
|
||||||
const arch = os.arch();
|
|
||||||
|
|
||||||
// todo dedupe between renderer and main
|
|
||||||
const isWindows = platform === 'win32';
|
|
||||||
const isMac = platform === 'darwin';
|
|
||||||
const isLinux = platform === 'linux';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
frontendBuildDir,
|
|
||||||
isWindows,
|
|
||||||
isMac,
|
|
||||||
isLinux,
|
|
||||||
platform,
|
|
||||||
arch,
|
|
||||||
};
|
|
|
@ -1,29 +0,0 @@
|
||||||
// eslint-disable-line unicorn/filename-case
|
|
||||||
import sharp from 'sharp';
|
|
||||||
import icongen from 'icon-gen';
|
|
||||||
|
|
||||||
const svg2png = (from, to, width, height) => sharp(from)
|
|
||||||
.png()
|
|
||||||
.resize(width, height, {
|
|
||||||
fit: sharp.fit.contain,
|
|
||||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
||||||
})
|
|
||||||
.toFile(to);
|
|
||||||
|
|
||||||
// Linux:
|
|
||||||
await svg2png('src/icon.svg', './icon-build/app-512.png', 512, 512);
|
|
||||||
|
|
||||||
// Windows Store
|
|
||||||
await svg2png('src/icon.svg', './build-resources/appx/StoreLogo.png', 50, 50);
|
|
||||||
await svg2png('src/icon.svg', './build-resources/appx/Square150x150Logo.png', 300, 300);
|
|
||||||
await svg2png('src/icon.svg', './build-resources/appx/Square44x44Logo.png', 44, 44);
|
|
||||||
await svg2png('src/icon.svg', './build-resources/appx/Wide310x150Logo.png', 620, 300);
|
|
||||||
|
|
||||||
// MacOS:
|
|
||||||
// https://github.com/mifi/lossless-cut/issues/1820
|
|
||||||
await icongen('./src/icon-mac.svg', './icon-build', { icns: { sizes: [512, 1024] } });
|
|
||||||
|
|
||||||
// Windows ICO:
|
|
||||||
// https://github.com/mifi/lossless-cut/issues/778
|
|
||||||
// https://stackoverflow.com/questions/3236115/which-icon-sizes-should-my-windows-applications-icon-include
|
|
||||||
await icongen('./src/icon.svg', './icon-build', { ico: { sizes: [16, 24, 32, 40, 48, 64, 96, 128, 256, 512] } });
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
// eslint-disable-line unicorn/filename-case
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import icongenRaw from 'icon-gen';
|
||||||
|
|
||||||
|
const icongen = icongenRaw as unknown as typeof icongenRaw['default'];
|
||||||
|
|
||||||
|
const svg2png = (from: string, to: string, width: number, height: number) => sharp(from)
|
||||||
|
.png()
|
||||||
|
.resize(width, height, {
|
||||||
|
fit: sharp.fit.contain,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
})
|
||||||
|
.toFile(to);
|
||||||
|
|
||||||
|
const srcIcon = 'src/renderer/src/icon.svg';
|
||||||
|
// Linux:
|
||||||
|
await svg2png(srcIcon, './icon-build/app-512.png', 512, 512);
|
||||||
|
|
||||||
|
// Windows Store
|
||||||
|
await svg2png(srcIcon, './build-resources/appx/StoreLogo.png', 50, 50);
|
||||||
|
await svg2png(srcIcon, './build-resources/appx/Square150x150Logo.png', 300, 300);
|
||||||
|
await svg2png(srcIcon, './build-resources/appx/Square44x44Logo.png', 44, 44);
|
||||||
|
await svg2png(srcIcon, './build-resources/appx/Wide310x150Logo.png', 620, 300);
|
||||||
|
|
||||||
|
// MacOS:
|
||||||
|
// https://github.com/mifi/lossless-cut/issues/1820
|
||||||
|
await icongen('./src/renderer/src/icon-mac.svg', './icon-build', { icns: { sizes: [512, 1024] }, report: false });
|
||||||
|
|
||||||
|
// Windows ICO:
|
||||||
|
// https://github.com/mifi/lossless-cut/issues/778
|
||||||
|
// https://stackoverflow.com/questions/3236115/which-icon-sizes-should-my-windows-applications-icon-include
|
||||||
|
await icongen(srcIcon, './icon-build', { ico: { sizes: [16, 24, 32, 40, 48, 64, 96, 128, 256, 512] }, report: false });
|
|
@ -1,12 +1,11 @@
|
||||||
import { readFile, writeFile } from 'fs/promises';
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
||||||
// eslint-disable-next-line import/no-unresolved
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
const xmlUrl = new URL('../no.mifi.losslesscut.appdata.xml', import.meta.url);
|
const xmlUrl = new URL('../no.mifi.losslesscut.appdata.xml', import.meta.url);
|
||||||
const xmlData = await readFile(xmlUrl);
|
const xmlData = await readFile(xmlUrl);
|
||||||
|
|
||||||
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)));
|
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)) as unknown as string);
|
||||||
|
|
||||||
const parser = new XMLParser({ alwaysCreateTextNode: true, ignoreAttributes: false, ignoreDeclaration: false });
|
const parser = new XMLParser({ alwaysCreateTextNode: true, ignoreAttributes: false, ignoreDeclaration: false });
|
||||||
const xml = parser.parse(xmlData);
|
const xml = parser.parse(xmlData);
|
|
@ -1,5 +1,5 @@
|
||||||
// eslint-disable-line unicorn/filename-case
|
// eslint-disable-line unicorn/filename-case
|
||||||
import execa from 'execa';
|
import { execa } from 'execa';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
|
|
||||||
// we need a wrapper script because altool tends to error out very often
|
// we need a wrapper script because altool tends to error out very often
|
||||||
|
@ -17,7 +17,7 @@ const bundleId = args[4];
|
||||||
// seems to be the same
|
// seems to be the same
|
||||||
const ascPublicId = apiIssuer;
|
const ascPublicId = apiIssuer;
|
||||||
|
|
||||||
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)));
|
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)) as unknown as string);
|
||||||
|
|
||||||
console.log('Using version', packageJson.version);
|
console.log('Using version', packageJson.version);
|
||||||
|
|
||||||
|
@ -74,8 +74,11 @@ async function runAttempt() {
|
||||||
console.log('stdout', stdout);
|
console.log('stdout', stdout);
|
||||||
return false;
|
return false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.exitCode === 1 && err.stdout) {
|
if (err instanceof Error && 'exitCode' in err && err.exitCode === 1 && 'stdout' in err && err.stdout && typeof err.stdout === 'string') {
|
||||||
const errorJson = JSON.parse(err.stdout);
|
const errorJson = JSON.parse(err.stdout) as unknown;
|
||||||
|
if (!(errorJson != null && typeof errorJson === 'object' && 'product-errors' in errorJson && Array.isArray(errorJson['product-errors']))) {
|
||||||
|
throw new TypeError('Invalid JSON');
|
||||||
|
}
|
||||||
const productErrors = errorJson['product-errors'];
|
const productErrors = errorJson['product-errors'];
|
||||||
// Unable to authenticate
|
// Unable to authenticate
|
||||||
if (productErrors.some((error) => error.code === -19209)) {
|
if (productErrors.some((error) => error.code === -19209)) {
|
|
@ -1,3 +0,0 @@
|
||||||
const { isDev }: { isDev: boolean } = window.require('@electron/remote').require('./electron');
|
|
||||||
|
|
||||||
export default isDev;
|
|
|
@ -1,8 +1,12 @@
|
||||||
const logger = require('./logger');
|
import assert from 'assert';
|
||||||
const { createMediaSourceProcess, readOneJpegFrame } = require('./ffmpeg');
|
|
||||||
|
import logger from './logger.js';
|
||||||
|
import { createMediaSourceProcess, readOneJpegFrame as readOneJpegFrameRaw } from './ffmpeg.js';
|
||||||
|
|
||||||
|
|
||||||
function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }) {
|
export function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }: {
|
||||||
|
path: string, videoStreamIndex?: number | undefined, audioStreamIndex?: number | undefined, seekTo: number, size?: number | undefined, fps?: number | undefined,
|
||||||
|
}) {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
|
logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
|
||||||
const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps });
|
const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps });
|
||||||
|
@ -13,38 +17,39 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
|
||||||
process.kill('SIGKILL');
|
process.kill('SIGKILL');
|
||||||
};
|
};
|
||||||
|
|
||||||
process.stdout.pause();
|
const { stdout } = process;
|
||||||
|
assert(stdout != null);
|
||||||
|
|
||||||
async function readChunk() {
|
stdout.pause();
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let cleanup;
|
const readChunk = async () => new Promise((resolve, reject) => {
|
||||||
|
let cleanup: () => void;
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
resolve(null);
|
resolve(null);
|
||||||
};
|
};
|
||||||
const onData = (chunk) => {
|
const onData = (chunk: Buffer) => {
|
||||||
process.stdout.pause();
|
stdout.pause();
|
||||||
cleanup();
|
cleanup();
|
||||||
resolve(chunk);
|
resolve(chunk);
|
||||||
};
|
};
|
||||||
const onError = (err) => {
|
const onError = (err: Error) => {
|
||||||
cleanup();
|
cleanup();
|
||||||
reject(err);
|
reject(err);
|
||||||
};
|
};
|
||||||
cleanup = () => {
|
cleanup = () => {
|
||||||
process.stdout.off('data', onData);
|
stdout.off('data', onData);
|
||||||
process.stdout.off('error', onError);
|
stdout.off('error', onError);
|
||||||
process.stdout.off('close', onClose);
|
stdout.off('close', onClose);
|
||||||
};
|
};
|
||||||
|
|
||||||
process.stdout.once('data', onData);
|
stdout.once('data', onData);
|
||||||
process.stdout.once('error', onError);
|
stdout.once('error', onError);
|
||||||
process.stdout.once('close', onClose);
|
stdout.once('close', onClose);
|
||||||
|
|
||||||
process.stdout.resume();
|
stdout.resume();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function abort() {
|
function abort() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
|
@ -75,9 +80,9 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
|
||||||
return { abort, readChunk };
|
return { abort, readChunk };
|
||||||
}
|
}
|
||||||
|
|
||||||
function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
|
export function readOneJpegFrame({ path, seekTo, videoStreamIndex }: { path: string, seekTo: number, videoStreamIndex: number }) {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const process = readOneJpegFrame({ path, seekTo, videoStreamIndex });
|
const process = readOneJpegFrameRaw({ path, seekTo, videoStreamIndex });
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||||
abortController.signal.onabort = () => process.kill('SIGKILL');
|
abortController.signal.onabort = () => process.kill('SIGKILL');
|
||||||
|
@ -99,9 +104,3 @@ function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
|
||||||
|
|
||||||
return { promise, abort };
|
return { promise, abort };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createMediaSourceStream,
|
|
||||||
readOneJpegFrame: readOneJpegFrameWrapper,
|
|
||||||
};
|
|
|
@ -1,17 +1,17 @@
|
||||||
const Store = require('electron-store');
|
import Store from 'electron-store';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const electron = require('electron');
|
import electron from 'electron';
|
||||||
const os = require('os');
|
import { join, dirname } from 'path';
|
||||||
const { join, dirname } = require('path');
|
import { pathExists } from 'fs-extra';
|
||||||
const { pathExists } = require('fs-extra');
|
|
||||||
|
|
||||||
const logger = require('./logger');
|
import { KeyBinding, Config } from '../../types.js';
|
||||||
|
import logger from './logger.js';
|
||||||
|
import { isWindows } from './util.js';
|
||||||
|
|
||||||
const { app } = electron;
|
const { app } = electron;
|
||||||
|
|
||||||
|
|
||||||
/** @type {import('../types').KeyBinding[]} */
|
const defaultKeyBindings: KeyBinding[] = [
|
||||||
const defaultKeyBindings = [
|
|
||||||
{ keys: 'plus', action: 'addSegment' },
|
{ keys: 'plus', action: 'addSegment' },
|
||||||
{ keys: 'space', action: 'togglePlayResetSpeed' },
|
{ keys: 'space', action: 'togglePlayResetSpeed' },
|
||||||
{ keys: 'k', action: 'togglePlayNoResetSpeed' },
|
{ keys: 'k', action: 'togglePlayNoResetSpeed' },
|
||||||
|
@ -83,8 +83,7 @@ const defaultKeyBindings = [
|
||||||
{ keys: 'alt+down', action: 'decreaseVolume' },
|
{ keys: 'alt+down', action: 'decreaseVolume' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @type {import('../types').Config} */
|
const defaults: Config = {
|
||||||
const defaults = {
|
|
||||||
captureFormat: 'jpeg',
|
captureFormat: 'jpeg',
|
||||||
customOutDir: undefined,
|
customOutDir: undefined,
|
||||||
keyframeCut: true,
|
keyframeCut: true,
|
||||||
|
@ -145,7 +144,6 @@ const defaults = {
|
||||||
// For portable app: https://github.com/mifi/lossless-cut/issues/645
|
// For portable app: https://github.com/mifi/lossless-cut/issues/645
|
||||||
async function getCustomStoragePath() {
|
async function getCustomStoragePath() {
|
||||||
try {
|
try {
|
||||||
const isWindows = os.platform() === 'win32';
|
|
||||||
if (!isWindows || process.windowsStore) return undefined;
|
if (!isWindows || process.windowsStore) return undefined;
|
||||||
|
|
||||||
// https://github.com/mifi/lossless-cut/issues/645#issuecomment-1001363314
|
// https://github.com/mifi/lossless-cut/issues/645#issuecomment-1001363314
|
||||||
|
@ -161,28 +159,28 @@ async function getCustomStoragePath() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let store;
|
let store: Store;
|
||||||
|
|
||||||
/** @type {import('../types').StoreGetConfig} */
|
export function get<T extends keyof Config>(key: T): Config[T] {
|
||||||
function get(key) {
|
|
||||||
return store.get(key);
|
return store.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('../types').StoreSetConfig} */
|
export function set<T extends keyof Config>(key: T, val: Config[T]) {
|
||||||
function set(key, val) {
|
|
||||||
if (val === undefined) store.delete(key);
|
if (val === undefined) store.delete(key);
|
||||||
else store.set(key, val);
|
else store.set(key, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('../types').StoreResetConfig} */
|
export function reset<T extends keyof Config>(key: T) {
|
||||||
function reset(key) {
|
|
||||||
set(key, defaults[key]);
|
set(key, defaults[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryCreateStore({ customStoragePath }) {
|
async function tryCreateStore({ customStoragePath }: { customStoragePath: string | undefined }) {
|
||||||
for (let i = 0; i < 5; i += 1) {
|
for (let i = 0; i < 5; i += 1) {
|
||||||
try {
|
try {
|
||||||
store = new Store({ defaults, cwd: customStoragePath });
|
store = new Store({
|
||||||
|
defaults,
|
||||||
|
...(customStoragePath != null ? { cwd: customStoragePath } : {}),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
@ -194,7 +192,7 @@ async function tryCreateStore({ customStoragePath }) {
|
||||||
throw new Error('Timed out while creating config store');
|
throw new Error('Timed out while creating config store');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
export async function init() {
|
||||||
const customStoragePath = await getCustomStoragePath();
|
const customStoragePath = await getCustomStoragePath();
|
||||||
if (customStoragePath) logger.info('customStoragePath', customStoragePath);
|
if (customStoragePath) logger.info('customStoragePath', customStoragePath);
|
||||||
|
|
||||||
|
@ -214,10 +212,3 @@ async function init() {
|
||||||
set('cleanupChoices', { ...cleanupChoices, closeFile: true });
|
set('cleanupChoices', { ...cleanupChoices, closeFile: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
reset,
|
|
||||||
};
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const homepage = 'https://mifi.no/losslesscut/';
|
||||||
|
export const githubLink = 'https://github.com/mifi/lossless-cut/';
|
||||||
|
export const getReleaseUrl = (version: string) => `https://github.com/mifi/lossless-cut/releases/tag/v${version}`;
|
||||||
|
export const licensesPage = 'https://losslesscut.mifi.no/licenses.txt';
|
|
@ -1,8 +1,8 @@
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { Menu } = require('electron');
|
import { BrowserWindow, Menu } from 'electron';
|
||||||
|
|
||||||
// https://github.com/electron/electron/issues/4068#issuecomment-274159726
|
// https://github.com/electron/electron/issues/4068#issuecomment-274159726
|
||||||
module.exports = (window) => {
|
export default (window: BrowserWindow) => {
|
||||||
const selectionMenu = Menu.buildFromTemplate([
|
const selectionMenu = Menu.buildFromTemplate([
|
||||||
{ role: 'copy' },
|
{ role: 'copy' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
@ -23,9 +23,9 @@ module.exports = (window) => {
|
||||||
window.webContents.on('context-menu', (_e, props) => {
|
window.webContents.on('context-menu', (_e, props) => {
|
||||||
const { selectionText, isEditable } = props;
|
const { selectionText, isEditable } = props;
|
||||||
if (isEditable) {
|
if (isEditable) {
|
||||||
inputMenu.popup(window);
|
inputMenu.popup({ window });
|
||||||
} else if (selectionText && selectionText.trim() !== '') {
|
} else if (selectionText && selectionText.trim() !== '') {
|
||||||
selectionMenu.popup(window);
|
selectionMenu.popup({ window });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
|
@ -1,53 +1,60 @@
|
||||||
const { join } = require('path');
|
import { join } from 'path';
|
||||||
const isDev = require('electron-is-dev');
|
import readline from 'readline';
|
||||||
const readline = require('readline');
|
import stringToStream from 'string-to-stream';
|
||||||
const stringToStream = require('string-to-stream');
|
import { BufferEncodingOption, execa, ExecaChildProcess, Options as ExecaOptions } from 'execa';
|
||||||
const execa = require('execa');
|
import assert from 'assert';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { app } from 'electron';
|
||||||
|
|
||||||
const { platform, arch, isWindows, isMac, isLinux } = require('./util');
|
import { platform, arch, isWindows, isMac, isLinux } from './util.js';
|
||||||
|
import { CaptureFormat, Html5ifyMode, Waveform } from '../../types.js';
|
||||||
|
import isDev from './isDev.js';
|
||||||
|
|
||||||
|
|
||||||
const runningFfmpegs = new Set();
|
const runningFfmpegs = new Set<ExecaChildProcess<Buffer>>();
|
||||||
// setInterval(() => console.log(runningFfmpegs.size), 1000);
|
// setInterval(() => console.log(runningFfmpegs.size), 1000);
|
||||||
|
|
||||||
let customFfPath;
|
let customFfPath: string | undefined;
|
||||||
|
|
||||||
// Note that this does not work on MAS because of sandbox restrictions
|
// Note that this does not work on MAS because of sandbox restrictions
|
||||||
function setCustomFfPath(path) {
|
export function setCustomFfPath(path: string | undefined) {
|
||||||
customFfPath = path;
|
customFfPath = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFfCommandLine(cmd, args) {
|
export function getFfCommandLine(cmd: string, args: readonly string[]) {
|
||||||
return `${cmd} ${args.map((arg) => (/[^\w-]/.test(arg) ? `'${arg}'` : arg)).join(' ')}`;
|
return `${cmd} ${args.map((arg) => (/[^\w-]/.test(arg) ? `'${arg}'` : arg)).join(' ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFfPath(cmd) {
|
function getFfPath(cmd: string) {
|
||||||
const exeName = isWindows ? `${cmd}.exe` : cmd;
|
const exeName = isWindows ? `${cmd}.exe` : cmd;
|
||||||
|
|
||||||
if (customFfPath) return join(customFfPath, exeName);
|
if (customFfPath) return join(customFfPath, exeName);
|
||||||
|
|
||||||
if (isDev) {
|
if (app.isPackaged) {
|
||||||
|
return join(process.resourcesPath, exeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// local dev
|
||||||
const components = ['ffmpeg', `${platform}-${arch}`];
|
const components = ['ffmpeg', `${platform}-${arch}`];
|
||||||
if (isWindows || isLinux) components.push('lib');
|
if (isWindows || isLinux) components.push('lib');
|
||||||
components.push(exeName);
|
components.push(exeName);
|
||||||
return join(...components);
|
return join(...components);
|
||||||
}
|
|
||||||
|
|
||||||
return join(process.resourcesPath, exeName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFfprobePath = () => getFfPath('ffprobe');
|
const getFfprobePath = () => getFfPath('ffprobe');
|
||||||
const getFfmpegPath = () => getFfPath('ffmpeg');
|
export const getFfmpegPath = () => getFfPath('ffmpeg');
|
||||||
|
|
||||||
function abortFfmpegs() {
|
export function abortFfmpegs() {
|
||||||
console.log('Aborting', runningFfmpegs.size, 'ffmpeg process(es)');
|
console.log('Aborting', runningFfmpegs.size, 'ffmpeg process(es)');
|
||||||
runningFfmpegs.forEach((process) => {
|
runningFfmpegs.forEach((process) => {
|
||||||
process.kill('SIGTERM', { forceKillAfterTimeout: 10000 });
|
process.kill('SIGTERM', { forceKillAfterTimeout: 10000 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProgress(process, durationIn, onProgress, customMatcher = () => undefined) {
|
function handleProgress(process: { stderr: Readable | null }, durationIn: number | undefined, onProgress: (a: number) => void, customMatcher: (a: string) => void = () => undefined) {
|
||||||
if (!onProgress) return;
|
if (!onProgress) return;
|
||||||
|
if (process.stderr == null) return;
|
||||||
onProgress(0);
|
onProgress(0);
|
||||||
|
|
||||||
const rl = readline.createInterface({ input: process.stderr });
|
const rl = readline.createInterface({ input: process.stderr });
|
||||||
|
@ -61,20 +68,19 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
|
||||||
// eslint-disable-next-line unicorn/better-regex
|
// eslint-disable-next-line unicorn/better-regex
|
||||||
if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
|
if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
// @ts-expect-error todo
|
|
||||||
customMatcher(line);
|
customMatcher(line);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeStr = match[1];
|
const timeStr = match[1];
|
||||||
// console.log(timeStr);
|
// console.log(timeStr);
|
||||||
const match2 = timeStr.match(/^(\d+):(\d+):(\d+)\.(\d+)$/);
|
const match2 = timeStr!.match(/^(\d+):(\d+):(\d+)\.(\d+)$/);
|
||||||
if (!match2) throw new Error(`Invalid time from ffmpeg progress ${timeStr}`);
|
if (!match2) throw new Error(`Invalid time from ffmpeg progress ${timeStr}`);
|
||||||
|
|
||||||
const h = parseInt(match2[1], 10);
|
const h = parseInt(match2[1]!, 10);
|
||||||
const m = parseInt(match2[2], 10);
|
const m = parseInt(match2[2]!, 10);
|
||||||
const s = parseInt(match2[3], 10);
|
const s = parseInt(match2[3]!, 10);
|
||||||
const cs = parseInt(match2[4], 10);
|
const cs = parseInt(match2[4]!, 10);
|
||||||
const time = (((h * 60) + m) * 60 + s) + cs / 100;
|
const time = (((h * 60) + m) * 60 + s) + cs / 100;
|
||||||
// console.log(time);
|
// console.log(time);
|
||||||
|
|
||||||
|
@ -93,18 +99,22 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error todo
|
function getExecaOptions({ env, ...customExecaOptions }: Omit<ExecaOptions<BufferEncodingOption>, 'buffer'> = {}) {
|
||||||
function getExecaOptions({ env, ...customExecaOptions } = {}) {
|
const execaOptions: Omit<ExecaOptions<BufferEncodingOption>, 'buffer'> = { ...customExecaOptions, encoding: 'buffer' };
|
||||||
const execaOptions = { ...customExecaOptions, env: { ...env } };
|
|
||||||
// https://github.com/mifi/lossless-cut/issues/1143#issuecomment-1500883489
|
// https://github.com/mifi/lossless-cut/issues/1143#issuecomment-1500883489
|
||||||
if (isLinux && !isDev && !customFfPath) execaOptions.env.LD_LIBRARY_PATH = process.resourcesPath;
|
if (isLinux && !isDev && !customFfPath) {
|
||||||
|
return {
|
||||||
|
...execaOptions,
|
||||||
|
env: { ...env, LD_LIBRARY_PATH: process.resourcesPath },
|
||||||
|
};
|
||||||
|
}
|
||||||
return execaOptions;
|
return execaOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo collect warnings from ffmpeg output and show them after export? example: https://github.com/mifi/lossless-cut/issues/1469
|
// todo collect warnings from ffmpeg output and show them after export? example: https://github.com/mifi/lossless-cut/issues/1469
|
||||||
function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
|
function runFfmpegProcess(args: readonly string[], customExecaOptions?: Omit<ExecaOptions<BufferEncodingOption>, 'encoding'>, additionalOptions?: { logCli?: boolean }) {
|
||||||
const ffmpegPath = getFfmpegPath();
|
const ffmpegPath = getFfmpegPath();
|
||||||
if (logCli) console.log(getFfCommandLine('ffmpeg', args));
|
if (additionalOptions?.logCli) console.log(getFfCommandLine('ffmpeg', args));
|
||||||
|
|
||||||
const process = execa(ffmpegPath, args, getExecaOptions(customExecaOptions));
|
const process = execa(ffmpegPath, args, getExecaOptions(customExecaOptions));
|
||||||
|
|
||||||
|
@ -121,23 +131,29 @@ function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
|
||||||
return process;
|
return process;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runFfmpegConcat({ ffmpegArgs, concatTxt, totalDuration, onProgress }) {
|
export async function runFfmpegConcat({ ffmpegArgs, concatTxt, totalDuration, onProgress }: {
|
||||||
|
ffmpegArgs: string[], concatTxt: string, totalDuration: number, onProgress: (a: number) => void
|
||||||
|
}) {
|
||||||
const process = runFfmpegProcess(ffmpegArgs);
|
const process = runFfmpegProcess(ffmpegArgs);
|
||||||
|
|
||||||
handleProgress(process, totalDuration, onProgress);
|
handleProgress(process, totalDuration, onProgress);
|
||||||
|
|
||||||
|
assert(process.stdin != null);
|
||||||
stringToStream(concatTxt).pipe(process.stdin);
|
stringToStream(concatTxt).pipe(process.stdin);
|
||||||
|
|
||||||
return process;
|
return process;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runFfmpegWithProgress({ ffmpegArgs, duration, onProgress }) {
|
export async function runFfmpegWithProgress({ ffmpegArgs, duration, onProgress }: {
|
||||||
|
ffmpegArgs: string[], duration: number | undefined, onProgress: (a: number) => void,
|
||||||
|
}) {
|
||||||
const process = runFfmpegProcess(ffmpegArgs);
|
const process = runFfmpegProcess(ffmpegArgs);
|
||||||
|
assert(process.stderr != null);
|
||||||
handleProgress(process, duration, onProgress);
|
handleProgress(process, duration, onProgress);
|
||||||
return process;
|
return process;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runFfprobe(args, { timeout = isDev ? 10000 : 30000 } = {}) {
|
export async function runFfprobe(args: readonly string[], { timeout = isDev ? 10000 : 30000 } = {}) {
|
||||||
const ffprobePath = getFfprobePath();
|
const ffprobePath = getFfprobePath();
|
||||||
console.log(getFfCommandLine('ffprobe', args));
|
console.log(getFfCommandLine('ffprobe', args));
|
||||||
const ps = execa(ffprobePath, args, getExecaOptions());
|
const ps = execa(ffprobePath, args, getExecaOptions());
|
||||||
|
@ -152,13 +168,14 @@ async function runFfprobe(args, { timeout = isDev ? 10000 : 30000 } = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {(a: any) => Promise<import('../types').Waveform>} */
|
export async function renderWaveformPng({ filePath, start, duration, color, streamIndex }: {
|
||||||
async function renderWaveformPng({ filePath, start, duration, color, streamIndex }) {
|
filePath: string, start: number, duration: number, color: string, streamIndex: number,
|
||||||
|
}): Promise<Waveform> {
|
||||||
const args1 = [
|
const args1 = [
|
||||||
'-hide_banner',
|
'-hide_banner',
|
||||||
'-i', filePath,
|
'-i', filePath,
|
||||||
'-ss', start,
|
'-ss', String(start),
|
||||||
'-t', duration,
|
'-t', String(duration),
|
||||||
'-c', 'copy',
|
'-c', 'copy',
|
||||||
'-vn',
|
'-vn',
|
||||||
'-map', `0:${streamIndex}`,
|
'-map', `0:${streamIndex}`,
|
||||||
|
@ -179,16 +196,18 @@ async function renderWaveformPng({ filePath, start, duration, color, streamIndex
|
||||||
console.log(getFfCommandLine('ffmpeg1', args1));
|
console.log(getFfCommandLine('ffmpeg1', args1));
|
||||||
console.log('|', getFfCommandLine('ffmpeg2', args2));
|
console.log('|', getFfCommandLine('ffmpeg2', args2));
|
||||||
|
|
||||||
let ps1;
|
let ps1: ExecaChildProcess<Buffer> | undefined;
|
||||||
let ps2;
|
let ps2: ExecaChildProcess<Buffer> | undefined;
|
||||||
try {
|
try {
|
||||||
ps1 = runFfmpegProcess(args1, { encoding: null, buffer: false }, { logCli: false });
|
ps1 = runFfmpegProcess(args1, { buffer: false }, { logCli: false });
|
||||||
ps2 = runFfmpegProcess(args2, { encoding: null }, { logCli: false });
|
ps2 = runFfmpegProcess(args2, undefined, { logCli: false });
|
||||||
|
assert(ps1.stdout != null);
|
||||||
|
assert(ps2.stdin != null);
|
||||||
ps1.stdout.pipe(ps2.stdin);
|
ps1.stdout.pipe(ps2.stdin);
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
ps1.kill();
|
ps1?.kill();
|
||||||
ps2.kill();
|
ps2?.kill();
|
||||||
console.warn('ffmpeg timed out');
|
console.warn('ffmpeg timed out');
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
@ -209,14 +228,14 @@ async function renderWaveformPng({ filePath, start, duration, color, streamIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInputSeekArgs = ({ filePath, from, to }) => [
|
const getInputSeekArgs = ({ filePath, from, to }: { filePath: string, from?: number | undefined, to?: number | undefined }) => [
|
||||||
...(from != null ? ['-ss', from.toFixed(5)] : []),
|
...(from != null ? ['-ss', from.toFixed(5)] : []),
|
||||||
'-i', filePath,
|
'-i', filePath,
|
||||||
...(to != null ? ['-t', (to - from).toFixed(5)] : []),
|
...(from != null && to != null ? ['-t', (to - from).toFixed(5)] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
function mapTimesToSegments(times) {
|
export function mapTimesToSegments(times: number[]) {
|
||||||
const segments = [];
|
const segments: { start: number, end: number | undefined }[] = [];
|
||||||
for (let i = 0; i < times.length; i += 1) {
|
for (let i = 0; i < times.length; i += 1) {
|
||||||
const start = times[i];
|
const start = times[i];
|
||||||
const end = times[i + 1];
|
const end = times[i + 1];
|
||||||
|
@ -225,32 +244,35 @@ function mapTimesToSegments(times) {
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSegmentOffset = (from) => (from != null ? from : 0);
|
const getSegmentOffset = (from?: number) => (from != null ? from : 0);
|
||||||
|
|
||||||
function adjustSegmentsWithOffset({ segments, from }) {
|
function adjustSegmentsWithOffset({ segments, from }: { segments: { start: number, end: number | undefined }[], from?: number | undefined }) {
|
||||||
const offset = getSegmentOffset(from);
|
const offset = getSegmentOffset(from);
|
||||||
return segments.map(({ start, end }) => ({ start: start + offset, end: end != null ? end + offset : end }));
|
return segments.map(({ start, end }) => ({ start: start + offset, end: end != null ? end + offset : end }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/35675529/using-ffmpeg-how-to-do-a-scene-change-detection-with-timecode
|
// https://stackoverflow.com/questions/35675529/using-ffmpeg-how-to-do-a-scene-change-detection-with-timecode
|
||||||
async function detectSceneChanges({ filePath, minChange, onProgress, from, to }) {
|
export async function detectSceneChanges({ filePath, minChange, onProgress, from, to }: {
|
||||||
|
filePath: string, minChange: number | string, onProgress: (p: number) => void, from: number, to: number,
|
||||||
|
}) {
|
||||||
const args = [
|
const args = [
|
||||||
'-hide_banner',
|
'-hide_banner',
|
||||||
...getInputSeekArgs({ filePath, from, to }),
|
...getInputSeekArgs({ filePath, from, to }),
|
||||||
'-filter_complex', `select='gt(scene,${minChange})',metadata=print:file=-`,
|
'-filter_complex', `select='gt(scene,${minChange})',metadata=print:file=-`,
|
||||||
'-f', 'null', '-',
|
'-f', 'null', '-',
|
||||||
];
|
];
|
||||||
const process = runFfmpegProcess(args, { encoding: null, buffer: false });
|
const process = runFfmpegProcess(args, { buffer: false });
|
||||||
|
|
||||||
const times = [0];
|
const times = [0];
|
||||||
|
|
||||||
handleProgress(process, to - from, onProgress);
|
handleProgress(process, to - from, onProgress);
|
||||||
|
assert(process.stdout != null);
|
||||||
const rl = readline.createInterface({ input: process.stdout });
|
const rl = readline.createInterface({ input: process.stdout });
|
||||||
rl.on('line', (line) => {
|
rl.on('line', (line) => {
|
||||||
// eslint-disable-next-line unicorn/better-regex
|
// eslint-disable-next-line unicorn/better-regex
|
||||||
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
|
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
const time = parseFloat(match[1]);
|
const time = parseFloat(match[1]!);
|
||||||
// @ts-expect-error todo
|
// @ts-expect-error todo
|
||||||
if (Number.isNaN(time) || time <= times.at(-1)) return;
|
if (Number.isNaN(time) || time <= times.at(-1)) return;
|
||||||
times.push(time);
|
times.push(time);
|
||||||
|
@ -263,89 +285,134 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
|
||||||
return adjustSegmentsWithOffset({ segments, from });
|
return adjustSegmentsWithOffset({ segments, from });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectIntervals({ filePath, customArgs, onProgress, from, to, matchLineTokens }) {
|
async function detectIntervals({ filePath, customArgs, onProgress, from, to, matchLineTokens }: {
|
||||||
|
filePath: string, customArgs: string[], onProgress: (p: number) => void, from: number, to: number, matchLineTokens: (line: string) => { start?: string | number | undefined, end?: string | number | undefined },
|
||||||
|
}) {
|
||||||
const args = [
|
const args = [
|
||||||
'-hide_banner',
|
'-hide_banner',
|
||||||
...getInputSeekArgs({ filePath, from, to }),
|
...getInputSeekArgs({ filePath, from, to }),
|
||||||
...customArgs,
|
...customArgs,
|
||||||
'-f', 'null', '-',
|
'-f', 'null', '-',
|
||||||
];
|
];
|
||||||
const process = runFfmpegProcess(args, { encoding: null, buffer: false });
|
const process = runFfmpegProcess(args, { buffer: false });
|
||||||
|
|
||||||
const segments = [];
|
const segments: { start: number, end: number }[] = [];
|
||||||
|
|
||||||
function customMatcher(line) {
|
function customMatcher(line: string) {
|
||||||
const { start: startStr, end: endStr } = matchLineTokens(line);
|
const { start: startRaw, end: endRaw } = matchLineTokens(line);
|
||||||
const start = parseFloat(startStr);
|
if (typeof startRaw === 'number' && typeof endRaw === 'number') {
|
||||||
const end = parseFloat(endStr);
|
if (startRaw == null || endRaw == null) return;
|
||||||
if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) return;
|
if (Number.isNaN(startRaw) || Number.isNaN(endRaw)) return;
|
||||||
|
segments.push({ start: startRaw, end: endRaw });
|
||||||
|
} else if (typeof startRaw === 'string' && typeof endRaw === 'string') {
|
||||||
|
if (startRaw == null || endRaw == null) return;
|
||||||
|
const start = parseFloat(startRaw);
|
||||||
|
const end = parseFloat(endRaw);
|
||||||
|
if (Number.isNaN(start) || Number.isNaN(end)) return;
|
||||||
segments.push({ start, end });
|
segments.push({ start, end });
|
||||||
|
} else {
|
||||||
|
throw new TypeError('Invalid line match');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// @ts-expect-error todo
|
|
||||||
handleProgress(process, to - from, onProgress, customMatcher);
|
handleProgress(process, to - from, onProgress, customMatcher);
|
||||||
|
|
||||||
await process;
|
await process;
|
||||||
return adjustSegmentsWithOffset({ segments, from });
|
return adjustSegmentsWithOffset({ segments, from });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapFilterOptions = (options) => Object.entries(options).map(([key, value]) => `${key}=${value}`).join(':');
|
const mapFilterOptions = (options: Record<string, string>) => Object.entries(options).map(([key, value]) => `${key}=${value}`).join(':');
|
||||||
|
|
||||||
async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
|
export async function blackDetect({ filePath, filterOptions, onProgress, from, to }: { filePath: string, filterOptions: Record<string, string>, onProgress: (p: number) => void, from: number, to: number }) {
|
||||||
function matchLineTokens(line) {
|
const customArgs = ['-vf', `blackdetect=${mapFilterOptions(filterOptions)}`, '-an'];
|
||||||
|
return detectIntervals({
|
||||||
|
filePath,
|
||||||
|
onProgress,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
matchLineTokens: (line) => {
|
||||||
// eslint-disable-next-line unicorn/better-regex
|
// eslint-disable-next-line unicorn/better-regex
|
||||||
const match = line.match(/^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/);
|
const match = line.match(/^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/);
|
||||||
if (!match) return {};
|
if (!match) {
|
||||||
return {
|
return {
|
||||||
start: parseFloat(match[1]),
|
start: undefined,
|
||||||
end: parseFloat(match[2]),
|
end: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const customArgs = ['-vf', `blackdetect=${mapFilterOptions(filterOptions)}`, '-an'];
|
return {
|
||||||
return detectIntervals({ filePath, onProgress, from, to, matchLineTokens, customArgs });
|
start: match[1],
|
||||||
|
end: match[2],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
customArgs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) {
|
export async function silenceDetect({ filePath, filterOptions, onProgress, from, to }: {
|
||||||
function matchLineTokens(line) {
|
filePath: string, filterOptions: Record<string, string>, onProgress: (p: number) => void, from: number, to: number,
|
||||||
|
}) {
|
||||||
|
return detectIntervals({
|
||||||
|
filePath,
|
||||||
|
onProgress,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
matchLineTokens: (line) => {
|
||||||
// eslint-disable-next-line unicorn/better-regex
|
// eslint-disable-next-line unicorn/better-regex
|
||||||
const match = line.match(/^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/);
|
const match = line.match(/^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/);
|
||||||
if (!match) return {};
|
if (!match) {
|
||||||
const end = parseFloat(match[1]);
|
return {
|
||||||
const silenceDuration = parseFloat(match[2]);
|
start: undefined,
|
||||||
if (Number.isNaN(end) || Number.isNaN(silenceDuration)) return {};
|
end: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const end = parseFloat(match[1]!);
|
||||||
|
const silenceDuration = parseFloat(match[2]!);
|
||||||
|
if (Number.isNaN(end) || Number.isNaN(silenceDuration)) {
|
||||||
|
return {
|
||||||
|
start: undefined,
|
||||||
|
end: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
const start = end - silenceDuration;
|
const start = end - silenceDuration;
|
||||||
if (start < 0 || end <= 0 || start >= end) return {};
|
if (start < 0 || end <= 0 || start >= end) {
|
||||||
|
return {
|
||||||
|
start: undefined,
|
||||||
|
end: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
const customArgs = ['-af', `silencedetect=${mapFilterOptions(filterOptions)}`, '-vn'];
|
customArgs: ['-af', `silencedetect=${mapFilterOptions(filterOptions)}`, '-vn'],
|
||||||
return detectIntervals({ filePath, onProgress, from, to, matchLineTokens, customArgs });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFffmpegJpegQuality(quality) {
|
function getFffmpegJpegQuality(quality: number) {
|
||||||
// Normal range for JPEG is 2-31 with 31 being the worst quality.
|
// Normal range for JPEG is 2-31 with 31 being the worst quality.
|
||||||
const qMin = 2;
|
const qMin = 2;
|
||||||
const qMax = 31;
|
const qMax = 31;
|
||||||
return Math.min(Math.max(qMin, quality, Math.round((1 - quality) * (qMax - qMin) + qMin)), qMax);
|
return Math.min(Math.max(qMin, quality, Math.round((1 - quality) * (qMax - qMin) + qMin)), qMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQualityOpts({ captureFormat, quality }) {
|
function getQualityOpts({ captureFormat, quality }: { captureFormat: CaptureFormat, quality: number }) {
|
||||||
if (captureFormat === 'jpeg') return ['-q:v', getFffmpegJpegQuality(quality)];
|
if (captureFormat === 'jpeg') return ['-q:v', String(getFffmpegJpegQuality(quality))];
|
||||||
if (captureFormat === 'webp') return ['-q:v', Math.max(0, Math.min(100, Math.round(quality * 100)))];
|
if (captureFormat === 'webp') return ['-q:v', String(Math.max(0, Math.min(100, Math.round(quality * 100))))];
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCodecOpts(captureFormat) {
|
function getCodecOpts(captureFormat: CaptureFormat) {
|
||||||
if (captureFormat === 'webp') return ['-c:v', 'libwebp']; // else we get only a single file for webp https://github.com/mifi/lossless-cut/issues/1693
|
if (captureFormat === 'webp') return ['-c:v', 'libwebp']; // else we get only a single file for webp https://github.com/mifi/lossless-cut/issues/1693
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function captureFrames({ from, to, videoPath, outPathTemplate, quality, filter, framePts, onProgress, captureFormat }) {
|
export async function captureFrames({ from, to, videoPath, outPathTemplate, quality, filter, framePts, onProgress, captureFormat }: {
|
||||||
|
from: number, to: number, videoPath: string, outPathTemplate: string, quality: number, filter?: string | undefined, framePts?: boolean | undefined, onProgress: (p: number) => void, captureFormat: CaptureFormat,
|
||||||
|
}) {
|
||||||
const args = [
|
const args = [
|
||||||
'-ss', from,
|
'-ss', String(from),
|
||||||
'-i', videoPath,
|
'-i', videoPath,
|
||||||
'-t', Math.max(0, to - from),
|
'-t', String(Math.max(0, to - from)),
|
||||||
...getQualityOpts({ captureFormat, quality }),
|
...getQualityOpts({ captureFormat, quality }),
|
||||||
...(filter != null ? ['-vf', filter] : []),
|
...(filter != null ? ['-vf', filter] : []),
|
||||||
// https://superuser.com/questions/1336285/use-ffmpeg-for-thumbnail-selections
|
// https://superuser.com/questions/1336285/use-ffmpeg-for-thumbnail-selections
|
||||||
|
@ -356,39 +423,43 @@ async function captureFrames({ from, to, videoPath, outPathTemplate, quality, fi
|
||||||
'-y', outPathTemplate,
|
'-y', outPathTemplate,
|
||||||
];
|
];
|
||||||
|
|
||||||
const process = runFfmpegProcess(args, { encoding: null, buffer: false });
|
const process = runFfmpegProcess(args, { buffer: false });
|
||||||
|
|
||||||
handleProgress(process, to - from, onProgress);
|
handleProgress(process, to - from, onProgress);
|
||||||
|
|
||||||
await process;
|
await process;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function captureFrame({ timestamp, videoPath, outPath, quality }) {
|
export async function captureFrame({ timestamp, videoPath, outPath, quality }: {
|
||||||
|
timestamp: number, videoPath: string, outPath: string, quality: number,
|
||||||
|
}) {
|
||||||
const ffmpegQuality = getFffmpegJpegQuality(quality);
|
const ffmpegQuality = getFffmpegJpegQuality(quality);
|
||||||
await runFfmpegProcess([
|
await runFfmpegProcess([
|
||||||
'-ss', timestamp,
|
'-ss', String(timestamp),
|
||||||
'-i', videoPath,
|
'-i', videoPath,
|
||||||
'-vframes', '1',
|
'-vframes', '1',
|
||||||
'-q:v', ffmpegQuality,
|
'-q:v', String(ffmpegQuality),
|
||||||
'-y', outPath,
|
'-y', outPath,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function readFormatData(filePath) {
|
async function readFormatData(filePath: string) {
|
||||||
console.log('readFormatData', filePath);
|
console.log('readFormatData', filePath);
|
||||||
|
|
||||||
const { stdout } = await runFfprobe([
|
const { stdout } = await runFfprobe([
|
||||||
'-of', 'json', '-show_format', '-i', filePath, '-hide_banner',
|
'-of', 'json', '-show_format', '-i', filePath, '-hide_banner',
|
||||||
]);
|
]);
|
||||||
return JSON.parse(stdout).format;
|
return JSON.parse(stdout as unknown as string).format;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDuration(filePath) {
|
export async function getDuration(filePath: string) {
|
||||||
return parseFloat((await readFormatData(filePath)).duration);
|
return parseFloat((await readFormatData(filePath)).duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }) {
|
export async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }: {
|
||||||
|
outPath: string, filePath: string, speed: Html5ifyMode, hasAudio: boolean, hasVideo: boolean, onProgress: (p: number) => void,
|
||||||
|
}) {
|
||||||
let audio;
|
let audio;
|
||||||
if (hasAudio) {
|
if (hasAudio) {
|
||||||
if (speed === 'slowest') audio = 'hq';
|
if (speed === 'slowest') audio = 'hq';
|
||||||
|
@ -492,14 +563,14 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
|
||||||
if (duration) handleProgress(process, duration, onProgress);
|
if (duration) handleProgress(process, duration, onProgress);
|
||||||
|
|
||||||
const { stdout } = await process;
|
const { stdout } = await process;
|
||||||
console.log(stdout);
|
console.log(stdout.toString('utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function readOneJpegFrame({ path, seekTo, videoStreamIndex }) {
|
export function readOneJpegFrame({ path, seekTo, videoStreamIndex }: { path: string, seekTo: number, videoStreamIndex: number }) {
|
||||||
const args = [
|
const args = [
|
||||||
'-hide_banner', '-loglevel', 'error',
|
'-hide_banner', '-loglevel', 'error',
|
||||||
|
|
||||||
'-ss', seekTo,
|
'-ss', String(seekTo),
|
||||||
|
|
||||||
'-noautorotate',
|
'-noautorotate',
|
||||||
|
|
||||||
|
@ -516,17 +587,19 @@ function readOneJpegFrame({ path, seekTo, videoStreamIndex }) {
|
||||||
|
|
||||||
// console.log(args);
|
// console.log(args);
|
||||||
|
|
||||||
return runFfmpegProcess(args, { encoding: 'buffer' }, { logCli: true });
|
return runFfmpegProcess(args, undefined, { logCli: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const enableLog = false;
|
const enableLog = false;
|
||||||
const encode = true;
|
const encode = true;
|
||||||
|
|
||||||
function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }) {
|
export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps }: {
|
||||||
|
path: string, videoStreamIndex?: number | undefined, audioStreamIndex?: number | undefined, seekTo: number, size?: number | undefined, fps?: number | undefined,
|
||||||
|
}) {
|
||||||
function getVideoFilters() {
|
function getVideoFilters() {
|
||||||
if (videoStreamIndex == null) return [];
|
if (videoStreamIndex == null) return [];
|
||||||
|
|
||||||
const filters = [];
|
const filters: string[] = [];
|
||||||
if (fps != null) filters.push(`fps=${fps}`);
|
if (fps != null) filters.push(`fps=${fps}`);
|
||||||
if (size != null) filters.push(`scale=${size}:${size}:flags=lanczos:force_original_aspect_ratio=decrease`);
|
if (size != null) filters.push(`scale=${size}:${size}:flags=lanczos:force_original_aspect_ratio=decrease`);
|
||||||
if (filters.length === 0) return [];
|
if (filters.length === 0) return [];
|
||||||
|
@ -547,7 +620,7 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
|
||||||
|
|
||||||
'-vsync', 'passthrough',
|
'-vsync', 'passthrough',
|
||||||
|
|
||||||
'-ss', seekTo,
|
'-ss', String(seekTo),
|
||||||
|
|
||||||
'-noautorotate',
|
'-noautorotate',
|
||||||
|
|
||||||
|
@ -582,27 +655,5 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
|
||||||
return execa(getFfmpegPath(), args, { encoding: null, buffer: false, stderr: enableLog ? 'inherit' : 'pipe' });
|
return execa(getFfmpegPath(), args, { encoding: null, buffer: false, stderr: enableLog ? 'inherit' : 'pipe' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't pass complex objects over the bridge
|
// Don't pass complex objects over the bridge (process)
|
||||||
const runFfmpeg = async (...args) => runFfmpegProcess(...args);
|
export const runFfmpeg = async (...args: Parameters<typeof runFfmpegProcess>) => runFfmpegProcess(...args);
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setCustomFfPath,
|
|
||||||
abortFfmpegs,
|
|
||||||
getFfmpegPath,
|
|
||||||
runFfprobe,
|
|
||||||
runFfmpeg,
|
|
||||||
runFfmpegConcat,
|
|
||||||
runFfmpegWithProgress,
|
|
||||||
renderWaveformPng,
|
|
||||||
mapTimesToSegments,
|
|
||||||
detectSceneChanges,
|
|
||||||
captureFrames,
|
|
||||||
captureFrame,
|
|
||||||
getFfCommandLine,
|
|
||||||
html5ify,
|
|
||||||
getDuration,
|
|
||||||
readOneJpegFrame,
|
|
||||||
blackDetect,
|
|
||||||
silenceDetect,
|
|
||||||
createMediaSourceProcess,
|
|
||||||
};
|
|
|
@ -1,14 +1,16 @@
|
||||||
const express = require('express');
|
import express from 'express';
|
||||||
const morgan = require('morgan');
|
import morgan from 'morgan';
|
||||||
const http = require('http');
|
import http from 'http';
|
||||||
const asyncHandler = require('express-async-handler');
|
import asyncHandler from 'express-async-handler';
|
||||||
const { homepage } = require('./constants');
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import { homepage } from './constants.js';
|
||||||
|
import logger from './logger.js';
|
||||||
|
|
||||||
|
|
||||||
const logger = require('./logger');
|
export default ({ port, onKeyboardAction }: {
|
||||||
|
port: number, onKeyboardAction: (a: string) => Promise<void>,
|
||||||
|
}) => {
|
||||||
module.exports = ({ port, onKeyboardAction }) => {
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// https://expressjs.com/en/resources/middleware/morgan.html
|
// https://expressjs.com/en/resources/middleware/morgan.html
|
||||||
|
@ -25,7 +27,10 @@ module.exports = ({ port, onKeyboardAction }) => {
|
||||||
app.use('/api', apiRouter);
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
apiRouter.post('/shortcuts/:action', express.json(), asyncHandler(async (req, res) => {
|
apiRouter.post('/shortcuts/:action', express.json(), asyncHandler(async (req, res) => {
|
||||||
await onKeyboardAction(req.params.action);
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
const action = req.params['action'];
|
||||||
|
assert(action != null);
|
||||||
|
await onKeyboardAction(action);
|
||||||
res.end();
|
res.end();
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const i18n = require('i18next');
|
import i18n from 'i18next';
|
||||||
const Backend = require('i18next-fs-backend');
|
import Backend from 'i18next-fs-backend';
|
||||||
|
|
||||||
const { commonI18nOptions, loadPath, addPath } = require('./i18n-common');
|
import { commonI18nOptions, loadPath, addPath } from './i18nCommon.js';
|
||||||
|
|
||||||
// See also renderer
|
// See also renderer
|
||||||
|
|
||||||
// https://github.com/i18next/i18next/issues/869
|
// https://github.com/i18next/i18next/issues/869
|
||||||
i18n
|
export default i18n
|
||||||
.use(Backend)
|
.use(Backend)
|
||||||
// detect user language
|
// detect user language
|
||||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
||||||
|
@ -23,5 +23,3 @@ i18n
|
||||||
addPath,
|
addPath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = i18n;
|
|
|
@ -1,28 +1,26 @@
|
||||||
// eslint-disable-line unicorn/filename-case
|
|
||||||
// intentionally disabled because I don't know the quality of the languages, so better to default to english
|
// intentionally disabled because I don't know the quality of the languages, so better to default to english
|
||||||
// const LanguageDetector = window.require('i18next-electron-language-detector');
|
// const LanguageDetector = window.require('i18next-electron-language-detector');
|
||||||
const isDev = require('electron-is-dev');
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { app } = require('electron');
|
import { app } from 'electron';
|
||||||
const { join } = require('path');
|
import { join } from 'path';
|
||||||
|
import { InitOptions } from 'i18next';
|
||||||
|
|
||||||
const { frontendBuildDir } = require('./util');
|
|
||||||
|
|
||||||
let customLocalesPath;
|
let customLocalesPath: string | undefined;
|
||||||
function setCustomLocalesPath(p) {
|
export function setCustomLocalesPath(p: string) {
|
||||||
customLocalesPath = p;
|
customLocalesPath = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLangPath(subPath) {
|
function getLangPath(subPath: string) {
|
||||||
if (customLocalesPath != null) return join(customLocalesPath, subPath);
|
if (customLocalesPath != null) return join(customLocalesPath, subPath);
|
||||||
if (isDev) return join('public', subPath);
|
if (app.isPackaged) return join(process.resourcesPath, 'locales', subPath);
|
||||||
return join(app.getAppPath(), frontendBuildDir, subPath);
|
return join('locales', subPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weblate hardcodes different lang codes than electron
|
// Weblate hardcodes different lang codes than electron
|
||||||
// https://www.electronjs.org/docs/api/app#appgetlocale
|
// https://www.electronjs.org/docs/api/app#appgetlocale
|
||||||
// https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc
|
// https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc
|
||||||
const mapLang = (lng) => ({
|
const mapLang = (lng: string) => ({
|
||||||
nb: 'nb_NO',
|
nb: 'nb_NO',
|
||||||
no: 'nb_NO',
|
no: 'nb_NO',
|
||||||
zh: 'zh_Hans',
|
zh: 'zh_Hans',
|
||||||
|
@ -39,29 +37,21 @@ const mapLang = (lng) => ({
|
||||||
'ru-RU': 'ru',
|
'ru-RU': 'ru',
|
||||||
}[lng] || lng);
|
}[lng] || lng);
|
||||||
|
|
||||||
const fallbackLng = 'en';
|
export const fallbackLng = 'en';
|
||||||
|
|
||||||
const commonI18nOptions = {
|
export const commonI18nOptions: InitOptions = {
|
||||||
fallbackLng,
|
fallbackLng,
|
||||||
// debug: isDev,
|
// debug: isDev,
|
||||||
// saveMissing: isDev,
|
// saveMissing: isDev,
|
||||||
// updateMissing: isDev,
|
// updateMissing: isDev,
|
||||||
// saveMissingTo: 'all',
|
// saveMissingTo: 'all',
|
||||||
|
|
||||||
// Keep in sync between i18next-parser.config.js and i18n-common.js:
|
// Keep in sync between i18next-parser.config.js and i18nCommon.js:
|
||||||
// TODO improve keys?
|
// TODO improve keys?
|
||||||
// Maybe do something like this: https://stackoverflow.com/a/19405314/6519037
|
// Maybe do something like this: https://stackoverflow.com/a/19405314/6519037
|
||||||
keySeparator: false,
|
keySeparator: false,
|
||||||
nsSeparator: false,
|
nsSeparator: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadPath = (lng, ns) => getLangPath(`locales/${mapLang(lng)}/${ns}.json`);
|
export const loadPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.json`);
|
||||||
const addPath = (lng, ns) => getLangPath(`locales/${mapLang(lng)}/${ns}.missing.json`);
|
export const addPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.missing.json`);
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fallbackLng,
|
|
||||||
loadPath,
|
|
||||||
addPath,
|
|
||||||
commonI18nOptions,
|
|
||||||
setCustomLocalesPath,
|
|
||||||
};
|
|
|
@ -1,31 +1,55 @@
|
||||||
|
/// <reference types="electron-vite/node" />
|
||||||
process.traceDeprecation = true;
|
process.traceDeprecation = true;
|
||||||
process.traceProcessWarnings = true;
|
process.traceProcessWarnings = true;
|
||||||
|
|
||||||
|
/* eslint-disable import/first */
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const electron = require('electron');
|
import electron, { AboutPanelOptionsOptions, BrowserWindow, BrowserWindowConstructorOptions, nativeTheme, shell, app, ipcMain } from 'electron';
|
||||||
const isDev = require('electron-is-dev');
|
import unhandled from 'electron-unhandled';
|
||||||
const unhandled = require('electron-unhandled');
|
import i18n from 'i18next';
|
||||||
const i18n = require('i18next');
|
import debounce from 'lodash/debounce';
|
||||||
const debounce = require('lodash/debounce');
|
import yargsParser from 'yargs-parser';
|
||||||
const yargsParser = require('yargs-parser');
|
import JSON5 from 'json5';
|
||||||
const JSON5 = require('json5');
|
import remote from '@electron/remote/main';
|
||||||
const remote = require('@electron/remote/main');
|
import { stat } from 'fs/promises';
|
||||||
const { stat } = require('fs/promises');
|
import assert from 'assert';
|
||||||
|
|
||||||
const logger = require('./logger');
|
import logger from './logger.js';
|
||||||
const menu = require('./menu');
|
import menu from './menu.js';
|
||||||
const configStore = require('./configStore');
|
import * as configStore from './configStore.js';
|
||||||
const { frontendBuildDir, isLinux } = require('./util');
|
import { isLinux } from './util.js';
|
||||||
const attachContextMenu = require('./contextMenu');
|
import attachContextMenu from './contextMenu.js';
|
||||||
const HttpServer = require('./httpServer');
|
import HttpServer from './httpServer.js';
|
||||||
|
import isDev from './isDev.js';
|
||||||
|
|
||||||
const { checkNewVersion } = require('./update-checker');
|
import { checkNewVersion } from './updateChecker.js';
|
||||||
|
|
||||||
const i18nCommon = require('./i18n-common');
|
import * as i18nCommon from './i18nCommon.js';
|
||||||
|
|
||||||
require('./i18n');
|
import './i18n.js';
|
||||||
|
import { ApiKeyboardActionRequest } from '../../types.js';
|
||||||
|
|
||||||
const { app, ipcMain, shell, BrowserWindow, nativeTheme } = electron;
|
export * as ffmpeg from './ffmpeg.js';
|
||||||
|
|
||||||
|
export * as i18n from './i18nCommon.js';
|
||||||
|
|
||||||
|
export * as compatPlayer from './compatPlayer.js';
|
||||||
|
|
||||||
|
export * as configStore from './configStore.js';
|
||||||
|
|
||||||
|
export { isLinux, isWindows, isMac, platform } from './util.js';
|
||||||
|
|
||||||
|
|
||||||
|
// https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz
|
||||||
|
// todo This should not be necessary anymore since v23.0.0
|
||||||
|
declare module 'i18next' {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
returnNull: false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-export-from
|
||||||
|
export { isDev };
|
||||||
|
|
||||||
// https://chromestatus.com/feature/5748496434987008
|
// https://chromestatus.com/feature/5748496434987008
|
||||||
// https://peter.sh/experiments/chromium-command-line-switches/
|
// https://peter.sh/experiments/chromium-command-line-switches/
|
||||||
|
@ -49,8 +73,7 @@ const isStoreBuild = process.windowsStore || process.mas;
|
||||||
|
|
||||||
const showVersion = !isStoreBuild;
|
const showVersion = !isStoreBuild;
|
||||||
|
|
||||||
/** @type import('electron').AboutPanelOptionsOptions */
|
const aboutPanelOptions: AboutPanelOptionsOptions = {
|
||||||
const aboutPanelOptions = {
|
|
||||||
applicationName: appName,
|
applicationName: appName,
|
||||||
copyright: `Copyright © ${copyrightYear} Mikael Finstad ❤️ 🇳🇴`,
|
copyright: `Copyright © ${copyrightYear} Mikael Finstad ❤️ 🇳🇴`,
|
||||||
version: '', // not very useful (MacOS only, and same as applicationVersion)
|
version: '', // not very useful (MacOS only, and same as applicationVersion)
|
||||||
|
@ -69,28 +92,28 @@ if (!showVersion) {
|
||||||
// https://www.electronjs.org/docs/latest/api/app#appsetaboutpaneloptionsoptions
|
// https://www.electronjs.org/docs/latest/api/app#appsetaboutpaneloptionsoptions
|
||||||
app.setAboutPanelOptions(aboutPanelOptions);
|
app.setAboutPanelOptions(aboutPanelOptions);
|
||||||
|
|
||||||
let filesToOpen = [];
|
let filesToOpen: string[] = [];
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
let mainWindow;
|
let mainWindow: BrowserWindow | null;
|
||||||
|
|
||||||
let askBeforeClose = false;
|
let askBeforeClose = false;
|
||||||
let rendererReady = false;
|
let rendererReady = false;
|
||||||
let newVersion;
|
let newVersion: string | undefined;
|
||||||
let disableNetworking;
|
let disableNetworking: boolean;
|
||||||
|
|
||||||
const openFiles = (paths) => mainWindow.webContents.send('openFiles', paths);
|
const openFiles = (paths: string[]) => mainWindow!.webContents.send('openFiles', paths);
|
||||||
|
|
||||||
let apiKeyboardActionRequestsId = 0;
|
let apiKeyboardActionRequestsId = 0;
|
||||||
const apiKeyboardActionRequests = new Map();
|
const apiKeyboardActionRequests = new Map<number, () => void>();
|
||||||
|
|
||||||
async function sendApiKeyboardAction(action) {
|
async function sendApiKeyboardAction(action: string) {
|
||||||
try {
|
try {
|
||||||
const id = apiKeyboardActionRequestsId;
|
const id = apiKeyboardActionRequestsId;
|
||||||
apiKeyboardActionRequestsId += 1;
|
apiKeyboardActionRequestsId += 1;
|
||||||
mainWindow.webContents.send('apiKeyboardAction', { id, action });
|
mainWindow!.webContents.send('apiKeyboardAction', { id, action } satisfies ApiKeyboardActionRequest);
|
||||||
await new Promise((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
apiKeyboardActionRequests.set(id, resolve);
|
apiKeyboardActionRequests.set(id, resolve);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -101,7 +124,7 @@ async function sendApiKeyboardAction(action) {
|
||||||
// https://github.com/electron/electron/issues/526#issuecomment-563010533
|
// https://github.com/electron/electron/issues/526#issuecomment-563010533
|
||||||
function getSizeOptions() {
|
function getSizeOptions() {
|
||||||
const bounds = configStore.get('windowBounds');
|
const bounds = configStore.get('windowBounds');
|
||||||
const options = {};
|
const options: BrowserWindowConstructorOptions = {};
|
||||||
if (bounds) {
|
if (bounds) {
|
||||||
const area = electron.screen.getDisplayMatching(bounds).workArea;
|
const area = electron.screen.getDisplayMatching(bounds).workArea;
|
||||||
// If the saved position still valid (the window is entirely inside the display area), use it.
|
// If the saved position still valid (the window is entirely inside the display area), use it.
|
||||||
|
@ -147,7 +170,7 @@ function createWindow() {
|
||||||
|
|
||||||
if (isDev) mainWindow.loadURL('http://localhost:3001');
|
if (isDev) mainWindow.loadURL('http://localhost:3001');
|
||||||
// Need to useloadFile for special characters https://github.com/mifi/lossless-cut/issues/40
|
// Need to useloadFile for special characters https://github.com/mifi/lossless-cut/issues/40
|
||||||
else mainWindow.loadFile(`${frontendBuildDir}/index.html`);
|
else mainWindow.loadFile('out/renderer/index.html');
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
// mainWindow.webContents.openDevTools()
|
// mainWindow.webContents.openDevTools()
|
||||||
|
@ -164,6 +187,7 @@ function createWindow() {
|
||||||
mainWindow.on('close', (e) => {
|
mainWindow.on('close', (e) => {
|
||||||
if (!askBeforeClose) return;
|
if (!askBeforeClose) return;
|
||||||
|
|
||||||
|
assert(mainWindow);
|
||||||
const choice = electron.dialog.showMessageBoxSync(mainWindow, {
|
const choice = electron.dialog.showMessageBoxSync(mainWindow, {
|
||||||
type: 'question',
|
type: 'question',
|
||||||
buttons: ['Yes', 'No'],
|
buttons: ['Yes', 'No'],
|
||||||
|
@ -186,10 +210,11 @@ function createWindow() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMenu() {
|
function updateMenu() {
|
||||||
|
assert(mainWindow);
|
||||||
menu({ app, mainWindow, newVersion, isStoreBuild });
|
menu({ app, mainWindow, newVersion, isStoreBuild });
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFilesEventually(paths) {
|
function openFilesEventually(paths: string[]) {
|
||||||
if (rendererReady) openFiles(paths);
|
if (rendererReady) openFiles(paths);
|
||||||
else filesToOpen = paths;
|
else filesToOpen = paths;
|
||||||
}
|
}
|
||||||
|
@ -201,18 +226,18 @@ function openFilesEventually(paths) {
|
||||||
function parseCliArgs(rawArgv = process.argv) {
|
function parseCliArgs(rawArgv = process.argv) {
|
||||||
const ignoreFirstArgs = process.defaultApp ? 2 : 1;
|
const ignoreFirstArgs = process.defaultApp ? 2 : 1;
|
||||||
// production: First arg is the LosslessCut executable
|
// production: First arg is the LosslessCut executable
|
||||||
// dev: First 2 args are electron and the electron.js
|
// dev: First 2 args are electron and the index.js
|
||||||
const argsWithoutAppName = rawArgv.length > ignoreFirstArgs ? rawArgv.slice(ignoreFirstArgs) : [];
|
const argsWithoutAppName = rawArgv.length > ignoreFirstArgs ? rawArgv.slice(ignoreFirstArgs) : [];
|
||||||
|
|
||||||
return yargsParser(argsWithoutAppName, { boolean: ['allow-multiple-instances', 'disable-networking'] });
|
return yargsParser(argsWithoutAppName, { boolean: ['allow-multiple-instances', 'disable-networking'], string: ['settings-json'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const argv = parseCliArgs();
|
const argv = parseCliArgs();
|
||||||
|
|
||||||
if (argv.localesPath != null) i18nCommon.setCustomLocalesPath(argv.localesPath);
|
if (argv['localesPath'] != null) i18nCommon.setCustomLocalesPath(argv['localesPath']);
|
||||||
|
|
||||||
|
|
||||||
function safeRequestSingleInstanceLock(additionalData) {
|
function safeRequestSingleInstanceLock(additionalData: Record<string, unknown>) {
|
||||||
if (process.mas) return true; // todo remove when fixed https://github.com/electron/electron/issues/35540
|
if (process.mas) return true; // todo remove when fixed https://github.com/electron/electron/issues/35540
|
||||||
|
|
||||||
// using additionalData because the built in "argv" passing is a bit broken:
|
// using additionalData because the built in "argv" passing is a bit broken:
|
||||||
|
@ -232,16 +257,14 @@ function initApp() {
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error todo
|
if (!(additionalData != null && typeof additionalData === 'object' && 'argv' in additionalData) || !Array.isArray(additionalData.argv)) return;
|
||||||
if (!Array.isArray(additionalData?.argv)) return;
|
|
||||||
|
|
||||||
// @ts-expect-error todo
|
|
||||||
const argv2 = parseCliArgs(additionalData.argv);
|
const argv2 = parseCliArgs(additionalData.argv);
|
||||||
|
|
||||||
logger.info('second-instance', argv2);
|
logger.info('second-instance', argv2);
|
||||||
|
|
||||||
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._);
|
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._.map(String));
|
||||||
else if (argv2.keyboardAction) sendApiKeyboardAction(argv2.keyboardAction);
|
else if (argv2['keyboardAction']) sendApiKeyboardAction(argv2['keyboardAction']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quit when all windows are closed.
|
// Quit when all windows are closed.
|
||||||
|
@ -322,7 +345,7 @@ const readyPromise = app.whenReady();
|
||||||
|
|
||||||
logger.info('CLI arguments', argv);
|
logger.info('CLI arguments', argv);
|
||||||
// Only if no files to open already (open-file might have already added some files)
|
// Only if no files to open already (open-file might have already added some files)
|
||||||
if (filesToOpen.length === 0) filesToOpen = argv._;
|
if (filesToOpen.length === 0) filesToOpen = argv._.map(String);
|
||||||
const { settingsJson } = argv;
|
const { settingsJson } = argv;
|
||||||
|
|
||||||
({ disableNetworking } = argv);
|
({ disableNetworking } = argv);
|
||||||
|
@ -330,6 +353,7 @@ const readyPromise = app.whenReady();
|
||||||
if (settingsJson != null) {
|
if (settingsJson != null) {
|
||||||
logger.info('initializing settings', settingsJson);
|
logger.info('initializing settings', settingsJson);
|
||||||
Object.entries(JSON5.parse(settingsJson)).forEach(([key, value]) => {
|
Object.entries(JSON5.parse(settingsJson)).forEach(([key, value]) => {
|
||||||
|
// @ts-expect-error todo use zod?
|
||||||
configStore.set(key, value);
|
configStore.set(key, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -340,6 +364,7 @@ const readyPromise = app.whenReady();
|
||||||
const port = typeof httpApi === 'number' ? httpApi : 8080;
|
const port = typeof httpApi === 'number' ? httpApi : 8080;
|
||||||
const { startHttpServer } = HttpServer({ port, onKeyboardAction: sendApiKeyboardAction });
|
const { startHttpServer } = HttpServer({ port, onKeyboardAction: sendApiKeyboardAction });
|
||||||
await startHttpServer();
|
await startHttpServer();
|
||||||
|
logger.info('HTTP API listening on port', port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -347,8 +372,8 @@ const readyPromise = app.whenReady();
|
||||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies
|
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies
|
||||||
|
|
||||||
installExtension(REACT_DEVELOPER_TOOLS)
|
installExtension(REACT_DEVELOPER_TOOLS)
|
||||||
.then((name) => logger.info('Added Extension', name))
|
.then((name: string) => logger.info('Added Extension', name))
|
||||||
.catch((err) => logger.error('Failed to add extension', err));
|
.catch((err: unknown) => logger.error('Failed to add extension', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
@ -366,7 +391,7 @@ const readyPromise = app.whenReady();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function focusWindow() {
|
export function focusWindow() {
|
||||||
try {
|
try {
|
||||||
app.focus({ steal: true });
|
app.focus({ steal: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -374,10 +399,8 @@ function focusWindow() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function quitApp() {
|
export function quitApp() {
|
||||||
electron.app.quit();
|
electron.app.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasDisabledNetworking = () => !!disableNetworking;
|
export const hasDisabledNetworking = () => !!disableNetworking;
|
||||||
|
|
||||||
module.exports = { focusWindow, isDev, hasDisabledNetworking, quitApp };
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
const isDev = import.meta.env.MODE === 'development';
|
||||||
|
export default isDev;
|
|
@ -1,15 +1,18 @@
|
||||||
const winston = require('winston');
|
import winston from 'winston';
|
||||||
const util = require('util');
|
import util from 'util';
|
||||||
const isDev = require('electron-is-dev');
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { app } = require('electron');
|
import { app } from 'electron';
|
||||||
const { join } = require('path');
|
import { join } from 'path';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import type { TransformableInfo } from 'logform';
|
||||||
|
|
||||||
|
|
||||||
// https://mifi.no/blog/winston-electron-logger/
|
// https://mifi.no/blog/winston-electron-logger/
|
||||||
|
|
||||||
// https://github.com/winstonjs/winston/issues/1427
|
// https://github.com/winstonjs/winston/issues/1427
|
||||||
const combineMessageAndSplat = () => ({
|
const combineMessageAndSplat = () => ({
|
||||||
transform(info) {
|
transform(info: TransformableInfo) {
|
||||||
|
// @ts-expect-error todo
|
||||||
const { [Symbol.for('splat')]: args = [], message } = info;
|
const { [Symbol.for('splat')]: args = [], message } = info;
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
info.message = util.format(message, ...args);
|
info.message = util.format(message, ...args);
|
||||||
|
@ -21,14 +24,14 @@ const createLogger = () => winston.createLogger({
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.timestamp(),
|
winston.format.timestamp(),
|
||||||
combineMessageAndSplat(),
|
combineMessageAndSplat(),
|
||||||
winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),
|
winston.format.printf((info) => `${info['timestamp']} ${info.level}: ${info.message}`),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const logDirPath = isDev ? '.' : app.getPath('userData');
|
const logDirPath = app.isPackaged ? app.getPath('userData') : '.';
|
||||||
|
|
||||||
const logger = createLogger();
|
const logger = createLogger();
|
||||||
logger.add(new winston.transports.File({ level: 'debug', filename: join(logDirPath, 'app.log'), options: { flags: 'a' } }));
|
logger.add(new winston.transports.File({ level: 'debug', filename: join(logDirPath, 'app.log'), options: { flags: 'a' } }));
|
||||||
if (isDev) logger.add(new winston.transports.Console());
|
if (!app.isPackaged) logger.add(new winston.transports.Console());
|
||||||
|
|
||||||
module.exports = logger;
|
export default logger;
|
|
@ -1,16 +1,19 @@
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const electron = require('electron');
|
import electron, { BrowserWindow } from 'electron';
|
||||||
const { t } = require('i18next');
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
import { homepage, getReleaseUrl, licensesPage } from './constants.js';
|
||||||
|
|
||||||
|
|
||||||
// menu-safe i18n.t:
|
// menu-safe i18n.t:
|
||||||
// https://github.com/mifi/lossless-cut/issues/1456
|
// https://github.com/mifi/lossless-cut/issues/1456
|
||||||
const esc = (val) => val.replaceAll('&', '&&');
|
const esc = (val: string) => val.replaceAll('&', '&&');
|
||||||
|
|
||||||
const { Menu } = electron;
|
const { Menu } = electron;
|
||||||
|
|
||||||
const { homepage, getReleaseUrl, licensesPage } = require('./constants');
|
export default ({ app, mainWindow, newVersion, isStoreBuild }: {
|
||||||
|
app: Electron.App, mainWindow: BrowserWindow, newVersion?: string | undefined, isStoreBuild: boolean,
|
||||||
module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
|
}) => {
|
||||||
const menu = [
|
const menu = [
|
||||||
...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []),
|
...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []),
|
||||||
|
|
|
@ -1,24 +1,33 @@
|
||||||
// eslint-disable-line unicorn/filename-case
|
|
||||||
const GitHub = require('github-api');
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const electron = require('electron');
|
import electron from 'electron';
|
||||||
const semver = require('semver');
|
import semver from 'semver';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { Octokit } from '@octokit/core';
|
||||||
|
|
||||||
const logger = require('./logger');
|
import logger from './logger.js';
|
||||||
|
|
||||||
|
|
||||||
const { app } = electron;
|
const { app } = electron;
|
||||||
|
|
||||||
const gh = new GitHub();
|
const octokit = new Octokit();
|
||||||
const repo = gh.getRepo('mifi', 'lossless-cut');
|
|
||||||
|
|
||||||
async function checkNewVersion() {
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export async function checkNewVersion() {
|
||||||
try {
|
try {
|
||||||
// From API: https://developer.github.com/v3/repos/releases/#get-the-latest-release
|
// From API: https://developer.github.com/v3/repos/releases/#get-the-latest-release
|
||||||
// View the latest published full release for the repository.
|
// View the latest published full release for the repository.
|
||||||
// Draft releases and prereleases are not returned by this endpoint.
|
// Draft releases and prereleases are not returned by this endpoint.
|
||||||
const res = (await repo.getRelease('latest')).data;
|
|
||||||
const newestVersion = res.tag_name.replace(/^v?/, '');
|
const { data } = await octokit.request('GET /repos/{owner}/{repo}/releases/latest', {
|
||||||
|
owner: 'mifi',
|
||||||
|
repo: 'lossless-cut',
|
||||||
|
headers: {
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newestVersion = data.tag_name.replace(/^v?/, '');
|
||||||
|
|
||||||
const currentVersion = app.getVersion();
|
const currentVersion = app.getVersion();
|
||||||
// const currentVersion = '3.17.2';
|
// const currentVersion = '3.17.2';
|
||||||
|
@ -34,5 +43,3 @@ async function checkNewVersion() {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { checkNewVersion };
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
export const platform = os.platform();
|
||||||
|
export const arch = os.arch();
|
||||||
|
|
||||||
|
export const isWindows = platform === 'win32';
|
||||||
|
export const isMac = platform === 'darwin';
|
||||||
|
export const isLinux = platform === 'linux';
|
|
@ -0,0 +1,2 @@
|
||||||
|
// todo
|
||||||
|
console.log('preload');
|
|
@ -86,9 +86,9 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
|
||||||
import BigWaveform from './components/BigWaveform';
|
import BigWaveform from './components/BigWaveform';
|
||||||
|
|
||||||
import isDev from './isDev';
|
import isDev from './isDev';
|
||||||
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, Html5ifyMode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
|
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
|
||||||
import { CaptureFormat, KeyboardAction } from '../types';
|
import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types';
|
||||||
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../ffprobe';
|
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
|
||||||
|
|
||||||
const electron = window.require('electron');
|
const electron = window.require('electron');
|
||||||
const { exists } = window.require('fs-extra');
|
const { exists } = window.require('fs-extra');
|
||||||
|
@ -96,8 +96,7 @@ const { lstat } = window.require('fs/promises');
|
||||||
const filePathToUrl = window.require('file-url');
|
const filePathToUrl = window.require('file-url');
|
||||||
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
|
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
|
||||||
|
|
||||||
const remote = window.require('@electron/remote');
|
const { focusWindow, hasDisabledNetworking, quitApp } = window.require('@electron/remote').require('./index.js');
|
||||||
const { focusWindow, hasDisabledNetworking, quitApp } = remote.require('./electron');
|
|
||||||
|
|
||||||
|
|
||||||
const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' };
|
const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' };
|
||||||
|
@ -1338,8 +1337,10 @@ function App() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('stdout' in err) console.error('stdout:', err.stdout);
|
// @ts-expect-error todo
|
||||||
if ('stderr' in err) console.error('stderr:', err.stderr);
|
if ('stdout' in err && err.stdout != null) console.error('stdout:', err.stdout.toString('utf8'));
|
||||||
|
// @ts-expect-error todo
|
||||||
|
if ('stderr' in err && err.stderr != null) console.error('stderr:', err.stderr.toString('utf8'));
|
||||||
|
|
||||||
if (isExecaFailure(err)) {
|
if (isExecaFailure(err)) {
|
||||||
if (isOutOfSpaceError(err)) {
|
if (isOutOfSpaceError(err)) {
|
|
@ -4,10 +4,9 @@ import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
import isDev from './isDev';
|
import isDev from './isDev';
|
||||||
import { ChromiumHTMLVideoElement } from './types';
|
import { ChromiumHTMLVideoElement } from './types';
|
||||||
import { FFprobeStream } from '../ffprobe';
|
import { FFprobeStream } from '../../../ffprobe';
|
||||||
|
|
||||||
const remote = window.require('@electron/remote');
|
const { compatPlayer: { createMediaSourceStream, readOneJpegFrame } } = window.require('@electron/remote').require('./index.js');
|
||||||
const { createMediaSourceStream, readOneJpegFrame } = remote.require('./compatPlayer');
|
|
||||||
|
|
||||||
|
|
||||||
async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex, seekTo, signal, playSafe, onCanPlay, getTargetTime, size, fps }: {
|
async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex, seekTo, signal, playSafe, onCanPlay, getTargetTime, size, fps }: {
|
||||||
|
@ -231,7 +230,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex,
|
||||||
processChunk();
|
processChunk();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
|
function drawJpegFrame(canvas: HTMLCanvasElement | null, jpegImage: Buffer) {
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
@ -248,7 +247,9 @@ function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
|
||||||
img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`;
|
img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPauseImage({ path, seekTo, videoStreamIndex, canvas, signal }) {
|
async function createPauseImage({ path, seekTo, videoStreamIndex, canvas, signal }: {
|
||||||
|
path: string, seekTo: number, videoStreamIndex: number, canvas: HTMLCanvasElement | null, signal: AbortSignal,
|
||||||
|
}) {
|
||||||
const { promise, abort } = readOneJpegFrame({ path, seekTo, videoStreamIndex });
|
const { promise, abort } = readOneJpegFrame({ path, seekTo, videoStreamIndex });
|
||||||
signal.addEventListener('abort', () => abort());
|
signal.addEventListener('abort', () => abort());
|
||||||
const jpegImage = await promise;
|
const jpegImage = await promise;
|
|
@ -14,7 +14,7 @@ import OutputFormatSelect from './OutputFormatSelect';
|
||||||
import useUserSettings from '../hooks/useUserSettings';
|
import useUserSettings from '../hooks/useUserSettings';
|
||||||
import { isMov } from '../util/streams';
|
import { isMov } from '../util/streams';
|
||||||
import { getOutFileExtension, getSuffixedFileName } from '../util';
|
import { getOutFileExtension, getSuffixedFileName } from '../util';
|
||||||
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../ffprobe';
|
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
|
||||||
|
|
||||||
const { basename } = window.require('path');
|
const { basename } = window.require('path');
|
||||||
|
|
|
@ -26,8 +26,8 @@ import useUserSettings from '../hooks/useUserSettings';
|
||||||
import styles from './ExportConfirm.module.css';
|
import styles from './ExportConfirm.module.css';
|
||||||
import { InverseCutSegment, SegmentToExport } from '../types';
|
import { InverseCutSegment, SegmentToExport } from '../types';
|
||||||
import { GenerateOutSegFileNames } from '../util/outputNameTemplate';
|
import { GenerateOutSegFileNames } from '../util/outputNameTemplate';
|
||||||
import { FFprobeStream } from '../../ffprobe';
|
import { FFprobeStream } from '../../../../ffprobe';
|
||||||
import { AvoidNegativeTs } from '../../types';
|
import { AvoidNegativeTs } from '../../../../types';
|
||||||
|
|
||||||
|
|
||||||
const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };
|
const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };
|
|
@ -12,7 +12,7 @@ import Swal from '../swal';
|
||||||
import SetCutpointButton from './SetCutpointButton';
|
import SetCutpointButton from './SetCutpointButton';
|
||||||
import SegmentCutpointButton from './SegmentCutpointButton';
|
import SegmentCutpointButton from './SegmentCutpointButton';
|
||||||
import { getModifier } from '../hooks/useTimelineScroll';
|
import { getModifier } from '../hooks/useTimelineScroll';
|
||||||
import { KeyBinding, KeyboardAction } from '../../types';
|
import { KeyBinding, KeyboardAction } from '../../../../types';
|
||||||
import { StateSegment } from '../types';
|
import { StateSegment } from '../types';
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue