@decartai/sdk 0.1.22 → 0.1.23

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.
@@ -27,6 +27,12 @@ type FilesClient = {
27
27
  */
28
28
  upload: (file: FileUploadInput, options?: UploadFileOptions) => Promise<FileReference>;
29
29
  get: (fileId: string) => Promise<FileReference>;
30
+ /**
31
+ * Look up an upload by the MD5 of its bytes when you no longer have the id.
32
+ * Resolves the newest non-expired match; rejects with `FILES_GET_ERROR` on
33
+ * 404, exactly like `get` does for an unknown id.
34
+ */
35
+ getByMd5: (md5: string) => Promise<FileReference>;
30
36
  delete: (fileId: string) => Promise<void>;
31
37
  };
32
38
  //#endregion
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  //#region src/files/client.ts
5
5
  const MAX_TTL_SECONDS = 720 * 60 * 60;
6
6
  const ttlSecondsSchema = z.union([z.number().int().min(60).max(MAX_TTL_SECONDS), z.literal("persistent")]);
7
+ const MD5_HEX = /^[0-9a-f]{32}$/i;
7
8
  const createFilesClient = (opts) => {
8
9
  const { baseUrl, apiKey, integration } = opts;
9
10
  const upload = async (file, options) => {
@@ -28,8 +29,8 @@ const createFilesClient = (opts) => {
28
29
  }
29
30
  return response.json();
30
31
  };
31
- const get = async (fileId) => {
32
- const response = await fetch(`${baseUrl}/v1/files/${encodeURIComponent(fileId)}`, {
32
+ const fetchReference = async (path) => {
33
+ const response = await fetch(`${baseUrl}${path}`, {
33
34
  method: "GET",
34
35
  headers: buildAuthHeaders({
35
36
  apiKey,
@@ -42,6 +43,11 @@ const createFilesClient = (opts) => {
42
43
  }
43
44
  return response.json();
44
45
  };
46
+ const get = (fileId) => fetchReference(`/v1/files/${encodeURIComponent(fileId)}`);
47
+ const getByMd5 = async (md5) => {
48
+ if (!MD5_HEX.test(md5)) throw createInvalidInputError("md5 must be 32 hex characters");
49
+ return fetchReference(`/v1/files/by-md5/${md5.toLowerCase()}`);
50
+ };
45
51
  const deleteFile = async (fileId) => {
46
52
  const response = await fetch(`${baseUrl}/v1/files/${encodeURIComponent(fileId)}`, {
47
53
  method: "DELETE",
@@ -58,6 +64,7 @@ const createFilesClient = (opts) => {
58
64
  return {
59
65
  upload,
60
66
  get,
67
+ getByMd5,
61
68
  delete: deleteFile
62
69
  };
63
70
  };
@@ -14,6 +14,12 @@ interface FileReference {
14
14
  filename: string | null;
15
15
  mime_type: string;
16
16
  size_bytes: number;
17
+ /**
18
+ * Lowercase hex MD5 of the uploaded bytes; pass it to `client.files.getByMd5(...)`
19
+ * to find this file again without its id. `null` on files uploaded before
20
+ * hashes were recorded.
21
+ */
22
+ md5: string | null;
17
23
  created_at: string;
18
24
  expires_at: string | null;
19
25
  }
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.1.22";
2
+ var version = "0.1.23";
3
3
  //#endregion
4
4
  export { version };
@@ -1,4 +1,16 @@
1
1
  //#region src/realtime/config-realtime.ts
2
+ /** Publish bitrate floor for the primary video layer (bps). */
3
+ const MIN_VIDEO_BITRATE_BPS = 11e5;
4
+ /** Publish bitrate cap for the primary video layer (bps). */
5
+ const MAX_VIDEO_BITRATE_BPS = 35e5;
6
+ /** Combined max bitrate of livekit-client's default lower simulcast layers (bps); pinned by a unit test. */
7
+ const SIMULCAST_LOWER_LAYERS_BPS = 61e4;
8
+ /** Fraction of the bandwidth estimate available to the video encoders. */
9
+ const BWE_VIDEO_SHARE = .8;
10
+ /** Bandwidth estimate (kbps) needed for the primary layer to reach `primaryBps`. */
11
+ function estimateKbpsFor(primaryBps) {
12
+ return Math.round((primaryBps + SIMULCAST_LOWER_LAYERS_BPS) / BWE_VIDEO_SHARE / 1e3);
13
+ }
2
14
  const REALTIME_CONFIG = {
3
15
  signaling: {
4
16
  connectTimeoutMs: 6e4,
@@ -34,8 +46,12 @@ const REALTIME_CONFIG = {
34
46
  dynacast: false
35
47
  },
36
48
  defaultVideoCodec: "h264",
37
- defaultMaxVideoBitrateBps: 35e5,
49
+ defaultMaxVideoBitrateBps: MAX_VIDEO_BITRATE_BPS,
38
50
  vp9MaxVideoBitrateBps: 3e6,
51
+ /** Seeds the publisher's bandwidth estimate (a start bitrate, not a minimum). */
52
+ minVideoBitrateBps: MIN_VIDEO_BITRATE_BPS,
53
+ simulcastLowerLayersBitrateBps: SIMULCAST_LOWER_LAYERS_BPS,
54
+ bweVideoShare: BWE_VIDEO_SHARE,
39
55
  defaultPublishFps: 30
40
56
  },
41
57
  observability: {
@@ -109,12 +125,11 @@ const REALTIME_CONFIG = {
109
125
  fair: .01,
110
126
  poor: .05
111
127
  },
112
- /** Upstream headroom = available BWE ÷ the intended publish bitrate (requiredUpstreamKbps). */
128
+ /** Available upstream bandwidth bands (kbps). Chromium-only. */
113
129
  upstream: {
114
- goodRatio: 1,
115
- fairRatio: .8,
116
- poorRatio: .5,
117
- requiredUpstreamKbps: 3500
130
+ goodKbps: estimateKbpsFor(MAX_VIDEO_BITRATE_BPS),
131
+ fairKbps: estimateKbpsFor(MIN_VIDEO_BITRATE_BPS),
132
+ poorKbps: estimateKbpsFor(MIN_VIDEO_BITRATE_BPS / 2)
118
133
  },
119
134
  /** Rendered (inbound) frames-per-second. */
120
135
  stall: {
@@ -3,13 +3,32 @@ import { REALTIME_CONFIG } from "./config-realtime.js";
3
3
  import { loadLiveKitClient } from "./livekit.js";
4
4
  import mitt from "mitt";
5
5
  //#region src/realtime/media-channel.ts
6
+ const START_BITRATE_PARAM = "x-google-start-bitrate";
7
+ /** Add `x-google-start-bitrate=<kbps>` to every video codec's fmtp line; libwebrtc reads it from the remote description. */
8
+ function withVideoStartBitrate(sdp, kbps) {
9
+ const param = `${START_BITRATE_PARAM}=${kbps}`;
10
+ return sdp.split(/(?=^m=)/m).map((section) => {
11
+ if (!section.startsWith("m=video")) return section;
12
+ const withFmtp = new Set(Array.from(section.matchAll(/^a=fmtp:(\d+) /gm), (m) => m[1]));
13
+ return section.replace(/^a=fmtp:\d+ [^\r\n]*/gm, (line) => line.includes(START_BITRATE_PARAM) ? line : `${line};${param}`).replace(/^a=rtpmap:(\d+) (?!rtx|red|ulpfec|flexfec)[^\r\n]*(\r?\n)/gim, (line, pt, eol) => withFmtp.has(pt) ? line : `${line}a=fmtp:${pt} ${param}${eol}`);
14
+ }).join("");
15
+ }
16
+ function usesSimulcast(videoCodec) {
17
+ return (videoCodec ?? REALTIME_CONFIG.livekit.defaultVideoCodec) !== "vp9";
18
+ }
19
+ /** Bandwidth-estimator seed (kbps) for the publisher. */
20
+ function getVideoStartBitrateKbps(videoCodec) {
21
+ const { minVideoBitrateBps, simulcastLowerLayersBitrateBps, bweVideoShare } = REALTIME_CONFIG.livekit;
22
+ const lowerLayers = usesSimulcast(videoCodec) ? simulcastLowerLayersBitrateBps : 0;
23
+ return Math.round((minVideoBitrateBps + lowerLayers) / bweVideoShare / 1e3);
24
+ }
6
25
  function getDefaultVideoPublishOptions(source, videoCodec, frameMetadata = false) {
7
26
  const resolvedCodec = videoCodec ?? REALTIME_CONFIG.livekit.defaultVideoCodec;
8
27
  const maxBitrate = resolvedCodec === "vp9" ? REALTIME_CONFIG.livekit.vp9MaxVideoBitrateBps : REALTIME_CONFIG.livekit.defaultMaxVideoBitrateBps;
9
28
  return {
10
29
  source,
11
30
  videoCodec: resolvedCodec,
12
- simulcast: resolvedCodec !== "vp9",
31
+ simulcast: usesSimulcast(resolvedCodec),
13
32
  videoEncoding: {
14
33
  maxBitrate,
15
34
  maxFramerate: REALTIME_CONFIG.livekit.defaultPublishFps
@@ -82,6 +101,7 @@ var LiveKitMediaChannel = class {
82
101
  this.config.observability?.startPhase("webrtc-handshake");
83
102
  await room.connect(opts.url, opts.token);
84
103
  this.config.observability?.endPhase("webrtc-handshake", { success: true });
104
+ this.seedStartBitrate(room);
85
105
  this.config.observability?.setLiveKitRoom(room);
86
106
  }
87
107
  async publishLocalTracks() {
@@ -106,6 +126,21 @@ var LiveKitMediaChannel = class {
106
126
  this.config.observability?.setLiveKitRoom(null);
107
127
  if (room) room.disconnect().catch(() => {});
108
128
  }
129
+ /**
130
+ * livekit-client has no start-bitrate option for H264/VP8, so seed the estimator through the
131
+ * SFU answer. Installed once per room: if livekit-client itself performs a full reconnect it
132
+ * creates a new publisher transport, and the rest of that session ramps from the default.
133
+ */
134
+ seedStartBitrate(room) {
135
+ const publisher = room.engine?.pcManager?.publisher;
136
+ const original = publisher?.setRemoteDescription;
137
+ if (!publisher || typeof original !== "function") return;
138
+ const kbps = getVideoStartBitrateKbps(this.config.videoCodec);
139
+ publisher.setRemoteDescription = (sd, offerId) => original.call(publisher, sd.type === "answer" && sd.sdp ? {
140
+ type: sd.type,
141
+ sdp: withVideoStartBitrate(sd.sdp, kbps)
142
+ } : sd, offerId);
143
+ }
109
144
  async publishTracks(stream) {
110
145
  if (!this.room) return;
111
146
  for (const track of stream.getTracks()) if (track.kind === "video") {
@@ -116,4 +151,4 @@ var LiveKitMediaChannel = class {
116
151
  };
117
152
  const createLiveKitMediaChannel = (config) => new LiveKitMediaChannel(config);
118
153
  //#endregion
119
- export { LiveKitMediaChannel, createLiveKitMediaChannel, getDefaultVideoPublishOptions };
154
+ export { LiveKitMediaChannel, createLiveKitMediaChannel, getDefaultVideoPublishOptions, getVideoStartBitrateKbps, withVideoStartBitrate };
@@ -7,7 +7,10 @@ const setInputSchema = z.object({
7
7
  prompt: z.string().min(1).optional(),
8
8
  enhance: z.boolean().optional().default(true),
9
9
  /**
10
- * - `Blob`/`File`/data:/http(s):/base64 string: bytes traverse the wire as base64.
10
+ * - `Blob`/`File`/data:/base64 string: bytes traverse the wire as base64.
11
+ * - `http(s):` URL: fetched, then sent as base64. Browser-oriented — the fetch
12
+ * relies on the same-origin policy. Server-side, pass bytes rather than a
13
+ * caller-supplied URL (fetching an arbitrary URL from a server is an SSRF risk).
11
14
  * - `"file_..."` id (from `client.files.upload(...).id`): sent as a server-side reference.
12
15
  */
13
16
  image: z.union([
@@ -47,7 +47,8 @@ function scoreMetrics(signals, thresholds, options = {}) {
47
47
  const loss = scoreLowerBetter(signals.fractionLost, thresholds.loss.good, thresholds.loss.fair, thresholds.loss.poor);
48
48
  let bandwidth = "good";
49
49
  if (!options.skipBitrate) {
50
- bandwidth = scoreHigherBetter(signals.availableOutgoingKbps != null ? signals.availableOutgoingKbps / thresholds.upstream.requiredUpstreamKbps : null, thresholds.upstream.goodRatio, thresholds.upstream.fairRatio, thresholds.upstream.poorRatio);
50
+ const { goodKbps, fairKbps, poorKbps } = thresholds.upstream;
51
+ bandwidth = scoreHigherBetter(signals.availableOutgoingKbps, goodKbps, fairKbps, poorKbps);
51
52
  if (signals.qualityLimitationReason === "bandwidth") bandwidth = worst(bandwidth, "fair");
52
53
  }
53
54
  let stall = scoreHigherBetter(signals.fps, thresholds.stall.goodFps, thresholds.stall.fairFps, thresholds.stall.poorFps);
@@ -19,6 +19,21 @@ async function blobToBase64(blob) {
19
19
  reader.readAsDataURL(blob);
20
20
  });
21
21
  }
22
+ /**
23
+ * Normalizes an image input to a raw base64 string (no `data:` prefix).
24
+ *
25
+ * String inputs are interpreted by URL scheme:
26
+ * - `data:` URL — decoded locally.
27
+ * - `http:`/`https:` URL — fetched, then encoded. This is a browser-oriented
28
+ * convenience that leans on the browser's same-origin policy to bound what
29
+ * the fetch can read. Do NOT rely on it server-side (Node/SSR/edge) with a
30
+ * caller-supplied URL: fetching an arbitrary URL from a server is an SSRF
31
+ * vector, and this helper can't distinguish an internal host from an external
32
+ * one. Server-side callers should pass binary (`Blob`/`File`) instead.
33
+ * - any other string that parses as a URL (e.g. `file:`, `blob:`, `ftp:`) — a
34
+ * clear error, rather than being forwarded to the API as if it were base64.
35
+ * - a string that isn't a URL — assumed to already be raw base64.
36
+ */
22
37
  async function imageToBase64(image) {
23
38
  if (typeof image === "string") {
24
39
  let url = null;
@@ -35,6 +50,7 @@ async function imageToBase64(image) {
35
50
  if (!response.ok) throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
36
51
  return blobToBase64(await response.blob());
37
52
  }
53
+ if (url) throw new Error(`Unsupported image URL scheme: ${url.protocol}`);
38
54
  return image;
39
55
  }
40
56
  return blobToBase64(image);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decartai/sdk",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Decart's JavaScript SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",