@meframe/core 0.0.39 → 0.0.41
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 +1 -0
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +2 -1
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +16 -5
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/cache/resource/MP4IndexCache.d.ts +4 -0
- package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -1
- package/dist/cache/resource/MP4IndexCache.js +6 -0
- package/dist/cache/resource/MP4IndexCache.js.map +1 -1
- package/dist/cache/resource/ResourceCache.d.ts +3 -8
- package/dist/cache/resource/ResourceCache.d.ts.map +1 -1
- package/dist/cache/resource/ResourceCache.js +26 -11
- package/dist/cache/resource/ResourceCache.js.map +1 -1
- package/dist/cache/storage/opfs/OPFSManager.d.ts +28 -4
- package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -1
- package/dist/cache/storage/opfs/OPFSManager.js +110 -4
- package/dist/cache/storage/opfs/OPFSManager.js.map +1 -1
- package/dist/cache/storage/opfs/types.d.ts +5 -0
- package/dist/cache/storage/opfs/types.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts +5 -0
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +45 -57
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +2 -1
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +3 -1
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/demux/MP4IndexParser.d.ts +1 -0
- package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -1
- package/dist/stages/demux/MP4IndexParser.js +20 -10
- package/dist/stages/demux/MP4IndexParser.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +3 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +45 -4
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +7 -1
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +40 -2
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +32 -0
- package/dist/utils/errors.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { OPFSQuotaExceededError } from "../../../utils/errors.js";
|
|
2
|
+
function isDOMException(error, name) {
|
|
3
|
+
return error instanceof Error && "name" in error && error.name === name;
|
|
4
|
+
}
|
|
1
5
|
class OPFSManager {
|
|
2
6
|
opfsRoot = null;
|
|
3
7
|
initPromise = null;
|
|
8
|
+
maxSizeBytes;
|
|
9
|
+
quotaThreshold;
|
|
10
|
+
constructor(maxSizeMB = 5120, quotaThresholdPercent = 0.85) {
|
|
11
|
+
this.maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
12
|
+
this.quotaThreshold = quotaThresholdPercent;
|
|
13
|
+
}
|
|
4
14
|
async init() {
|
|
5
15
|
if (this.initPromise) return this.initPromise;
|
|
6
16
|
this.initPromise = this.initStorage();
|
|
@@ -20,9 +30,52 @@ class OPFSManager {
|
|
|
20
30
|
return this.opfsRoot.getDirectoryHandle(dirName, { create: true });
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
|
-
* Write file
|
|
33
|
+
* Write file with automatic quota management
|
|
34
|
+
* Proactively checks quota before write; evicts old projects if threshold exceeded
|
|
24
35
|
*/
|
|
25
36
|
async writeFile(path, data) {
|
|
37
|
+
await this.ensureQuota(path.projectId, path.prefix);
|
|
38
|
+
try {
|
|
39
|
+
await this.writeFileInternal(path, data);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (isDOMException(error, "QuotaExceededError")) {
|
|
42
|
+
const evictedCount = await this.evictOldestProjects(path.projectId, path.prefix, 1);
|
|
43
|
+
if (evictedCount === 0) {
|
|
44
|
+
throw new OPFSQuotaExceededError(path.projectId, path.prefix, false);
|
|
45
|
+
}
|
|
46
|
+
if (data instanceof ReadableStream) {
|
|
47
|
+
throw new OPFSQuotaExceededError(path.projectId, path.prefix, true);
|
|
48
|
+
}
|
|
49
|
+
await this.writeFileInternal(path, data);
|
|
50
|
+
} else {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Ensure quota is within threshold before write
|
|
57
|
+
* Proactively evicts old projects if usage exceeds threshold
|
|
58
|
+
*/
|
|
59
|
+
async ensureQuota(currentProjectId, prefix) {
|
|
60
|
+
const totalSize = await this.getTotalSize(prefix);
|
|
61
|
+
const usagePercent = totalSize / this.maxSizeBytes;
|
|
62
|
+
if (usagePercent > this.quotaThreshold) {
|
|
63
|
+
const needToFreeBytes = totalSize - this.maxSizeBytes * this.quotaThreshold;
|
|
64
|
+
const projectsToEvict = Math.ceil(needToFreeBytes / (totalSize / 10));
|
|
65
|
+
console.log(
|
|
66
|
+
`[OPFSManager] Quota usage ${(usagePercent * 100).toFixed(1)}% exceeds threshold ${(this.quotaThreshold * 100).toFixed(0)}%, evicting old projects`
|
|
67
|
+
);
|
|
68
|
+
await this.evictOldestProjects(currentProjectId, prefix, Math.max(1, projectsToEvict));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get total size of all projects for a prefix
|
|
73
|
+
*/
|
|
74
|
+
async getTotalSize(prefix) {
|
|
75
|
+
const projects = await this.listProjectsWithMetadata(prefix);
|
|
76
|
+
return projects.reduce((sum, p) => sum + p.size, 0);
|
|
77
|
+
}
|
|
78
|
+
async writeFileInternal(path, data) {
|
|
26
79
|
const projectDir = await this.getProjectDir(path.projectId, path.prefix);
|
|
27
80
|
const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });
|
|
28
81
|
const writable = await fileHandle.createWritable();
|
|
@@ -75,7 +128,7 @@ class OPFSManager {
|
|
|
75
128
|
const projectDir = await this.getProjectDir(path.projectId, path.prefix);
|
|
76
129
|
await projectDir.removeEntry(path.fileName);
|
|
77
130
|
} catch (error) {
|
|
78
|
-
if (error
|
|
131
|
+
if (!isDOMException(error, "NotFoundError")) {
|
|
79
132
|
console.warn(`[OPFSManager] Failed to delete file ${path.fileName}:`, error);
|
|
80
133
|
}
|
|
81
134
|
}
|
|
@@ -89,7 +142,7 @@ class OPFSManager {
|
|
|
89
142
|
await projectDir.getFileHandle(path.fileName, { create: false });
|
|
90
143
|
return true;
|
|
91
144
|
} catch (error) {
|
|
92
|
-
if (error
|
|
145
|
+
if (isDOMException(error, "NotFoundError")) {
|
|
93
146
|
return false;
|
|
94
147
|
}
|
|
95
148
|
throw error;
|
|
@@ -121,11 +174,64 @@ class OPFSManager {
|
|
|
121
174
|
const dirName = `meframe-${prefix}-${projectId}`;
|
|
122
175
|
await this.opfsRoot.removeEntry(dirName, { recursive: true });
|
|
123
176
|
} catch (error) {
|
|
124
|
-
if (error
|
|
177
|
+
if (!isDOMException(error, "NotFoundError")) {
|
|
125
178
|
console.warn(`[OPFSManager] Failed to remove directory ${prefix}-${projectId}:`, error);
|
|
126
179
|
}
|
|
127
180
|
}
|
|
128
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* List all projects with size and lastModified metadata
|
|
184
|
+
* Used for LRU eviction
|
|
185
|
+
*/
|
|
186
|
+
async listProjectsWithMetadata(prefix) {
|
|
187
|
+
if (!this.opfsRoot) {
|
|
188
|
+
throw new Error("[OPFSManager] Not initialized");
|
|
189
|
+
}
|
|
190
|
+
const projects = [];
|
|
191
|
+
const searchPrefix = `meframe-${prefix}-`;
|
|
192
|
+
for await (const [name, handle] of this.opfsRoot.entries()) {
|
|
193
|
+
if (handle.kind === "directory" && name.startsWith(searchPrefix)) {
|
|
194
|
+
const projectId = name.slice(searchPrefix.length);
|
|
195
|
+
const projectDir = handle;
|
|
196
|
+
let totalSize = 0;
|
|
197
|
+
let maxLastModified = 0;
|
|
198
|
+
for await (const [_fileName, fileHandle] of projectDir.entries()) {
|
|
199
|
+
if (fileHandle.kind === "file") {
|
|
200
|
+
const file = await fileHandle.getFile();
|
|
201
|
+
totalSize += file.size;
|
|
202
|
+
maxLastModified = Math.max(maxLastModified, file.lastModified);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (totalSize > 0) {
|
|
206
|
+
projects.push({ projectId, size: totalSize, lastModified: maxLastModified });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return projects;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Evict oldest projects (by lastModified) excluding current project
|
|
214
|
+
* Returns number of projects evicted
|
|
215
|
+
*/
|
|
216
|
+
async evictOldestProjects(currentProjectId, prefix, count) {
|
|
217
|
+
const projects = await this.listProjectsWithMetadata(prefix);
|
|
218
|
+
const candidates = projects.filter((p) => p.projectId !== currentProjectId).sort((a, b) => a.lastModified - b.lastModified).slice(0, count);
|
|
219
|
+
if (candidates.length === 0) {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
let freedBytes = 0;
|
|
223
|
+
for (const project of candidates) {
|
|
224
|
+
await this.deleteProjectDirectory(project.projectId, prefix);
|
|
225
|
+
freedBytes += project.size;
|
|
226
|
+
console.log(
|
|
227
|
+
`[OPFSManager] Evicted ${prefix} project ${project.projectId} (${(project.size / 1024 / 1024).toFixed(2)}MB, last modified: ${new Date(project.lastModified).toLocaleString()})`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
console.log(
|
|
231
|
+
`[OPFSManager] Freed ${(freedBytes / 1024 / 1024).toFixed(2)}MB by evicting ${candidates.length} old project(s)`
|
|
232
|
+
);
|
|
233
|
+
return candidates.length;
|
|
234
|
+
}
|
|
129
235
|
}
|
|
130
236
|
export {
|
|
131
237
|
OPFSManager
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OPFSManager.js","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"sourcesContent":["import type { OPFSPrefix, OPFSPath } from './types';\n\n/**\n * OPFSManager - Unified OPFS management infrastructure\n *\n * Supports:\n * - Directory isolation by prefix (l2/resource)\n * - Range reads for on-demand decoding\n * - Streaming writes\n * - File operations (exists, delete, getFileSize)\n */\nexport class OPFSManager {\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n private initPromise: Promise<void> | null = null;\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.initStorage();\n return this.initPromise;\n }\n\n private async initStorage(): Promise<void> {\n this.opfsRoot = await navigator.storage.getDirectory();\n }\n\n /**\n * Get project directory by prefix\n */\n async getProjectDir(projectId: string, prefix: OPFSPrefix): Promise<FileSystemDirectoryHandle> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const dirName = `meframe-${prefix}-${projectId}`;\n return this.opfsRoot.getDirectoryHandle(dirName, { create: true });\n }\n\n /**\n * Write file (ArrayBuffer or ReadableStream)\n */\n async writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n const writable = await fileHandle.createWritable();\n\n if (data instanceof ArrayBuffer) {\n await writable.write(data);\n } else {\n // Stream mode\n const reader = data.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n // Convert Uint8Array to ArrayBuffer for FileSystemWritableFileStream\n const buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n );\n await writable.write(buffer as ArrayBuffer);\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n await writable.close();\n }\n\n /**\n * Read entire file\n */\n async readFile(path: OPFSPath): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return await file.arrayBuffer();\n }\n\n /**\n * Read byte range from file (for on-demand decoding)\n */\n async readRange(path: OPFSPath, start: number, end: number): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n const slice = file.slice(start, end);\n return await slice.arrayBuffer();\n }\n\n /**\n * Delete file\n */\n async deleteFile(path: OPFSPath): Promise<void> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.removeEntry(path.fileName);\n } catch (error) {\n if ((error as any)?.name !== 'NotFoundError') {\n console.warn(`[OPFSManager] Failed to delete file ${path.fileName}:`, error);\n }\n }\n }\n\n /**\n * Check if file exists\n */\n async exists(path: OPFSPath): Promise<boolean> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.getFileHandle(path.fileName, { create: false });\n return true;\n } catch (error) {\n if ((error as any)?.name === 'NotFoundError') {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Get file size\n */\n async getFileSize(path: OPFSPath): Promise<number> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return file.size;\n }\n\n /**\n * Create writable stream for streaming writes\n */\n async createWritableStream(path: OPFSPath): Promise<FileSystemWritableFileStream> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n return await fileHandle.createWritable();\n }\n\n /**\n * Delete entire project directory\n */\n async deleteProjectDirectory(projectId: string, prefix: OPFSPrefix): Promise<void> {\n if (!this.opfsRoot) return;\n\n try {\n const dirName = `meframe-${prefix}-${projectId}`;\n await this.opfsRoot.removeEntry(dirName, { recursive: true });\n } catch (error) {\n if ((error as any)?.name !== 'NotFoundError') {\n console.warn(`[OPFSManager] Failed to remove directory ${prefix}-${projectId}:`, error);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OPFSManager.js","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"sourcesContent":["import type { OPFSPrefix, OPFSPath, ProjectMetadata } from './types';\nimport { OPFSQuotaExceededError } from '../../../utils/errors';\n\nfunction isDOMException(error: unknown, name: string): boolean {\n return error instanceof Error && 'name' in error && error.name === name;\n}\n\n/**\n * OPFSManager - Unified OPFS management infrastructure\n *\n * Supports:\n * - Directory isolation by prefix (l2/resource)\n * - Range reads for on-demand decoding\n * - Streaming writes with automatic quota management\n * - Project-level LRU eviction\n */\nexport class OPFSManager {\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n private initPromise: Promise<void> | null = null;\n readonly maxSizeBytes: number;\n readonly quotaThreshold: number;\n\n constructor(maxSizeMB: number = 5120, quotaThresholdPercent: number = 0.85) {\n this.maxSizeBytes = maxSizeMB * 1024 * 1024;\n this.quotaThreshold = quotaThresholdPercent;\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.initStorage();\n return this.initPromise;\n }\n\n private async initStorage(): Promise<void> {\n this.opfsRoot = await navigator.storage.getDirectory();\n }\n\n /**\n * Get project directory by prefix\n */\n async getProjectDir(projectId: string, prefix: OPFSPrefix): Promise<FileSystemDirectoryHandle> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const dirName = `meframe-${prefix}-${projectId}`;\n return this.opfsRoot.getDirectoryHandle(dirName, { create: true });\n }\n\n /**\n * Write file with automatic quota management\n * Proactively checks quota before write; evicts old projects if threshold exceeded\n */\n async writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void> {\n await this.ensureQuota(path.projectId, path.prefix);\n\n try {\n await this.writeFileInternal(path, data);\n } catch (error) {\n if (isDOMException(error, 'QuotaExceededError')) {\n const evictedCount = await this.evictOldestProjects(path.projectId, path.prefix, 1);\n\n if (evictedCount === 0) {\n throw new OPFSQuotaExceededError(path.projectId, path.prefix, false);\n }\n\n if (data instanceof ReadableStream) {\n throw new OPFSQuotaExceededError(path.projectId, path.prefix, true);\n }\n\n await this.writeFileInternal(path, data);\n } else {\n throw error;\n }\n }\n }\n\n /**\n * Ensure quota is within threshold before write\n * Proactively evicts old projects if usage exceeds threshold\n */\n private async ensureQuota(currentProjectId: string, prefix: OPFSPrefix): Promise<void> {\n const totalSize = await this.getTotalSize(prefix);\n const usagePercent = totalSize / this.maxSizeBytes;\n\n if (usagePercent > this.quotaThreshold) {\n const needToFreeBytes = totalSize - this.maxSizeBytes * this.quotaThreshold;\n const projectsToEvict = Math.ceil(needToFreeBytes / (totalSize / 10));\n\n console.log(\n `[OPFSManager] Quota usage ${(usagePercent * 100).toFixed(1)}% exceeds threshold ${(this.quotaThreshold * 100).toFixed(0)}%, evicting old projects`\n );\n\n await this.evictOldestProjects(currentProjectId, prefix, Math.max(1, projectsToEvict));\n }\n }\n\n /**\n * Get total size of all projects for a prefix\n */\n async getTotalSize(prefix: OPFSPrefix): Promise<number> {\n const projects = await this.listProjectsWithMetadata(prefix);\n return projects.reduce((sum, p) => sum + p.size, 0);\n }\n\n private async writeFileInternal(\n path: OPFSPath,\n data: ArrayBuffer | ReadableStream<Uint8Array>\n ): Promise<void> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n const writable = await fileHandle.createWritable();\n\n if (data instanceof ArrayBuffer) {\n await writable.write(data);\n } else {\n const reader = data.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n );\n await writable.write(buffer as ArrayBuffer);\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n await writable.close();\n }\n\n /**\n * Read entire file\n */\n async readFile(path: OPFSPath): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return await file.arrayBuffer();\n }\n\n /**\n * Read byte range from file (for on-demand decoding)\n */\n async readRange(path: OPFSPath, start: number, end: number): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n const slice = file.slice(start, end);\n return await slice.arrayBuffer();\n }\n\n /**\n * Delete file\n */\n async deleteFile(path: OPFSPath): Promise<void> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.removeEntry(path.fileName);\n } catch (error) {\n if (!isDOMException(error, 'NotFoundError')) {\n console.warn(`[OPFSManager] Failed to delete file ${path.fileName}:`, error);\n }\n }\n }\n\n /**\n * Check if file exists\n */\n async exists(path: OPFSPath): Promise<boolean> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.getFileHandle(path.fileName, { create: false });\n return true;\n } catch (error) {\n if (isDOMException(error, 'NotFoundError')) {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Get file size\n */\n async getFileSize(path: OPFSPath): Promise<number> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return file.size;\n }\n\n /**\n * Create writable stream for streaming writes\n */\n async createWritableStream(path: OPFSPath): Promise<FileSystemWritableFileStream> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n return await fileHandle.createWritable();\n }\n\n /**\n * Delete entire project directory\n */\n async deleteProjectDirectory(projectId: string, prefix: OPFSPrefix): Promise<void> {\n if (!this.opfsRoot) return;\n\n try {\n const dirName = `meframe-${prefix}-${projectId}`;\n await this.opfsRoot.removeEntry(dirName, { recursive: true });\n } catch (error) {\n if (!isDOMException(error, 'NotFoundError')) {\n console.warn(`[OPFSManager] Failed to remove directory ${prefix}-${projectId}:`, error);\n }\n }\n }\n\n /**\n * List all projects with size and lastModified metadata\n * Used for LRU eviction\n */\n async listProjectsWithMetadata(prefix: OPFSPrefix): Promise<ProjectMetadata[]> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const projects: ProjectMetadata[] = [];\n const searchPrefix = `meframe-${prefix}-`;\n\n // @ts-expect-error - AsyncIterator type not well-supported\n for await (const [name, handle] of this.opfsRoot.entries()) {\n if (handle.kind === 'directory' && name.startsWith(searchPrefix)) {\n const projectId = name.slice(searchPrefix.length);\n const projectDir = handle as FileSystemDirectoryHandle;\n let totalSize = 0;\n let maxLastModified = 0;\n\n // @ts-expect-error - AsyncIterator type not well-supported\n for await (const [_fileName, fileHandle] of projectDir.entries()) {\n if (fileHandle.kind === 'file') {\n const file = await (fileHandle as FileSystemFileHandle).getFile();\n totalSize += file.size;\n maxLastModified = Math.max(maxLastModified, file.lastModified);\n }\n }\n\n if (totalSize > 0) {\n projects.push({ projectId, size: totalSize, lastModified: maxLastModified });\n }\n }\n }\n\n return projects;\n }\n\n /**\n * Evict oldest projects (by lastModified) excluding current project\n * Returns number of projects evicted\n */\n async evictOldestProjects(\n currentProjectId: string,\n prefix: OPFSPrefix,\n count: number\n ): Promise<number> {\n const projects = await this.listProjectsWithMetadata(prefix);\n const candidates = projects\n .filter((p) => p.projectId !== currentProjectId)\n .sort((a, b) => a.lastModified - b.lastModified)\n .slice(0, count);\n\n if (candidates.length === 0) {\n return 0;\n }\n\n let freedBytes = 0;\n\n for (const project of candidates) {\n await this.deleteProjectDirectory(project.projectId, prefix);\n freedBytes += project.size;\n\n console.log(\n `[OPFSManager] Evicted ${prefix} project ${project.projectId} ` +\n `(${(project.size / 1024 / 1024).toFixed(2)}MB, ` +\n `last modified: ${new Date(project.lastModified).toLocaleString()})`\n );\n }\n\n console.log(\n `[OPFSManager] Freed ${(freedBytes / 1024 / 1024).toFixed(2)}MB ` +\n `by evicting ${candidates.length} old project(s)`\n );\n\n return candidates.length;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cache/storage/opfs/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,UAAU,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;CACvB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cache/storage/opfs/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,UAAU,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB"}
|
package/dist/config/defaults.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.js","sources":["../../src/config/defaults.ts"],"sourcesContent":["import type { ResolvedConfig } from './types';\nimport { CANVAS_PRESETS } from './presets';\n\n/**\n * Detect if running in development mode\n * In dev mode, workers are loaded from source (Vite dev server)\n * In prod mode, workers are loaded from dist\n */\nconst isDev = import.meta.env?.DEV ?? false;\n\n/**\n * Default configuration values based on 1080p30 memory budget\n *\n * Note: Canvas dimensions are fixed for the entire project.\n * All video clips will be scaled/fitted to this resolution.\n * Choose based on your primary content type:\n * - 720×1280: Mobile vertical video\n * - 1080×1920: HD vertical video (TikTok, Instagram Reels) ← default\n * - 1920×1080: HD horizontal video (YouTube)\n */\nexport const DEFAULT_CONFIG: ResolvedConfig = {\n global: {\n projectId: `project-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n logLevel: 'info',\n enablePerfMonitor: false,\n defaultCanvasWidth: CANVAS_PRESETS.HD_PORTRAIT.width,\n defaultCanvasHeight: CANVAS_PRESETS.HD_PORTRAIT.height,\n defaultFps: 30,\n workerPath: isDev ? '/src' : '/meframe-workers',\n workerExtension: isDev ? '.ts' : '.js',\n },\n\n load: {\n maxConcurrent:
|
|
1
|
+
{"version":3,"file":"defaults.js","sources":["../../src/config/defaults.ts"],"sourcesContent":["import type { ResolvedConfig } from './types';\nimport { CANVAS_PRESETS } from './presets';\n\n/**\n * Detect if running in development mode\n * In dev mode, workers are loaded from source (Vite dev server)\n * In prod mode, workers are loaded from dist\n */\nconst isDev = import.meta.env?.DEV ?? false;\n\n/**\n * Default configuration values based on 1080p30 memory budget\n *\n * Note: Canvas dimensions are fixed for the entire project.\n * All video clips will be scaled/fitted to this resolution.\n * Choose based on your primary content type:\n * - 720×1280: Mobile vertical video\n * - 1080×1920: HD vertical video (TikTok, Instagram Reels) ← default\n * - 1920×1080: HD horizontal video (YouTube)\n */\nexport const DEFAULT_CONFIG: ResolvedConfig = {\n global: {\n projectId: `project-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n logLevel: 'info',\n enablePerfMonitor: false,\n defaultCanvasWidth: CANVAS_PRESETS.HD_PORTRAIT.width,\n defaultCanvasHeight: CANVAS_PRESETS.HD_PORTRAIT.height,\n defaultFps: 30,\n workerPath: isDev ? '/src' : '/meframe-workers',\n workerExtension: isDev ? '.ts' : '.js',\n },\n\n load: {\n maxConcurrent: 3,\n backpressure: {\n highWaterMark: 64 * 1024, // 64 KB\n stallTimeoutMs: 500,\n },\n retry: {\n maxAttempts: 3,\n baseDelayMs: 500,\n },\n window: {\n maxInflightPerClip: 1,\n maxInflight: 4,\n chunkSize: 1 * 1024 * 1024, // 1 MB\n },\n },\n\n demux: {\n backpressure: {\n highWaterMark: 10, // 10 EncodedChunks\n },\n },\n\n decode: {\n video: {\n backpressure: {\n highWaterMark: 4, // 4 EncodedVideoChunks\n decodeQueueThreshold: 16, // Pause when decoder has 16+ chunks\n },\n maxGOPs: 4, // Cache 4 GOPs for seeking\n },\n audio: {\n backpressure: {\n highWaterMark: 20, // 20 EncodedAudioChunks\n },\n },\n },\n\n compose: {\n visual: {},\n audio: {\n enableDucking: false, // Default: no ducking\n },\n },\n\n encode: {\n video: {},\n audio: {},\n },\n\n cache: {\n l1: {\n windowSizeUs: 6_000_000, // ±3 seconds window\n maxMemoryMB: 200,\n },\n l2: {\n autoFill: true,\n fillPriority: 'low' as const,\n },\n opfs: {\n enabled: true,\n resource: {\n maxSizeMB: 5120, // 5GB for original resources\n evictionPolicy: 'lru' as const,\n },\n l2: {\n maxSizeMB: 2048, // 2GB for encoded chunks\n },\n },\n },\n\n mux: {},\n};\n\n/**\n * Tuning presets for common scenarios\n */\nexport const TUNING_PRESETS = {\n /**\n * Low-latency live streaming\n * Minimize buffering for real-time playback\n */\n lowLatency: {\n global: {\n logLevel: 'info' as const,\n },\n load: {\n backpressure: {\n highWaterMark: 16 * 1024, // 16 KB\n },\n },\n demux: {\n backpressure: {\n highWaterMark: 3,\n },\n },\n decode: {\n video: {\n backpressure: {\n highWaterMark: 2,\n decodeQueueThreshold: 4,\n },\n },\n audio: {\n backpressure: {\n highWaterMark: 10,\n },\n },\n },\n },\n\n /**\n * 4K60 playback/export\n * Larger buffers for high bitrate content\n */\n highQuality: {\n global: {\n logLevel: 'info' as const,\n },\n load: {\n backpressure: {\n highWaterMark: 256 * 1024, // 256 KB\n },\n retry: {\n maxAttempts: 3,\n baseDelayMs: 500,\n },\n },\n demux: {\n backpressure: {\n highWaterMark: 20,\n },\n },\n decode: {\n video: {\n backpressure: {\n highWaterMark: 8,\n decodeQueueThreshold: 24,\n },\n codecHints: ['h264', 'hevc'] as ('h264' | 'hevc')[],\n },\n audio: {\n backpressure: {\n highWaterMark: 30,\n },\n },\n },\n cache: {\n l2: {\n quotaGb: 1,\n },\n },\n },\n\n /**\n * Batch offline transcode\n * Maximum throughput, memory not a concern\n */\n offline: {\n global: {\n logLevel: 'warn' as const,\n },\n load: {\n backpressure: {\n highWaterMark: 512 * 1024, // 512 KB\n },\n },\n demux: {\n backpressure: {\n highWaterMark: 25,\n },\n },\n decode: {\n video: {\n backpressure: {\n highWaterMark: 12,\n decodeQueueThreshold: 32,\n },\n },\n audio: {\n backpressure: {\n highWaterMark: 40,\n },\n },\n },\n },\n} as const;\n"],"names":[],"mappings":"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"}
|
|
@@ -71,6 +71,11 @@ export declare class GlobalAudioSession {
|
|
|
71
71
|
private startExportEncoderReader;
|
|
72
72
|
finalizeExportAudio(): Promise<void>;
|
|
73
73
|
private stopAllAudioSources;
|
|
74
|
+
/**
|
|
75
|
+
* Core method to ensure audio for all clips in a time range
|
|
76
|
+
* Unified implementation used by ensureAudioForTime, scheduleAudio, and export
|
|
77
|
+
*/
|
|
78
|
+
private ensureAudioForTimeRange;
|
|
74
79
|
/**
|
|
75
80
|
* Ensure audio window for a clip (aligned with video architecture)
|
|
76
81
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalAudioSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"GlobalAudioSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { OfflineAudioMixer } from "../stages/compose/OfflineAudioMixer.js";
|
|
|
2
2
|
import { MeframeEvent } from "../event/events.js";
|
|
3
3
|
import { AudioChunkEncoder } from "../stages/encode/AudioChunkEncoder.js";
|
|
4
4
|
import { AudioChunkDecoder } from "../stages/decode/AudioChunkDecoder.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isAudioClip, hasResourceId } from "../model/types.js";
|
|
6
6
|
class GlobalAudioSession {
|
|
7
7
|
mixer;
|
|
8
8
|
activeClips = /* @__PURE__ */ new Set();
|
|
@@ -28,7 +28,6 @@ class GlobalAudioSession {
|
|
|
28
28
|
}
|
|
29
29
|
setModel(model) {
|
|
30
30
|
this.model = model;
|
|
31
|
-
void this.activateAllAudioClips();
|
|
32
31
|
}
|
|
33
32
|
onAudioData(message) {
|
|
34
33
|
const { sessionId, audioData, clipStartUs, clipDurationUs } = message;
|
|
@@ -36,41 +35,11 @@ class GlobalAudioSession {
|
|
|
36
35
|
this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs, globalTimeUs);
|
|
37
36
|
}
|
|
38
37
|
async ensureAudioForTime(timeUs, options) {
|
|
39
|
-
|
|
40
|
-
if (!model) return;
|
|
38
|
+
if (!this.model) return;
|
|
41
39
|
const immediate = options?.immediate ?? false;
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const clips = model.getClipsAtTime(timeUs, track.id);
|
|
46
|
-
activeClips.push(...clips);
|
|
47
|
-
}
|
|
48
|
-
const ensureAudioWindowPromises = activeClips.map(async (clip) => {
|
|
49
|
-
if (!hasResourceId(clip)) return;
|
|
50
|
-
const resource = model.getResource(clip.resourceId);
|
|
51
|
-
if (!resource) return;
|
|
52
|
-
if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {
|
|
53
|
-
const resource2 = model.getResource(clip.resourceId);
|
|
54
|
-
if (resource2?.state === "ready") {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
await this.deps.resourceLoader.load(clip.resourceId, {
|
|
58
|
-
isPreload: false,
|
|
59
|
-
clipId: clip.id,
|
|
60
|
-
trackId: clip.trackId
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
const clipRelativeTimeUs = timeUs - clip.startUs;
|
|
64
|
-
const WINDOW_DURATION = 3e6;
|
|
65
|
-
const startUs = clipRelativeTimeUs;
|
|
66
|
-
const endUs = Math.min(clip.durationUs, startUs + WINDOW_DURATION);
|
|
67
|
-
await this.ensureAudioWindow(clip.id, startUs, endUs);
|
|
68
|
-
});
|
|
69
|
-
if (immediate) {
|
|
70
|
-
void Promise.all(ensureAudioWindowPromises);
|
|
71
|
-
} else {
|
|
72
|
-
await Promise.all(ensureAudioWindowPromises);
|
|
73
|
-
}
|
|
40
|
+
const WINDOW_DURATION = 3e6;
|
|
41
|
+
const windowEndUs = Math.min(this.model.durationUs, timeUs + WINDOW_DURATION);
|
|
42
|
+
await this.ensureAudioForTimeRange(timeUs, windowEndUs, { immediate, loadResource: true });
|
|
74
43
|
}
|
|
75
44
|
async activateAllAudioClips() {
|
|
76
45
|
const model = this.model;
|
|
@@ -145,6 +114,10 @@ class GlobalAudioSession {
|
|
|
145
114
|
break;
|
|
146
115
|
}
|
|
147
116
|
try {
|
|
117
|
+
await this.ensureAudioForTimeRange(startUs, endUs, {
|
|
118
|
+
immediate: false,
|
|
119
|
+
loadResource: true
|
|
120
|
+
});
|
|
148
121
|
const mixedBuffer = await this.mixer.mix(startUs, endUs);
|
|
149
122
|
const source = audioContext.createBufferSource();
|
|
150
123
|
source.buffer = mixedBuffer;
|
|
@@ -213,27 +186,7 @@ class GlobalAudioSession {
|
|
|
213
186
|
* Decodes from AudioSampleCache (replaces Worker pipeline)
|
|
214
187
|
*/
|
|
215
188
|
async ensureAudioForSegment(startUs, endUs) {
|
|
216
|
-
|
|
217
|
-
if (!model) return;
|
|
218
|
-
const clips = [];
|
|
219
|
-
for (const track of model.tracks) {
|
|
220
|
-
if (track.kind !== "audio" && track.kind !== "video") continue;
|
|
221
|
-
for (const clip of track.clips) {
|
|
222
|
-
const clipEndUs = clip.startUs + clip.durationUs;
|
|
223
|
-
if (clip.startUs < endUs && clipEndUs > startUs) {
|
|
224
|
-
clips.push(clip);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
for (const clip of clips) {
|
|
229
|
-
if (!hasResourceId(clip)) continue;
|
|
230
|
-
if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);
|
|
234
|
-
const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);
|
|
235
|
-
await this.ensureAudioWindow(clip.id, clipRelativeStartUs, clipRelativeEndUs);
|
|
236
|
-
}
|
|
189
|
+
await this.ensureAudioForTimeRange(startUs, endUs, { immediate: false, loadResource: false });
|
|
237
190
|
}
|
|
238
191
|
exportEncoder = null;
|
|
239
192
|
exportEncoderStream = null;
|
|
@@ -270,6 +223,41 @@ class GlobalAudioSession {
|
|
|
270
223
|
}
|
|
271
224
|
this.scheduledSources.clear();
|
|
272
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Core method to ensure audio for all clips in a time range
|
|
228
|
+
* Unified implementation used by ensureAudioForTime, scheduleAudio, and export
|
|
229
|
+
*/
|
|
230
|
+
async ensureAudioForTimeRange(startUs, endUs, options) {
|
|
231
|
+
const model = this.model;
|
|
232
|
+
if (!model) return;
|
|
233
|
+
const { immediate = false, loadResource = true } = options;
|
|
234
|
+
const activeClips = model.getActiveClips(startUs, endUs);
|
|
235
|
+
const ensurePromises = activeClips.map(async (clip) => {
|
|
236
|
+
if (clip.trackKind !== "audio" && clip.trackKind !== "video") return;
|
|
237
|
+
if (!hasResourceId(clip)) return;
|
|
238
|
+
if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {
|
|
239
|
+
if (!loadResource) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const resource = model.getResource(clip.resourceId);
|
|
243
|
+
if (resource?.state !== "ready") {
|
|
244
|
+
await this.deps.resourceLoader.load(clip.resourceId, {
|
|
245
|
+
isPreload: false,
|
|
246
|
+
clipId: clip.id,
|
|
247
|
+
trackId: clip.trackId
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);
|
|
252
|
+
const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);
|
|
253
|
+
await this.ensureAudioWindow(clip.id, clipRelativeStartUs, clipRelativeEndUs);
|
|
254
|
+
});
|
|
255
|
+
if (immediate) {
|
|
256
|
+
void Promise.all(ensurePromises);
|
|
257
|
+
} else {
|
|
258
|
+
await Promise.all(ensurePromises);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
273
261
|
/**
|
|
274
262
|
* Ensure audio window for a clip (aligned with video architecture)
|
|
275
263
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalAudioSession.js","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { OfflineAudioMixer } from '../stages/compose/OfflineAudioMixer';\nimport type { CompositionModel, Clip } from '../model';\nimport type { WorkerPool } from '../worker/WorkerPool';\nimport type { ResourceLoader } from '../stages/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 '../stages/encode/AudioChunkEncoder';\nimport { AudioChunkDecoder } from '../stages/decode/AudioChunkDecoder';\nimport { isAudioClip, hasResourceId } 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 workerPool: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private deps: AudioSessionDeps;\n private model: CompositionModel | null = null;\n private audioContext: AudioContext | null = null;\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n // Lookahead scheduling state\n private nextScheduleTime = 0; // Next AudioContext time to schedule\n private nextContentTimeUs = 0; // Next timeline position (Us)\n private scheduledSources = new Set<AudioBufferSourceNode>();\n private readonly LOOKAHEAD_TIME = 0.2; // 200ms lookahead\n private readonly CHUNK_DURATION = 0.1; // 100ms chunks\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, () => this.model);\n }\n\n setModel(model: CompositionModel): void {\n this.model = model;\n void this.activateAllAudioClips();\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipStartUs, clipDurationUs } = message;\n const globalTimeUs = clipStartUs + (audioData.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs, globalTimeUs);\n }\n\n async ensureAudioForTime(timeUs: TimeUs, options?: { immediate?: boolean }): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const immediate = options?.immediate ?? false;\n\n // 1. Find all audio/video clips active at this time\n const activeClips: Clip[] = [];\n for (const track of model.tracks) {\n if (track.kind !== 'audio' && track.kind !== 'video') continue;\n const clips = model.getClipsAtTime(timeUs, track.id);\n activeClips.push(...clips);\n }\n\n // 2. Ensure audio window for each clip (using window-based strategy)\n const ensureAudioWindowPromises = activeClips.map(async (clip) => {\n if (!hasResourceId(clip)) return;\n\n const resource = model.getResource(clip.resourceId);\n if (!resource) return;\n\n // All audio (both audio clips and video audio) use AudioSampleCache → window decode\n // Check if resource samples are cached\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Check if resource is already loaded (but has no audio track)\n const resource = model.getResource(clip.resourceId);\n if (resource?.state === 'ready') {\n // Resource loaded but no audio track - skip this clip (video files may not have audio)\n return;\n }\n\n // Resource not yet loaded\n // Blocking: wait for resource to load\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n }\n\n const clipRelativeTimeUs = timeUs - clip.startUs;\n const WINDOW_DURATION = 3_000_000;\n const startUs = clipRelativeTimeUs;\n const endUs = Math.min(clip.durationUs, startUs + WINDOW_DURATION);\n // Now decode window from AudioSampleCache\n await this.ensureAudioWindow(clip.id, startUs, endUs);\n });\n\n if (immediate) {\n void Promise.all(ensureAudioWindowPromises);\n } else {\n await Promise.all(ensureAudioWindowPromises);\n }\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.model;\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n if (audioTracks.length === 0) return;\n\n // Find maximum clip count across all audio tracks\n const maxClipCount = Math.max(...audioTracks.map((track) => track.clips.length));\n\n // Horizontal loading: activate clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of audioTracks) {\n const clip = track.clips[clipIndex];\n if (!clip || this.activeClips.has(clip.id)) continue;\n\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // Preview: Use main-thread parsing → AudioSampleCache → on-demand decode\n // Check if we have cached samples (already parsed in ResourceLoader)\n if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Already parsed, mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n continue;\n }\n\n // Ensure resource is loaded (will be parsed and cached in main thread)\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: track.id,\n });\n\n // Mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n\n async deactivateClip(clipId: string): Promise<void> {\n if (!this.activeClips.has(clipId)) {\n return;\n }\n\n this.activeClips.delete(clipId);\n this.deps.cacheManager.clearClipAudioData(clipId);\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 // Ensure audio is decoded and ready (blocking mode for startup)\n await this.ensureAudioForTime(timeUs, { immediate: false });\n\n this.isPlaying = true;\n // Reset playback states when starting to initialize scheduling from current time\n this.resetPlaybackStates();\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllAudioSources();\n }\n\n updateTime(_timeUs: TimeUs): void {\n // Kept for compatibility\n }\n\n /**\n * Schedule audio chunks ahead of playback cursor\n * Uses OfflineAudioMixer for proper mixing, then plays the result\n */\n async scheduleAudio(currentTimelineUs: TimeUs, audioContext: AudioContext): Promise<void> {\n if (!this.isPlaying || !this.model || !this.audioContext) {\n return;\n }\n\n const lookaheadTime = audioContext.currentTime + this.LOOKAHEAD_TIME;\n\n // Initialize on first call\n if (this.nextScheduleTime === 0) {\n this.nextScheduleTime = audioContext.currentTime + 0.01;\n this.nextContentTimeUs = currentTimelineUs;\n }\n\n // Schedule chunks until we reach lookahead limit\n while (this.nextScheduleTime < lookaheadTime) {\n const chunkDurationUs = Math.round(this.CHUNK_DURATION * 1_000_000);\n const startUs = this.nextContentTimeUs;\n const endUs = startUs + chunkDurationUs;\n\n // Check if we need audio for this time range\n if (endUs > this.model.durationUs) {\n break; // Reached end of composition\n }\n\n try {\n // Mix audio using OfflineAudioMixer (handles resampling + mixing)\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\n\n // Create source and play\n const source = audioContext.createBufferSource();\n source.buffer = mixedBuffer;\n source.playbackRate.value = this.playbackRate;\n\n const gainNode = audioContext.createGain();\n gainNode.gain.value = this.volume;\n\n source.connect(gainNode);\n gainNode.connect(audioContext.destination);\n\n source.start(this.nextScheduleTime);\n this.scheduledSources.add(source);\n\n source.onended = () => {\n source.disconnect();\n gainNode.disconnect();\n this.scheduledSources.delete(source);\n };\n\n // Advance scheduling state\n const actualDuration = mixedBuffer.duration;\n this.nextScheduleTime += actualDuration;\n this.nextContentTimeUs += chunkDurationUs;\n } catch (error) {\n console.warn('[GlobalAudioSession] Mix error, skipping chunk:', error);\n // Skip this chunk and continue\n this.nextScheduleTime += this.CHUNK_DURATION;\n this.nextContentTimeUs += chunkDurationUs;\n }\n }\n }\n\n /**\n * Reset playback states (called on seek)\n */\n resetPlaybackStates(): void {\n this.stopAllAudioSources();\n this.nextScheduleTime = 0;\n this.nextContentTimeUs = 0;\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n // Note: We can't easily update volume of already scheduled sources in this lookahead model\n // without keeping track of gain nodes. For now, volume changes will apply to next chunks.\n // If immediate volume change is needed, we'd need to store GainNodes in SchedulingState.\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n // Playback rate change requires reset of scheduling to avoid pitch shift artifacts on existing nodes\n // or complicated time mapping updates.\n this.resetPlaybackStates();\n }\n\n reset(): void {\n this.stopAllAudioSources();\n this.deps.cacheManager.clearAudioCache();\n this.activeClips.clear();\n }\n\n /**\n * Mix and encode audio for a specific segment (used by ExportScheduler)\n */\n async mixAndEncodeSegment(\n startUs: TimeUs,\n endUs: TimeUs,\n onChunk: (chunk: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) => void\n ): Promise<void> {\n // Wait for audio clips in this time range to be ready (on-demand wait)\n await this.ensureAudioForSegment(startUs, endUs);\n\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, startUs);\n\n if (!audioData) return;\n\n if (!this.exportEncoder) {\n this.exportEncoder = new AudioChunkEncoder();\n await this.exportEncoder.initialize();\n this.exportEncoderStream = this.exportEncoder.createStream();\n this.exportEncoderWriter = this.exportEncoderStream.writable.getWriter();\n\n // Start reader immediately (but don't await - it's a long-running loop)\n void this.startExportEncoderReader(this.exportEncoderStream.readable, onChunk);\n\n // Wait a bit to ensure reader is ready before first write\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n\n await this.exportEncoderWriter?.write(audioData);\n }\n\n /**\n * Ensure audio clips in time range are decoded (for export)\n * Decodes from AudioSampleCache (replaces Worker pipeline)\n */\n private async ensureAudioForSegment(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n // Find all clips (audio + video with audio tracks) that overlap with [startUs, endUs]\n const clips: Clip[] = [];\n for (const track of model.tracks) {\n if (track.kind !== 'audio' && track.kind !== 'video') continue;\n\n for (const clip of track.clips) {\n const clipEndUs = clip.startUs + clip.durationUs;\n // Check if clip overlaps with segment\n if (clip.startUs < endUs && clipEndUs > startUs) {\n clips.push(clip);\n }\n }\n }\n\n // Decode audio window for each clip from AudioSampleCache\n for (const clip of clips) {\n if (!hasResourceId(clip)) continue;\n\n // Skip if no audio track in resource\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n continue;\n }\n\n // Calculate clip-relative window (decoder expects clip-relative time)\n const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);\n const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);\n\n // Decode window from AudioSampleCache (main thread)\n await this.ensureAudioWindow(clip.id, clipRelativeStartUs, clipRelativeEndUs);\n }\n }\n\n private exportEncoder: AudioChunkEncoder | null = null;\n private exportEncoderStream: TransformStream<\n AudioData,\n { chunk: EncodedAudioChunk; metadata: any }\n > | null = null;\n private exportEncoderWriter: WritableStreamDefaultWriter<AudioData> | null = null;\n\n private async startExportEncoderReader(\n stream: ReadableStream<{ chunk: EncodedAudioChunk; metadata: any }>,\n onChunk: (chunk: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) => void\n ) {\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n onChunk(value.chunk, value.metadata);\n }\n }\n } catch (e) {\n console.error('Export encoder reader error', e);\n }\n }\n\n async finalizeExportAudio(): Promise<void> {\n if (this.exportEncoderWriter) {\n await this.exportEncoderWriter.close();\n this.exportEncoderWriter = null;\n }\n this.exportEncoder = null;\n this.exportEncoderStream = null;\n }\n\n private stopAllAudioSources(): void {\n for (const source of this.scheduledSources) {\n try {\n source.stop();\n source.disconnect();\n } catch {\n // Ignore\n }\n }\n this.scheduledSources.clear();\n }\n\n /**\n * Ensure audio window for a clip (aligned with video architecture)\n *\n * Note: Unlike video getFrame(), this method doesn't need a 'preheat' parameter\n * Why: Audio cache check is window-level (range query) via hasWindowPCM()\n * It verifies the entire window has ≥80% data, not just a single point\n * This naturally prevents premature return during preheating\n */\n async ensureAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Check L1 cache - window-level verification (not point-level like video)\n if (this.deps.cacheManager.hasWindowPCM(clipId, startUs, endUs)) {\n return; // Entire window has sufficient data (≥80%)\n }\n\n await this.decodeAudioWindow(clipId, startUs, endUs);\n }\n\n /**\n * Decode audio window for a clip (aligned with video architecture)\n * Simple strategy: decode entire window range, cache handles duplicates\n */\n private async decodeAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const clip = this.model?.findClip(clipId);\n if (!clip || !hasResourceId(clip)) {\n return;\n }\n\n // Get audio samples from AudioSampleCache\n const audioRecord = this.deps.cacheManager.audioSampleCache.get(clip.resourceId);\n if (!audioRecord) {\n // Resource has no audio track (common for some video files)\n return;\n }\n\n // Filter chunks within window (aligned with video GOP filtering)\n const windowChunks = audioRecord.samples.filter((s) => {\n const sampleEndUs = s.timestamp + (s.duration ?? 0);\n return s.timestamp < endUs && sampleEndUs > startUs;\n });\n\n if (windowChunks.length === 0) {\n return;\n }\n\n // Decode chunks to AudioL1Cache (cache will handle append/merge)\n await this.decodeAudioSamples(\n clipId,\n windowChunks,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n }\n\n /**\n * Decode audio samples to PCM and cache\n * Uses AudioChunkDecoder for consistency with project architecture\n * Resamples to AudioContext sample rate if needed for better quality\n */\n private async decodeAudioSamples(\n clipId: string,\n samples: EncodedAudioChunk[],\n config: AudioDecoderConfig,\n clipDurationUs: number,\n clipStartUs: TimeUs\n ): Promise<void> {\n // Use AudioChunkDecoder for consistency with project architecture\n // Convert description to ArrayBuffer if needed for type compatibility\n let description: ArrayBuffer | undefined;\n if (config.description) {\n if (config.description instanceof ArrayBuffer) {\n description = config.description;\n } else if (ArrayBuffer.isView(config.description)) {\n // Convert TypedArray to ArrayBuffer\n const view = config.description as Uint8Array;\n // Create a new ArrayBuffer and copy data to ensure proper type\n const newBuffer = new ArrayBuffer(view.byteLength);\n new Uint8Array(newBuffer).set(\n new Uint8Array(view.buffer, view.byteOffset, view.byteLength)\n );\n description = newBuffer;\n }\n }\n\n const decoderConfig = {\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n description,\n };\n const decoder = new AudioChunkDecoder(`audio-${clipId}`, decoderConfig);\n\n try {\n // Create chunk stream\n const chunkStream = new ReadableStream<EncodedAudioChunk>({\n start(controller) {\n for (const sample of samples) {\n controller.enqueue(sample);\n }\n controller.close();\n },\n });\n\n // Decode through stream\n const audioDataStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = audioDataStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n // Store original sample rate - OfflineAudioMixer will handle resampling\n const globalTimeUs = clipStartUs + (value.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(clipId, value, clipDurationUs, globalTimeUs);\n }\n }\n } finally {\n reader.releaseLock();\n }\n } catch (error) {\n console.error(`[GlobalAudioSession] Decoder error for clip ${clipId}:`, error);\n throw error;\n } finally {\n await decoder.close();\n }\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"],"names":["resource"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GlobalAudioSession.js","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { OfflineAudioMixer } from '../stages/compose/OfflineAudioMixer';\nimport type { CompositionModel } from '../model';\nimport type { WorkerPool } from '../worker/WorkerPool';\nimport type { ResourceLoader } from '../stages/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 '../stages/encode/AudioChunkEncoder';\nimport { AudioChunkDecoder } from '../stages/decode/AudioChunkDecoder';\nimport { isAudioClip, hasResourceId } 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 workerPool: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private deps: AudioSessionDeps;\n private model: CompositionModel | null = null;\n private audioContext: AudioContext | null = null;\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n // Lookahead scheduling state\n private nextScheduleTime = 0; // Next AudioContext time to schedule\n private nextContentTimeUs = 0; // Next timeline position (Us)\n private scheduledSources = new Set<AudioBufferSourceNode>();\n private readonly LOOKAHEAD_TIME = 0.2; // 200ms lookahead\n private readonly CHUNK_DURATION = 0.1; // 100ms chunks\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, () => this.model);\n }\n\n setModel(model: CompositionModel): void {\n this.model = model;\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipStartUs, clipDurationUs } = message;\n const globalTimeUs = clipStartUs + (audioData.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs, globalTimeUs);\n }\n\n async ensureAudioForTime(timeUs: TimeUs, options?: { immediate?: boolean }): Promise<void> {\n if (!this.model) return;\n\n const immediate = options?.immediate ?? false;\n const WINDOW_DURATION = 3_000_000; // 3s preheat window\n const windowEndUs = Math.min(this.model.durationUs, timeUs + WINDOW_DURATION);\n\n await this.ensureAudioForTimeRange(timeUs, windowEndUs, { immediate, loadResource: true });\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.model;\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n if (audioTracks.length === 0) return;\n\n // Find maximum clip count across all audio tracks\n const maxClipCount = Math.max(...audioTracks.map((track) => track.clips.length));\n\n // Horizontal loading: activate clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of audioTracks) {\n const clip = track.clips[clipIndex];\n if (!clip || this.activeClips.has(clip.id)) continue;\n\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // Preview: Use main-thread parsing → AudioSampleCache → on-demand decode\n // Check if we have cached samples (already parsed in ResourceLoader)\n if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Already parsed, mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n continue;\n }\n\n // Ensure resource is loaded (will be parsed and cached in main thread)\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: track.id,\n });\n\n // Mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n\n async deactivateClip(clipId: string): Promise<void> {\n if (!this.activeClips.has(clipId)) {\n return;\n }\n\n this.activeClips.delete(clipId);\n this.deps.cacheManager.clearClipAudioData(clipId);\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 // Ensure audio is decoded and ready (blocking mode for startup)\n await this.ensureAudioForTime(timeUs, { immediate: false });\n\n this.isPlaying = true;\n // Reset playback states when starting to initialize scheduling from current time\n this.resetPlaybackStates();\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllAudioSources();\n }\n\n updateTime(_timeUs: TimeUs): void {\n // Kept for compatibility\n }\n\n /**\n * Schedule audio chunks ahead of playback cursor\n * Uses OfflineAudioMixer for proper mixing, then plays the result\n */\n async scheduleAudio(currentTimelineUs: TimeUs, audioContext: AudioContext): Promise<void> {\n if (!this.isPlaying || !this.model || !this.audioContext) {\n return;\n }\n\n const lookaheadTime = audioContext.currentTime + this.LOOKAHEAD_TIME;\n\n // Initialize on first call\n if (this.nextScheduleTime === 0) {\n this.nextScheduleTime = audioContext.currentTime + 0.01;\n this.nextContentTimeUs = currentTimelineUs;\n }\n\n // Schedule chunks until we reach lookahead limit\n while (this.nextScheduleTime < lookaheadTime) {\n const chunkDurationUs = Math.round(this.CHUNK_DURATION * 1_000_000);\n const startUs = this.nextContentTimeUs;\n const endUs = startUs + chunkDurationUs;\n\n // Check if we need audio for this time range\n if (endUs > this.model.durationUs) {\n break; // Reached end of composition\n }\n\n try {\n // Ensure audio for all clips in the mixing window (not just clips at current time point)\n // This fixes the issue where boundary clips are missed by getClipsAtTime()\n await this.ensureAudioForTimeRange(startUs, endUs, {\n immediate: false,\n loadResource: true,\n });\n\n // Mix audio using OfflineAudioMixer (handles resampling + mixing)\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\n\n // Create source and play\n const source = audioContext.createBufferSource();\n source.buffer = mixedBuffer;\n source.playbackRate.value = this.playbackRate;\n\n const gainNode = audioContext.createGain();\n gainNode.gain.value = this.volume;\n\n source.connect(gainNode);\n gainNode.connect(audioContext.destination);\n\n source.start(this.nextScheduleTime);\n this.scheduledSources.add(source);\n\n source.onended = () => {\n source.disconnect();\n gainNode.disconnect();\n this.scheduledSources.delete(source);\n };\n\n // Advance scheduling state\n const actualDuration = mixedBuffer.duration;\n this.nextScheduleTime += actualDuration;\n this.nextContentTimeUs += chunkDurationUs;\n } catch (error) {\n console.warn('[GlobalAudioSession] Mix error, skipping chunk:', error);\n // Skip this chunk and continue\n this.nextScheduleTime += this.CHUNK_DURATION;\n this.nextContentTimeUs += chunkDurationUs;\n }\n }\n }\n\n /**\n * Reset playback states (called on seek)\n */\n resetPlaybackStates(): void {\n this.stopAllAudioSources();\n this.nextScheduleTime = 0;\n this.nextContentTimeUs = 0;\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n // Note: We can't easily update volume of already scheduled sources in this lookahead model\n // without keeping track of gain nodes. For now, volume changes will apply to next chunks.\n // If immediate volume change is needed, we'd need to store GainNodes in SchedulingState.\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n // Playback rate change requires reset of scheduling to avoid pitch shift artifacts on existing nodes\n // or complicated time mapping updates.\n this.resetPlaybackStates();\n }\n\n reset(): void {\n this.stopAllAudioSources();\n this.deps.cacheManager.clearAudioCache();\n this.activeClips.clear();\n }\n\n /**\n * Mix and encode audio for a specific segment (used by ExportScheduler)\n */\n async mixAndEncodeSegment(\n startUs: TimeUs,\n endUs: TimeUs,\n onChunk: (chunk: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) => void\n ): Promise<void> {\n // Wait for audio clips in this time range to be ready (on-demand wait)\n await this.ensureAudioForSegment(startUs, endUs);\n\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, startUs);\n\n if (!audioData) return;\n\n if (!this.exportEncoder) {\n this.exportEncoder = new AudioChunkEncoder();\n await this.exportEncoder.initialize();\n this.exportEncoderStream = this.exportEncoder.createStream();\n this.exportEncoderWriter = this.exportEncoderStream.writable.getWriter();\n\n // Start reader immediately (but don't await - it's a long-running loop)\n void this.startExportEncoderReader(this.exportEncoderStream.readable, onChunk);\n\n // Wait a bit to ensure reader is ready before first write\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n\n await this.exportEncoderWriter?.write(audioData);\n }\n\n /**\n * Ensure audio clips in time range are decoded (for export)\n * Decodes from AudioSampleCache (replaces Worker pipeline)\n */\n private async ensureAudioForSegment(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Export mode: don't load resources (they should already be loaded), only decode cached samples\n await this.ensureAudioForTimeRange(startUs, endUs, { immediate: false, loadResource: false });\n }\n\n private exportEncoder: AudioChunkEncoder | null = null;\n private exportEncoderStream: TransformStream<\n AudioData,\n { chunk: EncodedAudioChunk; metadata: any }\n > | null = null;\n private exportEncoderWriter: WritableStreamDefaultWriter<AudioData> | null = null;\n\n private async startExportEncoderReader(\n stream: ReadableStream<{ chunk: EncodedAudioChunk; metadata: any }>,\n onChunk: (chunk: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) => void\n ) {\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n onChunk(value.chunk, value.metadata);\n }\n }\n } catch (e) {\n console.error('Export encoder reader error', e);\n }\n }\n\n async finalizeExportAudio(): Promise<void> {\n if (this.exportEncoderWriter) {\n await this.exportEncoderWriter.close();\n this.exportEncoderWriter = null;\n }\n this.exportEncoder = null;\n this.exportEncoderStream = null;\n }\n\n private stopAllAudioSources(): void {\n for (const source of this.scheduledSources) {\n try {\n source.stop();\n source.disconnect();\n } catch {\n // Ignore\n }\n }\n this.scheduledSources.clear();\n }\n\n /**\n * Core method to ensure audio for all clips in a time range\n * Unified implementation used by ensureAudioForTime, scheduleAudio, and export\n */\n private async ensureAudioForTimeRange(\n startUs: TimeUs,\n endUs: TimeUs,\n options: { immediate?: boolean; loadResource?: boolean }\n ): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const { immediate = false, loadResource = true } = options;\n\n // Find all clips that overlap with [startUs, endUs]\n const activeClips = model.getActiveClips(startUs, endUs);\n\n const ensurePromises = activeClips.map(async (clip) => {\n // Only process audio and video clips\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') return;\n if (!hasResourceId(clip)) return;\n\n // Ensure AudioSampleCache has data\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n if (!loadResource) {\n // Export mode: skip clips without cached samples\n return;\n }\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n // Resource not yet loaded - wait for it\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n }\n }\n\n // Calculate clip-relative time range\n const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);\n const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);\n\n // Ensure audio window for this clip-relative range\n await this.ensureAudioWindow(clip.id, clipRelativeStartUs, clipRelativeEndUs);\n });\n\n if (immediate) {\n void Promise.all(ensurePromises);\n } else {\n await Promise.all(ensurePromises);\n }\n }\n\n /**\n * Ensure audio window for a clip (aligned with video architecture)\n *\n * Note: Unlike video getFrame(), this method doesn't need a 'preheat' parameter\n * Why: Audio cache check is window-level (range query) via hasWindowPCM()\n * It verifies the entire window has ≥80% data, not just a single point\n * This naturally prevents premature return during preheating\n */\n async ensureAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Check L1 cache - window-level verification (not point-level like video)\n if (this.deps.cacheManager.hasWindowPCM(clipId, startUs, endUs)) {\n return; // Entire window has sufficient data (≥80%)\n }\n\n await this.decodeAudioWindow(clipId, startUs, endUs);\n }\n\n /**\n * Decode audio window for a clip (aligned with video architecture)\n * Simple strategy: decode entire window range, cache handles duplicates\n */\n private async decodeAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const clip = this.model?.findClip(clipId);\n if (!clip || !hasResourceId(clip)) {\n return;\n }\n\n // Get audio samples from AudioSampleCache\n const audioRecord = this.deps.cacheManager.audioSampleCache.get(clip.resourceId);\n if (!audioRecord) {\n // Resource has no audio track (common for some video files)\n return;\n }\n\n // Filter chunks within window (aligned with video GOP filtering)\n const windowChunks = audioRecord.samples.filter((s) => {\n const sampleEndUs = s.timestamp + (s.duration ?? 0);\n return s.timestamp < endUs && sampleEndUs > startUs;\n });\n\n if (windowChunks.length === 0) {\n return;\n }\n\n // Decode chunks to AudioL1Cache (cache will handle append/merge)\n await this.decodeAudioSamples(\n clipId,\n windowChunks,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n }\n\n /**\n * Decode audio samples to PCM and cache\n * Uses AudioChunkDecoder for consistency with project architecture\n * Resamples to AudioContext sample rate if needed for better quality\n */\n private async decodeAudioSamples(\n clipId: string,\n samples: EncodedAudioChunk[],\n config: AudioDecoderConfig,\n clipDurationUs: number,\n clipStartUs: TimeUs\n ): Promise<void> {\n // Use AudioChunkDecoder for consistency with project architecture\n // Convert description to ArrayBuffer if needed for type compatibility\n let description: ArrayBuffer | undefined;\n if (config.description) {\n if (config.description instanceof ArrayBuffer) {\n description = config.description;\n } else if (ArrayBuffer.isView(config.description)) {\n // Convert TypedArray to ArrayBuffer\n const view = config.description as Uint8Array;\n // Create a new ArrayBuffer and copy data to ensure proper type\n const newBuffer = new ArrayBuffer(view.byteLength);\n new Uint8Array(newBuffer).set(\n new Uint8Array(view.buffer, view.byteOffset, view.byteLength)\n );\n description = newBuffer;\n }\n }\n\n const decoderConfig = {\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n description,\n };\n const decoder = new AudioChunkDecoder(`audio-${clipId}`, decoderConfig);\n\n try {\n // Create chunk stream\n const chunkStream = new ReadableStream<EncodedAudioChunk>({\n start(controller) {\n for (const sample of samples) {\n controller.enqueue(sample);\n }\n controller.close();\n },\n });\n\n // Decode through stream\n const audioDataStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = audioDataStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n // Store original sample rate - OfflineAudioMixer will handle resampling\n const globalTimeUs = clipStartUs + (value.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(clipId, value, clipDurationUs, globalTimeUs);\n }\n }\n } finally {\n reader.releaseLock();\n }\n } catch (error) {\n console.error(`[GlobalAudioSession] Decoder error for clip ${clipId}:`, error);\n throw error;\n } finally {\n await decoder.close();\n }\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"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Orchestrator.d.ts","sourceRoot":"","sources":["../../src/orchestrator/Orchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAErF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAY,MAAM,EAAE,OAAO,EAAQ,MAAM,UAAU,CAAC;AAE/F,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,EAAE,UAAU,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IACjD,cAAc,EAAE,cAAc,CAAC;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,kBAAkB,CAAC;IACjC,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,eAAe,CAAC;IAEjC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,wBAAwB,CAAuB;IACvD,OAAO,CAAC,qBAAqB,CAAqC;IAClE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;gBAE5D,MAAM,EAAE,kBAAkB;
|
|
1
|
+
{"version":3,"file":"Orchestrator.d.ts","sourceRoot":"","sources":["../../src/orchestrator/Orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,7 +46,8 @@ class Orchestrator {
|
|
|
46
46
|
maxGOPs
|
|
47
47
|
},
|
|
48
48
|
resource: {
|
|
49
|
-
projectId: config.projectId || this.config.global.projectId || "default"
|
|
49
|
+
projectId: config.projectId || this.config.global.projectId || "default",
|
|
50
|
+
maxSizeMB: this.config.cache?.opfs?.resource?.maxSizeMB
|
|
50
51
|
}
|
|
51
52
|
},
|
|
52
53
|
this.eventBus
|