@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 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 the [Transcodely](https://transcodely.com) video transcoding API.
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.videos // upload helpers, multipart, createFromUrl, get / list / update / delete / watch / getStats / listTopVideos
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: