Skip to content

Commit e11b3df

Browse files
Recover recordings after unexpected exit/crash (#899)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 63a239a commit e11b3df

21 files changed

+802
-25
lines changed

docs/plugins.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ The record service is a plain object defining some metadata and hooks:
240240
- `didStartRecording`: Function that is called after the recording starts. [Read more below.](#hooks)
241241
- `didStopRecording`: Function that is called after the recording stops. [Read more below.](#hooks)
242242
- `willEnable`: Function that is called when the user enables the service. [Read more below.](#hooks)
243+
- `cleanUp`: Function that is called if Kap exited unexpectedly last time it was run (for example, if it crashed), without the `didStopRecording` hook being called. This hook will only receive the `persistedState` object from the `state` passed to the rest of the hooks. Use this to clean up any effects introduced when the recording started and don't automatically clear out once Kap stops. For example, if your plugin killed a running app with intent to restart it after the recording was over, you can use `cleanUp` to ensure the app is properly restarted even in the event that Kap crashed, so the `didStopRecording` wasn't called.
243244

244245
The `config`, `configDescription` and hook properties are optional.
245246

@@ -286,7 +287,8 @@ You can use this to check if you have enough permissions for the service to work
286287

287288
The hook functions receive a `context` argument with some metadata and utility methods.
288289

289-
- `.state`: A plain empty object that will be shared and passed to all hooks in the same recording process. It can be useful to persist data between the different hooks.
290+
- `.state`: An object that will be shared and passed to all hooks in the same recording process. It can be useful to persist data between the different hooks.
291+
- `state.persistedState`: An object under `state` which should only contain serializable fields. It will be passed to the `cleanUp` hook if Kap didn't shut down correctly last time it was run. Use this to store fields necessary to clean up remaining effects.
290292
- `.apertureOptions`: An object with the options passed to [Aperture](https://github.com/wulkano/aperture-node). The API is described [here](https://github.com/wulkano/aperture-node#options).
291293
- `.config`: Get and set config for your plugin. It’s an instance of [`electron-store`](https://github.com/sindresorhus/electron-store#instance).
292294
- `.request()`: Do a network request, like uploading. It’s a wrapper around [`got`](https://github.com/sindresorhus/got).

main/common/aperture.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const plugins = require('./plugins');
1717
const {getAudioDevices} = require('../utils/devices');
1818
const {showError} = require('../utils/errors');
1919
const {RecordServiceContext} = require('../service-context');
20+
const {setCurrentRecording, updatePluginState, stopCurrentRecording} = require('../recording-history');
2021

2122
const aperture = createAperture();
2223
const {videoCodecs} = createAperture;
@@ -35,6 +36,20 @@ const setRecordingName = name => {
3536
recordingName = name;
3637
};
3738

39+
const serializeEditPluginState = () => {
40+
const result = {};
41+
42+
for (const {plugin, service} of recordingPlugins) {
43+
if (!result[plugin.name]) {
44+
result[plugin.name] = {};
45+
}
46+
47+
result[plugin.name][service.title] = serviceState.get(service.title).persistedState;
48+
}
49+
50+
return result;
51+
};
52+
3853
const callPlugins = async method => Promise.all(recordingPlugins.map(async ({plugin, service}) => {
3954
if (service[method] && typeof service[method] === 'function') {
4055
try {
@@ -126,14 +141,21 @@ const startRecording = async options => {
126141
);
127142

128143
for (const {service, plugin} of recordingPlugins) {
129-
serviceState.set(service.title, {});
144+
serviceState.set(service.title, {persistedState: {}});
130145
track(`plugins/used/record/${plugin.name}`);
131146
}
132147

133148
await callPlugins('willStartRecording');
134149

135150
try {
136-
await aperture.startRecording(apertureOptions);
151+
const filePath = await aperture.startRecording(apertureOptions);
152+
153+
setCurrentRecording({
154+
filePath,
155+
name: recordingName,
156+
apertureOptions,
157+
editPlugins: serializeEditPluginState()
158+
});
137159
} catch (error) {
138160
track('recording/stopped/error');
139161
showError(error, {title: 'Recording error'});
@@ -167,6 +189,7 @@ const startRecording = async options => {
167189
});
168190

169191
await callPlugins('didStartRecording');
192+
updatePluginState(serializeEditPluginState());
170193
};
171194

172195
const stopRecording = async () => {
@@ -200,8 +223,10 @@ const stopRecording = async () => {
200223
// if (recordHevc) {
201224
// openEditorWindow(await convertToH264(filePath), {recordedFps, isNewRecording: true, originalFilePath: filePath});
202225
// } else {
203-
openEditorWindow(filePath, {recordedFps, isNewRecording: true, recordingName});
226+
await openEditorWindow(filePath, {recordedFps, isNewRecording: true, recordingName});
204227
// }
228+
229+
stopCurrentRecording(recordingName);
205230
}
206231
};
207232

main/common/plugins.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,19 @@ class Plugins {
3434
this.updateExportOptions = updateExportOptions;
3535
}
3636

37-
async enableService(service) {
37+
async enableService(service, plugin) {
3838
const wasEnabled = recordPluginServiceState.get(service.title) || false;
3939

4040
if (wasEnabled) {
4141
recordPluginServiceState.set(service.title, false);
4242
return this.refreshRecordPluginServices();
4343
}
4444

45+
if (!plugin.config.validServices.includes(service.title)) {
46+
openPrefsWindow({target: {name: plugin.name, action: 'configure'}});
47+
return;
48+
}
49+
4550
if (service.willEnable) {
4651
try {
4752
const canEnable = await service.willEnable();
@@ -69,7 +74,7 @@ class Plugins {
6974
plugin => plugin.recordServices.map(service => ({
7075
...service,
7176
isEnabled: recordPluginServiceState.get(service.title) || false,
72-
toggleEnabled: () => this.enableService(service)
77+
toggleEnabled: () => this.enableService(service, plugin)
7378
}))
7479
)
7580
);

main/editor.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ const EventEmitter = require('events');
77
const pify = require('pify');
88
const {ipcMain: ipc} = require('electron-better-ipc');
99
const {is} = require('electron-util');
10-
const moment = require('moment');
1110

1211
const getFps = require('./utils/fps');
1312
const loadRoute = require('./utils/routes');
13+
const {generateTimestampedName} = require('./utils/timestamped-name');
1414

1515
const editors = new Map();
1616
let allOptions;
@@ -22,7 +22,7 @@ const MIN_WINDOW_HEIGHT = MIN_VIDEO_HEIGHT + OPTIONS_BAR_HEIGHT;
2222
const editorEmitter = new EventEmitter();
2323
const editorsWithNotSavedDialogs = new Map();
2424

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

2727
const openEditorWindow = async (
2828
filePath,
@@ -121,10 +121,8 @@ const getEditors = () => editors.values();
121121
const getEditor = path => editors.get(path);
122122

123123
ipc.answerRenderer('save-original', async ({inputPath}) => {
124-
const now = moment();
125-
126124
const {filePath} = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow(), {
127-
defaultPath: `Kapture ${now.format('YYYY-MM-DD')} at ${now.format('H.mm.ss')}.mp4`
125+
defaultPath: generateTimestampedName('Kapture', '.mp4')
128126
});
129127

130128
if (filePath) {

main/export-list.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const ffmpeg = require('@ffmpeg-installer/ffmpeg');
1010
const util = require('electron-util');
1111
const execa = require('execa');
1212
const makeDir = require('make-dir');
13-
const moment = require('moment');
1413

1514
const settings = require('./common/settings');
1615
const {track} = require('./common/analytics');
@@ -20,6 +19,7 @@ const {openEditorWindow} = require('./editor');
2019
const {toggleExportMenuItem} = require('./menus');
2120
const Export = require('./export');
2221
const {ensureDockIsShowingSync} = require('./utils/dock');
22+
const {generateTimestampedName} = require('./utils/timestamped-name');
2323

2424
const ffmpegPath = util.fixPathForAsarUnpack(ffmpeg.path);
2525
let lastSavedDirectory;
@@ -58,10 +58,8 @@ const getDragIcon = async inputPath => {
5858
};
5959

6060
const saveSnapshot = async ({inputPath, time}) => {
61-
const now = moment();
62-
6361
const {filePath: outputPath} = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow(), {
64-
defaultPath: `Snapshot ${now.format('YYYY-MM-DD')} at ${now.format('H.mm.ss')}.jpg`
62+
defaultPath: generateTimestampedName('Snapshot', '.jpg')
6563
});
6664

6765
if (outputPath) {

main/export.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
const path = require('path');
44
const PCancelable = require('p-cancelable');
5-
const moment = require('moment');
65

76
const {track} = require('./common/analytics');
87
const {convertTo} = require('./convert');
98
const {ShareServiceContext} = require('./service-context');
109
const {getFormatExtension} = require('./common/constants');
1110
const PluginConfig = require('./utils/plugin-config');
11+
const {generateTimestampedName} = require('./utils/timestamped-name');
1212

1313
class Export {
1414
constructor(options) {
@@ -41,8 +41,7 @@ class Export {
4141
this.isSaveFileService = options.sharePlugin.pluginName === '_saveToDisk';
4242
this.disableOutputActions = false;
4343

44-
const now = moment();
45-
const fileName = options.recordingName || (options.isNewRecording ? `Kapture ${now.format('YYYY-MM-DD')} at ${now.format('H.mm.ss')}` : path.parse(this.inputPath).name);
44+
const fileName = options.recordingName || (options.isNewRecording ? generateTimestampedName('Kapture') : path.parse(this.inputPath).name);
4645
this.defaultFileName = `${fileName}.${getFormatExtension(this.format)}`;
4746

4847
this.context = new ShareServiceContext({

main/index.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {initializeExportOptions} = require('./export-options');
2020
const settings = require('./common/settings');
2121
const {hasMicrophoneAccess, ensureScreenCapturePermissions} = require('./common/system-permissions');
2222
const {handleDeepLink} = require('./utils/deep-linking');
23+
const {hasActiveRecording, cleanPastRecordings} = require('./recording-history');
2324

2425
require('./utils/sentry');
2526
require('./utils/errors').setupErrorHandling();
@@ -88,21 +89,23 @@ const checkForUpdates = () => {
8889
initializeExportOptions();
8990
setApplicationMenu();
9091

92+
if (!app.isDefaultProtocolClient('kap')) {
93+
app.setAsDefaultProtocolClient('kap');
94+
}
95+
9196
if (filesToOpen.length > 0) {
9297
track('editor/opened/startup');
9398
openFiles(...filesToOpen);
99+
hasActiveRecording();
94100
} else if (
101+
!(await hasActiveRecording()) &&
95102
!app.getLoginItemSettings().wasOpenedAtLogin &&
96103
ensureScreenCapturePermissions() &&
97104
(!settings.get('recordAudio') || hasMicrophoneAccess())
98105
) {
99106
openCropperWindow();
100107
}
101108

102-
if (!app.isDefaultProtocolClient('kap')) {
103-
app.setAsDefaultProtocolClient('kap');
104-
}
105-
106109
checkForUpdates();
107110
})();
108111

@@ -125,3 +128,7 @@ app.on('will-finish-launching', () => {
125128
handleDeepLink(url);
126129
});
127130
});
131+
132+
app.on('quit', () => {
133+
cleanPastRecordings();
134+
});

0 commit comments

Comments
 (0)