@videxo/sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +70 -2
- package/dist/client.d.ts +173 -0
- package/dist/client.js +331 -0
- package/dist/errors.d.ts +80 -0
- package/dist/errors.js +132 -0
- package/dist/generated/openapi.d.ts +6812 -0
- package/dist/generated/openapi.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/transport.d.ts +45 -0
- package/dist/transport.js +148 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/dist/uploads.d.ts +19 -0
- package/dist/uploads.js +42 -0
- package/dist/webhooks.d.ts +18 -0
- package/dist/webhooks.js +85 -0
- package/package.json +51 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BEO TECHNOLOGY sp. z o.o.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @videxo/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The TypeScript client of the [Videxo](https://videxo.io) API: hosted ffmpeg jobs over HTTP,
|
|
4
|
+
described as JSON operations or as an ffmpeg command. Types are generated from the API's OpenAPI
|
|
5
|
+
3.1 document; requests go through `fetch` and webhooks are verified with WebCrypto, so the package
|
|
6
|
+
has one small dependency and no Node built-ins.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i @videxo/sdk
|
|
10
|
+
# or: pnpm add @videxo/sdk, yarn add @videxo/sdk, bun add @videxo/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It is an ES module with its own type declarations. It runs on Node 20 or newer, Bun, Deno and
|
|
14
|
+
Cloudflare Workers. In a browser, use only `sendUpload()` with a `File`, `Blob` or bytes.
|
|
15
|
+
|
|
16
|
+
## Keep the API key on a server
|
|
17
|
+
|
|
18
|
+
The API key gives full access to your account. Use it only in server code (a route handler, a
|
|
19
|
+
worker, a job queue), never in code that runs in a browser or an app. A browser only sends files
|
|
20
|
+
to an upload URL your server created, with `sendUpload()`, which needs no key.
|
|
21
|
+
|
|
22
|
+
## Use it
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Videxo, VidexoError } from '@videxo/sdk';
|
|
26
|
+
|
|
27
|
+
const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
|
|
28
|
+
|
|
29
|
+
const job = await videxo.transcode({ input: 'https://example.com/talk.mp4', preset: 'web-720p' });
|
|
30
|
+
const done = await videxo.jobs.wait(job.id);
|
|
31
|
+
if (done.state === 'succeeded') {
|
|
32
|
+
const response = await videxo.outputs.download(done.id, 'video');
|
|
33
|
+
// response.body is the file
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Call | API |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `new Videxo({ apiKey, baseUrl?, fetch?, maxRetries?, timeoutMs? })` | `baseUrl` is `https://api.videxo.io` by default; throws without a key |
|
|
40
|
+
| `jobs.create(body, { idempotencyKey? })` | `POST /v1/jobs`, a command body or an operations body |
|
|
41
|
+
| `transcode`, `thumbnails`, `storyboard`, `gif`, `audio`, `concat` | `POST /v1/<operation>` |
|
|
42
|
+
| `jobs.get(id)`, `jobs.cancel(id)`, `jobs.deleteFiles(id)` | `GET /v1/jobs/{id}`, `POST .../cancel`, `DELETE .../files` |
|
|
43
|
+
| `jobs.list(params)`, `jobs.page(params)` | `GET /v1/jobs`: every job as an async iterator, or one page |
|
|
44
|
+
| `jobs.wait(id, { timeoutMs?, intervalMs?, signal? })` | polls until `succeeded`, `failed` or `cancelled` |
|
|
45
|
+
| `jobs.compile(body)` | `POST /v1/jobs/compile` |
|
|
46
|
+
| `presets.list()` | `GET /v1/presets` |
|
|
47
|
+
| `fonts.list()` | `GET /v1/fonts` |
|
|
48
|
+
| `uploads.create(params)`, `uploads.upload(data, { filename, contentType?, size? })` | `POST /v1/uploads`, then the PUT to its URL |
|
|
49
|
+
| `outputs.url(jobId, output)`, `outputs.download(jobId, output)` | `GET /v1/jobs/{id}/outputs/{alias}`: a fresh signed link, or the file |
|
|
50
|
+
| `webhooks.verify(body, headers, secret)`, `verifyWebhook(...)` | Standard Webhooks signature check; answers the event |
|
|
51
|
+
| `sendUpload(uploadUrl, data, { contentType? })` | the PUT alone, without a key |
|
|
52
|
+
|
|
53
|
+
The docs page [TypeScript SDK](https://videxo.io/docs/sdk) covers each call with examples.
|
|
54
|
+
|
|
55
|
+
## Errors
|
|
56
|
+
|
|
57
|
+
Every failed call throws a `VidexoError` with `status`, `code`, `title`, `detail`, `type`,
|
|
58
|
+
`errors`, `problems`, `retryAfter` and the raw `body` of the API's problem details answer; programs
|
|
59
|
+
branch on `code` ([error codes](https://videxo.io/docs/errors)). Without such an answer `code` is
|
|
60
|
+
one of the SDK's own, listed in `SDK_ERROR_CODES`: `network_error`, `timeout`, `aborted`,
|
|
61
|
+
`wait_timeout`, `http_error`, `invalid_response`, `output_not_found` and `output_ambiguous`. An
|
|
62
|
+
error's message never holds the API key. A webhook that fails verification throws a
|
|
63
|
+
`VidexoWebhookError`.
|
|
64
|
+
|
|
65
|
+
## Retries and idempotency
|
|
66
|
+
|
|
67
|
+
Every creation sends an `Idempotency-Key` (a random UUID unless you pass `idempotencyKey`) and
|
|
68
|
+
reuses it on retries. A request is retried after `429`, `502`, `503`, `504`, a timeout or a network
|
|
69
|
+
error, at most `maxRetries` times (2 by default), with backoff and jitter or after its
|
|
70
|
+
`Retry-After`. Other answers are not retried. `POST /v1/uploads` takes no Idempotency-Key, so it is
|
|
71
|
+
retried only after a `429`; bytes sent to an upload URL are never sent twice.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { type Client } from 'openapi-fetch';
|
|
2
|
+
import type { paths } from './generated/openapi.ts';
|
|
3
|
+
import { type RetryPolicy, type Transport } from './transport.ts';
|
|
4
|
+
import type { AudioRequest, CompileRequest, CompileResult, CompletedUpload, ConcatRequest, CreateUploadRequest, GifRequest, Job, JobList, JobRequest, ListJobsParams, FontList, PresetList, StoryboardRequest, ThumbnailsRequest, TranscodeRequest, Upload, WebhookEvent } from './types.ts';
|
|
5
|
+
import { type UploadData } from './uploads.ts';
|
|
6
|
+
import { type VerifyWebhookOptions, type WebhookHeaders } from './webhooks.ts';
|
|
7
|
+
export declare const DEFAULT_BASE_URL = "https://api.videxo.io";
|
|
8
|
+
export declare const DEFAULT_MAX_RETRIES = 2;
|
|
9
|
+
export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
10
|
+
export declare const DEFAULT_WAIT: Readonly<{
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
intervalMs: 2000;
|
|
13
|
+
}>;
|
|
14
|
+
export type VidexoOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* An API key from the dashboard (`vdx_...`). Keep it on a server, never in browser code. Typed to
|
|
17
|
+
* take an unset environment variable (`process.env.VIDEXO_API_KEY`): the constructor throws then.
|
|
18
|
+
*/
|
|
19
|
+
apiKey: string | undefined;
|
|
20
|
+
/** `https://api.videxo.io` by default. */
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
/** The `fetch` to send requests with; the runtime's own by default. */
|
|
23
|
+
fetch?(input: Request, init?: RequestInit): Promise<Response>;
|
|
24
|
+
/** Retries of a failed request (429, 502, 503, 504, a timeout, a network error); 2 by default. */
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
/** Per attempt, until the whole answer arrived (a download: its headers); 60 seconds by default. */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
};
|
|
29
|
+
export type RequestOptions = {
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
/** Overrides the client's `timeoutMs` for this call. */
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
};
|
|
34
|
+
export type CreateOptions = RequestOptions & {
|
|
35
|
+
/**
|
|
36
|
+
* Makes the creation safe to retry for 24 hours. The SDK sends a new random key when you leave it
|
|
37
|
+
* out, and reuses it on its own retries; pass your own (an order id) to also cover your retries.
|
|
38
|
+
*/
|
|
39
|
+
idempotencyKey?: string;
|
|
40
|
+
};
|
|
41
|
+
export type WaitOptions = {
|
|
42
|
+
/** How long to wait for the job to end; 10 minutes by default. */
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
/** Between two reads of the job; 2 seconds by default. */
|
|
45
|
+
intervalMs?: number;
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
};
|
|
48
|
+
export type UploadOptions = RequestOptions & {
|
|
49
|
+
/** A plain file name: letters, digits, dots, underscores and hyphens. */
|
|
50
|
+
filename: string;
|
|
51
|
+
/** The type a job sees; the blob's own type or `application/octet-stream` by default. */
|
|
52
|
+
contentType?: string;
|
|
53
|
+
/** The exact byte length; needed for a stream, read from the data otherwise. */
|
|
54
|
+
size?: number;
|
|
55
|
+
/** For sending the bytes; no timeout by default, as a large file may take a while. */
|
|
56
|
+
uploadTimeoutMs?: number;
|
|
57
|
+
};
|
|
58
|
+
type Result<T> = {
|
|
59
|
+
data?: T;
|
|
60
|
+
error?: unknown;
|
|
61
|
+
response: Response;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The Videxo API client. One instance per API key; it holds no connection and is cheap to create.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
|
|
68
|
+
* const job = await videxo.transcode({ input: 'https://example.com/talk.mp4', preset: 'web-720p' });
|
|
69
|
+
* const done = await videxo.jobs.wait(job.id);
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare class Videxo {
|
|
73
|
+
readonly baseUrl: string;
|
|
74
|
+
readonly jobs: Jobs;
|
|
75
|
+
readonly presets: Presets;
|
|
76
|
+
readonly fonts: Fonts;
|
|
77
|
+
readonly uploads: Uploads;
|
|
78
|
+
readonly outputs: Outputs;
|
|
79
|
+
readonly webhooks: Webhooks;
|
|
80
|
+
/** @internal */
|
|
81
|
+
readonly transport: Transport;
|
|
82
|
+
/** @internal */
|
|
83
|
+
readonly client: Client<paths>;
|
|
84
|
+
constructor(options: VidexoOptions);
|
|
85
|
+
/** @internal The per-request `fetch` that openapi-fetch calls: the transport with this call's policy. */
|
|
86
|
+
fetchFor(retry: RetryPolicy, options?: RequestOptions): (request: Request) => Promise<Response>;
|
|
87
|
+
/** @internal The data of a 2xx answer, or the error of any other. */
|
|
88
|
+
unwrap<T>(pending: Promise<Result<T>>): Promise<T>;
|
|
89
|
+
/** @internal A job creation at `path`, with an Idempotency-Key that its retries reuse. */
|
|
90
|
+
createAt(path: CreatePath, body: unknown, options?: CreateOptions): Promise<Job>;
|
|
91
|
+
/** `POST /v1/transcode`: one video output from `input`, from a preset or your own options. */
|
|
92
|
+
transcode(body: TranscodeRequest, options?: CreateOptions): Promise<Job>;
|
|
93
|
+
/** `POST /v1/thumbnails`: one image at `at`, or one per time in `times`. */
|
|
94
|
+
thumbnails(body: ThumbnailsRequest, options?: CreateOptions): Promise<Job>;
|
|
95
|
+
/** `POST /v1/storyboard`: frames at a fixed interval in a grid on one image. */
|
|
96
|
+
storyboard(body: StoryboardRequest, options?: CreateOptions): Promise<Job>;
|
|
97
|
+
/** `POST /v1/gif`: an animated GIF with its own palette. */
|
|
98
|
+
gif(body: GifRequest, options?: CreateOptions): Promise<Job>;
|
|
99
|
+
/** `POST /v1/audio`: the sound of `input` as an audio file. */
|
|
100
|
+
audio(body: AudioRequest, options?: CreateOptions): Promise<Job>;
|
|
101
|
+
/** `POST /v1/concat`: the `inputs` one after another, as one file. */
|
|
102
|
+
concat(body: ConcatRequest, options?: CreateOptions): Promise<Job>;
|
|
103
|
+
}
|
|
104
|
+
type CreatePath = '/v1/jobs' | '/v1/transcode' | '/v1/thumbnails' | '/v1/storyboard' | '/v1/gif' | '/v1/audio' | '/v1/concat';
|
|
105
|
+
export declare class Jobs {
|
|
106
|
+
#private;
|
|
107
|
+
constructor(videxo: Videxo);
|
|
108
|
+
/** `POST /v1/jobs`: a command body (`command`, `input_files`, `output_files`) or an operations body (`inputs`, `outputs`). */
|
|
109
|
+
create(body: JobRequest, options?: CreateOptions): Promise<Job>;
|
|
110
|
+
/** `GET /v1/jobs/{id}`. */
|
|
111
|
+
get(id: string, options?: RequestOptions): Promise<Job>;
|
|
112
|
+
/** `GET /v1/jobs`: one page, newest first; pass its `next_cursor` as `cursor` for the next. */
|
|
113
|
+
page(params?: ListJobsParams, options?: RequestOptions): Promise<JobList>;
|
|
114
|
+
/** Every job, newest first, reading page after page as you iterate (`for await (const job of videxo.jobs.list())`). */
|
|
115
|
+
list(params?: ListJobsParams, options?: RequestOptions): AsyncGenerator<Job, void, undefined>;
|
|
116
|
+
/** `POST /v1/jobs/{id}/cancel`: a queued or running job becomes cancelled; an ended one is answered as it is. */
|
|
117
|
+
cancel(id: string, options?: RequestOptions): Promise<Job>;
|
|
118
|
+
/** `DELETE /v1/jobs/{id}/files`: deletes an ended job's stored outputs now. */
|
|
119
|
+
deleteFiles(id: string, options?: RequestOptions): Promise<Job>;
|
|
120
|
+
/** `POST /v1/jobs/compile`: the spec, command and `ffmpeg_command` a job would get; no job is made. */
|
|
121
|
+
compile(body: CompileRequest, options?: RequestOptions): Promise<CompileResult>;
|
|
122
|
+
/**
|
|
123
|
+
* Reads the job until it is `succeeded`, `failed` or `cancelled` and answers it then (a failed job
|
|
124
|
+
* is answered, not thrown: see `job.error`). Waits out a 429 by its `Retry-After`. Throws
|
|
125
|
+
* `wait_timeout` when the job has not ended within `timeoutMs`, `aborted` when `signal` fires.
|
|
126
|
+
*/
|
|
127
|
+
wait(id: string, options?: WaitOptions): Promise<Job>;
|
|
128
|
+
}
|
|
129
|
+
export declare class Presets {
|
|
130
|
+
#private;
|
|
131
|
+
constructor(videxo: Videxo);
|
|
132
|
+
/** `GET /v1/presets`: every preset with its type, its full options and whether it is deprecated. */
|
|
133
|
+
list(options?: RequestOptions): Promise<PresetList>;
|
|
134
|
+
}
|
|
135
|
+
export declare class Fonts {
|
|
136
|
+
#private;
|
|
137
|
+
constructor(videxo: Videxo);
|
|
138
|
+
/** `GET /v1/fonts`: the fonts a text layer can name, with their family, weight, scripts and licence. */
|
|
139
|
+
list(options?: RequestOptions): Promise<FontList>;
|
|
140
|
+
}
|
|
141
|
+
export declare class Uploads {
|
|
142
|
+
#private;
|
|
143
|
+
constructor(videxo: Videxo);
|
|
144
|
+
/**
|
|
145
|
+
* `POST /v1/uploads`: a one-time upload URL for a private input. The route takes no
|
|
146
|
+
* Idempotency-Key, so only a 429 is retried.
|
|
147
|
+
*/
|
|
148
|
+
create(params: CreateUploadRequest, options?: RequestOptions): Promise<Upload>;
|
|
149
|
+
/**
|
|
150
|
+
* Creates an upload and sends the bytes to its URL; answers the completed upload, whose
|
|
151
|
+
* `input_url` (`videxo://uploads/<id>`) a job takes as an input.
|
|
152
|
+
*/
|
|
153
|
+
upload(data: UploadData, options: UploadOptions): Promise<CompletedUpload>;
|
|
154
|
+
}
|
|
155
|
+
export declare class Outputs {
|
|
156
|
+
#private;
|
|
157
|
+
constructor(videxo: Videxo);
|
|
158
|
+
/**
|
|
159
|
+
* A fresh signed download URL of one output (`GET /v1/jobs/{id}/outputs/{alias}`). `output` is its
|
|
160
|
+
* alias (`out_video`), or its name or file name when only one file of the job has it.
|
|
161
|
+
*/
|
|
162
|
+
url(jobId: string, output: string, options?: RequestOptions): Promise<string>;
|
|
163
|
+
/**
|
|
164
|
+
* The output's bytes as a `Response` (stream `response.body`, or read it with `arrayBuffer()`),
|
|
165
|
+
* fetched from a fresh signed URL. The API key is not sent to the file URL.
|
|
166
|
+
*/
|
|
167
|
+
download(jobId: string, output: string, options?: RequestOptions): Promise<Response>;
|
|
168
|
+
}
|
|
169
|
+
export declare class Webhooks {
|
|
170
|
+
/** Checks a delivery's Standard Webhooks signature and answers its event (`verifyWebhook`). */
|
|
171
|
+
verify(body: string | Uint8Array | ArrayBuffer, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): Promise<WebhookEvent>;
|
|
172
|
+
}
|
|
173
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import createClient, {} from 'openapi-fetch';
|
|
2
|
+
import { VidexoError, errorFromResponse, sdkError } from "./errors.js";
|
|
3
|
+
import { abortableSleep, redacted, runtimeFetch, send } from "./transport.js";
|
|
4
|
+
import { sendUpload, sizeOf } from "./uploads.js";
|
|
5
|
+
import { verifyWebhook } from "./webhooks.js";
|
|
6
|
+
export const DEFAULT_BASE_URL = 'https://api.videxo.io';
|
|
7
|
+
export const DEFAULT_MAX_RETRIES = 2;
|
|
8
|
+
export const DEFAULT_TIMEOUT_MS = 60_000;
|
|
9
|
+
export const DEFAULT_WAIT = Object.freeze({ timeoutMs: 10 * 60_000, intervalMs: 2_000 });
|
|
10
|
+
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
|
|
11
|
+
const OUTPUT_ALIAS = /^out_[A-Za-z0-9_]{1,48}$/;
|
|
12
|
+
/** What an API key may hold: printable ASCII without spaces (the `Authorization` header carries it as is). */
|
|
13
|
+
const API_KEY_PATTERN = /^[\x21-\x7e]+$/;
|
|
14
|
+
/**
|
|
15
|
+
* The Videxo API client. One instance per API key; it holds no connection and is cheap to create.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
|
|
19
|
+
* const job = await videxo.transcode({ input: 'https://example.com/talk.mp4', preset: 'web-720p' });
|
|
20
|
+
* const done = await videxo.jobs.wait(job.id);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export class Videxo {
|
|
24
|
+
baseUrl;
|
|
25
|
+
jobs;
|
|
26
|
+
presets;
|
|
27
|
+
fonts;
|
|
28
|
+
uploads;
|
|
29
|
+
outputs;
|
|
30
|
+
webhooks;
|
|
31
|
+
/** @internal */
|
|
32
|
+
transport;
|
|
33
|
+
/** @internal */
|
|
34
|
+
client;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
const apiKey = typeof options?.apiKey === 'string' ? options.apiKey.trim() : '';
|
|
37
|
+
if (!apiKey)
|
|
38
|
+
throw new TypeError('new Videxo() needs an apiKey: an API key from the dashboard, kept on your server.');
|
|
39
|
+
// Checked here so that no runtime refuses the Authorization header later with the key in its message.
|
|
40
|
+
if (!API_KEY_PATTERN.test(apiKey))
|
|
41
|
+
throw new TypeError('The apiKey is not valid: an API key has only printable ASCII characters, without spaces or line breaks.');
|
|
42
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
43
|
+
if (!/^https?:\/\//.test(baseUrl))
|
|
44
|
+
throw new TypeError('baseUrl must be an http or https URL.');
|
|
45
|
+
const userFetch = options.fetch;
|
|
46
|
+
this.baseUrl = baseUrl;
|
|
47
|
+
this.transport = {
|
|
48
|
+
fetch: userFetch ? (input, init) => userFetch(input, init) : runtimeFetch,
|
|
49
|
+
maxRetries: Math.max(0, Math.floor(options.maxRetries ?? DEFAULT_MAX_RETRIES)),
|
|
50
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
51
|
+
redact: (text) => text.replaceAll(apiKey, '[api key]'),
|
|
52
|
+
};
|
|
53
|
+
this.client = createClient({ baseUrl, headers: { Authorization: `Bearer ${apiKey}` } });
|
|
54
|
+
this.jobs = new Jobs(this);
|
|
55
|
+
this.presets = new Presets(this);
|
|
56
|
+
this.fonts = new Fonts(this);
|
|
57
|
+
this.uploads = new Uploads(this);
|
|
58
|
+
this.outputs = new Outputs(this);
|
|
59
|
+
this.webhooks = new Webhooks();
|
|
60
|
+
}
|
|
61
|
+
/** @internal The per-request `fetch` that openapi-fetch calls: the transport with this call's policy. */
|
|
62
|
+
fetchFor(retry, options = {}) {
|
|
63
|
+
return (request) => send(this.transport, request, { retry, signal: options.signal, timeoutMs: options.timeoutMs, buffer: true });
|
|
64
|
+
}
|
|
65
|
+
/** @internal The data of a 2xx answer, or the error of any other. */
|
|
66
|
+
async unwrap(pending) {
|
|
67
|
+
let result;
|
|
68
|
+
try {
|
|
69
|
+
result = await pending;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof SyntaxError)
|
|
73
|
+
throw sdkError('invalid_response', 'The API answered with a body that is not JSON.', { cause: error });
|
|
74
|
+
throw redacted(this.transport, error);
|
|
75
|
+
}
|
|
76
|
+
if (!result.response.ok)
|
|
77
|
+
throw errorFromResponse(result.response, result.error);
|
|
78
|
+
if (result.data === undefined)
|
|
79
|
+
throw sdkError('invalid_response', 'The API answered without a body.', { status: result.response.status });
|
|
80
|
+
return result.data;
|
|
81
|
+
}
|
|
82
|
+
/** @internal A job creation at `path`, with an Idempotency-Key that its retries reuse. */
|
|
83
|
+
createAt(path, body, options = {}) {
|
|
84
|
+
const key = options.idempotencyKey ?? crypto.randomUUID();
|
|
85
|
+
return this.unwrap(this.client.POST(path, {
|
|
86
|
+
body: body,
|
|
87
|
+
params: { header: { 'idempotency-key': key } },
|
|
88
|
+
fetch: this.fetchFor('safe', options),
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
/** `POST /v1/transcode`: one video output from `input`, from a preset or your own options. */
|
|
92
|
+
transcode(body, options) {
|
|
93
|
+
return this.createAt('/v1/transcode', body, options);
|
|
94
|
+
}
|
|
95
|
+
/** `POST /v1/thumbnails`: one image at `at`, or one per time in `times`. */
|
|
96
|
+
thumbnails(body, options) {
|
|
97
|
+
return this.createAt('/v1/thumbnails', body, options);
|
|
98
|
+
}
|
|
99
|
+
/** `POST /v1/storyboard`: frames at a fixed interval in a grid on one image. */
|
|
100
|
+
storyboard(body, options) {
|
|
101
|
+
return this.createAt('/v1/storyboard', body, options);
|
|
102
|
+
}
|
|
103
|
+
/** `POST /v1/gif`: an animated GIF with its own palette. */
|
|
104
|
+
gif(body, options) {
|
|
105
|
+
return this.createAt('/v1/gif', body, options);
|
|
106
|
+
}
|
|
107
|
+
/** `POST /v1/audio`: the sound of `input` as an audio file. */
|
|
108
|
+
audio(body, options) {
|
|
109
|
+
return this.createAt('/v1/audio', body, options);
|
|
110
|
+
}
|
|
111
|
+
/** `POST /v1/concat`: the `inputs` one after another, as one file. */
|
|
112
|
+
concat(body, options) {
|
|
113
|
+
return this.createAt('/v1/concat', body, options);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export class Jobs {
|
|
117
|
+
#videxo;
|
|
118
|
+
constructor(videxo) {
|
|
119
|
+
this.#videxo = videxo;
|
|
120
|
+
}
|
|
121
|
+
/** `POST /v1/jobs`: a command body (`command`, `input_files`, `output_files`) or an operations body (`inputs`, `outputs`). */
|
|
122
|
+
create(body, options) {
|
|
123
|
+
return this.#videxo.createAt('/v1/jobs', body, options);
|
|
124
|
+
}
|
|
125
|
+
/** `GET /v1/jobs/{id}`. */
|
|
126
|
+
get(id, options = {}) {
|
|
127
|
+
const videxo = this.#videxo;
|
|
128
|
+
return videxo.unwrap(videxo.client.GET('/v1/jobs/{id}', { params: { path: { id } }, fetch: videxo.fetchFor('safe', options) }));
|
|
129
|
+
}
|
|
130
|
+
/** `GET /v1/jobs`: one page, newest first; pass its `next_cursor` as `cursor` for the next. */
|
|
131
|
+
page(params = {}, options = {}) {
|
|
132
|
+
const videxo = this.#videxo;
|
|
133
|
+
return videxo.unwrap(videxo.client.GET('/v1/jobs', { params: { query: params }, fetch: videxo.fetchFor('safe', options) }));
|
|
134
|
+
}
|
|
135
|
+
/** Every job, newest first, reading page after page as you iterate (`for await (const job of videxo.jobs.list())`). */
|
|
136
|
+
async *list(params = {}, options = {}) {
|
|
137
|
+
let cursor = params.cursor;
|
|
138
|
+
for (;;) {
|
|
139
|
+
const page = await this.page({ ...params, ...(cursor === undefined ? {} : { cursor }) }, options);
|
|
140
|
+
yield* page.data;
|
|
141
|
+
if (!page.next_cursor)
|
|
142
|
+
return;
|
|
143
|
+
cursor = page.next_cursor;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** `POST /v1/jobs/{id}/cancel`: a queued or running job becomes cancelled; an ended one is answered as it is. */
|
|
147
|
+
cancel(id, options = {}) {
|
|
148
|
+
const videxo = this.#videxo;
|
|
149
|
+
return videxo.unwrap(videxo.client.POST('/v1/jobs/{id}/cancel', { params: { path: { id } }, fetch: videxo.fetchFor('safe', options) }));
|
|
150
|
+
}
|
|
151
|
+
/** `DELETE /v1/jobs/{id}/files`: deletes an ended job's stored outputs now. */
|
|
152
|
+
deleteFiles(id, options = {}) {
|
|
153
|
+
const videxo = this.#videxo;
|
|
154
|
+
return videxo.unwrap(videxo.client.DELETE('/v1/jobs/{id}/files', { params: { path: { id } }, fetch: videxo.fetchFor('safe', options) }));
|
|
155
|
+
}
|
|
156
|
+
/** `POST /v1/jobs/compile`: the spec, command and `ffmpeg_command` a job would get; no job is made. */
|
|
157
|
+
compile(body, options = {}) {
|
|
158
|
+
const videxo = this.#videxo;
|
|
159
|
+
return videxo.unwrap(videxo.client.POST('/v1/jobs/compile', { body, fetch: videxo.fetchFor('safe', options) }));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Reads the job until it is `succeeded`, `failed` or `cancelled` and answers it then (a failed job
|
|
163
|
+
* is answered, not thrown: see `job.error`). Waits out a 429 by its `Retry-After`. Throws
|
|
164
|
+
* `wait_timeout` when the job has not ended within `timeoutMs`, `aborted` when `signal` fires.
|
|
165
|
+
*/
|
|
166
|
+
async wait(id, options = {}) {
|
|
167
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT.timeoutMs;
|
|
168
|
+
const intervalMs = options.intervalMs ?? DEFAULT_WAIT.intervalMs;
|
|
169
|
+
const sleep = this.#videxo.transport.sleep ?? abortableSleep;
|
|
170
|
+
// The deadline also stops a read in flight and the retry pauses inside it (a 429's Retry-After).
|
|
171
|
+
const deadline = new AbortController();
|
|
172
|
+
const onAbort = () => deadline.abort(options.signal?.reason);
|
|
173
|
+
if (options.signal?.aborted)
|
|
174
|
+
onAbort();
|
|
175
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
176
|
+
const timer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
177
|
+
let job;
|
|
178
|
+
const timedOut = () => sdkError('wait_timeout', `Job ${id} did not end within ${timeoutMs} ms.`, { body: job });
|
|
179
|
+
try {
|
|
180
|
+
for (;;) {
|
|
181
|
+
let pause = intervalMs;
|
|
182
|
+
try {
|
|
183
|
+
job = await this.get(id, { signal: deadline.signal });
|
|
184
|
+
if (TERMINAL.has(job.state))
|
|
185
|
+
return job;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (!(error instanceof VidexoError && error.status === 429))
|
|
189
|
+
throw error;
|
|
190
|
+
pause = Math.max(intervalMs, (error.retryAfter ?? 0) * 1000);
|
|
191
|
+
}
|
|
192
|
+
if (deadline.signal.aborted)
|
|
193
|
+
throw timedOut();
|
|
194
|
+
await sleep(pause, deadline.signal);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (error instanceof VidexoError && error.code === 'aborted' && !options.signal?.aborted)
|
|
199
|
+
throw timedOut();
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export class Presets {
|
|
209
|
+
#videxo;
|
|
210
|
+
constructor(videxo) {
|
|
211
|
+
this.#videxo = videxo;
|
|
212
|
+
}
|
|
213
|
+
/** `GET /v1/presets`: every preset with its type, its full options and whether it is deprecated. */
|
|
214
|
+
list(options = {}) {
|
|
215
|
+
const videxo = this.#videxo;
|
|
216
|
+
return videxo.unwrap(videxo.client.GET('/v1/presets', { fetch: videxo.fetchFor('safe', options) }));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export class Fonts {
|
|
220
|
+
#videxo;
|
|
221
|
+
constructor(videxo) {
|
|
222
|
+
this.#videxo = videxo;
|
|
223
|
+
}
|
|
224
|
+
/** `GET /v1/fonts`: the fonts a text layer can name, with their family, weight, scripts and licence. */
|
|
225
|
+
list(options = {}) {
|
|
226
|
+
const videxo = this.#videxo;
|
|
227
|
+
return videxo.unwrap(videxo.client.GET('/v1/fonts', { fetch: videxo.fetchFor('safe', options) }));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export class Uploads {
|
|
231
|
+
#videxo;
|
|
232
|
+
constructor(videxo) {
|
|
233
|
+
this.#videxo = videxo;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* `POST /v1/uploads`: a one-time upload URL for a private input. The route takes no
|
|
237
|
+
* Idempotency-Key, so only a 429 is retried.
|
|
238
|
+
*/
|
|
239
|
+
create(params, options = {}) {
|
|
240
|
+
const videxo = this.#videxo;
|
|
241
|
+
return videxo.unwrap(videxo.client.POST('/v1/uploads', { body: params, fetch: videxo.fetchFor('rate_limit', options) }));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Creates an upload and sends the bytes to its URL; answers the completed upload, whose
|
|
245
|
+
* `input_url` (`videxo://uploads/<id>`) a job takes as an input.
|
|
246
|
+
*/
|
|
247
|
+
async upload(data, options) {
|
|
248
|
+
const size = options.size ?? sizeOf(data);
|
|
249
|
+
if (size === null)
|
|
250
|
+
throw new TypeError('uploads.upload() needs options.size for a stream.');
|
|
251
|
+
const blobType = typeof Blob !== 'undefined' && data instanceof Blob ? data.type : '';
|
|
252
|
+
const contentType = options.contentType ?? (blobType || 'application/octet-stream');
|
|
253
|
+
const upload = await this.create({ filename: options.filename, size_bytes: size, content_type: contentType }, options);
|
|
254
|
+
return sendUpload(upload.upload_url, data, {
|
|
255
|
+
contentType,
|
|
256
|
+
fetch: this.#videxo.transport.fetch,
|
|
257
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
258
|
+
...(options.uploadTimeoutMs === undefined ? {} : { timeoutMs: options.uploadTimeoutMs }),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export class Outputs {
|
|
263
|
+
#videxo;
|
|
264
|
+
constructor(videxo) {
|
|
265
|
+
this.#videxo = videxo;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* A fresh signed download URL of one output (`GET /v1/jobs/{id}/outputs/{alias}`). `output` is its
|
|
269
|
+
* alias (`out_video`), or its name or file name when only one file of the job has it.
|
|
270
|
+
*/
|
|
271
|
+
async url(jobId, output, options = {}) {
|
|
272
|
+
const videxo = this.#videxo;
|
|
273
|
+
const alias = await this.#alias(jobId, output, options);
|
|
274
|
+
const pending = videxo.client.GET('/v1/jobs/{id}/outputs/{alias}', {
|
|
275
|
+
params: { path: { id: jobId, alias } },
|
|
276
|
+
redirect: 'manual',
|
|
277
|
+
parseAs: 'text',
|
|
278
|
+
fetch: videxo.fetchFor('safe', options),
|
|
279
|
+
});
|
|
280
|
+
const { response, error } = await pending.catch((failure) => {
|
|
281
|
+
throw redacted(videxo.transport, failure);
|
|
282
|
+
});
|
|
283
|
+
const location = response.headers.get('location');
|
|
284
|
+
if (response.status >= 300 && response.status < 400 && location)
|
|
285
|
+
return new URL(location, videxo.baseUrl).href;
|
|
286
|
+
if (!response.ok && response.type !== 'opaqueredirect')
|
|
287
|
+
throw errorFromResponse(response, error);
|
|
288
|
+
throw sdkError('invalid_response', 'The API did not answer with the download URL.', { status: response.status });
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The output's bytes as a `Response` (stream `response.body`, or read it with `arrayBuffer()`),
|
|
292
|
+
* fetched from a fresh signed URL. The API key is not sent to the file URL.
|
|
293
|
+
*/
|
|
294
|
+
async download(jobId, output, options = {}) {
|
|
295
|
+
const url = await this.url(jobId, output, options);
|
|
296
|
+
const response = await send(this.#videxo.transport, new Request(url), {
|
|
297
|
+
retry: 'safe',
|
|
298
|
+
signal: options.signal,
|
|
299
|
+
timeoutMs: options.timeoutMs,
|
|
300
|
+
buffer: false,
|
|
301
|
+
});
|
|
302
|
+
if (response.ok)
|
|
303
|
+
return response;
|
|
304
|
+
const text = await response.text();
|
|
305
|
+
let body = text;
|
|
306
|
+
try {
|
|
307
|
+
body = JSON.parse(text);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Not JSON: kept as text.
|
|
311
|
+
}
|
|
312
|
+
throw errorFromResponse(response, body);
|
|
313
|
+
}
|
|
314
|
+
async #alias(jobId, output, options) {
|
|
315
|
+
if (OUTPUT_ALIAS.test(output))
|
|
316
|
+
return output;
|
|
317
|
+
const job = await this.#videxo.jobs.get(jobId, options);
|
|
318
|
+
const aliases = [...new Set(job.outputs.filter((file) => file.name === output || file.filename === output).map((file) => file.alias))];
|
|
319
|
+
if (aliases.length === 1)
|
|
320
|
+
return aliases[0];
|
|
321
|
+
if (aliases.length === 0)
|
|
322
|
+
throw sdkError('output_not_found', `Job ${jobId} has no output named ${output}.`);
|
|
323
|
+
throw sdkError('output_ambiguous', `Several files of job ${jobId} are named ${output}: ${aliases.join(', ')}.`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export class Webhooks {
|
|
327
|
+
/** Checks a delivery's Standard Webhooks signature and answers its event (`verifyWebhook`). */
|
|
328
|
+
verify(body, headers, secret, options) {
|
|
329
|
+
return verifyWebhook(body, headers, secret, options);
|
|
330
|
+
}
|
|
331
|
+
}
|