@xmaxai/web-sdk 1.0.7 → 1.0.9

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
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="./docs/images/brand/xmax-sdk.png" alt="XmaxSDK — Realtime Interactive Video Generation" width="880">
2
+ <img src="./materials/images/brand/xmax-sdk.png" alt="XmaxSDK — Realtime Interactive Video Generation" width="880">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -10,7 +10,7 @@
10
10
 
11
11
  We introduce XmaxSDK, a JavaScript SDK designed for real-time interactive video generation via Xmax models. XmaxSDK implements an end-to-end pipeline covering media acquisition, video streaming, frame-by-frame generation, and on-device rendering, enabling developers to seamlessly integrate low-latency, high-fidelity video transformations into creative applications at a much lower cost than alternative solutions.
12
12
 
13
- <p align="center"><img src="./docs/images/xlab/generation-demo.gif" alt="X-Lab realtime generation demo" width="33%" /><img src="./docs/images/xlab/index-demo.gif" alt="X-Lab index demo" width="33%" /><img src="./docs/images/xlab/storage-demo.gif" alt="X-Lab storage demo" width="33%" /></p>
13
+ <p align="center"><img src="./materials/images/xlab/generation-demo.gif" alt="X-Lab realtime generation demo" width="33%" /><img src="./materials/images/xlab/index-demo.gif" alt="X-Lab index demo" width="33%" /><img src="./materials/images/xlab/storage-demo.gif" alt="X-Lab storage demo" width="33%" /></p>
14
14
 
15
15
  <br>
16
16
 
@@ -30,8 +30,8 @@ We introduce XmaxSDK, a JavaScript SDK designed for real-time interactive video
30
30
  Replace anyone in your live feed with a designated avatar in real-time.
31
31
  </td>
32
32
  <td rowspan="2" width="16%" align="center" valign="middle">
33
- <a href="./docs/videos/use-cases/character-swapping.mp4">
34
- <img src="./docs/images/use-cases/character-swapping-poster.png" alt="Play the Character Swapping demo" width="120">
33
+ <a href="./materials/videos/use-cases/character-swapping.mp4">
34
+ <img src="./materials/images/use-cases/character-swapping-poster.png" alt="Play the Character Swapping demo" width="120">
35
35
  <br>
36
36
  <sub>▶ Play demo</sub>
37
37
  </a>
@@ -53,8 +53,8 @@ We introduce XmaxSDK, a JavaScript SDK designed for real-time interactive video
53
53
  authentic fit.
54
54
  </td>
55
55
  <td rowspan="2" width="16%" align="center" valign="middle">
56
- <a href="./docs/videos/use-cases/virtual-try-on.mp4">
57
- <img src="./docs/images/use-cases/virtual-try-on-poster.png" alt="Play the Virtual Try-On demo" width="120">
56
+ <a href="./materials/videos/use-cases/virtual-try-on.mp4">
57
+ <img src="./materials/images/use-cases/virtual-try-on-poster.png" alt="Play the Virtual Try-On demo" width="120">
58
58
  <br>
59
59
  <sub>▶ Play demo</sub>
60
60
  </a>
@@ -75,8 +75,8 @@ We introduce XmaxSDK, a JavaScript SDK designed for real-time interactive video
75
75
  Reimagine your world in any style with an immersive visual experience.
76
76
  </td>
77
77
  <td rowspan="2" width="16%" align="center" valign="middle">
78
- <a href="./docs/videos/use-cases/video-restyling.mp4">
79
- <img src="./docs/images/use-cases/video-restyling-poster.png" alt="Play the Video Restyling demo" width="120">
78
+ <a href="./materials/videos/use-cases/video-restyling.mp4">
79
+ <img src="./materials/images/use-cases/video-restyling-poster.png" alt="Play the Video Restyling demo" width="120">
80
80
  <br>
81
81
  <sub>▶ Play demo</sub>
82
82
  </a>
@@ -98,8 +98,8 @@ We introduce XmaxSDK, a JavaScript SDK designed for real-time interactive video
98
98
  through gestures.
99
99
  </td>
100
100
  <td rowspan="2" width="16%" align="center" valign="middle">
