dashcam 1.4.0-beta → 1.4.2-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.
Files changed (2) hide show
  1. package/lib/recorder.js +117 -13
  2. package/package.json +1 -1
package/lib/recorder.js CHANGED
@@ -3,7 +3,7 @@ import { logger, logFunctionCall } from './logger.js';
3
3
  import { createGif, createSnapshot } from './ffmpeg.js';
4
4
  import { applicationTracker } from './applicationTracker.js';
5
5
  import { logsTrackerManager, trimLogs } from './logs/index.js';
6
- import { getFfmpegPath } from './binaries.js';
6
+ import { getFfmpegPath, getFfprobePath } from './binaries.js';
7
7
  import path from 'path';
8
8
  import os from 'os';
9
9
  import fs from 'fs';
@@ -320,8 +320,14 @@ export async function startRecording({
320
320
 
321
321
  // Construct FFmpeg command arguments
322
322
  const platformArgs = await getPlatformArgs({ fps, includeAudio });
323
+
324
+ // Detect platform for encoder settings
325
+ const platform = os.platform();
326
+ const isWindows = platform === 'win32';
323
327
 
324
328
  const outputArgs = [
329
+ // Convert pixel format first to handle transparency issues on Windows
330
+ '-pix_fmt', 'yuv420p', // Force YUV420P format (no alpha channel)
325
331
  '-c:v', 'libvpx', // Use VP9 codec for better quality and compression
326
332
  '-quality', 'realtime', // Use realtime quality preset for faster encoding
327
333
  '-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
@@ -330,6 +336,7 @@ export async function startRecording({
330
336
  '-r', fps.toString(), // Ensure output framerate matches input
331
337
  '-g', fps.toString(), // Keyframe interval = 1 second (every fps frames) - ensures frequent keyframes
332
338
  '-force_key_frames', `expr:gte(t,n_forced*1)`, // Force keyframe every 1 second
339
+ '-auto-alt-ref', '0', // Disable auto alternate reference frames (fixes transparency encoding error)
333
340
  // WebM options for more frequent disk writes and proper stream handling
334
341
  '-f', 'webm', // Force WebM container format
335
342
  '-flush_packets', '1', // Flush packets immediately to disk - critical for short recordings
@@ -536,6 +543,9 @@ export async function stopRecording() {
536
543
  }
537
544
 
538
545
  try {
546
+ const platform = os.platform();
547
+ const isWindows = platform === 'win32';
548
+
539
549
  // First try to gracefully stop FFmpeg by sending 'q'
540
550
  if (currentRecording && currentRecording.stdin) {
541
551
  logger.debug('Sending quit signal to FFmpeg...');
@@ -546,18 +556,36 @@ export async function stopRecording() {
546
556
  // Wait for FFmpeg to finish gracefully with realtime encoding
547
557
  const gracefulTimeout = setTimeout(() => {
548
558
  if (currentRecording && !currentRecording.killed) {
549
- logger.warn('FFmpeg did not exit gracefully after 10s, sending SIGTERM...');
550
- // If still running, try SIGTERM
551
- process.kill(currentRecording.pid, 'SIGTERM');
559
+ logger.warn('FFmpeg did not exit gracefully after 10s, forcing termination...');
560
+ // Use platform-appropriate termination
561
+ if (isWindows) {
562
+ // On Windows, use taskkill for clean termination
563
+ try {
564
+ execa.sync('taskkill', ['/PID', currentRecording.pid.toString(), '/T']);
565
+ } catch (e) {
566
+ logger.warn('taskkill failed, ffmpeg may have already exited');
567
+ }
568
+ } else {
569
+ // On Unix, use SIGTERM
570
+ process.kill(currentRecording.pid, 'SIGTERM');
571
+ }
552
572
  }
553
573
  }, 10000); // Wait longer for graceful shutdown
554
574
 
555
- // Wait up to 20 seconds for SIGTERM to work
575
+ // Wait up to 20 seconds for termination to work
556
576
  const hardKillTimeout = setTimeout(() => {
557
577
  if (currentRecording && !currentRecording.killed) {
558
- logger.error('FFmpeg still running after SIGTERM, using SIGKILL...');
559
- // If still not dead, use SIGKILL as last resort
560
- process.kill(currentRecording.pid, 'SIGKILL');
578
+ logger.error('FFmpeg still running after termination, forcing kill...');
579
+ // Use platform-appropriate forced kill
580
+ if (isWindows) {
581
+ try {
582
+ execa.sync('taskkill', ['/F', '/PID', currentRecording.pid.toString(), '/T']);
583
+ } catch (e) {
584
+ logger.warn('taskkill /F failed, ffmpeg may have already exited');
585
+ }
586
+ } else {
587
+ process.kill(currentRecording.pid, 'SIGKILL');
588
+ }
561
589
  }
562
590
  }, 20000); // Longer timeout to avoid killing prematurely
563
591
 
@@ -571,20 +599,39 @@ export async function stopRecording() {
571
599
  clearTimeout(hardKillTimeout);
572
600
 
573
601
  // Wait for filesystem to sync and file to be written
602
+ // This is critical on Windows where file writes may be buffered
603
+
604
+ // Windows needs extra time for file system sync
605
+ const extraSyncTime = isWindows ? 3000 : 1000;
606
+ logger.debug(`Waiting ${extraSyncTime}ms for file system to sync...`);
607
+ await new Promise(resolve => setTimeout(resolve, extraSyncTime));
608
+
574
609
  // Poll for the file to exist with content
575
610
  logger.debug('Waiting for temp file to be written...');
576
- const maxWaitTime = 10000; // Wait up to 10 seconds
611
+ const maxWaitTime = 15000; // Increased from 10s to 15s for Windows
577
612
  const startWait = Date.now();
578
613
  let tempFileReady = false;
614
+ let lastSize = 0;
615
+ let stableCount = 0;
579
616
 
580
617
  while (!tempFileReady && (Date.now() - startWait) < maxWaitTime) {
581
618
  const tempFile = currentTempFile;
582
619
  if (tempFile && fs.existsSync(tempFile)) {
583
620
  const stats = fs.statSync(tempFile);
584
621
  if (stats.size > 0) {
585
- logger.debug('Temp file is ready', { size: stats.size });
586
- tempFileReady = true;
587
- break;
622
+ // Check if file size is stable (hasn't changed)
623
+ if (stats.size === lastSize) {
624
+ stableCount++;
625
+ // Consider ready after size is stable for 2 consecutive checks
626
+ if (stableCount >= 2) {
627
+ logger.debug('Temp file is ready and stable', { size: stats.size });
628
+ tempFileReady = true;
629
+ break;
630
+ }
631
+ } else {
632
+ stableCount = 0;
633
+ }
634
+ lastSize = stats.size;
588
635
  }
589
636
  }
590
637
  // Wait a bit before checking again
@@ -592,7 +639,7 @@ export async function stopRecording() {
592
639
  }
593
640
 
594
641
  if (!tempFileReady) {
595
- logger.warn('Temp file not ready after waiting, proceeding anyway');
642
+ logger.warn('Temp file not confirmed stable after waiting, proceeding anyway', { lastSize });
596
643
  }
597
644
 
598
645
  // Read temp file path from disk (for cross-process access)
@@ -641,6 +688,63 @@ export async function stopRecording() {
641
688
  path: tempFile
642
689
  });
643
690
 
691
+ // Validate that the file is readable by ffprobe before attempting re-mux
692
+ logger.debug('Validating temp file integrity with ffprobe...');
693
+ const ffprobePath = await getFfprobePath();
694
+ try {
695
+ const { exitCode, stderr } = await execa(ffprobePath, [
696
+ '-v', 'error',
697
+ '-select_streams', 'v:0',
698
+ '-show_entries', 'stream=codec_type',
699
+ '-of', 'default=noprint_wrappers=1:nokey=1',
700
+ tempFile
701
+ ], { reject: false });
702
+
703
+ if (exitCode !== 0) {
704
+ logger.warn('Temp file validation failed', { exitCode, stderr });
705
+ // File may be corrupted, but we'll try to process it anyway
706
+ // Skip re-muxing and just copy the file
707
+ logger.warn('Skipping re-mux due to validation failure, copying file directly');
708
+ fs.copyFileSync(tempFile, outputPath);
709
+
710
+ // Clean up temp file
711
+ try {
712
+ fs.unlinkSync(tempFile);
713
+ } catch (e) {
714
+ logger.debug('Failed to delete temp file:', e);
715
+ }
716
+
717
+ // Skip the re-mux process below
718
+ const result = {
719
+ outputPath,
720
+ gifPath: `${outputPath.substring(0, outputPath.lastIndexOf('.'))}.gif`,
721
+ snapshotPath: `${outputPath.substring(0, outputPath.lastIndexOf('.'))}.png`,
722
+ duration: Date.now() - recordingStartTime,
723
+ fileSize: fs.existsSync(outputPath) ? fs.statSync(outputPath).size : 0,
724
+ clientStartDate: recordingStartTime,
725
+ apps: applicationTracker.stop().apps,
726
+ icons: applicationTracker.stop().icons,
727
+ logs: await logsTrackerManager.stop({ recorderId: path.basename(outputPath).replace('.webm', ''), screenId: '1' })
728
+ };
729
+
730
+ currentRecording = null;
731
+ recordingStartTime = null;
732
+ currentTempFile = null;
733
+
734
+ if (fs.existsSync(TEMP_FILE_INFO_PATH)) {
735
+ fs.unlinkSync(TEMP_FILE_INFO_PATH);
736
+ }
737
+
738
+ logExit();
739
+ return result;
740
+ }
741
+
742
+ logger.debug('Temp file validation passed');
743
+ } catch (error) {
744
+ logger.warn('Failed to validate temp file', { error: error.message });
745
+ // Continue with re-mux attempt anyway
746
+ }
747
+
644
748
  // Re-mux to ensure proper container metadata (duration, seekability, etc.)
645
749
  logger.debug('Re-muxing temp file to fix container metadata...');
646
750
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.4.0-beta",
3
+ "version": "1.4.2-beta",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
5
  "main": "bin/dashcam.js",
6
6
  "bin": {