dashcam 1.0.1-beta.2 → 1.0.1-beta.21
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/.github/workflows/publish.yml +26 -20
- package/691cc08dc2fc02f59ae66f08 (1).mp4 +0 -0
- package/NPM_PUBLISH_FIX.md +104 -0
- package/SINGLE_FRAME_VIDEO_FIX.md +129 -0
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.js +276 -132
- package/lib/ffmpeg.js +1 -2
- package/lib/logs/index.js +67 -11
- package/lib/processManager.js +104 -42
- package/lib/recorder.js +128 -27
- package/lib/tracking/FileTracker.js +7 -0
- package/lib/tracking/LogsTracker.js +21 -7
- package/lib/tracking/icons/index.js +3 -2
- package/lib/tracking/icons/linux.js +277 -0
- package/lib/uploader.js +10 -3
- package/package.json +4 -1
- package/scripts/sync-version.sh +48 -0
- package/test-short-recording.js +287 -0
- package/test_workflow.sh +99 -25
- package/.github/workflows/build.yml +0 -103
- package/.github/workflows/release.yml +0 -107
package/bin/dashcam.js
CHANGED
|
@@ -26,7 +26,7 @@ if (!fs.existsSync(APP.recordingsDir)) {
|
|
|
26
26
|
|
|
27
27
|
program
|
|
28
28
|
.name('dashcam')
|
|
29
|
-
.description('
|
|
29
|
+
.description('Capture the steps to reproduce every bug.')
|
|
30
30
|
.version(APP.version)
|
|
31
31
|
.option('-v, --verbose', 'Enable verbose logging output')
|
|
32
32
|
.hook('preAction', (thisCommand) => {
|
|
@@ -39,8 +39,8 @@ program
|
|
|
39
39
|
|
|
40
40
|
program
|
|
41
41
|
.command('auth')
|
|
42
|
-
.description(
|
|
43
|
-
.argument('<
|
|
42
|
+
.description("Authenticate the dashcam desktop using a team's apiKey")
|
|
43
|
+
.argument('<api-key>', 'Your team API key')
|
|
44
44
|
.action(async (apiKey, options, command) => {
|
|
45
45
|
try {
|
|
46
46
|
logger.verbose('Starting authentication process', {
|
|
@@ -75,121 +75,199 @@ program
|
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
// Shared recording action to avoid duplication
|
|
79
|
+
async function recordingAction(options, command) {
|
|
80
|
+
try {
|
|
81
|
+
const silent = options.silent;
|
|
82
|
+
const log = (...args) => { if (!silent) console.log(...args); };
|
|
83
|
+
const logError = (...args) => { if (!silent) console.error(...args); };
|
|
84
|
+
|
|
85
|
+
// Check if recording is already active
|
|
86
|
+
if (processManager.isRecordingActive()) {
|
|
87
|
+
const status = processManager.getActiveStatus();
|
|
88
|
+
const duration = ((Date.now() - status.startTime) / 1000).toFixed(1);
|
|
89
|
+
log('Recording already in progress');
|
|
90
|
+
log(`Duration: ${duration} seconds`);
|
|
91
|
+
log(`PID: ${status.pid}`);
|
|
92
|
+
log('Use "dashcam stop" to stop the recording');
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check authentication
|
|
97
|
+
if (!await auth.isAuthenticated()) {
|
|
98
|
+
log('You need to login first. Run: dashcam auth <api-key>');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for piped input (description from stdin) if description option not set
|
|
103
|
+
let description = options.description;
|
|
104
|
+
if (!description && !process.stdin.isTTY) {
|
|
105
|
+
const chunks = [];
|
|
106
|
+
for await (const chunk of process.stdin) {
|
|
107
|
+
chunks.push(chunk);
|
|
108
|
+
}
|
|
109
|
+
description = Buffer.concat(chunks).toString('utf-8');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check screen recording permissions (macOS only)
|
|
113
|
+
const { ensurePermissions } = await import('../lib/permissions.js');
|
|
114
|
+
const hasPermissions = await ensurePermissions();
|
|
115
|
+
if (!hasPermissions) {
|
|
116
|
+
log('\n⚠️ Cannot start recording without screen recording permission.');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Start recording in background mode
|
|
121
|
+
log('Starting recording in background...');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = await processManager.startRecording({
|
|
125
|
+
fps: parseInt(options.fps) || 30,
|
|
126
|
+
audio: options.audio,
|
|
127
|
+
output: options.output,
|
|
128
|
+
title: options.title,
|
|
129
|
+
description: description,
|
|
130
|
+
project: options.project || options.k // Support both -p and -k for project
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
log(`✅ Recording started successfully (PID: ${result.pid})`);
|
|
134
|
+
log(`Output: ${result.outputPath}`);
|
|
135
|
+
log('');
|
|
136
|
+
log('Use "dashcam status" to check progress');
|
|
137
|
+
log('Use "dashcam stop" to stop recording and upload');
|
|
138
|
+
|
|
139
|
+
process.exit(0);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logError('Failed to start recording:', error.message);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error('Failed to start recording:', error);
|
|
146
|
+
if (!options.silent) console.error('Failed to start recording:', error.message);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 'create' command - creates a clip from current recording (like stop but with more options)
|
|
78
152
|
program
|
|
79
|
-
.command('
|
|
80
|
-
.description('
|
|
81
|
-
.option('-
|
|
82
|
-
.option('-
|
|
83
|
-
.option('
|
|
84
|
-
.option('-
|
|
85
|
-
.
|
|
86
|
-
.option('-p, --project <project>', 'Project ID to upload the recording to')
|
|
87
|
-
.action(async (options, command) => {
|
|
153
|
+
.command('create')
|
|
154
|
+
.description('Create a clip and output the resulting url or markdown. Will launch desktop app for local editing before publishing.')
|
|
155
|
+
.option('-t, --title <string>', 'Title of the replay. Automatically generated if not supplied.')
|
|
156
|
+
.option('-d, --description [text]', 'Replay markdown body. This may also be piped in: `cat README.md | dashcam create`')
|
|
157
|
+
.option('--md', 'Returns code for a rich markdown image link.')
|
|
158
|
+
.option('-k, --project <project>', 'Project ID to publish to')
|
|
159
|
+
.action(async (options) => {
|
|
88
160
|
try {
|
|
89
|
-
// Check
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
process.exit(0);
|
|
161
|
+
// Check for piped input (description from stdin)
|
|
162
|
+
let description = options.description;
|
|
163
|
+
if (!description && !process.stdin.isTTY) {
|
|
164
|
+
const chunks = [];
|
|
165
|
+
for await (const chunk of process.stdin) {
|
|
166
|
+
chunks.push(chunk);
|
|
167
|
+
}
|
|
168
|
+
description = Buffer.concat(chunks).toString('utf-8');
|
|
98
169
|
}
|
|
99
170
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
console.log('
|
|
103
|
-
process.exit(
|
|
171
|
+
if (!processManager.isRecordingActive()) {
|
|
172
|
+
console.log('No active recording to create clip from');
|
|
173
|
+
console.log('Start a recording first with "dashcam record" or "dashcam start"');
|
|
174
|
+
process.exit(0);
|
|
104
175
|
}
|
|
105
176
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
177
|
+
const activeStatus = processManager.getActiveStatus();
|
|
178
|
+
|
|
179
|
+
console.log('Creating clip from recording...');
|
|
180
|
+
|
|
181
|
+
const result = await processManager.stopActiveRecording();
|
|
182
|
+
|
|
183
|
+
if (!result) {
|
|
184
|
+
console.log('Failed to stop recording');
|
|
111
185
|
process.exit(1);
|
|
112
186
|
}
|
|
113
187
|
|
|
114
|
-
|
|
115
|
-
console.log('Starting recording...');
|
|
188
|
+
console.log('Recording stopped successfully');
|
|
116
189
|
|
|
190
|
+
// Upload the recording
|
|
191
|
+
console.log('Uploading clip...');
|
|
117
192
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
193
|
+
const uploadResult = await upload(result.outputPath, {
|
|
194
|
+
title: options.title || activeStatus?.options?.title || 'Dashcam Recording',
|
|
195
|
+
description: description || activeStatus?.options?.description,
|
|
196
|
+
project: options.project || options.k || activeStatus?.options?.project,
|
|
197
|
+
duration: result.duration,
|
|
198
|
+
clientStartDate: result.clientStartDate,
|
|
199
|
+
apps: result.apps,
|
|
200
|
+
icons: result.icons,
|
|
201
|
+
gifPath: result.gifPath,
|
|
202
|
+
snapshotPath: result.snapshotPath
|
|
125
203
|
});
|
|
126
|
-
|
|
127
|
-
console.log(`Recording started successfully (PID: ${result.pid})`);
|
|
128
|
-
console.log(`Output: ${result.outputPath}`);
|
|
129
|
-
console.log('Use "dashcam status" to check progress');
|
|
130
|
-
console.log('Use "dashcam stop" to stop recording and upload');
|
|
131
|
-
|
|
132
|
-
// Keep this process alive for background recording
|
|
133
|
-
console.log('Recording is running in background...');
|
|
134
|
-
|
|
135
|
-
// Set up signal handlers for graceful shutdown
|
|
136
|
-
let isShuttingDown = false;
|
|
137
|
-
const handleShutdown = async (signal) => {
|
|
138
|
-
if (isShuttingDown) {
|
|
139
|
-
console.log('Shutdown already in progress...');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
isShuttingDown = true;
|
|
143
|
-
|
|
144
|
-
console.log(`\nReceived ${signal}, stopping background recording...`);
|
|
145
|
-
try {
|
|
146
|
-
// Stop the recording using the recorder directly (not processManager)
|
|
147
|
-
const { stopRecording } = await import('../lib/recorder.js');
|
|
148
|
-
const stopResult = await stopRecording();
|
|
149
|
-
|
|
150
|
-
if (stopResult) {
|
|
151
|
-
console.log('Recording stopped:', stopResult.outputPath);
|
|
152
|
-
|
|
153
|
-
// Import and call upload function with the correct format
|
|
154
|
-
const { upload } = await import('../lib/uploader.js');
|
|
155
|
-
|
|
156
|
-
console.log('Starting upload...');
|
|
157
|
-
await upload(stopResult.outputPath, {
|
|
158
|
-
title: options.title || 'Dashcam Recording',
|
|
159
|
-
description: options.description || 'Recorded with Dashcam CLI',
|
|
160
|
-
project: options.project,
|
|
161
|
-
duration: stopResult.duration,
|
|
162
|
-
clientStartDate: stopResult.clientStartDate,
|
|
163
|
-
apps: stopResult.apps,
|
|
164
|
-
logs: stopResult.logs,
|
|
165
|
-
gifPath: stopResult.gifPath,
|
|
166
|
-
snapshotPath: stopResult.snapshotPath
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
console.log('Upload completed successfully!');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Clean up process files
|
|
173
|
-
processManager.cleanup();
|
|
174
|
-
} catch (error) {
|
|
175
|
-
console.error('Error during shutdown:', error.message);
|
|
176
|
-
logger.error('Error during shutdown:', error);
|
|
177
|
-
}
|
|
178
|
-
process.exit(0);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
182
|
-
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
183
204
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
205
|
+
// Output based on format option
|
|
206
|
+
if (options.md) {
|
|
207
|
+
const replayId = uploadResult.replay?.id;
|
|
208
|
+
const shareKey = uploadResult.shareLink.split('share=')[1];
|
|
209
|
+
console.log(`[](${uploadResult.shareLink})`);
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log(`Watch [Dashcam - ${options.title || 'New Replay'}](${uploadResult.shareLink}) on Dashcam`);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(uploadResult.shareLink);
|
|
214
|
+
}
|
|
215
|
+
} catch (uploadError) {
|
|
216
|
+
console.error('Upload failed:', uploadError.message);
|
|
217
|
+
console.log('Recording saved locally:', result.outputPath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.exit(0);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
logger.error('Error creating clip:', error);
|
|
223
|
+
console.error('Failed to create clip:', error.message);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// 'record' command - the main recording command with all options
|
|
229
|
+
program
|
|
230
|
+
.command('record')
|
|
231
|
+
.description('Start a recording terminal to be included in your dashcam video recording')
|
|
232
|
+
.option('-a, --audio', 'Include audio in the recording')
|
|
233
|
+
.option('-f, --fps <fps>', 'Frames per second (default: 30)', '30')
|
|
234
|
+
.option('-o, --output <path>', 'Custom output path')
|
|
235
|
+
.option('-t, --title <title>', 'Title for the recording')
|
|
236
|
+
.option('-d, --description <description>', 'Description for the recording (supports markdown)')
|
|
237
|
+
.option('-p, --project <project>', 'Project ID to upload the recording to')
|
|
238
|
+
.option('-s, --silent', 'Silent mode - suppress all output')
|
|
239
|
+
.action(recordingAction);
|
|
240
|
+
|
|
241
|
+
program
|
|
242
|
+
.command('pipe')
|
|
243
|
+
.description('Pipe command output to dashcam to be included in recorded video')
|
|
244
|
+
.action(async () => {
|
|
245
|
+
try {
|
|
246
|
+
// Check if recording is active
|
|
247
|
+
if (!processManager.isRecordingActive()) {
|
|
248
|
+
console.error('No active recording. Start a recording first with "dashcam record" or "dashcam start"');
|
|
188
249
|
process.exit(1);
|
|
189
250
|
}
|
|
251
|
+
|
|
252
|
+
// Read from stdin
|
|
253
|
+
const chunks = [];
|
|
254
|
+
for await (const chunk of process.stdin) {
|
|
255
|
+
chunks.push(chunk);
|
|
256
|
+
// Also output to stdout so pipe continues to work
|
|
257
|
+
process.stdout.write(chunk);
|
|
258
|
+
}
|
|
259
|
+
const content = Buffer.concat(chunks).toString('utf-8');
|
|
260
|
+
|
|
261
|
+
// Import the log tracker to add the piped content
|
|
262
|
+
const { logsTrackerManager } = await import('../lib/logs/index.js');
|
|
263
|
+
|
|
264
|
+
// Add piped content as a log entry
|
|
265
|
+
logsTrackerManager.addPipedLog(content);
|
|
266
|
+
|
|
267
|
+
process.exit(0);
|
|
190
268
|
} catch (error) {
|
|
191
|
-
logger.error('Failed to
|
|
192
|
-
console.error('Failed to
|
|
269
|
+
logger.error('Failed to pipe content:', error);
|
|
270
|
+
console.error('Failed to pipe content:', error.message);
|
|
193
271
|
process.exit(1);
|
|
194
272
|
}
|
|
195
273
|
});
|
|
@@ -216,43 +294,94 @@ program
|
|
|
216
294
|
|
|
217
295
|
|
|
218
296
|
|
|
297
|
+
// 'start' command - alias for record with simple instant replay mode
|
|
298
|
+
program
|
|
299
|
+
.command('start')
|
|
300
|
+
.description('Start instant replay recording on dashcam')
|
|
301
|
+
.action(async () => {
|
|
302
|
+
// Call recordingAction with minimal options for instant replay
|
|
303
|
+
await recordingAction({
|
|
304
|
+
fps: '30',
|
|
305
|
+
audio: false,
|
|
306
|
+
silent: false
|
|
307
|
+
}, null);
|
|
308
|
+
});
|
|
309
|
+
|
|
219
310
|
program
|
|
220
311
|
.command('track')
|
|
221
|
-
.description('
|
|
222
|
-
.option('--
|
|
223
|
-
.option('--
|
|
224
|
-
.option('--
|
|
312
|
+
.description('Add a logs config to Dashcam')
|
|
313
|
+
.option('--name <name>', 'Name for the tracking configuration (required)')
|
|
314
|
+
.option('--type <type>', 'Type of tracker: "application" or "web" (required)')
|
|
315
|
+
.option('--pattern <pattern>', 'Pattern to track (can be used multiple times)', (value, previous) => {
|
|
316
|
+
return previous ? previous.concat([value]) : [value];
|
|
317
|
+
})
|
|
318
|
+
.option('--web <pattern>', 'Web URL pattern to track (can use wildcards like *) - deprecated, use --type=web --pattern instead')
|
|
319
|
+
.option('--app <pattern>', 'Application file pattern to track (can use wildcards like *) - deprecated, use --type=application --pattern instead')
|
|
225
320
|
.action(async (options) => {
|
|
226
321
|
try {
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
322
|
+
// Support both old and new syntax
|
|
323
|
+
// New syntax: --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"
|
|
324
|
+
// Old syntax: --web <pattern> --app <pattern>
|
|
325
|
+
|
|
326
|
+
if (options.type && options.pattern) {
|
|
327
|
+
// New syntax validation
|
|
328
|
+
if (!options.name) {
|
|
329
|
+
console.error('Error: --name is required when using --type and --pattern');
|
|
330
|
+
console.log('Example: dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (options.type !== 'web' && options.type !== 'application') {
|
|
335
|
+
console.error('Error: --type must be either "web" or "application"');
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
240
338
|
|
|
241
|
-
await createPattern(config);
|
|
242
|
-
console.log('Web tracking pattern added successfully:', options.web);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (options.app) {
|
|
246
339
|
const config = {
|
|
247
|
-
name: options.name
|
|
248
|
-
type:
|
|
249
|
-
patterns:
|
|
340
|
+
name: options.name,
|
|
341
|
+
type: options.type,
|
|
342
|
+
patterns: options.pattern,
|
|
250
343
|
enabled: true
|
|
251
344
|
};
|
|
252
345
|
|
|
253
346
|
await createPattern(config);
|
|
254
|
-
console.log('Application tracking pattern added successfully
|
|
347
|
+
console.log(`${options.type === 'web' ? 'Web' : 'Application'} tracking pattern added successfully:`, options.name);
|
|
348
|
+
console.log('Patterns:', options.pattern.join(', '));
|
|
349
|
+
|
|
350
|
+
} else if (options.web || options.app) {
|
|
351
|
+
// Old syntax for backward compatibility
|
|
352
|
+
if (options.web) {
|
|
353
|
+
const config = {
|
|
354
|
+
name: options.name || 'Web Pattern',
|
|
355
|
+
type: 'web',
|
|
356
|
+
patterns: [options.web],
|
|
357
|
+
enabled: true
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
await createPattern(config);
|
|
361
|
+
console.log('Web tracking pattern added successfully:', options.web);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (options.app) {
|
|
365
|
+
const config = {
|
|
366
|
+
name: options.name || 'App Pattern',
|
|
367
|
+
type: 'application',
|
|
368
|
+
patterns: [options.app],
|
|
369
|
+
enabled: true
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
await createPattern(config);
|
|
373
|
+
console.log('Application tracking pattern added successfully:', options.app);
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
console.error('Error: Must provide either:');
|
|
377
|
+
console.log(' --name --type --pattern (new syntax)');
|
|
378
|
+
console.log(' --web or --app (old syntax)');
|
|
379
|
+
console.log('\nExamples:');
|
|
380
|
+
console.log(' dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
|
|
381
|
+
console.log(' dashcam track --web "*facebook.com*"');
|
|
382
|
+
process.exit(1);
|
|
255
383
|
}
|
|
384
|
+
|
|
256
385
|
process.exit(0);
|
|
257
386
|
} catch (error) {
|
|
258
387
|
console.error('Failed to add tracking pattern:', error.message);
|
|
@@ -287,7 +416,6 @@ program
|
|
|
287
416
|
}
|
|
288
417
|
|
|
289
418
|
console.log('Recording stopped successfully');
|
|
290
|
-
console.log('Output saved to:', result.outputPath);
|
|
291
419
|
|
|
292
420
|
// Check if files still exist - if not, background process already uploaded
|
|
293
421
|
const filesExist = fs.existsSync(result.outputPath) &&
|
|
@@ -295,8 +423,24 @@ program
|
|
|
295
423
|
(!result.snapshotPath || fs.existsSync(result.snapshotPath));
|
|
296
424
|
|
|
297
425
|
if (!filesExist) {
|
|
298
|
-
|
|
299
|
-
|
|
426
|
+
// Files were deleted, meaning background process uploaded
|
|
427
|
+
// Wait for the upload result to be written
|
|
428
|
+
logger.debug('Waiting for upload result from background process');
|
|
429
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
430
|
+
|
|
431
|
+
// Try to read the upload result from the background process
|
|
432
|
+
const uploadResult = processManager.readUploadResult();
|
|
433
|
+
logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
|
|
434
|
+
|
|
435
|
+
if (uploadResult && uploadResult.shareLink) {
|
|
436
|
+
console.log('📹 Watch your recording:', uploadResult.shareLink);
|
|
437
|
+
// Clean up the result file now that we've read it
|
|
438
|
+
processManager.cleanup();
|
|
439
|
+
} else {
|
|
440
|
+
console.log('✅ Recording uploaded (share link not available)');
|
|
441
|
+
logger.warn('Upload result not available from background process');
|
|
442
|
+
}
|
|
443
|
+
|
|
300
444
|
process.exit(0);
|
|
301
445
|
}
|
|
302
446
|
|
|
@@ -315,7 +459,7 @@ program
|
|
|
315
459
|
snapshotPath: result.snapshotPath
|
|
316
460
|
});
|
|
317
461
|
|
|
318
|
-
console.log('
|
|
462
|
+
console.log('📹 Watch your recording:', uploadResult.shareLink);
|
|
319
463
|
} catch (uploadError) {
|
|
320
464
|
console.error('Upload failed:', uploadError.message);
|
|
321
465
|
console.log('Recording saved locally:', result.outputPath);
|
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/logs/index.js
CHANGED
|
@@ -15,7 +15,13 @@ const CLI_CONFIG_FILE = path.join(process.cwd(), '.dashcam', 'cli-config.json');
|
|
|
15
15
|
|
|
16
16
|
// Simple trim function for CLI (adapted from desktop app)
|
|
17
17
|
async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipId) {
|
|
18
|
-
logger.info('Trimming logs', {
|
|
18
|
+
logger.info('Trimming logs', {
|
|
19
|
+
count: groupLogStatuses.length,
|
|
20
|
+
startMS,
|
|
21
|
+
endMS,
|
|
22
|
+
clientStartDate,
|
|
23
|
+
clientStartDateReadable: new Date(clientStartDate).toISOString()
|
|
24
|
+
});
|
|
19
25
|
|
|
20
26
|
const REPLAY_DIR = path.join(os.tmpdir(), 'dashcam', 'recordings');
|
|
21
27
|
|
|
@@ -36,12 +42,24 @@ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipI
|
|
|
36
42
|
let events = content;
|
|
37
43
|
|
|
38
44
|
// Convert events to relative time
|
|
39
|
-
let relativeEvents = events.map((event) => {
|
|
45
|
+
let relativeEvents = events.map((event, index) => {
|
|
46
|
+
const originalTime = event.time;
|
|
40
47
|
event.time = parseInt(event.time + '') - startMS;
|
|
41
48
|
// Check if it's not already relative time
|
|
42
49
|
if (event.time > 1_000_000_000_000) {
|
|
43
50
|
// relative time = absolute time - clip start time
|
|
44
51
|
event.time = event.time - clientStartDate;
|
|
52
|
+
if (index === 0) {
|
|
53
|
+
// Log first event for debugging
|
|
54
|
+
logger.info('First event timestamp conversion', {
|
|
55
|
+
originalTime,
|
|
56
|
+
clientStartDate,
|
|
57
|
+
relativeTime: event.time,
|
|
58
|
+
relativeSeconds: (event.time / 1000).toFixed(2),
|
|
59
|
+
startMS,
|
|
60
|
+
line: event.line ? event.line.substring(0, 50) : 'N/A'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
45
63
|
}
|
|
46
64
|
return event;
|
|
47
65
|
});
|
|
@@ -56,16 +74,19 @@ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipI
|
|
|
56
74
|
});
|
|
57
75
|
|
|
58
76
|
if (status.type === 'cli') {
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
logFile: name,
|
|
67
|
-
};
|
|
77
|
+
// For CLI logs, keep the file paths in logFile field for UI display
|
|
78
|
+
// No need to remap to numeric indices
|
|
79
|
+
|
|
80
|
+
// Create items array showing unique files and their event counts
|
|
81
|
+
const fileCounts = {};
|
|
82
|
+
filteredEvents.forEach(event => {
|
|
83
|
+
fileCounts[event.logFile] = (fileCounts[event.logFile] || 0) + 1;
|
|
68
84
|
});
|
|
85
|
+
|
|
86
|
+
status.items = Object.entries(fileCounts).map(([filePath, count]) => ({
|
|
87
|
+
item: filePath,
|
|
88
|
+
count
|
|
89
|
+
}));
|
|
69
90
|
}
|
|
70
91
|
} else if (status.type === 'web' && !webHandled) {
|
|
71
92
|
logger.debug('Found web groupLog, handling all web groupLogs at once');
|
|
@@ -243,6 +264,41 @@ class LogsTrackerManager {
|
|
|
243
264
|
this.removeCliTrackedPath(filePath);
|
|
244
265
|
}
|
|
245
266
|
|
|
267
|
+
addPipedLog(content) {
|
|
268
|
+
// Find active recording instance
|
|
269
|
+
const activeInstances = Object.values(this.instances);
|
|
270
|
+
if (activeInstances.length === 0) {
|
|
271
|
+
logger.warn('No active recording to add piped content to');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Add to the most recent active instance (last one)
|
|
276
|
+
const instance = activeInstances[activeInstances.length - 1];
|
|
277
|
+
const timestamp = Date.now();
|
|
278
|
+
|
|
279
|
+
// Write to CLI tracker's temp file
|
|
280
|
+
if (instance.trackers && instance.trackers.cli) {
|
|
281
|
+
const pipedLog = {
|
|
282
|
+
time: timestamp,
|
|
283
|
+
logFile: 'piped-input',
|
|
284
|
+
content: content
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Write to the CLI tracker's output file
|
|
288
|
+
try {
|
|
289
|
+
const outputFile = path.join(instance.directory, 'cli-logs.jsonl');
|
|
290
|
+
const logLine = JSON.stringify(pipedLog) + '\n';
|
|
291
|
+
fs.appendFileSync(outputFile, logLine);
|
|
292
|
+
logger.info('Added piped content to recording', {
|
|
293
|
+
recorderId: instance.recorderId,
|
|
294
|
+
contentLength: content.length
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
logger.error('Failed to write piped content', { error });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
246
302
|
removeTracker(id) {
|
|
247
303
|
// Try removing from web trackers first
|
|
248
304
|
if (this.webLogsConfig[id]) {
|