codeplay-common 4.3.4 → 4.3.5
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.
|
@@ -3,9 +3,12 @@ const path = require('node:path');
|
|
|
3
3
|
const readline = require('node:readline/promises');
|
|
4
4
|
const { spawn, spawnSync } = require('node:child_process');
|
|
5
5
|
|
|
6
|
-
const screenshotDirectory = path.join(__dirname, 'Auto-Screenshot', '
|
|
7
|
-
const inFrameDirectory = path.join(__dirname, 'Auto-Screenshot', '
|
|
8
|
-
const
|
|
6
|
+
const screenshotDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Screenshot');
|
|
7
|
+
const inFrameDirectory = path.join(__dirname, 'Auto-Screenshot', 'Store-image-Frame');
|
|
8
|
+
const amazonAssetsDirectory = path.join(__dirname, 'Auto-Screenshot', 'Amazon-assets');
|
|
9
|
+
const samsungAssetsDirectory = path.join(__dirname, 'Auto-Screenshot', 'Samsung-assets');
|
|
10
|
+
const capacitorConfigPath = path.join(__dirname, 'capacitor.config.json');
|
|
11
|
+
const amazonIconPath = path.join(__dirname, 'resources', 'icon-only.png');
|
|
9
12
|
const temporaryDirectory = path.join(__dirname, 'agent-temp');
|
|
10
13
|
const browserExecutablePaths = [
|
|
11
14
|
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
@@ -26,9 +29,9 @@ const deviceProfiles = [
|
|
|
26
29
|
orientationSensitive: true,
|
|
27
30
|
mobile: true,
|
|
28
31
|
},
|
|
29
|
-
{
|
|
30
|
-
name: 'android-tablet',
|
|
31
|
-
label: 'Android tablet',
|
|
32
|
+
{
|
|
33
|
+
name: 'android-tablet',
|
|
34
|
+
label: 'Android tablet',
|
|
32
35
|
outputDirectory: 'Android-Tab',
|
|
33
36
|
frameType: 'tablet',
|
|
34
37
|
width: 800,
|
|
@@ -36,7 +39,18 @@ const deviceProfiles = [
|
|
|
36
39
|
deviceScaleFactor: 2,
|
|
37
40
|
orientationSensitive: true,
|
|
38
41
|
mobile: true,
|
|
39
|
-
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'android-tablet-10-inch',
|
|
45
|
+
label: 'Android 10-inch Play Store tablet',
|
|
46
|
+
outputDirectory: 'Android-Tab-10-Inch',
|
|
47
|
+
frameType: 'tablet',
|
|
48
|
+
width: 720,
|
|
49
|
+
height: 1280,
|
|
50
|
+
deviceScaleFactor: 2,
|
|
51
|
+
orientationSensitive: true,
|
|
52
|
+
mobile: true,
|
|
53
|
+
},
|
|
40
54
|
{
|
|
41
55
|
name: 'iphone',
|
|
42
56
|
label: 'iPhone 6.9-inch App Store viewport',
|
|
@@ -141,16 +155,16 @@ const deviceProfiles = [
|
|
|
141
155
|
deviceScaleFactor: 1,
|
|
142
156
|
mobile: false,
|
|
143
157
|
},
|
|
144
|
-
{
|
|
145
|
-
name: 'chromebook',
|
|
146
|
-
label: 'Chromebook
|
|
147
|
-
outputDirectory: 'Chromebook',
|
|
148
|
-
frameType: 'laptop',
|
|
149
|
-
width:
|
|
150
|
-
height:
|
|
151
|
-
deviceScaleFactor: 1,
|
|
152
|
-
mobile: false,
|
|
153
|
-
},
|
|
158
|
+
{
|
|
159
|
+
name: 'chromebook',
|
|
160
|
+
label: 'Chromebook Play Store viewport',
|
|
161
|
+
outputDirectory: 'Chromebook',
|
|
162
|
+
frameType: 'laptop',
|
|
163
|
+
width: 1920,
|
|
164
|
+
height: 1080,
|
|
165
|
+
deviceScaleFactor: 1,
|
|
166
|
+
mobile: false,
|
|
167
|
+
},
|
|
154
168
|
{
|
|
155
169
|
name: 'android-xr',
|
|
156
170
|
label: 'Android XR 2D panel approximation',
|
|
@@ -339,7 +353,9 @@ function getScreenshotFileName(fileNameInput, selectedProfiles) {
|
|
|
339
353
|
|
|
340
354
|
function parseArguments() {
|
|
341
355
|
const args = process.argv.slice(2);
|
|
342
|
-
const options = {
|
|
356
|
+
const options = {
|
|
357
|
+
amazonAssets: false,
|
|
358
|
+
samsungAssets: false,
|
|
343
359
|
dryRun: false,
|
|
344
360
|
frameExisting: false,
|
|
345
361
|
help: false,
|
|
@@ -351,7 +367,11 @@ function parseArguments() {
|
|
|
351
367
|
for (let index = 0; index < args.length; index += 1) {
|
|
352
368
|
const argument = args[index];
|
|
353
369
|
|
|
354
|
-
if (argument === '--
|
|
370
|
+
if (argument === '--amazon-assets') {
|
|
371
|
+
options.amazonAssets = true;
|
|
372
|
+
} else if (argument === '--samsung-assets') {
|
|
373
|
+
options.samsungAssets = true;
|
|
374
|
+
} else if (argument === '--dry-run') {
|
|
355
375
|
options.dryRun = true;
|
|
356
376
|
} else if (argument === '--frame-existing') {
|
|
357
377
|
options.frameExisting = true;
|
|
@@ -395,12 +415,14 @@ function printHelp() {
|
|
|
395
415
|
console.log('Capture the current live Android WebView at multiple responsive viewport sizes.');
|
|
396
416
|
console.log('');
|
|
397
417
|
console.log('Usage:');
|
|
398
|
-
console.log(' node take-
|
|
399
|
-
console.log(' node take-
|
|
400
|
-
console.log(' node take-
|
|
401
|
-
console.log(' node take-
|
|
402
|
-
console.log(' node take-
|
|
403
|
-
console.log(' node take-
|
|
418
|
+
console.log(' node take-screen-image.js');
|
|
419
|
+
console.log(' node take-screen-image.js --profiles iphone,ipad');
|
|
420
|
+
console.log(' node take-screen-image.js --prefix home --profiles android-phone,ipad');
|
|
421
|
+
console.log(' node take-screen-image.js --frame-existing');
|
|
422
|
+
console.log(' node take-screen-image.js --amazon-assets');
|
|
423
|
+
console.log(' node take-screen-image.js --samsung-assets');
|
|
424
|
+
console.log(' node take-screen-image.js --dry-run');
|
|
425
|
+
console.log(' node take-screen-image.js --list');
|
|
404
426
|
}
|
|
405
427
|
|
|
406
428
|
function selectProfiles(input) {
|
|
@@ -876,7 +898,7 @@ function getFrameAdornment(frameType) {
|
|
|
876
898
|
return '<div class="xr-status"></div>';
|
|
877
899
|
}
|
|
878
900
|
|
|
879
|
-
function createDeviceFrameHtml(profile, screenshotBuffer) {
|
|
901
|
+
function createDeviceFrameHtml(profile, screenshotBuffer) {
|
|
880
902
|
const screenshotDataUrl = `data:image/png;base64,${screenshotBuffer.toString('base64')}`;
|
|
881
903
|
const aspectRatio = `${profile.width} / ${profile.height}`;
|
|
882
904
|
|
|
@@ -1114,9 +1136,397 @@ function createDeviceFrameHtml(profile, screenshotBuffer) {
|
|
|
1114
1136
|
${getFrameAdornment(profile.frameType)}
|
|
1115
1137
|
</main>
|
|
1116
1138
|
</body>
|
|
1117
|
-
</html>`;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1139
|
+
</html>`;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function escapeHtml(value) {
|
|
1143
|
+
return String(value)
|
|
1144
|
+
.replace(/&/g, '&')
|
|
1145
|
+
.replace(/</g, '<')
|
|
1146
|
+
.replace(/>/g, '>')
|
|
1147
|
+
.replace(/"/g, '"')
|
|
1148
|
+
.replace(/'/g, ''');
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function createAmazonAssetHtml(asset, appName, iconBuffer) {
|
|
1152
|
+
const iconDataUrl = `data:image/png;base64,${iconBuffer.toString('base64')}`;
|
|
1153
|
+
const safeAppName = escapeHtml(appName);
|
|
1154
|
+
const logoMarkup = asset.type === 'featured-logo'
|
|
1155
|
+
? `<main class="featured-logo"><img src="${iconDataUrl}" /><div><small>POCKET RHYTHM STUDIO</small><strong>${safeAppName}</strong></div></main>`
|
|
1156
|
+
: '';
|
|
1157
|
+
const iconMarkup = asset.type === 'icon'
|
|
1158
|
+
? `<main class="icon-card"><img src="${iconDataUrl}" /><div><small>MAKE THE NEXT BEAT YOURS</small><strong>${safeAppName}</strong></div></main>`
|
|
1159
|
+
: '';
|
|
1160
|
+
const backgroundMarkup = asset.type.includes('background')
|
|
1161
|
+
? '<main class="background-art"><div class="pulse pulse-one"></div><div class="pulse pulse-two"></div><div class="equalizer"><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div><div class="pad-grid"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div></main>'
|
|
1162
|
+
: '';
|
|
1163
|
+
|
|
1164
|
+
return `<!doctype html>
|
|
1165
|
+
<html>
|
|
1166
|
+
<head>
|
|
1167
|
+
<meta charset="utf-8" />
|
|
1168
|
+
<style>
|
|
1169
|
+
* { box-sizing: border-box; }
|
|
1170
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
|
1171
|
+
body {
|
|
1172
|
+
position: relative;
|
|
1173
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
1174
|
+
color: #f8fbff;
|
|
1175
|
+
background: transparent;
|
|
1176
|
+
}
|
|
1177
|
+
body.icon,
|
|
1178
|
+
body.background-image,
|
|
1179
|
+
body.featured-background {
|
|
1180
|
+
background:
|
|
1181
|
+
radial-gradient(circle at 16% 18%, rgba(52, 211, 235, 0.32), transparent 28%),
|
|
1182
|
+
radial-gradient(circle at 82% 82%, rgba(201, 255, 65, 0.24), transparent 30%),
|
|
1183
|
+
linear-gradient(135deg, #24113c 0%, #15152d 48%, #071725 100%);
|
|
1184
|
+
}
|
|
1185
|
+
body.icon::before,
|
|
1186
|
+
body.background-image::before,
|
|
1187
|
+
body.featured-background::before {
|
|
1188
|
+
content: '';
|
|
1189
|
+
position: absolute;
|
|
1190
|
+
inset: 0;
|
|
1191
|
+
opacity: 0.22;
|
|
1192
|
+
background-image:
|
|
1193
|
+
linear-gradient(rgba(255,255,255,0.07) 1px, transparent 1px),
|
|
1194
|
+
linear-gradient(90deg, rgba(255,255,255,0.07) 1px, transparent 1px);
|
|
1195
|
+
background-size: 72px 72px;
|
|
1196
|
+
}
|
|
1197
|
+
.icon-card {
|
|
1198
|
+
position: relative;
|
|
1199
|
+
z-index: 2;
|
|
1200
|
+
width: 100%;
|
|
1201
|
+
height: 100%;
|
|
1202
|
+
display: flex;
|
|
1203
|
+
align-items: center;
|
|
1204
|
+
justify-content: center;
|
|
1205
|
+
gap: 72px;
|
|
1206
|
+
padding: 72px 110px;
|
|
1207
|
+
}
|
|
1208
|
+
.icon-card img {
|
|
1209
|
+
width: 500px;
|
|
1210
|
+
height: 500px;
|
|
1211
|
+
object-fit: contain;
|
|
1212
|
+
border-radius: 104px;
|
|
1213
|
+
box-shadow: 0 38px 80px rgba(0,0,0,0.48), 0 0 70px rgba(52,211,235,0.2);
|
|
1214
|
+
}
|
|
1215
|
+
.icon-card div { display: flex; flex-direction: column; gap: 18px; }
|
|
1216
|
+
.icon-card small,
|
|
1217
|
+
.featured-logo small {
|
|
1218
|
+
color: #35d4eb;
|
|
1219
|
+
font-size: 24px;
|
|
1220
|
+
font-weight: 700;
|
|
1221
|
+
letter-spacing: 7px;
|
|
1222
|
+
}
|
|
1223
|
+
.icon-card strong { font-size: 86px; line-height: 0.96; max-width: 500px; }
|
|
1224
|
+
.featured-logo {
|
|
1225
|
+
width: 100%;
|
|
1226
|
+
height: 100%;
|
|
1227
|
+
display: flex;
|
|
1228
|
+
align-items: center;
|
|
1229
|
+
gap: 24px;
|
|
1230
|
+
padding: 20px 26px;
|
|
1231
|
+
color: #111827;
|
|
1232
|
+
}
|
|
1233
|
+
.featured-logo img {
|
|
1234
|
+
width: 210px;
|
|
1235
|
+
height: 210px;
|
|
1236
|
+
object-fit: contain;
|
|
1237
|
+
border-radius: 44px;
|
|
1238
|
+
}
|
|
1239
|
+
.featured-logo div { display: flex; min-width: 0; flex-direction: column; gap: 8px; }
|
|
1240
|
+
.featured-logo small { font-size: 14px; letter-spacing: 3px; }
|
|
1241
|
+
.featured-logo strong {
|
|
1242
|
+
max-width: 360px;
|
|
1243
|
+
overflow: hidden;
|
|
1244
|
+
color: #ffffff;
|
|
1245
|
+
font-size: 56px;
|
|
1246
|
+
line-height: 1;
|
|
1247
|
+
text-overflow: ellipsis;
|
|
1248
|
+
white-space: nowrap;
|
|
1249
|
+
text-shadow: 0 3px 12px rgba(0,0,0,0.75);
|
|
1250
|
+
}
|
|
1251
|
+
.background-art { position: absolute; inset: 0; }
|
|
1252
|
+
.pulse {
|
|
1253
|
+
position: absolute;
|
|
1254
|
+
border: 3px solid rgba(53,212,235,0.28);
|
|
1255
|
+
border-radius: 50%;
|
|
1256
|
+
box-shadow: 0 0 80px rgba(53,212,235,0.15);
|
|
1257
|
+
}
|
|
1258
|
+
.pulse-one { width: 520px; height: 520px; top: -180px; left: -110px; }
|
|
1259
|
+
.pulse-two { width: 620px; height: 620px; right: -180px; bottom: -310px; border-color: rgba(201,255,65,0.22); }
|
|
1260
|
+
.equalizer {
|
|
1261
|
+
position: absolute;
|
|
1262
|
+
left: 8%;
|
|
1263
|
+
top: 50%;
|
|
1264
|
+
display: flex;
|
|
1265
|
+
align-items: center;
|
|
1266
|
+
gap: 20px;
|
|
1267
|
+
height: 44%;
|
|
1268
|
+
transform: translateY(-50%);
|
|
1269
|
+
}
|
|
1270
|
+
.equalizer i { width: 18px; border-radius: 999px; background: #35d4eb; box-shadow: 0 0 25px rgba(53,212,235,0.5); }
|
|
1271
|
+
.equalizer i:nth-child(1), .equalizer i:nth-child(7) { height: 28%; }
|
|
1272
|
+
.equalizer i:nth-child(2), .equalizer i:nth-child(6) { height: 52%; background: #ff5964; }
|
|
1273
|
+
.equalizer i:nth-child(3), .equalizer i:nth-child(5) { height: 76%; background: #ff9f43; }
|
|
1274
|
+
.equalizer i:nth-child(4) { height: 100%; background: #c9ff41; }
|
|
1275
|
+
.pad-grid {
|
|
1276
|
+
position: absolute;
|
|
1277
|
+
right: 7%;
|
|
1278
|
+
top: 50%;
|
|
1279
|
+
display: grid;
|
|
1280
|
+
width: 46%;
|
|
1281
|
+
height: 64%;
|
|
1282
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1283
|
+
gap: 20px;
|
|
1284
|
+
transform: translateY(-50%) perspective(900px) rotateY(-10deg) rotateX(5deg);
|
|
1285
|
+
}
|
|
1286
|
+
.pad-grid i {
|
|
1287
|
+
border: 2px solid rgba(255,255,255,0.17);
|
|
1288
|
+
border-top: 7px solid #35d4eb;
|
|
1289
|
+
border-radius: 22px;
|
|
1290
|
+
background: linear-gradient(145deg, rgba(42,55,75,0.96), rgba(11,18,29,0.96));
|
|
1291
|
+
box-shadow: 0 20px 28px rgba(0,0,0,0.28);
|
|
1292
|
+
}
|
|
1293
|
+
.pad-grid i:nth-child(2), .pad-grid i:nth-child(6) { border-top-color: #ff9f43; }
|
|
1294
|
+
.pad-grid i:nth-child(3), .pad-grid i:nth-child(7) { border-top-color: #c9ff41; }
|
|
1295
|
+
.pad-grid i:nth-child(4), .pad-grid i:nth-child(8) { border-top-color: #ff5964; }
|
|
1296
|
+
.pad-grid i:nth-child(5) { border-top-color: #ac7cff; }
|
|
1297
|
+
body.featured-background .pad-grid { width: 49%; height: 72%; }
|
|
1298
|
+
body.featured-background .equalizer { height: 52%; }
|
|
1299
|
+
</style>
|
|
1300
|
+
</head>
|
|
1301
|
+
<body class="${asset.type}">
|
|
1302
|
+
${iconMarkup}
|
|
1303
|
+
${logoMarkup}
|
|
1304
|
+
${backgroundMarkup}
|
|
1305
|
+
</body>
|
|
1306
|
+
</html>`;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
async function createAmazonAssets(client) {
|
|
1310
|
+
if (!fs.existsSync(amazonIconPath)) {
|
|
1311
|
+
throw new Error(`Amazon asset source icon was not found: ${amazonIconPath}`);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const config = JSON.parse(fs.readFileSync(capacitorConfigPath, 'utf8'));
|
|
1315
|
+
const appName = config.appName || 'App';
|
|
1316
|
+
const iconBuffer = fs.readFileSync(amazonIconPath);
|
|
1317
|
+
const assets = [
|
|
1318
|
+
{ fileName: 'icon-1280x720.png', type: 'icon', width: 1280, height: 720 },
|
|
1319
|
+
{ fileName: 'background-image-1920x1080.png', type: 'background-image', width: 1920, height: 1080 },
|
|
1320
|
+
{ fileName: 'featured-content-logo-640x260.png', type: 'featured-logo', width: 640, height: 260, transparent: true },
|
|
1321
|
+
{ fileName: 'featured-content-background-1920x720.png', type: 'featured-background', width: 1920, height: 720 },
|
|
1322
|
+
];
|
|
1323
|
+
|
|
1324
|
+
fs.mkdirSync(amazonAssetsDirectory, { recursive: true });
|
|
1325
|
+
|
|
1326
|
+
for (const asset of assets) {
|
|
1327
|
+
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
1328
|
+
width: asset.width,
|
|
1329
|
+
height: asset.height,
|
|
1330
|
+
deviceScaleFactor: 1,
|
|
1331
|
+
mobile: false,
|
|
1332
|
+
screenWidth: asset.width,
|
|
1333
|
+
screenHeight: asset.height,
|
|
1334
|
+
positionX: 0,
|
|
1335
|
+
positionY: 0,
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
const frameTree = await client.send('Page.getFrameTree');
|
|
1339
|
+
await client.send('Page.setDocumentContent', {
|
|
1340
|
+
frameId: frameTree.frameTree.frame.id,
|
|
1341
|
+
html: createAmazonAssetHtml(asset, appName, iconBuffer),
|
|
1342
|
+
});
|
|
1343
|
+
await client.send('Runtime.evaluate', {
|
|
1344
|
+
expression: `Promise.all([...document.images].map((image) => image.complete
|
|
1345
|
+
? Promise.resolve()
|
|
1346
|
+
: new Promise((resolve) => {
|
|
1347
|
+
image.addEventListener('load', resolve, { once: true });
|
|
1348
|
+
image.addEventListener('error', resolve, { once: true });
|
|
1349
|
+
}))).then(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))`,
|
|
1350
|
+
awaitPromise: true,
|
|
1351
|
+
returnByValue: true,
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
const screenshot = await client.send('Page.captureScreenshot', {
|
|
1355
|
+
format: 'png',
|
|
1356
|
+
fromSurface: true,
|
|
1357
|
+
captureBeyondViewport: false,
|
|
1358
|
+
omitBackground: Boolean(asset.transparent),
|
|
1359
|
+
});
|
|
1360
|
+
const outputPath = path.join(amazonAssetsDirectory, asset.fileName);
|
|
1361
|
+
fs.writeFileSync(outputPath, Buffer.from(screenshot.data, 'base64'));
|
|
1362
|
+
console.log(`Saved Amazon asset ${asset.width}x${asset.height}: ${outputPath}`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function createSamsungAssetHtml(asset, appName, iconBuffer) {
|
|
1367
|
+
const iconDataUrl = `data:image/png;base64,${iconBuffer.toString('base64')}`;
|
|
1368
|
+
const safeAppName = escapeHtml(appName);
|
|
1369
|
+
const pads = Array.from({ length: 9 }, (_, index) => (
|
|
1370
|
+
`<button><i>0${index + 1}</i><span>${['OPEN', 'CLOSED', 'HIGH', 'CRASH', 'KICK', 'MID', 'RIDE', 'SNARE', 'FLOOR'][index]}</span></button>`
|
|
1371
|
+
)).join('');
|
|
1372
|
+
const content = asset.type === 'edge-screen'
|
|
1373
|
+
? `<main class="edge-panel"><header><img src="${iconDataUrl}" /><strong>${safeAppName}</strong></header><section>${pads}</section></main>`
|
|
1374
|
+
: `<main class="single-panel"><header><img src="${iconDataUrl}" /><div><small>POCKET RHYTHM</small><strong>${safeAppName}</strong></div></header><div class="single-action"><span>+</span><strong>OPEN STUDIO</strong><small>Tap to start playing</small></div><div class="rhythm"><i></i><i></i><i></i><i></i><i></i></div></main>`;
|
|
1375
|
+
|
|
1376
|
+
return `<!doctype html>
|
|
1377
|
+
<html>
|
|
1378
|
+
<head>
|
|
1379
|
+
<meta charset="utf-8" />
|
|
1380
|
+
<style>
|
|
1381
|
+
* { box-sizing: border-box; }
|
|
1382
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
|
1383
|
+
body {
|
|
1384
|
+
position: relative;
|
|
1385
|
+
color: #f7fbff;
|
|
1386
|
+
background:
|
|
1387
|
+
radial-gradient(circle at 50% 8%, rgba(53,212,235,0.26), transparent 24%),
|
|
1388
|
+
radial-gradient(circle at 50% 88%, rgba(201,255,65,0.16), transparent 25%),
|
|
1389
|
+
linear-gradient(180deg, #231039, #111729 48%, #071522);
|
|
1390
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
1391
|
+
}
|
|
1392
|
+
body::before {
|
|
1393
|
+
content: '';
|
|
1394
|
+
position: absolute;
|
|
1395
|
+
inset: 0;
|
|
1396
|
+
opacity: 0.18;
|
|
1397
|
+
background: repeating-linear-gradient(0deg, transparent 0 79px, rgba(255,255,255,0.08) 80px);
|
|
1398
|
+
}
|
|
1399
|
+
main { position: relative; z-index: 1; width: 100%; height: 100%; }
|
|
1400
|
+
.edge-panel { display: flex; flex-direction: column; padding: 28px 12px; }
|
|
1401
|
+
.edge-panel header { display: flex; flex-direction: column; align-items: center; gap: 12px; min-height: 230px; }
|
|
1402
|
+
.edge-panel header img { width: 112px; height: 112px; border-radius: 25px; object-fit: contain; }
|
|
1403
|
+
.edge-panel header strong { max-width: 136px; overflow: hidden; font-size: 18px; text-overflow: ellipsis; white-space: nowrap; }
|
|
1404
|
+
.edge-panel section { display: grid; flex: 1; grid-template-rows: repeat(9, 1fr); gap: 13px; }
|
|
1405
|
+
.edge-panel button {
|
|
1406
|
+
display: flex;
|
|
1407
|
+
width: 100%;
|
|
1408
|
+
min-height: 0;
|
|
1409
|
+
flex-direction: column;
|
|
1410
|
+
align-items: center;
|
|
1411
|
+
justify-content: center;
|
|
1412
|
+
gap: 8px;
|
|
1413
|
+
color: #f7fbff;
|
|
1414
|
+
border: 1px solid rgba(255,255,255,0.16);
|
|
1415
|
+
border-top: 5px solid #35d4eb;
|
|
1416
|
+
border-radius: 18px;
|
|
1417
|
+
background: linear-gradient(145deg, rgba(41,55,76,0.98), rgba(10,17,29,0.98));
|
|
1418
|
+
box-shadow: 0 10px 18px rgba(0,0,0,0.3);
|
|
1419
|
+
}
|
|
1420
|
+
.edge-panel button:nth-child(3n+2) { border-top-color: #ff9f43; }
|
|
1421
|
+
.edge-panel button:nth-child(3n) { border-top-color: #c9ff41; }
|
|
1422
|
+
.edge-panel button i { color: #35d4eb; font-size: 14px; font-style: normal; font-weight: 700; }
|
|
1423
|
+
.edge-panel button span { font-size: 12px; font-weight: 700; letter-spacing: 1px; }
|
|
1424
|
+
.single-panel { display: flex; flex-direction: column; align-items: center; padding: 100px 44px; }
|
|
1425
|
+
.single-panel header { display: flex; width: 100%; align-items: center; gap: 24px; }
|
|
1426
|
+
.single-panel header img { width: 160px; height: 160px; border-radius: 36px; object-fit: contain; }
|
|
1427
|
+
.single-panel header div { display: flex; min-width: 0; flex-direction: column; gap: 9px; }
|
|
1428
|
+
.single-panel header small { color: #35d4eb; font-size: 17px; font-weight: 700; letter-spacing: 3px; }
|
|
1429
|
+
.single-panel header strong { overflow: hidden; font-size: 48px; text-overflow: ellipsis; white-space: nowrap; }
|
|
1430
|
+
.single-action {
|
|
1431
|
+
display: flex;
|
|
1432
|
+
width: 100%;
|
|
1433
|
+
height: 880px;
|
|
1434
|
+
margin-top: 380px;
|
|
1435
|
+
flex-direction: column;
|
|
1436
|
+
align-items: center;
|
|
1437
|
+
justify-content: center;
|
|
1438
|
+
border: 3px solid rgba(53,212,235,0.5);
|
|
1439
|
+
border-radius: 90px;
|
|
1440
|
+
background: linear-gradient(145deg, rgba(39,53,75,0.96), rgba(8,16,28,0.98));
|
|
1441
|
+
box-shadow: 0 50px 90px rgba(0,0,0,0.42), 0 0 75px rgba(53,212,235,0.16);
|
|
1442
|
+
}
|
|
1443
|
+
.single-action span {
|
|
1444
|
+
display: grid;
|
|
1445
|
+
width: 260px;
|
|
1446
|
+
height: 260px;
|
|
1447
|
+
place-items: center;
|
|
1448
|
+
color: #0d1721;
|
|
1449
|
+
border-radius: 50%;
|
|
1450
|
+
background: linear-gradient(145deg, #35d4eb, #c9ff41);
|
|
1451
|
+
box-shadow: 0 25px 55px rgba(53,212,235,0.3);
|
|
1452
|
+
font-size: 170px;
|
|
1453
|
+
font-weight: 200;
|
|
1454
|
+
line-height: 1;
|
|
1455
|
+
}
|
|
1456
|
+
.single-action strong { margin-top: 65px; font-size: 42px; letter-spacing: 5px; }
|
|
1457
|
+
.single-action small { margin-top: 18px; color: #a9b7c9; font-size: 24px; }
|
|
1458
|
+
.rhythm { display: flex; align-items: center; gap: 18px; height: 300px; margin-top: auto; }
|
|
1459
|
+
.rhythm i { width: 14px; height: 70px; border-radius: 999px; background: #35d4eb; }
|
|
1460
|
+
.rhythm i:nth-child(2), .rhythm i:nth-child(4) { height: 140px; background: #ff5964; }
|
|
1461
|
+
.rhythm i:nth-child(3) { height: 210px; background: #c9ff41; }
|
|
1462
|
+
</style>
|
|
1463
|
+
</head>
|
|
1464
|
+
<body>${content}</body>
|
|
1465
|
+
</html>`;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
async function createSamsungAssets(client) {
|
|
1469
|
+
if (!fs.existsSync(amazonIconPath)) {
|
|
1470
|
+
throw new Error(`Samsung asset source icon was not found: ${amazonIconPath}`);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const config = JSON.parse(fs.readFileSync(capacitorConfigPath, 'utf8'));
|
|
1474
|
+
const appName = config.appName || 'App';
|
|
1475
|
+
const iconBuffer = fs.readFileSync(amazonIconPath);
|
|
1476
|
+
const assets = [
|
|
1477
|
+
{ fileName: 'edge-screen-160x2560.png', type: 'edge-screen', width: 160, height: 2560 },
|
|
1478
|
+
{ fileName: 'edge-screen-single-plus-550x2560.png', type: 'edge-screen-single-plus', width: 550, height: 2560 },
|
|
1479
|
+
];
|
|
1480
|
+
const maximumFileSize = 1024 * 1024;
|
|
1481
|
+
|
|
1482
|
+
fs.mkdirSync(samsungAssetsDirectory, { recursive: true });
|
|
1483
|
+
|
|
1484
|
+
for (const asset of assets) {
|
|
1485
|
+
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
1486
|
+
width: asset.width,
|
|
1487
|
+
height: asset.height,
|
|
1488
|
+
deviceScaleFactor: 1,
|
|
1489
|
+
mobile: false,
|
|
1490
|
+
screenWidth: asset.width,
|
|
1491
|
+
screenHeight: asset.height,
|
|
1492
|
+
positionX: 0,
|
|
1493
|
+
positionY: 0,
|
|
1494
|
+
});
|
|
1495
|
+
const frameTree = await client.send('Page.getFrameTree');
|
|
1496
|
+
await client.send('Page.setDocumentContent', {
|
|
1497
|
+
frameId: frameTree.frameTree.frame.id,
|
|
1498
|
+
html: createSamsungAssetHtml(asset, appName, iconBuffer),
|
|
1499
|
+
});
|
|
1500
|
+
await client.send('Runtime.evaluate', {
|
|
1501
|
+
expression: `Promise.all([...document.images].map((image) => image.complete
|
|
1502
|
+
? Promise.resolve()
|
|
1503
|
+
: new Promise((resolve) => image.addEventListener('load', resolve, { once: true }))))
|
|
1504
|
+
.then(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))`,
|
|
1505
|
+
awaitPromise: true,
|
|
1506
|
+
returnByValue: true,
|
|
1507
|
+
});
|
|
1508
|
+
const screenshot = await client.send('Page.captureScreenshot', {
|
|
1509
|
+
format: 'png',
|
|
1510
|
+
fromSurface: true,
|
|
1511
|
+
captureBeyondViewport: false,
|
|
1512
|
+
});
|
|
1513
|
+
const outputPath = path.join(samsungAssetsDirectory, asset.fileName);
|
|
1514
|
+
const outputBuffer = Buffer.from(screenshot.data, 'base64');
|
|
1515
|
+
|
|
1516
|
+
if (outputBuffer.length >= maximumFileSize) {
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
`Samsung asset exceeds 1024 KB (${Math.ceil(outputBuffer.length / 1024)} KB): ${asset.fileName}`,
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
fs.writeFileSync(outputPath, outputBuffer);
|
|
1523
|
+
console.log(
|
|
1524
|
+
`Saved Samsung asset ${asset.width}x${asset.height} `
|
|
1525
|
+
+ `(${Math.ceil(outputBuffer.length / 1024)} KB): ${outputPath}`,
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1120
1530
|
async function captureFramedScreenshot(
|
|
1121
1531
|
client,
|
|
1122
1532
|
screenshotBuffer,
|
|
@@ -1332,8 +1742,13 @@ async function runInteractiveCaptureLoop(
|
|
|
1332
1742
|
console.log('\nResponsive screenshot tool closed.');
|
|
1333
1743
|
}
|
|
1334
1744
|
|
|
1335
|
-
async function getInteractiveOptions(options) {
|
|
1336
|
-
if (
|
|
1745
|
+
async function getInteractiveOptions(options) {
|
|
1746
|
+
if (
|
|
1747
|
+
!process.stdin.isTTY
|
|
1748
|
+
|| options.dryRun
|
|
1749
|
+
|| options.amazonAssets
|
|
1750
|
+
|| options.samsungAssets
|
|
1751
|
+
) {
|
|
1337
1752
|
return options;
|
|
1338
1753
|
}
|
|
1339
1754
|
|
|
@@ -1370,9 +1785,43 @@ async function main() {
|
|
|
1370
1785
|
return;
|
|
1371
1786
|
}
|
|
1372
1787
|
|
|
1373
|
-
options = await getInteractiveOptions(options);
|
|
1374
|
-
|
|
1375
|
-
|
|
1788
|
+
options = await getInteractiveOptions(options);
|
|
1789
|
+
|
|
1790
|
+
if (options.amazonAssets) {
|
|
1791
|
+
let amazonBrowser;
|
|
1792
|
+
let amazonClient;
|
|
1793
|
+
|
|
1794
|
+
try {
|
|
1795
|
+
amazonBrowser = await launchReplayBrowser();
|
|
1796
|
+
amazonClient = await createReplayPage(amazonBrowser);
|
|
1797
|
+
await createAmazonAssets(amazonClient);
|
|
1798
|
+
} finally {
|
|
1799
|
+
if (amazonClient) amazonClient.close();
|
|
1800
|
+
if (amazonBrowser) await closeReplayBrowser(amazonBrowser);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
console.log(`Amazon asset creation completed: ${amazonAssetsDirectory}`);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
if (options.samsungAssets) {
|
|
1808
|
+
let samsungBrowser;
|
|
1809
|
+
let samsungClient;
|
|
1810
|
+
|
|
1811
|
+
try {
|
|
1812
|
+
samsungBrowser = await launchReplayBrowser();
|
|
1813
|
+
samsungClient = await createReplayPage(samsungBrowser);
|
|
1814
|
+
await createSamsungAssets(samsungClient);
|
|
1815
|
+
} finally {
|
|
1816
|
+
if (samsungClient) samsungClient.close();
|
|
1817
|
+
if (samsungBrowser) await closeReplayBrowser(samsungBrowser);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
console.log(`Samsung asset creation completed: ${samsungAssetsDirectory}`);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
const selectedProfiles = selectProfiles(options.profiles);
|
|
1376
1825
|
|
|
1377
1826
|
if (options.frameExisting) {
|
|
1378
1827
|
await frameExistingScreenshots(selectedProfiles);
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
//node take-screen-video.js
|
|
2
|
+
|
|
3
|
+
//--audio without //with,without
|
|
4
|
+
//--duration 0 //30,120 (0 is used "Enter" key to stop)
|
|
5
|
+
//--countdown 0//0,5,10
|
|
6
|
+
//--name test-name
|
|
7
|
+
//--profiles youtube,non-spatial-xr,samsung,amazon
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
14
|
+
|
|
15
|
+
const scriptDirectory = __dirname;
|
|
16
|
+
const projectDirectory = scriptDirectory;
|
|
17
|
+
const temporaryDirectory = path.join(projectDirectory, 'agent-temp', 'store-video');
|
|
18
|
+
const outputDirectory = path.join(projectDirectory, 'Auto-Screenshot', 'Video', 'Output');
|
|
19
|
+
const deviceRecordingPath = '/sdcard/codeplay_store_video.mp4';
|
|
20
|
+
const ffmpegFallbackDirectory = 'W:\\Tools\\ffmpeg\\bin';
|
|
21
|
+
const scrcpyFallbackPath = 'W:\\Tools\\scrcpy\\scrcpy.exe';
|
|
22
|
+
const capacitorConfigPath = path.join(projectDirectory, 'capacitor.config.json');
|
|
23
|
+
|
|
24
|
+
const videoProfiles = {
|
|
25
|
+
youtube: {
|
|
26
|
+
label: 'YouTube preview',
|
|
27
|
+
directory: 'YouTube',
|
|
28
|
+
fileSuffix: 'youtube',
|
|
29
|
+
width: 1920,
|
|
30
|
+
height: 1080,
|
|
31
|
+
videoBitrate: '8M',
|
|
32
|
+
h264Level: '4.2',
|
|
33
|
+
},
|
|
34
|
+
'non-spatial-xr': {
|
|
35
|
+
label: 'Google Play non-spatial XR',
|
|
36
|
+
directory: 'Google-Play-XR-Non-Spatial',
|
|
37
|
+
fileSuffix: 'non-spatial-xr',
|
|
38
|
+
width: 3840,
|
|
39
|
+
height: 2160,
|
|
40
|
+
videoBitrate: '12M',
|
|
41
|
+
h264Level: '5.1',
|
|
42
|
+
},
|
|
43
|
+
samsung: {
|
|
44
|
+
label: 'Samsung Galaxy Store YouTube preview',
|
|
45
|
+
directory: 'Samsung',
|
|
46
|
+
fileSuffix: 'samsung',
|
|
47
|
+
width: 1920,
|
|
48
|
+
height: 1080,
|
|
49
|
+
videoBitrate: '8M',
|
|
50
|
+
h264Level: '4.2',
|
|
51
|
+
},
|
|
52
|
+
amazon: {
|
|
53
|
+
label: 'Amazon Appstore',
|
|
54
|
+
directory: 'Amazon',
|
|
55
|
+
fileSuffix: 'amazon',
|
|
56
|
+
width: 1920,
|
|
57
|
+
height: 1080,
|
|
58
|
+
videoBitrate: '4M',
|
|
59
|
+
h264Level: '4.2',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function run(command, args, options = {}) {
|
|
64
|
+
const result = spawnSync(command, args, {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
stdio: options.inherit ? 'inherit' : 'pipe',
|
|
68
|
+
...options,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (result.error) {
|
|
72
|
+
if (result.error.code === 'ENOENT') {
|
|
73
|
+
throw new Error(`${command} was not found in PATH.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw result.error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (result.status !== 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
String(result.stderr || result.stdout || `${command} exited with code ${result.status}.`).trim(),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return String(result.stdout || '').trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function commandExists(command) {
|
|
89
|
+
const result = spawnSync(command, ['-version'], {
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
});
|
|
93
|
+
return !result.error && result.status === 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getExecutable(command) {
|
|
97
|
+
if (commandExists(command)) {
|
|
98
|
+
return command;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fallbackPath = path.join(ffmpegFallbackDirectory, `${command}.exe`);
|
|
102
|
+
if (fs.existsSync(fallbackPath)) {
|
|
103
|
+
return fallbackPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function wait(milliseconds) {
|
|
110
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runAsync(command, args, options = {}) {
|
|
114
|
+
const child = spawn(command, args, {
|
|
115
|
+
windowsHide: true,
|
|
116
|
+
stdio: 'inherit',
|
|
117
|
+
...options,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
child,
|
|
122
|
+
completion: new Promise((resolve, reject) => {
|
|
123
|
+
child.once('error', reject);
|
|
124
|
+
child.once('exit', (code) => {
|
|
125
|
+
if (code === 0) resolve();
|
|
126
|
+
else reject(new Error(`${command} exited with code ${code}.`));
|
|
127
|
+
});
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getConnectedDevice() {
|
|
133
|
+
const devices = run('adb', ['devices'])
|
|
134
|
+
.split(/\r?\n/)
|
|
135
|
+
.map((line) => line.match(/^(\S+)\s+device$/))
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.map((match) => match[1]);
|
|
138
|
+
|
|
139
|
+
if (!devices.length) {
|
|
140
|
+
throw new Error('no_connected_device');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return devices[0];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sanitizeName(value) {
|
|
147
|
+
const name = String(value || '')
|
|
148
|
+
.trim()
|
|
149
|
+
.replace(/\.mp4$/i, '')
|
|
150
|
+
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_')
|
|
151
|
+
.replace(/[. ]+$/g, '');
|
|
152
|
+
return name || 'store-video';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getCapacitorConfig() {
|
|
156
|
+
return JSON.parse(fs.readFileSync(capacitorConfigPath, 'utf8'));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getDefaultVideoName() {
|
|
160
|
+
return sanitizeName(getCapacitorConfig().appName || 'store-video');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseArguments() {
|
|
164
|
+
const args = process.argv.slice(2);
|
|
165
|
+
const options = {
|
|
166
|
+
audio: 'without',
|
|
167
|
+
countdown: 5,
|
|
168
|
+
duration: 0,
|
|
169
|
+
help: false,
|
|
170
|
+
input: '',
|
|
171
|
+
list: false,
|
|
172
|
+
name: '',
|
|
173
|
+
profiles: 'youtube,non-spatial-xr,samsung,amazon',
|
|
174
|
+
spatialInput: '',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
178
|
+
const argument = args[index];
|
|
179
|
+
|
|
180
|
+
if (argument === '--audio') {
|
|
181
|
+
options.audio = String(args[index + 1] || '').toLowerCase();
|
|
182
|
+
index += 1;
|
|
183
|
+
} else if (argument === '--countdown') {
|
|
184
|
+
options.countdown = Number(args[index + 1]);
|
|
185
|
+
index += 1;
|
|
186
|
+
} else if (argument === '--duration') {
|
|
187
|
+
options.duration = Number(args[index + 1]);
|
|
188
|
+
index += 1;
|
|
189
|
+
} else if (argument === '--input') {
|
|
190
|
+
options.input = args[index + 1] || '';
|
|
191
|
+
index += 1;
|
|
192
|
+
} else if (argument === '--name') {
|
|
193
|
+
options.name = args[index + 1] || '';
|
|
194
|
+
index += 1;
|
|
195
|
+
} else if (argument === '--profiles') {
|
|
196
|
+
options.profiles = args[index + 1] || '';
|
|
197
|
+
index += 1;
|
|
198
|
+
} else if (argument === '--spatial-input') {
|
|
199
|
+
options.spatialInput = args[index + 1] || '';
|
|
200
|
+
index += 1;
|
|
201
|
+
} else if (argument === '--list') {
|
|
202
|
+
options.list = true;
|
|
203
|
+
} else if (argument === '--help' || argument === '-h') {
|
|
204
|
+
options.help = true;
|
|
205
|
+
} else {
|
|
206
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!Number.isInteger(options.duration) || options.duration < 0 || options.duration > 180) {
|
|
211
|
+
throw new Error('--duration must be 0 (unlimited) or a whole number from 1 to 180 seconds.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!Number.isInteger(options.countdown) || options.countdown < 0 || options.countdown > 30) {
|
|
215
|
+
throw new Error('--countdown must be a whole number from 0 to 30 seconds.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!['with', 'without'].includes(options.audio)) {
|
|
219
|
+
throw new Error('--audio must be with or without.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
options.name = options.name
|
|
223
|
+
? sanitizeName(options.name)
|
|
224
|
+
: getDefaultVideoName();
|
|
225
|
+
return options;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function selectProfiles(input) {
|
|
229
|
+
const names = String(input)
|
|
230
|
+
.split(',')
|
|
231
|
+
.map((name) => name.trim().toLowerCase())
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
|
|
234
|
+
if (names.includes('all')) {
|
|
235
|
+
return Object.keys(videoProfiles);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const name of names) {
|
|
239
|
+
if (!videoProfiles[name] && name !== 'spatial-xr') {
|
|
240
|
+
throw new Error(`Unknown profile: ${name}. Run with --list to see valid profiles.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return [...new Set(names)];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function printProfiles() {
|
|
248
|
+
for (const [name, profile] of Object.entries(videoProfiles)) {
|
|
249
|
+
console.log(
|
|
250
|
+
`${name.padEnd(16)} ${profile.label} (${profile.width}x${profile.height}, H.264 MP4)`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log('spatial-xr Genuine 360/180/3D source passthrough (requires --spatial-input)');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function printHelp() {
|
|
258
|
+
console.log('Record the connected Android device and create store-ready video variants.');
|
|
259
|
+
console.log('');
|
|
260
|
+
console.log('Usage:');
|
|
261
|
+
console.log(' node take-screen-video.js --name gameplay --duration 30');
|
|
262
|
+
console.log(' node take-screen-video.js --audio with');
|
|
263
|
+
console.log(' node take-screen-video.js --input recording.mp4 --profiles youtube,amazon');
|
|
264
|
+
console.log(' node take-screen-video.js --profiles spatial-xr --spatial-input genuine-360.mp4');
|
|
265
|
+
console.log(' node take-screen-video.js --profiles all');
|
|
266
|
+
console.log(' node take-screen-video.js --list');
|
|
267
|
+
console.log('');
|
|
268
|
+
console.log('Defaults: --audio without --duration 0 (unlimited)');
|
|
269
|
+
console.log(`Default name: ${getDefaultVideoName()} (capacitor.config.json appName)`);
|
|
270
|
+
console.log('Manual mode: press Enter to stop; a positive --duration sets a safety maximum.');
|
|
271
|
+
console.log('Audio recording uses scrcpy and requires Android 11 or newer.');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function waitForManualStop(recordingCompletion, duration) {
|
|
275
|
+
if (!process.stdin.isTTY) {
|
|
276
|
+
throw new Error('Manual interaction requires an interactive terminal so Enter can stop recording.');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const finish = (reason) => {
|
|
281
|
+
if (timeout) clearTimeout(timeout);
|
|
282
|
+
process.stdin.removeListener('data', onInput);
|
|
283
|
+
process.stdin.pause();
|
|
284
|
+
resolve(reason);
|
|
285
|
+
};
|
|
286
|
+
const onInput = (input) => {
|
|
287
|
+
if (/\r|\n/.test(String(input))) finish('enter');
|
|
288
|
+
};
|
|
289
|
+
const timeout = duration > 0
|
|
290
|
+
? setTimeout(() => finish('maximum-duration'), duration * 1000)
|
|
291
|
+
: null;
|
|
292
|
+
|
|
293
|
+
process.stdin.resume();
|
|
294
|
+
process.stdin.on('data', onInput);
|
|
295
|
+
recordingCompletion.then(
|
|
296
|
+
() => finish('recorder-completed'),
|
|
297
|
+
(error) => {
|
|
298
|
+
if (timeout) clearTimeout(timeout);
|
|
299
|
+
process.stdin.removeListener('data', onInput);
|
|
300
|
+
process.stdin.pause();
|
|
301
|
+
reject(error);
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getScrcpyCommand() {
|
|
308
|
+
if (commandExists('scrcpy')) return 'scrcpy';
|
|
309
|
+
return fs.existsSync(scrcpyFallbackPath) ? scrcpyFallbackPath : '';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function recordConnectedDevice(options, destinationPath) {
|
|
313
|
+
const { audio, countdown, duration } = options;
|
|
314
|
+
const deviceId = getConnectedDevice();
|
|
315
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
316
|
+
if (fs.existsSync(destinationPath)) {
|
|
317
|
+
fs.unlinkSync(destinationPath);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(`Connected device: ${deviceId}`);
|
|
321
|
+
console.log(`Interaction: manual; audio: ${audio}.`);
|
|
322
|
+
console.log('Open the required first app screen, then operate the physical device while recording.');
|
|
323
|
+
|
|
324
|
+
for (let seconds = countdown; seconds > 0; seconds -= 1) {
|
|
325
|
+
console.log(`Recording starts in ${seconds}...`);
|
|
326
|
+
wait(1000);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log(
|
|
330
|
+
duration > 0
|
|
331
|
+
? `RECORDING NOW — maximum ${duration} seconds.`
|
|
332
|
+
: 'RECORDING NOW — unlimited duration; press Enter to stop.',
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
let recording;
|
|
336
|
+
if (audio === 'with') {
|
|
337
|
+
const scrcpyCommand = getScrcpyCommand();
|
|
338
|
+
if (!scrcpyCommand) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
'Audio recording requires scrcpy. Install it at W:\\Tools\\scrcpy or add it to PATH.',
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const scrcpyArguments = [
|
|
345
|
+
'--serial', deviceId,
|
|
346
|
+
'--no-window',
|
|
347
|
+
'--no-playback',
|
|
348
|
+
'--no-control',
|
|
349
|
+
'--require-audio',
|
|
350
|
+
'--record', destinationPath,
|
|
351
|
+
];
|
|
352
|
+
if (duration > 0) {
|
|
353
|
+
scrcpyArguments.push('--time-limit', String(duration));
|
|
354
|
+
}
|
|
355
|
+
recording = runAsync(scrcpyCommand, scrcpyArguments);
|
|
356
|
+
} else {
|
|
357
|
+
recording = runAsync('adb', [
|
|
358
|
+
'-s', deviceId,
|
|
359
|
+
'shell', 'screenrecord',
|
|
360
|
+
'--bit-rate', '12000000',
|
|
361
|
+
'--time-limit', String(duration),
|
|
362
|
+
deviceRecordingPath,
|
|
363
|
+
]);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let manualStopRequested = false;
|
|
367
|
+
console.log('MANUAL RECORDING — use the device, then press Enter here to stop and convert.');
|
|
368
|
+
const stopReason = await waitForManualStop(recording.completion, duration);
|
|
369
|
+
|
|
370
|
+
if (stopReason === 'enter') {
|
|
371
|
+
manualStopRequested = true;
|
|
372
|
+
console.log('Enter pressed — stopping and finalizing the recording...');
|
|
373
|
+
if (audio === 'with') {
|
|
374
|
+
run('adb', [
|
|
375
|
+
'-s', deviceId,
|
|
376
|
+
'shell', 'pkill', '-TERM', '-f', 'com.genymobile.scrcpy.Server',
|
|
377
|
+
]);
|
|
378
|
+
} else {
|
|
379
|
+
run('adb', ['-s', deviceId, 'shell', 'pkill', '-INT', 'screenrecord']);
|
|
380
|
+
}
|
|
381
|
+
} else if (stopReason === 'maximum-duration') {
|
|
382
|
+
console.log(`Safety maximum reached after ${duration} seconds.`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await recording.completion;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (!manualStopRequested) throw error;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (audio === 'without') {
|
|
392
|
+
try {
|
|
393
|
+
run('adb', ['-s', deviceId, 'pull', deviceRecordingPath, destinationPath], { inherit: true });
|
|
394
|
+
} finally {
|
|
395
|
+
try {
|
|
396
|
+
run('adb', ['-s', deviceId, 'shell', 'rm', deviceRecordingPath]);
|
|
397
|
+
} catch {
|
|
398
|
+
// The device may have disconnected after recording.
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log('RECORDING FINISHED — converting the captured video.');
|
|
404
|
+
console.log(`Recorded connected device ${deviceId}: ${destinationPath}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getUniqueOutputPath(profile, name) {
|
|
408
|
+
const profileDirectory = path.join(outputDirectory, profile.directory);
|
|
409
|
+
fs.mkdirSync(profileDirectory, { recursive: true });
|
|
410
|
+
|
|
411
|
+
let outputPath = path.join(profileDirectory, `${name}-${profile.fileSuffix}.mp4`);
|
|
412
|
+
let duplicateNumber = 2;
|
|
413
|
+
|
|
414
|
+
while (fs.existsSync(outputPath)) {
|
|
415
|
+
outputPath = path.join(
|
|
416
|
+
profileDirectory,
|
|
417
|
+
`${name}-${profile.fileSuffix}-${duplicateNumber}.mp4`,
|
|
418
|
+
);
|
|
419
|
+
duplicateNumber += 1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return outputPath;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function getVideoDetails(ffprobeCommand, videoPath) {
|
|
426
|
+
const output = run(ffprobeCommand, [
|
|
427
|
+
'-v', 'error',
|
|
428
|
+
'-show_entries', 'stream=width,height,codec_name,codec_type,bit_rate',
|
|
429
|
+
'-show_entries', 'format=duration,size,bit_rate',
|
|
430
|
+
'-of', 'json',
|
|
431
|
+
videoPath,
|
|
432
|
+
]);
|
|
433
|
+
return JSON.parse(output);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function validateVideo(ffprobeCommand, videoPath, profile) {
|
|
437
|
+
const details = getVideoDetails(ffprobeCommand, videoPath);
|
|
438
|
+
const stream = details.streams?.find((candidate) => candidate.codec_type === 'video') || {};
|
|
439
|
+
const format = details.format || {};
|
|
440
|
+
const bitRate = Number(stream.bit_rate || format.bit_rate || 0);
|
|
441
|
+
|
|
442
|
+
if (stream.codec_name !== 'h264') {
|
|
443
|
+
throw new Error(`Validation failed for ${videoPath}: expected H.264 video.`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (Number(stream.width) !== profile.width || Number(stream.height) !== profile.height) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Validation failed for ${videoPath}: expected ${profile.width}x${profile.height}.`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (profile === videoProfiles.amazon && bitRate < 1200000) {
|
|
453
|
+
throw new Error(`Amazon video bitrate is below 1200 kbps: ${Math.round(bitRate / 1000)} kbps.`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
console.log(
|
|
457
|
+
`Validated ${profile.label}: ${stream.width}x${stream.height}, H.264, `
|
|
458
|
+
+ `${(Number(format.duration) || 0).toFixed(1)}s, ${Math.round(bitRate / 1000)} kbps.`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function createVideoVariant(
|
|
463
|
+
ffmpegCommand,
|
|
464
|
+
ffprobeCommand,
|
|
465
|
+
inputPath,
|
|
466
|
+
profile,
|
|
467
|
+
name,
|
|
468
|
+
requireAudio,
|
|
469
|
+
) {
|
|
470
|
+
const destinationPath = getUniqueOutputPath(profile, name);
|
|
471
|
+
const videoFilter = [
|
|
472
|
+
'setparams=colorspace=bt709:color_primaries=bt709:color_trc=bt709:range=tv',
|
|
473
|
+
`scale=${profile.width}:${profile.height}:force_original_aspect_ratio=decrease`,
|
|
474
|
+
`pad=${profile.width}:${profile.height}:(ow-iw)/2:(oh-ih)/2:color=black`,
|
|
475
|
+
'setsar=1',
|
|
476
|
+
].join(',');
|
|
477
|
+
|
|
478
|
+
run(ffmpegCommand, [
|
|
479
|
+
'-hide_banner',
|
|
480
|
+
'-y',
|
|
481
|
+
'-i', inputPath,
|
|
482
|
+
'-map', '0:v:0',
|
|
483
|
+
'-map', '0:a?',
|
|
484
|
+
'-vf', videoFilter,
|
|
485
|
+
'-c:v', 'libx264',
|
|
486
|
+
'-preset', 'medium',
|
|
487
|
+
'-profile:v', 'high',
|
|
488
|
+
'-level', profile.h264Level,
|
|
489
|
+
'-pix_fmt', 'yuv420p',
|
|
490
|
+
'-b:v', profile.videoBitrate,
|
|
491
|
+
'-minrate', profile.videoBitrate,
|
|
492
|
+
'-maxrate', profile.videoBitrate,
|
|
493
|
+
'-bufsize', profile.videoBitrate,
|
|
494
|
+
'-x264-params', 'nal-hrd=cbr:force-cfr=1',
|
|
495
|
+
'-r', '30',
|
|
496
|
+
'-c:a', 'aac',
|
|
497
|
+
'-b:a', '192k',
|
|
498
|
+
'-movflags', '+faststart',
|
|
499
|
+
destinationPath,
|
|
500
|
+
], { inherit: true });
|
|
501
|
+
|
|
502
|
+
console.log(`Created ${profile.label}: ${destinationPath}`);
|
|
503
|
+
validateVideo(ffprobeCommand, destinationPath, profile);
|
|
504
|
+
|
|
505
|
+
if (requireAudio) {
|
|
506
|
+
const details = getVideoDetails(ffprobeCommand, destinationPath);
|
|
507
|
+
const audioStream = details.streams?.find((stream) => stream.codec_type === 'audio');
|
|
508
|
+
if (!audioStream) {
|
|
509
|
+
throw new Error(`Validation failed for ${destinationPath}: audio track is missing.`);
|
|
510
|
+
}
|
|
511
|
+
console.log(`Validated audio: ${audioStream.codec_name} track included.`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function copySpatialSource(spatialInput, name) {
|
|
516
|
+
if (!spatialInput) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
'Spatial XR requires --spatial-input with a genuine 360-degree, 180-degree, or 3D video.',
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const sourcePath = path.resolve(spatialInput);
|
|
523
|
+
if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
|
|
524
|
+
throw new Error(`Spatial XR source was not found: ${sourcePath}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const spatialDirectory = path.join(outputDirectory, 'Google-Play-XR-Spatial');
|
|
528
|
+
fs.mkdirSync(spatialDirectory, { recursive: true });
|
|
529
|
+
const extension = path.extname(sourcePath) || '.mp4';
|
|
530
|
+
let destinationPath = path.join(spatialDirectory, `${name}-spatial-xr${extension}`);
|
|
531
|
+
let duplicateNumber = 2;
|
|
532
|
+
|
|
533
|
+
while (fs.existsSync(destinationPath)) {
|
|
534
|
+
destinationPath = path.join(
|
|
535
|
+
spatialDirectory,
|
|
536
|
+
`${name}-spatial-xr-${duplicateNumber}${extension}`,
|
|
537
|
+
);
|
|
538
|
+
duplicateNumber += 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fs.copyFileSync(sourcePath, destinationPath, fs.constants.COPYFILE_EXCL);
|
|
542
|
+
console.log(`Copied genuine Spatial XR source without stripping its metadata: ${destinationPath}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function main() {
|
|
546
|
+
const options = parseArguments();
|
|
547
|
+
|
|
548
|
+
if (options.help) {
|
|
549
|
+
printHelp();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (options.list) {
|
|
554
|
+
printProfiles();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const selectedProfiles = selectProfiles(options.profiles);
|
|
559
|
+
const flatProfiles = selectedProfiles.filter((name) => name !== 'spatial-xr');
|
|
560
|
+
|
|
561
|
+
if (selectedProfiles.includes('spatial-xr')) {
|
|
562
|
+
copySpatialSource(options.spatialInput, options.name);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!flatProfiles.length) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const ffmpegCommand = getExecutable('ffmpeg');
|
|
570
|
+
const ffprobeCommand = getExecutable('ffprobe');
|
|
571
|
+
|
|
572
|
+
if (!ffmpegCommand || !ffprobeCommand) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
'FFmpeg was not found in PATH. Install FFmpeg, reopen the terminal, and run this script again.',
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let inputPath;
|
|
579
|
+
if (options.input) {
|
|
580
|
+
inputPath = path.resolve(options.input);
|
|
581
|
+
if (!fs.existsSync(inputPath) || !fs.statSync(inputPath).isFile()) {
|
|
582
|
+
throw new Error(`Input video was not found: ${inputPath}`);
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
inputPath = path.join(temporaryDirectory, `${options.name}-adb-source.mp4`);
|
|
586
|
+
await recordConnectedDevice(options, inputPath);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
for (const profileName of flatProfiles) {
|
|
590
|
+
createVideoVariant(
|
|
591
|
+
ffmpegCommand,
|
|
592
|
+
ffprobeCommand,
|
|
593
|
+
inputPath,
|
|
594
|
+
videoProfiles[profileName],
|
|
595
|
+
options.name,
|
|
596
|
+
options.audio === 'with',
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
main().catch((error) => {
|
|
602
|
+
console.error(`Store video creation failed: ${error.message}`);
|
|
603
|
+
process.exitCode = 1;
|
|
604
|
+
});
|
package/package.json
CHANGED
package/files/take-screenshot.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const readline = require('node:readline');
|
|
4
|
-
const { spawnSync } = require('node:child_process');
|
|
5
|
-
|
|
6
|
-
const screenshotDirectory = path.join(__dirname, 'Auto-Screenshot');
|
|
7
|
-
|
|
8
|
-
function getNextNumberedPath() {
|
|
9
|
-
const highestNumber = fs.readdirSync(screenshotDirectory, { withFileTypes: true })
|
|
10
|
-
.filter((entry) => entry.isFile())
|
|
11
|
-
.reduce((highest, entry) => {
|
|
12
|
-
const match = entry.name.match(/^(\d+)\.png$/i);
|
|
13
|
-
return match ? Math.max(highest, Number(match[1])) : highest;
|
|
14
|
-
}, 0);
|
|
15
|
-
|
|
16
|
-
return path.join(screenshotDirectory, `${highestNumber + 1}.png`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function getCustomPath(input) {
|
|
20
|
-
let fileName = input.trim().replace(/\.png$/i, '');
|
|
21
|
-
fileName = fileName
|
|
22
|
-
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_')
|
|
23
|
-
.replace(/[. ]+$/g, '');
|
|
24
|
-
|
|
25
|
-
if (!fileName) {
|
|
26
|
-
return getNextNumberedPath();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(fileName)) {
|
|
30
|
-
fileName = `${fileName}_`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let screenshotPath = path.join(screenshotDirectory, `${fileName}.png`);
|
|
34
|
-
let duplicateNumber = 2;
|
|
35
|
-
|
|
36
|
-
while (fs.existsSync(screenshotPath)) {
|
|
37
|
-
screenshotPath = path.join(
|
|
38
|
-
screenshotDirectory,
|
|
39
|
-
`${fileName}-${duplicateNumber}.png`,
|
|
40
|
-
);
|
|
41
|
-
duplicateNumber += 1;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return screenshotPath;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function takeScreenshot(fileNameInput) {
|
|
48
|
-
const result = spawnSync('adb', ['exec-out', 'screencap', '-p'], {
|
|
49
|
-
encoding: null,
|
|
50
|
-
maxBuffer: 100 * 1024 * 1024,
|
|
51
|
-
timeout: 30000,
|
|
52
|
-
windowsHide: true,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
if (result.error) {
|
|
56
|
-
if (result.error.code === 'ENOENT') {
|
|
57
|
-
throw new Error('ADB was not found. Add Android platform-tools to your PATH.');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (result.error.code === 'ETIMEDOUT') {
|
|
61
|
-
throw new Error('ADB did not respond within 30 seconds. Check the device connection.');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
throw result.error;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (result.status !== 0) {
|
|
68
|
-
const adbMessage = result.stderr.toString().trim();
|
|
69
|
-
throw new Error(adbMessage || `ADB exited with code ${result.status}.`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!result.stdout.length) {
|
|
73
|
-
throw new Error('ADB returned an empty screenshot. Check the connected device.');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const screenshotPath = fileNameInput.trim()
|
|
77
|
-
? getCustomPath(fileNameInput)
|
|
78
|
-
: getNextNumberedPath();
|
|
79
|
-
|
|
80
|
-
fs.writeFileSync(screenshotPath, result.stdout, { flag: 'wx' });
|
|
81
|
-
return screenshotPath;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
fs.mkdirSync(screenshotDirectory, { recursive: true });
|
|
85
|
-
|
|
86
|
-
const terminal = readline.createInterface({
|
|
87
|
-
input: process.stdin,
|
|
88
|
-
output: process.stdout,
|
|
89
|
-
prompt: 'Enter a filename, or press Enter for the next number: ',
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
console.log(`Screenshots will be saved in: ${screenshotDirectory}`);
|
|
93
|
-
console.log('Press Ctrl+C to exit.');
|
|
94
|
-
terminal.prompt();
|
|
95
|
-
|
|
96
|
-
terminal.on('line', (fileNameInput) => {
|
|
97
|
-
try {
|
|
98
|
-
const screenshotPath = takeScreenshot(fileNameInput);
|
|
99
|
-
console.log(`Screenshot saved: ${screenshotPath}`);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
console.error(`Screenshot failed: ${error.message}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
terminal.prompt();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
terminal.on('SIGINT', () => {
|
|
108
|
-
console.log('\nScreenshot tool closed.');
|
|
109
|
-
terminal.close();
|
|
110
|
-
});
|