@editframe/elements 0.38.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/elements/EFCaptions.js +1 -1
  2. package/dist/elements/EFCaptions.js.map +1 -1
  3. package/dist/elements/EFImage.js +3 -4
  4. package/dist/elements/EFImage.js.map +1 -1
  5. package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
  6. package/dist/elements/EFMedia/CachedFetcher.js +99 -0
  7. package/dist/elements/EFMedia/CachedFetcher.js.map +1 -0
  8. package/dist/elements/EFMedia/MediaEngine.d.ts +19 -0
  9. package/dist/elements/EFMedia/MediaEngine.js +129 -0
  10. package/dist/elements/EFMedia/MediaEngine.js.map +1 -0
  11. package/dist/elements/EFMedia/SegmentIndex.d.ts +32 -0
  12. package/dist/elements/EFMedia/SegmentIndex.js +185 -0
  13. package/dist/elements/EFMedia/SegmentIndex.js.map +1 -0
  14. package/dist/elements/EFMedia/SegmentTransport.d.ts +12 -0
  15. package/dist/elements/EFMedia/SegmentTransport.js +69 -0
  16. package/dist/elements/EFMedia/SegmentTransport.js.map +1 -0
  17. package/dist/elements/EFMedia/TimingModel.d.ts +10 -0
  18. package/dist/elements/EFMedia/TimingModel.js +28 -0
  19. package/dist/elements/EFMedia/TimingModel.js.map +1 -0
  20. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +7 -6
  21. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  22. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +13 -34
  23. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  24. package/dist/elements/EFMedia.d.ts +2 -1
  25. package/dist/elements/EFMedia.js +14 -31
  26. package/dist/elements/EFMedia.js.map +1 -1
  27. package/dist/elements/EFSourceMixin.js +1 -1
  28. package/dist/elements/EFSourceMixin.js.map +1 -1
  29. package/dist/elements/EFTemporal.js +2 -1
  30. package/dist/elements/EFTemporal.js.map +1 -1
  31. package/dist/elements/EFTimegroup.js +2 -1
  32. package/dist/elements/EFTimegroup.js.map +1 -1
  33. package/dist/elements/EFVideo.js +204 -187
  34. package/dist/elements/EFVideo.js.map +1 -1
  35. package/dist/gui/EFConfiguration.d.ts +0 -7
  36. package/dist/gui/EFConfiguration.js +0 -5
  37. package/dist/gui/EFConfiguration.js.map +1 -1
  38. package/dist/gui/EFWorkbench.d.ts +2 -0
  39. package/dist/gui/EFWorkbench.js +68 -1
  40. package/dist/gui/EFWorkbench.js.map +1 -1
  41. package/dist/gui/PlaybackController.d.ts +2 -0
  42. package/dist/gui/PlaybackController.js +11 -1
  43. package/dist/gui/PlaybackController.js.map +1 -1
  44. package/dist/gui/ef-theme.css +11 -0
  45. package/dist/gui/timeline/tracks/AudioTrack.js +28 -30
  46. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  47. package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +1 -0
  48. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +41 -8
  49. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
  50. package/dist/gui/timeline/tracks/VideoTrack.js +2 -2
  51. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  52. package/dist/gui/timeline/tracks/waveformUtils.js +19 -19
  53. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  54. package/dist/preview/QualityUpgradeScheduler.d.ts +8 -0
  55. package/dist/preview/QualityUpgradeScheduler.js +13 -1
  56. package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
  57. package/dist/preview/renderTimegroupToVideo.js +3 -3
  58. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  59. package/dist/preview/renderVideoToVideo.js +5 -6
  60. package/dist/preview/renderVideoToVideo.js.map +1 -1
  61. package/dist/transcoding/types/index.d.ts +6 -94
  62. package/dist/transcoding/utils/UrlGenerator.d.ts +3 -12
  63. package/dist/transcoding/utils/UrlGenerator.js +3 -29
  64. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  65. package/package.json +2 -2
  66. package/test/setup.ts +1 -1
  67. package/test/useAssetMSW.ts +0 -100
  68. package/dist/elements/EFMedia/AssetMediaEngine.js +0 -284
  69. package/dist/elements/EFMedia/AssetMediaEngine.js.map +0 -1
  70. package/dist/elements/EFMedia/BaseMediaEngine.js +0 -200
  71. package/dist/elements/EFMedia/BaseMediaEngine.js.map +0 -1
  72. package/dist/elements/EFMedia/FileMediaEngine.js +0 -122
  73. package/dist/elements/EFMedia/FileMediaEngine.js.map +0 -1
  74. package/dist/elements/EFMedia/JitMediaEngine.js +0 -157
  75. package/dist/elements/EFMedia/JitMediaEngine.js.map +0 -1
@@ -1,106 +1,6 @@
1
- /**
2
- * Asset-specific MSW handlers for testing
3
- * Provides pre-configured handlers for asset fragment indexes and track data
4
- */
5
-
6
1
  import { HttpResponse, http } from "msw";
7
2
 
