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.
- package/lib/recorder.js +117 -13
- 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,
|
|
550
|
-
//
|
|
551
|
-
|
|
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
|
|
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
|
|
559
|
-
//
|
|
560
|
-
|
|
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 =
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
|
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
|
|