101
- <a href="./docs/videos/use-cases/ai-companions.mp4">
102
- <img src="./docs/images/use-cases/ai-companions-poster.png" alt="Play the AI Companions demo" width="120">
101
+ <a href="./materials/videos/use-cases/ai-companions.mp4">
102
+ <img src="./materials/images/use-cases/ai-companions-poster.png" alt="Play the AI Companions demo" width="120">
103
103
  <br>
104
104
  <sub>▶ Play demo</sub>
105
105
  </a>
@@ -121,8 +121,8 @@ We introduce XmaxSDK, a JavaScript SDK designed for real-time interactive video
121
121
  trajectories.
122
122
  </td>
123
123
  <td rowspan="2" width="16%" align="center" valign="middle">
124
- <a href="./docs/videos/use-cases/live-photo.mp4">
125
- <img src="./docs/images/use-cases/live-photo-poster.png" alt="Play the Live Photo demo" width="120">
124
+ <a href="./materials/videos/use-cases/live-photo.mp4">
125
+ <img src="./materials/images/use-cases/live-photo-poster.png" alt="Play the Live Photo demo" width="120">
126
126
  <br>
127
127
  <sub>▶ Play demo</sub>
128
128
  </a>
@@ -145,21 +145,21 @@ We introduce XmaxSDK, a JavaScript SDK designed for real-time interactive video
145
145
  <thead>
146
146
  <tr>
147
147
  <th height="104" align="center" valign="middle">
148
- <img src="./docs/images/why/low-latency.svg" alt="Low latency" width="36" height="36"><br>Low latency
148
+ <img src="./materials/images/why/low-latency.svg" alt="Low latency" width="36" height="36"><br>Low latency
149
149
  </th>
150
150
  <th height="104" align="center" valign="middle">
151
- <img src="./docs/images/why/low-cost.svg" alt="Cost efficiency" width="36" height="36"><br>Cost efficiency
151
+ <img src="./materials/images/why/low-cost.svg" alt="Cost efficiency" width="36" height="36"><br>Cost efficiency
152
152
  </th>
153
153
  <th height="104" align="center" valign="middle">
154
- <img src="./docs/images/why/high-fidelity.svg" alt="High fidelity" width="36" height="36"><br>High fidelity
154
+ <img src="./materials/images/why/high-fidelity.svg" alt="High fidelity" width="36" height="36"><br>High fidelity
155
155
  </th>
156
156
  </tr>
157
157
  </thead>
158
158
  <tbody>
159
159
  <tr>
160
- <td>End-to-end latency is measured in <img src="./docs/images/why/latency-highlight.svg" alt="hundreds of milliseconds" width="192" height="20" align="absmiddle">, ensuring that updates to generation conditions and interaction controls are reflected instantly.</td>
161
- <td>Run on a <img src="./docs/images/why/gpu-highlight.svg" alt="single RTX 5090" width="126" height="20" align="absmiddle">, reducing inference costs by orders of magnitude versus datacenter GPUs like H100.</td>
162
- <td>Our models support real-time generation at up to <img src="./docs/images/why/resolution-highlight.svg" alt="1080p" width="48" height="20" align="absmiddle">, delivering production-ready, high-quality video output.</td>
160
+ <td>End-to-end latency is measured in <img src="./materials/images/why/latency-highlight.svg" alt="hundreds of milliseconds" width="192" height="20" align="absmiddle">, ensuring that updates to generation conditions and interaction controls are reflected instantly.</td>
161
+ <td>Run on a <img src="./materials/images/why/gpu-highlight.svg" alt="single RTX 5090" width="126" height="20" align="absmiddle">, reducing inference costs by orders of magnitude versus datacenter GPUs like H100.</td>
162
+ <td>Our models support real-time generation at up to <img src="./materials/images/why/resolution-highlight.svg" alt="1080p" width="48" height="20" align="absmiddle">, delivering production-ready, high-quality video output.</td>
163
163
  </tr>
164
164
  </tbody>
165
165
  </table>
