@meframe/core 0.5.6 → 0.5.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ExportScheduler.d.ts","sourceRoot":"","sources":["../../src/orchestrator/ExportScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAQ,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAQ7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAgB,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKhE,UAAU,mBAAmB;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,kBAAkB,CAAC;IACjC,qBAAqB,EAAE,MAAM,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACrD,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACrC;AAED,UAAU,qBAAsB,SAAQ,aAAa;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC/B,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAKD,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAsB;gBAEtB,IAAI,EAAE,mBAAmB;IAI/B,OAAO,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;YAe9E,eAAe;YA8If,gBAAgB;YAyDhB,qBAAqB;IAwBnC;;;OAGG;YACW,oBAAoB;YAyIpB,wBAAwB;YA4DxB,gBAAgB;IA8B9B,OAAO,CAAC,qBAAqB;YAKf,0BAA0B;IA2BxC,OAAO,CAAC,+BAA+B;YAazB,mBAAmB;CA0ClC"}
1
+ {"version":3,"file":"ExportScheduler.d.ts","sourceRoot":"","sources":["../../src/orchestrator/ExportScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAQ,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAQ7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAgB,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKhE,UAAU,mBAAmB;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,kBAAkB,CAAC;IACjC,qBAAqB,EAAE,MAAM,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACrD,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACrC;AAED,UAAU,qBAAsB,SAAQ,aAAa;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC/B,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAKD,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAsB;gBAEtB,IAAI,EAAE,mBAAmB;IAI/B,OAAO,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;YAe9E,eAAe;YA2Jf,gBAAgB;YAyDhB,qBAAqB;IAwBnC;;;OAGG;YACW,oBAAoB;YAyIpB,wBAAwB;YA4DxB,gBAAgB;IA8B9B,OAAO,CAAC,qBAAqB;YAKf,0BAA0B;IA2BxC,OAAO,CAAC,+BAA+B;YAazB,mBAAmB;CA0ClC"}
@@ -24,7 +24,9 @@ class ExportScheduler {
24
24
  const signal = options.signal;
25
25
  const controller = options.controller;
26
26
  const exportMode = options.exportMode ?? "blob";
27
+ const exportStartedAt = performance.now();
27
28
  console.info("[ExportScheduler] Export is starting, exportMode:", exportMode);
29
+ console.info("[ExportScheduler] [trace:total] EXPORT START");
28
30
  if (exportMode === "stream" && typeof options.onMuxData !== "function") {
29
31
  throw new Error("onMuxData callback is required when exportMode is stream");
30
32
  }
@@ -104,8 +106,15 @@ class ExportScheduler {
104
106
  if (signal?.aborted) {
105
107
  throw new DOMException("Export aborted", "AbortError");
106
108
  }
109
+ const renderingDoneAt = performance.now();
110
+ console.info(
111
+ `[ExportScheduler] [trace:total] rendering done +${((renderingDoneAt - exportStartedAt) / 1e3).toFixed(2)}s (encode + compose, before muxer.finalize)`
112
+ );
107
113
  console.info("[ExportScheduler] Export is finalizing");
108
114
  const blob = await muxManager.finalize();
115
+ console.info(
116
+ `[ExportScheduler] [trace:total] muxer.finalize +${((performance.now() - renderingDoneAt) / 1e3).toFixed(2)}s`
117
+ );
109
118
  eventBus.emit(MeframeEvent.ExportComplete, {
110
119
  size: blob?.size ?? streamedBytes,
111
120
  durationMs: model.durationUs / 1e3,
@@ -125,6 +134,10 @@ class ExportScheduler {
125
134
  });
126
135
  throw error;
127
136
  } finally {
137
+ const totalSec = ((performance.now() - exportStartedAt) / 1e3).toFixed(2);
138
+ console.info(
139
+ `[ExportScheduler] [trace:total] EXPORT END total=${totalSec}s (${(parseFloat(totalSec) / 60).toFixed(2)} minutes)`
140
+ );
128
141
  console.info("[ExportScheduler] Export is completed");
129
142
  this.deps.cacheManager.endExport();
130
143
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ExportScheduler.js","sources":["../../src/orchestrator/ExportScheduler.ts"],"sourcesContent":["import { CompositionModel, Clip } from '../model';\nimport { ExportOptions } from '../types';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { AudioExportSession } from './AudioExportSession';\nimport { VideoWindowDecodeSession } from './VideoWindowDecodeSession';\nimport { WorkerType } from '../worker/types';\nimport {\n hasResourceId,\n isVideoClip,\n type TimeUs,\n type Resource,\n videoClipPlaybackRate,\n} from '../model/types';\nimport type { ExportController } from '../controllers/ExportController';\nimport type { BaseWorker } from '../worker/BaseWorker';\nimport { EventBus } from '../event/EventBus';\nimport { MeframeEvent, EventPayloadMap } from '../event/events';\nimport { AudioChunkEncoder } from '../stages/encode/AudioChunkEncoder';\nimport type { ClipInstructionSet } from '../stages/compose/instructions';\nimport { computeGOPAlignedWindows, computeFixedWindows } from '../utils/time-utils';\n\ninterface ExportSchedulerDeps {\n workerPool: WorkerPool;\n planner: CompositionPlanner;\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n muxManager: MuxManager;\n audioSession: AudioExportSession;\n workerConfigsProvider: () => Record<WorkerType, any>;\n eventBus: EventBus<EventPayloadMap>;\n}\n\ninterface ExtendedExportOptions extends ExportOptions {\n signal?: AbortSignal;\n controller?: ExportController;\n exportMode?: 'blob' | 'stream';\n onMuxData?: (data: Uint8Array, position: number) => void;\n muxChunkSizeBytes?: number;\n muxChunked?: boolean;\n}\n\n// 5 seconds per window, ~150 frames at 30fps\nconst VIDEO_WINDOW_DURATION_US: TimeUs = 5_000_000;\n\nexport class ExportScheduler {\n private deps: ExportSchedulerDeps;\n\n constructor(deps: ExportSchedulerDeps) {\n this.deps = deps;\n }\n\n async execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob | null> {\n this.deps.cacheManager.clear();\n\n const projectId = this.deps.cacheManager.resourceCache.projectId;\n\n console.info('[ExportScheduler] Export is starting, projectId:', projectId);\n\n if (!navigator.locks) {\n return this.executeInternal(model, options);\n }\n\n const lockName = `meframe-resource-${projectId}`;\n return navigator.locks.request(lockName, () => this.executeInternal(model, options));\n }\n\n private async executeInternal(\n model: CompositionModel,\n options: ExtendedExportOptions\n ): Promise<Blob | null> {\n const { muxManager, audioSession, eventBus, resourceLoader } = this.deps;\n const signal = options.signal;\n const controller = options.controller;\n const exportMode = options.exportMode ?? 'blob';\n\n console.info('[ExportScheduler] Export is starting, exportMode:', exportMode);\n\n if (exportMode === 'stream' && typeof options.onMuxData !== 'function') {\n throw new Error('onMuxData callback is required when exportMode is stream');\n }\n\n let streamedBytes = 0;\n\n const checkStatus = async () => {\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n if (controller?.isPaused()) {\n while (controller.isPaused()) {\n if (signal?.aborted) throw new DOMException('Export aborted', 'AbortError');\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n };\n\n const width = options.width || model.renderConfig?.width || 720;\n const height = options.height || model.renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n this.deps.cacheManager.beginExport();\n\n try {\n // 1. Preload and parse all resources (0-40%)\n console.info('[ExportScheduler] Export is preloading resources');\n await this.preloadResources(model, resourceLoader, eventBus, checkStatus);\n\n const hasAudioSamples = this.deps.cacheManager.audioSampleCache.getTotalBytes() > 0;\n const { enableAudio, disabledReason } = await this.decideAudioStrategy({\n hasAudioSamples,\n requestedFormat: options.format ?? 'mp4',\n requestedAudioCodec: options.audioCodec ?? 'aac',\n });\n if (disabledReason) {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0.4,\n stage: 'encoding',\n message: disabledReason,\n });\n }\n\n // 2. Start Muxer\n console.info('[ExportScheduler] Export is starting muxer');\n muxManager.start({\n width,\n height,\n fps,\n enableAudio,\n output:\n exportMode === 'stream'\n ? {\n kind: 'stream',\n onData: (data, position) => {\n streamedBytes = Math.max(streamedBytes, position + data.byteLength);\n options.onMuxData?.(data, position);\n },\n chunkSize: options.muxChunkSizeBytes,\n chunked: options.muxChunked,\n }\n : { kind: 'blob' },\n });\n\n // 3. Process Video and Audio\n console.info('[ExportScheduler] Export is processing video and audio');\n const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);\n if (mainTrack && mainTrack.clips.length > 0) {\n const audioPromise = enableAudio\n ? this.processAudioInWindows(model.durationUs, audioSession, muxManager, checkStatus)\n : Promise.resolve();\n\n await this.processVideoWindowed(mainTrack.clips, muxManager, model, checkStatus);\n\n try {\n await audioPromise;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0.95,\n stage: 'encoding',\n message: `Audio skipped (runtime failure): ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n } else {\n console.warn('[ExportScheduler] No video clips found');\n }\n\n await audioSession.finalizeExportAudio();\n\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n\n // 4. Finalize\n console.info('[ExportScheduler] Export is finalizing');\n const blob = await muxManager.finalize();\n\n eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob?.size ?? streamedBytes,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 1,\n stage: 'muxing',\n });\n\n console.info('[ExportScheduler] Export is completed successfully');\n return blob;\n } catch (error) {\n console.error('[ExportScheduler] Export error:', error);\n eventBus.emit(MeframeEvent.ExportError, {\n error: error instanceof Error ? error : new Error(String(error)),\n stage: 'export',\n });\n throw error;\n } finally {\n console.info('[ExportScheduler] Export is completed');\n this.deps.cacheManager.endExport();\n }\n }\n\n private async preloadResources(\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n eventBus: EventBus<EventPayloadMap>,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n const tracks = model.tracks.filter((track) => ['video', 'audio'].includes(track.kind));\n if (tracks.length === 0) return;\n\n const maxClipCount = Math.max(...tracks.map((track) => track.clips.length));\n const resourcesToLoad: string[] = [];\n const seen = new Set<string>();\n\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of tracks) {\n const clip = track.clips[clipIndex];\n if (clip && hasResourceId(clip)) {\n if (!seen.has(clip.resourceId)) {\n seen.add(clip.resourceId);\n resourcesToLoad.push(clip.resourceId);\n }\n }\n }\n }\n\n const total = resourcesToLoad.length;\n let completed = 0;\n\n await Promise.all(\n resourcesToLoad.map(async (resourceId) => {\n await checkStatus();\n await resourceLoader.load(resourceId, { isPreload: false });\n\n const resource = model.getResource(resourceId);\n if (resource?.state === 'error') {\n const details = resource.error?.message ?? 'Unknown resource error';\n throw new Error(`Export preload failed for ${resourceId}: ${details}`);\n }\n\n completed++;\n\n const progress = total > 0 ? (completed / total) * 0.4 : 0.4;\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'preparing',\n message: `Loading resources... (${completed}/${total})`,\n });\n })\n );\n }\n\n private async processAudioInWindows(\n totalDurationUs: TimeUs,\n audioSession: AudioExportSession,\n muxManager: MuxManager,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n const WINDOW_DURATION_US = 5 * 60 * 1_000_000;\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n await checkStatus();\n\n const endUs = Math.min(currentUs + WINDOW_DURATION_US, totalDurationUs);\n\n await audioSession.mixAndEncodeSegment(currentUs, endUs, (chunk, meta) =>\n muxManager.writeAudioChunk(chunk, meta)\n );\n\n this.deps.cacheManager.clearAudioCache();\n\n currentUs = endUs;\n }\n }\n\n /**\n * Windowed video processing: one ExportWorker for all clips and windows.\n * Memory bounded by window size (~150 frames).\n */\n private async processVideoWindowed(\n clips: Clip[],\n muxManager: MuxManager,\n model: CompositionModel,\n checkStatus: () => Promise<void>\n ) {\n const { workerPool, planner, cacheManager, resourceLoader } = this.deps;\n const workerConfigs = this.deps.workerConfigsProvider();\n\n // --- One-time setup: create and configure ExportWorker ---\n const exportWorker = await workerPool.getOrCreate('videoExport', 'export', { lazy: true });\n const exportConfig = workerConfigs.videoExport ?? {};\n\n // Apply render overrides from model\n const composeConfig = { ...(exportConfig.compose ?? {}) };\n const encodeConfig = { ...(exportConfig.encode ?? {}) };\n if (model.renderConfig?.width) {\n composeConfig.width = model.renderConfig.width;\n encodeConfig.width = model.renderConfig.width;\n }\n if (model.renderConfig?.height) {\n composeConfig.height = model.renderConfig.height;\n encodeConfig.height = model.renderConfig.height;\n }\n\n await exportWorker.send('configure', { compose: composeConfig, encode: encodeConfig });\n\n // --- Stream receiver: called once per window's encoded output ---\n let windowResolver!: () => void;\n let windowRejecter!: (err: any) => void;\n let nextClipStartUs = 0;\n let lastChunkEndUs = 0;\n\n exportWorker.receiveStream(async (stream, _metadata) => {\n const reader = stream.getReader();\n try {\n while (true) {\n await checkStatus();\n\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n\n const { chunk: originalChunk, metadata } = value as {\n chunk: EncodedVideoChunk;\n metadata: EncodedVideoChunkMetadata;\n };\n const chunkDuration = originalChunk.duration ?? 33333;\n\n // Chunks arrive with clip-relative timestamps; remap to global timeline\n const remappedTimestamp = originalChunk.timestamp + nextClipStartUs;\n\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: remappedTimestamp,\n duration: chunkDuration,\n data: buffer,\n });\n\n muxManager.writeVideoChunk(remappedChunk, metadata);\n\n lastChunkEndUs = remappedTimestamp + chunkDuration;\n\n const encodingProgress = remappedTimestamp / model.durationUs;\n const totalProgress = 0.4 + encodingProgress * 0.6;\n\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: Math.min(1.0, totalProgress),\n stage: 'encoding',\n timeUs: remappedTimestamp,\n });\n }\n windowResolver();\n } catch (error) {\n windowRejecter(error);\n } finally {\n reader.releaseLock();\n }\n });\n\n // --- Per-clip + per-window processing ---\n try {\n for (const clip of clips) {\n await checkStatus();\n\n const resource = hasResourceId(clip) ? model.getResource(clip.resourceId) : null;\n if (!resource) {\n console.warn('[ExportScheduler] Resource not found for clip:', clip.id);\n continue;\n }\n\n const instructions = this.buildClipInstructions(clip, planner);\n\n const createWindowPromise = () =>\n new Promise<void>((resolve, reject) => {\n windowResolver = resolve;\n windowRejecter = reject;\n });\n\n if (resource.type === 'image') {\n await this.processImageClip(\n exportWorker,\n clip,\n resource,\n instructions,\n model,\n resourceLoader,\n createWindowPromise\n );\n } else {\n await this.processVideoClipWindowed(\n exportWorker,\n clip,\n resource,\n instructions,\n model,\n cacheManager,\n resourceLoader,\n checkStatus,\n createWindowPromise\n );\n }\n\n await exportWorker.send('dispose_clip');\n nextClipStartUs = lastChunkEndUs;\n }\n\n // Flush encoder to drain remaining frames\n await exportWorker.send('flush');\n } finally {\n workerPool.terminate('videoExport', 'export');\n }\n }\n\n private async processVideoClipWindowed(\n exportWorker: BaseWorker,\n clip: Clip,\n resource: Resource,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n cacheManager: CacheManager,\n resourceLoader: ResourceLoader,\n checkStatus: () => Promise<void>,\n createWindowPromise: () => Promise<void>\n ) {\n await exportWorker.send('install_instructions', instructions);\n await this.loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader);\n\n const trimStartUs = clip.trimStartUs ?? 0;\n const playbackRate = isVideoClip(clip) ? videoClipPlaybackRate(clip) : 1;\n const trimEndUs = trimStartUs + clip.durationUs * playbackRate;\n const fps = model.fps ?? 30;\n\n const index = cacheManager.mp4IndexCache.get(resource.id);\n const gopIndex = index?.tracks?.video?.gopIndex;\n const windows =\n gopIndex && gopIndex.length > 0\n ? computeGOPAlignedWindows(gopIndex, trimStartUs, trimEndUs, VIDEO_WINDOW_DURATION_US)\n : computeFixedWindows(trimStartUs, trimEndUs, VIDEO_WINDOW_DURATION_US);\n\n const decodeSession = await VideoWindowDecodeSession.create({\n clipId: clip.id,\n resourceId: resource.id,\n resourceUri: resource.uri,\n targetTimeUs: trimStartUs,\n globalTimeUs: clip.startUs,\n mp4IndexCache: cacheManager.mp4IndexCache,\n cacheManager,\n compositionModel: model,\n resourceLoader,\n fps,\n });\n\n try {\n for (const { startUs, endUs } of windows) {\n await checkStatus();\n\n const windowDone = createWindowPromise();\n\n const frameStream = await decodeSession.decodeRangeToStream(startUs, endUs);\n\n await exportWorker.sendStream(frameStream, {\n streamType: 'video',\n windowStartUs: startUs,\n windowEndUs: endUs,\n });\n\n await windowDone;\n }\n } finally {\n await decodeSession.dispose();\n }\n }\n\n private async processImageClip(\n exportWorker: BaseWorker,\n clip: Clip,\n resource: Resource,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n createWindowPromise: () => Promise<void>\n ) {\n await this.loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader);\n\n const clipDone = createWindowPromise();\n\n const imageBitmap = await resourceLoader.loadImage(resource);\n await exportWorker.send(\n 'receive_image',\n {\n resourceId: resource.id,\n sessionId: clip.id,\n imageBitmap,\n instructions,\n },\n { transfer: [imageBitmap] }\n );\n\n // ExportWorker's startImageFrameStream sends encoded stream back;\n // the receiveStream handler resolves clipDone when stream ends\n await clipDone;\n }\n\n private buildClipInstructions(clip: Clip, planner: CompositionPlanner): ClipInstructionSet {\n const plan = planner.buildClipPlan(clip, { cache: false });\n return plan.instructions;\n }\n\n private async loadAndTransferAttachments(\n exportWorker: BaseWorker,\n clip: Clip,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n resourceLoader: ResourceLoader\n ): Promise<void> {\n const attachmentResources = this.extractAttachmentImageResources(instructions);\n if (attachmentResources.length === 0) return;\n\n await Promise.all(\n attachmentResources.map(async (resourceId) => {\n const resource = model.getResource(resourceId);\n if (!resource) return;\n\n const imageBitmap = await resourceLoader.loadImage(resource);\n if (!imageBitmap) return;\n\n await exportWorker.send(\n 'receive_image',\n { clipId: clip.id, resourceId, sessionId: clip.id, imageBitmap },\n { transfer: [imageBitmap] }\n );\n })\n );\n }\n\n private extractAttachmentImageResources(instructions: ClipInstructionSet): string[] {\n const resourceIds = new Set<string>();\n for (const layer of instructions.layers) {\n if (!layer.payload.attachmentId) continue;\n if (layer.type === 'image') {\n const payload = layer.payload;\n if (payload.oldResourceId) resourceIds.add(payload.oldResourceId);\n if (payload.resourceId) resourceIds.add(payload.resourceId);\n }\n }\n return Array.from(resourceIds);\n }\n\n private async decideAudioStrategy(input: {\n hasAudioSamples: boolean;\n requestedFormat: NonNullable<ExportOptions['format']>;\n requestedAudioCodec: NonNullable<ExportOptions['audioCodec']>;\n }): Promise<{ enableAudio: boolean; disabledReason?: string }> {\n if (!input.hasAudioSamples) {\n return { enableAudio: false };\n }\n\n if (input.requestedFormat !== 'mp4') {\n return {\n enableAudio: false,\n disabledReason: `Audio skipped: format ${input.requestedFormat} is not supported.`,\n };\n }\n\n if (input.requestedAudioCodec !== 'aac') {\n return {\n enableAudio: false,\n disabledReason: `Audio skipped: mp4 output currently supports only AAC (requested ${input.requestedAudioCodec}).`,\n };\n }\n\n if (typeof AudioEncoder === 'undefined') {\n return {\n enableAudio: false,\n disabledReason:\n 'Audio skipped: WebCodecs AudioEncoder is not available in this browser/runtime (AAC required for mp4).',\n };\n }\n\n const support = await AudioEncoder.isConfigSupported(AudioChunkEncoder.DEFAULT_CONFIG);\n if (!support.supported) {\n return {\n enableAudio: false,\n disabledReason:\n 'Audio skipped: WebCodecs AAC (mp4a.40.2) AudioEncoder is not supported in this browser/runtime.',\n };\n }\n\n return { enableAudio: true };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExportScheduler.js","sources":["../../src/orchestrator/ExportScheduler.ts"],"sourcesContent":["import { CompositionModel, Clip } from '../model';\nimport { ExportOptions } from '../types';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { AudioExportSession } from './AudioExportSession';\nimport { VideoWindowDecodeSession } from './VideoWindowDecodeSession';\nimport { WorkerType } from '../worker/types';\nimport {\n hasResourceId,\n isVideoClip,\n type TimeUs,\n type Resource,\n videoClipPlaybackRate,\n} from '../model/types';\nimport type { ExportController } from '../controllers/ExportController';\nimport type { BaseWorker } from '../worker/BaseWorker';\nimport { EventBus } from '../event/EventBus';\nimport { MeframeEvent, EventPayloadMap } from '../event/events';\nimport { AudioChunkEncoder } from '../stages/encode/AudioChunkEncoder';\nimport type { ClipInstructionSet } from '../stages/compose/instructions';\nimport { computeGOPAlignedWindows, computeFixedWindows } from '../utils/time-utils';\n\ninterface ExportSchedulerDeps {\n workerPool: WorkerPool;\n planner: CompositionPlanner;\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n muxManager: MuxManager;\n audioSession: AudioExportSession;\n workerConfigsProvider: () => Record<WorkerType, any>;\n eventBus: EventBus<EventPayloadMap>;\n}\n\ninterface ExtendedExportOptions extends ExportOptions {\n signal?: AbortSignal;\n controller?: ExportController;\n exportMode?: 'blob' | 'stream';\n onMuxData?: (data: Uint8Array, position: number) => void;\n muxChunkSizeBytes?: number;\n muxChunked?: boolean;\n}\n\n// 5 seconds per window, ~150 frames at 30fps\nconst VIDEO_WINDOW_DURATION_US: TimeUs = 5_000_000;\n\nexport class ExportScheduler {\n private deps: ExportSchedulerDeps;\n\n constructor(deps: ExportSchedulerDeps) {\n this.deps = deps;\n }\n\n async execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob | null> {\n this.deps.cacheManager.clear();\n\n const projectId = this.deps.cacheManager.resourceCache.projectId;\n\n console.info('[ExportScheduler] Export is starting, projectId:', projectId);\n\n if (!navigator.locks) {\n return this.executeInternal(model, options);\n }\n\n const lockName = `meframe-resource-${projectId}`;\n return navigator.locks.request(lockName, () => this.executeInternal(model, options));\n }\n\n private async executeInternal(\n model: CompositionModel,\n options: ExtendedExportOptions\n ): Promise<Blob | null> {\n const { muxManager, audioSession, eventBus, resourceLoader } = this.deps;\n const signal = options.signal;\n const controller = options.controller;\n const exportMode = options.exportMode ?? 'blob';\n\n const exportStartedAt = performance.now();\n console.info('[ExportScheduler] Export is starting, exportMode:', exportMode);\n console.info('[ExportScheduler] [trace:total] EXPORT START');\n\n if (exportMode === 'stream' && typeof options.onMuxData !== 'function') {\n throw new Error('onMuxData callback is required when exportMode is stream');\n }\n\n let streamedBytes = 0;\n\n const checkStatus = async () => {\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n if (controller?.isPaused()) {\n while (controller.isPaused()) {\n if (signal?.aborted) throw new DOMException('Export aborted', 'AbortError');\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n };\n\n const width = options.width || model.renderConfig?.width || 720;\n const height = options.height || model.renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n this.deps.cacheManager.beginExport();\n\n try {\n // 1. Preload and parse all resources (0-40%)\n console.info('[ExportScheduler] Export is preloading resources');\n await this.preloadResources(model, resourceLoader, eventBus, checkStatus);\n\n const hasAudioSamples = this.deps.cacheManager.audioSampleCache.getTotalBytes() > 0;\n const { enableAudio, disabledReason } = await this.decideAudioStrategy({\n hasAudioSamples,\n requestedFormat: options.format ?? 'mp4',\n requestedAudioCodec: options.audioCodec ?? 'aac',\n });\n if (disabledReason) {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0.4,\n stage: 'encoding',\n message: disabledReason,\n });\n }\n\n // 2. Start Muxer\n console.info('[ExportScheduler] Export is starting muxer');\n muxManager.start({\n width,\n height,\n fps,\n enableAudio,\n output:\n exportMode === 'stream'\n ? {\n kind: 'stream',\n onData: (data, position) => {\n streamedBytes = Math.max(streamedBytes, position + data.byteLength);\n options.onMuxData?.(data, position);\n },\n chunkSize: options.muxChunkSizeBytes,\n chunked: options.muxChunked,\n }\n : { kind: 'blob' },\n });\n\n // 3. Process Video and Audio\n console.info('[ExportScheduler] Export is processing video and audio');\n const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);\n if (mainTrack && mainTrack.clips.length > 0) {\n const audioPromise = enableAudio\n ? this.processAudioInWindows(model.durationUs, audioSession, muxManager, checkStatus)\n : Promise.resolve();\n\n await this.processVideoWindowed(mainTrack.clips, muxManager, model, checkStatus);\n\n try {\n await audioPromise;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0.95,\n stage: 'encoding',\n message: `Audio skipped (runtime failure): ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n } else {\n console.warn('[ExportScheduler] No video clips found');\n }\n\n await audioSession.finalizeExportAudio();\n\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n\n // 4. Finalize\n const renderingDoneAt = performance.now();\n console.info(\n `[ExportScheduler] [trace:total] rendering done +${((renderingDoneAt - exportStartedAt) / 1000).toFixed(2)}s (encode + compose, before muxer.finalize)`\n );\n console.info('[ExportScheduler] Export is finalizing');\n const blob = await muxManager.finalize();\n console.info(\n `[ExportScheduler] [trace:total] muxer.finalize +${((performance.now() - renderingDoneAt) / 1000).toFixed(2)}s`\n );\n\n eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob?.size ?? streamedBytes,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 1,\n stage: 'muxing',\n });\n\n console.info('[ExportScheduler] Export is completed successfully');\n return blob;\n } catch (error) {\n console.error('[ExportScheduler] Export error:', error);\n eventBus.emit(MeframeEvent.ExportError, {\n error: error instanceof Error ? error : new Error(String(error)),\n stage: 'export',\n });\n throw error;\n } finally {\n const totalSec = ((performance.now() - exportStartedAt) / 1000).toFixed(2);\n console.info(\n `[ExportScheduler] [trace:total] EXPORT END total=${totalSec}s (${(parseFloat(totalSec) / 60).toFixed(2)} minutes)`\n );\n console.info('[ExportScheduler] Export is completed');\n this.deps.cacheManager.endExport();\n }\n }\n\n private async preloadResources(\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n eventBus: EventBus<EventPayloadMap>,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n const tracks = model.tracks.filter((track) => ['video', 'audio'].includes(track.kind));\n if (tracks.length === 0) return;\n\n const maxClipCount = Math.max(...tracks.map((track) => track.clips.length));\n const resourcesToLoad: string[] = [];\n const seen = new Set<string>();\n\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of tracks) {\n const clip = track.clips[clipIndex];\n if (clip && hasResourceId(clip)) {\n if (!seen.has(clip.resourceId)) {\n seen.add(clip.resourceId);\n resourcesToLoad.push(clip.resourceId);\n }\n }\n }\n }\n\n const total = resourcesToLoad.length;\n let completed = 0;\n\n await Promise.all(\n resourcesToLoad.map(async (resourceId) => {\n await checkStatus();\n await resourceLoader.load(resourceId, { isPreload: false });\n\n const resource = model.getResource(resourceId);\n if (resource?.state === 'error') {\n const details = resource.error?.message ?? 'Unknown resource error';\n throw new Error(`Export preload failed for ${resourceId}: ${details}`);\n }\n\n completed++;\n\n const progress = total > 0 ? (completed / total) * 0.4 : 0.4;\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'preparing',\n message: `Loading resources... (${completed}/${total})`,\n });\n })\n );\n }\n\n private async processAudioInWindows(\n totalDurationUs: TimeUs,\n audioSession: AudioExportSession,\n muxManager: MuxManager,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n const WINDOW_DURATION_US = 5 * 60 * 1_000_000;\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n await checkStatus();\n\n const endUs = Math.min(currentUs + WINDOW_DURATION_US, totalDurationUs);\n\n await audioSession.mixAndEncodeSegment(currentUs, endUs, (chunk, meta) =>\n muxManager.writeAudioChunk(chunk, meta)\n );\n\n this.deps.cacheManager.clearAudioCache();\n\n currentUs = endUs;\n }\n }\n\n /**\n * Windowed video processing: one ExportWorker for all clips and windows.\n * Memory bounded by window size (~150 frames).\n */\n private async processVideoWindowed(\n clips: Clip[],\n muxManager: MuxManager,\n model: CompositionModel,\n checkStatus: () => Promise<void>\n ) {\n const { workerPool, planner, cacheManager, resourceLoader } = this.deps;\n const workerConfigs = this.deps.workerConfigsProvider();\n\n // --- One-time setup: create and configure ExportWorker ---\n const exportWorker = await workerPool.getOrCreate('videoExport', 'export', { lazy: true });\n const exportConfig = workerConfigs.videoExport ?? {};\n\n // Apply render overrides from model\n const composeConfig = { ...(exportConfig.compose ?? {}) };\n const encodeConfig = { ...(exportConfig.encode ?? {}) };\n if (model.renderConfig?.width) {\n composeConfig.width = model.renderConfig.width;\n encodeConfig.width = model.renderConfig.width;\n }\n if (model.renderConfig?.height) {\n composeConfig.height = model.renderConfig.height;\n encodeConfig.height = model.renderConfig.height;\n }\n\n await exportWorker.send('configure', { compose: composeConfig, encode: encodeConfig });\n\n // --- Stream receiver: called once per window's encoded output ---\n let windowResolver!: () => void;\n let windowRejecter!: (err: any) => void;\n let nextClipStartUs = 0;\n let lastChunkEndUs = 0;\n\n exportWorker.receiveStream(async (stream, _metadata) => {\n const reader = stream.getReader();\n try {\n while (true) {\n await checkStatus();\n\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n\n const { chunk: originalChunk, metadata } = value as {\n chunk: EncodedVideoChunk;\n metadata: EncodedVideoChunkMetadata;\n };\n const chunkDuration = originalChunk.duration ?? 33333;\n\n // Chunks arrive with clip-relative timestamps; remap to global timeline\n const remappedTimestamp = originalChunk.timestamp + nextClipStartUs;\n\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: remappedTimestamp,\n duration: chunkDuration,\n data: buffer,\n });\n\n muxManager.writeVideoChunk(remappedChunk, metadata);\n\n lastChunkEndUs = remappedTimestamp + chunkDuration;\n\n const encodingProgress = remappedTimestamp / model.durationUs;\n const totalProgress = 0.4 + encodingProgress * 0.6;\n\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: Math.min(1.0, totalProgress),\n stage: 'encoding',\n timeUs: remappedTimestamp,\n });\n }\n windowResolver();\n } catch (error) {\n windowRejecter(error);\n } finally {\n reader.releaseLock();\n }\n });\n\n // --- Per-clip + per-window processing ---\n try {\n for (const clip of clips) {\n await checkStatus();\n\n const resource = hasResourceId(clip) ? model.getResource(clip.resourceId) : null;\n if (!resource) {\n console.warn('[ExportScheduler] Resource not found for clip:', clip.id);\n continue;\n }\n\n const instructions = this.buildClipInstructions(clip, planner);\n\n const createWindowPromise = () =>\n new Promise<void>((resolve, reject) => {\n windowResolver = resolve;\n windowRejecter = reject;\n });\n\n if (resource.type === 'image') {\n await this.processImageClip(\n exportWorker,\n clip,\n resource,\n instructions,\n model,\n resourceLoader,\n createWindowPromise\n );\n } else {\n await this.processVideoClipWindowed(\n exportWorker,\n clip,\n resource,\n instructions,\n model,\n cacheManager,\n resourceLoader,\n checkStatus,\n createWindowPromise\n );\n }\n\n await exportWorker.send('dispose_clip');\n nextClipStartUs = lastChunkEndUs;\n }\n\n // Flush encoder to drain remaining frames\n await exportWorker.send('flush');\n } finally {\n workerPool.terminate('videoExport', 'export');\n }\n }\n\n private async processVideoClipWindowed(\n exportWorker: BaseWorker,\n clip: Clip,\n resource: Resource,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n cacheManager: CacheManager,\n resourceLoader: ResourceLoader,\n checkStatus: () => Promise<void>,\n createWindowPromise: () => Promise<void>\n ) {\n await exportWorker.send('install_instructions', instructions);\n await this.loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader);\n\n const trimStartUs = clip.trimStartUs ?? 0;\n const playbackRate = isVideoClip(clip) ? videoClipPlaybackRate(clip) : 1;\n const trimEndUs = trimStartUs + clip.durationUs * playbackRate;\n const fps = model.fps ?? 30;\n\n const index = cacheManager.mp4IndexCache.get(resource.id);\n const gopIndex = index?.tracks?.video?.gopIndex;\n const windows =\n gopIndex && gopIndex.length > 0\n ? computeGOPAlignedWindows(gopIndex, trimStartUs, trimEndUs, VIDEO_WINDOW_DURATION_US)\n : computeFixedWindows(trimStartUs, trimEndUs, VIDEO_WINDOW_DURATION_US);\n\n const decodeSession = await VideoWindowDecodeSession.create({\n clipId: clip.id,\n resourceId: resource.id,\n resourceUri: resource.uri,\n targetTimeUs: trimStartUs,\n globalTimeUs: clip.startUs,\n mp4IndexCache: cacheManager.mp4IndexCache,\n cacheManager,\n compositionModel: model,\n resourceLoader,\n fps,\n });\n\n try {\n for (const { startUs, endUs } of windows) {\n await checkStatus();\n\n const windowDone = createWindowPromise();\n\n const frameStream = await decodeSession.decodeRangeToStream(startUs, endUs);\n\n await exportWorker.sendStream(frameStream, {\n streamType: 'video',\n windowStartUs: startUs,\n windowEndUs: endUs,\n });\n\n await windowDone;\n }\n } finally {\n await decodeSession.dispose();\n }\n }\n\n private async processImageClip(\n exportWorker: BaseWorker,\n clip: Clip,\n resource: Resource,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n createWindowPromise: () => Promise<void>\n ) {\n await this.loadAndTransferAttachments(exportWorker, clip, instructions, model, resourceLoader);\n\n const clipDone = createWindowPromise();\n\n const imageBitmap = await resourceLoader.loadImage(resource);\n await exportWorker.send(\n 'receive_image',\n {\n resourceId: resource.id,\n sessionId: clip.id,\n imageBitmap,\n instructions,\n },\n { transfer: [imageBitmap] }\n );\n\n // ExportWorker's startImageFrameStream sends encoded stream back;\n // the receiveStream handler resolves clipDone when stream ends\n await clipDone;\n }\n\n private buildClipInstructions(clip: Clip, planner: CompositionPlanner): ClipInstructionSet {\n const plan = planner.buildClipPlan(clip, { cache: false });\n return plan.instructions;\n }\n\n private async loadAndTransferAttachments(\n exportWorker: BaseWorker,\n clip: Clip,\n instructions: ClipInstructionSet,\n model: CompositionModel,\n resourceLoader: ResourceLoader\n ): Promise<void> {\n const attachmentResources = this.extractAttachmentImageResources(instructions);\n if (attachmentResources.length === 0) return;\n\n await Promise.all(\n attachmentResources.map(async (resourceId) => {\n const resource = model.getResource(resourceId);\n if (!resource) return;\n\n const imageBitmap = await resourceLoader.loadImage(resource);\n if (!imageBitmap) return;\n\n await exportWorker.send(\n 'receive_image',\n { clipId: clip.id, resourceId, sessionId: clip.id, imageBitmap },\n { transfer: [imageBitmap] }\n );\n })\n );\n }\n\n private extractAttachmentImageResources(instructions: ClipInstructionSet): string[] {\n const resourceIds = new Set<string>();\n for (const layer of instructions.layers) {\n if (!layer.payload.attachmentId) continue;\n if (layer.type === 'image') {\n const payload = layer.payload;\n if (payload.oldResourceId) resourceIds.add(payload.oldResourceId);\n if (payload.resourceId) resourceIds.add(payload.resourceId);\n }\n }\n return Array.from(resourceIds);\n }\n\n private async decideAudioStrategy(input: {\n hasAudioSamples: boolean;\n requestedFormat: NonNullable<ExportOptions['format']>;\n requestedAudioCodec: NonNullable<ExportOptions['audioCodec']>;\n }): Promise<{ enableAudio: boolean; disabledReason?: string }> {\n if (!input.hasAudioSamples) {\n return { enableAudio: false };\n }\n\n if (input.requestedFormat !== 'mp4') {\n return {\n enableAudio: false,\n disabledReason: `Audio skipped: format ${input.requestedFormat} is not supported.`,\n };\n }\n\n if (input.requestedAudioCodec !== 'aac') {\n return {\n enableAudio: false,\n disabledReason: `Audio skipped: mp4 output currently supports only AAC (requested ${input.requestedAudioCodec}).`,\n };\n }\n\n if (typeof AudioEncoder === 'undefined') {\n return {\n enableAudio: false,\n disabledReason:\n 'Audio skipped: WebCodecs AudioEncoder is not available in this browser/runtime (AAC required for mp4).',\n };\n }\n\n const support = await AudioEncoder.isConfigSupported(AudioChunkEncoder.DEFAULT_CONFIG);\n if (!support.supported) {\n return {\n enableAudio: false,\n disabledReason:\n 'Audio skipped: WebCodecs AAC (mp4a.40.2) AudioEncoder is not supported in this browser/runtime.',\n };\n }\n\n return { enableAudio: true };\n }\n}\n"],"names":[],"mappings":";;;;;AA8CA,MAAM,2BAAmC;AAElC,MAAM,gBAAgB;AAAA,EACnB;AAAA,EAER,YAAY,MAA2B;AACrC,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,MAAM,QAAQ,OAAyB,SAAsD;AAC3F,SAAK,KAAK,aAAa,MAAA;AAEvB,UAAM,YAAY,KAAK,KAAK,aAAa,cAAc;AAEvD,YAAQ,KAAK,oDAAoD,SAAS;AAE1E,QAAI,CAAC,UAAU,OAAO;AACpB,aAAO,KAAK,gBAAgB,OAAO,OAAO;AAAA,IAC5C;AAEA,UAAM,WAAW,oBAAoB,SAAS;AAC9C,WAAO,UAAU,MAAM,QAAQ,UAAU,MAAM,KAAK,gBAAgB,OAAO,OAAO,CAAC;AAAA,EACrF;AAAA,EAEA,MAAc,gBACZ,OACA,SACsB;AACtB,UAAM,EAAE,YAAY,cAAc,UAAU,eAAA,IAAmB,KAAK;AACpE,UAAM,SAAS,QAAQ;AACvB,UAAM,aAAa,QAAQ;AAC3B,UAAM,aAAa,QAAQ,cAAc;AAEzC,UAAM,kBAAkB,YAAY,IAAA;AACpC,YAAQ,KAAK,qDAAqD,UAAU;AAC5E,YAAQ,KAAK,8CAA8C;AAE3D,QAAI,eAAe,YAAY,OAAO,QAAQ,cAAc,YAAY;AACtE,YAAM,IAAI,MAAM,0DAA0D;AAAA,IAC5E;AAEA,QAAI,gBAAgB;AAEpB,UAAM,cAAc,YAAY;AAC9B,UAAI,QAAQ,SAAS;AACnB,cAAM,IAAI,aAAa,kBAAkB,YAAY;AAAA,MACvD;AACA,UAAI,YAAY,YAAY;AAC1B,eAAO,WAAW,YAAY;AAC5B,cAAI,QAAQ,QAAS,OAAM,IAAI,aAAa,kBAAkB,YAAY;AAC1E,gBAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAG,CAAC;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,QAAQ,SAAS,MAAM,cAAc,SAAS;AAC5D,UAAM,SAAS,QAAQ,UAAU,MAAM,cAAc,UAAU;AAC/D,UAAM,MAAM,QAAQ,OAAO,MAAM,OAAO;AAExC,aAAS,KAAK,aAAa,aAAa;AAAA,MACtC,QAAQ,QAAQ,UAAU;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAY,MAAM;AAAA,IAAA,CACnB;AAED,SAAK,KAAK,aAAa,YAAA;AAEvB,QAAI;AAEF,cAAQ,KAAK,kDAAkD;AAC/D,YAAM,KAAK,iBAAiB,OAAO,gBAAgB,UAAU,WAAW;AAExE,YAAM,kBAAkB,KAAK,KAAK,aAAa,iBAAiB,kBAAkB;AAClF,YAAM,EAAE,aAAa,eAAA,IAAmB,MAAM,KAAK,oBAAoB;AAAA,QACrE;AAAA,QACA,iBAAiB,QAAQ,UAAU;AAAA,QACnC,qBAAqB,QAAQ,cAAc;AAAA,MAAA,CAC5C;AACD,UAAI,gBAAgB;AAClB,iBAAS,KAAK,aAAa,gBAAgB;AAAA,UACzC,UAAU;AAAA,UACV,OAAO;AAAA,UACP,SAAS;AAAA,QAAA,CACV;AAAA,MACH;AAGA,cAAQ,KAAK,4CAA4C;AACzD,iBAAW,MAAM;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,QACE,eAAe,WACX;AAAA,UACE,MAAM;AAAA,UACN,QAAQ,CAAC,MAAM,aAAa;AAC1B,4BAAgB,KAAK,IAAI,eAAe,WAAW,KAAK,UAAU;AAClE,oBAAQ,YAAY,MAAM,QAAQ;AAAA,UACpC;AAAA,UACA,WAAW,QAAQ;AAAA,UACnB,SAAS,QAAQ;AAAA,QAAA,IAEnB,EAAE,MAAM,OAAA;AAAA,MAAO,CACtB;AAGD,cAAQ,KAAK,wDAAwD;AACrE,YAAM,YAAY,MAAM,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM,WAAW;AACrE,UAAI,aAAa,UAAU,MAAM,SAAS,GAAG;AAC3C,cAAM,eAAe,cACjB,KAAK,sBAAsB,MAAM,YAAY,cAAc,YAAY,WAAW,IAClF,QAAQ,QAAA;AAEZ,cAAM,KAAK,qBAAqB,UAAU,OAAO,YAAY,OAAO,WAAW;AAE/E,YAAI;AACF,gBAAM;AAAA,QACR,SAAS,OAAO;AACd,mBAAS,KAAK,aAAa,gBAAgB;AAAA,YACzC,UAAU;AAAA,YACV,OAAO;AAAA,YACP,SAAS,oCAAoC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,UAAA,CACpG;AAAA,QACH;AAAA,MACF,OAAO;AACL,gBAAQ,KAAK,wCAAwC;AAAA,MACvD;AAEA,YAAM,aAAa,oBAAA;AAEnB,UAAI,QAAQ,SAAS;AACnB,cAAM,IAAI,aAAa,kBAAkB,YAAY;AAAA,MACvD;AAGA,YAAM,kBAAkB,YAAY,IAAA;AACpC,cAAQ;AAAA,QACN,qDAAqD,kBAAkB,mBAAmB,KAAM,QAAQ,CAAC,CAAC;AAAA,MAAA;AAE5G,cAAQ,KAAK,wCAAwC;AACrD,YAAM,OAAO,MAAM,WAAW,SAAA;AAC9B,cAAQ;AAAA,QACN,qDAAqD,YAAY,IAAA,IAAQ,mBAAmB,KAAM,QAAQ,CAAC,CAAC;AAAA,MAAA;AAG9G,eAAS,KAAK,aAAa,gBAAgB;AAAA,QACzC,MAAM,MAAM,QAAQ;AAAA,QACpB,YAAY,MAAM,aAAa;AAAA,QAC/B,QAAQ,QAAQ,UAAU;AAAA,MAAA,CAC3B;AAED,eAAS,KAAK,aAAa,gBAAgB;AAAA,QACzC,UAAU;AAAA,QACV,OAAO;AAAA,MAAA,CACR;AAED,cAAQ,KAAK,oDAAoD;AACjE,aAAO;AAAA,IACT,SAAS,OAAO;AACd,cAAQ,MAAM,mCAAmC,KAAK;AACtD,eAAS,KAAK,aAAa,aAAa;AAAA,QACtC,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,QAC/D,OAAO;AAAA,MAAA,CACR;AACD,YAAM;AAAA,IACR,UAAA;AACE,YAAM,aAAa,YAAY,IAAA,IAAQ,mBAAmB,KAAM,QAAQ,CAAC;AACzE,cAAQ;AAAA,QACN,oDAAoD,QAAQ,OAAO,WAAW,QAAQ,IAAI,IAAI,QAAQ,CAAC,CAAC;AAAA,MAAA;AAE1G,cAAQ,KAAK,uCAAuC;AACpD,WAAK,KAAK,aAAa,UAAA;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,MAAc,iBACZ,OACA,gBACA,UACA,aACe;AACf,aAAS,KAAK,aAAa,gBAAgB;AAAA,MACzC,UAAU;AAAA,MACV,OAAO;AAAA,MACP,SAAS;AAAA,IAAA,CACV;AAED,UAAM,SAAS,MAAM,OAAO,OAAO,CAAC,UAAU,CAAC,SAAS,OAAO,EAAE,SAAS,MAAM,IAAI,CAAC;AACrF,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,eAAe,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,UAAU,MAAM,MAAM,MAAM,CAAC;AAC1E,UAAM,kBAA4B,CAAA;AAClC,UAAM,2BAAW,IAAA;AAEjB,aAAS,YAAY,GAAG,YAAY,cAAc,aAAa;AAC7D,iBAAW,SAAS,QAAQ;AAC1B,cAAM,OAAO,MAAM,MAAM,SAAS;AAClC,YAAI,QAAQ,cAAc,IAAI,GAAG;AAC/B,cAAI,CAAC,KAAK,IAAI,KAAK,UAAU,GAAG;AAC9B,iBAAK,IAAI,KAAK,UAAU;AACxB,4BAAgB,KAAK,KAAK,UAAU;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,gBAAgB;AAC9B,QAAI,YAAY;AAEhB,UAAM,QAAQ;AAAA,MACZ,gBAAgB,IAAI,OAAO,eAAe;AACxC,cAAM,YAAA;AACN,cAAM,eAAe,KAAK,YAAY,EAAE,WAAW,OAAO;AAE1D,cAAM,WAAW,MAAM,YAAY,UAAU;AAC7C,YAAI,UAAU,UAAU,SAAS;AAC/B,gBAAM,UAAU,SAAS,OAAO,WAAW;AAC3C,gBAAM,IAAI,MAAM,6BAA6B,UAAU,KAAK,OAAO,EAAE;AAAA,QACvE;AAEA;AAEA,cAAM,WAAW,QAAQ,IAAK,YAAY,QAAS,MAAM;AACzD,iBAAS,KAAK,aAAa,gBAAgB;AAAA,UACzC;AAAA,UACA,OAAO;AAAA,UACP,SAAS,yBAAyB,SAAS,IAAI,KAAK;AAAA,QAAA,CACrD;AAAA,MACH,CAAC;AAAA,IAAA;AAAA,EAEL;AAAA,EAEA,MAAc,sBACZ,iBACA,cACA,YACA,aACe;AACf,UAAM,qBAAqB,IAAI,KAAK;AACpC,QAAI,YAAY;AAEhB,WAAO,YAAY,iBAAiB;AAClC,YAAM,YAAA;AAEN,YAAM,QAAQ,KAAK,IAAI,YAAY,oBAAoB,eAAe;AAEtE,YAAM,aAAa;AAAA,QAAoB;AAAA,QAAW;AAAA,QAAO,CAAC,OAAO,SAC/D,WAAW,gBAAgB,OAAO,IAAI;AAAA,MAAA;AAGxC,WAAK,KAAK,aAAa,gBAAA;AAEvB,kBAAY;AAAA,IACd;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,qBACZ,OACA,YACA,OACA,aACA;AACA,UAAM,EAAE,YAAY,SAAS,cAAc,eAAA,IAAmB,KAAK;AACnE,UAAM,gBAAgB,KAAK,KAAK,sBAAA;AAGhC,UAAM,eAAe,MAAM,WAAW,YAAY,eAAe,UAAU,EAAE,MAAM,MAAM;AACzF,UAAM,eAAe,cAAc,eAAe,CAAA;AAGlD,UAAM,gBAAgB,EAAE,GAAI,aAAa,WAAW,CAAA,EAAC;AACrD,UAAM,eAAe,EAAE,GAAI,aAAa,UAAU,CAAA,EAAC;AACnD,QAAI,MAAM,cAAc,OAAO;AAC7B,oBAAc,QAAQ,MAAM,aAAa;AACzC,mBAAa,QAAQ,MAAM,aAAa;AAAA,IAC1C;AACA,QAAI,MAAM,cAAc,QAAQ;AAC9B,oBAAc,SAAS,MAAM,aAAa;AAC1C,mBAAa,SAAS,MAAM,aAAa;AAAA,IAC3C;AAEA,UAAM,aAAa,KAAK,aAAa,EAAE,SAAS,eAAe,QAAQ,cAAc;AAGrF,QAAI;AACJ,QAAI;AACJ,QAAI,kBAAkB;AACtB,QAAI,iBAAiB;AAErB,iBAAa,cAAc,OAAO,QAAQ,cAAc;AACtD,YAAM,SAAS,OAAO,UAAA;AACtB,UAAI;AACF,eAAO,MAAM;AACX,gBAAM,YAAA;AAEN,gBAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,cAAI,KAAM;AACV,cAAI,CAAC,MAAO;AAEZ,gBAAM,EAAE,OAAO,eAAe,SAAA,IAAa;AAI3C,gBAAM,gBAAgB,cAAc,YAAY;AAGhD,gBAAM,oBAAoB,cAAc,YAAY;AAEpD,gBAAM,SAAS,IAAI,YAAY,cAAc,UAAU;AACvD,wBAAc,OAAO,MAAM;AAE3B,gBAAM,gBAAgB,IAAI,kBAAkB;AAAA,YAC1C,MAAM,cAAc;AAAA,YACpB,WAAW;AAAA,YACX,UAAU;AAAA,YACV,MAAM;AAAA,UAAA,CACP;AAED,qBAAW,gBAAgB,eAAe,QAAQ;AAElD,2BAAiB,oBAAoB;AAErC,gBAAM,mBAAmB,oBAAoB,MAAM;AACnD,gBAAM,gBAAgB,MAAM,mBAAmB;AAE/C,eAAK,KAAK,SAAS,KAAK,aAAa,gBAAgB;AAAA,YACnD,UAAU,KAAK,IAAI,GAAK,aAAa;AAAA,YACrC,OAAO;AAAA,YACP,QAAQ;AAAA,UAAA,CACT;AAAA,QACH;AACA,uBAAA;AAAA,MACF,SAAS,OAAO;AACd,uBAAe,KAAK;AAAA,MACtB,UAAA;AACE,eAAO,YAAA;AAAA,MACT;AAAA,IACF,CAAC;AAGD,QAAI;AACF,iBAAW,QAAQ,OAAO;AACxB,cAAM,YAAA;AAEN,cAAM,WAAW,cAAc,IAAI,IAAI,MAAM,YAAY,KAAK,UAAU,IAAI;AAC5E,YAAI,CAAC,UAAU;AACb,kBAAQ,KAAK,kDAAkD,KAAK,EAAE;AACtE;AAAA,QACF;AAEA,cAAM,eAAe,KAAK,sBAAsB,MAAM,OAAO;AAE7D,cAAM,sBAAsB,MAC1B,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,2BAAiB;AACjB,2BAAiB;AAAA,QACnB,CAAC;AAEH,YAAI,SAAS,SAAS,SAAS;AAC7B,gBAAM,KAAK;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UAAA;AAAA,QAEJ,OAAO;AACL,gBAAM,KAAK;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UAAA;AAAA,QAEJ;AAEA,cAAM,aAAa,KAAK,cAAc;AACtC,0BAAkB;AAAA,MACpB;AAGA,YAAM,aAAa,KAAK,OAAO;AAAA,IACjC,UAAA;AACE,iBAAW,UAAU,eAAe,QAAQ;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAc,yBACZ,cACA,MACA,UACA,cACA,OACA,cACA,gBACA,aACA,qBACA;AACA,UAAM,aAAa,KAAK,wBAAwB,YAAY;AAC5D,UAAM,KAAK,2BAA2B,cAAc,MAAM,cAAc,OAAO,cAAc;AAE7F,UAAM,cAAc,KAAK,eAAe;AACxC,UAAM,eAAe,YAAY,IAAI,IAAI,sBAAsB,IAAI,IAAI;AACvE,UAAM,YAAY,cAAc,KAAK,aAAa;AAClD,UAAM,MAAM,MAAM,OAAO;AAEzB,UAAM,QAAQ,aAAa,cAAc,IAAI,SAAS,EAAE;AACxD,UAAM,WAAW,OAAO,QAAQ,OAAO;AACvC,UAAM,UACJ,YAAY,SAAS,SAAS,IAC1B,yBAAyB,UAAU,aAAa,WAAW,wBAAwB,IACnF,oBAAoB,aAAa,WAAW,wBAAwB;AAE1E,UAAM,gBAAgB,MAAM,yBAAyB,OAAO;AAAA,MAC1D,QAAQ,KAAK;AAAA,MACb,YAAY,SAAS;AAAA,MACrB,aAAa,SAAS;AAAA,MACtB,cAAc;AAAA,MACd,cAAc,KAAK;AAAA,MACnB,eAAe,aAAa;AAAA,MAC5B;AAAA,MACA,kBAAkB;AAAA,MAClB;AAAA,MACA;AAAA,IAAA,CACD;AAED,QAAI;AACF,iBAAW,EAAE,SAAS,MAAA,KAAW,SAAS;AACxC,cAAM,YAAA;AAEN,cAAM,aAAa,oBAAA;AAEnB,cAAM,cAAc,MAAM,cAAc,oBAAoB,SAAS,KAAK;AAE1E,cAAM,aAAa,WAAW,aAAa;AAAA,UACzC,YAAY;AAAA,UACZ,eAAe;AAAA,UACf,aAAa;AAAA,QAAA,CACd;AAED,cAAM;AAAA,MACR;AAAA,IACF,UAAA;AACE,YAAM,cAAc,QAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAc,iBACZ,cACA,MACA,UACA,cACA,OACA,gBACA,qBACA;AACA,UAAM,KAAK,2BAA2B,cAAc,MAAM,cAAc,OAAO,cAAc;AAE7F,UAAM,WAAW,oBAAA;AAEjB,UAAM,cAAc,MAAM,eAAe,UAAU,QAAQ;AAC3D,UAAM,aAAa;AAAA,MACjB;AAAA,MACA;AAAA,QACE,YAAY,SAAS;AAAA,QACrB,WAAW,KAAK;AAAA,QAChB;AAAA,QACA;AAAA,MAAA;AAAA,MAEF,EAAE,UAAU,CAAC,WAAW,EAAA;AAAA,IAAE;AAK5B,UAAM;AAAA,EACR;AAAA,EAEQ,sBAAsB,MAAY,SAAiD;AACzF,UAAM,OAAO,QAAQ,cAAc,MAAM,EAAE,OAAO,OAAO;AACzD,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,2BACZ,cACA,MACA,cACA,OACA,gBACe;AACf,UAAM,sBAAsB,KAAK,gCAAgC,YAAY;AAC7E,QAAI,oBAAoB,WAAW,EAAG;AAEtC,UAAM,QAAQ;AAAA,MACZ,oBAAoB,IAAI,OAAO,eAAe;AAC5C,cAAM,WAAW,MAAM,YAAY,UAAU;AAC7C,YAAI,CAAC,SAAU;AAEf,cAAM,cAAc,MAAM,eAAe,UAAU,QAAQ;AAC3D,YAAI,CAAC,YAAa;AAElB,cAAM,aAAa;AAAA,UACjB;AAAA,UACA,EAAE,QAAQ,KAAK,IAAI,YAAY,WAAW,KAAK,IAAI,YAAA;AAAA,UACnD,EAAE,UAAU,CAAC,WAAW,EAAA;AAAA,QAAE;AAAA,MAE9B,CAAC;AAAA,IAAA;AAAA,EAEL;AAAA,EAEQ,gCAAgC,cAA4C;AAClF,UAAM,kCAAkB,IAAA;AACxB,eAAW,SAAS,aAAa,QAAQ;AACvC,UAAI,CAAC,MAAM,QAAQ,aAAc;AACjC,UAAI,MAAM,SAAS,SAAS;AAC1B,cAAM,UAAU,MAAM;AACtB,YAAI,QAAQ,cAAe,aAAY,IAAI,QAAQ,aAAa;AAChE,YAAI,QAAQ,WAAY,aAAY,IAAI,QAAQ,UAAU;AAAA,MAC5D;AAAA,IACF;AACA,WAAO,MAAM,KAAK,WAAW;AAAA,EAC/B;AAAA,EAEA,MAAc,oBAAoB,OAI6B;AAC7D,QAAI,CAAC,MAAM,iBAAiB;AAC1B,aAAO,EAAE,aAAa,MAAA;AAAA,IACxB;AAEA,QAAI,MAAM,oBAAoB,OAAO;AACnC,aAAO;AAAA,QACL,aAAa;AAAA,QACb,gBAAgB,yBAAyB,MAAM,eAAe;AAAA,MAAA;AAAA,IAElE;AAEA,QAAI,MAAM,wBAAwB,OAAO;AACvC,aAAO;AAAA,QACL,aAAa;AAAA,QACb,gBAAgB,oEAAoE,MAAM,mBAAmB;AAAA,MAAA;AAAA,IAEjH;AAEA,QAAI,OAAO,iBAAiB,aAAa;AACvC,aAAO;AAAA,QACL,aAAa;AAAA,QACb,gBACE;AAAA,MAAA;AAAA,IAEN;AAEA,UAAM,UAAU,MAAM,aAAa,kBAAkB,kBAAkB,cAAc;AACrF,QAAI,CAAC,QAAQ,WAAW;AACtB,aAAO;AAAA,QACL,aAAa;AAAA,QACb,gBACE;AAAA,MAAA;AAAA,IAEN;AAEA,WAAO,EAAE,aAAa,KAAA;AAAA,EACxB;AACF;"}
@@ -1 +1 @@
1
- {"version":3,"file":"text-wrapper.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-utils/text-wrapper.ts"],"names":[],"mappings":"AAuKA,wBAAgB,QAAQ,CACtB,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAM,GAAG,MAAY,GAChC,MAAM,EAAE,CAuBV;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,OAAO,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAM,GAAG,MAAY,GAChC,MAAM,EAAE,CA0BV;AAED,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,OAAO,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAM,GAAG,MAAY,GAChC,MAAM,EAAE,CAqDV"}
1
+ {"version":3,"file":"text-wrapper.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-utils/text-wrapper.ts"],"names":[],"mappings":"AAuGA,wBAAgB,QAAQ,CACtB,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAM,GAAG,MAAY,GAChC,MAAM,EAAE,CAQV;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,OAAO,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAM,GAAG,MAAY,GAChC,MAAM,EAAE,CA0BV;AAED,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,OAAO,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,MAAM,GAAG,MAAY,GAChC,MAAM,EAAE,CAqDV"}
@@ -16,129 +16,54 @@ function findAllBreakPoints(text) {
16
16
  breakPoints.push(chars.length);
17
17
  return breakPoints;
18
18
  }
19
- function evaluateBalance(lines, ctx, fontSize, fontFamily, fontWeight) {
20
- if (lines.length <= 1) return 0;
21
- const lengths = lines.map(
22
- (line) => measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight)
23
- );
24
- const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
25
- return lengths.reduce((sum, len) => sum + Math.abs(len - avgLength), 0);
26
- }
27
- function tryBreakPointsForMultipleLines(ctx, text, start, remainingLines, currentLines, maxWidth, fontSize, fontFamily, fontWeight, breakPoints) {
28
- let bestLines = [];
29
- let bestBalance = Infinity;
30
- if (remainingLines === 1) {
31
- const lastLine = text.slice(start).trim();
32
- const lastLineWidth = measureTextWidth(ctx, lastLine, fontSize, fontFamily, fontWeight);
33
- if (lastLineWidth <= maxWidth) {
34
- const allLines = [...currentLines, lastLine];
35
- const balance = evaluateBalance(allLines, ctx, fontSize, fontFamily, fontWeight);
36
- if (balance < bestBalance) {
37
- bestBalance = balance;
38
- bestLines = allLines;
39
- }
40
- } else {
41
- const words = lastLine.split(/\s+/);
42
- let currentLine = "";
43
- let tempLines = [...currentLines];
44
- for (const word of words) {
45
- const testLine = currentLine ? `${currentLine} ${word}` : word;
46
- const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
47
- if (lineWidth <= maxWidth) {
48
- currentLine = testLine;
49
- } else {
50
- if (currentLine) {
51
- tempLines.push(currentLine);
52
- currentLine = word;
53
- } else {
54
- tempLines.push(word);
55
- currentLine = "";
56
- }
57
- }
58
- }
59
- if (currentLine) {
60
- tempLines.push(currentLine);
61
- }
62
- const balance = evaluateBalance(tempLines, ctx, fontSize, fontFamily, fontWeight);
63
- if (balance < bestBalance) {
64
- bestBalance = balance;
65
- bestLines = tempLines;
66
- }
67
- }
68
- return { bestLines, bestBalance };
69
- }
70
- let foundValidBreak = false;
71
- for (let i = 0; i < breakPoints.length; i++) {
19
+ function greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight) {
20
+ const breakPoints = findAllBreakPoints(text);
21
+ const lines = [];
22
+ let current = "";
23
+ let prev = breakPoints[0];
24
+ for (let i = 1; i < breakPoints.length; i++) {
72
25
  const bp = breakPoints[i];
73
- if (bp <= start || bp >= text.length) continue;
74
- const line = text.slice(start, bp).trim();
75
- const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
76
- if (lineWidth <= maxWidth) {
77
- foundValidBreak = true;
78
- const result = tryBreakPointsForMultipleLines(
79
- ctx,
80
- text,
81
- bp,
82
- remainingLines - 1,
83
- [...currentLines, line],
84
- maxWidth,
85
- fontSize,
86
- fontFamily,
87
- fontWeight,
88
- breakPoints
89
- );
90
- if (result.bestBalance < bestBalance) {
91
- bestBalance = result.bestBalance;
92
- bestLines = result.bestLines;
93
- }
26
+ if (bp <= prev) continue;
27
+ const segment = text.slice(prev, bp);
28
+ const candidate = current + segment;
29
+ const candidateWidth = measureTextWidth(ctx, candidate, fontSize, fontFamily, fontWeight);
30
+ if (candidateWidth <= maxWidth || current === "") {
31
+ current = candidate;
32
+ } else {
33
+ lines.push(current.trim());
34
+ current = segment;
94
35
  }
36
+ prev = bp;
95
37
  }
96
- if (!foundValidBreak) {
97
- const textPortion = text.slice(start);
98
- const words = textPortion.split(/\s+/);
99
- let currentLine = "";
100
- let tempLines = [...currentLines];
101
- for (const word of words) {
102
- const testLine = currentLine ? `${currentLine} ${word}` : word;
103
- const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
104
- if (lineWidth <= maxWidth) {
105
- currentLine = testLine;
106
- } else {
107
- if (currentLine) {
108
- tempLines.push(currentLine);
109
- currentLine = word;
110
- } else {
111
- tempLines.push(word);
112
- }
113
- }
114
- }
115
- if (currentLine) {
116
- tempLines.push(currentLine);
117
- }
118
- bestLines = tempLines;
38
+ if (current.trim()) {
39
+ lines.push(current.trim());
119
40
  }
120
- return { bestLines, bestBalance };
41
+ fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight);
42
+ return lines.length > 0 ? lines : [text];
43
+ }
44
+ function fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight) {
45
+ if (lines.length < 2) return;
46
+ const last = lines[lines.length - 1];
47
+ const lastWords = last.split(/\s+/).filter(Boolean);
48
+ if (lastWords.length > 1) return;
49
+ const prevLine = lines[lines.length - 2];
50
+ const prevWords = prevLine.split(/\s+/).filter(Boolean);
51
+ if (prevWords.length < 2) return;
52
+ const moved = prevWords.pop();
53
+ const newPrev = prevWords.join(" ");
54
+ const newLast = `${moved} ${last}`.trim();
55
+ const newPrevWidth = measureTextWidth(ctx, newPrev, fontSize, fontFamily, fontWeight);
56
+ const newLastWidth = measureTextWidth(ctx, newLast, fontSize, fontFamily, fontWeight);
57
+ if (newPrevWidth > maxWidth || newLastWidth > maxWidth) return;
58
+ lines[lines.length - 2] = newPrev;
59
+ lines[lines.length - 1] = newLast;
121
60
  }
122
61
  function wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight = 400) {
123
62
  const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);
124
63
  if (textWidth <= maxWidth) {
125
64
  return [text];
126
65
  }
127
- const estimatedLines = Math.ceil(textWidth / maxWidth);
128
- const breakPoints = findAllBreakPoints(text);
129
- const { bestLines } = tryBreakPointsForMultipleLines(
130
- ctx,
131
- text,
132
- 0,
133
- estimatedLines,
134
- [],
135
- maxWidth,
136
- fontSize,
137
- fontFamily,
138
- fontWeight,
139
- breakPoints
140
- );
141
- return bestLines.length > 0 ? bestLines : [text];
66
+ return greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
142
67
  }
