Skip to content

Commit

Permalink
Support HEVC files in the editor (wulkano#626)
Browse files Browse the repository at this point in the history
  • Loading branch information
George Karagkiaouris authored and sindresorhus committed Feb 15, 2019
1 parent abf2ed1 commit fb017c8
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 1,300 deletions.
15 changes: 13 additions & 2 deletions main/common/aperture.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ const {openEditorWindow} = require('../editor');
const {closePrefsWindow} = require('../preferences');
const {setRecordingTray, disableTray} = require('../tray');
const {disableCroppers, setRecordingCroppers, closeAllCroppers} = require('../cropper');
const {convertToH264} = require('../utils/encoding');
const settings = require('./settings');
const {track} = require('./analytics');

const aperture = createAperture();
const {audioDevices} = createAperture;
const {audioDevices, videoCodecs} = createAperture;
const recordHevc = videoCodecs.has('hevc');

let wasDoNotDisturbAlreadyEnabled;
let lastUsedSettings;
Expand Down Expand Up @@ -65,6 +67,10 @@ const startRecording = async options => {
}
}

if (recordHevc) {
apertureOpts.videoCodec = 'hevc';
}

console.log(`Collected settings after ${(Date.now() - past) / 1000}s`);

if (hideDesktopIcons) {
Expand Down Expand Up @@ -132,7 +138,12 @@ const stopRecording = async () => {
}

track('editor/opened/recording');
openEditorWindow(filePath, recordedFps, {isNewRecording: true});

if (recordHevc) {
openEditorWindow(await convertToH264(filePath), {recordedFps, isNewRecording: true, originalFilePath: filePath});
} else {
openEditorWindow(filePath, {recordedFps, isNewRecording: true});
}
};

