dashcam 1.3.31 → 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.
- package/bin/dashcam-background.js +7 -50
- package/bin/dashcam.js +151 -8
- package/lib/ffmpeg.js +10 -6
- package/lib/recorder.js +114 -16
- package/package.json +1 -1
- package/test_workflow.sh +6 -6
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This script runs detached from the parent process to handle long-running recordings
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { startRecording
|
|
7
|
+
import { startRecording } from '../lib/recorder.js';
|
|
8
8
|
import { logger, setVerbose } from '../lib/logger.js';
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import path from 'path';
|
|
@@ -161,56 +161,13 @@ async function runBackgroundRecording() {
|
|
|
161
161
|
}
|
|
162
162
|
isShuttingDown = true;
|
|
163
163
|
|
|
164
|
-
logger.info(`Received ${signal},
|
|
165
|
-
console.log('[Background] Received stop signal,
|
|
164
|
+
logger.info(`Received ${signal}, background process will be killed`);
|
|
165
|
+
console.log('[Background] Received stop signal, process will be terminated...');
|
|
166
166
|
|
|
167
|
-
try
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (stopResult) {
|
|
172
|
-
logger.info('Recording stopped successfully', {
|
|
173
|
-
outputPath: stopResult.outputPath,
|
|
174
|
-
duration: stopResult.duration
|
|
175
|
-
});
|
|
176
|
-
console.log('[Background] Recording stopped successfully:', {
|
|
177
|
-
outputPath: stopResult.outputPath,
|
|
178
|
-
duration: stopResult.duration
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Write recording result for stop command to upload
|
|
182
|
-
console.log('[Background] Writing recording result for stop command...');
|
|
183
|
-
writeRecordingResult({
|
|
184
|
-
outputPath: stopResult.outputPath,
|
|
185
|
-
duration: stopResult.duration,
|
|
186
|
-
clientStartDate: stopResult.clientStartDate,
|
|
187
|
-
apps: stopResult.apps,
|
|
188
|
-
logs: stopResult.logs,
|
|
189
|
-
gifPath: stopResult.gifPath,
|
|
190
|
-
snapshotPath: stopResult.snapshotPath,
|
|
191
|
-
// Include options so stop command can use them for upload
|
|
192
|
-
title: options.title,
|
|
193
|
-
description: options.description,
|
|
194
|
-
project: options.project || options.k
|
|
195
|
-
});
|
|
196
|
-
console.log('[Background] Recording result written successfully');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Update status to indicate recording stopped
|
|
200
|
-
writeStatus({
|
|
201
|
-
isRecording: false,
|
|
202
|
-
completedTime: Date.now(),
|
|
203
|
-
pid: process.pid
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
console.log('[Background] Background process exiting successfully');
|
|
207
|
-
logger.info('Background process exiting successfully');
|
|
208
|
-
process.exit(0);
|
|
209
|
-
} catch (error) {
|
|
210
|
-
console.error('[Background] Error during shutdown:', error.message);
|
|
211
|
-
logger.error('Error during shutdown:', error);
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
167
|
+
// Don't try to stop recording here - the main process will handle cleanup
|
|
168
|
+
// after killing this process. Just exit.
|
|
169
|
+
logger.info('Background process exiting');
|
|
170
|
+
process.exit(0);
|
|
214
171
|
};
|
|
215
172
|
|
|
216
173
|
// Register signal handlers
|
package/bin/dashcam.js
CHANGED
|
@@ -137,7 +137,7 @@ async function recordingAction(options, command) {
|
|
|
137
137
|
|
|
138
138
|
// Add timeout to prevent hanging
|
|
139
139
|
const startRecordingPromise = processManager.startRecording({
|
|
140
|
-
fps: parseInt(options.fps) ||
|
|
140
|
+
fps: parseInt(options.fps) || 10,
|
|
141
141
|
audio: options.audio,
|
|
142
142
|
output: options.output,
|
|
143
143
|
title: options.title,
|
|
@@ -241,7 +241,7 @@ program
|
|
|
241
241
|
.command('record')
|
|
242
242
|
.description('Start a recording terminal to be included in your dashcam video recording')
|
|
243
243
|
.option('-a, --audio', 'Include audio in the recording')
|
|
244
|
-
.option('-f, --fps <fps>', 'Frames per second (default:
|
|
244
|
+
.option('-f, --fps <fps>', 'Frames per second (default: 10)', '10')
|
|
245
245
|
.option('-o, --output <path>', 'Custom output path')
|
|
246
246
|
.option('-t, --title <title>', 'Title for the recording')
|
|
247
247
|
.option('-d, --description <description>', 'Description for the recording (supports markdown)')
|
|
@@ -484,9 +484,36 @@ program
|
|
|
484
484
|
logger.error('Failed to read temp file info', { error });
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
|
|
487
|
+
// Fallback: look for temp files in the same directory
|
|
488
|
+
let tempFile = tempFileInfo?.tempFile;
|
|
489
|
+
if (!tempFile || !fs.existsSync(tempFile)) {
|
|
490
|
+
// Try to find any temp-*.webm file in the output directory
|
|
491
|
+
const outputDir = path.dirname(result.outputPath);
|
|
492
|
+
if (fs.existsSync(outputDir)) {
|
|
493
|
+
const files = fs.readdirSync(outputDir);
|
|
494
|
+
const tempFiles = files.filter(f => f.startsWith('temp-') && f.endsWith('.webm'));
|
|
495
|
+
if (tempFiles.length > 0) {
|
|
496
|
+
// Use the most recent temp file
|
|
497
|
+
tempFiles.sort().reverse();
|
|
498
|
+
tempFile = path.join(outputDir, tempFiles[0]);
|
|
499
|
+
logger.info('Found temp file in output directory', { tempFile });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
488
504
|
const outputPath = result.outputPath;
|
|
489
505
|
|
|
506
|
+
// Verify we have a temp file to work with
|
|
507
|
+
if (!tempFile) {
|
|
508
|
+
console.error('No temp file found for recording');
|
|
509
|
+
logger.error('Temp file missing', {
|
|
510
|
+
tempFileInfo,
|
|
511
|
+
outputDir: path.dirname(result.outputPath),
|
|
512
|
+
expectedPattern: 'temp-*.webm'
|
|
513
|
+
});
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
|
|
490
517
|
// Fix the video with FFmpeg (handle incomplete recordings)
|
|
491
518
|
logger.info('Fixing video with FFmpeg', { tempFile, outputPath });
|
|
492
519
|
if (fs.existsSync(tempFile)) {
|
|
@@ -502,14 +529,129 @@ program
|
|
|
502
529
|
logger.warn('Temp file not found, using output path directly', { tempFile });
|
|
503
530
|
}
|
|
504
531
|
|
|
505
|
-
//
|
|
506
|
-
|
|
532
|
+
// Verify the output file exists before proceeding
|
|
533
|
+
if (!fs.existsSync(outputPath)) {
|
|
534
|
+
console.error('Recording file not found after processing');
|
|
535
|
+
logger.error('Output file missing', { outputPath, tempFile, tempFileExists: fs.existsSync(tempFile) });
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
logger.info('Output file ready for upload', {
|
|
540
|
+
outputPath,
|
|
541
|
+
size: fs.statSync(outputPath).size
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Generate paths for additional assets
|
|
545
|
+
const basePath = outputPath.substring(0, outputPath.lastIndexOf('.'));
|
|
546
|
+
const gifPath = `${basePath}.gif`;
|
|
547
|
+
const snapshotPath = `${basePath}.png`;
|
|
548
|
+
|
|
549
|
+
// Create GIF and snapshot (non-blocking, don't fail if these fail)
|
|
550
|
+
console.log('Creating preview assets...');
|
|
551
|
+
const { createGif, createSnapshot } = await import('../lib/ffmpeg.js');
|
|
552
|
+
|
|
553
|
+
// Add timeout wrapper to prevent hanging on large files
|
|
554
|
+
const withTimeout = (promise, timeoutMs, name) => {
|
|
555
|
+
return Promise.race([
|
|
556
|
+
promise,
|
|
557
|
+
new Promise((_, reject) =>
|
|
558
|
+
setTimeout(() => reject(new Error(`${name} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
559
|
+
)
|
|
560
|
+
]);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await Promise.all([
|
|
565
|
+
withTimeout(
|
|
566
|
+
createGif(outputPath, gifPath),
|
|
567
|
+
30000, // 30 second timeout
|
|
568
|
+
'GIF creation'
|
|
569
|
+
).catch(err => {
|
|
570
|
+
logger.warn('Failed to create GIF', { error: err.message });
|
|
571
|
+
}),
|
|
572
|
+
withTimeout(
|
|
573
|
+
createSnapshot(outputPath, snapshotPath, 0),
|
|
574
|
+
10000, // 10 second timeout
|
|
575
|
+
'Snapshot creation'
|
|
576
|
+
).catch(err => {
|
|
577
|
+
logger.warn('Failed to create snapshot', { error: err.message });
|
|
578
|
+
})
|
|
579
|
+
]);
|
|
580
|
+
logger.debug('Preview assets created successfully');
|
|
581
|
+
} catch (error) {
|
|
582
|
+
logger.warn('Failed to create preview assets', { error: error.message });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Collect logs and app tracking data
|
|
586
|
+
console.log('Collecting tracking data...');
|
|
587
|
+
|
|
588
|
+
// Get app tracking data
|
|
589
|
+
const { applicationTracker } = await import('../lib/applicationTracker.js');
|
|
590
|
+
const appTrackingResults = applicationTracker.stop();
|
|
591
|
+
logger.info('Collected app tracking results', {
|
|
592
|
+
apps: appTrackingResults.apps?.length || 0,
|
|
593
|
+
icons: appTrackingResults.icons?.length || 0,
|
|
594
|
+
events: appTrackingResults.events?.length || 0
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Get log tracking data - read directly from files since background process was killed
|
|
598
|
+
const recordingDir = path.dirname(outputPath);
|
|
599
|
+
let logTrackingResults = [];
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
// Check for CLI logs file
|
|
603
|
+
const cliLogsFile = path.join(recordingDir, 'dashcam_logs_cli.jsonl');
|
|
604
|
+
if (fs.existsSync(cliLogsFile)) {
|
|
605
|
+
const { jsonl } = await import('../lib/utilities/jsonl.js');
|
|
606
|
+
const cliLogs = jsonl.read(cliLogsFile);
|
|
607
|
+
if (cliLogs && cliLogs.length > 0) {
|
|
608
|
+
logTrackingResults.push({
|
|
609
|
+
type: 'cli',
|
|
610
|
+
name: 'CLI Logs',
|
|
611
|
+
fileLocation: cliLogsFile,
|
|
612
|
+
count: cliLogs.length,
|
|
613
|
+
trimmedFileLocation: cliLogsFile
|
|
614
|
+
});
|
|
615
|
+
logger.info('Found CLI logs', { count: cliLogs.length, file: cliLogsFile });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Check for web logs file
|
|
620
|
+
const webLogsFile = path.join(recordingDir, 'dashcam_logs_web_events.jsonl');
|
|
621
|
+
if (fs.existsSync(webLogsFile)) {
|
|
622
|
+
const { jsonl } = await import('../lib/utilities/jsonl.js');
|
|
623
|
+
const webLogs = jsonl.read(webLogsFile);
|
|
624
|
+
if (webLogs && webLogs.length > 0) {
|
|
625
|
+
logTrackingResults.push({
|
|
626
|
+
type: 'web',
|
|
627
|
+
name: 'Web Logs',
|
|
628
|
+
fileLocation: webLogsFile,
|
|
629
|
+
count: webLogs.length,
|
|
630
|
+
trimmedFileLocation: webLogsFile
|
|
631
|
+
});
|
|
632
|
+
logger.info('Found web logs', { count: webLogs.length, file: webLogsFile });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
logger.info('Collected log tracking results', {
|
|
637
|
+
trackers: logTrackingResults.length,
|
|
638
|
+
totalEvents: logTrackingResults.reduce((sum, result) => sum + result.count, 0)
|
|
639
|
+
});
|
|
640
|
+
} catch (error) {
|
|
641
|
+
logger.warn('Failed to collect log tracking results', { error: error.message });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// The recording is on disk and can be uploaded with full metadata
|
|
507
645
|
const recordingResult = {
|
|
508
646
|
outputPath,
|
|
647
|
+
gifPath,
|
|
648
|
+
snapshotPath,
|
|
509
649
|
duration: result.duration,
|
|
650
|
+
fileSize: fs.statSync(outputPath).size,
|
|
510
651
|
clientStartDate: activeStatus.startTime,
|
|
511
|
-
apps:
|
|
512
|
-
|
|
652
|
+
apps: appTrackingResults.apps,
|
|
653
|
+
icons: appTrackingResults.icons,
|
|
654
|
+
logs: logTrackingResults,
|
|
513
655
|
title: activeStatus?.options?.title,
|
|
514
656
|
description: activeStatus?.options?.description,
|
|
515
657
|
project: activeStatus?.options?.project
|
|
@@ -532,6 +674,7 @@ program
|
|
|
532
674
|
duration: recordingResult.duration,
|
|
533
675
|
clientStartDate: recordingResult.clientStartDate,
|
|
534
676
|
apps: recordingResult.apps,
|
|
677
|
+
icons: recordingResult.icons,
|
|
535
678
|
logs: recordingResult.logs,
|
|
536
679
|
gifPath: recordingResult.gifPath,
|
|
537
680
|
snapshotPath: recordingResult.snapshotPath
|
|
@@ -749,7 +892,7 @@ program
|
|
|
749
892
|
if (!targetFile) {
|
|
750
893
|
console.error('Please provide a file path or use --recover option');
|
|
751
894
|
console.log('Examples:');
|
|
752
|
-
console.log(' dashcam upload /path/to/recording.
|
|
895
|
+
console.log(' dashcam upload /path/to/recording.mp4');
|
|
753
896
|
console.log(' dashcam upload --recover');
|
|
754
897
|
process.exit(1);
|
|
755
898
|
}
|
package/lib/ffmpeg.js
CHANGED
|
@@ -72,8 +72,8 @@ export async function createGif(inputVideoPath, outputGifPath) {
|
|
|
72
72
|
throw new Error('Video file is not ready or is corrupted');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const gifFps = 4
|
|
76
|
-
const gifDuration = 10
|
|
75
|
+
const gifFps = 2; // Reduced from 4 to 2 fps for faster generation
|
|
76
|
+
const gifDuration = 5; // Reduced from 10 to 5 seconds for faster generation
|
|
77
77
|
const gifFrames = Math.ceil(gifDuration * gifFps);
|
|
78
78
|
|
|
79
79
|
// Get video duration in seconds
|
|
@@ -94,8 +94,8 @@ export async function createGif(inputVideoPath, outputGifPath) {
|
|
|
94
94
|
stdout
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
// Fallback:
|
|
98
|
-
const filters = `fps=
|
|
97
|
+
// Fallback: Fast GIF creation with reduced quality
|
|
98
|
+
const filters = `fps=2,scale=480:-1:flags=fast_bilinear,split[s0][s1];[s0]palettegen=max_colors=64[p];[s1][p]paletteuse=dither=none`;
|
|
99
99
|
|
|
100
100
|
await execa(ffmpegPath, [
|
|
101
101
|
'-i', inputVideoPath,
|
|
@@ -111,8 +111,12 @@ export async function createGif(inputVideoPath, outputGifPath) {
|
|
|
111
111
|
|
|
112
112
|
const extractedFramesInterval = videoDuration / gifFrames;
|
|
113
113
|
|
|
114
|
-
//
|
|
115
|
-
|
|
114
|
+
// Fast GIF creation with reduced quality for speed
|
|
115
|
+
// - Smaller scale (480 vs 640)
|
|
116
|
+
// - Fewer colors (64 vs 128)
|
|
117
|
+
// - Faster scaling algorithm (fast_bilinear vs lanczos)
|
|
118
|
+
// - No dithering for faster processing
|
|
119
|
+
const filters = `fps=1/${extractedFramesInterval},scale=480:-1:flags=fast_bilinear,split[s0][s1];[s0]palettegen=max_colors=64[p];[s1][p]paletteuse=dither=none`;
|
|
116
120
|
|
|
117
121
|
await execa(ffmpegPath, [
|
|
118
122
|
'-i', inputVideoPath,
|
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';
|
|
@@ -27,7 +27,7 @@ async function fixVideoContainer(inputVideoPath, outputVideoPath) {
|
|
|
27
27
|
'-i', inputVideoPath,
|
|
28
28
|
'-vcodec', 'copy', // Copy video stream without re-encoding
|
|
29
29
|
'-acodec', 'copy', // Copy audio stream without re-encoding
|
|
30
|
-
'-
|
|
30
|
+
'-f', 'webm', // Force WebM format
|
|
31
31
|
outputVideoPath,
|
|
32
32
|
'-y', // Overwrite output file
|
|
33
33
|
'-hide_banner'
|
|
@@ -320,8 +320,9 @@ export async function startRecording({
|
|
|
320
320
|
|
|
321
321
|
// Construct FFmpeg command arguments
|
|
322
322
|
const platformArgs = await getPlatformArgs({ fps, includeAudio });
|
|
323
|
+
|
|
323
324
|
const outputArgs = [
|
|
324
|
-
'-c:v', 'libvpx
|
|
325
|
+
'-c:v', 'libvpx', // Use VP9 codec for better quality and compression
|
|
325
326
|
'-quality', 'realtime', // Use realtime quality preset for faster encoding
|
|
326
327
|
'-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
|
|
327
328
|
'-deadline', 'realtime',// Realtime encoding mode for lowest latency
|
|
@@ -340,7 +341,7 @@ export async function startRecording({
|
|
|
340
341
|
|
|
341
342
|
if (includeAudio) {
|
|
342
343
|
outputArgs.push(
|
|
343
|
-
'-c:a', '
|
|
344
|
+
'-c:a', 'libvorbis', // Vorbis audio codec for WebM
|
|
344
345
|
'-b:a', '128k'
|
|
345
346
|
);
|
|
346
347
|
}
|
|
@@ -535,6 +536,9 @@ export async function stopRecording() {
|
|
|
535
536
|
}
|
|
536
537
|
|
|
537
538
|
try {
|
|
539
|
+
const platform = os.platform();
|
|
540
|
+
const isWindows = platform === 'win32';
|
|
541
|
+
|
|
538
542
|
// First try to gracefully stop FFmpeg by sending 'q'
|
|
539
543
|
if (currentRecording && currentRecording.stdin) {
|
|
540
544
|
logger.debug('Sending quit signal to FFmpeg...');
|
|
@@ -545,18 +549,36 @@ export async function stopRecording() {
|
|
|
545
549
|
// Wait for FFmpeg to finish gracefully with realtime encoding
|
|
546
550
|
const gracefulTimeout = setTimeout(() => {
|
|
547
551
|
if (currentRecording && !currentRecording.killed) {
|
|
548
|
-
logger.warn('FFmpeg did not exit gracefully after 10s,
|
|
549
|
-
//
|
|
550
|
-
|
|
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
|
+
}
|
|
551
565
|
}
|
|
552
566
|
}, 10000); // Wait longer for graceful shutdown
|
|
553
567
|
|
|
554
|
-
// Wait up to 20 seconds for
|
|
568
|
+
// Wait up to 20 seconds for termination to work
|
|
555
569
|
const hardKillTimeout = setTimeout(() => {
|
|
556
570
|
if (currentRecording && !currentRecording.killed) {
|
|
557
|
-
logger.error('FFmpeg still running after
|
|
558
|
-
//
|
|
559
|
-
|
|
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
|
+
}
|
|
560
582
|
}
|
|
561
583
|
}, 20000); // Longer timeout to avoid killing prematurely
|
|
562
584
|
|
|
@@ -570,20 +592,39 @@ export async function stopRecording() {
|
|
|
570
592
|
clearTimeout(hardKillTimeout);
|
|
571
593
|
|
|
572
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
|
+
|
|
573
602
|
// Poll for the file to exist with content
|
|
574
603
|
logger.debug('Waiting for temp file to be written...');
|
|
575
|
-
const maxWaitTime =
|
|
604
|
+
const maxWaitTime = 15000; // Increased from 10s to 15s for Windows
|
|
576
605
|
const startWait = Date.now();
|
|
577
606
|
let tempFileReady = false;
|
|
607
|
+
let lastSize = 0;
|
|
608
|
+
let stableCount = 0;
|
|
578
609
|
|
|
579
610
|
while (!tempFileReady && (Date.now() - startWait) < maxWaitTime) {
|
|
580
611
|
const tempFile = currentTempFile;
|
|
581
612
|
if (tempFile && fs.existsSync(tempFile)) {
|
|
582
613
|
const stats = fs.statSync(tempFile);
|
|
583
614
|
if (stats.size > 0) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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;
|
|
587
628
|
}
|
|
588
629
|
}
|
|
589
630
|
// Wait a bit before checking again
|
|
@@ -591,7 +632,7 @@ export async function stopRecording() {
|
|
|
591
632
|
}
|
|
592
633
|
|
|
593
634
|
if (!tempFileReady) {
|
|
594
|
-
logger.warn('Temp file not
|
|
635
|
+
logger.warn('Temp file not confirmed stable after waiting, proceeding anyway', { lastSize });
|
|
595
636
|
}
|
|
596
637
|
|
|
597
638
|
// Read temp file path from disk (for cross-process access)
|
|
@@ -640,6 +681,63 @@ export async function stopRecording() {
|
|
|
640
681
|
path: tempFile
|
|
641
682
|
});
|
|
642
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
|
+
|
|
643
741
|
// Re-mux to ensure proper container metadata (duration, seekability, etc.)
|
|
644
742
|
logger.debug('Re-muxing temp file to fix container metadata...');
|
|
645
743
|
|
package/package.json
CHANGED
package/test_workflow.sh
CHANGED
|
@@ -34,12 +34,11 @@ echo "✅ File tracking configured"
|
|
|
34
34
|
echo ""
|
|
35
35
|
echo "4. Starting dashcam recording in background..."
|
|
36
36
|
# Start recording and redirect output to a log file so we can still monitor it
|
|
37
|
-
./bin/dashcam.js record --title "Sync Test Recording" --description "Testing video/log synchronization with timestamped events" > /tmp/dashcam-recording.log 2>&1
|
|
38
|
-
RECORD_PID=$!
|
|
37
|
+
./bin/dashcam.js record --title "Sync Test Recording" --description "Testing video/log synchronization with timestamped events" > /tmp/dashcam-recording.log 2>&1
|
|
39
38
|
|
|
40
39
|
# Wait for recording to initialize and log tracker to start
|
|
41
|
-
echo "Waiting for recording to initialize
|
|
42
|
-
sleep
|
|
40
|
+
echo "Waiting for recording to initialize..."
|
|
41
|
+
sleep 2
|
|
43
42
|
|
|
44
43
|
# Write first event after log tracker is fully ready
|
|
45
44
|
RECORDING_START=$(date +%s)
|
|
@@ -48,11 +47,12 @@ echo "🔴 EVENT 1: Recording START at $(date '+%H:%M:%S')"
|
|
|
48
47
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
49
48
|
echo "[EVENT 1] 🔴 Recording started with emoji at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_START" >> "$TEMP_FILE"
|
|
50
49
|
|
|
51
|
-
# Verify recording is actually running
|
|
52
|
-
if
|
|
50
|
+
# Verify background recording process is actually running
|
|
51
|
+
if pgrep -f "dashcam-background.js" > /dev/null; then
|
|
53
52
|
echo "✅ Recording started successfully"
|
|
54
53
|
else
|
|
55
54
|
echo "❌ Recording process died, check /tmp/dashcam-recording.log"
|
|
55
|
+
cat /tmp/dashcam-recording.log
|
|
56
56
|
exit 1
|
|
57
57
|
fi
|
|
58
58
|
|