@meframe/core 0.0.40 → 0.0.42

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.
Files changed (63) hide show
  1. package/dist/Meframe.d.ts +1 -0
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +2 -2
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/cache/CacheManager.d.ts +1 -0
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +2 -1
  8. package/dist/cache/CacheManager.js.map +1 -1
  9. package/dist/cache/resource/MP4IndexCache.d.ts +4 -0
  10. package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -1
  11. package/dist/cache/resource/MP4IndexCache.js +6 -0
  12. package/dist/cache/resource/MP4IndexCache.js.map +1 -1
  13. package/dist/cache/resource/ResourceCache.d.ts +3 -8
  14. package/dist/cache/resource/ResourceCache.d.ts.map +1 -1
  15. package/dist/cache/resource/ResourceCache.js +26 -11
  16. package/dist/cache/resource/ResourceCache.js.map +1 -1
  17. package/dist/cache/storage/opfs/OPFSManager.d.ts +28 -4
  18. package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -1
  19. package/dist/cache/storage/opfs/OPFSManager.js +110 -4
  20. package/dist/cache/storage/opfs/OPFSManager.js.map +1 -1
  21. package/dist/cache/storage/opfs/types.d.ts +5 -0
  22. package/dist/cache/storage/opfs/types.d.ts.map +1 -1
  23. package/dist/config/defaults.js +1 -1
  24. package/dist/config/defaults.js.map +1 -1
  25. package/dist/controllers/PlaybackController.d.ts +0 -3
  26. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  27. package/dist/controllers/PlaybackController.js +29 -53
  28. package/dist/controllers/PlaybackController.js.map +1 -1
  29. package/dist/orchestrator/GlobalAudioSession.d.ts +5 -0
  30. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  31. package/dist/orchestrator/GlobalAudioSession.js +45 -57
  32. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  33. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  34. package/dist/orchestrator/Orchestrator.js +3 -2
  35. package/dist/orchestrator/Orchestrator.js.map +1 -1
  36. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  37. package/dist/orchestrator/VideoClipSession.js +1 -0
  38. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  39. package/dist/stages/compose/FrameRateConverter.d.ts +12 -0
  40. package/dist/stages/compose/FrameRateConverter.d.ts.map +1 -1
  41. package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
  42. package/dist/stages/compose/OfflineAudioMixer.js +3 -1
  43. package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
  44. package/dist/stages/demux/MP4IndexParser.d.ts +1 -0
  45. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -1
  46. package/dist/stages/demux/MP4IndexParser.js +20 -10
  47. package/dist/stages/demux/MP4IndexParser.js.map +1 -1
  48. package/dist/stages/load/ResourceLoader.d.ts +3 -1
  49. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  50. package/dist/stages/load/ResourceLoader.js +45 -4
  51. package/dist/stages/load/ResourceLoader.js.map +1 -1
  52. package/dist/stages/load/TaskManager.d.ts +7 -1
  53. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  54. package/dist/stages/load/TaskManager.js +40 -2
  55. package/dist/stages/load/TaskManager.js.map +1 -1
  56. package/dist/utils/errors.d.ts +27 -0
  57. package/dist/utils/errors.d.ts.map +1 -1
  58. package/dist/utils/errors.js +32 -0
  59. package/dist/utils/errors.js.map +1 -1
  60. package/dist/workers/stages/compose/{video-compose.worker.DM_bsOY8.js → video-compose.worker.Ckk8AtaQ.js} +51 -21
  61. package/dist/workers/stages/compose/{video-compose.worker.DM_bsOY8.js.map → video-compose.worker.Ckk8AtaQ.js.map} +1 -1
  62. package/dist/workers/worker-manifest.json +1 -1
  63. 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 (ArrayBuffer or ReadableStream)
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?.name !== "NotFoundError") {
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?.name === "NotFoundError") {
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?.name !== "NotFoundError") {
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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"}
@@ -13,4 +13,9 @@ export interface ChunkBatch {
13
13
  byteLength: number;
14
14
  type: 'key' | 'delta';
15
15
  }
16
+ export interface ProjectMetadata {
17
+ projectId: string;
18
+ size: number;
19
+ lastModified: number;
20
+ }
16
21
  //# sourceMappingURL=types.d.ts.map
@@ -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"}
@@ -11,7 +11,7 @@ const DEFAULT_CONFIG = {
11
11
  workerExtension: ".js"
12
12
  },
13
13
  load: {
14
- maxConcurrent: 2,
14
+ maxConcurrent: 3,
15
15
  backpressure: {
16
16
  highWaterMark: 64 * 1024,
17
17
  // 64 KB
@@ -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: 2,\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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"}
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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"}
@@ -1,5 +1,4 @@
1
1
  import { IPlaybackController, PlaybackOptions, IEventBus, PreviewHandle, TimeUs } from './types';
2
- import { GlobalAudioSession } from '../orchestrator/GlobalAudioSession';
3
2
  import { Orchestrator } from '../orchestrator';
4
3
 
5
4
  /**
@@ -43,7 +42,6 @@ export declare class PlaybackController implements IPlaybackController, PreviewH
43
42
  setLoop(loop: boolean): void;
44
43
  get duration(): TimeUs;
45
44
  get isPlaying(): boolean;
46
- setAudioSession(session: GlobalAudioSession): void;
47
45
  resume(): void;
48
46
  on(event: string, handler: (payload: any) => void): void;
49
47
  off(event: string, handler: (payload: any) => void): void;
@@ -76,6 +74,5 @@ export declare class PlaybackController implements IPlaybackController, PreviewH
76
74
  private onCacheCover;
77
75
  private onModelSet;
78
76
  private setupEventListeners;
79
- private ensureAudioContext;
80
77
  }
81
78
  //# sourceMappingURL=PlaybackController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EAEnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKpD;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB,EAAE,aAAa;IAC3E,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,aAAa,CAA8B;IAGnD,aAAa,EAAE,MAAM,CAAK;IAC1B,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,IAAI,CAAS;IAGrB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAa;IAGhC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,YAAY,CAAmC;IAGvD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,oBAAoB,CAAS;IAGrC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAa;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IAC9C,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe;IA0C/E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAKlC,IAAI,IAAI,IAAI;YAYE,aAAa;IAoC3B,KAAK,IAAI,IAAI;IAgBb,IAAI,IAAI,IAAI;IAuBN,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4FzC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAW3B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAO/B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAQ7B,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI5B,IAAI,QAAQ,IAAI,MAAM,CAOrB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAKlD,MAAM,IAAI,IAAI;IAId,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIxD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAKzD,OAAO,CAAC,YAAY;IAqEpB,OAAO,CAAC,UAAU;IAiClB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAQlB;;;;;;;OAOG;IACH,OAAO,CAAC,qBAAqB;IAqB7B;;;;OAIG;YACW,iBAAiB;IA8CzB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAiCzC,uBAAuB;IA2DrC,OAAO,CAAC,SAAS;IAKjB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY,CASlB;IAEF,OAAO,CAAC,UAAU,CAmBhB;IAEF,OAAO,CAAC,mBAAmB;YAKb,kBAAkB;CAOjC"}
1
+ {"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EAEnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAGjB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKpD;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB,EAAE,aAAa;IAC3E,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,aAAa,CAA8B;IAGnD,aAAa,EAAE,MAAM,CAAK;IAC1B,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,IAAI,CAAS;IAGrB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAa;IAGhC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAqB;IAGzC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,oBAAoB,CAAS;IAGrC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAa;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IAC9C,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe;IA4C/E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAKlC,IAAI,IAAI,IAAI;YAYE,aAAa;IAgC3B,KAAK,IAAI,IAAI;IAgBb,IAAI,IAAI,IAAI;IAuBN,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0FzC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAW3B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAO/B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAQ7B,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI5B,IAAI,QAAQ,IAAI,MAAM,CAOrB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAGD,MAAM,IAAI,IAAI;IAId,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIxD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAKzD,OAAO,CAAC,YAAY;IAiEpB,OAAO,CAAC,UAAU;IAiClB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAQlB;;;;;;;OAOG;IACH,OAAO,CAAC,qBAAqB;IAqB7B;;;;OAIG;YACW,iBAAiB;IA0CzB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAiCzC,uBAAuB;IAyDrC,OAAO,CAAC,SAAS;IAKjB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY,CASlB;IAEF,OAAO,CAAC,UAAU,CAiBhB;IAEF,OAAO,CAAC,mBAAmB;CAI5B"}
@@ -21,8 +21,8 @@ class PlaybackController {
21
21
  frameCount = 0;
22
22
  lastFrameTime = 0;
23
23
  fps = 0;
24
- audioContext = null;
25
- audioSession = null;
24
+ audioContext;
25
+ audioSession;
26
26
  // Buffering state
27
27
  isBuffering = false;
28
28
  currentSeekId = 0;
@@ -36,8 +36,10 @@ class PlaybackController {
36
36
  preheatInProgress = false;
37
37
  constructor(orchestrator, eventBus, options) {
38
38
  this.orchestrator = orchestrator;
39
+ this.audioSession = orchestrator.audioSession;
39
40
  this.eventBus = eventBus;
40
41
  this.canvas = options.canvas;
42
+ this.audioContext = new AudioContext();
41
43
  const model = orchestrator.compositionModel;
42
44
  const width = model?.renderConfig?.width || this.canvas.width || 720;
43
45
  const height = model?.renderConfig?.height || this.canvas.height || 1280;
@@ -69,25 +71,22 @@ class PlaybackController {
69
71
  play() {
70
72
  if (this.state === "playing") return;
71
73
  if (this.state === "ended") {
72
- this.audioSession?.resetPlaybackStates();
74
+ this.audioSession.resetPlaybackStates();
73
75
  }
74
76
  this.wasPlayingBeforeSeek = true;
75
- void this.startPlayback();
77
+ this.startPlayback();
76
78
  }
77
79
  async startPlayback() {
78
80
  const wasIdle = this.state === "idle";
79
81
  const seekId = this.currentSeekId;
82
+ this.state = "playing";
80
83
  try {
81
84
  await this.renderCurrentFrame(this.currentTimeUs);
82
85
  if (seekId !== this.currentSeekId) {
83
86
  return;
84
87
  }
85
- this.state = "playing";
86
- await this.ensureAudioContext();
87
88
  this.initWindow(this.currentTimeUs);
88
- if (this.audioSession && this.audioContext) {
89
- await this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);
90
- }
89
+ await this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);
91
90
  this.startTimeUs = this.audioContext.currentTime * 1e6 - this.currentTimeUs / this.playbackRate;
92
91
  this.playbackLoop();
93
92
  this.eventBus.emit(MeframeEvent.PlaybackPlay);
@@ -105,7 +104,7 @@ class PlaybackController {
105
104
  cancelAnimationFrame(this.rafId);
106
105
  this.rafId = null;
107
106
  }
108
- this.audioSession?.stopPlayback();
107
+ this.audioSession.stopPlayback();
109
108
  this.eventBus.emit(MeframeEvent.PlaybackPause);
110
109
  }
111
110
  stop() {
@@ -119,8 +118,8 @@ class PlaybackController {
119
118
  if (ctx && "clearRect" in ctx) {
120
119
  ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
121
120
  }
122
- this.audioSession?.reset();
123
- this.audioSession?.resetPlaybackStates();
121
+ this.audioSession.reset();
122
+ this.audioSession.resetPlaybackStates();
124
123
  this.eventBus.emit(MeframeEvent.PlaybackStop);
125
124
  }
126
125
  async seek(timeUs) {
@@ -130,8 +129,8 @@ class PlaybackController {
130
129
  cancelAnimationFrame(this.rafId);
131
130
  this.rafId = null;
132
131
  }
133
- this.audioSession?.stopPlayback();
134
- this.audioSession?.resetPlaybackStates();
132
+ this.audioSession.stopPlayback();
133
+ this.audioSession.resetPlaybackStates();
135
134
  const clamped = this.clampTime(timeUs);
136
135
  this.currentTimeUs = clamped;
137
136
  this.currentSeekId++;
@@ -157,9 +156,7 @@ class PlaybackController {
157
156
  if (seekId !== this.currentSeekId) {
158
157
  return;
159
158
  }
160
- if (this.audioSession) {
161
- await this.audioSession.ensureAudioForTime(clamped, { immediate: false });
162
- }
159
+ await this.audioSession.ensureAudioForTime(clamped, { immediate: false });
163
160
  await this.orchestrator.getFrame(clamped, {
164
161
  immediate: false,
165
162
  preheat: true
@@ -192,18 +189,18 @@ class PlaybackController {
192
189
  this.playbackRate = rate;
193
190
  this.startTimeUs = this.audioContext.currentTime * 1e6 - currentTimeUs / rate;
194
191
  this.eventBus.emit(MeframeEvent.PlaybackRateChange, { rate });
195
- this.audioSession?.setPlaybackRate(this.playbackRate);
192
+ this.audioSession.setPlaybackRate(this.playbackRate);
196
193
  }
197
194
  setVolume(volume) {
198
195
  this.volume = Math.max(0, Math.min(1, volume));
199
196
  this.eventBus.emit(MeframeEvent.PlaybackVolumeChange, { volume: this.volume });
200
- this.audioSession?.setVolume(this.volume);
197
+ this.audioSession.setVolume(this.volume);
201
198
  }
202
199
  setMute(muted) {
203
200
  if (muted) {
204
- this.audioSession?.stopPlayback();
205
- } else if (this.state === "playing" && this.audioContext) {
206
- this.audioSession?.startPlayback(this.currentTimeUs, this.audioContext);
201
+ this.audioSession.stopPlayback();
202
+ } else if (this.state === "playing") {
203
+ this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);
207
204
  }
208
205
  }
209
206
  setLoop(loop) {
@@ -219,9 +216,6 @@ class PlaybackController {
219
216
  get isPlaying() {
220
217
  return this.state === "playing";
221
218
  }
222
- setAudioSession(session) {
223
- this.audioSession = session;
224
- }
225
219
  // Resume is just an alias for play
226
220
  resume() {
227
221
  this.play();
@@ -249,12 +243,8 @@ class PlaybackController {
249
243
  if (this.state !== "playing") {
250
244
  return;
251
245
  }
252
- if (this.audioSession) {
253
- await this.audioSession.ensureAudioForTime(this.currentTimeUs, { immediate: true });
254
- }
255
- if (this.audioContext && this.audioSession) {
256
- await this.audioSession.scheduleAudio(this.currentTimeUs, this.audioContext);
257
- }
246
+ await this.audioSession.ensureAudioForTime(this.currentTimeUs, { immediate: true });
247
+ await this.audioSession.scheduleAudio(this.currentTimeUs, this.audioContext);
258
248
  await this.renderCurrentFrame(this.currentTimeUs);
259
249
  if (this.state !== "playing") {
260
250
  return;
@@ -278,7 +268,7 @@ class PlaybackController {
278
268
  if (this.loop) {
279
269
  this.currentTimeUs = 0;
280
270
  this.startTimeUs = this.audioContext.currentTime * 1e6;
281
- this.audioSession?.resetPlaybackStates();
271
+ this.audioSession.resetPlaybackStates();
282
272
  this.initWindow(0);
283
273
  } else {
284
274
  this.pause();
@@ -318,7 +308,7 @@ class PlaybackController {
318
308
  }
319
309
  if (distanceToWindowEnd > 0 && distanceToWindowEnd <= this.PREHEAT_DISTANCE) {
320
310
  this.preheatInProgress = true;
321
- void this.preheatNextWindow();
311
+ this.preheatNextWindow();
322
312
  }
323
313
  }
324
314
  /**
@@ -341,11 +331,7 @@ class PlaybackController {
341
331
  this.orchestrator.preheatClipWindow(clip.id, clipWindowStart, clipWindowEnd, windowStart)
342
332
  );
343
333
  }
344
- if (this.audioSession) {
345
- preheatPromises.push(
346
- this.audioSession.ensureAudioForTime(windowStart, { immediate: false })
347
- );
348
- }
334
+ preheatPromises.push(this.audioSession.ensureAudioForTime(windowStart, { immediate: false }));
349
335
  await Promise.all(preheatPromises);
350
336
  this.windowEnd = windowEnd;
351
337
  } catch (error) {
@@ -387,19 +373,17 @@ class PlaybackController {
387
373
  this.isBuffering = true;
388
374
  this.state = "buffering";
389
375
  this.eventBus.emit(MeframeEvent.PlaybackBuffering);
390
- this.audioSession?.stopPlayback();
376
+ this.audioSession.stopPlayback();
391
377
  try {
392
378
  this.orchestrator.cacheManager.setWindow(timeUs);
393
379
  await this.orchestrator.getFrame(timeUs, { immediate: false });
394
- await this.orchestrator.audioSession.ensureAudioForTime(timeUs, { immediate: false });
380
+ await this.audioSession.ensureAudioForTime(timeUs, { immediate: false });
395
381
  if (seekId !== this.currentSeekId) {
396
382
  return;
397
383
  }
398
384
  this.state = "playing";
399
385
  this.startTimeUs = this.audioContext.currentTime * 1e6 - timeUs / this.playbackRate;
400
- if (this.audioContext) {
401
- await this.audioSession?.startPlayback(timeUs, this.audioContext);
402
- }
386
+ await this.audioSession.startPlayback(timeUs, this.audioContext);
403
387
  this.eventBus.emit(MeframeEvent.PlaybackPlay);
404
388
  if (!this.rafId) {
405
389
  this.playbackLoop();
@@ -445,21 +429,13 @@ class PlaybackController {
445
429
  fps: model.fps || 30,
446
430
  backgroundColor: model.renderConfig?.backgroundColor || "#000"
447
431
  });
448
- if (this.audioSession) {
449
- void this.audioSession.ensureAudioForTime(this.currentTimeUs, { immediate: false });
450
- }
451
- void this.renderCurrentFrame(this.currentTimeUs);
432
+ this.audioSession.ensureAudioForTime(this.currentTimeUs, { immediate: false });
433
+ this.renderCurrentFrame(this.currentTimeUs);
452
434
  };
453
435
  setupEventListeners() {
454
436
  this.eventBus.on(MeframeEvent.CacheCover, this.onCacheCover);
455
437
  this.eventBus.on(MeframeEvent.ModelSet, this.onModelSet);
456
438
  }
457
- async ensureAudioContext() {
458
- if (this.audioContext) {
459
- return;
460
- }
461
- this.audioContext = new AudioContext();
462
- }
463
439
  }
464
440
  export {
465
441
  PlaybackController