@rendobar/sdk 2.1.2 → 3.0.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/README.md +77 -3
- package/dist/index.cjs +186 -11
- package/dist/index.d.cts +602 -63
- package/dist/index.d.mts +602 -63
- package/dist/index.mjs +185 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,8 +23,10 @@ const job = await client.jobs.create({
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
// Wait for completion
|
|
26
|
+
import { outputUrl } from "@rendobar/sdk";
|
|
27
|
+
|
|
26
28
|
const result = await client.jobs.wait(job.id);
|
|
27
|
-
console.log(result
|
|
29
|
+
console.log(outputUrl(result)); // primary download/playable URL
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
## Authentication
|
|
@@ -99,6 +101,78 @@ const logs = await client.jobs.logs("job_abc");
|
|
|
99
101
|
const types = await client.jobs.types();
|
|
100
102
|
```
|
|
101
103
|
|
|
104
|
+
### Job output
|
|
105
|
+
|
|
106
|
+
Every job type returns the same output shape. A completed job carries an
|
|
107
|
+
`output` with four fields:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
type Output = {
|
|
111
|
+
data: unknown; // structured result (probe, detections, transcript), or null
|
|
112
|
+
file: OutputFile | null; // the headline file or stream manifest, or null
|
|
113
|
+
files: OutputFile[]; // every produced file (empty for data-only jobs)
|
|
114
|
+
expiresAt: number | null; // Unix ms URL expiry, set when files is non-empty
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type OutputFile = {
|
|
118
|
+
url: string; // ready-to-fetch, time-limited URL
|
|
119
|
+
path: string; // path within the job output
|
|
120
|
+
type: "video" | "image" | "audio" | "captions" | "playlist" | "data" | "other";
|
|
121
|
+
size: number; // bytes
|
|
122
|
+
meta?: { format?: string; width?: number; height?: number; durationMs?: number };
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`output` exists only on a completed job (`status === "complete"`), so narrow on
|
|
127
|
+
status before reading it.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { outputUrl, jobData } from "@rendobar/sdk";
|
|
131
|
+
|
|
132
|
+
const job = await client.jobs.wait("job_abc");
|
|
133
|
+
|
|
134
|
+
// Headline URL: the single file, or a stream manifest (.m3u8/.mpd). Returns
|
|
135
|
+
// undefined for data-only jobs and pure file sets (no single headline).
|
|
136
|
+
const url = outputUrl(job);
|
|
137
|
+
|
|
138
|
+
// Or read it yourself after a status check
|
|
139
|
+
if (job.status === "complete") {
|
|
140
|
+
console.log(job.output.file?.url);
|
|
141
|
+
|
|
142
|
+
// Iterate every produced file (frame extraction, HLS segments, sprite sets)
|
|
143
|
+
for (const f of job.output.files) {
|
|
144
|
+
console.log(f.type, f.path, f.size, f.url);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A failed job carries a structured error instead
|
|
149
|
+
if (job.status === "failed") {
|
|
150
|
+
console.log(job.error.code, job.error.message, job.error.detail);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For data jobs (`extract.metadata`, `caption.extract`, detections), the answer is
|
|
155
|
+
in `output.data`. It is `unknown` at the contract level because its shape is
|
|
156
|
+
job-type-specific. Use `jobData<T>(job)` to read it with your expected type, or
|
|
157
|
+
read `job.output.data` and narrow it yourself:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { jobData } from "@rendobar/sdk";
|
|
161
|
+
|
|
162
|
+
type Metadata = { format: string; durationMs: number; width: number; height: number };
|
|
163
|
+
|
|
164
|
+
const job = await client.jobs.wait("job_abc");
|
|
165
|
+
const meta = jobData<Metadata>(job); // Metadata | null
|
|
166
|
+
|
|
167
|
+
if (meta) {
|
|
168
|
+
console.log(meta.format, meta.durationMs);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`T` is your claim about the shape for the job type you submitted. If the input is
|
|
173
|
+
untrusted, validate `output.data` yourself (e.g. with Zod) instead of asserting a
|
|
174
|
+
type.
|
|
175
|
+
|
|
102
176
|
## Raw FFmpeg
|
|
103
177
|
|
|
104
178
|
Run arbitrary FFmpeg commands in a sandboxed container. Commands are parsed, sanitized (protocol whitelist/blacklist flags blocked), and executed with a clean process environment.
|
|
@@ -120,7 +194,7 @@ const job = await client.jobs.create({
|
|
|
120
194
|
});
|
|
121
195
|
|
|
122
196
|
const done = await client.jobs.wait(job.id);
|
|
123
|
-
console.log(done
|
|
197
|
+
console.log(outputUrl(done));
|
|
124
198
|
```
|
|
125
199
|
|
|
126
200
|
Prefer named inputs? Pass them in the top-level `inputs` map and reference the keys in the command:
|
|
@@ -258,7 +332,7 @@ All response types are exported:
|
|
|
258
332
|
|
|
259
333
|
```typescript
|
|
260
334
|
import type {
|
|
261
|
-
Job, JobStep, JobType, JobCreatedResponse,
|
|
335
|
+
Job, JobStep, Output, OutputFile, FileType, JobError, Cost, JobType, JobCreatedResponse,
|
|
262
336
|
BillingState, Transaction, UsageSummary,
|
|
263
337
|
WebhookEndpoint, WebhookDelivery,
|
|
264
338
|
Organization, ApiKey, PaginationMeta,
|
package/dist/index.cjs
CHANGED
|
@@ -303,13 +303,130 @@ function createBillingResource(request) {
|
|
|
303
303
|
}
|
|
304
304
|
//#endregion
|
|
305
305
|
//#region src/resources/uploads.ts
|
|
306
|
+
function toBlob(file) {
|
|
307
|
+
return file instanceof Blob ? file : new Blob([file]);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* PUT a body to a presigned R2 url with bounded retry.
|
|
311
|
+
*
|
|
312
|
+
* Returns the ETag string when the bucket exposes it, or `null` when the
|
|
313
|
+
* response header is absent (e.g. bucket CORS not exposing ETag). Only
|
|
314
|
+
* network/HTTP failures trigger retries; a missing ETag is not retried because
|
|
315
|
+
* it is a deterministic misconfiguration, not a transient error.
|
|
316
|
+
*/
|
|
317
|
+
async function putWithRetry(url, body, signal, attempts = 3) {
|
|
318
|
+
let lastErr;
|
|
319
|
+
for (let i = 0; i < attempts; i++) {
|
|
320
|
+
if (signal?.aborted) throw new Error("Upload aborted");
|
|
321
|
+
try {
|
|
322
|
+
const res = await fetch(url, {
|
|
323
|
+
method: "PUT",
|
|
324
|
+
body,
|
|
325
|
+
signal
|
|
326
|
+
});
|
|
327
|
+
if (!res.ok) throw new Error(`Upload PUT failed: ${res.status}`);
|
|
328
|
+
return res.headers.get("etag");
|
|
329
|
+
} catch (e) {
|
|
330
|
+
lastErr = e;
|
|
331
|
+
if (signal?.aborted) throw e;
|
|
332
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, 200 * 2 ** i));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
throw lastErr;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Run `fn` over `items` with at most `limit` in flight; preserves order.
|
|
339
|
+
* Workers stop pulling new items once `signal` is aborted, enabling fast
|
|
340
|
+
* cancellation when one part fails.
|
|
341
|
+
*/
|
|
342
|
+
async function mapLimit(items, limit, fn, signal) {
|
|
343
|
+
const out = new Array(items.length);
|
|
344
|
+
let next = 0;
|
|
345
|
+
const worker = async () => {
|
|
346
|
+
while (next < items.length) {
|
|
347
|
+
if (signal?.aborted) break;
|
|
348
|
+
const i = next++;
|
|
349
|
+
out[i] = await fn(items[i]);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const workers = Math.min(Math.max(1, limit), items.length);
|
|
353
|
+
await Promise.all(Array.from({ length: workers }, worker));
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
306
356
|
function createUploadsResource(request) {
|
|
307
|
-
return { async
|
|
308
|
-
|
|
357
|
+
return { async create(file, options) {
|
|
358
|
+
const blob = toBlob(file);
|
|
359
|
+
const size = options.size ?? blob.size;
|
|
360
|
+
if (options.signal?.aborted) throw new Error("Upload aborted");
|
|
361
|
+
const init = await (await request("/assets", {
|
|
309
362
|
method: "POST",
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
363
|
+
body: {
|
|
364
|
+
filename: options.filename,
|
|
365
|
+
size,
|
|
366
|
+
contentType: options.contentType,
|
|
367
|
+
checksum: options.checksum,
|
|
368
|
+
lifecycle: options.persist ? "persisted" : "ephemeral"
|
|
369
|
+
},
|
|
370
|
+
raw: true,
|
|
371
|
+
signal: options.signal
|
|
372
|
+
})).json();
|
|
373
|
+
if (init.status === "deduplicated") {
|
|
374
|
+
options.onProgress?.({
|
|
375
|
+
loaded: size,
|
|
376
|
+
total: size
|
|
377
|
+
});
|
|
378
|
+
return init.data;
|
|
379
|
+
}
|
|
380
|
+
if (init.status === "presigned") {
|
|
381
|
+
await putWithRetry(init.upload.url, blob, options.signal);
|
|
382
|
+
options.onProgress?.({
|
|
383
|
+
loaded: size,
|
|
384
|
+
total: size
|
|
385
|
+
});
|
|
386
|
+
return request(`/assets/${init.data.id}/complete`, {
|
|
387
|
+
method: "POST",
|
|
388
|
+
body: {},
|
|
389
|
+
signal: options.signal
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const internal = new AbortController();
|
|
393
|
+
const combinedSignal = options.signal ? AbortSignal.any([options.signal, internal.signal]) : internal.signal;
|
|
394
|
+
const { partSize, parts } = init.upload;
|
|
395
|
+
let loaded = 0;
|
|
396
|
+
let partError = null;
|
|
397
|
+
const completed = await mapLimit(parts, options.concurrency ?? 5, async (p) => {
|
|
398
|
+
const start = (p.partNumber - 1) * partSize;
|
|
399
|
+
const chunk = blob.slice(start, Math.min(start + partSize, size));
|
|
400
|
+
let etag;
|
|
401
|
+
try {
|
|
402
|
+
etag = await putWithRetry(p.url, chunk, combinedSignal);
|
|
403
|
+
} catch (e) {
|
|
404
|
+
internal.abort();
|
|
405
|
+
partError = e instanceof Error ? e : new Error(String(e));
|
|
406
|
+
throw partError;
|
|
407
|
+
}
|
|
408
|
+
if (etag === null) {
|
|
409
|
+
internal.abort();
|
|
410
|
+
const corsErr = /* @__PURE__ */ new Error("Multipart upload requires the ETag response header. Configure the R2 bucket CORS to expose 'ETag'.");
|
|
411
|
+
partError = corsErr;
|
|
412
|
+
throw corsErr;
|
|
413
|
+
}
|
|
414
|
+
loaded += chunk.size;
|
|
415
|
+
options.onProgress?.({
|
|
416
|
+
loaded,
|
|
417
|
+
total: size
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
partNumber: p.partNumber,
|
|
421
|
+
etag
|
|
422
|
+
};
|
|
423
|
+
}, combinedSignal);
|
|
424
|
+
if (partError) throw partError;
|
|
425
|
+
completed.sort((a, b) => a.partNumber - b.partNumber);
|
|
426
|
+
return request(`/assets/${init.data.id}/complete`, {
|
|
427
|
+
method: "POST",
|
|
428
|
+
body: { parts: completed },
|
|
429
|
+
signal: options.signal
|
|
313
430
|
});
|
|
314
431
|
} };
|
|
315
432
|
}
|
|
@@ -449,12 +566,32 @@ function createBatchesResource(request) {
|
|
|
449
566
|
//#endregion
|
|
450
567
|
//#region src/resources/assets.ts
|
|
451
568
|
function createAssetsResource(request) {
|
|
452
|
-
return {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
569
|
+
return {
|
|
570
|
+
async list(params, options) {
|
|
571
|
+
return request("/assets", {
|
|
572
|
+
query: params,
|
|
573
|
+
signal: options?.signal
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
async get(id, options) {
|
|
577
|
+
return request(`/assets/${id}`, { signal: options?.signal });
|
|
578
|
+
},
|
|
579
|
+
async delete(id, options) {
|
|
580
|
+
return request(`/assets/${id}`, {
|
|
581
|
+
method: "DELETE",
|
|
582
|
+
signal: options?.signal
|
|
583
|
+
});
|
|
584
|
+
},
|
|
585
|
+
async download(id, options) {
|
|
586
|
+
return request(`/assets/${id}/download`, { signal: options?.signal });
|
|
587
|
+
},
|
|
588
|
+
async templates(params, options) {
|
|
589
|
+
return request("/assets/templates", {
|
|
590
|
+
query: params,
|
|
591
|
+
signal: options?.signal
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
};
|
|
458
595
|
}
|
|
459
596
|
//#endregion
|
|
460
597
|
//#region src/resources/team.ts
|
|
@@ -646,6 +783,44 @@ function createClient(config = {}) {
|
|
|
646
783
|
};
|
|
647
784
|
}
|
|
648
785
|
//#endregion
|
|
786
|
+
//#region src/types.ts
|
|
787
|
+
/**
|
|
788
|
+
* Primary playable/download URL for a completed job, or `undefined`.
|
|
789
|
+
*
|
|
790
|
+
* Returns `output.file.url` — the single download for a file job or the playlist
|
|
791
|
+
* URL for a stream. A pure set (multiple files, no headline) or a data-only job
|
|
792
|
+
* has no single primary URL: inspect `job.output.files` / `job.output.data`.
|
|
793
|
+
*/
|
|
794
|
+
function outputUrl(job) {
|
|
795
|
+
if (job.status !== "complete") return void 0;
|
|
796
|
+
return job.output.file?.url;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Typed accessor for a completed job's structured `data`, or `null`.
|
|
800
|
+
*
|
|
801
|
+
* `output.data` is job-type-specific (probe result, detections, transcript) and
|
|
802
|
+
* is `unknown` at the contract level — its shape is documented per job type, not
|
|
803
|
+
* by the API schema. This helper applies the caller-supplied type `T` so you can
|
|
804
|
+
* read `data` without sprinkling assertions at each call site. `T` is your claim
|
|
805
|
+
* about the shape for the job type you submitted; validate it yourself (e.g. with
|
|
806
|
+
* Zod) if the input is untrusted.
|
|
807
|
+
*
|
|
808
|
+
* Returns `null` for non-complete jobs and for jobs with no structured data
|
|
809
|
+
* (pure file transforms).
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* type Metadata = { format: string; durationMs: number };
|
|
813
|
+
* const meta = jobData<Metadata>(await client.jobs.wait(id));
|
|
814
|
+
* if (meta) console.log(meta.durationMs);
|
|
815
|
+
*/
|
|
816
|
+
function jobData(job) {
|
|
817
|
+
if (job.status !== "complete") return null;
|
|
818
|
+
if (job.output.data == null) return null;
|
|
819
|
+
return job.output.data;
|
|
820
|
+
}
|
|
821
|
+
//#endregion
|
|
649
822
|
exports.ApiError = ApiError;
|
|
650
823
|
exports.createClient = createClient;
|
|
651
824
|
exports.isApiError = isApiError;
|
|
825
|
+
exports.jobData = jobData;
|
|
826
|
+
exports.outputUrl = outputUrl;
|