dashcam 1.4.0-beta → 1.4.1-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 +110 -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';
@@ -536,6 +536,9 @@ export async function stopRecording() {
536
536
  }
537
537
 
538
538
  try {
539
+ const platform = os.platform();
540
+ const isWindows = platform === 'win32';
541
+
539
542
  // First try to gracefully stop FFmpeg by sending 'q'
540
543
  if (currentRecording && currentRecording.stdin) {
541
544
  logger.debug('Sending quit signal to FFmpeg...');
@@ -546,18 +549,36 @@ export async function stopRecording() {
546
549
  // Wait for FFmpeg to finish gracefully with realtime encoding
547
550
  const gracefulTimeout = setTimeout(() => {
548
551
  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');
552
+ logger.warn('FFmpeg did not exit gracefully after 10s, forcing termination...');
553
+ // Use platform-appropriate termination
554
+ if (isWindows) {
555
+ // On Windows, use taskkill for clean termination
556
+ try {
557
+ execa.sync('taskkill', ['/PID', currentRecording.pid.toString(), '/T']);
558
+ } catch (e) {
559
+ logger.warn('taskkill failed, ffmpeg may have already exited');
560
+ }
561
+ } else {
562
+ // On Unix, use SIGTERM
563
+ process.kill(currentRecording.pid, 'SIGTERM');
564
+ }
552
565
  }
553
566
  }, 10000); // Wait longer for graceful shutdown
554
567
 
555
- // Wait up to 20 seconds for SIGTERM to work
568
+ // Wait up to 20 seconds for termination to work
556
569
  const hardKillTimeout = setTimeout(() => {
557
570
  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');
571
+ logger.error('FFmpeg still running after termination, forcing kill...');
572
+ // Use platform-appropriate forced kill
573
+ if (isWindows) {
574
+ try {
575
+ execa.sync('taskkill', ['/F', '/PID', currentRecording.pid.toString(), '/T']);
576
+ } catch (e) {
577
+ logger.warn('taskkill /F failed, ffmpeg may have already exited');
578
+ }
579
+ } else {
580
+ process.kill(currentRecording.pid, 'SIGKILL');
581
+ }
561
582
  }
562
583
  }, 20000); // Longer timeout to avoid killing prematurely
563
584
 
@@ -571,20 +592,39 @@ export async function stopRecording() {
571
592
  clearTimeout(hardKillTimeout);
572
593
 
573
594
  // Wait for filesystem to sync and file to be written
595
+ // This is critical on Windows where file writes may be buffered
596
+
597
+ // Windows needs extra time for file system sync
598
+ const extraSyncTime = isWindows ? 3000 : 1000;
599
+ logger.debug(`Waiting ${extraSyncTime}ms for file system to sync...`);
600
+ await new Promise(resolve => setTimeout(resolve, extraSyncTime));
601
+
574
602
  // Poll for the file to exist with content
575
603
  logger.debug('Waiting for temp file to be written...');
576
- const maxWaitTime = 10000; // Wait up to 10 seconds
604
+ const maxWaitTime = 15000; // Increased from 10s to 15s for Windows
577
605
  const startWait = Date.now();
578
606
  let tempFileReady = false;
607
+ let lastSize = 0;
608
+ let stableCount = 0;
579
609
 
580
610
  while (!tempFileReady && (Date.now() - startWait) < maxWaitTime) {
581
611
  const tempFile = currentTempFile;
582
612
  if (tempFile && fs.existsSync(tempFile)) {
583
613
  const stats = fs.statSync(tempFile);
584
614
  if (stats.size > 0) {
585
- logger.debug('Temp file is ready', { size: stats.size });
586
- tempFileReady = true;
587
- break;
615
+ // Check if file size is stable (hasn't changed)
616
+ if (stats.size === lastSize) {
617
+ stableCount++;
618
+ // Consider ready after size is stable for 2 consecutive checks
619
+ if (stableCount >= 2) {
620
+ logger.debug('Temp file is ready and stable', { size: stats.size });
621
+ tempFileReady = true;
622
+ break;
623
+ }
624
+ } else {
625
+ stableCount = 0;
626
+ }
627
+ lastSize = stats.size;
588
628
  }
589
629
  }
590
630
  // Wait a bit before checking again
@@ -592,7 +632,7 @@ export async function stopRecording() {
592
632
  }
593
633
 
594
634
  if (!tempFileReady) {
595
- logger.warn('Temp file not ready after waiting, proceeding anyway');
635
+ logger.warn('Temp file not confirmed stable after waiting, proceeding anyway', { lastSize });
596
636
  }
597
637
 
598
638
  // Read temp file path from disk (for cross-process access)
@@ -641,6 +681,63 @@ export async function stopRecording() {
641
681
  path: tempFile
642
682
  });
643
683
 
684
+ // Validate that the file is readable by ffprobe before attempting re-mux
685
+ logger.debug('Validating temp file integrity with ffprobe...');
686
+ const ffprobePath = await getFfprobePath();
687
+ try {
688
+ const { exitCode, stderr } = await execa(ffprobePath, [
689
+ '-v', 'error',
690
+ '-select_streams', 'v:0',
691
+ '-show_entries', 'stream=codec_type',
692
+ '-of', 'default=noprint_wrappers=1:nokey=1',
693
+ tempFile
694
+ ], { reject: false });
695
+
696
+ if (exitCode !== 0) {
697
+ logger.warn('Temp file validation failed', { exitCode, stderr });
698
+ // File may be corrupted, but we'll try to process it anyway
699
+ // Skip re-muxing and just copy the file
700
+ logger.warn('Skipping re-mux due to validation failure, copying file directly');
701
+ fs.copyFileSync(tempFile, outputPath);
702
+
703
+ // Clean up temp file
704
+ try {
705
+ fs.unlinkSync(tempFile);
706
+ } catch (e) {
707
+ logger.debug('Failed to delete temp file:', e);
708
+ }
709
+
710
+ // Skip the re-mux process below
711
+ const result = {
712
+ outputPath,
713
+ gifPath: `${outputPath.substring(0, outputPath.lastIndexOf('.'))}.gif`,
714
+ snapshotPath: `${outputPath.substring(0, outputPath.lastIndexOf('.'))}.png`,
715
+ duration: Date.now() - recordingStartTime,
716
+ fileSize: fs.existsSync(outputPath) ? fs.statSync(outputPath).size : 0,
717
+ clientStartDate: recordingStartTime,
718
+ apps: applicationTracker.stop().apps,
719
+ icons: applicationTracker.stop().icons,
720
+ logs: await logsTrackerManager.stop({ recorderId: path.basename(outputPath).replace('.webm', ''), screenId: '1' })
721
+ };
722
+
723
+ currentRecording = null;
724
+ recordingStartTime = null;
725
+ currentTempFile = null;
726
+
727
+ if (fs.existsSync(TEMP_FILE_INFO_PATH)) {
728
+ fs.unlinkSync(TEMP_FILE_INFO_PATH);
729
+ }
730
+
731
+ logExit();
732
+ return result;
733
+ }
734
+
735
+ logger.debug('Temp file validation passed');
736
+ } catch (error) {
737
+ logger.warn('Failed to validate temp file', { error: error.message });
738
+ // Continue with re-mux attempt anyway
739
+ }
740
+
644
741
  // Re-mux to ensure proper container metadata (duration, seekability, etc.)
645
742
  logger.debug('Re-muxing temp file to fix container metadata...');
646
743
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.4.0-beta",
3
+ "version": "1.4.1-beta",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
5
  "main": "bin/dashcam.js",
6
6
  "bin": {