dashcam 1.0.1-beta.3 → 1.0.1-beta.31

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/lib/recorder.js CHANGED
@@ -4,10 +4,52 @@ import { createGif, createSnapshot } from './ffmpeg.js';
4
4
  import { applicationTracker } from './applicationTracker.js';
5
5
  import { logsTrackerManager, trimLogs } from './logs/index.js';
6
6
  import { getFfmpegPath } from './binaries.js';
7
+ import { APP } from './config.js';
7
8
  import path from 'path';
8
9
  import os from 'os';
9
10
  import fs from 'fs';
10
11
 
12
+ /**
13
+ * Fix/repair a video file by re-muxing it with proper container metadata
14
+ * This copies streams without re-encoding and ensures proper container finalization
15
+ */
16
+ async function fixVideoContainer(inputVideoPath, outputVideoPath) {
17
+ const logExit = logFunctionCall('fixVideoContainer', { inputVideoPath, outputVideoPath });
18
+
19
+ try {
20
+ const ffmpegPath = await getFfmpegPath();
21
+
22
+ logger.info('Re-muxing video to fix container metadata', {
23
+ input: inputVideoPath,
24
+ output: outputVideoPath
25
+ });
26
+
27
+ const args = [
28
+ '-i', inputVideoPath,
29
+ '-vcodec', 'copy', // Copy video stream without re-encoding
30
+ '-acodec', 'copy', // Copy audio stream without re-encoding
31
+ '-movflags', 'faststart', // Enable fast start for web playback
32
+ outputVideoPath,
33
+ '-y', // Overwrite output file
34
+ '-hide_banner'
35
+ ];
36
+
37
+ await execa(ffmpegPath, args);
38
+
39
+ logger.info('Successfully re-muxed video', {
40
+ outputPath: outputVideoPath,
41
+ outputSize: fs.existsSync(outputVideoPath) ? fs.statSync(outputVideoPath).size : 0
42
+ });
43
+
44
+ logExit();
45
+ return true;
46
+ } catch (error) {
47
+ logger.error('Failed to fix video container', { error: error.message });
48
+ logExit();
49
+ return false;
50
+ }
51
+ }
52
+
11
53
  /**
12
54
  * Dynamically detect the primary screen capture device for the current platform
13
55
  */
@@ -104,8 +146,8 @@ let outputPath = null;
104
146
  let recordingStartTime = null;
105
147
  let currentTempFile = null;
106
148
 
107
- // File paths - use system temp for runtime data
108
- const DASHCAM_TEMP_DIR = path.join(os.tmpdir(), 'dashcam');
149
+ // File paths - use APP config directory for better Windows compatibility
150
+ const DASHCAM_TEMP_DIR = APP.configDir;
109
151
  const TEMP_FILE_INFO_PATH = path.join(DASHCAM_TEMP_DIR, 'temp-file.json');
110
152
 
111
153
  // Platform-specific configurations
@@ -116,9 +158,8 @@ const PLATFORM_CONFIG = {
116
158
  audioInput: '0', // Default audio device if needed
117
159
  audioFormat: 'avfoundation',
118
160
  extraArgs: [
119
- '-video_size', '1920x1080', // Set explicit resolution
120
- '-pixel_format', 'uyvy422', // Use supported pixel format
121
- '-r', '30' // Set frame rate
161
+ '-capture_cursor', '1', // Capture mouse cursor
162
+ '-capture_mouse_clicks', '1' // Capture mouse clicks
122
163
  ]
123
164
  },
124
165
  win32: {
@@ -160,6 +201,7 @@ async function getPlatformArgs({ fps, includeAudio }) {
160
201
  });
161
202
 
162
203
  const args = [
204
+ '-thread_queue_size', '512', // Increase input buffer to prevent drops
163
205
  '-f', config.inputFormat
164
206
  ];
165
207
 
