Skip to content

Commit

Permalink
Support web export
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxou44 committed Mar 13, 2024
1 parent 5f2432a commit 8a6a4b2
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 204 deletions.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ To improve the quality of **Eagle Animation**, runtime errors are automatically

Some features are device-dependent or platform-limited. Here's a summary table.

| Feature | Windows | MacOS | Linux | Web (Chrome) | Web (Firefox) | Web (Safari) |
| -------------------------- | ------- | ----- | ----- | ------------ | ------------- | ------------ |
| Video export |||||||
| Frames export |||||||
| High-quality photo capture |||||||
| Workshop features |||||||
| Control webcam settings |||||||
| Use Canon cameras |||||||
| Feature | Windows | MacOS | Linux | Web (Chrome / Edge) | Web (Firefox) | Web (Safari) |
| ------------------------------------------------- | ------- | ----- | ----- | ------------------- | ------------- | ------------ |
| Use webcam to take photos |||||||
| Export captured frames |||||||
| Video export |||||||
| Improve quality by reducing the preview framerate |||||||
| Control webcam settings |||||||
| Workshop features |||||||
| Use Canon camera to take photos |||||||
40 changes: 20 additions & 20 deletions electron.vite.config.mjs
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
import { resolve } from 'node:path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'node:path';
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import svgr from 'vite-plugin-svgr';
import { normalizePath } from 'vite'
import { normalizePath } from 'vite';

export default defineConfig({
main: {
build: {
rollupOptions: {
external: ['@brick-a-brack/napi-canon-cameras']
}
external: ['@brick-a-brack/napi-canon-cameras'],
},
},
plugins: [externalizeDepsPlugin()]
plugins: [externalizeDepsPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()]
plugins: [externalizeDepsPlugin()],
},
renderer: {
resolve: {
alias: {
'~': resolve(__dirname)
}
'~': resolve(__dirname),
},
},
plugins: [
react(),
svgr({
include: '**/*.svg?jsx',
svgrOptions: {
// svgr options
}
},
}),
viteStaticCopy({
targets: [
{
src: normalizePath(resolve(__dirname, './resources/*')),
dest: '.'
}
]
})
targets: [
{
src: normalizePath(resolve(__dirname, './resources/*')),
dest: '.',
},
],
}),
],
}
})
},
});
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@braintree/browser-detection": "^2.0.0",
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@ffmpeg/core": "^0.12.6",
"@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1",
"animated-scroll-to": "^2.3.0",
Expand Down
96 changes: 96 additions & 0 deletions src/common/ffmpeg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const profiles = {
h264: {
codec: 'libx264',
extension: 'mp4',
pix_fmt: 'yuv420p',
preset: 'faster',
},
hevc: {
codec: 'libx265',
extension: 'mp4',
pix_fmt: 'yuv420p',
preset: 'faster',
},
prores: {
codec: 'prores_ks',
extension: 'mov',
pix_fmt: 'yuva444p10le',
},
vp8: {
codec: 'libvpx',
extension: 'webm',
pix_fmt: 'yuv420p',
},
vp9: {
codec: 'libvpx-vp9',
extension: 'webm',
pix_fmt: 'yuv420p',
},
};

export const getEncodingProfile = (format) => {
return profiles[format] || null;
};