@@ -282,6 +282,88 @@ await realtime.updateUplinkVideoFormat(new RealtimeVideoFormat({
282
282
 
283
283
  <br>
284
284
 
285
+ ### Generate from a local image
286
+
287
+ Use `createLocalImageStream` instead of `createLocalCameraStream` to use a local
288
+ image as the video input. It accepts a browser-decodable `File` or `Blob`, fits the
289
+ image into the model's input dimensions without cropping, and continuously sends
290
+ the same frame without audio. No image upload or camera permission is required.
291
+
292
+ ```javascript
293
+ const localStream = await realtime.createLocalImageStream({
294
+ file: imageFile, // File from an <input type="file" accept="image/*">.
295
+ videoFormat: new RealtimeVideoFormat({ width: 1024, height: 1920, fps: 30 }),
296
+ });
297
+
298
+ const remoteStream = await realtime.startGeneration({
299
+ localStream,
300
+ context: new RealtimeContext({ prompt: "Make the scene come alive" }),
301
+ });
302
+ ```
303
+
304
+ Bind the returned tracks to the same video views used for camera input.
305
+ TRTC, Agora, and VeRTC use the existing external-video publishing path.
306
+ `disconnect()` retains the image for reconnection; `close()` releases its resources.
307
+ Animated images are captured as a single still frame.
308
+
309
+ <br>
310
+
311
+ ### Record a generated clip
312
+
313
+ Recording is off by default. Set `record: true` when starting a new generation
314
+ to record approximately 20 seconds of the original remote video and audio.
315
+ Register the listener before starting:
316
+
317
+ ```javascript
318
+ import { RealtimeRecordingEventType } from "@xmaxai/web-sdk";
319
+
320
+ realtime.setRecordingListener(event => {
321
+ if (event.type === RealtimeRecordingEventType.failed) {
322
+ console.error("Recording failed", event.taskID, event.error);
323
+ return;
324
+ }
325
+
326
+ const { blob, hasAudio, reason } = event.result;
327
+ // Save, upload, or preview blob here. Use blob.type to identify its format.
328
+ // completed: reached the time limit; interrupted: finalized an earlier end.
329
+ console.log(event.taskID, blob, hasAudio, reason);
330
+ });
331
+
332
+ await realtime.startGeneration({
333
+ localStream,
334
+ context: new RealtimeContext({ prompt: "Make the scene come alive" }),
335
+ record: true,
336
+ });
337
+ ```
338
+
339
+ - The timer starts when a remote video frame is available, independently of the
340
+ attached view. Frame interpolation, interaction effects, and overlays are not recorded.
341
+ - Recording errors do not stop generation. `startGeneration` does not wait for the file.
342
+ - Before delivery, the SDK shortens unusually long video-frame intervals and cuts
343
+ the same intervals from audio. Cuts align to complete audio-packet boundaries;
344
+ neither track is re-encoded, and all video frames are preserved. The resulting
345
+ clip can be shorter than the recording period. Speech during removed intervals
346
+ is lost, and audio splices can be audible. This does not reconstruct missing
347
+ motion or smooth visual jumps.
348
+ - Gap removal runs after recording and does not delay RTC cleanup. It requires
349
+ a supported MP4 or WebM recording; if processing fails or times out, the SDK
350
+ logs a warning and returns the original clip instead.
351
+ - `record` is ignored on condition updates; it cannot stop or restart a recording.
352
+ - Restarting ends the old recording. `restart: true` with `record: true` reports a
353
+ recording failure for the new task; generation still restarts normally.
354
+ - An early end returns `interrupted` only if the recorder finalizes a nonempty file
355
+ without an encoding error. Empty or failed recordings return `failed`.
356
+ - Audio arriving later is included after its arrival. `hasAudio` indicates that an
357
+ enabled, unmuted remote audio track was observed during recording, not audible speech.
358
+ - Formats depend on browser support; MP4 is not guaranteed. Background scheduling
359
+ can delay the stop timer, so files are not cropped to exactly 20.000 seconds.
360
+ - Audio processing must be allowed by the browser. TRTC recording on mobile Safari
361
+ is currently unsupported because playback muting also disables audio reception.
362
+ - Recording buffers are limited to 64 MiB. The SDK does not upload recordings or
363
+ retain a copy after delivery. Clear the listener with `setRecordingListener(undefined)`.
364
+
365
+ <br>
366
+
285
367
  ### Using React
286
368
 
287
369
  Use `XmaxRealtimeVideo` as your primary React view. Store the local and remote
@@ -302,6 +384,23 @@ import { VideoContentMode } from "@xmaxai/web-sdk";
302
384
  See the [React component](./packages/xmax-sdk/src/react/XmaxRealtimeVideo.tsx) for state binding and the
303
385
  [example project](#example-project) for a complete implementation.
304
386
 
387
+ ### Interact with the generated video
388
+
389
+ `XmaxVideo` and `XmaxRealtimeVideo` support dragging on the remote output with a
390
+ mouse, pen, or multiple touch points. Interaction becomes available after generation
391
+ starts. The SDK maps the displayed coordinates to the model input and sends the
392
+ trajectory automatically; local previews do not send interaction events.
393
+
394
+ Set `isInteractionEnabled={false}` to disable interaction without stopping playback.
395
+ The framework-independent views expose the same `isInteractionEnabled` property.
396
+
397
+ Use `trajectoryRenderer` to supply a `TrajectoryEffectRendering` implementation for
398
+ custom visuals, or leave it undefined to use the default glowing trail. A renderer
399
+ only controls visuals, not sampling or signaling; use a separate instance per view
400
+ and release its animation resources in `reset()`.
401
+
402
+ The generated response to dragging depends on the model and generation prompt.
403
+
305
404
  <br>
306
405
 
307
406
  ### Listen for events
@@ -0,0 +1,211 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+ var _chunk4DE2IREAcjs = require('./chunk-4DE2IREA.cjs');
4
+
5
+ // src/Media/Recording/RecordingProcessor.ts
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+ var _mediabunny = require('mediabunny');
19
+
20
+ // src/Media/Recording/RecordingTimeline.ts
21
+ var _RecordingTimeline = class _RecordingTimeline {
22
+ /**
23
+ * 从显示时间戳估计正常帧间隔,只裁掉显著超长的等待部分。
24
+ */
25
+ constructor(timestamps, duration, audioPackets) {
26
+ _chunk4DE2IREAcjs.__publicField.call(void 0, this, "duration", duration);
27
+ /**
28
+ * 原始时长、正常帧时长与待删除区间
29
+ */
30
+ _chunk4DE2IREAcjs.__publicField.call(void 0, this, "frameDuration");
31
+ _chunk4DE2IREAcjs.__publicField.call(void 0, this, "cuts");
32
+ if (!Number.isFinite(duration) || duration <= 0 || timestamps.length === 0 || timestamps.some((value) => !Number.isFinite(value) || value < 0 || value > duration)) {
33
+ throw new Error("Invalid recording timeline");
34
+ }
35
+ const sorted = [...timestamps].sort((a, b) => a - b);
36
+ const intervals = sorted.slice(1).map((time, index) => time - sorted[index]).filter((value) => value > 0).sort((a, b) => a - b);
37
+ this.frameDuration = _nullishCoalesce(intervals[Math.floor(intervals.length / 2)], () => ( _RecordingTimeline.fallbackFrameDuration));
38
+ const threshold = Math.max(_RecordingTimeline.minimumGap, this.frameDuration * _RecordingTimeline.gapMultiplier);
39
+ const cuts = sorted.flatMap((start, index) => {
40
+ const end = _nullishCoalesce(sorted[index + 1], () => ( duration));
41
+ return end - start > threshold + 1e-6 ? [{ start: start + this.frameDuration, end }] : [];
42
+ });
43
+ this.cuts = audioPackets ? cuts.flatMap((cut) => {
44
+ const contained = audioPackets.filter((packet) => packet.end > packet.start && packet.start >= cut.start && packet.end <= cut.end);
45
+ if (contained.length === 0) {
46
+ return [];
47
+ }
48
+ return [{
49
+ start: Math.min(...contained.map((packet) => packet.start)),
50
+ end: Math.max(...contained.map((packet) => packet.end))
51
+ }];
52
+ }) : cuts;
53
+ }
54
+ /**
55
+ * 将原始时间映射到裁剪后的时间;删除区间内的时刻映射到同一接缝。
56
+ */
57
+ map(time) {
58
+ let removed = 0;
59
+ for (const cut of this.cuts) {
60
+ if (time <= cut.start) {
61
+ break;
62
+ }
63
+ removed += Math.min(time, cut.end) - cut.start;
64
+ }
65
+ return time - removed;
66
+ }
67
+ /**
68
+ * 判断一个音频包是否位于删除区间;区间已对齐包边界,不会切断压缩数据。
69
+ */
70
+ removes(start, end) {
71
+ return this.cuts.some((cut) => start >= cut.start && end <= cut.end);
72
+ }
73
+ };
74
+ /**
75
+ * 长间隔判定与默认帧时长
76
+ */
77
+ _chunk4DE2IREAcjs.__publicField.call(void 0, _RecordingTimeline, "minimumGap", 0.12);
78
+ _chunk4DE2IREAcjs.__publicField.call(void 0, _RecordingTimeline, "gapMultiplier", 3);
79
+ _chunk4DE2IREAcjs.__publicField.call(void 0, _RecordingTimeline, "fallbackFrameDuration", 1 / 30);
80
+ var RecordingTimeline = _RecordingTimeline;
81
+
82
+ // src/Media/Recording/RecordingProcessor.ts
83
+ var _RecordingProcessor = class _RecordingProcessor {
84
+ /**
85
+ * 裁剪成功返回新文件,无需裁剪时返回原文件;失败交由调用方保留原始录像。
86
+ */
87
+ async process(blob) {
88
+ if (blob.size > _RecordingProcessor.maximumBytes) {
89
+ throw new Error("Recording is too large to process");
90
+ }
91
+ const input = new (0, _mediabunny.Input)({ source: new (0, _mediabunny.BlobSource)(blob), formats: [_mediabunny.MP4, _mediabunny.WEBM] });
92
+ const controller = new AbortController();
93
+ let output;
94
+ let timer;
95
+ const timeout = new Promise((_, reject) => {
96
+ timer = setTimeout(() => {
97
+ controller.abort();
98
+ reject(new Error("Recording processing timed out"));
99
+ }, _RecordingProcessor.timeoutMs);
100
+ });
101
+ try {
102
+ return await Promise.race([
103
+ this.rewrite(input, blob, controller.signal, (value) => {
104
+ output = value;
105
+ }),
106
+ timeout
107
+ ]);
108
+ } finally {
109
+ controller.abort();
110
+ clearTimeout(timer);
111
+ input.dispose();
112
+ if (output && output.state !== "finalized" && output.state !== "canceled") {
113
+ void output.cancel().catch(() => {
114
+ });
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * 读取有限数量的编码包,维持原始解码顺序。
120
+ */
121
+ async readPackets(track, signal) {
122
+ const packets = [];
123
+ for await (const packet of new (0, _mediabunny.EncodedPacketSink)(track).packets()) {
124
+ signal.throwIfAborted();
125
+ packets.push(packet);
126
+ if (packets.length > _RecordingProcessor.maximumPackets) {
127
+ throw new Error("Recording has too many media packets");
128
+ }
129
+ }
130
+ return packets;
131
+ }
132
+ /**
133
+ * 保留所有视频帧,删除等待区间内的完整音频包,并重新封装两条时间线。
134
+ */
135
+ async rewrite(input, original, signal, registerOutput) {
136
+ const video = await input.getPrimaryVideoTrack();
137
+ if (!video) {
138
+ throw new Error("Recording has no video track");
139
+ }
140
+ const videoPackets = await this.readPackets(video, signal);
141
+ const audio = await input.getPrimaryAudioTrack();
142
+ const audioPackets = audio ? await this.readPackets(audio, signal) : [];
143
+ const duration = await input.computeDuration();
144
+ const audioRanges = audioPackets.map((packet, index) => ({
145
+ start: packet.timestamp,
146
+ end: packet.duration > 0 ? packet.timestamp + packet.duration : _nullishCoalesce(_optionalChain([audioPackets, 'access', _2 => _2[index + 1], 'optionalAccess', _3 => _3.timestamp]), () => ( duration))
147
+ }));
148
+ const timeline = new RecordingTimeline(videoPackets.map((packet) => packet.timestamp), duration, audio ? audioRanges : void 0);
149
+ if (timeline.cuts.length === 0) {
150
+ return original;
151
+ }
152
+ const videoCodec = await video.getCodec();
153
+ const videoConfig = await video.getDecoderConfig();
154
+ const audioCodec = await _optionalChain([audio, 'optionalAccess', _4 => _4.getCodec, 'call', _5 => _5()]);
155
+ const audioConfig = await _optionalChain([audio, 'optionalAccess', _6 => _6.getDecoderConfig, 'call', _7 => _7()]);
156
+ if (!videoCodec || !videoConfig || audio && (!audioCodec || !audioConfig)) {
157
+ throw new Error("Unsupported recording codec");
158
+ }
159
+ const isWebM = (await input.getFormat()).name === _mediabunny.WEBM.name;
160
+ const rotation = await video.getRotation();
161
+ const flip = await video.getFlip();
162
+ signal.throwIfAborted();
163
+ const output = new (0, _mediabunny.Output)({
164
+ format: isWebM ? new (0, _mediabunny.WebMOutputFormat)() : new (0, _mediabunny.Mp4OutputFormat)(),
165
+ target: new (0, _mediabunny.BufferTarget)()
166
+ });
167
+ registerOutput(output);
168
+ const videoSource = new (0, _mediabunny.EncodedVideoPacketSource)(videoCodec);
169
+ output.addVideoTrack(videoSource, { rotation, flip });
170
+ const audioSource = audioCodec ? new (0, _mediabunny.EncodedAudioPacketSource)(audioCodec) : void 0;
171
+ if (audioSource) {
172
+ output.addAudioTrack(audioSource);
173
+ }
174
+ await output.start();
175
+ for (const packet of videoPackets) {
176
+ signal.throwIfAborted();
177
+ const timestamp = timeline.map(packet.timestamp);
178
+ const duration2 = timeline.map(packet.timestamp + packet.duration) - timestamp;
179
+ await videoSource.add(packet.clone({ timestamp, duration: duration2 }), { decoderConfig: videoConfig });
180
+ }
181
+ videoSource.close();
182
+ if (audioSource && audioConfig) {
183
+ for (const [index, packet] of audioPackets.entries()) {
184
+ signal.throwIfAborted();
185
+ const range = audioRanges[index];
186
+ if (timeline.removes(range.start, range.end)) {
187
+ continue;
188
+ }
189
+ await audioSource.add(packet.clone({ timestamp: timeline.map(packet.timestamp) }), { decoderConfig: audioConfig });
190
+ }
191
+ audioSource.close();
192
+ }
193
+ signal.throwIfAborted();
194
+ await output.finalize();
195
+ if (!output.target.buffer || output.target.buffer.byteLength > _RecordingProcessor.maximumBytes) {
196
+ throw new Error("Processed recording exceeds the memory limit");
197
+ }
198
+ return new Blob([output.target.buffer], { type: output.format.mimeType });
199
+ }
200
+ };
201
+ /**
202
+ * 后处理资源上限,与实时连接的清理超时分离
203
+ */
204
+ _chunk4DE2IREAcjs.__publicField.call(void 0, _RecordingProcessor, "timeoutMs", 1e4);
205
+ _chunk4DE2IREAcjs.__publicField.call(void 0, _RecordingProcessor, "maximumBytes", 64 * 1024 * 1024);
206
+ _chunk4DE2IREAcjs.__publicField.call(void 0, _RecordingProcessor, "maximumPackets", 1e4);
207
+ var RecordingProcessor = _RecordingProcessor;
208
+
209
+
210
+ exports.RecordingProcessor = RecordingProcessor;
211
+ //# sourceMappingURL=RecordingProcessor-4TP5NICO.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/private/var/folders/5j/p3g3j18x2w3byrl5wtnk94tc0000gn/T/xmax-web-release-Zggzaq/source/packages/xmax-sdk/dist/RecordingProcessor-4TP5NICO.cjs","../src/Media/Recording/RecordingProcessor.ts","../src/Media/Recording/RecordingTimeline.ts"],"names":["duration"],"mappings":"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","file":"/private/var/folders/5j/p3g3j18x2w3byrl5wtnk94tc0000gn/T/xmax-web-release-Zggzaq/source/packages/xmax-sdk/dist/RecordingProcessor-4TP5NICO.cjs","sourcesContent":[null,"import {\n BlobSource, BufferTarget, EncodedAudioPacketSource, EncodedPacketSink,\n EncodedVideoPacketSource, Input, MP4, WEBM, Mp4OutputFormat, Output, WebMOutputFormat,\n type EncodedPacket, type InputTrack,\n} from \"mediabunny\";\nimport { RecordingTimeline } from \"./RecordingTimeline\";\n\n/**\n * 录制完成后裁掉长帧等待;音视频共用按音频包边界对齐的时间映射,不重新编码。\n */\nexport class RecordingProcessor {\n /**\n * 后处理资源上限,与实时连接的清理超时分离\n */\n private static readonly timeoutMs = 10_000;\n private static readonly maximumBytes = 64 * 1024 * 1024;\n private static readonly maximumPackets = 10_000;\n\n /**\n * 裁剪成功返回新文件,无需裁剪时返回原文件;失败交由调用方保留原始录像。\n */\n async process(blob: Blob): Promise<Blob> {\n if (blob.size > RecordingProcessor.maximumBytes) {\n throw new Error(\"Recording is too large to process\");\n }\n\n const input = new Input({ source: new BlobSource(blob), formats: [MP4, WEBM] });\n const controller = new AbortController();\n let output: Output | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n reject(new Error(\"Recording processing timed out\"));\n }, RecordingProcessor.timeoutMs);\n });\n\n try {\n return await Promise.race([\n this.rewrite(input, blob, controller.signal, value => { output = value; }),\n timeout,\n ]);\n } finally {\n controller.abort();\n clearTimeout(timer);\n input.dispose();\n if (output && output.state !== \"finalized\" && output.state !== \"canceled\") {\n void output.cancel().catch(() => {});\n }\n }\n }\n\n /**\n * 读取有限数量的编码包,维持原始解码顺序。\n */\n private async readPackets(track: InputTrack, signal: AbortSignal): Promise<EncodedPacket[]> {\n const packets: EncodedPacket[] = [];\n for await (const packet of new EncodedPacketSink(track).packets()) {\n signal.throwIfAborted();\n packets.push(packet);\n if (packets.length > RecordingProcessor.maximumPackets) {\n throw new Error(\"Recording has too many media packets\");\n }\n }\n return packets;\n }\n\n /**\n * 保留所有视频帧,删除等待区间内的完整音频包,并重新封装两条时间线。\n */\n private async rewrite(\n input: Input,\n original: Blob,\n signal: AbortSignal,\n registerOutput: (output: Output) => void,\n ): Promise<Blob> {\n const video = await input.getPrimaryVideoTrack();\n if (!video) {\n throw new Error(\"Recording has no video track\");\n }\n const videoPackets = await this.readPackets(video, signal);\n const audio = await input.getPrimaryAudioTrack();\n const audioPackets = audio ? await this.readPackets(audio, signal) : [];\n const duration = await input.computeDuration();\n const audioRanges = audioPackets.map((packet, index) => ({\n start: packet.timestamp,\n end: packet.duration > 0 ? packet.timestamp + packet.duration : audioPackets[index + 1]?.timestamp ?? duration,\n }));\n const timeline = new RecordingTimeline(videoPackets.map(packet => packet.timestamp), duration, audio ? audioRanges : undefined);\n if (timeline.cuts.length === 0) {\n return original;\n }\n\n const videoCodec = await video.getCodec();\n const videoConfig = await video.getDecoderConfig();\n const audioCodec = await audio?.getCodec();\n const audioConfig = await audio?.getDecoderConfig();\n if (!videoCodec || !videoConfig || (audio && (!audioCodec || !audioConfig))) {\n throw new Error(\"Unsupported recording codec\");\n }\n const isWebM = (await input.getFormat()).name === WEBM.name;\n const rotation = await video.getRotation();\n const flip = await video.getFlip();\n signal.throwIfAborted();\n\n const output = new Output({\n format: isWebM ? new WebMOutputFormat() : new Mp4OutputFormat(),\n target: new BufferTarget(),\n });\n registerOutput(output);\n const videoSource = new EncodedVideoPacketSource(videoCodec);\n output.addVideoTrack(videoSource, { rotation, flip });\n const audioSource = audioCodec ? new EncodedAudioPacketSource(audioCodec) : undefined;\n if (audioSource) {\n output.addAudioTrack(audioSource);\n }\n await output.start();\n\n // 不指定固定帧率,避免容器把原本不同的显示时间戳量化到同一帧。\n for (const packet of videoPackets) {\n signal.throwIfAborted();\n const timestamp = timeline.map(packet.timestamp);\n const duration = timeline.map(packet.timestamp + packet.duration) - timestamp;\n await videoSource.add(packet.clone({ timestamp, duration }), { decoderConfig: videoConfig });\n }\n videoSource.close();\n\n if (audioSource && audioConfig) {\n for (const [index, packet] of audioPackets.entries()) {\n signal.throwIfAborted();\n const range = audioRanges[index]!;\n if (timeline.removes(range.start, range.end)) {\n continue;\n }\n await audioSource.add(packet.clone({ timestamp: timeline.map(packet.timestamp) }), { decoderConfig: audioConfig });\n }\n audioSource.close();\n }\n signal.throwIfAborted();\n await output.finalize();\n if (!output.target.buffer || output.target.buffer.byteLength > RecordingProcessor.maximumBytes) {\n throw new Error(\"Processed recording exceeds the memory limit\");\n }\n return new Blob([output.target.buffer], { type: output.format.mimeType });\n }\n}\n","/**\n * 原始录制时间线上的区间,单位为秒。\n */\nexport interface RecordingTimeRange {\n start: number;\n end: number;\n}\n\n/**\n * 视频长帧裁剪计划。音视频共用同一映射,避免分别累计时间造成漂移。\n */\nexport class RecordingTimeline {\n /**\n * 长间隔判定与默认帧时长\n */\n private static readonly minimumGap = 0.12;\n private static readonly gapMultiplier = 3;\n private static readonly fallbackFrameDuration = 1 / 30;\n\n /**\n * 原始时长、正常帧时长与待删除区间\n */\n readonly frameDuration: number;\n readonly cuts: readonly RecordingTimeRange[];\n\n /**\n * 从显示时间戳估计正常帧间隔,只裁掉显著超长的等待部分。\n */\n constructor(timestamps: readonly number[], readonly duration: number, audioPackets?: readonly RecordingTimeRange[]) {\n if (!Number.isFinite(duration) || duration <= 0 || timestamps.length === 0 ||\n timestamps.some(value => !Number.isFinite(value) || value < 0 || value > duration)) {\n throw new Error(\"Invalid recording timeline\");\n }\n\n const sorted = [...timestamps].sort((a, b) => a - b);\n const intervals = sorted.slice(1).map((time, index) => time - sorted[index]!)\n .filter(value => value > 0).sort((a, b) => a - b);\n this.frameDuration = intervals[Math.floor(intervals.length / 2)] ?? RecordingTimeline.fallbackFrameDuration;\n const threshold = Math.max(RecordingTimeline.minimumGap, this.frameDuration * RecordingTimeline.gapMultiplier);\n\n const cuts = sorted.flatMap((start, index) => {\n const end = sorted[index + 1] ?? duration;\n return end - start > threshold + 1e-6 ? [{ start: start + this.frameDuration, end }] : [];\n });\n\n // 只删除完全落在等待区间内的音频包;同一裁剪边界同时用于视频,避免重编码延迟。\n this.cuts = audioPackets ? cuts.flatMap(cut => {\n const contained = audioPackets.filter(packet => packet.end > packet.start &&\n packet.start >= cut.start && packet.end <= cut.end);\n if (contained.length === 0) {\n return [];\n }\n return [{\n start: Math.min(...contained.map(packet => packet.start)),\n end: Math.max(...contained.map(packet => packet.end)),\n }];\n }) : cuts;\n }\n\n /**\n * 将原始时间映射到裁剪后的时间;删除区间内的时刻映射到同一接缝。\n */\n map(time: number): number {\n let removed = 0;\n for (const cut of this.cuts) {\n if (time <= cut.start) {\n break;\n }\n removed += Math.min(time, cut.end) - cut.start;\n }\n return time - removed;\n }\n\n /**\n * 判断一个音频包是否位于删除区间;区间已对齐包边界,不会切断压缩数据。\n */\n removes(start: number, end: number): boolean {\n return this.cuts.some(cut => start >= cut.start && end <= cut.end);\n }\n}\n"]}