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 +9 -39
- package/lib/processManager.js +20 -34
- package/lib/recorder.js +13 -15
- package/package.json +1 -1
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', '
|
|
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:
|
|
98
|
-
const
|
|
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',
|
|
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
|
-
//
|
|
130
|
-
const
|
|
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',
|
|
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
|
}
|
package/lib/processManager.js
CHANGED
|
@@ -174,41 +174,27 @@ class ProcessManager {
|
|
|
174
174
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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', '
|
|
326
|
-
'-cpu-used', '
|
|
327
|
-
'-deadline', '
|
|
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
|
|
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
|
|
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
|
-
},
|
|
538
|
+
}, 5000); // Faster with realtime encoding
|
|
540
539
|
|
|
541
|
-
// Wait up to
|
|
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
|
-
},
|
|
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
|
|
560
|
-
//
|
|
561
|
-
logger.debug('Waiting for filesystem sync
|
|
562
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
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
|
-
//
|
|
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 {
|