@editframe/elements 0.18.22-beta.0 → 0.18.26-beta.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 (99) hide show
  1. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +2 -1
  2. package/dist/elements/EFMedia/AssetMediaEngine.js +3 -0
  3. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +9 -0
  4. package/dist/elements/EFMedia/BaseMediaEngine.js +31 -0
  5. package/dist/elements/EFMedia/JitMediaEngine.d.ts +1 -0
  6. package/dist/elements/EFMedia/JitMediaEngine.js +12 -0
  7. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +11 -5
  8. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +19 -18
  9. package/dist/elements/EFMedia/shared/BufferUtils.js +24 -44
  10. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +8 -0
  11. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +5 -5
  12. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.d.ts +25 -0
  13. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +42 -0
  14. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.d.ts +8 -0
  15. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +70 -0
  16. package/dist/elements/EFMedia/videoTasks/{makeVideoInitSegmentFetchTask.d.ts → makeScrubVideoInitSegmentFetchTask.d.ts} +1 -1
  17. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +21 -0
  18. package/dist/elements/EFMedia/videoTasks/{makeVideoInputTask.d.ts → makeScrubVideoInputTask.d.ts} +1 -1
  19. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +27 -0
  20. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.d.ts +6 -0
  21. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +52 -0
  22. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.d.ts +4 -0
  23. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +23 -0
  24. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.d.ts +4 -0
  25. package/dist/elements/EFMedia/videoTasks/{makeVideoSegmentIdTask.js → makeScrubVideoSegmentIdTask.js} +9 -4
  26. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.d.ts +6 -0
  27. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +112 -0
  28. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +11 -5
  29. package/dist/elements/EFMedia.d.ts +0 -10
  30. package/dist/elements/EFMedia.js +1 -17
  31. package/dist/elements/EFVideo.d.ts +11 -9
  32. package/dist/elements/EFVideo.js +31 -23
  33. package/dist/gui/EFConfiguration.d.ts +1 -0
  34. package/dist/gui/EFConfiguration.js +5 -0
  35. package/dist/gui/EFFilmstrip.d.ts +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/transcoding/types/index.d.ts +11 -0
  38. package/package.json +2 -2
  39. package/src/elements/EFCaptions.ts +1 -1
  40. package/src/elements/EFImage.ts +1 -1
  41. package/src/elements/EFMedia/AssetMediaEngine.ts +6 -0
  42. package/src/elements/EFMedia/BaseMediaEngine.ts +60 -0
  43. package/src/elements/EFMedia/JitMediaEngine.ts +18 -0
  44. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +185 -59
  45. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +19 -6
  46. package/src/elements/EFMedia/shared/BufferUtils.ts +71 -85
  47. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +151 -112
  48. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +12 -5
  49. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +61 -0
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +113 -0
  51. package/src/elements/EFMedia/videoTasks/{makeVideoInitSegmentFetchTask.ts → makeScrubVideoInitSegmentFetchTask.ts} +15 -3
  52. package/src/elements/EFMedia/videoTasks/{makeVideoInputTask.ts → makeScrubVideoInputTask.ts} +11 -10
  53. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +118 -0
  54. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +44 -0
  55. package/src/elements/EFMedia/videoTasks/{makeVideoSegmentIdTask.ts → makeScrubVideoSegmentIdTask.ts} +14 -6
  56. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +258 -0
  57. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +19 -5
  58. package/src/elements/EFMedia.browsertest.ts +74 -11
  59. package/src/elements/EFMedia.ts +1 -23
  60. package/src/elements/EFVideo.browsertest.ts +204 -80
  61. package/src/elements/EFVideo.ts +38 -26
  62. package/src/elements/TargetController.browsertest.ts +1 -1
  63. package/src/gui/EFConfiguration.ts +4 -1
  64. package/src/gui/EFFilmstrip.ts +4 -4
  65. package/src/gui/EFFocusOverlay.ts +1 -1
  66. package/src/gui/EFPreview.ts +3 -4
  67. package/src/gui/EFScrubber.ts +1 -1
  68. package/src/gui/EFTimeDisplay.ts +1 -1
  69. package/src/gui/EFToggleLoop.ts +1 -1
  70. package/src/gui/EFTogglePlay.ts +1 -1
  71. package/src/gui/EFWorkbench.ts +1 -1
  72. package/src/transcoding/types/index.ts +16 -0
  73. package/test/__cache__/GET__api_v1_transcode_scrub_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__6ff5127ebeda578a679474347fbd6137/data.bin +0 -0
  74. package/test/__cache__/GET__api_v1_transcode_scrub_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__6ff5127ebeda578a679474347fbd6137/metadata.json +16 -0
  75. package/test/__cache__/GET__api_v1_transcode_scrub_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__f6d4793fc9ff854ee9a738917fb64a53/data.bin +0 -0
  76. package/test/__cache__/GET__api_v1_transcode_scrub_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__f6d4793fc9ff854ee9a738917fb64a53/metadata.json +16 -0
  77. package/test/cache-integration-verification.browsertest.ts +84 -0
  78. package/types.json +1 -1
  79. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +0 -1
  80. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +0 -9
  81. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +0 -9
  82. package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +0 -16
  83. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +0 -9
  84. package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +0 -27
  85. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +0 -7
  86. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +0 -34
  87. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +0 -9
  88. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +0 -4
  89. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +0 -28
  90. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +0 -9
  91. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +0 -4
  92. package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +0 -233
  93. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +0 -555
  94. package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +0 -59
  95. package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +0 -55
  96. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +0 -65
  97. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +0 -57
  98. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +0 -43
  99. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +0 -56
