Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,33 @@ version: 2
jobs:
build:
macos:
xcode: '13.4.1'
xcode: '14.3.1'
resource_class: m4pro.medium
steps:
- checkout
- run:
name: Use Node.js 16
command: |
nvm install 16
nvm alias default 16
{
echo 'export NVM_DIR="$HOME/.nvm"'
echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"'
echo 'nvm use 16 >/dev/null'
} >> "$BASH_ENV"
nvm use 16
npm install --global yarn@1.22.19
node --version
yarn --version
- run:
name: Install native build tools
command: |
export HOMEBREW_NO_AUTO_UPDATE=1
export HOMEBREW_NO_INSTALL_CLEANUP=1
brew install automake libtool pkg-config || true
echo 'export PATH="/opt/homebrew/opt/libtool/libexec/gnubin:$PATH"' >> "$BASH_ENV"
export PATH="/opt/homebrew/opt/libtool/libexec/gnubin:$PATH"
command -v aclocal
- run: yarn
- run: mkdir -p ~/reports
- run: yarn lint
Expand Down
14 changes: 12 additions & 2 deletions main/common/types/remote-states.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {App, Format} from './base';
import {ExportStatus} from './conversion-options';
import {ConversionOptions, ExportStatus} from './conversion-options';

// eslint-disable-next-line @typescript-eslint/ban-types
export type RemoteState<State = any, Actions extends Record<string, (...args: any[]) => any> = {}> = {
Expand All @@ -17,9 +17,10 @@ export type RemoteStateHook<Base extends RemoteState> = Base extends RemoteState

export type RemoteStateHandler<Base extends RemoteState> = Base extends RemoteState<infer State, infer Actions> ? (sendUpdate: (state: State, id?: string) => void) => {
actions: {
[Key in keyof Actions]: Actions[Key] extends (...args: any[]) => any ? (id: string, ...args: Parameters<Actions[Key]>) => void : never
[Key in keyof Actions]: Actions[Key] extends (...args: any[]) => infer Result ? (id: string, ...args: Parameters<Actions[Key]>) => Result : never
};
getState: (id: string) => State | undefined;
subscribe?: (id: string) => undefined | (() => void);
} : never;

export interface ExportOptionsPlugin {
Expand Down Expand Up @@ -59,6 +60,15 @@ export type EditorOptionsRemoteState = RemoteState<ExportOptions, {
format: Format;
fps: number;
}) => void;
estimateGifSize: ({filePath, conversionOptions}: {
filePath: string;
conversionOptions: ConversionOptions;
}) => Promise<{
bytes: number;
sampleCount: number;
sampledDuration: number;
} | undefined>;
cancelGifSizeEstimate: () => void;
}>;

