@meframe/core 0.0.6 → 0.0.8
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/cache/CacheManager.d.ts +5 -3
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +3 -10
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts +5 -0
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +31 -3
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +0 -4
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +11 -54
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +0 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +25 -37
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +1 -0
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +114 -92
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts +20 -25
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +252 -67
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +26 -8
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +1 -1
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +6 -2
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/types.d.ts +1 -1
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js.map +1 -1
- package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
- package/dist/workers/BaseDecoder.js +130 -0
- package/dist/workers/BaseDecoder.js.map +1 -0
- package/dist/workers/MP4Demuxer.js +4 -3
- package/dist/workers/MP4Demuxer.js.map +1 -1
- package/dist/workers/WorkerChannel.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js +13 -9
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/dist/workers/stages/decode/audio-decode.worker.js +243 -0
- package/dist/workers/stages/decode/audio-decode.worker.js.map +1 -0
- package/dist/workers/stages/decode/video-decode.worker.js +346 -0
- package/dist/workers/stages/decode/video-decode.worker.js.map +1 -0
- package/dist/workers/stages/demux/audio-demux.worker.js +28 -18
- package/dist/workers/stages/demux/audio-demux.worker.js.map +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js +5 -2
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -1
- package/dist/workers/stages/encode/video-encode.worker.js +325 -0
- package/dist/workers/stages/encode/video-encode.worker.js.map +1 -0
- package/package.json +1 -1
- package/dist/cache/l1/MixedAudioL1Cache.d.ts +0 -13
- package/dist/cache/l1/MixedAudioL1Cache.d.ts.map +0 -1
- package/dist/cache/l1/MixedAudioL1Cache.js +0 -52
- package/dist/cache/l1/MixedAudioL1Cache.js.map +0 -1
- package/dist/workers/stages/decode/decode.worker.js +0 -826
- package/dist/workers/stages/decode/decode.worker.js.map +0 -1
- package/dist/workers/stages/encode/encode.worker.js +0 -547
- package/dist/workers/stages/encode/encode.worker.js.map +0 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { W as WorkerChannel, a as WorkerMessageType, b as WorkerState } from "../../WorkerChannel.js";
|
|
2
|
+
import { B as BaseDecoder } from "../../BaseDecoder.js";
|
|
3
|
+
class AudioChunkDecoder extends BaseDecoder {
|
|
4
|
+
// Default values
|
|
5
|
+
static DEFAULT_HIGH_WATER_MARK = 20;
|
|
6
|
+
static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
|
|
7
|
+
// Exposed properties
|
|
8
|
+
trackId;
|
|
9
|
+
// Backpressure configuration
|
|
10
|
+
highWaterMark;
|
|
11
|
+
decodeQueueThreshold;
|
|
12
|
+
constructor(trackId, config) {
|
|
13
|
+
super(config);
|
|
14
|
+
this.trackId = trackId;
|
|
15
|
+
this.highWaterMark = config?.backpressure?.highWaterMark ?? AudioChunkDecoder.DEFAULT_HIGH_WATER_MARK;
|
|
16
|
+
this.decodeQueueThreshold = AudioChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
|
|
17
|
+
}
|
|
18
|
+
// Computed properties
|
|
19
|
+
get isConfigured() {
|
|
20
|
+
return this.isReady;
|
|
21
|
+
}
|
|
22
|
+
get state() {
|
|
23
|
+
return this.decoder?.state || "unconfigured";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Update configuration - can be called before or after initialization
|
|
27
|
+
*/
|
|
28
|
+
async updateConfig(config) {
|
|
29
|
+
if (!this.isReady && config.codec) {
|
|
30
|
+
await this.configure(config);
|
|
31
|
+
await this.processBufferedChunks();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Implement abstract methods
|
|
36
|
+
async isConfigSupported(config) {
|
|
37
|
+
const result = await AudioDecoder.isConfigSupported({
|
|
38
|
+
codec: config.codec,
|
|
39
|
+
sampleRate: config.sampleRate,
|
|
40
|
+
numberOfChannels: config.numberOfChannels
|
|
41
|
+
});
|
|
42
|
+
return { supported: result.supported ?? false };
|
|
43
|
+
}
|
|
44
|
+
createDecoder(init) {
|
|
45
|
+
return new AudioDecoder(init);
|
|
46
|
+
}
|
|
47
|
+
getDecoderType() {
|
|
48
|
+
return "Audio";
|
|
49
|
+
}
|
|
50
|
+
async configureDecoder(config) {
|
|
51
|
+
if (!this.decoder) return;
|
|
52
|
+
await this.decoder.configure({
|
|
53
|
+
codec: config.codec,
|
|
54
|
+
sampleRate: config.sampleRate,
|
|
55
|
+
numberOfChannels: config.numberOfChannels,
|
|
56
|
+
...config.description && { description: config.description }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
decode(chunk) {
|
|
60
|
+
this.decoder?.decode(chunk);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Configure the decoder with codec info (can be called after creation)
|
|
64
|
+
*/
|
|
65
|
+
async configure(config) {
|
|
66
|
+
if (this.isReady) {
|
|
67
|
+
await this.reconfigure(config);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.config = config;
|
|
71
|
+
await this.initialize();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Process any buffered chunks after configuration
|
|
75
|
+
* Note: Audio doesn't buffer in current implementation, but keeping for interface consistency
|
|
76
|
+
*/
|
|
77
|
+
async processBufferedChunks() {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const normalizeDescription = (desc) => {
|
|
81
|
+
if (!desc) return void 0;
|
|
82
|
+
if (desc instanceof ArrayBuffer) return desc;
|
|
83
|
+
const view = desc;
|
|
84
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
85
|
+
};
|
|
86
|
+
class AudioDecodeWorker {
|
|
87
|
+
channel;
|
|
88
|
+
decoder = null;
|
|
89
|
+
clipId = "";
|
|
90
|
+
trackId = "";
|
|
91
|
+
defaultConfig = {};
|
|
92
|
+
trackMetadata = null;
|
|
93
|
+
upstreamPort = null;
|
|
94
|
+
constructor() {
|
|
95
|
+
this.channel = new WorkerChannel(self, {
|
|
96
|
+
name: "AudioDecodeWorker",
|
|
97
|
+
timeout: 3e4
|
|
98
|
+
});
|
|
99
|
+
this.setupHandlers();
|
|
100
|
+
}
|
|
101
|
+
setupHandlers() {
|
|
102
|
+
this.channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
103
|
+
this.channel.registerHandler("connect", this.handleConnect.bind(this));
|
|
104
|
+
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
105
|
+
this.channel.registerHandler("reset", this.handleReset.bind(this));
|
|
106
|
+
this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
|
|
107
|
+
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
108
|
+
}
|
|
109
|
+
async handleConnect(payload) {
|
|
110
|
+
const { port, direction, sessionId, trackId } = payload;
|
|
111
|
+
if (direction === "upstream") {
|
|
112
|
+
this.upstreamPort = port;
|
|
113
|
+
this.clipId = sessionId || "default";
|
|
114
|
+
this.trackId = trackId || sessionId || "default";
|
|
115
|
+
const channel = new WorkerChannel(port, {
|
|
116
|
+
name: "Demux-AudioDecode",
|
|
117
|
+
timeout: 3e4
|
|
118
|
+
});
|
|
119
|
+
channel.receiveStream((stream, metadata) => {
|
|
120
|
+
this.handleReceiveStream(stream, {
|
|
121
|
+
...metadata,
|
|
122
|
+
clipStartUs: payload.clipStartUs,
|
|
123
|
+
clipDurationUs: payload.clipDurationUs
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
127
|
+
}
|
|
128
|
+
return { success: true };
|
|
129
|
+
}
|
|
130
|
+
async handleConfigure(payload) {
|
|
131
|
+
const { sessionId, streamType, codec, sampleRate, numberOfChannels, description, config } = payload;
|
|
132
|
+
if (sessionId && streamType === "audio") {
|
|
133
|
+
if (this.decoder) {
|
|
134
|
+
await this.decoder.updateConfig({
|
|
135
|
+
codec,
|
|
136
|
+
sampleRate,
|
|
137
|
+
numberOfChannels,
|
|
138
|
+
description: normalizeDescription(description)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { success: true };
|
|
142
|
+
}
|
|
143
|
+
if (config?.audio) {
|
|
144
|
+
Object.assign(this.defaultConfig, config.audio);
|
|
145
|
+
if (this.decoder) {
|
|
146
|
+
await this.decoder.updateConfig(config.audio);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this.channel.state = WorkerState.Ready;
|
|
150
|
+
return { success: true };
|
|
151
|
+
}
|
|
152
|
+
async handleReceiveStream(stream, metadata) {
|
|
153
|
+
const sessionId = metadata?.sessionId || this.clipId;
|
|
154
|
+
const trackId = metadata?.trackId || this.trackId;
|
|
155
|
+
if (!this.decoder) {
|
|
156
|
+
this.decoder = new AudioChunkDecoder(trackId, {
|
|
157
|
+
...this.defaultConfig,
|
|
158
|
+
codec: metadata?.codec,
|
|
159
|
+
sampleRate: metadata?.sampleRate,
|
|
160
|
+
numberOfChannels: metadata?.numberOfChannels,
|
|
161
|
+
description: normalizeDescription(metadata?.description)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
this.trackMetadata = {
|
|
165
|
+
clipId: this.clipId,
|
|
166
|
+
config: this.extractTrackConfig(metadata?.runtimeConfig),
|
|
167
|
+
sampleRate: metadata?.sampleRate,
|
|
168
|
+
numberOfChannels: metadata?.numberOfChannels,
|
|
169
|
+
type: metadata?.trackType ?? "other"
|
|
170
|
+
};
|
|
171
|
+
const transform = this.decoder.createStream();
|
|
172
|
+
this.channel.sendStream(transform.readable, {
|
|
173
|
+
streamType: "audio",
|
|
174
|
+
sessionId,
|
|
175
|
+
trackId,
|
|
176
|
+
clipStartUs: metadata?.clipStartUs ?? 0,
|
|
177
|
+
clipDurationUs: metadata?.clipDurationUs ?? 0,
|
|
178
|
+
trackMetadata: this.trackMetadata
|
|
179
|
+
});
|
|
180
|
+
stream.pipeTo(transform.writable).catch((error) => console.error("[AudioDecodeWorker] Audio stream pipe error:", error));
|
|
181
|
+
}
|
|
182
|
+
extractTrackConfig(config) {
|
|
183
|
+
return {
|
|
184
|
+
startTimeUs: config?.startTimeUs ?? 0,
|
|
185
|
+
durationUs: config?.durationUs,
|
|
186
|
+
volume: config?.volume ?? 1,
|
|
187
|
+
fadeIn: config?.fadeIn,
|
|
188
|
+
fadeOut: config?.fadeOut,
|
|
189
|
+
effects: config?.effects ?? [],
|
|
190
|
+
duckingTag: config?.duckingTag
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
async handleFlush() {
|
|
194
|
+
if (this.decoder) {
|
|
195
|
+
await this.decoder.flush();
|
|
196
|
+
}
|
|
197
|
+
return { success: true };
|
|
198
|
+
}
|
|
199
|
+
async handleReset() {
|
|
200
|
+
if (this.decoder) {
|
|
201
|
+
await this.decoder.reset();
|
|
202
|
+
}
|
|
203
|
+
this.channel.notify("reset_complete", {
|
|
204
|
+
type: "audio"
|
|
205
|
+
});
|
|
206
|
+
return { success: true };
|
|
207
|
+
}
|
|
208
|
+
async handleGetStats() {
|
|
209
|
+
if (!this.decoder) {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
audio: {
|
|
214
|
+
clipId: this.clipId,
|
|
215
|
+
trackId: this.trackId,
|
|
216
|
+
configured: this.decoder.isConfigured,
|
|
217
|
+
queueSize: this.decoder.queueSize,
|
|
218
|
+
state: this.decoder.state
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async handleDispose() {
|
|
223
|
+
if (this.decoder) {
|
|
224
|
+
await this.decoder.close();
|
|
225
|
+
this.decoder = null;
|
|
226
|
+
}
|
|
227
|
+
this.upstreamPort?.close();
|
|
228
|
+
this.upstreamPort = null;
|
|
229
|
+
this.trackMetadata = null;
|
|
230
|
+
this.channel.state = WorkerState.Disposed;
|
|
231
|
+
return { success: true };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const worker = new AudioDecodeWorker();
|
|
235
|
+
self.addEventListener("beforeunload", () => {
|
|
236
|
+
worker["handleDispose"]();
|
|
237
|
+
});
|
|
238
|
+
const audioDecode_worker = null;
|
|
239
|
+
export {
|
|
240
|
+
AudioDecodeWorker,
|
|
241
|
+
audioDecode_worker as default
|
|
242
|
+
};
|
|
243
|
+
//# sourceMappingURL=audio-decode.worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-decode.worker.js","sources":["../../../../src/stages/decode/AudioChunkDecoder.ts","../../../../src/stages/decode/audio-decode.worker.ts"],"sourcesContent":["import { AudioDecoderConfig } from './types';\nimport { BaseDecoder } from './BaseDecoder';\n\n/**\n * Audio decoder with streaming support\n * Extends BaseDecoder for common WebCodecs operations\n */\nexport class AudioChunkDecoder extends BaseDecoder<\n AudioDecoder,\n AudioDecoderConfig,\n EncodedAudioChunk,\n AudioData\n> {\n // Default values\n private static readonly DEFAULT_HIGH_WATER_MARK = 20;\n private static readonly DEFAULT_DECODE_QUEUE_THRESHOLD = 16;\n\n // Exposed properties\n readonly trackId: string;\n\n // Backpressure configuration\n protected readonly highWaterMark: number;\n protected readonly decodeQueueThreshold: number;\n\n constructor(trackId: string, config?: Partial<AudioDecoderConfig>) {\n // Initialize with empty config, will be configured later\n super(config as AudioDecoderConfig);\n\n this.trackId = trackId;\n\n // Set backpressure configuration\n this.highWaterMark =\n config?.backpressure?.highWaterMark ?? AudioChunkDecoder.DEFAULT_HIGH_WATER_MARK;\n this.decodeQueueThreshold = AudioChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;\n }\n\n // Computed properties\n get isConfigured(): boolean {\n return this.isReady;\n }\n\n get state(): string {\n return this.decoder?.state || 'unconfigured';\n }\n\n /**\n * Update configuration - can be called before or after initialization\n */\n async updateConfig(config: Partial<AudioDecoderConfig>): Promise<void> {\n // If decoder is not ready and we have codec info, configure it\n if (!this.isReady && config.codec) {\n await this.configure(config as AudioDecoderConfig);\n await this.processBufferedChunks();\n return;\n }\n\n // Note: AudioDecoder doesn't have many runtime-configurable options\n // Backpressure settings are readonly in this implementation\n // if (config.backpressure) {\n // console.warn('Backpressure settings cannot be changed at runtime');\n // }\n }\n\n // Implement abstract methods\n protected async isConfigSupported(config: AudioDecoderConfig): Promise<{ supported: boolean }> {\n const result = await AudioDecoder.isConfigSupported({\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n });\n return { supported: result.supported ?? false };\n }\n\n protected createDecoder(init: {\n output: (data: AudioData) => void;\n error: (error: DOMException) => void;\n }): AudioDecoder {\n return new AudioDecoder(init);\n }\n\n protected getDecoderType(): string {\n return 'Audio';\n }\n\n protected async configureDecoder(config: AudioDecoderConfig): Promise<void> {\n if (!this.decoder) return;\n\n await this.decoder.configure({\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n ...(config.description && { description: config.description }),\n });\n }\n\n protected decode(chunk: EncodedAudioChunk): void {\n this.decoder?.decode(chunk);\n }\n\n /**\n * Configure the decoder with codec info (can be called after creation)\n */\n async configure(config: AudioDecoderConfig): Promise<void> {\n if (this.isReady) {\n // If already configured, reconfigure\n await this.reconfigure(config);\n return;\n }\n\n this.config = config as any;\n\n // Initialize decoder with new config\n await this.initialize();\n }\n\n /**\n * Process any buffered chunks after configuration\n * Note: Audio doesn't buffer in current implementation, but keeping for interface consistency\n */\n async processBufferedChunks(): Promise<void> {\n // No-op for audio in current implementation\n }\n}\n","import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { AudioChunkDecoder } from './AudioChunkDecoder';\nimport { AudioDecoderConfig } from './types';\nimport type { AudioTrackConfig } from '../compose/types';\n\ninterface TrackMetadata {\n clipId: string;\n config: AudioTrackConfig;\n sampleRate?: number;\n numberOfChannels?: number;\n type: 'bgm' | 'voice' | 'sfx' | 'other';\n}\n\nconst normalizeDescription = (desc?: ArrayBuffer | ArrayBufferView): ArrayBuffer | undefined => {\n if (!desc) return undefined;\n\n if (desc instanceof ArrayBuffer) return desc;\n\n const view = desc as ArrayBufferView;\n return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer;\n};\n\n/**\n * AudioDecodeWorker (Clip Local) - Decodes audio for a single clip\n * Receives encoded audio chunks from AudioDemuxWorker and outputs decoded audio data\n *\n * Pipeline: AudioDemuxWorker → AudioDecodeWorker → Main Thread (OfflineAudioContext)\n *\n * Features:\n * - Single clip, single AudioDecoder instance\n * - Outputs AudioData stream to main thread for mixing\n * - Stream-based processing with backpressure\n * - Lifecycle tied to clip pipeline\n */\nexport class AudioDecodeWorker {\n private channel: WorkerChannel;\n private decoder: AudioChunkDecoder | null = null;\n private clipId: string = '';\n private trackId: string = '';\n\n private defaultConfig: Partial<AudioDecoderConfig> = {};\n private trackMetadata: TrackMetadata | null = null;\n\n private upstreamPort: MessagePort | null = null;\n\n constructor() {\n this.channel = new WorkerChannel(self as any, {\n name: 'AudioDecodeWorker',\n timeout: 30000,\n });\n\n this.setupHandlers();\n }\n\n private setupHandlers(): void {\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n this.channel.registerHandler('connect', this.handleConnect.bind(this));\n this.channel.registerHandler('flush', this.handleFlush.bind(this));\n this.channel.registerHandler('reset', this.handleReset.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n }\n\n private async handleConnect(payload: {\n direction: 'upstream' | 'downstream';\n port: MessagePort;\n streamType?: 'audio';\n sessionId?: string;\n trackId?: string;\n clipStartUs?: number;\n clipDurationUs?: number;\n }): Promise<{ success: boolean }> {\n const { port, direction, sessionId, trackId } = payload;\n\n if (direction === 'upstream') {\n this.upstreamPort = port;\n this.clipId = sessionId || 'default';\n this.trackId = trackId || sessionId || 'default';\n\n const channel = new WorkerChannel(port, {\n name: 'Demux-AudioDecode',\n timeout: 30000,\n });\n\n channel.receiveStream((stream, metadata) => {\n this.handleReceiveStream(stream, {\n ...metadata,\n clipStartUs: payload.clipStartUs,\n clipDurationUs: payload.clipDurationUs,\n });\n });\n\n channel.registerHandler('configure', this.handleConfigure.bind(this));\n }\n\n return { success: true };\n }\n\n private async handleConfigure(payload: {\n config?: { audio?: Partial<AudioDecoderConfig> };\n sessionId?: string;\n streamType?: 'audio';\n codec?: string;\n sampleRate?: number;\n numberOfChannels?: number;\n description?: ArrayBuffer | Uint8Array;\n }): Promise<{ success: boolean }> {\n const { sessionId, streamType, codec, sampleRate, numberOfChannels, description, config } =\n payload;\n\n if (sessionId && streamType === 'audio') {\n if (this.decoder) {\n await this.decoder.updateConfig({\n codec,\n sampleRate,\n numberOfChannels,\n description: normalizeDescription(description),\n });\n }\n return { success: true };\n }\n\n if (config?.audio) {\n Object.assign(this.defaultConfig, config.audio);\n\n if (this.decoder) {\n await this.decoder.updateConfig(config.audio);\n }\n }\n\n this.channel.state = WorkerState.Ready;\n\n return { success: true };\n }\n\n private async handleReceiveStream(\n stream: ReadableStream,\n metadata?: Record<string, any>\n ): Promise<void> {\n const sessionId = metadata?.sessionId || this.clipId;\n const trackId = metadata?.trackId || this.trackId;\n\n if (!this.decoder) {\n this.decoder = new AudioChunkDecoder(trackId, {\n ...this.defaultConfig,\n codec: metadata?.codec,\n sampleRate: metadata?.sampleRate,\n numberOfChannels: metadata?.numberOfChannels,\n description: normalizeDescription(metadata?.description),\n });\n }\n\n this.trackMetadata = {\n clipId: this.clipId,\n config: this.extractTrackConfig(metadata?.runtimeConfig),\n sampleRate: metadata?.sampleRate,\n numberOfChannels: metadata?.numberOfChannels,\n type: (metadata?.trackType as TrackMetadata['type']) ?? 'other',\n };\n\n const transform = this.decoder.createStream();\n\n this.channel.sendStream(transform.readable as ReadableStream<AudioData>, {\n streamType: 'audio',\n sessionId,\n trackId,\n clipStartUs: metadata?.clipStartUs ?? 0,\n clipDurationUs: metadata?.clipDurationUs ?? 0,\n trackMetadata: this.trackMetadata,\n });\n\n stream\n .pipeTo(transform.writable)\n .catch((error) => console.error('[AudioDecodeWorker] Audio stream pipe error:', error));\n }\n\n private extractTrackConfig(config: any): AudioTrackConfig {\n return {\n startTimeUs: config?.startTimeUs ?? 0,\n durationUs: config?.durationUs,\n volume: config?.volume ?? 1,\n fadeIn: config?.fadeIn,\n fadeOut: config?.fadeOut,\n effects: config?.effects ?? [],\n duckingTag: config?.duckingTag,\n };\n }\n\n private async handleFlush(): Promise<{ success: boolean }> {\n if (this.decoder) {\n await this.decoder.flush();\n }\n return { success: true };\n }\n\n private async handleReset(): Promise<{ success: boolean }> {\n if (this.decoder) {\n await this.decoder.reset();\n }\n\n this.channel.notify('reset_complete', {\n type: 'audio',\n });\n\n return { success: true };\n }\n\n private async handleGetStats(): Promise<{ audio?: any }> {\n if (!this.decoder) {\n return {};\n }\n\n return {\n audio: {\n clipId: this.clipId,\n trackId: this.trackId,\n configured: this.decoder.isConfigured,\n queueSize: this.decoder.queueSize,\n state: this.decoder.state,\n },\n };\n }\n\n private async handleDispose(): Promise<{ success: boolean }> {\n if (this.decoder) {\n await this.decoder.close();\n this.decoder = null;\n }\n\n this.upstreamPort?.close();\n this.upstreamPort = null;\n\n this.trackMetadata = null;\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n}\n\nconst worker = new AudioDecodeWorker();\n\nself.addEventListener('beforeunload', () => {\n worker['handleDispose']();\n});\n\nexport default null;\n"],"names":[],"mappings":";;AAOO,MAAM,0BAA0B,YAKrC;AAAA;AAAA,EAEA,OAAwB,0BAA0B;AAAA,EAClD,OAAwB,iCAAiC;AAAA;AAAA,EAGhD;AAAA;AAAA,EAGU;AAAA,EACA;AAAA,EAEnB,YAAY,SAAiB,QAAsC;AAEjE,UAAM,MAA4B;AAElC,SAAK,UAAU;AAGf,SAAK,gBACH,QAAQ,cAAc,iBAAiB,kBAAkB;AAC3D,SAAK,uBAAuB,kBAAkB;AAAA,EAChD;AAAA;AAAA,EAGA,IAAI,eAAwB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,QAAgB;AAClB,WAAO,KAAK,SAAS,SAAS;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,QAAoD;AAErE,QAAI,CAAC,KAAK,WAAW,OAAO,OAAO;AACjC,YAAM,KAAK,UAAU,MAA4B;AACjD,YAAM,KAAK,sBAAA;AACX;AAAA,IACF;AAAA,EAOF;AAAA;AAAA,EAGA,MAAgB,kBAAkB,QAA6D;AAC7F,UAAM,SAAS,MAAM,aAAa,kBAAkB;AAAA,MAClD,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,kBAAkB,OAAO;AAAA,IAAA,CAC1B;AACD,WAAO,EAAE,WAAW,OAAO,aAAa,MAAA;AAAA,EAC1C;AAAA,EAEU,cAAc,MAGP;AACf,WAAO,IAAI,aAAa,IAAI;AAAA,EAC9B;AAAA,EAEU,iBAAyB;AACjC,WAAO;AAAA,EACT;AAAA,EAEA,MAAgB,iBAAiB,QAA2C;AAC1E,QAAI,CAAC,KAAK,QAAS;AAEnB,UAAM,KAAK,QAAQ,UAAU;AAAA,MAC3B,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,kBAAkB,OAAO;AAAA,MACzB,GAAI,OAAO,eAAe,EAAE,aAAa,OAAO,YAAA;AAAA,IAAY,CAC7D;AAAA,EACH;AAAA,EAEU,OAAO,OAAgC;AAC/C,SAAK,SAAS,OAAO,KAAK;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,QAA2C;AACzD,QAAI,KAAK,SAAS;AAEhB,YAAM,KAAK,YAAY,MAAM;AAC7B;AAAA,IACF;AAEA,SAAK,SAAS;AAGd,UAAM,KAAK,WAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,wBAAuC;AAAA,EAE7C;AACF;AC5GA,MAAM,uBAAuB,CAAC,SAAkE;AAC9F,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,gBAAgB,YAAa,QAAO;AAExC,QAAM,OAAO;AACb,SAAO,KAAK,OAAO,MAAM,KAAK,YAAY,KAAK,aAAa,KAAK,UAAU;AAC7E;AAcO,MAAM,kBAAkB;AAAA,EACrB;AAAA,EACA,UAAoC;AAAA,EACpC,SAAiB;AAAA,EACjB,UAAkB;AAAA,EAElB,gBAA6C,CAAA;AAAA,EAC7C,gBAAsC;AAAA,EAEtC,eAAmC;AAAA,EAE3C,cAAc;AACZ,SAAK,UAAU,IAAI,cAAc,MAAa;AAAA,MAC5C,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAED,SAAK,cAAA;AAAA,EACP;AAAA,EAEQ,gBAAsB;AAC5B,SAAK,QAAQ,gBAAgB,aAAa,KAAK,gBAAgB,KAAK,IAAI,CAAC;AACzE,SAAK,QAAQ,gBAAgB,WAAW,KAAK,cAAc,KAAK,IAAI,CAAC;AACrE,SAAK,QAAQ,gBAAgB,SAAS,KAAK,YAAY,KAAK,IAAI,CAAC;AACjE,SAAK,QAAQ,gBAAgB,SAAS,KAAK,YAAY,KAAK,IAAI,CAAC;AACjE,SAAK,QAAQ,gBAAgB,aAAa,KAAK,eAAe,KAAK,IAAI,CAAC;AACxE,SAAK,QAAQ,gBAAgB,kBAAkB,SAAS,KAAK,cAAc,KAAK,IAAI,CAAC;AAAA,EACvF;AAAA,EAEA,MAAc,cAAc,SAQM;AAChC,UAAM,EAAE,MAAM,WAAW,WAAW,YAAY;AAEhD,QAAI,cAAc,YAAY;AAC5B,WAAK,eAAe;AACpB,WAAK,SAAS,aAAa;AAC3B,WAAK,UAAU,WAAW,aAAa;AAEvC,YAAM,UAAU,IAAI,cAAc,MAAM;AAAA,QACtC,MAAM;AAAA,QACN,SAAS;AAAA,MAAA,CACV;AAED,cAAQ,cAAc,CAAC,QAAQ,aAAa;AAC1C,aAAK,oBAAoB,QAAQ;AAAA,UAC/B,GAAG;AAAA,UACH,aAAa,QAAQ;AAAA,UACrB,gBAAgB,QAAQ;AAAA,QAAA,CACzB;AAAA,MACH,CAAC;AAED,cAAQ,gBAAgB,aAAa,KAAK,gBAAgB,KAAK,IAAI,CAAC;AAAA,IACtE;AAEA,WAAO,EAAE,SAAS,KAAA;AAAA,EACpB;AAAA,EAEA,MAAc,gBAAgB,SAQI;AAChC,UAAM,EAAE,WAAW,YAAY,OAAO,YAAY,kBAAkB,aAAa,WAC/E;AAEF,QAAI,aAAa,eAAe,SAAS;AACvC,UAAI,KAAK,SAAS;AAChB,cAAM,KAAK,QAAQ,aAAa;AAAA,UAC9B;AAAA,UACA;AAAA,UACA;AAAA,UACA,aAAa,qBAAqB,WAAW;AAAA,QAAA,CAC9C;AAAA,MACH;AACA,aAAO,EAAE,SAAS,KAAA;AAAA,IACpB;AAEA,QAAI,QAAQ,OAAO;AACjB,aAAO,OAAO,KAAK,eAAe,OAAO,KAAK;AAE9C,UAAI,KAAK,SAAS;AAChB,cAAM,KAAK,QAAQ,aAAa,OAAO,KAAK;AAAA,MAC9C;AAAA,IACF;AAEA,SAAK,QAAQ,QAAQ,YAAY;AAEjC,WAAO,EAAE,SAAS,KAAA;AAAA,EACpB;AAAA,EAEA,MAAc,oBACZ,QACA,UACe;AACf,UAAM,YAAY,UAAU,aAAa,KAAK;AAC9C,UAAM,UAAU,UAAU,WAAW,KAAK;AAE1C,QAAI,CAAC,KAAK,SAAS;AACjB,WAAK,UAAU,IAAI,kBAAkB,SAAS;AAAA,QAC5C,GAAG,KAAK;AAAA,QACR,OAAO,UAAU;AAAA,QACjB,YAAY,UAAU;AAAA,QACtB,kBAAkB,UAAU;AAAA,QAC5B,aAAa,qBAAqB,UAAU,WAAW;AAAA,MAAA,CACxD;AAAA,IACH;AAEA,SAAK,gBAAgB;AAAA,MACnB,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK,mBAAmB,UAAU,aAAa;AAAA,MACvD,YAAY,UAAU;AAAA,MACtB,kBAAkB,UAAU;AAAA,MAC5B,MAAO,UAAU,aAAuC;AAAA,IAAA;AAG1D,UAAM,YAAY,KAAK,QAAQ,aAAA;AAE/B,SAAK,QAAQ,WAAW,UAAU,UAAuC;AAAA,MACvE,YAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA,aAAa,UAAU,eAAe;AAAA,MACtC,gBAAgB,UAAU,kBAAkB;AAAA,MAC5C,eAAe,KAAK;AAAA,IAAA,CACrB;AAED,WACG,OAAO,UAAU,QAAQ,EACzB,MAAM,CAAC,UAAU,QAAQ,MAAM,gDAAgD,KAAK,CAAC;AAAA,EAC1F;AAAA,EAEQ,mBAAmB,QAA+B;AACxD,WAAO;AAAA,MACL,aAAa,QAAQ,eAAe;AAAA,MACpC,YAAY,QAAQ;AAAA,MACpB,QAAQ,QAAQ,UAAU;AAAA,MAC1B,QAAQ,QAAQ;AAAA,MAChB,SAAS,QAAQ;AAAA,MACjB,SAAS,QAAQ,WAAW,CAAA;AAAA,MAC5B,YAAY,QAAQ;AAAA,IAAA;AAAA,EAExB;AAAA,EAEA,MAAc,cAA6C;AACzD,QAAI,KAAK,SAAS;AAChB,YAAM,KAAK,QAAQ,MAAA;AAAA,IACrB;AACA,WAAO,EAAE,SAAS,KAAA;AAAA,EACpB;AAAA,EAEA,MAAc,cAA6C;AACzD,QAAI,KAAK,SAAS;AAChB,YAAM,KAAK,QAAQ,MAAA;AAAA,IACrB;AAEA,SAAK,QAAQ,OAAO,kBAAkB;AAAA,MACpC,MAAM;AAAA,IAAA,CACP;AAED,WAAO,EAAE,SAAS,KAAA;AAAA,EACpB;AAAA,EAEA,MAAc,iBAA2C;AACvD,QAAI,CAAC,KAAK,SAAS;AACjB,aAAO,CAAA;AAAA,IACT;AAEA,WAAO;AAAA,MACL,OAAO;AAAA,QACL,QAAQ,KAAK;AAAA,QACb,SAAS,KAAK;AAAA,QACd,YAAY,KAAK,QAAQ;AAAA,QACzB,WAAW,KAAK,QAAQ;AAAA,QACxB,OAAO,KAAK,QAAQ;AAAA,MAAA;AAAA,IACtB;AAAA,EAEJ;AAAA,EAEA,MAAc,gBAA+C;AAC3D,QAAI,KAAK,SAAS;AAChB,YAAM,KAAK,QAAQ,MAAA;AACnB,WAAK,UAAU;AAAA,IACjB;AAEA,SAAK,cAAc,MAAA;AACnB,SAAK,eAAe;AAEpB,SAAK,gBAAgB;AAErB,SAAK,QAAQ,QAAQ,YAAY;AAEjC,WAAO,EAAE,SAAS,KAAA;AAAA,EACpB;AACF;AAEA,MAAM,SAAS,IAAI,kBAAA;AAEnB,KAAK,iBAAiB,gBAAgB,MAAM;AAC1C,SAAO,eAAe,EAAA;AACxB,CAAC;AAED,MAAA,qBAAe;"}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { W as WorkerChannel, a as WorkerMessageType, b as WorkerState } from "../../WorkerChannel.js";
|
|
2
|
+
import { B as BaseDecoder } from "../../BaseDecoder.js";
|
|
3
|
+
class VideoChunkDecoder extends BaseDecoder {
|
|
4
|
+
static DEFAULT_HIGH_WATER_MARK = 4;
|
|
5
|
+
static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
|
|
6
|
+
trackId;
|
|
7
|
+
highWaterMark;
|
|
8
|
+
decodeQueueThreshold;
|
|
9
|
+
// GOP tracking (serial is derived from keyframe timestamp for idempotency)
|
|
10
|
+
currentGopSerial = -1;
|
|
11
|
+
isCurrentFrameKeyframe = false;
|
|
12
|
+
// Buffering support for delayed configuration
|
|
13
|
+
bufferedChunks = [];
|
|
14
|
+
isProcessingBuffer = false;
|
|
15
|
+
constructor(trackId, config) {
|
|
16
|
+
super(config || {});
|
|
17
|
+
this.trackId = trackId;
|
|
18
|
+
this.highWaterMark = config?.backpressure?.highWaterMark ?? VideoChunkDecoder.DEFAULT_HIGH_WATER_MARK;
|
|
19
|
+
this.decodeQueueThreshold = config?.backpressure?.decodeQueueThreshold ?? VideoChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
|
|
20
|
+
}
|
|
21
|
+
// Computed properties
|
|
22
|
+
get isConfigured() {
|
|
23
|
+
return this.isReady;
|
|
24
|
+
}
|
|
25
|
+
get state() {
|
|
26
|
+
return this.decoder?.state || "unconfigured";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Update configuration - can be called before or after initialization
|
|
30
|
+
*/
|
|
31
|
+
async updateConfig(config) {
|
|
32
|
+
if (!this.isReady && config.codec) {
|
|
33
|
+
await this.configure(config);
|
|
34
|
+
await this.processBufferedChunks();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Override createStream to handle GOP tracking and buffering
|
|
38
|
+
// Always create new stream for each clip (ReadableStreams can only be consumed once)
|
|
39
|
+
createStream() {
|
|
40
|
+
return new TransformStream(
|
|
41
|
+
{
|
|
42
|
+
start: async (controller) => {
|
|
43
|
+
this.controller = controller;
|
|
44
|
+
if (this.config?.codec && this.config?.description && !this.isReady) {
|
|
45
|
+
await this.initialize();
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
transform: async (chunk) => {
|
|
49
|
+
if (!this.isReady) {
|
|
50
|
+
this.bufferedChunks.push(chunk);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (this.isProcessingBuffer) {
|
|
54
|
+
this.bufferedChunks.push(chunk);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await this.processChunk(chunk);
|
|
58
|
+
},
|
|
59
|
+
flush: async () => {
|
|
60
|
+
if (this.isReady) {
|
|
61
|
+
await this.flush();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
highWaterMark: this.highWaterMark,
|
|
67
|
+
size: () => 1
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Process a single chunk (extracted from transform for reuse)
|
|
73
|
+
*/
|
|
74
|
+
async processChunk(chunk) {
|
|
75
|
+
if (!this.decoder) {
|
|
76
|
+
throw new Error("Decoder not initialized");
|
|
77
|
+
}
|
|
78
|
+
if (this.decoder.state !== "configured") {
|
|
79
|
+
console.error("[VideoChunkDecoder] Decoder in unexpected state:", this.decoder.state);
|
|
80
|
+
throw new Error(`Decoder not configured, state: ${this.decoder.state}`);
|
|
81
|
+
}
|
|
82
|
+
if (chunk.type === "key") {
|
|
83
|
+
this.handleKeyFrame(chunk.timestamp);
|
|
84
|
+
}
|
|
85
|
+
if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
|
|
86
|
+
await new Promise((resolve) => {
|
|
87
|
+
const check = () => {
|
|
88
|
+
if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
|
|
89
|
+
resolve();
|
|
90
|
+
} else {
|
|
91
|
+
setTimeout(check, 20);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
check();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
this.decode(chunk);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(`[VideoChunkDecoder] decode error:`, error);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Override handleOutput to attach GOP metadata
|
|
105
|
+
handleOutput(frame) {
|
|
106
|
+
const wrappedFrame = {
|
|
107
|
+
frame,
|
|
108
|
+
gopSerial: this.currentGopSerial,
|
|
109
|
+
isKeyframe: this.isCurrentFrameKeyframe,
|
|
110
|
+
timestamp: frame.timestamp
|
|
111
|
+
};
|
|
112
|
+
this.isCurrentFrameKeyframe = false;
|
|
113
|
+
super.handleOutput(wrappedFrame);
|
|
114
|
+
}
|
|
115
|
+
// Implement abstract methods
|
|
116
|
+
async isConfigSupported(config) {
|
|
117
|
+
const result = await VideoDecoder.isConfigSupported({
|
|
118
|
+
codec: config.codec,
|
|
119
|
+
codedWidth: config.width,
|
|
120
|
+
codedHeight: config.height
|
|
121
|
+
});
|
|
122
|
+
return { supported: result.supported ?? false };
|
|
123
|
+
}
|
|
124
|
+
createDecoder(init) {
|
|
125
|
+
return new VideoDecoder(init);
|
|
126
|
+
}
|
|
127
|
+
getDecoderType() {
|
|
128
|
+
return "Video";
|
|
129
|
+
}
|
|
130
|
+
async configureDecoder(config) {
|
|
131
|
+
if (!this.decoder) return;
|
|
132
|
+
const decoderConfig = {
|
|
133
|
+
codec: config.codec,
|
|
134
|
+
codedWidth: config.width,
|
|
135
|
+
codedHeight: config.height,
|
|
136
|
+
hardwareAcceleration: config.hardwareAcceleration || "no-preference",
|
|
137
|
+
optimizeForLatency: false,
|
|
138
|
+
...config.description && { description: config.description },
|
|
139
|
+
...config.displayAspectWidth && { displayAspectWidth: config.displayAspectWidth },
|
|
140
|
+
...config.displayAspectHeight && { displayAspectHeight: config.displayAspectHeight }
|
|
141
|
+
};
|
|
142
|
+
this.decoder.configure(decoderConfig);
|
|
143
|
+
}
|
|
144
|
+
decode(chunk) {
|
|
145
|
+
this.decoder?.decode(chunk);
|
|
146
|
+
}
|
|
147
|
+
// Override reset to clear GOP data
|
|
148
|
+
onReset() {
|
|
149
|
+
this.currentGopSerial = -1;
|
|
150
|
+
this.isCurrentFrameKeyframe = false;
|
|
151
|
+
}
|
|
152
|
+
// GOP management methods
|
|
153
|
+
handleKeyFrame(timestamp) {
|
|
154
|
+
this.currentGopSerial = timestamp;
|
|
155
|
+
this.isCurrentFrameKeyframe = true;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Configure the decoder with codec info (can be called after creation)
|
|
159
|
+
*/
|
|
160
|
+
async configure(config) {
|
|
161
|
+
if (this.isReady) {
|
|
162
|
+
await this.reconfigure(config);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.config = config;
|
|
166
|
+
await this.initialize();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Process any buffered chunks after configuration
|
|
170
|
+
*/
|
|
171
|
+
async processBufferedChunks() {
|
|
172
|
+
if (!this.isReady || this.bufferedChunks.length === 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.isProcessingBuffer = true;
|
|
176
|
+
const chunks = [...this.bufferedChunks];
|
|
177
|
+
this.bufferedChunks = [];
|
|
178
|
+
for (const chunk of chunks) {
|
|
179
|
+
try {
|
|
180
|
+
await this.processChunk(chunk);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error("[VideoChunkDecoder] Error processing buffered chunk:", error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
this.isProcessingBuffer = false;
|
|
186
|
+
if (this.bufferedChunks.length > 0) {
|
|
187
|
+
await this.processBufferedChunks();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Override close to clean up GOP data
|
|
191
|
+
async close() {
|
|
192
|
+
this.bufferedChunks = [];
|
|
193
|
+
this.onReset();
|
|
194
|
+
await super.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const normalizeDescription = (desc) => {
|
|
198
|
+
if (!desc) return void 0;
|
|
199
|
+
if (desc instanceof ArrayBuffer) return desc;
|
|
200
|
+
const view = desc;
|
|
201
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
202
|
+
};
|
|
203
|
+
class VideoDecodeWorker {
|
|
204
|
+
channel;
|
|
205
|
+
decoder = null;
|
|
206
|
+
clipId = "";
|
|
207
|
+
defaultConfig = {};
|
|
208
|
+
upstreamPort = null;
|
|
209
|
+
downstreamPort = null;
|
|
210
|
+
constructor() {
|
|
211
|
+
this.channel = new WorkerChannel(self, {
|
|
212
|
+
name: "VideoDecodeWorker",
|
|
213
|
+
timeout: 3e4
|
|
214
|
+
});
|
|
215
|
+
this.setupHandlers();
|
|
216
|
+
}
|
|
217
|
+
setupHandlers() {
|
|
218
|
+
this.channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
219
|
+
this.channel.registerHandler("connect", this.handleConnect.bind(this));
|
|
220
|
+
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
221
|
+
this.channel.registerHandler("reset", this.handleReset.bind(this));
|
|
222
|
+
this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
|
|
223
|
+
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
224
|
+
}
|
|
225
|
+
async handleConnect(payload) {
|
|
226
|
+
const { port, direction, sessionId } = payload;
|
|
227
|
+
if (direction === "upstream") {
|
|
228
|
+
this.upstreamPort = port;
|
|
229
|
+
this.clipId = sessionId || "default";
|
|
230
|
+
const channel = new WorkerChannel(port, {
|
|
231
|
+
name: "Demux-VideoDecode",
|
|
232
|
+
timeout: 3e4
|
|
233
|
+
});
|
|
234
|
+
channel.receiveStream((stream, metadata) => {
|
|
235
|
+
this.handleReceiveStream(stream, {
|
|
236
|
+
...metadata,
|
|
237
|
+
clipStartUs: payload.clipStartUs,
|
|
238
|
+
clipDurationUs: payload.clipDurationUs
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
242
|
+
}
|
|
243
|
+
if (direction === "downstream") {
|
|
244
|
+
this.downstreamPort = port;
|
|
245
|
+
}
|
|
246
|
+
return { success: true };
|
|
247
|
+
}
|
|
248
|
+
async handleConfigure(payload) {
|
|
249
|
+
const { sessionId, streamType, codec, width, height, description, config } = payload;
|
|
250
|
+
if (sessionId && streamType === "video") {
|
|
251
|
+
if (this.decoder) {
|
|
252
|
+
await this.decoder.updateConfig({
|
|
253
|
+
codec,
|
|
254
|
+
width,
|
|
255
|
+
height,
|
|
256
|
+
description: normalizeDescription(description)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return { success: true };
|
|
260
|
+
}
|
|
261
|
+
if (config?.video) {
|
|
262
|
+
Object.assign(this.defaultConfig, config.video);
|
|
263
|
+
if (this.decoder) {
|
|
264
|
+
await this.decoder.updateConfig(config.video);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.channel.state = WorkerState.Ready;
|
|
268
|
+
return { success: true };
|
|
269
|
+
}
|
|
270
|
+
async handleReceiveStream(stream, metadata) {
|
|
271
|
+
const sessionId = metadata?.sessionId || this.clipId;
|
|
272
|
+
if (!this.decoder) {
|
|
273
|
+
this.decoder = new VideoChunkDecoder(sessionId, {
|
|
274
|
+
...this.defaultConfig,
|
|
275
|
+
codec: metadata?.codec,
|
|
276
|
+
width: metadata?.width,
|
|
277
|
+
height: metadata?.height,
|
|
278
|
+
description: normalizeDescription(metadata?.description)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const transform = this.decoder.createStream();
|
|
282
|
+
if (this.downstreamPort) {
|
|
283
|
+
const channel = new WorkerChannel(this.downstreamPort, {
|
|
284
|
+
name: "VideoDecode-Compose",
|
|
285
|
+
timeout: 3e4
|
|
286
|
+
});
|
|
287
|
+
channel.sendStream(transform.readable, {
|
|
288
|
+
streamType: "video",
|
|
289
|
+
sessionId
|
|
290
|
+
});
|
|
291
|
+
stream.pipeTo(transform.writable).catch(
|
|
292
|
+
(error) => console.error("[VideoDecodeWorker] Video stream pipe error:", sessionId, error)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async handleFlush() {
|
|
297
|
+
if (this.decoder) {
|
|
298
|
+
await this.decoder.flush();
|
|
299
|
+
}
|
|
300
|
+
return { success: true };
|
|
301
|
+
}
|
|
302
|
+
async handleReset() {
|
|
303
|
+
if (this.decoder) {
|
|
304
|
+
await this.decoder.reset();
|
|
305
|
+
}
|
|
306
|
+
this.channel.notify("reset_complete", {
|
|
307
|
+
type: "video"
|
|
308
|
+
});
|
|
309
|
+
return { success: true };
|
|
310
|
+
}
|
|
311
|
+
async handleGetStats() {
|
|
312
|
+
if (!this.decoder) {
|
|
313
|
+
return {};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
video: {
|
|
317
|
+
clipId: this.clipId,
|
|
318
|
+
configured: this.decoder.isConfigured,
|
|
319
|
+
queueSize: this.decoder.queueSize,
|
|
320
|
+
state: this.decoder.state
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async handleDispose() {
|
|
325
|
+
if (this.decoder) {
|
|
326
|
+
await this.decoder.close();
|
|
327
|
+
this.decoder = null;
|
|
328
|
+
}
|
|
329
|
+
this.upstreamPort?.close();
|
|
330
|
+
this.upstreamPort = null;
|
|
331
|
+
this.downstreamPort?.close();
|
|
332
|
+
this.downstreamPort = null;
|
|
333
|
+
this.channel.state = WorkerState.Disposed;
|
|
334
|
+
return { success: true };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const worker = new VideoDecodeWorker();
|
|
338
|
+
self.addEventListener("beforeunload", () => {
|
|
339
|
+
worker["handleDispose"]();
|
|
340
|
+
});
|
|
341
|
+
const videoDecode_worker = null;
|
|
342
|
+
export {
|
|
343
|
+
VideoDecodeWorker,
|
|
344
|
+
videoDecode_worker as default
|
|
345
|
+
};
|
|
346
|
+
//# sourceMappingURL=video-decode.worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-decode.worker.js","sources":["../../../../src/stages/decode/VideoChunkDecoder.ts","../../../../src/stages/decode/video-decode.worker.ts"],"sourcesContent":["import { VideoDecoderConfig } from './types';\nimport { BaseDecoder } from './BaseDecoder';\n\n/**\n * Video decoder with GOP tracking\n * Tracks keyframe boundaries and attaches GOP metadata to decoded frames\n */\nexport class VideoChunkDecoder extends BaseDecoder<\n VideoDecoder,\n VideoDecoderConfig,\n EncodedVideoChunk,\n VideoFrame\n> {\n private static readonly DEFAULT_HIGH_WATER_MARK = 4;\n private static readonly DEFAULT_DECODE_QUEUE_THRESHOLD = 16;\n\n readonly trackId: string;\n\n protected readonly highWaterMark: number;\n protected readonly decodeQueueThreshold: number;\n\n // GOP tracking (serial is derived from keyframe timestamp for idempotency)\n private currentGopSerial = -1;\n private isCurrentFrameKeyframe = false;\n\n // Buffering support for delayed configuration\n private bufferedChunks: EncodedVideoChunk[] = [];\n private isProcessingBuffer: boolean = false;\n\n constructor(trackId: string, config?: Partial<VideoDecoderConfig>) {\n super((config || {}) as VideoDecoderConfig);\n\n this.trackId = trackId;\n this.highWaterMark =\n config?.backpressure?.highWaterMark ?? VideoChunkDecoder.DEFAULT_HIGH_WATER_MARK;\n this.decodeQueueThreshold =\n config?.backpressure?.decodeQueueThreshold ??\n VideoChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;\n }\n\n // Computed properties\n get isConfigured(): boolean {\n return this.isReady;\n }\n\n get state(): string {\n return this.decoder?.state || 'unconfigured';\n }\n\n /**\n * Update configuration - can be called before or after initialization\n */\n async updateConfig(config: Partial<VideoDecoderConfig>): Promise<void> {\n if (!this.isReady && config.codec) {\n await this.configure(config as VideoDecoderConfig);\n await this.processBufferedChunks();\n }\n }\n\n // Override createStream to handle GOP tracking and buffering\n // Always create new stream for each clip (ReadableStreams can only be consumed once)\n override createStream(): TransformStream<EncodedVideoChunk, VideoFrame> {\n return new TransformStream<EncodedVideoChunk, VideoFrame>(\n {\n start: async (controller) => {\n this.controller = controller;\n // Don't initialize if no codec config yet\n if (this.config?.codec && this.config?.description && !this.isReady) {\n await this.initialize();\n }\n },\n\n transform: async (chunk) => {\n // If not configured yet, buffer the chunk\n if (!this.isReady) {\n this.bufferedChunks.push(chunk);\n return; // Don't process yet\n }\n\n // If we're processing buffered chunks, add to buffer to maintain order\n if (this.isProcessingBuffer) {\n this.bufferedChunks.push(chunk);\n return;\n }\n\n // Normal processing\n await this.processChunk(chunk);\n },\n\n flush: async () => {\n if (this.isReady) {\n await this.flush();\n }\n },\n },\n {\n highWaterMark: this.highWaterMark,\n size: () => 1,\n }\n );\n }\n\n /**\n * Process a single chunk (extracted from transform for reuse)\n */\n private async processChunk(chunk: EncodedVideoChunk): Promise<void> {\n if (!this.decoder) {\n throw new Error('Decoder not initialized');\n }\n\n if (this.decoder.state !== 'configured') {\n console.error('[VideoChunkDecoder] Decoder in unexpected state:', this.decoder.state);\n throw new Error(`Decoder not configured, state: ${this.decoder.state}`);\n }\n\n // Track GOP boundaries\n if (chunk.type === 'key') {\n this.handleKeyFrame(chunk.timestamp);\n }\n\n // Check decoder queue pressure\n if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {\n await new Promise<void>((resolve) => {\n const check = () => {\n if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {\n resolve();\n } else {\n setTimeout(check, 20);\n }\n };\n check();\n });\n }\n\n // Decode the chunk\n try {\n this.decode(chunk);\n } catch (error) {\n console.error(`[VideoChunkDecoder] decode error:`, error);\n throw error; // Re-throw to propagate\n }\n }\n\n // Override handleOutput to attach GOP metadata\n protected override handleOutput(frame: VideoFrame): void {\n // Wrap frame with GOP metadata as a plain object\n // The frame itself will be transferred, but metadata travels separately\n const wrappedFrame = {\n frame,\n gopSerial: this.currentGopSerial,\n isKeyframe: this.isCurrentFrameKeyframe,\n timestamp: frame.timestamp,\n };\n\n // Reset keyframe flag for subsequent frames\n this.isCurrentFrameKeyframe = false;\n\n // Call parent to enqueue wrapped frame\n super.handleOutput(wrappedFrame as any);\n }\n\n // Implement abstract methods\n protected async isConfigSupported(config: VideoDecoderConfig): Promise<{ supported: boolean }> {\n const result = await VideoDecoder.isConfigSupported({\n codec: config.codec,\n codedWidth: config.width,\n codedHeight: config.height,\n });\n return { supported: result.supported ?? false };\n }\n\n protected createDecoder(init: {\n output: (frame: VideoFrame) => void;\n error: (error: DOMException) => void;\n }): VideoDecoder {\n return new VideoDecoder(init);\n }\n\n protected getDecoderType(): string {\n return 'Video';\n }\n\n protected async configureDecoder(config: VideoDecoderConfig): Promise<void> {\n if (!this.decoder) return;\n\n const decoderConfig = {\n codec: config.codec,\n codedWidth: config.width,\n codedHeight: config.height,\n hardwareAcceleration: config.hardwareAcceleration || 'no-preference',\n optimizeForLatency: false,\n ...(config.description && { description: config.description }),\n ...(config.displayAspectWidth && { displayAspectWidth: config.displayAspectWidth }),\n ...(config.displayAspectHeight && { displayAspectHeight: config.displayAspectHeight }),\n };\n\n this.decoder.configure(decoderConfig as any);\n // console.log('[VideoChunkDecoder] Decoder configured, state:', this.decoder.state);\n }\n\n protected decode(chunk: EncodedVideoChunk): void {\n this.decoder?.decode(chunk);\n }\n\n // Override reset to clear GOP data\n protected override onReset(): void {\n this.currentGopSerial = -1;\n this.isCurrentFrameKeyframe = false;\n }\n\n // GOP management methods\n private handleKeyFrame(timestamp: number): void {\n // Use timestamp as gopSerial for idempotency (same keyframe = same serial)\n this.currentGopSerial = timestamp;\n this.isCurrentFrameKeyframe = true;\n }\n\n /**\n * Configure the decoder with codec info (can be called after creation)\n */\n async configure(config: VideoDecoderConfig): Promise<void> {\n // console.log('[VideoChunkDecoder] Configuring with:', config);\n\n if (this.isReady) {\n // If already configured, reconfigure\n await this.reconfigure(config);\n return;\n }\n\n this.config = config as any;\n\n // Initialize decoder with new config\n await this.initialize();\n }\n\n /**\n * Process any buffered chunks after configuration\n */\n async processBufferedChunks(): Promise<void> {\n if (!this.isReady || this.bufferedChunks.length === 0) {\n return;\n }\n\n // console.log('[VideoChunkDecoder] Processing', this.bufferedChunks.length, 'buffered chunks');\n this.isProcessingBuffer = true;\n\n // Process buffered chunks in order\n const chunks = [...this.bufferedChunks];\n this.bufferedChunks = [];\n\n for (const chunk of chunks) {\n try {\n await this.processChunk(chunk);\n } catch (error) {\n console.error('[VideoChunkDecoder] Error processing buffered chunk:', error);\n }\n }\n\n this.isProcessingBuffer = false;\n\n // Process any new chunks that arrived while processing buffer\n if (this.bufferedChunks.length > 0) {\n await this.processBufferedChunks();\n }\n }\n\n // Override close to clean up GOP data\n override async close(): Promise<void> {\n // Clear buffered chunks\n this.bufferedChunks = [];\n\n // Clean up GOP data first\n this.onReset();\n\n // Call parent close\n await super.close();\n }\n}\n","import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { VideoChunkDecoder } from './VideoChunkDecoder';\nimport { VideoDecoderConfig } from './types';\n\nconst normalizeDescription = (desc?: ArrayBuffer | ArrayBufferView): ArrayBuffer | undefined => {\n if (!desc) return undefined;\n\n if (desc instanceof ArrayBuffer) return desc;\n\n const view = desc as ArrayBufferView;\n return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer;\n};\n\n/**\n * VideoDecodeWorker (Clip Local) - Decodes video for a single clip\n * Receives encoded video chunks from VideoDemuxWorker and outputs decoded frames\n *\n * Pipeline: VideoDemuxWorker → VideoDecodeWorker → VideoComposeWorker\n *\n * Features:\n * - Single clip, single VideoDecoder instance (no routing)\n * - GOP-based decoding with metadata\n * - Stream-based processing with backpressure\n * - Lifecycle tied to clip pipeline\n */\nexport class VideoDecodeWorker {\n private channel: WorkerChannel;\n private decoder: VideoChunkDecoder | null = null;\n private clipId: string = '';\n\n private defaultConfig: Partial<VideoDecoderConfig> = {};\n\n private upstreamPort: MessagePort | null = null;\n private downstreamPort: MessagePort | null = null;\n\n constructor() {\n this.channel = new WorkerChannel(self as any, {\n name: 'VideoDecodeWorker',\n timeout: 30000,\n });\n\n this.setupHandlers();\n }\n\n private setupHandlers(): void {\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n this.channel.registerHandler('connect', this.handleConnect.bind(this));\n this.channel.registerHandler('flush', this.handleFlush.bind(this));\n this.channel.registerHandler('reset', this.handleReset.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n }\n\n private async handleConnect(payload: {\n direction: 'upstream' | 'downstream';\n port: MessagePort;\n streamType?: 'video';\n sessionId?: string;\n clipStartUs?: number;\n clipDurationUs?: number;\n }): Promise<{ success: boolean }> {\n const { port, direction, sessionId } = payload;\n\n if (direction === 'upstream') {\n this.upstreamPort = port;\n this.clipId = sessionId || 'default';\n\n const channel = new WorkerChannel(port, {\n name: 'Demux-VideoDecode',\n timeout: 30000,\n });\n\n channel.receiveStream((stream, metadata) => {\n this.handleReceiveStream(stream, {\n ...metadata,\n clipStartUs: payload.clipStartUs,\n clipDurationUs: payload.clipDurationUs,\n });\n });\n\n channel.registerHandler('configure', this.handleConfigure.bind(this));\n }\n\n if (direction === 'downstream') {\n this.downstreamPort = port;\n }\n\n return { success: true };\n }\n\n private async handleConfigure(payload: {\n config?: { video?: Partial<VideoDecoderConfig> };\n sessionId?: string;\n streamType?: 'video';\n codec?: string;\n width?: number;\n height?: number;\n description?: ArrayBuffer | Uint8Array;\n }): Promise<{ success: boolean }> {\n const { sessionId, streamType, codec, width, height, description, config } = payload;\n\n if (sessionId && streamType === 'video') {\n if (this.decoder) {\n await this.decoder.updateConfig({\n codec,\n width,\n height,\n description: normalizeDescription(description),\n });\n }\n return { success: true };\n }\n\n if (config?.video) {\n Object.assign(this.defaultConfig, config.video);\n\n if (this.decoder) {\n await this.decoder.updateConfig(config.video);\n }\n }\n\n this.channel.state = WorkerState.Ready;\n\n return { success: true };\n }\n\n private async handleReceiveStream(\n stream: ReadableStream,\n metadata?: Record<string, any>\n ): Promise<void> {\n const sessionId = metadata?.sessionId || this.clipId;\n\n if (!this.decoder) {\n this.decoder = new VideoChunkDecoder(sessionId, {\n ...this.defaultConfig,\n codec: metadata?.codec,\n width: metadata?.width,\n height: metadata?.height,\n description: normalizeDescription(metadata?.description),\n });\n }\n\n const transform = this.decoder.createStream();\n\n if (this.downstreamPort) {\n const channel = new WorkerChannel(this.downstreamPort, {\n name: 'VideoDecode-Compose',\n timeout: 30000,\n });\n\n channel.sendStream(transform.readable as ReadableStream, {\n streamType: 'video',\n sessionId,\n });\n\n stream\n .pipeTo(transform.writable)\n .catch((error) =>\n console.error('[VideoDecodeWorker] Video stream pipe error:', sessionId, error)\n );\n }\n }\n\n private async handleFlush(): Promise<{ success: boolean }> {\n if (this.decoder) {\n await this.decoder.flush();\n }\n return { success: true };\n }\n\n private async handleReset(): Promise<{ success: boolean }> {\n if (this.decoder) {\n await this.decoder.reset();\n }\n\n this.channel.notify('reset_complete', {\n type: 'video',\n });\n\n return { success: true };\n }\n\n private async handleGetStats(): Promise<{ video?: any }> {\n if (!this.decoder) {\n return {};\n }\n\n return {\n video: {\n clipId: this.clipId,\n configured: this.decoder.isConfigured,\n queueSize: this.decoder.queueSize,\n state: this.decoder.state,\n },\n };\n }\n\n private async handleDispose(): Promise<{ success: boolean }> {\n if (this.decoder) {\n await this.decoder.close();\n this.decoder = null;\n }\n\n this.upstreamPort?.close();\n this.upstreamPort = null;\n\n this.downstreamPort?.close();\n this.downstreamPort = null;\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n}\n\nconst worker = new VideoDecodeWorker();\n\nself.addEventListener('beforeunload', () => {\n worker['handleDispose']();\n});\n\nexport default null;\n"],"names":[],"mappings":"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"}
|