@@ -1,5 +1,5 @@
1
1
  import { TrackFragmentIndex } from '../../../../assets/src/index.ts';
2
- import { AudioRendition, InitSegmentPaths, MediaEngine, SegmentTimeRange } from '../../transcoding/types';
2
+ import { AudioRendition, InitSegmentPaths, MediaEngine, SegmentTimeRange, VideoRendition } from '../../transcoding/types';
3
3
  import { UrlGenerator } from '../../transcoding/utils/UrlGenerator';
4
4
  import { EFMedia } from '../EFMedia';
5
5
  import { BaseMediaEngine } from './BaseMediaEngine';
@@ -41,4 +41,5 @@ export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEn
41
41
  */
42
42
  calculateAudioSegmentRange(fromMs: number, toMs: number, rendition: AudioRendition, _durationMs: number): SegmentTimeRange[];
43
43
  computeSegmentId(seekTimeMs: number, rendition: MediaRendition): number;
44
+ getScrubVideoRendition(): VideoRendition | undefined;
44
45
  }
@@ -142,5 +142,8 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
142
142
  }
143
143
  return nearestSegmentIndex;
144
144
  }
145
+ getScrubVideoRendition() {
146
+ return this.videoRendition;
147
+ }
145
148
  };
146
149
  export { AssetMediaEngine };
@@ -71,4 +71,13 @@ export declare abstract class BaseMediaEngine {
71
71
  * Each media engine implements this based on their segment structure
72
72
  */
73
73
  calculateAudioSegmentRange(fromMs: number, toMs: number, rendition: AudioRendition, durationMs: number): SegmentTimeRange[];
74
+ /**
75
+ * Check if a segment is cached for a given rendition
76
+ * This needs to check the URL-based cache since that's where segments are actually stored
77
+ */
78
+ isSegmentCached(segmentId: number, rendition: AudioRendition | VideoRendition): boolean;
79
+ /**
80
+ * Get cached segment IDs from a list for a given rendition
81
+ */
82
+ getCachedSegments(segmentIds: number[], rendition: AudioRendition | VideoRendition): Set<number>;
74
83
  }
@@ -163,5 +163,36 @@ var BaseMediaEngine = class {
163
163
  }
164
164
  return segments;
165
165
  }
166
+ /**
167
+ * Check if a segment is cached for a given rendition
168
+ * This needs to check the URL-based cache since that's where segments are actually stored
169
+ */
170
+ isSegmentCached(segmentId, rendition) {
171
+ try {
172
+ const maybeJitEngine = this;
173
+ if (maybeJitEngine.urlGenerator && typeof maybeJitEngine.urlGenerator.generateSegmentUrl === "function") {
174
+ if (!rendition.id) {
175
+ console.log(`🎬 BaseMediaEngine: No rendition ID for segment ${segmentId}`);
176
+ return false;
177
+ }
178
+ const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(segmentId, rendition.id, maybeJitEngine);
179
+ const urlIsCached = mediaCache.has(segmentUrl);
180
+ return urlIsCached;
181
+ }
182
+ const cacheKey = `${rendition.src}-${rendition.id || "default"}-${segmentId}-${rendition.trackId}`;
183
+ const isCached = mediaCache.has(cacheKey);
184
+ console.log(`🎬 BaseMediaEngine: Non-JIT engine, using segment key: "${cacheKey}", result: ${isCached}`);
185
+ return isCached;
186
+ } catch (error) {
187
+ console.warn(`🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`, error);
188
+ return false;
189
+ }
190
+ }
191
+ /**
192
+ * Get cached segment IDs from a list for a given rendition
193
+ */
194
+ getCachedSegments(segmentIds, rendition) {
195
+ return new Set(segmentIds.filter((id) => this.isSegmentCached(id, rendition)));
196
+ }
166
197
  };
167
198
  export { BaseMediaEngine };
