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
6 changes: 5 additions & 1 deletion 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 Down Expand Up @@ -59,6 +59,10 @@ export type EditorOptionsRemoteState = RemoteState<ExportOptions, {
format: Format;
fps: number;
}) => void;
estimateGifSize: ({filePath, conversionOptions}: {
filePath: string;
conversionOptions: ConversionOptions;
}) => Promise<string | undefined>;
}>;

export interface ExportState {
Expand Down
35 changes: 34 additions & 1 deletion 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 {Video} from '../video';
import {estimateGifSize} from '../utils/gif-size-estimate';

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,6 +137,35 @@ 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);
}
}
}
};

Expand Down
55 changes: 55 additions & 0 deletions main/utils/gif-size-estimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import fs from 'fs';
import path from 'path';
import PCancelable from 'p-cancelable';
import prettyBytes from 'pretty-bytes';
import {convertTo} from '../converters';
import {ConversionOptions, Format} from '../common/types';
import {Video} from '../video';

const maximumSampleDuration = 2;
const noop = () => undefined;

export const estimateGifSize = PCancelable.fn(async (
video: Video,
options: ConversionOptions,
onCancel: PCancelable.OnCancelFunction
) => {
const duration = Math.max(options.endTime - options.startTime, 0);

if (duration === 0) {
return;
}

await video.whenReady();

const sampleDuration = Math.min(duration, maximumSampleDuration);
let samplePath: string | undefined;

const conversionProcess = convertTo(
Format.gif,
{
...options,
defaultFileName: `${video.title}-size-estimate`,
endTime: options.startTime + sampleDuration,
inputPath: video.filePath,
onCancel: noop,
onProgress: noop
},
video.encoding
);

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

try {
samplePath = await conversionProcess;
const {size} = await fs.promises.stat(samplePath);
return prettyBytes(Math.ceil(size * (duration / sampleDuration)));
} finally {
if (samplePath) {
await fs.promises.unlink(samplePath).catch(noop);
await fs.promises.rmdir(path.dirname(samplePath)).catch(noop);
}
}
});
101 changes: 101 additions & 0 deletions renderer/components/editor/options/gif-size-estimate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {useEffect, useRef, useState} from 'react';
import {Format} from 'common/types';
import useEditorOptions from 'hooks/editor/use-editor-options';
import useEditorWindowState from 'hooks/editor/use-editor-window-state';
import OptionsContainer from '../options-container';
import VideoTimeContainer from '../video-time-container';

const estimateDelay = 500;

const GifSizeEstimate = () => {
const {format, width, height, fps} = OptionsContainer.useContainer();
const {startTime, endTime} = VideoTimeContainer.useContainer();
const {filePath} = useEditorWindowState();
const {estimateGifSize} = useEditorOptions();

const [size, setSize] = useState<string>();
const [isEstimating, setIsEstimating] = useState(false);
const requestId = useRef(0);

useEffect(() => {
const canEstimate = (
format === Format.gif &&
filePath &&
width &&
height &&
fps &&
endTime > startTime &&
estimateGifSize
);

if (!canEstimate) {
requestId.current++;
setSize(undefined);
setIsEstimating(false);
return;
}

const id = ++requestId.current;
setSize(undefined);
setIsEstimating(true);

const timer = window.setTimeout(() => {
estimateGifSize({
filePath,
conversionOptions: {
width,
height,
startTime,
endTime,
fps,
shouldCrop: true,
shouldMute: true
}
}).then(estimatedSize => {
if (id === requestId.current) {
setSize(estimatedSize);
}
}).catch(() => {
if (id === requestId.current) {
setSize(undefined);
}
}).finally(() => {
if (id === requestId.current) {
setIsEstimating(false);
}
});
}, estimateDelay);

return () => {
window.clearTimeout(timer);
};
}, [endTime, estimateGifSize, filePath, format, fps, height, startTime, width]);

if (format !== Format.gif) {
return null;
}

const label = size ? `GIF ~${size}` : (isEstimating ? 'GIF ...' : 'GIF N/A');

return (
<div className="gif-size-estimate" title="Estimated GIF size">
{label}
<style jsx>{`
.gif-size-estimate {
color: #aaaaaa;
flex-shrink: 0;
font-size: 12px;
line-height: 24px;
margin-right: 8px;
min-width: 80px;
overflow: hidden;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
`}</style>
</div>
);
};

export default GifSizeEstimate;
2 changes: 2 additions & 0 deletions renderer/components/editor/options/right.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import VideoTimeContainer from '../video-time-container';
import VideoControlsContainer from '../video-controls-container';
import useSharePlugins from 'hooks/editor/use-share-plugins';
import useEditorOptions from 'hooks/editor/use-editor-options';
import GifSizeEstimate from './gif-size-estimate';

const FormatSelect = () => {
const {formats, format, updateFormat} = OptionsContainer.useContainer();
Expand Down Expand Up @@ -195,6 +196,7 @@ const RightOptions = () => {
<EditPluginsControl/>
<div className="format"><FormatSelect/></div>
<div className="plugin"><PluginsSelect/></div>
<GifSizeEstimate/>
<ConvertButton/>
<style jsx>{`
.container {
Expand Down
35 changes: 35 additions & 0 deletions test/gif-size-estimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import test from 'ava';
import path from 'path';
import {mockImport} from './helpers/mocks';

mockImport('../common/analytics', 'analytics');
mockImport('../plugins/service-context', 'service-context');
mockImport('../plugins', 'plugins');
mockImport('../common/settings', 'settings');

import {estimateGifSize} from '../main/utils/gif-size-estimate';
import {Encoding} from '../main/common/types';
import {Video} from '../main/video';

const input = path.resolve(__dirname, 'fixtures', 'input.mp4');

test('estimates GIF size', async t => {
const video = new Video({
filePath: input,
title: 'input',
fps: 30,
encoding: Encoding.h264
});

const estimate = await estimateGifSize(video, {
fps: 10,
width: 255,
height: 143,
startTime: 0,
endTime: 2,
shouldCrop: true,
shouldMute: true
});

t.regex(estimate!, /\d+.*B$/);
});