dashcam 1.0.1-beta.21 → 1.0.1-beta.22

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/ffmpeg.js CHANGED
@@ -18,7 +18,7 @@ export async function createSnapshot(inputVideoPath, outputSnapshotPath, snapsho
18
18
  '-i', inputVideoPath,
19
19
  '-frames:v', '1',
20
20
  '-vf', 'scale=640:-1:force_original_aspect_ratio=decrease:eval=frame',
21
- '-compression_level', '6', // Use default compression (was 100, which is extremely slow)
21
+ '-compression_level', '0', // Fast compression for speed (0 = fastest, 9 = slowest)
22
22
  outputSnapshotPath,
23
23
  '-y',
24
24
  '-hide_banner'
@@ -94,62 +94,32 @@ export async function createGif(inputVideoPath, outputGifPath) {
94
94
  stdout
95
95
  });
96
96
 
97
- // Fallback: Just sample the video at a fixed rate (e.g., 1 frame every 3 seconds)
98
- const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
97
+ // Fallback: Single-pass GIF creation at fixed rate
98
+ const filters = `fps=1/3,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5`;
99
+
99
100
  await execa(ffmpegPath, [
100
101
  '-i', inputVideoPath,
101
- '-vf', `fps=1/3`, // Sample 1 frame every 3 seconds
102
- framesPath
103
- ]);
104
-
105
- // Create GIF from frames
106
- await execa(ffmpegPath, [
107
- '-framerate', `${gifFps}`,
108
- '-i', framesPath,
102
+ '-vf', filters,
109
103
  '-loop', '0',
110
104
  outputGifPath,
111
105
  '-y',
112
106
  '-hide_banner'
113
107
  ]);
114
-
115
- // Clean up temporary frame files
116
- const framesToDelete = fs.readdirSync(os.tmpdir())
117
- .filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
118
- .map(file => path.join(os.tmpdir(), file));
119
-
120
- for (const frame of framesToDelete) {
121
- fs.unlinkSync(frame);
122
- }
123
108
 
124
109
  return;
125
110
  }
126
111
 
127
112
  const extractedFramesInterval = videoDuration / gifFrames;
128
113
 
129
- // Extract frames
130
- const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
114
+ // Single-pass GIF creation with palette for better quality and performance
115
+ const filters = `fps=1/${extractedFramesInterval},scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5`;
116
+
131
117
  await execa(ffmpegPath, [
132
118
  '-i', inputVideoPath,
133
- '-vf', `fps=1/${extractedFramesInterval}`,
134
- framesPath
135
- ]);
136
-
137
- // Create GIF from frames
138
- await execa(ffmpegPath, [
139
- '-framerate', `${gifFps}`,
140
- '-i', framesPath,
119
+ '-vf', filters,
141
120
  '-loop', '0',
142
121
  outputGifPath,
143
122
  '-y',
144
123
  '-hide_banner'
145
124
  ]);
146
-
147
- // Clean up temporary frame files
148
- const framesToDelete = fs.readdirSync(os.tmpdir())
149
- .filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
150
- .map(file => path.join(os.tmpdir(), file));
151
-
152
- for (const frame of framesToDelete) {
153
- fs.unlinkSync(frame);
154
- }
155
125
  }
@@ -174,41 +174,27 @@ class ProcessManager {
174
174
  await new Promise(resolve => setTimeout(resolve, 1000));
175
175
  }
176
176
 
177
- // Call the actual recorder's stopRecording function to get complete results
177
+ // The background process handles stopRecording() internally via SIGINT
178
+ // We just need to return the basic result from the status file
178
179
  if (status) {
179
- try {
180
- const { stopRecording } = await import('./recorder.js');
181
- const result = await stopRecording();
182
-
183
- logger.info('Recording stopped successfully via recorder', {
184
- outputPath: result.outputPath,
185
- duration: result.duration,
186
- hasLogs: result.logs?.length > 0,
187
- hasApps: result.apps?.length > 0
188
- });
189
-
190
- // Cleanup process files but preserve upload result for stop command
191
- this.cleanup({ preserveResult: true });
192
-
193
- return result;
194
- } catch (recorderError) {
195
- logger.warn('Failed to stop via recorder, falling back to basic result', { error: recorderError.message });
196
-
197
- // Fallback to basic result if recorder fails
198
- const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
199
- const result = {
200
- outputPath: status.outputPath,
201
- gifPath: `${basePath}.gif`,
202
- snapshotPath: `${basePath}.png`,
203
- duration: Date.now() - status.startTime,
204
- clientStartDate: status.startTime,
205
- apps: [],
206
- logs: []
207
- };
208
-
209
- this.cleanup({ preserveResult: true });
210
- return result;
211
- }
180
+ logger.info('Background recording stopped, returning status', {
181
+ outputPath: status.outputPath,
182
+ duration: Date.now() - status.startTime
183
+ });
184
+
185
+ const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
186
+ const result = {
187
+ outputPath: status.outputPath,
188
+ gifPath: `${basePath}.gif`,
189
+ snapshotPath: `${basePath}.png`,
190
+ duration: Date.now() - status.startTime,
191
+ clientStartDate: status.startTime,
192
+ apps: [],
193
+ logs: []
194
+ };
195
+
196
+ this.cleanup({ preserveResult: true });
197
+ return result;
212
198
  } else {
213
199
  throw new Error('No status information available for active recording');
214
200
  }
