@wetspace/wetrtc 3.0.1 → 4.0.0
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 +156 -27
- package/dist/es/__test__/codec-preference.test.js +36 -0
- package/dist/es/__test__/codec-preference.test.js.map +1 -0
- package/dist/es/__test__/data-manager.test.js +60 -0
- package/dist/es/__test__/data-manager.test.js.map +1 -0
- package/dist/es/__test__/fsm.test.js +33 -0
- package/dist/es/__test__/fsm.test.js.map +1 -0
- package/dist/es/__test__/media-manager.test.js +41 -0
- package/dist/es/__test__/media-manager.test.js.map +1 -0
- package/dist/es/__test__/signal-manager.test.js +100 -0
- package/dist/es/__test__/signal-manager.test.js.map +1 -0
- package/dist/es/__test__/wetrtc-lifecycle.test.js +152 -0
- package/dist/es/__test__/wetrtc-lifecycle.test.js.map +1 -0
- package/dist/es/data/data-manager.d.ts +37 -0
- package/dist/es/data/data-manager.d.ts.map +1 -0
- package/dist/es/data/data-manager.js +282 -0
- package/dist/es/data/data-manager.js.map +1 -0
- package/dist/es/data/types.d.ts +34 -0
- package/dist/es/data/types.d.ts.map +1 -0
- package/dist/es/data/types.js +0 -0
- package/dist/es/disposable.d.ts +12 -0
- package/dist/es/disposable.d.ts.map +1 -0
- package/dist/es/disposable.js +36 -0
- package/dist/es/disposable.js.map +1 -0
- package/dist/es/fsm.d.ts +26 -0
- package/dist/es/fsm.d.ts.map +1 -0
- package/dist/es/fsm.js +63 -0
- package/dist/es/fsm.js.map +1 -0
- package/dist/es/index.d.ts +22 -0
- package/dist/es/index.d.ts.map +1 -0
- package/dist/es/index.js +48 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/media/audio-encoding.d.ts +10 -0
- package/dist/es/media/audio-encoding.d.ts.map +1 -0
- package/dist/es/media/audio-encoding.js +41 -0
- package/dist/es/media/audio-encoding.js.map +1 -0
- package/dist/es/media/codec-preference.d.ts +11 -0
- package/dist/es/media/codec-preference.d.ts.map +1 -0
- package/dist/es/media/codec-preference.js +77 -0
- package/dist/es/media/codec-preference.js.map +1 -0
- package/dist/es/media/encoding-utils.d.ts +2 -0
- package/dist/es/media/encoding-utils.d.ts.map +1 -0
- package/dist/es/media/encoding-utils.js +8 -0
- package/dist/es/media/encoding-utils.js.map +1 -0
- package/dist/es/media/media-manager.d.ts +39 -0
- package/dist/es/media/media-manager.d.ts.map +1 -0
- package/dist/es/media/media-manager.js +121 -0
- package/dist/es/media/media-manager.js.map +1 -0
- package/dist/es/media/types.d.ts +25 -0
- package/dist/es/media/types.d.ts.map +1 -0
- package/dist/es/media/types.js +0 -0
- package/dist/es/media/video-encoding.d.ts +12 -0
- package/dist/es/media/video-encoding.d.ts.map +1 -0
- package/dist/es/media/video-encoding.js +60 -0
- package/dist/es/media/video-encoding.js.map +1 -0
- package/dist/es/signal/signal-manager.d.ts +45 -0
- package/dist/es/signal/signal-manager.d.ts.map +1 -0
- package/dist/es/signal/signal-manager.js +250 -0
- package/dist/es/signal/signal-manager.js.map +1 -0
- package/dist/es/signal/types.d.ts +26 -0
- package/dist/es/signal/types.d.ts.map +1 -0
- package/dist/es/signal/types.js +8 -0
- package/dist/es/signal/types.js.map +1 -0
- package/dist/es/stats/stats-monitor.d.ts +32 -0
- package/dist/es/stats/stats-monitor.d.ts.map +1 -0
- package/dist/es/stats/stats-monitor.js +191 -0
- package/dist/es/stats/stats-monitor.js.map +1 -0
- package/dist/es/stats/types.d.ts +33 -0
- package/dist/es/stats/types.d.ts.map +1 -0
- package/dist/es/stats/types.js +0 -0
- package/dist/es/utils/types.d.ts +46 -0
- package/dist/es/utils/types.d.ts.map +1 -0
- package/dist/es/utils/types.js +80 -0
- package/dist/es/utils/types.js.map +1 -0
- package/dist/es/wetrtc.d.ts +92 -0
- package/dist/es/wetrtc.d.ts.map +1 -0
- package/dist/es/wetrtc.js +403 -0
- package/dist/es/wetrtc.js.map +1 -0
- package/dist/lib/__test__/codec-preference.test.js +34 -0
- package/dist/lib/__test__/codec-preference.test.js.map +1 -0
- package/dist/lib/__test__/data-manager.test.js +61 -0
- package/dist/lib/__test__/data-manager.test.js.map +1 -0
- package/dist/lib/__test__/fsm.test.js +34 -0
- package/dist/lib/__test__/fsm.test.js.map +1 -0
- package/dist/lib/__test__/media-manager.test.js +42 -0
- package/dist/lib/__test__/media-manager.test.js.map +1 -0
- package/dist/lib/__test__/signal-manager.test.js +101 -0
- package/dist/lib/__test__/signal-manager.test.js.map +1 -0
- package/dist/lib/__test__/wetrtc-lifecycle.test.js +153 -0
- package/dist/lib/__test__/wetrtc-lifecycle.test.js.map +1 -0
- package/dist/lib/data/data-manager.js +306 -0
- package/dist/lib/data/data-manager.js.map +1 -0
- package/dist/lib/data/types.js +18 -0
- package/dist/lib/data/types.js.map +1 -0
- package/dist/lib/disposable.js +60 -0
- package/dist/lib/disposable.js.map +1 -0
- package/dist/lib/fsm.js +87 -0
- package/dist/lib/fsm.js.map +1 -0
- package/dist/lib/index.js +75 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/media/audio-encoding.js +66 -0
- package/dist/lib/media/audio-encoding.js.map +1 -0
- package/dist/lib/media/codec-preference.js +106 -0
- package/dist/lib/media/codec-preference.js.map +1 -0
- package/dist/lib/media/encoding-utils.js +32 -0
- package/dist/lib/media/encoding-utils.js.map +1 -0
- package/dist/lib/media/media-manager.js +145 -0
- package/dist/lib/media/media-manager.js.map +1 -0
- package/dist/lib/media/types.js +18 -0
- package/dist/lib/media/types.js.map +1 -0
- package/dist/lib/media/video-encoding.js +87 -0
- package/dist/lib/media/video-encoding.js.map +1 -0
- package/dist/lib/signal/signal-manager.js +274 -0
- package/dist/lib/signal/signal-manager.js.map +1 -0
- package/dist/lib/signal/types.js +32 -0
- package/dist/lib/signal/types.js.map +1 -0
- package/dist/lib/stats/stats-monitor.js +215 -0
- package/dist/lib/stats/stats-monitor.js.map +1 -0
- package/dist/lib/stats/types.js +18 -0
- package/dist/lib/stats/types.js.map +1 -0
- package/dist/lib/utils/types.js +108 -0
- package/dist/lib/utils/types.js.map +1 -0
- package/dist/lib/wetrtc.js +415 -0
- package/dist/lib/wetrtc.js.map +1 -0
- package/package.json +38 -43
- package/es/core/constant.d.ts +0 -6
- package/es/core/hook.d.ts +0 -31
- package/es/core/index.d.ts +0 -39
- package/es/index.d.ts +0 -6
- package/es/index.js +0 -1
- package/es/libs/index.d.ts +0 -41
- package/es/libs/record.d.ts +0 -8
- package/lib/core/constant.d.ts +0 -6
- package/lib/core/hook.d.ts +0 -31
- package/lib/core/index.d.ts +0 -39
- package/lib/index.d.ts +0 -6
- package/lib/index.js +0 -1
- package/lib/libs/index.d.ts +0 -41
- package/lib/libs/record.d.ts +0 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"mappings":"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","names":[],"ignoreList":[],"sources":["../../../src/__test__/wetrtc-lifecycle.test.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\r\nimport { WetRTC } from '../wetrtc';\r\nimport type { SignalChannel } from '../signal/types';\r\n\r\nconst pcs: MockPeerConnection[] = [];\r\n\r\nclass MockPeerConnection {\r\n iceConnectionState: RTCIceConnectionState = 'new';\r\n connectionState: RTCPeerConnectionState = 'new';\r\n signalingState: RTCSignalingState = 'stable';\r\n localDescription: RTCSessionDescription | null = null;\r\n remoteDescription: RTCSessionDescription | null = null;\r\n senders: RTCRtpSender[] = [];\r\n transceivers: RTCRtpTransceiver[] = [];\r\n closed = false;\r\n\r\n constructor() {\r\n pcs.push(this);\r\n }\r\n\r\n addEventListener = vi.fn();\r\n removeEventListener = vi.fn();\r\n createDataChannel = vi.fn(() => ({ close: vi.fn(), readyState: 'open' }));\r\n createOffer = vi.fn().mockResolvedValue({ type: 'offer', sdp: 'offer' });\r\n createAnswer = vi.fn().mockResolvedValue({ type: 'answer', sdp: 'answer' });\r\n setLocalDescription = vi.fn().mockImplementation(async (desc) => {\r\n this.localDescription = desc as RTCSessionDescription;\r\n });\r\n setRemoteDescription = vi.fn().mockImplementation(async (desc) => {\r\n this.remoteDescription = desc as RTCSessionDescription;\r\n });\r\n addIceCandidate = vi.fn().mockResolvedValue(undefined);\r\n getStats = vi.fn().mockResolvedValue({ forEach: vi.fn() });\r\n close = vi.fn(() => {\r\n this.closed = true;\r\n });\r\n getSenders = vi.fn(() => this.senders);\r\n getTransceivers = vi.fn(() => this.transceivers);\r\n addTransceiver = vi.fn((kind: string, init?: RTCRtpTransceiverInit) => {\r\n const transceiver = {\r\n direction: init?.direction ?? 'sendrecv',\r\n sender: {},\r\n receiver: {},\r\n mid: null,\r\n currentDirection: null,\r\n stop: vi.fn(),\r\n } as unknown as RTCRtpTransceiver;\r\n this.transceivers.push(transceiver);\r\n return transceiver;\r\n });\r\n addTrack = vi.fn((track: MediaStreamTrack) => {\r\n const sender = {\r\n track,\r\n replaceTrack: vi.fn().mockResolvedValue(undefined),\r\n } as unknown as RTCRtpSender;\r\n const transceiver = {\r\n direction: 'sendrecv',\r\n sender,\r\n receiver: {},\r\n mid: null,\r\n currentDirection: null,\r\n stop: vi.fn(),\r\n } as unknown as RTCRtpTransceiver;\r\n this.senders.push(sender);\r\n this.transceivers.push(transceiver);\r\n return sender;\r\n });\r\n removeTrack = vi.fn();\r\n}\r\n\r\nfunction signal(): SignalChannel {\r\n return {\r\n send: vi.fn().mockResolvedValue(undefined),\r\n onMessage: vi.fn(() => () => {}),\r\n };\r\n}\r\n\r\nfunction streamWithTrack(kind: 'audio' | 'video' = 'video'): { stream: MediaStream; track: MediaStreamTrack } {\r\n const track = {\r\n id: `${kind}-1`,\r\n kind,\r\n stop: vi.fn(),\r\n addEventListener: vi.fn(),\r\n } as unknown as MediaStreamTrack;\r\n const tracks: MediaStreamTrack[] = [track];\r\n const stream = {\r\n getTracks: () => tracks,\r\n getVideoTracks: () => kind === 'video' ? tracks : [],\r\n addTrack: vi.fn((next: MediaStreamTrack) => {\r\n if (!tracks.includes(next)) tracks.push(next);\r\n }),\r\n removeTrack: vi.fn((next: MediaStreamTrack) => {\r\n const idx = tracks.indexOf(next);\r\n if (idx >= 0) tracks.splice(idx, 1);\r\n }),\r\n } as unknown as MediaStream;\r\n return { stream, track };\r\n}\r\n\r\ndescribe('WetRTC lifecycle', () => {\r\n beforeEach(() => {\r\n pcs.length = 0;\r\n vi.stubGlobal('RTCPeerConnection', MockPeerConnection);\r\n });\r\n\r\n afterEach(() => {\r\n vi.unstubAllGlobals();\r\n });\r\n\r\n it('disconnect rebinds peer connection and restores local tracks', async () => {\r\n const channel = signal();\r\n const rtc = new WetRTC({ signal: channel, reconnect: false });\r\n const { stream, track } = streamWithTrack();\r\n const firstPc = rtc.peerConnection as unknown as MockPeerConnection;\r\n\r\n rtc.addTrack(track, stream);\r\n await rtc.disconnect();\r\n\r\n expect(channel.send).toHaveBeenCalledWith({ type: 'bye' });\r\n const nextPc = rtc.peerConnection as unknown as MockPeerConnection;\r\n expect(nextPc).not.toBe(firstPc);\r\n expect(firstPc.close).toHaveBeenCalled();\r\n expect(nextPc.addTrack).toHaveBeenCalledWith(track, stream);\r\n expect(rtc.state).toBe('idle');\r\n });\r\n\r\n it('recvonly connect creates recvonly transceivers', async () => {\r\n const rtc = new WetRTC({ signal: signal(), direction: 'recvonly', reconnect: false });\r\n\r\n await rtc.connect();\r\n\r\n const pc = rtc.peerConnection as unknown as MockPeerConnection;\r\n expect(pc.addTransceiver).toHaveBeenCalledWith('video', { direction: 'recvonly' });\r\n expect(pc.addTransceiver).toHaveBeenCalledWith('audio', { direction: 'recvonly' });\r\n });\r\n\r\n it('recvonly initiator sends offer', async () => {\r\n const ch = signal();\r\n const rtc = new WetRTC({\r\n signal: ch,\r\n direction: 'recvonly',\r\n initiator: true,\r\n reconnect: false,\r\n });\r\n\r\n await rtc.connect();\r\n\r\n expect(ch.send).toHaveBeenCalledWith(\r\n expect.objectContaining({ type: 'offer' }),\r\n );\r\n });\r\n\r\n it('sendonly passive does not send offer', async () => {\r\n const ch = signal();\r\n const rtc = new WetRTC({\r\n signal: ch,\r\n direction: 'sendonly',\r\n initiator: false,\r\n reconnect: false,\r\n });\r\n\r\n await rtc.connect();\r\n\r\n expect(ch.send).not.toHaveBeenCalledWith(\r\n expect.objectContaining({ type: 'offer' }),\r\n );\r\n });\r\n});\r\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IiIsIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZXMiOltdLCJzb3VyY2VzQ29udGVudCI6W119"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { IDisposable } from '../disposable';
|
|
2
|
+
import type { DataChannelOptions, DataChannelConfig, FileTransferOptions, FileMeta } from './types';
|
|
3
|
+
export type { DataChannelOptions, DataChannelConfig, FileTransferOptions, FileMeta } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* 数据通道管理器 — DataChannel + 文件传输
|
|
6
|
+
*/
|
|
7
|
+
export declare class DataManager implements IDisposable {
|
|
8
|
+
private pc;
|
|
9
|
+
private logger;
|
|
10
|
+
private channels;
|
|
11
|
+
private channelConfigs;
|
|
12
|
+
private messageHandlers;
|
|
13
|
+
private fileHandlers;
|
|
14
|
+
private fileReceives;
|
|
15
|
+
constructor(pc: RTCPeerConnection);
|
|
16
|
+
/** 切换 PeerConnection(disconnect / reconnect 后 rebind) */
|
|
17
|
+
setPeerConnection(pc: RTCPeerConnection): void;
|
|
18
|
+
createChannel(label: string, options?: DataChannelOptions): RTCDataChannel;
|
|
19
|
+
registerRemoteChannel(event: RTCDataChannelEvent): void;
|
|
20
|
+
configureChannels(configs: DataChannelConfig[]): void;
|
|
21
|
+
initConfiguredChannels(): void;
|
|
22
|
+
getChannel(label: string): RTCDataChannel | undefined;
|
|
23
|
+
send(data: ArrayBuffer | Blob | string, channelLabel?: string): void;
|
|
24
|
+
onMessage(handler: (data: ArrayBuffer | Blob | string, channel?: string) => void): () => void;
|
|
25
|
+
/** 订阅文件接收(与 sendFile 协议对称) */
|
|
26
|
+
onFile(handler: (meta: FileMeta, blob: Blob) => void): () => void;
|
|
27
|
+
sendFile(file: File, options?: FileTransferOptions): Promise<void>;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
private registerChannel;
|
|
30
|
+
private handleChannelMessage;
|
|
31
|
+
private doSend;
|
|
32
|
+
private waitForChannelOpen;
|
|
33
|
+
private waitForBuffer;
|
|
34
|
+
private throwIfAborted;
|
|
35
|
+
private sendChunkWithRetry;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=data-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-manager.d.ts","sourceRoot":"","sources":["../../../src/data/data-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEpG,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAWpG;;GAEG;AACH,qBAAa,WAAY,YAAW,WAAW;IAQjC,OAAO,CAAC,EAAE;IAPtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,cAAc,CAA2B;IACjD,OAAO,CAAC,eAAe,CAAyE;IAChG,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,YAAY,CAAuC;gBAEvC,EAAE,EAAE,iBAAiB;IAIzC,yDAAyD;IACzD,iBAAiB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;IAS9C,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,cAAc;IAU1E,qBAAqB,CAAC,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAKvD,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAIrD,sBAAsB,IAAI,IAAI;IAM9B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAIrD,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAuBpE,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAO7F,8BAA8B;IAC9B,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,GAAG,MAAM,IAAI;IAO3D,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CxE,OAAO,IAAI,IAAI;IAaf,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,oBAAoB;IAgE5B,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,aAAa;IAsCrB,OAAO,CAAC,cAAc;YAMR,kBAAkB;CAejC"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createLogger } from "../utils/types";
|
|
2
|
+
const MAX_BUFFER = 256 * 1024;
|
|
3
|
+
class DataManager {
|
|
4
|
+
constructor(pc) {
|
|
5
|
+
this.pc = pc;
|
|
6
|
+
this.channels = /* @__PURE__ */ new Map();
|
|
7
|
+
this.channelConfigs = [];
|
|
8
|
+
this.messageHandlers = [];
|
|
9
|
+
this.fileHandlers = [];
|
|
10
|
+
this.fileReceives = /* @__PURE__ */ new Map();
|
|
11
|
+
this.logger = createLogger();
|
|
12
|
+
}
|
|
13
|
+
/** 切换 PeerConnection(disconnect / reconnect 后 rebind) */
|
|
14
|
+
setPeerConnection(pc) {
|
|
15
|
+
for (const [, channel] of this.channels) {
|
|
16
|
+
channel.close();
|
|
17
|
+
}
|
|
18
|
+
this.channels.clear();
|
|
19
|
+
this.fileReceives.clear();
|
|
20
|
+
this.pc = pc;
|
|
21
|
+
}
|
|
22
|
+
createChannel(label, options) {
|
|
23
|
+
if (this.channels.has(label)) {
|
|
24
|
+
return this.channels.get(label);
|
|
25
|
+
}
|
|
26
|
+
const channel = this.pc.createDataChannel(label, options);
|
|
27
|
+
this.registerChannel(label, channel);
|
|
28
|
+
return channel;
|
|
29
|
+
}
|
|
30
|
+
registerRemoteChannel(event) {
|
|
31
|
+
const { channel } = event;
|
|
32
|
+
this.registerChannel(channel.label, channel);
|
|
33
|
+
}
|
|
34
|
+
configureChannels(configs) {
|
|
35
|
+
this.channelConfigs = configs;
|
|
36
|
+
}
|
|
37
|
+
initConfiguredChannels() {
|
|
38
|
+
for (const config of this.channelConfigs) {
|
|
39
|
+
this.createChannel(config.label, config.options);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
getChannel(label) {
|
|
43
|
+
return this.channels.get(label);
|
|
44
|
+
}
|
|
45
|
+
send(data, channelLabel) {
|
|
46
|
+
if (channelLabel) {
|
|
47
|
+
const ch = this.channels.get(channelLabel);
|
|
48
|
+
if (!ch || ch.readyState !== "open") {
|
|
49
|
+
this.logger.warn(`DataChannel "${channelLabel}" not ready`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.doSend(ch, data);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let sent = false;
|
|
56
|
+
for (const ch of this.channels.values()) {
|
|
57
|
+
if (ch.readyState === "open") {
|
|
58
|
+
this.doSend(ch, data);
|
|
59
|
+
sent = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!sent) {
|
|
63
|
+
this.logger.warn("No open DataChannel available");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
onMessage(handler) {
|
|
67
|
+
this.messageHandlers.push(handler);
|
|
68
|
+
return () => {
|
|
69
|
+
this.messageHandlers = this.messageHandlers.filter((h) => h !== handler);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** 订阅文件接收(与 sendFile 协议对称) */
|
|
73
|
+
onFile(handler) {
|
|
74
|
+
this.fileHandlers.push(handler);
|
|
75
|
+
return () => {
|
|
76
|
+
this.fileHandlers = this.fileHandlers.filter((h) => h !== handler);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async sendFile(file, options) {
|
|
80
|
+
const chunkSize = options?.chunkSize ?? 16384;
|
|
81
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
82
|
+
const bufferThreshold = options?.bufferThreshold ?? MAX_BUFFER;
|
|
83
|
+
const bufferTimeout = options?.bufferTimeout ?? 1e4;
|
|
84
|
+
const total = Math.ceil(file.size / chunkSize);
|
|
85
|
+
this.throwIfAborted(options?.signal);
|
|
86
|
+
if (options?.maxFileSize && file.size > options.maxFileSize) {
|
|
87
|
+
throw new Error(`File exceeds maxFileSize (${file.size} > ${options.maxFileSize})`);
|
|
88
|
+
}
|
|
89
|
+
const fileChannel = this.createChannel(`file-${file.name}`, {
|
|
90
|
+
ordered: true
|
|
91
|
+
});
|
|
92
|
+
await this.waitForChannelOpen(fileChannel);
|
|
93
|
+
await this.waitForBuffer(fileChannel, bufferThreshold, bufferTimeout);
|
|
94
|
+
fileChannel.send(JSON.stringify({
|
|
95
|
+
type: "file-meta",
|
|
96
|
+
name: file.name,
|
|
97
|
+
size: file.size,
|
|
98
|
+
chunks: total,
|
|
99
|
+
mimeType: file.type
|
|
100
|
+
}));
|
|
101
|
+
let offset = 0;
|
|
102
|
+
while (offset < file.size) {
|
|
103
|
+
this.throwIfAborted(options?.signal);
|
|
104
|
+
const end = Math.min(offset + chunkSize, file.size);
|
|
105
|
+
const chunk = file.slice(offset, end);
|
|
106
|
+
await this.waitForBuffer(fileChannel, bufferThreshold, bufferTimeout);
|
|
107
|
+
await this.sendChunkWithRetry(fileChannel, chunk, maxRetries);
|
|
108
|
+
offset = end;
|
|
109
|
+
options?.onProgress?.(offset, file.size);
|
|
110
|
+
}
|
|
111
|
+
await this.waitForBuffer(fileChannel, bufferThreshold, bufferTimeout);
|
|
112
|
+
fileChannel.send(JSON.stringify({ type: "file-complete", name: file.name }));
|
|
113
|
+
}
|
|
114
|
+
dispose() {
|
|
115
|
+
for (const [, channel] of this.channels) {
|
|
116
|
+
channel.close();
|
|
117
|
+
}
|
|
118
|
+
this.channels.clear();
|
|
119
|
+
this.messageHandlers.length = 0;
|
|
120
|
+
this.fileHandlers.length = 0;
|
|
121
|
+
this.fileReceives.clear();
|
|
122
|
+
this.logger.info("DataManager disposed");
|
|
123
|
+
}
|
|
124
|
+
// ── private ──
|
|
125
|
+
registerChannel(label, channel) {
|
|
126
|
+
channel.onopen = () => this.logger.info(`DataChannel "${label}" opened`);
|
|
127
|
+
channel.onclose = () => {
|
|
128
|
+
this.logger.info(`DataChannel "${label}" closed`);
|
|
129
|
+
this.fileReceives.delete(label);
|
|
130
|
+
};
|
|
131
|
+
channel.onerror = (ev) => this.logger.warn(`DataChannel "${label}" error:`, ev);
|
|
132
|
+
channel.onmessage = (ev) => {
|
|
133
|
+
this.handleChannelMessage(label, ev.data);
|
|
134
|
+
};
|
|
135
|
+
this.channels.set(label, channel);
|
|
136
|
+
}
|
|
137
|
+
handleChannelMessage(label, data) {
|
|
138
|
+
if (typeof data === "string") {
|
|
139
|
+
try {
|
|
140
|
+
const msg = JSON.parse(data);
|
|
141
|
+
if (msg.type === "file-meta") {
|
|
142
|
+
this.fileReceives.set(label, {
|
|
143
|
+
meta: {
|
|
144
|
+
name: msg.name,
|
|
145
|
+
size: msg.size,
|
|
146
|
+
type: msg.mimeType ?? "application/octet-stream"
|
|
147
|
+
},
|
|
148
|
+
chunks: [],
|
|
149
|
+
expectedChunks: msg.chunks,
|
|
150
|
+
receivedBytes: 0
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (msg.type === "file-complete") {
|
|
155
|
+
const state = this.fileReceives.get(label);
|
|
156
|
+
if (state) {
|
|
157
|
+
if (state.chunks.length !== state.expectedChunks) {
|
|
158
|
+
this.logger.warn(
|
|
159
|
+
`File "${state.meta.name}" incomplete: ${state.chunks.length}/${state.expectedChunks} chunks`
|
|
160
|
+
);
|
|
161
|
+
this.fileReceives.delete(label);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (state.receivedBytes !== state.meta.size) {
|
|
165
|
+
this.logger.warn(
|
|
166
|
+
`File "${state.meta.name}" size mismatch: ${state.receivedBytes}/${state.meta.size}`
|
|
167
|
+
);
|
|
168
|
+
this.fileReceives.delete(label);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const blob = new Blob(state.chunks, { type: state.meta.type });
|
|
172
|
+
for (const handler of this.fileHandlers) {
|
|
173
|
+
try {
|
|
174
|
+
handler(state.meta, blob);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.fileReceives.delete(label);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const receiveState = this.fileReceives.get(label);
|
|
186
|
+
if (receiveState && (data instanceof Blob || data instanceof ArrayBuffer)) {
|
|
187
|
+
const size = data instanceof Blob ? data.size : data.byteLength;
|
|
188
|
+
receiveState.chunks.push(
|
|
189
|
+
data instanceof Blob ? data : new Blob([data])
|
|
190
|
+
);
|
|
191
|
+
receiveState.receivedBytes += size;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (const handler of this.messageHandlers) {
|
|
195
|
+
try {
|
|
196
|
+
handler(data, label);
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
doSend(channel, data) {
|
|
202
|
+
if (typeof data === "string") {
|
|
203
|
+
channel.send(data);
|
|
204
|
+
} else if (data instanceof ArrayBuffer) {
|
|
205
|
+
if (data.byteLength > 256 * 1024) {
|
|
206
|
+
this.logger.warn(`Large message (${data.byteLength} bytes), consider chunking`);
|
|
207
|
+
}
|
|
208
|
+
channel.send(data);
|
|
209
|
+
} else {
|
|
210
|
+
channel.send(data);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
waitForChannelOpen(channel, timeout = 1e4) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
if (channel.readyState === "open")
|
|
216
|
+
return resolve();
|
|
217
|
+
const timer = setTimeout(() => reject(new Error("DataChannel open timeout")), timeout);
|
|
218
|
+
const prev = channel.onopen;
|
|
219
|
+
channel.onopen = (ev) => {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
prev?.call(channel, ev);
|
|
222
|
+
resolve();
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
waitForBuffer(channel, maxBuffer = MAX_BUFFER, timeout = 1e4) {
|
|
227
|
+
if (channel.bufferedAmount <= maxBuffer)
|
|
228
|
+
return Promise.resolve();
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const timer = setTimeout(() => {
|
|
231
|
+
cleanup();
|
|
232
|
+
reject(new Error("DataChannel buffer drain timeout"));
|
|
233
|
+
}, timeout);
|
|
234
|
+
const onClose = () => {
|
|
235
|
+
cleanup();
|
|
236
|
+
reject(new Error("DataChannel closed while waiting for buffer"));
|
|
237
|
+
};
|
|
238
|
+
const onError = () => {
|
|
239
|
+
cleanup();
|
|
240
|
+
reject(new Error("DataChannel error while waiting for buffer"));
|
|
241
|
+
};
|
|
242
|
+
const cleanup = () => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
channel.removeEventListener("bufferedamountlow", check);
|
|
245
|
+
channel.removeEventListener("close", onClose);
|
|
246
|
+
channel.removeEventListener("error", onError);
|
|
247
|
+
};
|
|
248
|
+
const check = () => {
|
|
249
|
+
if (channel.bufferedAmount <= maxBuffer) {
|
|
250
|
+
cleanup();
|
|
251
|
+
resolve();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
channel.bufferedAmountLowThreshold = maxBuffer;
|
|
255
|
+
channel.addEventListener("bufferedamountlow", check);
|
|
256
|
+
channel.addEventListener("close", onClose, { once: true });
|
|
257
|
+
channel.addEventListener("error", onError, { once: true });
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
throwIfAborted(signal) {
|
|
261
|
+
if (signal?.aborted) {
|
|
262
|
+
throw new DOMException("File transfer aborted", "AbortError");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async sendChunkWithRetry(channel, chunk, maxRetries) {
|
|
266
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
267
|
+
try {
|
|
268
|
+
channel.send(chunk);
|
|
269
|
+
return;
|
|
270
|
+
} catch {
|
|
271
|
+
if (i === maxRetries)
|
|
272
|
+
throw new Error("Chunk send failed after retries");
|
|
273
|
+
await new Promise((r) => setTimeout(r, 500 * (i + 1)));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export {
|
|
279
|
+
DataManager
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
//# sourceMappingURL=data-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"mappings":"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","names":[],"ignoreList":[],"sources":["../../../src/data/data-manager.ts"],"sourcesContent":["import { createLogger, type Logger } from '../utils/types';\r\nimport type { IDisposable } from '../disposable';\r\nimport type { DataChannelOptions, DataChannelConfig, FileTransferOptions, FileMeta } from './types';\r\n\r\nexport type { DataChannelOptions, DataChannelConfig, FileTransferOptions, FileMeta } from './types';\r\n\r\ninterface FileReceiveState {\r\n meta: FileMeta;\r\n chunks: Blob[];\r\n expectedChunks: number;\r\n receivedBytes: number;\r\n}\r\n\r\nconst MAX_BUFFER = 256 * 1024;\r\n\r\n/**\r\n * 数据通道管理器 — DataChannel + 文件传输\r\n */\r\nexport class DataManager implements IDisposable {\r\n private logger: Logger;\r\n private channels = new Map<string, RTCDataChannel>();\r\n private channelConfigs: DataChannelConfig[] = [];\r\n private messageHandlers: ((data: ArrayBuffer | Blob | string, channel?: string) => void)[] = [];\r\n private fileHandlers: ((meta: FileMeta, blob: Blob) => void)[] = [];\r\n private fileReceives = new Map<string, FileReceiveState>();\r\n\r\n constructor(private pc: RTCPeerConnection) {\r\n this.logger = createLogger();\r\n }\r\n\r\n /** 切换 PeerConnection(disconnect / reconnect 后 rebind) */\r\n setPeerConnection(pc: RTCPeerConnection): void {\r\n for (const [, channel] of this.channels) {\r\n channel.close();\r\n }\r\n this.channels.clear();\r\n this.fileReceives.clear();\r\n this.pc = pc;\r\n }\r\n\r\n createChannel(label: string, options?: DataChannelOptions): RTCDataChannel {\r\n if (this.channels.has(label)) {\r\n return this.channels.get(label)!;\r\n }\r\n\r\n const channel = this.pc.createDataChannel(label, options);\r\n this.registerChannel(label, channel);\r\n return channel;\r\n }\r\n\r\n registerRemoteChannel(event: RTCDataChannelEvent): void {\r\n const { channel } = event;\r\n this.registerChannel(channel.label, channel);\r\n }\r\n\r\n configureChannels(configs: DataChannelConfig[]): void {\r\n this.channelConfigs = configs;\r\n }\r\n\r\n initConfiguredChannels(): void {\r\n for (const config of this.channelConfigs) {\r\n this.createChannel(config.label, config.options);\r\n }\r\n }\r\n\r\n getChannel(label: string): RTCDataChannel | undefined {\r\n return this.channels.get(label);\r\n }\r\n\r\n send(data: ArrayBuffer | Blob | string, channelLabel?: string): void {\r\n if (channelLabel) {\r\n const ch = this.channels.get(channelLabel);\r\n if (!ch || ch.readyState !== 'open') {\r\n this.logger.warn(`DataChannel \"${channelLabel}\" not ready`);\r\n return;\r\n }\r\n this.doSend(ch, data);\r\n return;\r\n }\r\n\r\n let sent = false;\r\n for (const ch of this.channels.values()) {\r\n if (ch.readyState === 'open') {\r\n this.doSend(ch, data);\r\n sent = true;\r\n }\r\n }\r\n if (!sent) {\r\n this.logger.warn('No open DataChannel available');\r\n }\r\n }\r\n\r\n onMessage(handler: (data: ArrayBuffer | Blob | string, channel?: string) => void): () => void {\r\n this.messageHandlers.push(handler);\r\n return () => {\r\n this.messageHandlers = this.messageHandlers.filter(h => h !== handler);\r\n };\r\n }\r\n\r\n /** 订阅文件接收(与 sendFile 协议对称) */\r\n onFile(handler: (meta: FileMeta, blob: Blob) => void): () => void {\r\n this.fileHandlers.push(handler);\r\n return () => {\r\n this.fileHandlers = this.fileHandlers.filter(h => h !== handler);\r\n };\r\n }\r\n\r\n async sendFile(file: File, options?: FileTransferOptions): Promise<void> {\r\n const chunkSize = options?.chunkSize ?? 16384;\r\n const maxRetries = options?.maxRetries ?? 3;\r\n const bufferThreshold = options?.bufferThreshold ?? MAX_BUFFER;\r\n const bufferTimeout = options?.bufferTimeout ?? 10_000;\r\n const total = Math.ceil(file.size / chunkSize);\r\n\r\n this.throwIfAborted(options?.signal);\r\n if (options?.maxFileSize && file.size > options.maxFileSize) {\r\n throw new Error(`File exceeds maxFileSize (${file.size} > ${options.maxFileSize})`);\r\n }\r\n\r\n const fileChannel = this.createChannel(`file-${file.name}`, {\r\n ordered: true,\r\n });\r\n\r\n await this.waitForChannelOpen(fileChannel);\r\n\r\n await this.waitForBuffer(fileChannel, bufferThreshold, bufferTimeout);\r\n fileChannel.send(JSON.stringify({\r\n type: 'file-meta',\r\n name: file.name,\r\n size: file.size,\r\n chunks: total,\r\n mimeType: file.type,\r\n }));\r\n\r\n let offset = 0;\r\n\r\n while (offset < file.size) {\r\n this.throwIfAborted(options?.signal);\r\n const end = Math.min(offset + chunkSize, file.size);\r\n const chunk = file.slice(offset, end);\r\n await this.waitForBuffer(fileChannel, bufferThreshold, bufferTimeout);\r\n await this.sendChunkWithRetry(fileChannel, chunk, maxRetries);\r\n offset = end;\r\n options?.onProgress?.(offset, file.size);\r\n }\r\n\r\n await this.waitForBuffer(fileChannel, bufferThreshold, bufferTimeout);\r\n fileChannel.send(JSON.stringify({ type: 'file-complete', name: file.name }));\r\n }\r\n\r\n dispose(): void {\r\n for (const [, channel] of this.channels) {\r\n channel.close();\r\n }\r\n this.channels.clear();\r\n this.messageHandlers.length = 0;\r\n this.fileHandlers.length = 0;\r\n this.fileReceives.clear();\r\n this.logger.info('DataManager disposed');\r\n }\r\n\r\n // ── private ──\r\n\r\n private registerChannel(label: string, channel: RTCDataChannel): void {\r\n channel.onopen = () => this.logger.info(`DataChannel \"${label}\" opened`);\r\n channel.onclose = () => {\r\n this.logger.info(`DataChannel \"${label}\" closed`);\r\n this.fileReceives.delete(label);\r\n };\r\n channel.onerror = (ev) => this.logger.warn(`DataChannel \"${label}\" error:`, ev);\r\n\r\n channel.onmessage = (ev) => {\r\n this.handleChannelMessage(label, ev.data);\r\n };\r\n\r\n this.channels.set(label, channel);\r\n }\r\n\r\n private handleChannelMessage(label: string, data: unknown): void {\r\n if (typeof data === 'string') {\r\n try {\r\n const msg = JSON.parse(data);\r\n if (msg.type === 'file-meta') {\r\n this.fileReceives.set(label, {\r\n meta: {\r\n name: msg.name,\r\n size: msg.size,\r\n type: msg.mimeType ?? 'application/octet-stream',\r\n },\r\n chunks: [],\r\n expectedChunks: msg.chunks,\r\n receivedBytes: 0,\r\n });\r\n return;\r\n }\r\n if (msg.type === 'file-complete') {\r\n const state = this.fileReceives.get(label);\r\n if (state) {\r\n if (state.chunks.length !== state.expectedChunks) {\r\n this.logger.warn(\r\n `File \"${state.meta.name}\" incomplete: ${state.chunks.length}/${state.expectedChunks} chunks`,\r\n );\r\n this.fileReceives.delete(label);\r\n return;\r\n }\r\n if (state.receivedBytes !== state.meta.size) {\r\n this.logger.warn(\r\n `File \"${state.meta.name}\" size mismatch: ${state.receivedBytes}/${state.meta.size}`,\r\n );\r\n this.fileReceives.delete(label);\r\n return;\r\n }\r\n const blob = new Blob(state.chunks, { type: state.meta.type });\r\n for (const handler of this.fileHandlers) {\r\n try { handler(state.meta, blob); } catch {}\r\n }\r\n this.fileReceives.delete(label);\r\n }\r\n return;\r\n }\r\n } catch {\r\n // 非 JSON,继续作为普通消息\r\n }\r\n }\r\n\r\n const receiveState = this.fileReceives.get(label);\r\n if (receiveState && (data instanceof Blob || data instanceof ArrayBuffer)) {\r\n const size = data instanceof Blob ? data.size : data.byteLength;\r\n receiveState.chunks.push(\r\n data instanceof Blob ? data : new Blob([data]),\r\n );\r\n receiveState.receivedBytes += size;\r\n return;\r\n }\r\n\r\n for (const handler of this.messageHandlers) {\r\n try {\r\n handler(data as ArrayBuffer | Blob | string, label);\r\n } catch {}\r\n }\r\n }\r\n\r\n private doSend(channel: RTCDataChannel, data: ArrayBuffer | Blob | string): void {\r\n if (typeof data === 'string') {\r\n channel.send(data);\r\n } else if (data instanceof ArrayBuffer) {\r\n if (data.byteLength > 256 * 1024) {\r\n this.logger.warn(`Large message (${data.byteLength} bytes), consider chunking`);\r\n }\r\n channel.send(data);\r\n } else {\r\n channel.send(data);\r\n }\r\n }\r\n\r\n private waitForChannelOpen(channel: RTCDataChannel, timeout = 10_000): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (channel.readyState === 'open') return resolve();\r\n const timer = setTimeout(() => reject(new Error('DataChannel open timeout')), timeout);\r\n const prev = channel.onopen;\r\n channel.onopen = (ev) => {\r\n clearTimeout(timer);\r\n prev?.call(channel, ev);\r\n resolve();\r\n };\r\n });\r\n }\r\n\r\n private waitForBuffer(\r\n channel: RTCDataChannel,\r\n maxBuffer = MAX_BUFFER,\r\n timeout = 10_000,\r\n ): Promise<void> {\r\n if (channel.bufferedAmount <= maxBuffer) return Promise.resolve();\r\n return new Promise((resolve, reject) => {\r\n const timer = setTimeout(() => {\r\n cleanup();\r\n reject(new Error('DataChannel buffer drain timeout'));\r\n }, timeout);\r\n const onClose = () => {\r\n cleanup();\r\n reject(new Error('DataChannel closed while waiting for buffer'));\r\n };\r\n const onError = () => {\r\n cleanup();\r\n reject(new Error('DataChannel error while waiting for buffer'));\r\n };\r\n const cleanup = () => {\r\n clearTimeout(timer);\r\n channel.removeEventListener('bufferedamountlow', check);\r\n channel.removeEventListener('close', onClose);\r\n channel.removeEventListener('error', onError);\r\n };\r\n const check = () => {\r\n if (channel.bufferedAmount <= maxBuffer) {\r\n cleanup();\r\n resolve();\r\n }\r\n };\r\n channel.bufferedAmountLowThreshold = maxBuffer;\r\n channel.addEventListener('bufferedamountlow', check);\r\n channel.addEventListener('close', onClose, { once: true });\r\n channel.addEventListener('error', onError, { once: true });\r\n });\r\n }\r\n\r\n private throwIfAborted(signal?: AbortSignal): void {\r\n if (signal?.aborted) {\r\n throw new DOMException('File transfer aborted', 'AbortError');\r\n }\r\n }\r\n\r\n private async sendChunkWithRetry(\r\n channel: RTCDataChannel,\r\n chunk: Blob,\r\n maxRetries: number,\r\n ): Promise<void> {\r\n for (let i = 0; i <= maxRetries; i++) {\r\n try {\r\n channel.send(chunk);\r\n return;\r\n } catch {\r\n if (i === maxRetries) throw new Error('Chunk send failed after retries');\r\n await new Promise(r => setTimeout(r, 500 * (i + 1)));\r\n }\r\n }\r\n }\r\n}\r\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IiIsIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZXMiOltdLCJzb3VyY2VzQ29udGVudCI6W119"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface DataChannelOptions {
|
|
2
|
+
ordered?: boolean;
|
|
3
|
+
maxPacketLifeTime?: number;
|
|
4
|
+
maxRetransmits?: number;
|
|
5
|
+
protocol?: string;
|
|
6
|
+
negotiated?: boolean;
|
|
7
|
+
id?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DataChannelConfig {
|
|
10
|
+
label: string;
|
|
11
|
+
options?: DataChannelOptions;
|
|
12
|
+
}
|
|
13
|
+
export interface FileTransferOptions {
|
|
14
|
+
/** 分片大小 (bytes),默认 16384 */
|
|
15
|
+
chunkSize?: number;
|
|
16
|
+
/** 超时重试次数,默认 3 */
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
/** 进度回调 */
|
|
19
|
+
onProgress?: (sent: number, total: number) => void;
|
|
20
|
+
/** 最大文件大小,默认不限制 */
|
|
21
|
+
maxFileSize?: number;
|
|
22
|
+
/** 取消传输 */
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
/** DataChannel 背压阈值,默认 256 KiB */
|
|
25
|
+
bufferThreshold?: number;
|
|
26
|
+
/** 等待背压释放超时,默认 10000ms */
|
|
27
|
+
bufferTimeout?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface FileMeta {
|
|
30
|
+
name: string;
|
|
31
|
+
size: number;
|
|
32
|
+
type: string;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/data/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW;IACX,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,mBAAmB;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW;IACX,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,kCAAkC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0BAA0B;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd"}
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface IDisposable {
|
|
2
|
+
dispose(): void;
|
|
3
|
+
}
|
|
4
|
+
export declare class DisposableStack implements IDisposable {
|
|
5
|
+
private disposables;
|
|
6
|
+
private disposed;
|
|
7
|
+
push(d: IDisposable): void;
|
|
8
|
+
defer(fn: () => void): void;
|
|
9
|
+
dispose(): void;
|
|
10
|
+
get isDisposed(): boolean;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=disposable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disposable.d.ts","sourceRoot":"","sources":["../../src/disposable.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,qBAAa,eAAgB,YAAW,WAAW;IACjD,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAS;IAEzB,IAAI,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI;IAQ1B,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAI3B,OAAO,IAAI,IAAI;IAaf,IAAI,UAAU,IAAI,OAAO,CAExB;CACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class DisposableStack {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.disposables = [];
|
|
4
|
+
this.disposed = false;
|
|
5
|
+
}
|
|
6
|
+
push(d) {
|
|
7
|
+
if (this.disposed) {
|
|
8
|
+
d.dispose();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
this.disposables.push(d);
|
|
12
|
+
}
|
|
13
|
+
defer(fn) {
|
|
14
|
+
this.push({ dispose: fn });
|
|
15
|
+
}
|
|
16
|
+
dispose() {
|
|
17
|
+
if (this.disposed)
|
|
18
|
+
return;
|
|
19
|
+
this.disposed = true;
|
|
20
|
+
for (let i = this.disposables.length - 1; i >= 0; i--) {
|
|
21
|
+
try {
|
|
22
|
+
this.disposables[i].dispose();
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
this.disposables.length = 0;
|
|
27
|
+
}
|
|
28
|
+
get isDisposed() {
|
|
29
|
+
return this.disposed;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
DisposableStack
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=disposable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"mappings":"AAIO,MAAM,gBAAuC;AAAA,EAA7C;AACL,SAAQ,cAA6B,CAAC;AACtC,SAAQ,WAAW;AAAA;AAAA,EAEnB,KAAK,GAAsB;AACzB,QAAI,KAAK,UAAU;AACjB,QAAE,QAAQ;AACV;AAAA,IACF;AACA,SAAK,YAAY,KAAK,CAAC;AAAA,EACzB;AAAA,EAEA,MAAM,IAAsB;AAC1B,SAAK,KAAK,EAAE,SAAS,GAAG,CAAC;AAAA,EAC3B;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK;AAAU;AACnB,SAAK,WAAW;AAChB,aAAS,IAAI,KAAK,YAAY,SAAS,GAAG,KAAK,GAAG,KAAK;AACrD,UAAI;AACF,aAAK,YAAY,CAAC,EAAE,QAAQ;AAAA,MAC9B,QAAQ;AAAA,MAER;AAAA,IACF;AACA,SAAK,YAAY,SAAS;AAAA,EAC5B;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AACF","names":[],"ignoreList":[],"sources":["../../src/disposable.ts"],"sourcesContent":["export interface IDisposable {\r\n dispose(): void;\r\n}\r\n\r\nexport class DisposableStack implements IDisposable {\r\n private disposables: IDisposable[] = [];\r\n private disposed = false;\r\n\r\n push(d: IDisposable): void {\r\n if (this.disposed) {\r\n d.dispose();\r\n return;\r\n }\r\n this.disposables.push(d);\r\n }\r\n\r\n defer(fn: () => void): void {\r\n this.push({ dispose: fn });\r\n }\r\n\r\n dispose(): void {\r\n if (this.disposed) return;\r\n this.disposed = true;\r\n for (let i = this.disposables.length - 1; i >= 0; i--) {\r\n try {\r\n this.disposables[i].dispose();\r\n } catch {\r\n // 忽略单个资源释放错误,继续释放其他资源\r\n }\r\n }\r\n this.disposables.length = 0;\r\n }\r\n\r\n get isDisposed(): boolean {\r\n return this.disposed;\r\n }\r\n}"]}
|
package/dist/es/fsm.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IDisposable } from './disposable';
|
|
2
|
+
export type ConnectionState = 'idle' | 'signaling' | 'connecting' | 'connected' | 'reconnecting' | 'failed' | 'disposed';
|
|
3
|
+
/**
|
|
4
|
+
* 连接状态机 — 防止非法状态调用
|
|
5
|
+
*
|
|
6
|
+
* idle → signaling → connecting → connected
|
|
7
|
+
* ↓ ↓ ↓
|
|
8
|
+
* disposed failed reconnecting
|
|
9
|
+
* ↓ ↓
|
|
10
|
+
* connected failed
|
|
11
|
+
*/
|
|
12
|
+
export declare class ConnectionStateMachine implements IDisposable {
|
|
13
|
+
private _state;
|
|
14
|
+
private listeners;
|
|
15
|
+
private static transitions;
|
|
16
|
+
get state(): ConnectionState;
|
|
17
|
+
/** 尝试转换状态,非法转换抛错 */
|
|
18
|
+
transition(to: ConnectionState): void;
|
|
19
|
+
/** 强制设置状态(仅用于 dispose / 错误恢复) */
|
|
20
|
+
force(to: ConnectionState): void;
|
|
21
|
+
is(...states: ConnectionState[]): boolean;
|
|
22
|
+
onChange(fn: (newState: ConnectionState, prev: ConnectionState) => void): () => void;
|
|
23
|
+
private notify;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=fsm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fsm.d.ts","sourceRoot":"","sources":["../../src/fsm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,MAAM,eAAe,GACvB,MAAM,GACN,WAAW,GACX,YAAY,GACZ,WAAW,GACX,cAAc,GACd,QAAQ,GACR,UAAU,CAAC;AAMf;;;;;;;;GAQG;AACH,qBAAa,sBAAuB,YAAW,WAAW;IACxD,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,SAAS,CAAsE;IAEvF,OAAO,CAAC,MAAM,CAAC,WAAW,CAQxB;IAEF,IAAI,KAAK,IAAI,eAAe,CAE3B;IAED,oBAAoB;IACpB,UAAU,CAAC,EAAE,EAAE,eAAe,GAAG,IAAI;IAarC,iCAAiC;IACjC,KAAK,CAAC,EAAE,EAAE,eAAe,GAAG,IAAI;IAMhC,EAAE,CAAC,GAAG,MAAM,EAAE,eAAe,EAAE,GAAG,OAAO;IAIzC,QAAQ,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI;IAOpF,OAAO,CAAC,MAAM;IAMd,OAAO,IAAI,IAAI;CAIhB"}
|
package/dist/es/fsm.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const _ConnectionStateMachine = class _ConnectionStateMachine {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._state = "idle";
|
|
4
|
+
this.listeners = [];
|
|
5
|
+
}
|
|
6
|
+
get state() {
|
|
7
|
+
return this._state;
|
|
8
|
+
}
|
|
9
|
+
/** 尝试转换状态,非法转换抛错 */
|
|
10
|
+
transition(to) {
|
|
11
|
+
const allowed = _ConnectionStateMachine.transitions[this._state];
|
|
12
|
+
if (!allowed.includes(to)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`[WetRTC] Invalid state transition: ${this._state} → ${to}. Allowed: ${allowed.join(", ")}`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const prev = this._state;
|
|
18
|
+
this._state = to;
|
|
19
|
+
this.notify(to, prev);
|
|
20
|
+
}
|
|
21
|
+
/** 强制设置状态(仅用于 dispose / 错误恢复) */
|
|
22
|
+
force(to) {
|
|
23
|
+
const prev = this._state;
|
|
24
|
+
this._state = to;
|
|
25
|
+
this.notify(to, prev);
|
|
26
|
+
}
|
|
27
|
+
is(...states) {
|
|
28
|
+
return states.includes(this._state);
|
|
29
|
+
}
|
|
30
|
+
onChange(fn) {
|
|
31
|
+
this.listeners.push(fn);
|
|
32
|
+
return () => {
|
|
33
|
+
this.listeners = this.listeners.filter((l) => l !== fn);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
notify(newState, prev) {
|
|
37
|
+
for (const fn of this.listeners) {
|
|
38
|
+
try {
|
|
39
|
+
fn(newState, prev);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
dispose() {
|
|
45
|
+
this.listeners.length = 0;
|
|
46
|
+
this.force("disposed");
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
_ConnectionStateMachine.transitions = {
|
|
50
|
+
idle: ["signaling", "disposed"],
|
|
51
|
+
signaling: ["connecting", "failed", "disposed"],
|
|
52
|
+
connecting: ["connected", "failed", "disposed"],
|
|
53
|
+
connected: ["reconnecting", "failed", "disposed"],
|
|
54
|
+
reconnecting: ["connected", "failed", "disposed"],
|
|
55
|
+
failed: ["idle", "disposed"],
|
|
56
|
+
disposed: []
|
|
57
|
+
};
|
|
58
|
+
let ConnectionStateMachine = _ConnectionStateMachine;
|
|
59
|
+
export {
|
|
60
|
+
ConnectionStateMachine
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=fsm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"mappings":"AAwBO,MAAM,0BAAN,MAAM,wBAA8C;AAAA,EAApD;AACL,SAAQ,SAA0B;AAClC,SAAQ,YAA4E,CAAC;AAAA;AAAA,EAYrF,IAAI,QAAyB;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,WAAW,IAA2B;AACpC,UAAM,UAAU,wBAAuB,YAAY,KAAK,MAAM;AAC9D,QAAI,CAAC,QAAQ,SAAS,EAAE,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,sCAAsC,KAAK,MAAM,MAAM,EAAE,cAC7C,QAAQ,KAAK,IAAI,CAAC;AAAA,MAChC;AAAA,IACF;AACA,UAAM,OAAO,KAAK;AAClB,SAAK,SAAS;AACd,SAAK,OAAO,IAAI,IAAI;AAAA,EACtB;AAAA;AAAA,EAGA,MAAM,IAA2B;AAC/B,UAAM,OAAO,KAAK;AAClB,SAAK,SAAS;AACd,SAAK,OAAO,IAAI,IAAI;AAAA,EACtB;AAAA,EAEA,MAAM,QAAoC;AACxC,WAAO,OAAO,SAAS,KAAK,MAAM;AAAA,EACpC;AAAA,EAEA,SAAS,IAA4E;AACnF,SAAK,UAAU,KAAK,EAAE;AACtB,WAAO,MAAM;AACX,WAAK,YAAY,KAAK,UAAU,OAAO,OAAK,MAAM,EAAE;AAAA,IACtD;AAAA,EACF;AAAA,EAEQ,OAAO,UAA2B,MAA6B;AACrE,eAAW,MAAM,KAAK,WAAW;AAC/B,UAAI;AAAE,WAAG,UAAU,IAAI;AAAA,MAAG,QAAQ;AAAA,MAAgB;AAAA,IACpD;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,UAAU,SAAS;AACxB,SAAK,MAAM,UAAU;AAAA,EACvB;AACF;AA5Da,wBAII,cAAkC;AAAA,EAC/C,MAAc,CAAC,aAAa,UAAU;AAAA,EACtC,WAAc,CAAC,cAAc,UAAU,UAAU;AAAA,EACjD,YAAc,CAAC,aAAa,UAAU,UAAU;AAAA,EAChD,WAAc,CAAC,gBAAgB,UAAU,UAAU;AAAA,EACnD,cAAc,CAAC,aAAa,UAAU,UAAU;AAAA,EAChD,QAAc,CAAC,QAAQ,UAAU;AAAA,EACjC,UAAc,CAAC;AACjB;AAZK,IAAM,yBAAN","names":[],"ignoreList":[],"sources":["../../src/fsm.ts"],"sourcesContent":["import type { IDisposable } from './disposable';\r\n\r\nexport type ConnectionState =\r\n | 'idle'\r\n | 'signaling'\r\n | 'connecting'\r\n | 'connected'\r\n | 'reconnecting'\r\n | 'failed'\r\n | 'disposed';\r\n\r\ntype StateTransitionMap = {\r\n [S in ConnectionState]: ConnectionState[];\r\n};\r\n\r\n/**\r\n * 连接状态机 — 防止非法状态调用\r\n *\r\n * idle → signaling → connecting → connected\r\n * ↓ ↓ ↓\r\n * disposed failed reconnecting\r\n * ↓ ↓\r\n * connected failed\r\n */\r\nexport class ConnectionStateMachine implements IDisposable {\r\n private _state: ConnectionState = 'idle';\r\n private listeners: ((newState: ConnectionState, prev: ConnectionState) => void)[] = [];\r\n\r\n private static transitions: StateTransitionMap = {\r\n idle: ['signaling', 'disposed'],\r\n signaling: ['connecting', 'failed', 'disposed'],\r\n connecting: ['connected', 'failed', 'disposed'],\r\n connected: ['reconnecting', 'failed', 'disposed'],\r\n reconnecting: ['connected', 'failed', 'disposed'],\r\n failed: ['idle', 'disposed'],\r\n disposed: [],\r\n };\r\n\r\n get state(): ConnectionState {\r\n return this._state;\r\n }\r\n\r\n /** 尝试转换状态,非法转换抛错 */\r\n transition(to: ConnectionState): void {\r\n const allowed = ConnectionStateMachine.transitions[this._state];\r\n if (!allowed.includes(to)) {\r\n throw new Error(\r\n `[WetRTC] Invalid state transition: ${this._state} → ${to}. ` +\r\n `Allowed: ${allowed.join(', ')}`\r\n );\r\n }\r\n const prev = this._state;\r\n this._state = to;\r\n this.notify(to, prev);\r\n }\r\n\r\n /** 强制设置状态(仅用于 dispose / 错误恢复) */\r\n force(to: ConnectionState): void {\r\n const prev = this._state;\r\n this._state = to;\r\n this.notify(to, prev);\r\n }\r\n\r\n is(...states: ConnectionState[]): boolean {\r\n return states.includes(this._state);\r\n }\r\n\r\n onChange(fn: (newState: ConnectionState, prev: ConnectionState) => void): () => void {\r\n this.listeners.push(fn);\r\n return () => {\r\n this.listeners = this.listeners.filter(l => l !== fn);\r\n };\r\n }\r\n\r\n private notify(newState: ConnectionState, prev: ConnectionState): void {\r\n for (const fn of this.listeners) {\r\n try { fn(newState, prev); } catch { /* 忽略监听器异常 */ }\r\n }\r\n }\r\n\r\n dispose(): void {\r\n this.listeners.length = 0;\r\n this.force('disposed');\r\n }\r\n}//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IiIsIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZXMiOltdLCJzb3VyY2VzQ29udGVudCI6W119"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { WetRTC } from './wetrtc';
|
|
2
|
+
export { SignalManager } from './signal/signal-manager';
|
|
3
|
+
export { MediaManager } from './media/media-manager';
|
|
4
|
+
export { DataManager } from './data/data-manager';
|
|
5
|
+
export { StatsMonitor } from './stats/stats-monitor';
|
|
6
|
+
export { ConnectionStateMachine } from './fsm';
|
|
7
|
+
export type { ConnectionState } from './fsm';
|
|
8
|
+
export { DisposableStack, type IDisposable } from './disposable';
|
|
9
|
+
export type { WetRTCConfig } from './wetrtc';
|
|
10
|
+
export type { SignalChannel, SignalMessage } from './signal/types';
|
|
11
|
+
export type { SignalConfig } from './signal/signal-manager';
|
|
12
|
+
export type { DeviceInfo, DisplayMediaOptions, UserMediaOptions, MediaConstraints, } from './media/types';
|
|
13
|
+
export type { VideoEncodingOptions } from './media/video-encoding';
|
|
14
|
+
export type { AudioEncodingOptions } from './media/audio-encoding';
|
|
15
|
+
export type { PreferredVideoCodec, PreferredAudioCodec, VideoRtpCodec, AudioRtpCodec, } from './media/codec-preference';
|
|
16
|
+
export { applyVideoTrackContentHint, applyVideoSenderEncoding, applyReceiverPlayoutDelay, applyVideoEncodingToConnection, } from './media/video-encoding';
|
|
17
|
+
export { applyAudioSenderEncoding, applyAudioEncodingToConnection, } from './media/audio-encoding';
|
|
18
|
+
export { sortVideoCodecsH264First, applyH264CodecPreference, applyH264CodecPreferences, sortAudioCodecsOpusFirst, applyOpusCodecPreference, applyOpusCodecPreferences, } from './media/codec-preference';
|
|
19
|
+
export type { DataChannelOptions, DataChannelConfig, FileTransferOptions, FileMeta, } from './data/types';
|
|
20
|
+
export type { StatsSnapshot, DiagnosticReport, StatsMonitorOptions, } from './stats/types';
|
|
21
|
+
export type { WetRTCEvent, WetRTCEventMap, WetRTCError, WetRTCErrorCode, } from './utils/types';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,OAAO,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAEjE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnE,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,YAAY,EACV,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,aAAa,EACb,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,wBAAwB,EACxB,8BAA8B,GAC/B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,QAAQ,GACT,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,WAAW,EACX,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,eAAe,CAAC"}
|