@editframe/elements 0.38.1 → 0.40.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 (78) hide show
  1. package/dist/EF_FRAMEGEN.js +1 -0
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/elements/EFCaptions.d.ts +2 -2
  4. package/dist/elements/EFCaptions.js +1 -1
  5. package/dist/elements/EFCaptions.js.map +1 -1
  6. package/dist/elements/EFImage.js +3 -4
  7. package/dist/elements/EFImage.js.map +1 -1
  8. package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
  9. package/dist/elements/EFMedia/CachedFetcher.js +99 -0
  10. package/dist/elements/EFMedia/CachedFetcher.js.map +1 -0
  11. package/dist/elements/EFMedia/MediaEngine.d.ts +19 -0
  12. package/dist/elements/EFMedia/MediaEngine.js +129 -0
  13. package/dist/elements/EFMedia/MediaEngine.js.map +1 -0
  14. package/dist/elements/EFMedia/SegmentIndex.d.ts +32 -0
  15. package/dist/elements/EFMedia/SegmentIndex.js +185 -0
  16. package/dist/elements/EFMedia/SegmentIndex.js.map +1 -0
  17. package/dist/elements/EFMedia/SegmentTransport.d.ts +12 -0
  18. package/dist/elements/EFMedia/SegmentTransport.js +69 -0
  19. package/dist/elements/EFMedia/SegmentTransport.js.map +1 -0
  20. package/dist/elements/EFMedia/TimingModel.d.ts +10 -0
  21. package/dist/elements/EFMedia/TimingModel.js +28 -0
  22. package/dist/elements/EFMedia/TimingModel.js.map +1 -0
  23. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +7 -6
  24. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  25. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +13 -34
  26. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  27. package/dist/elements/EFMedia.d.ts +4 -3
  28. package/dist/elements/EFMedia.js +14 -31
  29. package/dist/elements/EFMedia.js.map +1 -1
  30. package/dist/elements/EFSourceMixin.js +1 -1
  31. package/dist/elements/EFSourceMixin.js.map +1 -1
  32. package/dist/elements/EFTemporal.js +2 -1
  33. package/dist/elements/EFTemporal.js.map +1 -1
  34. package/dist/elements/EFTimegroup.js +2 -1
  35. package/dist/elements/EFTimegroup.js.map +1 -1
  36. package/dist/elements/EFVideo.js +204 -187
  37. package/dist/elements/EFVideo.js.map +1 -1
  38. package/dist/gui/EFConfiguration.d.ts +0 -7
  39. package/dist/gui/EFConfiguration.js +0 -5
  40. package/dist/gui/EFConfiguration.js.map +1 -1
  41. package/dist/gui/EFWorkbench.d.ts +2 -0
  42. package/dist/gui/EFWorkbench.js +68 -1
  43. package/dist/gui/EFWorkbench.js.map +1 -1
  44. package/dist/gui/PlaybackController.d.ts +2 -0
  45. package/dist/gui/PlaybackController.js +11 -1
  46. package/dist/gui/PlaybackController.js.map +1 -1
  47. package/dist/gui/ef-theme.css +11 -0
  48. package/dist/gui/timeline/tracks/AudioTrack.js +28 -30
  49. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  50. package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +1 -0
  51. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +41 -8
  52. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
  53. package/dist/gui/timeline/tracks/VideoTrack.js +2 -2
  54. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  55. package/dist/gui/timeline/tracks/waveformUtils.js +19 -19
  56. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  57. package/dist/preview/QualityUpgradeScheduler.d.ts +8 -0
  58. package/dist/preview/QualityUpgradeScheduler.js +13 -1
  59. package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
  60. package/dist/preview/renderTimegroupToVideo.js +3 -3
  61. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  62. package/dist/preview/renderVideoToVideo.js +5 -6
  63. package/dist/preview/renderVideoToVideo.js.map +1 -1
  64. package/dist/transcoding/types/index.d.ts +6 -94
  65. package/dist/transcoding/utils/UrlGenerator.d.ts +3 -12
  66. package/dist/transcoding/utils/UrlGenerator.js +3 -29
  67. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  68. package/package.json +2 -2
  69. package/test/setup.ts +1 -1
  70. package/test/useAssetMSW.ts +0 -100
  71. package/dist/elements/EFMedia/AssetMediaEngine.js +0 -284
  72. package/dist/elements/EFMedia/AssetMediaEngine.js.map +0 -1
  73. package/dist/elements/EFMedia/BaseMediaEngine.js +0 -200
  74. package/dist/elements/EFMedia/BaseMediaEngine.js.map +0 -1
  75. package/dist/elements/EFMedia/FileMediaEngine.js +0 -122
  76. package/dist/elements/EFMedia/FileMediaEngine.js.map +0 -1
  77. package/dist/elements/EFMedia/JitMediaEngine.js +0 -157
  78. package/dist/elements/EFMedia/JitMediaEngine.js.map +0 -1