package/lib/recorder.js CHANGED
@@ -322,9 +322,9 @@ export async function startRecording({
322
322
  const platformArgs = await getPlatformArgs({ fps, includeAudio });
323
323
  const outputArgs = [
324
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
325
+ '-quality', 'realtime', // Use realtime quality preset for faster encoding
326
+ '-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
327
+ '-deadline', 'realtime',// Realtime encoding mode for lowest latency
328
328
  '-b:v', '2M', // Target bitrate
329
329
  '-r', fps.toString(), // Ensure output framerate matches input
330
330
  '-g', fps.toString(), // Keyframe interval = 1 second (every fps frames) - ensures frequent keyframes
@@ -528,24 +528,23 @@ export async function stopRecording() {
528
528
  currentRecording.stdin.end(); // Close stdin to signal end
529
529
  }
530
530
 
531
- // Wait longer for FFmpeg to finish gracefully - critical for VP9 encoding
532
- // VP9 encoding needs time to flush buffers and finalize the container
531
+ // Wait for FFmpeg to finish gracefully with realtime encoding
533
532
  const gracefulTimeout = setTimeout(() => {
534
533
  if (currentRecording && !currentRecording.killed) {
535
- logger.warn('FFmpeg did not exit gracefully after 8s, sending SIGTERM...');
534
+ logger.warn('FFmpeg did not exit gracefully after 5s, sending SIGTERM...');
536
535
  // If still running, try SIGTERM
537
536
  process.kill(currentRecording.pid, 'SIGTERM');
538
537
  }
539
- }, 8000); // Increased to 8 seconds for VP9 finalization
538
+ }, 5000); // Faster with realtime encoding
540
539
 
541
- // Wait up to 15 seconds for SIGTERM to work
540
+ // Wait up to 10 seconds for SIGTERM to work
542
541
  const hardKillTimeout = setTimeout(() => {
543
542
  if (currentRecording && !currentRecording.killed) {
544
543
  logger.error('FFmpeg still running after SIGTERM, using SIGKILL...');
545
544
  // If still not dead, use SIGKILL as last resort
546
545
  process.kill(currentRecording.pid, 'SIGKILL');
547
546
  }
548
- }, 15000); // Increased to 15 seconds
547
+ }, 10000); // Faster timeout
549
548
 
550
549
  // Wait for the process to fully exit
551
550
  if (currentRecording) {
@@ -556,10 +555,10 @@ export async function stopRecording() {
556
555
  clearTimeout(gracefulTimeout);
557
556
  clearTimeout(hardKillTimeout);
558
557
 
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
558
+ // Additional wait to ensure filesystem is synced
559
+ // Realtime encoding has smaller buffers so this can be shorter
560
+ logger.debug('Waiting for filesystem sync...');
561
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Reduced from 3s with realtime encoding
563
562
 
564
563
  // Read temp file path from disk (for cross-process access)
565
564
  let tempFile = currentTempFile; // Try in-memory first
@@ -607,8 +606,7 @@ export async function stopRecording() {
607
606
  path: tempFile
608
607
  });
609
608
 
610
- // Since WebM is already a valid streaming format, re-mux it to ensure
611
- // proper container metadata (duration, seekability, etc.)
609
+ // Re-mux to ensure proper container metadata (duration, seekability, etc.)
612
610
  logger.debug('Re-muxing temp file to fix container metadata...');
613
611
 
614
612
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.0.1-beta.21",
3
+ "version": "1.0.1-beta.22",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
5
  "main": "bin/index.js",
6
6
  "bin": {