@playdrop/playdrop-cli 0.11.9 → 0.12.0
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/config/client-meta.json +2 -2
- package/dist/apps/build.js +2 -3
- package/dist/apps/index.d.ts +4 -5
- package/dist/apps/index.js +14 -16
- package/dist/apps/loadCheck.d.ts +76 -0
- package/dist/apps/loadCheck.js +874 -0
- package/dist/apps/upload.d.ts +3 -2
- package/dist/apps/upload.js +110 -6
- package/dist/catalogue-utils.d.ts +1 -0
- package/dist/catalogue-utils.js +1 -0
- package/dist/catalogue.d.ts +2 -0
- package/dist/catalogue.js +17 -0
- package/dist/commands/build.js +4 -1
- package/dist/commands/captureListing.d.ts +30 -1
- package/dist/commands/captureListing.js +305 -120
- package/dist/commands/check.d.ts +8 -0
- package/dist/commands/check.js +188 -0
- package/dist/commands/review.d.ts +20 -0
- package/dist/commands/review.js +112 -0
- package/dist/commands/upload-content.d.ts +1 -1
- package/dist/commands/upload.d.ts +2 -1
- package/dist/commands/upload.js +9 -52
- package/dist/commands/validate.js +0 -155
- package/dist/commands/worker/runtime.d.ts +1 -0
- package/dist/commands/worker/runtime.js +43 -3
- package/dist/commands/worker.d.ts +1 -1
- package/dist/commands/worker.js +22 -44
- package/dist/index.js +33 -49
- package/dist/playwright.d.ts +5 -0
- package/dist/playwright.js +60 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts +4 -2
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/client.js +0 -10
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
- package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/auth.js +22 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +4 -3
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +10 -5
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/config/dist/src/client-meta.d.ts +8 -0
- package/node_modules/@playdrop/config/dist/src/client-meta.d.ts.map +1 -0
- package/node_modules/@playdrop/config/dist/src/client-meta.js +51 -0
- package/node_modules/@playdrop/config/dist/src/deployment.d.ts.map +1 -1
- package/node_modules/@playdrop/config/dist/src/deployment.js +2 -32
- package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
- package/node_modules/@playdrop/config/dist/src/index.js +2 -28
- package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts +0 -1
- package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts.map +1 -1
- package/node_modules/@playdrop/config/dist/src/runtime-service-origins.js +21 -5
- package/node_modules/@playdrop/config/dist/test/runtime-service-origins.test.js +8 -0
- package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +19 -14
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +5 -10
- package/node_modules/@playdrop/types/dist/version.d.ts +14 -0
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/apps/launchCheck.d.ts +0 -39
- package/dist/apps/launchCheck.js +0 -414
- package/dist/captureRuntime.d.ts +0 -73
- package/dist/captureRuntime.js +0 -1021
- package/dist/commands/capture.d.ts +0 -14
- package/dist/commands/capture.js +0 -548
- package/dist/commands/captureRemote.d.ts +0 -17
- package/dist/commands/captureRemote.js +0 -394
- package/node_modules/@playdrop/types/dist/entity.d.ts +0 -31
- package/node_modules/@playdrop/types/dist/entity.d.ts.map +0 -1
- package/node_modules/@playdrop/types/dist/entity.js +0 -67
|
@@ -34,22 +34,32 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.resolveListingCaptureRouteSegment = resolveListingCaptureRouteSegment;
|
|
37
|
+
exports.resolveCaptureListingDevRouterPort = resolveCaptureListingDevRouterPort;
|
|
38
|
+
exports.resolveListingCaptureLocalDevPort = resolveListingCaptureLocalDevPort;
|
|
39
|
+
exports.buildListingCaptureFrameUrl = buildListingCaptureFrameUrl;
|
|
37
40
|
exports.resolveListingCaptureSceneId = resolveListingCaptureSceneId;
|
|
38
41
|
exports.parseCaptureListingOptions = parseCaptureListingOptions;
|
|
39
42
|
exports.assertSupportedListingEnvironment = assertSupportedListingEnvironment;
|
|
40
43
|
exports.resolveListingRecorderPath = resolveListingRecorderPath;
|
|
44
|
+
exports.buildSurfaceOutputPaths = buildSurfaceOutputPaths;
|
|
45
|
+
exports.resolveCaptureDimensions = resolveCaptureDimensions;
|
|
46
|
+
exports.resolveListingCaptureBrowserContextOptions = resolveListingCaptureBrowserContextOptions;
|
|
41
47
|
exports.computeRecordedCrop = computeRecordedCrop;
|
|
42
48
|
exports.captureListing = captureListing;
|
|
43
49
|
const types_1 = require("@playdrop/types");
|
|
44
50
|
const node_child_process_1 = require("node:child_process");
|
|
51
|
+
const node_crypto_1 = require("node:crypto");
|
|
45
52
|
const promises_1 = require("node:fs/promises");
|
|
46
53
|
const node_path_1 = require("node:path");
|
|
47
54
|
const appUrls_1 = require("../appUrls");
|
|
55
|
+
const catalogue_1 = require("../catalogue");
|
|
48
56
|
const catalogue_utils_1 = require("../catalogue-utils");
|
|
49
57
|
const commandContext_1 = require("../commandContext");
|
|
50
58
|
const http_1 = require("../http");
|
|
51
59
|
const messages_1 = require("../messages");
|
|
52
60
|
const playwright_1 = require("../playwright");
|
|
61
|
+
const dev_1 = require("./dev");
|
|
62
|
+
const devRuntimeAssets_1 = require("./devRuntimeAssets");
|
|
53
63
|
const devServer_1 = require("./devServer");
|
|
54
64
|
const devShared_1 = require("./devShared");
|
|
55
65
|
const CAPTURE_FRAME_SELECTOR = 'iframe[title="Game"]';
|
|
@@ -63,10 +73,43 @@ const MIN_CAPTURE_SECONDS = 1;
|
|
|
63
73
|
const MIN_DIMENSION = 16;
|
|
64
74
|
const MAX_DIMENSION = 8192;
|
|
65
75
|
const MAX_FPS = 120;
|
|
66
|
-
const
|
|
76
|
+
const MIN_SUPPORTED_MACOS_MAJOR = 26;
|
|
77
|
+
const MIN_SUPPORTED_MACOS_MINOR = 4;
|
|
67
78
|
const LISTING_CAPTURE_HOOK_TIMEOUT_MS = 60000;
|
|
79
|
+
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
|
|
80
|
+
const LISTING_CAPTURE_MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
|
|
81
|
+
const SURFACE_CAPTURE_DIMENSIONS = {
|
|
82
|
+
DESKTOP: { width: 1280, height: 720 },
|
|
83
|
+
MOBILE_LANDSCAPE: { width: 844, height: 390 },
|
|
84
|
+
MOBILE_PORTRAIT: { width: 390, height: 844 },
|
|
85
|
+
};
|
|
86
|
+
const SURFACE_OUTPUT_SLUG = {
|
|
87
|
+
DESKTOP: 'desktop',
|
|
88
|
+
MOBILE_LANDSCAPE: 'mobile-landscape',
|
|
89
|
+
MOBILE_PORTRAIT: 'mobile-portrait',
|
|
90
|
+
};
|
|
68
91
|
function resolveListingCaptureRouteSegment(previewable) {
|
|
69
|
-
|
|
92
|
+
if (!previewable) {
|
|
93
|
+
throw new Error('listing_preview_support_missing');
|
|
94
|
+
}
|
|
95
|
+
return 'dev-preview';
|
|
96
|
+
}
|
|
97
|
+
function resolveCaptureListingDevRouterPort(env = process.env) {
|
|
98
|
+
return (0, dev_1.resolveCliDevRouterPort)(env);
|
|
99
|
+
}
|
|
100
|
+
function resolveListingCaptureLocalDevPort(devRouterPort) {
|
|
101
|
+
return devRouterPort === devServer_1.DEV_ROUTER_PORT ? null : devRouterPort;
|
|
102
|
+
}
|
|
103
|
+
function buildListingCaptureFrameUrl({ webBase, currentUsername, appTypeSlug, appName, routeSegment, localDevPort, }) {
|
|
104
|
+
const frameUrlObject = new URL(`${(0, appUrls_1.normalizePlaydropWebBase)(webBase)}/creators/${encodeURIComponent(currentUsername)}/apps/${appTypeSlug}/${encodeURIComponent(appName)}/${routeSegment}`);
|
|
105
|
+
frameUrlObject.searchParams.set('devAuth', 'anonymous');
|
|
106
|
+
frameUrlObject.searchParams.set('launchCheck', '1');
|
|
107
|
+
frameUrlObject.searchParams.set('captureShell', 'listing');
|
|
108
|
+
frameUrlObject.searchParams.set('mobileFrameHost', 'admin');
|
|
109
|
+
if (localDevPort !== null) {
|
|
110
|
+
frameUrlObject.searchParams.set('localDevPort', String(localDevPort));
|
|
111
|
+
}
|
|
112
|
+
return frameUrlObject.toString();
|
|
70
113
|
}
|
|
71
114
|
function resolveListingCaptureSceneId(width, height) {
|
|
72
115
|
return height > width ? 'listing-portrait' : 'listing-landscape';
|
|
@@ -92,6 +135,7 @@ function parseCaptureListingOptions(targetArg, options = {}) {
|
|
|
92
135
|
minimum: MIN_CAPTURE_SECONDS,
|
|
93
136
|
maximum: MAX_CAPTURE_SECONDS,
|
|
94
137
|
});
|
|
138
|
+
const explicitDimensions = options.width !== undefined || options.height !== undefined;
|
|
95
139
|
const width = parsePositiveNumber(options.width, DEFAULT_WIDTH, 'width', {
|
|
96
140
|
minimum: MIN_DIMENSION,
|
|
97
141
|
maximum: MAX_DIMENSION,
|
|
@@ -112,6 +156,12 @@ function parseCaptureListingOptions(targetArg, options = {}) {
|
|
|
112
156
|
maximum: durationSeconds,
|
|
113
157
|
});
|
|
114
158
|
const outputDir = options.outputDir?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.outputDir.trim()) : null;
|
|
159
|
+
const contentHash = typeof options.contentHash === 'string' && options.contentHash.trim().length > 0
|
|
160
|
+
? options.contentHash.trim().toLowerCase()
|
|
161
|
+
: null;
|
|
162
|
+
if (contentHash && !SHA256_HEX_REGEX.test(contentHash)) {
|
|
163
|
+
throw new Error('content_hash_invalid');
|
|
164
|
+
}
|
|
115
165
|
return {
|
|
116
166
|
targetArg,
|
|
117
167
|
appName: options.app?.trim() || undefined,
|
|
@@ -123,6 +173,8 @@ function parseCaptureListingOptions(targetArg, options = {}) {
|
|
|
123
173
|
audio: Boolean(options.audio),
|
|
124
174
|
outputDir,
|
|
125
175
|
keepRaw: Boolean(options.keepRaw),
|
|
176
|
+
contentHash,
|
|
177
|
+
explicitDimensions,
|
|
126
178
|
};
|
|
127
179
|
}
|
|
128
180
|
function readMacOsVersion() {
|
|
@@ -138,7 +190,13 @@ function assertSupportedListingEnvironment(platform = process.platform, macosVer
|
|
|
138
190
|
if (platform !== 'darwin') {
|
|
139
191
|
throw new Error('unsupported_platform');
|
|
140
192
|
}
|
|
141
|
-
|
|
193
|
+
const [majorRaw, minorRaw] = macosVersion.split('.');
|
|
194
|
+
const major = Number.parseInt(majorRaw ?? '', 10);
|
|
195
|
+
const minor = Number.parseInt(minorRaw ?? '', 10);
|
|
196
|
+
if (!Number.isInteger(major)
|
|
197
|
+
|| !Number.isInteger(minor)
|
|
198
|
+
|| major !== MIN_SUPPORTED_MACOS_MAJOR
|
|
199
|
+
|| minor < MIN_SUPPORTED_MACOS_MINOR) {
|
|
142
200
|
throw new Error(`unsupported_macos_version:${macosVersion}`);
|
|
143
201
|
}
|
|
144
202
|
}
|
|
@@ -165,11 +223,40 @@ async function ensureOutputPaths(appName, explicitOutputDir) {
|
|
|
165
223
|
await (0, promises_1.mkdir)(outputDir, { recursive: true });
|
|
166
224
|
return {
|
|
167
225
|
outputDir,
|
|
168
|
-
rawVideoPath: (0, node_path_1.join)(outputDir, 'listing-raw.mp4'),
|
|
169
|
-
metadataPath: (0, node_path_1.join)(outputDir, 'listing-raw.json'),
|
|
170
226
|
reportPath: (0, node_path_1.join)(outputDir, 'capture-report.json'),
|
|
171
|
-
|
|
172
|
-
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function buildSurfaceOutputPaths(outputDir, surface, useSurfacePrefix) {
|
|
230
|
+
const prefix = useSurfacePrefix ? `${SURFACE_OUTPUT_SLUG[surface]}-` : '';
|
|
231
|
+
return {
|
|
232
|
+
rawVideoPath: (0, node_path_1.join)(outputDir, `${prefix}listing-raw.mp4`),
|
|
233
|
+
metadataPath: (0, node_path_1.join)(outputDir, `${prefix}listing-raw.json`),
|
|
234
|
+
posterPath: (0, node_path_1.join)(outputDir, `${prefix}poster.png`),
|
|
235
|
+
finalVideoPath: (0, node_path_1.join)(outputDir, `${prefix}listing.mp4`),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function resolveCaptureDimensions(surface, parsedOptions) {
|
|
239
|
+
if (parsedOptions.explicitDimensions) {
|
|
240
|
+
return {
|
|
241
|
+
width: parsedOptions.width,
|
|
242
|
+
height: parsedOptions.height,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return SURFACE_CAPTURE_DIMENSIONS[surface];
|
|
246
|
+
}
|
|
247
|
+
function resolveListingCaptureBrowserContextOptions(surface, dimensions) {
|
|
248
|
+
if (surface === 'DESKTOP') {
|
|
249
|
+
return { viewport: null };
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
viewport: {
|
|
253
|
+
width: dimensions.width,
|
|
254
|
+
height: dimensions.height,
|
|
255
|
+
},
|
|
256
|
+
deviceScaleFactor: 2,
|
|
257
|
+
isMobile: true,
|
|
258
|
+
hasTouch: true,
|
|
259
|
+
userAgent: LISTING_CAPTURE_MOBILE_USER_AGENT,
|
|
173
260
|
};
|
|
174
261
|
}
|
|
175
262
|
async function grantListingBrowserLocalNetworkAccess(context, origin) {
|
|
@@ -179,7 +266,7 @@ async function grantListingBrowserLocalNetworkAccess(context, origin) {
|
|
|
179
266
|
}
|
|
180
267
|
await grantPermissions.call(context, ['local-network-access'], { origin });
|
|
181
268
|
}
|
|
182
|
-
async function launchListingBrowser(initialViewport, automationOrigin) {
|
|
269
|
+
async function launchListingBrowser(initialViewport, automationOrigin, contextOptions) {
|
|
183
270
|
const playwright = await Promise.resolve().then(() => __importStar(require('playwright-core')));
|
|
184
271
|
const args = [
|
|
185
272
|
'--no-sandbox',
|
|
@@ -206,7 +293,7 @@ async function launchListingBrowser(initialViewport, automationOrigin) {
|
|
|
206
293
|
throw new Error('browser_process_missing');
|
|
207
294
|
}
|
|
208
295
|
const browser = await playwright.chromium.connect(server.wsEndpoint());
|
|
209
|
-
const context = await browser.newContext(
|
|
296
|
+
const context = await browser.newContext(contextOptions);
|
|
210
297
|
await grantListingBrowserLocalNetworkAccess(context, automationOrigin);
|
|
211
298
|
const page = await context.newPage();
|
|
212
299
|
return {
|
|
@@ -470,6 +557,9 @@ async function probeMediaFile(filePath) {
|
|
|
470
557
|
], 'ffprobe_failed');
|
|
471
558
|
return JSON.parse(stdout);
|
|
472
559
|
}
|
|
560
|
+
async function computeFileSha256(filePath) {
|
|
561
|
+
return (0, node_crypto_1.createHash)('sha256').update(await (0, promises_1.readFile)(filePath)).digest('hex');
|
|
562
|
+
}
|
|
473
563
|
function parseFrameRate(raw) {
|
|
474
564
|
if (!raw) {
|
|
475
565
|
return null;
|
|
@@ -532,14 +622,14 @@ async function runListingRecorder(recorderPath, pid, durationSeconds, rawOutputP
|
|
|
532
622
|
function formatCommandError(error) {
|
|
533
623
|
if (error.message === 'unsupported_platform') {
|
|
534
624
|
return {
|
|
535
|
-
message: '
|
|
625
|
+
message: 'Game media capture only runs on macOS.',
|
|
536
626
|
suggestions: [],
|
|
537
627
|
};
|
|
538
628
|
}
|
|
539
629
|
if (error.message.startsWith('unsupported_macos_version:')) {
|
|
540
630
|
const version = error.message.split(':')[1] || 'unknown';
|
|
541
631
|
return {
|
|
542
|
-
message: `
|
|
632
|
+
message: `Game media capture requires macOS ${MIN_SUPPORTED_MACOS_MAJOR}.${MIN_SUPPORTED_MACOS_MINOR} or newer on this machine. Current version: ${version}.`,
|
|
543
633
|
suggestions: [],
|
|
544
634
|
};
|
|
545
635
|
}
|
|
@@ -549,6 +639,18 @@ function formatCommandError(error) {
|
|
|
549
639
|
suggestions: [],
|
|
550
640
|
};
|
|
551
641
|
}
|
|
642
|
+
if (error.message === 'content_hash_invalid') {
|
|
643
|
+
return {
|
|
644
|
+
message: 'The --content-hash value must be a 64-character SHA-256 hex string.',
|
|
645
|
+
suggestions: [],
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (error.message === 'listing_preview_support_missing') {
|
|
649
|
+
return {
|
|
650
|
+
message: 'This game does not declare preview support, so listing capture cannot run.',
|
|
651
|
+
suggestions: ['Set previewable: true in catalogue.json, implement the preview scene and __listingCapture hooks, then rerun "playdrop project capture".'],
|
|
652
|
+
};
|
|
653
|
+
}
|
|
552
654
|
if (error.message.startsWith('listing_recorder_failed:')) {
|
|
553
655
|
return {
|
|
554
656
|
message: error.message.slice('listing_recorder_failed:'.length),
|
|
@@ -628,7 +730,7 @@ function ensureCaptureListingToolAvailable(command) {
|
|
|
628
730
|
return true;
|
|
629
731
|
}
|
|
630
732
|
(0, messages_1.printErrorWithHelp)(`${command} is required for listing capture.`, [`Install ${command} and make sure it is available on PATH.`], {
|
|
631
|
-
command: 'project capture
|
|
733
|
+
command: 'project capture',
|
|
632
734
|
});
|
|
633
735
|
process.exitCode = 1;
|
|
634
736
|
return false;
|
|
@@ -639,7 +741,7 @@ async function ensureListingRecorderAvailable(recorderPath) {
|
|
|
639
741
|
return true;
|
|
640
742
|
}
|
|
641
743
|
catch {
|
|
642
|
-
(0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Build it with: (cd clients/apple/playdrop-listing-recorder && swift build -c release)'], { command: 'project capture
|
|
744
|
+
(0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Build it with: (cd clients/apple/playdrop-listing-recorder && swift build -c release)'], { command: 'project capture' });
|
|
643
745
|
process.exitCode = 1;
|
|
644
746
|
return false;
|
|
645
747
|
}
|
|
@@ -656,20 +758,22 @@ function resolveCaptureListingTarget(parsedOptions) {
|
|
|
656
758
|
: code === 'catalogue_missing'
|
|
657
759
|
? ['Run the command inside a Playdrop workspace or pass the direct HTML file path.']
|
|
658
760
|
: ['Provide a valid app target or HTML file path.'];
|
|
659
|
-
(0, messages_1.printErrorWithHelp)(message, suggestions, { command: 'project capture
|
|
761
|
+
(0, messages_1.printErrorWithHelp)(message, suggestions, { command: 'project capture' });
|
|
660
762
|
process.exitCode = 1;
|
|
661
763
|
return null;
|
|
662
764
|
}
|
|
663
765
|
}
|
|
664
766
|
async function captureListing(targetArg, options = {}) {
|
|
665
767
|
let parsedOptions;
|
|
768
|
+
let devRouterPort;
|
|
666
769
|
try {
|
|
667
770
|
parsedOptions = parseCaptureListingOptions(targetArg, options);
|
|
771
|
+
devRouterPort = resolveCaptureListingDevRouterPort();
|
|
668
772
|
assertSupportedListingEnvironment();
|
|
669
773
|
}
|
|
670
774
|
catch (error) {
|
|
671
775
|
const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
|
|
672
|
-
(0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture
|
|
776
|
+
(0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
|
|
673
777
|
process.exitCode = 1;
|
|
674
778
|
return;
|
|
675
779
|
}
|
|
@@ -686,20 +790,30 @@ async function captureListing(targetArg, options = {}) {
|
|
|
686
790
|
}
|
|
687
791
|
let appName = resolvedTarget.appName;
|
|
688
792
|
let appTypeSlug = (0, appUrls_1.getAppTypeSlug)(null);
|
|
793
|
+
let declaresPreviewSupport = false;
|
|
689
794
|
try {
|
|
690
|
-
const match = (0, catalogue_utils_1.
|
|
795
|
+
const match = (0, catalogue_utils_1.findAppCatalogueInfo)(resolvedTarget.htmlPath);
|
|
691
796
|
appName = match.name;
|
|
692
797
|
appTypeSlug = (0, appUrls_1.getAppTypeSlug)(match.type);
|
|
798
|
+
declaresPreviewSupport = match.previewable;
|
|
693
799
|
}
|
|
694
800
|
catch {
|
|
695
|
-
|
|
801
|
+
declaresPreviewSupport = false;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
resolveListingCaptureRouteSegment(declaresPreviewSupport);
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
|
|
808
|
+
(0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
|
|
809
|
+
process.exitCode = 1;
|
|
810
|
+
return;
|
|
696
811
|
}
|
|
697
812
|
const outputPaths = await ensureOutputPaths(appName, parsedOptions.outputDir);
|
|
698
813
|
const projectInfo = (0, devShared_1.findProjectInfo)(resolvedTarget.htmlPath);
|
|
699
814
|
const devScriptAvailable = Boolean(projectInfo.projectDir && projectInfo.packageJson && typeof projectInfo.packageJson.scripts?.dev === 'string');
|
|
700
|
-
await (0, commandContext_1.withEnvironment)('project capture
|
|
815
|
+
await (0, commandContext_1.withEnvironment)('project capture', 'Capturing game media', async ({ client, env, envConfig }) => {
|
|
701
816
|
let currentUsername = '';
|
|
702
|
-
let registeredApp = null;
|
|
703
817
|
try {
|
|
704
818
|
const currentUser = await (0, devShared_1.fetchDevUser)(client);
|
|
705
819
|
currentUsername = currentUser.username.trim();
|
|
@@ -717,36 +831,36 @@ async function captureListing(targetArg, options = {}) {
|
|
|
717
831
|
(0, messages_1.printErrorWithHelp)(`Could not fetch your account (status ${error.status}).`, [
|
|
718
832
|
'Run "playdrop auth login" to refresh your session.',
|
|
719
833
|
'Use "playdrop auth whoami" afterwards to verify the current account.',
|
|
720
|
-
], { command: 'project capture
|
|
834
|
+
], { command: 'project capture' });
|
|
721
835
|
process.exitCode = 1;
|
|
722
836
|
return;
|
|
723
837
|
}
|
|
724
838
|
if ((0, devShared_1.isNetworkError)(error)) {
|
|
725
|
-
(0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to resolve your account.', 'project capture
|
|
839
|
+
(0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to resolve your account.', 'project capture');
|
|
726
840
|
process.exitCode = 1;
|
|
727
841
|
return;
|
|
728
842
|
}
|
|
729
843
|
throw error;
|
|
730
844
|
}
|
|
731
845
|
try {
|
|
732
|
-
|
|
846
|
+
await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
|
|
733
847
|
}
|
|
734
848
|
catch (error) {
|
|
735
849
|
if (error instanceof http_1.CLIUnsupportedClientError) {
|
|
736
850
|
return;
|
|
737
851
|
}
|
|
738
852
|
if (error instanceof types_1.UnsupportedClientError) {
|
|
739
|
-
(0, http_1.handleUnsupportedError)(error, '
|
|
853
|
+
(0, http_1.handleUnsupportedError)(error, 'Game media capture');
|
|
740
854
|
process.exitCode = 1;
|
|
741
855
|
return;
|
|
742
856
|
}
|
|
743
857
|
if (error instanceof types_1.ApiError && error.status === 404) {
|
|
744
|
-
(0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [`Run "playdrop project create app ${appName}" before using listing capture.`], { command: 'project capture
|
|
858
|
+
(0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [`Run "playdrop project create app ${appName}" before using listing capture.`], { command: 'project capture' });
|
|
745
859
|
process.exitCode = 1;
|
|
746
860
|
return;
|
|
747
861
|
}
|
|
748
862
|
if ((0, devShared_1.isNetworkError)(error)) {
|
|
749
|
-
(0, messages_1.printNetworkIssue)('Could not verify the app registration.', 'project capture
|
|
863
|
+
(0, messages_1.printNetworkIssue)('Could not verify the app registration.', 'project capture');
|
|
750
864
|
process.exitCode = 1;
|
|
751
865
|
return;
|
|
752
866
|
}
|
|
@@ -754,11 +868,41 @@ async function captureListing(targetArg, options = {}) {
|
|
|
754
868
|
}
|
|
755
869
|
const entryLabel = (0, node_path_1.relative)(process.cwd(), resolvedTarget.htmlPath) || resolvedTarget.htmlPath;
|
|
756
870
|
console.log(`[listing] Preparing ${entryLabel} for ${env} (${appTypeSlug}).`);
|
|
871
|
+
const taskLookup = (0, catalogue_1.findAppTaskByFile)(resolvedTarget.htmlPath);
|
|
872
|
+
if (taskLookup.errors.length > 0) {
|
|
873
|
+
(0, messages_1.printErrorWithHelp)(taskLookup.errors[0] || 'Failed to resolve the app task from catalogue.json.', taskLookup.errors.slice(1), { command: 'project capture' });
|
|
874
|
+
process.exitCode = 1;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const localDevAppUrl = (0, devServer_1.buildLocalDevAppUrl)({
|
|
878
|
+
creatorUsername: currentUsername,
|
|
879
|
+
appType: appTypeSlug,
|
|
880
|
+
appName,
|
|
881
|
+
port: devRouterPort,
|
|
882
|
+
});
|
|
883
|
+
let runtimeAssetManifest = (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)();
|
|
884
|
+
if (taskLookup.task) {
|
|
885
|
+
try {
|
|
886
|
+
runtimeAssetManifest = await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
|
|
887
|
+
client,
|
|
888
|
+
apiBase: envConfig.apiBase,
|
|
889
|
+
task: taskLookup.task,
|
|
890
|
+
creatorUsername: currentUsername,
|
|
891
|
+
appBaseUrl: new URL('.', localDevAppUrl).toString(),
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
const formatted = (0, dev_1.formatDevRuntimeAssetManifestFailure)(error);
|
|
896
|
+
(0, messages_1.printErrorWithHelp)(formatted.message, formatted.suggestions, { command: 'project capture' });
|
|
897
|
+
process.exitCode = 1;
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
757
901
|
const serverAlreadyRunning = await (0, devServer_1.isDevServerAvailable)({
|
|
758
902
|
creatorUsername: currentUsername,
|
|
759
903
|
appType: appTypeSlug,
|
|
760
904
|
appName,
|
|
761
|
-
port:
|
|
905
|
+
port: devRouterPort,
|
|
762
906
|
}, 750);
|
|
763
907
|
const devServerStartedByCapture = !serverAlreadyRunning;
|
|
764
908
|
let serverHandle = null;
|
|
@@ -778,12 +922,13 @@ async function captureListing(targetArg, options = {}) {
|
|
|
778
922
|
};
|
|
779
923
|
try {
|
|
780
924
|
if (serverAlreadyRunning) {
|
|
781
|
-
|
|
925
|
+
await (0, devServer_1.updateMountedDevRuntimeAssetManifest)({
|
|
782
926
|
creatorUsername: currentUsername,
|
|
783
|
-
appType: appTypeSlug,
|
|
784
927
|
appName,
|
|
785
|
-
|
|
786
|
-
|
|
928
|
+
runtimeAssetManifest,
|
|
929
|
+
port: devRouterPort,
|
|
930
|
+
});
|
|
931
|
+
console.log(`[listing] Reusing dev server at ${localDevAppUrl}`);
|
|
787
932
|
}
|
|
788
933
|
else {
|
|
789
934
|
serverHandle = await (0, devServer_1.startDevServer)({
|
|
@@ -791,8 +936,9 @@ async function captureListing(targetArg, options = {}) {
|
|
|
791
936
|
appType: appTypeSlug,
|
|
792
937
|
creatorUsername: currentUsername,
|
|
793
938
|
htmlPath: resolvedTarget.htmlPath,
|
|
794
|
-
port:
|
|
939
|
+
port: devRouterPort,
|
|
795
940
|
projectInfo,
|
|
941
|
+
runtimeAssetManifest,
|
|
796
942
|
});
|
|
797
943
|
}
|
|
798
944
|
if (!serverAlreadyRunning && projectInfo.projectDir && !devScriptAvailable && projectInfo.packageJsonPath) {
|
|
@@ -805,114 +951,153 @@ async function captureListing(targetArg, options = {}) {
|
|
|
805
951
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
806
952
|
}
|
|
807
953
|
const webBase = envConfig.webBase ?? 'https://www.playdrop.ai';
|
|
808
|
-
const routeSegment = resolveListingCaptureRouteSegment(
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
954
|
+
const routeSegment = resolveListingCaptureRouteSegment(declaresPreviewSupport);
|
|
955
|
+
const localDevPort = resolveListingCaptureLocalDevPort(devRouterPort);
|
|
956
|
+
const frameUrl = buildListingCaptureFrameUrl({
|
|
957
|
+
webBase,
|
|
958
|
+
currentUsername,
|
|
959
|
+
appTypeSlug,
|
|
960
|
+
appName,
|
|
961
|
+
routeSegment,
|
|
962
|
+
localDevPort,
|
|
963
|
+
});
|
|
964
|
+
const frameUrlObject = new URL(frameUrl);
|
|
965
|
+
console.log(`[listing] Opening ${frameUrl}`);
|
|
813
966
|
const shouldExportPreviewAudio = parsedOptions.audio && routeSegment === 'dev-preview';
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
967
|
+
const surfaces = resolvedTarget.surfaceTargets.list;
|
|
968
|
+
const useSurfacePrefix = surfaces.length > 1;
|
|
969
|
+
const captures = [];
|
|
970
|
+
const artifactHashes = {};
|
|
971
|
+
const capturedSurfaces = [];
|
|
972
|
+
const allWarnings = [];
|
|
973
|
+
for (const surface of surfaces) {
|
|
974
|
+
const dimensions = resolveCaptureDimensions(surface, parsedOptions);
|
|
975
|
+
const captureSceneId = resolveListingCaptureSceneId(dimensions.width, dimensions.height);
|
|
976
|
+
const surfaceOutputPaths = buildSurfaceOutputPaths(outputPaths.outputDir, surface, useSurfacePrefix);
|
|
977
|
+
console.log(`[listing] Capturing ${surface} at ${dimensions.width}x${dimensions.height}.`);
|
|
978
|
+
const contextOptions = resolveListingCaptureBrowserContextOptions(surface, dimensions);
|
|
979
|
+
browserHandle = await launchListingBrowser(dimensions, frameUrlObject.origin, contextOptions);
|
|
980
|
+
try {
|
|
981
|
+
await browserHandle.page.goto(frameUrl, { waitUntil: 'domcontentloaded' });
|
|
982
|
+
await browserHandle.page.waitForTimeout(1000);
|
|
983
|
+
if (routeSegment === 'dev-preview') {
|
|
984
|
+
console.log(`[listing] Preparing preview scene ${captureSceneId}.`);
|
|
985
|
+
await prepareHostedListingScene(browserHandle.page, captureSceneId);
|
|
986
|
+
await browserHandle.page.waitForTimeout(1000);
|
|
987
|
+
}
|
|
988
|
+
if (shouldExportPreviewAudio) {
|
|
989
|
+
console.log('[listing] Starting in-app preview audio capture.');
|
|
990
|
+
await startHostedListingAudioCapture(browserHandle.page);
|
|
991
|
+
}
|
|
992
|
+
const measurement = await fitWindowToRequestedGameplay(browserHandle.page, dimensions.width, dimensions.height);
|
|
993
|
+
console.log(`[listing] Gameplay frame ${Math.round(measurement.iframeRect.width)}x${Math.round(measurement.iframeRect.height)} in window ${Math.round(measurement.outerWidth)}x${Math.round(measurement.outerHeight)}.`);
|
|
994
|
+
await browserHandle.page.waitForTimeout(750);
|
|
995
|
+
const recorderMetadata = await runListingRecorder(recorderPath, browserHandle.processId, parsedOptions.durationSeconds, surfaceOutputPaths.rawVideoPath, surfaceOutputPaths.metadataPath, shouldExportPreviewAudio ? false : parsedOptions.audio);
|
|
996
|
+
const exportedAudioPath = shouldExportPreviewAudio
|
|
997
|
+
? await stopHostedListingAudioCapture(browserHandle.page, outputPaths.outputDir)
|
|
998
|
+
: null;
|
|
999
|
+
const rawProbe = await probeMediaFile(surfaceOutputPaths.rawVideoPath);
|
|
1000
|
+
const rawVideoStream = extractVideoStream(rawProbe);
|
|
1001
|
+
const rawDurationSeconds = readDurationSeconds(rawProbe);
|
|
1002
|
+
const rawAudioTrackCount = countAudioStreams(rawProbe);
|
|
1003
|
+
const crop = computeRecordedCrop(measurement, rawVideoStream.width, rawVideoStream.height);
|
|
1004
|
+
console.log(`[listing] Cropping raw capture to gameplay at ${crop.width}x${crop.height}+${crop.x}+${crop.y}.`);
|
|
1005
|
+
await encodeListingVideo(surfaceOutputPaths.rawVideoPath, surfaceOutputPaths.finalVideoPath, surfaceOutputPaths.posterPath, crop, dimensions.width, dimensions.height, parsedOptions.fps, parsedOptions.posterAtSeconds);
|
|
1006
|
+
if (exportedAudioPath) {
|
|
1007
|
+
await muxListingAudio(surfaceOutputPaths.finalVideoPath, exportedAudioPath);
|
|
1008
|
+
}
|
|
1009
|
+
const finalProbe = await probeMediaFile(surfaceOutputPaths.finalVideoPath);
|
|
1010
|
+
const finalVideoStream = extractVideoStream(finalProbe);
|
|
1011
|
+
const finalDurationSeconds = readDurationSeconds(finalProbe);
|
|
1012
|
+
const finalAudioTrackCount = countAudioStreams(finalProbe);
|
|
1013
|
+
const finalFps = parseFrameRate(finalVideoStream.avg_frame_rate);
|
|
1014
|
+
ensureTolerance(finalVideoStream.width, dimensions.width, 0, 'final_width_mismatch');
|
|
1015
|
+
ensureTolerance(finalVideoStream.height, dimensions.height, 0, 'final_height_mismatch');
|
|
1016
|
+
ensureTolerance(finalDurationSeconds, parsedOptions.durationSeconds, 1.5, 'final_duration_mismatch');
|
|
1017
|
+
if (parsedOptions.audio && finalAudioTrackCount === 0) {
|
|
1018
|
+
throw new Error('audio_track_missing');
|
|
1019
|
+
}
|
|
1020
|
+
const warnings = createWarnings(finalProbe, parsedOptions.audio);
|
|
1021
|
+
for (const warning of warnings) {
|
|
1022
|
+
allWarnings.push(`${surface}: ${warning}`);
|
|
1023
|
+
}
|
|
1024
|
+
artifactHashes[(0, node_path_1.relative)(outputPaths.outputDir, surfaceOutputPaths.finalVideoPath) || 'listing.mp4'] = await computeFileSha256(surfaceOutputPaths.finalVideoPath);
|
|
1025
|
+
artifactHashes[(0, node_path_1.relative)(outputPaths.outputDir, surfaceOutputPaths.posterPath) || 'poster.png'] = await computeFileSha256(surfaceOutputPaths.posterPath);
|
|
1026
|
+
capturedSurfaces.push(surface);
|
|
1027
|
+
captures.push({
|
|
1028
|
+
surface,
|
|
1029
|
+
targetUrl: frameUrl,
|
|
1030
|
+
command: {
|
|
1031
|
+
durationSeconds: parsedOptions.durationSeconds,
|
|
1032
|
+
width: dimensions.width,
|
|
1033
|
+
height: dimensions.height,
|
|
1034
|
+
fps: parsedOptions.fps,
|
|
1035
|
+
posterAtSeconds: parsedOptions.posterAtSeconds,
|
|
1036
|
+
audio: parsedOptions.audio,
|
|
1037
|
+
keepRaw: parsedOptions.keepRaw,
|
|
1038
|
+
},
|
|
1039
|
+
measurement,
|
|
1040
|
+
recorder: recorderMetadata,
|
|
1041
|
+
crop,
|
|
1042
|
+
rawVideo: {
|
|
1043
|
+
path: parsedOptions.keepRaw ? surfaceOutputPaths.rawVideoPath : null,
|
|
1044
|
+
width: rawVideoStream.width,
|
|
1045
|
+
height: rawVideoStream.height,
|
|
1046
|
+
durationSeconds: rawDurationSeconds,
|
|
1047
|
+
audioTrackCount: rawAudioTrackCount,
|
|
1048
|
+
},
|
|
1049
|
+
finalVideo: {
|
|
1050
|
+
path: surfaceOutputPaths.finalVideoPath,
|
|
1051
|
+
width: finalVideoStream.width,
|
|
1052
|
+
height: finalVideoStream.height,
|
|
1053
|
+
durationSeconds: finalDurationSeconds,
|
|
1054
|
+
audioTrackCount: finalAudioTrackCount,
|
|
1055
|
+
fps: finalFps,
|
|
1056
|
+
},
|
|
1057
|
+
posterPath: surfaceOutputPaths.posterPath,
|
|
1058
|
+
warnings,
|
|
1059
|
+
});
|
|
1060
|
+
if (!parsedOptions.keepRaw) {
|
|
1061
|
+
await (0, promises_1.rm)(surfaceOutputPaths.rawVideoPath, { force: true });
|
|
1062
|
+
}
|
|
1063
|
+
console.log(`[listing] Saved ${surface} video to ${(0, node_path_1.relative)(process.cwd(), surfaceOutputPaths.finalVideoPath) || surfaceOutputPaths.finalVideoPath}`);
|
|
1064
|
+
console.log(`[listing] Saved ${surface} poster to ${(0, node_path_1.relative)(process.cwd(), surfaceOutputPaths.posterPath) || surfaceOutputPaths.posterPath}`);
|
|
1065
|
+
if (warnings.length > 0) {
|
|
1066
|
+
for (const warning of warnings) {
|
|
1067
|
+
console.warn(`[listing][warn][${surface}] ${warning}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
finally {
|
|
1072
|
+
await browserHandle.close();
|
|
1073
|
+
browserHandle = null;
|
|
1074
|
+
}
|
|
856
1075
|
}
|
|
857
|
-
const warnings = createWarnings(finalProbe, parsedOptions.audio);
|
|
858
1076
|
const report = {
|
|
859
1077
|
appName,
|
|
860
|
-
targetUrl: frameUrl,
|
|
861
1078
|
outputDir: outputPaths.outputDir,
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
audio: parsedOptions.audio,
|
|
869
|
-
keepRaw: parsedOptions.keepRaw,
|
|
870
|
-
},
|
|
871
|
-
measurement,
|
|
872
|
-
recorder: recorderMetadata,
|
|
873
|
-
crop,
|
|
874
|
-
rawVideo: {
|
|
875
|
-
path: parsedOptions.keepRaw ? outputPaths.rawVideoPath : null,
|
|
876
|
-
width: rawVideoStream.width,
|
|
877
|
-
height: rawVideoStream.height,
|
|
878
|
-
durationSeconds: rawDurationSeconds,
|
|
879
|
-
audioTrackCount: rawAudioTrackCount,
|
|
880
|
-
},
|
|
881
|
-
finalVideo: {
|
|
882
|
-
path: outputPaths.finalVideoPath,
|
|
883
|
-
width: finalVideoStream.width,
|
|
884
|
-
height: finalVideoStream.height,
|
|
885
|
-
durationSeconds: finalDurationSeconds,
|
|
886
|
-
audioTrackCount: finalAudioTrackCount,
|
|
887
|
-
fps: finalFps,
|
|
1079
|
+
captures,
|
|
1080
|
+
warnings: allWarnings,
|
|
1081
|
+
binding: {
|
|
1082
|
+
contentHash: parsedOptions.contentHash,
|
|
1083
|
+
artifactHashes,
|
|
1084
|
+
capturedSurfaces,
|
|
888
1085
|
},
|
|
889
|
-
posterPath: outputPaths.posterPath,
|
|
890
|
-
warnings,
|
|
891
1086
|
};
|
|
892
1087
|
await (0, promises_1.writeFile)(outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
893
|
-
if (!parsedOptions.keepRaw) {
|
|
894
|
-
await (0, promises_1.rm)(outputPaths.rawVideoPath, { force: true });
|
|
895
|
-
}
|
|
896
|
-
console.log(`[listing] Saved video to ${(0, node_path_1.relative)(process.cwd(), outputPaths.finalVideoPath) || outputPaths.finalVideoPath}`);
|
|
897
|
-
console.log(`[listing] Saved poster to ${(0, node_path_1.relative)(process.cwd(), outputPaths.posterPath) || outputPaths.posterPath}`);
|
|
898
1088
|
console.log(`[listing] Saved report to ${(0, node_path_1.relative)(process.cwd(), outputPaths.reportPath) || outputPaths.reportPath}`);
|
|
899
|
-
if (warnings.length > 0) {
|
|
900
|
-
for (const warning of warnings) {
|
|
901
|
-
console.warn(`[listing][warn] ${warning}`);
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
1089
|
}
|
|
905
1090
|
catch (error) {
|
|
906
1091
|
if (error instanceof http_1.CLIUnsupportedClientError) {
|
|
907
1092
|
return;
|
|
908
1093
|
}
|
|
909
1094
|
if (error instanceof types_1.UnsupportedClientError) {
|
|
910
|
-
(0, http_1.handleUnsupportedError)(error, '
|
|
1095
|
+
(0, http_1.handleUnsupportedError)(error, 'Game media capture');
|
|
911
1096
|
process.exitCode = 1;
|
|
912
1097
|
return;
|
|
913
1098
|
}
|
|
914
1099
|
const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
|
|
915
|
-
(0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture
|
|
1100
|
+
(0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
|
|
916
1101
|
process.exitCode = 1;
|
|
917
1102
|
}
|
|
918
1103
|
finally {
|