export const getFFmpegArgs = (width = 1920, height = 1080, encodingProfile = false, outputFile = false, fps = 24, opts = {}) => {
if (typeof profiles[encodingProfile] === 'undefined') {
throw new Error('UNKNOWN_PROFILE');
}

// Get profile
const profile = profiles[encodingProfile];

// Invalid output file
if (outputFile === false) {
throw new Error('UNDEFINED_OUTPUT');
}

// Default -y to overwite
const args = ['-y'];

// Input framerate
args.push('-r', `${Number(fps) > 0 && Number(fps) <= 240 ? Number(fps) : 12}`);

// Add all images in the path
args.push('-i', 'frame-%06d.jpg');

// Output resolution
const customHeight = opts.resolution && opts.resolution !== 'original' ? `,scale=w=-2:h=${opts.resolution}:force_original_aspect_ratio=1` : '';

// AutoScale input to ratio
args.push('-vf', `scale=w=${width}:h=${height}:force_original_aspect_ratio=1,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2${customHeight}`);

// Codec
args.push('-c:v', profile.codec);

// Bitrate
//args.push('-b:v', '128M');

// Preset
if (profile.preset) {
args.push('-preset', profile.preset);
}

// Fast start for streaming
if (profile.extension === 'mp4') {
args.push('-movflags', '+faststart');
}

// Custom output framerate
if (opts.customOutputFramerate && opts.customOutputFramerateNumber) {
args.push('-r', `${Number(opts.customOutputFramerateNumber)}`);
}

// Pixel mode
args.push('-pix_fmt', profile.pix_fmt);

// Prores flags
if (encodingProfile === 'prores') {
args.push('-profile:v', '3', '-vendor', 'apl0', '-bits_per_mb', '4000', '-f', 'mov');
}

// Output file
args.push(`${outputFile}`);

return args;
};
17 changes: 14 additions & 3 deletions src/main/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import {
import { getSettings, saveSettings } from './core/settings';
import { selectFile, selectFolder } from './core/utils';
import { exportProjectScene, getSyncList, normalizePictures, saveSyncList } from './core/export';
import { getProfile } from './core/ffmpeg';

import { uploadFile } from './core/api';
import { existsSync } from 'fs';
import { flushCamera, getCamera, getCameras } from './cameras';
import { getEncodingProfile } from '../common/ffmpeg';

const OLD_PROJECTS_PATH = join(homedir(), DIRECTORY_NAME);
const PROJECTS_PATH = existsSync(OLD_PROJECTS_PATH) ? OLD_PROJECTS_PATH : envPaths(DIRECTORY_NAME, { suffix: '' }).data;
Expand Down Expand Up @@ -203,7 +204,17 @@ const actions = {
}
},
APP_CAPABILITIES: async () => {
const capabilities = ['EXPORT_VIDEO', 'EXPORT_FRAMES', 'BACKGROUND_SYNC', 'LOW_FRAMERATE_QUALITY_IMPROVEMENT'];
const capabilities = [
'EXPORT_VIDEO',
'EXPORT_FRAMES',
'BACKGROUND_SYNC',
'LOW_FRAMERATE_QUALITY_IMPROVEMENT',
'EXPORT_VIDEO_H264',
'EXPORT_VIDEO_HEVC',
'EXPORT_VIDEO_PRORES',
'EXPORT_VIDEO_VP8',
'EXPORT_VIDEO_VP9',
];
return capabilities;
},
EXPORT: async (
Expand Down Expand Up @@ -242,7 +253,7 @@ const actions = {
return true;
}

const profile = getProfile(format);
const profile = getEncodingProfile(format);

// Create sync folder if needed
if (mode === 'send') {
Expand Down
95 changes: 5 additions & 90 deletions src/main/core/ffmpeg.js
Original file line number Diff line number Diff line change
@@ -1,99 +1,13 @@
import ffmpeg from 'ffmpeg-static';
import { execFile } from 'child_process';
import { getFFmpegArgs } from '../../common/ffmpeg';

// eslint-disable-next-line
const ffmpegPath = ffmpeg ? ffmpeg.replace('app.asar', 'app.asar.unpacked') : false;

const profiles = {
h264: {
codec: 'libx264',
extension: 'mp4',
pix_fmt: 'yuv420p',
preset: 'faster',
},
hevc: {
codec: 'libx265',
extension: 'mp4',
pix_fmt: 'yuv420p',
preset: 'faster',
},
prores: {
codec: 'prores_ks',
extension: 'mov',
pix_fmt: 'yuva444p10le',
},
vp8: {
codec: 'libvpx',
extension: 'webm',
pix_fmt: 'yuv420p',
},
vp9: {
codec: 'libvpx-vp9',
extension: 'webm',
pix_fmt: 'yuv420p',
},
};

export const getProfile = (format) => {
return profiles[format] || null;
};

export const generate = (width = 1920, height = 1080, directory = false, outputProfil = false, outputFile = false, fps = 24, opts = {}) =>
new Promise((resolve, reject) => {
if (typeof profiles[outputProfil] === 'undefined') return reject(new Error('UNKNOWN PROFILE'));

// Get profile
const profile = profiles[outputProfil];

// Invalid output file
if (outputFile === false) return reject(new Error('UNDEFINED_OUTPUT'));

// Default -y to overwite
const args = ['-y'];

// Input framerate
args.push('-r', Number(fps) > 0 && Number(fps) <= 240 ? Number(fps) : 12);

// Add all images in the path
args.push('-i', 'frame-%06d.jpg');

// Output resolution
const customHeight = opts.resolution && opts.resolution !== 'original' ? `,scale=w=-2:h=${opts.resolution}:force_original_aspect_ratio=1` : '';

// AutoScale input to ratio
args.push('-vf', `scale=w=${width}:h=${height}:force_original_aspect_ratio=1,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2${customHeight}`);

// Codec
args.push('-c:v', profile.codec);

// Bitrate
//args.push('-b:v', '128M');

// Preset
if (profile.preset) {
args.push('-preset', profile.preset);
}

// Fast start for streaming
if (profile.extension === 'mp4') {
args.push('-movflags', '+faststart');
}

// Custom output framerate
if (opts.customOutputFramerate && opts.customOutputFramerateNumber) {
args.push('-r', `${Number(opts.customOutputFramerateNumber)}`);
}

// Pixel mode
args.push('-pix_fmt', profile.pix_fmt);

// Prores flags
if (outputProfil === 'prores') {
args.push('-profile:v', '3', '-vendor', 'apl0', '-bits_per_mb', '4000', '-f', 'mov');
}

// Output file
args.push(`${outputFile}`);
export const generate = (width = 1920, height = 1080, directory = false, outputProfil = false, outputFile = false, fps = 24, opts = {}) => {
return new Promise((resolve, reject) => {
const args = getFFmpegArgs(width, height, outputProfil, outputFile, fps, opts).catch(reject);

console.log(`ffmpeg.exe ${args.map((e) => `"${e}"`).join(' ')}`);

Expand Down Expand Up @@ -122,3 +36,4 @@ export const generate = (width = 1920, height = 1080, directory = false, outputP
resolve();
});
});
};
17 changes: 17 additions & 0 deletions src/renderer/actions/ffmpeg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { toBlobURL } from '@ffmpeg/util';

export const getFFmpeg = async (callback = () => {}) => {
const baseURL = '/';
const ffmpeg = new FFmpeg();
ffmpeg.on('log', callback);
ffmpeg.on('error', callback);
ffmpeg.on('progress', callback);
await ffmpeg
.load({
coreURL: await toBlobURL(`${baseURL}ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}ffmpeg-core.wasm`, 'application/wasm'),
})
.catch(console.error);
return ffmpeg;
};
Loading

0 comments on commit 8a6a4b2

Please sign in to comment.