@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.
- package/dist/EF_FRAMEGEN.js +1 -0
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +2 -2
- package/dist/elements/EFCaptions.js +1 -1
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.js +3 -4
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js +99 -0
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -0
- package/dist/elements/EFMedia/MediaEngine.d.ts +19 -0
- package/dist/elements/EFMedia/MediaEngine.js +129 -0
- package/dist/elements/EFMedia/MediaEngine.js.map +1 -0
- package/dist/elements/EFMedia/SegmentIndex.d.ts +32 -0
- package/dist/elements/EFMedia/SegmentIndex.js +185 -0
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -0
- package/dist/elements/EFMedia/SegmentTransport.d.ts +12 -0
- package/dist/elements/EFMedia/SegmentTransport.js +69 -0
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -0
- package/dist/elements/EFMedia/TimingModel.d.ts +10 -0
- package/dist/elements/EFMedia/TimingModel.js +28 -0
- package/dist/elements/EFMedia/TimingModel.js.map +1 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +7 -6
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +13 -34
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +4 -3
- package/dist/elements/EFMedia.js +14 -31
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFTemporal.js +2 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFTimegroup.js +2 -1
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.js +204 -187
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +0 -7
- package/dist/gui/EFConfiguration.js +0 -5
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +2 -0
- package/dist/gui/EFWorkbench.js +68 -1
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/PlaybackController.d.ts +2 -0
- package/dist/gui/PlaybackController.js +11 -1
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/ef-theme.css +11 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +28 -30
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +1 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +41 -8
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.js +2 -2
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js +19 -19
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +8 -0
- package/dist/preview/QualityUpgradeScheduler.js +13 -1
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.js +3 -3
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderVideoToVideo.js +5 -6
- package/dist/preview/renderVideoToVideo.js.map +1 -1
- package/dist/transcoding/types/index.d.ts +6 -94
- package/dist/transcoding/utils/UrlGenerator.d.ts +3 -12
- package/dist/transcoding/utils/UrlGenerator.js +3 -29
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/package.json +2 -2
- package/test/setup.ts +1 -1
- package/test/useAssetMSW.ts +0 -100
- package/dist/elements/EFMedia/AssetMediaEngine.js +0 -284
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +0 -200
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/FileMediaEngine.js +0 -122
- package/dist/elements/EFMedia/FileMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +0 -157
- package/dist/elements/EFMedia/JitMediaEngine.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"BaseMediaEngine.js","names":["result","tEnd","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/BaseMediaEngine.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport type {\n AudioRendition,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\n\n// Global instances shared across all media engines\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport abstract class BaseMediaEngine {\n protected host: EFMedia;\n\n constructor(host: EFMedia) {\n this.host = host;\n }\n\n // Use protected abstract methods instead of abstract getters to avoid TypeScript bug\n // See: https://github.com/microsoft/TypeScript/issues/58020\n // Note: Abstract getters ALSO trigger this bug, not just getters in object literals\n protected abstract getVideoRenditionInternal(): VideoRendition | undefined;\n protected abstract getAudioRenditionInternal(): AudioRendition | undefined;\n\n /**\n * Get video rendition if available. Returns undefined for audio-only assets.\n * Callers should handle undefined gracefully.\n */\n getVideoRendition(): VideoRendition | undefined {\n return this.getVideoRenditionInternal();\n }\n\n /**\n * Get audio rendition if available. Returns undefined for video-only assets.\n * Callers should handle undefined appropriately.\n */\n getAudioRendition(): AudioRendition | undefined {\n return this.getAudioRenditionInternal();\n }\n\n /**\n * Unified fetch method with caching and global deduplication\n * All requests (media, manifest, init segments) go through this method\n */\n protected async fetchWithCache(\n url: string,\n options: {\n responseType: \"arrayBuffer\" | \"json\";\n headers?: Record<string, string>;\n signal?: AbortSignal;\n },\n ): Promise<any> {\n return withSpan(\n \"mediaEngine.fetchWithCache\",\n {\n url: url.length > 100 ? `${url.substring(0, 100)}...` : url,\n responseType: options.responseType,\n hasHeaders: !!options.headers,\n },\n undefined,\n async (span) => {\n const t0 = performance.now();\n const { responseType, headers, signal } = options;\n\n // Create cache key that includes URL and headers for proper isolation\n // Note: We don't include signal in cache key as it would prevent proper deduplication\n const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;\n\n // Check cache first\n const t1 = performance.now();\n const cached = mediaCache.get(cacheKey);\n const t2 = performance.now();\n span.setAttribute(\"cacheLookupMs\", Math.round((t2 - t1) * 1000) / 1000);\n\n if (cached) {\n span.setAttribute(\"cacheHit\", true);\n // If we have a cached promise, we need to handle the caller's abort signal\n // without affecting the underlying request that other instances might be using\n if (signal) {\n const t3 = performance.now();\n const result = await this.handleAbortForCachedRequest(\n cached,\n signal,\n );\n const t4 = performance.now();\n span.setAttribute(\n \"handleAbortMs\",\n Math.round((t4 - t3) * 100) / 100,\n );\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t4 - t0) * 100) / 100,\n );\n return result;\n }\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t2 - t0) * 100) / 100,\n );\n return cached;\n }\n\n span.setAttribute(\"cacheHit\", false);\n\n // Use global deduplicator to prevent concurrent requests for the same resource\n // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort\n const promise = globalRequestDeduplicator.executeRequest(\n cacheKey,\n async () => {\n const fetchStart = performance.now();\n try {\n const response = await this.host.fetch(url, { headers, signal });\n const fetchEnd = performance.now();\n span.setAttribute(\"fetchMs\", fetchEnd - fetchStart);\n\n // Check headers first (doesn't consume body)\n const contentType = response.headers.get(\"content-type\");\n\n // For JSON responses, check both status and content type before consuming body\n if (responseType === \"json\") {\n // If response is not ok or content type is wrong, clone to read body for error message\n if (\n !response.ok ||\n (contentType &&\n !contentType.includes(\"application/json\") &&\n !contentType.includes(\"text/json\"))\n ) {\n const text = await response.clone().text();\n if (!response.ok) {\n throw new Error(\n `Failed to fetch: ${response.status} ${text.substring(0, 100)}`,\n );\n }\n throw new Error(\n `Expected JSON but got ${contentType}: ${text.substring(0, 100)}`,\n );\n }\n\n // Response is ok and content type is correct, parse as JSON\n try {\n return await response.json();\n } catch (error) {\n // Body already consumed, can't read again for error details\n throw new Error(\n `Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n // For arrayBuffer responses, check status before consuming body\n if (!response.ok) {\n const text = await response.clone().text();\n throw new Error(\n `Failed to fetch: ${response.status} ${text.substring(0, 100)}`,\n );\n }\n\n const buffer = await response.arrayBuffer();\n span.setAttribute(\"sizeBytes\", buffer.byteLength);\n return buffer;\n } catch (error) {\n // If the request was aborted, don't cache the error\n if (\n error instanceof DOMException &&\n error.name === \"AbortError\"\n ) {\n // Remove from cache so other requests can retry\n mediaCache.delete(cacheKey);\n }\n throw error;\n }\n },\n );\n\n // Cache the promise (not the result) to handle concurrent requests\n mediaCache.set(cacheKey, promise);\n\n // Suppress unhandled rejection on the cached promise — errors still propagate\n // to awaiters. Without this, a rejection while the promise sits in cache (with\n // no active awaiter) registers as an unhandled rejection in the browser/runtime.\n promise.catch((error) => {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n // All other errors are intentionally swallowed here; they will be thrown\n // again when the caller awaits fetchWithCache (lines below).\n });\n\n // If the caller has a signal, handle abort logic without affecting the underlying request\n if (signal) {\n const result = await this.handleAbortForCachedRequest(\n promise,\n signal,\n );\n const tEnd = performance.now();\n span.setAttribute(\n \"totalFetchMs\",\n Math.round((tEnd - t0) * 100) / 100,\n );\n return result;\n }\n\n const result = await promise;\n const tEnd = performance.now();\n span.setAttribute(\"totalFetchMs\", Math.round((tEnd - t0) * 100) / 100);\n return result;\n },\n );\n }\n\n /**\n * Handles abort logic for a cached request without affecting the underlying fetch\n * This allows multiple instances to share the same cached request while each\n * manages their own abort behavior\n */\n private handleAbortForCachedRequest<T>(\n promise: Promise<T>,\n signal: AbortSignal,\n ): Promise<T> {\n // If signal is already aborted, reject immediately\n if (signal.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Return a promise that respects the caller's abort signal\n // but doesn't affect the underlying cached request.\n // The abort promise must have .catch(() => {}) to prevent unhandled rejections\n // when the main promise resolves first and the abort fires later during cleanup.\n const abortPromise = new Promise<never>((_, reject) => {\n signal.addEventListener(\"abort\", () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n });\n });\n abortPromise.catch(() => {});\n\n const racePromise = Promise.race([promise, abortPromise]);\n racePromise.catch(() => {});\n return racePromise;\n }\n\n // Public wrapper methods that delegate to fetchWithCache\n async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"arrayBuffer\", signal });\n }\n\n async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"json\", signal });\n }\n\n async fetchMediaWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, {\n responseType: \"arrayBuffer\",\n headers,\n signal,\n });\n }\n\n /**\n * Abstract method for actual segment fetching - implemented by subclasses\n */\n abstract fetchMediaSegment(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract fetchInitSegment(\n rendition: { trackId: number | undefined; src: string },\n signal: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: MediaRendition,\n ): number | undefined;\n\n /**\n * Calculate audio segments needed for a time range\n * Each media engine implements this based on their segment structure\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n durationMs: number,\n ): SegmentTimeRange[] {\n // Default implementation for uniform segments (used by JitMediaEngine)\n if (fromMs >= toMs) {\n return [];\n }\n\n const segments: SegmentTimeRange[] = [];\n\n // Use actual segment durations if available (more accurate)\n if (\n rendition.segmentDurationsMs &&\n rendition.segmentDurationsMs.length > 0\n ) {\n let cumulativeTime = 0;\n\n for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {\n const segmentDuration = rendition.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n continue; // Skip undefined segment durations\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1, // Convert to 1-based\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= durationMs) {\n break;\n }\n }\n\n return segments;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n const segmentDurationMs = rendition.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; // Convert to 1-based\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\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 * Check if a segment is cached for a given rendition\n * Each engine implements its own cache key strategy\n */\n abstract isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean;\n\n /**\n * Extract thumbnail canvases at multiple timestamps efficiently\n * Default implementation provides helpful error information\n */\n async extractThumbnails(\n timestamps: number[],\n _signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const engineName = this.constructor.name;\n console.warn(\n `${engineName}: extractThumbnails not properly implemented. ` +\n \"This MediaEngine type does not support thumbnail generation. \" +\n \"Supported engines: JitMediaEngine. \" +\n `Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? \"\" : \"s\"}.`,\n );\n return timestamps.map(() => null);\n }\n\n abstract convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[];\n}\n"],"mappings":"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"}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { AssetMediaEngine } from "./AssetMediaEngine.js";
|
|
2
|
-
|
|
3
|
-
//#region src/elements/EFMedia/FileMediaEngine.ts
|
|
4
|
-
var FileMediaEngine = class FileMediaEngine extends AssetMediaEngine {
|
|
5
|
-
static async fetchByFileId(host, _urlGenerator, fileId, apiHost, requiredTracks = "both", signal) {
|
|
6
|
-
const url = `${apiHost}/api/v1/files/${fileId}/index`;
|
|
7
|
-
const response = await host.fetch(url, { signal });
|
|
8
|
-
signal?.throwIfAborted();
|
|
9
|
-
const contentType = response.headers.get("content-type");
|
|
10
|
-
if (!response.ok || contentType && !contentType.includes("application/json")) {
|
|
11
|
-
const text = await response.clone().text();
|
|
12
|
-
if (!response.ok) throw new Error(`Failed to fetch asset index: ${response.status} ${text}`);
|
|
13
|
-
throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);
|
|
14
|
-
}
|
|
15
|
-
let data;
|
|
16
|
-
try {
|
|
17
|
-
data = await response.json();
|
|
18
|
-
signal?.throwIfAborted();
|
|
19
|
-
} catch (error) {
|
|
20
|
-
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
21
|
-
throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);
|
|
22
|
-
}
|
|
23
|
-
const engine = new FileMediaEngine(host, fileId, data, apiHost, _urlGenerator);
|
|
24
|
-
signal?.throwIfAborted();
|
|
25
|
-
if (signal) {
|
|
26
|
-
const videoTrack = engine.getVideoTrackIndex();
|
|
27
|
-
const audioTrack = engine.getAudioTrackIndex();
|
|
28
|
-
const needsVideo = requiredTracks === "video" || requiredTracks === "both";
|
|
29
|
-
const needsAudio = requiredTracks === "audio" || requiredTracks === "both";
|
|
30
|
-
if (needsVideo && videoTrack && videoTrack.track !== void 0) try {
|
|
31
|
-
await engine.fetchInitSegment({
|
|
32
|
-
trackId: videoTrack.track,
|
|
33
|
-
src: engine.src
|
|
34
|
-
}, signal);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
37
|
-
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}`);
|
|
38
|
-
}
|
|
39
|
-
signal?.throwIfAborted();
|
|
40
|
-
if (needsAudio && audioTrack && audioTrack.track !== void 0) try {
|
|
41
|
-
await engine.fetchInitSegment({
|
|
42
|
-
trackId: audioTrack.track,
|
|
43
|
-
src: engine.src
|
|
44
|
-
}, signal);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
47
|
-
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}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return engine;
|
|
51
|
-
}
|
|
52
|
-
static {
|
|
53
|
-
this.fetchByAssetId = FileMediaEngine.fetchByFileId;
|
|
54
|
-
}
|
|
55
|
-
constructor(host, fileId, data, apiHost, urlGenerator) {
|
|
56
|
-
super(host, fileId, urlGenerator);
|
|
57
|
-
this.apiHost = apiHost;
|
|
58
|
-
this.fileId = fileId;
|
|
59
|
-
this.data = data;
|
|
60
|
-
this.durationMs = Object.values(this.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0) * 1e3;
|
|
61
|
-
this.templates = {
|
|
62
|
-
initSegment: `${apiHost}/api/v1/files/${fileId}/tracks/{trackId}`,
|
|
63
|
-
mediaSegment: `${apiHost}/api/v1/files/${fileId}/tracks/{trackId}`
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/** @deprecated Use fileId instead */
|
|
67
|
-
get assetId() {
|
|
68
|
-
return this.fileId;
|
|
69
|
-
}
|
|
70
|
-
getInitSegmentPaths() {
|
|
71
|
-
const paths = {};
|
|
72
|
-
const audioTrack = this.getAudioTrackIndex();
|
|
73
|
-
const videoTrack = this.getVideoTrackIndex();
|
|
74
|
-
if (audioTrack !== void 0) paths.audio = {
|
|
75
|
-
path: `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${audioTrack.track}`,
|
|
76
|
-
pos: audioTrack.initSegment.offset,
|
|
77
|
-
size: audioTrack.initSegment.size
|
|
78
|
-
};
|
|
79
|
-
if (videoTrack !== void 0) paths.video = {
|
|
80
|
-
path: `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${videoTrack.track}`,
|
|
81
|
-
pos: videoTrack.initSegment.offset,
|
|
82
|
-
size: videoTrack.initSegment.size
|
|
83
|
-
};
|
|
84
|
-
return paths;
|
|
85
|
-
}
|
|
86
|
-
buildInitSegmentUrl(trackId) {
|
|
87
|
-
return `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${trackId}`;
|
|
88
|
-
}
|
|
89
|
-
buildMediaSegmentUrl(trackId, _segmentId) {
|
|
90
|
-
return `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${trackId}`;
|
|
91
|
-
}
|
|
92
|
-
async fetchInitSegment(rendition, signal) {
|
|
93
|
-
if (!rendition.trackId) throw new Error("[fetchInitSegment] Track ID is required for file-based media");
|
|
94
|
-
const trackData = this.data[rendition.trackId];
|
|
95
|
-
if (!trackData) throw new Error(`Track ${rendition.trackId} not found`);
|
|
96
|
-
const { offset, size } = trackData.initSegment;
|
|
97
|
-
const url = this.buildInitSegmentUrl(rendition.trackId);
|
|
98
|
-
return (await this.fetchMedia(url, signal)).slice(offset, offset + size);
|
|
99
|
-
}
|
|
100
|
-
async fetchMediaSegment(segmentId, rendition, signal) {
|
|
101
|
-
if (!rendition.trackId) throw new Error("[fetchMediaSegment] Track ID is required for file-based media");
|
|
102
|
-
const trackData = this.data[rendition.trackId];
|
|
103
|
-
if (!trackData) throw new Error(`Track ${rendition.trackId} not found`);
|
|
104
|
-
const segment = trackData.segments[segmentId];
|
|
105
|
-
if (!segment) throw new Error(`Segment ${segmentId} not found for track ${rendition.trackId}`);
|
|
106
|
-
const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);
|
|
107
|
-
return (await this.fetchMedia(url, signal)).slice(segment.offset, segment.offset + segment.size);
|
|
108
|
-
}
|
|
109
|
-
convertToSegmentRelativeTimestamps(globalTimestamps, segmentId, rendition) {
|
|
110
|
-
if (!rendition.trackId) throw new Error("[convertToSegmentRelativeTimestamps] Track ID is required for asset metadata");
|
|
111
|
-
const trackData = this.data[rendition.trackId];
|
|
112
|
-
if (!trackData) throw new Error("Track not found");
|
|
113
|
-
const segment = trackData.segments?.[segmentId];
|
|
114
|
-
if (!segment) throw new Error("Segment not found");
|
|
115
|
-
const segmentStartMs = segment.cts / trackData.timescale * 1e3;
|
|
116
|
-
return globalTimestamps.map((globalMs) => (globalMs - segmentStartMs) / 1e3);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
//#endregion
|
|
121
|
-
export { FileMediaEngine };
|
|
122
|
-
//# sourceMappingURL=FileMediaEngine.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"FileMediaEngine.js","names":["data: Record<number, TrackFragmentIndex>","apiHost: string","paths: InitSegmentPaths"],"sources":["../../../src/elements/EFMedia/FileMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type {\n InitSegmentPaths,\n MediaEngine,\n RenditionId,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { AssetMediaEngine } from \"./AssetMediaEngine\";\n\nexport class FileMediaEngine extends AssetMediaEngine implements MediaEngine {\n static async fetchByFileId(\n host: EFMedia,\n _urlGenerator: UrlGenerator,\n fileId: string,\n apiHost: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const url = `${apiHost}/api/v1/files/${fileId}/index`;\n const response = await host.fetch(url, { signal });\n\n signal?.throwIfAborted();\n\n const contentType = response.headers.get(\"content-type\");\n\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 let data: Record<number, TrackFragmentIndex>;\n try {\n data = (await response.json()) as Record<number, TrackFragmentIndex>;\n signal?.throwIfAborted();\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 const engine = new FileMediaEngine(\n host,\n fileId,\n data,\n apiHost,\n _urlGenerator,\n );\n\n signal?.throwIfAborted();\n\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 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 (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 `Video segments require authentication: ${error.message}`,\n );\n }\n }\n }\n\n signal?.throwIfAborted();\n\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 (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 `Audio segments require authentication: ${error.message}`,\n );\n }\n }\n }\n }\n\n return engine;\n }\n\n /** @deprecated Use fetchByFileId instead */\n static fetchByAssetId = FileMediaEngine.fetchByFileId;\n\n public fileId: string;\n\n constructor(\n host: EFMedia,\n fileId: string,\n data: Record<number, TrackFragmentIndex>,\n private apiHost: string,\n urlGenerator: UrlGenerator,\n ) {\n super(host, fileId, urlGenerator);\n this.fileId = fileId;\n this.data = data;\n\n const longestFragment = Object.values(this.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n this.durationMs = longestFragment * 1000;\n\n this.templates = {\n initSegment: `${apiHost}/api/v1/files/${fileId}/tracks/{trackId}`,\n mediaSegment: `${apiHost}/api/v1/files/${fileId}/tracks/{trackId}`,\n };\n }\n\n /** @deprecated Use fileId instead */\n get assetId(): string {\n return this.fileId;\n }\n\n getInitSegmentPaths(): InitSegmentPaths {\n const paths: InitSegmentPaths = {};\n const audioTrack = this.getAudioTrackIndex();\n const videoTrack = this.getVideoTrackIndex();\n\n if (audioTrack !== undefined) {\n paths.audio = {\n path: `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${audioTrack.track}`,\n pos: audioTrack.initSegment.offset,\n size: audioTrack.initSegment.size,\n };\n }\n\n if (videoTrack !== undefined) {\n paths.video = {\n path: `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${videoTrack.track}`,\n pos: videoTrack.initSegment.offset,\n size: videoTrack.initSegment.size,\n };\n }\n\n return paths;\n }\n\n templates!: { initSegment: string; mediaSegment: string };\n\n buildInitSegmentUrl(trackId: number) {\n return `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${trackId}`;\n }\n\n buildMediaSegmentUrl(trackId: number, _segmentId: number) {\n return `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${trackId}`;\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for file-based media\",\n );\n }\n\n const trackData = this.data[rendition.trackId];\n if (!trackData) {\n throw new Error(`Track ${rendition.trackId} not found`);\n }\n\n const { offset, size } = trackData.initSegment;\n const url = this.buildInitSegmentUrl(rendition.trackId);\n const fullTrack = await this.fetchMedia(url, signal);\n return fullTrack.slice(offset, offset + size);\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for file-based media\",\n );\n }\n\n const trackData = this.data[rendition.trackId];\n if (!trackData) {\n throw new Error(`Track ${rendition.trackId} not found`);\n }\n\n const segment = trackData.segments[segmentId];\n if (!segment) {\n throw new Error(\n `Segment ${segmentId} not found for track ${rendition.trackId}`,\n );\n }\n\n const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);\n const fullTrack = await this.fetchMedia(url, signal);\n return fullTrack.slice(segment.offset, segment.offset + segment.size);\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n if (!rendition.trackId) {\n throw new Error(\n \"[convertToSegmentRelativeTimestamps] Track ID is required for asset metadata\",\n );\n }\n const trackData = this.data[rendition.trackId];\n if (!trackData) {\n throw new Error(\"Track not found\");\n }\n const segment = trackData.segments?.[segmentId];\n if (!segment) {\n throw new Error(\"Segment not found\");\n }\n const segmentStartMs = (segment.cts / trackData.timescale) * 1000;\n\n return globalTimestamps.map(\n (globalMs) => (globalMs - segmentStartMs) / 1000,\n );\n }\n}\n\n/** @deprecated Use FileMediaEngine instead */\nexport const AssetIdMediaEngine = FileMediaEngine;\n"],"mappings":"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"}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { ThumbnailExtractor } from "./shared/ThumbnailExtractor.js";
|
|
2
|
-
import { BaseMediaEngine, mediaCache } from "./BaseMediaEngine.js";
|
|
3
|
-
|
|
4
|
-
//#region src/elements/EFMedia/JitMediaEngine.ts
|
|
5
|
-
var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
6
|
-
static async fetch(host, urlGenerator, url, signal) {
|
|
7
|
-
const engine = new JitMediaEngine(host, urlGenerator);
|
|
8
|
-
const data = await engine.fetchManifest(url, signal);
|
|
9
|
-
signal?.throwIfAborted();
|
|
10
|
-
engine.data = data;
|
|
11
|
-
engine.durationMs = data.durationMs;
|
|
12
|
-
engine.src = data.sourceUrl;
|
|
13
|
-
engine.templates = data.endpoints;
|
|
14
|
-
return engine;
|
|
15
|
-
}
|
|
16
|
-
constructor(host, urlGenerator) {
|
|
17
|
-
super(host);
|
|
18
|
-
this.data = {};
|
|
19
|
-
this.durationMs = 0;
|
|
20
|
-
this.src = "";
|
|
21
|
-
this.urlGenerator = urlGenerator;
|
|
22
|
-
this.thumbnailExtractor = new ThumbnailExtractor(this);
|
|
23
|
-
}
|
|
24
|
-
#cachedVideoRendition = null;
|
|
25
|
-
#cachedAudioRendition = null;
|
|
26
|
-
getVideoRenditionInternal() {
|
|
27
|
-
return this.videoRendition;
|
|
28
|
-
}
|
|
29
|
-
getAudioRenditionInternal() {
|
|
30
|
-
return this.audioRendition;
|
|
31
|
-
}
|
|
32
|
-
get audioRendition() {
|
|
33
|
-
if (this.#cachedAudioRendition !== null) return this.#cachedAudioRendition;
|
|
34
|
-
if (!this.data.audioRenditions || this.data.audioRenditions.length === 0) {
|
|
35
|
-
this.#cachedAudioRendition = void 0;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const rendition = this.data.audioRenditions[0];
|
|
39
|
-
if (!rendition) {
|
|
40
|
-
this.#cachedAudioRendition = void 0;
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
this.#cachedAudioRendition = {
|
|
44
|
-
id: rendition.id,
|
|
45
|
-
trackId: void 0,
|
|
46
|
-
src: this.data.sourceUrl,
|
|
47
|
-
segmentDurationMs: rendition.segmentDurationMs,
|
|
48
|
-
segmentDurationsMs: rendition.segmentDurationsMs,
|
|
49
|
-
startTimeOffsetMs: rendition.startTimeOffsetMs
|
|
50
|
-
};
|
|
51
|
-
return this.#cachedAudioRendition;
|
|
52
|
-
}
|
|
53
|
-
get videoRendition() {
|
|
54
|
-
if (this.#cachedVideoRendition !== null) return this.#cachedVideoRendition;
|
|
55
|
-
if (!this.data.videoRenditions || this.data.videoRenditions.length === 0) {
|
|
56
|
-
this.#cachedVideoRendition = void 0;
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const rendition = this.data.videoRenditions[0];
|
|
60
|
-
if (!rendition) {
|
|
61
|
-
this.#cachedVideoRendition = void 0;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
this.#cachedVideoRendition = {
|
|
65
|
-
id: rendition.id,
|
|
66
|
-
trackId: void 0,
|
|
67
|
-
src: this.data.sourceUrl,
|
|
68
|
-
segmentDurationMs: rendition.segmentDurationMs,
|
|
69
|
-
segmentDurationsMs: rendition.segmentDurationsMs,
|
|
70
|
-
startTimeOffsetMs: rendition.startTimeOffsetMs
|
|
71
|
-
};
|
|
72
|
-
return this.#cachedVideoRendition;
|
|
73
|
-
}
|
|
74
|
-
async fetchInitSegment(rendition, signal) {
|
|
75
|
-
if (!rendition.id) throw new Error("Rendition ID is required for JIT metadata");
|
|
76
|
-
const url = this.urlGenerator.generateSegmentUrl("init", rendition.id, this);
|
|
77
|
-
return this.fetchMedia(url, signal);
|
|
78
|
-
}
|
|
79
|
-
async fetchMediaSegment(segmentId, rendition, signal) {
|
|
80
|
-
if (!rendition.id) throw new Error("Rendition ID is required for JIT metadata");
|
|
81
|
-
const url = this.urlGenerator.generateSegmentUrl(segmentId, rendition.id, this);
|
|
82
|
-
return this.fetchMedia(url, signal);
|
|
83
|
-
}
|
|
84
|
-
computeSegmentId(desiredSeekTimeMs, rendition) {
|
|
85
|
-
if (desiredSeekTimeMs > this.durationMs) return;
|
|
86
|
-
if (rendition.segmentDurationsMs && rendition.segmentDurationsMs.length > 0) {
|
|
87
|
-
let cumulativeTime = 0;
|
|
88
|
-
for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {
|
|
89
|
-
const segmentDuration = rendition.segmentDurationsMs[i];
|
|
90
|
-
if (segmentDuration === void 0) throw new Error("Segment duration is required for JIT metadata");
|
|
91
|
-
const segmentStartMs = cumulativeTime;
|
|
92
|
-
const segmentEndMs = cumulativeTime + segmentDuration;
|
|
93
|
-
const includesEndTime = i === rendition.segmentDurationsMs.length - 1 && desiredSeekTimeMs === this.durationMs;
|
|
94
|
-
if (desiredSeekTimeMs >= segmentStartMs && (desiredSeekTimeMs < segmentEndMs || includesEndTime)) return i + 1;
|
|
95
|
-
cumulativeTime += segmentDuration;
|
|
96
|
-
if (cumulativeTime >= this.durationMs) break;
|
|
97
|
-
}
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (!rendition.segmentDurationMs) throw new Error("Segment duration is required for JIT metadata");
|
|
101
|
-
const segmentIndex = Math.floor(desiredSeekTimeMs / rendition.segmentDurationMs);
|
|
102
|
-
if (segmentIndex * rendition.segmentDurationMs >= this.durationMs) return;
|
|
103
|
-
return segmentIndex + 1;
|
|
104
|
-
}
|
|
105
|
-
getBufferConfig() {
|
|
106
|
-
return {
|
|
107
|
-
videoBufferDurationMs: 4e3,
|
|
108
|
-
audioBufferDurationMs: 4e3,
|
|
109
|
-
maxVideoBufferFetches: 2,
|
|
110
|
-
maxAudioBufferFetches: 2,
|
|
111
|
-
bufferThresholdMs: 3e4
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
getScrubVideoRendition() {
|
|
115
|
-
if (!this.data.videoRenditions) return void 0;
|
|
116
|
-
const scrubManifestRendition = this.data.videoRenditions.find((r) => r.id === "scrub");
|
|
117
|
-
if (!scrubManifestRendition) return this.getVideoRenditionInternal();
|
|
118
|
-
return {
|
|
119
|
-
id: scrubManifestRendition.id,
|
|
120
|
-
trackId: void 0,
|
|
121
|
-
src: this.src,
|
|
122
|
-
segmentDurationMs: scrubManifestRendition.segmentDurationMs,
|
|
123
|
-
segmentDurationsMs: scrubManifestRendition.segmentDurationsMs
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
isSegmentCached(segmentId, rendition) {
|
|
127
|
-
if (!rendition.id) return false;
|
|
128
|
-
const segmentUrl = this.urlGenerator.generateSegmentUrl(segmentId, rendition.id, this);
|
|
129
|
-
return mediaCache.has(segmentUrl);
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Extract thumbnail canvases using same rendition priority as video playback for frame alignment
|
|
133
|
-
*/
|
|
134
|
-
async extractThumbnails(timestamps, signal) {
|
|
135
|
-
let rendition;
|
|
136
|
-
try {
|
|
137
|
-
const mainRendition = this.getVideoRenditionInternal();
|
|
138
|
-
if (mainRendition) rendition = mainRendition;
|
|
139
|
-
else {
|
|
140
|
-
const scrubRendition = this.getScrubVideoRendition();
|
|
141
|
-
if (scrubRendition) rendition = scrubRendition;
|
|
142
|
-
else throw new Error("No video rendition available");
|
|
143
|
-
}
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.warn("JitMediaEngine: No video rendition available for thumbnails", error);
|
|
146
|
-
return timestamps.map(() => null);
|
|
147
|
-
}
|
|
148
|
-
return this.thumbnailExtractor.extractThumbnails(timestamps, rendition, this.durationMs, signal);
|
|
149
|
-
}
|
|
150
|
-
convertToSegmentRelativeTimestamps(globalTimestamps, _segmentId, _rendition) {
|
|
151
|
-
return globalTimestamps.map((timestamp) => timestamp / 1e3);
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
//#endregion
|
|
156
|
-
export { JitMediaEngine };
|
|
157
|
-
//# sourceMappingURL=JitMediaEngine.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"JitMediaEngine.js","names":["#cachedAudioRendition","#cachedVideoRendition","rendition: VideoRendition"],"sources":["../../../src/elements/EFMedia/JitMediaEngine.ts"],"sourcesContent":["import type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { ManifestResponse } from \"../../transcoding/types/index.js\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport { BaseMediaEngine, mediaCache } from \"./BaseMediaEngine\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class JitMediaEngine extends BaseMediaEngine implements MediaEngine {\n private urlGenerator: UrlGenerator;\n private data: ManifestResponse = {} as ManifestResponse;\n private thumbnailExtractor: ThumbnailExtractor;\n\n static async fetch(\n host: EFMedia,\n urlGenerator: UrlGenerator,\n url: string,\n signal?: AbortSignal,\n ) {\n const engine = new JitMediaEngine(host, urlGenerator);\n const data = await engine.fetchManifest(url, signal);\n\n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n\n engine.data = data;\n // Set MediaEngine interface properties\n engine.durationMs = data.durationMs;\n engine.src = data.sourceUrl;\n engine.templates = data.endpoints;\n return engine;\n }\n\n // MediaEngine interface properties\n durationMs = 0;\n src = \"\";\n templates!: { initSegment: string; mediaSegment: string };\n\n constructor(host: EFMedia, urlGenerator: UrlGenerator) {\n super(host);\n this.urlGenerator = urlGenerator;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\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 // Implement abstract methods required by BaseMediaEngine\n protected getVideoRenditionInternal(): VideoRendition | undefined {\n return this.videoRendition;\n }\n\n protected getAudioRenditionInternal(): AudioRendition | undefined {\n return this.audioRendition;\n }\n\n get audioRendition(): AudioRendition | undefined {\n if (this.#cachedAudioRendition !== null) {\n return this.#cachedAudioRendition;\n }\n if (!this.data.audioRenditions || this.data.audioRenditions.length === 0) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n const rendition = this.data.audioRenditions[0];\n if (!rendition) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n this.#cachedAudioRendition = {\n id: rendition.id as RenditionId,\n trackId: undefined,\n src: this.data.sourceUrl,\n segmentDurationMs: rendition.segmentDurationMs,\n segmentDurationsMs: rendition.segmentDurationsMs,\n startTimeOffsetMs: rendition.startTimeOffsetMs,\n };\n return this.#cachedAudioRendition;\n }\n\n get videoRendition(): VideoRendition | undefined {\n if (this.#cachedVideoRendition !== null) {\n return this.#cachedVideoRendition;\n }\n if (!this.data.videoRenditions || this.data.videoRenditions.length === 0) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n const rendition = this.data.videoRenditions[0];\n if (!rendition) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n this.#cachedVideoRendition = {\n id: rendition.id as RenditionId,\n trackId: undefined,\n src: this.data.sourceUrl,\n segmentDurationMs: rendition.segmentDurationMs,\n segmentDurationsMs: rendition.segmentDurationsMs,\n startTimeOffsetMs: rendition.startTimeOffsetMs,\n };\n return this.#cachedVideoRendition;\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n if (!rendition.id) {\n throw new Error(\"Rendition ID is required for JIT metadata\");\n }\n const url = this.urlGenerator.generateSegmentUrl(\n \"init\",\n rendition.id,\n this,\n );\n\n // Use unified fetch method\n return this.fetchMedia(url, signal);\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n if (!rendition.id) {\n throw new Error(\"Rendition ID is required for JIT metadata\");\n }\n const url = this.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n this,\n );\n return this.fetchMedia(url, signal);\n }\n\n computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: VideoRendition | AudioRendition,\n ) {\n // Don't request segments beyond the actual file duration\n // Note: seeking to exactly durationMs should be allowed (it's the last moment of the file)\n if (desiredSeekTimeMs > this.durationMs) {\n return undefined;\n }\n\n // Use actual segment durations if available (more accurate)\n if (\n rendition.segmentDurationsMs &&\n rendition.segmentDurationsMs.length > 0\n ) {\n let cumulativeTime = 0;\n\n for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {\n const segmentDuration = rendition.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 // Check if the desired seek time falls within this segment\n // Special case: for the last segment, include the exact end time\n const isLastSegment = i === rendition.segmentDurationsMs.length - 1;\n const includesEndTime =\n isLastSegment && desiredSeekTimeMs === this.durationMs;\n\n if (\n desiredSeekTimeMs >= segmentStartMs &&\n (desiredSeekTimeMs < segmentEndMs || includesEndTime)\n ) {\n return i + 1; // Convert 0-based to 1-based segment ID\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= this.durationMs) {\n break;\n }\n }\n\n // If we didn't find a segment, return undefined\n return undefined;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n if (!rendition.segmentDurationMs) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n\n const segmentIndex = Math.floor(\n desiredSeekTimeMs / rendition.segmentDurationMs,\n );\n\n // Calculate the actual segment start time\n const segmentStartMs = segmentIndex * rendition.segmentDurationMs;\n\n // If this segment would start at or beyond file duration, it doesn't exist\n if (segmentStartMs >= this.durationMs) {\n return undefined;\n }\n\n return segmentIndex + 1; // Convert 0-based to 1-based\n }\n\n getBufferConfig() {\n return {\n videoBufferDurationMs: 4000,\n audioBufferDurationMs: 4000,\n maxVideoBufferFetches: 2,\n maxAudioBufferFetches: 2,\n bufferThresholdMs: 30000,\n };\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n if (!this.data.videoRenditions) return undefined;\n\n const scrubManifestRendition = this.data.videoRenditions.find(\n (r) => r.id === \"scrub\",\n );\n\n if (!scrubManifestRendition) return this.getVideoRenditionInternal(); // Fallback to main\n\n return {\n id: scrubManifestRendition.id as any,\n trackId: undefined,\n src: this.src,\n segmentDurationMs: scrubManifestRendition.segmentDurationMs,\n segmentDurationsMs: scrubManifestRendition.segmentDurationsMs,\n };\n }\n\n isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n if (!rendition.id) {\n return false;\n }\n\n const segmentUrl = this.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n this,\n );\n return mediaCache.has(segmentUrl);\n }\n\n /**\n * Extract thumbnail canvases using same rendition priority as video playback for frame alignment\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use same rendition priority as video: try main rendition first for frame alignment\n let rendition: VideoRendition;\n try {\n const mainRendition = this.getVideoRenditionInternal();\n if (mainRendition) {\n rendition = mainRendition;\n } else {\n const scrubRendition = this.getScrubVideoRendition();\n if (scrubRendition) {\n rendition = scrubRendition;\n } else {\n throw new Error(\"No video rendition available\");\n }\n }\n } catch (error) {\n console.warn(\n \"JitMediaEngine: No video rendition available for thumbnails\",\n error,\n );\n return timestamps.map(() => null);\n }\n\n // Use shared thumbnail extraction logic\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 return globalTimestamps.map((timestamp) => timestamp / 1000);\n }\n}\n"],"mappings":"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"}
|