dashcam 1.0.1-beta.22 โ 1.0.1-beta.24
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.js +17 -18
- package/lib/auth.js +5 -5
- package/lib/config.js +1 -1
- package/lib/recorder.js +28 -8
- package/lib/tracking/FileTracker.js +1 -1
- package/lib/tracking/icons/linux.js +94 -1
- package/lib/uploader.js +2 -2
- package/package.json +1 -1
- package/test_workflow.sh +19 -14
package/bin/dashcam.js
CHANGED
|
@@ -417,30 +417,29 @@ program
|
|
|
417
417
|
|
|
418
418
|
console.log('Recording stopped successfully');
|
|
419
419
|
|
|
420
|
+
// Wait a moment for upload to complete (background process handles this)
|
|
421
|
+
logger.debug('Waiting for background upload to complete...');
|
|
422
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
423
|
+
|
|
424
|
+
// Try to read the upload result from the background process
|
|
425
|
+
const uploadResult = processManager.readUploadResult();
|
|
426
|
+
logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
|
|
427
|
+
|
|
428
|
+
if (uploadResult && uploadResult.shareLink) {
|
|
429
|
+
console.log('๐น Watch your recording:', uploadResult.shareLink);
|
|
430
|
+
// Clean up the result file now that we've read it
|
|
431
|
+
processManager.cleanup();
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
|
|
420
435
|
// Check if files still exist - if not, background process already uploaded
|
|
421
436
|
const filesExist = fs.existsSync(result.outputPath) &&
|
|
422
437
|
(!result.gifPath || fs.existsSync(result.gifPath)) &&
|
|
423
438
|
(!result.snapshotPath || fs.existsSync(result.snapshotPath));
|
|
424
439
|
|
|
425
440
|
if (!filesExist) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
logger.debug('Waiting for upload result from background process');
|
|
429
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
430
|
-
|
|
431
|
-
// Try to read the upload result from the background process
|
|
432
|
-
const uploadResult = processManager.readUploadResult();
|
|
433
|
-
logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
|
|
434
|
-
|
|
435
|
-
if (uploadResult && uploadResult.shareLink) {
|
|
436
|
-
console.log('๐น Watch your recording:', uploadResult.shareLink);
|
|
437
|
-
// Clean up the result file now that we've read it
|
|
438
|
-
processManager.cleanup();
|
|
439
|
-
} else {
|
|
440
|
-
console.log('โ
Recording uploaded (share link not available)');
|
|
441
|
-
logger.warn('Upload result not available from background process');
|
|
442
|
-
}
|
|
443
|
-
|
|
441
|
+
console.log('โ
Recording uploaded by background process');
|
|
442
|
+
logger.info('Files were cleaned up by background process');
|
|
444
443
|
process.exit(0);
|
|
445
444
|
}
|
|
446
445
|
|
package/lib/auth.js
CHANGED
|
@@ -18,7 +18,7 @@ const auth = {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
// Exchange API key for token
|
|
21
|
-
const { token } = await got.post('https://api.
|
|
21
|
+
const { token } = await got.post('https://testdriver-api.onrender.com/auth/exchange-api-key', {
|
|
22
22
|
json: { apiKey },
|
|
23
23
|
timeout: 30000 // 30 second timeout
|
|
24
24
|
}).json();
|
|
@@ -34,7 +34,7 @@ const auth = {
|
|
|
34
34
|
|
|
35
35
|
// Get user info to verify the token works
|
|
36
36
|
logger.debug('Fetching user information to validate token...');
|
|
37
|
-
const user = await got.get('https://api.
|
|
37
|
+
const user = await got.get('https://testdriver-api.onrender.com/api/v1/whoami', {
|
|
38
38
|
headers: {
|
|
39
39
|
Authorization: `Bearer ${token}`
|
|
40
40
|
},
|
|
@@ -105,7 +105,7 @@ const auth = {
|
|
|
105
105
|
const token = await this.getToken();
|
|
106
106
|
|
|
107
107
|
try {
|
|
108
|
-
const response = await got.get('https://api.
|
|
108
|
+
const response = await got.get('https://testdriver-api.onrender.com/api/v1/projects', {
|
|
109
109
|
headers: {
|
|
110
110
|
Authorization: `Bearer ${token}`
|
|
111
111
|
},
|
|
@@ -160,7 +160,7 @@ const auth = {
|
|
|
160
160
|
requestBody.project = replayData.project;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
const response = await got.post('https://api.
|
|
163
|
+
const response = await got.post('https://testdriver-api.onrender.com/api/v1/replay/upload', {
|
|
164
164
|
headers: {
|
|
165
165
|
Authorization: `Bearer ${token}`
|
|
166
166
|
},
|
|
@@ -188,7 +188,7 @@ const auth = {
|
|
|
188
188
|
const token = await this.getToken();
|
|
189
189
|
|
|
190
190
|
try {
|
|
191
|
-
const response = await got.post('https://api.
|
|
191
|
+
const response = await got.post('https://testdriver-api.onrender.com/api/v1/logs', {
|
|
192
192
|
headers: {
|
|
193
193
|
Authorization: `Bearer ${token}`
|
|
194
194
|
},
|
package/lib/config.js
CHANGED
|
@@ -17,7 +17,7 @@ export const auth0Config = {
|
|
|
17
17
|
export const apiEndpoints = {
|
|
18
18
|
development: process.env.API_ENDPOINT || 'http://localhost:3000',
|
|
19
19
|
staging: 'https://replayable-api-staging.herokuapp.com',
|
|
20
|
-
production: 'https://api.
|
|
20
|
+
production: 'https://testdriver-api.onrender.com'
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export const API_ENDPOINT = apiEndpoints[ENV];
|
package/lib/recorder.js
CHANGED
|
@@ -531,20 +531,20 @@ export async function stopRecording() {
|
|
|
531
531
|
// Wait for FFmpeg to finish gracefully with realtime encoding
|
|
532
532
|
const gracefulTimeout = setTimeout(() => {
|
|
533
533
|
if (currentRecording && !currentRecording.killed) {
|
|
534
|
-
logger.warn('FFmpeg did not exit gracefully after
|
|
534
|
+
logger.warn('FFmpeg did not exit gracefully after 10s, sending SIGTERM...');
|
|
535
535
|
// If still running, try SIGTERM
|
|
536
536
|
process.kill(currentRecording.pid, 'SIGTERM');
|
|
537
537
|
}
|
|
538
|
-
},
|
|
538
|
+
}, 10000); // Wait longer for graceful shutdown
|
|
539
539
|
|
|
540
|
-
// Wait up to
|
|
540
|
+
// Wait up to 20 seconds for SIGTERM to work
|
|
541
541
|
const hardKillTimeout = setTimeout(() => {
|
|
542
542
|
if (currentRecording && !currentRecording.killed) {
|
|
543
543
|
logger.error('FFmpeg still running after SIGTERM, using SIGKILL...');
|
|
544
544
|
// If still not dead, use SIGKILL as last resort
|
|
545
545
|
process.kill(currentRecording.pid, 'SIGKILL');
|
|
546
546
|
}
|
|
547
|
-
},
|
|
547
|
+
}, 20000); // Longer timeout to avoid killing prematurely
|
|
548
548
|
|
|
549
549
|
// Wait for the process to fully exit
|
|
550
550
|
if (currentRecording) {
|
|
@@ -555,10 +555,30 @@ export async function stopRecording() {
|
|
|
555
555
|
clearTimeout(gracefulTimeout);
|
|
556
556
|
clearTimeout(hardKillTimeout);
|
|
557
557
|
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
logger.debug('Waiting for
|
|
561
|
-
|
|
558
|
+
// Wait for filesystem to sync and file to be written
|
|
559
|
+
// Poll for the file to exist with content
|
|
560
|
+
logger.debug('Waiting for temp file to be written...');
|
|
561
|
+
const maxWaitTime = 10000; // Wait up to 10 seconds
|
|
562
|
+
const startWait = Date.now();
|
|
563
|
+
let tempFileReady = false;
|
|
564
|
+
|
|
565
|
+
while (!tempFileReady && (Date.now() - startWait) < maxWaitTime) {
|
|
566
|
+
const tempFile = currentTempFile;
|
|
567
|
+
if (tempFile && fs.existsSync(tempFile)) {
|
|
568
|
+
const stats = fs.statSync(tempFile);
|
|
569
|
+
if (stats.size > 0) {
|
|
570
|
+
logger.debug('Temp file is ready', { size: stats.size });
|
|
571
|
+
tempFileReady = true;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Wait a bit before checking again
|
|
576
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!tempFileReady) {
|
|
580
|
+
logger.warn('Temp file not ready after waiting, proceeding anyway');
|
|
581
|
+
}
|
|
562
582
|
|
|
563
583
|
// Read temp file path from disk (for cross-process access)
|
|
564
584
|
let tempFile = currentTempFile; // Try in-memory first
|
|
@@ -47,7 +47,7 @@ export class FileTracker {
|
|
|
47
47
|
fs.writeFileSync(this.trackedFile, '', 'utf8');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
this.tail = new Tail(this.trackedFile, { encoding: '
|
|
50
|
+
this.tail = new Tail(this.trackedFile, { encoding: 'utf8' });
|
|
51
51
|
this.tail.on('line', (line) => {
|
|
52
52
|
const time = Date.now();
|
|
53
53
|
this.eventTimes.push(time);
|
|
@@ -47,6 +47,63 @@ const findLinuxIcon = async (appName) => {
|
|
|
47
47
|
return null;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Calculate string similarity (Levenshtein distance based)
|
|
52
|
+
* Returns a value between 0 (no match) and 1 (perfect match)
|
|
53
|
+
*/
|
|
54
|
+
const calculateSimilarity = (str1, str2) => {
|
|
55
|
+
const s1 = str1.toLowerCase();
|
|
56
|
+
const s2 = str2.toLowerCase();
|
|
57
|
+
|
|
58
|
+
// Exact match
|
|
59
|
+
if (s1 === s2) return 1.0;
|
|
60
|
+
|
|
61
|
+
// Check if one contains the other
|
|
62
|
+
if (s1.includes(s2) || s2.includes(s1)) {
|
|
63
|
+
return 0.8;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Simple Levenshtein-based similarity
|
|
67
|
+
const longer = s1.length > s2.length ? s1 : s2;
|
|
68
|
+
const shorter = s1.length > s2.length ? s2 : s1;
|
|
69
|
+
|
|
70
|
+
if (longer.length === 0) return 1.0;
|
|
71
|
+
|
|
72
|
+
const editDistance = levenshteinDistance(s1, s2);
|
|
73
|
+
return (longer.length - editDistance) / longer.length;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calculate Levenshtein distance between two strings
|
|
78
|
+
*/
|
|
79
|
+
const levenshteinDistance = (str1, str2) => {
|
|
80
|
+
const matrix = [];
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
83
|
+
matrix[i] = [i];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
87
|
+
matrix[0][j] = j;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
91
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
92
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
93
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
94
|
+
} else {
|
|
95
|
+
matrix[i][j] = Math.min(
|
|
96
|
+
matrix[i - 1][j - 1] + 1, // substitution
|
|
97
|
+
matrix[i][j - 1] + 1, // insertion
|
|
98
|
+
matrix[i - 1][j] + 1 // deletion
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return matrix[str2.length][str1.length];
|
|
105
|
+
};
|
|
106
|
+
|
|
50
107
|
/**
|
|
51
108
|
* Find .desktop file for an application
|
|
52
109
|
*/
|
|
@@ -61,11 +118,12 @@ const findDesktopFile = async (appName) => {
|
|
|
61
118
|
for (const dir of desktopDirs) {
|
|
62
119
|
const desktopFile = path.join(dir, `${appName}.desktop`);
|
|
63
120
|
if (fs.existsSync(desktopFile)) {
|
|
121
|
+
logger.debug("Found desktop file (exact match)", { appName, desktopFile });
|
|
64
122
|
return desktopFile;
|
|
65
123
|
}
|
|
66
124
|
}
|
|
67
125
|
|
|
68
|
-
// Try case-insensitive
|
|
126
|
+
// Try case-insensitive exact match
|
|
69
127
|
for (const dir of desktopDirs) {
|
|
70
128
|
try {
|
|
71
129
|
if (!fs.existsSync(dir)) continue;
|
|
@@ -75,6 +133,7 @@ const findDesktopFile = async (appName) => {
|
|
|
75
133
|
(f) => f.toLowerCase() === `${appName.toLowerCase()}.desktop`
|
|
76
134
|
);
|
|
77
135
|
if (match) {
|
|
136
|
+
logger.debug("Found desktop file (case-insensitive)", { appName, match });
|
|
78
137
|
return path.join(dir, match);
|
|
79
138
|
}
|
|
80
139
|
} catch (error) {
|
|
@@ -82,6 +141,40 @@ const findDesktopFile = async (appName) => {
|
|
|
82
141
|
}
|
|
83
142
|
}
|
|
84
143
|
|
|
144
|
+
// Try fuzzy matching - find best match based on string similarity
|
|
145
|
+
let bestMatch = null;
|
|
146
|
+
let bestScore = 0.6; // Minimum similarity threshold
|
|
147
|
+
|
|
148
|
+
for (const dir of desktopDirs) {
|
|
149
|
+
try {
|
|
150
|
+
if (!fs.existsSync(dir)) continue;
|
|
151
|
+
|
|
152
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.desktop'));
|
|
153
|
+
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
const baseName = file.replace('.desktop', '');
|
|
156
|
+
const similarity = calculateSimilarity(appName, baseName);
|
|
157
|
+
|
|
158
|
+
if (similarity > bestScore) {
|
|
159
|
+
bestScore = similarity;
|
|
160
|
+
bestMatch = path.join(dir, file);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
logger.debug("Error in fuzzy desktop file search", { dir, error: error.message });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (bestMatch) {
|
|
169
|
+
logger.debug("Found desktop file (fuzzy match)", {
|
|
170
|
+
appName,
|
|
171
|
+
desktopFile: bestMatch,
|
|
172
|
+
similarity: bestScore.toFixed(2)
|
|
173
|
+
});
|
|
174
|
+
return bestMatch;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
logger.debug("No desktop file found", { appName });
|
|
85
178
|
return null;
|
|
86
179
|
};
|
|
87
180
|
|
package/lib/uploader.js
CHANGED
|
@@ -255,7 +255,7 @@ export async function upload(filePath, metadata = {}) {
|
|
|
255
255
|
|
|
256
256
|
let newReplay;
|
|
257
257
|
try {
|
|
258
|
-
newReplay = await got.post('https://api.
|
|
258
|
+
newReplay = await got.post('https://testdriver-api.onrender.com/api/v1/replay', {
|
|
259
259
|
headers: {
|
|
260
260
|
Authorization: `Bearer ${token}`
|
|
261
261
|
},
|
|
@@ -430,7 +430,7 @@ export async function upload(filePath, metadata = {}) {
|
|
|
430
430
|
|
|
431
431
|
// Publish the replay (like the desktop app does)
|
|
432
432
|
logger.debug('Publishing replay...');
|
|
433
|
-
await got.post('https://api.
|
|
433
|
+
await got.post('https://testdriver-api.onrender.com/api/v1/replay/publish', {
|
|
434
434
|
headers: {
|
|
435
435
|
Authorization: `Bearer ${token}`
|
|
436
436
|
},
|
package/package.json
CHANGED
package/test_workflow.sh
CHANGED
|
@@ -46,7 +46,7 @@ RECORDING_START=$(date +%s)
|
|
|
46
46
|
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
47
47
|
echo "๐ด EVENT 1: Recording START at $(date '+%H:%M:%S')"
|
|
48
48
|
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
49
|
-
echo "[EVENT 1] Recording started at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_START" >> "$TEMP_FILE"
|
|
49
|
+
echo "[EVENT 1] ๐ด Recording started with emoji at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_START" >> "$TEMP_FILE"
|
|
50
50
|
|
|
51
51
|
# Verify recording is actually running
|
|
52
52
|
if ps -p $RECORD_PID > /dev/null; then
|
|
@@ -71,25 +71,25 @@ sleep 3
|
|
|
71
71
|
# Event 2 - after 3 seconds
|
|
72
72
|
echo ""
|
|
73
73
|
echo "๐ก EVENT 2: 3 seconds mark at $(date '+%H:%M:%S')"
|
|
74
|
-
echo "[EVENT 2] 3 seconds elapsed at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
74
|
+
echo "[EVENT 2] ๐ก 3 seconds elapsed with emoji at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
75
75
|
sleep 3
|
|
76
76
|
|
|
77
77
|
# Event 3 - after 6 seconds
|
|
78
78
|
echo ""
|
|
79
79
|
echo "๐ข EVENT 3: 6 seconds mark at $(date '+%H:%M:%S')"
|
|
80
|
-
echo "[EVENT 3] 6 seconds elapsed at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
80
|
+
echo "[EVENT 3] ๐ข 6 seconds elapsed with emoji at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
81
81
|
sleep 3
|
|
82
82
|
|
|
83
83
|
# Event 4 - after 9 seconds
|
|
84
84
|
echo ""
|
|
85
85
|
echo "๐ต EVENT 4: 9 seconds mark at $(date '+%H:%M:%S')"
|
|
86
|
-
echo "[EVENT 4] 9 seconds elapsed at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
86
|
+
echo "[EVENT 4] ๐ต 9 seconds elapsed with emoji at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
87
87
|
sleep 3
|
|
88
88
|
|
|
89
89
|
# Event 5 - after 12 seconds
|
|
90
90
|
echo ""
|
|
91
91
|
echo "๐ฃ EVENT 5: 12 seconds mark at $(date '+%H:%M:%S')"
|
|
92
|
-
echo "[EVENT 5] 12 seconds elapsed at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
92
|
+
echo "[EVENT 5] ๐ฃ 12 seconds elapsed with emoji at $(date '+%H:%M:%S')" >> "$TEMP_FILE"
|
|
93
93
|
sleep 3
|
|
94
94
|
|
|
95
95
|
# Event 6 - before ending
|
|
@@ -98,7 +98,7 @@ echo "โโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
98
98
|
echo "โซ EVENT 6: Recording END at $(date '+%H:%M:%S')"
|
|
99
99
|
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
100
100
|
RECORDING_END=$(date +%s)
|
|
101
|
-
echo "[EVENT 6] Recording ending at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_END" >> "$TEMP_FILE"
|
|
101
|
+
echo "[EVENT 6] โซ Recording ending with emoji at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_END" >> "$TEMP_FILE"
|
|
102
102
|
|
|
103
103
|
DURATION=$((RECORDING_END - RECORDING_START))
|
|
104
104
|
echo ""
|
|
@@ -112,8 +112,13 @@ sleep 2
|
|
|
112
112
|
# 6. Stop recording and upload (this will kill the background recording process)
|
|
113
113
|
echo ""
|
|
114
114
|
echo "6. Stopping recording and uploading..."
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
# Check if recording is still active
|
|
116
|
+
if ./bin/dashcam.js status | grep -q "Recording in progress"; then
|
|
117
|
+
./bin/dashcam.js stop
|
|
118
|
+
echo "โ
Recording stopped and uploaded"
|
|
119
|
+
else
|
|
120
|
+
echo "โ ๏ธ Recording already completed (this is expected with background mode)"
|
|
121
|
+
fi
|
|
117
122
|
|
|
118
123
|
echo ""
|
|
119
124
|
echo "๐งน Cleaning up..."
|
|
@@ -140,12 +145,12 @@ echo "3. Verify these events appear at the correct times:"
|
|
|
140
145
|
echo ""
|
|
141
146
|
echo " Time | Terminal Display | Log Entry"
|
|
142
147
|
echo " -------|---------------------------|---------------------------"
|
|
143
|
-
echo " 0:00 | ๐ด EVENT 1 | [EVENT 1] Recording started"
|
|
144
|
-
echo " 0:03 | ๐ก EVENT 2 | [EVENT 2] 3 seconds elapsed"
|
|
145
|
-
echo " 0:06 | ๐ข EVENT 3 | [EVENT 3] 6 seconds elapsed"
|
|
146
|
-
echo " 0:09 | ๐ต EVENT 4 | [EVENT 4] 9 seconds elapsed"
|
|
147
|
-
echo " 0:12 | ๐ฃ EVENT 5 | [EVENT 5] 12 seconds elapsed"
|
|
148
|
-
echo " 0:15 | โซ EVENT 6 | [EVENT 6] Recording ending"
|
|
148
|
+
echo " 0:00 | ๐ด EVENT 1 | [EVENT 1] ๐ด Recording started"
|
|
149
|
+
echo " 0:03 | ๐ก EVENT 2 | [EVENT 2] ๐ก 3 seconds elapsed"
|
|
150
|
+
echo " 0:06 | ๐ข EVENT 3 | [EVENT 3] ๐ข 6 seconds elapsed"
|
|
151
|
+
echo " 0:09 | ๐ต EVENT 4 | [EVENT 4] ๐ต 9 seconds elapsed"
|
|
152
|
+
echo " 0:12 | ๐ฃ EVENT 5 | [EVENT 5] ๐ฃ 12 seconds elapsed"
|
|
153
|
+
echo " 0:15 | โซ EVENT 6 | [EVENT 6] โซ Recording ending"
|
|
149
154
|
echo ""
|
|
150
155
|
echo "4. The log timestamps should match the video timeline exactly"
|
|
151
156
|
echo "5. Each colored event marker should appear in the video"
|