dashcam 1.0.1-beta.6 → 1.0.1-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.js +5 -73
- package/lib/ffmpeg.js +1 -2
- package/lib/processManager.js +61 -33
- package/lib/recorder.js +5 -5
- package/lib/uploader.js +1 -2
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Background recording process for dashcam CLI
|
|
4
|
+
* This script runs detached from the parent process to handle long-running recordings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { startRecording, stopRecording } from '../lib/recorder.js';
|
|
8
|
+
import { upload } from '../lib/uploader.js';
|
|
9
|
+
import { logger, setVerbose } from '../lib/logger.js';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
|
|
14
|
+
// Get process directory for status files
|
|
15
|
+
const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
|
|
16
|
+
const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
|
|
17
|
+
const RESULT_FILE = path.join(PROCESS_DIR, 'upload-result.json');
|
|
18
|
+
|
|
19
|
+
// Parse options from command line argument
|
|
20
|
+
const optionsJson = process.argv[2];
|
|
21
|
+
if (!optionsJson) {
|
|
22
|
+
console.error('No options provided to background process');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const options = JSON.parse(optionsJson);
|
|
27
|
+
|
|
28
|
+
// Enable verbose logging in background
|
|
29
|
+
setVerbose(true);
|
|
30
|
+
|
|
31
|
+
logger.info('Background recording process started', {
|
|
32
|
+
pid: process.pid,
|
|
33
|
+
options
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Write status file
|
|
37
|
+
function writeStatus(status) {
|
|
38
|
+
try {
|
|
39
|
+
fs.writeFileSync(STATUS_FILE, JSON.stringify({
|
|
40
|
+
...status,
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
pid: process.pid
|
|
43
|
+
}, null, 2));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.error('Failed to write status file', { error });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Write upload result file
|
|
50
|
+
function writeUploadResult(result) {
|
|
51
|
+
try {
|
|
52
|
+
logger.info('Writing upload result to file', { path: RESULT_FILE, shareLink: result.shareLink });
|
|
53
|
+
fs.writeFileSync(RESULT_FILE, JSON.stringify({
|
|
54
|
+
...result,
|
|
55
|
+
timestamp: Date.now()
|
|
56
|
+
}, null, 2));
|
|
57
|
+
logger.info('Successfully wrote upload result to file');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.error('Failed to write upload result file', { error });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Main recording function
|
|
64
|
+
async function runBackgroundRecording() {
|
|
65
|
+
let recordingResult = null;
|
|
66
|
+
let isShuttingDown = false;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Start the recording
|
|
70
|
+
const recordingOptions = {
|
|
71
|
+
fps: parseInt(options.fps) || 10,
|
|
72
|
+
includeAudio: options.audio || false,
|
|
73
|
+
customOutputPath: options.output || null
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
logger.info('Starting recording with options', { recordingOptions });
|
|
77
|
+
|
|
78
|
+
recordingResult = await startRecording(recordingOptions);
|
|
79
|
+
|
|
80
|
+
// Write status to track the recording
|
|
81
|
+
writeStatus({
|
|
82
|
+
isRecording: true,
|
|
83
|
+
startTime: recordingResult.startTime,
|
|
84
|
+
options,
|
|
85
|
+
pid: process.pid,
|
|
86
|
+
outputPath: recordingResult.outputPath
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
logger.info('Recording started successfully', {
|
|
90
|
+
outputPath: recordingResult.outputPath,
|
|
91
|
+
startTime: recordingResult.startTime
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Set up signal handlers for graceful shutdown
|
|
95
|
+
const handleShutdown = async (signal) => {
|
|
96
|
+
if (isShuttingDown) {
|
|
97
|
+
logger.info('Shutdown already in progress...');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
isShuttingDown = true;
|
|
101
|
+
|
|
102
|
+
logger.info(`Received ${signal}, stopping background recording...`);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Stop the recording
|
|
106
|
+
const stopResult = await stopRecording();
|
|
107
|
+
|
|
108
|
+
if (stopResult) {
|
|
109
|
+
logger.info('Recording stopped successfully', {
|
|
110
|
+
outputPath: stopResult.outputPath,
|
|
111
|
+
duration: stopResult.duration
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Upload the recording
|
|
115
|
+
logger.info('Starting upload...');
|
|
116
|
+
const uploadResult = await upload(stopResult.outputPath, {
|
|
117
|
+
title: options.title || 'Dashcam Recording',
|
|
118
|
+
description: options.description || 'Recorded with Dashcam CLI',
|
|
119
|
+
project: options.project || options.k,
|
|
120
|
+
duration: stopResult.duration,
|
|
121
|
+
clientStartDate: stopResult.clientStartDate,
|
|
122
|
+
apps: stopResult.apps,
|
|
123
|
+
logs: stopResult.logs,
|
|
124
|
+
gifPath: stopResult.gifPath,
|
|
125
|
+
snapshotPath: stopResult.snapshotPath
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
logger.info('Upload complete', { shareLink: uploadResult.shareLink });
|
|
129
|
+
|
|
130
|
+
// Write upload result for stop command to read
|
|
131
|
+
writeUploadResult({
|
|
132
|
+
shareLink: uploadResult.shareLink,
|
|
133
|
+
replayId: uploadResult.replay?.id
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Update status to indicate recording stopped
|
|
138
|
+
writeStatus({
|
|
139
|
+
isRecording: false,
|
|
140
|
+
completedTime: Date.now(),
|
|
141
|
+
pid: process.pid
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
logger.info('Background process exiting successfully');
|
|
145
|
+
process.exit(0);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.error('Error during shutdown:', error);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
153
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
154
|
+
|
|
155
|
+
// Keep the process alive
|
|
156
|
+
logger.info('Background recording is now running. Waiting for stop signal...');
|
|
157
|
+
await new Promise(() => {}); // Wait indefinitely for signals
|
|
158
|
+
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger.error('Background recording failed:', error);
|
|
161
|
+
|
|
162
|
+
// Update status to indicate failure
|
|
163
|
+
writeStatus({
|
|
164
|
+
isRecording: false,
|
|
165
|
+
error: error.message,
|
|
166
|
+
pid: process.pid
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Run the background recording
|
|
174
|
+
runBackgroundRecording().catch(error => {
|
|
175
|
+
logger.error('Fatal error in background process:', error);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
package/bin/dashcam.js
CHANGED
|
@@ -117,8 +117,8 @@ async function recordingAction(options, command) {
|
|
|
117
117
|
process.exit(1);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
//
|
|
121
|
-
log('Starting recording...');
|
|
120
|
+
// Start recording in background mode
|
|
121
|
+
log('Starting recording in background...');
|
|
122
122
|
|
|
123
123
|
try {
|
|
124
124
|
const result = await processManager.startRecording({
|
|
@@ -130,81 +130,13 @@ async function recordingAction(options, command) {
|
|
|
130
130
|
project: options.project || options.k // Support both -p and -k for project
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
log(
|
|
133
|
+
log(`✅ Recording started successfully (PID: ${result.pid})`);
|
|
134
134
|
log(`Output: ${result.outputPath}`);
|
|
135
|
+
log('');
|
|
135
136
|
log('Use "dashcam status" to check progress');
|
|
136
137
|
log('Use "dashcam stop" to stop recording and upload');
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
log('Recording is running in background...');
|
|
140
|
-
|
|
141
|
-
// Set up signal handlers for graceful shutdown
|
|
142
|
-
let isShuttingDown = false;
|
|
143
|
-
const handleShutdown = async (signal) => {
|
|
144
|
-
if (isShuttingDown) {
|
|
145
|
-
log('Shutdown already in progress...');
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
isShuttingDown = true;
|
|
149
|
-
|
|
150
|
-
log(`\nReceived ${signal}, stopping background recording...`);
|
|
151
|
-
try {
|
|
152
|
-
// Stop the recording using the recorder directly (not processManager)
|
|
153
|
-
const { stopRecording } = await import('../lib/recorder.js');
|
|
154
|
-
const stopResult = await stopRecording();
|
|
155
|
-
|
|
156
|
-
if (stopResult) {
|
|
157
|
-
log('Recording stopped:', stopResult.outputPath);
|
|
158
|
-
|
|
159
|
-
// Import and call upload function with the correct format
|
|
160
|
-
const { upload } = await import('../lib/uploader.js');
|
|
161
|
-
|
|
162
|
-
log('Starting upload...');
|
|
163
|
-
const uploadResult = await upload(stopResult.outputPath, {
|
|
164
|
-
title: options.title || 'Dashcam Recording',
|
|
165
|
-
description: description || 'Recorded with Dashcam CLI',
|
|
166
|
-
project: options.project || options.k,
|
|
167
|
-
duration: stopResult.duration,
|
|
168
|
-
clientStartDate: stopResult.clientStartDate,
|
|
169
|
-
apps: stopResult.apps,
|
|
170
|
-
logs: stopResult.logs,
|
|
171
|
-
gifPath: stopResult.gifPath,
|
|
172
|
-
snapshotPath: stopResult.snapshotPath
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Write upload result for stop command to read
|
|
176
|
-
processManager.writeUploadResult({
|
|
177
|
-
shareLink: uploadResult.shareLink,
|
|
178
|
-
replayId: uploadResult.replay?.id
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Output based on format option (for create/markdown mode)
|
|
182
|
-
if (options.md) {
|
|
183
|
-
const replayId = uploadResult.replay?.id;
|
|
184
|
-
const shareKey = uploadResult.shareLink.split('share=')[1];
|
|
185
|
-
log(`[](${uploadResult.shareLink})`);
|
|
186
|
-
log('');
|
|
187
|
-
log(`Watch [Dashcam - ${options.title || 'New Replay'}](${uploadResult.shareLink}) on Dashcam`);
|
|
188
|
-
} else {
|
|
189
|
-
log('✅ Upload complete!');
|
|
190
|
-
log('📹 Watch your recording:', uploadResult.shareLink);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Clean up process files, but preserve upload result for stop command
|
|
195
|
-
processManager.cleanup({ preserveResult: true });
|
|
196
|
-
} catch (error) {
|
|
197
|
-
logError('Error during shutdown:', error.message);
|
|
198
|
-
logger.error('Error during shutdown:', error);
|
|
199
|
-
}
|
|
200
|
-
process.exit(0);
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
204
|
-
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
205
|
-
|
|
206
|
-
// Keep the process alive
|
|
207
|
-
await new Promise(() => {});
|
|
139
|
+
process.exit(0);
|
|
208
140
|
} catch (error) {
|
|
209
141
|
logError('Failed to start recording:', error.message);
|
|
210
142
|
process.exit(1);
|
package/lib/ffmpeg.js
CHANGED
|
@@ -18,8 +18,7 @@ export async function createSnapshot(inputVideoPath, outputSnapshotPath, snapsho
|
|
|
18
18
|
'-i', inputVideoPath,
|
|
19
19
|
'-frames:v', '1',
|
|
20
20
|
'-vf', 'scale=640:-1:force_original_aspect_ratio=decrease:eval=frame',
|
|
21
|
-
'-
|
|
22
|
-
'-compression_level', '100',
|
|
21
|
+
'-compression_level', '6', // Use default compression (was 100, which is extremely slow)
|
|
23
22
|
outputSnapshotPath,
|
|
24
23
|
'-y',
|
|
25
24
|
'-hide_banner'
|
package/lib/processManager.js
CHANGED
|
@@ -2,8 +2,12 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
5
6
|
import { logger } from './logger.js';
|
|
6
7
|
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
7
11
|
// Use a fixed directory in the user's home directory for cross-process communication
|
|
8
12
|
const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
|
|
9
13
|
const PID_FILE = path.join(PROCESS_DIR, 'recording.pid');
|
|
@@ -222,44 +226,68 @@ class ProcessManager {
|
|
|
222
226
|
throw new Error('Recording already in progress');
|
|
223
227
|
}
|
|
224
228
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
// Always run in background mode by spawning a detached process
|
|
230
|
+
logger.info('Starting recording in background mode');
|
|
231
|
+
|
|
232
|
+
// Get the path to the CLI binary
|
|
233
|
+
const binPath = path.join(__dirname, '..', 'bin', 'dashcam-background.js');
|
|
234
|
+
|
|
235
|
+
logger.debug('Background process path', { binPath, exists: fs.existsSync(binPath) });
|
|
236
|
+
|
|
237
|
+
// Create log files for background process
|
|
238
|
+
const logDir = PROCESS_DIR;
|
|
239
|
+
const stdoutLog = path.join(logDir, 'background-stdout.log');
|
|
240
|
+
const stderrLog = path.join(logDir, 'background-stderr.log');
|
|
241
|
+
|
|
242
|
+
const stdoutFd = fs.openSync(stdoutLog, 'a');
|
|
243
|
+
const stderrFd = fs.openSync(stderrLog, 'a');
|
|
244
|
+
|
|
245
|
+
// Spawn a detached process that will handle the recording
|
|
246
|
+
const backgroundProcess = spawn(process.execPath, [
|
|
247
|
+
binPath,
|
|
248
|
+
JSON.stringify(options)
|
|
249
|
+
], {
|
|
250
|
+
detached: true,
|
|
251
|
+
stdio: ['ignore', stdoutFd, stderrFd], // Log stdout and stderr
|
|
252
|
+
env: {
|
|
253
|
+
...process.env,
|
|
254
|
+
DASHCAM_BACKGROUND: 'true'
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Close the file descriptors in the parent process
|
|
259
|
+
fs.closeSync(stdoutFd);
|
|
260
|
+
fs.closeSync(stderrFd);
|
|
234
261
|
|
|
235
|
-
|
|
262
|
+
// Get the background process PID before unreffing
|
|
263
|
+
const backgroundPid = backgroundProcess.pid;
|
|
264
|
+
|
|
265
|
+
// Allow the parent process to exit independently
|
|
266
|
+
backgroundProcess.unref();
|
|
236
267
|
|
|
237
|
-
|
|
268
|
+
// Wait a moment for the background process to initialize
|
|
269
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
238
270
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
pid: process.pid,
|
|
246
|
-
outputPath: result.outputPath
|
|
247
|
-
});
|
|
271
|
+
// Read the status file to get recording details
|
|
272
|
+
const status = this.readStatus();
|
|
273
|
+
|
|
274
|
+
if (!status || !status.isRecording) {
|
|
275
|
+
throw new Error('Background process failed to start recording');
|
|
276
|
+
}
|
|
248
277
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
startTime: result.startTime
|
|
252
|
-
});
|
|
278
|
+
// Write PID file so other commands can find the background process
|
|
279
|
+
this.writePid(status.pid);
|
|
253
280
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
281
|
+
logger.info('Background recording process started', {
|
|
282
|
+
pid: status.pid,
|
|
283
|
+
outputPath: status.outputPath
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
pid: status.pid,
|
|
288
|
+
outputPath: status.outputPath,
|
|
289
|
+
startTime: status.startTime
|
|
290
|
+
};
|
|
263
291
|
}
|
|
264
292
|
|
|
265
293
|
async gracefulExit() {
|
package/lib/recorder.js
CHANGED
|
@@ -279,13 +279,13 @@ export async function startRecording({
|
|
|
279
279
|
const platformArgs = await getPlatformArgs({ fps, includeAudio });
|
|
280
280
|
const outputArgs = [
|
|
281
281
|
'-c:v', 'libvpx-vp9', // Use VP9 codec for better quality and compression
|
|
282
|
-
'-quality', '
|
|
283
|
-
'-cpu-used', '
|
|
284
|
-
'-deadline', '
|
|
285
|
-
'-b:v', '
|
|
282
|
+
'-quality', 'realtime', // Use realtime quality preset for lower CPU usage
|
|
283
|
+
'-cpu-used', '5', // Faster encoding (0-5, higher = faster but lower quality)
|
|
284
|
+
'-deadline', 'realtime', // Realtime encoding mode for lower latency/CPU
|
|
285
|
+
'-b:v', '2M', // Lower bitrate to reduce CPU load (was 5M)
|
|
286
286
|
// Remove explicit pixel format to let ffmpeg handle conversion automatically
|
|
287
287
|
'-r', fps.toString(), // Ensure output framerate matches input
|
|
288
|
-
'-g', '
|
|
288
|
+
'-g', '60', // Keyframe every 60 frames (reduced frequency)
|
|
289
289
|
// WebM options for more frequent disk writes
|
|
290
290
|
'-f', 'webm', // Force WebM container format
|
|
291
291
|
'-flush_packets', '1', // Flush packets immediately to disk
|
package/lib/uploader.js
CHANGED
|
@@ -445,8 +445,7 @@ export async function upload(filePath, metadata = {}) {
|
|
|
445
445
|
|
|
446
446
|
logExit();
|
|
447
447
|
|
|
448
|
-
|
|
449
|
-
const shareLink = newReplay.replay.shareLink?.replace('app.dashcam.io', 'app.testdriver.ai') || newReplay.replay.shareLink;
|
|
448
|
+
const shareLink = newReplay.replay.shareLink;
|
|
450
449
|
|
|
451
450
|
return {
|
|
452
451
|
replay: newReplay.replay,
|