@xmaxai/web-sdk 1.0.8 → 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 +117 -18
- package/dist/RecordingProcessor-4TP5NICO.cjs +211 -0
- package/dist/RecordingProcessor-4TP5NICO.cjs.map +1 -0
- package/dist/RecordingProcessor-ZSBJFF4J.js +211 -0
- package/dist/RecordingProcessor-ZSBJFF4J.js.map +1 -0
- package/dist/{VideoRenderStatistics-C7zqzfuS.d.cts → TrajectoryEffectRendering-C7E2KJbV.d.cts} +48 -1
- package/dist/{VideoRenderStatistics-C7zqzfuS.d.ts → TrajectoryEffectRendering-C7E2KJbV.d.ts} +48 -1
- package/dist/chunk-4DE2IREA.cjs +8 -0
- package/dist/chunk-4DE2IREA.cjs.map +1 -0
- package/dist/{chunk-JGDZ3RD3.js → chunk-ILZL4W2G.js} +2279 -395
- package/dist/chunk-ILZL4W2G.js.map +1 -0
- package/dist/{chunk-24VPU65T.cjs → chunk-KROY2O4A.cjs} +2831 -947
- package/dist/chunk-KROY2O4A.cjs.map +1 -0
- package/dist/chunk-V6TY7KAL.js +8 -0
- package/dist/chunk-V6TY7KAL.js.map +1 -0
- package/dist/index.cjs +7 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +173 -35
- package/dist/index.d.ts +173 -35
- package/dist/index.js +6 -1
- package/dist/react/index.cjs +25 -4
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +17 -1
- package/dist/react/index.d.ts +17 -1
- package/dist/react/index.js +22 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-24VPU65T.cjs.map +0 -1
- package/dist/chunk-JGDZ3RD3.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="./
|
|
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="./
|
|
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="./
|
|
34
|
-
<img src="./
|
|
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="./
|
|
57
|
-
<img src="./
|
|
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="./
|
|
79
|
-
<img src="./
|
|
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="./
|
|
102
|
-
<img src="./
|
|
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="./
|
|
125
|
-
<img src="./
|
|
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="./
|
|
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="./
|
|
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="./
|
|
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="./
|
|
161
|
-
<td>Run on a <img src="./
|
|
162
|
-
<td>Our models support real-time generation at up to <img src="./
|
|
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"]}
|