143
68
  function formLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
144
69
  const result = [];
@@ -1 +1 @@
1
- {"version":3,"file":"text-wrapper.js","sources":["../../../../src/stages/compose/text-utils/text-wrapper.ts"],"sourcesContent":["import { measureTextWidth } from './text-metrics';\n\nfunction findAllBreakPoints(text: string): number[] {\n const breakPoints = [0];\n const chars = Array.from(text);\n\n for (let i = 1; i < chars.length - 1; i++) {\n if (/[、。!?,,!?;:]/.test(chars[i]!)) {\n breakPoints.push(i + 1);\n } else if (\n /[\\u3040-\\u309F\\u30A0-\\u30FF\\u4E00-\\u9FAF]/.test(chars[i]!) &&\n /[\\u3040-\\u309F\\u30A0-\\u30FF\\u4E00-\\u9FAF]/.test(chars[i + 1]!)\n ) {\n breakPoints.push(i + 1);\n } else if (/[\\s\\-–—,.!?;:]/.test(chars[i]!)) {\n breakPoints.push(i + 1);\n } else if (/\\s/.test(chars[i]!) && /[a-zA-Z]/.test(chars[i + 1]!)) {\n breakPoints.push(i + 1);\n }\n }\n\n breakPoints.push(chars.length);\n return breakPoints;\n}\n\nfunction evaluateBalance(\n lines: string[],\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number\n): number {\n if (lines.length <= 1) return 0;\n const lengths = lines.map((line) =>\n measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight)\n );\n const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;\n return lengths.reduce((sum, len) => sum + Math.abs(len - avgLength), 0);\n}\n\nfunction tryBreakPointsForMultipleLines(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n text: string,\n start: number,\n remainingLines: number,\n currentLines: string[],\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number,\n breakPoints: number[]\n): {\n bestLines: string[];\n bestBalance: number;\n} {\n let bestLines: string[] = [];\n let bestBalance = Infinity;\n\n if (remainingLines === 1) {\n const lastLine = text.slice(start).trim();\n const lastLineWidth = measureTextWidth(ctx, lastLine, fontSize, fontFamily, fontWeight);\n\n if (lastLineWidth <= maxWidth) {\n const allLines = [...currentLines, lastLine];\n const balance = evaluateBalance(allLines, ctx, fontSize, fontFamily, fontWeight);\n\n if (balance < bestBalance) {\n bestBalance = balance;\n bestLines = allLines;\n }\n } else {\n const words = lastLine.split(/\\s+/);\n let currentLine = '';\n let tempLines = [...currentLines];\n\n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word;\n const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);\n\n if (lineWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n tempLines.push(currentLine);\n currentLine = word;\n } else {\n tempLines.push(word);\n currentLine = '';\n }\n }\n }\n\n if (currentLine) {\n tempLines.push(currentLine);\n }\n\n const balance = evaluateBalance(tempLines, ctx, fontSize, fontFamily, fontWeight);\n if (balance < bestBalance) {\n bestBalance = balance;\n bestLines = tempLines;\n }\n }\n return { bestLines, bestBalance };\n }\n\n let foundValidBreak = false;\n\n for (let i = 0; i < breakPoints.length; i++) {\n const bp = breakPoints[i]!;\n if (bp <= start || bp >= text.length) continue;\n\n const line = text.slice(start, bp).trim();\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n\n if (lineWidth <= maxWidth) {\n foundValidBreak = true;\n const result = tryBreakPointsForMultipleLines(\n ctx,\n text,\n bp,\n remainingLines - 1,\n [...currentLines, line],\n maxWidth,\n fontSize,\n fontFamily,\n fontWeight,\n breakPoints\n );\n if (result.bestBalance < bestBalance) {\n bestBalance = result.bestBalance;\n bestLines = result.bestLines;\n }\n }\n }\n\n if (!foundValidBreak) {\n const textPortion = text.slice(start);\n const words = textPortion.split(/\\s+/);\n let currentLine = '';\n let tempLines = [...currentLines];\n\n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word;\n const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);\n\n if (lineWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n if (currentLine) {\n tempLines.push(currentLine);\n currentLine = word;\n } else {\n tempLines.push(word);\n }\n }\n }\n\n if (currentLine) {\n tempLines.push(currentLine);\n }\n\n bestLines = tempLines;\n }\n\n return { bestLines, bestBalance };\n}\n\nexport function wrapText(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n text: string,\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);\n if (textWidth <= maxWidth) {\n return [text];\n }\n\n const estimatedLines = Math.ceil(textWidth / maxWidth);\n const breakPoints = findAllBreakPoints(text);\n\n const { bestLines } = tryBreakPointsForMultipleLines(\n ctx,\n text,\n 0,\n estimatedLines,\n [],\n maxWidth,\n fontSize,\n fontFamily,\n fontWeight,\n breakPoints\n );\n\n return bestLines.length > 0 ? bestLines : [text];\n}\n\nexport function formLinesWithWords(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n words: string[],\n maxWidth: number,\n fontSize: number,\n needsSpace: boolean,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n const result: string[] = [];\n let accumulatedWidth = 0;\n const spaceWidth = measureTextWidth(ctx, ' ', fontSize, fontFamily, fontWeight);\n let currentLine = '';\n\n for (const word of words) {\n let wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n if (needsSpace) {\n wordWidth += spaceWidth;\n }\n if (wordWidth + accumulatedWidth <= maxWidth) {\n currentLine += word + (needsSpace ? ' ' : '');\n accumulatedWidth += wordWidth;\n } else {\n if (currentLine) {\n result.push(currentLine);\n }\n currentLine = word + (needsSpace ? ' ' : '');\n accumulatedWidth = wordWidth;\n }\n }\n if (currentLine !== '') {\n result.push(currentLine);\n }\n return result;\n}\n\nexport function formEvenLinesWithWords(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n words: string[],\n maxWidth: number,\n fontSize: number,\n needsSpace: boolean,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n let minWidth = maxWidth / 2;\n for (const word of words) {\n const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n if (wordWidth > minWidth) {\n minWidth = wordWidth;\n }\n }\n\n const leastLineNum = formLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n ).length;\n\n let bestDelta = maxWidth;\n let bestWidth = minWidth;\n for (let width = maxWidth; width >= minWidth; width -= 1) {\n const lines = formLinesWithWords(\n ctx,\n words,\n width,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n if (lines.length > leastLineNum) {\n break;\n }\n let minLineWidth = Infinity;\n let maxLineWidth = 0;\n for (const line of lines) {\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n if (lineWidth < minLineWidth) {\n minLineWidth = lineWidth;\n }\n if (lineWidth > maxLineWidth) {\n maxLineWidth = lineWidth;\n }\n }\n const delta = maxLineWidth - minLineWidth;\n if (delta < bestDelta) {\n bestDelta = delta;\n bestWidth = width;\n }\n }\n\n return formLinesWithWords(ctx, words, bestWidth, fontSize, needsSpace, fontFamily, fontWeight);\n}\n"],"names":[],"mappings":";AAEA,SAAS,mBAAmB,MAAwB;AAClD,QAAM,cAAc,CAAC,CAAC;AACtB,QAAM,QAAQ,MAAM,KAAK,IAAI;AAE7B,WAAS,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,KAAK;AACzC,QAAI,eAAe,KAAK,MAAM,CAAC,CAAE,GAAG;AAClC,kBAAY,KAAK,IAAI,CAAC;AAAA,IACxB,WACE,4CAA4C,KAAK,MAAM,CAAC,CAAE,KAC1D,4CAA4C,KAAK,MAAM,IAAI,CAAC,CAAE,GAC9D;AACA,kBAAY,KAAK,IAAI,CAAC;AAAA,IACxB,WAAW,iBAAiB,KAAK,MAAM,CAAC,CAAE,GAAG;AAC3C,kBAAY,KAAK,IAAI,CAAC;AAAA,IACxB,WAAW,KAAK,KAAK,MAAM,CAAC,CAAE,KAAK,WAAW,KAAK,MAAM,IAAI,CAAC,CAAE,GAAG;AACjE,kBAAY,KAAK,IAAI,CAAC;AAAA,IACxB;AAAA,EACF;AAEA,cAAY,KAAK,MAAM,MAAM;AAC7B,SAAO;AACT;AAEA,SAAS,gBACP,OACA,KACA,UACA,YACA,YACQ;AACR,MAAI,MAAM,UAAU,EAAG,QAAO;AAC9B,QAAM,UAAU,MAAM;AAAA,IAAI,CAAC,SACzB,iBAAiB,KAAK,MAAM,UAAU,YAAY,UAAU;AAAA,EAAA;AAE9D,QAAM,YAAY,QAAQ,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,QAAQ;AAC/D,SAAO,QAAQ,OAAO,CAAC,KAAK,QAAQ,MAAM,KAAK,IAAI,MAAM,SAAS,GAAG,CAAC;AACxE;AAEA,SAAS,+BACP,KACA,MACA,OACA,gBACA,cACA,UACA,UACA,YACA,YACA,aAIA;AACA,MAAI,YAAsB,CAAA;AAC1B,MAAI,cAAc;AAElB,MAAI,mBAAmB,GAAG;AACxB,UAAM,WAAW,KAAK,MAAM,KAAK,EAAE,KAAA;AACnC,UAAM,gBAAgB,iBAAiB,KAAK,UAAU,UAAU,YAAY,UAAU;AAEtF,QAAI,iBAAiB,UAAU;AAC7B,YAAM,WAAW,CAAC,GAAG,cAAc,QAAQ;AAC3C,YAAM,UAAU,gBAAgB,UAAU,KAAK,UAAU,YAAY,UAAU;AAE/E,UAAI,UAAU,aAAa;AACzB,sBAAc;AACd,oBAAY;AAAA,MACd;AAAA,IACF,OAAO;AACL,YAAM,QAAQ,SAAS,MAAM,KAAK;AAClC,UAAI,cAAc;AAClB,UAAI,YAAY,CAAC,GAAG,YAAY;AAEhC,iBAAW,QAAQ,OAAO;AACxB,cAAM,WAAW,cAAc,GAAG,WAAW,IAAI,IAAI,KAAK;AAC1D,cAAM,YAAY,iBAAiB,KAAK,UAAU,UAAU,YAAY,UAAU;AAElF,YAAI,aAAa,UAAU;AACzB,wBAAc;AAAA,QAChB,OAAO;AACL,cAAI,aAAa;AACf,sBAAU,KAAK,WAAW;AAC1B,0BAAc;AAAA,UAChB,OAAO;AACL,sBAAU,KAAK,IAAI;AACnB,0BAAc;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAEA,UAAI,aAAa;AACf,kBAAU,KAAK,WAAW;AAAA,MAC5B;AAEA,YAAM,UAAU,gBAAgB,WAAW,KAAK,UAAU,YAAY,UAAU;AAChF,UAAI,UAAU,aAAa;AACzB,sBAAc;AACd,oBAAY;AAAA,MACd;AAAA,IACF;AACA,WAAO,EAAE,WAAW,YAAA;AAAA,EACtB;AAEA,MAAI,kBAAkB;AAEtB,WAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,UAAM,KAAK,YAAY,CAAC;AACxB,QAAI,MAAM,SAAS,MAAM,KAAK,OAAQ;AAEtC,UAAM,OAAO,KAAK,MAAM,OAAO,EAAE,EAAE,KAAA;AACnC,UAAM,YAAY,iBAAiB,KAAK,MAAM,UAAU,YAAY,UAAU;AAE9E,QAAI,aAAa,UAAU;AACzB,wBAAkB;AAClB,YAAM,SAAS;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA,iBAAiB;AAAA,QACjB,CAAC,GAAG,cAAc,IAAI;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAEF,UAAI,OAAO,cAAc,aAAa;AACpC,sBAAc,OAAO;AACrB,oBAAY,OAAO;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,iBAAiB;AACpB,UAAM,cAAc,KAAK,MAAM,KAAK;AACpC,UAAM,QAAQ,YAAY,MAAM,KAAK;AACrC,QAAI,cAAc;AAClB,QAAI,YAAY,CAAC,GAAG,YAAY;AAEhC,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,cAAc,GAAG,WAAW,IAAI,IAAI,KAAK;AAC1D,YAAM,YAAY,iBAAiB,KAAK,UAAU,UAAU,YAAY,UAAU;AAElF,UAAI,aAAa,UAAU;AACzB,sBAAc;AAAA,MAChB,OAAO;AACL,YAAI,aAAa;AACf,oBAAU,KAAK,WAAW;AAC1B,wBAAc;AAAA,QAChB,OAAO;AACL,oBAAU,KAAK,IAAI;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,aAAa;AACf,gBAAU,KAAK,WAAW;AAAA,IAC5B;AAEA,gBAAY;AAAA,EACd;AAEA,SAAO,EAAE,WAAW,YAAA;AACtB;AAEO,SAAS,SACd,KACA,MACA,UACA,UACA,YACA,aAA8B,KACpB;AACV,QAAM,YAAY,iBAAiB,KAAK,MAAM,UAAU,YAAY,UAAU;AAC9E,MAAI,aAAa,UAAU;AACzB,WAAO,CAAC,IAAI;AAAA,EACd;AAEA,QAAM,iBAAiB,KAAK,KAAK,YAAY,QAAQ;AACrD,QAAM,cAAc,mBAAmB,IAAI;AAE3C,QAAM,EAAE,cAAc;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,CAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,SAAO,UAAU,SAAS,IAAI,YAAY,CAAC,IAAI;AACjD;AAEO,SAAS,mBACd,KACA,OACA,UACA,UACA,YACA,YACA,aAA8B,KACpB;AACV,QAAM,SAAmB,CAAA;AACzB,MAAI,mBAAmB;AACvB,QAAM,aAAa,iBAAiB,KAAK,KAAK,UAAU,YAAY,UAAU;AAC9E,MAAI,cAAc;AAElB,aAAW,QAAQ,OAAO;AACxB,QAAI,YAAY,iBAAiB,KAAK,MAAM,UAAU,YAAY,UAAU;AAC5E,QAAI,YAAY;AACd,mBAAa;AAAA,IACf;AACA,QAAI,YAAY,oBAAoB,UAAU;AAC5C,qBAAe,QAAQ,aAAa,MAAM;AAC1C,0BAAoB;AAAA,IACtB,OAAO;AACL,UAAI,aAAa;AACf,eAAO,KAAK,WAAW;AAAA,MACzB;AACA,oBAAc,QAAQ,aAAa,MAAM;AACzC,yBAAmB;AAAA,IACrB;AAAA,EACF;AACA,MAAI,gBAAgB,IAAI;AACtB,WAAO,KAAK,WAAW;AAAA,EACzB;AACA,SAAO;AACT;AAEO,SAAS,uBACd,KACA,OACA,UACA,UACA,YACA,YACA,aAA8B,KACpB;AACV,MAAI,WAAW,WAAW;AAC1B,aAAW,QAAQ,OAAO;AACxB,UAAM,YAAY,iBAAiB,KAAK,MAAM,UAAU,YAAY,UAAU;AAC9E,QAAI,YAAY,UAAU;AACxB,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,eAAe;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,EACA;AAEF,MAAI,YAAY;AAChB,MAAI,YAAY;AAChB,WAAS,QAAQ,UAAU,SAAS,UAAU,SAAS,GAAG;AACxD,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAEF,QAAI,MAAM,SAAS,cAAc;AAC/B;AAAA,IACF;AACA,QAAI,eAAe;AACnB,QAAI,eAAe;AACnB,eAAW,QAAQ,OAAO;AACxB,YAAM,YAAY,iBAAiB,KAAK,MAAM,UAAU,YAAY,UAAU;AAC9E,UAAI,YAAY,cAAc;AAC5B,uBAAe;AAAA,MACjB;AACA,UAAI,YAAY,cAAc;AAC5B,uBAAe;AAAA,MACjB;AAAA,IACF;AACA,UAAM,QAAQ,eAAe;AAC7B,QAAI,QAAQ,WAAW;AACrB,kBAAY;AACZ,kBAAY;AAAA,IACd;AAAA,EACF;AAEA,SAAO,mBAAmB,KAAK,OAAO,WAAW,UAAU,YAAY,YAAY,UAAU;AAC/F;"}
1
+ {"version":3,"file":"text-wrapper.js","sources":["../../../../src/stages/compose/text-utils/text-wrapper.ts"],"sourcesContent":["import { measureTextWidth } from './text-metrics';\n\nfunction findAllBreakPoints(text: string): number[] {\n const breakPoints = [0];\n const chars = Array.from(text);\n\n for (let i = 1; i < chars.length - 1; i++) {\n if (/[、。!?,,!?;:]/.test(chars[i]!)) {\n breakPoints.push(i + 1);\n } else if (\n /[\\u3040-\\u309F\\u30A0-\\u30FF\\u4E00-\\u9FAF]/.test(chars[i]!) &&\n /[\\u3040-\\u309F\\u30A0-\\u30FF\\u4E00-\\u9FAF]/.test(chars[i + 1]!)\n ) {\n breakPoints.push(i + 1);\n } else if (/[\\s\\-–—,.!?;:]/.test(chars[i]!)) {\n breakPoints.push(i + 1);\n } else if (/\\s/.test(chars[i]!) && /[a-zA-Z]/.test(chars[i + 1]!)) {\n breakPoints.push(i + 1);\n }\n }\n\n breakPoints.push(chars.length);\n return breakPoints;\n}\n\n// 线性贪心折行:从左到右把合法片段往当前行塞,塞不下就换行。\n// 复杂度 O(B)(B = 断点数),替代原 O(B^N) 的递归回溯。\n// 视觉缺陷:最后一行可能只剩一个词(\"孤儿\"),由 fixTailOrphan 修复。\nfunction greedyWrap(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n text: string,\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number\n): string[] {\n const breakPoints = findAllBreakPoints(text);\n const lines: string[] = [];\n let current = '';\n let prev = breakPoints[0]!;\n\n for (let i = 1; i < breakPoints.length; i++) {\n const bp = breakPoints[i]!;\n if (bp <= prev) continue;\n\n const segment = text.slice(prev, bp);\n const candidate = current + segment;\n const candidateWidth = measureTextWidth(ctx, candidate, fontSize, fontFamily, fontWeight);\n\n // 能塞下就并入当前行;current === '' 兜底:单个 segment 已经超宽时也强制保留,\n // 避免死循环。这种超宽 segment(如超长无空格英文单词)由调用方处理或自然溢出。\n if (candidateWidth <= maxWidth || current === '') {\n current = candidate;\n } else {\n lines.push(current.trim());\n current = segment;\n }\n prev = bp;\n }\n\n if (current.trim()) {\n lines.push(current.trim());\n }\n\n fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight);\n\n return lines.length > 0 ? lines : [text];\n}\n\n// 尾行孤儿修正:贪心结束后若最后一行只剩 1 个词,\n// 从倒数第二行末尾挪 1 个词到最后一行,仅当移动后两行都不超宽时才接受。\n// 视觉上避免出现 \"断头\" 的最后一行。原地修改 lines。\nfunction fixTailOrphan(\n lines: string[],\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number\n): void {\n if (lines.length < 2) return;\n\n const last = lines[lines.length - 1]!;\n const lastWords = last.split(/\\s+/).filter(Boolean);\n if (lastWords.length > 1) return;\n\n const prevLine = lines[lines.length - 2]!;\n const prevWords = prevLine.split(/\\s+/).filter(Boolean);\n if (prevWords.length < 2) return;\n\n const moved = prevWords.pop()!;\n const newPrev = prevWords.join(' ');\n const newLast = `${moved} ${last}`.trim();\n\n // 移动后必须两行都不超宽,否则放弃修正、保留原贪心结果\n const newPrevWidth = measureTextWidth(ctx, newPrev, fontSize, fontFamily, fontWeight);\n const newLastWidth = measureTextWidth(ctx, newLast, fontSize, fontFamily, fontWeight);\n if (newPrevWidth > maxWidth || newLastWidth > maxWidth) return;\n\n lines[lines.length - 2] = newPrev;\n lines[lines.length - 1] = newLast;\n}\n\nexport function wrapText(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n text: string,\n maxWidth: number,\n fontSize: number,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n // 整段不超宽直接单行返回(最常见的快路径)\n const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);\n if (textWidth <= maxWidth) {\n return [text];\n }\n\n return greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);\n}\n\nexport function formLinesWithWords(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n words: string[],\n maxWidth: number,\n fontSize: number,\n needsSpace: boolean,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n const result: string[] = [];\n let accumulatedWidth = 0;\n const spaceWidth = measureTextWidth(ctx, ' ', fontSize, fontFamily, fontWeight);\n let currentLine = '';\n\n for (const word of words) {\n let wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n if (needsSpace) {\n wordWidth += spaceWidth;\n }\n if (wordWidth + accumulatedWidth <= maxWidth) {\n currentLine += word + (needsSpace ? ' ' : '');\n accumulatedWidth += wordWidth;\n } else {\n if (currentLine) {\n result.push(currentLine);\n }\n currentLine = word + (needsSpace ? ' ' : '');\n accumulatedWidth = wordWidth;\n }\n }\n if (currentLine !== '') {\n result.push(currentLine);\n }\n return result;\n}\n\nexport function formEvenLinesWithWords(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n words: string[],\n maxWidth: number,\n fontSize: number,\n needsSpace: boolean,\n fontFamily: string,\n fontWeight: string | number = 400\n): string[] {\n let minWidth = maxWidth / 2;\n for (const word of words) {\n const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n if (wordWidth > minWidth) {\n minWidth = wordWidth;\n }\n }\n\n const leastLineNum = formLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n ).length;\n\n let bestDelta = maxWidth;\n let bestWidth = minWidth;\n for (let width = maxWidth; width >= minWidth; width -= 1) {\n const lines = formLinesWithWords(\n ctx,\n words,\n width,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n if (lines.length > leastLineNum) {\n break;\n }\n let minLineWidth = Infinity;\n let maxLineWidth = 0;\n for (const line of lines) {\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n if (lineWidth < minLineWidth) {\n minLineWidth = lineWidth;\n }\n if (lineWidth > maxLineWidth) {\n maxLineWidth = lineWidth;\n }\n }\n const delta = maxLineWidth - minLineWidth;\n if (delta < bestDelta) {\n bestDelta = delta;\n bestWidth = width;\n }\n }\n\n return formLinesWithWords(ctx, words, bestWidth, fontSize, needsSpace, fontFamily, fontWeight);\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { StreamTarget, ArrayBufferTarget, Muxer } from "../../medeo-fe/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js";
1
+ import { StreamTarget, ArrayBufferTarget, Muxer } from "../../node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js";
2
2
  import { checkBrowserCompatibility } from "../../utils/platform-utils.js";
3
3
  class MP4Muxer {
4
4
  muxer;
@@ -1,4 +1,4 @@
1
- import * as mp4box_all from "../medeo-fe/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js";
1
+ import * as mp4box_all from "../node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js";
2
2
  const lib = mp4box_all;
3
3
  const MP4Box = lib.default && typeof lib.default.createFile === "function" ? lib.default : lib;
4
4
  if (typeof MP4Box.createFile !== "function") {
@@ -11,7 +11,6 @@ if (typeof MP4Box.createFile !== "function") {
11
11
  );
12
12
  }
13
13
  export {
14
- MP4Box,
15
- MP4Box as default
14
+ MP4Box
16
15
  };
17
16
  //# sourceMappingURL=mp4box.js.map
@@ -558,129 +558,54 @@ function findAllBreakPoints(text) {
558
558
  breakPoints.push(chars.length);
559
559
  return breakPoints;
560
560
  }
561
- function evaluateBalance(lines, ctx, fontSize, fontFamily, fontWeight) {
562
- if (lines.length <= 1) return 0;
563
- const lengths = lines.map(
564
- (line) => measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight)
565
- );
566
- const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
567
- return lengths.reduce((sum, len) => sum + Math.abs(len - avgLength), 0);
568
- }
569
- function tryBreakPointsForMultipleLines(ctx, text, start, remainingLines, currentLines, maxWidth, fontSize, fontFamily, fontWeight, breakPoints) {
570
- let bestLines = [];
571
- let bestBalance = Infinity;
572
- if (remainingLines === 1) {
573
- const lastLine = text.slice(start).trim();
574
- const lastLineWidth = measureTextWidth(ctx, lastLine, fontSize, fontFamily, fontWeight);
575
- if (lastLineWidth <= maxWidth) {
576
- const allLines = [...currentLines, lastLine];
577
- const balance = evaluateBalance(allLines, ctx, fontSize, fontFamily, fontWeight);
578
- if (balance < bestBalance) {
579
- bestBalance = balance;
580
- bestLines = allLines;
581
- }
582
- } else {
583
- const words = lastLine.split(/\s+/);
584
- let currentLine = "";
585
- let tempLines = [...currentLines];
586
- for (const word of words) {
587
- const testLine = currentLine ? `${currentLine} ${word}` : word;
588
- const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
589
- if (lineWidth <= maxWidth) {
590
- currentLine = testLine;
591
- } else {
592
- if (currentLine) {
593
- tempLines.push(currentLine);
594
- currentLine = word;
595
- } else {
596
- tempLines.push(word);
597
- currentLine = "";
598
- }
599
- }
600
- }
601
- if (currentLine) {
602
- tempLines.push(currentLine);
603
- }
604
- const balance = evaluateBalance(tempLines, ctx, fontSize, fontFamily, fontWeight);
605
- if (balance < bestBalance) {
606
- bestBalance = balance;
607
- bestLines = tempLines;
608
- }
609
- }
610
- return { bestLines, bestBalance };
611
- }
612
- let foundValidBreak = false;
613
- for (let i = 0; i < breakPoints.length; i++) {
561
+ function greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight) {
562
+ const breakPoints = findAllBreakPoints(text);
563
+ const lines = [];
564
+ let current = "";
565
+ let prev = breakPoints[0];
566
+ for (let i = 1; i < breakPoints.length; i++) {
614
567
  const bp = breakPoints[i];
615
- if (bp <= start || bp >= text.length) continue;
616
- const line = text.slice(start, bp).trim();
617
- const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
618
- if (lineWidth <= maxWidth) {
619
- foundValidBreak = true;
620
- const result = tryBreakPointsForMultipleLines(
621
- ctx,
622
- text,
623
- bp,
624
- remainingLines - 1,
625
- [...currentLines, line],
626
- maxWidth,
627
- fontSize,
628
- fontFamily,
629
- fontWeight,
630
- breakPoints
631
- );
632
- if (result.bestBalance < bestBalance) {
633
- bestBalance = result.bestBalance;
634
- bestLines = result.bestLines;
635
- }
636
- }
637
- }
638
- if (!foundValidBreak) {
639
- const textPortion = text.slice(start);
640
- const words = textPortion.split(/\s+/);
641
- let currentLine = "";
642
- let tempLines = [...currentLines];
643
- for (const word of words) {
644
- const testLine = currentLine ? `${currentLine} ${word}` : word;
645
- const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
646
- if (lineWidth <= maxWidth) {
647
- currentLine = testLine;
648
- } else {
649
- if (currentLine) {
650
- tempLines.push(currentLine);
651
- currentLine = word;
652
- } else {
653
- tempLines.push(word);
654
- }
655
- }
656
- }
657
- if (currentLine) {
658
- tempLines.push(currentLine);
659
- }
660
- bestLines = tempLines;
661
- }
662
- return { bestLines, bestBalance };
568
+ if (bp <= prev) continue;
569
+ const segment = text.slice(prev, bp);
570
+ const candidate = current + segment;
571
+ const candidateWidth = measureTextWidth(ctx, candidate, fontSize, fontFamily, fontWeight);
572
+ if (candidateWidth <= maxWidth || current === "") {
573
+ current = candidate;
574
+ } else {
575
+ lines.push(current.trim());
576
+ current = segment;
577
+ }
578
+ prev = bp;
579
+ }
580
+ if (current.trim()) {
581
+ lines.push(current.trim());
582
+ }
583
+ fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight);
584
+ return lines.length > 0 ? lines : [text];
585
+ }
586
+ function fixTailOrphan(lines, ctx, maxWidth, fontSize, fontFamily, fontWeight) {
587
+ if (lines.length < 2) return;
588
+ const last = lines[lines.length - 1];
589
+ const lastWords = last.split(/\s+/).filter(Boolean);
590
+ if (lastWords.length > 1) return;
591
+ const prevLine = lines[lines.length - 2];
592
+ const prevWords = prevLine.split(/\s+/).filter(Boolean);
593
+ if (prevWords.length < 2) return;
594
+ const moved = prevWords.pop();
595
+ const newPrev = prevWords.join(" ");
596
+ const newLast = `${moved} ${last}`.trim();
597
+ const newPrevWidth = measureTextWidth(ctx, newPrev, fontSize, fontFamily, fontWeight);
598
+ const newLastWidth = measureTextWidth(ctx, newLast, fontSize, fontFamily, fontWeight);
599
+ if (newPrevWidth > maxWidth || newLastWidth > maxWidth) return;
600
+ lines[lines.length - 2] = newPrev;
601
+ lines[lines.length - 1] = newLast;
663
602
  }
664
603
  function wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight = 400) {
665
604
  const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);
666
605
  if (textWidth <= maxWidth) {
667
606
  return [text];
668
607
  }
669
- const estimatedLines = Math.ceil(textWidth / maxWidth);
670
- const breakPoints = findAllBreakPoints(text);
671
- const { bestLines } = tryBreakPointsForMultipleLines(
672
- ctx,
673
- text,
674
- 0,
675
- estimatedLines,
676
- [],
677
- maxWidth,
678
- fontSize,
679
- fontFamily,
680
- fontWeight,
681
- breakPoints
682
- );
683
- return bestLines.length > 0 ? bestLines : [text];
608
+ return greedyWrap(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
684
609
  }
685
610
  function formLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
686
611
  const result = [];
@@ -3620,4 +3545,4 @@ const export_worker = null;
3620
3545
  export {
3621
3546
  export_worker as default
3622
3547
  };
3623
- //# sourceMappingURL=export.worker.CPqXBEVe.js.map
3548
+ //# sourceMappingURL=export.worker.p7X_YtxQ.js.map