@@ -26,4 +26,5 @@ export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngi
26
26
  src: string;
27
27
  }): Promise<ArrayBuffer>;
28
28
  computeSegmentId(desiredSeekTimeMs: number, rendition: VideoRendition | AudioRendition): number | undefined;
29
+ getScrubVideoRendition(): VideoRendition | undefined;
29
30
  }
@@ -77,5 +77,17 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
77
77
  if (segmentStartMs >= this.durationMs) return void 0;
78
78
  return segmentIndex + 1;
79
79
  }
80
+ getScrubVideoRendition() {
81
+ if (!this.data.videoRenditions) return void 0;
82
+ const scrubManifestRendition = this.data.videoRenditions.find((r) => r.id === "scrub");
83
+ if (!scrubManifestRendition) return this.videoRendition;
84
+ return {
85
+ id: scrubManifestRendition.id,
86
+ trackId: void 0,
87
+ src: this.src,
88
+ segmentDurationMs: scrubManifestRendition.segmentDurationMs,
89
+ segmentDurationsMs: scrubManifestRendition.segmentDurationsMs
90
+ };
91
+ }
80
92
  };
81
93
  export { JitMediaEngine };
@@ -6,12 +6,12 @@ import { Task } from "@lit/task";
6
6
  const makeAudioBufferTask = (host) => {
7
7
  let currentState = {
8
8
  currentSeekTimeMs: 0,
9
+ requestedSegments: /* @__PURE__ */ new Set(),
9
10
  activeRequests: /* @__PURE__ */ new Set(),
10
- cachedSegments: /* @__PURE__ */ new Set(),
11
11
  requestQueue: []
12
12
  };
13
13
  return new Task(host, {
14
- autoRun: EF_INTERACTIVE,
14
+ autoRun: EF_INTERACTIVE && !EF_RENDERING(),
15
15
  args: () => [host.desiredSeekTimeMs],
16
16
  onError: (error) => {
17
17
  console.error("audioBufferTask error", error);
@@ -20,19 +20,25 @@ const makeAudioBufferTask = (host) => {
20
20
  currentState = value;
21
21
  },
22
22
  task: async ([seekTimeMs], { signal }) => {
23
+ if (EF_RENDERING()) return currentState;
23
24
  const currentConfig = {
24
25
  bufferDurationMs: host.audioBufferDurationMs,
25
26
  maxParallelFetches: host.maxAudioBufferFetches,
26
- enableBuffering: host.enableAudioBuffering && !EF_RENDERING
27
+ enableBuffering: host.enableAudioBuffering
27
28
  };
28
29
  return manageMediaBuffer(seekTimeMs, currentConfig, currentState, host.intrinsicDurationMs || 1e4, signal, {
29
30
  computeSegmentId: async (timeMs, rendition) => {
30
31
  const mediaEngine = await getLatestMediaEngine(host, signal);
31
32
  return mediaEngine.computeSegmentId(timeMs, rendition);
32
33
  },
33
- fetchSegment: async (segmentId, rendition) => {
34
+ prefetchSegment: async (segmentId, rendition) => {
34
35
  const mediaEngine = await getLatestMediaEngine(host, signal);
35
- return mediaEngine.fetchMediaSegment(segmentId, rendition);
36
+ await mediaEngine.fetchMediaSegment(segmentId, rendition);
37
+ },
38
+ isSegmentCached: (segmentId, rendition) => {
39
+ const mediaEngine = host.mediaEngineTask.value;
40
+ if (!mediaEngine) return false;
41
+ return mediaEngine.isSegmentCached(segmentId, rendition);
36
42
  },
37
43
  getRendition: async () => {
38
44
  const mediaEngine = await getLatestMediaEngine(host, signal);
@@ -1,11 +1,11 @@
1
1
  import { AudioRendition, VideoRendition } from '../../../transcoding/types';
2
2
  /**
3
- * State interface for media buffering - generic for both audio and video
3
+ * State interface for media buffering - orchestration only, no data storage
4
4
  */
5
5
  export interface MediaBufferState {
6
6
  currentSeekTimeMs: number;
7
+ requestedSegments: Set<number>;
7
8
  activeRequests: Set<number>;
8
- cachedSegments: Set<number>;
9
9
  requestQueue: number[];
10
10
  }
11
11
  /**
@@ -18,11 +18,12 @@ export interface MediaBufferConfig {
18
18
  enableContinuousBuffering?: boolean;
19
19
  }
20
20
  /**
21
- * Dependencies interface for media buffering - generic for both audio and video
21
+ * Dependencies interface for media buffering - integrates with BaseMediaEngine
22
22
  */
23
23
  export interface MediaBufferDependencies<T extends AudioRendition | VideoRendition> {
24
24
  computeSegmentId: (timeMs: number, rendition: T) => Promise<number | undefined>;
25
- fetchSegment: (segmentId: number, rendition: T) => Promise<ArrayBuffer>;
25
+ prefetchSegment: (segmentId: number, rendition: T) => Promise<void>;
26
+ isSegmentCached: (segmentId: number, rendition: T) => boolean;
26
27
  getRendition: () => Promise<T>;
27
28
  logError: (message: string, error: any) => void;
28
29
  }
@@ -36,10 +37,10 @@ export declare const computeSegmentRange: <T extends AudioRendition | VideoRendi
36
37
  */
37
38
  export declare const computeSegmentRangeAsync: <T extends AudioRendition | VideoRendition>(startTimeMs: number, endTimeMs: number, durationMs: number, rendition: T, computeSegmentId: (timeMs: number, rendition: T) => Promise<number | undefined>) => Promise<number[]>;
38
39
  /**
39
- * Compute buffer queue based on current state and desired segments
40
- * Pure function - determines what segments should be fetched
40
+ * Compute buffer queue based on desired segments and what we've already requested
41
+ * Pure function - determines what new segments should be prefetched
41
42
  */
42
- export declare const computeBufferQueue: (desiredSegments: number[], activeRequests: Set<number>, cachedSegments: Set<number>) => number[];
43
+ export declare const computeBufferQueue: (desiredSegments: number[], requestedSegments: Set<number>) => number[];
43
44
  /**
44
45
  * Handle seek time change and recompute buffer queue
45
46
  * Pure function - computes new queue when seek time changes
@@ -49,22 +50,22 @@ export declare const handleSeekTimeChange: <T extends AudioRendition | VideoRend
49
50
  overlappingRequests: number[];
50
51
  };
51
52
  /**
52
- * Check if a specific segment is cached in the buffer
53
- * Pure function for accessing buffer cache state
53
+ * Check if a segment has been requested for buffering
54
+ * Pure function for checking buffer orchestration state
54
55
  */
55
- export declare const getCachedSegment: (segmentId: number, bufferState: MediaBufferState | undefined) => boolean;
56
+ export declare const isSegmentRequested: (segmentId: number, bufferState: MediaBufferState | undefined) => boolean;
56
57
  /**
57
- * Get cached segments from a list of segment IDs
58
- * Pure function that returns which segments are available in cache
58
+ * Get requested segments from a list of segment IDs
59
+ * Pure function that returns which segments have been requested for buffering
59
60
  */
60
- export declare const getCachedSegments: (segmentIds: number[], bufferState: MediaBufferState | undefined) => Set<number>;
61
+ export declare const getRequestedSegments: (segmentIds: number[], bufferState: MediaBufferState | undefined) => Set<number>;
61
62
  /**
62
- * Get missing segments from a list of segment IDs
63
- * Pure function that returns which segments need to be fetched
63
+ * Get unrequested segments from a list of segment IDs
64
+ * Pure function that returns which segments haven't been requested yet
64
65
  */
65
- export declare const getMissingSegments: (segmentIds: number[], bufferState: MediaBufferState | undefined) => number[];
66
+ export declare const getUnrequestedSegments: (segmentIds: number[], bufferState: MediaBufferState | undefined) => number[];
66
67
  /**
67
- * Core media buffering logic with explicit dependencies
68
- * Generic implementation that works for both audio and video
68
+ * Core media buffering orchestration logic - prefetch only, no data storage
69
+ * Integrates with BaseMediaEngine's existing caching and request deduplication
69
70
  */
70
71
  export declare const manageMediaBuffer: <T extends AudioRendition | VideoRendition>(seekTimeMs: number, config: MediaBufferConfig, currentState: MediaBufferState, durationMs: number, signal: AbortSignal, deps: MediaBufferDependencies<T>) => Promise<MediaBufferState>;
@@ -16,74 +16,54 @@ const computeSegmentRangeAsync = async (startTimeMs, endTimeMs, durationMs, rend
16
16
  return segments.filter((id, index, arr) => arr.indexOf(id) === index);
17
17
  };
18
18
  /**
19
- * Compute buffer queue based on current state and desired segments
20
- * Pure function - determines what segments should be fetched
19
+ * Compute buffer queue based on desired segments and what we've already requested
20
+ * Pure function - determines what new segments should be prefetched
21
21
  */
22
- const computeBufferQueue = (desiredSegments, activeRequests, cachedSegments) => {
23
- return desiredSegments.filter((segmentId) => !activeRequests.has(segmentId) && !cachedSegments.has(segmentId));
22
+ const computeBufferQueue = (desiredSegments, requestedSegments) => {
23
+ return desiredSegments.filter((segmentId) => !requestedSegments.has(segmentId));
24
24
  };
25
25
  /**
26
- * Core media buffering logic with explicit dependencies
27
- * Generic implementation that works for both audio and video
26
+ * Core media buffering orchestration logic - prefetch only, no data storage
27
+ * Integrates with BaseMediaEngine's existing caching and request deduplication
28
28
  */
29
29
  const manageMediaBuffer = async (seekTimeMs, config, currentState, durationMs, signal, deps) => {
30
30
  if (!config.enableBuffering) return currentState;
31
31
  const rendition = await deps.getRendition();
32
32
  const endTimeMs = seekTimeMs + config.bufferDurationMs;
33
33
  const desiredSegments = await computeSegmentRangeAsync(seekTimeMs, endTimeMs, durationMs, rendition, deps.computeSegmentId);
34
- const newQueue = computeBufferQueue(desiredSegments, currentState.activeRequests, currentState.cachedSegments);
35
- const segmentsToFetch = newQueue.slice(0, config.maxParallelFetches);
34
+ const uncachedSegments = desiredSegments.filter((segmentId) => !deps.isSegmentCached(segmentId, rendition));
35
+ const newQueue = computeBufferQueue(uncachedSegments, currentState.requestedSegments);
36
+ const newRequestedSegments = new Set(currentState.requestedSegments);
36
37
  const newActiveRequests = new Set(currentState.activeRequests);
37
- const newCachedSegments = new Set(currentState.cachedSegments);
38
- const startNextSegment = (remainingQueue) => {
39
- if (remainingQueue.length === 0 || signal.aborted) return;
40
- const availableSlots = config.maxParallelFetches - newActiveRequests.size;
41
- if (availableSlots <= 0) return;
42
- const nextSegmentId = remainingQueue[0];
38
+ const remainingQueue = [...newQueue];
39
+ const startNextSegment = () => {
40
+ if (newActiveRequests.size >= config.maxParallelFetches || remainingQueue.length === 0 || signal.aborted) return;
41
+ const nextSegmentId = remainingQueue.shift();
43
42
  if (nextSegmentId === void 0) return;
44
- if (newActiveRequests.has(nextSegmentId) || newCachedSegments.has(nextSegmentId)) {
45
- startNextSegment(remainingQueue.slice(1));
43
+ if (newRequestedSegments.has(nextSegmentId) || deps.isSegmentCached(nextSegmentId, rendition)) {
44
+ startNextSegment();
46
45
  return;
47
46
  }
47
+ newRequestedSegments.add(nextSegmentId);
48
48
  newActiveRequests.add(nextSegmentId);
49
- deps.fetchSegment(nextSegmentId, rendition).then(() => {
49
+ deps.prefetchSegment(nextSegmentId, rendition).then(() => {
50
50
  if (signal.aborted) return;
51
51
  newActiveRequests.delete(nextSegmentId);
52
- newCachedSegments.add(nextSegmentId);
53
- startNextSegment(remainingQueue.slice(1));
52
+ if (config.enableContinuousBuffering ?? true) startNextSegment();
54
53
  }).catch((error) => {
55
54
  if (signal.aborted) return;
56
55
  newActiveRequests.delete(nextSegmentId);
57
- deps.logError(`Failed to fetch segment ${nextSegmentId}`, error);
58
- startNextSegment(remainingQueue.slice(1));
56
+ deps.logError(`Failed to prefetch segment ${nextSegmentId}`, error);
57
+ if (config.enableContinuousBuffering ?? true) startNextSegment();
59
58
  });
60
59
  };
61
- for (const segmentId of segmentsToFetch) {
62
- if (signal.aborted) break;
63
- newActiveRequests.add(segmentId);
64
- deps.fetchSegment(segmentId, rendition).then(() => {
65
- if (signal.aborted) return;
66
- newActiveRequests.delete(segmentId);
67
- newCachedSegments.add(segmentId);
68
- if (config.enableContinuousBuffering ?? true) {
69
- const remainingQueue = newQueue.slice(segmentsToFetch.length);
70
- startNextSegment(remainingQueue);
71
- }
72
- }).catch((error) => {
73
- if (signal.aborted) return;
74
- newActiveRequests.delete(segmentId);
75
- deps.logError(`Failed to fetch segment ${segmentId}`, error);
76
- if (config.enableContinuousBuffering ?? true) {
77
- const remainingQueue = newQueue.slice(segmentsToFetch.length);
78
- startNextSegment(remainingQueue);
79
- }
80
- });
81
- }
60
+ const initialBatchSize = Math.min(config.maxParallelFetches, newQueue.length);
61
+ for (let i = 0; i < initialBatchSize; i++) startNextSegment();
82
62
  return {
83
63
  currentSeekTimeMs: seekTimeMs,
64
+ requestedSegments: newRequestedSegments,
84
65
  activeRequests: newActiveRequests,
85
- cachedSegments: newCachedSegments,
86
- requestQueue: newQueue.slice(segmentsToFetch.length)
66
+ requestQueue: remainingQueue
87
67
  };
88
68
  };
89
69
  export { manageMediaBuffer };
@@ -1 +1,9 @@
1
+ import { EFMedia } from '../../EFMedia.js';
2
+ declare class TestMediaEngine extends EFMedia {
3
+ }
4
+ declare global {
5
+ interface HTMLElementTagNameMap {
6
+ "test-media-engine": TestMediaEngine;
7
+ }
8
+ }
1
9
  export {};
@@ -29,11 +29,11 @@ const createMediaEngine = (host) => {
29
29
  return Promise.reject(/* @__PURE__ */ new Error("Unsupported media source"));
30
30
  }
31
31
  const lowerSrc = src.toLowerCase();
32
- if (lowerSrc.startsWith("http://") || lowerSrc.startsWith("https://")) {
33
- const url = urlGenerator.generateManifestUrl(src);
34
- return JitMediaEngine.fetch(host, urlGenerator, url);
35
- }
36
- return AssetMediaEngine.fetch(host, urlGenerator, src);
32
+ if (!lowerSrc.startsWith("http://") && !lowerSrc.startsWith("https://")) return AssetMediaEngine.fetch(host, urlGenerator, src);
33
+ const configuration = host.closest("ef-configuration");
34
+ if (configuration?.mediaEngine === "local") return AssetMediaEngine.fetch(host, urlGenerator, src);
35
+ const url = urlGenerator.generateManifestUrl(src);
36
+ return JitMediaEngine.fetch(host, urlGenerator, url);
37
37
  };
38
38
  /**
39
39
  * Handle completion of media engine task - triggers necessary updates.
@@ -0,0 +1,25 @@
1
+ import { BufferedSeekingInput } from '../BufferedSeekingInput';
2
+ /**
3
+ * Cache for scrub BufferedSeekingInput instances
4
+ * Since scrub segments are 30s long, we can reuse the same input for many seeks
5
+ * within that time range, making scrub seeking very efficient
6
+ */
7
+ export declare class ScrubInputCache {
8
+ private cache;
9
+ private maxCacheSize;
10
+ /**
11
+ * Get or create BufferedSeekingInput for a scrub segment
12
+ */
13
+ getOrCreateInput(segmentId: number, createInputFn: () => Promise<BufferedSeekingInput | undefined>): Promise<BufferedSeekingInput | undefined>;
14
+ /**
15
+ * Clear the entire cache (called when video changes)
16
+ */
17
+ clear(): void;
18
+ /**
19
+ * Get cache statistics
20
+ */
21
+ getStats(): {
22
+ size: number;
23
+ segmentIds: number[];
24
+ };
25
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cache for scrub BufferedSeekingInput instances
3
+ * Since scrub segments are 30s long, we can reuse the same input for many seeks
4
+ * within that time range, making scrub seeking very efficient
5
+ */
6
+ var ScrubInputCache = class {
7
+ constructor() {
8
+ this.cache = /* @__PURE__ */ new Map();
9
+ this.maxCacheSize = 5;
10
+ }
11
+ /**
12
+ * Get or create BufferedSeekingInput for a scrub segment
13
+ */
14
+ async getOrCreateInput(segmentId, createInputFn) {
15
+ const cached = this.cache.get(segmentId);
16
+ if (cached) return cached;
17
+ const input = await createInputFn();
18
+ if (!input) return void 0;
19
+ this.cache.set(segmentId, input);
20
+ if (this.cache.size > this.maxCacheSize) {
21
+ const oldestKey = this.cache.keys().next().value;
22
+ if (oldestKey !== void 0) this.cache.delete(oldestKey);
23
+ }
24
+ return input;
25
+ }
26
+ /**
27
+ * Clear the entire cache (called when video changes)
28
+ */
29
+ clear() {
30
+ this.cache.clear();
31
+ }
32
+ /**
33
+ * Get cache statistics
34
+ */
35
+ getStats() {
36
+ return {
37
+ size: this.cache.size,
38
+ segmentIds: Array.from(this.cache.keys())
39
+ };
40
+ }
41
+ };
42
+ export { ScrubInputCache };
@@ -0,0 +1,8 @@
1
+ import { Task } from '@lit/task';
2
+ import { EFVideo } from '../../EFVideo';
3
+ /**
4
+ * Scrub video buffer task - aggressively preloads the ENTIRE scrub track
5
+ * Unlike main video buffering, this loads the full duration with higher concurrency
6
+ * for instant visual feedback during seeking
7
+ */
8
+ export declare const makeScrubVideoBufferTask: (host: EFVideo) => Task<readonly [import('../../../transcoding/types').MediaEngine | undefined], unknown>;
@@ -0,0 +1,70 @@
1
+ import { EF_RENDERING } from "../../../EF_RENDERING.js";
2
+ import { manageMediaBuffer } from "../shared/BufferUtils.js";
3
+ import { Task } from "@lit/task";
4
+ /**
5
+ * Scrub video buffer task - aggressively preloads the ENTIRE scrub track
6
+ * Unlike main video buffering, this loads the full duration with higher concurrency
7
+ * for instant visual feedback during seeking
8
+ */
9
+ const makeScrubVideoBufferTask = (host) => {
10
+ let currentState = {
11
+ currentSeekTimeMs: 0,
12
+ requestedSegments: /* @__PURE__ */ new Set(),
13
+ activeRequests: /* @__PURE__ */ new Set(),
14
+ requestQueue: []
15
+ };
16
+ return new Task(host, {
17
+ autoRun: !EF_RENDERING(),
18
+ args: () => [host.mediaEngineTask.value],
19
+ onError: (error) => {
20
+ console.error("scrubVideoBufferTask error", error);
21
+ },
22
+ onComplete: (value) => {
23
+ currentState = value;
24
+ },
25
+ task: async ([mediaEngine], { signal }) => {
26
+ if (EF_RENDERING()) return currentState;
27
+ if (!host.enableVideoBuffering) return currentState;
28
+ if (!mediaEngine) return currentState;
29
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
30
+ if (!scrubRendition) return currentState;
31
+ const scrubRenditionWithSrc = {
32
+ ...scrubRendition,
33
+ src: mediaEngine.src
34
+ };
35
+ try {
36
+ try {
37
+ await mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal);
38
+ } catch (error) {
39
+ console.warn("ScrubBuffer: Failed to cache scrub init segment:", error);
40
+ }
41
+ const newState = await manageMediaBuffer(0, {
42
+ bufferDurationMs: mediaEngine.durationMs,
43
+ maxParallelFetches: 10,
44
+ enableBuffering: true,
45
+ enableContinuousBuffering: true
46
+ }, currentState, mediaEngine.durationMs, signal, {
47
+ computeSegmentId: async (timeMs, rendition) => {
48
+ return mediaEngine.computeSegmentId(timeMs, rendition);
49
+ },
50
+ prefetchSegment: async (segmentId, rendition) => {
51
+ await mediaEngine.fetchMediaSegment(segmentId, rendition);
52
+ },
53
+ isSegmentCached: (segmentId, rendition) => {
54
+ return mediaEngine.isSegmentCached(segmentId, rendition);
55
+ },
56
+ getRendition: async () => scrubRenditionWithSrc,
57
+ logError: (message, error) => {
58
+ console.warn(`ScrubBuffer: ${message}`, error);
59
+ }
60
+ });
61
+ return newState;
62
+ } catch (error) {
63
+ if (signal.aborted) return currentState;
64
+ console.warn("ScrubBuffer failed:", error);
65
+ return currentState;
66
+ }
67
+ }
68
+ });
69
+ };
70
+ export { makeScrubVideoBufferTask };
@@ -1,4 +1,4 @@
1
1
  import { Task } from '@lit/task';
2
2
  import { MediaEngine } from '../../../transcoding/types';
3
3
  import { EFVideo } from '../../EFVideo';
4
- export declare const makeVideoInitSegmentFetchTask: (host: EFVideo) => Task<readonly [MediaEngine | undefined], ArrayBuffer>;
4
+ export declare const makeScrubVideoInitSegmentFetchTask: (host: EFVideo) => Task<readonly [MediaEngine | undefined], ArrayBuffer>;
@@ -0,0 +1,21 @@
1
+ import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
2
+ import { Task } from "@lit/task";
3
+ const makeScrubVideoInitSegmentFetchTask = (host) => {
4
+ return new Task(host, {
5
+ args: () => [host.mediaEngineTask.value],
6
+ onError: (error) => {
7
+ console.error("scrubVideoInitSegmentFetchTask error", error);
8
+ },
9
+ onComplete: (_value) => {},
10
+ task: async ([_mediaEngine], { signal }) => {
11
+ const mediaEngine = await getLatestMediaEngine(host, signal);
12
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
13
+ if (!scrubRendition) throw new Error("No scrub rendition available");
14
+ return mediaEngine.fetchInitSegment({
15
+ ...scrubRendition,
16
+ src: mediaEngine.src
17
+ }, signal);
18
+ }
19
+ });
20
+ };
21
+ export { makeScrubVideoInitSegmentFetchTask };
@@ -1,3 +1,3 @@
1
1
  import { EFVideo } from '../../EFVideo';
2
2
  import { InputTask } from '../shared/MediaTaskUtils';
3
- export declare const makeVideoInputTask: (host: EFVideo) => InputTask;
3
+ export declare const makeScrubVideoInputTask: (host: EFVideo) => InputTask;
@@ -0,0 +1,27 @@
1
+ import { BufferedSeekingInput } from "../BufferedSeekingInput.js";
2
+ import { EFMedia } from "../../EFMedia.js";
3
+ import { Task } from "@lit/task";
4
+ const makeScrubVideoInputTask = (host) => {
5
+ return new Task(host, {
6
+ args: () => [host.scrubVideoInitSegmentFetchTask.value, host.scrubVideoSegmentFetchTask.value],
7
+ onError: (error) => {
8
+ console.error("scrubVideoInputTask error", error);
9
+ },
10
+ onComplete: (_value) => {},
11
+ task: async () => {
12
+ const initSegment = await host.scrubVideoInitSegmentFetchTask.taskComplete;
13
+ const segment = await host.scrubVideoSegmentFetchTask.taskComplete;
14
+ if (!initSegment || !segment) throw new Error("Scrub init segment or segment is not available");
15
+ const mediaEngine = await host.mediaEngineTask.taskComplete;
16
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
17
+ const startTimeOffsetMs = scrubRendition?.startTimeOffsetMs;
18
+ const input = new BufferedSeekingInput(await new Blob([initSegment, segment]).arrayBuffer(), {
19
+ videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
20
+ audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
21
+ startTimeOffsetMs
22
+ });
23
+ return input;
24
+ }
25
+ });
26
+ };
27
+ export { makeScrubVideoInputTask };
@@ -0,0 +1,6 @@
1
+ import { Task } from '@lit/task';
2
+ import { VideoSample } from 'mediabunny';
3
+ import { EFVideo } from '../../EFVideo';
4
+ type ScrubVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
5
+ export declare const makeScrubVideoSeekTask: (host: EFVideo) => ScrubVideoSeekTask;
6
+ export {};
@@ -0,0 +1,52 @@
1
+ import { ScrubInputCache } from "./ScrubInputCache.js";
2
+ import { Task } from "@lit/task";
3
+ const scrubInputCache = new ScrubInputCache();
4
+ const makeScrubVideoSeekTask = (host) => {
5
+ return new Task(host, {
6
+ args: () => [host.desiredSeekTimeMs],
7
+ onError: (error) => {
8
+ console.error("scrubVideoSeekTask error", error);
9
+ },
10
+ onComplete: (_value) => {},
11
+ task: async ([desiredSeekTimeMs], { signal }) => {
12
+ signal.throwIfAborted();
13
+ const mediaEngine = host.mediaEngineTask.value;
14
+ if (!mediaEngine) return void 0;
15
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
16
+ if (!scrubRendition) return void 0;
17
+ const scrubRenditionWithSrc = {
18
+ ...scrubRendition,
19
+ src: mediaEngine.src
20
+ };
21
+ const segmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, scrubRenditionWithSrc);
22
+ if (segmentId === void 0) return void 0;
23
+ const isCached = mediaEngine.isSegmentCached(segmentId, scrubRenditionWithSrc);
24
+ if (!isCached) return void 0;
25
+ signal.throwIfAborted();
26
+ try {
27
+ const scrubInput = await scrubInputCache.getOrCreateInput(segmentId, async () => {
28
+ const [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal), mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc)]);
29
+ if (!initSegment || !mediaSegment || signal.aborted) return void 0;
30
+ const { BufferedSeekingInput } = await import("../BufferedSeekingInput.js");
31
+ const { EFMedia } = await import("../../EFMedia.js");
32
+ return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
33
+ videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
34
+ audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
35
+ startTimeOffsetMs: scrubRendition.startTimeOffsetMs
36
+ });
37
+ });
38
+ if (!scrubInput) return void 0;
39
+ const videoTrack = await scrubInput.getFirstVideoTrack();
40
+ if (!videoTrack) return void 0;
41
+ signal.throwIfAborted();
42
+ const sample = await scrubInput.seek(videoTrack.id, desiredSeekTimeMs);
43
+ return sample;
44
+ } catch (error) {
45
+ if (signal.aborted) return void 0;
46
+ console.warn("Failed to get scrub video sample:", error);
47
+ return void 0;
48
+ }
49
+ }
50
+ });
51
+ };
52
+ export { makeScrubVideoSeekTask };
@@ -0,0 +1,4 @@
1
+ import { Task } from '@lit/task';
2
+ import { MediaEngine } from '../../../transcoding/types';
3
+ import { EFVideo } from '../../EFVideo';
4
+ export declare const makeScrubVideoSegmentFetchTask: (host: EFVideo) => Task<readonly [MediaEngine | undefined, number | undefined], ArrayBuffer>;