@transcodely/sdk 0.3.7 → 0.3.8
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/CHANGELOG.md +16 -0
- package/README.md +220 -2
- package/dist/index.cjs +3723 -807
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2049 -102
- package/dist/index.d.ts +2049 -102
- package/dist/index.mjs +3707 -808
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to the Transcodely JavaScript / TypeScript SDK will be docum
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Breaking changes are allowed on minor bumps until 1.0.0.
|
|
6
6
|
|
|
7
|
+
## [0.3.8](https://github.com/transcodely/transcodely-js/compare/v0.3.7...v0.3.8) (2026-09-15)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **proto:** sync content-analysis report (per-title) ([#68](https://github.com/transcodely/transcodely-js/issues/68)) ([e010f10](https://github.com/transcodely/transcodely-js/commit/e010f1010e78ad86cace86a3b67216669198cb39))
|
|
13
|
+
* **proto:** sync IngestRuleService (ingest rules) ([#63](https://github.com/transcodely/transcodely-js/issues/63)) ([af8992a](https://github.com/transcodely/transcodely-js/commit/af8992ade9d01ccaa314869198a20dab54e7af16))
|
|
14
|
+
* **proto:** sync OutputReport (job output facts + verdict) ([#61](https://github.com/transcodely/transcodely-js/issues/61)) ([4bf7f6c](https://github.com/transcodely/transcodely-js/commit/4bf7f6c2c26c462f62b2f269f93c22f5c94fcd2c))
|
|
15
|
+
* **proto:** sync protos to api 5.21.0 — optional app_id, per-title accepted, rule clear flags ([#69](https://github.com/transcodely/transcodely-js/issues/69)) ([1ad04ae](https://github.com/transcodely/transcodely-js/commit/1ad04aefc4ec6851b76e38adcea02ee78ded1043))
|
|
16
|
+
* **uploads:** client.uploads.putFile + the npx transcodely CLI ([#64](https://github.com/transcodely/transcodely-js/issues/64)) ([e3e7d58](https://github.com/transcodely/transcodely-js/commit/e3e7d58756492183c966e2eeb3ece4e4eaf7fc6a))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **types:** export HealthStatus and guard every enum against proto drift ([#67](https://github.com/transcodely/transcodely-js/issues/67)) ([d30da94](https://github.com/transcodely/transcodely-js/commit/d30da9462fbf5a68ac18884c5990de93b3c0d2f2))
|
|
22
|
+
|
|
7
23
|
## [0.3.7](https://github.com/transcodely/transcodely-js/compare/v0.3.6...v0.3.7) (2026-08-16)
|
|
8
24
|
|
|
9
25
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @transcodely/sdk
|
|
2
2
|
|
|
3
|
-
Official TypeScript / Node SDK for
|
|
3
|
+
Official TypeScript / Node SDK for [Transcodely](https://www.transcodely.com) — encode
|
|
4
|
+
video into HLS, DASH and MP4 and write it to your own S3, GCS or R2 bucket, with DRM,
|
|
5
|
+
signed playback and deterministic output paths. Or let Transcodely host and deliver it.
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
8
|
npm install @transcodely/sdk
|
|
@@ -41,6 +43,138 @@ for await (const event of client.jobs.watch(job.id)) {
|
|
|
41
43
|
}
|
|
42
44
|
```
|
|
43
45
|
|
|
46
|
+
## Upload a file
|
|
47
|
+
|
|
48
|
+
`client.uploads.putFile` takes a file from your disk to a hosted, playable
|
|
49
|
+
video in one call. It opens the upload, pushes the bytes straight to object
|
|
50
|
+
storage, and tells the API the bytes landed — the API never proxies the media.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const video = await client.uploads.putFile("./talk.mp4", {
|
|
54
|
+
appId: "app_k1l2m3n4o5",
|
|
55
|
+
title: "Conference talk",
|
|
56
|
+
onProgress: (p) => process.stdout.write(`\r${p.percent}%`),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
console.log(video.id, video.status); // "vid_a1b2c3d4e5f6g7" "processing"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The transcode starts on its own as soon as the upload completes, so the video
|
|
63
|
+
comes back already `processing`. Follow it with `client.videos.watch(video.id)`
|
|
64
|
+
until it reaches `ready` (or `error`, or `deleted`), or subscribe to the
|
|
65
|
+
`video.ready` webhook.
|
|
66
|
+
|
|
67
|
+
**The app does not need managed hosting turned on first.** An app that has never
|
|
68
|
+
hosted anything is provisioned by the create call itself — bucket, managed
|
|
69
|
+
origin and CDN pull zone — which is why a first upload takes a few seconds
|
|
70
|
+
longer than the ones after it. Two things can still refuse it, and they are
|
|
71
|
+
worth telling apart:
|
|
72
|
+
|
|
73
|
+
- `hosting_provisioning_failed` — provisioning did not complete. Nothing was
|
|
74
|
+
created, so the identical call is safe to retry.
|
|
75
|
+
- a billing or admission code — `billing_past_due`,
|
|
76
|
+
`outstanding_balance_exceeded`, `limit_exceeded`, `intake_paused`,
|
|
77
|
+
`app_suspended`. These are about the account, not about hosting, and retrying
|
|
78
|
+
will not clear them.
|
|
79
|
+
|
|
80
|
+
**Sources.** A filesystem path, a `Blob`/`File`, or a `ReadableStream`. A path
|
|
81
|
+
and a `File` supply their own name and size; a bare `Blob` needs `filename`,
|
|
82
|
+
and a stream needs both `filename` and `sizeBytes` because the API has to know
|
|
83
|
+
the total before the first byte moves.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
await client.uploads.putFile(blob, { appId, filename: "talk.mp4" });
|
|
87
|
+
await client.uploads.putFile(stream, { appId, filename: "talk.mp4", sizeBytes: 734003200 });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**How it moves the bytes.** A file that fits in one part goes up as a single
|
|
91
|
+
`PUT`. Anything larger becomes an S3 multipart upload: parts are uploaded
|
|
92
|
+
`concurrency` at a time (default 4), a part that fails on a 5xx, 429, 408 or a
|
|
93
|
+
network error is retried with jittered backoff, and a presigned URL that has
|
|
94
|
+
expired is re-signed and retried. If the upload cannot finish, the multipart
|
|
95
|
+
upload is aborted server-side before the error is thrown — nothing sweeps
|
|
96
|
+
orphaned multipart uploads, so an abandoned one would cost storage forever.
|
|
97
|
+
|
|
98
|
+
**Knobs.**
|
|
99
|
+
|
|
100
|
+
| Option | Default | Notes |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `partSize` | 25 MiB | Raised to the 5 MiB S3 minimum, and further if 10,000 parts would not cover the file. |
|
|
103
|
+
| `concurrency` | 4 | Parts in flight at once. For a stream source this also caps how much is buffered. |
|
|
104
|
+
| `maxRetries` | 4 | Retries per part, after the first attempt. |
|
|
105
|
+
| `signal` | — | Aborts the upload and the multipart upload behind it. |
|
|
106
|
+
| `onProgress` | — | Called once per completed part, not per byte. |
|
|
107
|
+
|
|
108
|
+
**Limits and errors.** The platform ceiling is 5 GB; an empty file or one over
|
|
109
|
+
the ceiling is refused before the first request. Transfer failures throw
|
|
110
|
+
`UploadError` (`UploadAbortedError` when your `signal` fired); everything the
|
|
111
|
+
API itself refuses keeps its usual error class.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { UploadAbortedError, UploadError } from "@transcodely/sdk";
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Need finer control? The individual RPCs are still there on `client.videos`
|
|
118
|
+
(`createUpload`, `createMultipartUpload`, `getUploadPartUrls`,
|
|
119
|
+
`completeMultipartUpload`, `abortMultipartUpload`).
|
|
120
|
+
|
|
121
|
+
## Command line
|
|
122
|
+
|
|
123
|
+
The same flows from a shell, with no code:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx transcodely ./talk.mp4
|
|
127
|
+
npx transcodely https://example.com/talk.mp4 --wait
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Not published yet.** The `transcodely` CLI lives in this repository under
|
|
131
|
+
[`packages/cli`](packages/cli/README.md) and ships once this SDK releases the
|
|
132
|
+
version it pins. Until then the commands above do not resolve on npm; build it
|
|
133
|
+
from the repo instead (`pnpm install && pnpm build`, then
|
|
134
|
+
`node packages/cli/dist/index.mjs --help`).
|
|
135
|
+
|
|
136
|
+
## Read the output report
|
|
137
|
+
|
|
138
|
+
Every completed output carries a report of what the produced file actually
|
|
139
|
+
turned out to be — measured from the encoded file rather than copied from the
|
|
140
|
+
request — plus the verdict of comparing those measurements against what was
|
|
141
|
+
asked for.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import type { OutputReport } from "@transcodely/sdk";
|
|
145
|
+
|
|
146
|
+
const done = await client.jobs.get(job.id);
|
|
147
|
+
|
|
148
|
+
for (const output of done.outputs) {
|
|
149
|
+
const report: OutputReport | undefined = output.report;
|
|
150
|
+
if (!report) continue; // not measured — never "nothing wrong"
|
|
151
|
+
|
|
152
|
+
console.log(
|
|
153
|
+
output.id,
|
|
154
|
+
report.video?.codec,
|
|
155
|
+
`${report.video?.width}x${report.video?.height}`,
|
|
156
|
+
`${report.durationSeconds}s`,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (report.verdict && !report.verdict.matchesRequest) {
|
|
160
|
+
for (const m of report.verdict.mismatches) {
|
|
161
|
+
console.log(` ${m.field}: asked for ${m.expected}, got ${m.actual}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Branch on `m.field` — it comes from a fixed vocabulary (`video.codec`,
|
|
168
|
+
`video.resolution`, `duration_seconds`, …) — rather than on the values beside
|
|
169
|
+
it. For an ABR ladder the facts describe the highest-resolution rendition, the
|
|
170
|
+
same one the verdict judges; per-rendition detail stays in `variantResults`.
|
|
171
|
+
|
|
172
|
+
An output encoded with per-title content-aware analysis also carries
|
|
173
|
+
`report.contentAware`: the VMAF target the search aimed at, the score it reached
|
|
174
|
+
on its samples, and the CRF it chose. It describes the SEARCH, not the delivered
|
|
175
|
+
file — `vmafAchieved` scores short samples taken before the real encode, which is
|
|
176
|
+
never scored itself. It is `undefined` on every ordinary output.
|
|
177
|
+
|
|
44
178
|
## AI captions
|
|
45
179
|
|
|
46
180
|
Add auto-generated captions to any output with a `generate` subtitle track.
|
|
@@ -92,9 +226,11 @@ const client = new Transcodely({ apiKey: process.env.TRANSCODELY_API_KEY! });
|
|
|
92
226
|
|
|
93
227
|
```ts
|
|
94
228
|
client.jobs // create / get / list / cancel / confirm / watch
|
|
95
|
-
client.
|
|
229
|
+
client.uploads // putFile — the whole create / PUT / complete upload, for a path, Blob or stream
|
|
230
|
+
client.videos // upload RPCs, multipart, createFromUrl, get / list / update / delete / watch / getStats / listTopVideos
|
|
96
231
|
client.presets // create / get / getBySlug / list / update / duplicate / archive
|
|
97
232
|
client.origins // create / get / list / update / validate / archive
|
|
233
|
+
client.ingestRules // create / get / list / update / delete / listEvents / test / replayEvent
|
|
98
234
|
client.apps // create / get / list / update / archive / enableHosting
|
|
99
235
|
client.apiKeys // create / get / list / revoke
|
|
100
236
|
client.organizations // create / get / list / update / checkSlug
|
|
@@ -103,6 +239,20 @@ client.users // getMe / get / list / updateMe
|
|
|
103
239
|
client.health // check
|
|
104
240
|
```
|
|
105
241
|
|
|
242
|
+
Every enum in the API schema is exported by name from the package root, so you
|
|
243
|
+
can branch on one without reaching into the generated code:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { HealthStatus } from "@transcodely/sdk";
|
|
247
|
+
|
|
248
|
+
const health = await client.health.check();
|
|
249
|
+
for (const component of health.components) {
|
|
250
|
+
if (component.status !== HealthStatus.HEALTHY) {
|
|
251
|
+
console.warn(component.name, component.message);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
106
256
|
## Origins
|
|
107
257
|
|
|
108
258
|
An origin tells Transcodely where to read source media from and where to write outputs. Every origin belongs to a single provider; pass exactly one provider-config field (`s3`, `gcs`, `http`, or `r2`) on create.
|
|
@@ -198,6 +348,74 @@ r2: {
|
|
|
198
348
|
|
|
199
349
|
Provide either `accountId` or `endpoint`, never both. `jurisdiction` only applies when `accountId` is set.
|
|
200
350
|
|
|
351
|
+
## Ingest rules
|
|
352
|
+
|
|
353
|
+
An ingest rule is a standing instruction on one readable origin: *when an object
|
|
354
|
+
matching these filters lands, create this job for it*. Your storage provider
|
|
355
|
+
posts its object-created events to the rule's endpoint, and no server of yours
|
|
356
|
+
is in the path. Amazon S3 via SNS, Google Cloud Storage via a Pub/Sub push
|
|
357
|
+
subscription, Supabase Storage via a database webhook, and a generic shape for
|
|
358
|
+
anything else are all recognised from the payload.
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
const { rule, secret } = await client.ingestRules.create({
|
|
362
|
+
originId: "ori_a1b2c3d4e5f6",
|
|
363
|
+
name: "Watch uploads/",
|
|
364
|
+
filters: {
|
|
365
|
+
prefix: "uploads/",
|
|
366
|
+
suffixes: [".mp4", ".mov"],
|
|
367
|
+
minBytes: 1024n, // ignore the zero-byte placeholder some clients write first
|
|
368
|
+
},
|
|
369
|
+
action: {
|
|
370
|
+
outputs: [{ preset: "web_1080p_standard" }],
|
|
371
|
+
managed: true, // host and deliver the result; set outputOriginId for your own bucket
|
|
372
|
+
priority: JobPriority.STANDARD,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
console.log("point your bucket notifications at", rule!.endpointUrl);
|
|
377
|
+
console.log("secret (shown once):", secret);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`secret` is the **only** time the inbound secret is readable — store it wherever
|
|
381
|
+
the event sender will read it from. A later `get` returns just `secretPrefix`
|
|
382
|
+
and `secretHint`. Lost it? `update` with `rotateSecret: true` issues a new one,
|
|
383
|
+
and the previous one keeps working for 24 hours so the sender can be changed
|
|
384
|
+
without dropping an event.
|
|
385
|
+
|
|
386
|
+
Every delivery is recorded, whether or not it became a job:
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
for await (const event of client.ingestRules
|
|
390
|
+
.listEvents({ ruleId: rule!.id })
|
|
391
|
+
.autoPage()) {
|
|
392
|
+
console.log(event.id, event.source, event.objectKey, event.status, event.reason);
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
A `SKIPPED` event names why in `reason` — `filter_prefix`, `filter_suffix`,
|
|
397
|
+
`filter_content_type`, `filter_size`, `bucket_mismatch`, `rule_disabled`, or
|
|
398
|
+
`duplicate`. A `FAILED` one carries the API error code that refused the job,
|
|
399
|
+
such as `limit_exceeded`.
|
|
400
|
+
|
|
401
|
+
Deduplication is permanent: an object is identified by (rule, bucket, key,
|
|
402
|
+
etag), so re-sending the event or re-uploading the same bytes produces nothing.
|
|
403
|
+
To give an object another pass — one that arrived while the rule was paused, or
|
|
404
|
+
was refused while the account was over its cap — replay it:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
const replayed = await client.ingestRules.replayEvent("sev_a1b2c3d4e5f6g7");
|
|
408
|
+
console.log(replayed.id, "is back in", StorageEventStatus[replayed.status]);
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Only `SKIPPED` and `FAILED` events can be replayed. When an `update` switches a
|
|
412
|
+
paused rule back on, the response reports how large that backlog is in
|
|
413
|
+
`eventsSkippedWhileDisabled`.
|
|
414
|
+
|
|
415
|
+
Before wiring the provider up, dry-run a key against the rule with
|
|
416
|
+
`client.ingestRules.test(...)`: it reports whether the filters match and, when
|
|
417
|
+
they do, the exact job request the rule would submit. Nothing is stored.
|
|
418
|
+
|
|
201
419
|
## Webhooks
|
|
202
420
|
|
|
203
421
|
Transcodely signs every webhook delivery with HMAC-SHA-256 using your endpoint's `whsec_…` secret. Verify the signature before trusting the body — `client.webhooks.constructEvent` validates the signature, parses the envelope, and returns a typed event:
|