module.exports = {
Expand Down
20 changes: 15 additions & 5 deletions main/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

const {BrowserWindow, dialog} = require('electron');
const path = require('path');
const {promisify} = require('util');
const fs = require('fs');
const EventEmitter = require('events');
const ipc = require('electron-better-ipc');
const {is} = require('electron-util');
const moment = require('moment');
Expand All @@ -16,19 +19,20 @@ const VIDEO_ASPECT = 9 / 16;
const MIN_VIDEO_WIDTH = 768;
const MIN_VIDEO_HEIGHT = MIN_VIDEO_WIDTH * VIDEO_ASPECT;
const MIN_WINDOW_HEIGHT = MIN_VIDEO_HEIGHT + OPTIONS_BAR_HEIGHT;
const editorEmitter = new EventEmitter();

const getEditorName = (filePath, isNewRecording) => isNewRecording ? `New Recording ${moment().format('YYYY-MM-DD')} at ${moment().format('H.mm.ss')}` : path.basename(filePath);

const openEditorWindow = async (filePath, recordFps, {isNewRecording} = {isNewRecording: false}) => {
const openEditorWindow = async (filePath, {recordedFps, isNewRecording, originalFilePath} = {}) => {
if (editors.has(filePath)) {
editors.get(filePath).show();
return;
}

const fps = recordFps || await getFps(filePath);
const fps = recordedFps || await getFps(filePath);

const editorWindow = new BrowserWindow({
title: getEditorName(filePath, isNewRecording),
title: getEditorName(originalFilePath || filePath, isNewRecording),
minWidth: MIN_VIDEO_WIDTH,
minHeight: MIN_WINDOW_HEIGHT,
width: MIN_VIDEO_WIDTH,
Expand Down Expand Up @@ -68,9 +72,12 @@ const openEditorWindow = async (filePath, recordFps, {isNewRecording} = {isNewRe
});
}

editorWindow.on('blur', () => editorEmitter.emit('blur'));
editorWindow.on('focus', () => editorEmitter.emit('focus'));

editorWindow.webContents.on('did-finish-load', async () => {
ipc.callRenderer(editorWindow, 'export-options', exportOptions);
await ipc.callRenderer(editorWindow, 'file', {filePath, fps});
await ipc.callRenderer(editorWindow, 'file', {filePath, fps, originalFilePath});
editorWindow.show();
});
};
Expand All @@ -81,8 +88,11 @@ const setOptions = options => {

const getEditors = () => editors.values();

ipc.answerRenderer('save-original', ({inputPath, outputPath}) => promisify(fs.copyFile)(inputPath, outputPath, fs.constants.COPYFILE_FICLONE));

module.exports = {
openEditorWindow,
setOptions,
getEditors
getEditors,
editorEmitter
};
2 changes: 1 addition & 1 deletion main/export-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ class ExportList {
track('export/history/opened/recording');
const exp = this.exports.find(exp => exp.createdAt === createdAt);
if (exp) {
openEditorWindow(exp.inputPath, exp.originalFps);
openEditorWindow(exp.previewPath, {recordedFps: exp.originalFps, originalFilePath: exp.inputPath});
}
}
}
Expand Down
1 change: 1 addition & 0 deletions main/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Export {
constructor(options) {
this.exportOptions = options.exportOptions;
this.inputPath = options.inputPath;
this.previewPath = options.previewPath;
this.pluginName = options.pluginName;
this.plugin = new Plugin(options.pluginName);
this.service = this.plugin.getSerivce(options.serviceTitle);
Expand Down
14 changes: 5 additions & 9 deletions main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ const plugins = require('./common/plugins');
const {initializeAnalytics} = require('./common/analytics');
const initializeExportList = require('./export-list');
const {openCropperWindow, isCropperOpen} = require('./cropper');
const {openEditorWindow} = require('./editor');
const {track} = require('./common/analytics');
const {initializeGlobalAccelerators} = require('./global-accelerators');
const {setApplicationMenu} = require('./menus');
const openFiles = require('./utils/open-files');

require('./utils/sentry');

Expand All @@ -26,7 +26,7 @@ app.on('open-file', (event, path) => {

if (app.isReady()) {
track('editor/opened/running');
openEditorWindow(path);
openFiles(path);
} else {
filesToOpen.push(path);
}
Expand Down Expand Up @@ -74,14 +74,10 @@ const checkForUpdates = () => {
initializeGlobalAccelerators();
setApplicationMenu();

for (const file of filesToOpen) {
if (filesToOpen.length > 0) {
track('editor/opened/startup');
openEditorWindow(file);
}

const {wasOpenedAtLogin} = app.getLoginItemSettings();
const isOpeningFile = filesToOpen.length > 0;
if (!isOpeningFile && !wasOpenedAtLogin) {
openFiles(...filesToOpen);
} else if (!app.getLoginItemSettings().wasOpenedAtLogin) {
openCropperWindow();
}

Expand Down
31 changes: 26 additions & 5 deletions main/menus.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
'use strict';

const os = require('os');
const {Menu, app, dialog} = require('electron');
const {Menu, app, dialog, BrowserWindow} = require('electron');
const {openNewGitHubIssue, appMenu} = require('electron-util');
const ipc = require('electron-better-ipc');

const {supportedVideoExtensions} = require('./common/constants');
const {openPrefsWindow} = require('./preferences');
const {openExportsWindow} = require('./exports');
const {openAboutWindow} = require('./about');
const {openEditorWindow} = require('./editor');
const {closeAllCroppers} = require('./cropper');
const {editorEmitter} = require('./editor');
const openFiles = require('./utils/open-files');

const issueBody = `
<!--
Expand Down Expand Up @@ -47,9 +50,7 @@ const openFileItem = {
properties: ['openFile']
}, filePaths => {
if (filePaths) {
for (const file of filePaths) {
openEditorWindow(file);
}
openFiles(...filePaths);
}
});
}
Expand Down Expand Up @@ -121,6 +122,17 @@ const applicationMenuTemplate = [
{
type: 'separator'
},
{
label: 'Save Original…',
id: 'saveOriginal',
accelerator: 'Command+S',
click: () => {
ipc.callRenderer(BrowserWindow.getFocusedWindow(), 'save-original');
}
},
{
type: 'separator'
},
{
role: 'close'
}
Expand Down Expand Up @@ -162,6 +174,7 @@ const cogExportsItem = cogMenu.getMenuItemById('exports');

const applicationMenu = Menu.buildFromTemplate(applicationMenuTemplate);
const applicationExportsItem = applicationMenu.getMenuItemById('exports');
const applicationSaveOriginalItem = applicationMenu.getMenuItemById('saveOriginal');

const toggleExportMenuItem = enabled => {
cogExportsItem.enabled = enabled;
Expand All @@ -172,6 +185,14 @@ const setApplicationMenu = () => {
Menu.setApplicationMenu(applicationMenu);
};

editorEmitter.on('blur', () => {
applicationSaveOriginalItem.visible = false;
});

editorEmitter.on('focus', () => {
applicationSaveOriginalItem.visible = true;
});

module.exports = {
cogMenu,
toggleExportMenuItem,
Expand Down
15 changes: 4 additions & 11 deletions main/tray.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
const {Tray} = require('electron');
const path = require('path');

const {supportedVideoExtensions} = require('./common/constants');
const {openCropperWindow} = require('./cropper');
const {openEditorWindow} = require('./editor');
const {cogMenu} = require('./menus');
const {track} = require('./common/analytics');
const openFiles = require('./utils/open-files');

let tray = null;
const fileExtensions = supportedVideoExtensions.map(ext => `.${ext}`);

const openContextMenu = () => {
tray.popUpContextMenu(cogMenu);
Expand All @@ -20,14 +18,9 @@ const initializeTray = () => {
tray = new Tray(path.join(__dirname, '..', 'static', 'menubarDefaultTemplate.png'));
tray.on('click', openCropperWindow);
tray.on('right-click', openContextMenu);
tray.on('drop-files', (event, files) => {
for (const file of files) {
const extension = path.extname(file).toLowerCase();
if (fileExtensions.includes(extension)) {
track('editor/opened/tray');
openEditorWindow(file);
}
}
tray.on('drop-files', (_, files) => {
track('editor/opened/tray');
openFiles(...files);
});

return tray;
Expand Down
41 changes: 41 additions & 0 deletions main/utils/encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* eslint-disable array-element-newline */
'use strict';
const path = require('path');
const tmp = require('tmp');
const ffmpeg = require('@ffmpeg-installer/ffmpeg');
const util = require('electron-util');
const execa = require('execa');

const {track} = require('../common/analytics');

const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path);

const getEncoding = async filePath => {
try {
await execa(ffmpegPath, ['-i', filePath]);
} catch (error) {
return /.*: Video: (.*?) \(.*/.exec(error.stderr)[1];
}
};

// `ffmpeg -i original.mp4 -vcodec libx264 -crf 27 -preset veryfast -c:a copy output.mp4`
const convertToH264 = async inputPath => {
const outputPath = tmp.tmpNameSync({postfix: path.extname(inputPath)});
track('encoding/converted/hevc');

await execa(ffmpegPath, [
'-i', inputPath,
'-vcodec', 'libx264',
'-crf', '27',
'-preset', 'veryfast',
'-c:a', 'copy',
outputPath
]);

return outputPath;
};

module.exports = {
getEncoding,
convertToH264
};
2 changes: 2 additions & 0 deletions main/utils/icon.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use strict';

const fileIcon = require('file-icon');

const getAppIcon = async () => {
Expand Down
25 changes: 25 additions & 0 deletions main/utils/open-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';
const path = require('path');

const {supportedVideoExtensions} = require('../common/constants');
const {openEditorWindow} = require('../editor');
const {getEncoding, convertToH264} = require('./encoding');

const fileExtensions = supportedVideoExtensions.map(ext => `.${ext}`);

const openFiles = (...filePaths) => {
return Promise.all(
filePaths
.filter(filePath => fileExtensions.includes(path.extname(filePath).toLowerCase()))
.map(async filePath => {
const encoding = await getEncoding(filePath);
if (encoding.toLowerCase() === 'hevc') {
openEditorWindow(await convertToH264(filePath), {originalFilePath: filePath});
} else {
openEditorWindow(filePath);
}
})
);
};

module.exports = openFiles;
27 changes: 23 additions & 4 deletions renderer/containers/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export default class EditorContainer extends Container {
this.videoContainer = videoContainer;
}

mount = (filePath, fps = 15, resolve) => {
mount = (filePath, fps = 15, originalFilePath, resolve) => {
const src = `file://${filePath}`;
this.finishLoading = resolve;

this.setState({src, filePath, fps, originalFps: fps, wasMuted: false});
this.setState({src, filePath, originalFilePath, fps, originalFps: fps, wasMuted: false});
this.videoContainer.setSrc(src);
}

Expand Down Expand Up @@ -98,6 +98,24 @@ export default class EditorContainer extends Container {
this.setState(updates);
}

saveOriginal = () => {
const {filePath, originalFilePath} = this.state;
const {remote} = electron;

const now = moment();

const path = remote.dialog.showSaveDialog(remote.BrowserWindow.getFocusedWindow(), {
defaultPath: `Kapture ${now.format('YYYY-MM-DD')} at ${now.format('H.mm.ss')}.mp4`
});

const ipc = require('electron-better-ipc');

ipc.callMain('save-original', {
inputPath: originalFilePath || filePath,
outputPath: path
});
}

selectFormat = format => {
const {plugin, options, wasMuted} = this.state;
const {plugins} = options.find(option => option.format === format);
Expand Down Expand Up @@ -170,7 +188,7 @@ export default class EditorContainer extends Container {
}

startExport = () => {
const {width, height, fps, filePath, options, format, plugin: serviceTitle, originalFps} = this.state;
const {width, height, fps, filePath, originalFilePath, options, format, plugin: serviceTitle, originalFps} = this.state;
const {startTime, endTime, isMuted} = this.videoContainer.state;

const plugin = options.find(option => option.format === format).plugins.find(p => p.title === serviceTitle);
Expand All @@ -185,7 +203,8 @@ export default class EditorContainer extends Container {
endTime,
isMuted
},
inputPath: filePath,
inputPath: originalFilePath || filePath,
previewPath: filePath,
pluginName,
isDefault,
serviceTitle,
Expand Down
Loading

0 comments on commit fb017c8

Please sign in to comment.