@editframe/elements 0.18.23-beta.0 → 0.18.27-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 +27 -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 +54 -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
@@ -0,0 +1,23 @@
1
+ import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
2
+ import { Task } from "@lit/task";
3
+ const makeScrubVideoSegmentFetchTask = (host) => {
4
+ return new Task(host, {
5
+ args: () => [host.mediaEngineTask.value, host.scrubVideoSegmentIdTask.value],
6
+ onError: (error) => {
7
+ console.error("scrubVideoSegmentFetchTask error", error);
8
+ },
9
+ onComplete: (_value) => {},
10
+ task: async (_, { signal }) => {
11
+ const mediaEngine = await getLatestMediaEngine(host, signal);
12
+ const segmentId = await host.scrubVideoSegmentIdTask.taskComplete;
13
+ if (segmentId === void 0) throw new Error("Scrub segment ID is not available for video");
14
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
15
+ if (!scrubRendition) throw new Error("No scrub rendition available");
16
+ return mediaEngine.fetchMediaSegment(segmentId, {
17
+ ...scrubRendition,
18
+ src: mediaEngine.src
19
+ }, signal);
20
+ }
21
+ });
22
+ };
23
+ export { makeScrubVideoSegmentFetchTask };
@@ -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 makeScrubVideoSegmentIdTask: (host: EFVideo) => Task<readonly [MediaEngine | undefined, number], number | undefined>;
@@ -1,17 +1,22 @@
1
1
  import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
2
2
  import { Task } from "@lit/task";
