Skip to content

Commit

Permalink
Add support for HEVC, use ffmpeg-static (#1021)
Browse files Browse the repository at this point in the history
  • Loading branch information
codetheweb authored Aug 11, 2021
1 parent 45a7a52 commit bc60c67
Show file tree
Hide file tree
Showing 18 changed files with 168 additions and 82 deletions.
6 changes: 3 additions & 3 deletions main/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import {Format} from './types';
export const supportedVideoExtensions = ['mp4', 'mov', 'm4v'];

const formatExtensions = new Map([
['av1', 'mp4']
['av1', 'mp4'],
['hevc', 'mp4']
]);

export const formats = [Format.mp4, Format.av1, Format.gif, Format.apng, Format.webm];
export const formats = [Format.mp4, Format.hevc, Format.av1, Format.gif, Format.apng, Format.webm];

export const getFormatExtension = (format: Format) => formatExtensions.get(format) ?? format;

export const defaultInputDeviceId = 'SYSTEM_DEFAULT';

1 change: 1 addition & 0 deletions main/common/types/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Rectangle} from 'electron';

export enum Format {
gif = 'gif',
hevc = 'hevc',
mp4 = 'mp4',
webm = 'webm',
apng = 'apng',
Expand Down
33 changes: 33 additions & 0 deletions main/converters/h264.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,38 @@ const convertToAv1 = (options: ConvertOptions) => convert(options.outputPath, {
options.outputPath
));

// eslint-disable-next-line @typescript-eslint/promise-function-async
const convertToHevc = (options: ConvertOptions) => convert(options.outputPath, {
onProgress: (progress, estimate) => {
options.onProgress('Converting', progress, estimate);
},
startTime: options.startTime,
endTime: options.endTime
}, conditionalArgs(
'-i', options.inputPath,
'-r', options.fps.toString(),
'-c:v', 'libx265',
'-c:a', 'libopus',
'-preset', 'medium',
'-tag:v', 'hvc1', // Metadata for macOS
{
args: ['-an'],
if: options.shouldMute
},
{
args: [
'-s',
`${makeEven(options.width)}x${makeEven(options.height)}`,
'-ss',
options.startTime.toString(),
'-to',
options.endTime.toString()
],
if: options.shouldCrop || !areDimensionsEven(options)
},
options.outputPath
));

// eslint-disable-next-line @typescript-eslint/promise-function-async
const convertToApng = (options: ConvertOptions) => convert(options.outputPath, {
onProgress: (progress, estimate) => {
Expand Down Expand Up @@ -250,6 +282,7 @@ export const crop = (options: ConvertOptions) => convert(options.outputPath, {
export default new Map([
[Format.gif, convertToGif],
[Format.mp4, convertToMp4],
[Format.hevc, convertToHevc],
[Format.webm, convertToWebm],
[Format.apng, convertToApng],
[Format.av1, convertToAv1]
Expand Down
5 changes: 2 additions & 3 deletions main/converters/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import {track} from '../common/analytics';
import {conditionalArgs, extractProgressFromStderr} from './utils';
import {settings} from '../common/settings';

const ffmpeg = require('@ffmpeg-installer/ffmpeg');
const gifsicle = require('gifsicle');
import ffmpegPath from '../utils/ffmpeg-path';

const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path);
const gifsicle = require('gifsicle');
const gifsiclePath = util.fixPathForAsarUnpack(gifsicle);

enum Mode {
Expand Down
3 changes: 1 addition & 2 deletions main/plugins/built-in/open-with-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const getAppsForFormat = (format: Format) => {
});
};

const appsForFormat = (['mp4', 'gif', 'apng', 'webm', 'av1'] as Format[])
const appsForFormat = (['mp4', 'gif', 'apng', 'webm', 'av1', 'hevc'] as Format[])
.map(format => ({
format,
apps: getAppsForFormat(format)
Expand All @@ -44,4 +44,3 @@ export const shareServices = [{
formats: [...apps.keys()],
action
}];

7 changes: 4 additions & 3 deletions main/plugins/built-in/save-file-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const saveFile = {
'mp4',
'webm',
'apng',
'av1'
'av1',
'hevc'
],
action
};
Expand All @@ -53,7 +54,8 @@ const filterMap = new Map([
[Format.webm, [{name: 'Movies', extensions: ['webm']}]],
[Format.gif, [{name: 'Images', extensions: ['gif']}]],
[Format.apng, [{name: 'Images', extensions: ['apng']}]],
[Format.av1, [{name: 'Movies', extensions: ['mp4']}]]
[Format.av1, [{name: 'Movies', extensions: ['mp4']}]],
[Format.hevc, [{name: 'Movies', extensions: ['mp4']}]]
]);

let lastSavedDirectory: string;
Expand Down Expand Up @@ -83,4 +85,3 @@ export const askForTargetFilePath = async (

return undefined;
};

4 changes: 1 addition & 3 deletions main/recording-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import {shell, clipboard} from 'electron';
import fs from 'fs';
import Store from 'electron-store';
import util from 'electron-util';
import execa from 'execa';
import tempy from 'tempy';
import {SetOptional} from 'type-fest';
Expand All @@ -16,8 +15,7 @@ import {Video} from './video';
import {ApertureOptions} from './common/types';
import Sentry, {isSentryEnabled} from './utils/sentry';

const ffmpeg = require('@ffmpeg-installer/ffmpeg');
const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path);
import ffmpegPath from './utils/ffmpeg-path';

export interface PastRecording {
filePath: string;
Expand Down
12 changes: 9 additions & 3 deletions main/remote-states/editor-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {prettifyFormat} from '../utils/formats';
const exportUsageHistory = new Store<{[key in Format]: {lastUsed: number; plugins: Record<string, number>}}>({
name: 'export-usage-history',
defaults: {
gif: {lastUsed: 5, plugins: {default: 1}},
mp4: {lastUsed: 4, plugins: {default: 1}},
webm: {lastUsed: 3, plugins: {default: 1}},
gif: {lastUsed: 6, plugins: {default: 1}},
mp4: {lastUsed: 5, plugins: {default: 1}},
webm: {lastUsed: 4, plugins: {default: 1}},
hevc: {lastUsed: 3, plugins: {default: 1}},
av1: {lastUsed: 2, plugins: {default: 1}},
apng: {lastUsed: 1, plugins: {default: 1}}
}
Expand Down Expand Up @@ -44,6 +45,11 @@ const fpsUsageHistory = new Store<{[key in Format]: number}>({
type: 'number',
minimum: 0,
default: 60
},
hevc: {
type: 'number',
minimum: 0,
default: 60
}
}
});
Expand Down
5 changes: 1 addition & 4 deletions main/utils/encoding.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
/* eslint-disable array-element-newline */

import path from 'path';
import util from 'electron-util';
import execa from 'execa';
import tempy from 'tempy';
import {track} from '../common/analytics';

const ffmpeg = require('@ffmpeg-installer/ffmpeg');
const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path);
import ffmpegPath from './ffmpeg-path';

export const getEncoding = async (filePath: string) => {
try {
Expand Down
6 changes: 6 additions & 0 deletions main/utils/ffmpeg-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import ffmpeg from 'ffmpeg-static';
import util from 'electron-util';

const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg);

export default ffmpegPath;
1 change: 1 addition & 0 deletions main/utils/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Format} from '../common/types';

const formats = new Map([
[Format.gif, 'GIF'],
[Format.hevc, 'MP4 (H265)'],
[Format.mp4, 'MP4 (H264)'],
[Format.av1, 'MP4 (AV1)'],
[Format.webm, 'WebM'],
Expand Down
5 changes: 1 addition & 4 deletions main/utils/fps.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import util from 'electron-util';
import execa from 'execa';

const ffmpeg = require('@ffmpeg-installer/ffmpeg');
const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path);
import ffmpegPath from './ffmpeg-path';

const getFps = async (filePath: string) => {
try {
Expand Down
4 changes: 1 addition & 3 deletions main/utils/image-preview.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
/* eslint-disable array-element-newline */

import {BrowserWindow, dialog} from 'electron';
import util from 'electron-util';
import execa from 'execa';
import tempy from 'tempy';
import {promisify} from 'util';
import type {Video} from '../video';
import {generateTimestampedName} from './timestamped-name';
import ffmpegPath from './ffmpeg-path';

const base64Img = require('base64-img');
const ffmpeg = require('@ffmpeg-installer/ffmpeg');
const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path);

const getBase64 = promisify(base64Img.base64);

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"name": "Kap"
},
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.0.20",
"@sentry/browser": "^6.2.2",
"@sentry/electron": "^2.4.0",
"@sindresorhus/to-milliseconds": "^1.2.0",
Expand All @@ -53,6 +52,7 @@
"electron-util": "^0.14.2",
"ensure-error": "^3.0.1",
"execa": "5.0.0",
"ffmpeg-static": "^4.4.0",
"file-icon": "^3.0.0",
"gifsicle": "^5.2.0",
"got": "^9.6.0",
Expand Down Expand Up @@ -96,6 +96,7 @@
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@sindresorhus/tsconfig": "^0.7.0",
"@types/ffmpeg-static": "^3.0.0",
"@types/got": "9.6.11",
"@types/insight": "^0.8.0",
"@types/lodash": "^4.14.168",
Expand Down
3 changes: 2 additions & 1 deletion renderer/hooks/editor/use-editor-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const useEditorOptions = createRemoteStateHook<EditorOptionsRemoteState>('editor
mp4: 60,
av1: 60,
webm: 60,
apng: 60
apng: 60,
hevc: 60
}
});

Expand Down
56 changes: 56 additions & 0 deletions test/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,59 @@ test('av1: non-retina', async t => {

t.false(meta.hasAudio);
});

// HEVC

test('HEVC: retina', async t => {
const onProgress = sinon.fake();

t.context.outputPath = await convert(Format.hevc, {
shouldMute: true,
inputPath: retinaInput,
fps: 15,
width: 469,
height: 839,
startTime: 30,
endTime: 43.5,
shouldCrop: true,
onProgress
});

const meta = await getVideoMetadata(t.context.outputPath);

// Makes dimensions even
t.is(meta.size.width, 470);
t.is(meta.size.height, 840);

t.is(meta.fps, 15);
t.is(meta.encoding, 'hevc');

t.false(meta.hasAudio);

t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number));
t.true(onProgress.calledWithMatch(sinon.match.string, sinon.match.number, sinon.match.string));
});

test('HEVC: non-retina', async t => {
t.context.outputPath = await convert(Format.hevc, {
shouldMute: true,
inputPath: input,
fps: 15,
width: 255,
height: 143,
startTime: 11.5,
endTime: 27,
shouldCrop: true
});

const meta = await getVideoMetadata(t.context.outputPath);

// Makes dimensions even
t.is(meta.size.width, 256);
t.is(meta.size.height, 144);

t.is(meta.fps, 15);
t.is(meta.encoding, 'hevc');

t.false(meta.hasAudio);
});
4 changes: 1 addition & 3 deletions test/helpers/video-utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import moment from 'moment';
import execa from 'execa';

const ffmpeg = require('@ffmpeg-installer/ffmpeg');

const ffmpegPath = ffmpeg.path;
const ffmpegPath = require('ffmpeg-static');

const getDuration = (text: string): number => {
const durationString = /Duration: ([\d:.]*)/.exec(text)?.[1];
Expand Down
Loading

0 comments on commit bc60c67

Please sign in to comment.