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 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
- // 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
-
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', '6', // Use default compression (was 100, which is extremely slow)
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: Just sample the video at a fixed rate (e.g., 1 frame every 3 seconds)
98
- const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
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', `fps=1/3`, // Sample 1 frame every 3 seconds
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
- // Extract frames
130
- const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
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', `fps=1/${extractedFramesInterval}`,
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
  }
@@ -174,41 +174,27 @@ class ProcessManager {
174
174
  await new Promise(resolve => setTimeout(resolve, 1000));
175
175
  }
176
176
 
177
- // Call the actual recorder's stopRecording function to get complete results
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
- try {
180
- const { stopRecording } = await import('./recorder.js');
181
- const result = await stopRecording();
182
-
183
- logger.info('Recording stopped successfully via recorder', {
184
- outputPath: result.outputPath,
185
- duration: result.duration,
186
- hasLogs: result.logs?.length > 0,
187
- hasApps: result.apps?.length > 0
188
- });
189
-
190
- // Cleanup process files but preserve upload result for stop command
191
- this.cleanup({ preserveResult: true });
192
-
193
- return result;
194
- } catch (recorderError) {
195
- logger.warn('Failed to stop via recorder, falling back to basic result', { error: recorderError.message });
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', 'good', // Use 'good' quality preset (better than realtime, not as slow as best)
326
- '-cpu-used', '4', // Faster encoding (0-8, higher = faster but lower quality)
327
- '-deadline', 'good', // Good quality encoding mode
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 longer for FFmpeg to finish gracefully - critical for VP9 encoding
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 8s, sending SIGTERM...');
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
- }, 8000); // Increased to 8 seconds for VP9 finalization
538
+ }, 10000); // Wait longer for graceful shutdown
540
539
 
541
- // Wait up to 15 seconds for SIGTERM to work
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
- }, 15000); // Increased to 15 seconds
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
- // Additional wait to ensure filesystem is synced and encoder buffers are flushed
560
- // This is especially important for VP9 which has larger encoding buffers
561
- logger.debug('Waiting for filesystem sync and VP9 encoder finalization...');
562
- await new Promise(resolve => setTimeout(resolve, 3000)); // Keep at 3 seconds after process exit
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
- // Since WebM is already a valid streaming format, re-mux it to ensure
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: 'ascii' });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.0.1-beta.21",
3
+ "version": "1.0.1-beta.23",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
5
  "main": "bin/index.js",
6
6
  "bin": {
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
- ./bin/dashcam.js stop
116
- echo "โœ… Recording stopped and uploaded"
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"