dashcam 1.3.31 → 1.4.0-beta
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 +7 -50
- package/bin/dashcam.js +151 -8
- package/lib/ffmpeg.js +10 -6
- package/lib/recorder.js +4 -3
- package/package.json +1 -1
- package/test_workflow.sh +6 -6
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This script runs detached from the parent process to handle long-running recordings
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { startRecording
|
|
7
|
+
import { startRecording } from '../lib/recorder.js';
|
|
8
8
|
import { logger, setVerbose } from '../lib/logger.js';
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import path from 'path';
|
|
@@ -161,56 +161,13 @@ async function runBackgroundRecording() {
|
|
|
161
161
|
}
|
|
162
162
|
isShuttingDown = true;
|
|
163
163
|
|
|
164
|
-
logger.info(`Received ${signal},
|
|
165
|
-
console.log('[Background] Received stop signal,
|
|
164
|
+
logger.info(`Received ${signal}, background process will be killed`);
|
|
165
|
+
console.log('[Background] Received stop signal, process will be terminated...');
|
|
166
166
|
|
|
167
|
-
try
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (stopResult) {
|
|
172
|
-
logger.info('Recording stopped successfully', {
|
|
173
|
-
outputPath: stopResult.outputPath,
|
|
174
|
-
duration: stopResult.duration
|
|
175
|
-
});
|
|
176
|
-
console.log('[Background] Recording stopped successfully:', {
|
|
177
|
-
outputPath: stopResult.outputPath,
|
|
178
|
-
duration: stopResult.duration
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Write recording result for stop command to upload
|
|
182
|
-
console.log('[Background] Writing recording result for stop command...');
|
|
183
|
-
writeRecordingResult({
|
|
184
|
-
outputPath: stopResult.outputPath,
|
|
185
|
-
duration: stopResult.duration,
|
|
186
|
-
clientStartDate: stopResult.clientStartDate,
|
|
187
|
-
apps: stopResult.apps,
|
|
188
|
-
logs: stopResult.logs,
|
|
189
|
-
gifPath: stopResult.gifPath,
|
|
190
|
-
snapshotPath: stopResult.snapshotPath,
|
|
191
|
-
// Include options so stop command can use them for upload
|
|
192
|
-
title: options.title,
|
|
193
|
-
description: options.description,
|
|
194
|
-
project: options.project || options.k
|
|
195
|
-
});
|
|
196
|
-
console.log('[Background] Recording result written successfully');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Update status to indicate recording stopped
|
|
200
|
-
writeStatus({
|
|
201
|
-
isRecording: false,
|
|
202
|
-
completedTime: Date.now(),
|
|
203
|
-
pid: process.pid
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
console.log('[Background] Background process exiting successfully');
|
|
207
|
-
logger.info('Background process exiting successfully');
|
|
208
|
-
process.exit(0);
|
|
209
|
-
} catch (error) {
|
|
210
|
-
console.error('[Background] Error during shutdown:', error.message);
|
|
211
|
-
logger.error('Error during shutdown:', error);
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
167
|
+
// Don't try to stop recording here - the main process will handle cleanup
|
|
168
|
+
// after killing this process. Just exit.
|
|
169
|
+
logger.info('Background process exiting');
|
|
170
|
+
process.exit(0);
|
|
214
171
|
};
|
|
215
172
|
|
|
216
173
|
// Register signal handlers
|
package/bin/dashcam.js
CHANGED
|
@@ -137,7 +137,7 @@ async function recordingAction(options, command) {
|
|
|
137
137
|
|
|
138
138
|
// Add timeout to prevent hanging
|
|
139
139
|
const startRecordingPromise = processManager.startRecording({
|
|
140
|
-
fps: parseInt(options.fps) ||
|
|
140
|
+
fps: parseInt(options.fps) || 10,
|
|
141
141
|
audio: options.audio,
|
|
142
142
|
output: options.output,
|
|
143
143
|
title: options.title,
|
|
@@ -241,7 +241,7 @@ program
|
|
|
241
241
|
.command('record')
|
|
242
242
|
.description('Start a recording terminal to be included in your dashcam video recording')
|
|
243
243
|
.option('-a, --audio', 'Include audio in the recording')
|
|
244
|
-
.option('-f, --fps <fps>', 'Frames per second (default:
|
|
244
|
+
.option('-f, --fps <fps>', 'Frames per second (default: 10)', '10')
|
|
245
245
|
.option('-o, --output <path>', 'Custom output path')
|
|
246
246
|
.option('-t, --title <title>', 'Title for the recording')
|
|
247
247
|
.option('-d, --description <description>', 'Description for the recording (supports markdown)')
|
|
@@ -484,9 +484,36 @@ program
|
|
|
484
484
|
logger.error('Failed to read temp file info', { error });
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
|
|
487
|
+
// Fallback: look for temp files in the same directory
|
|
488
|
+
let tempFile = tempFileInfo?.tempFile;
|
|
489
|
+
if (!tempFile || !fs.existsSync(tempFile)) {
|
|
490
|
+
// Try to find any temp-*.webm file in the output directory
|
|
491
|
+
const outputDir = path.dirname(result.outputPath);
|
|
492
|
+
if (fs.existsSync(outputDir)) {
|
|
493
|
+
const files = fs.readdirSync(outputDir);
|
|
494
|
+
const tempFiles = files.filter(f => f.startsWith('temp-') && f.endsWith('.webm'));
|
|
495
|
+
if (tempFiles.length > 0) {
|
|
496
|
+
// Use the most recent temp file
|
|
497
|
+
tempFiles.sort().reverse();
|
|
498
|
+
tempFile = path.join(outputDir, tempFiles[0]);
|
|
499
|
+
logger.info('Found temp file in output directory', { tempFile });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
488
504
|
const outputPath = result.outputPath;
|
|
489
505
|
|
|
506
|
+
// Verify we have a temp file to work with
|
|
507
|
+
if (!tempFile) {
|
|
508
|
+
console.error('No temp file found for recording');
|
|
509
|
+
logger.error('Temp file missing', {
|
|
510
|
+
tempFileInfo,
|
|
511
|
+
outputDir: path.dirname(result.outputPath),
|
|
512
|
+
expectedPattern: 'temp-*.webm'
|
|
513
|
+
});
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
|
|
490
517
|
// Fix the video with FFmpeg (handle incomplete recordings)
|
|
491
518
|
logger.info('Fixing video with FFmpeg', { tempFile, outputPath });
|
|
492
519
|
if (fs.existsSync(tempFile)) {
|
|
@@ -502,14 +529,129 @@ program
|
|
|
502
529
|
logger.warn('Temp file not found, using output path directly', { tempFile });
|
|
503
530
|
}
|
|
504
531
|
|
|
505
|
-
//
|
|
506
|
-
|
|
532
|
+
// Verify the output file exists before proceeding
|
|
533
|
+
if (!fs.existsSync(outputPath)) {
|
|
534
|
+
console.error('Recording file not found after processing');
|
|
535
|
+
logger.error('Output file missing', { outputPath, tempFile, tempFileExists: fs.existsSync(tempFile) });
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
logger.info('Output file ready for upload', {
|
|
540
|
+
outputPath,
|
|
541
|
+
size: fs.statSync(outputPath).size
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Generate paths for additional assets
|
|
545
|
+
const basePath = outputPath.substring(0, outputPath.lastIndexOf('.'));
|
|
546
|
+
const gifPath = `${basePath}.gif`;
|
|
547
|
+
const snapshotPath = `${basePath}.png`;
|
|
548
|
+
|
|
549
|
+
// Create GIF and snapshot (non-blocking, don't fail if these fail)
|
|
550
|
+
console.log('Creating preview assets...');
|
|
551
|
+
const { createGif, createSnapshot } = await import('../lib/ffmpeg.js');
|
|
552
|
+
|
|
553
|
+
// Add timeout wrapper to prevent hanging on large files
|
|
554
|
+
const withTimeout = (promise, timeoutMs, name) => {
|
|
555
|
+
return Promise.race([
|
|
556
|
+
promise,
|
|
557
|
+
new Promise((_, reject) =>
|
|
558
|
+
setTimeout(() => reject(new Error(`${name} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
559
|
+
)
|
|
560
|
+
]);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await Promise.all([
|
|
565
|
+
withTimeout(
|
|
566
|
+
createGif(outputPath, gifPath),
|
|
567
|
+
30000, // 30 second timeout
|
|
568
|
+
'GIF creation'
|
|
569
|
+
).catch(err => {
|
|
570
|
+
logger.warn('Failed to create GIF', { error: err.message });
|
|
571
|
+
}),
|
|
572
|
+
withTimeout(
|
|
573
|
+
createSnapshot(outputPath, snapshotPath, 0),
|
|
574
|
+
10000, // 10 second timeout
|
|
575
|
+
'Snapshot creation'
|
|
576
|
+
).catch(err => {
|
|
577
|
+
logger.warn('Failed to create snapshot', { error: err.message });
|
|
578
|
+
})
|
|
579
|
+
]);
|
|
580
|
+
logger.debug('Preview assets created successfully');
|
|
581
|
+
} catch (error) {
|
|
582
|
+
logger.warn('Failed to create preview assets', { error: error.message });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Collect logs and app tracking data
|
|
586
|
+
console.log('Collecting tracking data...');
|
|
587
|
+
|
|
588
|
+
// Get app tracking data
|
|
589
|
+
const { applicationTracker } = await import('../lib/applicationTracker.js');
|
|
590
|
+
const appTrackingResults = applicationTracker.stop();
|
|
591
|
+
logger.info('Collected app tracking results', {
|
|
592
|
+
apps: appTrackingResults.apps?.length || 0,
|
|
593
|
+
icons: appTrackingResults.icons?.length || 0,
|
|
594
|
+
events: appTrackingResults.events?.length || 0
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Get log tracking data - read directly from files since background process was killed
|
|
598
|
+
const recordingDir = path.dirname(outputPath);
|
|
599
|
+
let logTrackingResults = [];
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
// Check for CLI logs file
|
|
603
|
+
const cliLogsFile = path.join(recordingDir, 'dashcam_logs_cli.jsonl');
|
|
604
|
+
if (fs.existsSync(cliLogsFile)) {
|
|
605
|
+
const { jsonl } = await import('../lib/utilities/jsonl.js');
|
|
606
|
+
const cliLogs = jsonl.read(cliLogsFile);
|
|
607
|
+
if (cliLogs && cliLogs.length > 0) {
|
|
608
|
+
logTrackingResults.push({
|
|
609
|
+
type: 'cli',
|
|
610
|
+
name: 'CLI Logs',
|
|
611
|
+
fileLocation: cliLogsFile,
|
|
612
|
+
count: cliLogs.length,
|
|
613
|
+
trimmedFileLocation: cliLogsFile
|
|
614
|
+
});
|
|
615
|
+
logger.info('Found CLI logs', { count: cliLogs.length, file: cliLogsFile });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Check for web logs file
|
|
620
|
+
const webLogsFile = path.join(recordingDir, 'dashcam_logs_web_events.jsonl');
|
|
621
|
+
if (fs.existsSync(webLogsFile)) {
|
|
622
|
+
const { jsonl } = await import('../lib/utilities/jsonl.js');
|
|
623
|
+
const webLogs = jsonl.read(webLogsFile);
|
|
624
|
+
if (webLogs && webLogs.length > 0) {
|
|
625
|
+
logTrackingResults.push({
|
|
626
|
+
type: 'web',
|
|
627
|
+
name: 'Web Logs',
|
|
628
|
+
fileLocation: webLogsFile,
|
|
629
|
+
count: webLogs.length,
|
|
630
|
+
trimmedFileLocation: webLogsFile
|
|
631
|
+
});
|
|
632
|
+
logger.info('Found web logs', { count: webLogs.length, file: webLogsFile });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
logger.info('Collected log tracking results', {
|
|
637
|
+
trackers: logTrackingResults.length,
|
|
638
|
+
totalEvents: logTrackingResults.reduce((sum, result) => sum + result.count, 0)
|
|
639
|
+
});
|
|
640
|
+
} catch (error) {
|
|
641
|
+
logger.warn('Failed to collect log tracking results', { error: error.message });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// The recording is on disk and can be uploaded with full metadata
|
|
507
645
|
const recordingResult = {
|
|
508
646
|
outputPath,
|
|
647
|
+
gifPath,
|
|
648
|
+
snapshotPath,
|
|
509
649
|
duration: result.duration,
|
|
650
|
+
fileSize: fs.statSync(outputPath).size,
|
|
510
651
|
clientStartDate: activeStatus.startTime,
|
|
511
|
-
apps:
|
|
512
|
-
|
|
652
|
+
apps: appTrackingResults.apps,
|
|
653
|
+
icons: appTrackingResults.icons,
|
|
654
|
+
logs: logTrackingResults,
|
|
513
655
|
title: activeStatus?.options?.title,
|
|
514
656
|
description: activeStatus?.options?.description,
|
|
515
657
|
project: activeStatus?.options?.project
|
|
@@ -532,6 +674,7 @@ program
|
|
|
532
674
|
duration: recordingResult.duration,
|
|
533
675
|
clientStartDate: recordingResult.clientStartDate,
|
|
534
676
|
apps: recordingResult.apps,
|
|
677
|
+
icons: recordingResult.icons,
|
|
535
678
|
logs: recordingResult.logs,
|
|
536
679
|
gifPath: recordingResult.gifPath,
|
|
537
680
|
snapshotPath: recordingResult.snapshotPath
|
|
@@ -749,7 +892,7 @@ program
|
|
|
749
892
|
if (!targetFile) {
|
|
750
893
|
console.error('Please provide a file path or use --recover option');
|
|
751
894
|
console.log('Examples:');
|
|
752
|
-
console.log(' dashcam upload /path/to/recording.
|
|
895
|
+
console.log(' dashcam upload /path/to/recording.mp4');
|
|
753
896
|
console.log(' dashcam upload --recover');
|
|
754
897
|
process.exit(1);
|
|
755
898
|
}
|
package/lib/ffmpeg.js
CHANGED
|
@@ -72,8 +72,8 @@ export async function createGif(inputVideoPath, outputGifPath) {
|
|
|
72
72
|
throw new Error('Video file is not ready or is corrupted');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const gifFps = 4
|
|
76
|
-
const gifDuration = 10
|
|
75
|
+
const gifFps = 2; // Reduced from 4 to 2 fps for faster generation
|
|
76
|
+
const gifDuration = 5; // Reduced from 10 to 5 seconds for faster generation
|
|
77
77
|
const gifFrames = Math.ceil(gifDuration * gifFps);
|
|
78
78
|
|
|
79
79
|
// Get video duration in seconds
|
|
@@ -94,8 +94,8 @@ export async function createGif(inputVideoPath, outputGifPath) {
|
|
|
94
94
|
stdout
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
// Fallback:
|
|
98
|
-
const filters = `fps=
|
|
97
|
+
// Fallback: Fast GIF creation with reduced quality
|
|
98
|
+
const filters = `fps=2,scale=480:-1:flags=fast_bilinear,split[s0][s1];[s0]palettegen=max_colors=64[p];[s1][p]paletteuse=dither=none`;
|
|
99
99
|
|
|
100
100
|
await execa(ffmpegPath, [
|
|
101
101
|
'-i', inputVideoPath,
|
|
@@ -111,8 +111,12 @@ export async function createGif(inputVideoPath, outputGifPath) {
|
|
|
111
111
|
|
|
112
112
|
const extractedFramesInterval = videoDuration / gifFrames;
|
|
113
113
|
|
|
114
|
-
//
|
|
115
|
-
|
|
114
|
+
// Fast GIF creation with reduced quality for speed
|
|
115
|
+
// - Smaller scale (480 vs 640)
|
|
116
|
+
// - Fewer colors (64 vs 128)
|
|
117
|
+
// - Faster scaling algorithm (fast_bilinear vs lanczos)
|
|
118
|
+
// - No dithering for faster processing
|
|
119
|
+
const filters = `fps=1/${extractedFramesInterval},scale=480:-1:flags=fast_bilinear,split[s0][s1];[s0]palettegen=max_colors=64[p];[s1][p]paletteuse=dither=none`;
|
|
116
120
|
|
|
117
121
|
await execa(ffmpegPath, [
|
|
118
122
|
'-i', inputVideoPath,
|
package/lib/recorder.js
CHANGED
|
@@ -27,7 +27,7 @@ async function fixVideoContainer(inputVideoPath, outputVideoPath) {
|
|
|
27
27
|
'-i', inputVideoPath,
|
|
28
28
|
'-vcodec', 'copy', // Copy video stream without re-encoding
|
|
29
29
|
'-acodec', 'copy', // Copy audio stream without re-encoding
|
|
30
|
-
'-
|
|
30
|
+
'-f', 'webm', // Force WebM format
|
|
31
31
|
outputVideoPath,
|
|
32
32
|
'-y', // Overwrite output file
|
|
33
33
|
'-hide_banner'
|
|
@@ -320,8 +320,9 @@ export async function startRecording({
|
|
|
320
320
|
|
|
321
321
|
// Construct FFmpeg command arguments
|
|
322
322
|
const platformArgs = await getPlatformArgs({ fps, includeAudio });
|
|
323
|
+
|
|
323
324
|
const outputArgs = [
|
|
324
|
-
'-c:v', 'libvpx
|
|
325
|
+
'-c:v', 'libvpx', // Use VP9 codec for better quality and compression
|
|
325
326
|
'-quality', 'realtime', // Use realtime quality preset for faster encoding
|
|
326
327
|
'-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
|
|
327
328
|
'-deadline', 'realtime',// Realtime encoding mode for lowest latency
|
|
@@ -340,7 +341,7 @@ export async function startRecording({
|
|
|
340
341
|
|
|
341
342
|
if (includeAudio) {
|
|
342
343
|
outputArgs.push(
|
|
343
|
-
'-c:a', '
|
|
344
|
+
'-c:a', 'libvorbis', // Vorbis audio codec for WebM
|
|
344
345
|
'-b:a', '128k'
|
|
345
346
|
);
|
|
346
347
|
}
|
package/package.json
CHANGED
package/test_workflow.sh
CHANGED
|
@@ -34,12 +34,11 @@ echo "✅ File tracking configured"
|
|
|
34
34
|
echo ""
|
|
35
35
|
echo "4. Starting dashcam recording in background..."
|
|
36
36
|
# Start recording and redirect output to a log file so we can still monitor it
|
|
37
|
-
./bin/dashcam.js record --title "Sync Test Recording" --description "Testing video/log synchronization with timestamped events" > /tmp/dashcam-recording.log 2>&1
|
|
38
|
-
RECORD_PID=$!
|
|
37
|
+
./bin/dashcam.js record --title "Sync Test Recording" --description "Testing video/log synchronization with timestamped events" > /tmp/dashcam-recording.log 2>&1
|
|
39
38
|
|
|
40
39
|
# Wait for recording to initialize and log tracker to start
|
|
41
|
-
echo "Waiting for recording to initialize
|
|
42
|
-
sleep
|
|
40
|
+
echo "Waiting for recording to initialize..."
|
|
41
|
+
sleep 2
|
|
43
42
|
|
|
44
43
|
# Write first event after log tracker is fully ready
|
|
45
44
|
RECORDING_START=$(date +%s)
|
|
@@ -48,11 +47,12 @@ echo "🔴 EVENT 1: Recording START at $(date '+%H:%M:%S')"
|
|
|
48
47
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
49
48
|
echo "[EVENT 1] 🔴 Recording started with emoji at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_START" >> "$TEMP_FILE"
|
|
50
49
|
|
|
51
|
-
# Verify recording is actually running
|
|
52
|
-
if
|
|
50
|
+
# Verify background recording process is actually running
|
|
51
|
+
if pgrep -f "dashcam-background.js" > /dev/null; then
|
|
53
52
|
echo "✅ Recording started successfully"
|
|
54
53
|
else
|
|
55
54
|
echo "❌ Recording process died, check /tmp/dashcam-recording.log"
|
|
55
|
+
cat /tmp/dashcam-recording.log
|
|
56
56
|
exit 1
|
|
57
57
|
fi
|
|
58
58
|
|