dashcam 1.0.1-beta.9 → 1.0.2-beta.1

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.
@@ -160,8 +160,9 @@ class ProcessManager {
160
160
  logger.info('Stopping active recording process', { pid });
161
161
  process.kill(pid, 'SIGINT');
162
162
 
163
- // Wait for the process to actually finish
164
- const maxWaitTime = 30000; // 30 seconds max
163
+ // Wait for the process to actually finish and upload
164
+ // Increase timeout to allow for upload to complete
165
+ const maxWaitTime = 120000; // 2 minutes max to allow for upload
165
166
  const startWait = Date.now();
166
167
 
167
168
  while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
@@ -174,41 +175,27 @@ class ProcessManager {
174
175
  await new Promise(resolve => setTimeout(resolve, 1000));
175
176
  }
176
177
 
177
- // Call the actual recorder's stopRecording function to get complete results
178
+ // The background process handles stopRecording() internally via SIGINT
179
+ // We just need to return the basic result from the status file
178
180
  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
- }
181
+ logger.info('Background recording stopped, returning status', {
182
+ outputPath: status.outputPath,
183
+ duration: Date.now() - status.startTime
184
+ });
185
+
186
+ const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
187
+ const result = {
188
+ outputPath: status.outputPath,
189
+ gifPath: `${basePath}.gif`,
190
+ snapshotPath: `${basePath}.png`,
191
+ duration: Date.now() - status.startTime,
192
+ clientStartDate: status.startTime,
193
+ apps: [],
194
+ logs: []
195
+ };
196
+
197
+ this.cleanup({ preserveResult: true });
198
+ return result;
212
199
  } else {
213
200
  throw new Error('No status information available for active recording');
214
201
  }
package/lib/recorder.js CHANGED
@@ -8,6 +8,47 @@ import path from 'path';
8
8
  import os from 'os';
9
9
  import fs from 'fs';
10
10
 
11
+ /**
12
+ * Fix/repair a video file by re-muxing it with proper container metadata
13
+ * This copies streams without re-encoding and ensures proper container finalization
14
+ */
15
+ async function fixVideoContainer(inputVideoPath, outputVideoPath) {
16
+ const logExit = logFunctionCall('fixVideoContainer', { inputVideoPath, outputVideoPath });
17
+
18
+ try {
19
+ const ffmpegPath = await getFfmpegPath();
20
+
21
+ logger.info('Re-muxing video to fix container metadata', {
22
+ input: inputVideoPath,
23
+ output: outputVideoPath
24
+ });
25
+
26
+ const args = [
27
+ '-i', inputVideoPath,
28
+ '-vcodec', 'copy', // Copy video stream without re-encoding
29
+ '-acodec', 'copy', // Copy audio stream without re-encoding
30
+ '-movflags', 'faststart', // Enable fast start for web playback
31
+ outputVideoPath,
32
+ '-y', // Overwrite output file
33
+ '-hide_banner'
34
+ ];
35
+
36
+ await execa(ffmpegPath, args);
37
+
38
+ logger.info('Successfully re-muxed video', {
39
+ outputPath: outputVideoPath,
40
+ outputSize: fs.existsSync(outputVideoPath) ? fs.statSync(outputVideoPath).size : 0
41
+ });
42
+
43
+ logExit();
44
+ return true;
45
+ } catch (error) {
46
+ logger.error('Failed to fix video container', { error: error.message });
47
+ logExit();
48
+ return false;
49
+ }
50
+ }
51
+
11
52
  /**
12
53
  * Dynamically detect the primary screen capture device for the current platform
13
54
  */
@@ -116,9 +157,8 @@ const PLATFORM_CONFIG = {
116
157
  audioInput: '0', // Default audio device if needed
117
158
  audioFormat: 'avfoundation',
118
159
  extraArgs: [
119
- '-video_size', '1920x1080', // Set explicit resolution
120
- '-pixel_format', 'uyvy422', // Use supported pixel format
121
- '-r', '30' // Set frame rate
160
+ '-capture_cursor', '1', // Capture mouse cursor
161
+ '-capture_mouse_clicks', '1' // Capture mouse clicks
122
162
  ]
123
163
  },
124
164
  win32: {
@@ -160,6 +200,7 @@ async function getPlatformArgs({ fps, includeAudio }) {
160
200
  });
161
201
 
162
202
  const args = [
203
+ '-thread_queue_size', '512', // Increase input buffer to prevent drops
163
204
  '-f', config.inputFormat
164
205
  ];
