Check capabilities before accepting a promo-video job, then generate the game's responsive thumbnails during upload. The deciding constraint is simple: resolution and duration are promises only when the selected processing path can actually satisfy them.
TL;DR: treat capability discovery as input to admission control, not as copy for a settings screen. Resolve one internal capability document, validate the requested video against it, reserve an output profile, and only then return an accepted job. Generate a small, fixed thumbnail set from the uploaded source while that source is already being inspected. This keeps page artwork ready without making every image request depend on video decoding.
How should a Node.js API check video generation capabilities?
The fragile mental model goes like this: accept a request, send it to a generator or transcoder, and learn much later that the requested duration, aspect ratio, or output size is unavailable. By then the UI has made a promise and the queue owns a job that cannot finish as specified.
The better model is a short chain. In words: client intent enters; the server resolves current capabilities; admission control picks one exact profile; processing begins; observed output is checked against that profile; publishing exposes the video and its thumbnails together. Each arrow has a result worth logging.
Reject early.
This distinction matters because a capability response and a successful render answer different questions. The first says a request is admissible under the current contract. The second proves one particular job produced an artifact that matches it.
Use an internal shape that can represent limits without borrowing one provider's vocabulary. Keep it small.
type Size = { width: number; height: number };
type VideoCapabilities = {
checkedAt: string;
source: string;
durationsSeconds: number[];
outputSizes: Size[];
};
type PromoRequest = {
gameId: string;
durationSeconds: number;
outputSize: Size;
};
type Admission =
| { accepted: true; profile: PromoRequest; capabilityCheckedAt: string }
| { accepted: false; reason: "duration_unsupported" | "size_unsupported" };
function admit(
request: PromoRequest,
capabilities: VideoCapabilities,
): Admission {
if (!capabilities.durationsSeconds.includes(request.durationSeconds)) {
return { accepted: false, reason: "duration_unsupported" };
}
const sizeAllowed = capabilities.outputSizes.some(
({ width, height }) =>
width === request.outputSize.width && height === request.outputSize.height,
);
return sizeAllowed
? {
accepted: true,
profile: request,
capabilityCheckedAt: capabilities.checkedAt,
}
: { accepted: false, reason: "size_unsupported" };
}
Notice what this code does not do. It does not silently choose a nearby duration. It does not stretch an unsupported size into existence. Product policy can offer alternatives, but the server should return those as alternatives and wait for an explicit choice. Quiet substitution makes support tickets nearly impossible to explain.
Should thumbnails be created on upload or on demand?
For a game catalog, create the standard responsive thumbnail set during upload. The source video is already present, validation is already happening, and the resulting images can be published as one coherent media package. This is the clean default when store listings, library views, and social previews use a known set of image slots.
On-demand generation still has a place. Use it for rare editorial crops or newly introduced display sizes that were not part of the upload contract. Do not put ordinary page loads on that path unless request-time decoding, duplicate work, and cold-cache latency are deliberate product choices.
The trade-off is concrete:
| Decision | Upload-time thumbnails | On-demand thumbnails |
|---|---|---|
| First view | Reads a prepared image | May trigger processing |
| Work pattern | One bounded batch per accepted upload | Work follows requested variants |
| New crop policy | Requires regeneration | Can be applied at first request |
| Failure visibility | Upload workflow owns the failure | A reader-facing request may see it |
Do not invent twenty variants because storage feels cheap. Start from actual layout slots. For example, a team may define three internal roles—poster, card, and compact list—then map each role to an allowed encoded image format and dimensions. The number three here is an example of a product contract, not a universal optimum.
Image format is part of that contract. Browsers do not support every image format equally, and format choice affects capabilities such as animation, transparency, and compression. The MDN image format guide is a useful compatibility reference; the application should still publish fallbacks that match its supported browser policy.
A copyable admission and observability boundary
Capability checks should be cached briefly enough to avoid probing an upstream system for every click, but no universal cache lifetime exists. Refresh rules depend on how the capability source changes and how quickly the application must stop offering a withdrawn profile. Record the observation time so the decision remains explainable.
Picture one concrete request moving through the boundary. A publisher selects a duration and resolution for a new game's promo clip. The API loads its capability document and either reserves that exact pair or rejects it with one reason. An accepted request carries the observation timestamp and selected profile into the durable job. The worker renders the clip, inspects the artifact, extracts frames for the standard poster, card, and compact-list roles, and validates every derivative before the package becomes visible. If a new capability document arrives while the job waits, it does not rewrite the promise already attached to that job. This longer path is intentional: every stage can say what it received, what it decided, and what it produced, so an operator can follow one correlation ID without guessing which set of limits applied.
Here is a vendor-neutral boundary for Node.js. The injected loader may read a local registry, an internal service, or an external API. The caller does not care.
type CapabilityLoader = () => Promise<VideoCapabilities>;
type MetricSink = {
increment(name: string, labels: Record<string, string>): void;
observe(name: string, value: number, labels: Record<string, string>): void;
};
async function admitPromoJob(
request: PromoRequest,
loadCapabilities: CapabilityLoader,
metrics: MetricSink,
): Promise<Admission> {
const startedAt = performance.now();
try {
const capabilities = await loadCapabilities();
const result = admit(request, capabilities);
metrics.increment("promo_admission_total", {
outcome: result.accepted ? "accepted" : "rejected",
reason: result.accepted ? "none" : result.reason,
});
return result;
} finally {
metrics.observe(
"promo_capability_check_duration_ms",
performance.now() - startedAt,
{ operation: "admission" },
);
}
}
Keep metric labels bounded. A game ID, raw dimensions, prompt text, or job ID can create an expanding set of time series; those values belong in structured logs or traces instead. Metrics should answer fleet questions: How many admissions were rejected by reason? How long does capability resolution take? Are thumbnail batches succeeding?
Logs answer the single-job question. Include a correlation ID, the selected internal profile, the capability observation timestamp, and the stage that failed. Do not log uploaded media or prompt text by default. An alert should then describe user impact, such as sustained admission-check failures or a falling thumbnail completion ratio, rather than firing on one ordinary unsupported request.
Sharp boundary. Clear signal.
No guesswork.
What if capabilities change after admission?
Bind the accepted job to the exact internal profile selected at admission. A worker should not reinterpret a queued request against a fresh capability document and quietly produce something different. If the selected path becomes unavailable, fail the job with a stable machine-readable reason and let product policy decide whether to ask for another profile or retry later.
There is an important deployment consequence: capability parsing is production code. Test it with missing fields, empty arrays, duplicated sizes, unexpected values, and stale observations. Reject malformed documents at the boundary. A permissive parser can turn an upstream schema change into false promises across every newly accepted job.
Roll out new profiles in two steps. First, teach workers and verification code to process them. Then expose them through admission control. Reversing that order creates a window where requests are accepted faster than the fleet can honor them.
The post-render check closes the loop. Read the produced artifact's duration and dimensions, compare them with the reserved profile, and publish only on a match. The same rule applies to thumbnails: decode each generated image, verify its role-specific dimensions and expected format, then make the media package visible.
Doesn't upload-time processing make uploads slower?
It adds work to the upload workflow, but it does not require holding the original upload connection open until every derivative exists. Persist the source, create a durable job, and expose explicit states such as processing, ready, and failed. Publish only when the required derivatives pass verification.
This choice moves latency to a visible preparation phase. That is usually easier to operate than hiding video-frame extraction behind the first player's page request. Still, measure both queue delay and processing time. One combined latency number cannot tell an undersized worker pool from a slow decoder.
Retries need boundaries too. Retry transient execution failures with a capped policy; do not retry an unsupported duration or size. Make each derivative write idempotent by addressing it with the upload identity, profile version, and thumbnail role. A repeated worker attempt should replace or confirm the same intended artifact, not create another public variant.
The practical rule is to promise only an admitted profile and publish only a verified media package. Capability discovery prevents an invalid promise. Output inspection catches drift. Upload-time thumbnails keep common game surfaces predictable, while an on-demand escape hatch covers uncommon future crops.
Top comments (0)