@decartai/sdk 0.0.66 → 0.0.67

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 CHANGED
@@ -62,6 +62,23 @@ realtimeClient.setPrompt("Cyberpunk city");
62
62
  realtimeClient.disconnect();
63
63
  ```
64
64
 
65
+ #### Front-camera mirroring
66
+
67
+ Pre-flip the input stream:
68
+
69
+ ```ts
70
+ const realtimeClient = await client.realtime.connect(stream, {
71
+ model,
72
+ mirror: "auto", // or true to always mirror
73
+ // ...
74
+ });
75
+ ```
76
+
77
+ Options:
78
+ - `false` (default) — never mirror.
79
+ - `"auto"` — mirror when the input track reports `facingMode: "user"` (mobile front cameras).
80
+ - `true` — always mirror (e.g. desktop webcams).
81
+
65
82
  ### Async Processing (Queue API)
66
83
 
67
84
  For video generation jobs, use the queue API to submit jobs and poll for results:
@@ -36,6 +36,7 @@ declare const realTimeClientConnectOptionsSchema: z.ZodObject<{
36
36
  image: z.ZodOptional<z.ZodUnion<readonly [z.ZodCustom<Blob, Blob>, z.ZodCustom<File, File>, z.ZodString]>>;
37
37
  }, z.core.$strip>>;
38
38
  customizeOffer: z.ZodOptional<z.ZodCustom<z.core.$InferInnerFunctionTypeAsync<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut>, z.core.$InferInnerFunctionTypeAsync<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut>>>;
39
+ mirror: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"auto">, z.ZodBoolean]>>;
39
40
  }, z.core.$strip>;
40
41
  type RealTimeClientConnectOptions = Omit<z.infer<typeof realTimeClientConnectOptionsSchema>, "model"> & {
41
42
  model: ModelDefinition | CustomModelDefinition;
@@ -3,6 +3,7 @@ import { modelDefinitionSchema } from "../shared/model.js";
3
3
  import { modelStateSchema } from "../shared/types.js";
4
4
  import { createEventBuffer } from "./event-buffer.js";
5
5
  import { realtimeMethods } from "./methods.js";
6
+ import { createMirroredStream, shouldMirrorTrack } from "./mirror-stream.js";
6
7
  import { decodeSubscribeToken, encodeSubscribeToken } from "./subscribe-client.js";
7
8
  import { NullTelemetryReporter, TelemetryReporter } from "./telemetry-reporter.js";
8
9
  import { WebRTCManager } from "./webrtc-manager.js";
@@ -56,7 +57,8 @@ const realTimeClientConnectOptionsSchema = z.object({
56
57
  model: modelDefinitionSchema,
57
58
  onRemoteStream: z.custom((val) => typeof val === "function", { message: "onRemoteStream must be a function" }),
58
59
  initialState: realTimeClientInitialStateSchema.optional(),
59
- customizeOffer: createAsyncFunctionSchema(z.function()).optional()
60
+ customizeOffer: createAsyncFunctionSchema(z.function()).optional(),
61
+ mirror: z.union([z.literal("auto"), z.boolean()]).optional()
60
62
  });
61
63
  const createRealTimeClient = (opts) => {
62
64
  const { baseUrl, apiKey, integration, logger } = opts;
@@ -64,7 +66,18 @@ const createRealTimeClient = (opts) => {
64
66
  const parsedOptions = realTimeClientConnectOptionsSchema.safeParse(options);
65
67
  if (!parsedOptions.success) throw parsedOptions.error;
66
68
  const { onRemoteStream, initialState } = parsedOptions.data;
67
- const inputStream = stream ?? new MediaStream();
69
+ const mirror = parsedOptions.data.mirror ?? false;
70
+ let inputStream = stream ?? new MediaStream();
71
+ let mirroredStream;
72
+ if (mirror !== false) try {
73
+ const firstVideoTrack = inputStream.getVideoTracks?.()[0];
74
+ if (firstVideoTrack && (mirror === true || shouldMirrorTrack(firstVideoTrack))) {
75
+ mirroredStream = createMirroredStream(inputStream, { fps: options.model.fps });
76
+ inputStream = mirroredStream.stream;
77
+ } else if (mirror === true && !firstVideoTrack) logger.warn("mirror: true requested but no video track was found on the input stream");
78
+ } catch (error) {
79
+ logger.warn("Failed to mirror input stream; falling back to un-mirrored input", { error: error instanceof Error ? error.message : String(error) });
80
+ }
68
81
  let webrtcManager;
69
82
  let telemetryReporter = new NullTelemetryReporter();
70
83
  let handleConnectionStateChange = null;
@@ -219,6 +232,7 @@ const createRealTimeClient = (opts) => {
219
232
  telemetryReporter.stop();
220
233
  stop();
221
234
  manager.cleanup();
235
+ mirroredStream?.dispose();
222
236
  },
223
237
  on: eventEmitter.on,
224
238
  off: eventEmitter.off,
@@ -239,6 +253,7 @@ const createRealTimeClient = (opts) => {
239
253
  } catch (error) {
240
254
  telemetryReporter.stop();
241
255
  webrtcManager?.cleanup();
256
+ mirroredStream?.dispose();
242
257
  throw error;
243
258
  }
244
259
  };
@@ -0,0 +1,116 @@
1
+ //#region src/realtime/mirror-stream.ts
2
+ function isMediaStreamTrackProcessorSupported() {
3
+ return typeof globalThis !== "undefined" && typeof globalThis.MediaStreamTrackProcessor === "function" && typeof globalThis.MediaStreamTrackGenerator === "function";
4
+ }
5
+ function shouldMirrorTrack(track) {
6
+ if (track.kind !== "video") return false;
7
+ let facingMode;
8
+ try {
9
+ facingMode = track.getSettings?.().facingMode;
10
+ } catch {
11
+ return false;
12
+ }
13
+ return facingMode === "user";
14
+ }
15
+ function createMirroredStream(input, opts) {
16
+ const [sourceVideo] = input.getVideoTracks();
17
+ const audioTracks = input.getAudioTracks();
18
+ if (!sourceVideo) return {
19
+ stream: input,
20
+ dispose: () => {},
21
+ impl: "noop"
22
+ };
23
+ if (isMediaStreamTrackProcessorSupported()) return createWithTrackProcessor(sourceVideo, audioTracks);
24
+ return createWithCanvas(sourceVideo, audioTracks, opts.fps);
25
+ }
26
+ function createWithTrackProcessor(sourceVideo, audioTracks) {
27
+ const Processor = globalThis.MediaStreamTrackProcessor;
28
+ const Generator = globalThis.MediaStreamTrackGenerator;
29
+ if (!new OffscreenCanvas(1, 1).getContext("2d")) throw new Error("createMirroredStream: OffscreenCanvas 2D context unavailable");
30
+ const processor = new Processor({ track: sourceVideo });
31
+ const generator = new Generator({ kind: "video" });
32
+ let canvas = new OffscreenCanvas(1, 1);
33
+ let ctx = canvas.getContext("2d");
34
+ const transform = new TransformStream({ transform(frame, controller) {
35
+ const w = frame.displayWidth;
36
+ const h = frame.displayHeight;
37
+ if (canvas.width !== w || canvas.height !== h) {
38
+ canvas = new OffscreenCanvas(w, h);
39
+ ctx = canvas.getContext("2d");
40
+ }
41
+ let flipped;
42
+ try {
43
+ ctx.save();
44
+ ctx.setTransform(-1, 0, 0, 1, w, 0);
45
+ ctx.drawImage(frame, 0, 0, w, h);
46
+ ctx.restore();
47
+ flipped = new VideoFrame(canvas, {
48
+ timestamp: frame.timestamp,
49
+ alpha: "discard"
50
+ });
51
+ controller.enqueue(flipped);
52
+ flipped = void 0;
53
+ } finally {
54
+ flipped?.close();
55
+ frame.close();
56
+ }
57
+ } });
58
+ processor.readable.pipeThrough(transform).pipeTo(generator.writable).catch(() => {});
59
+ const stream = new MediaStream([generator, ...audioTracks]);
60
+ let disposed = false;
61
+ return {
62
+ stream,
63
+ impl: "track-processor",
64
+ dispose: () => {
65
+ if (disposed) return;
66
+ disposed = true;
67
+ generator.stop();
68
+ }
69
+ };
70
+ }
71
+ function createWithCanvas(sourceVideo, audioTracks, fps) {
72
+ if (typeof document === "undefined") throw new Error("createMirroredStream requires a DOM environment (document is undefined)");
73
+ const canvas = document.createElement("canvas");
74
+ const ctx = canvas.getContext("2d");
75
+ if (!ctx) throw new Error("createMirroredStream: 2D canvas context unavailable");
76
+ if (typeof canvas.captureStream !== "function") throw new Error("createMirroredStream: canvas.captureStream unavailable");
77
+ const [flippedTrack] = canvas.captureStream(fps).getVideoTracks();
78
+ if (!flippedTrack) throw new Error("createMirroredStream: canvas.captureStream produced no video track");
79
+ const video = document.createElement("video");
80
+ video.muted = true;
81
+ video.playsInline = true;
82
+ video.autoplay = true;
83
+ video.srcObject = new MediaStream([sourceVideo]);
84
+ let disposed = false;
85
+ let rafHandle = null;
86
+ const draw = () => {
87
+ if (disposed) return;
88
+ const w = video.videoWidth;
89
+ const h = video.videoHeight;
90
+ if (w > 0 && h > 0) {
91
+ if (canvas.width !== w) canvas.width = w;
92
+ if (canvas.height !== h) canvas.height = h;
93
+ ctx.save();
94
+ ctx.setTransform(-1, 0, 0, 1, w, 0);
95
+ ctx.drawImage(video, 0, 0, w, h);
96
+ ctx.restore();
97
+ }
98
+ rafHandle = requestAnimationFrame(draw);
99
+ };
100
+ video.play().catch(() => {});
101
+ rafHandle = requestAnimationFrame(draw);
102
+ return {
103
+ stream: new MediaStream([flippedTrack, ...audioTracks]),
104
+ impl: "canvas",
105
+ dispose: () => {
106
+ if (disposed) return;
107
+ disposed = true;
108
+ if (rafHandle !== null) cancelAnimationFrame(rafHandle);
109
+ flippedTrack.stop();
110
+ video.srcObject = null;
111
+ }
112
+ };
113
+ }
114
+
115
+ //#endregion
116
+ export { createMirroredStream, shouldMirrorTrack };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decartai/sdk",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "description": "Decart's JavaScript SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",