@editframe/elements 0.45.2 → 0.45.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DelayedLoadingState.js.map +1 -1
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.js +3 -3
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.js.map +1 -1
- package/dist/canvas/api/CanvasAPI.js.map +1 -1
- package/dist/canvas/getElementBounds.js.map +1 -1
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js +25 -23
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/canvas/selection/SelectionModel.js.map +1 -1
- package/dist/canvas/selection/selectionContext.js.map +1 -1
- package/dist/elements/ContainerInfo.js.map +1 -1
- package/dist/elements/CrossUpdateController.js.map +1 -1
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.js +1 -1
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
- package/dist/elements/EFMedia/MediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -1
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
- package/dist/elements/EFMedia/TimingModel.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/GlobalInputCache.js.map +1 -1
- package/dist/elements/EFMedia/shared/PrecisionUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.js +9 -8
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +4 -4
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTimegroup.js +7 -8
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +4 -4
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +4 -4
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.js.map +1 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SampleBuffer.js.map +1 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.js.map +1 -1
- package/dist/elements/durationConverter.js.map +1 -1
- package/dist/elements/easingUtils.js.map +1 -1
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js.map +1 -1
- package/dist/elements/updateAnimations.js +1 -1
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +4 -4
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayItem.js.map +1 -1
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.js.map +1 -1
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFPreview.js.map +1 -1
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.js.map +1 -1
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTogglePlay.js.map +1 -1
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +4 -4
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin2.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/currentTimeContext.js.map +1 -1
- package/dist/gui/efContext.js.map +1 -1
- package/dist/gui/fetchContext.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -1
- package/dist/gui/panZoomTransformContext.js.map +1 -1
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.js +0 -1
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +4 -4
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/transformCalculations.js.map +1 -1
- package/dist/gui/transformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.d.ts +4 -4
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/BridgeSpanExporter.js.map +1 -1
- package/dist/otel/setupBrowserTracing.js.map +1 -1
- package/dist/otel/tracingHelpers.js.map +1 -1
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js.map +1 -1
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +2 -44
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.js +2 -2
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderVideoToVideo.js +2 -2
- package/dist/preview/renderVideoToVideo.js.map +1 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js.map +1 -1
- package/dist/preview/rendering/loadImage.js.map +1 -1
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/utils/LRUCache.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFImage.js","names":["EFImage","#renderVersion","#hasAlpha","#fileId","#imageLoaded","#lastLoadedPath","#imageLoadPromise","#waitForImageElement","#doLoadImage","#currentObjectUrl"],"sources":["../../src/elements/EFImage.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n PRIORITY_IMAGE,\n} from \"../preview/FrameController.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\n@customElement(\"ef-image\")\nexport class EFImage\n extends EFTemporal(\n EFSourceMixin(FetchMixin(LitElement), {\n assetType: \"image_files\",\n }),\n )\n implements FrameRenderable\n{\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n object-fit: contain;\n object-position: center;\n }\n canvas, img {\n width: 100%;\n height: 100%;\n object-fit: inherit;\n object-position: inherit;\n }\n `,\n ];\n\n static get observedAttributes() {\n // biome-ignore lint/complexity/noThisInStatic: We need to access super\n const parentAttributes = super.observedAttributes || [];\n return [...parentAttributes, \"asset-id\"];\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ): void {\n if (name === \"asset-id\") {\n this.fileId = newValue;\n return;\n }\n super.attributeChangedCallback(name, oldValue, newValue);\n }\n\n imageRef = createRef<HTMLImageElement>();\n canvasRef = createRef<HTMLCanvasElement>();\n\n /**\n * Render version counter - increments when visual content changes.\n * Used by RenderContext to cache rendered dataURLs.\n */\n #renderVersion = 0;\n\n /**\n * Get the current render version.\n * Version increments when src or fileId changes.\n * @public\n */\n get renderVersion(): number {\n return this.#renderVersion;\n }\n\n /**\n * Whether the loaded image has an alpha channel.\n * JPEG images don't have alpha, PNG/WebP may have alpha.\n */\n #hasAlpha = true; // Default to true (preserve alpha) until we know otherwise\n\n /**\n * Get whether the image has an alpha channel.\n * Used to determine if we should encode as PNG (alpha) or JPEG (no alpha).\n * @public\n */\n get hasAlpha(): boolean {\n return this.#hasAlpha;\n }\n\n #fileId: string | null = null;\n\n @property({ type: String, attribute: \"file-id\", reflect: true })\n set fileId(value: string | null) {\n this.#fileId = value;\n }\n\n get fileId() {\n return (\n this.#fileId ??\n this.getAttribute(\"file-id\") ??\n this.getAttribute(\"asset-id\")\n );\n }\n\n /** @deprecated Use fileId instead */\n get assetId(): string | null {\n return this.fileId;\n }\n set assetId(value: string | null) {\n this.fileId = value;\n }\n\n render() {\n const assetPath = this.assetPath();\n const isDirectUrl = this.isDirectUrl(assetPath);\n return isDirectUrl\n ? html`<img ${ref(this.imageRef)} src=${assetPath} />`\n : html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n\n private isDirectUrl(src: string): boolean {\n if (this.fileId) {\n return false;\n }\n return src.startsWith(\"data:\");\n }\n\n assetPath() {\n if (this.fileId) {\n const path = `${this.apiHost}/api/v1/files/${this.fileId}`;\n return path;\n }\n if (this.isDirectUrl(this.src)) {\n return this.src;\n }\n // Normalize local paths: remove leading slashes (remote URLs are passed as-is)\n const normalizedSrc = this.src.startsWith(\"/\")\n ? this.src.replace(/^\\/+/, \"\")\n : this.src;\n return `/api/v1/assets/image?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n get hasOwnDuration() {\n return this.hasExplicitDuration;\n }\n\n // ============================================================================\n // Image Loading - async method instead of Task\n // ============================================================================\n\n #imageLoaded = false;\n #imageLoadPromise: Promise<void> | null = null;\n #lastLoadedPath: string | null = null;\n #currentObjectUrl: string | null = null;\n\n override shouldAutoReady(): boolean {\n return !this.src && !this.fileId;\n }\n\n /**\n * Load image from the configured source\n */\n async loadImage(signal?: AbortSignal): Promise<void> {\n const assetPath = this.assetPath();\n\n // Skip if no source\n if (!this.src && !this.fileId) {\n return;\n }\n\n // Return cached if path hasn't changed\n if (this.#imageLoaded && this.#lastLoadedPath === assetPath) {\n this.setContentReadyState(\"ready\");\n return;\n }\n\n // Return in-flight promise\n if (this.#imageLoadPromise && this.#lastLoadedPath === assetPath) {\n return this.#imageLoadPromise;\n }\n\n this.setContentReadyState(\"loading\");\n\n // For direct URLs, wait for the img element to load\n if (this.isDirectUrl(assetPath)) {\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#waitForImageElement(signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n this.setContentReadyState(\"ready\");\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"EFImage img element load error\", error);\n this.setContentReadyState(\"error\");\n throw error;\n } finally {\n this.#imageLoadPromise = null;\n }\n return;\n }\n\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#doLoadImage(assetPath, signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n this.setContentReadyState(\"ready\");\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Canvas not ready errors are expected during lifecycle\n if (error instanceof Error && error.message === \"Canvas not ready\") {\n return;\n }\n console.error(\"EFImage load error\", error);\n this.setContentReadyState(\"error\");\n } finally {\n this.#imageLoadPromise = null;\n }\n }\n\n async #waitForImageElement(signal?: AbortSignal): Promise<void> {\n if (!this.imageRef.value) {\n throw new Error(\"Image element not ready\");\n }\n\n const img = this.imageRef.value;\n\n // If already loaded (cached), return immediately\n if (img.complete && img.naturalHeight !== 0) {\n return;\n }\n\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const abortHandler = () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\n img.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n img.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n reject(error);\n };\n });\n }\n\n async #doLoadImage(assetPath: string, signal?: AbortSignal): Promise<void> {\n const response = await this.fetch(assetPath, { signal });\n signal?.throwIfAborted();\n\n const image = new Image();\n const blob = await response.blob();\n signal?.throwIfAborted();\n\n // Detect if image has alpha channel based on MIME type\n // JPEG images don't have alpha, PNG/WebP may have alpha\n const mimeType = blob.type.toLowerCase();\n this.#hasAlpha = !mimeType.includes(\"jpeg\") && !mimeType.includes(\"jpg\");\n\n image.src = URL.createObjectURL(blob);\n\n await new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const abortHandler = () => {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\n image.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n image.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n URL.revokeObjectURL(image.src);\n reject(error);\n };\n });\n\n signal?.throwIfAborted();\n\n if (!this.canvasRef.value) throw new Error(\"Canvas not ready\");\n const ctx = this.canvasRef.value.getContext(\"2d\", {\n willReadFrequently: true,\n });\n if (!ctx) throw new Error(\"Canvas 2d context not ready\");\n\n // Determine canvas dimensions\n // For SVG images without explicit dimensions, image.width/height may be 0\n // In that case, fall back to naturalWidth/naturalHeight or element's computed size\n let canvasWidth = image.width || image.naturalWidth;\n let canvasHeight = image.height || image.naturalHeight;\n\n // If still zero (common with SVGs that only have viewBox), use element's computed size\n if (canvasWidth === 0 || canvasHeight === 0) {\n const computedStyle = getComputedStyle(this);\n const elementWidth = parseFloat(computedStyle.width);\n const elementHeight = parseFloat(computedStyle.height);\n\n // Use element dimensions if available, otherwise use a reasonable default\n if (elementWidth > 0 && elementHeight > 0) {\n canvasWidth = elementWidth;\n canvasHeight = elementHeight;\n } else {\n // Default to 300x150 (standard canvas default size)\n canvasWidth = 300;\n canvasHeight = 150;\n }\n }\n\n this.canvasRef.value.width = canvasWidth;\n this.canvasRef.value.height = canvasHeight;\n\n // Ensure the image is fully decoded before drawing\n // This is especially important for SVGs\n try {\n await image.decode();\n } catch (decodeError) {\n // Image decode failed, attempting to draw anyway\n }\n\n // Clear canvas first to ensure we're starting fresh\n ctx.clearRect(0, 0, canvasWidth, canvasHeight);\n\n try {\n ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight);\n } catch (drawError) {\n console.error(`[EFImage] drawImage failed:`, drawError);\n throw drawError;\n }\n\n // DON'T revoke the URL yet - keep it alive in case we need to redraw\n // URL.revokeObjectURL(image.src);\n\n // Store the object URL for cleanup later\n if (this.#currentObjectUrl && this.#currentObjectUrl !== image.src) {\n URL.revokeObjectURL(this.#currentObjectUrl);\n }\n this.#currentObjectUrl = image.src;\n }\n\n // ============================================================================\n // FrameRenderable Implementation\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n return {\n needsPreparation: !this.#imageLoaded,\n isReady: this.#imageLoaded,\n priority: PRIORITY_IMAGE,\n };\n }\n\n /**\n * Async preparation - waits for image to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadImage(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - image is already displayed via img element or canvas.\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Image is already displayed - no explicit render action needed\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n super.updated(changedProperties);\n\n if (changedProperties.has(\"src\") || changedProperties.has(\"fileId\")) {\n this.#imageLoaded = false;\n if (\n changedProperties.get(\"src\") !== undefined ||\n changedProperties.get(\"fileId\") !== undefined\n ) {\n this.emitContentChange(\"source\");\n }\n this.loadImage().catch(() => {});\n this.#renderVersion++;\n }\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n // Clean up object URL when element is removed\n if (this.#currentObjectUrl) {\n URL.revokeObjectURL(this.#currentObjectUrl);\n this.#currentObjectUrl = null;\n }\n }\n\n /**\n * Get the natural dimensions of the image.\n * Returns null if the image hasn't loaded yet.\n *\n * @public\n */\n getNaturalDimensions(): { width: number; height: number } | null {\n // For direct URLs, check img element\n const img = this.imageRef.value;\n if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {\n return {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n\n // For canvas-based images, check canvas dimensions\n const canvas = this.canvasRef.value;\n if (canvas && canvas.width > 0 && canvas.height > 0) {\n return {\n width: canvas.width,\n height: canvas.height,\n };\n }\n\n return null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-image\": EFImage;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFImage.js","names":["EFImage","#renderVersion","#hasAlpha","#fileId","#imageLoaded","#lastLoadedPath","#imageLoadPromise","#waitForImageElement","#doLoadImage","#currentObjectUrl"],"sources":["../../src/elements/EFImage.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n PRIORITY_IMAGE,\n} from \"../preview/FrameController.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\n@customElement(\"ef-image\")\nexport class EFImage\n extends EFTemporal(\n EFSourceMixin(FetchMixin(LitElement), {\n assetType: \"image_files\",\n }),\n )\n implements FrameRenderable\n{\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n object-fit: contain;\n object-position: center;\n }\n canvas, img {\n width: 100%;\n height: 100%;\n object-fit: inherit;\n object-position: inherit;\n }\n `,\n ];\n\n static get observedAttributes() {\n // biome-ignore lint/complexity/noThisInStatic: We need to access super\n const parentAttributes = super.observedAttributes || [];\n return [...parentAttributes, \"asset-id\"];\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (name === \"asset-id\") {\n this.fileId = newValue;\n return;\n }\n super.attributeChangedCallback(name, oldValue, newValue);\n }\n\n imageRef = createRef<HTMLImageElement>();\n canvasRef = createRef<HTMLCanvasElement>();\n\n /**\n * Render version counter - increments when visual content changes.\n * Used by RenderContext to cache rendered dataURLs.\n */\n #renderVersion = 0;\n\n /**\n * Get the current render version.\n * Version increments when src or fileId changes.\n * @public\n */\n get renderVersion(): number {\n return this.#renderVersion;\n }\n\n /**\n * Whether the loaded image has an alpha channel.\n * JPEG images don't have alpha, PNG/WebP may have alpha.\n */\n #hasAlpha = true; // Default to true (preserve alpha) until we know otherwise\n\n /**\n * Get whether the image has an alpha channel.\n * Used to determine if we should encode as PNG (alpha) or JPEG (no alpha).\n * @public\n */\n get hasAlpha(): boolean {\n return this.#hasAlpha;\n }\n\n #fileId: string | null = null;\n\n @property({ type: String, attribute: \"file-id\", reflect: true })\n set fileId(value: string | null) {\n this.#fileId = value;\n }\n\n get fileId() {\n return this.#fileId ?? this.getAttribute(\"file-id\") ?? this.getAttribute(\"asset-id\");\n }\n\n /** @deprecated Use fileId instead */\n get assetId(): string | null {\n return this.fileId;\n }\n set assetId(value: string | null) {\n this.fileId = value;\n }\n\n render() {\n const assetPath = this.assetPath();\n const isDirectUrl = this.isDirectUrl(assetPath);\n return isDirectUrl\n ? html`<img ${ref(this.imageRef)} src=${assetPath} />`\n : html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n\n private isDirectUrl(src: string): boolean {\n if (this.fileId) {\n return false;\n }\n return src.startsWith(\"data:\");\n }\n\n assetPath() {\n if (this.fileId) {\n const path = `${this.apiHost}/api/v1/files/${this.fileId}`;\n return path;\n }\n if (this.isDirectUrl(this.src)) {\n return this.src;\n }\n // Normalize local paths: remove leading slashes (remote URLs are passed as-is)\n const normalizedSrc = this.src.startsWith(\"/\") ? this.src.replace(/^\\/+/, \"\") : this.src;\n return `/api/v1/assets/image?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n get hasOwnDuration() {\n return this.hasExplicitDuration;\n }\n\n // ============================================================================\n // Image Loading - async method instead of Task\n // ============================================================================\n\n #imageLoaded = false;\n #imageLoadPromise: Promise<void> | null = null;\n #lastLoadedPath: string | null = null;\n #currentObjectUrl: string | null = null;\n\n override shouldAutoReady(): boolean {\n return !this.src && !this.fileId;\n }\n\n /**\n * Load image from the configured source\n */\n async loadImage(signal?: AbortSignal): Promise<void> {\n const assetPath = this.assetPath();\n\n // Skip if no source\n if (!this.src && !this.fileId) {\n return;\n }\n\n // Return cached if path hasn't changed\n if (this.#imageLoaded && this.#lastLoadedPath === assetPath) {\n this.setContentReadyState(\"ready\");\n return;\n }\n\n // Return in-flight promise\n if (this.#imageLoadPromise && this.#lastLoadedPath === assetPath) {\n return this.#imageLoadPromise;\n }\n\n this.setContentReadyState(\"loading\");\n\n // For direct URLs, wait for the img element to load\n if (this.isDirectUrl(assetPath)) {\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#waitForImageElement(signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n this.setContentReadyState(\"ready\");\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"EFImage img element load error\", error);\n this.setContentReadyState(\"error\");\n throw error;\n } finally {\n this.#imageLoadPromise = null;\n }\n return;\n }\n\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#doLoadImage(assetPath, signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n this.setContentReadyState(\"ready\");\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Canvas not ready errors are expected during lifecycle\n if (error instanceof Error && error.message === \"Canvas not ready\") {\n return;\n }\n console.error(\"EFImage load error\", error);\n this.setContentReadyState(\"error\");\n } finally {\n this.#imageLoadPromise = null;\n }\n }\n\n async #waitForImageElement(signal?: AbortSignal): Promise<void> {\n if (!this.imageRef.value) {\n throw new Error(\"Image element not ready\");\n }\n\n const img = this.imageRef.value;\n\n // If already loaded (cached), return immediately\n if (img.complete && img.naturalHeight !== 0) {\n return;\n }\n\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const abortHandler = () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\n img.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n img.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n reject(error);\n };\n });\n }\n\n async #doLoadImage(assetPath: string, signal?: AbortSignal): Promise<void> {\n const response = await this.fetch(assetPath, { signal });\n signal?.throwIfAborted();\n\n const image = new Image();\n const blob = await response.blob();\n signal?.throwIfAborted();\n\n // Detect if image has alpha channel based on MIME type\n // JPEG images don't have alpha, PNG/WebP may have alpha\n const mimeType = blob.type.toLowerCase();\n this.#hasAlpha = !mimeType.includes(\"jpeg\") && !mimeType.includes(\"jpg\");\n\n image.src = URL.createObjectURL(blob);\n\n await new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const abortHandler = () => {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\n image.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n image.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n URL.revokeObjectURL(image.src);\n reject(error);\n };\n });\n\n signal?.throwIfAborted();\n\n if (!this.canvasRef.value) throw new Error(\"Canvas not ready\");\n const ctx = this.canvasRef.value.getContext(\"2d\", {\n willReadFrequently: true,\n });\n if (!ctx) throw new Error(\"Canvas 2d context not ready\");\n\n // Determine canvas dimensions\n // For SVG images without explicit dimensions, image.width/height may be 0\n // In that case, fall back to naturalWidth/naturalHeight or element's computed size\n let canvasWidth = image.width || image.naturalWidth;\n let canvasHeight = image.height || image.naturalHeight;\n\n // If still zero (common with SVGs that only have viewBox), use element's computed size\n if (canvasWidth === 0 || canvasHeight === 0) {\n const computedStyle = getComputedStyle(this);\n const elementWidth = parseFloat(computedStyle.width);\n const elementHeight = parseFloat(computedStyle.height);\n\n // Use element dimensions if available, otherwise use a reasonable default\n if (elementWidth > 0 && elementHeight > 0) {\n canvasWidth = elementWidth;\n canvasHeight = elementHeight;\n } else {\n // Default to 300x150 (standard canvas default size)\n canvasWidth = 300;\n canvasHeight = 150;\n }\n }\n\n this.canvasRef.value.width = canvasWidth;\n this.canvasRef.value.height = canvasHeight;\n\n // Ensure the image is fully decoded before drawing\n // This is especially important for SVGs\n try {\n await image.decode();\n } catch (_decodeError) {\n // Image decode failed, attempting to draw anyway\n }\n\n // Clear canvas first to ensure we're starting fresh\n ctx.clearRect(0, 0, canvasWidth, canvasHeight);\n\n try {\n ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight);\n } catch (drawError) {\n console.error(`[EFImage] drawImage failed:`, drawError);\n throw drawError;\n }\n\n // DON'T revoke the URL yet - keep it alive in case we need to redraw\n // URL.revokeObjectURL(image.src);\n\n // Store the object URL for cleanup later\n if (this.#currentObjectUrl && this.#currentObjectUrl !== image.src) {\n URL.revokeObjectURL(this.#currentObjectUrl);\n }\n this.#currentObjectUrl = image.src;\n }\n\n // ============================================================================\n // FrameRenderable Implementation\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n return {\n needsPreparation: !this.#imageLoaded,\n isReady: this.#imageLoaded,\n priority: PRIORITY_IMAGE,\n };\n }\n\n /**\n * Async preparation - waits for image to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadImage(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - image is already displayed via img element or canvas.\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Image is already displayed - no explicit render action needed\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {\n super.updated(changedProperties);\n\n if (changedProperties.has(\"src\") || changedProperties.has(\"fileId\")) {\n this.#imageLoaded = false;\n if (\n changedProperties.get(\"src\") !== undefined ||\n changedProperties.get(\"fileId\") !== undefined\n ) {\n this.emitContentChange(\"source\");\n }\n this.loadImage().catch(() => {});\n this.#renderVersion++;\n }\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n // Clean up object URL when element is removed\n if (this.#currentObjectUrl) {\n URL.revokeObjectURL(this.#currentObjectUrl);\n this.#currentObjectUrl = null;\n }\n }\n\n /**\n * Get the natural dimensions of the image.\n * Returns null if the image hasn't loaded yet.\n *\n * @public\n */\n getNaturalDimensions(): { width: number; height: number } | null {\n // For direct URLs, check img element\n const img = this.imageRef.value;\n if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {\n return {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n\n // For canvas-based images, check canvas dimensions\n const canvas = this.canvasRef.value;\n if (canvas && canvas.width > 0 && canvas.height > 0) {\n return {\n width: canvas.width,\n height: canvas.height,\n };\n }\n\n return null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-image\": EFImage;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BufferedSeekingInput.js","names":["defaultOptions: BufferedSeekingInputOptions","track","bufferSize","#seekLock","contents"],"sources":["../../../src/elements/EFMedia/BufferedSeekingInput.ts"],"sourcesContent":["import {\n AudioSampleSink,\n BufferSource,\n Input,\n InputAudioTrack,\n type InputTrack,\n InputVideoTrack,\n MP4,\n VideoSampleSink,\n} from \"mediabunny\";\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { type MediaSample, SampleBuffer } from \"../SampleBuffer\";\nimport { roundToMilliseconds } from \"./shared/PrecisionUtils\";\nimport {\n withTimeout,\n DEFAULT_MEDIABUNNY_TIMEOUT_MS,\n} from \"./shared/timeoutUtils\";\n\ninterface BufferedSeekingInputOptions {\n videoBufferSize?: number;\n audioBufferSize?: number;\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n startTimeOffsetMs?: number;\n}\n\nconst defaultOptions: BufferedSeekingInputOptions = {\n videoBufferSize: 30,\n audioBufferSize: 100,\n startTimeOffsetMs: 0,\n};\n\nexport class NoSample extends RangeError {}\n\nexport class ConcurrentSeekError extends RangeError {}\n\nexport class BufferedSeekingInput {\n private input: Input;\n private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();\n private trackBuffers: Map<number, SampleBuffer> = new Map();\n private options: BufferedSeekingInputOptions;\n // Separate locks for different operation types to prevent unnecessary blocking\n private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();\n private trackSeekPromises: Map<number, Promise<any>> = new Map();\n\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n private readonly startTimeOffsetMs: number;\n\n constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {\n const bufferSource = new BufferSource(arrayBuffer);\n const input = new Input({\n source: bufferSource,\n formats: [MP4],\n });\n this.input = input;\n this.options = { ...defaultOptions, ...options };\n this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;\n }\n\n // Buffer inspection API for testing\n getBufferSize(trackId: number): number {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? buffer.length : 0;\n }\n\n getBufferContents(trackId: number): readonly MediaSample[] {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? Object.freeze([...buffer.getContents()]) : [];\n }\n\n getBufferTimestamps(trackId: number): number[] {\n const contents = this.getBufferContents(trackId);\n return contents.map((sample) => sample.timestamp || 0);\n }\n\n clearBuffer(trackId: number): void {\n const buffer = this.trackBuffers.get(trackId);\n if (buffer) {\n buffer.clear();\n }\n }\n\n computeDuration() {\n return this.input.computeDuration();\n }\n\n async getTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getTracks(),\n 5000,\n \"BufferedSeekingInput.getTracks\",\n );\n const track = tracks.find((track) => track.id === trackId);\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getAudioTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getAudioTracks(),\n 5000,\n \"BufferedSeekingInput.getAudioTracks\",\n );\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"audio\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getVideoTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getVideoTracks(),\n 5000,\n \"BufferedSeekingInput.getVideoTracks\",\n );\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"video\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getFirstVideoTrack() {\n const tracks = await withTimeout(\n this.input.getVideoTracks(),\n 5000,\n \"BufferedSeekingInput.getFirstVideoTrack\",\n );\n return tracks[0];\n }\n\n async getFirstAudioTrack() {\n const tracks = await withTimeout(\n this.input.getAudioTracks(),\n 5000,\n \"BufferedSeekingInput.getFirstAudioTrack\",\n );\n return tracks[0];\n }\n\n getTrackIterator(track: InputTrack) {\n if (this.trackIterators.has(track.id)) {\n // biome-ignore lint/style/noNonNullAssertion: we know the map has the key\n return this.trackIterators.get(track.id)!;\n }\n\n const trackIterator = this.createTrackIterator(track);\n\n this.trackIterators.set(track.id, trackIterator);\n\n return trackIterator;\n }\n\n createTrackSampleSink(track: InputTrack) {\n if (track instanceof InputAudioTrack) {\n return new AudioSampleSink(track);\n }\n if (track instanceof InputVideoTrack) {\n return new VideoSampleSink(track);\n }\n throw new Error(`Unsupported track type ${track.type}`);\n }\n\n createTrackIterator(track: InputTrack) {\n const sampleSink = this.createTrackSampleSink(track);\n return sampleSink.samples();\n }\n\n createTrackBuffer(track: InputTrack) {\n if (track.type === \"audio\") {\n const bufferSize = this.options.audioBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n const bufferSize = this.options.videoBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n\n getTrackBuffer(track: InputTrack) {\n const maybeTrackBuffer = this.trackBuffers.get(track.id);\n\n if (maybeTrackBuffer) {\n return maybeTrackBuffer;\n }\n\n const trackBuffer = this.createTrackBuffer(track);\n this.trackBuffers.set(track.id, trackBuffer);\n return trackBuffer;\n }\n\n async seek(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seek\",\n {\n trackId,\n timeMs,\n startTimeOffsetMs: this.startTimeOffsetMs,\n },\n undefined,\n async (span) => {\n // Apply timeline offset to map user timeline to media timeline\n const mediaTimeMs = timeMs + this.startTimeOffsetMs;\n\n // Round using consistent precision handling\n const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);\n span.setAttribute(\"roundedMediaTimeMs\", roundedMediaTimeMs);\n\n // Serialize seek operations per track (but don't block iterator creation)\n const existingSeek = this.trackSeekPromises.get(trackId);\n if (existingSeek) {\n span.setAttribute(\"waitedForExistingSeek\", true);\n await existingSeek;\n }\n\n const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);\n this.trackSeekPromises.set(trackId, seekPromise);\n\n try {\n const result = await seekPromise;\n return result;\n } finally {\n this.trackSeekPromises.delete(trackId);\n }\n },\n );\n }\n\n private async resetIterator(track: InputTrack) {\n const trackBuffer = this.trackBuffers.get(track.id);\n trackBuffer?.clear();\n // Clean up iterator safely - wait for any ongoing iterator creation\n const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(\n track.id,\n );\n if (ongoingIteratorCreation) {\n await ongoingIteratorCreation;\n }\n\n const iterator = this.trackIterators.get(track.id);\n if (iterator) {\n try {\n await iterator.return?.();\n } catch (_error) {\n // Iterator cleanup failed, continue anyway\n }\n }\n this.trackIterators.delete(track.id);\n }\n\n #seekLock?: PromiseWithResolvers<void>;\n\n private async seekSafe(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seekSafe\",\n {\n trackId,\n timeMs,\n },\n undefined,\n async (span) => {\n if (this.#seekLock) {\n span.setAttribute(\"waitedForSeekLock\", true);\n await this.#seekLock.promise;\n }\n const seekLock = Promise.withResolvers<void>();\n this.#seekLock = seekLock;\n\n try {\n const track = await this.getTrack(trackId);\n span.setAttribute(\"trackType\", track.type);\n\n const trackBuffer = this.getTrackBuffer(track);\n\n const roundedTimeMs = roundToMilliseconds(timeMs);\n\n // Add timeout to detect if getFirstTimestamp hangs\n const timeoutMs = 5000;\n const firstTimestamp = await Promise.race([\n track.getFirstTimestamp(),\n new Promise<number>((_, reject) =>\n setTimeout(\n () =>\n reject(\n new Error(`getFirstTimestamp timeout after ${timeoutMs}ms`),\n ),\n timeoutMs,\n ),\n ),\n ]);\n const firstTimestampMs = roundToMilliseconds(firstTimestamp * 1000);\n\n span.setAttribute(\"firstTimestampMs\", firstTimestampMs);\n\n // Use tolerance for floating point comparison (0.01ms tolerance)\n // This handles rounding errors like 20916.666 vs 20916.667\n const PRECISION_TOLERANCE_MS = 0.01;\n if (roundedTimeMs < firstTimestampMs - PRECISION_TOLERANCE_MS) {\n console.error(\n `[BufferedSeekingInput.seekSafe] OUT_OF_BOUNDS trackId=${trackId} roundedTimeMs=${roundedTimeMs} firstTimestampMs=${firstTimestampMs}`,\n );\n throw new NoSample(\n `Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,\n );\n }\n\n // Check if we need to reset iterator for seeks outside current buffer range\n const bufferContents = trackBuffer.getContents();\n span.setAttribute(\"bufferContentsLength\", bufferContents.length);\n\n if (bufferContents.length > 0) {\n const bufferStartMs = roundToMilliseconds(\n trackBuffer.firstTimestamp * 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n\n if (roundedTimeMs < bufferStartMs) {\n span.setAttribute(\"resetIterator\", true);\n await this.resetIterator(track);\n }\n }\n\n const alreadyInBuffer = trackBuffer.find(timeMs);\n if (alreadyInBuffer) {\n span.setAttribute(\"foundInBuffer\", true);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n span.setAttribute(\n \"bufferTimestamps\",\n contents\n .map((s) => Math.round((s.timestamp || 0) * 1000))\n .slice(0, 10)\n .join(\",\"),\n );\n }\n return alreadyInBuffer;\n }\n\n // Buffer miss - record buffer state\n span.setAttribute(\"foundInBuffer\", false);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n span.setAttribute(\"requestedTimeMs\", Math.round(timeMs));\n\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n const firstSample = contents[0];\n const lastSample = contents[contents.length - 1];\n if (firstSample && lastSample) {\n const bufferStartMs = Math.round(\n (firstSample.timestamp || 0) * 1000,\n );\n const bufferEndMs = Math.round(\n ((lastSample.timestamp || 0) + (lastSample.duration || 0)) *\n 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n span.setAttribute(\"bufferEndMs\", bufferEndMs);\n span.setAttribute(\n \"bufferRangeMs\",\n `${bufferStartMs}-${bufferEndMs}`,\n );\n }\n }\n\n const iterator = this.getTrackIterator(track);\n let iterationCount = 0;\n const decodeStart = performance.now();\n\n while (true) {\n iterationCount++;\n const iterStart = performance.now();\n const { done, value: decodedSample } = await withTimeout(\n iterator.next(),\n DEFAULT_MEDIABUNNY_TIMEOUT_MS,\n `iterator.next() for ${track.type} track ${trackId} iteration ${iterationCount}`,\n );\n const iterEnd = performance.now();\n\n // Record individual iteration timing for first 5 iterations\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Ms`,\n Math.round((iterEnd - iterStart) * 100) / 100,\n );\n }\n\n if (decodedSample) {\n trackBuffer.push(decodedSample);\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Timestamp`,\n Math.round((decodedSample.timestamp || 0) * 1000),\n );\n }\n }\n\n const foundSample = trackBuffer.find(roundedTimeMs);\n if (foundSample) {\n const decodeEnd = performance.now();\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\n \"decodeMs\",\n Math.round((decodeEnd - decodeStart) * 100) / 100,\n );\n span.setAttribute(\n \"avgIterMs\",\n Math.round(((decodeEnd - decodeStart) / iterationCount) * 100) /\n 100,\n );\n span.setAttribute(\"foundSample\", true);\n span.setAttribute(\n \"foundTimestamp\",\n Math.round((foundSample.timestamp || 0) * 1000),\n );\n return foundSample;\n }\n if (done) {\n break;\n }\n }\n\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\"reachedEnd\", true);\n\n // Check if we're seeking to the exact end of the track (legitimate use case)\n const finalBufferContents = trackBuffer.getContents();\n if (finalBufferContents.length > 0) {\n const lastSample =\n finalBufferContents[finalBufferContents.length - 1];\n const lastSampleEndMs = roundToMilliseconds(\n ((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) *\n 1000,\n );\n\n // Only return last sample if seeking to exactly the track duration\n // (end of video) AND we have the final segment loaded\n const trackDurationMs = (await track.computeDuration()) * 1000;\n const isSeekingToTrackEnd =\n roundToMilliseconds(timeMs) ===\n roundToMilliseconds(trackDurationMs);\n const isAtEndOfTrack =\n roundToMilliseconds(timeMs) >= lastSampleEndMs;\n\n if (isSeekingToTrackEnd && isAtEndOfTrack) {\n span.setAttribute(\"returnedLastSample\", true);\n return lastSample;\n }\n }\n\n // For all other cases (seeking within track but outside buffer range), throw error\n // The caller should ensure the correct segment is loaded before seeking\n throw new NoSample(\n `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,\n );\n } finally {\n this.#seekLock = undefined;\n seekLock.resolve();\n }\n },\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BufferedSeekingInput.js","names":["defaultOptions: BufferedSeekingInputOptions","track","bufferSize","#seekLock","contents"],"sources":["../../../src/elements/EFMedia/BufferedSeekingInput.ts"],"sourcesContent":["import {\n AudioSampleSink,\n BufferSource,\n Input,\n InputAudioTrack,\n type InputTrack,\n InputVideoTrack,\n MP4,\n VideoSampleSink,\n} from \"mediabunny\";\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { type MediaSample, SampleBuffer } from \"../SampleBuffer\";\nimport { roundToMilliseconds } from \"./shared/PrecisionUtils\";\nimport { withTimeout, DEFAULT_MEDIABUNNY_TIMEOUT_MS } from \"./shared/timeoutUtils\";\n\ninterface BufferedSeekingInputOptions {\n videoBufferSize?: number;\n audioBufferSize?: number;\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n startTimeOffsetMs?: number;\n}\n\nconst defaultOptions: BufferedSeekingInputOptions = {\n videoBufferSize: 30,\n audioBufferSize: 100,\n startTimeOffsetMs: 0,\n};\n\nexport class NoSample extends RangeError {}\n\nexport class ConcurrentSeekError extends RangeError {}\n\nexport class BufferedSeekingInput {\n private input: Input;\n private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();\n private trackBuffers: Map<number, SampleBuffer> = new Map();\n private options: BufferedSeekingInputOptions;\n // Separate locks for different operation types to prevent unnecessary blocking\n private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();\n private trackSeekPromises: Map<number, Promise<any>> = new Map();\n\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n private readonly startTimeOffsetMs: number;\n\n constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {\n const bufferSource = new BufferSource(arrayBuffer);\n const input = new Input({\n source: bufferSource,\n formats: [MP4],\n });\n this.input = input;\n this.options = { ...defaultOptions, ...options };\n this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;\n }\n\n // Buffer inspection API for testing\n getBufferSize(trackId: number): number {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? buffer.length : 0;\n }\n\n getBufferContents(trackId: number): readonly MediaSample[] {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? Object.freeze([...buffer.getContents()]) : [];\n }\n\n getBufferTimestamps(trackId: number): number[] {\n const contents = this.getBufferContents(trackId);\n return contents.map((sample) => sample.timestamp || 0);\n }\n\n clearBuffer(trackId: number): void {\n const buffer = this.trackBuffers.get(trackId);\n if (buffer) {\n buffer.clear();\n }\n }\n\n computeDuration() {\n return this.input.computeDuration();\n }\n\n async getTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getTracks(),\n 5000,\n \"BufferedSeekingInput.getTracks\",\n );\n const track = tracks.find((track) => track.id === trackId);\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getAudioTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getAudioTracks(),\n 5000,\n \"BufferedSeekingInput.getAudioTracks\",\n );\n const track = tracks.find((track) => track.id === trackId && track.type === \"audio\");\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getVideoTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getVideoTracks(),\n 5000,\n \"BufferedSeekingInput.getVideoTracks\",\n );\n const track = tracks.find((track) => track.id === trackId && track.type === \"video\");\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getFirstVideoTrack() {\n const tracks = await withTimeout(\n this.input.getVideoTracks(),\n 5000,\n \"BufferedSeekingInput.getFirstVideoTrack\",\n );\n return tracks[0];\n }\n\n async getFirstAudioTrack() {\n const tracks = await withTimeout(\n this.input.getAudioTracks(),\n 5000,\n \"BufferedSeekingInput.getFirstAudioTrack\",\n );\n return tracks[0];\n }\n\n getTrackIterator(track: InputTrack) {\n if (this.trackIterators.has(track.id)) {\n // biome-ignore lint/style/noNonNullAssertion: we know the map has the key\n return this.trackIterators.get(track.id)!;\n }\n\n const trackIterator = this.createTrackIterator(track);\n\n this.trackIterators.set(track.id, trackIterator);\n\n return trackIterator;\n }\n\n createTrackSampleSink(track: InputTrack) {\n if (track instanceof InputAudioTrack) {\n return new AudioSampleSink(track);\n }\n if (track instanceof InputVideoTrack) {\n return new VideoSampleSink(track);\n }\n throw new Error(`Unsupported track type ${track.type}`);\n }\n\n createTrackIterator(track: InputTrack) {\n const sampleSink = this.createTrackSampleSink(track);\n return sampleSink.samples();\n }\n\n createTrackBuffer(track: InputTrack) {\n if (track.type === \"audio\") {\n const bufferSize = this.options.audioBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n const bufferSize = this.options.videoBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n\n getTrackBuffer(track: InputTrack) {\n const maybeTrackBuffer = this.trackBuffers.get(track.id);\n\n if (maybeTrackBuffer) {\n return maybeTrackBuffer;\n }\n\n const trackBuffer = this.createTrackBuffer(track);\n this.trackBuffers.set(track.id, trackBuffer);\n return trackBuffer;\n }\n\n async seek(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seek\",\n {\n trackId,\n timeMs,\n startTimeOffsetMs: this.startTimeOffsetMs,\n },\n undefined,\n async (span) => {\n // Apply timeline offset to map user timeline to media timeline\n const mediaTimeMs = timeMs + this.startTimeOffsetMs;\n\n // Round using consistent precision handling\n const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);\n span.setAttribute(\"roundedMediaTimeMs\", roundedMediaTimeMs);\n\n // Serialize seek operations per track (but don't block iterator creation)\n const existingSeek = this.trackSeekPromises.get(trackId);\n if (existingSeek) {\n span.setAttribute(\"waitedForExistingSeek\", true);\n await existingSeek;\n }\n\n const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);\n this.trackSeekPromises.set(trackId, seekPromise);\n\n try {\n const result = await seekPromise;\n return result;\n } finally {\n this.trackSeekPromises.delete(trackId);\n }\n },\n );\n }\n\n private async resetIterator(track: InputTrack) {\n const trackBuffer = this.trackBuffers.get(track.id);\n trackBuffer?.clear();\n // Clean up iterator safely - wait for any ongoing iterator creation\n const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(track.id);\n if (ongoingIteratorCreation) {\n await ongoingIteratorCreation;\n }\n\n const iterator = this.trackIterators.get(track.id);\n if (iterator) {\n try {\n await iterator.return?.();\n } catch (_error) {\n // Iterator cleanup failed, continue anyway\n }\n }\n this.trackIterators.delete(track.id);\n }\n\n #seekLock?: PromiseWithResolvers<void>;\n\n private async seekSafe(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seekSafe\",\n {\n trackId,\n timeMs,\n },\n undefined,\n async (span) => {\n if (this.#seekLock) {\n span.setAttribute(\"waitedForSeekLock\", true);\n await this.#seekLock.promise;\n }\n const seekLock = Promise.withResolvers<void>();\n this.#seekLock = seekLock;\n\n try {\n const track = await this.getTrack(trackId);\n span.setAttribute(\"trackType\", track.type);\n\n const trackBuffer = this.getTrackBuffer(track);\n\n const roundedTimeMs = roundToMilliseconds(timeMs);\n\n // Add timeout to detect if getFirstTimestamp hangs\n const timeoutMs = 5000;\n const firstTimestamp = await Promise.race([\n track.getFirstTimestamp(),\n new Promise<number>((_, reject) =>\n setTimeout(\n () => reject(new Error(`getFirstTimestamp timeout after ${timeoutMs}ms`)),\n timeoutMs,\n ),\n ),\n ]);\n const firstTimestampMs = roundToMilliseconds(firstTimestamp * 1000);\n\n span.setAttribute(\"firstTimestampMs\", firstTimestampMs);\n\n // Use tolerance for floating point comparison (0.01ms tolerance)\n // This handles rounding errors like 20916.666 vs 20916.667\n const PRECISION_TOLERANCE_MS = 0.01;\n if (roundedTimeMs < firstTimestampMs - PRECISION_TOLERANCE_MS) {\n console.error(\n `[BufferedSeekingInput.seekSafe] OUT_OF_BOUNDS trackId=${trackId} roundedTimeMs=${roundedTimeMs} firstTimestampMs=${firstTimestampMs}`,\n );\n throw new NoSample(\n `Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,\n );\n }\n\n // Check if we need to reset iterator for seeks outside current buffer range\n const bufferContents = trackBuffer.getContents();\n span.setAttribute(\"bufferContentsLength\", bufferContents.length);\n\n if (bufferContents.length > 0) {\n const bufferStartMs = roundToMilliseconds(trackBuffer.firstTimestamp * 1000);\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n\n if (roundedTimeMs < bufferStartMs) {\n span.setAttribute(\"resetIterator\", true);\n await this.resetIterator(track);\n }\n }\n\n const alreadyInBuffer = trackBuffer.find(timeMs);\n if (alreadyInBuffer) {\n span.setAttribute(\"foundInBuffer\", true);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n span.setAttribute(\n \"bufferTimestamps\",\n contents\n .map((s) => Math.round((s.timestamp || 0) * 1000))\n .slice(0, 10)\n .join(\",\"),\n );\n }\n return alreadyInBuffer;\n }\n\n // Buffer miss - record buffer state\n span.setAttribute(\"foundInBuffer\", false);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n span.setAttribute(\"requestedTimeMs\", Math.round(timeMs));\n\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n const firstSample = contents[0];\n const lastSample = contents[contents.length - 1];\n if (firstSample && lastSample) {\n const bufferStartMs = Math.round((firstSample.timestamp || 0) * 1000);\n const bufferEndMs = Math.round(\n ((lastSample.timestamp || 0) + (lastSample.duration || 0)) * 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n span.setAttribute(\"bufferEndMs\", bufferEndMs);\n span.setAttribute(\"bufferRangeMs\", `${bufferStartMs}-${bufferEndMs}`);\n }\n }\n\n const iterator = this.getTrackIterator(track);\n let iterationCount = 0;\n const decodeStart = performance.now();\n\n while (true) {\n iterationCount++;\n const iterStart = performance.now();\n const { done, value: decodedSample } = await withTimeout(\n iterator.next(),\n DEFAULT_MEDIABUNNY_TIMEOUT_MS,\n `iterator.next() for ${track.type} track ${trackId} iteration ${iterationCount}`,\n );\n const iterEnd = performance.now();\n\n // Record individual iteration timing for first 5 iterations\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Ms`,\n Math.round((iterEnd - iterStart) * 100) / 100,\n );\n }\n\n if (decodedSample) {\n trackBuffer.push(decodedSample);\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Timestamp`,\n Math.round((decodedSample.timestamp || 0) * 1000),\n );\n }\n }\n\n const foundSample = trackBuffer.find(roundedTimeMs);\n if (foundSample) {\n const decodeEnd = performance.now();\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\"decodeMs\", Math.round((decodeEnd - decodeStart) * 100) / 100);\n span.setAttribute(\n \"avgIterMs\",\n Math.round(((decodeEnd - decodeStart) / iterationCount) * 100) / 100,\n );\n span.setAttribute(\"foundSample\", true);\n span.setAttribute(\"foundTimestamp\", Math.round((foundSample.timestamp || 0) * 1000));\n return foundSample;\n }\n if (done) {\n break;\n }\n }\n\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\"reachedEnd\", true);\n\n // Check if we're seeking to the exact end of the track (legitimate use case)\n const finalBufferContents = trackBuffer.getContents();\n if (finalBufferContents.length > 0) {\n const lastSample = finalBufferContents[finalBufferContents.length - 1];\n const lastSampleEndMs = roundToMilliseconds(\n ((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) * 1000,\n );\n\n // Only return last sample if seeking to exactly the track duration\n // (end of video) AND we have the final segment loaded\n const trackDurationMs = (await track.computeDuration()) * 1000;\n const isSeekingToTrackEnd =\n roundToMilliseconds(timeMs) === roundToMilliseconds(trackDurationMs);\n const isAtEndOfTrack = roundToMilliseconds(timeMs) >= lastSampleEndMs;\n\n if (isSeekingToTrackEnd && isAtEndOfTrack) {\n span.setAttribute(\"returnedLastSample\", true);\n return lastSample;\n }\n }\n\n // For all other cases (seeking within track but outside buffer range), throw error\n // The caller should ensure the correct segment is loaded before seeking\n throw new NoSample(\n `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,\n );\n } finally {\n this.#seekLock = undefined;\n seekLock.resolve();\n }\n },\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CachedFetcher.js","names":["#fetchFn","#fetchWithCache","#handleAbortForCachedRequest"],"sources":["../../../src/elements/EFMedia/CachedFetcher.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\n\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024);\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport interface FetchFn {\n (\n url: string,\n init?: { headers?: Record<string, string>; signal?: AbortSignal },\n ): Promise<Response>;\n}\n\nexport class CachedFetcher {\n #fetchFn: FetchFn;\n\n constructor(fetchFn: FetchFn) {\n this.#fetchFn = fetchFn;\n }\n\n has(key: string): boolean {\n return mediaCache.has(key);\n }\n\n async fetchArrayBuffer(
|
|
1
|
+
{"version":3,"file":"CachedFetcher.js","names":["#fetchFn","#fetchWithCache","#handleAbortForCachedRequest"],"sources":["../../../src/elements/EFMedia/CachedFetcher.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\n\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024);\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport interface FetchFn {\n (\n url: string,\n init?: { headers?: Record<string, string>; signal?: AbortSignal },\n ): Promise<Response>;\n}\n\nexport class CachedFetcher {\n #fetchFn: FetchFn;\n\n constructor(fetchFn: FetchFn) {\n this.#fetchFn = fetchFn;\n }\n\n has(key: string): boolean {\n return mediaCache.has(key);\n }\n\n async fetchArrayBuffer(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.#fetchWithCache(url, { responseType: \"arrayBuffer\", signal });\n }\n\n async fetchJson(url: string, signal?: AbortSignal): Promise<any> {\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.#fetchWithCache(url, { responseType: \"json\", signal });\n }\n\n async #fetchWithCache(\n url: string,\n options: {\n responseType: \"arrayBuffer\" | \"json\";\n headers?: Record<string, string>;\n signal?: AbortSignal;\n },\n ): Promise<any> {\n return withSpan(\n \"cachedFetcher.fetchWithCache\",\n {\n url: url.length > 100 ? `${url.substring(0, 100)}...` : url,\n responseType: options.responseType,\n },\n undefined,\n async (span) => {\n const { responseType, headers, signal } = options;\n\n const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;\n\n const cached = mediaCache.get(cacheKey);\n if (cached) {\n span.setAttribute(\"cacheHit\", true);\n if (signal) {\n return this.#handleAbortForCachedRequest(cached, signal);\n }\n return cached;\n }\n\n span.setAttribute(\"cacheHit\", false);\n\n const promise = globalRequestDeduplicator.executeRequest(cacheKey, async () => {\n try {\n const response = await this.#fetchFn(url, { headers, signal });\n const contentType = response.headers.get(\"content-type\");\n\n if (responseType === \"json\") {\n if (\n !response.ok ||\n (contentType &&\n !contentType.includes(\"application/json\") &&\n !contentType.includes(\"text/json\"))\n ) {\n const text = await response.clone().text();\n if (!response.ok) {\n throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);\n }\n throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);\n }\n try {\n return await response.json();\n } catch (error) {\n throw new Error(\n `Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n if (!response.ok) {\n const text = await response.clone().text();\n throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);\n }\n\n const buffer = await response.arrayBuffer();\n span.setAttribute(\"sizeBytes\", buffer.byteLength);\n return buffer;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n throw error;\n }\n });\n\n mediaCache.set(cacheKey, promise);\n\n promise.catch((error) => {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n });\n\n if (signal) {\n return this.#handleAbortForCachedRequest(promise, signal);\n }\n\n return promise;\n },\n );\n }\n\n #handleAbortForCachedRequest<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {\n if (signal.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const abortPromise = new Promise<never>((_, reject) => {\n signal.addEventListener(\"abort\", () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n });\n });\n abortPromise.catch(() => {});\n\n const racePromise = Promise.race([promise, abortPromise]);\n racePromise.catch(() => {});\n return racePromise;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaEngine.js","names":["toValidate: TrackRef[]","indexData: IndexData"],"sources":["../../../src/elements/EFMedia/MediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type {\n ManifestResponse,\n ThumbnailResult,\n} from \"../../transcoding/types/index.js\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator.js\";\nimport { CachedFetcher, type FetchFn } from \"./CachedFetcher.js\";\nimport {\n type SegmentIndex,\n type TrackRef,\n type TrackSet,\n createFragmentIndex,\n createManifestIndex,\n} from \"./SegmentIndex.js\";\nimport {\n type SegmentTransport,\n createByteRangeTransport,\n createUrlTransport,\n} from \"./SegmentTransport.js\";\nimport {\n type TimingModel,\n createByteRangeTiming,\n createJitTiming,\n} from \"./TimingModel.js\";\n\nexport interface MediaEngine {\n readonly durationMs: number;\n readonly src: string;\n readonly index: SegmentIndex;\n readonly transport: SegmentTransport;\n readonly timing: TimingModel;\n readonly tracks: TrackSet;\n extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]>;\n}\n\nexport function createMediaEngine(\n index: SegmentIndex,\n transport: SegmentTransport,\n timing: TimingModel,\n src: string,\n): MediaEngine {\n return {\n durationMs: index.durationMs,\n src,\n index,\n transport,\n timing,\n tracks: index.tracks,\n\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const track = index.tracks.video ?? index.tracks.scrub;\n if (!track) {\n return timestamps.map(() => null);\n }\n\n // Use dynamic import to keep ThumbnailExtractor out of initial bundle\n const { ThumbnailExtractor } =\n await import(\"./shared/ThumbnailExtractor.js\");\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const engine = this as MediaEngine;\n const extractor = new ThumbnailExtractor(engine);\n return extractor.extractThumbnails(\n timestamps,\n track,\n index.durationMs,\n signal,\n );\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Index data fetching\n// ---------------------------------------------------------------------------\n\ntype IndexData =\n | {\n type: \"fragment\";\n data: Record<number, TrackFragmentIndex>;\n src: string;\n apiHost: string;\n fileId: string;\n }\n | {\n type: \"manifest\";\n data: ManifestResponse;\n src: string;\n };\n\nexport async function fetchFileIndex(\n fetchFn: FetchFn,\n fileId: string,\n apiHost: string,\n signal?: AbortSignal,\n): Promise<Record<number, TrackFragmentIndex>> {\n const url = `${apiHost}/api/v1/files/${fileId}/index`;\n const response = await fetchFn(url, { signal });\n\n signal?.throwIfAborted();\n\n const contentType = response.headers.get(\"content-type\");\n if (\n !response.ok ||\n (contentType && !contentType.includes(\"application/json\"))\n ) {\n const text = await response.clone().text();\n if (!response.ok) {\n throw new Error(\n `Failed to fetch asset index: ${response.status} ${text}`,\n );\n }\n throw new Error(\n `Expected JSON but got ${contentType}: ${text.substring(0, 100)}`,\n );\n }\n\n try {\n const data = await response.json();\n signal?.throwIfAborted();\n return data as Record<number, TrackFragmentIndex>;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new Error(\n `Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport async function validateTrackAccess(\n transport: SegmentTransport,\n tracks: TrackSet,\n requiredTracks: \"audio\" | \"video\" | \"both\",\n signal?: AbortSignal,\n): Promise<void> {\n if (!signal) return;\n\n const toValidate: TrackRef[] = [];\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n\n if (needsVideo && tracks.video) toValidate.push(tracks.video);\n if (needsAudio && tracks.audio) toValidate.push(tracks.audio);\n\n for (const track of toValidate) {\n signal.throwIfAborted();\n try {\n await transport.fetchInitSegment(track, signal);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") &&\n error.message.includes(\"401\")))\n ) {\n throw new Error(\n `${track.role} segments require authentication: ${error.message}`,\n );\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Engine composition from index data\n// ---------------------------------------------------------------------------\n\nfunction buildEngineComponents(\n indexData: IndexData,\n fetcher: CachedFetcher,\n): {\n index: SegmentIndex;\n transport: SegmentTransport;\n timing: TimingModel;\n src: string;\n} {\n switch (indexData.type) {\n case \"fragment\": {\n const index = createFragmentIndex(indexData.data, indexData.src);\n const transport = createByteRangeTransport(\n indexData.data,\n indexData.fileId,\n indexData.apiHost,\n fetcher,\n );\n const timing = createByteRangeTiming(indexData.data);\n return { index, transport, timing, src: indexData.src };\n }\n\n case \"manifest\": {\n const index = createManifestIndex(indexData.data);\n const transport = createUrlTransport({\n fetcher,\n src: indexData.data.sourceUrl,\n templates: indexData.data.endpoints,\n audioTrackId: undefined,\n videoTrackId: undefined,\n });\n const timing = createJitTiming();\n return { index, transport, timing, src: indexData.data.sourceUrl };\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Top-level factory — called by EFMedia.#createMediaEngine\n// ---------------------------------------------------------------------------\n\nexport interface CreateMediaEngineOptions {\n src?: string | null;\n fileId?: string | null;\n apiHost?: string;\n requiredTracks: \"audio\" | \"video\" | \"both\";\n fetchFn: FetchFn;\n urlGenerator: UrlGenerator;\n signal?: AbortSignal;\n}\n\nexport async function createMediaEngineFromSource(\n opts: CreateMediaEngineOptions,\n): Promise<MediaEngine | undefined> {\n const {\n src,\n fileId,\n apiHost,\n requiredTracks,\n fetchFn,\n urlGenerator,\n signal,\n } = opts;\n\n const fetcher = new CachedFetcher(fetchFn);\n\n let indexData: IndexData;\n\n // File-ID mode: byte-range transport against cloud API\n if (fileId !== null && fileId !== undefined && fileId.trim() !== \"\") {\n if (!apiHost) {\n throw new Error(\"API host is required for file-id mode\");\n }\n const data = await fetchFileIndex(fetchFn, fileId, apiHost, signal);\n signal?.throwIfAborted();\n indexData = {\n type: \"fragment\",\n data,\n src: fileId,\n apiHost,\n fileId,\n };\n } else if (!src || typeof src !== \"string\" || src.trim() === \"\") {\n return undefined;\n } else {\n // Src-based mode: always fetch manifest from the server.\n // The server decides the transcoding strategy (local ffmpeg or cloud JIT).\n const manifestSrc = resolveManifestSrc(src, apiHost);\n const url = urlGenerator.generateManifestUrl(manifestSrc);\n const manifest = await fetcher.fetchJson(url, signal);\n signal?.throwIfAborted();\n indexData = { type: \"manifest\", data: manifest, src: manifest.sourceUrl };\n }\n\n const {\n index,\n transport,\n timing,\n src: engineSrc,\n } = buildEngineComponents(indexData, fetcher);\n\n await validateTrackAccess(transport, index.tracks, requiredTracks, signal);\n\n return createMediaEngine(index, transport, timing, engineSrc);\n}\n\n/**\n * Resolve a src value to the URL the server should transcode.\n * - Remote URLs (http/https) pass through as-is\n * - Local paths are made absolute using apiHost when available\n */\nfunction resolveManifestSrc(src: string, apiHost?: string): string {\n const lower = src.toLowerCase();\n if (lower.startsWith(\"http://\") || lower.startsWith(\"https://\")) {\n return src;\n }\n if (apiHost) {\n const base = apiHost.replace(/\\/$/, \"\");\n const normalizedPath = src.replace(/^\\.\\//, \"/src/\");\n return `${base}${normalizedPath.startsWith(\"/\") ? \"\" : \"/\"}${normalizedPath}`;\n }\n return src;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MediaEngine.js","names":["toValidate: TrackRef[]","indexData: IndexData"],"sources":["../../../src/elements/EFMedia/MediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { ManifestResponse, ThumbnailResult } from \"../../transcoding/types/index.js\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator.js\";\nimport { CachedFetcher, type FetchFn } from \"./CachedFetcher.js\";\nimport {\n type SegmentIndex,\n type TrackRef,\n type TrackSet,\n createFragmentIndex,\n createManifestIndex,\n} from \"./SegmentIndex.js\";\nimport {\n type SegmentTransport,\n createByteRangeTransport,\n createUrlTransport,\n} from \"./SegmentTransport.js\";\nimport { type TimingModel, createByteRangeTiming, createJitTiming } from \"./TimingModel.js\";\n\nexport interface MediaEngine {\n readonly durationMs: number;\n readonly src: string;\n readonly index: SegmentIndex;\n readonly transport: SegmentTransport;\n readonly timing: TimingModel;\n readonly tracks: TrackSet;\n extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]>;\n}\n\nexport function createMediaEngine(\n index: SegmentIndex,\n transport: SegmentTransport,\n timing: TimingModel,\n src: string,\n): MediaEngine {\n return {\n durationMs: index.durationMs,\n src,\n index,\n transport,\n timing,\n tracks: index.tracks,\n\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const track = index.tracks.video ?? index.tracks.scrub;\n if (!track) {\n return timestamps.map(() => null);\n }\n\n // Use dynamic import to keep ThumbnailExtractor out of initial bundle\n const { ThumbnailExtractor } = await import(\"./shared/ThumbnailExtractor.js\");\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const engine = this as MediaEngine;\n const extractor = new ThumbnailExtractor(engine);\n return extractor.extractThumbnails(timestamps, track, index.durationMs, signal);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Index data fetching\n// ---------------------------------------------------------------------------\n\ntype IndexData =\n | {\n type: \"fragment\";\n data: Record<number, TrackFragmentIndex>;\n src: string;\n apiHost: string;\n fileId: string;\n }\n | {\n type: \"manifest\";\n data: ManifestResponse;\n src: string;\n };\n\nexport async function fetchFileIndex(\n fetchFn: FetchFn,\n fileId: string,\n apiHost: string,\n signal?: AbortSignal,\n): Promise<Record<number, TrackFragmentIndex>> {\n const url = `${apiHost}/api/v1/files/${fileId}/index`;\n const response = await fetchFn(url, { signal });\n\n signal?.throwIfAborted();\n\n const contentType = response.headers.get(\"content-type\");\n if (!response.ok || (contentType && !contentType.includes(\"application/json\"))) {\n const text = await response.clone().text();\n if (!response.ok) {\n throw new Error(`Failed to fetch asset index: ${response.status} ${text}`);\n }\n throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);\n }\n\n try {\n const data = await response.json();\n signal?.throwIfAborted();\n return data as Record<number, TrackFragmentIndex>;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new Error(\n `Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport async function validateTrackAccess(\n transport: SegmentTransport,\n tracks: TrackSet,\n requiredTracks: \"audio\" | \"video\" | \"both\",\n signal?: AbortSignal,\n): Promise<void> {\n if (!signal) return;\n\n const toValidate: TrackRef[] = [];\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n\n if (needsVideo && tracks.video) toValidate.push(tracks.video);\n if (needsAudio && tracks.audio) toValidate.push(tracks.audio);\n\n for (const track of toValidate) {\n signal.throwIfAborted();\n try {\n await transport.fetchInitSegment(track, signal);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`${track.role} segments require authentication: ${error.message}`);\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Engine composition from index data\n// ---------------------------------------------------------------------------\n\nfunction buildEngineComponents(\n indexData: IndexData,\n fetcher: CachedFetcher,\n): {\n index: SegmentIndex;\n transport: SegmentTransport;\n timing: TimingModel;\n src: string;\n} {\n switch (indexData.type) {\n case \"fragment\": {\n const index = createFragmentIndex(indexData.data, indexData.src);\n const transport = createByteRangeTransport(\n indexData.data,\n indexData.fileId,\n indexData.apiHost,\n fetcher,\n );\n const timing = createByteRangeTiming(indexData.data);\n return { index, transport, timing, src: indexData.src };\n }\n\n case \"manifest\": {\n const index = createManifestIndex(indexData.data);\n const transport = createUrlTransport({\n fetcher,\n src: indexData.data.sourceUrl,\n templates: indexData.data.endpoints,\n audioTrackId: undefined,\n videoTrackId: undefined,\n });\n const timing = createJitTiming();\n return { index, transport, timing, src: indexData.data.sourceUrl };\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Top-level factory — called by EFMedia.#createMediaEngine\n// ---------------------------------------------------------------------------\n\nexport interface CreateMediaEngineOptions {\n src?: string | null;\n fileId?: string | null;\n apiHost?: string;\n requiredTracks: \"audio\" | \"video\" | \"both\";\n fetchFn: FetchFn;\n urlGenerator: UrlGenerator;\n signal?: AbortSignal;\n}\n\nexport async function createMediaEngineFromSource(\n opts: CreateMediaEngineOptions,\n): Promise<MediaEngine | undefined> {\n const { src, fileId, apiHost, requiredTracks, fetchFn, urlGenerator, signal } = opts;\n\n const fetcher = new CachedFetcher(fetchFn);\n\n let indexData: IndexData;\n\n // File-ID mode: byte-range transport against cloud API\n if (fileId !== null && fileId !== undefined && fileId.trim() !== \"\") {\n if (!apiHost) {\n throw new Error(\"API host is required for file-id mode\");\n }\n const data = await fetchFileIndex(fetchFn, fileId, apiHost, signal);\n signal?.throwIfAborted();\n indexData = {\n type: \"fragment\",\n data,\n src: fileId,\n apiHost,\n fileId,\n };\n } else if (!src || typeof src !== \"string\" || src.trim() === \"\") {\n return undefined;\n } else {\n // Src-based mode: always fetch manifest from the server.\n // The server decides the transcoding strategy (local ffmpeg or cloud JIT).\n const manifestSrc = resolveManifestSrc(src, apiHost);\n const url = urlGenerator.generateManifestUrl(manifestSrc);\n const manifest = await fetcher.fetchJson(url, signal);\n signal?.throwIfAborted();\n indexData = { type: \"manifest\", data: manifest, src: manifest.sourceUrl };\n }\n\n const { index, transport, timing, src: engineSrc } = buildEngineComponents(indexData, fetcher);\n\n await validateTrackAccess(transport, index.tracks, requiredTracks, signal);\n\n return createMediaEngine(index, transport, timing, engineSrc);\n}\n\n/**\n * Resolve a src value to the URL the server should transcode.\n * - Remote URLs (http/https) pass through as-is\n * - Local paths are made absolute using apiHost when available\n */\nfunction resolveManifestSrc(src: string, apiHost?: string): string {\n const lower = src.toLowerCase();\n if (lower.startsWith(\"http://\") || lower.startsWith(\"https://\")) {\n return src;\n }\n if (apiHost) {\n const base = apiHost.replace(/\\/$/, \"\");\n const normalizedPath = src.replace(/^\\.\\//, \"/src/\");\n return `${base}${normalizedPath.startsWith(\"/\") ? \"\" : \"/\"}${normalizedPath}`;\n }\n return src;\n}\n"],"mappings":";;;;;;AA+BA,SAAgB,kBACd,OACA,WACA,QACA,KACa;AACb,QAAO;EACL,YAAY,MAAM;EAClB;EACA;EACA;EACA;EACA,QAAQ,MAAM;EAEd,MAAM,kBACJ,YACA,QACqC;GACrC,MAAM,QAAQ,MAAM,OAAO,SAAS,MAAM,OAAO;AACjD,OAAI,CAAC,MACH,QAAO,WAAW,UAAU,KAAK;GAInC,MAAM,EAAE,uBAAuB,MAAM,OAAO;AAI5C,UADkB,IAAI,mBADP,KACiC,CAC/B,kBAAkB,YAAY,OAAO,MAAM,YAAY,OAAO;;EAElF;;AAqBH,eAAsB,eACpB,SACA,QACA,SACA,QAC6C;CAE7C,MAAM,WAAW,MAAM,QADX,GAAG,QAAQ,gBAAgB,OAAO,SACV,EAAE,QAAQ,CAAC;AAE/C,SAAQ,gBAAgB;CAExB,MAAM,cAAc,SAAS,QAAQ,IAAI,eAAe;AACxD,KAAI,CAAC,SAAS,MAAO,eAAe,CAAC,YAAY,SAAS,mBAAmB,EAAG;EAC9E,MAAM,OAAO,MAAM,SAAS,OAAO,CAAC,MAAM;AAC1C,MAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,gCAAgC,SAAS,OAAO,GAAG,OAAO;AAE5E,QAAM,IAAI,MAAM,yBAAyB,YAAY,IAAI,KAAK,UAAU,GAAG,IAAI,GAAG;;AAGpF,KAAI;EACF,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,UAAQ,gBAAgB;AACxB,SAAO;UACA,OAAO;AACd,MAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,QAAM,IAAI,MACR,kCAAkC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GACzF;;;AAQL,eAAsB,oBACpB,WACA,QACA,gBACA,QACe;AACf,KAAI,CAAC,OAAQ;CAEb,MAAMA,aAAyB,EAAE;CACjC,MAAM,aAAa,mBAAmB,WAAW,mBAAmB;CACpE,MAAM,aAAa,mBAAmB,WAAW,mBAAmB;AAEpE,KAAI,cAAc,OAAO,MAAO,YAAW,KAAK,OAAO,MAAM;AAC7D,KAAI,cAAc,OAAO,MAAO,YAAW,KAAK,OAAO,MAAM;AAE7D,MAAK,MAAM,SAAS,YAAY;AAC9B,SAAO,gBAAgB;AACvB,MAAI;AACF,SAAM,UAAU,iBAAiB,OAAO,OAAO;WACxC,OAAO;AACd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,OACE,iBAAiB,UAChB,MAAM,QAAQ,SAAS,MAAM,IAC5B,MAAM,QAAQ,SAAS,eAAe,IACrC,MAAM,QAAQ,SAAS,kBAAkB,IAAI,MAAM,QAAQ,SAAS,MAAM,EAE7E,OAAM,IAAI,MAAM,GAAG,MAAM,KAAK,oCAAoC,MAAM,UAAU;;;;AAU1F,SAAS,sBACP,WACA,SAMA;AACA,SAAQ,UAAU,MAAlB;EACE,KAAK,WASH,QAAO;GAAE,OARK,oBAAoB,UAAU,MAAM,UAAU,IAAI;GAQhD,WAPE,yBAChB,UAAU,MACV,UAAU,QACV,UAAU,SACV,QACD;GAE0B,QADZ,sBAAsB,UAAU,KAAK;GACjB,KAAK,UAAU;GAAK;EAGzD,KAAK,WAUH,QAAO;GAAE,OATK,oBAAoB,UAAU,KAAK;GASjC,WARE,mBAAmB;IACnC;IACA,KAAK,UAAU,KAAK;IACpB,WAAW,UAAU,KAAK;IAC1B,cAAc;IACd,cAAc;IACf,CAAC;GAEyB,QADZ,iBAAiB;GACG,KAAK,UAAU,KAAK;GAAW;;;AAmBxE,eAAsB,4BACpB,MACkC;CAClC,MAAM,EAAE,KAAK,QAAQ,SAAS,gBAAgB,SAAS,cAAc,WAAW;CAEhF,MAAM,UAAU,IAAI,cAAc,QAAQ;CAE1C,IAAIC;AAGJ,KAAI,WAAW,QAAQ,WAAW,UAAa,OAAO,MAAM,KAAK,IAAI;AACnE,MAAI,CAAC,QACH,OAAM,IAAI,MAAM,wCAAwC;EAE1D,MAAM,OAAO,MAAM,eAAe,SAAS,QAAQ,SAAS,OAAO;AACnE,UAAQ,gBAAgB;AACxB,cAAY;GACV,MAAM;GACN;GACA,KAAK;GACL;GACA;GACD;YACQ,CAAC,OAAO,OAAO,QAAQ,YAAY,IAAI,MAAM,KAAK,GAC3D;MACK;EAGL,MAAM,cAAc,mBAAmB,KAAK,QAAQ;EACpD,MAAM,MAAM,aAAa,oBAAoB,YAAY;EACzD,MAAM,WAAW,MAAM,QAAQ,UAAU,KAAK,OAAO;AACrD,UAAQ,gBAAgB;AACxB,cAAY;GAAE,MAAM;GAAY,MAAM;GAAU,KAAK,SAAS;GAAW;;CAG3E,MAAM,EAAE,OAAO,WAAW,QAAQ,KAAK,cAAc,sBAAsB,WAAW,QAAQ;AAE9F,OAAM,oBAAoB,WAAW,MAAM,QAAQ,gBAAgB,OAAO;AAE1E,QAAO,kBAAkB,OAAO,WAAW,QAAQ,UAAU;;;;;;;AAQ/D,SAAS,mBAAmB,KAAa,SAA0B;CACjE,MAAM,QAAQ,IAAI,aAAa;AAC/B,KAAI,MAAM,WAAW,UAAU,IAAI,MAAM,WAAW,WAAW,CAC7D,QAAO;AAET,KAAI,SAAS;EACX,MAAM,OAAO,QAAQ,QAAQ,OAAO,GAAG;EACvC,MAAM,iBAAiB,IAAI,QAAQ,SAAS,QAAQ;AACpD,SAAO,GAAG,OAAO,eAAe,WAAW,IAAI,GAAG,KAAK,MAAM;;AAE/D,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentIndex.js","names":["tracks: TrackSet","distance: number","ranges: SegmentTimeRange[]","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/SegmentIndex.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { ManifestResponse } from \"../../transcoding/types/index.js\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils.js\";\n\nexport type TrackRole = \"video\" | \"audio\" | \"scrub\";\n\nexport interface TrackRef {\n readonly role: TrackRole;\n readonly id: string | number;\n readonly src: string;\n readonly segmentDurationMs?: number;\n readonly segmentDurationsMs?: number[];\n readonly startTimeOffsetMs?: number;\n}\n\nexport interface TrackSet {\n video?: TrackRef;\n audio?: TrackRef;\n scrub?: TrackRef;\n}\n\nexport interface SegmentTimeRange {\n segmentId: number;\n startMs: number;\n endMs: number;\n}\n\nexport interface SegmentIndex {\n readonly durationMs: number;\n readonly tracks: TrackSet;\n segmentAt(timeMs: number, track: TrackRef): number | undefined;\n segmentsInRange(\n fromMs: number,\n toMs: number,\n track: TrackRef,\n ): SegmentTimeRange[];\n}\n\n// ---------------------------------------------------------------------------\n// FragmentIndex — backed by TrackFragmentIndex (local and file-id files)\n// ---------------------------------------------------------------------------\n\nexport function createFragmentIndex(\n data: Record<number, TrackFragmentIndex>,\n src: string,\n): SegmentIndex {\n const longestFragment = Object.values(data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n const durationMs = longestFragment * 1000;\n\n const audioTrack = Object.values(data).find((t) => t.type === \"audio\");\n const videoTrack = Object.values(data).find(\n (t) => t.type === \"video\" && t.track !== undefined && t.track > 0,\n );\n const scrubTrack = data[-1];\n\n const tracks: TrackSet = {};\n\n if (videoTrack && videoTrack.track !== undefined) {\n tracks.video = {\n role: \"video\",\n id: videoTrack.track,\n src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n }\n\n if (audioTrack && audioTrack.track !== undefined) {\n tracks.audio = {\n role: \"audio\",\n id: audioTrack.track,\n src,\n };\n }\n\n if (scrubTrack && scrubTrack.track !== undefined) {\n const segmentDurationsMs =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map(\n (s) => (s.duration / scrubTrack.timescale) * 1000,\n )\n : undefined;\n tracks.scrub = {\n role: \"scrub\",\n id: scrubTrack.track,\n src,\n segmentDurationMs: 30000,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n return {\n durationMs,\n tracks,\n\n segmentAt(timeMs: number, track: TrackRef): number | undefined {\n const trackId =\n typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n const trackData = data[trackId];\n if (!trackData) {\n throw new Error(`Track ${trackId} not found`);\n }\n const { timescale, segments } = trackData;\n\n const startTimeOffsetMs = track.startTimeOffsetMs || 0;\n const offsetSeekTimeMs = roundToMilliseconds(timeMs + startTimeOffsetMs);\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n for (let i = segments.length - 1; i >= 0; i--) {\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Gap handling: find nearest segment\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segment.cts) {\n distance = segment.cts - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n distance = scaledSeekTime - segmentEndTime;\n } else {\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n },\n\n segmentsInRange(\n fromMs: number,\n toMs: number,\n track: TrackRef,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs) return [];\n\n const trackId =\n typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n const trackData = data[trackId];\n if (!trackData) return [];\n\n const { timescale, segments } = trackData;\n const ranges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]!;\n const segmentStartMs = (segment.cts / timescale) * 1000;\n const segmentEndMs =\n ((segment.cts + segment.duration) / timescale) * 1000;\n\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n ranges.push({\n segmentId: i,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return ranges;\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// ManifestIndex — backed by ManifestResponse (JIT transcoding)\n// ---------------------------------------------------------------------------\n\nexport function createManifestIndex(manifest: ManifestResponse): SegmentIndex {\n const durationMs = manifest.durationMs;\n const tracks: TrackSet = {};\n\n if (manifest.videoRenditions && manifest.videoRenditions.length > 0) {\n const r = manifest.videoRenditions[0]!;\n tracks.video = {\n role: \"video\",\n id: r.id,\n src: manifest.sourceUrl,\n segmentDurationMs: r.segmentDurationMs,\n segmentDurationsMs: r.segmentDurationsMs,\n startTimeOffsetMs: r.startTimeOffsetMs,\n };\n\n const scrubRendition = manifest.videoRenditions.find(\n (v) => v.id === \"scrub\",\n );\n if (scrubRendition) {\n tracks.scrub = {\n role: \"scrub\",\n id: scrubRendition.id,\n src: manifest.sourceUrl,\n segmentDurationMs: scrubRendition.segmentDurationMs,\n segmentDurationsMs: scrubRendition.segmentDurationsMs,\n startTimeOffsetMs: scrubRendition.startTimeOffsetMs,\n };\n }\n }\n\n if (manifest.audioRenditions && manifest.audioRenditions.length > 0) {\n const r = manifest.audioRenditions[0]!;\n tracks.audio = {\n role: \"audio\",\n id: r.id,\n src: manifest.sourceUrl,\n segmentDurationMs: r.segmentDurationMs,\n segmentDurationsMs: r.segmentDurationsMs,\n startTimeOffsetMs: r.startTimeOffsetMs,\n };\n }\n\n function computeSegmentIdForTrack(\n desiredSeekTimeMs: number,\n track: TrackRef,\n ): number | undefined {\n if (desiredSeekTimeMs > durationMs) {\n return undefined;\n }\n\n if (track.segmentDurationsMs && track.segmentDurationsMs.length > 0) {\n let cumulativeTime = 0;\n for (let i = 0; i < track.segmentDurationsMs.length; i++) {\n const segmentDuration = track.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = cumulativeTime + segmentDuration;\n\n const isLastSegment = i === track.segmentDurationsMs.length - 1;\n const includesEndTime =\n isLastSegment && desiredSeekTimeMs === durationMs;\n\n if (\n desiredSeekTimeMs >= segmentStartMs &&\n (desiredSeekTimeMs < segmentEndMs || includesEndTime)\n ) {\n return i + 1;\n }\n\n cumulativeTime += segmentDuration;\n if (cumulativeTime >= durationMs) break;\n }\n return undefined;\n }\n\n if (!track.segmentDurationMs) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n\n const segmentIndex = Math.floor(\n desiredSeekTimeMs / track.segmentDurationMs,\n );\n const segmentStartMs = segmentIndex * track.segmentDurationMs;\n if (segmentStartMs >= durationMs) {\n return undefined;\n }\n return segmentIndex + 1;\n }\n\n return {\n durationMs,\n tracks,\n\n segmentAt(timeMs: number, track: TrackRef): number | undefined {\n return computeSegmentIdForTrack(timeMs, track);\n },\n\n segmentsInRange(\n fromMs: number,\n toMs: number,\n track: TrackRef,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs) return [];\n\n const segments: SegmentTimeRange[] = [];\n\n if (track.segmentDurationsMs && track.segmentDurationsMs.length > 0) {\n let cumulativeTime = 0;\n for (let i = 0; i < track.segmentDurationsMs.length; i++) {\n const segmentDuration = track.segmentDurationsMs[i];\n if (segmentDuration === undefined) continue;\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n if (segmentStartMs >= durationMs) break;\n\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n if (cumulativeTime >= durationMs) break;\n }\n return segments;\n }\n\n const segmentDurationMs = track.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1;\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n if (segmentStartMs >= durationMs) break;\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SegmentIndex.js","names":["tracks: TrackSet","distance: number","ranges: SegmentTimeRange[]","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/SegmentIndex.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { ManifestResponse } from \"../../transcoding/types/index.js\";\nimport { convertToScaledTime, roundToMilliseconds } from \"./shared/PrecisionUtils.js\";\n\nexport type TrackRole = \"video\" | \"audio\" | \"scrub\";\n\nexport interface TrackRef {\n readonly role: TrackRole;\n readonly id: string | number;\n readonly src: string;\n readonly segmentDurationMs?: number;\n readonly segmentDurationsMs?: number[];\n readonly startTimeOffsetMs?: number;\n}\n\nexport interface TrackSet {\n video?: TrackRef;\n audio?: TrackRef;\n scrub?: TrackRef;\n}\n\nexport interface SegmentTimeRange {\n segmentId: number;\n startMs: number;\n endMs: number;\n}\n\nexport interface SegmentIndex {\n readonly durationMs: number;\n readonly tracks: TrackSet;\n segmentAt(timeMs: number, track: TrackRef): number | undefined;\n segmentsInRange(fromMs: number, toMs: number, track: TrackRef): SegmentTimeRange[];\n}\n\n// ---------------------------------------------------------------------------\n// FragmentIndex — backed by TrackFragmentIndex (local and file-id files)\n// ---------------------------------------------------------------------------\n\nexport function createFragmentIndex(\n data: Record<number, TrackFragmentIndex>,\n src: string,\n): SegmentIndex {\n const longestFragment = Object.values(data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n const durationMs = longestFragment * 1000;\n\n const audioTrack = Object.values(data).find((t) => t.type === \"audio\");\n const videoTrack = Object.values(data).find(\n (t) => t.type === \"video\" && t.track !== undefined && t.track > 0,\n );\n const scrubTrack = data[-1];\n\n const tracks: TrackSet = {};\n\n if (videoTrack && videoTrack.track !== undefined) {\n tracks.video = {\n role: \"video\",\n id: videoTrack.track,\n src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n }\n\n if (audioTrack && audioTrack.track !== undefined) {\n tracks.audio = {\n role: \"audio\",\n id: audioTrack.track,\n src,\n };\n }\n\n if (scrubTrack && scrubTrack.track !== undefined) {\n const segmentDurationsMs =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map((s) => (s.duration / scrubTrack.timescale) * 1000)\n : undefined;\n tracks.scrub = {\n role: \"scrub\",\n id: scrubTrack.track,\n src,\n segmentDurationMs: 30000,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n return {\n durationMs,\n tracks,\n\n segmentAt(timeMs: number, track: TrackRef): number | undefined {\n const trackId = typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n const trackData = data[trackId];\n if (!trackData) {\n throw new Error(`Track ${trackId} not found`);\n }\n const { timescale, segments } = trackData;\n\n const startTimeOffsetMs = track.startTimeOffsetMs || 0;\n const offsetSeekTimeMs = roundToMilliseconds(timeMs + startTimeOffsetMs);\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n for (let i = segments.length - 1; i >= 0; i--) {\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Gap handling: find nearest segment\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segment.cts) {\n distance = segment.cts - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n distance = scaledSeekTime - segmentEndTime;\n } else {\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n },\n\n segmentsInRange(fromMs: number, toMs: number, track: TrackRef): SegmentTimeRange[] {\n if (fromMs >= toMs) return [];\n\n const trackId = typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n const trackData = data[trackId];\n if (!trackData) return [];\n\n const { timescale, segments } = trackData;\n const ranges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]!;\n const segmentStartMs = (segment.cts / timescale) * 1000;\n const segmentEndMs = ((segment.cts + segment.duration) / timescale) * 1000;\n\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n ranges.push({\n segmentId: i,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return ranges;\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// ManifestIndex — backed by ManifestResponse (JIT transcoding)\n// ---------------------------------------------------------------------------\n\nexport function createManifestIndex(manifest: ManifestResponse): SegmentIndex {\n const durationMs = manifest.durationMs;\n const tracks: TrackSet = {};\n\n if (manifest.videoRenditions && manifest.videoRenditions.length > 0) {\n const r = manifest.videoRenditions[0]!;\n tracks.video = {\n role: \"video\",\n id: r.id,\n src: manifest.sourceUrl,\n segmentDurationMs: r.segmentDurationMs,\n segmentDurationsMs: r.segmentDurationsMs,\n startTimeOffsetMs: r.startTimeOffsetMs,\n };\n\n const scrubRendition = manifest.videoRenditions.find((v) => v.id === \"scrub\");\n if (scrubRendition) {\n tracks.scrub = {\n role: \"scrub\",\n id: scrubRendition.id,\n src: manifest.sourceUrl,\n segmentDurationMs: scrubRendition.segmentDurationMs,\n segmentDurationsMs: scrubRendition.segmentDurationsMs,\n startTimeOffsetMs: scrubRendition.startTimeOffsetMs,\n };\n }\n }\n\n if (manifest.audioRenditions && manifest.audioRenditions.length > 0) {\n const r = manifest.audioRenditions[0]!;\n tracks.audio = {\n role: \"audio\",\n id: r.id,\n src: manifest.sourceUrl,\n segmentDurationMs: r.segmentDurationMs,\n segmentDurationsMs: r.segmentDurationsMs,\n startTimeOffsetMs: r.startTimeOffsetMs,\n };\n }\n\n function computeSegmentIdForTrack(\n desiredSeekTimeMs: number,\n track: TrackRef,\n ): number | undefined {\n if (desiredSeekTimeMs > durationMs) {\n return undefined;\n }\n\n if (track.segmentDurationsMs && track.segmentDurationsMs.length > 0) {\n let cumulativeTime = 0;\n for (let i = 0; i < track.segmentDurationsMs.length; i++) {\n const segmentDuration = track.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = cumulativeTime + segmentDuration;\n\n const isLastSegment = i === track.segmentDurationsMs.length - 1;\n const includesEndTime = isLastSegment && desiredSeekTimeMs === durationMs;\n\n if (\n desiredSeekTimeMs >= segmentStartMs &&\n (desiredSeekTimeMs < segmentEndMs || includesEndTime)\n ) {\n return i + 1;\n }\n\n cumulativeTime += segmentDuration;\n if (cumulativeTime >= durationMs) break;\n }\n return undefined;\n }\n\n if (!track.segmentDurationMs) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n\n const segmentIndex = Math.floor(desiredSeekTimeMs / track.segmentDurationMs);\n const segmentStartMs = segmentIndex * track.segmentDurationMs;\n if (segmentStartMs >= durationMs) {\n return undefined;\n }\n return segmentIndex + 1;\n }\n\n return {\n durationMs,\n tracks,\n\n segmentAt(timeMs: number, track: TrackRef): number | undefined {\n return computeSegmentIdForTrack(timeMs, track);\n },\n\n segmentsInRange(fromMs: number, toMs: number, track: TrackRef): SegmentTimeRange[] {\n if (fromMs >= toMs) return [];\n\n const segments: SegmentTimeRange[] = [];\n\n if (track.segmentDurationsMs && track.segmentDurationsMs.length > 0) {\n let cumulativeTime = 0;\n for (let i = 0; i < track.segmentDurationsMs.length; i++) {\n const segmentDuration = track.segmentDurationsMs[i];\n if (segmentDuration === undefined) continue;\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(cumulativeTime + segmentDuration, durationMs);\n\n if (segmentStartMs >= durationMs) break;\n\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n if (cumulativeTime >= durationMs) break;\n }\n return segments;\n }\n\n const segmentDurationMs = track.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1;\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n if (segmentStartMs >= durationMs) break;\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentTransport.js","names":[],"sources":["../../../src/elements/EFMedia/SegmentTransport.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { RenditionId } from \"../../transcoding/types/index.js\";\nimport type { TrackRef } from \"./SegmentIndex.js\";\nimport type { CachedFetcher } from \"./CachedFetcher.js\";\n\nexport interface SegmentTransport {\n fetchInitSegment(track: TrackRef, signal: AbortSignal): Promise<ArrayBuffer>;\n fetchMediaSegment(
|
|
1
|
+
{"version":3,"file":"SegmentTransport.js","names":[],"sources":["../../../src/elements/EFMedia/SegmentTransport.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { RenditionId } from \"../../transcoding/types/index.js\";\nimport type { TrackRef } from \"./SegmentIndex.js\";\nimport type { CachedFetcher } from \"./CachedFetcher.js\";\n\nexport interface SegmentTransport {\n fetchInitSegment(track: TrackRef, signal: AbortSignal): Promise<ArrayBuffer>;\n fetchMediaSegment(segmentId: number, track: TrackRef, signal: AbortSignal): Promise<ArrayBuffer>;\n isCached(segmentId: number, track: TrackRef): boolean;\n}\n\n// ---------------------------------------------------------------------------\n// UrlTransport — each segment has its own URL\n// Used by AssetMediaEngine (via JIT URLs) and JitMediaEngine natively.\n// ---------------------------------------------------------------------------\n\ninterface UrlTransportOptions {\n fetcher: CachedFetcher;\n src: string;\n templates: { initSegment: string; mediaSegment: string };\n audioTrackId: number | undefined;\n videoTrackId: number | undefined;\n}\n\nfunction resolveRenditionId(track: TrackRef): RenditionId {\n if (track.role === \"audio\") return \"audio\";\n if (track.role === \"scrub\") return \"scrub\";\n if (typeof track.id === \"string\") return track.id as RenditionId;\n // For numeric IDs (fragment-based), map to JIT rendition names\n if (track.id === -1) return \"scrub\";\n if (track.id === 2) return \"audio\";\n return \"high\";\n}\n\nexport function createUrlTransport(opts: UrlTransportOptions): SegmentTransport {\n const { fetcher, src, templates, audioTrackId, videoTrackId } = opts;\n\n function buildSegmentUrl(segmentId: \"init\" | number, track: TrackRef): string {\n const renditionId = resolveRenditionId(track);\n const template = segmentId === \"init\" ? templates.initSegment : templates.mediaSegment;\n const trackId =\n typeof track.id === \"number\"\n ? track.id\n : track.role === \"audio\"\n ? audioTrackId\n : videoTrackId;\n return template\n .replace(\"{rendition}\", renditionId)\n .replace(\"{segmentId}\", segmentId.toString())\n .replace(\"{src}\", src)\n .replace(\"{trackId}\", trackId?.toString() ?? \"\");\n }\n\n return {\n async fetchInitSegment(track: TrackRef, signal: AbortSignal): Promise<ArrayBuffer> {\n const url = buildSegmentUrl(\"init\", track);\n return fetcher.fetchArrayBuffer(url, signal);\n },\n\n async fetchMediaSegment(\n segmentId: number,\n track: TrackRef,\n signal: AbortSignal,\n ): Promise<ArrayBuffer> {\n const url = buildSegmentUrl(segmentId, track);\n return fetcher.fetchArrayBuffer(url, signal);\n },\n\n isCached(segmentId: number, track: TrackRef): boolean {\n const url = buildSegmentUrl(segmentId, track);\n return fetcher.has(url);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// ByteRangeTransport — fetches full track binary, slices segments\n// Used by FileMediaEngine.\n// ---------------------------------------------------------------------------\n\nexport function createByteRangeTransport(\n data: Record<number, TrackFragmentIndex>,\n fileId: string,\n apiHost: string,\n fetcher: CachedFetcher,\n): SegmentTransport {\n function buildTrackUrl(trackId: number): string {\n return `${apiHost}/api/v1/files/${fileId}/tracks/${trackId}`;\n }\n\n function getTrackId(track: TrackRef): number {\n const trackId = typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n if (Number.isNaN(trackId)) {\n throw new Error(`Invalid track ID: ${track.id}`);\n }\n return trackId;\n }\n\n return {\n async fetchInitSegment(track: TrackRef, signal: AbortSignal): Promise<ArrayBuffer> {\n const trackId = getTrackId(track);\n const trackData = data[trackId];\n if (!trackData) throw new Error(`Track ${trackId} not found`);\n\n const { offset, size } = trackData.initSegment;\n const url = buildTrackUrl(trackId);\n const fullTrack = await fetcher.fetchArrayBuffer(url, signal);\n return fullTrack.slice(offset, offset + size);\n },\n\n async fetchMediaSegment(\n segmentId: number,\n track: TrackRef,\n signal: AbortSignal,\n ): Promise<ArrayBuffer> {\n const trackId = getTrackId(track);\n const trackData = data[trackId];\n if (!trackData) throw new Error(`Track ${trackId} not found`);\n\n const segment = trackData.segments[segmentId];\n if (!segment) {\n throw new Error(`Segment ${segmentId} not found for track ${trackId}`);\n }\n\n const url = buildTrackUrl(trackId);\n const fullTrack = await fetcher.fetchArrayBuffer(url, signal);\n return fullTrack.slice(segment.offset, segment.offset + segment.size);\n },\n\n isCached(_segmentId: number, track: TrackRef): boolean {\n const trackId = getTrackId(track);\n const url = buildTrackUrl(trackId);\n return fetcher.has(url);\n },\n };\n}\n"],"mappings":";AAwBA,SAAS,mBAAmB,OAA8B;AACxD,KAAI,MAAM,SAAS,QAAS,QAAO;AACnC,KAAI,MAAM,SAAS,QAAS,QAAO;AACnC,KAAI,OAAO,MAAM,OAAO,SAAU,QAAO,MAAM;AAE/C,KAAI,MAAM,OAAO,GAAI,QAAO;AAC5B,KAAI,MAAM,OAAO,EAAG,QAAO;AAC3B,QAAO;;AAGT,SAAgB,mBAAmB,MAA6C;CAC9E,MAAM,EAAE,SAAS,KAAK,WAAW,cAAc,iBAAiB;CAEhE,SAAS,gBAAgB,WAA4B,OAAyB;EAC5E,MAAM,cAAc,mBAAmB,MAAM;EAC7C,MAAM,WAAW,cAAc,SAAS,UAAU,cAAc,UAAU;EAC1E,MAAM,UACJ,OAAO,MAAM,OAAO,WAChB,MAAM,KACN,MAAM,SAAS,UACb,eACA;AACR,SAAO,SACJ,QAAQ,eAAe,YAAY,CACnC,QAAQ,eAAe,UAAU,UAAU,CAAC,CAC5C,QAAQ,SAAS,IAAI,CACrB,QAAQ,aAAa,SAAS,UAAU,IAAI,GAAG;;AAGpD,QAAO;EACL,MAAM,iBAAiB,OAAiB,QAA2C;GACjF,MAAM,MAAM,gBAAgB,QAAQ,MAAM;AAC1C,UAAO,QAAQ,iBAAiB,KAAK,OAAO;;EAG9C,MAAM,kBACJ,WACA,OACA,QACsB;GACtB,MAAM,MAAM,gBAAgB,WAAW,MAAM;AAC7C,UAAO,QAAQ,iBAAiB,KAAK,OAAO;;EAG9C,SAAS,WAAmB,OAA0B;GACpD,MAAM,MAAM,gBAAgB,WAAW,MAAM;AAC7C,UAAO,QAAQ,IAAI,IAAI;;EAE1B;;AAQH,SAAgB,yBACd,MACA,QACA,SACA,SACkB;CAClB,SAAS,cAAc,SAAyB;AAC9C,SAAO,GAAG,QAAQ,gBAAgB,OAAO,UAAU;;CAGrD,SAAS,WAAW,OAAyB;EAC3C,MAAM,UAAU,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,OAAO,SAAS,MAAM,IAAI,GAAG;AACvF,MAAI,OAAO,MAAM,QAAQ,CACvB,OAAM,IAAI,MAAM,qBAAqB,MAAM,KAAK;AAElD,SAAO;;AAGT,QAAO;EACL,MAAM,iBAAiB,OAAiB,QAA2C;GACjF,MAAM,UAAU,WAAW,MAAM;GACjC,MAAM,YAAY,KAAK;AACvB,OAAI,CAAC,UAAW,OAAM,IAAI,MAAM,SAAS,QAAQ,YAAY;GAE7D,MAAM,EAAE,QAAQ,SAAS,UAAU;GACnC,MAAM,MAAM,cAAc,QAAQ;AAElC,WADkB,MAAM,QAAQ,iBAAiB,KAAK,OAAO,EAC5C,MAAM,QAAQ,SAAS,KAAK;;EAG/C,MAAM,kBACJ,WACA,OACA,QACsB;GACtB,MAAM,UAAU,WAAW,MAAM;GACjC,MAAM,YAAY,KAAK;AACvB,OAAI,CAAC,UAAW,OAAM,IAAI,MAAM,SAAS,QAAQ,YAAY;GAE7D,MAAM,UAAU,UAAU,SAAS;AACnC,OAAI,CAAC,QACH,OAAM,IAAI,MAAM,WAAW,UAAU,uBAAuB,UAAU;GAGxE,MAAM,MAAM,cAAc,QAAQ;AAElC,WADkB,MAAM,QAAQ,iBAAiB,KAAK,OAAO,EAC5C,MAAM,QAAQ,QAAQ,QAAQ,SAAS,QAAQ,KAAK;;EAGvE,SAAS,YAAoB,OAA0B;GAErD,MAAM,MAAM,cADI,WAAW,MAAM,CACC;AAClC,UAAO,QAAQ,IAAI,IAAI;;EAE1B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimingModel.js","names":[],"sources":["../../../src/elements/EFMedia/TimingModel.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { TrackRef } from \"./SegmentIndex.js\";\n\nexport interface TimingModel {\n toContainerSeconds(
|
|
1
|
+
{"version":3,"file":"TimingModel.js","names":[],"sources":["../../../src/elements/EFMedia/TimingModel.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { TrackRef } from \"./SegmentIndex.js\";\n\nexport interface TimingModel {\n toContainerSeconds(timeMs: number, segmentId: number, track: TrackRef): number;\n}\n\n/**\n * For byte-range sliced segments from full track files (FileMediaEngine).\n * mediabunny sees segment-relative timestamps since we sliced at segment boundaries,\n * so we subtract the segment's CTS to get relative time.\n */\nexport function createByteRangeTiming(data: Record<number, TrackFragmentIndex>): TimingModel {\n return {\n toContainerSeconds(timeMs: number, segmentId: number, track: TrackRef): number {\n const trackId = typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n const trackData = data[trackId];\n if (!trackData) throw new Error(\"Track not found\");\n\n const segment = trackData.segments[segmentId];\n if (!segment) throw new Error(\"Segment not found\");\n\n const segmentStartMs = (segment.cts / trackData.timescale) * 1000;\n return (timeMs - segmentStartMs) / 1000;\n },\n };\n}\n\n/**\n * For JIT transcoded segments (JitMediaEngine).\n * Segments are self-contained — just convert ms to seconds.\n */\nexport function createJitTiming(): TimingModel {\n return {\n toContainerSeconds(timeMs: number, _segmentId: number, _track: TrackRef): number {\n return timeMs / 1000;\n },\n };\n}\n"],"mappings":";;;;;;AAYA,SAAgB,sBAAsB,MAAuD;AAC3F,QAAO,EACL,mBAAmB,QAAgB,WAAmB,OAAyB;EAE7E,MAAM,YAAY,KADF,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,OAAO,SAAS,MAAM,IAAI,GAAG;AAEvF,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,kBAAkB;EAElD,MAAM,UAAU,UAAU,SAAS;AACnC,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,oBAAoB;AAGlD,UAAQ,SADgB,QAAQ,MAAM,UAAU,YAAa,OAC1B;IAEtC;;;;;;AAOH,SAAgB,kBAA+B;AAC7C,QAAO,EACL,mBAAmB,QAAgB,YAAoB,QAA0B;AAC/E,SAAO,SAAS;IAEnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioSpanUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/AudioSpanUtils.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"AudioSpanUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/AudioSpanUtils.ts"],"sourcesContent":["import type { AudioSpan, MediaEngine, SegmentTimeRange } from \"../../../transcoding/types\";\nimport type { EFMedia } from \"../../EFMedia\";\n\n/**\n * Fetch audio segment data using MediaEngine\n * Pure function with explicit dependencies\n */\nconst fetchAudioSegmentData = async (\n segmentIds: number[],\n mediaEngine: MediaEngine,\n signal: AbortSignal,\n): Promise<Map<number, ArrayBuffer>> => {\n const audioTrack = mediaEngine.tracks.audio;\n if (!audioTrack) {\n throw new Error(\"Audio track not available\");\n }\n\n const segmentData = new Map<number, ArrayBuffer>();\n\n const fetchPromises = segmentIds.map(async (segmentId) => {\n const arrayBuffer = await mediaEngine.transport.fetchMediaSegment(\n segmentId,\n audioTrack,\n signal,\n );\n return [segmentId, arrayBuffer] as [number, ArrayBuffer];\n });\n\n const fetchedSegments = await Promise.all(fetchPromises);\n signal.throwIfAborted();\n\n for (const [segmentId, arrayBuffer] of fetchedSegments) {\n segmentData.set(segmentId, arrayBuffer);\n }\n\n return segmentData;\n};\n\n/**\n * Create audio span blob from init segment and media segments\n * Pure function for blob creation\n */\nconst createAudioSpanBlob = (initSegment: ArrayBuffer, mediaSegments: ArrayBuffer[]): Blob => {\n const chunks = [initSegment, ...mediaSegments];\n return new Blob(chunks, { type: \"audio/mp4\" });\n};\n\n/**\n * Fetch audio spanning a time range\n * Main function that orchestrates segment calculation, fetching, and blob creation\n */\nexport const fetchAudioSpanningTime = async (\n host: EFMedia,\n fromMs: number,\n toMs: number,\n signal: AbortSignal,\n): Promise<AudioSpan | undefined> => {\n // Validate inputs\n if (fromMs >= toMs || fromMs < 0) {\n throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);\n }\n\n // Get media engine using the new async method\n const mediaEngine = await host.getMediaEngine(signal);\n signal.throwIfAborted();\n\n const audioTrack = mediaEngine?.tracks.audio;\n if (!audioTrack) {\n return undefined;\n }\n\n // Fetch the init segment\n const initSegment = await mediaEngine.transport.fetchInitSegment(audioTrack, signal);\n signal.throwIfAborted();\n\n if (!initSegment) {\n return undefined;\n }\n\n // Calculate segments needed\n const segmentRanges = mediaEngine.index.segmentsInRange(fromMs, toMs, audioTrack);\n\n if (segmentRanges.length === 0) {\n throw new Error(`No segments found for time range ${fromMs}-${toMs}ms`);\n }\n\n // Fetch segment data\n const segmentIds = segmentRanges.map((r: SegmentTimeRange) => r.segmentId);\n const segmentData = await fetchAudioSegmentData(segmentIds, mediaEngine, signal);\n\n // Create ordered array of segments\n const orderedSegments = segmentIds.map((id: number) => {\n const segment = segmentData.get(id);\n if (!segment) {\n throw new Error(`Missing segment data for segment ID ${id}`);\n }\n return segment;\n });\n\n // Create blob\n const blob = createAudioSpanBlob(initSegment, orderedSegments);\n\n // Calculate actual time boundaries\n const actualStartMs = Math.min(...segmentRanges.map((r: SegmentTimeRange) => r.startMs));\n const actualEndMs = Math.max(...segmentRanges.map((r: SegmentTimeRange) => r.endMs));\n\n return {\n startMs: actualStartMs,\n endMs: actualEndMs,\n blob,\n };\n};\n"],"mappings":";;;;;AAOA,MAAM,wBAAwB,OAC5B,YACA,aACA,WACsC;CACtC,MAAM,aAAa,YAAY,OAAO;AACtC,KAAI,CAAC,WACH,OAAM,IAAI,MAAM,4BAA4B;CAG9C,MAAM,8BAAc,IAAI,KAA0B;CAElD,MAAM,gBAAgB,WAAW,IAAI,OAAO,cAAc;AAMxD,SAAO,CAAC,WALY,MAAM,YAAY,UAAU,kBAC9C,WACA,YACA,OACD,CAC8B;GAC/B;CAEF,MAAM,kBAAkB,MAAM,QAAQ,IAAI,cAAc;AACxD,QAAO,gBAAgB;AAEvB,MAAK,MAAM,CAAC,WAAW,gBAAgB,gBACrC,aAAY,IAAI,WAAW,YAAY;AAGzC,QAAO;;;;;;AAOT,MAAM,uBAAuB,aAA0B,kBAAuC;CAC5F,MAAM,SAAS,CAAC,aAAa,GAAG,cAAc;AAC9C,QAAO,IAAI,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;;;;;;AAOhD,MAAa,yBAAyB,OACpC,MACA,QACA,MACA,WACmC;AAEnC,KAAI,UAAU,QAAQ,SAAS,EAC7B,OAAM,IAAI,MAAM,8BAA8B,OAAO,SAAS,OAAO;CAIvE,MAAM,cAAc,MAAM,KAAK,eAAe,OAAO;AACrD,QAAO,gBAAgB;CAEvB,MAAM,aAAa,aAAa,OAAO;AACvC,KAAI,CAAC,WACH;CAIF,MAAM,cAAc,MAAM,YAAY,UAAU,iBAAiB,YAAY,OAAO;AACpF,QAAO,gBAAgB;AAEvB,KAAI,CAAC,YACH;CAIF,MAAM,gBAAgB,YAAY,MAAM,gBAAgB,QAAQ,MAAM,WAAW;AAEjF,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,oCAAoC,OAAO,GAAG,KAAK,IAAI;CAIzE,MAAM,aAAa,cAAc,KAAK,MAAwB,EAAE,UAAU;CAC1E,MAAM,cAAc,MAAM,sBAAsB,YAAY,aAAa,OAAO;CAYhF,MAAM,OAAO,oBAAoB,aATT,WAAW,KAAK,OAAe;EACrD,MAAM,UAAU,YAAY,IAAI,GAAG;AACnC,MAAI,CAAC,QACH,OAAM,IAAI,MAAM,uCAAuC,KAAK;AAE9D,SAAO;GACP,CAG4D;AAM9D,QAAO;EACL,SAJoB,KAAK,IAAI,GAAG,cAAc,KAAK,MAAwB,EAAE,QAAQ,CAAC;EAKtF,OAJkB,KAAK,IAAI,GAAG,cAAc,KAAK,MAAwB,EAAE,MAAM,CAAC;EAKlF;EACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalInputCache.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/GlobalInputCache.ts"],"sourcesContent":["import type { Input } from \"mediabunny\";\nimport { LRUCache } from \"../../../utils/LRUCache.js\";\n\n/**\n * Global cache for MediaBunny Input instances\n * Shared across all MediaEngine instances to prevent duplicate decoding\n * of the same segment data\n */\nclass GlobalInputCache {\n private cache = new LRUCache<string, Input>(50); // 50 Input instances max\n\n /**\n * Generate standardized cache key for Input objects\n * Format: \"input:{src}:{segmentId}:{renditionId}\"\n */\n private generateKey(
|
|
1
|
+
{"version":3,"file":"GlobalInputCache.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/GlobalInputCache.ts"],"sourcesContent":["import type { Input } from \"mediabunny\";\nimport { LRUCache } from \"../../../utils/LRUCache.js\";\n\n/**\n * Global cache for MediaBunny Input instances\n * Shared across all MediaEngine instances to prevent duplicate decoding\n * of the same segment data\n */\nclass GlobalInputCache {\n private cache = new LRUCache<string, Input>(50); // 50 Input instances max\n\n /**\n * Generate standardized cache key for Input objects\n * Format: \"input:{src}:{segmentId}:{renditionId}\"\n */\n private generateKey(src: string, segmentId: number, renditionId?: string): string {\n return `input:${src}:${segmentId}:${renditionId || \"default\"}`;\n }\n\n /**\n * Get cached Input object\n */\n get(src: string, segmentId: number, renditionId?: string): Input | undefined {\n const key = this.generateKey(src, segmentId, renditionId);\n return this.cache.get(key);\n }\n\n /**\n * Cache Input object\n */\n set(src: string, segmentId: number, input: Input, renditionId?: string): void {\n const key = this.generateKey(src, segmentId, renditionId);\n this.cache.set(key, input);\n }\n\n /**\n * Check if Input is cached\n */\n has(src: string, segmentId: number, renditionId?: string): boolean {\n const key = this.generateKey(src, segmentId, renditionId);\n return this.cache.has(key);\n }\n\n /**\n * Clear all cached Input objects\n */\n clear(): void {\n this.cache.clear();\n }\n\n /**\n * Get cache statistics for debugging\n */\n getStats() {\n return {\n size: this.cache.size,\n cachedKeys: Array.from((this.cache as any).cache.keys()),\n };\n }\n}\n\n// Single global instance shared across all MediaEngine instances\nexport const globalInputCache = new GlobalInputCache();\n\n// Export for debugging (works in both browser and server)\n(globalThis as typeof globalThis & { debugInputCache: typeof globalInputCache }).debugInputCache =\n globalInputCache;\n"],"mappings":";;;;;;;;AAQA,IAAM,mBAAN,MAAuB;;eACL,IAAI,SAAwB,GAAG;;;;;;CAM/C,AAAQ,YAAY,KAAa,WAAmB,aAA8B;AAChF,SAAO,SAAS,IAAI,GAAG,UAAU,GAAG,eAAe;;;;;CAMrD,IAAI,KAAa,WAAmB,aAAyC;EAC3E,MAAM,MAAM,KAAK,YAAY,KAAK,WAAW,YAAY;AACzD,SAAO,KAAK,MAAM,IAAI,IAAI;;;;;CAM5B,IAAI,KAAa,WAAmB,OAAc,aAA4B;EAC5E,MAAM,MAAM,KAAK,YAAY,KAAK,WAAW,YAAY;AACzD,OAAK,MAAM,IAAI,KAAK,MAAM;;;;;CAM5B,IAAI,KAAa,WAAmB,aAA+B;EACjE,MAAM,MAAM,KAAK,YAAY,KAAK,WAAW,YAAY;AACzD,SAAO,KAAK,MAAM,IAAI,IAAI;;;;;CAM5B,QAAc;AACZ,OAAK,MAAM,OAAO;;;;;CAMpB,WAAW;AACT,SAAO;GACL,MAAM,KAAK,MAAM;GACjB,YAAY,MAAM,KAAM,KAAK,MAAc,MAAM,MAAM,CAAC;GACzD;;;AAKL,MAAa,mBAAmB,IAAI,kBAAkB;AAGtD,AAAC,WAAgF,kBAC/E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PrecisionUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/PrecisionUtils.ts"],"sourcesContent":["/**\n * Centralized precision utilities for consistent timing calculations across the media pipeline.\n *\n * The key insight is that floating-point precision errors can cause inconsistencies between:\n * 1. Segment selection logic (in AssetMediaEngine.computeSegmentId)\n * 2. Sample finding logic (in SampleBuffer.find)\n * 3. Timeline mapping (in BufferedSeekingInput.seek)\n *\n * All timing calculations must use the same rounding strategy to ensure consistency.\n */\n\n/**\n * Round time to millisecond precision to handle floating-point precision issues.\n * Uses Math.round for consistent behavior across the entire pipeline.\n *\n * This function should be used for ALL time-related calculations that need to be\n * compared between different parts of the system.\n */\nexport const roundToMilliseconds = (timeMs: number): number => {\n // Round to 3 decimal places (microsecond precision)\n return Math.round(timeMs * 1000) / 1000;\n};\n\n/**\n * Convert media time (in seconds) to scaled time units using consistent rounding.\n * This is used in segment selection to convert from milliseconds to timescale units.\n */\nexport const convertToScaledTime = (
|
|
1
|
+
{"version":3,"file":"PrecisionUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/PrecisionUtils.ts"],"sourcesContent":["/**\n * Centralized precision utilities for consistent timing calculations across the media pipeline.\n *\n * The key insight is that floating-point precision errors can cause inconsistencies between:\n * 1. Segment selection logic (in AssetMediaEngine.computeSegmentId)\n * 2. Sample finding logic (in SampleBuffer.find)\n * 3. Timeline mapping (in BufferedSeekingInput.seek)\n *\n * All timing calculations must use the same rounding strategy to ensure consistency.\n */\n\n/**\n * Round time to millisecond precision to handle floating-point precision issues.\n * Uses Math.round for consistent behavior across the entire pipeline.\n *\n * This function should be used for ALL time-related calculations that need to be\n * compared between different parts of the system.\n */\nexport const roundToMilliseconds = (timeMs: number): number => {\n // Round to 3 decimal places (microsecond precision)\n return Math.round(timeMs * 1000) / 1000;\n};\n\n/**\n * Convert media time (in seconds) to scaled time units using consistent rounding.\n * This is used in segment selection to convert from milliseconds to timescale units.\n */\nexport const convertToScaledTime = (timeMs: number, timescale: number): number => {\n const scaledTime = (timeMs / 1000) * timescale;\n return Math.round(scaledTime);\n};\n\n/**\n * Convert scaled time units back to media time (in milliseconds) using consistent rounding.\n * This is the inverse of convertToScaledTime.\n */\nexport const convertFromScaledTime = (scaledTime: number, timescale: number): number => {\n const timeMs = (scaledTime / timescale) * 1000;\n return roundToMilliseconds(timeMs);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,MAAa,uBAAuB,WAA2B;AAE7D,QAAO,KAAK,MAAM,SAAS,IAAK,GAAG;;;;;;AAOrC,MAAa,uBAAuB,QAAgB,cAA8B;CAChF,MAAM,aAAc,SAAS,MAAQ;AACrC,QAAO,KAAK,MAAM,WAAW"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThumbnailExtractor.js","names":["mediaEngine: MediaEngine"],"sources":["../../../../src/elements/EFMedia/shared/ThumbnailExtractor.ts"],"sourcesContent":["import { ALL_FORMATS, BlobSource, CanvasSink, Input } from \"mediabunny\";\nimport type { ThumbnailResult } from \"../../../transcoding/types/index.js\";\nimport type { MediaEngine } from \"../MediaEngine.js\";\nimport type { TrackRef } from \"../SegmentIndex.js\";\nimport { globalInputCache } from \"./GlobalInputCache.js\";\nimport { withTimeout, DEFAULT_MEDIABUNNY_TIMEOUT_MS } from \"./timeoutUtils.js\";\n\nexport class ThumbnailExtractor {\n constructor(private mediaEngine: MediaEngine) {}\n\n async extractThumbnails(\n timestamps: number[],\n track: TrackRef,\n durationMs: number,\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n if (timestamps.length === 0) {\n return [];\n }\n\n const validTimestamps = timestamps.filter(
|
|
1
|
+
{"version":3,"file":"ThumbnailExtractor.js","names":["mediaEngine: MediaEngine"],"sources":["../../../../src/elements/EFMedia/shared/ThumbnailExtractor.ts"],"sourcesContent":["import { ALL_FORMATS, BlobSource, CanvasSink, Input } from \"mediabunny\";\nimport type { ThumbnailResult } from \"../../../transcoding/types/index.js\";\nimport type { MediaEngine } from \"../MediaEngine.js\";\nimport type { TrackRef } from \"../SegmentIndex.js\";\nimport { globalInputCache } from \"./GlobalInputCache.js\";\nimport { withTimeout, DEFAULT_MEDIABUNNY_TIMEOUT_MS } from \"./timeoutUtils.js\";\n\nexport class ThumbnailExtractor {\n constructor(private mediaEngine: MediaEngine) {}\n\n async extractThumbnails(\n timestamps: number[],\n track: TrackRef,\n durationMs: number,\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n if (timestamps.length === 0) {\n return [];\n }\n\n const validTimestamps = timestamps.filter((timeMs) => timeMs >= 0 && timeMs <= durationMs);\n\n if (validTimestamps.length === 0) {\n console.warn(`ThumbnailExtractor: All timestamps out of bounds (0-${durationMs}ms)`);\n return timestamps.map(() => null);\n }\n\n const segmentGroups = this.groupTimestampsBySegment(validTimestamps, track);\n const results = new Map<number, ThumbnailResult | null>();\n\n for (const [segmentId, segmentTimestamps] of segmentGroups) {\n signal?.throwIfAborted();\n\n try {\n const segmentResults = await this.extractSegmentThumbnails(\n segmentId,\n segmentTimestamps,\n track,\n signal,\n );\n\n for (const [timestamp, thumbnail] of segmentResults) {\n results.set(timestamp, thumbnail);\n }\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n for (const timestamp of segmentTimestamps) {\n results.set(timestamp, null);\n }\n }\n }\n\n return timestamps.map((t) => {\n if (t < 0 || t > durationMs) {\n return null;\n }\n return results.get(t) || null;\n });\n }\n\n private groupTimestampsBySegment(timestamps: number[], track: TrackRef): Map<number, number[]> {\n const segmentGroups = new Map<number, number[]>();\n\n for (const timeMs of timestamps) {\n try {\n const segmentId = this.mediaEngine.index.segmentAt(timeMs, track);\n if (segmentId !== undefined) {\n if (!segmentGroups.has(segmentId)) {\n segmentGroups.set(segmentId, []);\n }\n const segmentGroup = segmentGroups.get(segmentId)!;\n segmentGroup.push(timeMs);\n }\n } catch (error) {\n console.warn(\n `ThumbnailExtractor: Could not compute segment for timestamp ${timeMs}:`,\n error,\n );\n }\n }\n\n return segmentGroups;\n }\n\n private async extractSegmentThumbnails(\n segmentId: number,\n timestamps: number[],\n track: TrackRef,\n signal?: AbortSignal,\n ): Promise<Map<number, ThumbnailResult | null>> {\n const results = new Map<number, ThumbnailResult | null>();\n\n try {\n signal?.throwIfAborted();\n\n const initP = this.mediaEngine.transport.fetchInitSegment(track, signal!);\n const mediaP = this.mediaEngine.transport.fetchMediaSegment(segmentId, track, signal!);\n initP.catch(() => {});\n mediaP.catch(() => {});\n const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);\n\n signal?.throwIfAborted();\n\n const segmentBlob = new Blob([initSegment, mediaSegment]);\n const renditionId = typeof track.id === \"string\" ? track.id : undefined;\n\n let input = globalInputCache.get(track.src, segmentId, renditionId);\n if (!input) {\n input = new Input({\n formats: ALL_FORMATS,\n source: new BlobSource(segmentBlob),\n });\n globalInputCache.set(track.src, segmentId, input, renditionId);\n }\n\n const videoTrack = await withTimeout(\n input.getPrimaryVideoTrack(),\n 5000,\n \"ThumbnailExtractor.getPrimaryVideoTrack\",\n signal,\n );\n if (!videoTrack) {\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n return results;\n }\n\n const sink = new CanvasSink(videoTrack);\n const sortedTimestamps = [...timestamps].sort((a, b) => a - b);\n\n const relativeTimestamps = sortedTimestamps.map((ms) =>\n this.mediaEngine.timing.toContainerSeconds(ms, segmentId, track),\n );\n\n const timestampResults = [];\n const canvasIterator = sink.canvasesAtTimestamps(relativeTimestamps);\n for await (const result of canvasIterator) {\n const canvasResult = await withTimeout(\n Promise.resolve(result),\n DEFAULT_MEDIABUNNY_TIMEOUT_MS,\n \"ThumbnailExtractor canvasesAtTimestamps iteration\",\n signal,\n );\n timestampResults.push(canvasResult);\n }\n\n for (let i = 0; i < sortedTimestamps.length; i++) {\n const globalTimestamp = sortedTimestamps[i];\n if (globalTimestamp === undefined) {\n continue;\n }\n\n const result = timestampResults[i];\n\n if (result?.canvas) {\n const canvas = result.canvas;\n if (canvas instanceof HTMLCanvasElement || canvas instanceof OffscreenCanvas) {\n results.set(globalTimestamp, {\n timestamp: globalTimestamp,\n thumbnail: canvas,\n });\n } else {\n results.set(globalTimestamp, null);\n }\n } else {\n results.set(globalTimestamp, null);\n }\n }\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n }\n\n return results;\n }\n}\n"],"mappings":";;;;;AAOA,IAAa,qBAAb,MAAgC;CAC9B,YAAY,AAAQA,aAA0B;EAA1B;;CAEpB,MAAM,kBACJ,YACA,OACA,YACA,QACqC;AACrC,MAAI,WAAW,WAAW,EACxB,QAAO,EAAE;EAGX,MAAM,kBAAkB,WAAW,QAAQ,WAAW,UAAU,KAAK,UAAU,WAAW;AAE1F,MAAI,gBAAgB,WAAW,GAAG;AAChC,WAAQ,KAAK,uDAAuD,WAAW,KAAK;AACpF,UAAO,WAAW,UAAU,KAAK;;EAGnC,MAAM,gBAAgB,KAAK,yBAAyB,iBAAiB,MAAM;EAC3E,MAAM,0BAAU,IAAI,KAAqC;AAEzD,OAAK,MAAM,CAAC,WAAW,sBAAsB,eAAe;AAC1D,WAAQ,gBAAgB;AAExB,OAAI;IACF,MAAM,iBAAiB,MAAM,KAAK,yBAChC,WACA,mBACA,OACA,OACD;AAED,SAAK,MAAM,CAAC,WAAW,cAAc,eACnC,SAAQ,IAAI,WAAW,UAAU;YAE5B,OAAO;AACd,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,YAAQ,KACN,gEAAgE,UAAU,IAC1E,MACD;AACD,SAAK,MAAM,aAAa,kBACtB,SAAQ,IAAI,WAAW,KAAK;;;AAKlC,SAAO,WAAW,KAAK,MAAM;AAC3B,OAAI,IAAI,KAAK,IAAI,WACf,QAAO;AAET,UAAO,QAAQ,IAAI,EAAE,IAAI;IACzB;;CAGJ,AAAQ,yBAAyB,YAAsB,OAAwC;EAC7F,MAAM,gCAAgB,IAAI,KAAuB;AAEjD,OAAK,MAAM,UAAU,WACnB,KAAI;GACF,MAAM,YAAY,KAAK,YAAY,MAAM,UAAU,QAAQ,MAAM;AACjE,OAAI,cAAc,QAAW;AAC3B,QAAI,CAAC,cAAc,IAAI,UAAU,CAC/B,eAAc,IAAI,WAAW,EAAE,CAAC;AAGlC,IADqB,cAAc,IAAI,UAAU,CACpC,KAAK,OAAO;;WAEpB,OAAO;AACd,WAAQ,KACN,+DAA+D,OAAO,IACtE,MACD;;AAIL,SAAO;;CAGT,MAAc,yBACZ,WACA,YACA,OACA,QAC8C;EAC9C,MAAM,0BAAU,IAAI,KAAqC;AAEzD,MAAI;AACF,WAAQ,gBAAgB;GAExB,MAAM,QAAQ,KAAK,YAAY,UAAU,iBAAiB,OAAO,OAAQ;GACzE,MAAM,SAAS,KAAK,YAAY,UAAU,kBAAkB,WAAW,OAAO,OAAQ;AACtF,SAAM,YAAY,GAAG;AACrB,UAAO,YAAY,GAAG;GACtB,MAAM,CAAC,aAAa,gBAAgB,MAAM,QAAQ,IAAI,CAAC,OAAO,OAAO,CAAC;AAEtE,WAAQ,gBAAgB;GAExB,MAAM,cAAc,IAAI,KAAK,CAAC,aAAa,aAAa,CAAC;GACzD,MAAM,cAAc,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK;GAE9D,IAAI,QAAQ,iBAAiB,IAAI,MAAM,KAAK,WAAW,YAAY;AACnE,OAAI,CAAC,OAAO;AACV,YAAQ,IAAI,MAAM;KAChB,SAAS;KACT,QAAQ,IAAI,WAAW,YAAY;KACpC,CAAC;AACF,qBAAiB,IAAI,MAAM,KAAK,WAAW,OAAO,YAAY;;GAGhE,MAAM,aAAa,MAAM,YACvB,MAAM,sBAAsB,EAC5B,KACA,2CACA,OACD;AACD,OAAI,CAAC,YAAY;AACf,SAAK,MAAM,aAAa,WACtB,SAAQ,IAAI,WAAW,KAAK;AAE9B,WAAO;;GAGT,MAAM,OAAO,IAAI,WAAW,WAAW;GACvC,MAAM,mBAAmB,CAAC,GAAG,WAAW,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE;GAE9D,MAAM,qBAAqB,iBAAiB,KAAK,OAC/C,KAAK,YAAY,OAAO,mBAAmB,IAAI,WAAW,MAAM,CACjE;GAED,MAAM,mBAAmB,EAAE;GAC3B,MAAM,iBAAiB,KAAK,qBAAqB,mBAAmB;AACpE,cAAW,MAAM,UAAU,gBAAgB;IACzC,MAAM,eAAe,MAAM,YACzB,QAAQ,QAAQ,OAAO,EACvB,+BACA,qDACA,OACD;AACD,qBAAiB,KAAK,aAAa;;AAGrC,QAAK,IAAI,IAAI,GAAG,IAAI,iBAAiB,QAAQ,KAAK;IAChD,MAAM,kBAAkB,iBAAiB;AACzC,QAAI,oBAAoB,OACtB;IAGF,MAAM,SAAS,iBAAiB;AAEhC,QAAI,QAAQ,QAAQ;KAClB,MAAM,SAAS,OAAO;AACtB,SAAI,kBAAkB,qBAAqB,kBAAkB,gBAC3D,SAAQ,IAAI,iBAAiB;MAC3B,WAAW;MACX,WAAW;MACZ,CAAC;SAEF,SAAQ,IAAI,iBAAiB,KAAK;UAGpC,SAAQ,IAAI,iBAAiB,KAAK;;WAG/B,OAAO;AACd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,WAAQ,KACN,gEAAgE,UAAU,IAC1E,MACD;AACD,QAAK,MAAM,aAAa,WACtB,SAAQ,IAAI,WAAW,KAAK;;AAIhC,SAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MainVideoInputCache.js","names":["#getCacheKey","#cache","#pendingPromises","#maxCacheSize"],"sources":["../../../../src/elements/EFMedia/videoTasks/MainVideoInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for main video BufferedSeekingInput instances\n * Main video segments are typically 2s long, so we can reuse the same input\n * for multiple frames within that segment (e.g., 60 frames at 30fps)\n */\nexport class MainVideoInputCache {\n #cache = new Map<string, BufferedSeekingInput>();\n #pendingPromises = new Map
|
|
1
|
+
{"version":3,"file":"MainVideoInputCache.js","names":["#getCacheKey","#cache","#pendingPromises","#maxCacheSize"],"sources":["../../../../src/elements/EFMedia/videoTasks/MainVideoInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for main video BufferedSeekingInput instances\n * Main video segments are typically 2s long, so we can reuse the same input\n * for multiple frames within that segment (e.g., 60 frames at 30fps)\n */\nexport class MainVideoInputCache {\n #cache = new Map<string, BufferedSeekingInput>();\n #pendingPromises = new Map<string, Promise<BufferedSeekingInput | undefined>>();\n #maxCacheSize = 10; // Keep last 10 main inputs (covers 20 seconds at 2s/segment)\n\n /**\n * Create a cache key that uniquely identifies a segment\n */\n #getCacheKey(src: string, segmentId: number, renditionId: string | undefined): string {\n return `${src}:${renditionId || \"default\"}:${segmentId}`;\n }\n\n /**\n * Get or create BufferedSeekingInput for a main video segment.\n *\n * Uses promise deduplication to prevent race conditions when multiple\n * concurrent requests arrive for the same segment. Without this,\n * the first segment often fails when DevTools is closed because:\n * 1. Video display and thumbnail extraction both request segment 0\n * 2. Both find cache empty and start createInputFn()\n * 3. Both create separate instances, causing conflicts\n */\n async getOrCreateInput(\n src: string,\n segmentId: number,\n renditionId: string | undefined,\n createInputFn: () => Promise<BufferedSeekingInput | undefined>,\n ): Promise<BufferedSeekingInput | undefined> {\n const cacheKey = this.#getCacheKey(src, segmentId, renditionId);\n\n // Check if we already have a completed result cached\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Check if there's already a pending request for this segment (deduplication!)\n // This prevents the race condition where multiple concurrent requests\n // each create their own BufferedSeekingInput instance.\n const pending = this.#pendingPromises.get(cacheKey);\n if (pending) {\n return pending;\n }\n\n // Create the promise and cache it IMMEDIATELY to prevent race conditions\n const promise = createInputFn()\n .then((input) => {\n // Clean up pending promise\n this.#pendingPromises.delete(cacheKey);\n\n if (input) {\n // Add to completed cache\n this.#cache.set(cacheKey, input);\n\n // Evict oldest entries if cache is too large (LRU-like behavior)\n if (this.#cache.size > this.#maxCacheSize) {\n const oldestKey = this.#cache.keys().next().value;\n if (oldestKey !== undefined) {\n this.#cache.delete(oldestKey);\n }\n }\n }\n\n return input;\n })\n .catch((error) => {\n // Clean up pending promise on failure so retry is possible\n this.#pendingPromises.delete(cacheKey);\n throw error;\n });\n\n this.#pendingPromises.set(cacheKey, promise);\n return promise;\n }\n\n /**\n * Clear the entire cache (called when video changes)\n */\n clear() {\n this.#cache.clear();\n this.#pendingPromises.clear();\n }\n\n /**\n * Get cache statistics\n */\n getStats() {\n return {\n size: this.#cache.size,\n pendingSize: this.#pendingPromises.size,\n cacheKeys: Array.from(this.#cache.keys()),\n };\n }\n}\n"],"mappings":";;;;;;AAOA,IAAa,sBAAb,MAAiC;CAC/B,yBAAS,IAAI,KAAmC;CAChD,mCAAmB,IAAI,KAAwD;CAC/E,gBAAgB;;;;CAKhB,aAAa,KAAa,WAAmB,aAAyC;AACpF,SAAO,GAAG,IAAI,GAAG,eAAe,UAAU,GAAG;;;;;;;;;;;;CAa/C,MAAM,iBACJ,KACA,WACA,aACA,eAC2C;EAC3C,MAAM,WAAW,MAAKA,YAAa,KAAK,WAAW,YAAY;EAG/D,MAAM,SAAS,MAAKC,MAAO,IAAI,SAAS;AACxC,MAAI,OACF,QAAO;EAMT,MAAM,UAAU,MAAKC,gBAAiB,IAAI,SAAS;AACnD,MAAI,QACF,QAAO;EAIT,MAAM,UAAU,eAAe,CAC5B,MAAM,UAAU;AAEf,SAAKA,gBAAiB,OAAO,SAAS;AAEtC,OAAI,OAAO;AAET,UAAKD,MAAO,IAAI,UAAU,MAAM;AAGhC,QAAI,MAAKA,MAAO,OAAO,MAAKE,cAAe;KACzC,MAAM,YAAY,MAAKF,MAAO,MAAM,CAAC,MAAM,CAAC;AAC5C,SAAI,cAAc,OAChB,OAAKA,MAAO,OAAO,UAAU;;;AAKnC,UAAO;IACP,CACD,OAAO,UAAU;AAEhB,SAAKC,gBAAiB,OAAO,SAAS;AACtC,SAAM;IACN;AAEJ,QAAKA,gBAAiB,IAAI,UAAU,QAAQ;AAC5C,SAAO;;;;;CAMT,QAAQ;AACN,QAAKD,MAAO,OAAO;AACnB,QAAKC,gBAAiB,OAAO;;;;;CAM/B,WAAW;AACT,SAAO;GACL,MAAM,MAAKD,MAAO;GAClB,aAAa,MAAKC,gBAAiB;GACnC,WAAW,MAAM,KAAK,MAAKD,MAAO,MAAM,CAAC;GAC1C"}
|