165
206
 
@@ -170,12 +211,14 @@ async function getPlatformArgs({ fps, includeAudio }) {
170
211
 
171
212
  args.push(
172
213
  '-framerate', fps.toString(),
214
+ '-probesize', '50M', // Increase probe size for better stream detection
173
215
  '-i', screenInput
174
216
  );
175
217
 
176
218
  // Add audio capture if enabled
177
219
  if (includeAudio) {
178
220
  args.push(
221
+ '-thread_queue_size', '512', // Increase audio buffer too
179
222
  '-f', config.audioFormat,
180
223
  '-i', config.audioInput
181
224
  );
@@ -279,17 +322,20 @@ export async function startRecording({
279
322
  const platformArgs = await getPlatformArgs({ fps, includeAudio });
280
323
  const outputArgs = [
281
324
  '-c:v', 'libvpx-vp9', // Use VP9 codec for better quality and compression
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
- // Remove explicit pixel format to let ffmpeg handle conversion automatically
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
+ '-b:v', '2M', // Target bitrate
287
329
  '-r', fps.toString(), // Ensure output framerate matches input
288
- '-g', '60', // Keyframe every 60 frames (reduced frequency)
289
- // WebM options for more frequent disk writes
290
- '-f', 'webm', // Force WebM container format
291
- '-flush_packets', '1', // Flush packets immediately to disk
292
- '-max_muxing_queue_size', '1024' // Limit muxing queue to prevent delays
330
+ '-g', fps.toString(), // Keyframe interval = 1 second (every fps frames) - ensures frequent keyframes
331
+ '-force_key_frames', `expr:gte(t,n_forced*1)`, // Force keyframe every 1 second
332
+ // WebM options for more frequent disk writes and proper stream handling
333
+ '-f', 'webm', // Force WebM container format
334
+ '-flush_packets', '1', // Flush packets immediately to disk - critical for short recordings
335
+ '-fflags', '+genpts', // Generate presentation timestamps
336
+ '-avoid_negative_ts', 'make_zero', // Avoid negative timestamps
337
+ '-vsync', '1', // Ensure every frame is encoded (CFR - constant frame rate)
338
+ '-max_muxing_queue_size', '9999' // Large queue to prevent frame drops
293
339
  ];
294
340
 
295
341
  if (includeAudio) {
@@ -465,29 +511,40 @@ export async function stopRecording() {
465
511
  duration: recordingDuration,
466
512
  durationSeconds: (recordingDuration / 1000).toFixed(1)
467
513
  });
514
+
515
+ // Enforce minimum recording duration to prevent single-frame videos
516
+ const MIN_RECORDING_DURATION = 2000; // 2 seconds minimum
517
+ if (recordingDuration < MIN_RECORDING_DURATION) {
518
+ const waitTime = MIN_RECORDING_DURATION - recordingDuration;
519
+ logger.info(`Recording too short (${recordingDuration}ms), waiting ${waitTime}ms to ensure multiple frames`);
520
+ await new Promise(resolve => setTimeout(resolve, waitTime));
521
+ }
468
522
 
469
523
  try {
470
524
  // First try to gracefully stop FFmpeg by sending 'q'
471
525
  if (currentRecording && currentRecording.stdin) {
472
526
  logger.debug('Sending quit signal to FFmpeg...');
473
527
  currentRecording.stdin.write('q');
528
+ currentRecording.stdin.end(); // Close stdin to signal end
474
529
  }
475
530
 
476
- // Wait for FFmpeg to finish gracefully
531
+ // Wait for FFmpeg to finish gracefully with realtime encoding
477
532
  const gracefulTimeout = setTimeout(() => {
478
533
  if (currentRecording && !currentRecording.killed) {
534
+ logger.warn('FFmpeg did not exit gracefully after 10s, sending SIGTERM...');
479
535
  // If still running, try SIGTERM
480
536
  process.kill(currentRecording.pid, 'SIGTERM');
481
537
  }
482
- }, 2000);
538
+ }, 10000); // Wait longer for graceful shutdown
483
539
 
484
- // Wait up to 5 seconds for SIGTERM to work
540
+ // Wait up to 20 seconds for SIGTERM to work
485
541
  const hardKillTimeout = setTimeout(() => {
486
542
  if (currentRecording && !currentRecording.killed) {
543
+ logger.error('FFmpeg still running after SIGTERM, using SIGKILL...');
487
544
  // If still not dead, use SIGKILL as last resort
488
545
  process.kill(currentRecording.pid, 'SIGKILL');
489
546
  }
490
- }, 5000);
547
+ }, 20000); // Longer timeout to avoid killing prematurely
491
548
 
492
549
  // Wait for the process to fully exit
493
550
  if (currentRecording) {
@@ -498,8 +555,30 @@ export async function stopRecording() {
498
555
  clearTimeout(gracefulTimeout);
499
556
  clearTimeout(hardKillTimeout);
500
557
 
501
- // Additional wait to ensure filesystem is synced - increased for reliability
502
- await new Promise(resolve => setTimeout(resolve, 3000));
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
+ }
503
582
 
504
583
  // Read temp file path from disk (for cross-process access)
505
584
  let tempFile = currentTempFile; // Try in-memory first
@@ -547,21 +626,41 @@ export async function stopRecording() {
547
626
  path: tempFile
548
627
  });
549
628
 
550
- // Since WebM is already a valid streaming format, just copy the temp file
551
- // instead of trying to re-encode it which can hang
552
- logger.debug('Copying temp file to final output...');
629
+ // Re-mux to ensure proper container metadata (duration, seekability, etc.)
630
+ logger.debug('Re-muxing temp file to fix container metadata...');
553
631
 
554
632
  try {
555
- fs.copyFileSync(tempFile, outputPath);
556
- logger.info('Successfully copied temp file to final output');
633
+ // First, create a temporary fixed version
634
+ const fixedTempFile = tempFile.replace('.webm', '-fixed.webm');
635
+
636
+ const fixSuccess = await fixVideoContainer(tempFile, fixedTempFile);
637
+
638
+ if (fixSuccess && fs.existsSync(fixedTempFile) && fs.statSync(fixedTempFile).size > 0) {
639
+ // Use the fixed version
640
+ logger.info('Using re-muxed version with proper container metadata');
641
+ fs.copyFileSync(fixedTempFile, outputPath);
642
+
643
+ // Clean up the fixed temp file
644
+ try {
645
+ fs.unlinkSync(fixedTempFile);
646
+ } catch (e) {
647
+ logger.debug('Failed to delete fixed temp file:', e);
648
+ }
649
+ } else {
650
+ // Fallback: just copy the original temp file
651
+ logger.warn('Re-muxing failed, using original file');
652
+ fs.copyFileSync(tempFile, outputPath);
653
+ }
654
+
655
+ logger.info('Successfully finalized recording to output');
557
656
 
558
657
  // Verify the final file exists and has content
559
658
  if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
560
- throw new Error('Final output file is empty or missing after copy');
659
+ throw new Error('Final output file is empty or missing after processing');
561
660
  }
562
661
 
563
662
  } catch (error) {
564
- logger.error('Failed to copy temp file:', error);
663
+ logger.error('Failed to process temp file:', error);
565
664
  throw new Error('Failed to finalize recording: ' + error.message);
566
665
  }
