dashcam 0.8.3 → 1.0.1-beta.2
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/.dashcam/cli-config.json +3 -0
- package/.dashcam/recording.log +135 -0
- package/.dashcam/web-config.json +11 -0
- package/.github/RELEASE.md +59 -0
- package/.github/workflows/build.yml +103 -0
- package/.github/workflows/publish.yml +43 -0
- package/.github/workflows/release.yml +107 -0
- package/LOG_TRACKING_GUIDE.md +225 -0
- package/README.md +709 -155
- package/bin/dashcam.cjs +8 -0
- package/bin/dashcam.js +542 -0
- package/bin/index.js +63 -0
- package/examples/execute-script.js +152 -0
- package/examples/simple-test.js +37 -0
- package/lib/applicationTracker.js +311 -0
- package/lib/auth.js +222 -0
- package/lib/binaries.js +21 -0
- package/lib/config.js +34 -0
- package/lib/extension-logs/helpers.js +182 -0
- package/lib/extension-logs/index.js +347 -0
- package/lib/extension-logs/manager.js +344 -0
- package/lib/ffmpeg.js +156 -0
- package/lib/logTracker.js +23 -0
- package/lib/logger.js +118 -0
- package/lib/logs/index.js +432 -0
- package/lib/permissions.js +85 -0
- package/lib/processManager.js +255 -0
- package/lib/recorder.js +675 -0
- package/lib/store.js +58 -0
- package/lib/tracking/FileTracker.js +98 -0
- package/lib/tracking/FileTrackerManager.js +62 -0
- package/lib/tracking/LogsTracker.js +147 -0
- package/lib/tracking/active-win.js +212 -0
- package/lib/tracking/icons/darwin.js +39 -0
- package/lib/tracking/icons/index.js +167 -0
- package/lib/tracking/icons/windows.js +27 -0
- package/lib/tracking/idle.js +82 -0
- package/lib/tracking.js +23 -0
- package/lib/uploader.js +449 -0
- package/lib/utilities/jsonl.js +77 -0
- package/lib/webLogsDaemon.js +234 -0
- package/lib/websocket/server.js +223 -0
- package/package.json +50 -21
- package/recording.log +814 -0
- package/sea-bundle.mjs +34595 -0
- package/test-page.html +15 -0
- package/test.log +1 -0
- package/test_run.log +48 -0
- package/test_workflow.sh +80 -0
- package/examples/crash-test.js +0 -11
- package/examples/github-issue.sh +0 -1
- package/examples/protocol.html +0 -22
- package/index.js +0 -177
- package/lib.js +0 -199
- package/recorder.js +0 -85
package/lib/ffmpeg.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import { getFfmpegPath, getFfprobePath } from './binaries.js';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a snapshot (PNG) from a video at a specific timestamp
|
|
10
|
+
*/
|
|
11
|
+
export async function createSnapshot(inputVideoPath, outputSnapshotPath, snapshotTimeSeconds = 0) {
|
|
12
|
+
logger.debug('Creating snapshot', { inputVideoPath, outputSnapshotPath, snapshotTimeSeconds });
|
|
13
|
+
|
|
14
|
+
const ffmpegPath = await getFfmpegPath();
|
|
15
|
+
|
|
16
|
+
const command = [
|
|
17
|
+
'-ss', snapshotTimeSeconds,
|
|
18
|
+
'-i', inputVideoPath,
|
|
19
|
+
'-frames:v', '1',
|
|
20
|
+
'-vf', 'scale=640:-1:force_original_aspect_ratio=decrease:eval=frame',
|
|
21
|
+
'-pred', 'mixed',
|
|
22
|
+
'-compression_level', '100',
|
|
23
|
+
outputSnapshotPath,
|
|
24
|
+
'-y',
|
|
25
|
+
'-hide_banner'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
await execa(ffmpegPath, command);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create an animated GIF from a video
|
|
33
|
+
*/
|
|
34
|
+
export async function createGif(inputVideoPath, outputGifPath) {
|
|
35
|
+
logger.debug('Creating GIF', { inputVideoPath, outputGifPath });
|
|
36
|
+
|
|
37
|
+
const ffmpegPath = await getFfmpegPath();
|
|
38
|
+
const ffprobePath = await getFfprobePath();
|
|
39
|
+
|
|
40
|
+
// Function to check if video is ready
|
|
41
|
+
const isVideoReady = async () => {
|
|
42
|
+
try {
|
|
43
|
+
// Check if file exists and is not empty
|
|
44
|
+
if (!fs.existsSync(inputVideoPath) || fs.statSync(inputVideoPath).size === 0) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Try to read video info with ffprobe
|
|
49
|
+
const { exitCode } = await execa(ffprobePath, [
|
|
50
|
+
'-v', 'error',
|
|
51
|
+
'-select_streams', 'v:0',
|
|
52
|
+
'-show_entries', 'stream=codec_type',
|
|
53
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
54
|
+
inputVideoPath
|
|
55
|
+
], { reject: false });
|
|
56
|
+
|
|
57
|
+
return exitCode === 0;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Wait for up to 5 seconds for the video to be ready
|
|
64
|
+
for (let i = 0; i < 10; i++) {
|
|
65
|
+
if (await isVideoReady()) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Final check
|
|
72
|
+
if (!await isVideoReady()) {
|
|
73
|
+
throw new Error('Video file is not ready or is corrupted');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const gifFps = 4;
|
|
77
|
+
const gifDuration = 10;
|
|
78
|
+
const gifFrames = Math.ceil(gifDuration * gifFps);
|
|
79
|
+
|
|
80
|
+
// Get video duration in seconds
|
|
81
|
+
const { stdout } = await execa(ffprobePath, [
|
|
82
|
+
'-v', 'error',
|
|
83
|
+
'-show_entries', 'format=duration',
|
|
84
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
85
|
+
inputVideoPath
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const videoDuration = parseFloat(stdout);
|
|
89
|
+
const id = (Math.random() + 1).toString(36).substring(7);
|
|
90
|
+
|
|
91
|
+
// Handle NaN or invalid duration
|
|
92
|
+
if (!videoDuration || isNaN(videoDuration) || videoDuration <= 0) {
|
|
93
|
+
logger.warn('Video duration unavailable or invalid, using default sampling for GIF', {
|
|
94
|
+
duration: videoDuration,
|
|
95
|
+
stdout
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Fallback: Just sample the video at a fixed rate (e.g., 1 frame every 3 seconds)
|
|
99
|
+
const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
|
|
100
|
+
await execa(ffmpegPath, [
|
|
101
|
+
'-i', inputVideoPath,
|
|
102
|
+
'-vf', `fps=1/3`, // Sample 1 frame every 3 seconds
|
|
103
|
+
framesPath
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// Create GIF from frames
|
|
107
|
+
await execa(ffmpegPath, [
|
|
108
|
+
'-framerate', `${gifFps}`,
|
|
109
|
+
'-i', framesPath,
|
|
110
|
+
'-loop', '0',
|
|
111
|
+
outputGifPath,
|
|
112
|
+
'-y',
|
|
113
|
+
'-hide_banner'
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
// Clean up temporary frame files
|
|
117
|
+
const framesToDelete = fs.readdirSync(os.tmpdir())
|
|
118
|
+
.filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
|
|
119
|
+
.map(file => path.join(os.tmpdir(), file));
|
|
120
|
+
|
|
121
|
+
for (const frame of framesToDelete) {
|
|
122
|
+
fs.unlinkSync(frame);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const extractedFramesInterval = videoDuration / gifFrames;
|
|
129
|
+
|
|
130
|
+
// Extract frames
|
|
131
|
+
const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
|
|
132
|
+
await execa(ffmpegPath, [
|
|
133
|
+
'-i', inputVideoPath,
|
|
134
|
+
'-vf', `fps=1/${extractedFramesInterval}`,
|
|
135
|
+
framesPath
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// Create GIF from frames
|
|
139
|
+
await execa(ffmpegPath, [
|
|
140
|
+
'-framerate', `${gifFps}`,
|
|
141
|
+
'-i', framesPath,
|
|
142
|
+
'-loop', '0',
|
|
143
|
+
outputGifPath,
|
|
144
|
+
'-y',
|
|
145
|
+
'-hide_banner'
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
// Clean up temporary frame files
|
|
149
|
+
const framesToDelete = fs.readdirSync(os.tmpdir())
|
|
150
|
+
.filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
|
|
151
|
+
.map(file => path.join(os.tmpdir(), file));
|
|
152
|
+
|
|
153
|
+
for (const frame of framesToDelete) {
|
|
154
|
+
fs.unlinkSync(frame);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { LogsTracker } from './tracking/LogsTracker.js';
|
|
2
|
+
import { FileTrackerManager } from './tracking/FileTrackerManager.js';
|
|
3
|
+
|
|
4
|
+
// Create a shared file tracker manager for efficient resource usage
|
|
5
|
+
const fileTrackerManager = new FileTrackerManager();
|
|
6
|
+
|
|
7
|
+
// Create a singleton instance for watch-only mode (no directory)
|
|
8
|
+
const logTracker = new LogsTracker({
|
|
9
|
+
config: {},
|
|
10
|
+
fileTrackerManager
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Helper function to create a new tracker for recording (with directory)
|
|
14
|
+
export function createRecordingTracker(directory, config = {}) {
|
|
15
|
+
return new LogsTracker({
|
|
16
|
+
config,
|
|
17
|
+
directory,
|
|
18
|
+
fileTrackerManager
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Export the singleton for backwards compatibility
|
|
23
|
+
export { logTracker, fileTrackerManager };
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
// Constants that match the desktop app
|
|
6
|
+
const DD_TOKEN = process.env.DD_TOKEN || 'pubfd7949e46d22d1e71fc8fa6d95ecc5f2';
|
|
7
|
+
const ENV = process.env.NODE_ENV || 'production';
|
|
8
|
+
|
|
9
|
+
// Configure HTTP transport for Datadog
|
|
10
|
+
const httpTransportOptions = {
|
|
11
|
+
host: 'http-intake.logs.datadoghq.com',
|
|
12
|
+
path: `/api/v2/logs?dd-api-key=${DD_TOKEN}`,
|
|
13
|
+
ssl: true,
|
|
14
|
+
level: 'silly'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Metadata that matches the desktop app
|
|
18
|
+
const httpMeta = {
|
|
19
|
+
ddsource: 'nodejs',
|
|
20
|
+
service: 'dashcam-cli',
|
|
21
|
+
env: ENV,
|
|
22
|
+
os_type: os.type(),
|
|
23
|
+
os_release: os.release()
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Verbose level configuration
|
|
27
|
+
let isVerbose = false;
|
|
28
|
+
|
|
29
|
+
// Create custom format to include version and user info
|
|
30
|
+
const updateFormat = winston.format((info) => {
|
|
31
|
+
info.version = process.env.npm_package_version;
|
|
32
|
+
return info;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Custom format for console output with more detail
|
|
36
|
+
const verboseConsoleFormat = winston.format.combine(
|
|
37
|
+
winston.format.timestamp({ format: 'HH:mm:ss.SSS' }),
|
|
38
|
+
winston.format.colorize(),
|
|
39
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
40
|
+
let output = `${timestamp} [${level}] ${message}`;
|
|
41
|
+
|
|
42
|
+
// Add metadata if present and in verbose mode
|
|
43
|
+
if (isVerbose && Object.keys(meta).length > 0) {
|
|
44
|
+
// Filter out internal winston metadata
|
|
45
|
+
const cleanMeta = Object.fromEntries(
|
|
46
|
+
Object.entries(meta).filter(([key]) =>
|
|
47
|
+
!['timestamp', 'level', 'message', 'ddsource', 'service', 'env', 'os_type', 'os_release', 'version'].includes(key)
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (Object.keys(cleanMeta).length > 0) {
|
|
52
|
+
output += `\n ${JSON.stringify(cleanMeta, null, 2).split('\n').join('\n ')}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return output;
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Function to set verbose mode
|
|
61
|
+
export function setVerbose(verbose = true) {
|
|
62
|
+
isVerbose = verbose;
|
|
63
|
+
// Update console transport level based on verbose mode
|
|
64
|
+
const consoleTransport = logger.transports.find(t => t.constructor.name === 'Console');
|
|
65
|
+
if (consoleTransport) {
|
|
66
|
+
consoleTransport.level = verbose ? 'silly' : (ENV === 'production' ? 'info' : 'debug');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create the logger instance
|
|
71
|
+
export const logger = winston.createLogger({
|
|
72
|
+
format: winston.format.combine(
|
|
73
|
+
updateFormat(),
|
|
74
|
+
winston.format.timestamp(),
|
|
75
|
+
winston.format.json()
|
|
76
|
+
),
|
|
77
|
+
defaultMeta: {
|
|
78
|
+
...httpMeta
|
|
79
|
+
},
|
|
80
|
+
transports: [
|
|
81
|
+
// Log to console in development with enhanced formatting
|
|
82
|
+
new winston.transports.Console({
|
|
83
|
+
format: verboseConsoleFormat,
|
|
84
|
+
level: ENV === 'production' ? 'info' : 'debug'
|
|
85
|
+
}),
|
|
86
|
+
// Log to files
|
|
87
|
+
new winston.transports.File({
|
|
88
|
+
filename: path.join(os.homedir(), '.dashcam', 'logs', 'error.log'),
|
|
89
|
+
level: 'error'
|
|
90
|
+
}),
|
|
91
|
+
new winston.transports.File({
|
|
92
|
+
filename: path.join(os.homedir(), '.dashcam', 'logs', 'combined.log'),
|
|
93
|
+
level: 'silly' // Capture all levels in file
|
|
94
|
+
}),
|
|
95
|
+
// Log debug info to separate file when verbose
|
|
96
|
+
new winston.transports.File({
|
|
97
|
+
filename: path.join(os.homedir(), '.dashcam', 'logs', 'debug.log'),
|
|
98
|
+
level: 'debug'
|
|
99
|
+
}),
|
|
100
|
+
// Send to Datadog in production
|
|
101
|
+
...(ENV === 'production' ? [new winston.transports.Http(httpTransportOptions)] : [])
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Add convenience methods for common logging patterns
|
|
106
|
+
logger.verbose = (message, meta) => logger.debug(`[VERBOSE] ${message}`, meta);
|
|
107
|
+
logger.trace = (message, meta) => logger.silly(`[TRACE] ${message}`, meta);
|
|
108
|
+
|
|
109
|
+
// Function to log function entry/exit for debugging
|
|
110
|
+
export function logFunctionCall(functionName, args = {}) {
|
|
111
|
+
if (isVerbose) {
|
|
112
|
+
logger.debug(`→ Entering ${functionName}`, { args });
|
|
113
|
+
return (result) => {
|
|
114
|
+
logger.debug(`← Exiting ${functionName}`, { result: typeof result });
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return () => {}; // No-op if not verbose
|
|
118
|
+
}
|