@@ -0,0 +1,129 @@
1
+ import { CachedFetcher } from "./CachedFetcher.js";
2
+ import { createFragmentIndex, createManifestIndex } from "./SegmentIndex.js";
3
+ import { createByteRangeTransport, createUrlTransport } from "./SegmentTransport.js";
4
+ import { createByteRangeTiming, createJitTiming } from "./TimingModel.js";
5
+
6
+ //#region src/elements/EFMedia/MediaEngine.ts
7
+ function createMediaEngine(index, transport, timing, src) {
8
+ return {
9
+ durationMs: index.durationMs,
10
+ src,
11
+ index,
12
+ transport,
13
+ timing,
14
+ tracks: index.tracks,
15
+ async extractThumbnails(timestamps, signal) {
16
+ const track = index.tracks.video ?? index.tracks.scrub;
17
+ if (!track) return timestamps.map(() => null);
18
+ const { ThumbnailExtractor } = await import("./shared/ThumbnailExtractor.js");
19
+ return new ThumbnailExtractor(this).extractThumbnails(timestamps, track, index.durationMs, signal);
20
+ }
21
+ };
22
+ }
23
+ async function fetchFileIndex(fetchFn, fileId, apiHost, signal) {
24
+ const response = await fetchFn(`${apiHost}/api/v1/files/${fileId}/index`, { signal });
25
+ signal?.throwIfAborted();
26
+ const contentType = response.headers.get("content-type");
27
+ if (!response.ok || contentType && !contentType.includes("application/json")) {
28
+ const text = await response.clone().text();
29
+ if (!response.ok) throw new Error(`Failed to fetch asset index: ${response.status} ${text}`);
30
+ throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);
31
+ }
32
+ try {
33
+ const data = await response.json();
34
+ signal?.throwIfAborted();
35
+ return data;
36
+ } catch (error) {
37
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
38
+ throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);
39
+ }
40
+ }
41
+ async function validateTrackAccess(transport, tracks, requiredTracks, signal) {
42
+ if (!signal) return;
43
+ const toValidate = [];
44
+ const needsVideo = requiredTracks === "video" || requiredTracks === "both";
45
+ const needsAudio = requiredTracks === "audio" || requiredTracks === "both";
46
+ if (needsVideo && tracks.video) toValidate.push(tracks.video);
47
+ if (needsAudio && tracks.audio) toValidate.push(tracks.audio);
48
+ for (const track of toValidate) {
49
+ signal.throwIfAborted();
50
+ try {
51
+ await transport.fetchInitSegment(track, 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(`${track.role} segments require authentication: ${error.message}`);
55
+ }
56
+ }
57
+ }
58
+ function buildEngineComponents(indexData, fetcher) {
59
+ switch (indexData.type) {
60
+ case "fragment": return {
61
+ index: createFragmentIndex(indexData.data, indexData.src),
62
+ transport: createByteRangeTransport(indexData.data, indexData.fileId, indexData.apiHost, fetcher),
63
+ timing: createByteRangeTiming(indexData.data),
64
+ src: indexData.src
65
+ };
66
+ case "manifest": return {
67
+ index: createManifestIndex(indexData.data),
68
+ transport: createUrlTransport({
69
+ fetcher,
70
+ src: indexData.data.sourceUrl,
71
+ templates: indexData.data.endpoints,
72
+ audioTrackId: void 0,
73
+ videoTrackId: void 0
74
+ }),
75
+ timing: createJitTiming(),
76
+ src: indexData.data.sourceUrl
77
+ };
78
+ }
79
+ }
80
+ async function createMediaEngineFromSource(opts) {
81
+ const { src, fileId, apiHost, requiredTracks, fetchFn, urlGenerator, signal } = opts;
82
+ const fetcher = new CachedFetcher(fetchFn);
83
+ let indexData;
84
+ if (fileId !== null && fileId !== void 0 && fileId.trim() !== "") {
85
+ if (!apiHost) throw new Error("API host is required for file-id mode");
86
+ const data = await fetchFileIndex(fetchFn, fileId, apiHost, signal);
87
+ signal?.throwIfAborted();
88
+ indexData = {
89
+ type: "fragment",
90
+ data,
91
+ src: fileId,
92
+ apiHost,
93
+ fileId
94
+ };
95
+ } else if (!src || typeof src !== "string" || src.trim() === "") return;
96
+ else {
97
+ const manifestSrc = resolveManifestSrc(src, apiHost);
98
+ const url = urlGenerator.generateManifestUrl(manifestSrc);
99
+ const manifest = await fetcher.fetchJson(url, signal);
100
+ signal?.throwIfAborted();
101
+ indexData = {
102
+ type: "manifest",
103
+ data: manifest,
104
+ src: manifest.sourceUrl
105
+ };
106
+ }
107
+ const { index, transport, timing, src: engineSrc } = buildEngineComponents(indexData, fetcher);
108
+ await validateTrackAccess(transport, index.tracks, requiredTracks, signal);
109
+ return createMediaEngine(index, transport, timing, engineSrc);
110
+ }
111
+ /**
112
+ * Resolve a src value to the URL the server should transcode.
113
+ * - Remote URLs (http/https) pass through as-is
114
+ * - Local paths are made absolute using apiHost when available
115
+ */
116
+ function resolveManifestSrc(src, apiHost) {
117
+ const lower = src.toLowerCase();
118
+ if (lower.startsWith("http://") || lower.startsWith("https://")) return src;
119
+ if (apiHost) {
120
+ const base = apiHost.replace(/\/$/, "");
121
+ const normalizedPath = src.replace(/^\.\//, "/src/");
122
+ return `${base}${normalizedPath.startsWith("/") ? "" : "/"}${normalizedPath}`;
123
+ }
124
+ return src;
125
+ }
126
+
127
+ //#endregion
128
+ export { createMediaEngineFromSource };
129
+ //# sourceMappingURL=MediaEngine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaEngine.js","names":["toValidate: TrackRef[]","indexData: IndexData"],"sources":["../../../src/elements/EFMedia/MediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type {\n ManifestResponse,\n ThumbnailResult,\n} from \"../../transcoding/types/index.js\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator.js\";\nimport { CachedFetcher, type FetchFn } from \"./CachedFetcher.js\";\nimport {\n type SegmentIndex,\n type TrackRef,\n type TrackSet,\n createFragmentIndex,\n createManifestIndex,\n} from \"./SegmentIndex.js\";\nimport {\n type SegmentTransport,\n createByteRangeTransport,\n createUrlTransport,\n} from \"./SegmentTransport.js\";\nimport {\n type TimingModel,\n createByteRangeTiming,\n createJitTiming,\n} from \"./TimingModel.js\";\n\nexport interface MediaEngine {\n readonly durationMs: number;\n readonly src: string;\n readonly index: SegmentIndex;\n readonly transport: SegmentTransport;\n readonly timing: TimingModel;\n readonly tracks: TrackSet;\n extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]>;\n}\n\nexport function createMediaEngine(\n index: SegmentIndex,\n transport: SegmentTransport,\n timing: TimingModel,\n src: string,\n): MediaEngine {\n return {\n durationMs: index.durationMs,\n src,\n index,\n transport,\n timing,\n tracks: index.tracks,\n\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const track = index.tracks.video ?? index.tracks.scrub;\n if (!track) {\n return timestamps.map(() => null);\n }\n\n // Use dynamic import to keep ThumbnailExtractor out of initial bundle\n const { ThumbnailExtractor } =\n await import(\"./shared/ThumbnailExtractor.js\");\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const engine = this as MediaEngine;\n const extractor = new ThumbnailExtractor(engine);\n return extractor.extractThumbnails(\n timestamps,\n track,\n index.durationMs,\n signal,\n );\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Index data fetching\n// ---------------------------------------------------------------------------\n\ntype IndexData =\n | {\n type: \"fragment\";\n data: Record<number, TrackFragmentIndex>;\n src: string;\n apiHost: string;\n fileId: string;\n }\n | {\n type: \"manifest\";\n data: ManifestResponse;\n src: string;\n };\n\nexport async function fetchFileIndex(\n fetchFn: FetchFn,\n fileId: string,\n apiHost: string,\n signal?: AbortSignal,\n): Promise<Record<number, TrackFragmentIndex>> {\n const url = `${apiHost}/api/v1/files/${fileId}/index`;\n const response = await fetchFn(url, { signal });\n\n signal?.throwIfAborted();\n\n const contentType = response.headers.get(\"content-type\");\n if (\n !response.ok ||\n (contentType && !contentType.includes(\"application/json\"))\n ) {\n const text = await response.clone().text();\n if (!response.ok) {\n throw new Error(\n `Failed to fetch asset index: ${response.status} ${text}`,\n );\n }\n throw new Error(\n `Expected JSON but got ${contentType}: ${text.substring(0, 100)}`,\n );\n }\n\n try {\n const data = await response.json();\n signal?.throwIfAborted();\n return data as Record<number, TrackFragmentIndex>;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new Error(\n `Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport async function validateTrackAccess(\n transport: SegmentTransport,\n tracks: TrackSet,\n requiredTracks: \"audio\" | \"video\" | \"both\",\n signal?: AbortSignal,\n): Promise<void> {\n if (!signal) return;\n\n const toValidate: TrackRef[] = [];\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n\n if (needsVideo && tracks.video) toValidate.push(tracks.video);\n if (needsAudio && tracks.audio) toValidate.push(tracks.audio);\n\n for (const track of toValidate) {\n signal.throwIfAborted();\n try {\n await transport.fetchInitSegment(track, signal);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") &&\n error.message.includes(\"401\")))\n ) {\n throw new Error(\n `${track.role} segments require authentication: ${error.message}`,\n );\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Engine composition from index data\n// ---------------------------------------------------------------------------\n\nfunction buildEngineComponents(\n indexData: IndexData,\n fetcher: CachedFetcher,\n): {\n index: SegmentIndex;\n transport: SegmentTransport;\n timing: TimingModel;\n src: string;\n} {\n switch (indexData.type) {\n case \"fragment\": {\n const index = createFragmentIndex(indexData.data, indexData.src);\n const transport = createByteRangeTransport(\n indexData.data,\n indexData.fileId,\n indexData.apiHost,\n fetcher,\n );\n const timing = createByteRangeTiming(indexData.data);\n return { index, transport, timing, src: indexData.src };\n }\n\n case \"manifest\": {\n const index = createManifestIndex(indexData.data);\n const transport = createUrlTransport({\n fetcher,\n src: indexData.data.sourceUrl,\n templates: indexData.data.endpoints,\n audioTrackId: undefined,\n videoTrackId: undefined,\n });\n const timing = createJitTiming();\n return { index, transport, timing, src: indexData.data.sourceUrl };\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Top-level factory — called by EFMedia.#createMediaEngine\n// ---------------------------------------------------------------------------\n\nexport interface CreateMediaEngineOptions {\n src?: string | null;\n fileId?: string | null;\n apiHost?: string;\n requiredTracks: \"audio\" | \"video\" | \"both\";\n fetchFn: FetchFn;\n urlGenerator: UrlGenerator;\n signal?: AbortSignal;\n}\n\nexport async function createMediaEngineFromSource(\n opts: CreateMediaEngineOptions,\n): Promise<MediaEngine | undefined> {\n const {\n src,\n fileId,\n apiHost,\n requiredTracks,\n fetchFn,\n urlGenerator,\n signal,\n } = opts;\n\n const fetcher = new CachedFetcher(fetchFn);\n\n let indexData: IndexData;\n\n // File-ID mode: byte-range transport against cloud API\n if (fileId !== null && fileId !== undefined && fileId.trim() !== \"\") {\n if (!apiHost) {\n throw new Error(\"API host is required for file-id mode\");\n }\n const data = await fetchFileIndex(fetchFn, fileId, apiHost, signal);\n signal?.throwIfAborted();\n indexData = {\n type: \"fragment\",\n data,\n src: fileId,\n apiHost,\n fileId,\n };\n } else if (!src || typeof src !== \"string\" || src.trim() === \"\") {\n return undefined;\n } else {\n // Src-based mode: always fetch manifest from the server.\n // The server decides the transcoding strategy (local ffmpeg or cloud JIT).\n const manifestSrc = resolveManifestSrc(src, apiHost);\n const url = urlGenerator.generateManifestUrl(manifestSrc);\n const manifest = await fetcher.fetchJson(url, signal);\n signal?.throwIfAborted();\n indexData = { type: \"manifest\", data: manifest, src: manifest.sourceUrl };\n }\n\n const {\n index,\n transport,\n timing,\n src: engineSrc,\n } = buildEngineComponents(indexData, fetcher);\n\n await validateTrackAccess(transport, index.tracks, requiredTracks, signal);\n\n return createMediaEngine(index, transport, timing, engineSrc);\n}\n\n/**\n * Resolve a src value to the URL the server should transcode.\n * - Remote URLs (http/https) pass through as-is\n * - Local paths are made absolute using apiHost when available\n */\nfunction resolveManifestSrc(src: string, apiHost?: string): string {\n const lower = src.toLowerCase();\n if (lower.startsWith(\"http://\") || lower.startsWith(\"https://\")) {\n return src;\n }\n if (apiHost) {\n const base = apiHost.replace(/\\/$/, \"\");\n const normalizedPath = src.replace(/^\\.\\//, \"/src/\");\n return `${base}${normalizedPath.startsWith(\"/\") ? \"\" : \"/\"}${normalizedPath}`;\n }\n return src;\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import "../../transcoding/types/index.js";
2
+ import "@editframe/assets";
3
+
4
+ //#region src/elements/EFMedia/SegmentIndex.d.ts
5
+ type TrackRole = "video" | "audio" | "scrub";
6
+ interface TrackRef {
7
+ readonly role: TrackRole;
8
+ readonly id: string | number;
9
+ readonly src: string;
10
+ readonly segmentDurationMs?: number;
11
+ readonly segmentDurationsMs?: number[];
12
+ readonly startTimeOffsetMs?: number;
13
+ }
14
+ interface TrackSet {
15
+ video?: TrackRef;
16
+ audio?: TrackRef;
17
+ scrub?: TrackRef;
18
+ }
19
+ interface SegmentTimeRange {
20
+ segmentId: number;
21
+ startMs: number;
22
+ endMs: number;
23
+ }
24
+ interface SegmentIndex {
25
+ readonly durationMs: number;
26
+ readonly tracks: TrackSet;
27
+ segmentAt(timeMs: number, track: TrackRef): number | undefined;
28
+ segmentsInRange(fromMs: number, toMs: number, track: TrackRef): SegmentTimeRange[];
29
+ }
30
+ //#endregion
31
+ export { SegmentIndex, SegmentTimeRange, TrackRef, TrackRole, TrackSet };
32
+ //# sourceMappingURL=SegmentIndex.d.ts.map
@@ -0,0 +1,185 @@
1
+ import { convertToScaledTime, roundToMilliseconds } from "./shared/PrecisionUtils.js";
2
+
3
+ //#region src/elements/EFMedia/SegmentIndex.ts
4
+ function createFragmentIndex(data, src) {
5
+ const durationMs = Object.values(data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0) * 1e3;
6
+ const audioTrack = Object.values(data).find((t) => t.type === "audio");
7
+ const videoTrack = Object.values(data).find((t) => t.type === "video" && t.track !== void 0 && t.track > 0);
8
+ const scrubTrack = data[-1];
9
+ const tracks = {};
10
+ if (videoTrack && videoTrack.track !== void 0) tracks.video = {
11
+ role: "video",
12
+ id: videoTrack.track,
13
+ src,
14
+ startTimeOffsetMs: videoTrack.startTimeOffsetMs
15
+ };
16
+ if (audioTrack && audioTrack.track !== void 0) tracks.audio = {
17
+ role: "audio",
18
+ id: audioTrack.track,
19
+ src
20
+ };
21
+ if (scrubTrack && scrubTrack.track !== void 0) {
22
+ const segmentDurationsMs = scrubTrack.segments.length > 0 ? scrubTrack.segments.map((s) => s.duration / scrubTrack.timescale * 1e3) : void 0;
23
+ tracks.scrub = {
24
+ role: "scrub",
25
+ id: scrubTrack.track,
26
+ src,
27
+ segmentDurationMs: 3e4,
28
+ segmentDurationsMs,
29
+ startTimeOffsetMs: scrubTrack.startTimeOffsetMs
30
+ };
31
+ }
32
+ return {
33
+ durationMs,
34
+ tracks,
35
+ segmentAt(timeMs, track) {
36
+ const trackId = typeof track.id === "number" ? track.id : Number.parseInt(track.id, 10);
37
+ const trackData = data[trackId];
38
+ if (!trackData) throw new Error(`Track ${trackId} not found`);
39
+ const { timescale, segments } = trackData;
40
+ const scaledSeekTime = convertToScaledTime(roundToMilliseconds(timeMs + (track.startTimeOffsetMs || 0)), timescale);
41
+ for (let i = segments.length - 1; i >= 0; i--) {
42
+ const segment = segments[i];
43
+ const segmentEndTime = segment.cts + segment.duration;
44
+ if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) return i;
45
+ }
46
+ let nearestSegmentIndex = 0;
47
+ let nearestDistance = Number.MAX_SAFE_INTEGER;
48
+ for (let i = 0; i < segments.length; i++) {
49
+ const segment = segments[i];
50
+ const segmentEndTime = segment.cts + segment.duration;
51
+ let distance;
52
+ if (scaledSeekTime < segment.cts) distance = segment.cts - scaledSeekTime;
53
+ else if (scaledSeekTime >= segmentEndTime) distance = scaledSeekTime - segmentEndTime;
54
+ else return i;
55
+ if (distance < nearestDistance) {
56
+ nearestDistance = distance;
57
+ nearestSegmentIndex = i;
58
+ }
59
+ }
60
+ return nearestSegmentIndex;
61
+ },
62
+ segmentsInRange(fromMs, toMs, track) {
63
+ if (fromMs >= toMs) return [];
64
+ const trackData = data[typeof track.id === "number" ? track.id : Number.parseInt(track.id, 10)];
65
+ if (!trackData) return [];
66
+ const { timescale, segments } = trackData;
67
+ const ranges = [];
68
+ for (let i = 0; i < segments.length; i++) {
69
+ const segment = segments[i];
70
+ const segmentStartMs = segment.cts / timescale * 1e3;
71
+ const segmentEndMs = (segment.cts + segment.duration) / timescale * 1e3;
72
+ if (segmentStartMs < toMs && segmentEndMs > fromMs) ranges.push({
73
+ segmentId: i,
74
+ startMs: segmentStartMs,
75
+ endMs: segmentEndMs
76
+ });
77
+ }
78
+ return ranges;
79
+ }
80
+ };
81
+ }
82
+ function createManifestIndex(manifest) {
83
+ const durationMs = manifest.durationMs;
84
+ const tracks = {};
85
+ if (manifest.videoRenditions && manifest.videoRenditions.length > 0) {
86
+ const r = manifest.videoRenditions[0];
87
+ tracks.video = {
88
+ role: "video",
89
+ id: r.id,
90
+ src: manifest.sourceUrl,
91
+ segmentDurationMs: r.segmentDurationMs,
92
+ segmentDurationsMs: r.segmentDurationsMs,
93
+ startTimeOffsetMs: r.startTimeOffsetMs
94
+ };
95
+ const scrubRendition = manifest.videoRenditions.find((v) => v.id === "scrub");
96
+ if (scrubRendition) tracks.scrub = {
97
+ role: "scrub",
98
+ id: scrubRendition.id,
99
+ src: manifest.sourceUrl,
100
+ segmentDurationMs: scrubRendition.segmentDurationMs,
101
+ segmentDurationsMs: scrubRendition.segmentDurationsMs,
102
+ startTimeOffsetMs: scrubRendition.startTimeOffsetMs
103
+ };
104
+ }
105
+ if (manifest.audioRenditions && manifest.audioRenditions.length > 0) {
106
+ const r = manifest.audioRenditions[0];
107
+ tracks.audio = {
108
+ role: "audio",
109
+ id: r.id,
110
+ src: manifest.sourceUrl,
111
+ segmentDurationMs: r.segmentDurationMs,
112
+ segmentDurationsMs: r.segmentDurationsMs,
113
+ startTimeOffsetMs: r.startTimeOffsetMs
114
+ };
115
+ }
116
+ function computeSegmentIdForTrack(desiredSeekTimeMs, track) {
117
+ if (desiredSeekTimeMs > durationMs) return;
118
+ if (track.segmentDurationsMs && track.segmentDurationsMs.length > 0) {
119
+ let cumulativeTime = 0;
120
+ for (let i = 0; i < track.segmentDurationsMs.length; i++) {
121
+ const segmentDuration = track.segmentDurationsMs[i];
122
+ if (segmentDuration === void 0) throw new Error("Segment duration is required for JIT metadata");
123
+ const segmentStartMs = cumulativeTime;
124
+ const segmentEndMs = cumulativeTime + segmentDuration;
125
+ const includesEndTime = i === track.segmentDurationsMs.length - 1 && desiredSeekTimeMs === durationMs;
126
+ if (desiredSeekTimeMs >= segmentStartMs && (desiredSeekTimeMs < segmentEndMs || includesEndTime)) return i + 1;
127
+ cumulativeTime += segmentDuration;
128
+ if (cumulativeTime >= durationMs) break;
129
+ }
130
+ return;
131
+ }
132
+ if (!track.segmentDurationMs) throw new Error("Segment duration is required for JIT metadata");
133
+ const segmentIndex = Math.floor(desiredSeekTimeMs / track.segmentDurationMs);
134
+ if (segmentIndex * track.segmentDurationMs >= durationMs) return;
135
+ return segmentIndex + 1;
136
+ }
137
+ return {
138
+ durationMs,
139
+ tracks,
140
+ segmentAt(timeMs, track) {
141
+ return computeSegmentIdForTrack(timeMs, track);
142
+ },
143
+ segmentsInRange(fromMs, toMs, track) {
144
+ if (fromMs >= toMs) return [];
145
+ const segments = [];
146
+ if (track.segmentDurationsMs && track.segmentDurationsMs.length > 0) {
147
+ let cumulativeTime = 0;
148
+ for (let i = 0; i < track.segmentDurationsMs.length; i++) {
149
+ const segmentDuration = track.segmentDurationsMs[i];
150
+ if (segmentDuration === void 0) continue;
151
+ const segmentStartMs = cumulativeTime;
152
+ const segmentEndMs = Math.min(cumulativeTime + segmentDuration, durationMs);
153
+ if (segmentStartMs >= durationMs) break;
154
+ if (segmentStartMs < toMs && segmentEndMs > fromMs) segments.push({
155
+ segmentId: i + 1,
156
+ startMs: segmentStartMs,
157
+ endMs: segmentEndMs
158
+ });
159
+ cumulativeTime += segmentDuration;
160
+ if (cumulativeTime >= durationMs) break;
161
+ }
162
+ return segments;
163
+ }
164
+ const segmentDurationMs = track.segmentDurationMs || 1e3;
165
+ const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);
166
+ const endSegmentIndex = Math.floor(toMs / segmentDurationMs);
167
+ for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
168
+ const segmentId = i + 1;
169
+ const segmentStartMs = i * segmentDurationMs;
170
+ const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);
171
+ if (segmentStartMs >= durationMs) break;
172
+ if (segmentStartMs < toMs && segmentEndMs > fromMs) segments.push({
173
+ segmentId,
174
+ startMs: segmentStartMs,
175
+ endMs: segmentEndMs
176
+ });
177
+ }
178
+ return segments;
179
+ }
180
+ };
181
+ }
182
+
183
+ //#endregion
184
+ export { createFragmentIndex, createManifestIndex };
185
+ //# sourceMappingURL=SegmentIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SegmentIndex.js","names":["tracks: TrackSet","distance: number","ranges: SegmentTimeRange[]","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/SegmentIndex.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { ManifestResponse } from \"../../transcoding/types/index.js\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils.js\";\n\nexport type TrackRole = \"video\" | \"audio\" | \"scrub\";\n\nexport interface TrackRef {\n readonly role: TrackRole;\n readonly id: string | number;\n readonly src: string;\n readonly segmentDurationMs?: number;\n readonly segmentDurationsMs?: number[];\n readonly startTimeOffsetMs?: number;\n}\n\nexport interface TrackSet {\n video?: TrackRef;\n audio?: TrackRef;\n scrub?: TrackRef;\n}\n\nexport interface SegmentTimeRange {\n segmentId: number;\n startMs: number;\n endMs: number;\n}\n\nexport interface SegmentIndex {\n readonly durationMs: number;\n readonly tracks: TrackSet;\n segmentAt(timeMs: number, track: TrackRef): number | undefined;\n segmentsInRange(\n fromMs: number,\n toMs: number,\n track: TrackRef,\n ): SegmentTimeRange[];\n}\n\n// ---------------------------------------------------------------------------\n// FragmentIndex — backed by TrackFragmentIndex (local and file-id files)\n// ---------------------------------------------------------------------------\n\nexport function createFragmentIndex(\n data: Record<number, TrackFragmentIndex>,\n src: string,\n): SegmentIndex {\n const longestFragment = Object.values(data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n const durationMs = longestFragment * 1000;\n\n const audioTrack = Object.values(data).find((t) => t.type === \"audio\");\n const videoTrack = Object.values(data).find(\n (t) => t.type === \"video\" && t.track !== undefined && t.track > 0,\n );\n const scrubTrack = data[-1];\n\n const tracks: TrackSet = {};\n\n if (videoTrack && videoTrack.track !== undefined) {\n tracks.video = {\n role: \"video\",\n id: videoTrack.track,\n src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n }\n\n if (audioTrack && audioTrack.track !== undefined) {\n tracks.audio = {\n role: \"audio\",\n id: audioTrack.track,\n src,\n };\n }\n\n if (scrubTrack && scrubTrack.track !== undefined) {\n const segmentDurationsMs =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map(\n (s) => (s.duration / scrubTrack.timescale) * 1000,\n )\n : undefined;\n tracks.scrub = {\n role: \"scrub\",\n id: scrubTrack.track,\n src,\n segmentDurationMs: 30000,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n return {\n durationMs,\n tracks,\n\n segmentAt(timeMs: number, track: TrackRef): number | undefined {\n const trackId =\n typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n const trackData = data[trackId];\n if (!trackData) {\n throw new Error(`Track ${trackId} not found`);\n }\n const { timescale, segments } = trackData;\n\n const startTimeOffsetMs = track.startTimeOffsetMs || 0;\n const offsetSeekTimeMs = roundToMilliseconds(timeMs + startTimeOffsetMs);\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n for (let i = segments.length - 1; i >= 0; i--) {\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Gap handling: find nearest segment\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segment.cts) {\n distance = segment.cts - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n distance = scaledSeekTime - segmentEndTime;\n } else {\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n },\n\n segmentsInRange(\n fromMs: number,\n toMs: number,\n track: TrackRef,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs) return [];\n\n const trackId =\n typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n const trackData = data[trackId];\n if (!trackData) return [];\n\n const { timescale, segments } = trackData;\n const ranges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]!;\n const segmentStartMs = (segment.cts / timescale) * 1000;\n const segmentEndMs =\n ((segment.cts + segment.duration) / timescale) * 1000;\n\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n ranges.push({\n segmentId: i,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return ranges;\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// ManifestIndex — backed by ManifestResponse (JIT transcoding)\n// ---------------------------------------------------------------------------\n\nexport function createManifestIndex(manifest: ManifestResponse): SegmentIndex {\n const durationMs = manifest.durationMs;\n const tracks: TrackSet = {};\n\n if (manifest.videoRenditions && manifest.videoRenditions.length > 0) {\n const r = manifest.videoRenditions[0]!;\n tracks.video = {\n role: \"video\",\n id: r.id,\n src: manifest.sourceUrl,\n segmentDurationMs: r.segmentDurationMs,\n segmentDurationsMs: r.segmentDurationsMs,\n startTimeOffsetMs: r.startTimeOffsetMs,\n };\n\n const scrubRendition = manifest.videoRenditions.find(\n (v) => v.id === \"scrub\",\n );\n if (scrubRendition) {\n tracks.scrub = {\n role: \"scrub\",\n id: scrubRendition.id,\n src: manifest.sourceUrl,\n segmentDurationMs: scrubRendition.segmentDurationMs,\n segmentDurationsMs: scrubRendition.segmentDurationsMs,\n startTimeOffsetMs: scrubRendition.startTimeOffsetMs,\n };\n }\n }\n\n if (manifest.audioRenditions && manifest.audioRenditions.length > 0) {\n const r = manifest.audioRenditions[0]!;\n tracks.audio = {\n role: \"audio\",\n id: r.id,\n src: manifest.sourceUrl,\n segmentDurationMs: r.segmentDurationMs,\n segmentDurationsMs: r.segmentDurationsMs,\n startTimeOffsetMs: r.startTimeOffsetMs,\n };\n }\n\n function computeSegmentIdForTrack(\n desiredSeekTimeMs: number,\n track: TrackRef,\n ): number | undefined {\n if (desiredSeekTimeMs > durationMs) {\n return undefined;\n }\n\n if (track.segmentDurationsMs && track.segmentDurationsMs.length > 0) {\n let cumulativeTime = 0;\n for (let i = 0; i < track.segmentDurationsMs.length; i++) {\n const segmentDuration = track.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = cumulativeTime + segmentDuration;\n\n const isLastSegment = i === track.segmentDurationsMs.length - 1;\n const includesEndTime =\n isLastSegment && desiredSeekTimeMs === durationMs;\n\n if (\n desiredSeekTimeMs >= segmentStartMs &&\n (desiredSeekTimeMs < segmentEndMs || includesEndTime)\n ) {\n return i + 1;\n }\n\n cumulativeTime += segmentDuration;\n if (cumulativeTime >= durationMs) break;\n }\n return undefined;\n }\n\n if (!track.segmentDurationMs) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n\n const segmentIndex = Math.floor(\n desiredSeekTimeMs / track.segmentDurationMs,\n );\n const segmentStartMs = segmentIndex * track.segmentDurationMs;\n if (segmentStartMs >= durationMs) {\n return undefined;\n }\n return segmentIndex + 1;\n }\n\n return {\n durationMs,\n tracks,\n\n segmentAt(timeMs: number, track: TrackRef): number | undefined {\n return computeSegmentIdForTrack(timeMs, track);\n },\n\n segmentsInRange(\n fromMs: number,\n toMs: number,\n track: TrackRef,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs) return [];\n\n const segments: SegmentTimeRange[] = [];\n\n if (track.segmentDurationsMs && track.segmentDurationsMs.length > 0) {\n let cumulativeTime = 0;\n for (let i = 0; i < track.segmentDurationsMs.length; i++) {\n const segmentDuration = track.segmentDurationsMs[i];\n if (segmentDuration === undefined) continue;\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n if (segmentStartMs >= durationMs) break;\n\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n if (cumulativeTime >= durationMs) break;\n }\n return segments;\n }\n\n const segmentDurationMs = track.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1;\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n if (segmentStartMs >= durationMs) break;\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { TrackRef } from "./SegmentIndex.js";
2
+ import "@editframe/assets";
3
+
4
+ //#region src/elements/EFMedia/SegmentTransport.d.ts
5
+ interface SegmentTransport {
6
+ fetchInitSegment(track: TrackRef, signal: AbortSignal): Promise<ArrayBuffer>;
7
+ fetchMediaSegment(segmentId: number, track: TrackRef, signal: AbortSignal): Promise<ArrayBuffer>;
8
+ isCached(segmentId: number, track: TrackRef): boolean;
9
+ }
10
+ //#endregion
11
+ export { SegmentTransport };
12
+ //# sourceMappingURL=SegmentTransport.d.ts.map
@@ -0,0 +1,69 @@
1
+ //#region src/elements/EFMedia/SegmentTransport.ts
2
+ function resolveRenditionId(track) {
3
+ if (track.role === "audio") return "audio";
4
+ if (track.role === "scrub") return "scrub";
5
+ if (typeof track.id === "string") return track.id;
6
+ if (track.id === -1) return "scrub";
7
+ if (track.id === 2) return "audio";
8
+ return "high";
9
+ }
10
+ function createUrlTransport(opts) {
11
+ const { fetcher, src, templates, audioTrackId, videoTrackId } = opts;
12
+ function buildSegmentUrl(segmentId, track) {
13
+ const renditionId = resolveRenditionId(track);
14
+ const template = segmentId === "init" ? templates.initSegment : templates.mediaSegment;
15
+ const trackId = typeof track.id === "number" ? track.id : track.role === "audio" ? audioTrackId : videoTrackId;
16
+ return template.replace("{rendition}", renditionId).replace("{segmentId}", segmentId.toString()).replace("{src}", src).replace("{trackId}", trackId?.toString() ?? "");
17
+ }
18
+ return {
19
+ async fetchInitSegment(track, signal) {
20
+ const url = buildSegmentUrl("init", track);
21
+ return fetcher.fetchArrayBuffer(url, signal);
22
+ },
23
+ async fetchMediaSegment(segmentId, track, signal) {
24
+ const url = buildSegmentUrl(segmentId, track);
25
+ return fetcher.fetchArrayBuffer(url, signal);
26
+ },
27
+ isCached(segmentId, track) {
28
+ const url = buildSegmentUrl(segmentId, track);
29
+ return fetcher.has(url);
30
+ }
31
+ };
32
+ }
33
+ function createByteRangeTransport(data, fileId, apiHost, fetcher) {
34
+ function buildTrackUrl(trackId) {
35
+ return `${apiHost}/api/v1/files/${fileId}/tracks/${trackId}`;
36
+ }
37
+ function getTrackId(track) {
38
+ const trackId = typeof track.id === "number" ? track.id : Number.parseInt(track.id, 10);
39
+ if (Number.isNaN(trackId)) throw new Error(`Invalid track ID: ${track.id}`);
40
+ return trackId;
41
+ }
42
+ return {
43
+ async fetchInitSegment(track, signal) {
44
+ const trackId = getTrackId(track);
45
+ const trackData = data[trackId];
46
+ if (!trackData) throw new Error(`Track ${trackId} not found`);
47
+ const { offset, size } = trackData.initSegment;
48
+ const url = buildTrackUrl(trackId);
49
+ return (await fetcher.fetchArrayBuffer(url, signal)).slice(offset, offset + size);
50
+ },
51
+ async fetchMediaSegment(segmentId, track, signal) {
52
+ const trackId = getTrackId(track);
53
+ const trackData = data[trackId];
54
+ if (!trackData) throw new Error(`Track ${trackId} not found`);
55
+ const segment = trackData.segments[segmentId];
56
+ if (!segment) throw new Error(`Segment ${segmentId} not found for track ${trackId}`);
57
+ const url = buildTrackUrl(trackId);
58
+ return (await fetcher.fetchArrayBuffer(url, signal)).slice(segment.offset, segment.offset + segment.size);
59
+ },
60
+ isCached(_segmentId, track) {
61
+ const url = buildTrackUrl(getTrackId(track));
62
+ return fetcher.has(url);
63
+ }
64
+ };
65
+ }
66
+
67
+ //#endregion
68
+ export { createByteRangeTransport, createUrlTransport };
69
+ //# sourceMappingURL=SegmentTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SegmentTransport.js","names":[],"sources":["../../../src/elements/EFMedia/SegmentTransport.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { RenditionId } from \"../../transcoding/types/index.js\";\nimport type { TrackRef } from \"./SegmentIndex.js\";\nimport type { CachedFetcher } from \"./CachedFetcher.js\";\n\nexport interface SegmentTransport {\n fetchInitSegment(track: TrackRef, signal: AbortSignal): Promise<ArrayBuffer>;\n fetchMediaSegment(\n segmentId: number,\n track: TrackRef,\n signal: AbortSignal,\n ): Promise<ArrayBuffer>;\n isCached(segmentId: number, track: TrackRef): boolean;\n}\n\n// ---------------------------------------------------------------------------\n// UrlTransport — each segment has its own URL\n// Used by AssetMediaEngine (via JIT URLs) and JitMediaEngine natively.\n// ---------------------------------------------------------------------------\n\ninterface UrlTransportOptions {\n fetcher: CachedFetcher;\n src: string;\n templates: { initSegment: string; mediaSegment: string };\n audioTrackId: number | undefined;\n videoTrackId: number | undefined;\n}\n\nfunction resolveRenditionId(track: TrackRef): RenditionId {\n if (track.role === \"audio\") return \"audio\";\n if (track.role === \"scrub\") return \"scrub\";\n if (typeof track.id === \"string\") return track.id as RenditionId;\n // For numeric IDs (fragment-based), map to JIT rendition names\n if (track.id === -1) return \"scrub\";\n if (track.id === 2) return \"audio\";\n return \"high\";\n}\n\nexport function createUrlTransport(\n opts: UrlTransportOptions,\n): SegmentTransport {\n const { fetcher, src, templates, audioTrackId, videoTrackId } = opts;\n\n function buildSegmentUrl(\n segmentId: \"init\" | number,\n track: TrackRef,\n ): string {\n const renditionId = resolveRenditionId(track);\n const template =\n segmentId === \"init\" ? templates.initSegment : templates.mediaSegment;\n const trackId =\n typeof track.id === \"number\"\n ? track.id\n : track.role === \"audio\"\n ? audioTrackId\n : videoTrackId;\n return template\n .replace(\"{rendition}\", renditionId)\n .replace(\"{segmentId}\", segmentId.toString())\n .replace(\"{src}\", src)\n .replace(\"{trackId}\", trackId?.toString() ?? \"\");\n }\n\n return {\n async fetchInitSegment(\n track: TrackRef,\n signal: AbortSignal,\n ): Promise<ArrayBuffer> {\n const url = buildSegmentUrl(\"init\", track);\n return fetcher.fetchArrayBuffer(url, signal);\n },\n\n async fetchMediaSegment(\n segmentId: number,\n track: TrackRef,\n signal: AbortSignal,\n ): Promise<ArrayBuffer> {\n const url = buildSegmentUrl(segmentId, track);\n return fetcher.fetchArrayBuffer(url, signal);\n },\n\n isCached(segmentId: number, track: TrackRef): boolean {\n const url = buildSegmentUrl(segmentId, track);\n return fetcher.has(url);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// ByteRangeTransport — fetches full track binary, slices segments\n// Used by FileMediaEngine.\n// ---------------------------------------------------------------------------\n\nexport function createByteRangeTransport(\n data: Record<number, TrackFragmentIndex>,\n fileId: string,\n apiHost: string,\n fetcher: CachedFetcher,\n): SegmentTransport {\n function buildTrackUrl(trackId: number): string {\n return `${apiHost}/api/v1/files/${fileId}/tracks/${trackId}`;\n }\n\n function getTrackId(track: TrackRef): number {\n const trackId =\n typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n if (Number.isNaN(trackId)) {\n throw new Error(`Invalid track ID: ${track.id}`);\n }\n return trackId;\n }\n\n return {\n async fetchInitSegment(\n track: TrackRef,\n signal: AbortSignal,\n ): Promise<ArrayBuffer> {\n const trackId = getTrackId(track);\n const trackData = data[trackId];\n if (!trackData) throw new Error(`Track ${trackId} not found`);\n\n const { offset, size } = trackData.initSegment;\n const url = buildTrackUrl(trackId);\n const fullTrack = await fetcher.fetchArrayBuffer(url, signal);\n return fullTrack.slice(offset, offset + size);\n },\n\n async fetchMediaSegment(\n segmentId: number,\n track: TrackRef,\n signal: AbortSignal,\n ): Promise<ArrayBuffer> {\n const trackId = getTrackId(track);\n const trackData = data[trackId];\n if (!trackData) throw new Error(`Track ${trackId} not found`);\n\n const segment = trackData.segments[segmentId];\n if (!segment) {\n throw new Error(`Segment ${segmentId} not found for track ${trackId}`);\n }\n\n const url = buildTrackUrl(trackId);\n const fullTrack = await fetcher.fetchArrayBuffer(url, signal);\n return fullTrack.slice(segment.offset, segment.offset + segment.size);\n },\n\n isCached(_segmentId: number, track: TrackRef): boolean {\n const trackId = getTrackId(track);\n const url = buildTrackUrl(trackId);\n return fetcher.has(url);\n },\n };\n}\n"],"mappings":";AA4BA,SAAS,mBAAmB,OAA8B;AACxD,KAAI,MAAM,SAAS,QAAS,QAAO;AACnC,KAAI,MAAM,SAAS,QAAS,QAAO;AACnC,KAAI,OAAO,MAAM,OAAO,SAAU,QAAO,MAAM;AAE/C,KAAI,MAAM,OAAO,GAAI,QAAO;AAC5B,KAAI,MAAM,OAAO,EAAG,QAAO;AAC3B,QAAO;;AAGT,SAAgB,mBACd,MACkB;CAClB,MAAM,EAAE,SAAS,KAAK,WAAW,cAAc,iBAAiB;CAEhE,SAAS,gBACP,WACA,OACQ;EACR,MAAM,cAAc,mBAAmB,MAAM;EAC7C,MAAM,WACJ,cAAc,SAAS,UAAU,cAAc,UAAU;EAC3D,MAAM,UACJ,OAAO,MAAM,OAAO,WAChB,MAAM,KACN,MAAM,SAAS,UACb,eACA;AACR,SAAO,SACJ,QAAQ,eAAe,YAAY,CACnC,QAAQ,eAAe,UAAU,UAAU,CAAC,CAC5C,QAAQ,SAAS,IAAI,CACrB,QAAQ,aAAa,SAAS,UAAU,IAAI,GAAG;;AAGpD,QAAO;EACL,MAAM,iBACJ,OACA,QACsB;GACtB,MAAM,MAAM,gBAAgB,QAAQ,MAAM;AAC1C,UAAO,QAAQ,iBAAiB,KAAK,OAAO;;EAG9C,MAAM,kBACJ,WACA,OACA,QACsB;GACtB,MAAM,MAAM,gBAAgB,WAAW,MAAM;AAC7C,UAAO,QAAQ,iBAAiB,KAAK,OAAO;;EAG9C,SAAS,WAAmB,OAA0B;GACpD,MAAM,MAAM,gBAAgB,WAAW,MAAM;AAC7C,UAAO,QAAQ,IAAI,IAAI;;EAE1B;;AAQH,SAAgB,yBACd,MACA,QACA,SACA,SACkB;CAClB,SAAS,cAAc,SAAyB;AAC9C,SAAO,GAAG,QAAQ,gBAAgB,OAAO,UAAU;;CAGrD,SAAS,WAAW,OAAyB;EAC3C,MAAM,UACJ,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,OAAO,SAAS,MAAM,IAAI,GAAG;AACzE,MAAI,OAAO,MAAM,QAAQ,CACvB,OAAM,IAAI,MAAM,qBAAqB,MAAM,KAAK;AAElD,SAAO;;AAGT,QAAO;EACL,MAAM,iBACJ,OACA,QACsB;GACtB,MAAM,UAAU,WAAW,MAAM;GACjC,MAAM,YAAY,KAAK;AACvB,OAAI,CAAC,UAAW,OAAM,IAAI,MAAM,SAAS,QAAQ,YAAY;GAE7D,MAAM,EAAE,QAAQ,SAAS,UAAU;GACnC,MAAM,MAAM,cAAc,QAAQ;AAElC,WADkB,MAAM,QAAQ,iBAAiB,KAAK,OAAO,EAC5C,MAAM,QAAQ,SAAS,KAAK;;EAG/C,MAAM,kBACJ,WACA,OACA,QACsB;GACtB,MAAM,UAAU,WAAW,MAAM;GACjC,MAAM,YAAY,KAAK;AACvB,OAAI,CAAC,UAAW,OAAM,IAAI,MAAM,SAAS,QAAQ,YAAY;GAE7D,MAAM,UAAU,UAAU,SAAS;AACnC,OAAI,CAAC,QACH,OAAM,IAAI,MAAM,WAAW,UAAU,uBAAuB,UAAU;GAGxE,MAAM,MAAM,cAAc,QAAQ;AAElC,WADkB,MAAM,QAAQ,iBAAiB,KAAK,OAAO,EAC5C,MAAM,QAAQ,QAAQ,QAAQ,SAAS,QAAQ,KAAK;;EAGvE,SAAS,YAAoB,OAA0B;GAErD,MAAM,MAAM,cADI,WAAW,MAAM,CACC;AAClC,UAAO,QAAQ,IAAI,IAAI;;EAE1B"}
@@ -0,0 +1,10 @@
1
+ import { TrackRef } from "./SegmentIndex.js";
2
+ import "@editframe/assets";
3
+
4
+ //#region src/elements/EFMedia/TimingModel.d.ts
5
+ interface TimingModel {
6
+ toContainerSeconds(timeMs: number, segmentId: number, track: TrackRef): number;
7
+ }
8
+ //#endregion
9
+ export { TimingModel };
10
+ //# sourceMappingURL=TimingModel.d.ts.map
@@ -0,0 +1,28 @@
1
+ //#region src/elements/EFMedia/TimingModel.ts
2
+ /**
3
+ * For byte-range sliced segments from full track files (FileMediaEngine).
4
+ * mediabunny sees segment-relative timestamps since we sliced at segment boundaries,
5
+ * so we subtract the segment's CTS to get relative time.
6
+ */
7
+ function createByteRangeTiming(data) {
8
+ return { toContainerSeconds(timeMs, segmentId, track) {
9
+ const trackData = data[typeof track.id === "number" ? track.id : Number.parseInt(track.id, 10)];
10
+ if (!trackData) throw new Error("Track not found");
11
+ const segment = trackData.segments[segmentId];
12
+ if (!segment) throw new Error("Segment not found");
13
+ return (timeMs - segment.cts / trackData.timescale * 1e3) / 1e3;
14
+ } };
15
+ }
16
+ /**
17
+ * For JIT transcoded segments (JitMediaEngine).
18
+ * Segments are self-contained — just convert ms to seconds.
19
+ */
20
+ function createJitTiming() {
21
+ return { toContainerSeconds(timeMs, _segmentId, _track) {
22
+ return timeMs / 1e3;
23
+ } };
24
+ }
25
+
26
+ //#endregion
27
+ export { createByteRangeTiming, createJitTiming };
28
+ //# sourceMappingURL=TimingModel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimingModel.js","names":[],"sources":["../../../src/elements/EFMedia/TimingModel.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type { TrackRef } from \"./SegmentIndex.js\";\n\nexport interface TimingModel {\n toContainerSeconds(\n timeMs: number,\n segmentId: number,\n track: TrackRef,\n ): number;\n}\n\n/**\n * For byte-range sliced segments from full track files (FileMediaEngine).\n * mediabunny sees segment-relative timestamps since we sliced at segment boundaries,\n * so we subtract the segment's CTS to get relative time.\n */\nexport function createByteRangeTiming(\n data: Record<number, TrackFragmentIndex>,\n): TimingModel {\n return {\n toContainerSeconds(\n timeMs: number,\n segmentId: number,\n track: TrackRef,\n ): number {\n const trackId =\n typeof track.id === \"number\" ? track.id : Number.parseInt(track.id, 10);\n const trackData = data[trackId];\n if (!trackData) throw new Error(\"Track not found\");\n\n const segment = trackData.segments[segmentId];\n if (!segment) throw new Error(\"Segment not found\");\n\n const segmentStartMs = (segment.cts / trackData.timescale) * 1000;\n return (timeMs - segmentStartMs) / 1000;\n },\n };\n}\n\n/**\n * For JIT transcoded segments (JitMediaEngine).\n * Segments are self-contained — just convert ms to seconds.\n */\nexport function createJitTiming(): TimingModel {\n return {\n toContainerSeconds(\n timeMs: number,\n _segmentId: number,\n _track: TrackRef,\n ): number {\n return timeMs / 1000;\n },\n };\n}\n"],"mappings":";;;;;;AAgBA,SAAgB,sBACd,MACa;AACb,QAAO,EACL,mBACE,QACA,WACA,OACQ;EAGR,MAAM,YAAY,KADhB,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,OAAO,SAAS,MAAM,IAAI,GAAG;AAEzE,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,kBAAkB;EAElD,MAAM,UAAU,UAAU,SAAS;AACnC,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,oBAAoB;AAGlD,UAAQ,SADgB,QAAQ,MAAM,UAAU,YAAa,OAC1B;IAEtC;;;;;;AAOH,SAAgB,kBAA+B;AAC7C,QAAO,EACL,mBACE,QACA,YACA,QACQ;AACR,SAAO,SAAS;IAEnB"}
@@ -4,11 +4,11 @@
4
4
  * Pure function with explicit dependencies
