@meframe/core 0.0.17 → 0.0.18
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/dist/controllers/PlaybackController.d.ts +1 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/{stages/compose → orchestrator}/GlobalAudioSession.d.ts +13 -7
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -0
- package/dist/{stages/compose → orchestrator}/GlobalAudioSession.js +56 -4
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -0
- package/dist/orchestrator/Orchestrator.d.ts +2 -8
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +17 -9
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +15 -1
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/decode/AudioChunkDecoder.js +169 -0
- package/dist/stages/decode/AudioChunkDecoder.js.map +1 -0
- package/dist/stages/demux/types.d.ts +6 -0
- package/dist/stages/demux/types.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.d.ts +1 -1
- package/dist/stages/mux/MuxManager.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.js.map +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js +92 -54
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +0 -1
- package/dist/stages/compose/GlobalAudioSession.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MuxManager.js","sources":["../../../src/stages/mux/MuxManager.ts"],"sourcesContent":["import type { CompositionModel } from '../../model/CompositionModel';\nimport type { ExportOptions } from '../../types';\nimport { GlobalAudioSession } from '../compose/GlobalAudioSession';\nimport { MP4Muxer } from './MP4Muxer';\nimport { CacheManager } from '@/cache/CacheManager';\n\n/**\n * MuxManager: Main thread muxing service\n * Reads encoded chunks from L2 cache and muxes into final video file\n */\nexport class MuxManager {\n private cacheManager: CacheManager;\n private audioSession: GlobalAudioSession;\n private audioEncoderConfig: AudioEncoderConfig;\n private audioEncodedStream: ReadableStream<EncodedAudioChunk> | null = null;\n private audioFirstChunk: EncodedAudioChunk | null = null;\n\n constructor(\n cacheManager: CacheManager,\n audioSession: GlobalAudioSession,\n audioEncoderConfig: AudioEncoderConfig\n ) {\n this.cacheManager = cacheManager;\n this.audioSession = audioSession;\n this.audioEncoderConfig = audioEncoderConfig;\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n const videoTrack = model.tracks.find((t) => t.kind === 'video');\n if (!videoTrack || videoTrack.clips.length === 0) {\n throw new Error('No video clips in composition');\n }\n\n const sortedClips = [...videoTrack.clips].sort((a, b) => a.startUs - b.startUs);\n await this.checkL2Coverage(sortedClips);\n\n const width = options.width || (model as any).renderConfig?.width || 720;\n const height = options.height || (model as any).renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n // Get video metadata from L2 cache (first clip)\n const videoChunkMeta = sortedClips[0]\n ? await this.cacheManager.getL2Metadata(sortedClips[0].id, 'video')\n : null;\n\n if (!videoChunkMeta) {\n console.warn('[MuxManager] No videoChunkMeta available, export may fail');\n }\n\n const audioTrack = model.tracks.find((t) => t.kind === 'audio');\n\n // Get audio metadata from first chunk if audio exists\n let audioChunkMeta: any = undefined;\n if (audioTrack?.clips.length) {\n audioChunkMeta = await this.getAudioChunkMeta();\n }\n\n const muxer = new MP4Muxer({\n width,\n height,\n fps,\n fastStart: 'in-memory',\n videoChunkMeta,\n audioChunkMeta,\n });\n\n await this.writeVideoChunks(muxer, sortedClips);\n if (audioTrack?.clips.length) {\n await this.writeAudioChunks(muxer);\n }\n\n return muxer.finalize();\n }\n\n /**\n * Get audio chunk metadata by reading first chunk from encoder\n * Caches the stream and first chunk for later use\n */\n private async getAudioChunkMeta(): Promise<any> {\n const audioEncoderConfig = this.audioEncoderConfig;\n let metadata: any = null;\n\n this.audioEncodedStream = await this.audioSession.createExportEncodedStream(\n audioEncoderConfig,\n (meta) => {\n // Extract decoderConfig from first chunk metadata\n if (meta.decoderConfig) {\n metadata = meta.decoderConfig;\n }\n }\n );\n\n if (!this.audioEncodedStream) {\n return null;\n }\n\n // Read first chunk to trigger metadata extraction and cache it\n const reader = this.audioEncodedStream.getReader();\n try {\n const { done, value } = await reader.read();\n if (!done && value) {\n this.audioFirstChunk = value;\n }\n reader.releaseLock();\n } catch (error) {\n console.error('[MuxManager] Failed to read first audio chunk:', error);\n reader.releaseLock();\n this.audioEncodedStream = null;\n return null;\n }\n\n return metadata;\n }\n\n private async writeVideoChunks(muxer: MP4Muxer, sortedClips: any[]): Promise<void> {\n let firstKeyframeWritten = false;\n let exportTimeUs = 0;\n\n for (const clip of sortedClips) {\n const stream = await this.cacheManager.createL2ReadStream(clip.id, 'video');\n if (!stream) {\n console.warn(`[MuxManager] No video stream for clip ${clip.id}`);\n continue;\n }\n\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const originalChunk = value as EncodedVideoChunk;\n\n // Copy chunk data\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n // Rewrite timestamp to export timeline (tight concatenation)\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: exportTimeUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n // Ensure first chunk in export is a keyframe\n if (!firstKeyframeWritten) {\n if (remappedChunk.type !== 'key') {\n console.warn(`[MuxManager] Skipping non-keyframe at start of clip ${clip.id}`);\n exportTimeUs += originalChunk.duration || 0;\n continue;\n }\n firstKeyframeWritten = true;\n }\n\n muxer.writeVideoChunk(remappedChunk);\n exportTimeUs += originalChunk.duration || 0;\n }\n } finally {\n reader.releaseLock();\n }\n }\n }\n\n private async writeAudioChunks(muxer: MP4Muxer): Promise<void> {\n // Use cached stream from getAudioChunkMeta()\n if (!this.audioEncodedStream) {\n console.warn('[MuxManager] No audio stream available');\n return;\n }\n\n let exportTimeUs = 0;\n\n // Write the cached first chunk with remapped timestamp\n if (this.audioFirstChunk) {\n const buffer = new ArrayBuffer(this.audioFirstChunk.byteLength);\n this.audioFirstChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedAudioChunk({\n type: this.audioFirstChunk.type,\n timestamp: exportTimeUs,\n duration: this.audioFirstChunk.duration ?? undefined,\n data: buffer,\n });\n muxer.writeAudioChunk(remappedChunk);\n exportTimeUs += this.audioFirstChunk.duration || 0;\n }\n\n // Continue reading remaining chunks\n const reader = this.audioEncodedStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const originalChunk = value as EncodedAudioChunk;\n\n // Copy chunk data\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n // Rewrite timestamp to export timeline\n const remappedChunk = new EncodedAudioChunk({\n type: originalChunk.type,\n timestamp: exportTimeUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n muxer.writeAudioChunk(remappedChunk);\n exportTimeUs += originalChunk.duration || 0;\n }\n } finally {\n reader.releaseLock();\n // Clean up cached state\n this.audioEncodedStream = null;\n this.audioFirstChunk = null;\n }\n }\n\n private async checkL2Coverage(clips: any[]): Promise<void> {\n const missingClips: string[] = [];\n for (const clip of clips) {\n const inL2 = await this.cacheManager.hasClipInL2(clip.id, 'video');\n if (!inL2) {\n missingClips.push(clip.id);\n }\n }\n\n if (missingClips.length > 0) {\n const clipList = missingClips.slice(0, 3).join(', ');\n const moreText = missingClips.length > 3 ? ` and ${missingClips.length - 3} more` : '';\n throw new Error(\n `Export failed: ${missingClips.length} clip(s) not cached (${clipList}${moreText}). ` +\n `Please start PreRenderService and wait for background caching to complete.`\n );\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MuxManager.js","sources":["../../../src/stages/mux/MuxManager.ts"],"sourcesContent":["import type { CompositionModel } from '../../model/CompositionModel';\nimport type { ExportOptions } from '../../types';\nimport { GlobalAudioSession } from '../../orchestrator/GlobalAudioSession';\nimport { MP4Muxer } from './MP4Muxer';\nimport { CacheManager } from '@/cache/CacheManager';\n\n/**\n * MuxManager: Main thread muxing service\n * Reads encoded chunks from L2 cache and muxes into final video file\n */\nexport class MuxManager {\n private cacheManager: CacheManager;\n private audioSession: GlobalAudioSession;\n private audioEncoderConfig: AudioEncoderConfig;\n private audioEncodedStream: ReadableStream<EncodedAudioChunk> | null = null;\n private audioFirstChunk: EncodedAudioChunk | null = null;\n\n constructor(\n cacheManager: CacheManager,\n audioSession: GlobalAudioSession,\n audioEncoderConfig: AudioEncoderConfig\n ) {\n this.cacheManager = cacheManager;\n this.audioSession = audioSession;\n this.audioEncoderConfig = audioEncoderConfig;\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n const videoTrack = model.tracks.find((t) => t.kind === 'video');\n if (!videoTrack || videoTrack.clips.length === 0) {\n throw new Error('No video clips in composition');\n }\n\n const sortedClips = [...videoTrack.clips].sort((a, b) => a.startUs - b.startUs);\n await this.checkL2Coverage(sortedClips);\n\n const width = options.width || (model as any).renderConfig?.width || 720;\n const height = options.height || (model as any).renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n // Get video metadata from L2 cache (first clip)\n const videoChunkMeta = sortedClips[0]\n ? await this.cacheManager.getL2Metadata(sortedClips[0].id, 'video')\n : null;\n\n if (!videoChunkMeta) {\n console.warn('[MuxManager] No videoChunkMeta available, export may fail');\n }\n\n const audioTrack = model.tracks.find((t) => t.kind === 'audio');\n\n // Get audio metadata from first chunk if audio exists\n let audioChunkMeta: any = undefined;\n if (audioTrack?.clips.length) {\n audioChunkMeta = await this.getAudioChunkMeta();\n }\n\n const muxer = new MP4Muxer({\n width,\n height,\n fps,\n fastStart: 'in-memory',\n videoChunkMeta,\n audioChunkMeta,\n });\n\n await this.writeVideoChunks(muxer, sortedClips);\n if (audioTrack?.clips.length) {\n await this.writeAudioChunks(muxer);\n }\n\n return muxer.finalize();\n }\n\n /**\n * Get audio chunk metadata by reading first chunk from encoder\n * Caches the stream and first chunk for later use\n */\n private async getAudioChunkMeta(): Promise<any> {\n const audioEncoderConfig = this.audioEncoderConfig;\n let metadata: any = null;\n\n this.audioEncodedStream = await this.audioSession.createExportEncodedStream(\n audioEncoderConfig,\n (meta) => {\n // Extract decoderConfig from first chunk metadata\n if (meta.decoderConfig) {\n metadata = meta.decoderConfig;\n }\n }\n );\n\n if (!this.audioEncodedStream) {\n return null;\n }\n\n // Read first chunk to trigger metadata extraction and cache it\n const reader = this.audioEncodedStream.getReader();\n try {\n const { done, value } = await reader.read();\n if (!done && value) {\n this.audioFirstChunk = value;\n }\n reader.releaseLock();\n } catch (error) {\n console.error('[MuxManager] Failed to read first audio chunk:', error);\n reader.releaseLock();\n this.audioEncodedStream = null;\n return null;\n }\n\n return metadata;\n }\n\n private async writeVideoChunks(muxer: MP4Muxer, sortedClips: any[]): Promise<void> {\n let firstKeyframeWritten = false;\n let exportTimeUs = 0;\n\n for (const clip of sortedClips) {\n const stream = await this.cacheManager.createL2ReadStream(clip.id, 'video');\n if (!stream) {\n console.warn(`[MuxManager] No video stream for clip ${clip.id}`);\n continue;\n }\n\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const originalChunk = value as EncodedVideoChunk;\n\n // Copy chunk data\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n // Rewrite timestamp to export timeline (tight concatenation)\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: exportTimeUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n // Ensure first chunk in export is a keyframe\n if (!firstKeyframeWritten) {\n if (remappedChunk.type !== 'key') {\n console.warn(`[MuxManager] Skipping non-keyframe at start of clip ${clip.id}`);\n exportTimeUs += originalChunk.duration || 0;\n continue;\n }\n firstKeyframeWritten = true;\n }\n\n muxer.writeVideoChunk(remappedChunk);\n exportTimeUs += originalChunk.duration || 0;\n }\n } finally {\n reader.releaseLock();\n }\n }\n }\n\n private async writeAudioChunks(muxer: MP4Muxer): Promise<void> {\n // Use cached stream from getAudioChunkMeta()\n if (!this.audioEncodedStream) {\n console.warn('[MuxManager] No audio stream available');\n return;\n }\n\n let exportTimeUs = 0;\n\n // Write the cached first chunk with remapped timestamp\n if (this.audioFirstChunk) {\n const buffer = new ArrayBuffer(this.audioFirstChunk.byteLength);\n this.audioFirstChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedAudioChunk({\n type: this.audioFirstChunk.type,\n timestamp: exportTimeUs,\n duration: this.audioFirstChunk.duration ?? undefined,\n data: buffer,\n });\n muxer.writeAudioChunk(remappedChunk);\n exportTimeUs += this.audioFirstChunk.duration || 0;\n }\n\n // Continue reading remaining chunks\n const reader = this.audioEncodedStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const originalChunk = value as EncodedAudioChunk;\n\n // Copy chunk data\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n // Rewrite timestamp to export timeline\n const remappedChunk = new EncodedAudioChunk({\n type: originalChunk.type,\n timestamp: exportTimeUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n muxer.writeAudioChunk(remappedChunk);\n exportTimeUs += originalChunk.duration || 0;\n }\n } finally {\n reader.releaseLock();\n // Clean up cached state\n this.audioEncodedStream = null;\n this.audioFirstChunk = null;\n }\n }\n\n private async checkL2Coverage(clips: any[]): Promise<void> {\n const missingClips: string[] = [];\n for (const clip of clips) {\n const inL2 = await this.cacheManager.hasClipInL2(clip.id, 'video');\n if (!inL2) {\n missingClips.push(clip.id);\n }\n }\n\n if (missingClips.length > 0) {\n const clipList = missingClips.slice(0, 3).join(', ');\n const moreText = missingClips.length > 3 ? ` and ${missingClips.length - 3} more` : '';\n throw new Error(\n `Export failed: ${missingClips.length} clip(s) not cached (${clipList}${moreText}). ` +\n `Please start PreRenderService and wait for background caching to complete.`\n );\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ class VideoDemuxWorker {
|
|
|
6
6
|
sessionId = null;
|
|
7
7
|
videoDownstreamPort = null;
|
|
8
8
|
audioDownstreamPort = null;
|
|
9
|
+
emitAudioToMain = false;
|
|
9
10
|
constructor() {
|
|
10
11
|
this.channel = new WorkerChannel(self, {
|
|
11
12
|
name: "VideoDemuxWorker",
|
|
@@ -21,6 +22,90 @@ class VideoDemuxWorker {
|
|
|
21
22
|
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
22
23
|
this.channel.receiveStream(this.handleReceiveStream.bind(this));
|
|
23
24
|
}
|
|
25
|
+
// ===== Helper methods (reduce duplication) =====
|
|
26
|
+
buildAudioDecoderConfig() {
|
|
27
|
+
const info = this.demuxer?.audioTrackInfo;
|
|
28
|
+
if (!info) return void 0;
|
|
29
|
+
return {
|
|
30
|
+
codec: info.codec,
|
|
31
|
+
sampleRate: info.sampleRate,
|
|
32
|
+
numberOfChannels: info.numberOfChannels,
|
|
33
|
+
description: info.description
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
sendVideoConfigure() {
|
|
37
|
+
if (!this.videoDownstreamPort || !this.demuxer) return;
|
|
38
|
+
const videoTrackInfo = this.demuxer.videoTrackInfo;
|
|
39
|
+
if (!videoTrackInfo) {
|
|
40
|
+
console.error("[VideoDemuxWorker] No video track found after ready");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const downstreamChannel = new WorkerChannel(this.videoDownstreamPort, {
|
|
44
|
+
name: "VideoDemux-Decoder",
|
|
45
|
+
timeout: 3e4
|
|
46
|
+
});
|
|
47
|
+
downstreamChannel.send("configure", {
|
|
48
|
+
sessionId: this.sessionId,
|
|
49
|
+
streamType: "video",
|
|
50
|
+
codec: videoTrackInfo.codec,
|
|
51
|
+
width: videoTrackInfo.width,
|
|
52
|
+
height: videoTrackInfo.height,
|
|
53
|
+
description: videoTrackInfo.description
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
sendAudioConfigure() {
|
|
57
|
+
if (!this.audioDownstreamPort) return;
|
|
58
|
+
const cfg = this.buildAudioDecoderConfig();
|
|
59
|
+
if (!cfg) return;
|
|
60
|
+
const audioChannel = new WorkerChannel(this.audioDownstreamPort, {
|
|
61
|
+
name: "VideoDemux-AudioDecoder",
|
|
62
|
+
timeout: 3e4
|
|
63
|
+
});
|
|
64
|
+
audioChannel.send("configure", {
|
|
65
|
+
sessionId: this.sessionId,
|
|
66
|
+
streamType: "audio",
|
|
67
|
+
...cfg
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
sendVideoStream(videoStream) {
|
|
71
|
+
if (!this.videoDownstreamPort) return;
|
|
72
|
+
const videoChannel = new WorkerChannel(this.videoDownstreamPort, {
|
|
73
|
+
name: "VideoDemux-Decoder",
|
|
74
|
+
timeout: 3e4
|
|
75
|
+
});
|
|
76
|
+
videoChannel.sendStream(videoStream, {
|
|
77
|
+
streamType: "video",
|
|
78
|
+
sessionId: this.sessionId
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
sendAudioStream(audioStream) {
|
|
82
|
+
if (!audioStream) return;
|
|
83
|
+
if (this.audioDownstreamPort) {
|
|
84
|
+
const audioChannel = new WorkerChannel(this.audioDownstreamPort, {
|
|
85
|
+
name: "VideoDemux-AudioDecoder",
|
|
86
|
+
timeout: 3e4
|
|
87
|
+
});
|
|
88
|
+
audioChannel.sendStream(audioStream, {
|
|
89
|
+
streamType: "audio",
|
|
90
|
+
sessionId: this.sessionId
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.emitAudioToMain) {
|
|
95
|
+
const mapped = audioStream.pipeThrough(
|
|
96
|
+
new TransformStream({
|
|
97
|
+
transform: (chunk, ctl) => {
|
|
98
|
+
const decoderConfig = this.buildAudioDecoderConfig();
|
|
99
|
+
ctl.enqueue({ chunk, metadata: { decoderConfig } });
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
this.channel.sendStream(mapped, {
|
|
104
|
+
streamType: "audio",
|
|
105
|
+
sessionId: this.sessionId
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
24
109
|
/**
|
|
25
110
|
* Handle connection from orchestrator
|
|
26
111
|
*/
|
|
@@ -52,6 +137,7 @@ class VideoDemuxWorker {
|
|
|
52
137
|
if (this.demuxer) {
|
|
53
138
|
this.demuxer.destroy();
|
|
54
139
|
}
|
|
140
|
+
this.emitAudioToMain = !!config.emitAudioToMain;
|
|
55
141
|
this.demuxer = new MP4Demuxer({
|
|
56
142
|
...config,
|
|
57
143
|
onReady: () => this.handleDemuxerReady()
|
|
@@ -83,71 +169,23 @@ class VideoDemuxWorker {
|
|
|
83
169
|
onReady: () => this.handleDemuxerReady()
|
|
84
170
|
});
|
|
85
171
|
}
|
|
86
|
-
if (!this.videoDownstreamPort) {
|
|
172
|
+
if (!this.videoDownstreamPort && !this.emitAudioToMain) {
|
|
87
173
|
console.error("[VideoDemuxWorker] Decoder not connected", this.sessionId);
|
|
88
174
|
return;
|
|
89
175
|
}
|
|
90
176
|
const videoStream = this.demuxer.createVideoStream();
|
|
91
177
|
const audioStream = this.demuxer.createAudioStream();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
timeout: 3e4
|
|
95
|
-
});
|
|
96
|
-
videoChannel.sendStream(videoStream, {
|
|
97
|
-
streamType: "video",
|
|
98
|
-
sessionId: this.sessionId
|
|
99
|
-
});
|
|
100
|
-
if (audioStream && this.audioDownstreamPort) {
|
|
101
|
-
const audioChannel = new WorkerChannel(this.audioDownstreamPort, {
|
|
102
|
-
name: "VideoDemux-AudioDecoder",
|
|
103
|
-
timeout: 3e4
|
|
104
|
-
});
|
|
105
|
-
audioChannel.sendStream(audioStream, {
|
|
106
|
-
streamType: "audio",
|
|
107
|
-
sessionId: this.sessionId
|
|
108
|
-
});
|
|
109
|
-
}
|
|
178
|
+
this.sendVideoStream(videoStream);
|
|
179
|
+
this.sendAudioStream(audioStream);
|
|
110
180
|
const inputStream = this.demuxer.createInputStream();
|
|
111
181
|
await stream.pipeTo(inputStream).catch((error) => {
|
|
112
182
|
console.error("[VideoDemuxWorker] Input stream error:", this.sessionId, error);
|
|
113
183
|
});
|
|
114
184
|
}
|
|
115
185
|
handleDemuxerReady() {
|
|
116
|
-
if (!this.demuxer
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const videoTrackInfo = this.demuxer.videoTrackInfo;
|
|
120
|
-
if (!videoTrackInfo) {
|
|
121
|
-
console.error("[VideoDemuxWorker] No video track found after ready");
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
const downstreamChannel = new WorkerChannel(this.videoDownstreamPort, {
|
|
125
|
-
name: "VideoDemux-Decoder",
|
|
126
|
-
timeout: 3e4
|
|
127
|
-
});
|
|
128
|
-
downstreamChannel.send("configure", {
|
|
129
|
-
sessionId: this.sessionId,
|
|
130
|
-
streamType: "video",
|
|
131
|
-
codec: videoTrackInfo.codec,
|
|
132
|
-
width: videoTrackInfo.width,
|
|
133
|
-
height: videoTrackInfo.height,
|
|
134
|
-
description: videoTrackInfo.description
|
|
135
|
-
});
|
|
136
|
-
const audioTrackInfo = this.demuxer.audioTrackInfo;
|
|
137
|
-
if (audioTrackInfo && this.audioDownstreamPort) {
|
|
138
|
-
const audioChannel = new WorkerChannel(this.audioDownstreamPort, {
|
|
139
|
-
name: "VideoDemux-AudioDecoder",
|
|
140
|
-
timeout: 3e4
|
|
141
|
-
});
|
|
142
|
-
audioChannel.send("configure", {
|
|
143
|
-
sessionId: this.sessionId,
|
|
144
|
-
streamType: "audio",
|
|
145
|
-
codec: audioTrackInfo.codec,
|
|
146
|
-
sampleRate: audioTrackInfo.sampleRate,
|
|
147
|
-
numberOfChannels: audioTrackInfo.numberOfChannels,
|
|
148
|
-
description: audioTrackInfo.description
|
|
149
|
-
});
|
|
150
|
-
}
|
|
186
|
+
if (!this.demuxer) return;
|
|
187
|
+
this.sendVideoConfigure();
|
|
188
|
+
this.sendAudioConfigure();
|
|
151
189
|
}
|
|
152
190
|
/**
|
|
153
191
|
* Get demuxer statistics
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-demux.worker.js","sources":["../../../../src/stages/demux/video-demux.worker.ts"],"sourcesContent":["import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { MP4Demuxer } from './MP4Demuxer';\nimport type { DemuxConfig } from './types';\n\ninterface LoaderStreamMetadata {\n sessionId?: string;\n byteStart?: number;\n byteEnd?: number;\n}\n/**\n * VideoDemuxWorker - First stage for video processing\n * Extracts video tracks from container formats (MP4, etc.)\n *\n * Pipeline: ResourceLoader (Main Thread) → VideoDemuxWorker → DecodeWorker\n *\n * Architecture Note:\n * - One VideoDemuxWorker instance per CLIP (not per resource)\n * - Multiple clips can share the same resource (different workers, independent processing)\n * - This enables clean 2-Clip strategy lifecycle management\n *\n * Features:\n * - MP4 container demuxing with mp4box.js\n * - Stream-based processing with backpressure\n * - Direct streaming to DecodeWorker\n */\nexport class VideoDemuxWorker {\n private channel: WorkerChannel;\n private demuxer: MP4Demuxer | null = null;\n private sessionId: string | null = null;\n private videoDownstreamPort: MessagePort | null = null;\n private audioDownstreamPort: MessagePort | null = null;\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as any, {\n name: 'VideoDemuxWorker',\n timeout: 30000,\n });\n this.setupHandlers();\n }\n\n /* @better-ai.mdc For test visibility */\n protected setupHandlers(): void {\n // Register message handlers\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n this.channel.registerHandler('connect', this.handleConnect.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n\n // Setup stream receiver from ResourceLoader (main thread)\n this.channel.receiveStream(this.handleReceiveStream.bind(this));\n }\n\n /**\n * Handle connection from orchestrator\n */\n private async handleConnect(payload: {\n port?: MessagePort;\n streamType?: string;\n sessionId?: string;\n direction?: string;\n }): Promise<{ success: boolean }> {\n const { port, sessionId, direction, streamType } = payload;\n\n if (!port) {\n return { success: false };\n }\n\n if (direction === 'downstream') {\n if (streamType === 'audio') {\n this.audioDownstreamPort = port;\n } else {\n this.videoDownstreamPort = port;\n }\n }\n\n this.sessionId = sessionId || null;\n return { success: true };\n }\n\n /**\n * Configure demuxer with format settings\n * @param payload.config - Demuxer configuration\n * @param payload.initial - If true, initialize worker state; otherwise just update config\n */\n private async handleConfigure(payload: {\n config: DemuxConfig;\n initial?: boolean;\n }): Promise<{ success: boolean; tracks?: any[] }> {\n const { config, initial = false } = payload;\n\n try {\n if (initial) {\n // Initial setup - set worker state to ready\n this.channel.state = WorkerState.Ready;\n\n // Create new demuxer instance\n if (this.demuxer) {\n this.demuxer.destroy();\n }\n\n this.demuxer = new MP4Demuxer({\n ...config,\n onReady: () => this.handleDemuxerReady(),\n });\n\n // Notify configuration complete\n this.channel.notify('configured');\n\n return { success: true };\n } else {\n // Update configuration only (e.g., backpressure settings)\n this.demuxer?.updateConfig(config);\n return { success: true };\n }\n } catch (error: any) {\n throw {\n code: error.code || 'CONFIG_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Handle input stream from ResourceLoader (main thread)\n * Strategy: Stream immediately, send codec info when ready\n */\n private async handleReceiveStream(\n stream: ReadableStream<Uint8Array | ArrayBuffer>,\n metadata?: LoaderStreamMetadata\n ): Promise<void> {\n // Store sessionId from metadata (only happens once per worker lifecycle)\n // Note: Do NOT override if already set by handleConnect (which has correct sessionId)\n if (!this.sessionId) {\n this.sessionId = metadata?.sessionId || this.sessionId;\n }\n\n // Initialize demuxer on first stream\n if (!this.demuxer) {\n this.demuxer = new MP4Demuxer({\n highWaterMark: 10,\n onReady: () => this.handleDemuxerReady(),\n });\n }\n\n if (!this.videoDownstreamPort) {\n console.error('[VideoDemuxWorker] Decoder not connected', this.sessionId);\n return;\n }\n\n // Create output streams (readable only)\n const videoStream = this.demuxer.createVideoStream();\n const audioStream = this.demuxer.createAudioStream();\n\n // Send video stream to decoder\n const videoChannel = new WorkerChannel(this.videoDownstreamPort, {\n name: 'VideoDemux-Decoder',\n timeout: 30000,\n });\n videoChannel.sendStream(videoStream, {\n streamType: 'video',\n sessionId: this.sessionId,\n });\n\n // Send audio stream to decoder if available\n if (audioStream && this.audioDownstreamPort) {\n const audioChannel = new WorkerChannel(this.audioDownstreamPort, {\n name: 'VideoDemux-AudioDecoder',\n timeout: 30000,\n });\n audioChannel.sendStream(audioStream, {\n streamType: 'audio',\n sessionId: this.sessionId,\n });\n }\n\n // Create input stream and pipe source to it (single appendBuffer)\n const inputStream = this.demuxer.createInputStream();\n await stream.pipeTo(inputStream).catch((error) => {\n console.error('[VideoDemuxWorker] Input stream error:', this.sessionId, error);\n });\n }\n\n private handleDemuxerReady(): void {\n if (!this.demuxer || !this.videoDownstreamPort) {\n return;\n }\n\n const videoTrackInfo = this.demuxer.videoTrackInfo;\n if (!videoTrackInfo) {\n console.error('[VideoDemuxWorker] No video track found after ready');\n return;\n }\n\n const downstreamChannel = new WorkerChannel(this.videoDownstreamPort, {\n name: 'VideoDemux-Decoder',\n timeout: 30000,\n });\n\n downstreamChannel.send('configure' as any, {\n sessionId: this.sessionId,\n streamType: 'video',\n codec: videoTrackInfo.codec,\n width: videoTrackInfo.width,\n height: videoTrackInfo.height,\n description: videoTrackInfo.description,\n });\n\n // Send audio track info if available\n const audioTrackInfo = this.demuxer.audioTrackInfo;\n if (audioTrackInfo && this.audioDownstreamPort) {\n const audioChannel = new WorkerChannel(this.audioDownstreamPort, {\n name: 'VideoDemux-AudioDecoder',\n timeout: 30000,\n });\n\n audioChannel.send('configure' as any, {\n sessionId: this.sessionId,\n streamType: 'audio',\n codec: audioTrackInfo.codec,\n sampleRate: audioTrackInfo.sampleRate,\n numberOfChannels: audioTrackInfo.numberOfChannels,\n description: audioTrackInfo.description,\n });\n }\n }\n\n /**\n * Get demuxer statistics\n */\n private async handleGetStats(): Promise<{\n queueSize?: number;\n tracksInfo?: any[];\n state?: WorkerState;\n }> {\n if (!this.demuxer) {\n return { state: this.channel.state };\n }\n\n return {\n tracksInfo: Array.from(this.demuxer.tracks.values()),\n state: this.channel.state,\n };\n }\n\n /**\n * Dispose worker and cleanup resources\n */\n private async handleDispose(): Promise<{ success: boolean }> {\n // Destroy demuxer\n this.demuxer?.destroy();\n this.demuxer = null;\n this.sessionId = null;\n\n // Close connections\n this.videoDownstreamPort?.close();\n this.videoDownstreamPort = null;\n this.audioDownstreamPort?.close();\n this.audioDownstreamPort = null;\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n}\n// Initialize worker\nconst worker = new VideoDemuxWorker();\n\n// Handle worker termination\nself.addEventListener('beforeunload', () => {\n worker['handleDispose']();\n});\n\nexport default null; // Required for TypeScript worker compilation\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"video-demux.worker.js","sources":["../../../../src/stages/demux/video-demux.worker.ts"],"sourcesContent":["import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { MP4Demuxer } from './MP4Demuxer';\nimport type { DemuxConfig } from './types';\n\ninterface LoaderStreamMetadata {\n sessionId?: string;\n byteStart?: number;\n byteEnd?: number;\n}\n/**\n * VideoDemuxWorker - First stage for video processing\n * Extracts video tracks from container formats (MP4, etc.)\n *\n * Pipeline: ResourceLoader (Main Thread) → VideoDemuxWorker → DecodeWorker\n *\n * Architecture Note:\n * - One VideoDemuxWorker instance per CLIP (not per resource)\n * - Multiple clips can share the same resource (different workers, independent processing)\n * - This enables clean 2-Clip strategy lifecycle management\n *\n * Features:\n * - MP4 container demuxing with mp4box.js\n * - Stream-based processing with backpressure\n * - Direct streaming to DecodeWorker\n */\nexport class VideoDemuxWorker {\n private channel: WorkerChannel;\n private demuxer: MP4Demuxer | null = null;\n private sessionId: string | null = null;\n private videoDownstreamPort: MessagePort | null = null;\n private audioDownstreamPort: MessagePort | null = null;\n private emitAudioToMain: boolean = false;\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as any, {\n name: 'VideoDemuxWorker',\n timeout: 30000,\n });\n this.setupHandlers();\n }\n\n /* @better-ai.mdc For test visibility */\n protected setupHandlers(): void {\n // Register message handlers\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n this.channel.registerHandler('connect', this.handleConnect.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n\n // Setup stream receiver from ResourceLoader (main thread)\n this.channel.receiveStream(this.handleReceiveStream.bind(this));\n }\n\n // ===== Helper methods (reduce duplication) =====\n private buildAudioDecoderConfig(): any {\n const info = this.demuxer?.audioTrackInfo;\n if (!info) return undefined;\n return {\n codec: info.codec,\n sampleRate: info.sampleRate,\n numberOfChannels: info.numberOfChannels,\n description: info.description,\n };\n }\n\n private sendVideoConfigure(): void {\n if (!this.videoDownstreamPort || !this.demuxer) return;\n const videoTrackInfo = this.demuxer.videoTrackInfo;\n if (!videoTrackInfo) {\n console.error('[VideoDemuxWorker] No video track found after ready');\n return;\n }\n const downstreamChannel = new WorkerChannel(this.videoDownstreamPort, {\n name: 'VideoDemux-Decoder',\n timeout: 30000,\n });\n downstreamChannel.send('configure' as any, {\n sessionId: this.sessionId,\n streamType: 'video',\n codec: videoTrackInfo.codec,\n width: videoTrackInfo.width,\n height: videoTrackInfo.height,\n description: videoTrackInfo.description,\n });\n }\n\n private sendAudioConfigure(): void {\n if (!this.audioDownstreamPort) return;\n const cfg = this.buildAudioDecoderConfig();\n if (!cfg) return;\n const audioChannel = new WorkerChannel(this.audioDownstreamPort, {\n name: 'VideoDemux-AudioDecoder',\n timeout: 30000,\n });\n audioChannel.send('configure' as any, {\n sessionId: this.sessionId,\n streamType: 'audio',\n ...cfg,\n });\n }\n\n private sendVideoStream(videoStream: ReadableStream<EncodedVideoChunk>): void {\n if (!this.videoDownstreamPort) return;\n const videoChannel = new WorkerChannel(this.videoDownstreamPort, {\n name: 'VideoDemux-Decoder',\n timeout: 30000,\n });\n videoChannel.sendStream(videoStream, {\n streamType: 'video',\n sessionId: this.sessionId,\n });\n }\n\n private sendAudioStream(audioStream: ReadableStream<EncodedAudioChunk> | null): void {\n if (!audioStream) return;\n\n // Prefer decoder when connected; otherwise emit to main when enabled\n if (this.audioDownstreamPort) {\n const audioChannel = new WorkerChannel(this.audioDownstreamPort, {\n name: 'VideoDemux-AudioDecoder',\n timeout: 30000,\n });\n audioChannel.sendStream(audioStream, {\n streamType: 'audio',\n sessionId: this.sessionId,\n });\n return;\n }\n\n if (this.emitAudioToMain) {\n // Wrap to { chunk, metadata: { decoderConfig } }, compute decoderConfig per chunk\n const mapped = audioStream.pipeThrough(\n new TransformStream<EncodedAudioChunk, { chunk: EncodedAudioChunk; metadata: any }>({\n transform: (chunk, ctl) => {\n const decoderConfig = this.buildAudioDecoderConfig();\n ctl.enqueue({ chunk, metadata: { decoderConfig } });\n },\n })\n );\n this.channel.sendStream(mapped, {\n streamType: 'audio',\n sessionId: this.sessionId,\n });\n }\n }\n\n /**\n * Handle connection from orchestrator\n */\n private async handleConnect(payload: {\n port?: MessagePort;\n streamType?: string;\n sessionId?: string;\n direction?: string;\n }): Promise<{ success: boolean }> {\n const { port, sessionId, direction, streamType } = payload;\n\n if (!port) {\n return { success: false };\n }\n\n if (direction === 'downstream') {\n if (streamType === 'audio') {\n this.audioDownstreamPort = port;\n } else {\n this.videoDownstreamPort = port;\n }\n }\n\n this.sessionId = sessionId || null;\n return { success: true };\n }\n\n /**\n * Configure demuxer with format settings\n * @param payload.config - Demuxer configuration\n * @param payload.initial - If true, initialize worker state; otherwise just update config\n */\n private async handleConfigure(payload: {\n config: DemuxConfig;\n initial?: boolean;\n }): Promise<{ success: boolean; tracks?: any[] }> {\n const { config, initial = false } = payload;\n\n try {\n if (initial) {\n // Initial setup - set worker state to ready\n this.channel.state = WorkerState.Ready;\n\n // Create new demuxer instance\n if (this.demuxer) {\n this.demuxer.destroy();\n }\n\n // Persist audio emission preference\n this.emitAudioToMain = !!config.emitAudioToMain;\n\n this.demuxer = new MP4Demuxer({\n ...config,\n onReady: () => this.handleDemuxerReady(),\n });\n\n // Notify configuration complete\n this.channel.notify('configured');\n\n return { success: true };\n } else {\n // Update configuration only (e.g., backpressure settings)\n this.demuxer?.updateConfig(config);\n return { success: true };\n }\n } catch (error: any) {\n throw {\n code: error.code || 'CONFIG_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Handle input stream from ResourceLoader (main thread)\n * Strategy: Stream immediately, send codec info when ready\n */\n private async handleReceiveStream(\n stream: ReadableStream<Uint8Array | ArrayBuffer>,\n metadata?: LoaderStreamMetadata\n ): Promise<void> {\n // Store sessionId from metadata (only happens once per worker lifecycle)\n // Note: Do NOT override if already set by handleConnect (which has correct sessionId)\n if (!this.sessionId) {\n this.sessionId = metadata?.sessionId || this.sessionId;\n }\n\n // Initialize demuxer on first stream\n if (!this.demuxer) {\n this.demuxer = new MP4Demuxer({\n highWaterMark: 10,\n onReady: () => this.handleDemuxerReady(),\n });\n }\n\n // If no video downstream is connected, we can still proceed when\n // we only need to emit audio to the main thread for L2 caching.\n if (!this.videoDownstreamPort && !this.emitAudioToMain) {\n console.error('[VideoDemuxWorker] Decoder not connected', this.sessionId);\n return;\n }\n\n // Create output streams (readable only)\n const videoStream = this.demuxer.createVideoStream();\n const audioStream = this.demuxer.createAudioStream();\n\n // Send streams\n this.sendVideoStream(videoStream);\n this.sendAudioStream(audioStream);\n\n // Create input stream and pipe source to it (single appendBuffer)\n const inputStream = this.demuxer.createInputStream();\n await stream.pipeTo(inputStream).catch((error) => {\n console.error('[VideoDemuxWorker] Input stream error:', this.sessionId, error);\n });\n }\n\n private handleDemuxerReady(): void {\n if (!this.demuxer) return;\n this.sendVideoConfigure();\n this.sendAudioConfigure();\n }\n\n /**\n * Get demuxer statistics\n */\n private async handleGetStats(): Promise<{\n queueSize?: number;\n tracksInfo?: any[];\n state?: WorkerState;\n }> {\n if (!this.demuxer) {\n return { state: this.channel.state };\n }\n\n return {\n tracksInfo: Array.from(this.demuxer.tracks.values()),\n state: this.channel.state,\n };\n }\n\n /**\n * Dispose worker and cleanup resources\n */\n private async handleDispose(): Promise<{ success: boolean }> {\n // Destroy demuxer\n this.demuxer?.destroy();\n this.demuxer = null;\n this.sessionId = null;\n\n // Close connections\n this.videoDownstreamPort?.close();\n this.videoDownstreamPort = null;\n this.audioDownstreamPort?.close();\n this.audioDownstreamPort = null;\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n}\n// Initialize worker\nconst worker = new VideoDemuxWorker();\n\n// Handle worker termination\nself.addEventListener('beforeunload', () => {\n worker['handleDispose']();\n});\n\nexport default null; // Required for TypeScript worker compilation\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalAudioSession.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/GlobalAudioSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,mBAAmB,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAQ,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAI7D,UAAU,gBAAgB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,gBAAgB;IACxB,YAAY,EAAE,YAAY,CAAC;IAC3B,OAAO,EAAE,UAAU,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACpC,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;IACxC,kBAAkB,EAAE,MAAM,GAAG,CAAC;CAC/B;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,IAAI,CAAmB;IAC/B,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,qBAAqB,CAAa;gBAE9B,IAAI,EAAE,gBAAgB;IAKlC,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAKtC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BtC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBnD,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI;IA2BhE,OAAO,CAAC,oBAAoB;IAoC5B,iBAAiB,CAAC,MAAM,EAAE,cAAc,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAgCnF,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9E,YAAY,IAAI,IAAI;IAKpB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQhC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAqB/B,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOnC,KAAK,IAAI,IAAI;IAOb;;OAEG;IACG,yBAAyB,CAC7B,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,EACpC,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,yBAAyB,KAAK,IAAI,GAC9D,OAAO,CAAC,cAAc,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC;IA2BpD;;OAEG;IACG,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;YA+B5D,sBAAsB;IAYpC,OAAO,CAAC,cAAc;IAwDtB,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,iBAAiB;IAiFzB,OAAO,CAAC,oBAAoB;IAiF5B,OAAO,CAAC,mBAAmB;IAsB3B,OAAO,CAAC,mBAAmB;IAsB3B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,sBAAsB;IAoB9B,OAAO,CAAC,oBAAoB;YAgBd,kBAAkB;CA6CjC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalAudioSession.js","sources":["../../../src/stages/compose/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs, AudioClip } from '../../model/types';\nimport { OfflineAudioMixer } from './OfflineAudioMixer';\nimport type { CompositionModel, Clip } from '../../model';\nimport type { WorkerPool } from '../../worker/WorkerPool';\nimport type { ResourceLoader } from '../load/ResourceLoader';\nimport type { EventBus } from '../../event/EventBus';\nimport type { EventPayloadMap } from '../../event/events';\nimport { MeframeEvent } from '../../event/events';\nimport type { CacheManager } from '../../cache/CacheManager';\nimport { AudioChunkEncoder } from '../encode/AudioChunkEncoder';\nimport { isAudioClip, hasAudioConfig } from '../../model/types';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\ninterface AudioSessionDeps {\n cacheManager: CacheManager;\n workers: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n getModel: () => CompositionModel | null;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private streamEndedClips = new Set<string>();\n private deps: AudioSessionDeps;\n private audioContext: AudioContext | null = null;\n private audioSources: AudioBufferSourceNode[] = [];\n private audioGainNodes: GainNode[] = [];\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n private currentPlaybackTimeUs: TimeUs = 0;\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, deps.getModel);\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipDurationUs } = message;\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs);\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n\n for (const track of audioTracks) {\n for (const clip of track.clips) {\n if (!this.activeClips.has(clip.id)) {\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n await this.setupAudioPipeline(clip);\n this.activeClips.add(clip.id);\n\n await this.deps.resourceLoader.fetch(clip.resourceId, {\n priority: 'high',\n sessionId: clip.id,\n trackId: track.id,\n });\n\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n }\n\n async deactivateClip(clipId: string): Promise<void> {\n if (!this.activeClips.has(clipId)) {\n return;\n }\n\n // Stop any playing audio sources for this clip\n this.stopClipAudioSources(clipId);\n\n this.deps.workers.terminate('audioDemux', clipId);\n this.deps.workers.terminate('audioDecode', clipId);\n\n this.activeClips.delete(clipId);\n\n this.deps.cacheManager.clearClipAudioData(clipId);\n }\n\n restartPlayingClip(clipId: string, currentTimeUs?: TimeUs): void {\n if (!this.isPlaying || !this.audioContext) {\n return;\n }\n\n const timeUs = currentTimeUs ?? this.currentPlaybackTimeUs;\n\n const model = this.deps.getModel();\n if (!model) {\n return;\n }\n\n const clip = model.findClip(clipId);\n if (!clip) {\n return;\n }\n\n // Check if clip should be playing at current time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (timeUs < clip.startUs || timeUs >= clipEndUs) {\n return;\n }\n\n // Start playback from current time\n this.startClipPlayback(clip, timeUs);\n }\n\n private stopClipAudioSources(clipId: string): void {\n const sourcesToStop: AudioBufferSourceNode[] = [];\n const gainNodesToDisconnect: GainNode[] = [];\n\n for (let i = this.audioSources.length - 1; i >= 0; i--) {\n const source = this.audioSources[i];\n if (source && (source as any)._meframeClipId === clipId) {\n sourcesToStop.push(source);\n this.audioSources.splice(i, 1);\n\n const gainNode = this.audioGainNodes[i];\n if (gainNode) {\n gainNodesToDisconnect.push(gainNode);\n this.audioGainNodes.splice(i, 1);\n }\n }\n }\n\n for (const source of sourcesToStop) {\n try {\n source.stop();\n source.disconnect();\n } catch (error) {\n // Ignore - source may have already stopped\n }\n }\n\n for (const gainNode of gainNodesToDisconnect) {\n try {\n gainNode.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n }\n\n handleAudioStream(stream: ReadableStream<AudioData>, metadata: Record<string, any>): void {\n const sessionId = metadata.sessionId || 'unknown';\n const clipStartUs = metadata.clipStartUs ?? 0;\n const clipDurationUs = metadata.clipDurationUs ?? 0;\n\n const reader = stream.getReader();\n const pump = async (): Promise<void> => {\n try {\n const { done, value } = await reader.read();\n if (done) {\n this.streamEndedClips.add(sessionId);\n reader.releaseLock();\n return;\n }\n\n this.onAudioData({\n sessionId,\n audioData: value,\n clipStartUs,\n clipDurationUs,\n });\n\n await pump();\n } catch (error) {\n console.error('[GlobalAudioSession] Audio stream error:', error);\n reader.releaseLock();\n }\n };\n\n pump();\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n\n // Resume AudioContext if suspended (required by modern browsers)\n if (audioContext.state === 'suspended') {\n await audioContext.resume();\n }\n\n this.isPlaying = true;\n this.startAllActiveClips(timeUs);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllAudioSources();\n }\n\n updateTime(timeUs: TimeUs): void {\n this.currentPlaybackTimeUs = timeUs;\n if (!this.isPlaying) {\n return;\n }\n this.checkAndStartNewClips(timeUs);\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n\n // Update existing gain nodes with clip-level config\n for (let i = 0; i < this.audioGainNodes.length; i++) {\n const gainNode = this.audioGainNodes[i];\n const source = this.audioSources[i];\n const clipId = (source as any)._meframeClipId;\n\n if (clipId && gainNode) {\n const model = this.deps.getModel();\n const clip = model?.findClip(clipId);\n if (clip && hasAudioConfig(clip)) {\n const clipVolume = clip.audioConfig?.volume ?? 1.0;\n const muted = clip.audioConfig?.muted ?? false;\n gainNode.gain.value = muted ? 0 : clipVolume * this.volume;\n }\n }\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n for (const source of this.audioSources) {\n source.playbackRate.value = this.playbackRate;\n }\n }\n\n reset(): void {\n this.stopAllAudioSources();\n this.deps.cacheManager.resetAudioCache();\n this.activeClips.clear();\n this.streamEndedClips.clear();\n }\n\n /**\n * Create export encoded audio stream\n */\n async createExportEncodedStream(\n config?: Partial<AudioEncoderConfig>,\n onFirstMetadata?: (metadata: EncodedAudioChunkMetadata) => void\n ): Promise<ReadableStream<EncodedAudioChunk> | null> {\n const audioDataStream = await this.createExportAudioStream();\n if (!audioDataStream) {\n return null;\n }\n\n const encoder = new AudioChunkEncoder(config);\n await encoder.initialize();\n\n const encodingTransform = encoder.createStream();\n const encodedStream = audioDataStream.pipeThrough(encodingTransform);\n\n let firstMetadataExtracted = false;\n\n return encodedStream.pipeThrough(\n new TransformStream({\n transform(encoderChunk, controller) {\n if (!firstMetadataExtracted && onFirstMetadata) {\n onFirstMetadata(encoderChunk.metadata as EncodedAudioChunkMetadata);\n firstMetadataExtracted = true;\n }\n controller.enqueue(encoderChunk.chunk as EncodedAudioChunk);\n },\n })\n );\n }\n\n /**\n * Create export audio stream\n */\n async createExportAudioStream(): Promise<ReadableStream<AudioData> | null> {\n const model = this.deps.getModel();\n if (!model) {\n return null;\n }\n\n const totalDurationUs = model.durationUs;\n\n await this.activateAllAudioClips();\n await this.waitForAudioClipsReady();\n\n return new ReadableStream<AudioData>({\n start: async (controller) => {\n const windowSize = 5_000_000;\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n const windowEndUs = Math.min(currentUs + windowSize, totalDurationUs);\n const mixedBuffer = await this.mixer.mix(currentUs, windowEndUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, currentUs);\n if (audioData) {\n controller.enqueue(audioData);\n }\n currentUs = windowEndUs;\n }\n\n controller.close();\n },\n });\n }\n\n private async waitForAudioClipsReady(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) return;\n\n const audioClips = model.tracks\n .filter((track) => track.kind === 'audio')\n .flatMap((track) => track.clips);\n\n const waitPromises = audioClips.map((clip) => this.waitForClipPCM(clip.id, 10000)); // 10s timeout\n await Promise.allSettled(waitPromises);\n }\n\n private waitForClipPCM(clipId: string, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const checkInterval = 100;\n let elapsed = 0;\n let lastFrameCount = 0;\n let stableCount = 0;\n let streamEndDetected = false;\n\n const check = () => {\n const pcm = this.deps.cacheManager.getClipPCM(clipId, 0, Number.MAX_SAFE_INTEGER);\n\n if (pcm && pcm.length > 0) {\n const currentFrameCount = pcm[0]?.length ?? 0;\n\n // Check if we have received stream end signal\n if (this.streamEndedClips.has(clipId)) {\n streamEndDetected = true;\n }\n\n // If stream has ended, we're done\n if (streamEndDetected) {\n resolve(true);\n return;\n }\n\n // Otherwise, check if frame count is stable (no new data for 500ms)\n if (currentFrameCount === lastFrameCount) {\n stableCount++;\n if (stableCount >= 5) {\n // 5 * 100ms = 500ms\n resolve(true);\n return;\n }\n } else {\n stableCount = 0;\n lastFrameCount = currentFrameCount;\n }\n }\n\n elapsed += checkInterval;\n if (elapsed >= timeoutMs) {\n console.warn('[GlobalAudioSession] Timeout waiting for clip', clipId, {\n frames: lastFrameCount,\n elapsed,\n });\n resolve(false);\n return;\n }\n\n setTimeout(check, checkInterval);\n };\n\n check();\n });\n }\n\n private startAllActiveClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n\n for (const clip of currentClips) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n\n private checkAndStartNewClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n const activeClipIds = new Set(\n this.audioSources.map((source) => (source as any)._meframeClipId).filter(Boolean)\n );\n\n for (const clip of currentClips) {\n // Check if clip should be playing at current time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (timeUs >= clipEndUs) {\n // Clip has already ended, skip\n continue;\n }\n\n // Check if current time is before clip starts\n if (timeUs < clip.startUs) {\n // Not yet time to play this clip\n continue;\n }\n\n // Check if clip is too close to ending (avoid glitches from very short playback)\n const MIN_REMAINING_TIME_US = 30000; // 30ms in microseconds\n if (clipEndUs - timeUs < MIN_REMAINING_TIME_US) {\n // Too close to the end, skip to avoid audio glitches\n continue;\n }\n\n if (!activeClipIds.has(clip.id)) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n }\n\n private startClipPlayback(clip: Clip, currentTimeUs: TimeUs): void {\n if (!this.audioContext) {\n console.warn('[GlobalAudioSession] No audioContext, cannot start playback');\n return;\n }\n\n // Use clip-relative time (0-based) like video cache\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n 0, // Start from beginning of clip (0-based)\n clip.durationUs // Full clip duration\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n // No data yet, will retry later via checkAndStartNewClips\n console.warn('[GlobalAudioSession] No PCM data for clip, will retry later', clip.id);\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n\n const offsetUs = Math.max(0, currentTimeUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n\n // Use actual buffer duration instead of clip.durationUs\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n // Early check: if remaining duration is too short, skip\n // (Note: checkAndStartNewClips should already filter these out)\n const MIN_PLAYBACK_DURATION_SECONDS = 0.03; // 30ms\n if (actualDurationSeconds < MIN_PLAYBACK_DURATION_SECONDS) {\n return;\n }\n\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n (source as any)._playedToUs = clip.startUs + buffer.duration * 1_000_000; // Track where it played to\n\n const gainNode = this.audioContext.createGain();\n\n // Apply audio config\n if (hasAudioConfig(clip)) {\n const volume = clip.audioConfig?.volume ?? 1.0;\n const muted = clip.audioConfig?.muted ?? false;\n gainNode.gain.value = muted ? 0 : volume * this.volume;\n } else {\n gainNode.gain.value = this.volume;\n }\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n\n // Check if more data has arrived and continue playing\n const playedToUs = (source as any)._playedToUs;\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (playedToUs < clipEndUs && this.isPlaying) {\n // There might be more data, try to continue\n setTimeout(() => {\n if (this.isPlaying) {\n this.continueClipPlayback(clip, playedToUs);\n }\n }, 50);\n }\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private continueClipPlayback(clip: Clip, fromUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n // Use clip-relative time (0-based) like video cache\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n 0, // 0-based start\n clip.durationUs // clip duration\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n const bufferEndUs = clip.startUs + buffer.duration * 1_000_000;\n\n // Check if there's new data beyond where we played to\n if (bufferEndUs <= fromUs + 100_000) {\n // 100ms tolerance\n // No significant new data\n return;\n }\n\n // Continue playback from where it left off\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n (source as any)._playedToUs = bufferEndUs;\n\n const gainNode = this.audioContext.createGain();\n\n // Apply audio config\n if (hasAudioConfig(clip)) {\n const volume = clip.audioConfig?.volume ?? 1.0;\n const muted = clip.audioConfig?.muted ?? false;\n gainNode.gain.value = muted ? 0 : volume * this.volume;\n } else {\n gainNode.gain.value = this.volume;\n }\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n const offsetUs = Math.max(0, fromUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n if (actualDurationSeconds <= 0) {\n return;\n }\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n\n // Check if more data has arrived\n const playedToUs = (source as any)._playedToUs;\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (playedToUs < clipEndUs && this.isPlaying) {\n setTimeout(() => {\n if (this.isPlaying) {\n this.continueClipPlayback(clip, playedToUs);\n }\n }, 50);\n }\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private stopAllAudioSources(): void {\n for (const source of this.audioSources) {\n try {\n source.stop();\n source.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n\n for (const gainNode of this.audioGainNodes) {\n try {\n gainNode.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n\n this.audioSources = [];\n this.audioGainNodes = [];\n }\n\n private getActiveAudioClips(timeUs: TimeUs): Clip[] {\n const model = this.deps.getModel();\n if (!model) {\n return [];\n }\n\n const clips: Clip[] = [];\n\n // Get clips from all tracks (video clips can have audio!)\n for (const track of model.tracks) {\n const trackClips = model.getClipsAtTime(timeUs, track.id);\n for (const clip of trackClips) {\n // Check if this clip has audio data in cache\n if (this.deps.cacheManager.hasClipPCM(clip.id)) {\n clips.push(clip);\n }\n }\n }\n\n return clips;\n }\n\n private pcmToAudioBuffer(planes: Float32Array[], sampleRate: number): AudioBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n\n const ctx = new OfflineAudioContext(numberOfChannels, 1, sampleRate);\n const buffer = ctx.createBuffer(numberOfChannels, numberOfFrames, sampleRate);\n\n for (let channel = 0; channel < numberOfChannels; channel++) {\n const plane = planes[channel];\n if (plane) {\n const channelData = buffer.getChannelData(channel);\n channelData.set(plane);\n }\n }\n\n return buffer;\n }\n\n private audioBufferToAudioData(buffer: AudioBuffer, timestampUs: TimeUs): AudioData | null {\n const sampleRate = buffer.sampleRate;\n const numberOfChannels = buffer.numberOfChannels;\n const numberOfFrames = buffer.length;\n\n const planes: Float32Array[] = [];\n for (let channel = 0; channel < numberOfChannels; channel++) {\n planes.push(buffer.getChannelData(channel));\n }\n\n return new AudioData({\n format: 'f32', // interleaved format\n sampleRate,\n numberOfFrames,\n numberOfChannels,\n timestamp: timestampUs,\n data: this.interleavePlanarData(planes),\n });\n }\n\n private interleavePlanarData(planes: Float32Array[]): ArrayBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n const totalSamples = numberOfChannels * numberOfFrames;\n\n const interleaved = new Float32Array(totalSamples);\n\n for (let frame = 0; frame < numberOfFrames; frame++) {\n for (let channel = 0; channel < numberOfChannels; channel++) {\n interleaved[frame * numberOfChannels + channel] = planes[channel]![frame]!;\n }\n }\n\n return interleaved.buffer;\n }\n\n private async setupAudioPipeline(clip: AudioClip): Promise<void> {\n const { id: clipId, resourceId, startUs, durationUs } = clip;\n const audioDemuxWorker = await this.deps.workers.getOrCreate('audioDemux', clipId, {\n lazy: true,\n });\n const audioDecodeWorker = await this.deps.workers.getOrCreate('audioDecode', clipId, {\n lazy: true,\n });\n\n const demuxToDecodeChannel = new MessageChannel();\n await audioDemuxWorker.send(\n 'connect',\n { direction: 'downstream', port: demuxToDecodeChannel.port1, streamType: 'audio', clipId },\n { transfer: [demuxToDecodeChannel.port1] }\n );\n await audioDecodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: demuxToDecodeChannel.port2,\n streamType: 'audio',\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n },\n { transfer: [demuxToDecodeChannel.port2] }\n );\n\n audioDecodeWorker.receiveStream((stream, metadata) => {\n this.handleAudioStream(stream as ReadableStream<AudioData>, {\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n ...metadata,\n });\n });\n\n const demuxConfig = this.deps.buildWorkerConfigs().audioDemux;\n await audioDemuxWorker.send('configure', {\n initial: true,\n resourceId,\n clipId,\n config: demuxConfig,\n });\n }\n}\n"],"names":[],"mappings":";;;;AA4BO,MAAM,mBAAmB;AAAA,EACtB;AAAA,EACA,kCAAkB,IAAA;AAAA,EAClB,uCAAuB,IAAA;AAAA,EACvB;AAAA,EACA,eAAoC;AAAA,EACpC,eAAwC,CAAA;AAAA,EACxC,iBAA6B,CAAA;AAAA,EAC7B,SAAS;AAAA,EACT,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,wBAAgC;AAAA,EAExC,YAAY,MAAwB;AAClC,SAAK,OAAO;AACZ,SAAK,QAAQ,IAAI,kBAAkB,KAAK,cAAc,KAAK,QAAQ;AAAA,EACrE;AAAA,EAEA,YAAY,SAAiC;AAC3C,UAAM,EAAE,WAAW,WAAW,eAAA,IAAmB;AACjD,SAAK,KAAK,aAAa,iBAAiB,WAAW,WAAW,cAAc;AAAA,EAC9E;AAAA,EAEA,MAAM,wBAAuC;AAC3C,UAAM,QAAQ,KAAK,KAAK,SAAA;AACxB,QAAI,CAAC,OAAO;AACV;AAAA,IACF;AAEA,UAAM,cAAc,MAAM,OAAO,OAAO,CAAC,UAAU,MAAM,SAAS,OAAO;AAEzE,eAAW,SAAS,aAAa;AAC/B,iBAAW,QAAQ,MAAM,OAAO;AAC9B,YAAI,CAAC,KAAK,YAAY,IAAI,KAAK,EAAE,GAAG;AAClC,cAAI,CAAC,YAAY,IAAI,GAAG;AACtB,kBAAM,IAAI,MAAM,QAAQ,KAAK,EAAE,sCAAsC;AAAA,UACvE;AAEA,gBAAM,KAAK,mBAAmB,IAAI;AAClC,eAAK,YAAY,IAAI,KAAK,EAAE;AAE5B,gBAAM,KAAK,KAAK,eAAe,MAAM,KAAK,YAAY;AAAA,YACpD,UAAU;AAAA,YACV,WAAW,KAAK;AAAA,YAChB,SAAS,MAAM;AAAA,UAAA,CAChB;AAED,eAAK,KAAK,SAAS,KAAK,aAAa,eAAe,EAAE,QAAQ,KAAK,IAAI;AAAA,QACzE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,QAA+B;AAClD,QAAI,CAAC,KAAK,YAAY,IAAI,MAAM,GAAG;AACjC;AAAA,IACF;AAGA,SAAK,qBAAqB,MAAM;AAEhC,SAAK,KAAK,QAAQ,UAAU,cAAc,MAAM;AAChD,SAAK,KAAK,QAAQ,UAAU,eAAe,MAAM;AAEjD,SAAK,YAAY,OAAO,MAAM;AAE9B,SAAK,KAAK,aAAa,mBAAmB,MAAM;AAAA,EAClD;AAAA,EAEA,mBAAmB,QAAgB,eAA8B;AAC/D,QAAI,CAAC,KAAK,aAAa,CAAC,KAAK,cAAc;AACzC;AAAA,IACF;AAEA,UAAM,SAAS,iBAAiB,KAAK;AAErC,UAAM,QAAQ,KAAK,KAAK,SAAA;AACxB,QAAI,CAAC,OAAO;AACV;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,SAAS,MAAM;AAClC,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AAGA,UAAM,YAAY,KAAK,UAAU,KAAK;AACtC,QAAI,SAAS,KAAK,WAAW,UAAU,WAAW;AAChD;AAAA,IACF;AAGA,SAAK,kBAAkB,MAAM,MAAM;AAAA,EACrC;AAAA,EAEQ,qBAAqB,QAAsB;AACjD,UAAM,gBAAyC,CAAA;AAC/C,UAAM,wBAAoC,CAAA;AAE1C,aAAS,IAAI,KAAK,aAAa,SAAS,GAAG,KAAK,GAAG,KAAK;AACtD,YAAM,SAAS,KAAK,aAAa,CAAC;AAClC,UAAI,UAAW,OAAe,mBAAmB,QAAQ;AACvD,sBAAc,KAAK,MAAM;AACzB,aAAK,aAAa,OAAO,GAAG,CAAC;AAE7B,cAAM,WAAW,KAAK,eAAe,CAAC;AACtC,YAAI,UAAU;AACZ,gCAAsB,KAAK,QAAQ;AACnC,eAAK,eAAe,OAAO,GAAG,CAAC;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAEA,eAAW,UAAU,eAAe;AAClC,UAAI;AACF,eAAO,KAAA;AACP,eAAO,WAAA;AAAA,MACT,SAAS,OAAO;AAAA,MAEhB;AAAA,IACF;AAEA,eAAW,YAAY,uBAAuB;AAC5C,UAAI;AACF,iBAAS,WAAA;AAAA,MACX,SAAS,OAAO;AAAA,MAEhB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,kBAAkB,QAAmC,UAAqC;AACxF,UAAM,YAAY,SAAS,aAAa;AACxC,UAAM,cAAc,SAAS,eAAe;AAC5C,UAAM,iBAAiB,SAAS,kBAAkB;AAElD,UAAM,SAAS,OAAO,UAAA;AACtB,UAAM,OAAO,YAA2B;AACtC,UAAI;AACF,cAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,YAAI,MAAM;AACR,eAAK,iBAAiB,IAAI,SAAS;AACnC,iBAAO,YAAA;AACP;AAAA,QACF;AAEA,aAAK,YAAY;AAAA,UACf;AAAA,UACA,WAAW;AAAA,UACX;AAAA,UACA;AAAA,QAAA,CACD;AAED,cAAM,KAAA;AAAA,MACR,SAAS,OAAO;AACd,gBAAQ,MAAM,4CAA4C,KAAK;AAC/D,eAAO,YAAA;AAAA,MACT;AAAA,IACF;AAEA,SAAA;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,QAAgB,cAA2C;AAC7E,SAAK,eAAe;AAGpB,QAAI,aAAa,UAAU,aAAa;AACtC,YAAM,aAAa,OAAA;AAAA,IACrB;AAEA,SAAK,YAAY;AACjB,SAAK,oBAAoB,MAAM;AAAA,EACjC;AAAA,EAEA,eAAqB;AACnB,SAAK,YAAY;AACjB,SAAK,oBAAA;AAAA,EACP;AAAA,EAEA,WAAW,QAAsB;AAC/B,SAAK,wBAAwB;AAC7B,QAAI,CAAC,KAAK,WAAW;AACnB;AAAA,IACF;AACA,SAAK,sBAAsB,MAAM;AAAA,EACnC;AAAA,EAEA,UAAU,QAAsB;AAC9B,SAAK,SAAS;AAGd,aAAS,IAAI,GAAG,IAAI,KAAK,eAAe,QAAQ,KAAK;AACnD,YAAM,WAAW,KAAK,eAAe,CAAC;AACtC,YAAM,SAAS,KAAK,aAAa,CAAC;AAClC,YAAM,SAAU,OAAe;AAE/B,UAAI,UAAU,UAAU;AACtB,cAAM,QAAQ,KAAK,KAAK,SAAA;AACxB,cAAM,OAAO,OAAO,SAAS,MAAM;AACnC,YAAI,QAAQ,eAAe,IAAI,GAAG;AAChC,gBAAM,aAAa,KAAK,aAAa,UAAU;AAC/C,gBAAM,QAAQ,KAAK,aAAa,SAAS;AACzC,mBAAS,KAAK,QAAQ,QAAQ,IAAI,aAAa,KAAK;AAAA,QACtD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,gBAAgB,MAAoB;AAClC,SAAK,eAAe;AACpB,eAAW,UAAU,KAAK,cAAc;AACtC,aAAO,aAAa,QAAQ,KAAK;AAAA,IACnC;AAAA,EACF;AAAA,EAEA,QAAc;AACZ,SAAK,oBAAA;AACL,SAAK,KAAK,aAAa,gBAAA;AACvB,SAAK,YAAY,MAAA;AACjB,SAAK,iBAAiB,MAAA;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,0BACJ,QACA,iBACmD;AACnD,UAAM,kBAAkB,MAAM,KAAK,wBAAA;AACnC,QAAI,CAAC,iBAAiB;AACpB,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,IAAI,kBAAkB,MAAM;AAC5C,UAAM,QAAQ,WAAA;AAEd,UAAM,oBAAoB,QAAQ,aAAA;AAClC,UAAM,gBAAgB,gBAAgB,YAAY,iBAAiB;AAEnE,QAAI,yBAAyB;AAE7B,WAAO,cAAc;AAAA,MACnB,IAAI,gBAAgB;AAAA,QAClB,UAAU,cAAc,YAAY;AAClC,cAAI,CAAC,0BAA0B,iBAAiB;AAC9C,4BAAgB,aAAa,QAAqC;AAClE,qCAAyB;AAAA,UAC3B;AACA,qBAAW,QAAQ,aAAa,KAA0B;AAAA,QAC5D;AAAA,MAAA,CACD;AAAA,IAAA;AAAA,EAEL;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,0BAAqE;AACzE,UAAM,QAAQ,KAAK,KAAK,SAAA;AACxB,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,UAAM,kBAAkB,MAAM;AAE9B,UAAM,KAAK,sBAAA;AACX,UAAM,KAAK,uBAAA;AAEX,WAAO,IAAI,eAA0B;AAAA,MACnC,OAAO,OAAO,eAAe;AAC3B,cAAM,aAAa;AACnB,YAAI,YAAY;AAEhB,eAAO,YAAY,iBAAiB;AAClC,gBAAM,cAAc,KAAK,IAAI,YAAY,YAAY,eAAe;AACpE,gBAAM,cAAc,MAAM,KAAK,MAAM,IAAI,WAAW,WAAW;AAC/D,gBAAM,YAAY,KAAK,uBAAuB,aAAa,SAAS;AACpE,cAAI,WAAW;AACb,uBAAW,QAAQ,SAAS;AAAA,UAC9B;AACA,sBAAY;AAAA,QACd;AAEA,mBAAW,MAAA;AAAA,MACb;AAAA,IAAA,CACD;AAAA,EACH;AAAA,EAEA,MAAc,yBAAwC;AACpD,UAAM,QAAQ,KAAK,KAAK,SAAA;AACxB,QAAI,CAAC,MAAO;AAEZ,UAAM,aAAa,MAAM,OACtB,OAAO,CAAC,UAAU,MAAM,SAAS,OAAO,EACxC,QAAQ,CAAC,UAAU,MAAM,KAAK;AAEjC,UAAM,eAAe,WAAW,IAAI,CAAC,SAAS,KAAK,eAAe,KAAK,IAAI,GAAK,CAAC;AACjF,UAAM,QAAQ,WAAW,YAAY;AAAA,EACvC;AAAA,EAEQ,eAAe,QAAgB,WAAqC;AAC1E,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,YAAM,gBAAgB;AACtB,UAAI,UAAU;AACd,UAAI,iBAAiB;AACrB,UAAI,cAAc;AAClB,UAAI,oBAAoB;AAExB,YAAM,QAAQ,MAAM;AAClB,cAAM,MAAM,KAAK,KAAK,aAAa,WAAW,QAAQ,GAAG,OAAO,gBAAgB;AAEhF,YAAI,OAAO,IAAI,SAAS,GAAG;AACzB,gBAAM,oBAAoB,IAAI,CAAC,GAAG,UAAU;AAG5C,cAAI,KAAK,iBAAiB,IAAI,MAAM,GAAG;AACrC,gCAAoB;AAAA,UACtB;AAGA,cAAI,mBAAmB;AACrB,oBAAQ,IAAI;AACZ;AAAA,UACF;AAGA,cAAI,sBAAsB,gBAAgB;AACxC;AACA,gBAAI,eAAe,GAAG;AAEpB,sBAAQ,IAAI;AACZ;AAAA,YACF;AAAA,UACF,OAAO;AACL,0BAAc;AACd,6BAAiB;AAAA,UACnB;AAAA,QACF;AAEA,mBAAW;AACX,YAAI,WAAW,WAAW;AACxB,kBAAQ,KAAK,iDAAiD,QAAQ;AAAA,YACpE,QAAQ;AAAA,YACR;AAAA,UAAA,CACD;AACD,kBAAQ,KAAK;AACb;AAAA,QACF;AAEA,mBAAW,OAAO,aAAa;AAAA,MACjC;AAEA,YAAA;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,oBAAoB,QAAsB;AAChD,QAAI,CAAC,KAAK,cAAc;AACtB;AAAA,IACF;AAEA,UAAM,eAAe,KAAK,oBAAoB,MAAM;AAEpD,eAAW,QAAQ,cAAc;AAC/B,WAAK,kBAAkB,MAAM,MAAM;AAAA,IACrC;AAAA,EACF;AAAA,EAEQ,sBAAsB,QAAsB;AAClD,QAAI,CAAC,KAAK,cAAc;AACtB;AAAA,IACF;AAEA,UAAM,eAAe,KAAK,oBAAoB,MAAM;AACpD,UAAM,gBAAgB,IAAI;AAAA,MACxB,KAAK,aAAa,IAAI,CAAC,WAAY,OAAe,cAAc,EAAE,OAAO,OAAO;AAAA,IAAA;AAGlF,eAAW,QAAQ,cAAc;AAE/B,YAAM,YAAY,KAAK,UAAU,KAAK;AACtC,UAAI,UAAU,WAAW;AAEvB;AAAA,MACF;AAGA,UAAI,SAAS,KAAK,SAAS;AAEzB;AAAA,MACF;AAGA,YAAM,wBAAwB;AAC9B,UAAI,YAAY,SAAS,uBAAuB;AAE9C;AAAA,MACF;AAEA,UAAI,CAAC,cAAc,IAAI,KAAK,EAAE,GAAG;AAC/B,aAAK,kBAAkB,MAAM,MAAM;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,kBAAkB,MAAY,eAA6B;AACjE,QAAI,CAAC,KAAK,cAAc;AACtB,cAAQ,KAAK,6DAA6D;AAC1E;AAAA,IACF;AAGA,UAAM,cAAc,KAAK,KAAK,aAAa;AAAA,MACzC,KAAK;AAAA,MACL;AAAA;AAAA,MACA,KAAK;AAAA;AAAA,IAAA;AAGP,QAAI,CAAC,eAAe,YAAY,OAAO,WAAW,GAAG;AAEnD,cAAQ,KAAK,+DAA+D,KAAK,EAAE;AACnF;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,iBAAiB,YAAY,QAAQ,YAAY,UAAU;AAE/E,UAAM,WAAW,KAAK,IAAI,GAAG,gBAAgB,KAAK,OAAO;AACzD,UAAM,gBAAgB,WAAW;AAGjC,UAAM,wBAAwB,OAAO,WAAW;AAIhD,UAAM,gCAAgC;AACtC,QAAI,wBAAwB,+BAA+B;AACzD;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,aAAa,mBAAA;AACjC,WAAO,SAAS;AAChB,WAAO,aAAa,QAAQ,KAAK;AAChC,WAAe,iBAAiB,KAAK;AACrC,WAAe,cAAc,KAAK,UAAU,OAAO,WAAW;AAE/D,UAAM,WAAW,KAAK,aAAa,WAAA;AAGnC,QAAI,eAAe,IAAI,GAAG;AACxB,YAAM,SAAS,KAAK,aAAa,UAAU;AAC3C,YAAM,QAAQ,KAAK,aAAa,SAAS;AACzC,eAAS,KAAK,QAAQ,QAAQ,IAAI,SAAS,KAAK;AAAA,IAClD,OAAO;AACL,eAAS,KAAK,QAAQ,KAAK;AAAA,IAC7B;AAEA,WAAO,QAAQ,QAAQ;AACvB,aAAS,QAAQ,KAAK,aAAa,WAAW;AAE9C,WAAO,MAAM,GAAG,eAAe,qBAAqB;AAEpD,WAAO,UAAU,MAAM;AACrB,YAAM,QAAQ,KAAK,aAAa,QAAQ,MAAM;AAC9C,UAAI,SAAS,GAAG;AACd,aAAK,aAAa,OAAO,OAAO,CAAC;AACjC,aAAK,eAAe,OAAO,OAAO,CAAC;AAAA,MACrC;AAGA,YAAM,aAAc,OAAe;AACnC,YAAM,YAAY,KAAK,UAAU,KAAK;AAEtC,UAAI,aAAa,aAAa,KAAK,WAAW;AAE5C,mBAAW,MAAM;AACf,cAAI,KAAK,WAAW;AAClB,iBAAK,qBAAqB,MAAM,UAAU;AAAA,UAC5C;AAAA,QACF,GAAG,EAAE;AAAA,MACP;AAAA,IACF;AAEA,SAAK,aAAa,KAAK,MAAM;AAC7B,SAAK,eAAe,KAAK,QAAQ;AAAA,EACnC;AAAA,EAEQ,qBAAqB,MAAY,QAAsB;AAC7D,QAAI,CAAC,KAAK,cAAc;AACtB;AAAA,IACF;AAGA,UAAM,cAAc,KAAK,KAAK,aAAa;AAAA,MACzC,KAAK;AAAA,MACL;AAAA;AAAA,MACA,KAAK;AAAA;AAAA,IAAA;AAGP,QAAI,CAAC,eAAe,YAAY,OAAO,WAAW,GAAG;AACnD;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,iBAAiB,YAAY,QAAQ,YAAY,UAAU;AAC/E,UAAM,cAAc,KAAK,UAAU,OAAO,WAAW;AAGrD,QAAI,eAAe,SAAS,KAAS;AAGnC;AAAA,IACF;AAGA,UAAM,SAAS,KAAK,aAAa,mBAAA;AACjC,WAAO,SAAS;AAChB,WAAO,aAAa,QAAQ,KAAK;AAChC,WAAe,iBAAiB,KAAK;AACrC,WAAe,cAAc;AAE9B,UAAM,WAAW,KAAK,aAAa,WAAA;AAGnC,QAAI,eAAe,IAAI,GAAG;AACxB,YAAM,SAAS,KAAK,aAAa,UAAU;AAC3C,YAAM,QAAQ,KAAK,aAAa,SAAS;AACzC,eAAS,KAAK,QAAQ,QAAQ,IAAI,SAAS,KAAK;AAAA,IAClD,OAAO;AACL,eAAS,KAAK,QAAQ,KAAK;AAAA,IAC7B;AAEA,WAAO,QAAQ,QAAQ;AACvB,aAAS,QAAQ,KAAK,aAAa,WAAW;AAE9C,UAAM,WAAW,KAAK,IAAI,GAAG,SAAS,KAAK,OAAO;AAClD,UAAM,gBAAgB,WAAW;AACjC,UAAM,wBAAwB,OAAO,WAAW;AAEhD,QAAI,yBAAyB,GAAG;AAC9B;AAAA,IACF;AAEA,WAAO,MAAM,GAAG,eAAe,qBAAqB;AAEpD,WAAO,UAAU,MAAM;AACrB,YAAM,QAAQ,KAAK,aAAa,QAAQ,MAAM;AAC9C,UAAI,SAAS,GAAG;AACd,aAAK,aAAa,OAAO,OAAO,CAAC;AACjC,aAAK,eAAe,OAAO,OAAO,CAAC;AAAA,MACrC;AAGA,YAAM,aAAc,OAAe;AACnC,YAAM,YAAY,KAAK,UAAU,KAAK;AAEtC,UAAI,aAAa,aAAa,KAAK,WAAW;AAC5C,mBAAW,MAAM;AACf,cAAI,KAAK,WAAW;AAClB,iBAAK,qBAAqB,MAAM,UAAU;AAAA,UAC5C;AAAA,QACF,GAAG,EAAE;AAAA,MACP;AAAA,IACF;AAEA,SAAK,aAAa,KAAK,MAAM;AAC7B,SAAK,eAAe,KAAK,QAAQ;AAAA,EACnC;AAAA,EAEQ,sBAA4B;AAClC,eAAW,UAAU,KAAK,cAAc;AACtC,UAAI;AACF,eAAO,KAAA;AACP,eAAO,WAAA;AAAA,MACT,SAAS,OAAO;AAAA,MAEhB;AAAA,IACF;AAEA,eAAW,YAAY,KAAK,gBAAgB;AAC1C,UAAI;AACF,iBAAS,WAAA;AAAA,MACX,SAAS,OAAO;AAAA,MAEhB;AAAA,IACF;AAEA,SAAK,eAAe,CAAA;AACpB,SAAK,iBAAiB,CAAA;AAAA,EACxB;AAAA,EAEQ,oBAAoB,QAAwB;AAClD,UAAM,QAAQ,KAAK,KAAK,SAAA;AACxB,QAAI,CAAC,OAAO;AACV,aAAO,CAAA;AAAA,IACT;AAEA,UAAM,QAAgB,CAAA;AAGtB,eAAW,SAAS,MAAM,QAAQ;AAChC,YAAM,aAAa,MAAM,eAAe,QAAQ,MAAM,EAAE;AACxD,iBAAW,QAAQ,YAAY;AAE7B,YAAI,KAAK,KAAK,aAAa,WAAW,KAAK,EAAE,GAAG;AAC9C,gBAAM,KAAK,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,iBAAiB,QAAwB,YAAiC;AAChF,UAAM,mBAAmB,OAAO;AAChC,UAAM,iBAAiB,OAAO,CAAC,GAAG,UAAU;AAE5C,UAAM,MAAM,IAAI,oBAAoB,kBAAkB,GAAG,UAAU;AACnE,UAAM,SAAS,IAAI,aAAa,kBAAkB,gBAAgB,UAAU;AAE5E,aAAS,UAAU,GAAG,UAAU,kBAAkB,WAAW;AAC3D,YAAM,QAAQ,OAAO,OAAO;AAC5B,UAAI,OAAO;AACT,cAAM,cAAc,OAAO,eAAe,OAAO;AACjD,oBAAY,IAAI,KAAK;AAAA,MACvB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,uBAAuB,QAAqB,aAAuC;AACzF,UAAM,aAAa,OAAO;AAC1B,UAAM,mBAAmB,OAAO;AAChC,UAAM,iBAAiB,OAAO;AAE9B,UAAM,SAAyB,CAAA;AAC/B,aAAS,UAAU,GAAG,UAAU,kBAAkB,WAAW;AAC3D,aAAO,KAAK,OAAO,eAAe,OAAO,CAAC;AAAA,IAC5C;AAEA,WAAO,IAAI,UAAU;AAAA,MACnB,QAAQ;AAAA;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX,MAAM,KAAK,qBAAqB,MAAM;AAAA,IAAA,CACvC;AAAA,EACH;AAAA,EAEQ,qBAAqB,QAAqC;AAChE,UAAM,mBAAmB,OAAO;AAChC,UAAM,iBAAiB,OAAO,CAAC,GAAG,UAAU;AAC5C,UAAM,eAAe,mBAAmB;AAExC,UAAM,cAAc,IAAI,aAAa,YAAY;AAEjD,aAAS,QAAQ,GAAG,QAAQ,gBAAgB,SAAS;AACnD,eAAS,UAAU,GAAG,UAAU,kBAAkB,WAAW;AAC3D,oBAAY,QAAQ,mBAAmB,OAAO,IAAI,OAAO,OAAO,EAAG,KAAK;AAAA,MAC1E;AAAA,IACF;AAEA,WAAO,YAAY;AAAA,EACrB;AAAA,EAEA,MAAc,mBAAmB,MAAgC;AAC/D,UAAM,EAAE,IAAI,QAAQ,YAAY,SAAS,eAAe;AACxD,UAAM,mBAAmB,MAAM,KAAK,KAAK,QAAQ,YAAY,cAAc,QAAQ;AAAA,MACjF,MAAM;AAAA,IAAA,CACP;AACD,UAAM,oBAAoB,MAAM,KAAK,KAAK,QAAQ,YAAY,eAAe,QAAQ;AAAA,MACnF,MAAM;AAAA,IAAA,CACP;AAED,UAAM,uBAAuB,IAAI,eAAA;AACjC,UAAM,iBAAiB;AAAA,MACrB;AAAA,MACA,EAAE,WAAW,cAAc,MAAM,qBAAqB,OAAO,YAAY,SAAS,OAAA;AAAA,MAClF,EAAE,UAAU,CAAC,qBAAqB,KAAK,EAAA;AAAA,IAAE;AAE3C,UAAM,kBAAkB;AAAA,MACtB;AAAA,MACA;AAAA,QACE,WAAW;AAAA,QACX,MAAM,qBAAqB;AAAA,QAC3B,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,aAAa,WAAW;AAAA,QACxB,gBAAgB,cAAc;AAAA,MAAA;AAAA,MAEhC,EAAE,UAAU,CAAC,qBAAqB,KAAK,EAAA;AAAA,IAAE;AAG3C,sBAAkB,cAAc,CAAC,QAAQ,aAAa;AACpD,WAAK,kBAAkB,QAAqC;AAAA,QAC1D,WAAW;AAAA,QACX,aAAa,WAAW;AAAA,QACxB,gBAAgB,cAAc;AAAA,QAC9B,GAAG;AAAA,MAAA,CACJ;AAAA,IACH,CAAC;AAED,UAAM,cAAc,KAAK,KAAK,mBAAA,EAAqB;AACnD,UAAM,iBAAiB,KAAK,aAAa;AAAA,MACvC,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AAAA,EACH;AACF;"}
|