@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.
- package/dist/Meframe.d.ts +1 -0
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +2 -2
- package/dist/Meframe.js.map +1 -1
- 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/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/controllers/PlaybackController.d.ts +0 -3
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +29 -53
- package/dist/controllers/PlaybackController.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 +3 -2
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +1 -0
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/FrameRateConverter.d.ts +12 -0
- package/dist/stages/compose/FrameRateConverter.d.ts.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/dist/workers/stages/compose/{video-compose.worker.DM_bsOY8.js → video-compose.worker.Ckk8AtaQ.js} +51 -21
- package/dist/workers/stages/compose/{video-compose.worker.DM_bsOY8.js.map → video-compose.worker.Ckk8AtaQ.js.map} +1 -1
- package/dist/workers/worker-manifest.json +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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"}
|
|
@@ -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;
|
|
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
|
|
25
|
-
audioSession
|
|
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
|
|
74
|
+
this.audioSession.resetPlaybackStates();
|
|
73
75
|
}
|
|
74
76
|
this.wasPlayingBeforeSeek = true;
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
123
|
-
this.audioSession
|
|
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
|
|
134
|
-
this.audioSession
|
|
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
|
-
|
|
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
|
|
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
|
|
197
|
+
this.audioSession.setVolume(this.volume);
|
|
201
198
|
}
|
|
202
199
|
setMute(muted) {
|
|
203
200
|
if (muted) {
|
|
204
|
-
this.audioSession
|
|
205
|
-
} else if (this.state === "playing"
|
|
206
|
-
this.audioSession
|
|
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
|
-
|
|
253
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
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
|