@@ -170,12 +212,14 @@ async function getPlatformArgs({ fps, includeAudio }) {
170
212
 
171
213
  args.push(
172
214
  '-framerate', fps.toString(),
215
+ '-probesize', '50M', // Increase probe size for better stream detection
173
216
  '-i', screenInput
174
217
  );
175
218
 
176
219
  // Add audio capture if enabled
177
220
  if (includeAudio) {
178
221
  args.push(
222
+ '-thread_queue_size', '512', // Increase audio buffer too
179
223
  '-f', config.audioFormat,
180
224
  '-i', config.audioInput
181
225
  );
@@ -195,12 +239,12 @@ async function getPlatformArgs({ fps, includeAudio }) {
195
239
  }
196
240
 
197
241
  /**
198
- * Clear the tmp/recordings directory
242
+ * Clear the recordings directory
199
243
  */
200
244
  function clearRecordingsDirectory() {
201
245
  const logExit = logFunctionCall('clearRecordingsDirectory');
202
246
 
203
- const directory = path.join(process.cwd(), 'tmp', 'recordings');
247
+ const directory = APP.recordingsDir;
204
248
 
205
249
  try {
206
250
  if (fs.existsSync(directory)) {
@@ -238,8 +282,8 @@ function generateOutputPath() {
238
282
  const logExit = logFunctionCall('generateOutputPath');
239
283
 
240
284
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
241
- // Use system temp directory with dashcam subdirectory
242
- const directory = path.join(os.tmpdir(), 'dashcam', 'recordings');
285
+ // Use APP recordings directory for consistent cross-platform location
286
+ const directory = APP.recordingsDir;
243
287
  const filepath = path.join(directory, `recording-${timestamp}.webm`);
244
288
 
245
289
  logger.verbose('Generating output path', {
@@ -278,15 +322,21 @@ export async function startRecording({
278
322
  // Construct FFmpeg command arguments
279
323
  const platformArgs = await getPlatformArgs({ fps, includeAudio });
280
324
  const outputArgs = [
281
- '-c:v', 'libvpx', // Use VP8 codec instead of VP9 for better compatibility
282
- '-b:v', '1M', // Set specific bitrate instead of variable
283
- // Remove explicit pixel format to let ffmpeg handle conversion automatically
325
+ '-c:v', 'libvpx-vp9', // Use VP9 codec for better quality and compression
326
+ '-quality', 'realtime', // Use realtime quality preset for faster encoding
327
+ '-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
328
+ '-deadline', 'realtime',// Realtime encoding mode for lowest latency
329
+ '-b:v', '2M', // Target bitrate
284
330
  '-r', fps.toString(), // Ensure output framerate matches input
285
- '-g', '30', // Keyframe every 30 frames
286
- // WebM options for more frequent disk writes
287
- '-f', 'webm', // Force WebM container format
288
- '-flush_packets', '1', // Flush packets immediately to disk
289
- '-max_muxing_queue_size', '1024' // Limit muxing queue to prevent delays
331
+ '-g', fps.toString(), // Keyframe interval = 1 second (every fps frames) - ensures frequent keyframes
332
+ '-force_key_frames', `expr:gte(t,n_forced*1)`, // Force keyframe every 1 second
333
+ // WebM options for more frequent disk writes and proper stream handling
334
+ '-f', 'webm', // Force WebM container format
335
+ '-flush_packets', '1', // Flush packets immediately to disk - critical for short recordings
336
+ '-fflags', '+genpts', // Generate presentation timestamps
337
+ '-avoid_negative_ts', 'make_zero', // Avoid negative timestamps
338
+ '-vsync', '1', // Ensure every frame is encoded (CFR - constant frame rate)
339
+ '-max_muxing_queue_size', '9999' // Large queue to prevent frame drops
290
340
  ];
291
341
 
292
342
  if (includeAudio) {
@@ -358,13 +408,11 @@ export async function startRecording({
358
408
  all: true, // Capture both stdout and stderr
359
409
  stdin: 'pipe' // Enable stdin for sending 'q' to stop recording
360
410
  });
361
-
362
- recordingStartTime = Date.now();
363
411
 
364
412
  logger.info('FFmpeg process spawned', {
365
413
  pid: currentRecording.pid,
366
414
  args: args.slice(-5), // Log last 5 args including output file
367
- tempFile
415
+ tempFile
368
416
  });
369
417
 
370
418
  // Check if temp file gets created within first few seconds
@@ -392,6 +440,14 @@ export async function startRecording({
392
440
  directory: path.dirname(outputPath)
393
441
  });
394
442
 
443
+ // Set recording start time AFTER log tracker is initialized
444
+ // This ensures the timeline starts when the tracker is ready to capture events
445
+ recordingStartTime = Date.now();
446
+ logger.info('Recording timeline started', {
447
+ recordingStartTime,
448
+ recordingStartTimeReadable: new Date(recordingStartTime).toISOString()
449
+ });
450
+
395
451
  if (currentRecording.all) {
396
452
  currentRecording.all.setEncoding('utf8');
397
453
  currentRecording.all.on('data', (data) => {
@@ -456,29 +512,40 @@ export async function stopRecording() {
456
512
  duration: recordingDuration,
457
513
  durationSeconds: (recordingDuration / 1000).toFixed(1)
458
514
  });
515
+
516
+ // Enforce minimum recording duration to prevent single-frame videos
517
+ const MIN_RECORDING_DURATION = 2000; // 2 seconds minimum
518
+ if (recordingDuration < MIN_RECORDING_DURATION) {
519
+ const waitTime = MIN_RECORDING_DURATION - recordingDuration;
520
+ logger.info(`Recording too short (${recordingDuration}ms), waiting ${waitTime}ms to ensure multiple frames`);
521
+ await new Promise(resolve => setTimeout(resolve, waitTime));
522
+ }
459
523
 
460
524
  try {
461
525
  // First try to gracefully stop FFmpeg by sending 'q'
462
526
  if (currentRecording && currentRecording.stdin) {
463
527
  logger.debug('Sending quit signal to FFmpeg...');
464
528
  currentRecording.stdin.write('q');
529
+ currentRecording.stdin.end(); // Close stdin to signal end
465
530
  }
466
531
 
467
- // Wait for FFmpeg to finish gracefully
532
+ // Wait for FFmpeg to finish gracefully with realtime encoding
468
533
  const gracefulTimeout = setTimeout(() => {
469
534
  if (currentRecording && !currentRecording.killed) {
535
+ logger.warn('FFmpeg did not exit gracefully after 10s, sending SIGTERM...');
470
536
  // If still running, try SIGTERM
471
537
  process.kill(currentRecording.pid, 'SIGTERM');
472
538
  }
473
- }, 2000);
539
+ }, 10000); // Wait longer for graceful shutdown
474
540
 
475
- // Wait up to 5 seconds for SIGTERM to work
541
+ // Wait up to 20 seconds for SIGTERM to work
476
542
  const hardKillTimeout = setTimeout(() => {
477
543
  if (currentRecording && !currentRecording.killed) {
544
+ logger.error('FFmpeg still running after SIGTERM, using SIGKILL...');
478
545
  // If still not dead, use SIGKILL as last resort
479
546
  process.kill(currentRecording.pid, 'SIGKILL');
480
547
  }
481
- }, 5000);
548
+ }, 20000); // Longer timeout to avoid killing prematurely
482
549
 
483
550
  // Wait for the process to fully exit
484
551
  if (currentRecording) {
@@ -489,8 +556,30 @@ export async function stopRecording() {
489
556
  clearTimeout(gracefulTimeout);
490
557
  clearTimeout(hardKillTimeout);
491
558
 
492
- // Additional wait to ensure filesystem is synced - increased for reliability
493
- await new Promise(resolve => setTimeout(resolve, 3000));
559
+ // Wait for filesystem to sync and file to be written
560
+ // Poll for the file to exist with content
561
+ logger.debug('Waiting for temp file to be written...');
562
+ const maxWaitTime = 10000; // Wait up to 10 seconds
563
+ const startWait = Date.now();
564
+ let tempFileReady = false;
565
+
566
+ while (!tempFileReady && (Date.now() - startWait) < maxWaitTime) {
567
+ const tempFile = currentTempFile;
568
+ if (tempFile && fs.existsSync(tempFile)) {
569
+ const stats = fs.statSync(tempFile);
570
+ if (stats.size > 0) {
571
+ logger.debug('Temp file is ready', { size: stats.size });
572
+ tempFileReady = true;
573
+ break;
574
+ }
575
+ }
576
+ // Wait a bit before checking again
577
+ await new Promise(resolve => setTimeout(resolve, 500));
578
+ }
579
+
580
+ if (!tempFileReady) {
581
+ logger.warn('Temp file not ready after waiting, proceeding anyway');
582
+ }
494
583
 
495
584
  // Read temp file path from disk (for cross-process access)
496
585
  let tempFile = currentTempFile; // Try in-memory first
@@ -538,21 +627,41 @@ export async function stopRecording() {
538
627
  path: tempFile
539
628
  });
540
629
 
541
- // Since WebM is already a valid streaming format, just copy the temp file
542
- // instead of trying to re-encode it which can hang
543
- logger.debug('Copying temp file to final output...');
630
+ // Re-mux to ensure proper container metadata (duration, seekability, etc.)
631
+ logger.debug('Re-muxing temp file to fix container metadata...');
544
632
 
545
633
  try {
546
- fs.copyFileSync(tempFile, outputPath);
547
- logger.info('Successfully copied temp file to final output');
634
+ // First, create a temporary fixed version
635
+ const fixedTempFile = tempFile.replace('.webm', '-fixed.webm');
636
+
637
+ const fixSuccess = await fixVideoContainer(tempFile, fixedTempFile);
638
+
639
+ if (fixSuccess && fs.existsSync(fixedTempFile) && fs.statSync(fixedTempFile).size > 0) {
640
+ // Use the fixed version
641
+ logger.info('Using re-muxed version with proper container metadata');
642
+ fs.copyFileSync(fixedTempFile, outputPath);
643
+
644
+ // Clean up the fixed temp file
645
+ try {
646
+ fs.unlinkSync(fixedTempFile);
647
+ } catch (e) {
648
+ logger.debug('Failed to delete fixed temp file:', e);
649
+ }
650
+ } else {
651
+ // Fallback: just copy the original temp file
652
+ logger.warn('Re-muxing failed, using original file');
653
+ fs.copyFileSync(tempFile, outputPath);
654
+ }
655
+
656
+ logger.info('Successfully finalized recording to output');
548
657
 
549
658
  // Verify the final file exists and has content
550
659
  if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
551
- throw new Error('Final output file is empty or missing after copy');
660
+ throw new Error('Final output file is empty or missing after processing');
552
661
  }
553
662
 
554
663
  } catch (error) {
555
- logger.error('Failed to copy temp file:', error);
664
+ logger.error('Failed to process temp file:', error);
556
665
  throw new Error('Failed to finalize recording: ' + error.message);
557
666
  }
558
667
 
@@ -622,6 +731,12 @@ export async function stopRecording() {
622
731
  icons: appTrackingResults.icons, // Include application icons metadata
623
732
  logs: logTrackingResults // Include log tracking results
624
733
  };
734
+
735
+ logger.info('Recording stopped with clientStartDate', {
736
+ clientStartDate: recordingStartTime,
737
+ clientStartDateReadable: new Date(recordingStartTime).toISOString(),
738
+ duration: result.duration
739
+ });
625
740
 
626
741
  currentRecording = null;
627
742
  recordingStartTime = null;
@@ -673,3 +788,8 @@ export function getRecordingStatus() {
673
788
  outputPath
674
789
  };
675
790
  }
791
+
792
+ /**
793
+ * Export the fix function for external use
794
+ */
795
+ 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
+ }
@@ -1,5 +1,6 @@
1
1
  import { Tail } from 'tail';
2
2
  import { logger } from '../logger.js';
3
+ import fs from 'fs';
3
4
 
4
5
  // Simple function to get stats for events in the last minute
5
6
  function getStats(eventTimes = []) {
@@ -40,7 +41,13 @@ export class FileTracker {
40
41
  this.trackedFile = trackedFile;
41
42
 
42
43
  try {
43
- this.tail = new Tail(this.trackedFile, { encoding: 'ascii' });
44
+ // Ensure the file exists before creating the Tail watcher
45
+ if (!fs.existsSync(this.trackedFile)) {
46
+ logger.warn(`File does not exist, creating: ${this.trackedFile}`);
47
+ fs.writeFileSync(this.trackedFile, '', 'utf8');
48
+ }
49
+
50
+ this.tail = new Tail(this.trackedFile, { encoding: 'utf8' });
44
51
  this.tail.on('line', (line) => {
45
52
  const time = Date.now();
46
53
  this.eventTimes.push(time);
@@ -55,6 +55,7 @@ export class LogsTracker {
55
55
 
56
56
  this.files[filePath] = {
57
57
  status,
58
+ filePath, // Store file path for later reference
58
59
  unsubscribe: this.fileTrackerManager.subscribe(filePath, callback),
59
60
  };
60
61
 
@@ -98,13 +99,25 @@ export class LogsTracker {
98
99
 
99
100
  getStatus() {
100
101
  let items = [];
102
+ let filePathMap = {};
103
+
101
104
  if (this.isWatchOnly) {
102
- items = Object.keys(this.files).map((filePath) => ({
103
- ...this.fileTrackerManager.getStats(filePath),
104
- item: this.fileToIndex[filePath],
105
- }));
105
+ items = Object.keys(this.files).map((filePath) => {
106
+ const index = this.fileToIndex[filePath];
107
+ filePathMap[index] = filePath;
108
+ return {
109
+ ...this.fileTrackerManager.getStats(filePath),
110
+ item: index, // Keep numeric index to match events
111
+ };
112
+ });
106
113
  } else {
107
- items = Object.values(this.files).map(({ status }) => status);
114
+ items = Object.values(this.files).map(({ status, filePath }) => {
115
+ const index = status.item;
116
+ filePathMap[index] = filePath;
117
+ return {
118
+ ...status,
119
+ };
120
+ });
108
121
  }
109
122
 
110
123
  const totalCount = items.reduce((acc, status) => acc + status.count, 0);
@@ -112,11 +125,12 @@ export class LogsTracker {
112
125
  return [
113
126
  {
114
127
  id: 'CLI',
115
- name: 'CLI',
128
+ name: 'File Logs', // More descriptive name
116
129
  type: 'cli',
117
130
  fileLocation: this.fileLocation,
118
131
  items: items,
119
132
  count: totalCount,
133
+ filePathMap: filePathMap, // Include mapping for UI to display file paths
120
134
  },
121
135
  ];
122
136
  }
@@ -126,7 +140,7 @@ export class LogsTracker {
126
140
  const status = this.getStatus();
127
141
  return status.length > 0 ? status[0] : {
128
142
  id: 'CLI',
129
- name: 'CLI',
143
+ name: 'File Logs',
130
144
  type: 'cli',
131
145
  fileLocation: this.fileLocation,
132
146
  items: [],
@@ -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;