8
- /**
9
- * Asset MSW handlers that redirect requests to real test assets
10
- * Use with MSW worker.use() to proxy asset requests to /test-assets/asset-mode/
11
- */
12
3
  export const assetMSWHandlers = [
13
- // Fragment index handler - rewrite to test asset
14
- http.get("/@ef-track-fragment-index/*", async () => {
15
- const response = await fetch("/asset-mode/index.json");
16
- const data = await response.json();
17
- return HttpResponse.json(data);
18
- }),
19
-
20
- // Track data handler - rewrite to test asset with proper range support
21
- http.get("/@ef-track/*", async ({ request }) => {
22
- const url = new URL(request.url);
23
- const trackId = url.searchParams.get("trackId");
24
- if (!trackId) {
25
- return new HttpResponse(null, { status: 400 });
26
- }
27
-
28
- const rangeHeader = request.headers.get("range");
29
- const response = await fetch(`/asset-mode/track-${trackId}.mp4`, {
30
- headers: {
31
- ...(rangeHeader && {
32
- range: rangeHeader,
33
- }),
34
- },
35
- });
36
-
37
- const contentRangeHeader = response.headers.get("Content-Range");
38
- return new HttpResponse(await response.arrayBuffer(), {
39
- status: response.status,
40
- headers: {
41
- "Content-Type": "video/mp4",
42
- "Accept-Ranges": "bytes",
43
- ...(contentRangeHeader && {
44
- "Content-Range": contentRangeHeader,
45
- }),
46
- },
47
- });
48
- }),
49
-
50
- // Asset ID API handlers - these are needed when tests set assetId properties
51
- http.get("/api/v1/isobmff_files/:assetId/index", async () => {
52
- const mockIndex = {
53
- 0: {
54
- duration: 10000,
55
- timescale: 1000,
56
- fragments: [
57
- {
58
- offset: 0,
59
- size: 1024,
60
- timestamp: 0,
61
- duration: 10000,
62
- },
63
- ],
64
- },
65
- };
66
-
67
- return HttpResponse.json(mockIndex, {
68
- headers: {
69
- "Content-Type": "application/json",
70
- },
71
- });
72
- }),
73
-
74
- http.get("/api/v1/isobmff_tracks/:assetId/:trackId", async ({ request }) => {
75
- // Check if this is a range request
76
- const rangeHeader = request.headers.get("range");
77
-
78
- if (rangeHeader) {
79
- // Return a mock MP4 segment with proper range headers
80
- const mockData = new ArrayBuffer(1024); // 1KB mock data
81
- return new HttpResponse(mockData, {
82
- status: 206,
83
- headers: {
84
- "Content-Type": "video/mp4",
85
- "Accept-Ranges": "bytes",
86
- "Content-Range": rangeHeader,
87
- "Content-Length": "1024",
88
- },
89
- });
90
- }
91
-
92
- // Return the full mock track
93
- const mockData = new ArrayBuffer(1024);
94
- return new HttpResponse(mockData, {
95
- status: 200,
96
- headers: {
97
- "Content-Type": "video/mp4",
98
- "Accept-Ranges": "bytes",
99
- "Content-Length": "1024",
100
- },
101
- });
102
- }),
103
-
104
4
  http.get("/api/v1/files/:id/index", async () => {
105
5
  const mockIndex = {
106
6
  0: {
@@ -1,284 +0,0 @@
1
- import { withSpan } from "../../otel/tracingHelpers.js";
2
- import { ThumbnailExtractor } from "./shared/ThumbnailExtractor.js";
3
- import { BaseMediaEngine, mediaCache } from "./BaseMediaEngine.js";
4
- import { convertToScaledTime, roundToMilliseconds } from "./shared/PrecisionUtils.js";
5
-
6
- //#region src/elements/EFMedia/AssetMediaEngine.ts
7
- var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
8
- constructor(host, src, urlGenerator) {
9
- super(host);
10
- this.data = {};
11
- this.durationMs = 0;
12
- this.src = src;
13
- this.thumbnailExtractor = new ThumbnailExtractor(this);
14
- this.urlGenerator = urlGenerator;
15
- }
16
- static async fetch(host, urlGenerator, src, requiredTracks = "both", signal) {
17
- const engine = new AssetMediaEngine(host, src, urlGenerator);
18
- let normalizedSrc = src.startsWith("/") ? src.slice(1) : src;
19
- normalizedSrc = normalizedSrc.replace(/^\/+/, "");
20
- const apiBaseUrl = urlGenerator.getBaseUrl();
21
- const url = apiBaseUrl ? `${apiBaseUrl}/api/v1/files/local/index?src=${encodeURIComponent(normalizedSrc)}` : `/api/v1/files/local/index?src=${encodeURIComponent(normalizedSrc)}`;
22
- engine.data = await engine.fetchManifest(url, signal);
23
- signal?.throwIfAborted();
24
- engine.durationMs = Object.values(engine.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0) * 1e3;
25
- if (src.startsWith("/")) engine.src = src.slice(1);
26
- const sourceUrl = engine.getSourceUrlForJit();
27
- const jitBaseUrl = engine.getBaseUrlForJit();
28
- engine.templates = {
29
- initSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,
30
- mediaSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`
31
- };
32
- if (signal) {
33
- const videoTrack = engine.getVideoTrackIndex();
34
- const audioTrack = engine.getAudioTrackIndex();
35
- const needsVideo = requiredTracks === "video" || requiredTracks === "both";
36
- const needsAudio = requiredTracks === "audio" || requiredTracks === "both";
37
- if (needsVideo && videoTrack && videoTrack.track !== void 0) try {
38
- await engine.fetchInitSegment({
39
- trackId: videoTrack.track,
40
- src: engine.src
41
- }, signal);
42
- } catch (error) {
43
- if (error instanceof DOMException && error.name === "AbortError") throw error;
44
- if (error instanceof Error && (error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch") && error.message.includes("401"))) throw new Error(`Video segments require authentication: ${error.message}`);
45
- }
46
- signal?.throwIfAborted();
47
- if (needsAudio && audioTrack && audioTrack.track !== void 0) try {
48
- await engine.fetchInitSegment({
49
- trackId: audioTrack.track,
50
- src: engine.src
51
- }, signal);
52
- } catch (error) {
53
- if (error instanceof DOMException && error.name === "AbortError") throw error;
54
- if (error instanceof Error && (error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch") && error.message.includes("401"))) throw new Error(`Audio segments require authentication: ${error.message}`);
55
- }
56
- }
57
- return engine;
58
- }
59
- getAudioTrackIndex() {
60
- return Object.values(this.data).find((track) => track.type === "audio");
61
- }
62
- getVideoTrackIndex() {
63
- return Object.values(this.data).find((track) => track.type === "video" && track.track !== void 0 && track.track > 0);
64
- }
65
- getScrubTrackIndex() {
66
- return this.data[-1];
67
- }
68
- #cachedVideoRendition = null;
69
- #cachedAudioRendition = null;
70
- getVideoRenditionInternal() {
71
- if (this.#cachedVideoRendition !== null) return this.#cachedVideoRendition;
72
- const videoTrack = this.getVideoTrackIndex();
73
- if (!videoTrack || videoTrack.track === void 0) {
74
- this.#cachedVideoRendition = void 0;
75
- return;
76
- }
77
- this.#cachedVideoRendition = {
78
- id: "high",
79
- trackId: videoTrack.track,
80
- src: this.src,
81
- startTimeOffsetMs: videoTrack.startTimeOffsetMs
82
- };
83
- return this.#cachedVideoRendition;
84
- }
85
- getAudioRenditionInternal() {
86
- if (this.#cachedAudioRendition !== null) return this.#cachedAudioRendition;
87
- const audioTrack = this.getAudioTrackIndex();
88
- if (!audioTrack || audioTrack.track === void 0) {
89
- this.#cachedAudioRendition = void 0;
90
- return;
91
- }
92
- this.#cachedAudioRendition = {
93
- id: "audio",
94
- trackId: audioTrack.track,
95
- src: this.src
96
- };
97
- return this.#cachedAudioRendition;
98
- }
99
- get videoRendition() {
100
- return this.getVideoRenditionInternal();
101
- }
102
- get audioRendition() {
103
- return this.getAudioRenditionInternal();
104
- }
105
- /**
106
- * Get the source URL for JIT format (needs to be absolute URL)
107
- */
108
- getSourceUrlForJit() {
109
- if (this.src.startsWith("http://") || this.src.startsWith("https://")) return this.src;
110
- let baseUrl = this.urlGenerator.getBaseUrl();
111
- if (!baseUrl) baseUrl = typeof window !== "undefined" ? window.location.origin : "";
112
- const normalizedSrc = this.src.startsWith("/") ? this.src : `/${this.src}`;
113
- return `${baseUrl}${normalizedSrc}`;
114
- }
115
- /**
116
- * Get the base URL for constructing JIT endpoints
117
- */
118
- getBaseUrlForJit() {
119
- let baseUrl = this.urlGenerator.getBaseUrl();
120
- if (!baseUrl) baseUrl = typeof window !== "undefined" ? window.location.origin : "";
121
- return baseUrl;
122
- }
123
- /**
124
- * Map trackId to JIT rendition ID for URL generation
125
- * - trackId 1 (video) -> "high" (default video rendition)
126
- * - trackId 2 (audio) -> "audio"
127
- * - trackId -1 (scrub) -> "scrub"
128
- */
129
- getRenditionId(trackId) {
130
- if (trackId === -1) return "scrub";
131
- if (trackId === 2) return "audio";
132
- return "high";
133
- }
134
- /**
135
- * Override isSegmentCached to use URL-based cache checking (like JitMediaEngine)
136
- */
137
- isSegmentCached(segmentId, rendition) {
138
- if (!rendition.id) return false;
139
- const jitSegmentId = segmentId + 1;
140
- const segmentUrl = this.urlGenerator.generateSegmentUrl(jitSegmentId, rendition.id, this);
141
- return mediaCache.has(segmentUrl);
142
- }
143
- async fetchInitSegment(rendition, signal) {
144
- return withSpan("assetEngine.fetchInitSegment", {
145
- trackId: rendition.trackId || -1,
146
- src: rendition.src
147
- }, void 0, async () => {
148
- if (!rendition.trackId) throw new Error("[fetchInitSegment] Track ID is required for asset metadata");
149
- const renditionId = rendition.id || this.getRenditionId(rendition.trackId);
150
- const url = this.urlGenerator.generateSegmentUrl("init", renditionId, this);
151
- return this.fetchMedia(url, signal);
152
- });
153
- }
154
- async fetchMediaSegment(segmentId, rendition, signal) {
155
- return withSpan("assetEngine.fetchMediaSegment", {
156
- segmentId,
157
- trackId: rendition.trackId || -1,
158
- src: rendition.src
159
- }, void 0, async () => {
160
- if (!rendition.trackId) throw new Error("[fetchMediaSegment] Track ID is required for asset metadata");
161
- if (segmentId === void 0) throw new Error("Segment ID is not available");
162
- const renditionId = rendition.id || this.getRenditionId(rendition.trackId);
163
- const jitSegmentId = segmentId + 1;
164
- const url = this.urlGenerator.generateSegmentUrl(jitSegmentId, renditionId, this);
165
- return this.fetchMedia(url, signal);
166
- });
167
- }
168
- /**
169
- * Calculate audio segments for variable-duration segments using track fragment index
170
- */
171
- calculateAudioSegmentRange(fromMs, toMs, rendition, _durationMs) {
172
- if (fromMs >= toMs || !rendition.trackId) {
173
- console.warn(`calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(rendition)}`);
174
- return [];
175
- }
176
- const track = this.data[rendition.trackId];
177
- if (!track) {
178
- console.warn(`calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(rendition)}`);
179
- return [];
180
- }
181
- const { timescale, segments } = track;
182
- const segmentRanges = [];
183
- for (let i = 0; i < segments.length; i++) {
184
- const segment = segments[i];
185
- const segmentStartTime = segment.cts;
186
- const segmentEndTime = segment.cts + segment.duration;
187
- const segmentStartMs = segmentStartTime / timescale * 1e3;
188
- const segmentEndMs = segmentEndTime / timescale * 1e3;
189
- if (segmentStartMs < toMs && segmentEndMs > fromMs) segmentRanges.push({
190
- segmentId: i,
191
- startMs: segmentStartMs,
192
- endMs: segmentEndMs
193
- });
194
- }
195
- if (segmentRanges.length === 0) console.warn(`calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify({
196
- rendition,
197
- track
198
- })}`);
199
- return segmentRanges;
200
- }
201
- computeSegmentId(seekTimeMs, rendition) {
202
- if (!rendition.trackId) {
203
- console.warn(`computeSegmentId: trackId not found for rendition ${JSON.stringify(rendition)}`);
204
- throw new Error("[computeSegmentId] Track ID is required for asset metadata");
205
- }
206
- const track = this.data[rendition.trackId];
207
- if (!track) throw new Error("Track not found");
208
- const { timescale, segments } = track;
209
- const scaledSeekTime = convertToScaledTime(roundToMilliseconds(seekTimeMs + ("startTimeOffsetMs" in rendition && rendition.startTimeOffsetMs || 0)), timescale);
210
- for (let i = segments.length - 1; i >= 0; i--) {
211
- const segment = segments[i];
212
- const segmentEndTime = segment.cts + segment.duration;
213
- if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) return i;
214
- }
215
- let nearestSegmentIndex = 0;
216
- let nearestDistance = Number.MAX_SAFE_INTEGER;
217
- for (let i = 0; i < segments.length; i++) {
218
- const segment = segments[i];
219
- const segmentStartTime = segment.cts;
220
- const segmentEndTime = segment.cts + segment.duration;
221
- let distance;
222
- if (scaledSeekTime < segmentStartTime) distance = segmentStartTime - scaledSeekTime;
223
- else if (scaledSeekTime >= segmentEndTime) distance = scaledSeekTime - segmentEndTime;
224
- else return i;
225
- if (distance < nearestDistance) {
226
- nearestDistance = distance;
227
- nearestSegmentIndex = i;
228
- }
229
- }
230
- return nearestSegmentIndex;
231
- }
232
- getScrubVideoRendition() {
233
- const scrubTrack = this.getScrubTrackIndex();
234
- if (!scrubTrack || scrubTrack.track === void 0) return;
235
- const scrubSegmentDurationMs = 3e4;
236
- const segmentDurationsMs = scrubTrack.segments.length > 0 ? scrubTrack.segments.map((segment) => {
237
- return segment.duration / scrubTrack.timescale * 1e3;
238
- }) : void 0;
239
- return {
240
- id: "scrub",
241
- trackId: scrubTrack.track,
242
- src: this.src,
243
- segmentDurationMs: scrubSegmentDurationMs,
244
- segmentDurationsMs,
245
- startTimeOffsetMs: scrubTrack.startTimeOffsetMs
246
- };
247
- }
248
- /**
249
- * Get preferred buffer configuration for this media engine
250
- * AssetMediaEngine uses lower buffering since segments are already optimized
251
- */
252
- getBufferConfig() {
253
- return {
254
- videoBufferDurationMs: 2e3,
255
- audioBufferDurationMs: 2e3,
256
- maxVideoBufferFetches: 1,
257
- maxAudioBufferFetches: 1,
258
- bufferThresholdMs: 3e4
259
- };
260
- }
261
- /**
262
- * Extract thumbnail canvases using main video rendition
263
- * Note: We prefer main video over scrub track because scrub track in AssetMediaEngine
264
- * may have incomplete segment data that doesn't cover the full video duration.
265
- */
266
- async extractThumbnails(timestamps, signal) {
267
- const rendition = this.getVideoRenditionInternal();
268
- if (!rendition) {
269
- console.warn("AssetMediaEngine: No video rendition available for thumbnails");
270
- return timestamps.map(() => null);
271
- }
272
- return this.thumbnailExtractor.extractThumbnails(timestamps, rendition, this.durationMs, signal);
273
- }
274
- convertToSegmentRelativeTimestamps(globalTimestamps, _segmentId, rendition) {
275
- const startTimeOffsetMs = rendition.startTimeOffsetMs || 0;
276
- return globalTimestamps.map((globalMs) => {
277
- return (globalMs + startTimeOffsetMs) / 1e3;
278
- });
279
- }
280
- };
281
-
282
- //#endregion
283
- export { AssetMediaEngine };
284
- //# sourceMappingURL=AssetMediaEngine.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AssetMediaEngine.js","names":["#cachedVideoRendition","#cachedAudioRendition","segmentRanges: SegmentTimeRange[]","distance: number","segmentDurationsMs: number[] | undefined"],"sources":["../../../src/elements/EFMedia/AssetMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\n\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { BaseMediaEngine, mediaCache } from \"./BaseMediaEngine\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {\n public src: string;\n protected data: Record<number, TrackFragmentIndex> = {};\n durationMs = 0;\n private thumbnailExtractor: ThumbnailExtractor;\n protected urlGenerator: UrlGenerator;\n\n // MediaEngine interface properties\n templates!: { initSegment: string; mediaSegment: string };\n\n constructor(host: EFMedia, src: string, urlGenerator: UrlGenerator) {\n super(host);\n this.src = src;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\n this.urlGenerator = urlGenerator;\n }\n\n static async fetch(\n host: EFMedia,\n urlGenerator: UrlGenerator,\n src: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const engine = new AssetMediaEngine(host, src, urlGenerator);\n\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = src.startsWith(\"/\") ? src.slice(1) : src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n\n // Use production API format: /api/v1/files/local/index?src={src}\n // This route is handled by the vite plugin for local development\n const apiBaseUrl = urlGenerator.getBaseUrl();\n const url = apiBaseUrl\n ? `${apiBaseUrl}/api/v1/files/local/index?src=${encodeURIComponent(normalizedSrc)}`\n : `/api/v1/files/local/index?src=${encodeURIComponent(normalizedSrc)}`;\n const data = await engine.fetchManifest(url, signal);\n engine.data = data as Record<number, TrackFragmentIndex>;\n\n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n\n // Calculate duration from the data\n const longestFragment = Object.values(engine.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n engine.durationMs = longestFragment * 1000;\n\n if (src.startsWith(\"/\")) {\n engine.src = src.slice(1);\n }\n\n // Initialize MediaEngine interface properties\n const sourceUrl = engine.getSourceUrlForJit();\n const jitBaseUrl = engine.getBaseUrlForJit();\n engine.templates = {\n initSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,\n mediaSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`,\n };\n\n // Validate that segments are accessible by trying to fetch the first init segment\n // This prevents creating a media engine that will fail on all subsequent segment fetches\n // If segments require authentication that's not available, fail early\n // Only validate tracks that are actually required by the consumer (e.g., EFAudio only needs audio)\n // Skip validation if no signal provided (backwards compatibility) - validation is optional\n if (signal) {\n const videoTrack = engine.getVideoTrackIndex();\n const audioTrack = engine.getAudioTrackIndex();\n const needsVideo =\n requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio =\n requiredTracks === \"audio\" || requiredTracks === \"both\";\n\n // Validate video track if required and available\n if (needsVideo && videoTrack && videoTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: videoTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") &&\n error.message.includes(\"401\")))\n ) {\n throw new Error(\n `Video segments require authentication: ${error.message}`,\n );\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n\n // Check for abort between validations\n signal?.throwIfAborted();\n\n // Validate audio track if required and available\n if (needsAudio && audioTrack && audioTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: audioTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") &&\n error.message.includes(\"401\")))\n ) {\n throw new Error(\n `Audio segments require authentication: ${error.message}`,\n );\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n }\n\n return engine;\n }\n\n getAudioTrackIndex() {\n return Object.values(this.data).find((track) => track.type === \"audio\");\n }\n\n getVideoTrackIndex() {\n return Object.values(this.data).find(\n (track) =>\n track.type === \"video\" && track.track !== undefined && track.track > 0,\n );\n }\n\n getScrubTrackIndex() {\n // Scrub track uses track ID -1\n return this.data[-1];\n }\n\n // Cache renditions to avoid recomputing on every access\n #cachedVideoRendition: VideoRendition | undefined | null = null;\n #cachedAudioRendition: AudioRendition | undefined | null = null;\n\n protected getVideoRenditionInternal(): VideoRendition | undefined {\n if (this.#cachedVideoRendition !== null) {\n return this.#cachedVideoRendition;\n }\n const videoTrack = this.getVideoTrackIndex();\n\n if (!videoTrack || videoTrack.track === undefined) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n this.#cachedVideoRendition = {\n id: \"high\" as RenditionId, // Use JIT-style rendition ID\n trackId: videoTrack.track,\n src: this.src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n return this.#cachedVideoRendition;\n }\n\n protected getAudioRenditionInternal(): AudioRendition | undefined {\n if (this.#cachedAudioRendition !== null) {\n return this.#cachedAudioRendition;\n }\n const audioTrack = this.getAudioTrackIndex();\n\n if (!audioTrack || audioTrack.track === undefined) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n this.#cachedAudioRendition = {\n id: \"audio\" as RenditionId, // Use JIT-style rendition ID\n trackId: audioTrack.track,\n src: this.src,\n };\n return this.#cachedAudioRendition;\n }\n\n // MediaEngine interface properties\n get videoRendition(): VideoRendition | undefined {\n return this.getVideoRenditionInternal();\n }\n\n get audioRendition(): AudioRendition | undefined {\n return this.getAudioRenditionInternal();\n }\n\n /**\n * Get the source URL for JIT format (needs to be absolute URL)\n */\n private getSourceUrlForJit(): string {\n // If src is already an absolute URL, use it\n if (this.src.startsWith(\"http://\") || this.src.startsWith(\"https://\")) {\n return this.src;\n }\n\n // Otherwise, construct absolute URL from baseUrl or current origin\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n // If src starts with /, keep it as-is (absolute path)\n // Otherwise, prepend with /\n const normalizedSrc = this.src.startsWith(\"/\") ? this.src : `/${this.src}`;\n return `${baseUrl}${normalizedSrc}`;\n }\n\n /**\n * Get the base URL for constructing JIT endpoints\n */\n private getBaseUrlForJit(): string {\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n return baseUrl;\n }\n\n /**\n * Map trackId to JIT rendition ID for URL generation\n * - trackId 1 (video) -> \"high\" (default video rendition)\n * - trackId 2 (audio) -> \"audio\"\n * - trackId -1 (scrub) -> \"scrub\"\n */\n private getRenditionId(trackId: number): RenditionId {\n if (trackId === -1) return \"scrub\";\n if (trackId === 2) return \"audio\";\n return \"high\"; // Default video rendition (trackId 1)\n }\n\n /**\n * Override isSegmentCached to use URL-based cache checking (like JitMediaEngine)\n */\n override isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n // Use URL-based cache checking (same as JitMediaEngine)\n if (!rendition.id) {\n return false;\n }\n\n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n const jitSegmentId = segmentId + 1;\n const segmentUrl = this.urlGenerator.generateSegmentUrl(\n jitSegmentId,\n rendition.id,\n this,\n );\n return mediaCache.has(segmentUrl);\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchInitSegment\",\n {\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for asset metadata\",\n );\n }\n\n // Use rendition ID if provided, otherwise map from trackId\n const renditionId =\n rendition.id || this.getRenditionId(rendition.trackId);\n const url = this.urlGenerator.generateSegmentUrl(\n \"init\",\n renditionId,\n this,\n );\n\n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchMediaSegment\",\n {\n segmentId,\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for asset metadata\",\n );\n }\n if (segmentId === undefined) {\n throw new Error(\"Segment ID is not available\");\n }\n\n // Use rendition ID if provided, otherwise map from trackId\n const renditionId =\n rendition.id || this.getRenditionId(rendition.trackId);\n\n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n // So we need to add 1 to segmentId for the URL\n const jitSegmentId = segmentId + 1;\n const url = this.urlGenerator.generateSegmentUrl(\n jitSegmentId,\n renditionId,\n this,\n );\n\n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n /**\n * Calculate audio segments for variable-duration segments using track fragment index\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n _durationMs: number,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs || !rendition.trackId) {\n console.warn(\n `calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const track = this.data[rendition.trackId];\n if (!track) {\n console.warn(\n `calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const { timescale, segments } = track;\n const segmentRanges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Convert to milliseconds\n const segmentStartMs = (segmentStartTime / timescale) * 1000;\n const segmentEndMs = (segmentEndTime / timescale) * 1000;\n\n // Check if segment overlaps with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segmentRanges.push({\n segmentId: i, // AssetMediaEngine uses 0-based segment IDs\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n if (segmentRanges.length === 0) {\n console.warn(\n `calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n {\n rendition,\n track,\n },\n )}`,\n );\n }\n\n return segmentRanges;\n }\n\n computeSegmentId(seekTimeMs: number, rendition: MediaRendition) {\n if (!rendition.trackId) {\n console.warn(\n `computeSegmentId: trackId not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n throw new Error(\n \"[computeSegmentId] Track ID is required for asset metadata\",\n );\n }\n const track = this.data[rendition.trackId];\n if (!track) {\n throw new Error(\"Track not found\");\n }\n const { timescale, segments } = track;\n\n // Apply startTimeOffsetMs to map user timeline to media timeline for segment selection\n const startTimeOffsetMs =\n (\"startTimeOffsetMs\" in rendition && rendition.startTimeOffsetMs) || 0;\n\n const offsetSeekTimeMs = roundToMilliseconds(\n seekTimeMs + startTimeOffsetMs,\n );\n // Convert to timescale units using consistent precision\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n // Find the segment that contains the actual seek time\n for (let i = segments.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Check if the seek time falls within this segment\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Handle gaps: if no exact segment contains the time, find the nearest one\n // This handles cases where seek time falls between segments (like 8041.667ms)\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segmentStartTime) {\n // Time is before this segment\n distance = segmentStartTime - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n // Time is after this segment\n distance = scaledSeekTime - segmentEndTime;\n } else {\n // Time is within this segment (should have been caught above, but just in case)\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n const scrubTrack = this.getScrubTrackIndex();\n\n if (!scrubTrack || scrubTrack.track === undefined) {\n return undefined;\n }\n\n // Calculate segment duration from scrub track segments\n // Scrub tracks use 30-second segments\n const scrubSegmentDurationMs = 30000;\n\n // Calculate segment durations array if segments exist\n const segmentDurationsMs: number[] | undefined =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map((segment) => {\n // Convert segment duration from timescale units to milliseconds\n return (segment.duration / scrubTrack.timescale) * 1000;\n })\n : undefined;\n\n return {\n id: \"scrub\" as RenditionId, // Use JIT-style rendition ID\n trackId: scrubTrack.track,\n src: this.src,\n segmentDurationMs: scrubSegmentDurationMs,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n /**\n * Get preferred buffer configuration for this media engine\n * AssetMediaEngine uses lower buffering since segments are already optimized\n */\n getBufferConfig() {\n return {\n // Buffer just 1 segment ahead (~2 seconds) for assets\n videoBufferDurationMs: 2000,\n audioBufferDurationMs: 2000,\n maxVideoBufferFetches: 1,\n maxAudioBufferFetches: 1,\n bufferThresholdMs: 30000, // Timeline-aware buffering threshold\n };\n }\n\n /**\n * Extract thumbnail canvases using main video rendition\n * Note: We prefer main video over scrub track because scrub track in AssetMediaEngine\n * may have incomplete segment data that doesn't cover the full video duration.\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use main video rendition for thumbnails - scrub track may have incomplete segments\n const rendition = this.getVideoRenditionInternal();\n\n if (!rendition) {\n console.warn(\n \"AssetMediaEngine: No video rendition available for thumbnails\",\n );\n return timestamps.map(() => null);\n }\n\n return this.thumbnailExtractor.extractThumbnails(\n timestamps,\n rendition,\n this.durationMs,\n signal,\n );\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n _segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n // For fragmented MP4 (Asset), when we create a mediabunny Input from init+media segment,\n // mediabunny sees the samples with their ABSOLUTE timestamps from the container.\n // This is because the tfdt box contains the baseMediaDecodeTime which is the absolute\n // position of this segment in the container timeline.\n //\n // So we just need to convert user time to container time by adding startTimeOffsetMs,\n // then pass that to mediabunny (in seconds).\n\n const startTimeOffsetMs = rendition.startTimeOffsetMs || 0;\n\n return globalTimestamps.map((globalMs) => {\n // User time -> container time -> seconds for mediabunny\n const containerTimeMs = globalMs + startTimeOffsetMs;\n return containerTimeMs / 1000;\n });\n }\n}\n"],"mappings":";;;;;;AAqBA,IAAa,mBAAb,MAAa,yBAAyB,gBAAuC;CAU3E,YAAY,MAAe,KAAa,cAA4B;AAClE,QAAM,KAAK;cATwC,EAAE;oBAC1C;AASX,OAAK,MAAM;AACX,OAAK,qBAAqB,IAAI,mBAAmB,KAAK;AACtD,OAAK,eAAe;;CAGtB,aAAa,MACX,MACA,cACA,KACA,iBAA6C,QAC7C,QACA;EACA,MAAM,SAAS,IAAI,iBAAiB,MAAM,KAAK,aAAa;EAG5D,IAAI,gBAAgB,IAAI,WAAW,IAAI,GAAG,IAAI,MAAM,EAAE,GAAG;AACzD,kBAAgB,cAAc,QAAQ,QAAQ,GAAG;EAIjD,MAAM,aAAa,aAAa,YAAY;EAC5C,MAAM,MAAM,aACR,GAAG,WAAW,gCAAgC,mBAAmB,cAAc,KAC/E,iCAAiC,mBAAmB,cAAc;AAEtE,SAAO,OADM,MAAM,OAAO,cAAc,KAAK,OAAO;AAIpD,UAAQ,gBAAgB;AAOxB,SAAO,aAJiB,OAAO,OAAO,OAAO,KAAK,CAAC,QAChD,KAAK,aAAa,KAAK,IAAI,KAAK,SAAS,WAAW,SAAS,UAAU,EACxE,EACD,GACqC;AAEtC,MAAI,IAAI,WAAW,IAAI,CACrB,QAAO,MAAM,IAAI,MAAM,EAAE;EAI3B,MAAM,YAAY,OAAO,oBAAoB;EAC7C,MAAM,aAAa,OAAO,kBAAkB;AAC5C,SAAO,YAAY;GACjB,aAAa,GAAG,WAAW,6CAA6C,mBAAmB,UAAU;GACrG,cAAc,GAAG,WAAW,oDAAoD,mBAAmB,UAAU;GAC9G;AAOD,MAAI,QAAQ;GACV,MAAM,aAAa,OAAO,oBAAoB;GAC9C,MAAM,aAAa,OAAO,oBAAoB;GAC9C,MAAM,aACJ,mBAAmB,WAAW,mBAAmB;GACnD,MAAM,aACJ,mBAAmB,WAAW,mBAAmB;AAGnD,OAAI,cAAc,cAAc,WAAW,UAAU,OACnD,KAAI;AACF,UAAM,OAAO,iBACX;KAAE,SAAS,WAAW;KAAO,KAAK,OAAO;KAAK,EAC9C,OACD;YACM,OAAO;AAEd,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAIR,QACE,iBAAiB,UAChB,MAAM,QAAQ,SAAS,MAAM,IAC5B,MAAM,QAAQ,SAAS,eAAe,IACrC,MAAM,QAAQ,SAAS,kBAAkB,IACxC,MAAM,QAAQ,SAAS,MAAM,EAEjC,OAAM,IAAI,MACR,0CAA0C,MAAM,UACjD;;AAQP,WAAQ,gBAAgB;AAGxB,OAAI,cAAc,cAAc,WAAW,UAAU,OACnD,KAAI;AACF,UAAM,OAAO,iBACX;KAAE,SAAS,WAAW;KAAO,KAAK,OAAO;KAAK,EAC9C,OACD;YACM,OAAO;AAEd,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAIR,QACE,iBAAiB,UAChB,MAAM,QAAQ,SAAS,MAAM,IAC5B,MAAM,QAAQ,SAAS,eAAe,IACrC,MAAM,QAAQ,SAAS,kBAAkB,IACxC,MAAM,QAAQ,SAAS,MAAM,EAEjC,OAAM,IAAI,MACR,0CAA0C,MAAM,UACjD;;;AAQT,SAAO;;CAGT,qBAAqB;AACnB,SAAO,OAAO,OAAO,KAAK,KAAK,CAAC,MAAM,UAAU,MAAM,SAAS,QAAQ;;CAGzE,qBAAqB;AACnB,SAAO,OAAO,OAAO,KAAK,KAAK,CAAC,MAC7B,UACC,MAAM,SAAS,WAAW,MAAM,UAAU,UAAa,MAAM,QAAQ,EACxE;;CAGH,qBAAqB;AAEnB,SAAO,KAAK,KAAK;;CAInB,wBAA2D;CAC3D,wBAA2D;CAE3D,AAAU,4BAAwD;AAChE,MAAI,MAAKA,yBAA0B,KACjC,QAAO,MAAKA;EAEd,MAAM,aAAa,KAAK,oBAAoB;AAE5C,MAAI,CAAC,cAAc,WAAW,UAAU,QAAW;AACjD,SAAKA,uBAAwB;AAC7B;;AAGF,QAAKA,uBAAwB;GAC3B,IAAI;GACJ,SAAS,WAAW;GACpB,KAAK,KAAK;GACV,mBAAmB,WAAW;GAC/B;AACD,SAAO,MAAKA;;CAGd,AAAU,4BAAwD;AAChE,MAAI,MAAKC,yBAA0B,KACjC,QAAO,MAAKA;EAEd,MAAM,aAAa,KAAK,oBAAoB;AAE5C,MAAI,CAAC,cAAc,WAAW,UAAU,QAAW;AACjD,SAAKA,uBAAwB;AAC7B;;AAGF,QAAKA,uBAAwB;GAC3B,IAAI;GACJ,SAAS,WAAW;GACpB,KAAK,KAAK;GACX;AACD,SAAO,MAAKA;;CAId,IAAI,iBAA6C;AAC/C,SAAO,KAAK,2BAA2B;;CAGzC,IAAI,iBAA6C;AAC/C,SAAO,KAAK,2BAA2B;;;;;CAMzC,AAAQ,qBAA6B;AAEnC,MAAI,KAAK,IAAI,WAAW,UAAU,IAAI,KAAK,IAAI,WAAW,WAAW,CACnE,QAAO,KAAK;EAId,IAAI,UAAU,KAAK,aAAa,YAAY;AAE5C,MAAI,CAAC,QACH,WAAU,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;EAIrE,MAAM,gBAAgB,KAAK,IAAI,WAAW,IAAI,GAAG,KAAK,MAAM,IAAI,KAAK;AACrE,SAAO,GAAG,UAAU;;;;;CAMtB,AAAQ,mBAA2B;EACjC,IAAI,UAAU,KAAK,aAAa,YAAY;AAE5C,MAAI,CAAC,QACH,WAAU,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AAErE,SAAO;;;;;;;;CAST,AAAQ,eAAe,SAA8B;AACnD,MAAI,YAAY,GAAI,QAAO;AAC3B,MAAI,YAAY,EAAG,QAAO;AAC1B,SAAO;;;;;CAMT,AAAS,gBACP,WACA,WACS;AAET,MAAI,CAAC,UAAU,GACb,QAAO;EAIT,MAAM,eAAe,YAAY;EACjC,MAAM,aAAa,KAAK,aAAa,mBACnC,cACA,UAAU,IACV,KACD;AACD,SAAO,WAAW,IAAI,WAAW;;CAGnC,MAAM,iBACJ,WACA,QACA;AACA,SAAO,SACL,gCACA;GACE,SAAS,UAAU,WAAW;GAC9B,KAAK,UAAU;GAChB,EACD,QACA,YAAY;AACV,OAAI,CAAC,UAAU,QACb,OAAM,IAAI,MACR,6DACD;GAIH,MAAM,cACJ,UAAU,MAAM,KAAK,eAAe,UAAU,QAAQ;GACxD,MAAM,MAAM,KAAK,aAAa,mBAC5B,QACA,aACA,KACD;AAGD,UAAO,KAAK,WAAW,KAAK,OAAO;IAEtC;;CAGH,MAAM,kBACJ,WACA,WACA,QACA;AACA,SAAO,SACL,iCACA;GACE;GACA,SAAS,UAAU,WAAW;GAC9B,KAAK,UAAU;GAChB,EACD,QACA,YAAY;AACV,OAAI,CAAC,UAAU,QACb,OAAM,IAAI,MACR,8DACD;AAEH,OAAI,cAAc,OAChB,OAAM,IAAI,MAAM,8BAA8B;GAIhD,MAAM,cACJ,UAAU,MAAM,KAAK,eAAe,UAAU,QAAQ;GAIxD,MAAM,eAAe,YAAY;GACjC,MAAM,MAAM,KAAK,aAAa,mBAC5B,cACA,aACA,KACD;AAGD,UAAO,KAAK,WAAW,KAAK,OAAO;IAEtC;;;;;CAMH,2BACE,QACA,MACA,WACA,aACoB;AACpB,MAAI,UAAU,QAAQ,CAAC,UAAU,SAAS;AACxC,WAAQ,KACN,8CAA8C,OAAO,QAAQ,KAAK,aAAa,KAAK,UAClF,UACD,GACF;AACD,UAAO,EAAE;;EAGX,MAAM,QAAQ,KAAK,KAAK,UAAU;AAClC,MAAI,CAAC,OAAO;AACV,WAAQ,KACN,6DAA6D,KAAK,UAChE,UACD,GACF;AACD,UAAO,EAAE;;EAGX,MAAM,EAAE,WAAW,aAAa;EAChC,MAAMC,gBAAoC,EAAE;AAE5C,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GAExC,MAAM,UAAU,SAAS;GACzB,MAAM,mBAAmB,QAAQ;GACjC,MAAM,iBAAiB,QAAQ,MAAM,QAAQ;GAG7C,MAAM,iBAAkB,mBAAmB,YAAa;GACxD,MAAM,eAAgB,iBAAiB,YAAa;AAGpD,OAAI,iBAAiB,QAAQ,eAAe,OAC1C,eAAc,KAAK;IACjB,WAAW;IACX,SAAS;IACT,OAAO;IACR,CAAC;;AAGN,MAAI,cAAc,WAAW,EAC3B,SAAQ,KACN,4DAA4D,OAAO,QAAQ,KAAK,aAAa,KAAK,UAChG;GACE;GACA;GACD,CACF,GACF;AAGH,SAAO;;CAGT,iBAAiB,YAAoB,WAA2B;AAC9D,MAAI,CAAC,UAAU,SAAS;AACtB,WAAQ,KACN,qDAAqD,KAAK,UACxD,UACD,GACF;AACD,SAAM,IAAI,MACR,6DACD;;EAEH,MAAM,QAAQ,KAAK,KAAK,UAAU;AAClC,MAAI,CAAC,MACH,OAAM,IAAI,MAAM,kBAAkB;EAEpC,MAAM,EAAE,WAAW,aAAa;EAUhC,MAAM,iBAAiB,oBAJE,oBACvB,cAHC,uBAAuB,aAAa,UAAU,qBAAsB,GAItE,EAE4D,UAAU;AAGvE,OAAK,IAAI,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;GAE7C,MAAM,UAAU,SAAS;GACzB,MAAM,iBAAiB,QAAQ,MAAM,QAAQ;AAG7C,OAAI,QAAQ,OAAO,kBAAkB,iBAAiB,eACpD,QAAO;;EAMX,IAAI,sBAAsB;EAC1B,IAAI,kBAAkB,OAAO;AAE7B,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GAExC,MAAM,UAAU,SAAS;GACzB,MAAM,mBAAmB,QAAQ;GACjC,MAAM,iBAAiB,QAAQ,MAAM,QAAQ;GAE7C,IAAIC;AACJ,OAAI,iBAAiB,iBAEnB,YAAW,mBAAmB;YACrB,kBAAkB,eAE3B,YAAW,iBAAiB;OAG5B,QAAO;AAGT,OAAI,WAAW,iBAAiB;AAC9B,sBAAkB;AAClB,0BAAsB;;;AAI1B,SAAO;;CAGT,yBAAqD;EACnD,MAAM,aAAa,KAAK,oBAAoB;AAE5C,MAAI,CAAC,cAAc,WAAW,UAAU,OACtC;EAKF,MAAM,yBAAyB;EAG/B,MAAMC,qBACJ,WAAW,SAAS,SAAS,IACzB,WAAW,SAAS,KAAK,YAAY;AAEnC,UAAQ,QAAQ,WAAW,WAAW,YAAa;IACnD,GACF;AAEN,SAAO;GACL,IAAI;GACJ,SAAS,WAAW;GACpB,KAAK,KAAK;GACV,mBAAmB;GACnB;GACA,mBAAmB,WAAW;GAC/B;;;;;;CAOH,kBAAkB;AAChB,SAAO;GAEL,uBAAuB;GACvB,uBAAuB;GACvB,uBAAuB;GACvB,uBAAuB;GACvB,mBAAmB;GACpB;;;;;;;CAQH,MAAM,kBACJ,YACA,QACqC;EAErC,MAAM,YAAY,KAAK,2BAA2B;AAElD,MAAI,CAAC,WAAW;AACd,WAAQ,KACN,gEACD;AACD,UAAO,WAAW,UAAU,KAAK;;AAGnC,SAAO,KAAK,mBAAmB,kBAC7B,YACA,WACA,KAAK,YACL,OACD;;CAGH,mCACE,kBACA,YACA,WACU;EASV,MAAM,oBAAoB,UAAU,qBAAqB;AAEzD,SAAO,iBAAiB,KAAK,aAAa;AAGxC,WADwB,WAAW,qBACV;IACzB"}
@@ -1,200 +0,0 @@
1
- import { withSpan } from "../../otel/tracingHelpers.js";
2
- import { SizeAwareLRUCache } from "../../utils/LRUCache.js";
3
- import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
4
-
5
- //#region src/elements/EFMedia/BaseMediaEngine.ts
6
- const mediaCache = new SizeAwareLRUCache(100 * 1024 * 1024);
7
- const globalRequestDeduplicator = new RequestDeduplicator();
8
- var BaseMediaEngine = class {
9
- constructor(host) {
10
- this.host = host;
11
- }
12
- /**
13
- * Get video rendition if available. Returns undefined for audio-only assets.
14
- * Callers should handle undefined gracefully.
15
- */
16
- getVideoRendition() {
17
- return this.getVideoRenditionInternal();
18
- }
19
- /**
20
- * Get audio rendition if available. Returns undefined for video-only assets.
21
- * Callers should handle undefined appropriately.
22
- */
23
- getAudioRendition() {
24
- return this.getAudioRenditionInternal();
25
- }
26
- /**
27
- * Unified fetch method with caching and global deduplication
28
- * All requests (media, manifest, init segments) go through this method
29
- */
30
- async fetchWithCache(url, options) {
31
- return withSpan("mediaEngine.fetchWithCache", {
32
- url: url.length > 100 ? `${url.substring(0, 100)}...` : url,
33
- responseType: options.responseType,
34
- hasHeaders: !!options.headers
35
- }, void 0, async (span) => {
36
- const t0 = performance.now();
37
- const { responseType, headers, signal } = options;
38
- const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
39
- const t1 = performance.now();
40
- const cached = mediaCache.get(cacheKey);
41
- const t2 = performance.now();
42
- span.setAttribute("cacheLookupMs", Math.round((t2 - t1) * 1e3) / 1e3);
43
- if (cached) {
44
- span.setAttribute("cacheHit", true);
45
- if (signal) {
46
- const t3 = performance.now();
47
- const result$1 = await this.handleAbortForCachedRequest(cached, signal);
48
- const t4 = performance.now();
49
- span.setAttribute("handleAbortMs", Math.round((t4 - t3) * 100) / 100);
50
- span.setAttribute("totalCacheHitMs", Math.round((t4 - t0) * 100) / 100);
51
- return result$1;
52
- }
53
- span.setAttribute("totalCacheHitMs", Math.round((t2 - t0) * 100) / 100);
54
- return cached;
55
- }
56
- span.setAttribute("cacheHit", false);
57
- const promise = globalRequestDeduplicator.executeRequest(cacheKey, async () => {
58
- const fetchStart = performance.now();
59
- try {
60
- const response = await this.host.fetch(url, {
61
- headers,
62
- signal
63
- });
64
- const fetchEnd = performance.now();
65
- span.setAttribute("fetchMs", fetchEnd - fetchStart);
66
- const contentType = response.headers.get("content-type");
67
- if (responseType === "json") {
68
- if (!response.ok || contentType && !contentType.includes("application/json") && !contentType.includes("text/json")) {
69
- const text = await response.clone().text();
70
- if (!response.ok) throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);
71
- throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);
72
- }
73
- try {
74
- return await response.json();
75
- } catch (error) {
76
- throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);
77
- }
78
- }
79
- if (!response.ok) {
80
- const text = await response.clone().text();
81
- throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);
82
- }
83
- const buffer = await response.arrayBuffer();
84
- span.setAttribute("sizeBytes", buffer.byteLength);
85
- return buffer;
86
- } catch (error) {
87
- if (error instanceof DOMException && error.name === "AbortError") mediaCache.delete(cacheKey);
88
- throw error;
89
- }
90
- });
91
- mediaCache.set(cacheKey, promise);
92
- promise.catch((error) => {
93
- if (error instanceof DOMException && error.name === "AbortError") mediaCache.delete(cacheKey);
94
- });
95
- if (signal) {
96
- const result$1 = await this.handleAbortForCachedRequest(promise, signal);
97
- const tEnd$1 = performance.now();
98
- span.setAttribute("totalFetchMs", Math.round((tEnd$1 - t0) * 100) / 100);
99
- return result$1;
100
- }
101
- const result = await promise;
102
- const tEnd = performance.now();
103
- span.setAttribute("totalFetchMs", Math.round((tEnd - t0) * 100) / 100);
104
- return result;
105
- });
106
- }
107
- /**
108
- * Handles abort logic for a cached request without affecting the underlying fetch
109
- * This allows multiple instances to share the same cached request while each
110
- * manages their own abort behavior
111
- */
112
- handleAbortForCachedRequest(promise, signal) {
113
- if (signal.aborted) throw new DOMException("Aborted", "AbortError");
114
- const abortPromise = new Promise((_, reject) => {
115
- signal.addEventListener("abort", () => {
116
- reject(new DOMException("Aborted", "AbortError"));
117
- });
118
- });
119
- abortPromise.catch(() => {});
120
- const racePromise = Promise.race([promise, abortPromise]);
121
- racePromise.catch(() => {});
122
- return racePromise;
123
- }
124
- async fetchMedia(url, signal) {
125
- if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
126
- return this.fetchWithCache(url, {
127
- responseType: "arrayBuffer",
128
- signal
129
- });
130
- }
131
- async fetchManifest(url, signal) {
132
- if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
133
- return this.fetchWithCache(url, {
134
- responseType: "json",
135
- signal
136
- });
137
- }
138
- async fetchMediaWithHeaders(url, headers, signal) {
139
- if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
140
- return this.fetchWithCache(url, {
141
- responseType: "arrayBuffer",
142
- headers,
143
- signal
144
- });
145
- }
146
- /**
147
- * Calculate audio segments needed for a time range
148
- * Each media engine implements this based on their segment structure
149
- */
150
- calculateAudioSegmentRange(fromMs, toMs, rendition, durationMs) {
151
- if (fromMs >= toMs) return [];
152
- const segments = [];
153
- if (rendition.segmentDurationsMs && rendition.segmentDurationsMs.length > 0) {
154
- let cumulativeTime = 0;
155
- for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {
156
- const segmentDuration = rendition.segmentDurationsMs[i];
157
- if (segmentDuration === void 0) continue;
158
- const segmentStartMs = cumulativeTime;
159
- const segmentEndMs = Math.min(cumulativeTime + segmentDuration, durationMs);
160
- if (segmentStartMs >= durationMs) break;
161
- if (segmentStartMs < toMs && segmentEndMs > fromMs) segments.push({
162
- segmentId: i + 1,
163
- startMs: segmentStartMs,
164
- endMs: segmentEndMs
165
- });
166
- cumulativeTime += segmentDuration;
167
- if (cumulativeTime >= durationMs) break;
168
- }
169
- return segments;
170
- }
171
- const segmentDurationMs = rendition.segmentDurationMs || 1e3;
172
- const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);
173
- const endSegmentIndex = Math.floor(toMs / segmentDurationMs);
174
- for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
175
- const segmentId = i + 1;
176
- const segmentStartMs = i * segmentDurationMs;
177
- const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);
178
- if (segmentStartMs >= durationMs) break;
179
- if (segmentStartMs < toMs && segmentEndMs > fromMs) segments.push({
180
- segmentId,
181
- startMs: segmentStartMs,
182
- endMs: segmentEndMs
183
- });
184
- }
185
- return segments;
186
- }
187
- /**
188
- * Extract thumbnail canvases at multiple timestamps efficiently
189
- * Default implementation provides helpful error information
190
- */
191
- async extractThumbnails(timestamps, _signal) {
192
- const engineName = this.constructor.name;
193
- console.warn(`${engineName}: extractThumbnails not properly implemented. This MediaEngine type does not support thumbnail generation. Supported engines: JitMediaEngine. Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? "" : "s"}.`);
194
- return timestamps.map(() => null);
195
- }
196
- };
197
-
198
- //#endregion
199
- export { BaseMediaEngine, mediaCache };
200
- //# sourceMappingURL=BaseMediaEngine.js.map