@playdrop/playdrop-cli 0.13.17 → 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
CHANGED
|
@@ -44,6 +44,10 @@ type HostedGameMeasurement = {
|
|
|
44
44
|
devicePixelRatio: number;
|
|
45
45
|
iframeRect: CaptureRect;
|
|
46
46
|
};
|
|
47
|
+
type BrowserWindowBounds = {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
};
|
|
47
51
|
type CropRect = {
|
|
48
52
|
x: number;
|
|
49
53
|
y: number;
|
|
@@ -141,8 +145,13 @@ export declare function resolveListingCaptureBrowserContextOptions(surface: AppS
|
|
|
141
145
|
height: number;
|
|
142
146
|
}): BrowserContextOptions;
|
|
143
147
|
export declare function assertExportedListingAudio(value: unknown): ExportedListingAudio;
|
|
148
|
+
export declare function assertListingCaptureWindowCanContainViewport(measurement: HostedGameMeasurement, windowBounds: BrowserWindowBounds): void;
|
|
144
149
|
export declare function computeRecordedCrop(measurement: HostedGameMeasurement, recorder: ListingRecorderMetadata, rawWidth: number, rawHeight: number): CropRect;
|
|
145
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
|
+
};
|
|
146
155
|
export declare function captureCommandMatchesExceptPoster(capture: ListingCaptureSurfaceReport, surface: AppSurface, parsedOptions: ParsedCaptureListingOptions): boolean;
|
|
147
156
|
export declare function resolveCurrentCaptureReuseAction(captures: ListingCaptureSurfaceReport[], surfaces: AppSurface[], parsedOptions: ParsedCaptureListingOptions): 'reuse' | 'refresh_poster';
|
|
148
157
|
export declare function captureListing(targetArg: string | undefined, options?: CaptureListingOptions): Promise<void>;
|
|
@@ -47,8 +47,10 @@ exports.resolveWorkerCaptureOutputDir = resolveWorkerCaptureOutputDir;
|
|
|
47
47
|
exports.resolveCaptureDimensions = resolveCaptureDimensions;
|
|
48
48
|
exports.resolveListingCaptureBrowserContextOptions = resolveListingCaptureBrowserContextOptions;
|
|
49
49
|
exports.assertExportedListingAudio = assertExportedListingAudio;
|
|
50
|
+
exports.assertListingCaptureWindowCanContainViewport = assertListingCaptureWindowCanContainViewport;
|
|
50
51
|
exports.computeRecordedCrop = computeRecordedCrop;
|
|
51
52
|
exports.runListingRecorder = runListingRecorder;
|
|
53
|
+
exports.formatCommandError = formatCommandError;
|
|
52
54
|
exports.captureCommandMatchesExceptPoster = captureCommandMatchesExceptPoster;
|
|
53
55
|
exports.resolveCurrentCaptureReuseAction = resolveCurrentCaptureReuseAction;
|
|
54
56
|
exports.captureListing = captureListing;
|
|
@@ -501,11 +503,6 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
|
|
|
501
503
|
const cdpSession = await page.context().newCDPSession(page);
|
|
502
504
|
let measurement = await waitForHostedGameMeasurement(page, 15000);
|
|
503
505
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
504
|
-
const deltaWidth = requestedWidth - Math.round(measurement.iframeRect.width);
|
|
505
|
-
const deltaHeight = requestedHeight - Math.round(measurement.iframeRect.height);
|
|
506
|
-
if (Math.abs(deltaWidth) <= 1 && Math.abs(deltaHeight) <= 1) {
|
|
507
|
-
return measurement;
|
|
508
|
-
}
|
|
509
506
|
const windowState = await cdpSession.send('Browser.getWindowForTarget');
|
|
510
507
|
const currentWidth = typeof windowState.bounds.width === 'number'
|
|
511
508
|
? windowState.bounds.width
|
|
@@ -513,6 +510,15 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
|
|
|
513
510
|
const currentHeight = typeof windowState.bounds.height === 'number'
|
|
514
511
|
? windowState.bounds.height
|
|
515
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
|
+
}
|
|
516
522
|
const nextWidth = Math.max(400, currentWidth + deltaWidth);
|
|
517
523
|
const nextHeight = Math.max(300, currentHeight + deltaHeight);
|
|
518
524
|
await cdpSession.send('Browser.setWindowBounds', {
|
|
@@ -527,6 +533,15 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
|
|
|
527
533
|
}
|
|
528
534
|
throw new Error('hosted_game_resize_failed');
|
|
529
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
|
+
}
|
|
530
545
|
function roundCropValue(value) {
|
|
531
546
|
return Math.round(value);
|
|
532
547
|
}
|
|
@@ -726,6 +741,13 @@ function formatCommandError(error) {
|
|
|
726
741
|
suggestions: ['Fix or remove the incomplete canonical capture directory, then record the runtime once.'],
|
|
727
742
|
};
|
|
728
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
|
+
}
|
|
729
751
|
if (error.message.startsWith('listing_recorder_failed:')) {
|
|
730
752
|
return {
|
|
731
753
|
message: error.message.slice('listing_recorder_failed:'.length),
|
|
@@ -1195,14 +1217,14 @@ async function captureListing(targetArg, options = {}) {
|
|
|
1195
1217
|
await prepareHostedListingScene(browserHandle.page, captureSceneId);
|
|
1196
1218
|
await browserHandle.page.waitForTimeout(1000);
|
|
1197
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)}.`);
|
|
1198
1222
|
if (shouldExportPreviewAudio) {
|
|
1199
1223
|
console.log('[listing] Checking the in-app preview audio export contract.');
|
|
1200
1224
|
await assertHostedListingAudioCaptureContract(browserHandle.page, outputPaths.outputDir);
|
|
1201
1225
|
console.log('[listing] Starting in-app preview audio capture.');
|
|
1202
1226
|
await startHostedListingAudioCapture(browserHandle.page);
|
|
1203
1227
|
}
|
|
1204
|
-
const measurement = await fitWindowToRequestedGameplay(browserHandle.page, dimensions.width, dimensions.height);
|
|
1205
|
-
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)}.`);
|
|
1206
1228
|
await browserHandle.page.waitForTimeout(750);
|
|
1207
1229
|
console.log('[listing] Waiting for the machine capture lease.');
|
|
1208
1230
|
const captureLease = await (0, listingCaptureLease_1.acquireListingCaptureLease)({
|
package/dist/commands/worker.js
CHANGED
|
@@ -2584,7 +2584,8 @@ function buildAgentFailureCode(agent, result, request) {
|
|
|
2584
2584
|
if (providerStatus === 529 || (/\b529\b/.test(combinedOutput) && /\boverloaded?\b/i.test(combinedOutput))) {
|
|
2585
2585
|
return `agent_provider_overloaded:${provider}:529`;
|
|
2586
2586
|
}
|
|
2587
|
-
if (/\boverloaded?\b/i.test(combinedOutput)
|
|
2587
|
+
if (/\boverloaded?\b/i.test(combinedOutput)
|
|
2588
|
+
|| /\b(?:selected\s+)?model\s+is\s+at\s+capacity\b/i.test(combinedOutput)) {
|
|
2588
2589
|
return `agent_provider_overloaded:${provider}`;
|
|
2589
2590
|
}
|
|
2590
2591
|
if (providerStatus === 429
|
|
@@ -5427,7 +5428,10 @@ async function materialTask(options) {
|
|
|
5427
5428
|
}
|
|
5428
5429
|
catch (error) {
|
|
5429
5430
|
const message = error instanceof Error ? error.message : String(error);
|
|
5430
|
-
|
|
5431
|
+
const suggestion = message.startsWith('task_material_asset_background_not_removed:')
|
|
5432
|
+
? ' Remove the background and retry with --asset, or omit --asset if the image is honestly useful as a full frame.'
|
|
5433
|
+
: '';
|
|
5434
|
+
console.warn(`Warning: task material was not shared: ${message}.${suggestion}`);
|
|
5431
5435
|
node_process_1.default.exitCode = 0;
|
|
5432
5436
|
}
|
|
5433
5437
|
}
|
package/dist/listingPreflight.js
CHANGED
|
@@ -117,13 +117,13 @@ function assertListingScreenshots(task) {
|
|
|
117
117
|
const portraitPaths = task.listing?.screenshotPortraitPaths ?? [];
|
|
118
118
|
const landscapePaths = task.listing?.screenshotLandscapePaths ?? [];
|
|
119
119
|
if (portraitPaths.length === 0 && landscapePaths.length === 0) {
|
|
120
|
-
throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG
|
|
120
|
+
throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG marketing images.');
|
|
121
121
|
}
|
|
122
122
|
if (task.surfaceTargets.includes('MOBILE_PORTRAIT') && portraitPaths.length === 0) {
|
|
123
|
-
throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG
|
|
123
|
+
throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG marketing image.');
|
|
124
124
|
}
|
|
125
125
|
if ((task.surfaceTargets.includes('DESKTOP') || task.surfaceTargets.includes('MOBILE_LANDSCAPE')) && landscapePaths.length === 0) {
|
|
126
|
-
throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG
|
|
126
|
+
throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG marketing image.');
|
|
127
127
|
}
|
|
128
128
|
for (const filePath of portraitPaths) {
|
|
129
129
|
assertScreenshotPath(task, filePath, 'listing.screenshotsPortrait');
|
|
@@ -137,10 +137,8 @@ function assertListingScreenshots(task) {
|
|
|
137
137
|
function isSha256Hex(value) {
|
|
138
138
|
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
|
|
139
139
|
}
|
|
140
|
-
function
|
|
141
|
-
return fileKey.startsWith('
|
|
142
|
-
|| fileKey.startsWith('screenshotsLandscape:')
|
|
143
|
-
|| fileKey.startsWith('videosPortrait:')
|
|
140
|
+
function isCaptureVideoFileKey(fileKey) {
|
|
141
|
+
return fileKey.startsWith('videosPortrait:')
|
|
144
142
|
|| fileKey.startsWith('videosLandscape:');
|
|
145
143
|
}
|
|
146
144
|
function readMediaCaptureProof(input) {
|
|
@@ -175,20 +173,21 @@ function readMediaCaptureProof(input) {
|
|
|
175
173
|
if (!rawArtifactHashes || typeof rawArtifactHashes !== 'object' || Array.isArray(rawArtifactHashes)) {
|
|
176
174
|
throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must be an object.`);
|
|
177
175
|
}
|
|
178
|
-
const captureArtifactHashEntries = Object.
|
|
179
|
-
.
|
|
176
|
+
const captureArtifactHashEntries = Object.entries(rawArtifactHashes)
|
|
177
|
+
.filter(([artifactKey]) => (0, node_path_1.extname)(artifactKey).toLowerCase() === '.mp4')
|
|
178
|
+
.map(([, value]) => typeof value === 'string' ? value.trim().toLowerCase() : '')
|
|
180
179
|
.filter((value) => isSha256Hex(value));
|
|
181
180
|
const captureArtifactHashes = new Set(captureArtifactHashEntries);
|
|
182
181
|
if (captureArtifactHashEntries.length === 0) {
|
|
183
|
-
throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes
|
|
182
|
+
throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must include at least one MP4 gameplay video.`);
|
|
184
183
|
}
|
|
185
184
|
const captureArtifactHashCounts = new Map();
|
|
186
185
|
for (const hash of captureArtifactHashEntries) {
|
|
187
186
|
captureArtifactHashCounts.set(hash, (captureArtifactHashCounts.get(hash) ?? 0) + 1);
|
|
188
187
|
}
|
|
189
|
-
const mediaFiles = input.preparedSessionFiles.filter((file) =>
|
|
188
|
+
const mediaFiles = input.preparedSessionFiles.filter((file) => isCaptureVideoFileKey(file.fileKey));
|
|
190
189
|
if (mediaFiles.length === 0) {
|
|
191
|
-
throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder
|
|
190
|
+
throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder gameplay video in listing.videos*.`);
|
|
192
191
|
}
|
|
193
192
|
const uploadedMediaHashCounts = new Map();
|
|
194
193
|
for (const file of mediaFiles) {
|
|
@@ -197,21 +196,19 @@ function readMediaCaptureProof(input) {
|
|
|
197
196
|
const missingUploadedCaptureArtifacts = [...captureArtifactHashCounts.entries()]
|
|
198
197
|
.filter(([hash, expectedCount]) => (uploadedMediaHashCounts.get(hash) ?? 0) < expectedCount);
|
|
199
198
|
if (missingUploadedCaptureArtifacts.length > 0) {
|
|
200
|
-
throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder
|
|
199
|
+
throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder gameplay video from listing.captureReport in catalogue.json.`);
|
|
201
200
|
}
|
|
202
|
-
let hasStill = false;
|
|
203
201
|
let hasVideo = false;
|
|
204
202
|
const artifactHashes = {};
|
|
205
203
|
for (const file of mediaFiles) {
|
|
206
204
|
if (!captureArtifactHashes.has(file.sha256)) {
|
|
207
205
|
throw new Error(`agent_task_media_capture_artifact_mismatch: ${file.fileKey} was not produced by listing.captureReport for ${input.task.name}.`);
|
|
208
206
|
}
|
|
209
|
-
hasStill = hasStill || file.fileKey.startsWith('screenshots');
|
|
210
207
|
hasVideo = hasVideo || file.fileKey.startsWith('videos');
|
|
211
208
|
artifactHashes[file.fileKey] = file.sha256;
|
|
212
209
|
}
|
|
213
|
-
if (!
|
|
214
|
-
throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder
|
|
210
|
+
if (!hasVideo) {
|
|
211
|
+
throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder gameplay video.`);
|
|
215
212
|
}
|
|
216
213
|
const capturedSurfaces = Array.isArray(binding.capturedSurfaces)
|
|
217
214
|
? Array.from(new Set(binding.capturedSurfaces
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playdrop/playdrop-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Official Playdrop CLI for publishing browser games, creator apps, and AI-generated game assets on playdrop.ai",
|
|
5
5
|
"homepage": "https://www.playdrop.ai",
|
|
6
6
|
"repository": {
|