export interface ExportState {
Expand Down
158 changes: 82 additions & 76 deletions main/converters/h264.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,93 +12,99 @@ import fs from 'fs';
const convertToGif = PCancelable.fn(async (options: ConvertOptions, onCancel: PCancelable.OnCancelFunction) => {
const palettePath = tempy.file({extension: 'png'});

const paletteProcess = convert(palettePath, {shouldTrack: false}, conditionalArgs(
'-i', options.inputPath,
'-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''},palettegen`,
{
args: [
'-ss',
options.startTime.toString(),
'-to',
options.endTime.toString()
],
if: options.shouldCrop
},
palettePath
));
try {
const paletteProcess = convert(palettePath, {shouldTrack: false}, conditionalArgs(
'-i', options.inputPath,
'-vf', `fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''},palettegen`,
{
args: [
'-ss',
options.startTime.toString(),
'-to',
options.endTime.toString()
],
if: options.shouldCrop
},
palettePath
));

onCancel(() => {
paletteProcess.cancel();
});
onCancel(() => {
paletteProcess.cancel();
});

await paletteProcess;
await paletteProcess;

// Sometimes if the clip is too short or fps too low, the palette is not generated
const hasPalette = fs.existsSync(palettePath);
// Sometimes if the clip is too short or fps too low, the palette is not generated
const hasPalette = fs.existsSync(palettePath);

const shouldLoop = settings.get('loopExports');
const shouldLoop = settings.get('loopExports');

const conversionProcess = convert(options.outputPath, {
onProgress: (progress, estimate) => {
options.onProgress('Converting', progress, estimate);
},
startTime: options.startTime,
endTime: options.endTime
}, conditionalArgs(
'-i', options.inputPath,
{
args: [
'-i',
palettePath,
'-filter_complex',
`fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}[x]; [x][1:v]paletteuse`
],
if: hasPalette
},
{
args: [
'-vf',
`fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}`
],
if: !hasPalette
},
'-loop', shouldLoop ? '0' : '-1', // 0 == forever; -1 == no loop
{
args: [
'-ss',
options.startTime.toString(),
'-to',
options.endTime.toString()
],
if: options.shouldCrop
},
options.outputPath
));
const conversionProcess = convert(options.outputPath, {
shouldTrack: options.shouldTrack,
onProgress: (progress, estimate) => {
options.onProgress('Converting', progress, estimate);
},
startTime: options.startTime,
endTime: options.endTime
}, conditionalArgs(
'-i', options.inputPath,
{
args: [
'-i',
palettePath,
'-filter_complex',
`fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}[x]; [x][1:v]paletteuse`
],
if: hasPalette
},
{
args: [
'-vf',
`fps=${options.fps}${options.shouldCrop ? `,scale=${options.width}:${options.height}:flags=lanczos` : ''}`
],
if: !hasPalette
},
'-loop', shouldLoop ? '0' : '-1', // 0 == forever; -1 == no loop
{
args: [
'-ss',
options.startTime.toString(),
'-to',
options.endTime.toString()
],
if: options.shouldCrop
},
options.outputPath
));

onCancel(() => {
conversionProcess.cancel();
});
onCancel(() => {
conversionProcess.cancel();
});

await conversionProcess;
await conversionProcess;

const compressProcess = compress(options.outputPath, {
onProgress: (progress, estimate) => {
options.onProgress('Compressing', progress, estimate);
},
startTime: options.startTime,
endTime: options.endTime
}, [
'--batch',
options.outputPath
]);
const compressProcess = compress(options.outputPath, {
shouldTrack: options.shouldTrack,
onProgress: (progress, estimate) => {
options.onProgress('Compressing', progress, estimate);
},
startTime: options.startTime,
endTime: options.endTime
}, [
'--batch',
options.outputPath
]);

onCancel(() => {
compressProcess.cancel();
});
onCancel(() => {
compressProcess.cancel();
});

await compressProcess;
await compressProcess;

return options.outputPath;
return options.outputPath;
} finally {
await fs.promises.unlink(palettePath).catch(() => undefined);
}
});

// eslint-disable-next-line @typescript-eslint/promise-function-async
Expand Down
12 changes: 7 additions & 5 deletions main/converters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const croppingHandlers = new Map([
// eslint-disable-next-line @typescript-eslint/promise-function-async
export const convertTo = (
format: Format,
options: Except<ConvertOptions, 'outputPath'> & {defaultFileName: string},
options: Except<ConvertOptions, 'outputPath'> & {defaultFileName: string; outputPath?: string},
encoding: Encoding = Encoding.h264
) => {
if (!converters.has(encoding)) {
Expand All @@ -36,12 +36,14 @@ export const convertTo = (
throw new Error(`Unsupported file format for ${encoding}: ${format}`);
}

track(`file/export/encoding/${encoding}`);
track(`file/export/format/${format}`);
if (options.shouldTrack !== false) {
track(`file/export/encoding/${encoding}`);
track(`file/export/format/${format}`);
}

const conversionOptions = {
outputPath: path.join(tempy.directory(), `${options.defaultFileName}.${getFormatExtension(format)}`),
...options
...options,
outputPath: options.outputPath ?? path.join(tempy.directory(), `${options.defaultFileName}.${getFormatExtension(format)}`)
};

if (options.editService) {
Expand Down
1 change: 1 addition & 0 deletions main/converters/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ConvertOptions {
shouldMute: boolean;
onCancel: () => void;
onProgress: (action: string, progress: number, estimate?: string) => void;
shouldTrack?: boolean;
editService?: {
pluginName: string;
serviceTitle: string;
Expand Down
45 changes: 43 additions & 2 deletions main/remote-states/editor-options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import Store from 'electron-store';
import {EditorOptionsRemoteState, ExportOptions, ExportOptionsPlugin, Format, RemoteStateHandler} from '../common/types';
import {ConversionOptions, EditorOptionsRemoteState, ExportOptions, ExportOptionsPlugin, Format, RemoteStateHandler} from '../common/types';
import {formats} from '../common/constants';

import {plugins} from '../plugins';
import {apps} from '../plugins/built-in/open-with-plugin';
import {prettifyFormat} from '../utils/formats';
import {estimateGifSize} from '../utils/gif-size-estimate';
import {Video} from '../video';

const exportUsageHistory = new Store<{[key in Format]: {lastUsed: number; plugins: Record<string, number>}}>({
name: 'export-usage-history',
Expand Down Expand Up @@ -54,6 +56,8 @@ const fpsUsageHistory = new Store<{[key in Format]: number}>({
}
});

const gifSizeEstimateProcesses = new Map<string, ReturnType<typeof estimateGifSize>>();

const getEditOptions = () => {
return plugins.editPlugins.flatMap(
plugin => plugin.editServices
Expand Down Expand Up @@ -133,12 +137,49 @@ const editorOptionsRemoteState: RemoteStateHandler<EditorOptionsRemoteState> = s
fpsUsageHistory.set(format, fps);
state.fpsHistory = fpsUsageHistory.store;
sendUpdate(state);
},
estimateGifSize: async (id: string, {filePath, conversionOptions}: {
filePath: string;
conversionOptions: ConversionOptions;
}) => {
const video = Video.fromId(filePath);

if (!video) {
return;
}

gifSizeEstimateProcesses.get(id)?.cancel();

const process = estimateGifSize(video, conversionOptions);
gifSizeEstimateProcesses.set(id, process);

try {
return await process;
} catch (error) {
if ((error as any)?.isCanceled) {
return;
}

throw error;
} finally {
if (gifSizeEstimateProcesses.get(id) === process) {
gifSizeEstimateProcesses.delete(id);
}
}
},
cancelGifSizeEstimate: (id: string) => {
gifSizeEstimateProcesses.get(id)?.cancel();
gifSizeEstimateProcesses.delete(id);
}
};

return {
actions,
getState: () => state
getState: () => state,
subscribe: id => () => {
gifSizeEstimateProcesses.get(id)?.cancel();
gifSizeEstimateProcesses.delete(id);
}
};
};

Expand Down
Loading