upgrade to electron-vite

pull/1939/head
Mikael Finstad 2024-03-21 23:28:25 +08:00
rodzic 8a7c1f8a17
commit f677619039
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
195 zmienionych plików z 1399 dodań i 996 usunięć

Wyświetl plik

@ -1,3 +1,3 @@
/dist
/vite-dist
/out
/ts-dist

Wyświetl plik

@ -12,7 +12,7 @@ module.exports = {
overrides: [
{
files: ['./src/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
files: ['./src/renderer/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
env: {
node: false,
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: {
'import/no-extraneous-dependencies': ['error', {
devDependencies: true,

Wyświetl plik

@ -98,7 +98,7 @@ jobs:
- name: (MacOS) Upload to Mac App Store
if: startsWith(matrix.os, 'macos') && env.is_tag == 'true'
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
uses: actions/upload-artifact@v3

2
.gitignore vendored
Wyświetl plik

@ -11,7 +11,7 @@ node_modules
!.yarn/versions
/dist
/vite-dist
/out
/icon-build
/build-resources
/doc

Wyświetl plik

@ -1,5 +1,5 @@
{
"search.exclude": {
"/public/locales/**": true,
"/src/main/locales/**": true,
}
}

Wyświetl plik

@ -1,6 +1,6 @@
<div align="center">
<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>
The swiss army knife of lossless video/audio editing
<br>

2
cli.md
Wyświetl plik

@ -26,7 +26,7 @@ LosslessCut file1.mp4 file2.mkv
```
## 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
LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}'
```

Wyświetl plik

@ -5,10 +5,6 @@
This app is built using Electron.
Make sure you have at least Node v16. The app uses ffmpeg from PATH when developing.
```bash
npm install -g yarn
```
```bash
git clone https://github.com/mifi/lossless-cut.git
cd lossless-cut

Wyświetl plik

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

Wyświetl plik

@ -1,8 +1,8 @@
// eslint-disable-line unicorn/filename-case
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,
sort: true,
@ -16,7 +16,7 @@ export default {
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,
namespaceSeparator: false,
};

Wyświetl plik

@ -4,31 +4,27 @@
"description": "The swiss army knife of lossless video/audio editing",
"copyright": "Copyright © 2021 ${author}",
"version": "3.60.0",
"main": "public/electron.js",
"main": "./out/main/index.js",
"homepage": "./",
"scripts": {
"dev": "concurrently -k \"npm run dev:frontend\" \"npm run dev:electron\"",
"dev:frontend": "cross-env vite --port 3001",
"dev:electron": "wait-on tcp:3001 && electron .",
"icon-gen": "mkdirp icon-build build-resources/appx && node script/icon-gen.mjs",
"clean": "rimraf dist out ts-dist build-resources icon-build",
"start": "electron-vite preview",
"dev": "electron-vite dev -w",
"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-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-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",
"test": "vitest",
"lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs .",
"pack-mac": "electron-builder --mac -m dmg",
"prepack-mac": "yarn build",
"pack-mas-dev": "electron-builder --mac -m mas-dev -c.mas.provisioningProfile=LosslessCut_Dev.provisionprofile -c.mas.identity='Apple Development: Mikael Finstad (JH4PH8B3C8)'",
"prepack-mas-dev": "yarn build",
"pack-win": "electron-builder --win",
"prepack-win": "yarn build",
"lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs,.mts .",
"pack-mac": "yarn build && electron-builder --mac -m dmg",
"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-win": "yarn build && electron-builder --win",
"postinstall": "electron-builder install-app-deps",
"version": "node script/postversion.mjs && git add no.mifi.losslesscut.appdata.xml",
"pack-linux": "electron-builder --linux",
"prepack-linux": "yarn build",
"version": "tsx script/postversion.mts && git add no.mifi.losslesscut.appdata.xml",
"pack-linux": "yarn build && electron-builder --linux",
"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"
},
@ -44,29 +40,36 @@
"license": "GPL-2.0-only",
"devDependencies": {
"@fontsource/open-sans": "^4.5.14",
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-switch": "^1.0.1",
"@tsconfig/node18": "^18.2.2",
"@tsconfig/strictest": "^2.0.2",
"@tsconfig/vite-react": "^3.0.0",
"@types/color": "^3.0.6",
"@types/css-modules": "^1.0.5",
"@types/eslint": "^8",
"@types/express": "^4.17.21",
"@types/lodash": "^4.14.202",
"@types/luxon": "^3.4.2",
"@types/morgan": "^1.9.9",
"@types/node": "18",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/sortablejs": "^1.15.0",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^3.1.0",
"color": "^3.1.0",
"concurrently": "^6.0.0",
"cross-env": "^7.0.3",
"csv-parse": "^4.15.3",
"csv-stringify": "^5.6.2",
"data-uri-to-buffer": "^4.0.0",
"electron": "^27.0.0",
"electron-builder": "^24.6.4",
"electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.1.0",
"eslint": "^8.2.0",
"eslint-config-mifi": "^0.0.3",
"eslint-plugin-import": "^2.25.3",
@ -78,7 +81,7 @@
"fast-xml-parser": "^4.2.5",
"framer-motion": "^9.0.3",
"i18next-parser": "^7.6.0",
"icon-gen": "^3.0.0",
"icon-gen": "^4.0.0",
"immer": "^10.0.2",
"ky": "^0.33.1",
"luxon": "^3.3.0",
@ -97,6 +100,7 @@
"react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.4.3",
"react-use": "^17.4.0",
"rimraf": "^5.0.5",
"screenfull": "^6.0.2",
"scroll-into-view-if-needed": "^2.2.28",
"sharp": "^0.32.6",
@ -105,28 +109,25 @@
"sweetalert2": "^11.0.0",
"sweetalert2-react-content": "^5.0.7",
"tiny-invariant": "^1.3.3",
"tsx": "^4.7.1",
"typescript": "~5.2.0",
"use-debounce": "^5.1.0",
"use-trace-update": "^1.3.0",
"vite": "^4.5.2",
"vitest": "^1.2.2",
"wait-on": "^7.0.1"
"vitest": "^1.2.2"
},
"dependencies": {
"@electron/remote": "^2.0.10",
"@radix-ui/colors": "^0.1.8",
"@octokit/core": "5",
"cue-parser": "^0.3.0",
"data-uri-to-buffer": "^4.0.0",
"electron-is-dev": "^2.0.0",
"electron-store": "5.1.1",
"electron-unhandled": "^4.0.1",
"execa": "5",
"execa": "^8.0.1",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"file-type": "16",
"file-url": "^3.0.0",
"fs-extra": "^8.1.0",
"github-api": "^3.2.2",
"i18next": "^22.4.10",
"i18next-fs-backend": "^2.1.1",
"json5": "^2.2.2",
@ -134,25 +135,28 @@
"mime-types": "^2.1.14",
"morgan": "^1.10.0",
"semver": "^7.5.2",
"string-to-stream": "^1.1.1",
"string-to-stream": "^3.0.1",
"winston": "^3.8.1",
"yargs-parser": "^21.0.0"
"yargs-parser": "^21.1.1"
},
"build": {
"directories": {
"buildResources": "build-resources"
},
"extraMetadata": {
"main": "vite-dist/electron.js"
},
"files": [
"vite-dist/**/*"
"out/**/*"
],
"asar": {
"smartUnpack": false
},
"appId": "no.mifi.losslesscut",
"artifactName": "${productName}-${os}-${arch}.${ext}",
"extraResources": [
{
"from": "locales",
"to": "locales"
}
],
"mac": {
"hardenedRuntime": true,
"appId": "no.mifi.losslesscut-mac",

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,12 +1,11 @@
import { readFile, writeFile } from 'fs/promises';
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
// eslint-disable-next-line import/no-unresolved
import { DateTime } from 'luxon';
const xmlUrl = new URL('../no.mifi.losslesscut.appdata.xml', import.meta.url);
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 xml = parser.parse(xmlData);

Wyświetl plik

@ -1,5 +1,5 @@
// eslint-disable-line unicorn/filename-case
import execa from 'execa';
import { execa } from 'execa';
import { readFile } from 'fs/promises';
// 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
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);
@ -74,8 +74,11 @@ async function runAttempt() {
console.log('stdout', stdout);
return false;
} catch (err) {
if (err.exitCode === 1 && err.stdout) {
const errorJson = JSON.parse(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) 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'];
// Unable to authenticate
if (productErrors.some((error) => error.code === -19209)) {

Wyświetl plik

@ -1,3 +0,0 @@
const { isDev }: { isDev: boolean } = window.require('@electron/remote').require('./electron');
export default isDev;

Wyświetl plik

@ -1,8 +1,12 @@
const logger = require('./logger');
const { createMediaSourceProcess, readOneJpegFrame } = require('./ffmpeg');
import assert from 'assert';
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();
logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps });
@ -13,38 +17,39 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
process.kill('SIGKILL');
};
process.stdout.pause();
const { stdout } = process;
assert(stdout != null);
async function readChunk() {
return new Promise((resolve, reject) => {
let cleanup;
stdout.pause();
const onClose = () => {
cleanup();
resolve(null);
};
const onData = (chunk) => {
process.stdout.pause();
cleanup();
resolve(chunk);
};
const onError = (err) => {
cleanup();
reject(err);
};
cleanup = () => {
process.stdout.off('data', onData);
process.stdout.off('error', onError);
process.stdout.off('close', onClose);
};
const readChunk = async () => new Promise((resolve, reject) => {
let cleanup: () => void;
process.stdout.once('data', onData);
process.stdout.once('error', onError);
process.stdout.once('close', onClose);
const onClose = () => {
cleanup();
resolve(null);
};
const onData = (chunk: Buffer) => {
stdout.pause();
cleanup();
resolve(chunk);
};
const onError = (err: Error) => {
cleanup();
reject(err);
};
cleanup = () => {
stdout.off('data', onData);
stdout.off('error', onError);
stdout.off('close', onClose);
};
process.stdout.resume();
});
}
stdout.once('data', onData);
stdout.once('error', onError);
stdout.once('close', onClose);
stdout.resume();
});
function abort() {
abortController.abort();
@ -75,9 +80,9 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
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 process = readOneJpegFrame({ path, seekTo, videoStreamIndex });
const process = readOneJpegFrameRaw({ path, seekTo, videoStreamIndex });
// eslint-disable-next-line unicorn/prefer-add-event-listener
abortController.signal.onabort = () => process.kill('SIGKILL');
@ -99,9 +104,3 @@ function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
return { promise, abort };
}
module.exports = {
createMediaSourceStream,
readOneJpegFrame: readOneJpegFrameWrapper,
};

Wyświetl plik

@ -1,17 +1,17 @@
const Store = require('electron-store');
import Store from 'electron-store';
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');
const os = require('os');
const { join, dirname } = require('path');
const { pathExists } = require('fs-extra');
import electron from 'electron';
import { join, dirname } from 'path';
import { pathExists } from '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;
/** @type {import('../types').KeyBinding[]} */
const defaultKeyBindings = [
const defaultKeyBindings: KeyBinding[] = [
{ keys: 'plus', action: 'addSegment' },
{ keys: 'space', action: 'togglePlayResetSpeed' },
{ keys: 'k', action: 'togglePlayNoResetSpeed' },
@ -83,8 +83,7 @@ const defaultKeyBindings = [
{ keys: 'alt+down', action: 'decreaseVolume' },
];
/** @type {import('../types').Config} */
const defaults = {
const defaults: Config = {
captureFormat: 'jpeg',
customOutDir: undefined,
keyframeCut: true,
@ -145,7 +144,6 @@ const defaults = {
// For portable app: https://github.com/mifi/lossless-cut/issues/645
async function getCustomStoragePath() {
try {
const isWindows = os.platform() === 'win32';
if (!isWindows || process.windowsStore) return undefined;
// 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} */
function get(key) {
export function get<T extends keyof Config>(key: T): Config[T] {
return store.get(key);
}
/** @type {import('../types').StoreSetConfig} */
function set(key, val) {
export function set<T extends keyof Config>(key: T, val: Config[T]) {
if (val === undefined) store.delete(key);
else store.set(key, val);
}
/** @type {import('../types').StoreResetConfig} */
function reset(key) {
export function reset<T extends keyof Config>(key: T) {
set(key, defaults[key]);
}
async function tryCreateStore({ customStoragePath }) {
async function tryCreateStore({ customStoragePath }: { customStoragePath: string | undefined }) {
for (let i = 0; i < 5; i += 1) {
try {
store = new Store({ defaults, cwd: customStoragePath });
store = new Store({
defaults,
...(customStoragePath != null ? { cwd: customStoragePath } : {}),
});
return;
} catch (err) {
// 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');
}
async function init() {
export async function init() {
const customStoragePath = await getCustomStoragePath();
if (customStoragePath) logger.info('customStoragePath', customStoragePath);
@ -214,10 +212,3 @@ async function init() {
set('cleanupChoices', { ...cleanupChoices, closeFile: true });
}
}
module.exports = {
init,
get,
set,
reset,
};

Wyświetl plik

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

Wyświetl plik

@ -1,8 +1,8 @@
// 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
module.exports = (window) => {
export default (window: BrowserWindow) => {
const selectionMenu = Menu.buildFromTemplate([
{ role: 'copy' },
{ type: 'separator' },
@ -23,9 +23,9 @@ module.exports = (window) => {
window.webContents.on('context-menu', (_e, props) => {
const { selectionText, isEditable } = props;
if (isEditable) {
inputMenu.popup(window);
inputMenu.popup({ window });
} else if (selectionText && selectionText.trim() !== '') {
selectionMenu.popup(window);
selectionMenu.popup({ window });
}
});
};

Wyświetl plik

@ -1,53 +1,60 @@
const { join } = require('path');
const isDev = require('electron-is-dev');
const readline = require('readline');
const stringToStream = require('string-to-stream');
const execa = require('execa');
import { join } from 'path';
import readline from 'readline';
import stringToStream from 'string-to-stream';
import { BufferEncodingOption, execa, ExecaChildProcess, Options as ExecaOptions } from '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);
let customFfPath;
let customFfPath: string | undefined;
// Note that this does not work on MAS because of sandbox restrictions
function setCustomFfPath(path) {
export function setCustomFfPath(path: string | undefined) {
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(' ')}`;
}
function getFfPath(cmd) {
function getFfPath(cmd: string) {
const exeName = isWindows ? `${cmd}.exe` : cmd;
if (customFfPath) return join(customFfPath, exeName);
if (isDev) {
const components = ['ffmpeg', `${platform}-${arch}`];
if (isWindows || isLinux) components.push('lib');
components.push(exeName);
return join(...components);
if (app.isPackaged) {
return join(process.resourcesPath, exeName);
}
return join(process.resourcesPath, exeName);
// local dev
const components = ['ffmpeg', `${platform}-${arch}`];
if (isWindows || isLinux) components.push('lib');
components.push(exeName);
return join(...components);
}
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)');
runningFfmpegs.forEach((process) => {
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 (process.stderr == null) return;
onProgress(0);
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
if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
if (!match) {
// @ts-expect-error todo
customMatcher(line);
return;
}
const timeStr = match[1];
// 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}`);
const h = parseInt(match2[1], 10);
const m = parseInt(match2[2], 10);
const s = parseInt(match2[3], 10);
const cs = parseInt(match2[4], 10);
const h = parseInt(match2[1]!, 10);
const m = parseInt(match2[2]!, 10);
const s = parseInt(match2[3]!, 10);
const cs = parseInt(match2[4]!, 10);
const time = (((h * 60) + m) * 60 + s) + cs / 100;
// console.log(time);
@ -93,18 +99,22 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
});
}
// @ts-expect-error todo
function getExecaOptions({ env, ...customExecaOptions } = {}) {
const execaOptions = { ...customExecaOptions, env: { ...env } };
function getExecaOptions({ env, ...customExecaOptions }: Omit<ExecaOptions<BufferEncodingOption>, 'buffer'> = {}) {
const execaOptions: Omit<ExecaOptions<BufferEncodingOption>, 'buffer'> = { ...customExecaOptions, encoding: 'buffer' };
// 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;
}
// 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();
if (logCli) console.log(getFfCommandLine('ffmpeg', args));
if (additionalOptions?.logCli) console.log(getFfCommandLine('ffmpeg', args));
const process = execa(ffmpegPath, args, getExecaOptions(customExecaOptions));
@ -121,23 +131,29 @@ function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
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);
handleProgress(process, totalDuration, onProgress);
assert(process.stdin != null);
stringToStream(concatTxt).pipe(process.stdin);
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);
assert(process.stderr != null);
handleProgress(process, duration, onProgress);
return process;
}
async function runFfprobe(args, { timeout = isDev ? 10000 : 30000 } = {}) {
export async function runFfprobe(args: readonly string[], { timeout = isDev ? 10000 : 30000 } = {}) {
const ffprobePath = getFfprobePath();
console.log(getFfCommandLine('ffprobe', args));
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>} */
async function renderWaveformPng({ filePath, start, duration, color, streamIndex }) {
export async function renderWaveformPng({ filePath, start, duration, color, streamIndex }: {
filePath: string, start: number, duration: number, color: string, streamIndex: number,
}): Promise<Waveform> {
const args1 = [
'-hide_banner',
'-i', filePath,
'-ss', start,
'-t', duration,
'-ss', String(start),
'-t', String(duration),
'-c', 'copy',
'-vn',
'-map', `0:${streamIndex}`,
@ -179,16 +196,18 @@ async function renderWaveformPng({ filePath, start, duration, color, streamIndex
console.log(getFfCommandLine('ffmpeg1', args1));
console.log('|', getFfCommandLine('ffmpeg2', args2));
let ps1;
let ps2;
let ps1: ExecaChildProcess<Buffer> | undefined;
let ps2: ExecaChildProcess<Buffer> | undefined;
try {
ps1 = runFfmpegProcess(args1, { encoding: null, buffer: false }, { logCli: false });
ps2 = runFfmpegProcess(args2, { encoding: null }, { logCli: false });
ps1 = runFfmpegProcess(args1, { buffer: false }, { logCli: false });
ps2 = runFfmpegProcess(args2, undefined, { logCli: false });
assert(ps1.stdout != null);
assert(ps2.stdin != null);
ps1.stdout.pipe(ps2.stdin);
const timer = setTimeout(() => {
ps1.kill();
ps2.kill();
ps1?.kill();
ps2?.kill();
console.warn('ffmpeg timed out');
}, 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)] : []),
'-i', filePath,
...(to != null ? ['-t', (to - from).toFixed(5)] : []),
...(from != null && to != null ? ['-t', (to - from).toFixed(5)] : []),
];
function mapTimesToSegments(times) {
const segments = [];
export function mapTimesToSegments(times: number[]) {
const segments: { start: number, end: number | undefined }[] = [];
for (let i = 0; i < times.length; i += 1) {
const start = times[i];
const end = times[i + 1];
@ -225,32 +244,35 @@ function mapTimesToSegments(times) {
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);
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
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 = [
'-hide_banner',
...getInputSeekArgs({ filePath, from, to }),
'-filter_complex', `select='gt(scene,${minChange})',metadata=print:file=-`,
'-f', 'null', '-',
];
const process = runFfmpegProcess(args, { encoding: null, buffer: false });
const process = runFfmpegProcess(args, { buffer: false });
const times = [0];
handleProgress(process, to - from, onProgress);
assert(process.stdout != null);
const rl = readline.createInterface({ input: process.stdout });
rl.on('line', (line) => {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
if (!match) return;
const time = parseFloat(match[1]);
const time = parseFloat(match[1]!);
// @ts-expect-error todo
if (Number.isNaN(time) || time <= times.at(-1)) return;
times.push(time);
@ -263,89 +285,134 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
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 = [
'-hide_banner',
...getInputSeekArgs({ filePath, from, to }),
...customArgs,
'-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) {
const { start: startStr, end: endStr } = matchLineTokens(line);
const start = parseFloat(startStr);
const end = parseFloat(endStr);
if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) return;
segments.push({ start, end });
function customMatcher(line: string) {
const { start: startRaw, end: endRaw } = matchLineTokens(line);
if (typeof startRaw === 'number' && typeof endRaw === 'number') {
if (startRaw == null || endRaw == null) 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 });
} else {
throw new TypeError('Invalid line match');
}
}
// @ts-expect-error todo
handleProgress(process, to - from, onProgress, customMatcher);
await process;
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 }) {
function matchLineTokens(line) {
// 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\\.]+/);
if (!match) return {};
return {
start: parseFloat(match[1]),
end: parseFloat(match[2]),
};
}
export async function blackDetect({ filePath, filterOptions, onProgress, from, to }: { filePath: string, filterOptions: Record<string, string>, onProgress: (p: number) => void, from: number, to: number }) {
const customArgs = ['-vf', `blackdetect=${mapFilterOptions(filterOptions)}`, '-an'];
return detectIntervals({ filePath, onProgress, from, to, matchLineTokens, customArgs });
return detectIntervals({
filePath,
onProgress,
from,
to,
matchLineTokens: (line) => {
// 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\\.]+/);
if (!match) {
return {
start: undefined,
end: undefined,
};
}
return {
start: match[1],
end: match[2],
};
},
customArgs,
});
}
async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) {
function matchLineTokens(line) {
// 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\\.]+)/);
if (!match) return {};
const end = parseFloat(match[1]);
const silenceDuration = parseFloat(match[2]);
if (Number.isNaN(end) || Number.isNaN(silenceDuration)) return {};
const start = end - silenceDuration;
if (start < 0 || end <= 0 || start >= end) return {};
return {
start,
end,
};
}
const customArgs = ['-af', `silencedetect=${mapFilterOptions(filterOptions)}`, '-vn'];
return detectIntervals({ filePath, onProgress, from, to, matchLineTokens, customArgs });
export async function silenceDetect({ filePath, filterOptions, onProgress, from, to }: {
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
const match = line.match(/^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/);
if (!match) {
return {
start: undefined,
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;
if (start < 0 || end <= 0 || start >= end) {
return {
start: undefined,
end: undefined,
};
}
return {
start,
end,
};
},
customArgs: ['-af', `silencedetect=${mapFilterOptions(filterOptions)}`, '-vn'],
});
}
function getFffmpegJpegQuality(quality) {
function getFffmpegJpegQuality(quality: number) {
// Normal range for JPEG is 2-31 with 31 being the worst quality.
const qMin = 2;
const qMax = 31;
return Math.min(Math.max(qMin, quality, Math.round((1 - quality) * (qMax - qMin) + qMin)), qMax);
}
function getQualityOpts({ captureFormat, quality }) {
if (captureFormat === 'jpeg') return ['-q:v', getFffmpegJpegQuality(quality)];
if (captureFormat === 'webp') return ['-q:v', Math.max(0, Math.min(100, Math.round(quality * 100)))];
function getQualityOpts({ captureFormat, quality }: { captureFormat: CaptureFormat, quality: number }) {
if (captureFormat === 'jpeg') return ['-q:v', String(getFffmpegJpegQuality(quality))];
if (captureFormat === 'webp') return ['-q:v', String(Math.max(0, Math.min(100, Math.round(quality * 100))))];
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
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 = [
'-ss', from,
'-ss', String(from),
'-i', videoPath,
'-t', Math.max(0, to - from),
'-t', String(Math.max(0, to - from)),
...getQualityOpts({ captureFormat, quality }),
...(filter != null ? ['-vf', filter] : []),
// 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,
];
const process = runFfmpegProcess(args, { encoding: null, buffer: false });
const process = runFfmpegProcess(args, { buffer: false });
handleProgress(process, to - from, onProgress);
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);
await runFfmpegProcess([
'-ss', timestamp,
'-ss', String(timestamp),
'-i', videoPath,
'-vframes', '1',
'-q:v', ffmpegQuality,
'-q:v', String(ffmpegQuality),
'-y', outPath,
]);
}
async function readFormatData(filePath) {
async function readFormatData(filePath: string) {
console.log('readFormatData', filePath);
const { stdout } = await runFfprobe([
'-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);
}
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;
if (hasAudio) {
if (speed === 'slowest') audio = 'hq';
@ -492,14 +563,14 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
if (duration) handleProgress(process, duration, onProgress);
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 = [
'-hide_banner', '-loglevel', 'error',
'-ss', seekTo,
'-ss', String(seekTo),
'-noautorotate',
@ -516,17 +587,19 @@ function readOneJpegFrame({ path, seekTo, videoStreamIndex }) {
// console.log(args);
return runFfmpegProcess(args, { encoding: 'buffer' }, { logCli: true });
return runFfmpegProcess(args, undefined, { logCli: true });
}
const enableLog = false;
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() {
if (videoStreamIndex == null) return [];
const filters = [];
const filters: string[] = [];
if (fps != null) filters.push(`fps=${fps}`);
if (size != null) filters.push(`scale=${size}:${size}:flags=lanczos:force_original_aspect_ratio=decrease`);
if (filters.length === 0) return [];
@ -547,7 +620,7 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
'-vsync', 'passthrough',
'-ss', seekTo,
'-ss', String(seekTo),
'-noautorotate',
@ -582,27 +655,5 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
return execa(getFfmpegPath(), args, { encoding: null, buffer: false, stderr: enableLog ? 'inherit' : 'pipe' });
}
// Don't pass complex objects over the bridge
const runFfmpeg = async (...args) => runFfmpegProcess(...args);
module.exports = {
setCustomFfPath,
abortFfmpegs,
getFfmpegPath,
runFfprobe,
runFfmpeg,
runFfmpegConcat,
runFfmpegWithProgress,
renderWaveformPng,
mapTimesToSegments,
detectSceneChanges,
captureFrames,
captureFrame,
getFfCommandLine,
html5ify,
getDuration,
readOneJpegFrame,
blackDetect,
silenceDetect,
createMediaSourceProcess,
};
// Don't pass complex objects over the bridge (process)
export const runFfmpeg = async (...args: Parameters<typeof runFfmpegProcess>) => runFfmpegProcess(...args);

Wyświetl plik

@ -1,14 +1,16 @@
const express = require('express');
const morgan = require('morgan');
const http = require('http');
const asyncHandler = require('express-async-handler');
const { homepage } = require('./constants');
import express from 'express';
import morgan from 'morgan';
import http from 'http';
import asyncHandler from 'express-async-handler';
import assert from 'assert';
import { homepage } from './constants.js';
import logger from './logger.js';
const logger = require('./logger');
module.exports = ({ port, onKeyboardAction }) => {
export default ({ port, onKeyboardAction }: {
port: number, onKeyboardAction: (a: string) => Promise<void>,
}) => {
const app = express();
// https://expressjs.com/en/resources/middleware/morgan.html
@ -25,7 +27,10 @@ module.exports = ({ port, onKeyboardAction }) => {
app.use('/api', apiRouter);
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();
}));

Wyświetl plik

@ -1,12 +1,12 @@
const i18n = require('i18next');
const Backend = require('i18next-fs-backend');
import i18n from 'i18next';
import Backend from 'i18next-fs-backend';
const { commonI18nOptions, loadPath, addPath } = require('./i18n-common');
import { commonI18nOptions, loadPath, addPath } from './i18nCommon.js';
// See also renderer
// https://github.com/i18next/i18next/issues/869
i18n
export default i18n
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
@ -23,5 +23,3 @@ i18n
addPath,
},
});
module.exports = i18n;

Wyświetl plik

@ -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
// const LanguageDetector = window.require('i18next-electron-language-detector');
const isDev = require('electron-is-dev');
// eslint-disable-next-line import/no-extraneous-dependencies
const { app } = require('electron');
const { join } = require('path');
import { app } from 'electron';
import { join } from 'path';
import { InitOptions } from 'i18next';
const { frontendBuildDir } = require('./util');
let customLocalesPath;
function setCustomLocalesPath(p) {
let customLocalesPath: string | undefined;
export function setCustomLocalesPath(p: string) {
customLocalesPath = p;
}
function getLangPath(subPath) {
function getLangPath(subPath: string) {
if (customLocalesPath != null) return join(customLocalesPath, subPath);
if (isDev) return join('public', subPath);
return join(app.getAppPath(), frontendBuildDir, subPath);
if (app.isPackaged) return join(process.resourcesPath, 'locales', subPath);
return join('locales', subPath);
}
// Weblate hardcodes different lang codes than electron
// https://www.electronjs.org/docs/api/app#appgetlocale
// https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc
const mapLang = (lng) => ({
const mapLang = (lng: string) => ({
nb: 'nb_NO',
no: 'nb_NO',
zh: 'zh_Hans',
@ -39,29 +37,21 @@ const mapLang = (lng) => ({
'ru-RU': 'ru',
}[lng] || lng);
const fallbackLng = 'en';
export const fallbackLng = 'en';
const commonI18nOptions = {
export const commonI18nOptions: InitOptions = {
fallbackLng,
// debug: isDev,
// saveMissing: isDev,
// updateMissing: isDev,
// 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?
// Maybe do something like this: https://stackoverflow.com/a/19405314/6519037
keySeparator: false,
nsSeparator: false,
};
const loadPath = (lng, ns) => getLangPath(`locales/${mapLang(lng)}/${ns}.json`);
const addPath = (lng, ns) => getLangPath(`locales/${mapLang(lng)}/${ns}.missing.json`);
module.exports = {
fallbackLng,
loadPath,
addPath,
commonI18nOptions,
setCustomLocalesPath,
};
export const loadPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.json`);
export const addPath = (lng: string, ns: string) => getLangPath(`${mapLang(lng)}/${ns}.missing.json`);

Wyświetl plik

@ -1,31 +1,55 @@
/// <reference types="electron-vite/node" />
process.traceDeprecation = true;
process.traceProcessWarnings = true;
/* eslint-disable import/first */
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');
const isDev = require('electron-is-dev');
const unhandled = require('electron-unhandled');
const i18n = require('i18next');
const debounce = require('lodash/debounce');
const yargsParser = require('yargs-parser');
const JSON5 = require('json5');
const remote = require('@electron/remote/main');
const { stat } = require('fs/promises');
import electron, { AboutPanelOptionsOptions, BrowserWindow, BrowserWindowConstructorOptions, nativeTheme, shell, app, ipcMain } from 'electron';
import unhandled from 'electron-unhandled';
import i18n from 'i18next';
import debounce from 'lodash/debounce';
import yargsParser from 'yargs-parser';
import JSON5 from 'json5';
import remote from '@electron/remote/main';
import { stat } from 'fs/promises';
import assert from 'assert';
const logger = require('./logger');
const menu = require('./menu');
const configStore = require('./configStore');
const { frontendBuildDir, isLinux } = require('./util');
const attachContextMenu = require('./contextMenu');
const HttpServer = require('./httpServer');
import logger from './logger.js';
import menu from './menu.js';
import * as configStore from './configStore.js';
import { isLinux } from './util.js';
import attachContextMenu from './contextMenu.js';
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://peter.sh/experiments/chromium-command-line-switches/
@ -49,8 +73,7 @@ const isStoreBuild = process.windowsStore || process.mas;
const showVersion = !isStoreBuild;
/** @type import('electron').AboutPanelOptionsOptions */
const aboutPanelOptions = {
const aboutPanelOptions: AboutPanelOptionsOptions = {
applicationName: appName,
copyright: `Copyright © ${copyrightYear} Mikael Finstad ❤️ 🇳🇴`,
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
app.setAboutPanelOptions(aboutPanelOptions);
let filesToOpen = [];
let filesToOpen: string[] = [];
// 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.
let mainWindow;
let mainWindow: BrowserWindow | null;
let askBeforeClose = false;
let rendererReady = false;
let newVersion;
let disableNetworking;
let newVersion: string | undefined;
let disableNetworking: boolean;
const openFiles = (paths) => mainWindow.webContents.send('openFiles', paths);
const openFiles = (paths: string[]) => mainWindow!.webContents.send('openFiles', paths);
let apiKeyboardActionRequestsId = 0;
const apiKeyboardActionRequests = new Map();
const apiKeyboardActionRequests = new Map<number, () => void>();
async function sendApiKeyboardAction(action) {
async function sendApiKeyboardAction(action: string) {
try {
const id = apiKeyboardActionRequestsId;
apiKeyboardActionRequestsId += 1;
mainWindow.webContents.send('apiKeyboardAction', { id, action });
await new Promise((resolve) => {
mainWindow!.webContents.send('apiKeyboardAction', { id, action } satisfies ApiKeyboardActionRequest);
await new Promise<void>((resolve) => {
apiKeyboardActionRequests.set(id, resolve);
});
} catch (err) {
@ -101,7 +124,7 @@ async function sendApiKeyboardAction(action) {
// https://github.com/electron/electron/issues/526#issuecomment-563010533
function getSizeOptions() {
const bounds = configStore.get('windowBounds');
const options = {};
const options: BrowserWindowConstructorOptions = {};
if (bounds) {
const area = electron.screen.getDisplayMatching(bounds).workArea;
// 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');
// 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.
// mainWindow.webContents.openDevTools()
@ -164,6 +187,7 @@ function createWindow() {
mainWindow.on('close', (e) => {
if (!askBeforeClose) return;
assert(mainWindow);
const choice = electron.dialog.showMessageBoxSync(mainWindow, {
type: 'question',
buttons: ['Yes', 'No'],
@ -186,10 +210,11 @@ function createWindow() {
}
function updateMenu() {
assert(mainWindow);
menu({ app, mainWindow, newVersion, isStoreBuild });
}
function openFilesEventually(paths) {
function openFilesEventually(paths: string[]) {
if (rendererReady) openFiles(paths);
else filesToOpen = paths;
}
@ -201,18 +226,18 @@ function openFilesEventually(paths) {
function parseCliArgs(rawArgv = process.argv) {
const ignoreFirstArgs = process.defaultApp ? 2 : 1;
// 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) : [];
return yargsParser(argsWithoutAppName, { boolean: ['allow-multiple-instances', 'disable-networking'] });
return yargsParser(argsWithoutAppName, { boolean: ['allow-multiple-instances', 'disable-networking'], string: ['settings-json'] });
}
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
// using additionalData because the built in "argv" passing is a bit broken:
@ -232,16 +257,14 @@ function initApp() {
mainWindow.focus();
}
// @ts-expect-error todo
if (!Array.isArray(additionalData?.argv)) return;
if (!(additionalData != null && typeof additionalData === 'object' && 'argv' in additionalData) || !Array.isArray(additionalData.argv)) return;
// @ts-expect-error todo
const argv2 = parseCliArgs(additionalData.argv);
logger.info('second-instance', argv2);
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._);
else if (argv2.keyboardAction) sendApiKeyboardAction(argv2.keyboardAction);
if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._.map(String));
else if (argv2['keyboardAction']) sendApiKeyboardAction(argv2['keyboardAction']);
});
// Quit when all windows are closed.
@ -322,7 +345,7 @@ const readyPromise = app.whenReady();
logger.info('CLI arguments', argv);
// 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;
({ disableNetworking } = argv);
@ -330,6 +353,7 @@ const readyPromise = app.whenReady();
if (settingsJson != null) {
logger.info('initializing settings', settingsJson);
Object.entries(JSON5.parse(settingsJson)).forEach(([key, value]) => {
// @ts-expect-error todo use zod?
configStore.set(key, value);
});
}
@ -340,6 +364,7 @@ const readyPromise = app.whenReady();
const port = typeof httpApi === 'number' ? httpApi : 8080;
const { startHttpServer } = HttpServer({ port, onKeyboardAction: sendApiKeyboardAction });
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
installExtension(REACT_DEVELOPER_TOOLS)
.then((name) => logger.info('Added Extension', name))
.catch((err) => logger.error('Failed to add extension', err));
.then((name: string) => logger.info('Added Extension', name))
.catch((err: unknown) => logger.error('Failed to add extension', err));
}
createWindow();
@ -366,7 +391,7 @@ const readyPromise = app.whenReady();
}
})();
function focusWindow() {
export function focusWindow() {
try {
app.focus({ steal: true });
} catch (err) {
@ -374,10 +399,8 @@ function focusWindow() {
}
}
function quitApp() {
export function quitApp() {
electron.app.quit();
}
const hasDisabledNetworking = () => !!disableNetworking;
module.exports = { focusWindow, isDev, hasDisabledNetworking, quitApp };
export const hasDisabledNetworking = () => !!disableNetworking;

Wyświetl plik

@ -0,0 +1,2 @@
const isDev = import.meta.env.MODE === 'development';
export default isDev;

Wyświetl plik

@ -1,15 +1,18 @@
const winston = require('winston');
const util = require('util');
const isDev = require('electron-is-dev');
import winston from 'winston';
import util from 'util';
// eslint-disable-next-line import/no-extraneous-dependencies
const { app } = require('electron');
const { join } = require('path');
import { app } from 'electron';
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://github.com/winstonjs/winston/issues/1427
const combineMessageAndSplat = () => ({
transform(info) {
transform(info: TransformableInfo) {
// @ts-expect-error todo
const { [Symbol.for('splat')]: args = [], message } = info;
// eslint-disable-next-line no-param-reassign
info.message = util.format(message, ...args);
@ -21,14 +24,14 @@ const createLogger = () => winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
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();
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;

Wyświetl plik

@ -1,16 +1,19 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');
const { t } = require('i18next');
import electron, { BrowserWindow } from 'electron';
import { t } from 'i18next';
import { homepage, getReleaseUrl, licensesPage } from './constants.js';
// menu-safe i18n.t:
// https://github.com/mifi/lossless-cut/issues/1456
const esc = (val) => val.replaceAll('&', '&&');
const esc = (val: string) => val.replaceAll('&', '&&');
const { Menu } = electron;
const { homepage, getReleaseUrl, licensesPage } = require('./constants');
module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
export default ({ app, mainWindow, newVersion, isStoreBuild }: {
app: Electron.App, mainWindow: BrowserWindow, newVersion?: string | undefined, isStoreBuild: boolean,
}) => {
const menu = [
...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []),

Wyświetl plik

@ -1,24 +1,33 @@
// eslint-disable-line unicorn/filename-case
const GitHub = require('github-api');
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');
const semver = require('semver');
import electron from 'electron';
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 gh = new GitHub();
const repo = gh.getRepo('mifi', 'lossless-cut');
const octokit = new Octokit();
async function checkNewVersion() {
// eslint-disable-next-line import/prefer-default-export
export async function checkNewVersion() {
try {
// From API: https://developer.github.com/v3/repos/releases/#get-the-latest-release
// View the latest published full release for the repository.
// 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 = '3.17.2';
@ -34,5 +43,3 @@ async function checkNewVersion() {
return undefined;
}
}
module.exports = { checkNewVersion };

8
src/main/util.ts 100644
Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,2 @@
// todo
console.log('preload');

Wyświetl plik

@ -86,9 +86,9 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
import BigWaveform from './components/BigWaveform';
import isDev from './isDev';
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, Html5ifyMode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
import { CaptureFormat, KeyboardAction } from '../types';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../ffprobe';
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
const electron = window.require('electron');
const { exists } = window.require('fs-extra');
@ -96,8 +96,7 @@ const { lstat } = window.require('fs/promises');
const filePathToUrl = window.require('file-url');
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
const remote = window.require('@electron/remote');
const { focusWindow, hasDisabledNetworking, quitApp } = remote.require('./electron');
const { focusWindow, hasDisabledNetworking, quitApp } = window.require('@electron/remote').require('./index.js');
const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' };
@ -1338,8 +1337,10 @@ function App() {
return;
}
if ('stdout' in err) console.error('stdout:', err.stdout);
if ('stderr' in err) console.error('stderr:', err.stderr);
// @ts-expect-error todo
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 (isOutOfSpaceError(err)) {

Wyświetl plik

@ -4,10 +4,9 @@ import { useDebounce } from 'use-debounce';
import isDev from './isDev';
import { ChromiumHTMLVideoElement } from './types';
import { FFprobeStream } from '../ffprobe';
import { FFprobeStream } from '../../../ffprobe';
const remote = window.require('@electron/remote');
const { createMediaSourceStream, readOneJpegFrame } = remote.require('./compatPlayer');
const { compatPlayer: { createMediaSourceStream, readOneJpegFrame } } = window.require('@electron/remote').require('./index.js');
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();
}
function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
function drawJpegFrame(canvas: HTMLCanvasElement | null, jpegImage: Buffer) {
if (!canvas) return;
const ctx = canvas.getContext('2d');
@ -248,7 +247,9 @@ function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
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 });
signal.addEventListener('abort', () => abort());
const jpegImage = await promise;

Wyświetl plik

@ -14,7 +14,7 @@ import OutputFormatSelect from './OutputFormatSelect';
import useUserSettings from '../hooks/useUserSettings';
import { isMov } from '../util/streams';
import { getOutFileExtension, getSuffixedFileName } from '../util';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../ffprobe';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
const { basename } = window.require('path');

Wyświetl plik

@ -26,8 +26,8 @@ import useUserSettings from '../hooks/useUserSettings';
import styles from './ExportConfirm.module.css';
import { InverseCutSegment, SegmentToExport } from '../types';
import { GenerateOutSegFileNames } from '../util/outputNameTemplate';
import { FFprobeStream } from '../../ffprobe';
import { AvoidNegativeTs } from '../../types';
import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs } from '../../../../types';
const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };

Wyświetl plik

@ -12,7 +12,7 @@ import Swal from '../swal';
import SetCutpointButton from './SetCutpointButton';
import SegmentCutpointButton from './SegmentCutpointButton';
import { getModifier } from '../hooks/useTimelineScroll';
import { KeyBinding, KeyboardAction } from '../../types';
import { KeyBinding, KeyboardAction } from '../../../../types';
import { StateSegment } from '../types';

Some files were not shown because too many files have changed in this diff Show More