567
666
 
@@ -688,3 +787,8 @@ export function getRecordingStatus() {
688
787
  outputPath
689
788
  };
690
789
  }
790
+
791
+ /**
792
+ * Export the fix function for external use
793
+ */
794
+ export { fixVideoContainer };
@@ -0,0 +1,141 @@
1
+ import si from 'systeminformation';
2
+ import { logger } from './logger.js';
3
+
4
+ /**
5
+ * Collects comprehensive system information including CPU, memory, OS, and graphics data.
6
+ * This matches the data format expected by the Dashcam backend (same as desktop app).
7
+ *
8
+ * @returns {Promise<Object>} System information object
9
+ */
10
+ export async function getSystemInfo() {
11
+ try {
12
+ logger.debug('Collecting system information...');
13
+
14
+ // Collect only essential system information quickly
15
+ // Graphics info can be very slow, so we skip it or use a short timeout
16
+ const [cpu, mem, osInfo, system] = await Promise.all([
17
+ si.cpu(),
18
+ si.mem(),
19
+ si.osInfo(),
20
+ si.system()
21
+ ]);
22
+
23
+ // Try to get graphics info with a very short timeout (optional)
24
+ let graphics = { controllers: [], displays: [] };
25
+ try {
26
+ graphics = await Promise.race([
27
+ si.graphics(),
28
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Graphics timeout')), 2000))
29
+ ]);
30
+ } catch (error) {
31
+ logger.debug('Graphics info timed out, using empty graphics data');
32
+ }
33
+
34
+ const systemInfo = {
35
+ cpu: {
36
+ manufacturer: cpu.manufacturer,
37
+ brand: cpu.brand,
38
+ vendor: cpu.vendor,
39
+ family: cpu.family,
40
+ model: cpu.model,
41
+ stepping: cpu.stepping,
42
+ revision: cpu.revision,
43
+ voltage: cpu.voltage,
44
+ speed: cpu.speed,
45
+ speedMin: cpu.speedMin,
46
+ speedMax: cpu.speedMax,
47
+ cores: cpu.cores,
48
+ physicalCores: cpu.physicalCores,
49
+ processors: cpu.processors,
50
+ socket: cpu.socket,
51
+ cache: cpu.cache
52
+ },
53
+ mem: {
54
+ total: mem.total,
55
+ free: mem.free,
56
+ used: mem.used,
57
+ active: mem.active,
58
+ available: mem.available,
59
+ swaptotal: mem.swaptotal,
60
+ swapused: mem.swapused,
61
+ swapfree: mem.swapfree
62
+ },
63
+ os: {
64
+ platform: osInfo.platform,
65
+ distro: osInfo.distro,
66
+ release: osInfo.release,
67
+ codename: osInfo.codename,
68
+ kernel: osInfo.kernel,
69
+ arch: osInfo.arch,
70
+ hostname: osInfo.hostname,
71
+ fqdn: osInfo.fqdn,
72
+ codepage: osInfo.codepage,
73
+ logofile: osInfo.logofile,
74
+ build: osInfo.build,
75
+ servicepack: osInfo.servicepack,
76
+ uefi: osInfo.uefi
77
+ },
78
+ graphics: {
79
+ controllers: graphics.controllers?.map(controller => ({
80
+ vendor: controller.vendor,
81
+ model: controller.model,
82
+ bus: controller.bus,
83
+ vram: controller.vram,
84
+ vramDynamic: controller.vramDynamic
85
+ })),
86
+ displays: graphics.displays?.map(display => ({
87
+ vendor: display.vendor,
88
+ model: display.model,
89
+ main: display.main,
90
+ builtin: display.builtin,
91
+ connection: display.connection,
92
+ resolutionX: display.resolutionX,
93
+ resolutionY: display.resolutionY,
94
+ sizeX: display.sizeX,
95
+ sizeY: display.sizeY,
96
+ pixelDepth: display.pixelDepth,
97
+ currentResX: display.currentResX,
98
+ currentResY: display.currentResY,
99
+ currentRefreshRate: display.currentRefreshRate
100
+ }))
101
+ },
102
+ system: {
103
+ manufacturer: system.manufacturer,
104
+ model: system.model,
105
+ version: system.version,
106
+ serial: system.serial,
107
+ uuid: system.uuid,
108
+ sku: system.sku,
109
+ virtual: system.virtual
110
+ }
111
+ };
112
+
113
+ logger.verbose('System information collected', {
114
+ cpuBrand: cpu.brand,
115
+ totalMemoryGB: (mem.total / (1024 * 1024 * 1024)).toFixed(2),
116
+ os: `${osInfo.distro} ${osInfo.release}`,
117
+ graphicsControllers: graphics.controllers?.length || 0,
118
+ displays: graphics.displays?.length || 0
119
+ });
120
+
121
+ return systemInfo;
122
+ } catch (error) {
123
+ logger.error('Failed to collect system information:', {
124
+ message: error.message,
125
+ stack: error.stack
126
+ });
127
+
128
+ // Return minimal system info as fallback
129
+ return {
130
+ cpu: { brand: 'Unknown', cores: 0 },
131
+ mem: { total: 0 },
132
+ os: {
133
+ platform: process.platform,
134
+ arch: process.arch,
135
+ release: process.version
136
+ },
137
+ graphics: { controllers: [], displays: [] },
138
+ system: { manufacturer: 'Unknown', model: 'Unknown' }
139
+ };
140
+ }
141
+ }
@@ -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);
@@ -20,8 +20,9 @@ async function ensureIconModule() {
20
20
  const windowsModule = await import("./windows.js");
21
21
  getIconAsBuffer = windowsModule.getIconAsBuffer;
22
22
  } else {
23
- // Linux fallback
24
- getIconAsBuffer = () => null;
23
+ // Linux support
24
+ const linuxModule = await import("./linux.js");
25
+ getIconAsBuffer = linuxModule.getIconAsBuffer;
25
26
  }
26
27
 
27
28
  iconModuleLoaded = true;