@meframe/core 0.0.9 → 0.0.11
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.map +1 -1
- package/dist/Meframe.js +3 -0
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +12 -0
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +16 -4
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +3 -0
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/PreRenderService.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.js +15 -16
- package/dist/controllers/PreRenderService.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +20 -8
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +8 -1
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +1 -0
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +3 -0
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +8 -1
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/index.d.ts +1 -1
- package/dist/stages/load/index.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +1 -1
- package/dist/stages/mux/MP4Muxer.js.map +1 -1
- package/dist/stages/mux/MuxManager.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.js +36 -5
- package/dist/stages/mux/MuxManager.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js +21 -57
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -78,6 +78,7 @@ class MuxManager {
|
|
|
78
78
|
}
|
|
79
79
|
async writeVideoChunks(muxer, sortedClips) {
|
|
80
80
|
let firstKeyframeWritten = false;
|
|
81
|
+
let exportTimeUs = 0;
|
|
81
82
|
for (const clip of sortedClips) {
|
|
82
83
|
const stream = await this.cacheManager.createL2ReadStream(clip.id, "video");
|
|
83
84
|
if (!stream) {
|
|
@@ -89,15 +90,25 @@ class MuxManager {
|
|
|
89
90
|
while (true) {
|
|
90
91
|
const { done, value } = await reader.read();
|
|
91
92
|
if (done) break;
|
|
92
|
-
const
|
|
93
|
+
const originalChunk = value;
|
|
94
|
+
const buffer = new ArrayBuffer(originalChunk.byteLength);
|
|
95
|
+
originalChunk.copyTo(buffer);
|
|
96
|
+
const remappedChunk = new EncodedVideoChunk({
|
|
97
|
+
type: originalChunk.type,
|
|
98
|
+
timestamp: exportTimeUs,
|
|
99
|
+
duration: originalChunk.duration ?? void 0,
|
|
100
|
+
data: buffer
|
|
101
|
+
});
|
|
93
102
|
if (!firstKeyframeWritten) {
|
|
94
|
-
if (
|
|
103
|
+
if (remappedChunk.type !== "key") {
|
|
95
104
|
console.warn(`[MuxManager] Skipping non-keyframe at start of clip ${clip.id}`);
|
|
105
|
+
exportTimeUs += originalChunk.duration || 0;
|
|
96
106
|
continue;
|
|
97
107
|
}
|
|
98
108
|
firstKeyframeWritten = true;
|
|
99
109
|
}
|
|
100
|
-
muxer.writeVideoChunk(
|
|
110
|
+
muxer.writeVideoChunk(remappedChunk);
|
|
111
|
+
exportTimeUs += originalChunk.duration || 0;
|
|
101
112
|
}
|
|
102
113
|
} finally {
|
|
103
114
|
reader.releaseLock();
|
|
@@ -109,15 +120,35 @@ class MuxManager {
|
|
|
109
120
|
console.warn("[MuxManager] No audio stream available");
|
|
110
121
|
return;
|
|
111
122
|
}
|
|
123
|
+
let exportTimeUs = 0;
|
|
112
124
|
if (this.audioFirstChunk) {
|
|
113
|
-
|
|
125
|
+
const buffer = new ArrayBuffer(this.audioFirstChunk.byteLength);
|
|
126
|
+
this.audioFirstChunk.copyTo(buffer);
|
|
127
|
+
const remappedChunk = new EncodedAudioChunk({
|
|
128
|
+
type: this.audioFirstChunk.type,
|
|
129
|
+
timestamp: exportTimeUs,
|
|
130
|
+
duration: this.audioFirstChunk.duration ?? void 0,
|
|
131
|
+
data: buffer
|
|
132
|
+
});
|
|
133
|
+
muxer.writeAudioChunk(remappedChunk);
|
|
134
|
+
exportTimeUs += this.audioFirstChunk.duration || 0;
|
|
114
135
|
}
|
|
115
136
|
const reader = this.audioEncodedStream.getReader();
|
|
116
137
|
try {
|
|
117
138
|
while (true) {
|
|
118
139
|
const { done, value } = await reader.read();
|
|
119
140
|
if (done) break;
|
|
120
|
-
|
|
141
|
+
const originalChunk = value;
|
|
142
|
+
const buffer = new ArrayBuffer(originalChunk.byteLength);
|
|
143
|
+
originalChunk.copyTo(buffer);
|
|
144
|
+
const remappedChunk = new EncodedAudioChunk({
|
|
145
|
+
type: originalChunk.type,
|
|
146
|
+
timestamp: exportTimeUs,
|
|
147
|
+
duration: originalChunk.duration ?? void 0,
|
|
148
|
+
data: buffer
|
|
149
|
+
});
|
|
150
|
+
muxer.writeAudioChunk(remappedChunk);
|
|
151
|
+
exportTimeUs += originalChunk.duration || 0;
|
|
121
152
|
}
|
|
122
153
|
} finally {
|
|
123
154
|
reader.releaseLock();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MuxManager.js","sources":["../../../src/stages/mux/MuxManager.ts"],"sourcesContent":["import type { CompositionModel } from '../../model/CompositionModel';\nimport type { ExportOptions } from '../../types';\nimport { GlobalAudioSession } from '../compose/GlobalAudioSession';\nimport { MP4Muxer } from './MP4Muxer';\nimport { CacheManager } from '@/cache/CacheManager';\n\n/**\n * MuxManager: Main thread muxing service\n * Reads encoded chunks from L2 cache and muxes into final video file\n */\nexport class MuxManager {\n private cacheManager: CacheManager;\n private audioSession: GlobalAudioSession;\n private audioEncoderConfig: AudioEncoderConfig;\n private audioEncodedStream: ReadableStream<EncodedAudioChunk> | null = null;\n private audioFirstChunk: EncodedAudioChunk | null = null;\n\n constructor(\n cacheManager: CacheManager,\n audioSession: GlobalAudioSession,\n audioEncoderConfig: AudioEncoderConfig\n ) {\n this.cacheManager = cacheManager;\n this.audioSession = audioSession;\n this.audioEncoderConfig = audioEncoderConfig;\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n const videoTrack = model.tracks.find((t) => t.kind === 'video');\n if (!videoTrack || videoTrack.clips.length === 0) {\n throw new Error('No video clips in composition');\n }\n\n const sortedClips = [...videoTrack.clips].sort((a, b) => a.startUs - b.startUs);\n await this.checkL2Coverage(sortedClips);\n\n const width = options.width || (model as any).renderConfig?.width || 720;\n const height = options.height || (model as any).renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n // Get video metadata from L2 cache (first clip)\n const videoChunkMeta = sortedClips[0]\n ? await this.cacheManager.getL2Metadata(sortedClips[0].id, 'video')\n : null;\n\n if (!videoChunkMeta) {\n console.warn('[MuxManager] No videoChunkMeta available, export may fail');\n }\n\n const audioTrack = model.tracks.find((t) => t.kind === 'audio');\n\n // Get audio metadata from first chunk if audio exists\n let audioChunkMeta: any = undefined;\n if (audioTrack?.clips.length) {\n audioChunkMeta = await this.getAudioChunkMeta();\n }\n\n const muxer = new MP4Muxer({\n width,\n height,\n fps,\n fastStart: 'in-memory',\n videoChunkMeta,\n audioChunkMeta,\n });\n\n await this.writeVideoChunks(muxer, sortedClips);\n if (audioTrack?.clips.length) {\n await this.writeAudioChunks(muxer);\n }\n\n return muxer.finalize();\n }\n\n /**\n * Get audio chunk metadata by reading first chunk from encoder\n * Caches the stream and first chunk for later use\n */\n private async getAudioChunkMeta(): Promise<any> {\n const audioEncoderConfig = this.audioEncoderConfig;\n let metadata: any = null;\n\n this.audioEncodedStream = await this.audioSession.createExportEncodedStream(\n audioEncoderConfig,\n (meta) => {\n // Extract decoderConfig from first chunk metadata\n if (meta.decoderConfig) {\n metadata = meta.decoderConfig;\n }\n }\n );\n\n if (!this.audioEncodedStream) {\n return null;\n }\n\n // Read first chunk to trigger metadata extraction and cache it\n const reader = this.audioEncodedStream.getReader();\n try {\n const { done, value } = await reader.read();\n if (!done && value) {\n this.audioFirstChunk = value;\n }\n reader.releaseLock();\n } catch (error) {\n console.error('[MuxManager] Failed to read first audio chunk:', error);\n reader.releaseLock();\n this.audioEncodedStream = null;\n return null;\n }\n\n return metadata;\n }\n\n private async writeVideoChunks(muxer: MP4Muxer, sortedClips: any[]): Promise<void> {\n let firstKeyframeWritten = false;\n\n for (const clip of sortedClips) {\n const stream = await this.cacheManager.createL2ReadStream(clip.id, 'video');\n if (!stream) {\n console.warn(`[MuxManager] No video stream for clip ${clip.id}`);\n continue;\n }\n\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const chunk = value as EncodedVideoChunk;\n\n // Ensure first chunk in export is a keyframe\n if (!firstKeyframeWritten) {\n if (chunk.type !== 'key') {\n console.warn(`[MuxManager] Skipping non-keyframe at start of clip ${clip.id}`);\n continue;\n }\n firstKeyframeWritten = true;\n }\n\n muxer.writeVideoChunk(chunk);\n }\n } finally {\n reader.releaseLock();\n }\n }\n }\n\n private async writeAudioChunks(muxer: MP4Muxer): Promise<void> {\n // Use cached stream from getAudioChunkMeta()\n if (!this.audioEncodedStream) {\n console.warn('[MuxManager] No audio stream available');\n return;\n }\n\n // Write the cached first chunk\n if (this.audioFirstChunk) {\n muxer.writeAudioChunk(this.audioFirstChunk);\n }\n\n // Continue reading remaining chunks\n const reader = this.audioEncodedStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n muxer.writeAudioChunk(value);\n }\n } finally {\n reader.releaseLock();\n // Clean up cached state\n this.audioEncodedStream = null;\n this.audioFirstChunk = null;\n }\n }\n\n private async checkL2Coverage(clips: any[]): Promise<void> {\n const missingClips: string[] = [];\n for (const clip of clips) {\n const inL2 = await this.cacheManager.hasClipInL2(clip.id, 'video');\n if (!inL2) {\n missingClips.push(clip.id);\n }\n }\n\n if (missingClips.length > 0) {\n const clipList = missingClips.slice(0, 3).join(', ');\n const moreText = missingClips.length > 3 ? ` and ${missingClips.length - 3} more` : '';\n throw new Error(\n `Export failed: ${missingClips.length} clip(s) not cached (${clipList}${moreText}). ` +\n `Please start PreRenderService and wait for background caching to complete.`\n );\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MuxManager.js","sources":["../../../src/stages/mux/MuxManager.ts"],"sourcesContent":["import type { CompositionModel } from '../../model/CompositionModel';\nimport type { ExportOptions } from '../../types';\nimport { GlobalAudioSession } from '../compose/GlobalAudioSession';\nimport { MP4Muxer } from './MP4Muxer';\nimport { CacheManager } from '@/cache/CacheManager';\n\n/**\n * MuxManager: Main thread muxing service\n * Reads encoded chunks from L2 cache and muxes into final video file\n */\nexport class MuxManager {\n private cacheManager: CacheManager;\n private audioSession: GlobalAudioSession;\n private audioEncoderConfig: AudioEncoderConfig;\n private audioEncodedStream: ReadableStream<EncodedAudioChunk> | null = null;\n private audioFirstChunk: EncodedAudioChunk | null = null;\n\n constructor(\n cacheManager: CacheManager,\n audioSession: GlobalAudioSession,\n audioEncoderConfig: AudioEncoderConfig\n ) {\n this.cacheManager = cacheManager;\n this.audioSession = audioSession;\n this.audioEncoderConfig = audioEncoderConfig;\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n const videoTrack = model.tracks.find((t) => t.kind === 'video');\n if (!videoTrack || videoTrack.clips.length === 0) {\n throw new Error('No video clips in composition');\n }\n\n const sortedClips = [...videoTrack.clips].sort((a, b) => a.startUs - b.startUs);\n await this.checkL2Coverage(sortedClips);\n\n const width = options.width || (model as any).renderConfig?.width || 720;\n const height = options.height || (model as any).renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n // Get video metadata from L2 cache (first clip)\n const videoChunkMeta = sortedClips[0]\n ? await this.cacheManager.getL2Metadata(sortedClips[0].id, 'video')\n : null;\n\n if (!videoChunkMeta) {\n console.warn('[MuxManager] No videoChunkMeta available, export may fail');\n }\n\n const audioTrack = model.tracks.find((t) => t.kind === 'audio');\n\n // Get audio metadata from first chunk if audio exists\n let audioChunkMeta: any = undefined;\n if (audioTrack?.clips.length) {\n audioChunkMeta = await this.getAudioChunkMeta();\n }\n\n const muxer = new MP4Muxer({\n width,\n height,\n fps,\n fastStart: 'in-memory',\n videoChunkMeta,\n audioChunkMeta,\n });\n\n await this.writeVideoChunks(muxer, sortedClips);\n if (audioTrack?.clips.length) {\n await this.writeAudioChunks(muxer);\n }\n\n return muxer.finalize();\n }\n\n /**\n * Get audio chunk metadata by reading first chunk from encoder\n * Caches the stream and first chunk for later use\n */\n private async getAudioChunkMeta(): Promise<any> {\n const audioEncoderConfig = this.audioEncoderConfig;\n let metadata: any = null;\n\n this.audioEncodedStream = await this.audioSession.createExportEncodedStream(\n audioEncoderConfig,\n (meta) => {\n // Extract decoderConfig from first chunk metadata\n if (meta.decoderConfig) {\n metadata = meta.decoderConfig;\n }\n }\n );\n\n if (!this.audioEncodedStream) {\n return null;\n }\n\n // Read first chunk to trigger metadata extraction and cache it\n const reader = this.audioEncodedStream.getReader();\n try {\n const { done, value } = await reader.read();\n if (!done && value) {\n this.audioFirstChunk = value;\n }\n reader.releaseLock();\n } catch (error) {\n console.error('[MuxManager] Failed to read first audio chunk:', error);\n reader.releaseLock();\n this.audioEncodedStream = null;\n return null;\n }\n\n return metadata;\n }\n\n private async writeVideoChunks(muxer: MP4Muxer, sortedClips: any[]): Promise<void> {\n let firstKeyframeWritten = false;\n let exportTimeUs = 0;\n\n for (const clip of sortedClips) {\n const stream = await this.cacheManager.createL2ReadStream(clip.id, 'video');\n if (!stream) {\n console.warn(`[MuxManager] No video stream for clip ${clip.id}`);\n continue;\n }\n\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const originalChunk = value as EncodedVideoChunk;\n\n // Copy chunk data\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n // Rewrite timestamp to export timeline (tight concatenation)\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: exportTimeUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n // Ensure first chunk in export is a keyframe\n if (!firstKeyframeWritten) {\n if (remappedChunk.type !== 'key') {\n console.warn(`[MuxManager] Skipping non-keyframe at start of clip ${clip.id}`);\n exportTimeUs += originalChunk.duration || 0;\n continue;\n }\n firstKeyframeWritten = true;\n }\n\n muxer.writeVideoChunk(remappedChunk);\n exportTimeUs += originalChunk.duration || 0;\n }\n } finally {\n reader.releaseLock();\n }\n }\n }\n\n private async writeAudioChunks(muxer: MP4Muxer): Promise<void> {\n // Use cached stream from getAudioChunkMeta()\n if (!this.audioEncodedStream) {\n console.warn('[MuxManager] No audio stream available');\n return;\n }\n\n let exportTimeUs = 0;\n\n // Write the cached first chunk with remapped timestamp\n if (this.audioFirstChunk) {\n const buffer = new ArrayBuffer(this.audioFirstChunk.byteLength);\n this.audioFirstChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedAudioChunk({\n type: this.audioFirstChunk.type,\n timestamp: exportTimeUs,\n duration: this.audioFirstChunk.duration ?? undefined,\n data: buffer,\n });\n muxer.writeAudioChunk(remappedChunk);\n exportTimeUs += this.audioFirstChunk.duration || 0;\n }\n\n // Continue reading remaining chunks\n const reader = this.audioEncodedStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const originalChunk = value as EncodedAudioChunk;\n\n // Copy chunk data\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n // Rewrite timestamp to export timeline\n const remappedChunk = new EncodedAudioChunk({\n type: originalChunk.type,\n timestamp: exportTimeUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n muxer.writeAudioChunk(remappedChunk);\n exportTimeUs += originalChunk.duration || 0;\n }\n } finally {\n reader.releaseLock();\n // Clean up cached state\n this.audioEncodedStream = null;\n this.audioFirstChunk = null;\n }\n }\n\n private async checkL2Coverage(clips: any[]): Promise<void> {\n const missingClips: string[] = [];\n for (const clip of clips) {\n const inL2 = await this.cacheManager.hasClipInL2(clip.id, 'video');\n if (!inL2) {\n missingClips.push(clip.id);\n }\n }\n\n if (missingClips.length > 0) {\n const clipList = missingClips.slice(0, 3).join(', ');\n const moreText = missingClips.length > 3 ? ` and ${missingClips.length - 3} more` : '';\n throw new Error(\n `Export failed: ${missingClips.length} clip(s) not cached (${clipList}${moreText}). ` +\n `Please start PreRenderService and wait for background caching to complete.`\n );\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -12,30 +12,6 @@ function frameDurationFromFps(fps) {
|
|
|
12
12
|
const duration = MICROSECONDS_PER_SECOND / normalized;
|
|
13
13
|
return Math.max(Math.round(duration), 1);
|
|
14
14
|
}
|
|
15
|
-
function frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy = "nearest") {
|
|
16
|
-
const frameDurationUs = frameDurationFromFps(fps);
|
|
17
|
-
if (frameDurationUs <= 0) {
|
|
18
|
-
return 0;
|
|
19
|
-
}
|
|
20
|
-
const delta = timestampUs - baseTimestampUs;
|
|
21
|
-
const rawIndex = delta / frameDurationUs;
|
|
22
|
-
if (!Number.isFinite(rawIndex)) {
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
25
|
-
switch (strategy) {
|
|
26
|
-
case "floor":
|
|
27
|
-
return Math.floor(rawIndex);
|
|
28
|
-
case "ceil":
|
|
29
|
-
return Math.ceil(rawIndex);
|
|
30
|
-
default:
|
|
31
|
-
return Math.round(rawIndex);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function quantizeTimestampToFrame(timestampUs, baseTimestampUs, fps, strategy = "nearest") {
|
|
35
|
-
const frameDurationUs = frameDurationFromFps(fps);
|
|
36
|
-
const index = frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy);
|
|
37
|
-
return baseTimestampUs + index * frameDurationUs;
|
|
38
|
-
}
|
|
39
15
|
class LayerRenderer {
|
|
40
16
|
ctx;
|
|
41
17
|
width;
|
|
@@ -947,6 +923,7 @@ class VideoComposeWorker {
|
|
|
947
923
|
*/
|
|
948
924
|
async handleConfigure(payload) {
|
|
949
925
|
const { config, initial } = payload;
|
|
926
|
+
console.log("[VideoComposeWorker] handleConfigure", config, initial);
|
|
950
927
|
const hasValidDimensions = config.width > 0 && config.height > 0;
|
|
951
928
|
const hasValidFps = config.fps > 0;
|
|
952
929
|
if (!hasValidDimensions || !hasValidFps) {
|
|
@@ -1122,7 +1099,7 @@ class VideoComposeWorker {
|
|
|
1122
1099
|
if (!timeline) {
|
|
1123
1100
|
return;
|
|
1124
1101
|
}
|
|
1125
|
-
const { composeStream, cacheStream } = this.composer.createStreams();
|
|
1102
|
+
const { composeStream, cacheStream, encodeStream } = this.composer.createStreams();
|
|
1126
1103
|
const { clipDurationUs, compositionFps } = timeline;
|
|
1127
1104
|
let currentTimeUs = 0;
|
|
1128
1105
|
const readableStream = new ReadableStream({
|
|
@@ -1149,23 +1126,32 @@ class VideoComposeWorker {
|
|
|
1149
1126
|
readableStream.pipeTo(composeStream).catch((error) => {
|
|
1150
1127
|
console.error("[VideoComposeWorker] image frame stream error", this.sessionId, error);
|
|
1151
1128
|
});
|
|
1129
|
+
if (this.downstreamPort) {
|
|
1130
|
+
const encodeChannel = new WorkerChannel(this.downstreamPort, {
|
|
1131
|
+
name: "VideoCompose-Encode",
|
|
1132
|
+
timeout: 3e4
|
|
1133
|
+
});
|
|
1134
|
+
encodeChannel.sendStream(encodeStream, {
|
|
1135
|
+
streamType: "video",
|
|
1136
|
+
sessionId: this.sessionId
|
|
1137
|
+
});
|
|
1138
|
+
} else {
|
|
1139
|
+
encodeStream.cancel();
|
|
1140
|
+
}
|
|
1152
1141
|
}
|
|
1153
1142
|
buildComposeRequest(instruction, frame) {
|
|
1154
|
-
const
|
|
1155
|
-
const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
|
|
1143
|
+
const clipRelativeTime = this.computeTimelineTimestamp(frame, instruction.baseConfig);
|
|
1156
1144
|
const clipDurationUs = instruction.baseConfig.timeline?.clipDurationUs ?? Infinity;
|
|
1157
|
-
|
|
1158
|
-
if (normalizedTime < clipStartUs || normalizedTime >= clipEndUs) {
|
|
1145
|
+
if (clipRelativeTime < 0 || clipRelativeTime >= clipDurationUs) {
|
|
1159
1146
|
return null;
|
|
1160
1147
|
}
|
|
1161
|
-
const clipRelativeTime = normalizedTime - clipStartUs;
|
|
1162
1148
|
const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
|
|
1163
1149
|
if (!activeLayers.length) {
|
|
1164
1150
|
return null;
|
|
1165
1151
|
}
|
|
1166
1152
|
const layers = activeLayers.map((layer) => materializeLayer(layer, frame));
|
|
1167
1153
|
return {
|
|
1168
|
-
timeUs:
|
|
1154
|
+
timeUs: clipRelativeTime,
|
|
1169
1155
|
layers,
|
|
1170
1156
|
transition: VideoComposeWorker.buildTransition(
|
|
1171
1157
|
instruction.transitions,
|
|
@@ -1195,7 +1181,6 @@ class VideoComposeWorker {
|
|
|
1195
1181
|
computeTimelineTimestamp(frame, config) {
|
|
1196
1182
|
if (!this.streamState) {
|
|
1197
1183
|
this.streamState = {
|
|
1198
|
-
baseTimestamp: null,
|
|
1199
1184
|
lastSourceTimestamp: null,
|
|
1200
1185
|
nextFrameIndex: 0
|
|
1201
1186
|
};
|
|
@@ -1206,42 +1191,21 @@ class VideoComposeWorker {
|
|
|
1206
1191
|
this.streamState.lastSourceTimestamp = frame.timestamp ?? null;
|
|
1207
1192
|
return ts;
|
|
1208
1193
|
}
|
|
1209
|
-
const {
|
|
1194
|
+
const { compositionFps } = timeline;
|
|
1210
1195
|
const sourceTimestamp = frame.timestamp ?? null;
|
|
1211
1196
|
if (sourceTimestamp !== null && this.streamState.lastSourceTimestamp !== null && sourceTimestamp < this.streamState.lastSourceTimestamp) {
|
|
1212
|
-
this.streamState.baseTimestamp = null;
|
|
1213
1197
|
this.streamState.nextFrameIndex = 0;
|
|
1214
1198
|
}
|
|
1215
|
-
if (this.streamState.baseTimestamp === null) {
|
|
1216
|
-
this.streamState.baseTimestamp = sourceTimestamp ?? 0;
|
|
1217
|
-
this.streamState.nextFrameIndex = 0;
|
|
1218
|
-
if (this.streamState.baseTimestamp > 1e3) {
|
|
1219
|
-
console.warn(
|
|
1220
|
-
`[VideoComposeWorker] First frame timestamp is ${this.streamState.baseTimestamp}us, expected ~0. Check MP4Demuxer normalization.`
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
1199
|
const frameDuration = frameDurationFromFps(compositionFps);
|
|
1225
1200
|
let frameIndex = this.streamState.nextFrameIndex;
|
|
1226
1201
|
if (sourceTimestamp !== null) {
|
|
1227
|
-
const approxIndex =
|
|
1228
|
-
this.streamState.baseTimestamp,
|
|
1229
|
-
sourceTimestamp,
|
|
1230
|
-
compositionFps,
|
|
1231
|
-
"nearest"
|
|
1232
|
-
);
|
|
1202
|
+
const approxIndex = Math.round(sourceTimestamp / frameDuration);
|
|
1233
1203
|
frameIndex = Math.max(frameIndex, approxIndex);
|
|
1234
1204
|
}
|
|
1235
|
-
const
|
|
1236
|
-
const timelineTime = quantizeTimestampToFrame(
|
|
1237
|
-
rawTimeline,
|
|
1238
|
-
clipStartUs,
|
|
1239
|
-
compositionFps,
|
|
1240
|
-
"nearest"
|
|
1241
|
-
);
|
|
1205
|
+
const relativeTimeUs = frameIndex * frameDuration;
|
|
1242
1206
|
this.streamState.nextFrameIndex = frameIndex + 1;
|
|
1243
1207
|
this.streamState.lastSourceTimestamp = sourceTimestamp;
|
|
1244
|
-
return
|
|
1208
|
+
return relativeTimeUs;
|
|
1245
1209
|
}
|
|
1246
1210
|
}
|
|
1247
1211
|
const worker = new VideoComposeWorker();
|