@editframe/elements 0.47.1 → 0.47.2
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/LICENSE.md +58 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +6 -2
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/SegmentIndex.d.ts +2 -0
- package/dist/elements/EFMedia/SegmentIndex.js +9 -1
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +1 -1
- package/dist/elements/EFMedia.js +1 -1
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFMotionBlur.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +29 -1
- package/dist/elements/EFVideo.js +50 -3
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/gui/PlaybackController.js +35 -14
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +2 -0
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/LICENSE.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Editframe SDK License
|
|
2
|
+
|
|
3
|
+
The Editframe SDK is source-available and free for individuals and small teams, including for commercial use. We've kept the bar low intentionally — if you're building something, you should be able to just use it.
|
|
4
|
+
|
|
5
|
+
- [Free Tier](#free-tier)
|
|
6
|
+
- [Paid Tiers](#paid-tiers)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Free Tier
|
|
11
|
+
|
|
12
|
+
Copyright © 2026 [Editframe](https://editframe.com)
|
|
13
|
+
|
|
14
|
+
### Who Qualifies
|
|
15
|
+
|
|
16
|
+
- Individuals and freelancers
|
|
17
|
+
- Organizations with 3 or fewer total employees
|
|
18
|
+
- Non-profits and not-for-profit organizations (any size)
|
|
19
|
+
- Accredited educational institutions
|
|
20
|
+
- Organizations evaluating Editframe in good faith, prior to commercial deployment (30 days)
|
|
21
|
+
|
|
22
|
+
### What You Can Build
|
|
23
|
+
|
|
24
|
+
Use and modify the SDK to build any product that creates, processes, or delivers video — editors, workflows, motion design tools, content automation, and more. Platforms, coding environments, vibe coding tools, and AI agents may freely integrate the SDK. End users are each responsible for their own license compliance.
|
|
25
|
+
|
|
26
|
+
### What Is Not Permitted
|
|
27
|
+
|
|
28
|
+
You may not offer the Client-Side SDK or CLI itself as a managed rendering API or infrastructure product to third parties. You may not sell, sublicense, or redistribute the SDK or modifications to it as a standalone product.
|
|
29
|
+
|
|
30
|
+
### Warranty
|
|
31
|
+
|
|
32
|
+
The SDK is provided as-is without warranty of any kind. See the [Full License Agreement](./LICENSE-FULL.md) for complete terms.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Paid Tiers
|
|
37
|
+
|
|
38
|
+
Organizations outside the Free Tier must subscribe to a paid plan.
|
|
39
|
+
|
|
40
|
+
| Tier | Employees |
|
|
41
|
+
| :---- | :---- |
|
|
42
|
+
| Team | 4–10 |
|
|
43
|
+
| Cloud | 11–20, or any size needing Cloud Rendering & Player |
|
|
44
|
+
| Enterprise | 21+ |
|
|
45
|
+
|
|
46
|
+
All paid tiers include full SDK rights.
|
|
47
|
+
|
|
48
|
+
Cloud Rendering & Player — parallel rendering and CDN streaming optimized for editing workflows — requires a Cloud Tier subscription regardless of company size.
|
|
49
|
+
|
|
50
|
+
[View pricing →](https://editframe.com/pricing) · Enterprise: [hello@editframe.com](mailto:hello@editframe.com)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
Unsure which tier applies? See the [FAQ](https://editframe.com/faq) or reach out at [hello@editframe.com](mailto:hello@editframe.com).
|
|
55
|
+
|
|
56
|
+
[Full License Agreement](./LICENSE-FULL.md) · [Terms of Service](https://editframe.com/terms)
|
|
57
|
+
|
|
58
|
+
*© 2026 Editframe, Inc.*
|
|
@@ -2,7 +2,7 @@ import { withSpan } from "../../otel/tracingHelpers.js";
|
|
|
2
2
|
import { roundToMilliseconds } from "./shared/PrecisionUtils.js";
|
|
3
3
|
import { DEFAULT_MEDIABUNNY_TIMEOUT_MS, withTimeout } from "./shared/timeoutUtils.js";
|
|
4
4
|
import { SampleBuffer } from "../SampleBuffer.js";
|
|
5
|
-
import { AudioSampleSink, BufferSource, Input, InputAudioTrack, InputVideoTrack, MP4, VideoSampleSink } from "mediabunny";
|
|
5
|
+
import { AudioSampleSink, BufferSource, Input, InputAudioTrack, InputVideoTrack, MATROSKA, MP4, VideoSampleSink, WEBM } from "mediabunny";
|
|
6
6
|
|
|
7
7
|
//#region src/elements/EFMedia/BufferedSeekingInput.ts
|
|
8
8
|
const defaultOptions = {
|
|
@@ -19,7 +19,11 @@ var BufferedSeekingInput = class {
|
|
|
19
19
|
this.trackSeekPromises = /* @__PURE__ */ new Map();
|
|
20
20
|
this.input = new Input({
|
|
21
21
|
source: new BufferSource(arrayBuffer),
|
|
22
|
-
formats: [
|
|
22
|
+
formats: [
|
|
23
|
+
MP4,
|
|
24
|
+
MATROSKA,
|
|
25
|
+
WEBM
|
|
26
|
+
]
|
|
23
27
|
});
|
|
24
28
|
this.options = {
|
|
25
29
|
...defaultOptions,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BufferedSeekingInput.js","names":["defaultOptions: BufferedSeekingInputOptions","track","bufferSize","#seekLocks","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 #seekLocks = new Map<number, 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 const existingLock = this.#seekLocks.get(trackId);\n if (existingLock) {\n span.setAttribute(\"waitedForSeekLock\", true);\n await existingLock.promise;\n }\n const seekLock = Promise.withResolvers<void>();\n this.#seekLocks.set(trackId, 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.#seekLocks.delete(trackId);\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","#seekLocks","contents"],"sources":["../../../src/elements/EFMedia/BufferedSeekingInput.ts"],"sourcesContent":["import {\n AudioSampleSink,\n BufferSource,\n Input,\n InputAudioTrack,\n type InputTrack,\n InputVideoTrack,\n MATROSKA,\n MP4,\n VideoSampleSink,\n WEBM,\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, MATROSKA, WEBM],\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 #seekLocks = new Map<number, 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 const existingLock = this.#seekLocks.get(trackId);\n if (existingLock) {\n span.setAttribute(\"waitedForSeekLock\", true);\n await existingLock.promise;\n }\n const seekLock = Promise.withResolvers<void>();\n this.#seekLocks.set(trackId, 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.#seekLocks.delete(trackId);\n seekLock.resolve();\n }\n },\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,9 @@ import { convertToScaledTime, roundToMilliseconds } from "./shared/PrecisionUtil
|
|
|
4
4
|
function createFragmentIndex(data, src) {
|
|
5
5
|
const durationMs = Object.values(data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0) * 1e3;
|
|
6
6
|
const audioTrack = Object.values(data).find((t) => t.type === "audio");
|
|
7
|
-
const videoTrack = Object.values(data).
|
|
7
|
+
const videoTrack = Object.values(data).filter((t) => t.type === "video" && (t.track ?? 0) > 0).sort((a, b) => (a.track ?? 0) - (b.track ?? 0))[0];
|
|
8
|
+
const alphaTrackId = videoTrack?.type === "video" ? videoTrack.alphaTrackId : void 0;
|
|
9
|
+
const alphaTrack = alphaTrackId !== void 0 ? data[alphaTrackId] : void 0;
|
|
8
10
|
const scrubTrack = data[-1];
|
|
9
11
|
const tracks = {};
|
|
10
12
|
if (videoTrack && videoTrack.track !== void 0) tracks.video = {
|
|
@@ -13,6 +15,12 @@ function createFragmentIndex(data, src) {
|
|
|
13
15
|
src,
|
|
14
16
|
startTimeOffsetMs: videoTrack.startTimeOffsetMs
|
|
15
17
|
};
|
|
18
|
+
if (alphaTrack && alphaTrack.track !== void 0) tracks.alpha = {
|
|
19
|
+
role: "video",
|
|
20
|
+
id: alphaTrack.track,
|
|
21
|
+
src,
|
|
22
|
+
startTimeOffsetMs: alphaTrack.startTimeOffsetMs
|
|
23
|
+
};
|
|
16
24
|
if (audioTrack && audioTrack.track !== void 0) tracks.audio = {
|
|
17
25
|
role: "audio",
|
|
18
26
|
id: audioTrack.track,
|
|
@@ -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 { 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
|
+
{"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 /** Alpha matte track — grayscale H264, paired with `video`. */\n alpha?: 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 // Primary video track = lowest positive track number (track 2 is the alpha matte).\n const videoTrack = Object.values(data)\n .filter((t) => t.type === \"video\" && (t.track ?? 0) > 0)\n .sort((a, b) => (a.track ?? 0) - (b.track ?? 0))[0];\n const alphaTrackId = videoTrack?.type === \"video\" ? videoTrack.alphaTrackId : undefined;\n const alphaTrack = alphaTrackId !== undefined ? data[alphaTrackId] : undefined;\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 (alphaTrack && alphaTrack.track !== undefined) {\n tracks.alpha = {\n role: \"video\",\n id: alphaTrack.track,\n src,\n startTimeOffsetMs: alphaTrack.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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"}
|
|
@@ -45,7 +45,7 @@ declare class EFMedia extends EFMedia_base {
|
|
|
45
45
|
#private;
|
|
46
46
|
get efContext(): ControllableInterface | null;
|
|
47
47
|
shouldAutoReady(): boolean;
|
|
48
|
-
static readonly VIDEO_SAMPLE_BUFFER_SIZE =
|
|
48
|
+
static readonly VIDEO_SAMPLE_BUFFER_SIZE = 90;
|
|
49
49
|
static readonly AUDIO_SAMPLE_BUFFER_SIZE = 120;
|
|
50
50
|
/**
|
|
51
51
|
* Which tracks this media element requires.
|
package/dist/elements/EFMedia.js
CHANGED
|
@@ -172,7 +172,7 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
|
|
|
172
172
|
return false;
|
|
173
173
|
}
|
|
174
174
|
static {
|
|
175
|
-
this.VIDEO_SAMPLE_BUFFER_SIZE =
|
|
175
|
+
this.VIDEO_SAMPLE_BUFFER_SIZE = 90;
|
|
176
176
|
}
|
|
177
177
|
static {
|
|
178
178
|
this.AUDIO_SAMPLE_BUFFER_SIZE = 120;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFMedia.js","names":["assignedElements: Element[]","#value","#error","#status","#promise","#resolvePromise","#mediaEngineSrcKey","#mediaEngine","#mediaEnginePromise","#loadMediaEngine","#createMediaEngine","#mediaEngineError","#handleMediaEngineComplete","#frequencyDataCache","#analyzeFrequencies","#timeDomainDataCache","#analyzeTimeDomain"],"sources":["../../src/elements/EFMedia.ts"],"sourcesContent":["import { provide } from \"@lit/context\";\nimport { css, LitElement, type PropertyValueMap } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { isContextMixin } from \"../gui/ContextMixin.js\";\nimport type { ControllableInterface } from \"../gui/Controllable.js\";\nimport { efContext } from \"../gui/efContext.js\";\nimport { withSpan } from \"../otel/tracingHelpers.js\";\nimport type { MediaEngine } from \"../transcoding/types/index.ts\";\nimport type { AudioSpan } from \"../transcoding/types/index.ts\";\nimport { createMediaEngineFromSource } from \"./EFMedia/MediaEngine.js\";\nimport { UrlGenerator } from \"../transcoding/utils/UrlGenerator.ts\";\nimport { LRUCache } from \"../utils/LRUCache.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\nimport { renderTemporalAudio } from \"./renderTemporalAudio.js\";\nimport { EFTargetable } from \"./TargetController.ts\";\n\n// EF_FRAMEGEN is a global instance created in EF_FRAMEGEN.ts\ndeclare global {\n var EF_FRAMEGEN: import(\"../EF_FRAMEGEN.js\").EFFramegen;\n}\n\nconst freqWeightsCache = new Map<number, Float32Array>();\n\nexport class IgnorableError extends Error {}\n\n/**\n * Gets all child elements including slotted content for shadow DOM elements.\n * Duplicated here to avoid circular imports from EFTemporal.\n */\nconst getChildrenIncludingSlotted = (element: Element): Element[] => {\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll(\"slot\");\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== \"SLOT\") {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n return Array.from(element.children);\n};\n\nexport const deepGetMediaElements = (element: Element, medias: EFMedia[] = []) => {\n const children = getChildrenIncludingSlotted(element);\n for (const child of children) {\n if (child instanceof EFMedia) {\n medias.push(child);\n } else {\n deepGetMediaElements(child, medias);\n }\n }\n return medias;\n};\n\n// Import EFTemporal - use a function wrapper to defer evaluation until class definition\n// This breaks the circular dependency: EFTimegroup -> EFMedia -> EFTemporal\nimport { EFTemporal } from \"./EFTemporal.js\";\n\n/**\n * Simple async value wrapper that mimics Lit Task interface.\n * Used for backwards compatibility with code expecting task-like objects.\n */\nexport class AsyncValue<T> {\n #value: T | undefined = undefined;\n #error: Error | undefined = undefined;\n // oxlint-disable-next-line no-unused-private-class-members -- false positive; read via get status()\n #status: \"initial\" | \"pending\" | \"complete\" | \"error\" = \"initial\";\n #promise: Promise<T | undefined> = Promise.resolve(undefined);\n #resolvePromise: ((value: T | undefined) => void) | undefined;\n\n // Use properties instead of getters to avoid TypeScript declaration generation bug\n get value(): T | undefined {\n return this.#value;\n }\n\n get error(): Error | undefined {\n return this.#error;\n }\n\n get status(): number {\n // Match TaskStatus enum: INITIAL=0, PENDING=1, COMPLETE=2, ERROR=3\n switch (this.#status) {\n case \"initial\":\n return 0;\n case \"pending\":\n return 1;\n case \"complete\":\n return 2;\n case \"error\":\n return 3;\n }\n }\n\n get taskComplete(): Promise<T | undefined> {\n return this.#promise;\n }\n\n /**\n * Set the value (marks status as complete)\n */\n setValue(value: T): void {\n this.#value = value;\n this.#error = undefined;\n this.#status = \"complete\";\n this.#resolvePromise?.(value);\n }\n\n /**\n * Set an error (marks status as error)\n */\n setError(error: Error): void {\n this.#error = error;\n this.#value = undefined;\n this.#status = \"error\";\n // Don't reject - just resolve with undefined to match old behavior\n this.#resolvePromise?.(undefined);\n }\n\n /**\n * Start a new async operation\n */\n startPending(): void {\n this.#status = \"pending\";\n this.#promise = new Promise((resolve) => {\n this.#resolvePromise = resolve;\n });\n // Prevent unhandled rejection warnings\n this.#promise.catch(() => {});\n }\n\n /**\n * Run an async function and update status accordingly\n */\n async run(fn: () => Promise<T>): Promise<T | undefined> {\n this.startPending();\n try {\n const result = await fn();\n this.setValue(result);\n return result;\n } catch (error) {\n if (error instanceof Error) {\n this.setError(error);\n } else {\n this.setError(new Error(String(error)));\n }\n return undefined;\n }\n }\n}\n\n// Audio analysis helper functions\nconst DECAY_WEIGHT = 0.8;\n\nfunction processFFTData(fftData: Uint8Array, zeroThresholdPercent = 0.1): Uint8Array {\n const totalBins = fftData.length;\n const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);\n\n let zeroCount = 0;\n let cutoffIndex = totalBins;\n\n for (let i = totalBins - 1; i >= 0; i--) {\n if (fftData[i] ?? 0 < 10) {\n zeroCount++;\n } else {\n if (zeroCount >= zeroThresholdCount) {\n cutoffIndex = i + 1;\n break;\n }\n }\n }\n\n if (cutoffIndex < zeroThresholdCount) {\n return fftData;\n }\n\n const goodData = fftData.slice(0, cutoffIndex);\n const resampledData = interpolateData(goodData, fftData.length);\n\n const attenuationStartIndex = Math.floor(totalBins * 0.9);\n for (let i = attenuationStartIndex; i < totalBins; i++) {\n const attenuationProgress =\n (i - attenuationStartIndex) / (totalBins - attenuationStartIndex) + 0.2;\n const attenuationFactor = Math.max(0, 1 - attenuationProgress);\n resampledData[i] = Math.floor((resampledData[i] ?? 0) * attenuationFactor);\n }\n\n return resampledData;\n}\n\nfunction interpolateData(data: Uint8Array, targetSize: number): Uint8Array {\n const resampled = new Uint8Array(targetSize);\n const dataLength = data.length;\n\n for (let i = 0; i < targetSize; i++) {\n const ratio = (i / (targetSize - 1)) * (dataLength - 1);\n const index = Math.floor(ratio);\n const fraction = ratio - index;\n\n if (index >= dataLength - 1) {\n resampled[i] = data[dataLength - 1] ?? 0;\n } else {\n resampled[i] = Math.round(\n (data[index] ?? 0) * (1 - fraction) + (data[index + 1] ?? 0) * fraction,\n );\n }\n }\n\n return resampled;\n}\n\nexport class EFMedia extends EFTargetable(\n EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {\n assetType: \"isobmff_files\",\n }),\n) {\n @provide({ context: efContext })\n get efContext(): ControllableInterface | null {\n return this.rootTimegroup ?? this;\n }\n\n override shouldAutoReady(): boolean {\n return false;\n }\n\n // Sample buffer size configuration\n static readonly VIDEO_SAMPLE_BUFFER_SIZE = 30;\n static readonly AUDIO_SAMPLE_BUFFER_SIZE = 120;\n\n /**\n * Which tracks this media element requires.\n * Subclasses can override to specify their needs:\n * - \"audio\" - Only needs audio track (e.g., EFAudio)\n * - \"video\" - Only needs video track\n * - \"both\" - Needs both tracks (default for backwards compatibility)\n *\n * This is used during media engine creation to skip validation\n * of tracks that won't be used, avoiding unnecessary network requests.\n */\n get requiredTracks(): \"audio\" | \"video\" | \"both\" {\n return \"both\";\n }\n\n static get observedAttributes() {\n // biome-ignore lint/complexity/noThisInStatic: We need to access super\n const parentAttributes = super.observedAttributes || [];\n return [\n ...parentAttributes,\n \"mute\",\n \"fft-size\",\n \"fft-decay\",\n \"fft-gain\",\n \"interpolate-frequencies\",\n \"file-id\",\n \"asset-id\",\n \"audio-buffer-duration\",\n \"max-audio-buffer-fetches\",\n \"enable-audio-buffering\",\n \"sourcein\",\n \"sourceout\",\n ];\n }\n\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n overflow: hidden;\n }\n `,\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 /**\n * Duration in milliseconds for audio buffering ahead of current time\n * @domAttribute \"audio-buffer-duration\"\n */\n @property({ type: Number, attribute: \"audio-buffer-duration\" })\n audioBufferDurationMs = 10000; // 10 seconds - reasonable for JIT encoding\n\n /**\n * Maximum number of concurrent audio segment fetches for buffering\n * @domAttribute \"max-audio-buffer-fetches\"\n */\n @property({ type: Number, attribute: \"max-audio-buffer-fetches\" })\n maxAudioBufferFetches = 2;\n\n /**\n * Enable/disable audio buffering system\n * @domAttribute \"enable-audio-buffering\"\n */\n @property({ type: Boolean, attribute: \"enable-audio-buffering\" })\n enableAudioBuffering = true;\n\n /**\n * Mute/unmute the media element\n * @domAttribute \"mute\"\n */\n @property({\n type: Boolean,\n attribute: \"mute\",\n reflect: true,\n })\n mute = false;\n\n /**\n * FFT size for frequency analysis\n * @domAttribute \"fft-size\"\n */\n @property({ type: Number, attribute: \"fft-size\", reflect: true })\n fftSize = 128;\n\n /**\n * FFT decay rate for frequency analysis\n * @domAttribute \"fft-decay\"\n */\n @property({ type: Number, attribute: \"fft-decay\", reflect: true })\n fftDecay = 8;\n\n /**\n * FFT gain for frequency analysis\n * @domAttribute \"fft-gain\"\n */\n @property({ type: Number, attribute: \"fft-gain\", reflect: true })\n fftGain = 3.0;\n\n /**\n * Enable/disable frequency interpolation\n * @domAttribute \"interpolate-frequencies\"\n */\n @property({\n type: Boolean,\n attribute: \"interpolate-frequencies\",\n reflect: true,\n })\n interpolateFrequencies = false;\n\n // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value\n getFreqWeights() {\n if (freqWeightsCache.has(this.fftSize)) {\n // biome-ignore lint/style/noNonNullAssertion: We know the value is set due to the guard above\n return freqWeightsCache.get(this.fftSize)!;\n }\n\n const weights = new Float32Array(this.fftSize / 2).map((_, i) => {\n const frequency = (i * 48000) / this.fftSize;\n if (frequency < 60) return 0.3;\n if (frequency < 250) return 0.4;\n if (frequency < 500) return 0.6;\n if (frequency < 2000) return 0.8;\n if (frequency < 4000) return 1.2;\n if (frequency < 8000) return 1.6;\n return 2.0;\n });\n\n freqWeightsCache.set(this.fftSize, weights);\n return weights;\n }\n\n // Helper method for backwards compatibility\n getShouldInterpolateFrequencies() {\n return this.interpolateFrequencies;\n }\n\n getUrlGenerator() {\n return new UrlGenerator(() => this.apiHost ?? \"\");\n }\n\n // ============================================================================\n // Media Engine - replaced task with async method + cached wrapper\n // ============================================================================\n\n #mediaEngine: MediaEngine | undefined = undefined;\n #mediaEnginePromise: Promise<MediaEngine | undefined> | undefined = undefined;\n #mediaEngineError: Error | undefined = undefined;\n #mediaEngineSrcKey: string | null = null;\n\n /**\n * Async wrapper that mimics Task interface for backwards compatibility.\n * Code expecting mediaEngineTask.value, .taskComplete, .error, .status will still work.\n */\n mediaEngineTask = new AsyncValue<MediaEngine>();\n\n /**\n * Get or create the MediaEngine for this element.\n * Uses caching based on src/fileId to avoid redundant fetches.\n */\n async getMediaEngine(signal?: AbortSignal): Promise<MediaEngine | undefined> {\n if (!this.src && !this.fileId) {\n return undefined;\n }\n\n const srcKey = `${this.src}|${this.fileId}`;\n\n // Return cached if src hasn't changed\n if (this.#mediaEngineSrcKey === srcKey && this.#mediaEngine) {\n this.setContentReadyState(\"ready\");\n return this.#mediaEngine;\n }\n\n // If already loading for this src, wait for it\n if (this.#mediaEngineSrcKey === srcKey && this.#mediaEnginePromise) {\n return this.#mediaEnginePromise;\n }\n\n // Start new load\n this.#mediaEngineSrcKey = srcKey;\n this.mediaEngineTask.startPending();\n this.setContentReadyState(\"loading\");\n\n // Store the handled promise so that concurrent callers at the cache check\n // (line above) get a resolved promise, not a raw rejecting one.\n const loadPromise = this.#loadMediaEngine(signal);\n this.#mediaEnginePromise = loadPromise;\n return loadPromise;\n }\n\n async #loadMediaEngine(signal?: AbortSignal): Promise<MediaEngine | undefined> {\n try {\n this.#mediaEngine = await this.#createMediaEngine(signal);\n this.#mediaEngineError = undefined;\n if (this.#mediaEngine) {\n this.mediaEngineTask.setValue(this.#mediaEngine);\n this.#handleMediaEngineComplete();\n this.setContentReadyState(\"ready\");\n } else {\n // No engine (empty/invalid src) — return to idle\n this.setContentReadyState(\"idle\");\n }\n return this.#mediaEngine;\n } catch (error) {\n this.#mediaEngineError = error instanceof Error ? error : new Error(String(error));\n this.mediaEngineTask.setError(this.#mediaEngineError);\n this.setContentReadyState(\"error\");\n\n // Don't throw for expected errors\n const isExpectedError =\n (error instanceof DOMException && error.name === \"AbortError\") ||\n (error instanceof Error &&\n (error.message === \"No valid media source\" ||\n error.message.includes(\"File not found\") ||\n error.message.includes(\"404\") ||\n error.message.includes(\"Failed to fetch\")));\n\n if (!isExpectedError) {\n console.error(\"Media engine error:\", error);\n }\n\n return undefined;\n }\n }\n\n async #createMediaEngine(signal?: AbortSignal): Promise<MediaEngine | undefined> {\n const { src, fileId, apiHost, requiredTracks } = this;\n const urlGenerator = this.getUrlGenerator();\n return createMediaEngineFromSource({\n src,\n fileId,\n apiHost,\n requiredTracks,\n fetchFn: (url, init) => this.fetch(url, init),\n urlGenerator,\n signal,\n });\n }\n\n #handleMediaEngineComplete(): void {\n // Update self synchronously\n this.requestUpdate(\"intrinsicDurationMs\");\n this.requestUpdate(\"ownCurrentTimeMs\");\n\n // Defer updates to parent/root timegroup\n if (this.rootTimegroup) {\n queueMicrotask(() => {\n this.rootTimegroup?.requestUpdate(\"ownCurrentTimeMs\");\n this.rootTimegroup?.requestUpdate(\"durationMs\");\n });\n }\n }\n\n // ============================================================================\n // Audio Analysis - replaced tasks with async methods + cached wrappers\n // ============================================================================\n\n #frequencyDataCache = new LRUCache<string, Uint8Array>(100);\n #timeDomainDataCache = new LRUCache<string, Uint8Array>(100);\n\n /**\n * Async wrapper for frequency data - mimics Task interface for EFWaveform compatibility\n */\n frequencyDataTask = new AsyncValue<Uint8Array | null>();\n\n /**\n * Async wrapper for time domain data - mimics Task interface for EFWaveform compatibility\n */\n byteTimeDomainTask = new AsyncValue<Uint8Array | null>();\n\n /**\n * Get frequency data for audio visualization at a given time.\n */\n async getFrequencyData(timeMs: number, signal?: AbortSignal): Promise<Uint8Array | null> {\n if (timeMs < 0) return null;\n\n const cacheKey = `${this.getShouldInterpolateFrequencies()}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${timeMs}`;\n const cached = this.#frequencyDataCache.get(cacheKey);\n if (cached) return cached;\n\n try {\n const result = await this.#analyzeFrequencies(timeMs, signal);\n if (result) {\n this.#frequencyDataCache.set(cacheKey, result);\n this.frequencyDataTask.setValue(result);\n }\n return result;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n return null;\n }\n }\n\n /**\n * Get time domain data for audio visualization at a given time.\n */\n async getTimeDomainData(timeMs: number, signal?: AbortSignal): Promise<Uint8Array | null> {\n if (timeMs < 0) return null;\n\n const cacheKey = `${this.fftSize}:${timeMs}`;\n const cached = this.#timeDomainDataCache.get(cacheKey);\n if (cached) return cached;\n\n try {\n const result = await this.#analyzeTimeDomain(timeMs, signal);\n if (result) {\n this.#timeDomainDataCache.set(cacheKey, result);\n this.byteTimeDomainTask.setValue(result);\n }\n return result;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n return null;\n }\n }\n\n async #analyzeFrequencies(\n currentTimeMs: number,\n signal?: AbortSignal,\n ): Promise<Uint8Array | null> {\n const mediaEngine = await this.getMediaEngine(signal);\n signal?.throwIfAborted();\n\n if (!mediaEngine?.tracks.audio) {\n return null;\n }\n\n // Calculate exact audio window needed based on fftDecay and frame timing\n const frameIntervalMs = 1000 / 30;\n const earliestFrameMs = currentTimeMs - (this.fftDecay - 1) * frameIntervalMs;\n const fromMs = Math.max(0, earliestFrameMs);\n const maxToMs = currentTimeMs + frameIntervalMs;\n const videoDurationMs = this.intrinsicDurationMs || 0;\n const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;\n\n if (fromMs >= toMs) {\n return null;\n }\n\n const { fetchAudioSpanningTime: fetchAudioSpan } =\n await import(\"./EFMedia/shared/AudioSpanUtils.js\");\n\n let audioSpan;\n try {\n audioSpan = await fetchAudioSpan(this, fromMs, toMs, signal!);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n return null;\n }\n\n if (!audioSpan?.blob || audioSpan.blob.size < 100) {\n return null;\n }\n\n // Decode the real audio data\n const tempAudioContext = new OfflineAudioContext(2, 48000, 48000);\n const arrayBuffer = await audioSpan.blob.arrayBuffer();\n signal?.throwIfAborted();\n\n let audioBuffer;\n try {\n audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);\n signal?.throwIfAborted();\n } catch {\n return null;\n }\n\n const startOffsetMs = audioSpan.startMs;\n\n const framesData = await Promise.all(\n Array.from({ length: this.fftDecay }, async (_, i) => {\n const frameOffset = i * (1000 / 30);\n const startTime = Math.max(0, (currentTimeMs - frameOffset - startOffsetMs) / 1000);\n\n const SIZE = 48000 / 30;\n const audioContext = new OfflineAudioContext(2, SIZE, 48000);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = this.fftSize;\n analyser.minDecibels = -90;\n analyser.maxDecibels = -10;\n\n const gainNode = audioContext.createGain();\n gainNode.gain.value = this.fftGain;\n\n const filter = audioContext.createBiquadFilter();\n filter.type = \"bandpass\";\n filter.frequency.value = 15000;\n filter.Q.value = 0.05;\n\n const audioBufferSource = audioContext.createBufferSource();\n audioBufferSource.buffer = audioBuffer;\n\n audioBufferSource.connect(filter);\n filter.connect(gainNode);\n gainNode.connect(analyser);\n analyser.connect(audioContext.destination);\n\n audioBufferSource.start(0, startTime, 1 / 30);\n\n try {\n await audioContext.startRendering();\n signal?.throwIfAborted();\n\n const frameData = new Uint8Array(this.fftSize / 2);\n analyser.getByteFrequencyData(frameData);\n return frameData;\n } finally {\n audioBufferSource.disconnect();\n analyser.disconnect();\n }\n }),\n );\n\n const frameLength = framesData[0]?.length ?? 0;\n\n // Combine frames with decay\n const smoothedData = new Uint8Array(frameLength);\n for (let i = 0; i < frameLength; i++) {\n let weightedSum = 0;\n let weightSum = 0;\n\n framesData.forEach((frame: Uint8Array, frameIndex: number) => {\n const decayWeight = DECAY_WEIGHT ** frameIndex;\n weightedSum += (frame[i] ?? 0) * decayWeight;\n weightSum += decayWeight;\n });\n\n smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));\n }\n\n // Apply frequency weights\n smoothedData.forEach((value, i) => {\n const freqWeight = this.getFreqWeights()[i] ?? 0;\n smoothedData[i] = Math.min(255, Math.round(value * freqWeight));\n });\n\n // Only return the lower half of the frequency data\n const slicedData = smoothedData.slice(0, Math.floor(smoothedData.length / 2));\n return this.getShouldInterpolateFrequencies() ? processFFTData(slicedData) : slicedData;\n }\n\n async #analyzeTimeDomain(\n currentTimeMs: number,\n signal?: AbortSignal,\n ): Promise<Uint8Array | null> {\n const mediaEngine = await this.getMediaEngine(signal);\n signal?.throwIfAborted();\n\n if (!mediaEngine?.tracks.audio) {\n return null;\n }\n\n const frameIntervalMs = 1000 / 30;\n const earliestFrameMs = currentTimeMs - (this.fftDecay - 1) * frameIntervalMs;\n const fromMs = Math.max(0, earliestFrameMs);\n const maxToMs = currentTimeMs + frameIntervalMs;\n const videoDurationMs = this.intrinsicDurationMs || 0;\n const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;\n\n if (fromMs >= toMs) {\n return null;\n }\n\n const { fetchAudioSpanningTime: fetchAudioSpan } =\n await import(\"./EFMedia/shared/AudioSpanUtils.js\");\n\n let audioSpan;\n try {\n audioSpan = await fetchAudioSpan(this, fromMs, toMs, signal!);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n return null;\n }\n\n if (!audioSpan?.blob || audioSpan.blob.size < 100) {\n return null;\n }\n\n const tempAudioContext = new OfflineAudioContext(2, 48000, 48000);\n const arrayBuffer = await audioSpan.blob.arrayBuffer();\n signal?.throwIfAborted();\n\n let audioBuffer;\n try {\n audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);\n signal?.throwIfAborted();\n } catch {\n return null;\n }\n\n const startOffsetMs = audioSpan.startMs;\n\n const framesData = await Promise.all(\n Array.from({ length: this.fftDecay }, async (_, i) => {\n const frameOffset = i * (1000 / 30);\n const startTime = Math.max(0, (currentTimeMs - frameOffset - startOffsetMs) / 1000);\n\n const SIZE = 48000 / 30;\n const audioContext = new OfflineAudioContext(2, SIZE, 48000);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = this.fftSize;\n analyser.minDecibels = -90;\n analyser.maxDecibels = -10;\n\n const gainNode = audioContext.createGain();\n gainNode.gain.value = this.fftGain;\n\n const filter = audioContext.createBiquadFilter();\n filter.type = \"bandpass\";\n filter.frequency.value = 15000;\n filter.Q.value = 0.05;\n\n const audioBufferSource = audioContext.createBufferSource();\n audioBufferSource.buffer = audioBuffer;\n\n audioBufferSource.connect(filter);\n filter.connect(gainNode);\n gainNode.connect(analyser);\n analyser.connect(audioContext.destination);\n\n audioBufferSource.start(0, startTime, 1 / 30);\n\n try {\n await audioContext.startRendering();\n signal?.throwIfAborted();\n\n const frameData = new Uint8Array(this.fftSize);\n analyser.getByteTimeDomainData(frameData);\n return frameData;\n } finally {\n audioBufferSource.disconnect();\n analyser.disconnect();\n }\n }),\n );\n\n const frameLength = framesData[0]?.length ?? 0;\n\n // Use RMS calculation to preserve waveform shape\n const smoothedData = new Uint8Array(frameLength);\n for (let i = 0; i < frameLength; i++) {\n let sumSquares = 0;\n framesData.forEach((frame: Uint8Array) => {\n const value = (frame[i] ?? 128) - 128;\n sumSquares += value * value;\n });\n const rms = Math.sqrt(sumSquares / framesData.length);\n smoothedData[i] = Math.min(255, Math.max(0, Math.round(rms + 128)));\n }\n\n return smoothedData;\n }\n\n // ============================================================================\n // Removed task properties - these are kept as stubs for backwards compatibility\n // ============================================================================\n\n // These tasks are no longer used but kept for API compatibility\n audioSegmentIdTask = new AsyncValue<number | undefined>();\n audioInitSegmentFetchTask = new AsyncValue<ArrayBuffer | undefined>();\n audioSegmentFetchTask = new AsyncValue<ArrayBuffer | undefined>();\n audioInputTask = new AsyncValue<any>();\n audioSeekTask = new AsyncValue<any>();\n audioBufferTask = new AsyncValue<any>();\n\n /**\n * The unique identifier for the media file.\n * This property can be set programmatically or via the \"file-id\" attribute.\n * The \"asset-id\" attribute is also supported for backward compatibility.\n * @domAttribute \"file-id\"\n */\n @property({ type: String, attribute: \"file-id\", reflect: true })\n fileId: string | null = null;\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 get intrinsicDurationMs(): number | undefined {\n return this.#mediaEngine?.durationMs;\n }\n\n protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {\n super.updated(changedProperties);\n\n // Trigger media engine load when src or fileId changes\n if (changedProperties.has(\"src\") || changedProperties.has(\"fileId\")) {\n this.getMediaEngine().catch(() => {});\n // Source identity changed — cached renderable output is stale\n if (\n changedProperties.get(\"src\") !== undefined ||\n changedProperties.get(\"fileId\") !== undefined\n ) {\n this.emitContentChange(\"source\");\n }\n }\n\n // Check if our timeline position has actually changed, even if ownCurrentTimeMs isn't tracked as a property\n const newCurrentSourceTimeMs = this.currentSourceTimeMs;\n if (newCurrentSourceTimeMs !== this.desiredSeekTimeMs) {\n this.executeSeek(newCurrentSourceTimeMs);\n }\n\n if (changedProperties.has(\"ownCurrentTimeMs\")) {\n this.executeSeek(this.currentSourceTimeMs);\n }\n\n // Check if trim/source properties changed that affect duration\n const durationAffectingProps = [\"_trimStartMs\", \"_trimEndMs\", \"_sourceInMs\", \"_sourceOutMs\"];\n\n const hasDurationChange = durationAffectingProps.some((prop) => changedProperties.has(prop));\n\n if (hasDurationChange) {\n this.emitContentChange(\"bounds\");\n if (this.parentTimegroup) {\n this.parentTimegroup.requestUpdate(\"durationMs\");\n this.parentTimegroup.requestUpdate(\"currentTime\");\n\n // Also find and directly notify any context provider (ContextMixin)\n let parent = this.parentNode;\n while (parent) {\n if (isContextMixin(parent)) {\n parent.dispatchEvent(\n new CustomEvent(\"child-duration-changed\", {\n detail: { source: this },\n }),\n );\n break;\n }\n parent = parent.parentNode;\n }\n }\n }\n }\n\n get hasOwnDuration(): boolean {\n return true;\n }\n\n @state()\n private _desiredSeekTimeMs = 0; // Initialize to 0 for proper segment loading\n\n get desiredSeekTimeMs(): number {\n return this._desiredSeekTimeMs;\n }\n\n set desiredSeekTimeMs(value: number) {\n if (this._desiredSeekTimeMs !== value) {\n this._desiredSeekTimeMs = value;\n }\n }\n\n protected async executeSeek(seekToMs: number) {\n // The seekToMs parameter should be the timeline-relative media time\n // calculated from currentSourceTimeMs which includes timeline positioning\n this._desiredSeekTimeMs = seekToMs;\n }\n\n /**\n * Main integration method for EFTimegroup audio playback\n * Now powered by clean, testable utility functions\n * Returns undefined if no audio rendition is available\n */\n async fetchAudioSpanningTime(\n fromMs: number,\n toMs: number,\n signal?: AbortSignal,\n ): Promise<AudioSpan | undefined> {\n return withSpan(\n \"media.fetchAudioSpanningTime\",\n {\n elementId: this.id || \"unknown\",\n tagName: this.tagName.toLowerCase(),\n fromMs,\n toMs,\n durationMs: toMs - fromMs,\n src: this.src || \"none\",\n },\n undefined,\n async () => {\n // Create a default signal if not provided (public API convenience)\n const effectiveSignal = signal ?? new AbortController().signal;\n const { fetchAudioSpanningTime } = await import(\"./EFMedia/shared/AudioSpanUtils.js\");\n return fetchAudioSpanningTime(this, fromMs, toMs, effectiveSignal);\n },\n );\n }\n\n /**\n * Wait for media engine to load and determine duration\n * Ensures media is ready for playback\n */\n async waitForMediaDurations(signal?: AbortSignal): Promise<void> {\n if (this.#mediaEngine) {\n return;\n }\n\n try {\n await this.getMediaEngine(signal);\n } catch (error) {\n // Don't throw AbortError - these are intentional cancellations when element is disconnected\n const isAbortError =\n (error instanceof DOMException && error.name === \"AbortError\") ||\n (error instanceof Error &&\n (error.name === \"AbortError\" ||\n error.message.includes(\"signal is aborted\") ||\n error.message.includes(\"The user aborted a request\")));\n\n // If explicitly aborted via signal, throw to propagate cancellation\n if (signal?.aborted) {\n throw error;\n }\n\n // For task abort (element disconnected), silently return\n if (isAbortError) {\n return;\n }\n\n // Re-throw other errors\n throw error;\n }\n }\n\n /**\n * Returns media elements for playback audio rendering\n * For standalone media, returns [this]; for timegroups, returns all descendants\n * Used by PlaybackController for audio-driven playback\n */\n getMediaElements(): EFMedia[] {\n return [this];\n }\n\n /**\n * Render audio buffer for playback\n * Called by PlaybackController during live playback\n * Delegates to shared renderTemporalAudio utility for consistent behavior\n */\n async renderAudio(fromMs: number, toMs: number): Promise<AudioBuffer> {\n return renderTemporalAudio(this, fromMs, toMs);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFMedia.js","names":["assignedElements: Element[]","#value","#error","#status","#promise","#resolvePromise","#mediaEngineSrcKey","#mediaEngine","#mediaEnginePromise","#loadMediaEngine","#createMediaEngine","#mediaEngineError","#handleMediaEngineComplete","#frequencyDataCache","#analyzeFrequencies","#timeDomainDataCache","#analyzeTimeDomain"],"sources":["../../src/elements/EFMedia.ts"],"sourcesContent":["import { provide } from \"@lit/context\";\nimport { css, LitElement, type PropertyValueMap } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { isContextMixin } from \"../gui/ContextMixin.js\";\nimport type { ControllableInterface } from \"../gui/Controllable.js\";\nimport { efContext } from \"../gui/efContext.js\";\nimport { withSpan } from \"../otel/tracingHelpers.js\";\nimport type { MediaEngine } from \"../transcoding/types/index.ts\";\nimport type { AudioSpan } from \"../transcoding/types/index.ts\";\nimport { createMediaEngineFromSource } from \"./EFMedia/MediaEngine.js\";\nimport { UrlGenerator } from \"../transcoding/utils/UrlGenerator.ts\";\nimport { LRUCache } from \"../utils/LRUCache.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\nimport { renderTemporalAudio } from \"./renderTemporalAudio.js\";\nimport { EFTargetable } from \"./TargetController.ts\";\n\n// EF_FRAMEGEN is a global instance created in EF_FRAMEGEN.ts\ndeclare global {\n var EF_FRAMEGEN: import(\"../EF_FRAMEGEN.js\").EFFramegen;\n}\n\nconst freqWeightsCache = new Map<number, Float32Array>();\n\nexport class IgnorableError extends Error {}\n\n/**\n * Gets all child elements including slotted content for shadow DOM elements.\n * Duplicated here to avoid circular imports from EFTemporal.\n */\nconst getChildrenIncludingSlotted = (element: Element): Element[] => {\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll(\"slot\");\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== \"SLOT\") {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n return Array.from(element.children);\n};\n\nexport const deepGetMediaElements = (element: Element, medias: EFMedia[] = []) => {\n const children = getChildrenIncludingSlotted(element);\n for (const child of children) {\n if (child instanceof EFMedia) {\n medias.push(child);\n } else {\n deepGetMediaElements(child, medias);\n }\n }\n return medias;\n};\n\n// Import EFTemporal - use a function wrapper to defer evaluation until class definition\n// This breaks the circular dependency: EFTimegroup -> EFMedia -> EFTemporal\nimport { EFTemporal } from \"./EFTemporal.js\";\n\n/**\n * Simple async value wrapper that mimics Lit Task interface.\n * Used for backwards compatibility with code expecting task-like objects.\n */\nexport class AsyncValue<T> {\n #value: T | undefined = undefined;\n #error: Error | undefined = undefined;\n // oxlint-disable-next-line no-unused-private-class-members -- false positive; read via get status()\n #status: \"initial\" | \"pending\" | \"complete\" | \"error\" = \"initial\";\n #promise: Promise<T | undefined> = Promise.resolve(undefined);\n #resolvePromise: ((value: T | undefined) => void) | undefined;\n\n // Use properties instead of getters to avoid TypeScript declaration generation bug\n get value(): T | undefined {\n return this.#value;\n }\n\n get error(): Error | undefined {\n return this.#error;\n }\n\n get status(): number {\n // Match TaskStatus enum: INITIAL=0, PENDING=1, COMPLETE=2, ERROR=3\n switch (this.#status) {\n case \"initial\":\n return 0;\n case \"pending\":\n return 1;\n case \"complete\":\n return 2;\n case \"error\":\n return 3;\n }\n }\n\n get taskComplete(): Promise<T | undefined> {\n return this.#promise;\n }\n\n /**\n * Set the value (marks status as complete)\n */\n setValue(value: T): void {\n this.#value = value;\n this.#error = undefined;\n this.#status = \"complete\";\n this.#resolvePromise?.(value);\n }\n\n /**\n * Set an error (marks status as error)\n */\n setError(error: Error): void {\n this.#error = error;\n this.#value = undefined;\n this.#status = \"error\";\n // Don't reject - just resolve with undefined to match old behavior\n this.#resolvePromise?.(undefined);\n }\n\n /**\n * Start a new async operation\n */\n startPending(): void {\n this.#status = \"pending\";\n this.#promise = new Promise((resolve) => {\n this.#resolvePromise = resolve;\n });\n // Prevent unhandled rejection warnings\n this.#promise.catch(() => {});\n }\n\n /**\n * Run an async function and update status accordingly\n */\n async run(fn: () => Promise<T>): Promise<T | undefined> {\n this.startPending();\n try {\n const result = await fn();\n this.setValue(result);\n return result;\n } catch (error) {\n if (error instanceof Error) {\n this.setError(error);\n } else {\n this.setError(new Error(String(error)));\n }\n return undefined;\n }\n }\n}\n\n// Audio analysis helper functions\nconst DECAY_WEIGHT = 0.8;\n\nfunction processFFTData(fftData: Uint8Array, zeroThresholdPercent = 0.1): Uint8Array {\n const totalBins = fftData.length;\n const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);\n\n let zeroCount = 0;\n let cutoffIndex = totalBins;\n\n for (let i = totalBins - 1; i >= 0; i--) {\n if (fftData[i] ?? 0 < 10) {\n zeroCount++;\n } else {\n if (zeroCount >= zeroThresholdCount) {\n cutoffIndex = i + 1;\n break;\n }\n }\n }\n\n if (cutoffIndex < zeroThresholdCount) {\n return fftData;\n }\n\n const goodData = fftData.slice(0, cutoffIndex);\n const resampledData = interpolateData(goodData, fftData.length);\n\n const attenuationStartIndex = Math.floor(totalBins * 0.9);\n for (let i = attenuationStartIndex; i < totalBins; i++) {\n const attenuationProgress =\n (i - attenuationStartIndex) / (totalBins - attenuationStartIndex) + 0.2;\n const attenuationFactor = Math.max(0, 1 - attenuationProgress);\n resampledData[i] = Math.floor((resampledData[i] ?? 0) * attenuationFactor);\n }\n\n return resampledData;\n}\n\nfunction interpolateData(data: Uint8Array, targetSize: number): Uint8Array {\n const resampled = new Uint8Array(targetSize);\n const dataLength = data.length;\n\n for (let i = 0; i < targetSize; i++) {\n const ratio = (i / (targetSize - 1)) * (dataLength - 1);\n const index = Math.floor(ratio);\n const fraction = ratio - index;\n\n if (index >= dataLength - 1) {\n resampled[i] = data[dataLength - 1] ?? 0;\n } else {\n resampled[i] = Math.round(\n (data[index] ?? 0) * (1 - fraction) + (data[index + 1] ?? 0) * fraction,\n );\n }\n }\n\n return resampled;\n}\n\nexport class EFMedia extends EFTargetable(\n EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {\n assetType: \"isobmff_files\",\n }),\n) {\n @provide({ context: efContext })\n get efContext(): ControllableInterface | null {\n return this.rootTimegroup ?? this;\n }\n\n override shouldAutoReady(): boolean {\n return false;\n }\n\n // Sample buffer size configuration\n // 90 frames = 3 seconds at 30fps. A rolling buffer this large means backward\n // seeks within a 3-second window are pure buffer hits — no VideoDecoder reset,\n // no re-decode from the keyframe. SampleBuffer.push() closes evicted frames so\n // GPU memory stays bounded to ~90 frames × ~3 MB ≈ 270 MB across all instances.\n static readonly VIDEO_SAMPLE_BUFFER_SIZE = 90;\n static readonly AUDIO_SAMPLE_BUFFER_SIZE = 120;\n\n /**\n * Which tracks this media element requires.\n * Subclasses can override to specify their needs:\n * - \"audio\" - Only needs audio track (e.g., EFAudio)\n * - \"video\" - Only needs video track\n * - \"both\" - Needs both tracks (default for backwards compatibility)\n *\n * This is used during media engine creation to skip validation\n * of tracks that won't be used, avoiding unnecessary network requests.\n */\n get requiredTracks(): \"audio\" | \"video\" | \"both\" {\n return \"both\";\n }\n\n static get observedAttributes() {\n // biome-ignore lint/complexity/noThisInStatic: We need to access super\n const parentAttributes = super.observedAttributes || [];\n return [\n ...parentAttributes,\n \"mute\",\n \"fft-size\",\n \"fft-decay\",\n \"fft-gain\",\n \"interpolate-frequencies\",\n \"file-id\",\n \"asset-id\",\n \"audio-buffer-duration\",\n \"max-audio-buffer-fetches\",\n \"enable-audio-buffering\",\n \"sourcein\",\n \"sourceout\",\n ];\n }\n\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n overflow: hidden;\n }\n `,\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 /**\n * Duration in milliseconds for audio buffering ahead of current time\n * @domAttribute \"audio-buffer-duration\"\n */\n @property({ type: Number, attribute: \"audio-buffer-duration\" })\n audioBufferDurationMs = 10000; // 10 seconds - reasonable for JIT encoding\n\n /**\n * Maximum number of concurrent audio segment fetches for buffering\n * @domAttribute \"max-audio-buffer-fetches\"\n */\n @property({ type: Number, attribute: \"max-audio-buffer-fetches\" })\n maxAudioBufferFetches = 2;\n\n /**\n * Enable/disable audio buffering system\n * @domAttribute \"enable-audio-buffering\"\n */\n @property({ type: Boolean, attribute: \"enable-audio-buffering\" })\n enableAudioBuffering = true;\n\n /**\n * Mute/unmute the media element\n * @domAttribute \"mute\"\n */\n @property({\n type: Boolean,\n attribute: \"mute\",\n reflect: true,\n })\n mute = false;\n\n /**\n * FFT size for frequency analysis\n * @domAttribute \"fft-size\"\n */\n @property({ type: Number, attribute: \"fft-size\", reflect: true })\n fftSize = 128;\n\n /**\n * FFT decay rate for frequency analysis\n * @domAttribute \"fft-decay\"\n */\n @property({ type: Number, attribute: \"fft-decay\", reflect: true })\n fftDecay = 8;\n\n /**\n * FFT gain for frequency analysis\n * @domAttribute \"fft-gain\"\n */\n @property({ type: Number, attribute: \"fft-gain\", reflect: true })\n fftGain = 3.0;\n\n /**\n * Enable/disable frequency interpolation\n * @domAttribute \"interpolate-frequencies\"\n */\n @property({\n type: Boolean,\n attribute: \"interpolate-frequencies\",\n reflect: true,\n })\n interpolateFrequencies = false;\n\n // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value\n getFreqWeights() {\n if (freqWeightsCache.has(this.fftSize)) {\n // biome-ignore lint/style/noNonNullAssertion: We know the value is set due to the guard above\n return freqWeightsCache.get(this.fftSize)!;\n }\n\n const weights = new Float32Array(this.fftSize / 2).map((_, i) => {\n const frequency = (i * 48000) / this.fftSize;\n if (frequency < 60) return 0.3;\n if (frequency < 250) return 0.4;\n if (frequency < 500) return 0.6;\n if (frequency < 2000) return 0.8;\n if (frequency < 4000) return 1.2;\n if (frequency < 8000) return 1.6;\n return 2.0;\n });\n\n freqWeightsCache.set(this.fftSize, weights);\n return weights;\n }\n\n // Helper method for backwards compatibility\n getShouldInterpolateFrequencies() {\n return this.interpolateFrequencies;\n }\n\n getUrlGenerator() {\n return new UrlGenerator(() => this.apiHost ?? \"\");\n }\n\n // ============================================================================\n // Media Engine - replaced task with async method + cached wrapper\n // ============================================================================\n\n #mediaEngine: MediaEngine | undefined = undefined;\n #mediaEnginePromise: Promise<MediaEngine | undefined> | undefined = undefined;\n #mediaEngineError: Error | undefined = undefined;\n #mediaEngineSrcKey: string | null = null;\n\n /**\n * Async wrapper that mimics Task interface for backwards compatibility.\n * Code expecting mediaEngineTask.value, .taskComplete, .error, .status will still work.\n */\n mediaEngineTask = new AsyncValue<MediaEngine>();\n\n /**\n * Get or create the MediaEngine for this element.\n * Uses caching based on src/fileId to avoid redundant fetches.\n */\n async getMediaEngine(signal?: AbortSignal): Promise<MediaEngine | undefined> {\n if (!this.src && !this.fileId) {\n return undefined;\n }\n\n const srcKey = `${this.src}|${this.fileId}`;\n\n // Return cached if src hasn't changed\n if (this.#mediaEngineSrcKey === srcKey && this.#mediaEngine) {\n this.setContentReadyState(\"ready\");\n return this.#mediaEngine;\n }\n\n // If already loading for this src, wait for it\n if (this.#mediaEngineSrcKey === srcKey && this.#mediaEnginePromise) {\n return this.#mediaEnginePromise;\n }\n\n // Start new load\n this.#mediaEngineSrcKey = srcKey;\n this.mediaEngineTask.startPending();\n this.setContentReadyState(\"loading\");\n\n // Store the handled promise so that concurrent callers at the cache check\n // (line above) get a resolved promise, not a raw rejecting one.\n const loadPromise = this.#loadMediaEngine(signal);\n this.#mediaEnginePromise = loadPromise;\n return loadPromise;\n }\n\n async #loadMediaEngine(signal?: AbortSignal): Promise<MediaEngine | undefined> {\n try {\n this.#mediaEngine = await this.#createMediaEngine(signal);\n this.#mediaEngineError = undefined;\n if (this.#mediaEngine) {\n this.mediaEngineTask.setValue(this.#mediaEngine);\n this.#handleMediaEngineComplete();\n this.setContentReadyState(\"ready\");\n } else {\n // No engine (empty/invalid src) — return to idle\n this.setContentReadyState(\"idle\");\n }\n return this.#mediaEngine;\n } catch (error) {\n this.#mediaEngineError = error instanceof Error ? error : new Error(String(error));\n this.mediaEngineTask.setError(this.#mediaEngineError);\n this.setContentReadyState(\"error\");\n\n // Don't throw for expected errors\n const isExpectedError =\n (error instanceof DOMException && error.name === \"AbortError\") ||\n (error instanceof Error &&\n (error.message === \"No valid media source\" ||\n error.message.includes(\"File not found\") ||\n error.message.includes(\"404\") ||\n error.message.includes(\"Failed to fetch\")));\n\n if (!isExpectedError) {\n console.error(\"Media engine error:\", error);\n }\n\n return undefined;\n }\n }\n\n async #createMediaEngine(signal?: AbortSignal): Promise<MediaEngine | undefined> {\n const { src, fileId, apiHost, requiredTracks } = this;\n const urlGenerator = this.getUrlGenerator();\n return createMediaEngineFromSource({\n src,\n fileId,\n apiHost,\n requiredTracks,\n fetchFn: (url, init) => this.fetch(url, init),\n urlGenerator,\n signal,\n });\n }\n\n #handleMediaEngineComplete(): void {\n // Update self synchronously\n this.requestUpdate(\"intrinsicDurationMs\");\n this.requestUpdate(\"ownCurrentTimeMs\");\n\n // Defer updates to parent/root timegroup\n if (this.rootTimegroup) {\n queueMicrotask(() => {\n this.rootTimegroup?.requestUpdate(\"ownCurrentTimeMs\");\n this.rootTimegroup?.requestUpdate(\"durationMs\");\n });\n }\n }\n\n // ============================================================================\n // Audio Analysis - replaced tasks with async methods + cached wrappers\n // ============================================================================\n\n #frequencyDataCache = new LRUCache<string, Uint8Array>(100);\n #timeDomainDataCache = new LRUCache<string, Uint8Array>(100);\n\n /**\n * Async wrapper for frequency data - mimics Task interface for EFWaveform compatibility\n */\n frequencyDataTask = new AsyncValue<Uint8Array | null>();\n\n /**\n * Async wrapper for time domain data - mimics Task interface for EFWaveform compatibility\n */\n byteTimeDomainTask = new AsyncValue<Uint8Array | null>();\n\n /**\n * Get frequency data for audio visualization at a given time.\n */\n async getFrequencyData(timeMs: number, signal?: AbortSignal): Promise<Uint8Array | null> {\n if (timeMs < 0) return null;\n\n const cacheKey = `${this.getShouldInterpolateFrequencies()}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${timeMs}`;\n const cached = this.#frequencyDataCache.get(cacheKey);\n if (cached) return cached;\n\n try {\n const result = await this.#analyzeFrequencies(timeMs, signal);\n if (result) {\n this.#frequencyDataCache.set(cacheKey, result);\n this.frequencyDataTask.setValue(result);\n }\n return result;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n return null;\n }\n }\n\n /**\n * Get time domain data for audio visualization at a given time.\n */\n async getTimeDomainData(timeMs: number, signal?: AbortSignal): Promise<Uint8Array | null> {\n if (timeMs < 0) return null;\n\n const cacheKey = `${this.fftSize}:${timeMs}`;\n const cached = this.#timeDomainDataCache.get(cacheKey);\n if (cached) return cached;\n\n try {\n const result = await this.#analyzeTimeDomain(timeMs, signal);\n if (result) {\n this.#timeDomainDataCache.set(cacheKey, result);\n this.byteTimeDomainTask.setValue(result);\n }\n return result;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n return null;\n }\n }\n\n async #analyzeFrequencies(\n currentTimeMs: number,\n signal?: AbortSignal,\n ): Promise<Uint8Array | null> {\n const mediaEngine = await this.getMediaEngine(signal);\n signal?.throwIfAborted();\n\n if (!mediaEngine?.tracks.audio) {\n return null;\n }\n\n // Calculate exact audio window needed based on fftDecay and frame timing\n const frameIntervalMs = 1000 / 30;\n const earliestFrameMs = currentTimeMs - (this.fftDecay - 1) * frameIntervalMs;\n const fromMs = Math.max(0, earliestFrameMs);\n const maxToMs = currentTimeMs + frameIntervalMs;\n const videoDurationMs = this.intrinsicDurationMs || 0;\n const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;\n\n if (fromMs >= toMs) {\n return null;\n }\n\n const { fetchAudioSpanningTime: fetchAudioSpan } =\n await import(\"./EFMedia/shared/AudioSpanUtils.js\");\n\n let audioSpan;\n try {\n audioSpan = await fetchAudioSpan(this, fromMs, toMs, signal!);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n return null;\n }\n\n if (!audioSpan?.blob || audioSpan.blob.size < 100) {\n return null;\n }\n\n // Decode the real audio data\n const tempAudioContext = new OfflineAudioContext(2, 48000, 48000);\n const arrayBuffer = await audioSpan.blob.arrayBuffer();\n signal?.throwIfAborted();\n\n let audioBuffer;\n try {\n audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);\n signal?.throwIfAborted();\n } catch {\n return null;\n }\n\n const startOffsetMs = audioSpan.startMs;\n\n const framesData = await Promise.all(\n Array.from({ length: this.fftDecay }, async (_, i) => {\n const frameOffset = i * (1000 / 30);\n const startTime = Math.max(0, (currentTimeMs - frameOffset - startOffsetMs) / 1000);\n\n const SIZE = 48000 / 30;\n const audioContext = new OfflineAudioContext(2, SIZE, 48000);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = this.fftSize;\n analyser.minDecibels = -90;\n analyser.maxDecibels = -10;\n\n const gainNode = audioContext.createGain();\n gainNode.gain.value = this.fftGain;\n\n const filter = audioContext.createBiquadFilter();\n filter.type = \"bandpass\";\n filter.frequency.value = 15000;\n filter.Q.value = 0.05;\n\n const audioBufferSource = audioContext.createBufferSource();\n audioBufferSource.buffer = audioBuffer;\n\n audioBufferSource.connect(filter);\n filter.connect(gainNode);\n gainNode.connect(analyser);\n analyser.connect(audioContext.destination);\n\n audioBufferSource.start(0, startTime, 1 / 30);\n\n try {\n await audioContext.startRendering();\n signal?.throwIfAborted();\n\n const frameData = new Uint8Array(this.fftSize / 2);\n analyser.getByteFrequencyData(frameData);\n return frameData;\n } finally {\n audioBufferSource.disconnect();\n analyser.disconnect();\n }\n }),\n );\n\n const frameLength = framesData[0]?.length ?? 0;\n\n // Combine frames with decay\n const smoothedData = new Uint8Array(frameLength);\n for (let i = 0; i < frameLength; i++) {\n let weightedSum = 0;\n let weightSum = 0;\n\n framesData.forEach((frame: Uint8Array, frameIndex: number) => {\n const decayWeight = DECAY_WEIGHT ** frameIndex;\n weightedSum += (frame[i] ?? 0) * decayWeight;\n weightSum += decayWeight;\n });\n\n smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));\n }\n\n // Apply frequency weights\n smoothedData.forEach((value, i) => {\n const freqWeight = this.getFreqWeights()[i] ?? 0;\n smoothedData[i] = Math.min(255, Math.round(value * freqWeight));\n });\n\n // Only return the lower half of the frequency data\n const slicedData = smoothedData.slice(0, Math.floor(smoothedData.length / 2));\n return this.getShouldInterpolateFrequencies() ? processFFTData(slicedData) : slicedData;\n }\n\n async #analyzeTimeDomain(\n currentTimeMs: number,\n signal?: AbortSignal,\n ): Promise<Uint8Array | null> {\n const mediaEngine = await this.getMediaEngine(signal);\n signal?.throwIfAborted();\n\n if (!mediaEngine?.tracks.audio) {\n return null;\n }\n\n const frameIntervalMs = 1000 / 30;\n const earliestFrameMs = currentTimeMs - (this.fftDecay - 1) * frameIntervalMs;\n const fromMs = Math.max(0, earliestFrameMs);\n const maxToMs = currentTimeMs + frameIntervalMs;\n const videoDurationMs = this.intrinsicDurationMs || 0;\n const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;\n\n if (fromMs >= toMs) {\n return null;\n }\n\n const { fetchAudioSpanningTime: fetchAudioSpan } =\n await import(\"./EFMedia/shared/AudioSpanUtils.js\");\n\n let audioSpan;\n try {\n audioSpan = await fetchAudioSpan(this, fromMs, toMs, signal!);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n return null;\n }\n\n if (!audioSpan?.blob || audioSpan.blob.size < 100) {\n return null;\n }\n\n const tempAudioContext = new OfflineAudioContext(2, 48000, 48000);\n const arrayBuffer = await audioSpan.blob.arrayBuffer();\n signal?.throwIfAborted();\n\n let audioBuffer;\n try {\n audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);\n signal?.throwIfAborted();\n } catch {\n return null;\n }\n\n const startOffsetMs = audioSpan.startMs;\n\n const framesData = await Promise.all(\n Array.from({ length: this.fftDecay }, async (_, i) => {\n const frameOffset = i * (1000 / 30);\n const startTime = Math.max(0, (currentTimeMs - frameOffset - startOffsetMs) / 1000);\n\n const SIZE = 48000 / 30;\n const audioContext = new OfflineAudioContext(2, SIZE, 48000);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = this.fftSize;\n analyser.minDecibels = -90;\n analyser.maxDecibels = -10;\n\n const gainNode = audioContext.createGain();\n gainNode.gain.value = this.fftGain;\n\n const filter = audioContext.createBiquadFilter();\n filter.type = \"bandpass\";\n filter.frequency.value = 15000;\n filter.Q.value = 0.05;\n\n const audioBufferSource = audioContext.createBufferSource();\n audioBufferSource.buffer = audioBuffer;\n\n audioBufferSource.connect(filter);\n filter.connect(gainNode);\n gainNode.connect(analyser);\n analyser.connect(audioContext.destination);\n\n audioBufferSource.start(0, startTime, 1 / 30);\n\n try {\n await audioContext.startRendering();\n signal?.throwIfAborted();\n\n const frameData = new Uint8Array(this.fftSize);\n analyser.getByteTimeDomainData(frameData);\n return frameData;\n } finally {\n audioBufferSource.disconnect();\n analyser.disconnect();\n }\n }),\n );\n\n const frameLength = framesData[0]?.length ?? 0;\n\n // Use RMS calculation to preserve waveform shape\n const smoothedData = new Uint8Array(frameLength);\n for (let i = 0; i < frameLength; i++) {\n let sumSquares = 0;\n framesData.forEach((frame: Uint8Array) => {\n const value = (frame[i] ?? 128) - 128;\n sumSquares += value * value;\n });\n const rms = Math.sqrt(sumSquares / framesData.length);\n smoothedData[i] = Math.min(255, Math.max(0, Math.round(rms + 128)));\n }\n\n return smoothedData;\n }\n\n // ============================================================================\n // Removed task properties - these are kept as stubs for backwards compatibility\n // ============================================================================\n\n // These tasks are no longer used but kept for API compatibility\n audioSegmentIdTask = new AsyncValue<number | undefined>();\n audioInitSegmentFetchTask = new AsyncValue<ArrayBuffer | undefined>();\n audioSegmentFetchTask = new AsyncValue<ArrayBuffer | undefined>();\n audioInputTask = new AsyncValue<any>();\n audioSeekTask = new AsyncValue<any>();\n audioBufferTask = new AsyncValue<any>();\n\n /**\n * The unique identifier for the media file.\n * This property can be set programmatically or via the \"file-id\" attribute.\n * The \"asset-id\" attribute is also supported for backward compatibility.\n * @domAttribute \"file-id\"\n */\n @property({ type: String, attribute: \"file-id\", reflect: true })\n fileId: string | null = null;\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 get intrinsicDurationMs(): number | undefined {\n return this.#mediaEngine?.durationMs;\n }\n\n protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {\n super.updated(changedProperties);\n\n // Trigger media engine load when src or fileId changes\n if (changedProperties.has(\"src\") || changedProperties.has(\"fileId\")) {\n this.getMediaEngine().catch(() => {});\n // Source identity changed — cached renderable output is stale\n if (\n changedProperties.get(\"src\") !== undefined ||\n changedProperties.get(\"fileId\") !== undefined\n ) {\n this.emitContentChange(\"source\");\n }\n }\n\n // Check if our timeline position has actually changed, even if ownCurrentTimeMs isn't tracked as a property\n const newCurrentSourceTimeMs = this.currentSourceTimeMs;\n if (newCurrentSourceTimeMs !== this.desiredSeekTimeMs) {\n this.executeSeek(newCurrentSourceTimeMs);\n }\n\n if (changedProperties.has(\"ownCurrentTimeMs\")) {\n this.executeSeek(this.currentSourceTimeMs);\n }\n\n // Check if trim/source properties changed that affect duration\n const durationAffectingProps = [\"_trimStartMs\", \"_trimEndMs\", \"_sourceInMs\", \"_sourceOutMs\"];\n\n const hasDurationChange = durationAffectingProps.some((prop) => changedProperties.has(prop));\n\n if (hasDurationChange) {\n this.emitContentChange(\"bounds\");\n if (this.parentTimegroup) {\n this.parentTimegroup.requestUpdate(\"durationMs\");\n this.parentTimegroup.requestUpdate(\"currentTime\");\n\n // Also find and directly notify any context provider (ContextMixin)\n let parent = this.parentNode;\n while (parent) {\n if (isContextMixin(parent)) {\n parent.dispatchEvent(\n new CustomEvent(\"child-duration-changed\", {\n detail: { source: this },\n }),\n );\n break;\n }\n parent = parent.parentNode;\n }\n }\n }\n }\n\n get hasOwnDuration(): boolean {\n return true;\n }\n\n @state()\n private _desiredSeekTimeMs = 0; // Initialize to 0 for proper segment loading\n\n get desiredSeekTimeMs(): number {\n return this._desiredSeekTimeMs;\n }\n\n set desiredSeekTimeMs(value: number) {\n if (this._desiredSeekTimeMs !== value) {\n this._desiredSeekTimeMs = value;\n }\n }\n\n protected async executeSeek(seekToMs: number) {\n // The seekToMs parameter should be the timeline-relative media time\n // calculated from currentSourceTimeMs which includes timeline positioning\n this._desiredSeekTimeMs = seekToMs;\n }\n\n /**\n * Main integration method for EFTimegroup audio playback\n * Now powered by clean, testable utility functions\n * Returns undefined if no audio rendition is available\n */\n async fetchAudioSpanningTime(\n fromMs: number,\n toMs: number,\n signal?: AbortSignal,\n ): Promise<AudioSpan | undefined> {\n return withSpan(\n \"media.fetchAudioSpanningTime\",\n {\n elementId: this.id || \"unknown\",\n tagName: this.tagName.toLowerCase(),\n fromMs,\n toMs,\n durationMs: toMs - fromMs,\n src: this.src || \"none\",\n },\n undefined,\n async () => {\n // Create a default signal if not provided (public API convenience)\n const effectiveSignal = signal ?? new AbortController().signal;\n const { fetchAudioSpanningTime } = await import(\"./EFMedia/shared/AudioSpanUtils.js\");\n return fetchAudioSpanningTime(this, fromMs, toMs, effectiveSignal);\n },\n );\n }\n\n /**\n * Wait for media engine to load and determine duration\n * Ensures media is ready for playback\n */\n async waitForMediaDurations(signal?: AbortSignal): Promise<void> {\n if (this.#mediaEngine) {\n return;\n }\n\n try {\n await this.getMediaEngine(signal);\n } catch (error) {\n // Don't throw AbortError - these are intentional cancellations when element is disconnected\n const isAbortError =\n (error instanceof DOMException && error.name === \"AbortError\") ||\n (error instanceof Error &&\n (error.name === \"AbortError\" ||\n error.message.includes(\"signal is aborted\") ||\n error.message.includes(\"The user aborted a request\")));\n\n // If explicitly aborted via signal, throw to propagate cancellation\n if (signal?.aborted) {\n throw error;\n }\n\n // For task abort (element disconnected), silently return\n if (isAbortError) {\n return;\n }\n\n // Re-throw other errors\n throw error;\n }\n }\n\n /**\n * Returns media elements for playback audio rendering\n * For standalone media, returns [this]; for timegroups, returns all descendants\n * Used by PlaybackController for audio-driven playback\n */\n getMediaElements(): EFMedia[] {\n return [this];\n }\n\n /**\n * Render audio buffer for playback\n * Called by PlaybackController during live playback\n * Delegates to shared renderTemporalAudio utility for consistent behavior\n */\n async renderAudio(fromMs: number, toMs: number): Promise<AudioBuffer> {\n return renderTemporalAudio(this, fromMs, toMs);\n }\n}\n"],"mappings":"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"}
|