3
- const makeVideoSegmentIdTask = (host) => {
3
+ const makeScrubVideoSegmentIdTask = (host) => {
4
4
  return new Task(host, {
5
5
  args: () => [host.mediaEngineTask.value, host.desiredSeekTimeMs],
6
6
  onError: (error) => {
7
- console.error("videoSegmentIdTask error", error);
7
+ console.error("scrubVideoSegmentIdTask error", error);
8
8
  },
9
9
  onComplete: (_value) => {},
10
10
  task: async ([, targetSeekTimeMs], { signal }) => {
11
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
12
12
  signal.throwIfAborted();
13
- return mediaEngine.computeSegmentId(targetSeekTimeMs, mediaEngine.getVideoRendition());
13
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
14
+ if (!scrubRendition) return void 0;
15
+ return mediaEngine.computeSegmentId(targetSeekTimeMs, {
16
+ ...scrubRendition,
17
+ src: mediaEngine.src
18
+ });
14
19
  }
15
20
  });
16
21
  };
17
- export { makeVideoSegmentIdTask };
22
+ export { makeScrubVideoSegmentIdTask };
@@ -0,0 +1,6 @@
1
+ import { Task } from '@lit/task';
2
+ import { VideoSample } from 'mediabunny';
3
+ import { EFVideo } from '../../EFVideo';
4
+ type UnifiedVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
5
+ export declare const makeUnifiedVideoSeekTask: (host: EFVideo) => UnifiedVideoSeekTask;
6
+ export {};
@@ -0,0 +1,112 @@
1
+ import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
2
+ import { ScrubInputCache } from "./ScrubInputCache.js";
3
+ import { Task } from "@lit/task";
4
+ const scrubInputCache = new ScrubInputCache();
5
+ const makeUnifiedVideoSeekTask = (host) => {
6
+ return new Task(host, {
7
+ args: () => [host.desiredSeekTimeMs],
8
+ onError: (error) => {
9
+ console.error("unifiedVideoSeekTask error", error);
10
+ },
11
+ onComplete: (_value) => {},
12
+ task: async ([desiredSeekTimeMs], { signal }) => {
13
+ signal.throwIfAborted();
14
+ const mediaEngine = await getLatestMediaEngine(host, signal);
15
+ if (!mediaEngine) return void 0;
16
+ const mainRendition = mediaEngine.videoRendition;
17
+ if (mainRendition) {
18
+ const mainSegmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, mainRendition);
19
+ if (mainSegmentId !== void 0 && mediaEngine.isSegmentCached(mainSegmentId, mainRendition)) return await getMainVideoSample(host, mediaEngine, desiredSeekTimeMs, signal);
20
+ }
21
+ const scrubSample = await tryGetScrubSample(mediaEngine, desiredSeekTimeMs, signal);
22
+ if (scrubSample || signal.aborted) {
23
+ if (scrubSample) startMainQualityUpgrade(host, mediaEngine, desiredSeekTimeMs, signal).catch(() => {});
24
+ return scrubSample;
25
+ }
26
+ return await getMainVideoSample(host, mediaEngine, desiredSeekTimeMs, signal);
27
+ }
28
+ });
29
+ };
30
+ /**
31
+ * Try to get scrub sample from cache (instant if available)
32
+ */
33
+ async function tryGetScrubSample(mediaEngine, desiredSeekTimeMs, signal) {
34
+ try {
35
+ let scrubRendition;
36
+ if ("data" in mediaEngine && mediaEngine.data?.videoRenditions) scrubRendition = mediaEngine.data.videoRenditions.find((r) => r.id === "scrub");
37
+ if (!scrubRendition) return void 0;
38
+ const scrubRenditionWithSrc = {
39
+ ...scrubRendition,
40
+ src: mediaEngine.src
41
+ };
42
+ const segmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, scrubRenditionWithSrc);
43
+ if (segmentId === void 0) return void 0;
44
+ const isCached = mediaEngine.isSegmentCached(segmentId, scrubRenditionWithSrc);
45
+ if (!isCached) return void 0;
46
+ const scrubInput = await scrubInputCache.getOrCreateInput(segmentId, async () => {
47
+ const [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal), mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc)]);
48
+ if (!initSegment || !mediaSegment || signal.aborted) return void 0;
49
+ const { BufferedSeekingInput } = await import("../BufferedSeekingInput.js");
50
+ const { EFMedia } = await import("../../EFMedia.js");
51
+ return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
52
+ videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
53
+ audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
54
+ startTimeOffsetMs: scrubRendition.startTimeOffsetMs
55
+ });
56
+ });
57
+ if (!scrubInput) return void 0;
58
+ const videoTrack = await scrubInput.getFirstVideoTrack();
59
+ if (!videoTrack) return void 0;
60
+ const sample = await scrubInput.seek(videoTrack.id, desiredSeekTimeMs);
61
+ return sample;
62
+ } catch (_error) {
63
+ if (signal.aborted) return void 0;
64
+ return void 0;
65
+ }
66
+ }
67
+ /**
68
+ * Get main video sample (slower path with fetching)
69
+ */
70
+ async function getMainVideoSample(_host, mediaEngine, desiredSeekTimeMs, signal) {
71
+ try {
72
+ const segmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, mediaEngine.getVideoRendition());
73
+ if (segmentId === void 0) return void 0;
74
+ const [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(mediaEngine.getVideoRendition(), signal), mediaEngine.fetchMediaSegment(segmentId, mediaEngine.getVideoRendition(), signal)]);
75
+ if (!initSegment || !mediaSegment) return void 0;
76
+ signal.throwIfAborted();
77
+ const { BufferedSeekingInput } = await import("../BufferedSeekingInput.js");
78
+ const { EFMedia } = await import("../../EFMedia.js");
79
+ const videoRendition = mediaEngine.videoRendition;
80
+ const startTimeOffsetMs = videoRendition?.startTimeOffsetMs;
81
+ const mainInput = new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
82
+ videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
83
+ audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
84
+ startTimeOffsetMs
85
+ });
86
+ const videoTrack = await mainInput.getFirstVideoTrack();
87
+ if (!videoTrack) return void 0;
88
+ signal.throwIfAborted();
89
+ const sample = await mainInput.seek(videoTrack.id, desiredSeekTimeMs);
90
+ return sample;
91
+ } catch (error) {
92
+ if (signal.aborted) return void 0;
93
+ throw error;
94
+ }
95
+ }
96
+ /**
97
+ * Start background upgrade to main quality (non-blocking)
98
+ */
99
+ async function startMainQualityUpgrade(host, mediaEngine, targetSeekTimeMs, signal) {
100
+ await new Promise((resolve) => setTimeout(resolve, 50));
101
+ if (signal.aborted || host.desiredSeekTimeMs !== targetSeekTimeMs) return;
102
+ const mainSample = await getMainVideoSample(host, mediaEngine, targetSeekTimeMs, signal);
103
+ if (mainSample && !signal.aborted && host.desiredSeekTimeMs === targetSeekTimeMs) {
104
+ const videoFrame = mainSample.toVideoFrame();
105
+ try {
106
+ host.displayFrame(videoFrame, targetSeekTimeMs);
107
+ } finally {
108
+ videoFrame.close();
109
+ }
110
+ }
111
+ }
112
+ export { makeUnifiedVideoSeekTask };
@@ -6,12 +6,12 @@ import { Task } from "@lit/task";
6
6
  const makeVideoBufferTask = (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("videoBufferTask error", error);
@@ -20,19 +20,25 @@ const makeVideoBufferTask = (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.videoBufferDurationMs,
25
26
  maxParallelFetches: host.maxVideoBufferFetches,
26
- enableBuffering: host.enableVideoBuffering && !EF_RENDERING
27
+ enableBuffering: host.enableVideoBuffering
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);
@@ -84,15 +84,5 @@ export declare class EFMedia extends EFMedia_base {
84
84
  * Now powered by clean, testable utility functions
85
85
  */
86
86
  fetchAudioSpanningTime(fromMs: number, toMs: number, signal?: AbortSignal): Promise<AudioSpan>;
87
- /**
88
- * Check if an audio segment is cached in the unified buffer system
89
- * Now uses the same caching approach as video for consistency
90
- */
91
- getCachedAudioSegment(segmentId: number): boolean;
92
- /**
93
- * Get cached audio segments from the unified buffer system
94
- * Now uses the same caching approach as video for consistency
95
- */
96
- getCachedAudioSegments(segmentIds: number[]): Set<number>;
97
87
  }
98
88
  export {};
@@ -28,7 +28,7 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
28
28
  constructor(..._args) {
29
29
  super(..._args);
30
30
  this.currentTimeMs = 0;
31
- this.audioBufferDurationMs = 3e4;
31
+ this.audioBufferDurationMs = 1e4;
32
32
  this.maxAudioBufferFetches = 2;
33
33
  this.enableAudioBuffering = true;
34
34
  this.mute = false;
@@ -128,22 +128,6 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
128
128
  async fetchAudioSpanningTime(fromMs, toMs, signal = new AbortController().signal) {
129
129
  return fetchAudioSpanningTime(this, fromMs, toMs, signal);
130
130
  }
131
- /**
132
- * Check if an audio segment is cached in the unified buffer system
133
- * Now uses the same caching approach as video for consistency
134
- */
135
- getCachedAudioSegment(segmentId) {
136
- return this.audioBufferTask.value?.cachedSegments.has(segmentId) ?? false;
137
- }
138
- /**
139
- * Get cached audio segments from the unified buffer system
140
- * Now uses the same caching approach as video for consistency
141
- */
142
- getCachedAudioSegments(segmentIds) {
143
- const bufferState = this.audioBufferTask.value;
144
- if (!bufferState) return /* @__PURE__ */ new Set();
145
- return new Set(segmentIds.filter((id) => bufferState.cachedSegments.has(id)));
146
- }
147
131
  };
148
132
  _decorate([property({ type: Number })], EFMedia.prototype, "currentTimeMs", void 0);
149
133
  _decorate([property({
@@ -28,12 +28,14 @@ export declare class EFVideo extends EFVideo_base {
28
28
  * @domAttribute "enable-video-buffering"
29
29
  */
30
30
  enableVideoBuffering: boolean;
31
- videoSegmentIdTask: Task<readonly [import('../transcoding/types/index.ts').MediaEngine | undefined, number], number | undefined>;
32
- videoInitSegmentFetchTask: Task<readonly [import('../transcoding/types/index.ts').MediaEngine | undefined], ArrayBuffer>;
33
- videoSegmentFetchTask: Task<readonly [import('../transcoding/types/index.ts').MediaEngine | undefined, number | undefined], ArrayBuffer>;
34
- videoInputTask: import('./EFMedia/shared/MediaTaskUtils.ts').InputTask;
35
- videoSeekTask: Task<readonly [number, import('./EFMedia/BufferedSeekingInput.ts').BufferedSeekingInput | undefined], import('mediabunny').VideoSample | undefined>;
31
+ unifiedVideoSeekTask: Task<readonly [number], import('mediabunny').VideoSample | undefined>;
36
32
  videoBufferTask: Task<readonly [number], import('./EFMedia/videoTasks/makeVideoBufferTask.ts').VideoBufferState>;
33
+ scrubVideoBufferTask: Task<readonly [import('../transcoding/types/index.ts').MediaEngine | undefined], unknown>;
34
+ scrubVideoInputTask: import('./EFMedia/shared/MediaTaskUtils.ts').InputTask;
35
+ scrubVideoSeekTask: Task<readonly [number], import('mediabunny').VideoSample | undefined>;
36
+ scrubVideoSegmentIdTask: Task<readonly [import('../transcoding/types/index.ts').MediaEngine | undefined, number], number | undefined>;
37
+ scrubVideoSegmentFetchTask: Task<readonly [import('../transcoding/types/index.ts').MediaEngine | undefined, number | undefined], ArrayBuffer>;
38
+ scrubVideoInitSegmentFetchTask: Task<readonly [import('../transcoding/types/index.ts').MediaEngine | undefined], ArrayBuffer>;
37
39
  /**
38
40
  * Delayed loading state manager for user feedback
39
41
  */
@@ -69,7 +71,7 @@ export declare class EFVideo extends EFVideo_base {
69
71
  /**
70
72
  * Display a video frame on the canvas
71
73
  */
72
- private displayFrame;
74
+ displayFrame(frame: VideoFrame, seekToMs: number): number;
73
75
  /**
74
76
  * Check if we're in production rendering mode (EF_FRAMEGEN active) vs preview mode
75
77
  */
@@ -79,10 +81,10 @@ export declare class EFVideo extends EFVideo_base {
79
81
  */
80
82
  private isFrameRenderingActive;
81
83
  /**
82
- * Legacy getter for fragment index task (maps to videoSegmentIdTask)
83
- * Still used by EFCaptions
84
+ * Legacy getter for fragment index task
85
+ * Still used by EFCaptions - maps to unified video seek task
84
86
  */
85
- get fragmentIndexTask(): Task<readonly [import('../transcoding/types/index.ts').MediaEngine | undefined, number], number | undefined>;
87
+ get fragmentIndexTask(): Task<readonly [number], import('mediabunny').VideoSample | undefined>;
86
88
  /**
87
89
  * Clean up resources when component is disconnected
88
90
  */
@@ -2,12 +2,14 @@ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
2
2
  import { EFMedia } from "./EFMedia.js";
3
3
  import { TWMixin } from "../gui/TWMixin2.js";
4
4
  import { DelayedLoadingState } from "../DelayedLoadingState.js";
5
+ import { makeScrubVideoBufferTask } from "./EFMedia/videoTasks/makeScrubVideoBufferTask.js";
6
+ import { makeScrubVideoInitSegmentFetchTask } from "./EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js";
7
+ import { makeScrubVideoInputTask } from "./EFMedia/videoTasks/makeScrubVideoInputTask.js";
8
+ import { makeScrubVideoSeekTask } from "./EFMedia/videoTasks/makeScrubVideoSeekTask.js";
9
+ import { makeScrubVideoSegmentFetchTask } from "./EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js";
10
+ import { makeScrubVideoSegmentIdTask } from "./EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js";
11
+ import { makeUnifiedVideoSeekTask } from "./EFMedia/videoTasks/makeUnifiedVideoSeekTask.js";
5
12
  import { makeVideoBufferTask } from "./EFMedia/videoTasks/makeVideoBufferTask.js";
6
- import { makeVideoInitSegmentFetchTask } from "./EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js";
7
- import { makeVideoInputTask } from "./EFMedia/videoTasks/makeVideoInputTask.js";
8
- import { makeVideoSeekTask } from "./EFMedia/videoTasks/makeVideoSeekTask.js";
9
- import { makeVideoSegmentFetchTask } from "./EFMedia/videoTasks/makeVideoSegmentFetchTask.js";
10
- import { makeVideoSegmentIdTask } from "./EFMedia/videoTasks/makeVideoSegmentIdTask.js";
11
13
  import { Task } from "@lit/task";
12
14
  import debug from "debug";
13
15
  import { css, html } from "lit";
@@ -80,15 +82,17 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
80
82
  constructor() {
81
83
  super();
82
84
  this.canvasRef = createRef();
83
- this.videoBufferDurationMs = 6e4;
85
+ this.videoBufferDurationMs = 1e4;
84
86
  this.maxVideoBufferFetches = 2;
85
87
  this.enableVideoBuffering = true;
86
- this.videoSegmentIdTask = makeVideoSegmentIdTask(this);
87
- this.videoInitSegmentFetchTask = makeVideoInitSegmentFetchTask(this);
88
- this.videoSegmentFetchTask = makeVideoSegmentFetchTask(this);
89
- this.videoInputTask = makeVideoInputTask(this);
90
- this.videoSeekTask = makeVideoSeekTask(this);
88
+ this.unifiedVideoSeekTask = makeUnifiedVideoSeekTask(this);
91
89
  this.videoBufferTask = makeVideoBufferTask(this);
90
+ this.scrubVideoBufferTask = makeScrubVideoBufferTask(this);
91
+ this.scrubVideoInputTask = makeScrubVideoInputTask(this);
92
+ this.scrubVideoSeekTask = makeScrubVideoSeekTask(this);
93
+ this.scrubVideoSegmentIdTask = makeScrubVideoSegmentIdTask(this);
94
+ this.scrubVideoSegmentFetchTask = makeScrubVideoSegmentFetchTask(this);
95
+ this.scrubVideoInitSegmentFetchTask = makeScrubVideoInitSegmentFetchTask(this);
92
96
  this.loadingState = {
93
97
  isLoading: false,
94
98
  operation: null,
@@ -102,7 +106,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
102
106
  },
103
107
  onComplete: () => {},
104
108
  task: async ([_desiredSeekTimeMs], { signal }) => {
105
- await this.videoSeekTask.taskComplete;
109
+ await this.unifiedVideoSeekTask.taskComplete;
106
110
  await this.paintTask.taskComplete;
107
111
  if (signal.aborted) return;
108
112
  }
@@ -114,16 +118,20 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
114
118
  },
115
119
  onComplete: () => {},
116
120
  task: async ([_seekToMs], { signal }) => {
117
- await this.videoSeekTask.taskComplete;
118
121
  const isProductionRendering = this.isInProductionRenderingMode();
119
- const sample = this.videoSeekTask.value;
120
- if (sample) {
121
- const videoFrame = sample.toVideoFrame();
122
- try {
123
- this.displayFrame(videoFrame, _seekToMs);
124
- } finally {
125
- videoFrame.close();
122
+ try {
123
+ await this.unifiedVideoSeekTask.taskComplete;
124
+ const videoSample = this.unifiedVideoSeekTask.value;
125
+ if (videoSample) {
126
+ const videoFrame = videoSample.toVideoFrame();
127
+ try {
128
+ this.displayFrame(videoFrame, _seekToMs);
129
+ } finally {
130
+ videoFrame.close();
131
+ }
126
132
  }
133
+ } catch (error) {
134
+ console.warn("Unified video pipeline error:", error);
127
135
  }
128
136
  if (!isProductionRendering) {
129
137
  if (!this.rootTimegroup || this.rootTimegroup.currentTimeMs === 0 && this.desiredSeekTimeMs === 0) return;
@@ -243,11 +251,11 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
243
251
  return currentTime >= renderStartTime;
244
252
  }
245
253
  /**
246
- * Legacy getter for fragment index task (maps to videoSegmentIdTask)
247
- * Still used by EFCaptions
254
+ * Legacy getter for fragment index task
255
+ * Still used by EFCaptions - maps to unified video seek task
248
256
  */
249
257
  get fragmentIndexTask() {
250
- return this.videoSegmentIdTask;
258
+ return this.unifiedVideoSeekTask;
251
259
  }
252
260
  /**
253
261
  * Clean up resources when component is disconnected
@@ -7,6 +7,7 @@ export declare class EFConfiguration extends LitElement {
7
7
  efConfiguration: this;
8
8
  apiHost?: string;
9
9
  signingURL?: string;
10
+ mediaEngine?: "cloud" | "local";
10
11
  render(): import('lit-html').TemplateResult<1>;
11
12
  }
12
13
  declare global {
@@ -7,6 +7,7 @@ let EFConfiguration = class EFConfiguration$1 extends LitElement {
7
7
  constructor(..._args) {
8
8
  super(..._args);
9
9
  this.efConfiguration = this;
10
+ this.mediaEngine = "cloud";
10
11
  }
11
12
  static {
12
13
  this.styles = [css`
@@ -28,5 +29,9 @@ _decorate([property({
28
29
  type: String,
29
30
  attribute: "signing-url"
30
31
  })], EFConfiguration.prototype, "signingURL", void 0);
32
+ _decorate([property({
33
+ type: String,
34
+ attribute: "media-engine"
35
+ })], EFConfiguration.prototype, "mediaEngine", void 0);
31
36
  EFConfiguration = _decorate([customElement("ef-configuration")], EFConfiguration);
32
37
  export { EFConfiguration, efConfigurationContext };
@@ -1,4 +1,4 @@
1
- import { LitElement, PropertyValueMap, ReactiveController, TemplateResult, nothing } from 'lit';
1
+ import { LitElement, nothing, PropertyValueMap, ReactiveController, TemplateResult } from 'lit';
2
2
  import { EFAudio } from '../elements/EFAudio.js';
3
3
  import { EFImage } from '../elements/EFImage.js';
4
4
  import { TemporalMixinInterface } from '../elements/EFTemporal.js';
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { EFImage } from './elements/EFImage.js';
3
3
  export type { EFMedia } from './elements/EFMedia.js';
4
4
  export { EFAudio } from './elements/EFAudio.js';
5
5
  export { EFVideo } from './elements/EFVideo.js';
6
- export { EFCaptions, EFCaptionsActiveWord, EFCaptionsSegment, EFCaptionsBeforeActiveWord, EFCaptionsAfterActiveWord, } from './elements/EFCaptions.js';
6
+ export { EFCaptions, EFCaptionsActiveWord, EFCaptionsAfterActiveWord, EFCaptionsBeforeActiveWord, EFCaptionsSegment, } from './elements/EFCaptions.js';
7
7
  export { EFWaveform } from './elements/EFWaveform.js';
8
8
  export { EFConfiguration } from './gui/EFConfiguration.ts';
9
9
  export { EFWorkbench } from './gui/EFWorkbench.js';
@@ -214,6 +214,17 @@ export interface MediaEngine {
214
214
  * Get the audio rendition, or throws if no audio rendition is available
215
215
  */
216
216
  getAudioRendition: () => AudioRendition;
217
+ /**
218
+ * Check if a segment is cached for a given rendition
219
+ */
220
+ isSegmentCached: (segmentId: number, rendition: AudioRendition | VideoRendition) => boolean;
221
+ /**
222
+ * Get scrub video rendition if available, otherwise return undefined
223
+ * Each engine implements this based on their capabilities:
224
+ * - JitMediaEngine: looks for "scrub" rendition in manifest
225
+ * - AssetMediaEngine: returns regular video rendition (no separate scrub)
226
+ */
227
+ getScrubVideoRendition(): VideoRendition | undefined;
217
228
  /**
218
229
  * Calculate audio segments needed for a time range
219
230
  * Each media engine implements this based on their segment structure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.18.23-beta.0",
3
+ "version": "0.18.27-beta.0",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -27,7 +27,7 @@
27
27
  "license": "UNLICENSED",
28
28
  "dependencies": {
29
29
  "@bramus/style-observer": "^1.3.0",
30
- "@editframe/assets": "0.18.23-beta.0",
30
+ "@editframe/assets": "0.18.27-beta.0",
31
31
  "@lit/context": "^1.1.2",
32
32
  "@lit/task": "^1.0.1",
33
33
  "d3": "^7.9.0",
@@ -1,5 +1,5 @@
1
1
  import { Task } from "@lit/task";
2
- import { LitElement, type PropertyValueMap, css, html } from "lit";
2
+ import { css, html, LitElement, type PropertyValueMap } from "lit";
3
3
  import { customElement, property } from "lit/decorators.js";
4
4
  import type { GetISOBMFFFileTranscriptionResult } from "../../../api/src/index.js";
5
5
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
@@ -1,5 +1,5 @@
1
1
  import { Task } from "@lit/task";
2
- import { LitElement, css, html } from "lit";
2
+ import { css, html, LitElement } from "lit";
3
3
  import { customElement, property } from "lit/decorators.js";
4
4
  import { createRef, ref } from "lit/directives/ref.js";
5
5
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
@@ -5,6 +5,7 @@ import type {
5
5
  InitSegmentPaths,
6
6
  MediaEngine,
7
7
  SegmentTimeRange,
8
+ VideoRendition,
8
9
  } from "../../transcoding/types";
9
10
  import type { UrlGenerator } from "../../transcoding/utils/UrlGenerator";
10
11
  import type { EFMedia } from "../EFMedia";
@@ -277,4 +278,9 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
277
278
 
278
279
  return nearestSegmentIndex;
279
280
  }
281
+
282
+ getScrubVideoRendition(): VideoRendition | undefined {
283
+ // AssetMediaEngine only has one video rendition - return it for scrub too
284
+ return this.videoRendition;
285
+ }
280
286
  }
@@ -333,4 +333,58 @@ export abstract class BaseMediaEngine {
333
333
 
334
334
  return segments;
335
335
  }
336
+
337
+ /**
338
+ * Check if a segment is cached for a given rendition
339
+ * This needs to check the URL-based cache since that's where segments are actually stored
340
+ */
341
+ isSegmentCached(
342
+ segmentId: number,
343
+ rendition: AudioRendition | VideoRendition,
344
+ ): boolean {
345
+ try {
346
+ // Check if this is a JIT engine by looking for urlGenerator property
347
+ const maybeJitEngine = this as any;
348
+ if (
349
+ maybeJitEngine.urlGenerator &&
350
+ typeof maybeJitEngine.urlGenerator.generateSegmentUrl === "function"
351
+ ) {
352
+ // This is a JIT engine - generate the URL and check URL-based cache
353
+ if (!rendition.id) {
354
+ return false;
355
+ }
356
+
357
+ const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(
358
+ segmentId,
359
+ rendition.id,
360
+ maybeJitEngine,
361
+ );
362
+ const urlIsCached = mediaCache.has(segmentUrl);
363
+
364
+ return urlIsCached;
365
+ }
366
+ // For other engine types, fall back to the old segment-based key approach
367
+ const cacheKey = `${rendition.src}-${rendition.id || "default"}-${segmentId}-${rendition.trackId}`;
368
+ const isCached = mediaCache.has(cacheKey);
369
+ return isCached;
370
+ } catch (error) {
371
+ console.warn(
372
+ `🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`,
373
+ error,
374
+ );
375
+ return false;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Get cached segment IDs from a list for a given rendition
381
+ */
382
+ getCachedSegments(
383
+ segmentIds: number[],
384
+ rendition: AudioRendition | VideoRendition,
385
+ ): Set<number> {
386
+ return new Set(
387
+ segmentIds.filter((id) => this.isSegmentCached(id, rendition)),
388
+ );
389
+ }
336
390
  }
@@ -172,4 +172,22 @@ export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
172
172
 
173
173
  return segmentIndex + 1; // Convert 0-based to 1-based
174
174
  }
175
+
176
+ getScrubVideoRendition(): VideoRendition | undefined {
177
+ if (!this.data.videoRenditions) return undefined;
178
+
179
+ const scrubManifestRendition = this.data.videoRenditions.find(
180
+ (r) => r.id === "scrub",
181
+ );
182
+
183
+ if (!scrubManifestRendition) return this.videoRendition; // Fallback to main
184
+
185
+ return {
186
+ id: scrubManifestRendition.id as any,
187
+ trackId: undefined,
188
+ src: this.src,
189
+ segmentDurationMs: scrubManifestRendition.segmentDurationMs,
190
+ segmentDurationsMs: scrubManifestRendition.segmentDurationsMs,
191
+ };
192
+ }
175
193
  }