@playdrop/playdrop-cli 0.13.16 → 0.14.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/staticHtml.js +1 -0
- package/dist/commandContext.js +1 -1
- package/dist/commands/captureListing.d.ts +52 -0
- package/dist/commands/captureListing.js +213 -12
- package/dist/commands/upload.d.ts +12 -1
- package/dist/commands/upload.js +97 -88
- package/dist/commands/whoami.js +10 -1
- package/dist/commands/worker/runtime.js +2 -0
- package/dist/commands/worker.d.ts +28 -1
- package/dist/commands/worker.js +381 -29
- package/dist/index.js +17 -2
- package/dist/listingPreflight.d.ts +2 -1
- package/dist/listingPreflight.js +63 -35
- package/dist/workerAppProject.d.ts +2 -0
- package/dist/workerAppProject.js +33 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -2
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +27 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +103 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +4 -2
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +10 -0
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/types/dist/api.d.ts +150 -2
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +3 -1
- package/package.json +1 -1
package/config/client-meta.json
CHANGED
package/dist/apps/staticHtml.js
CHANGED
|
@@ -240,6 +240,7 @@ function buildStaticAppTask(input) {
|
|
|
240
240
|
hasValidateScript: false,
|
|
241
241
|
hasFormatScript: false,
|
|
242
242
|
displayName: input.metadata.title,
|
|
243
|
+
...(input.metadata.description ? { subtitle: input.metadata.description } : {}),
|
|
243
244
|
...(input.metadata.description ? { description: input.metadata.description } : {}),
|
|
244
245
|
type: 'GAME',
|
|
245
246
|
surfaceTargets: [...STATIC_GAME_SURFACES],
|
package/dist/commandContext.js
CHANGED
|
@@ -154,7 +154,7 @@ function resolveWorkspaceSelectedAccount(cfg, currentAccount, workspaceAuth, req
|
|
|
154
154
|
matchingSessions,
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
-
if (
|
|
157
|
+
if (currentAccount) {
|
|
158
158
|
if (currentAccount.env === preferredEnv) {
|
|
159
159
|
return {
|
|
160
160
|
account: currentAccount,
|
|
@@ -22,6 +22,7 @@ type ParsedCaptureListingOptions = {
|
|
|
22
22
|
posterAtSeconds: number;
|
|
23
23
|
audio: boolean;
|
|
24
24
|
outputDir: string | null;
|
|
25
|
+
outputDirInput: string | null;
|
|
25
26
|
keepRaw: boolean;
|
|
26
27
|
explicitDimensions: boolean;
|
|
27
28
|
};
|
|
@@ -43,6 +44,10 @@ type HostedGameMeasurement = {
|
|
|
43
44
|
devicePixelRatio: number;
|
|
44
45
|
iframeRect: CaptureRect;
|
|
45
46
|
};
|
|
47
|
+
type BrowserWindowBounds = {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
};
|
|
46
51
|
type CropRect = {
|
|
47
52
|
x: number;
|
|
48
53
|
y: number;
|
|
@@ -70,6 +75,43 @@ type ListingRecorderMetadata = {
|
|
|
70
75
|
recordedDurationSeconds: number;
|
|
71
76
|
recordedFileSizeBytes: number;
|
|
72
77
|
};
|
|
78
|
+
type ListingCaptureSurfaceReport = {
|
|
79
|
+
surface: AppSurface;
|
|
80
|
+
targetUrl: string;
|
|
81
|
+
command: {
|
|
82
|
+
durationSeconds: number;
|
|
83
|
+
width: number;
|
|
84
|
+
height: number;
|
|
85
|
+
fps: number;
|
|
86
|
+
posterAtSeconds: number;
|
|
87
|
+
audio: boolean;
|
|
88
|
+
keepRaw: boolean;
|
|
89
|
+
};
|
|
90
|
+
measurement: HostedGameMeasurement;
|
|
91
|
+
recorder: ListingRecorderMetadata;
|
|
92
|
+
crop: CropRect;
|
|
93
|
+
rawVideo: {
|
|
94
|
+
path: string | null;
|
|
95
|
+
width: number;
|
|
96
|
+
height: number;
|
|
97
|
+
durationSeconds: number;
|
|
98
|
+
audioTrackCount: number;
|
|
99
|
+
};
|
|
100
|
+
finalVideo: {
|
|
101
|
+
path: string;
|
|
102
|
+
width: number;
|
|
103
|
+
height: number;
|
|
104
|
+
durationSeconds: number;
|
|
105
|
+
audioTrackCount: number;
|
|
106
|
+
fps: number | null;
|
|
107
|
+
};
|
|
108
|
+
posterPath: string;
|
|
109
|
+
warnings: string[];
|
|
110
|
+
};
|
|
111
|
+
export type ExportedListingAudio = {
|
|
112
|
+
mimeType: string;
|
|
113
|
+
base64: string;
|
|
114
|
+
};
|
|
73
115
|
export declare function resolveListingCaptureRouteSegment(previewable: boolean): 'dev-preview';
|
|
74
116
|
export declare function resolveCaptureListingDevRouterPort(env?: NodeJS.ProcessEnv): number;
|
|
75
117
|
export declare function resolveListingCaptureLocalDevPort(devRouterPort: number): number | null;
|
|
@@ -84,6 +126,7 @@ export declare function buildListingCaptureFrameUrl({ webBase, currentUsername,
|
|
|
84
126
|
}): string;
|
|
85
127
|
export declare function resolveListingCaptureSceneId(width: number, height: number): 'listing-landscape' | 'listing-portrait';
|
|
86
128
|
export declare function parseCaptureListingOptions(targetArg: string | undefined, options?: CaptureListingOptions): ParsedCaptureListingOptions;
|
|
129
|
+
export declare function resolveCaptureListingExplicitOutputDir(outputDirInput: string | null, cwdResolvedOutputDir: string | null, projectDir: string): string | null;
|
|
87
130
|
export declare function assertSupportedListingEnvironment(platform?: NodeJS.Platform, macosVersion?: string): void;
|
|
88
131
|
export declare function resolveListingRecorderPath(workerHome?: string): string;
|
|
89
132
|
export declare function buildSurfaceOutputPaths(outputDir: string, surface: AppSurface, useSurfacePrefix: boolean): {
|
|
@@ -92,6 +135,7 @@ export declare function buildSurfaceOutputPaths(outputDir: string, surface: AppS
|
|
|
92
135
|
posterPath: string;
|
|
93
136
|
finalVideoPath: string;
|
|
94
137
|
};
|
|
138
|
+
export declare function resolveWorkerCaptureOutputDir(projectDir: string, isWorkerGameTask: boolean): string | null;
|
|
95
139
|
export declare function resolveCaptureDimensions(surface: AppSurface, parsedOptions: ParsedCaptureListingOptions): {
|
|
96
140
|
width: number;
|
|
97
141
|
height: number;
|
|
@@ -100,7 +144,15 @@ export declare function resolveListingCaptureBrowserContextOptions(surface: AppS
|
|
|
100
144
|
width: number;
|
|
101
145
|
height: number;
|
|
102
146
|
}): BrowserContextOptions;
|
|
147
|
+
export declare function assertExportedListingAudio(value: unknown): ExportedListingAudio;
|
|
148
|
+
export declare function assertListingCaptureWindowCanContainViewport(measurement: HostedGameMeasurement, windowBounds: BrowserWindowBounds): void;
|
|
103
149
|
export declare function computeRecordedCrop(measurement: HostedGameMeasurement, recorder: ListingRecorderMetadata, rawWidth: number, rawHeight: number): CropRect;
|
|
104
150
|
export declare function runListingRecorder(recorderPath: string, pid: number, durationSeconds: number, rawOutputPath: string, metadataPath: string, audio: boolean, deadlineAt: number): Promise<ListingRecorderMetadata>;
|
|
151
|
+
export declare function formatCommandError(error: Error): {
|
|
152
|
+
message: string;
|
|
153
|
+
suggestions: string[];
|
|
154
|
+
};
|
|
155
|
+
export declare function captureCommandMatchesExceptPoster(capture: ListingCaptureSurfaceReport, surface: AppSurface, parsedOptions: ParsedCaptureListingOptions): boolean;
|
|
156
|
+
export declare function resolveCurrentCaptureReuseAction(captures: ListingCaptureSurfaceReport[], surfaces: AppSurface[], parsedOptions: ParsedCaptureListingOptions): 'reuse' | 'refresh_poster';
|
|
105
157
|
export declare function captureListing(targetArg: string | undefined, options?: CaptureListingOptions): Promise<void>;
|
|
106
158
|
export {};
|
|
@@ -39,13 +39,20 @@ exports.resolveListingCaptureLocalDevPort = resolveListingCaptureLocalDevPort;
|
|
|
39
39
|
exports.buildListingCaptureFrameUrl = buildListingCaptureFrameUrl;
|
|
40
40
|
exports.resolveListingCaptureSceneId = resolveListingCaptureSceneId;
|
|
41
41
|
exports.parseCaptureListingOptions = parseCaptureListingOptions;
|
|
42
|
+
exports.resolveCaptureListingExplicitOutputDir = resolveCaptureListingExplicitOutputDir;
|
|
42
43
|
exports.assertSupportedListingEnvironment = assertSupportedListingEnvironment;
|
|
43
44
|
exports.resolveListingRecorderPath = resolveListingRecorderPath;
|
|
44
45
|
exports.buildSurfaceOutputPaths = buildSurfaceOutputPaths;
|
|
46
|
+
exports.resolveWorkerCaptureOutputDir = resolveWorkerCaptureOutputDir;
|
|
45
47
|
exports.resolveCaptureDimensions = resolveCaptureDimensions;
|
|
46
48
|
exports.resolveListingCaptureBrowserContextOptions = resolveListingCaptureBrowserContextOptions;
|
|
49
|
+
exports.assertExportedListingAudio = assertExportedListingAudio;
|
|
50
|
+
exports.assertListingCaptureWindowCanContainViewport = assertListingCaptureWindowCanContainViewport;
|
|
47
51
|
exports.computeRecordedCrop = computeRecordedCrop;
|
|
48
52
|
exports.runListingRecorder = runListingRecorder;
|
|
53
|
+
exports.formatCommandError = formatCommandError;
|
|
54
|
+
exports.captureCommandMatchesExceptPoster = captureCommandMatchesExceptPoster;
|
|
55
|
+
exports.resolveCurrentCaptureReuseAction = resolveCurrentCaptureReuseAction;
|
|
49
56
|
exports.captureListing = captureListing;
|
|
50
57
|
const types_1 = require("@playdrop/types");
|
|
51
58
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -53,10 +60,12 @@ const node_crypto_1 = require("node:crypto");
|
|
|
53
60
|
const promises_1 = require("node:fs/promises");
|
|
54
61
|
const node_path_1 = require("node:path");
|
|
55
62
|
const appUrls_1 = require("../appUrls");
|
|
63
|
+
const build_1 = require("../apps/build");
|
|
56
64
|
const catalogue_1 = require("../catalogue");
|
|
57
65
|
const catalogue_utils_1 = require("../catalogue-utils");
|
|
58
66
|
const commandContext_1 = require("../commandContext");
|
|
59
67
|
const http_1 = require("../http");
|
|
68
|
+
const listingPreflight_1 = require("../listingPreflight");
|
|
60
69
|
const messages_1 = require("../messages");
|
|
61
70
|
const playwright_1 = require("../playwright");
|
|
62
71
|
const recorderRelease_1 = require("../recorderRelease");
|
|
@@ -66,6 +75,7 @@ const devRuntimeAssets_1 = require("./devRuntimeAssets");
|
|
|
66
75
|
const devServer_1 = require("./devServer");
|
|
67
76
|
const devShared_1 = require("./devShared");
|
|
68
77
|
const taskCaptureSession_1 = require("./taskCaptureSession");
|
|
78
|
+
const workerAppProject_1 = require("../workerAppProject");
|
|
69
79
|
const CAPTURE_FRAME_SELECTOR = 'iframe[title="Game"]';
|
|
70
80
|
const DEFAULT_DURATION_SECONDS = 8;
|
|
71
81
|
const DEFAULT_WIDTH = 1280;
|
|
@@ -158,7 +168,8 @@ function parseCaptureListingOptions(targetArg, options = {}) {
|
|
|
158
168
|
minimum: 0,
|
|
159
169
|
maximum: durationSeconds,
|
|
160
170
|
});
|
|
161
|
-
const
|
|
171
|
+
const outputDirInput = options.outputDir?.trim() || null;
|
|
172
|
+
const outputDir = outputDirInput ? (0, node_path_1.resolve)(process.cwd(), outputDirInput) : null;
|
|
162
173
|
return {
|
|
163
174
|
targetArg,
|
|
164
175
|
appName: options.app?.trim() || undefined,
|
|
@@ -169,10 +180,18 @@ function parseCaptureListingOptions(targetArg, options = {}) {
|
|
|
169
180
|
posterAtSeconds,
|
|
170
181
|
audio: Boolean(options.audio),
|
|
171
182
|
outputDir,
|
|
183
|
+
outputDirInput,
|
|
172
184
|
keepRaw: Boolean(options.keepRaw),
|
|
173
185
|
explicitDimensions,
|
|
174
186
|
};
|
|
175
187
|
}
|
|
188
|
+
function resolveCaptureListingExplicitOutputDir(outputDirInput, cwdResolvedOutputDir, projectDir) {
|
|
189
|
+
if (!outputDirInput || !cwdResolvedOutputDir)
|
|
190
|
+
return null;
|
|
191
|
+
return (0, node_path_1.isAbsolute)(outputDirInput)
|
|
192
|
+
? cwdResolvedOutputDir
|
|
193
|
+
: (0, node_path_1.resolve)(projectDir, outputDirInput);
|
|
194
|
+
}
|
|
176
195
|
function readMacOsVersion() {
|
|
177
196
|
const result = (0, node_child_process_1.spawnSync)('sw_vers', ['-productVersion'], {
|
|
178
197
|
encoding: 'utf8',
|
|
@@ -231,6 +250,11 @@ function buildSurfaceOutputPaths(outputDir, surface, useSurfacePrefix) {
|
|
|
231
250
|
finalVideoPath: (0, node_path_1.join)(outputDir, `${prefix}listing.mp4`),
|
|
232
251
|
};
|
|
233
252
|
}
|
|
253
|
+
function resolveWorkerCaptureOutputDir(projectDir, isWorkerGameTask) {
|
|
254
|
+
return isWorkerGameTask
|
|
255
|
+
? (0, node_path_1.join)(projectDir, 'assets', 'marketing', 'playdrop', 'capture')
|
|
256
|
+
: null;
|
|
257
|
+
}
|
|
234
258
|
function resolveCaptureDimensions(surface, parsedOptions) {
|
|
235
259
|
if (parsedOptions.explicitDimensions) {
|
|
236
260
|
return {
|
|
@@ -432,6 +456,21 @@ async function startHostedListingAudioCapture(page) {
|
|
|
432
456
|
await hook.startAudioCapture();
|
|
433
457
|
});
|
|
434
458
|
}
|
|
459
|
+
function assertExportedListingAudio(value) {
|
|
460
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
461
|
+
throw new Error('listing_audio_capture_export_missing');
|
|
462
|
+
}
|
|
463
|
+
const mimeType = typeof value.mimeType === 'string'
|
|
464
|
+
? value.mimeType.trim()
|
|
465
|
+
: '';
|
|
466
|
+
const base64 = typeof value.base64 === 'string'
|
|
467
|
+
? value.base64.trim()
|
|
468
|
+
: '';
|
|
469
|
+
if (!mimeType || !base64) {
|
|
470
|
+
throw new Error('listing_audio_capture_export_missing');
|
|
471
|
+
}
|
|
472
|
+
return { mimeType, base64 };
|
|
473
|
+
}
|
|
435
474
|
function resolveExportedAudioFileName(mimeType) {
|
|
436
475
|
if (mimeType.includes('webm')) {
|
|
437
476
|
return 'listing-audio.webm';
|
|
@@ -443,27 +482,27 @@ function resolveExportedAudioFileName(mimeType) {
|
|
|
443
482
|
}
|
|
444
483
|
async function stopHostedListingAudioCapture(page, outputDir) {
|
|
445
484
|
const frame = await waitForHostedGameFrame(page, 20000);
|
|
446
|
-
const exportedAudio = await frame.evaluate(async () => {
|
|
485
|
+
const exportedAudio = assertExportedListingAudio(await frame.evaluate(async () => {
|
|
447
486
|
const captureWindow = window;
|
|
448
487
|
const hook = captureWindow.__listingCapture;
|
|
449
488
|
if (!hook || typeof hook.stopAudioCapture !== 'function') {
|
|
450
489
|
throw new Error('listing_audio_capture_hook_missing');
|
|
451
490
|
}
|
|
452
491
|
return hook.stopAudioCapture();
|
|
453
|
-
});
|
|
492
|
+
}));
|
|
454
493
|
const filePath = (0, node_path_1.join)(outputDir, resolveExportedAudioFileName(exportedAudio.mimeType));
|
|
455
494
|
await (0, promises_1.writeFile)(filePath, Buffer.from(exportedAudio.base64, 'base64'));
|
|
456
495
|
return filePath;
|
|
457
496
|
}
|
|
497
|
+
async function assertHostedListingAudioCaptureContract(page, outputDir) {
|
|
498
|
+
await startHostedListingAudioCapture(page);
|
|
499
|
+
await page.waitForTimeout(300);
|
|
500
|
+
await stopHostedListingAudioCapture(page, outputDir);
|
|
501
|
+
}
|
|
458
502
|
async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeight) {
|
|
459
503
|
const cdpSession = await page.context().newCDPSession(page);
|
|
460
504
|
let measurement = await waitForHostedGameMeasurement(page, 15000);
|
|
461
505
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
462
|
-
const deltaWidth = requestedWidth - Math.round(measurement.iframeRect.width);
|
|
463
|
-
const deltaHeight = requestedHeight - Math.round(measurement.iframeRect.height);
|
|
464
|
-
if (Math.abs(deltaWidth) <= 1 && Math.abs(deltaHeight) <= 1) {
|
|
465
|
-
return measurement;
|
|
466
|
-
}
|
|
467
506
|
const windowState = await cdpSession.send('Browser.getWindowForTarget');
|
|
468
507
|
const currentWidth = typeof windowState.bounds.width === 'number'
|
|
469
508
|
? windowState.bounds.width
|
|
@@ -471,6 +510,15 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
|
|
|
471
510
|
const currentHeight = typeof windowState.bounds.height === 'number'
|
|
472
511
|
? windowState.bounds.height
|
|
473
512
|
: Math.round(measurement.outerHeight);
|
|
513
|
+
const deltaWidth = requestedWidth - Math.round(measurement.iframeRect.width);
|
|
514
|
+
const deltaHeight = requestedHeight - Math.round(measurement.iframeRect.height);
|
|
515
|
+
if (Math.abs(deltaWidth) <= 1 && Math.abs(deltaHeight) <= 1) {
|
|
516
|
+
assertListingCaptureWindowCanContainViewport(measurement, {
|
|
517
|
+
width: currentWidth,
|
|
518
|
+
height: currentHeight,
|
|
519
|
+
});
|
|
520
|
+
return measurement;
|
|
521
|
+
}
|
|
474
522
|
const nextWidth = Math.max(400, currentWidth + deltaWidth);
|
|
475
523
|
const nextHeight = Math.max(300, currentHeight + deltaHeight);
|
|
476
524
|
await cdpSession.send('Browser.setWindowBounds', {
|
|
@@ -485,6 +533,15 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
|
|
|
485
533
|
}
|
|
486
534
|
throw new Error('hosted_game_resize_failed');
|
|
487
535
|
}
|
|
536
|
+
function assertListingCaptureWindowCanContainViewport(measurement, windowBounds) {
|
|
537
|
+
const requiredWidth = Math.ceil(measurement.innerWidth);
|
|
538
|
+
const requiredHeight = Math.ceil(measurement.innerHeight);
|
|
539
|
+
const availableWidth = Math.floor(windowBounds.width);
|
|
540
|
+
const availableHeight = Math.floor(windowBounds.height);
|
|
541
|
+
if (availableWidth + 1 < requiredWidth || availableHeight + 1 < requiredHeight) {
|
|
542
|
+
throw new Error(`listing_capture_dimensions_exceed_display:${requiredWidth}x${requiredHeight}:${availableWidth}x${availableHeight}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
488
545
|
function roundCropValue(value) {
|
|
489
546
|
return Math.round(value);
|
|
490
547
|
}
|
|
@@ -666,6 +723,31 @@ function formatCommandError(error) {
|
|
|
666
723
|
suggestions: ['Set previewable: true in catalogue.json, implement the preview scene and __listingCapture hooks, then rerun "playdrop project capture".'],
|
|
667
724
|
};
|
|
668
725
|
}
|
|
726
|
+
if (error.message === 'listing_audio_capture_export_missing') {
|
|
727
|
+
return {
|
|
728
|
+
message: 'The game preview audio hook did not export recorded audio.',
|
|
729
|
+
suggestions: ['Make window.__listingCapture.stopAudioCapture() resolve { mimeType, base64 }; toggling game audio on or off is not an export.'],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (error.message === 'listing_capture_runtime_already_recorded') {
|
|
733
|
+
return {
|
|
734
|
+
message: 'This exact game runtime already has a canonical recording with different capture settings.',
|
|
735
|
+
suggestions: ['Use the existing video, or change and revalidate the preview implementation before recording the changed runtime once.'],
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (error.message === 'listing_capture_current_report_incomplete') {
|
|
739
|
+
return {
|
|
740
|
+
message: 'The canonical capture report is incomplete for this runtime.',
|
|
741
|
+
suggestions: ['Fix or remove the incomplete canonical capture directory, then record the runtime once.'],
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
if (error.message.startsWith('listing_capture_dimensions_exceed_display:')) {
|
|
745
|
+
const [, requested = 'unknown', available = 'unknown'] = error.message.split(':');
|
|
746
|
+
return {
|
|
747
|
+
message: `Requested capture size ${requested} cannot fit in this worker's ${available} browser window.`,
|
|
748
|
+
suggestions: ['Omit --width and --height to use the safe surface defaults, or request dimensions that fit this display.'],
|
|
749
|
+
};
|
|
750
|
+
}
|
|
669
751
|
if (error.message.startsWith('listing_recorder_failed:')) {
|
|
670
752
|
return {
|
|
671
753
|
message: error.message.slice('listing_recorder_failed:'.length),
|
|
@@ -710,6 +792,77 @@ async function encodeListingVideo(rawVideoPath, finalVideoPath, posterPath, crop
|
|
|
710
792
|
posterPath,
|
|
711
793
|
], 'ffmpeg_failed', deadlineAt);
|
|
712
794
|
}
|
|
795
|
+
function captureCommandMatchesExceptPoster(capture, surface, parsedOptions) {
|
|
796
|
+
const dimensions = resolveCaptureDimensions(surface, parsedOptions);
|
|
797
|
+
return capture.command.durationSeconds === parsedOptions.durationSeconds
|
|
798
|
+
&& capture.command.width === dimensions.width
|
|
799
|
+
&& capture.command.height === dimensions.height
|
|
800
|
+
&& capture.command.fps === parsedOptions.fps
|
|
801
|
+
&& capture.command.audio === parsedOptions.audio
|
|
802
|
+
&& capture.command.keepRaw === parsedOptions.keepRaw;
|
|
803
|
+
}
|
|
804
|
+
function resolveCurrentCaptureReuseAction(captures, surfaces, parsedOptions) {
|
|
805
|
+
if (captures.length !== surfaces.length) {
|
|
806
|
+
throw new Error('listing_capture_current_report_incomplete');
|
|
807
|
+
}
|
|
808
|
+
const orderedCaptures = surfaces.map((surface) => captures.find((candidate) => candidate.surface === surface));
|
|
809
|
+
if (orderedCaptures.some((capture) => !capture)) {
|
|
810
|
+
throw new Error('listing_capture_current_report_incomplete');
|
|
811
|
+
}
|
|
812
|
+
if (orderedCaptures.some((capture, index) => !captureCommandMatchesExceptPoster(capture, surfaces[index], parsedOptions))) {
|
|
813
|
+
throw new Error('listing_capture_runtime_already_recorded');
|
|
814
|
+
}
|
|
815
|
+
return orderedCaptures.some((capture) => capture.command.posterAtSeconds !== parsedOptions.posterAtSeconds)
|
|
816
|
+
? 'refresh_poster'
|
|
817
|
+
: 'reuse';
|
|
818
|
+
}
|
|
819
|
+
async function reuseCurrentListingCapture(input) {
|
|
820
|
+
let report;
|
|
821
|
+
try {
|
|
822
|
+
report = JSON.parse(await (0, promises_1.readFile)(input.outputPaths.reportPath, 'utf8'));
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
if (error?.code === 'ENOENT') {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
throw new Error(`listing_capture_report_invalid:${input.outputPaths.reportPath}`);
|
|
829
|
+
}
|
|
830
|
+
if (report.appName !== input.appName) {
|
|
831
|
+
throw new Error(`listing_capture_report_app_mismatch:${report.appName}:${input.appName}`);
|
|
832
|
+
}
|
|
833
|
+
if (!input.runtimeBundleHash || report.binding?.runtimeBundleHash !== input.runtimeBundleHash) {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
const action = resolveCurrentCaptureReuseAction(report.captures, input.surfaces, input.parsedOptions);
|
|
837
|
+
const captures = input.surfaces.map((surface) => report.captures.find((candidate) => candidate.surface === surface));
|
|
838
|
+
for (const capture of captures) {
|
|
839
|
+
await (0, promises_1.access)(capture.finalVideo.path);
|
|
840
|
+
}
|
|
841
|
+
if (action === 'reuse') {
|
|
842
|
+
console.log('[listing] The current runtime already has a matching canonical capture; reusing it.');
|
|
843
|
+
console.log(`[listing] Existing report: ${(0, node_path_1.relative)(process.cwd(), input.outputPaths.reportPath) || input.outputPaths.reportPath}`);
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
for (const capture of captures) {
|
|
847
|
+
runTool('ffmpeg', [
|
|
848
|
+
'-y',
|
|
849
|
+
'-ss',
|
|
850
|
+
String(input.parsedOptions.posterAtSeconds),
|
|
851
|
+
'-i',
|
|
852
|
+
capture.finalVideo.path,
|
|
853
|
+
'-frames:v',
|
|
854
|
+
'1',
|
|
855
|
+
capture.posterPath,
|
|
856
|
+
], 'ffmpeg_failed', input.deadlineAt);
|
|
857
|
+
capture.command.posterAtSeconds = input.parsedOptions.posterAtSeconds;
|
|
858
|
+
const artifactKey = (0, node_path_1.relative)(input.outputPaths.outputDir, capture.posterPath) || 'poster.png';
|
|
859
|
+
report.binding.artifactHashes[artifactKey] = await computeFileSha256(capture.posterPath);
|
|
860
|
+
}
|
|
861
|
+
await (0, promises_1.writeFile)(input.outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
862
|
+
console.log(`[listing] Refreshed the canonical poster at ${input.parsedOptions.posterAtSeconds}s without relaunching the game.`);
|
|
863
|
+
console.log(`[listing] Updated report: ${(0, node_path_1.relative)(process.cwd(), input.outputPaths.reportPath) || input.outputPaths.reportPath}`);
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
713
866
|
async function muxListingAudio(finalVideoPath, audioPath, deadlineAt) {
|
|
714
867
|
const muxedVideoPath = `${finalVideoPath}.muxed.mp4`;
|
|
715
868
|
runTool('ffmpeg', [
|
|
@@ -824,8 +977,20 @@ async function captureListing(targetArg, options = {}) {
|
|
|
824
977
|
process.exitCode = 1;
|
|
825
978
|
return;
|
|
826
979
|
}
|
|
827
|
-
const outputPaths = await ensureOutputPaths(appName, parsedOptions.outputDir);
|
|
828
980
|
const projectInfo = (0, devShared_1.findProjectInfo)(resolvedTarget.htmlPath);
|
|
981
|
+
const projectDir = projectInfo.projectDir ?? (0, node_path_1.dirname)(resolvedTarget.htmlPath);
|
|
982
|
+
let isWorkerGameTask = false;
|
|
983
|
+
try {
|
|
984
|
+
isWorkerGameTask = Boolean((0, listingPreflight_1.readWorkerUploadPreflightContext)(projectDir));
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
|
|
988
|
+
(0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
|
|
989
|
+
process.exitCode = 1;
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const explicitOutputDir = resolveCaptureListingExplicitOutputDir(parsedOptions.outputDirInput, parsedOptions.outputDir, projectDir);
|
|
993
|
+
const outputPaths = await ensureOutputPaths(appName, explicitOutputDir ?? resolveWorkerCaptureOutputDir(projectDir, isWorkerGameTask));
|
|
829
994
|
const devScriptAvailable = Boolean(projectInfo.projectDir && projectInfo.packageJson && typeof projectInfo.packageJson.scripts?.dev === 'string');
|
|
830
995
|
await (0, commandContext_1.withEnvironment)('project capture', 'Capturing game media', async ({ client, env, envConfig, account, workspaceAuth }) => {
|
|
831
996
|
let currentUsername = '';
|
|
@@ -902,6 +1067,40 @@ async function captureListing(targetArg, options = {}) {
|
|
|
902
1067
|
process.exitCode = 1;
|
|
903
1068
|
return;
|
|
904
1069
|
}
|
|
1070
|
+
let runtimeBundleHash = null;
|
|
1071
|
+
if (taskLookup.task) {
|
|
1072
|
+
try {
|
|
1073
|
+
const captureTask = (0, listingPreflight_1.readWorkerUploadPreflightContext)(taskLookup.task.projectDir)
|
|
1074
|
+
? (0, workerAppProject_1.prepareWorkerAppTask)(taskLookup.task)
|
|
1075
|
+
: taskLookup.task;
|
|
1076
|
+
runtimeBundleHash = (await (0, build_1.buildApp)(captureTask)).bundleHash;
|
|
1077
|
+
}
|
|
1078
|
+
catch (error) {
|
|
1079
|
+
(0, messages_1.printErrorWithHelp)(error instanceof Error ? error.message : String(error), ['Fix the game build before recording final gameplay.'], { command: 'project capture' });
|
|
1080
|
+
process.exitCode = 1;
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
const captureDeadlineAt = Date.now() + listingCaptureLease_1.LISTING_CAPTURE_MAX_DURATION_MS;
|
|
1085
|
+
try {
|
|
1086
|
+
const reused = await reuseCurrentListingCapture({
|
|
1087
|
+
appName,
|
|
1088
|
+
outputPaths,
|
|
1089
|
+
parsedOptions,
|
|
1090
|
+
runtimeBundleHash,
|
|
1091
|
+
surfaces: resolvedTarget.surfaceTargets.list,
|
|
1092
|
+
deadlineAt: captureDeadlineAt,
|
|
1093
|
+
});
|
|
1094
|
+
if (reused) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
|
|
1100
|
+
(0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
|
|
1101
|
+
process.exitCode = 1;
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
905
1104
|
const localDevAppUrl = (0, devServer_1.buildLocalDevAppUrl)({
|
|
906
1105
|
creatorUsername: currentUsername,
|
|
907
1106
|
appType: appTypeSlug,
|
|
@@ -926,7 +1125,6 @@ async function captureListing(targetArg, options = {}) {
|
|
|
926
1125
|
return;
|
|
927
1126
|
}
|
|
928
1127
|
}
|
|
929
|
-
const captureDeadlineAt = Date.now() + listingCaptureLease_1.LISTING_CAPTURE_MAX_DURATION_MS;
|
|
930
1128
|
let serverAlreadyRunning = false;
|
|
931
1129
|
let devServerStartedByCapture = false;
|
|
932
1130
|
let serverHandle = null;
|
|
@@ -1019,12 +1217,14 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1019
1217
|
await prepareHostedListingScene(browserHandle.page, captureSceneId);
|
|
1020
1218
|
await browserHandle.page.waitForTimeout(1000);
|
|
1021
1219
|
}
|
|
1220
|
+
const measurement = await fitWindowToRequestedGameplay(browserHandle.page, dimensions.width, dimensions.height);
|
|
1221
|
+
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)}.`);
|
|
1022
1222
|
if (shouldExportPreviewAudio) {
|
|
1223
|
+
console.log('[listing] Checking the in-app preview audio export contract.');
|
|
1224
|
+
await assertHostedListingAudioCaptureContract(browserHandle.page, outputPaths.outputDir);
|
|
1023
1225
|
console.log('[listing] Starting in-app preview audio capture.');
|
|
1024
1226
|
await startHostedListingAudioCapture(browserHandle.page);
|
|
1025
1227
|
}
|
|
1026
|
-
const measurement = await fitWindowToRequestedGameplay(browserHandle.page, dimensions.width, dimensions.height);
|
|
1027
|
-
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)}.`);
|
|
1028
1228
|
await browserHandle.page.waitForTimeout(750);
|
|
1029
1229
|
console.log('[listing] Waiting for the machine capture lease.');
|
|
1030
1230
|
const captureLease = await (0, listingCaptureLease_1.acquireListingCaptureLease)({
|
|
@@ -1126,6 +1326,7 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1126
1326
|
binding: {
|
|
1127
1327
|
artifactHashes,
|
|
1128
1328
|
capturedSurfaces,
|
|
1329
|
+
runtimeBundleHash,
|
|
1129
1330
|
},
|
|
1130
1331
|
};
|
|
1131
1332
|
await (0, promises_1.writeFile)(outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApiClient } from '@playdrop/api-client';
|
|
2
|
-
import type { AgentExecutionTarget, UserResponse } from '@playdrop/types';
|
|
2
|
+
import type { AgentExecutionTarget, AppSurface, UserResponse } from '@playdrop/types';
|
|
3
3
|
import { type AppTask } from '../catalogue';
|
|
4
4
|
export type UploadCommandOptions = {
|
|
5
5
|
env?: string;
|
|
@@ -17,6 +17,9 @@ export type WorkerAppPublishInput = {
|
|
|
17
17
|
kind: 'NEW_GAME' | 'REMIX_GAME' | 'GAME_UPDATE';
|
|
18
18
|
executionTarget?: AgentExecutionTarget;
|
|
19
19
|
expectedAppName?: string | null;
|
|
20
|
+
expectedDisplayName?: string | null;
|
|
21
|
+
expectedSubtitle?: string | null;
|
|
22
|
+
expectedPrimarySurface?: AppSurface | null;
|
|
20
23
|
remixSourceRef?: string | null;
|
|
21
24
|
playdropAssetRequirement?: WorkerPlaydropAssetRequirement | null;
|
|
22
25
|
creatorRequest?: string | null;
|
|
@@ -69,6 +72,14 @@ export declare function assertWorkerAppLocalUploadPreflight(input: {
|
|
|
69
72
|
creatorUsername?: string | null;
|
|
70
73
|
playdropAssetRequirement?: WorkerPlaydropAssetRequirement | null;
|
|
71
74
|
creatorRequest?: string | null;
|
|
75
|
+
stage?: 'PRE_CAPTURE' | 'COMPLETE';
|
|
76
|
+
expectedRuntimeBundleHash?: string;
|
|
72
77
|
}): Promise<void>;
|
|
78
|
+
export type WorkerAppPreflightResult = {
|
|
79
|
+
appName: string;
|
|
80
|
+
creatorUsername: string;
|
|
81
|
+
warnings: string[];
|
|
82
|
+
};
|
|
83
|
+
export declare function preflightWorkerAppProject(input: WorkerAppPublishInput): Promise<WorkerAppPreflightResult>;
|
|
73
84
|
export declare function publishWorkerAppProject(input: WorkerAppPublishInput): Promise<WorkerAppPublishResult>;
|
|
74
85
|
export declare function publishStaticHtmlProject(input: StaticHtmlPublishInput): Promise<StaticHtmlPublishResult>;
|