dashcam 1.0.1-beta.2 → 1.0.1-beta.21
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/.github/workflows/publish.yml +26 -20
- package/691cc08dc2fc02f59ae66f08 (1).mp4 +0 -0
- package/NPM_PUBLISH_FIX.md +104 -0
- package/SINGLE_FRAME_VIDEO_FIX.md +129 -0
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.js +276 -132
- package/lib/ffmpeg.js +1 -2
- package/lib/logs/index.js +67 -11
- package/lib/processManager.js +104 -42
- package/lib/recorder.js +128 -27
- package/lib/tracking/FileTracker.js +7 -0
- package/lib/tracking/LogsTracker.js +21 -7
- package/lib/tracking/icons/index.js +3 -2
- package/lib/tracking/icons/linux.js +277 -0
- package/lib/uploader.js +10 -3
- package/package.json +4 -1
- package/scripts/sync-version.sh +48 -0
- package/test-short-recording.js +287 -0
- package/test_workflow.sh +99 -25
- package/.github/workflows/build.yml +0 -103
- package/.github/workflows/release.yml +0 -107
package/lib/processManager.js
CHANGED
|
@@ -2,12 +2,17 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
5
6
|
import { logger } from './logger.js';
|
|
6
7
|
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
7
11
|
// Use a fixed directory in the user's home directory for cross-process communication
|
|
8
12
|
const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
|
|
9
13
|
const PID_FILE = path.join(PROCESS_DIR, 'recording.pid');
|
|
10
14
|
const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
|
|
15
|
+
const RESULT_FILE = path.join(PROCESS_DIR, 'upload-result.json');
|
|
11
16
|
|
|
12
17
|
// Ensure process directory exists
|
|
13
18
|
if (!fs.existsSync(PROCESS_DIR)) {
|
|
@@ -47,6 +52,36 @@ class ProcessManager {
|
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
writeUploadResult(result) {
|
|
56
|
+
try {
|
|
57
|
+
logger.info('Writing upload result to file', { path: RESULT_FILE, shareLink: result.shareLink });
|
|
58
|
+
fs.writeFileSync(RESULT_FILE, JSON.stringify({
|
|
59
|
+
...result,
|
|
60
|
+
timestamp: Date.now()
|
|
61
|
+
}, null, 2));
|
|
62
|
+
logger.info('Successfully wrote upload result to file');
|
|
63
|
+
// Verify it was written
|
|
64
|
+
if (fs.existsSync(RESULT_FILE)) {
|
|
65
|
+
logger.info('Verified upload result file exists');
|
|
66
|
+
} else {
|
|
67
|
+
logger.error('Upload result file does not exist after write!');
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error('Failed to write upload result file', { error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
readUploadResult() {
|
|
75
|
+
try {
|
|
76
|
+
if (!fs.existsSync(RESULT_FILE)) return null;
|
|
77
|
+
const data = fs.readFileSync(RESULT_FILE, 'utf8');
|
|
78
|
+
return JSON.parse(data);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.error('Failed to read upload result file', { error });
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
50
85
|
writePid(pid = process.pid) {
|
|
51
86
|
try {
|
|
52
87
|
fs.writeFileSync(PID_FILE, pid.toString());
|
|
@@ -80,7 +115,8 @@ class ProcessManager {
|
|
|
80
115
|
const status = this.readStatus();
|
|
81
116
|
|
|
82
117
|
if (!pid || !this.isProcessRunning(pid)) {
|
|
83
|
-
|
|
118
|
+
// Clean up but preserve upload result in case the background process just finished uploading
|
|
119
|
+
this.cleanup({ preserveResult: true });
|
|
84
120
|
return false;
|
|
85
121
|
}
|
|
86
122
|
|
|
@@ -92,10 +128,12 @@ class ProcessManager {
|
|
|
92
128
|
return this.readStatus();
|
|
93
129
|
}
|
|
94
130
|
|
|
95
|
-
cleanup() {
|
|
131
|
+
cleanup(options = {}) {
|
|
132
|
+
const { preserveResult = false } = options;
|
|
96
133
|
try {
|
|
97
134
|
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
|
|
98
135
|
if (fs.existsSync(STATUS_FILE)) fs.unlinkSync(STATUS_FILE);
|
|
136
|
+
if (!preserveResult && fs.existsSync(RESULT_FILE)) fs.unlinkSync(RESULT_FILE);
|
|
99
137
|
} catch (error) {
|
|
100
138
|
logger.error('Failed to cleanup process files', { error });
|
|
101
139
|
}
|
|
@@ -149,8 +187,8 @@ class ProcessManager {
|
|
|
149
187
|
hasApps: result.apps?.length > 0
|
|
150
188
|
});
|
|
151
189
|
|
|
152
|
-
// Cleanup process files
|
|
153
|
-
this.cleanup();
|
|
190
|
+
// Cleanup process files but preserve upload result for stop command
|
|
191
|
+
this.cleanup({ preserveResult: true });
|
|
154
192
|
|
|
155
193
|
return result;
|
|
156
194
|
} catch (recorderError) {
|
|
@@ -168,7 +206,7 @@ class ProcessManager {
|
|
|
168
206
|
logs: []
|
|
169
207
|
};
|
|
170
208
|
|
|
171
|
-
this.cleanup();
|
|
209
|
+
this.cleanup({ preserveResult: true });
|
|
172
210
|
return result;
|
|
173
211
|
}
|
|
174
212
|
} else {
|
|
@@ -188,44 +226,68 @@ class ProcessManager {
|
|
|
188
226
|
throw new Error('Recording already in progress');
|
|
189
227
|
}
|
|
190
228
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
229
|
+
// Always run in background mode by spawning a detached process
|
|
230
|
+
logger.info('Starting recording in background mode');
|
|
231
|
+
|
|
232
|
+
// Get the path to the CLI binary
|
|
233
|
+
const binPath = path.join(__dirname, '..', 'bin', 'dashcam-background.js');
|
|
234
|
+
|
|
235
|
+
logger.debug('Background process path', { binPath, exists: fs.existsSync(binPath) });
|
|
236
|
+
|
|
237
|
+
// Create log files for background process
|
|
238
|
+
const logDir = PROCESS_DIR;
|
|
239
|
+
const stdoutLog = path.join(logDir, 'background-stdout.log');
|
|
240
|
+
const stderrLog = path.join(logDir, 'background-stderr.log');
|
|
241
|
+
|
|
242
|
+
const stdoutFd = fs.openSync(stdoutLog, 'a');
|
|
243
|
+
const stderrFd = fs.openSync(stderrLog, 'a');
|
|
244
|
+
|
|
245
|
+
// Spawn a detached process that will handle the recording
|
|
246
|
+
const backgroundProcess = spawn(process.execPath, [
|
|
247
|
+
binPath,
|
|
248
|
+
JSON.stringify(options)
|
|
249
|
+
], {
|
|
250
|
+
detached: true,
|
|
251
|
+
stdio: ['ignore', stdoutFd, stderrFd], // Log stdout and stderr
|
|
252
|
+
env: {
|
|
253
|
+
...process.env,
|
|
254
|
+
DASHCAM_BACKGROUND: 'true'
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Close the file descriptors in the parent process
|
|
259
|
+
fs.closeSync(stdoutFd);
|
|
260
|
+
fs.closeSync(stderrFd);
|
|
261
|
+
|
|
262
|
+
// Get the background process PID before unreffing
|
|
263
|
+
const backgroundPid = backgroundProcess.pid;
|
|
264
|
+
|
|
265
|
+
// Allow the parent process to exit independently
|
|
266
|
+
backgroundProcess.unref();
|
|
267
|
+
|
|
268
|
+
// Wait a moment for the background process to initialize
|
|
269
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
270
|
+
|
|
271
|
+
// Read the status file to get recording details
|
|
272
|
+
const status = this.readStatus();
|
|
273
|
+
|
|
274
|
+
if (!status || !status.isRecording) {
|
|
275
|
+
throw new Error('Background process failed to start recording');
|
|
228
276
|
}
|
|
277
|
+
|
|
278
|
+
// Write PID file so other commands can find the background process
|
|
279
|
+
this.writePid(status.pid);
|
|
280
|
+
|
|
281
|
+
logger.info('Background recording process started', {
|
|
282
|
+
pid: status.pid,
|
|
283
|
+
outputPath: status.outputPath
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
pid: status.pid,
|
|
288
|
+
outputPath: status.outputPath,
|
|
289
|
+
startTime: status.startTime
|
|
290
|
+
};
|
|
229
291
|
}
|
|
230
292
|
|
|
231
293
|
async gracefulExit() {
|
package/lib/recorder.js
CHANGED
|
@@ -8,6 +8,47 @@ import path from 'path';
|
|
|
8
8
|
import os from 'os';
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Fix/repair a video file by re-muxing it with proper container metadata
|
|
13
|
+
* This copies streams without re-encoding and ensures proper container finalization
|
|
14
|
+
*/
|
|
15
|
+
async function fixVideoContainer(inputVideoPath, outputVideoPath) {
|
|
16
|
+
const logExit = logFunctionCall('fixVideoContainer', { inputVideoPath, outputVideoPath });
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const ffmpegPath = await getFfmpegPath();
|
|
20
|
+
|
|
21
|
+
logger.info('Re-muxing video to fix container metadata', {
|
|
22
|
+
input: inputVideoPath,
|
|
23
|
+
output: outputVideoPath
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const args = [
|
|
27
|
+
'-i', inputVideoPath,
|
|
28
|
+
'-vcodec', 'copy', // Copy video stream without re-encoding
|
|
29
|
+
'-acodec', 'copy', // Copy audio stream without re-encoding
|
|
30
|
+
'-movflags', 'faststart', // Enable fast start for web playback
|
|
31
|
+
outputVideoPath,
|
|
32
|
+
'-y', // Overwrite output file
|
|
33
|
+
'-hide_banner'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
await execa(ffmpegPath, args);
|
|
37
|
+
|
|
38
|
+
logger.info('Successfully re-muxed video', {
|
|
39
|
+
outputPath: outputVideoPath,
|
|
40
|
+
outputSize: fs.existsSync(outputVideoPath) ? fs.statSync(outputVideoPath).size : 0
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
logExit();
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error('Failed to fix video container', { error: error.message });
|
|
47
|
+
logExit();
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
11
52
|
/**
|
|
12
53
|
* Dynamically detect the primary screen capture device for the current platform
|
|
13
54
|
*/
|
|
@@ -116,9 +157,8 @@ const PLATFORM_CONFIG = {
|
|
|
116
157
|
audioInput: '0', // Default audio device if needed
|
|
117
158
|
audioFormat: 'avfoundation',
|
|
118
159
|
extraArgs: [
|
|
119
|
-
'-
|
|
120
|
-
'-
|
|
121
|
-
'-r', '30' // Set frame rate
|
|
160
|
+
'-capture_cursor', '1', // Capture mouse cursor
|
|
161
|
+
'-capture_mouse_clicks', '1' // Capture mouse clicks
|
|
122
162
|
]
|
|
123
163
|
},
|
|
124
164
|
win32: {
|
|
@@ -160,6 +200,7 @@ async function getPlatformArgs({ fps, includeAudio }) {
|
|
|
160
200
|
});
|
|
161
201
|
|
|
162
202
|
const args = [
|
|
203
|
+
'-thread_queue_size', '512', // Increase input buffer to prevent drops
|
|
163
204
|
'-f', config.inputFormat
|
|
164
205
|
];
|
|
165
206
|
|
|
@@ -170,12 +211,14 @@ async function getPlatformArgs({ fps, includeAudio }) {
|
|
|
170
211
|
|
|
171
212
|
args.push(
|
|
172
213
|
'-framerate', fps.toString(),
|
|
214
|
+
'-probesize', '50M', // Increase probe size for better stream detection
|
|
173
215
|
'-i', screenInput
|
|
174
216
|
);
|
|
175
217
|
|
|
176
218
|
// Add audio capture if enabled
|
|
177
219
|
if (includeAudio) {
|
|
178
220
|
args.push(
|
|
221
|
+
'-thread_queue_size', '512', // Increase audio buffer too
|
|
179
222
|
'-f', config.audioFormat,
|
|
180
223
|
'-i', config.audioInput
|
|
181
224
|
);
|
|
@@ -278,15 +321,21 @@ export async function startRecording({
|
|
|
278
321
|
// Construct FFmpeg command arguments
|
|
279
322
|
const platformArgs = await getPlatformArgs({ fps, includeAudio });
|
|
280
323
|
const outputArgs = [
|
|
281
|
-
'-c:v', 'libvpx',
|
|
282
|
-
'-
|
|
283
|
-
//
|
|
324
|
+
'-c:v', 'libvpx-vp9', // Use VP9 codec for better quality and compression
|
|
325
|
+
'-quality', 'good', // Use 'good' quality preset (better than realtime, not as slow as best)
|
|
326
|
+
'-cpu-used', '4', // Faster encoding (0-8, higher = faster but lower quality)
|
|
327
|
+
'-deadline', 'good', // Good quality encoding mode
|
|
328
|
+
'-b:v', '2M', // Target bitrate
|
|
284
329
|
'-r', fps.toString(), // Ensure output framerate matches input
|
|
285
|
-
'-g',
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
'-
|
|
289
|
-
'-
|
|
330
|
+
'-g', fps.toString(), // Keyframe interval = 1 second (every fps frames) - ensures frequent keyframes
|
|
331
|
+
'-force_key_frames', `expr:gte(t,n_forced*1)`, // Force keyframe every 1 second
|
|
332
|
+
// WebM options for more frequent disk writes and proper stream handling
|
|
333
|
+
'-f', 'webm', // Force WebM container format
|
|
334
|
+
'-flush_packets', '1', // Flush packets immediately to disk - critical for short recordings
|
|
335
|
+
'-fflags', '+genpts', // Generate presentation timestamps
|
|
336
|
+
'-avoid_negative_ts', 'make_zero', // Avoid negative timestamps
|
|
337
|
+
'-vsync', '1', // Ensure every frame is encoded (CFR - constant frame rate)
|
|
338
|
+
'-max_muxing_queue_size', '9999' // Large queue to prevent frame drops
|
|
290
339
|
];
|
|
291
340
|
|
|
292
341
|
if (includeAudio) {
|
|
@@ -358,13 +407,11 @@ export async function startRecording({
|
|
|
358
407
|
all: true, // Capture both stdout and stderr
|
|
359
408
|
stdin: 'pipe' // Enable stdin for sending 'q' to stop recording
|
|
360
409
|
});
|
|
361
|
-
|
|
362
|
-
recordingStartTime = Date.now();
|
|
363
410
|
|
|
364
411
|
logger.info('FFmpeg process spawned', {
|
|
365
412
|
pid: currentRecording.pid,
|
|
366
413
|
args: args.slice(-5), // Log last 5 args including output file
|
|
367
|
-
tempFile
|
|
414
|
+
tempFile
|
|
368
415
|
});
|
|
369
416
|
|
|
370
417
|
// Check if temp file gets created within first few seconds
|
|
@@ -392,6 +439,14 @@ export async function startRecording({
|
|
|
392
439
|
directory: path.dirname(outputPath)
|
|
393
440
|
});
|
|
394
441
|
|
|
442
|
+
// Set recording start time AFTER log tracker is initialized
|
|
443
|
+
// This ensures the timeline starts when the tracker is ready to capture events
|
|
444
|
+
recordingStartTime = Date.now();
|
|
445
|
+
logger.info('Recording timeline started', {
|
|
446
|
+
recordingStartTime,
|
|
447
|
+
recordingStartTimeReadable: new Date(recordingStartTime).toISOString()
|
|
448
|
+
});
|
|
449
|
+
|
|
395
450
|
if (currentRecording.all) {
|
|
396
451
|
currentRecording.all.setEncoding('utf8');
|
|
397
452
|
currentRecording.all.on('data', (data) => {
|
|
@@ -456,29 +511,41 @@ export async function stopRecording() {
|
|
|
456
511
|
duration: recordingDuration,
|
|
457
512
|
durationSeconds: (recordingDuration / 1000).toFixed(1)
|
|
458
513
|
});
|
|
514
|
+
|
|
515
|
+
// Enforce minimum recording duration to prevent single-frame videos
|
|
516
|
+
const MIN_RECORDING_DURATION = 2000; // 2 seconds minimum
|
|
517
|
+
if (recordingDuration < MIN_RECORDING_DURATION) {
|
|
518
|
+
const waitTime = MIN_RECORDING_DURATION - recordingDuration;
|
|
519
|
+
logger.info(`Recording too short (${recordingDuration}ms), waiting ${waitTime}ms to ensure multiple frames`);
|
|
520
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
521
|
+
}
|
|
459
522
|
|
|
460
523
|
try {
|
|
461
524
|
// First try to gracefully stop FFmpeg by sending 'q'
|
|
462
525
|
if (currentRecording && currentRecording.stdin) {
|
|
463
526
|
logger.debug('Sending quit signal to FFmpeg...');
|
|
464
527
|
currentRecording.stdin.write('q');
|
|
528
|
+
currentRecording.stdin.end(); // Close stdin to signal end
|
|
465
529
|
}
|
|
466
530
|
|
|
467
|
-
// Wait for FFmpeg to finish gracefully
|
|
531
|
+
// Wait longer for FFmpeg to finish gracefully - critical for VP9 encoding
|
|
532
|
+
// VP9 encoding needs time to flush buffers and finalize the container
|
|
468
533
|
const gracefulTimeout = setTimeout(() => {
|
|
469
534
|
if (currentRecording && !currentRecording.killed) {
|
|
535
|
+
logger.warn('FFmpeg did not exit gracefully after 8s, sending SIGTERM...');
|
|
470
536
|
// If still running, try SIGTERM
|
|
471
537
|
process.kill(currentRecording.pid, 'SIGTERM');
|
|
472
538
|
}
|
|
473
|
-
},
|
|
539
|
+
}, 8000); // Increased to 8 seconds for VP9 finalization
|
|
474
540
|
|
|
475
|
-
// Wait up to
|
|
541
|
+
// Wait up to 15 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
|
-
},
|
|
548
|
+
}, 15000); // Increased to 15 seconds
|
|
482
549
|
|
|
483
550
|
// Wait for the process to fully exit
|
|
484
551
|
if (currentRecording) {
|
|
@@ -489,8 +556,10 @@ export async function stopRecording() {
|
|
|
489
556
|
clearTimeout(gracefulTimeout);
|
|
490
557
|
clearTimeout(hardKillTimeout);
|
|
491
558
|
|
|
492
|
-
// Additional wait to ensure filesystem is synced
|
|
493
|
-
|
|
559
|
+
// Additional wait to ensure filesystem is synced and encoder buffers are flushed
|
|
560
|
+
// This is especially important for VP9 which has larger encoding buffers
|
|
561
|
+
logger.debug('Waiting for filesystem sync and VP9 encoder finalization...');
|
|
562
|
+
await new Promise(resolve => setTimeout(resolve, 3000)); // Keep at 3 seconds after process exit
|
|
494
563
|
|
|
495
564
|
// Read temp file path from disk (for cross-process access)
|
|
496
565
|
let tempFile = currentTempFile; // Try in-memory first
|
|
@@ -538,21 +607,42 @@ export async function stopRecording() {
|
|
|
538
607
|
path: tempFile
|
|
539
608
|
});
|
|
540
609
|
|
|
541
|
-
// Since WebM is already a valid streaming format,
|
|
542
|
-
//
|
|
543
|
-
logger.debug('
|
|
610
|
+
// Since WebM is already a valid streaming format, re-mux it to ensure
|
|
611
|
+
// proper container metadata (duration, seekability, etc.)
|
|
612
|
+
logger.debug('Re-muxing temp file to fix container metadata...');
|
|
544
613
|
|
|
545
614
|
try {
|
|
546
|
-
|
|
547
|
-
|
|
615
|
+
// First, create a temporary fixed version
|
|
616
|
+
const fixedTempFile = tempFile.replace('.webm', '-fixed.webm');
|
|
617
|
+
|
|
618
|
+
const fixSuccess = await fixVideoContainer(tempFile, fixedTempFile);
|
|
619
|
+
|
|
620
|
+
if (fixSuccess && fs.existsSync(fixedTempFile) && fs.statSync(fixedTempFile).size > 0) {
|
|
621
|
+
// Use the fixed version
|
|
622
|
+
logger.info('Using re-muxed version with proper container metadata');
|
|
623
|
+
fs.copyFileSync(fixedTempFile, outputPath);
|
|
624
|
+
|
|
625
|
+
// Clean up the fixed temp file
|
|
626
|
+
try {
|
|
627
|
+
fs.unlinkSync(fixedTempFile);
|
|
628
|
+
} catch (e) {
|
|
629
|
+
logger.debug('Failed to delete fixed temp file:', e);
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
// Fallback: just copy the original temp file
|
|
633
|
+
logger.warn('Re-muxing failed, using original file');
|
|
634
|
+
fs.copyFileSync(tempFile, outputPath);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
logger.info('Successfully finalized recording to output');
|
|
548
638
|
|
|
549
639
|
// Verify the final file exists and has content
|
|
550
640
|
if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
|
|
551
|
-
throw new Error('Final output file is empty or missing after
|
|
641
|
+
throw new Error('Final output file is empty or missing after processing');
|
|
552
642
|
}
|
|
553
643
|
|
|
554
644
|
} catch (error) {
|
|
555
|
-
logger.error('Failed to
|
|
645
|
+
logger.error('Failed to process temp file:', error);
|
|
556
646
|
throw new Error('Failed to finalize recording: ' + error.message);
|
|
557
647
|
}
|
|
558
648
|
|
|
@@ -622,6 +712,12 @@ export async function stopRecording() {
|
|
|
622
712
|
icons: appTrackingResults.icons, // Include application icons metadata
|
|
623
713
|
logs: logTrackingResults // Include log tracking results
|
|
624
714
|
};
|
|
715
|
+
|
|
716
|
+
logger.info('Recording stopped with clientStartDate', {
|
|
717
|
+
clientStartDate: recordingStartTime,
|
|
718
|
+
clientStartDateReadable: new Date(recordingStartTime).toISOString(),
|
|
719
|
+
duration: result.duration
|
|
720
|
+
});
|
|
625
721
|
|
|
626
722
|
currentRecording = null;
|
|
627
723
|
recordingStartTime = null;
|
|
@@ -673,3 +769,8 @@ export function getRecordingStatus() {
|
|
|
673
769
|
outputPath
|
|
674
770
|
};
|
|
675
771
|
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Export the fix function for external use
|
|
775
|
+
*/
|
|
776
|
+
export { fixVideoContainer };
|
|
@@ -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,6 +41,12 @@ export class FileTracker {
|
|
|
40
41
|
this.trackedFile = trackedFile;
|
|
41
42
|
|
|
42
43
|
try {
|
|
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
|
+
|
|
43
50
|
this.tail = new Tail(this.trackedFile, { encoding: 'ascii' });
|
|
44
51
|
this.tail.on('line', (line) => {
|
|
45
52
|
const time = Date.now();
|
|
@@ -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
|
-
|
|
104
|
-
|
|
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 }) =>
|
|
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: '
|
|
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: '
|
|
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
|
|
24
|
-
|
|
23
|
+
// Linux support
|
|
24
|
+
const linuxModule = await import("./linux.js");
|
|
25
|
+
getIconAsBuffer = linuxModule.getIconAsBuffer;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
iconModuleLoaded = true;
|