5
5
  */
6
6
  const fetchAudioSegmentData = async (segmentIds, mediaEngine, signal) => {
7
- const audioRendition = mediaEngine.audioRendition;
8
- if (!audioRendition) throw new Error("Audio rendition not available");
7
+ const audioTrack = mediaEngine.tracks.audio;
8
+ if (!audioTrack) throw new Error("Audio track not available");
9
9
  const segmentData = /* @__PURE__ */ new Map();
10
10
  const fetchPromises = segmentIds.map(async (segmentId) => {
11
- return [segmentId, await mediaEngine.fetchMediaSegment(segmentId, audioRendition, signal)];
11
+ return [segmentId, await mediaEngine.transport.fetchMediaSegment(segmentId, audioTrack, signal)];
12
12
  });
13
13
  const fetchedSegments = await Promise.all(fetchPromises);
14
14
  signal.throwIfAborted();
@@ -31,11 +31,12 @@ const fetchAudioSpanningTime = async (host, fromMs, toMs, signal) => {
31
31
  if (fromMs >= toMs || fromMs < 0) throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);
32
32
  const mediaEngine = await host.getMediaEngine(signal);
33
33
  signal.throwIfAborted();
34
- if (!mediaEngine?.audioRendition) return;
35
- const initSegment = await mediaEngine.fetchInitSegment(mediaEngine.audioRendition, signal);
34
+ const audioTrack = mediaEngine?.tracks.audio;
35
+ if (!audioTrack) return;
36
+ const initSegment = await mediaEngine.transport.fetchInitSegment(audioTrack, signal);
36
37
  signal.throwIfAborted();
37
38
  if (!initSegment) return;
38
- const segmentRanges = mediaEngine.calculateAudioSegmentRange(fromMs, toMs, mediaEngine.audioRendition, host.intrinsicDurationMs || 1e4);
39
+ const segmentRanges = mediaEngine.index.segmentsInRange(fromMs, toMs, audioTrack);
39
40
  if (segmentRanges.length === 0) throw new Error(`No segments found for time range ${fromMs}-${toMs}ms`);
40
41
  const segmentIds = segmentRanges.map((r) => r.segmentId);
41
42
  const segmentData = await fetchAudioSegmentData(segmentIds, mediaEngine, signal);
@@ -1 +1 @@
1
- {"version":3,"file":"AudioSpanUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/AudioSpanUtils.ts"],"sourcesContent":["import type {\n AudioSpan,\n MediaEngine,\n SegmentTimeRange,\n} from \"../../../transcoding/types\";\nimport type { EFMedia } from \"../../EFMedia\";\n\n/**\n * Fetch audio segment data using MediaEngine\n * Pure function with explicit dependencies\n */\nconst fetchAudioSegmentData = async (\n segmentIds: number[],\n mediaEngine: MediaEngine,\n signal: AbortSignal,\n): Promise<Map<number, ArrayBuffer>> => {\n const audioRendition = mediaEngine.audioRendition;\n if (!audioRendition) {\n throw new Error(\"Audio rendition not available\");\n }\n\n const segmentData = new Map<number, ArrayBuffer>();\n\n // Fetch all segments - MediaEngine handles deduplication internally\n const fetchPromises = segmentIds.map(async (segmentId) => {\n const arrayBuffer = await mediaEngine.fetchMediaSegment(\n segmentId,\n audioRendition,\n signal,\n );\n return [segmentId, arrayBuffer] as [number, ArrayBuffer];\n });\n\n const fetchedSegments = await Promise.all(fetchPromises);\n signal.throwIfAborted();\n\n for (const [segmentId, arrayBuffer] of fetchedSegments) {\n segmentData.set(segmentId, arrayBuffer);\n }\n\n return segmentData;\n};\n\n/**\n * Create audio span blob from init segment and media segments\n * Pure function for blob creation\n */\nconst createAudioSpanBlob = (\n initSegment: ArrayBuffer,\n mediaSegments: ArrayBuffer[],\n): Blob => {\n const chunks = [initSegment, ...mediaSegments];\n return new Blob(chunks, { type: \"audio/mp4\" });\n};\n\n/**\n * Fetch audio spanning a time range\n * Main function that orchestrates segment calculation, fetching, and blob creation\n */\nexport const fetchAudioSpanningTime = async (\n host: EFMedia,\n fromMs: number,\n toMs: number,\n signal: AbortSignal,\n): Promise<AudioSpan | undefined> => {\n // Validate inputs\n if (fromMs >= toMs || fromMs < 0) {\n throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);\n }\n\n // Get media engine using the new async method\n const mediaEngine = await host.getMediaEngine(signal);\n signal.throwIfAborted();\n\n // Return undefined if no audio rendition available\n if (!mediaEngine?.audioRendition) {\n return undefined;\n }\n\n // Fetch the init segment directly from media engine\n const initSegment = await mediaEngine.fetchInitSegment(\n mediaEngine.audioRendition,\n signal,\n );\n signal.throwIfAborted();\n\n if (!initSegment) {\n return undefined;\n }\n\n // Calculate segments needed using the media engine's method\n const segmentRanges = mediaEngine.calculateAudioSegmentRange(\n fromMs,\n toMs,\n mediaEngine.audioRendition,\n host.intrinsicDurationMs || 10000,\n );\n\n if (segmentRanges.length === 0) {\n throw new Error(`No segments found for time range ${fromMs}-${toMs}ms`);\n }\n\n // Fetch segment data\n const segmentIds = segmentRanges.map((r: SegmentTimeRange) => r.segmentId);\n const segmentData = await fetchAudioSegmentData(\n segmentIds,\n mediaEngine,\n signal,\n );\n\n // Create ordered array of segments\n const orderedSegments = segmentIds.map((id: number) => {\n const segment = segmentData.get(id);\n if (!segment) {\n throw new Error(`Missing segment data for segment ID ${id}`);\n }\n return segment;\n });\n\n // Create blob\n const blob = createAudioSpanBlob(initSegment, orderedSegments);\n\n // Calculate actual time boundaries\n const actualStartMs = Math.min(\n ...segmentRanges.map((r: SegmentTimeRange) => r.startMs),\n );\n const actualEndMs = Math.max(\n ...segmentRanges.map((r: SegmentTimeRange) => r.endMs),\n );\n\n return {\n startMs: actualStartMs,\n endMs: actualEndMs,\n blob,\n };\n};\n"],"mappings":";;;;;AAWA,MAAM,wBAAwB,OAC5B,YACA,aACA,WACsC;CACtC,MAAM,iBAAiB,YAAY;AACnC,KAAI,CAAC,eACH,OAAM,IAAI,MAAM,gCAAgC;CAGlD,MAAM,8BAAc,IAAI,KAA0B;CAGlD,MAAM,gBAAgB,WAAW,IAAI,OAAO,cAAc;AAMxD,SAAO,CAAC,WALY,MAAM,YAAY,kBACpC,WACA,gBACA,OACD,CAC8B;GAC/B;CAEF,MAAM,kBAAkB,MAAM,QAAQ,IAAI,cAAc;AACxD,QAAO,gBAAgB;AAEvB,MAAK,MAAM,CAAC,WAAW,gBAAgB,gBACrC,aAAY,IAAI,WAAW,YAAY;AAGzC,QAAO;;;;;;AAOT,MAAM,uBACJ,aACA,kBACS;CACT,MAAM,SAAS,CAAC,aAAa,GAAG,cAAc;AAC9C,QAAO,IAAI,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;;;;;;AAOhD,MAAa,yBAAyB,OACpC,MACA,QACA,MACA,WACmC;AAEnC,KAAI,UAAU,QAAQ,SAAS,EAC7B,OAAM,IAAI,MAAM,8BAA8B,OAAO,SAAS,OAAO;CAIvE,MAAM,cAAc,MAAM,KAAK,eAAe,OAAO;AACrD,QAAO,gBAAgB;AAGvB,KAAI,CAAC,aAAa,eAChB;CAIF,MAAM,cAAc,MAAM,YAAY,iBACpC,YAAY,gBACZ,OACD;AACD,QAAO,gBAAgB;AAEvB,KAAI,CAAC,YACH;CAIF,MAAM,gBAAgB,YAAY,2BAChC,QACA,MACA,YAAY,gBACZ,KAAK,uBAAuB,IAC7B;AAED,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,oCAAoC,OAAO,GAAG,KAAK,IAAI;CAIzE,MAAM,aAAa,cAAc,KAAK,MAAwB,EAAE,UAAU;CAC1E,MAAM,cAAc,MAAM,sBACxB,YACA,aACA,OACD;CAYD,MAAM,OAAO,oBAAoB,aATT,WAAW,KAAK,OAAe;EACrD,MAAM,UAAU,YAAY,IAAI,GAAG;AACnC,MAAI,CAAC,QACH,OAAM,IAAI,MAAM,uCAAuC,KAAK;AAE9D,SAAO;GACP,CAG4D;AAU9D,QAAO;EACL,SARoB,KAAK,IACzB,GAAG,cAAc,KAAK,MAAwB,EAAE,QAAQ,CACzD;EAOC,OANkB,KAAK,IACvB,GAAG,cAAc,KAAK,MAAwB,EAAE,MAAM,CACvD;EAKC;EACD"}
1
+ {"version":3,"file":"AudioSpanUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/AudioSpanUtils.ts"],"sourcesContent":["import type {\n AudioSpan,\n MediaEngine,\n SegmentTimeRange,\n} from \"../../../transcoding/types\";\nimport type { EFMedia } from \"../../EFMedia\";\n\n/**\n * Fetch audio segment data using MediaEngine\n * Pure function with explicit dependencies\n */\nconst fetchAudioSegmentData = async (\n segmentIds: number[],\n mediaEngine: MediaEngine,\n signal: AbortSignal,\n): Promise<Map<number, ArrayBuffer>> => {\n const audioTrack = mediaEngine.tracks.audio;\n if (!audioTrack) {\n throw new Error(\"Audio track not available\");\n }\n\n const segmentData = new Map<number, ArrayBuffer>();\n\n const fetchPromises = segmentIds.map(async (segmentId) => {\n const arrayBuffer = await mediaEngine.transport.fetchMediaSegment(\n segmentId,\n audioTrack,\n signal,\n );\n return [segmentId, arrayBuffer] as [number, ArrayBuffer];\n });\n\n const fetchedSegments = await Promise.all(fetchPromises);\n signal.throwIfAborted();\n\n for (const [segmentId, arrayBuffer] of fetchedSegments) {\n segmentData.set(segmentId, arrayBuffer);\n }\n\n return segmentData;\n};\n\n/**\n * Create audio span blob from init segment and media segments\n * Pure function for blob creation\n */\nconst createAudioSpanBlob = (\n initSegment: ArrayBuffer,\n mediaSegments: ArrayBuffer[],\n): Blob => {\n const chunks = [initSegment, ...mediaSegments];\n return new Blob(chunks, { type: \"audio/mp4\" });\n};\n\n/**\n * Fetch audio spanning a time range\n * Main function that orchestrates segment calculation, fetching, and blob creation\n */\nexport const fetchAudioSpanningTime = async (\n host: EFMedia,\n fromMs: number,\n toMs: number,\n signal: AbortSignal,\n): Promise<AudioSpan | undefined> => {\n // Validate inputs\n if (fromMs >= toMs || fromMs < 0) {\n throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);\n }\n\n // Get media engine using the new async method\n const mediaEngine = await host.getMediaEngine(signal);\n signal.throwIfAborted();\n\n const audioTrack = mediaEngine?.tracks.audio;\n if (!audioTrack) {\n return undefined;\n }\n\n // Fetch the init segment\n const initSegment = await mediaEngine.transport.fetchInitSegment(\n audioTrack,\n signal,\n );\n signal.throwIfAborted();\n\n if (!initSegment) {\n return undefined;\n }\n\n // Calculate segments needed\n const segmentRanges = mediaEngine.index.segmentsInRange(\n fromMs,\n toMs,\n audioTrack,\n );\n\n if (segmentRanges.length === 0) {\n throw new Error(`No segments found for time range ${fromMs}-${toMs}ms`);\n }\n\n // Fetch segment data\n const segmentIds = segmentRanges.map((r: SegmentTimeRange) => r.segmentId);\n const segmentData = await fetchAudioSegmentData(\n segmentIds,\n mediaEngine,\n signal,\n );\n\n // Create ordered array of segments\n const orderedSegments = segmentIds.map((id: number) => {\n const segment = segmentData.get(id);\n if (!segment) {\n throw new Error(`Missing segment data for segment ID ${id}`);\n }\n return segment;\n });\n\n // Create blob\n const blob = createAudioSpanBlob(initSegment, orderedSegments);\n\n // Calculate actual time boundaries\n const actualStartMs = Math.min(\n ...segmentRanges.map((r: SegmentTimeRange) => r.startMs),\n );\n const actualEndMs = Math.max(\n ...segmentRanges.map((r: SegmentTimeRange) => r.endMs),\n );\n\n return {\n startMs: actualStartMs,\n endMs: actualEndMs,\n blob,\n };\n};\n"],"mappings":";;;;;AAWA,MAAM,wBAAwB,OAC5B,YACA,aACA,WACsC;CACtC,MAAM,aAAa,YAAY,OAAO;AACtC,KAAI,CAAC,WACH,OAAM,IAAI,MAAM,4BAA4B;CAG9C,MAAM,8BAAc,IAAI,KAA0B;CAElD,MAAM,gBAAgB,WAAW,IAAI,OAAO,cAAc;AAMxD,SAAO,CAAC,WALY,MAAM,YAAY,UAAU,kBAC9C,WACA,YACA,OACD,CAC8B;GAC/B;CAEF,MAAM,kBAAkB,MAAM,QAAQ,IAAI,cAAc;AACxD,QAAO,gBAAgB;AAEvB,MAAK,MAAM,CAAC,WAAW,gBAAgB,gBACrC,aAAY,IAAI,WAAW,YAAY;AAGzC,QAAO;;;;;;AAOT,MAAM,uBACJ,aACA,kBACS;CACT,MAAM,SAAS,CAAC,aAAa,GAAG,cAAc;AAC9C,QAAO,IAAI,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;;;;;;AAOhD,MAAa,yBAAyB,OACpC,MACA,QACA,MACA,WACmC;AAEnC,KAAI,UAAU,QAAQ,SAAS,EAC7B,OAAM,IAAI,MAAM,8BAA8B,OAAO,SAAS,OAAO;CAIvE,MAAM,cAAc,MAAM,KAAK,eAAe,OAAO;AACrD,QAAO,gBAAgB;CAEvB,MAAM,aAAa,aAAa,OAAO;AACvC,KAAI,CAAC,WACH;CAIF,MAAM,cAAc,MAAM,YAAY,UAAU,iBAC9C,YACA,OACD;AACD,QAAO,gBAAgB;AAEvB,KAAI,CAAC,YACH;CAIF,MAAM,gBAAgB,YAAY,MAAM,gBACtC,QACA,MACA,WACD;AAED,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,oCAAoC,OAAO,GAAG,KAAK,IAAI;CAIzE,MAAM,aAAa,cAAc,KAAK,MAAwB,EAAE,UAAU;CAC1E,MAAM,cAAc,MAAM,sBACxB,YACA,aACA,OACD;CAYD,MAAM,OAAO,oBAAoB,aATT,WAAW,KAAK,OAAe;EACrD,MAAM,UAAU,YAAY,IAAI,GAAG;AACnC,MAAI,CAAC,QACH,OAAM,IAAI,MAAM,uCAAuC,KAAK;AAE9D,SAAO;GACP,CAG4D;AAU9D,QAAO;EACL,SARoB,KAAK,IACzB,GAAG,cAAc,KAAK,MAAwB,EAAE,QAAQ,CACzD;EAOC,OANkB,KAAK,IACvB,GAAG,cAAc,KAAK,MAAwB,EAAE,MAAM,CACvD;EAKC;EACD"}