dashcam 1.0.1-beta.21 โ 1.0.1-beta.23
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.js +17 -18
- package/lib/ffmpeg.js +9 -39
- package/lib/processManager.js +20 -34
- package/lib/recorder.js +33 -15
- package/lib/tracking/FileTracker.js +1 -1
- package/package.json +1 -1
- package/test_workflow.sh +19 -14
package/bin/dashcam.js
CHANGED
|
@@ -417,30 +417,29 @@ program
|
|
|
417
417
|
|
|
418
418
|
console.log('Recording stopped successfully');
|
|
419
419
|
|
|
420
|
+
// Wait a moment for upload to complete (background process handles this)
|
|
421
|
+
logger.debug('Waiting for background upload to complete...');
|
|
422
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
423
|
+
|
|
424
|
+
// Try to read the upload result from the background process
|
|
425
|
+
const uploadResult = processManager.readUploadResult();
|
|
426
|
+
logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
|
|
427
|
+
|
|
428
|
+
if (uploadResult && uploadResult.shareLink) {
|
|
429
|
+
console.log('๐น Watch your recording:', uploadResult.shareLink);
|
|
430
|
+
// Clean up the result file now that we've read it
|
|
431
|
+
processManager.cleanup();
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
|
|
420
435
|
// Check if files still exist - if not, background process already uploaded
|
|
421
436
|
const filesExist = fs.existsSync(result.outputPath) &&
|
|
422
437
|
(!result.gifPath || fs.existsSync(result.gifPath)) &&
|
|
423
438
|
(!result.snapshotPath || fs.existsSync(result.snapshotPath));
|
|
424
439
|
|
|
425
440
|
if (!filesExist) {
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
441
|
+
console.log('โ
Recording uploaded by background process');
|
|
442
|
+
logger.info('Files were cleaned up by background process');
|
|
444
443
|
process.exit(0);
|
|
445
444
|
}
|
|
446
445
|
|
package/lib/ffmpeg.js
CHANGED
|
@@ -18,7 +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
|
-
'-compression_level', '
|
|
21
|
+
'-compression_level', '0', // Fast compression for speed (0 = fastest, 9 = slowest)
|
|
22
22
|
outputSnapshotPath,
|
|
23
23
|
'-y',
|
|
24
24
|
'-hide_banner'
|
|
@@ -94,62 +94,32 @@ export async function createGif(inputVideoPath, outputGifPath) {
|
|
|
94
94
|
stdout
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
// Fallback:
|
|
98
|
-
const
|
|
97
|
+
// Fallback: Single-pass GIF creation at fixed rate
|
|
98
|
+
const filters = `fps=1/3,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5`;
|
|
99
|
+
|
|
99
100
|
await execa(ffmpegPath, [
|
|
100
101
|
'-i', inputVideoPath,
|
|
101
|
-
'-vf',
|
|
102
|
-
framesPath
|
|
103
|
-
]);
|
|
104
|
-
|
|
105
|
-
// Create GIF from frames
|
|
106
|
-
await execa(ffmpegPath, [
|
|
107
|
-
'-framerate', `${gifFps}`,
|
|
108
|
-
'-i', framesPath,
|
|
102
|
+
'-vf', filters,
|
|
109
103
|
'-loop', '0',
|
|
110
104
|
outputGifPath,
|
|
111
105
|
'-y',
|
|
112
106
|
'-hide_banner'
|
|
113
107
|
]);
|
|
114
|
-
|
|
115
|
-
// Clean up temporary frame files
|
|
116
|
-
const framesToDelete = fs.readdirSync(os.tmpdir())
|
|
117
|
-
.filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
|
|
118
|
-
.map(file => path.join(os.tmpdir(), file));
|
|
119
|
-
|
|
120
|
-
for (const frame of framesToDelete) {
|
|
121
|
-
fs.unlinkSync(frame);
|
|
122
|
-
}
|
|
123
108
|
|
|
124
109
|
return;
|
|
125
110
|
}
|
|
126
111
|
|
|
127
112
|
const extractedFramesInterval = videoDuration / gifFrames;
|
|
128
113
|
|
|
129
|
-
//
|
|
130
|
-
const
|
|
114
|
+
// Single-pass GIF creation with palette for better quality and performance
|
|
115
|
+
const filters = `fps=1/${extractedFramesInterval},scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5`;
|
|
116
|
+
|
|
131
117
|
await execa(ffmpegPath, [
|
|
132
118
|
'-i', inputVideoPath,
|
|
133
|
-
'-vf',
|
|
134
|
-
framesPath
|
|
135
|
-
]);
|
|
136
|
-
|
|
137
|
-
// Create GIF from frames
|
|
138
|
-
await execa(ffmpegPath, [
|
|
139
|
-
'-framerate', `${gifFps}`,
|
|
140
|
-
'-i', framesPath,
|
|
119
|
+
'-vf', filters,
|
|
141
120
|
'-loop', '0',
|
|
142
121
|
outputGifPath,
|
|
143
122
|
'-y',
|
|
144
123
|
'-hide_banner'
|
|
145
124
|
]);
|
|
146
|
-
|
|
147
|
-
// Clean up temporary frame files
|
|
148
|
-
const framesToDelete = fs.readdirSync(os.tmpdir())
|
|
149
|
-
.filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
|
|
150
|
-
.map(file => path.join(os.tmpdir(), file));
|
|
151
|
-
|
|
152
|
-
for (const frame of framesToDelete) {
|
|
153
|
-
fs.unlinkSync(frame);
|
|
154
|
-
}
|
|
155
125
|
}
|
package/lib/processManager.js
CHANGED
|
@@ -174,41 +174,27 @@ class ProcessManager {
|
|
|
174
174
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
177
|
+
// The background process handles stopRecording() internally via SIGINT
|
|
178
|
+
// We just need to return the basic result from the status file
|
|
178
179
|
if (status) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Fallback to basic result if recorder fails
|
|
198
|
-
const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
|
|
199
|
-
const result = {
|
|
200
|
-
outputPath: status.outputPath,
|
|
201
|
-
gifPath: `${basePath}.gif`,
|
|
202
|
-
snapshotPath: `${basePath}.png`,
|
|
203
|
-
duration: Date.now() - status.startTime,
|
|
204
|
-
clientStartDate: status.startTime,
|
|
205
|
-
apps: [],
|
|
206
|
-
logs: []
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
this.cleanup({ preserveResult: true });
|
|
210
|
-
return result;
|
|
211
|
-
}
|
|
180
|
+
logger.info('Background recording stopped, returning status', {
|
|
181
|
+
outputPath: status.outputPath,
|
|
182
|
+
duration: Date.now() - status.startTime
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
|
|
186
|
+
const result = {
|
|
187
|
+
outputPath: status.outputPath,
|
|
188
|
+
gifPath: `${basePath}.gif`,
|
|
189
|
+
snapshotPath: `${basePath}.png`,
|
|
190
|
+
duration: Date.now() - status.startTime,
|
|
191
|
+
clientStartDate: status.startTime,
|
|
192
|
+
apps: [],
|
|
193
|
+
logs: []
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
this.cleanup({ preserveResult: true });
|
|
197
|
+
return result;
|
|
212
198
|
} else {
|
|
213
199
|
throw new Error('No status information available for active recording');
|
|
214
200
|
}
|
package/lib/recorder.js
CHANGED
|
@@ -322,9 +322,9 @@ export async function startRecording({
|
|
|
322
322
|
const platformArgs = await getPlatformArgs({ fps, includeAudio });
|
|
323
323
|
const outputArgs = [
|
|
324
324
|
'-c:v', 'libvpx-vp9', // Use VP9 codec for better quality and compression
|
|
325
|
-
'-quality', '
|
|
326
|
-
'-cpu-used', '
|
|
327
|
-
'-deadline', '
|
|
325
|
+
'-quality', 'realtime', // Use realtime quality preset for faster encoding
|
|
326
|
+
'-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
|
|
327
|
+
'-deadline', 'realtime',// Realtime encoding mode for lowest latency
|
|
328
328
|
'-b:v', '2M', // Target bitrate
|
|
329
329
|
'-r', fps.toString(), // Ensure output framerate matches input
|
|
330
330
|
'-g', fps.toString(), // Keyframe interval = 1 second (every fps frames) - ensures frequent keyframes
|
|
@@ -528,24 +528,23 @@ export async function stopRecording() {
|
|
|
528
528
|
currentRecording.stdin.end(); // Close stdin to signal end
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
// Wait
|
|
532
|
-
// VP9 encoding needs time to flush buffers and finalize the container
|
|
531
|
+
// Wait for FFmpeg to finish gracefully with realtime encoding
|
|
533
532
|
const gracefulTimeout = setTimeout(() => {
|
|
534
533
|
if (currentRecording && !currentRecording.killed) {
|
|
535
|
-
logger.warn('FFmpeg did not exit gracefully after
|
|
534
|
+
logger.warn('FFmpeg did not exit gracefully after 10s, sending SIGTERM...');
|
|
536
535
|
// If still running, try SIGTERM
|
|
537
536
|
process.kill(currentRecording.pid, 'SIGTERM');
|
|
538
537
|
}
|
|
539
|
-
},
|
|
538
|
+
}, 10000); // Wait longer for graceful shutdown
|
|
540
539
|
|
|
541
|
-
// Wait up to
|
|
540
|
+
// Wait up to 20 seconds for SIGTERM to work
|
|
542
541
|
const hardKillTimeout = setTimeout(() => {
|
|
543
542
|
if (currentRecording && !currentRecording.killed) {
|
|
544
543
|
logger.error('FFmpeg still running after SIGTERM, using SIGKILL...');
|
|
545
544
|
// If still not dead, use SIGKILL as last resort
|
|
546
545
|
process.kill(currentRecording.pid, 'SIGKILL');
|
|
547
546
|
}
|
|
548
|
-
},
|
|
547
|
+
}, 20000); // Longer timeout to avoid killing prematurely
|
|
549
548
|
|
|
550
549
|
// Wait for the process to fully exit
|
|
551
550
|
if (currentRecording) {
|
|
@@ -556,10 +555,30 @@ export async function stopRecording() {
|
|
|
556
555
|
clearTimeout(gracefulTimeout);
|
|
557
556
|
clearTimeout(hardKillTimeout);
|
|
558
557
|
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
logger.debug('Waiting for
|
|
562
|
-
|
|
558
|
+
// Wait for filesystem to sync and file to be written
|
|
559
|
+
// Poll for the file to exist with content
|
|
560
|
+
logger.debug('Waiting for temp file to be written...');
|
|
561
|
+
const maxWaitTime = 10000; // Wait up to 10 seconds
|
|
562
|
+
const startWait = Date.now();
|
|
563
|
+
let tempFileReady = false;
|
|
564
|
+
|
|
565
|
+
while (!tempFileReady && (Date.now() - startWait) < maxWaitTime) {
|
|
566
|
+
const tempFile = currentTempFile;
|
|
567
|
+
if (tempFile && fs.existsSync(tempFile)) {
|
|
568
|
+
const stats = fs.statSync(tempFile);
|
|
569
|
+
if (stats.size > 0) {
|
|
570
|
+
logger.debug('Temp file is ready', { size: stats.size });
|
|
571
|
+
tempFileReady = true;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Wait a bit before checking again
|
|
576
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!tempFileReady) {
|
|
580
|
+
logger.warn('Temp file not ready after waiting, proceeding anyway');
|
|
581
|
+
}
|
|
563
582
|
|
|
564
583
|
// Read temp file path from disk (for cross-process access)
|
|
565
584
|
let tempFile = currentTempFile; // Try in-memory first
|
|
@@ -607,8 +626,7 @@ export async function stopRecording() {
|
|
|
607
626
|
path: tempFile
|
|
608
627
|
});
|
|
609
628
|
|
|
610
|
-
//
|
|
611
|
-
// proper container metadata (duration, seekability, etc.)
|
|
629
|
+
// Re-mux to ensure proper container metadata (duration, seekability, etc.)
|
|
612
630
|
logger.debug('Re-muxing temp file to fix container metadata...');
|
|
613
631
|
|
|
614
632
|
try {
|
|
@@ -47,7 +47,7 @@ export class FileTracker {
|
|
|
47
47
|
fs.writeFileSync(this.trackedFile, '', 'utf8');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
this.tail = new Tail(this.trackedFile, { encoding: '
|
|
50
|
+
this.tail = new Tail(this.trackedFile, { encoding: 'utf8' });
|
|
51
51
|
this.tail.on('line', (line) => {
|
|
52
52
|
const time = Date.now();
|
|
53
53
|
this.eventTimes.push(time);
|
package/package.json
CHANGED
package/test_workflow.sh
CHANGED
|
@@ -46,7 +46,7 @@ RECORDING_START=$(date +%s)
|
|
|
46
46
|
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
47
47
|
echo "๐ด EVENT 1: Recording START at $(date '+%H:%M:%S')"
|
|
48
48
|
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
49
|
-
echo "[EVENT 1] Recording started at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_START" >> "$TEMP_FILE"
|
|
49
|
+
echo "[EVENT 1] ๐ด Recording started with emoji at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_START" >> "$TEMP_FILE"
|
|
50
50
|
|
|
51
51
|
# Verify recording is actually running
|
|
52
52
|
if ps -p $RECORD_PID > /dev/null; then
|
|
@@ -71,25 +71,25 @@ sleep 3
|
|
|
71
71
|
# Event 2 - after 3 seconds
|
|
72
72
|
echo ""
|
|
73
73
|
echo "๐ก EVENT 2: 3 seconds mark at $(date '+%H:%M:%S')"
|
|
74
|
-
echo "[EVENT 2] 3 seconds elapsed at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
74
|
+
echo "[EVENT 2] ๐ก 3 seconds elapsed with emoji at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
75
75
|
sleep 3
|
|
76
76
|
|
|
77
77
|
# Event 3 - after 6 seconds
|
|
78
78
|
echo ""
|
|
79
79
|
echo "๐ข EVENT 3: 6 seconds mark at $(date '+%H:%M:%S')"
|
|
80
|
-
echo "[EVENT 3] 6 seconds elapsed at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
80
|
+
echo "[EVENT 3] ๐ข 6 seconds elapsed with emoji at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
81
81
|
sleep 3
|
|
82
82
|
|
|
83
83
|
# Event 4 - after 9 seconds
|
|
84
84
|
echo ""
|
|
85
85
|
echo "๐ต EVENT 4: 9 seconds mark at $(date '+%H:%M:%S')"
|
|
86
|
-
echo "[EVENT 4] 9 seconds elapsed at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
86
|
+
echo "[EVENT 4] ๐ต 9 seconds elapsed with emoji at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
87
87
|
sleep 3
|
|
88
88
|
|
|
89
89
|
# Event 5 - after 12 seconds
|
|
90
90
|
echo ""
|
|
91
91
|
echo "๐ฃ EVENT 5: 12 seconds mark at $(date '+%H:%M:%S')"
|
|
92
|
-
echo "[EVENT 5] 12 seconds elapsed at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
92
|
+
echo "[EVENT 5] ๐ฃ 12 seconds elapsed with emoji at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
93
93
|
sleep 3
|
|
94
94
|
|
|
95
95
|
# Event 6 - before ending
|
|
@@ -98,7 +98,7 @@ echo "โโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
98
98
|
echo "โซ EVENT 6: Recording END at $(date '+%H:%M:%S')"
|
|
99
99
|
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
100
100
|
RECORDING_END=$(date +%s)
|
|
101
|
-
echo "[EVENT 6] Recording ending at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_END" >> "$TEMP_FILE"
|
|
101
|
+
echo "[EVENT 6] โซ Recording ending with emoji at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_END" >> "$TEMP_FILE"
|
|
102
102
|
|
|
103
103
|
DURATION=$((RECORDING_END - RECORDING_START))
|
|
104
104
|
echo ""
|
|
@@ -112,8 +112,13 @@ sleep 2
|
|
|
112
112
|
# 6. Stop recording and upload (this will kill the background recording process)
|
|
113
113
|
echo ""
|
|
114
114
|
echo "6. Stopping recording and uploading..."
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
# Check if recording is still active
|
|
116
|
+
if ./bin/dashcam.js status | grep -q "Recording in progress"; then
|
|
117
|
+
./bin/dashcam.js stop
|
|
118
|
+
echo "โ
Recording stopped and uploaded"
|
|
119
|
+
else
|
|
120
|
+
echo "โ ๏ธ Recording already completed (this is expected with background mode)"
|
|
121
|
+
fi
|
|
117
122
|
|
|
118
123
|
echo ""
|
|
119
124
|
echo "๐งน Cleaning up..."
|
|
@@ -140,12 +145,12 @@ echo "3. Verify these events appear at the correct times:"
|
|
|
140
145
|
echo ""
|
|
141
146
|
echo " Time | Terminal Display | Log Entry"
|
|
142
147
|
echo " -------|---------------------------|---------------------------"
|
|
143
|
-
echo " 0:00 | ๐ด EVENT 1 | [EVENT 1] Recording started"
|
|
144
|
-
echo " 0:03 | ๐ก EVENT 2 | [EVENT 2] 3 seconds elapsed"
|
|
145
|
-
echo " 0:06 | ๐ข EVENT 3 | [EVENT 3] 6 seconds elapsed"
|
|
146
|
-
echo " 0:09 | ๐ต EVENT 4 | [EVENT 4] 9 seconds elapsed"
|
|
147
|
-
echo " 0:12 | ๐ฃ EVENT 5 | [EVENT 5] 12 seconds elapsed"
|
|
148
|
-
echo " 0:15 | โซ EVENT 6 | [EVENT 6] Recording ending"
|
|
148
|
+
echo " 0:00 | ๐ด EVENT 1 | [EVENT 1] ๐ด Recording started"
|
|
149
|
+
echo " 0:03 | ๐ก EVENT 2 | [EVENT 2] ๐ก 3 seconds elapsed"
|
|
150
|
+
echo " 0:06 | ๐ข EVENT 3 | [EVENT 3] ๐ข 6 seconds elapsed"
|
|
151
|
+
echo " 0:09 | ๐ต EVENT 4 | [EVENT 4] ๐ต 9 seconds elapsed"
|
|
152
|
+
echo " 0:12 | ๐ฃ EVENT 5 | [EVENT 5] ๐ฃ 12 seconds elapsed"
|
|
153
|
+
echo " 0:15 | โซ EVENT 6 | [EVENT 6] โซ Recording ending"
|
|
149
154
|
echo ""
|
|
150
155
|
echo "4. The log timestamps should match the video timeline exactly"
|
|
151
156
|
echo "5. Each colored event marker should appear in the video"
|