@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.
Files changed (36) hide show
  1. package/dist/Meframe.d.ts.map +1 -1
  2. package/dist/Meframe.js +3 -0
  3. package/dist/Meframe.js.map +1 -1
  4. package/dist/cache/CacheManager.d.ts +12 -0
  5. package/dist/cache/CacheManager.d.ts.map +1 -1
  6. package/dist/cache/CacheManager.js +16 -4
  7. package/dist/cache/CacheManager.js.map +1 -1
  8. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  9. package/dist/controllers/PlaybackController.js +3 -0
  10. package/dist/controllers/PlaybackController.js.map +1 -1
  11. package/dist/controllers/PreRenderService.d.ts.map +1 -1
  12. package/dist/controllers/PreRenderService.js +15 -16
  13. package/dist/controllers/PreRenderService.js.map +1 -1
  14. package/dist/orchestrator/Orchestrator.d.ts +1 -1
  15. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  16. package/dist/orchestrator/Orchestrator.js +20 -8
  17. package/dist/orchestrator/Orchestrator.js.map +1 -1
  18. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  19. package/dist/orchestrator/VideoClipSession.js +8 -1
  20. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  21. package/dist/orchestrator/types.d.ts +1 -0
  22. package/dist/orchestrator/types.d.ts.map +1 -1
  23. package/dist/stages/load/ResourceLoader.d.ts +3 -0
  24. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  25. package/dist/stages/load/ResourceLoader.js +8 -1
  26. package/dist/stages/load/ResourceLoader.js.map +1 -1
  27. package/dist/stages/load/index.d.ts +1 -1
  28. package/dist/stages/load/index.d.ts.map +1 -1
  29. package/dist/stages/mux/MP4Muxer.js +1 -1
  30. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  31. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  32. package/dist/stages/mux/MuxManager.js +36 -5
  33. package/dist/stages/mux/MuxManager.js.map +1 -1
  34. package/dist/workers/stages/compose/video-compose.worker.js +21 -57
  35. package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
  36. 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 chunk = value;
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 (chunk.type !== "key") {
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(chunk);
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
- muxer.writeAudioChunk(this.audioFirstChunk);
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
- muxer.writeAudioChunk(value);
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 normalizedTime = this.computeTimelineTimestamp(frame, instruction.baseConfig);
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
- const clipEndUs = clipStartUs + clipDurationUs;
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: normalizedTime,
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 { clipStartUs, compositionFps } = timeline;
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 = frameIndexFromTimestamp(
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 rawTimeline = clipStartUs + frameIndex * frameDuration;
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 timelineTime;
1208
+ return relativeTimeUs;
1245
1209
  }
1246
1210
  }
1247
1211
  const worker = new VideoComposeWorker();