Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for pausing the recording #1132

Merged
merged 11 commits into from
Oct 27, 2022
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Or install with [Homebrew-Cask](https://caskroom.github.io):
brew install --cask kap
```

## How To Use Kap

Click the menu bar icon to bring up the screen recorder. After selecting what portion of the screen you'd like to record, hit the record button to start recording. Click the menu bar icon again to stop the recording.

> Tip: While recording, Option-click the menu bar icon to pause or right-click for more options.

## Contribute

Read the [contribution guide](contributing.md).
Expand Down
78 changes: 76 additions & 2 deletions main/aperture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {windowManager} from './windows/manager';
import {setRecordingTray, disableTray, resetTray} from './tray';
import {setRecordingTray, setPausedTray, disableTray, resetTray} from './tray';
import {setCropperShortcutAction} from './global-accelerators';
import {settings} from './common/settings';
import {track} from './common/analytics';
Expand All @@ -12,6 +12,7 @@ import {Recording} from './video';
import {ApertureOptions, StartRecordingOptions} from './common/types';
import {InstalledPlugin} from './plugins/plugin';
import {RecordService, RecordServiceHook} from './plugins/service';
import {getCurrentDurationStart, getOverallDuration, setCurrentDurationStart, setOverallDuration} from './utils/track-duration';

const createAperture = require('aperture');
const aperture = createAperture();
Expand Down Expand Up @@ -138,6 +139,8 @@ export const startRecording = async (options: StartRecordingOptions) => {

try {
const filePath = await aperture.startRecording(apertureOptions);
setOverallDuration(0);
setCurrentDurationStart(Date.now());

setCurrentRecording({
filePath,
Expand All @@ -162,7 +165,7 @@ export const startRecording = async (options: StartRecordingOptions) => {

console.log(`Started recording after ${startTime}s`);
windowManager.cropper?.setRecording();
setRecordingTray(stopRecording);
setRecordingTray();
setCropperShortcutAction(stopRecording);
past = Date.now();

Expand Down Expand Up @@ -194,6 +197,8 @@ export const stopRecording = async () => {

try {
filePath = await aperture.stopRecording();
setOverallDuration(0);
setCurrentDurationStart(0);
} catch (error) {
track('recording/stopped/error');
showError(error as any, {title: 'Recording error', plugin: undefined});
Expand All @@ -216,3 +221,72 @@ export const stopRecording = async () => {
stopCurrentRecording(recordingName);
}
};

export const stopRecordingWithNoEdit = async () => {
// Ensure we only stop recording once
if (!past) {
return;
}

console.log(`Stopped recording after ${(Date.now() - past) / 1000}s`);
past = undefined;

try {
await aperture.stopRecording();
setOverallDuration(0);
setCurrentDurationStart(0);
} catch (error) {
track('recording/quit/error');
showError(error as any, {title: 'Recording error', plugin: undefined});
cleanup();
return;
}

try {
cleanup();
} finally {
track('recording/quit');
stopCurrentRecording(recordingName);
}
};

export const pauseRecording = async () => {
// Ensure we only pause if there's a recording in progress and if it's currently not paused
const isPaused = await aperture.isPaused();
if (!past || isPaused) {
return;
}

try {
await aperture.pause();
setOverallDuration(getOverallDuration() + (Date.now() - getCurrentDurationStart()));
setCurrentDurationStart(0);
setPausedTray();
track('recording/paused');
console.log(`Paused recording after ${(Date.now() - past) / 1000}s`);
} catch (error) {
track('recording/paused/error');
showError(error as any, {title: 'Recording error', plugin: undefined});
cleanup();
}
};

export const resumeRecording = async () => {
// Ensure we only resume if there's a recording in progress and if it's currently paused
const isPaused = await aperture.isPaused();
if (!past || !isPaused) {
return;
}

try {
await aperture.resume();
setCurrentDurationStart(Date.now());
setRecordingTray();
track('recording/resumed');
console.log(`Resume recording after ${(Date.now() - past) / 1000}s`);
} catch (error) {
track('recording/resumed/error');
showError(error as any, {title: 'Recording error', plugin: undefined});
cleanup();
}
};
13 changes: 11 additions & 2 deletions main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ import {setupRemoteStates} from './remote-states';
import {setUpExportsListeners} from './export';
import {windowManager} from './windows/manager';
import {setupProtocol} from './utils/protocol';
import {stopRecordingWithNoEdit} from './aperture';

const prepareNext = require('electron-next');

const filesToOpen: string[] = [];

let onExitCleanupComplete = false;

app.commandLine.appendSwitch('--enable-features', 'OverlayScrollbar');

app.on('open-file', (event, path) => {
Expand Down Expand Up @@ -133,6 +136,12 @@ app.on('will-finish-launching', () => {
});
});

app.on('quit', () => {
cleanPastRecordings();
app.on('before-quit', async (event: any) => {
if (!onExitCleanupComplete) {
event.preventDefault();
await stopRecordingWithNoEdit();
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
cleanPastRecordings();
onExitCleanupComplete = true;
app.quit();
}
});
57 changes: 57 additions & 0 deletions main/menus/record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {Menu} from 'electron';
import {MenuItemId, MenuOptions} from './utils';
import {pauseRecording, resumeRecording, stopRecording} from '../aperture';
import formatTime from '../utils/format-time';
import {getCurrentDurationStart, getOverallDuration} from '../utils/track-duration';

const getDurationLabel = () => {
if (getCurrentDurationStart() <= 0) {
return formatTime((getOverallDuration()) / 1000, undefined);
}

return formatTime((getOverallDuration() + (Date.now() - getCurrentDurationStart())) / 1000, undefined);
};

const getDurationMenuItem = () => ({
id: MenuItemId.duration,
label: getDurationLabel(),
enabled: false
});

const getStopRecordingMenuItem = () => ({
id: MenuItemId.stopRecording,
label: 'Stop',
click: stopRecording
});

const getPauseRecordingMenuItem = () => ({
id: MenuItemId.pauseRecording,
label: 'Pause',
click: pauseRecording
});

const getResumeRecordingMenuItem = () => ({
id: MenuItemId.resumeRecording,
label: 'Resume',
click: resumeRecording
});

export const getRecordMenuTemplate = (isPaused: boolean): MenuOptions => [
getDurationMenuItem(),
{
type: 'separator'
},
isPaused ? getResumeRecordingMenuItem() : getPauseRecordingMenuItem(),
getStopRecordingMenuItem(),
{
type: 'separator'
},
{
role: 'quit',
accelerator: 'Command+Q'
}
];

export const getRecordMenu = async (isPaused: boolean) => {
return Menu.buildFromTemplate(getRecordMenuTemplate(isPaused));
};
6 changes: 5 additions & 1 deletion main/menus/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ export enum MenuItemId {
app = 'app',
saveOriginal = 'saveOriginal',
plugins = 'plugins',
audioDevices = 'audioDevices'
audioDevices = 'audioDevices',
stopRecording = 'stopRecording',
pauseRecording = 'pauseRecording',
resumeRecording = 'resumeRecording',
duration = 'duration'
}

export const getCurrentMenuItem = (id: MenuItemId) => {
Expand Down
40 changes: 38 additions & 2 deletions main/tray.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
'use strict';

import {Tray} from 'electron';
import {KeyboardEvent} from 'electron/main';
import path from 'path';
import {getCogMenu} from './menus/cog';
import {getRecordMenu} from './menus/record';
import {track} from './common/analytics';
import {openFiles} from './utils/open-files';
import {windowManager} from './windows/manager';
import {pauseRecording, resumeRecording, stopRecording} from './aperture';

let tray: Tray;
let trayAnimation: NodeJS.Timeout | undefined;
Expand All @@ -14,6 +17,14 @@ const openContextMenu = async () => {
tray.popUpContextMenu(await getCogMenu());
};

const openRecordingContextMenu = async () => {
tray.popUpContextMenu(await getRecordMenu(false));
};

const openPausedContextMenu = async () => {
tray.popUpContextMenu(await getRecordMenu(true));
};

const openCropperWindow = () => windowManager.cropper?.open();

export const initializeTray = () => {
Expand All @@ -39,17 +50,42 @@ export const resetTray = () => {
}

tray.removeAllListeners('click');
tray.removeAllListeners('right-click');

tray.setImage(path.join(__dirname, '..', 'static', 'menubarDefaultTemplate.png'));
tray.on('click', openCropperWindow);
tray.on('right-click', openContextMenu);
};

export const setRecordingTray = (stopRecording: () => void) => {
export const setRecordingTray = () => {
animateIcon();

tray.removeAllListeners('right-click');

// TODO: figure out why this is marked as missing. It's defined properly in the electron.d.ts file
tray.once('click', stopRecording);
tray.once('click', onRecordingTrayClick);
tray.on('right-click', openRecordingContextMenu);
};

export const setPausedTray = () => {
if (trayAnimation) {
clearTimeout(trayAnimation);
}

tray.removeAllListeners('right-click');

tray.setImage(path.join(__dirname, '..', 'static', 'pauseTemplate.png'));
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
tray.once('click', resumeRecording);
tray.on('right-click', openPausedContextMenu);
};

const onRecordingTrayClick = (event: KeyboardEvent) => {
if (event.altKey) {
pauseRecording();
return;
}

stopRecording();
};

const animateIcon = async () => new Promise<void>(resolve => {
Expand Down
22 changes: 22 additions & 0 deletions main/utils/format-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import moment from 'moment';

const formatTime = (time: number, options: any) => {
options = {
showMilliseconds: false,
...options
};

const durationFormatted = options.extra ?
` (${format(options.extra, options)})` :
'';

return `${format(time, options)}${durationFormatted}`;
};

const format = (time: number, {showMilliseconds} = {showMilliseconds: false}) => {
const formatString = `${time >= 60 * 60 ? 'hh:m' : ''}m:ss${showMilliseconds ? '.SS' : ''}`;

return moment().startOf('day').millisecond(time * 1000).format(formatString);
};

export default formatTime;
15 changes: 15 additions & 0 deletions main/utils/track-duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// TODO: Add interface to aperture-node for getting recording duration instead of using this https://github.com/wulkano/aperture-node/issues/29
let overallDuration = 0;
let currentDurationStart = 0;

export const getOverallDuration = (): number => overallDuration;

export const getCurrentDurationStart = (): number => currentDurationStart;

export const setOverallDuration = (duration: number): void => {
overallDuration = duration;
};

export const setCurrentDurationStart = (duration: number): void => {
currentDurationStart = duration;
};
Binary file added static/pauseTemplate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.