@editframe/elements 0.20.3-beta.0 → 0.20.4-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 (66) hide show
  1. package/dist/EF_FRAMEGEN.js +3 -20
  2. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +4 -4
  3. package/dist/elements/EFMedia/AssetMediaEngine.js +8 -4
  4. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +10 -2
  5. package/dist/elements/EFMedia/BaseMediaEngine.js +8 -2
  6. package/dist/elements/EFMedia/JitMediaEngine.js +13 -4
  7. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +1 -1
  8. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +0 -2
  9. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
  10. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +5 -4
  11. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +2 -12
  12. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
  13. package/dist/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.d.ts +1 -0
  14. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +5 -2
  15. package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +1 -1
  16. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
  17. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +1 -1
  18. package/dist/elements/EFMedia/shared/BufferUtils.js +3 -1
  19. package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +1 -1
  20. package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +1 -9
  21. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +1 -2
  22. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -6
  23. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +2 -1
  24. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +0 -2
  25. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +0 -2
  26. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +0 -2
  27. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +0 -2
  28. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +4 -5
  29. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +2 -2
  30. package/dist/elements/EFMedia.d.ts +2 -1
  31. package/dist/elements/EFMedia.js +1 -0
  32. package/dist/elements/EFTimegroup.js +1 -1
  33. package/dist/transcoding/types/index.d.ts +6 -4
  34. package/package.json +2 -2
  35. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +6 -4
  36. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +25 -23
  37. package/src/elements/EFMedia/AssetMediaEngine.ts +16 -6
  38. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +94 -0
  39. package/src/elements/EFMedia/BaseMediaEngine.ts +10 -8
  40. package/src/elements/EFMedia/JitMediaEngine.ts +20 -6
  41. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +5 -2
  42. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -5
  43. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +2 -1
  44. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +11 -5
  45. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +4 -16
  46. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +4 -2
  47. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +95 -0
  48. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +5 -6
  49. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +5 -4
  50. package/src/elements/EFMedia/shared/BufferUtils.ts +7 -3
  51. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +1 -1
  52. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +41 -42
  53. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -23
  54. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +1 -9
  55. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +3 -2
  56. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -5
  57. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +1 -5
  58. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -5
  59. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -5
  60. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +10 -19
  61. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +2 -5
  62. package/src/elements/EFMedia.ts +2 -1
  63. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +2 -1
  64. package/src/elements/EFTimegroup.ts +1 -1
  65. package/src/transcoding/types/index.ts +6 -4
  66. package/types.json +1 -1
@@ -92,7 +92,9 @@ var EFFramegen = class {
92
92
  const BRIDGE = this.BRIDGE;
93
93
  if (!BRIDGE) throw new Error("No BRIDGE when attempting to connect to bridge");
94
94
  BRIDGE.onInitialize(async (renderOptions) => {
95
- await this.initialize(renderOptions);
95
+ await this.initialize(renderOptions).catch((error) => {
96
+ console.error("[EF_FRAMEGEN.connectToBridge] error initializing", error);
97
+ });
96
98
  BRIDGE.initialized();
97
99
  });
98
100
  BRIDGE.onBeginFrame((frameNumber, isLast) => {
@@ -107,20 +109,6 @@ var EFFramegen = class {
107
109
  }
108
110
  async initialize(renderOptions) {
109
111
  this.renderOptions = renderOptions;
110
- const alignedFromMs = renderOptions.encoderOptions.alignedFromUs / 1e3;
111
- const alignedToMs = renderOptions.encoderOptions.alignedToUs / 1e3;
112
- const alignedDurationMs = alignedToMs - alignedFromMs;
113
- await this.syncLog("[EF_FRAMEGEN.initialize] Aligned boundary parameters:", {
114
- alignedFromUs: renderOptions.encoderOptions.alignedFromUs,
115
- alignedToUs: renderOptions.encoderOptions.alignedToUs,
116
- alignedFromMs: alignedFromMs.toFixed(3),
117
- alignedToMs: alignedToMs.toFixed(3),
118
- alignedDurationMs: alignedDurationMs.toFixed(3),
119
- sequenceNumber: renderOptions.encoderOptions.sequenceNumber,
120
- originalFromMs: renderOptions.encoderOptions.fromMs,
121
- originalToMs: renderOptions.encoderOptions.toMs,
122
- originalDurationMs: (renderOptions.encoderOptions.toMs - renderOptions.encoderOptions.fromMs).toFixed(3)
123
- });
124
112
  const workbench = document.querySelector("ef-workbench");
125
113
  if (!workbench) throw new Error("No workbench found");
126
114
  workbench.rendering = true;
@@ -147,11 +135,6 @@ var EFFramegen = class {
147
135
  document.body.prepend(this.frameBox);
148
136
  }
149
137
  this.triggerCanvas.initialize();
150
- await this.syncLog("[EF_FRAMEGEN.initialize] About to call renderAudio with:", {
151
- fromMs: alignedFromMs.toFixed(3),
152
- toMs: alignedToMs.toFixed(3),
153
- durationMs: alignedDurationMs.toFixed(3)
154
- });
155
138
  this.audioBufferPromise = firstGroup.renderAudio(renderOptions.encoderOptions.alignedFromUs / 1e3, renderOptions.encoderOptions.alignedToUs / 1e3);
156
139
  }
157
140
  async beginFrame(frameNumber, isLast) {
@@ -13,14 +13,14 @@ export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEn
13
13
  get audioTrackIndex(): import('../../../../assets/src/index.ts').AudioTrackFragmentIndex | undefined;
14
14
  get videoTrackIndex(): import('../../../../assets/src/index.ts').VideoTrackFragmentIndex | undefined;
15
15
  get videoRendition(): {
16
- trackId: number | undefined;
16
+ trackId: number;
17
17
  src: string;
18
18
  startTimeOffsetMs: number | undefined;
19
- };
19
+ } | undefined;
20
20
  get audioRendition(): {
21
- trackId: number | undefined;
21
+ trackId: number;
22
22
  src: string;
23
- };
23
+ } | undefined;
24
24
  get initSegmentPaths(): InitSegmentPaths;
25
25
  get templates(): {
26
26
  initSegment: string;
@@ -24,15 +24,19 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
24
24
  return Object.values(this.data).find((track) => track.type === "video");
25
25
  }
26
26
  get videoRendition() {
27
+ const videoTrack = this.videoTrackIndex;
28
+ if (!videoTrack || videoTrack.track === void 0) return void 0;
27
29
  return {
28
- trackId: this.videoTrackIndex?.track,
30
+ trackId: videoTrack.track,
29
31
  src: this.src,
30
- startTimeOffsetMs: this.videoTrackIndex?.startTimeOffsetMs
32
+ startTimeOffsetMs: videoTrack.startTimeOffsetMs
31
33
  };
32
34
  }
33
35
  get audioRendition() {
36
+ const audioTrack = this.audioTrackIndex;
37
+ if (!audioTrack || audioTrack.track === void 0) return void 0;
34
38
  return {
35
- trackId: this.audioTrackIndex?.track,
39
+ trackId: audioTrack.track,
36
40
  src: this.src
37
41
  };
38
42
  }
@@ -162,7 +166,7 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
162
166
  }
163
167
  convertToSegmentRelativeTimestamps(globalTimestamps, segmentId, rendition) {
164
168
  {
165
- if (!rendition.trackId) throw new Error("[convertToSegmentRelativeTimestamps] Track ID is required for asset metadata");
169
+ if (!rendition.trackId) throw new Error("Track ID is required for asset metadata");
166
170
  const trackData = this.data[rendition.trackId];
167
171
  if (!trackData) throw new Error("Track not found");
168
172
  const segment = trackData.segments?.[segmentId];
@@ -10,8 +10,16 @@ export declare abstract class BaseMediaEngine {
10
10
  constructor(host: EFMedia);
11
11
  abstract get videoRendition(): VideoRendition | undefined;
12
12
  abstract get audioRendition(): AudioRendition | undefined;
13
- getVideoRendition(): VideoRendition;
14
- getAudioRendition(): AudioRendition;
13
+ /**
14
+ * Get video rendition if available. Returns undefined for audio-only assets.
15
+ * Callers should handle undefined gracefully.
16
+ */
17
+ getVideoRendition(): VideoRendition | undefined;
18
+ /**
19
+ * Get audio rendition if available. Returns undefined for video-only assets.
20
+ * Callers should handle undefined gracefully.
21
+ */
22
+ getAudioRendition(): AudioRendition | undefined;
15
23
  /**
16
24
  * Generate cache key for segment requests
17
25
  */
@@ -6,12 +6,18 @@ var BaseMediaEngine = class {
6
6
  constructor(host) {
7
7
  this.host = host;
8
8
  }
9
+ /**
10
+ * Get video rendition if available. Returns undefined for audio-only assets.
11
+ * Callers should handle undefined gracefully.
12
+ */
9
13
  getVideoRendition() {
10
- if (!this.videoRendition) throw new Error("No video rendition available");
11
14
  return this.videoRendition;
12
15
  }
16
+ /**
17
+ * Get audio rendition if available. Returns undefined for video-only assets.
18
+ * Callers should handle undefined gracefully.
19
+ */
13
20
  getAudioRendition() {
14
- if (!this.audioRendition) throw new Error("No audio rendition available");
15
21
  return this.audioRendition;
16
22
  }
17
23
  /**
@@ -107,10 +107,19 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
107
107
  * Extract thumbnail canvases using same rendition priority as video playback for frame alignment
108
108
  */
109
109
  async extractThumbnails(timestamps) {
110
- const mainRendition = this.videoRendition;
111
- const scrubRendition = this.getScrubVideoRendition();
112
- const rendition = mainRendition || scrubRendition;
113
- if (!rendition) return timestamps.map(() => null);
110
+ let rendition;
111
+ try {
112
+ const mainRendition = this.getVideoRendition();
113
+ if (mainRendition) rendition = mainRendition;
114
+ else {
115
+ const scrubRendition = this.getScrubVideoRendition();
116
+ if (scrubRendition) rendition = scrubRendition;
117
+ else throw new Error("No video rendition available");
118
+ }
119
+ } catch (error) {
120
+ console.warn("JitMediaEngine: No video rendition available for thumbnails", error);
121
+ return timestamps.map(() => null);
122
+ }
114
123
  return this.thumbnailExtractor.extractThumbnails(timestamps, rendition, this.durationMs);
115
124
  }
116
125
  convertToSegmentRelativeTimestamps(globalTimestamps, _segmentId, _rendition) {
@@ -48,7 +48,7 @@ const makeAudioBufferTask = (host) => {
48
48
  getRendition: async () => {
49
49
  const mediaEngine$1 = await getLatestMediaEngine(host, signal);
50
50
  const audioRendition = mediaEngine$1.audioRendition;
51
- if (!audioRendition) throw new Error("No audio track available in source");
51
+ if (!audioRendition) throw new Error("Audio rendition not available");
52
52
  return audioRendition;
53
53
  },
54
54
  logError: console.error
@@ -51,8 +51,6 @@ function makeAudioFrequencyAnalysisTask(element) {
51
51
  ],
52
52
  task: async (_, { signal }) => {
53
53
  if (element.currentSourceTimeMs < 0) return null;
54
- const mediaEngine = element.mediaEngineTask.value;
55
- if (!mediaEngine?.audioRendition) return null;
56
54
  const currentTimeMs = element.currentSourceTimeMs;
57
55
  const frameIntervalMs = 1e3 / 30;
58
56
  const earliestFrameMs = currentTimeMs - (element.fftDecay - 1) * frameIntervalMs;
@@ -9,7 +9,7 @@ const makeAudioInitSegmentFetchTask = (host) => {
9
9
  onComplete: (_value) => {},
10
10
  task: async ([_mediaEngine], { signal }) => {
11
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
12
- const audioRendition = mediaEngine.audioRendition;
12
+ const audioRendition = mediaEngine.getAudioRendition();
13
13
  if (!audioRendition) return void 0;
14
14
  return mediaEngine.fetchInitSegment(audioRendition, signal);
15
15
  }
@@ -9,14 +9,15 @@ const makeAudioInputTask = (host) => {
9
9
  },
10
10
  onComplete: (_value) => {},
11
11
  task: async (_, { signal }) => {
12
+ const mediaEngine = await host.mediaEngineTask.taskComplete;
13
+ const audioRendition = mediaEngine?.audioRendition;
14
+ if (!audioRendition) return void 0;
12
15
  const initSegment = await host.audioInitSegmentFetchTask.taskComplete;
13
16
  signal.throwIfAborted();
14
17
  const segment = await host.audioSegmentFetchTask.taskComplete;
15
18
  signal.throwIfAborted();
16
- if (!initSegment || !segment) throw new Error("No audio track available in source");
17
- const mediaEngine = await host.mediaEngineTask.taskComplete;
18
- const audioRendition = mediaEngine?.audioRendition;
19
- const startTimeOffsetMs = audioRendition?.startTimeOffsetMs;
19
+ if (!initSegment || !segment) return void 0;
20
+ const startTimeOffsetMs = audioRendition.startTimeOffsetMs;
20
21
  const arrayBuffer = await new Blob([initSegment, segment]).arrayBuffer();
21
22
  signal.throwIfAborted();
22
23
  return new BufferedSeekingInput(arrayBuffer, {
@@ -9,19 +9,9 @@ const makeAudioSegmentFetchTask = (host) => {
9
9
  onComplete: (_value) => {},
10
10
  task: async (_, { signal }) => {
11
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
12
- const audioRendition = mediaEngine.audioRendition;
13
- if (!audioRendition) return void 0;
14
12
  const segmentId = await host.audioSegmentIdTask.taskComplete;
15
- if (segmentId === void 0) {
16
- const debugInfo = {
17
- hasRendition: true,
18
- segmentDurationMs: audioRendition.segmentDurationMs,
19
- segmentDurationsMs: audioRendition.segmentDurationsMs?.length || 0,
20
- desiredSeekTimeMs: host.desiredSeekTimeMs,
21
- intrinsicDurationMs: host.intrinsicDurationMs
22
- };
23
- throw new Error(`Segment ID is not available for audio. Debug info: ${JSON.stringify(debugInfo)}`);
24
- }
13
+ const audioRendition = mediaEngine.getAudioRendition();
14
+ if (!audioRendition || segmentId === void 0) return void 0;
25
15
  return mediaEngine.fetchMediaSegment(segmentId, audioRendition, signal);
26
16
  }
27
17
  });
@@ -10,7 +10,7 @@ const makeAudioSegmentIdTask = (host) => {
10
10
  task: async ([, targetSeekTimeMs], { signal }) => {
11
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
12
12
  signal.throwIfAborted();
13
- const audioRendition = mediaEngine.audioRendition;
13
+ const audioRendition = mediaEngine.getAudioRendition();
14
14
  if (!audioRendition) return void 0;
15
15
  return mediaEngine.computeSegmentId(targetSeekTimeMs, audioRendition);
16
16
  }
@@ -1,5 +1,6 @@
1
1
  import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
2
2
  import { LRUCache } from "../../../utils/LRUCache.js";
3
+ import { IgnorableError } from "../../EFMedia.js";
3
4
  import { Task } from "@lit/task";
4
5
  const DECAY_WEIGHT = .8;
5
6
  function makeAudioTimeDomainAnalysisTask(element) {
@@ -7,6 +8,10 @@ function makeAudioTimeDomainAnalysisTask(element) {
7
8
  return new Task(element, {
8
9
  autoRun: EF_INTERACTIVE,
9
10
  onError: (error) => {
11
+ if (error instanceof IgnorableError) {
12
+ console.info("byteTimeDomainTask skipped: no audio track");
13
+ return;
14
+ }
10
15
  console.error("byteTimeDomainTask error", error);
11
16
  },
12
17
  args: () => [
@@ -18,8 +23,6 @@ function makeAudioTimeDomainAnalysisTask(element) {
18
23
  ],
19
24
  task: async (_, { signal }) => {
20
25
  if (element.currentSourceTimeMs < 0) return null;
21
- const mediaEngine = element.mediaEngineTask.value;
22
- if (!mediaEngine?.audioRendition) return null;
23
26
  const currentTimeMs = element.currentSourceTimeMs;
24
27
  const frameIntervalMs = 1e3 / 30;
25
28
  const earliestFrameMs = currentTimeMs - (element.fftDecay - 1) * frameIntervalMs;
@@ -4,4 +4,4 @@ import { EFMedia } from '../../EFMedia';
4
4
  * Fetch audio spanning a time range
5
5
  * Main function that orchestrates segment calculation, fetching, and blob creation
6
6
  */
7
- export declare const fetchAudioSpanningTime: (host: EFMedia, fromMs: number, toMs: number, signal: AbortSignal) => Promise<AudioSpan>;
7
+ export declare const fetchAudioSpanningTime: (host: EFMedia, fromMs: number, toMs: number, signal: AbortSignal) => Promise<AudioSpan | undefined>;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  const fetchAudioSegmentData = async (segmentIds, mediaEngine, signal) => {
6
6
  const audioRendition = mediaEngine.audioRendition;
7
- if (!audioRendition) throw new Error("No audio track available in source");
7
+ if (!audioRendition) throw new Error("Audio rendition not available");
8
8
  const segmentData = /* @__PURE__ */ new Map();
9
9
  const fetchPromises = segmentIds.map(async (segmentId) => {
10
10
  const arrayBuffer = await mediaEngine.fetchMediaSegment(segmentId, audioRendition, signal);
@@ -31,8 +31,8 @@ const fetchAudioSpanningTime = async (host, fromMs, toMs, signal) => {
31
31
  if (fromMs >= toMs || fromMs < 0) throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);
32
32
  const mediaEngine = await host.mediaEngineTask.taskComplete;
33
33
  const initSegment = await host.audioInitSegmentFetchTask.taskComplete;
34
- if (!mediaEngine?.audioRendition) throw new Error("No audio track available in source");
35
- if (!initSegment) throw new Error("Audio init segment is not available");
34
+ if (!mediaEngine?.audioRendition) return void 0;
35
+ if (!initSegment) return void 0;
36
36
  const segmentRanges = mediaEngine.calculateAudioSegmentRange(fromMs, toMs, mediaEngine.audioRendition, host.intrinsicDurationMs || 1e4);
37
37
  if (segmentRanges.length === 0) throw new Error(`No segments found for time range ${fromMs}-${toMs}ms`);
38
38
  const segmentIds = segmentRanges.map((r) => r.segmentId);
@@ -24,7 +24,7 @@ export interface MediaBufferDependencies<T extends AudioRendition | VideoRenditi
24
24
  computeSegmentId: (timeMs: number, rendition: T) => Promise<number | undefined>;
25
25
  prefetchSegment: (segmentId: number, rendition: T) => Promise<void>;
26
26
  isSegmentCached: (segmentId: number, rendition: T) => boolean;
27
- getRendition: () => Promise<T>;
27
+ getRendition: () => Promise<T | undefined>;
28
28
  logError: (message: string, error: any) => void;
29
29
  }
30
30
  /**
@@ -29,6 +29,7 @@ const computeBufferQueue = (desiredSegments, requestedSegments) => {
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
+ if (!rendition) return currentState;
32
33
  const endTimeMs = seekTimeMs + config.bufferDurationMs;
33
34
  const desiredSegments = await computeSegmentRangeAsync(seekTimeMs, endTimeMs, durationMs, rendition, deps.computeSegmentId);
34
35
  const uncachedSegments = desiredSegments.filter((segmentId) => !deps.isSegmentCached(segmentId, rendition));
@@ -59,11 +60,12 @@ const manageMediaBuffer = async (seekTimeMs, config, currentState, durationMs, s
59
60
  };
60
61
  const initialBatchSize = Math.min(config.maxParallelFetches, newQueue.length);
61
62
  for (let i = 0; i < initialBatchSize; i++) startNextSegment();
62
- return {
63
+ const result = {
63
64
  currentSeekTimeMs: seekTimeMs,
64
65
  requestedSegments: newRequestedSegments,
65
66
  activeRequests: newActiveRequests,
66
67
  requestQueue: remainingQueue
67
68
  };
69
+ return result;
68
70
  };
69
71
  export { manageMediaBuffer };
@@ -20,4 +20,4 @@ export type SegmentFetchTask = Task<readonly [MediaEngine | undefined, number |
20
20
  /**
21
21
  * Generic task type for input creation
22
22
  */
23
- export type InputTask = Task<readonly [ArrayBuffer, ArrayBuffer], BufferedSeekingInput>;
23
+ export type InputTask = Task<readonly [ArrayBuffer, ArrayBuffer], BufferedSeekingInput | undefined>;
@@ -1,12 +1,4 @@
1
- import { AudioRendition, MediaEngine, VideoRendition } from '../../../transcoding/types';
2
- /**
3
- * Get audio rendition from media engine, throwing if not available
4
- */
5
- export declare const getAudioRendition: (mediaEngine: MediaEngine) => AudioRendition;
6
- /**
7
- * Get video rendition from media engine, throwing if not available
8
- */
9
- export declare const getVideoRendition: (mediaEngine: MediaEngine) => VideoRendition;
1
+ import { AudioRendition, VideoRendition } from '../../../transcoding/types';
10
2
  /**
11
3
  * Calculate which segment contains a given timestamp
12
4
  * Returns 1-based segment ID, or undefined if segmentDurationMs is not available
@@ -1,8 +1,7 @@
1
1
  import { Task } from '@lit/task';
2
- import { MediaEngine, VideoRendition } from '../../../transcoding/types';
2
+ import { MediaEngine } from '../../../transcoding/types';
3
3
  import { EFMedia } from '../../EFMedia';
4
4
  export declare const getLatestMediaEngine: (host: EFMedia, signal: AbortSignal) => Promise<MediaEngine>;
5
- export declare const getVideoRendition: (mediaEngine: MediaEngine) => VideoRendition;
6
5
  /**
7
6
  * Core logic for creating a MediaEngine with explicit dependencies.
8
7
  * Pure function that requires all dependencies to be provided.
@@ -9,11 +9,6 @@ const getLatestMediaEngine = async (host, signal) => {
9
9
  if (!mediaEngine) throw new Error("Media engine is not available");
10
10
  return mediaEngine;
11
11
  };
12
- const getVideoRendition = (mediaEngine) => {
13
- const videoRendition = mediaEngine.videoRendition;
14
- if (!videoRendition) throw new Error("No video track available in source");
15
- return videoRendition;
16
- };
17
12
  /**
18
13
  * Core logic for creating a MediaEngine with explicit dependencies.
19
14
  * Pure function that requires all dependencies to be provided.
@@ -57,4 +52,4 @@ const makeMediaEngineTask = (host) => {
57
52
  }
58
53
  });
59
54
  };
60
- export { getLatestMediaEngine, getVideoRendition, makeMediaEngineTask };
55
+ export { getLatestMediaEngine, makeMediaEngineTask };
@@ -1,3 +1,4 @@
1
+ import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
1
2
  import { EF_RENDERING } from "../../../EF_RENDERING.js";
2
3
  import { manageMediaBuffer } from "../shared/BufferUtils.js";
3
4
  import { Task } from "@lit/task";
@@ -14,7 +15,7 @@ const makeScrubVideoBufferTask = (host) => {
14
15
  requestQueue: []
15
16
  };
16
17
  return new Task(host, {
17
- autoRun: false,
18
+ autoRun: EF_INTERACTIVE,
18
19
  args: () => [host.mediaEngineTask.value],
19
20
  onError: (error) => {
20
21
  console.error("scrubVideoBufferTask error", error);
@@ -1,4 +1,3 @@
1
- import { EF_RENDERING } from "../../../EF_RENDERING.js";
2
1
  import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
3
2
  import { Task } from "@lit/task";
4
3
  const makeScrubVideoInitSegmentFetchTask = (host) => {
@@ -9,7 +8,6 @@ const makeScrubVideoInitSegmentFetchTask = (host) => {
9
8
  },
10
9
  onComplete: (_value) => {},
11
10
  task: async ([_mediaEngine], { signal }) => {
12
- if (EF_RENDERING()) return /* @__PURE__ */ new ArrayBuffer(0);
13
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
14
12
  const scrubRendition = mediaEngine.getScrubVideoRendition();
15
13
  if (!scrubRendition) throw new Error("No scrub rendition available");
@@ -1,4 +1,3 @@
1
- import { EF_RENDERING } from "../../../EF_RENDERING.js";
2
1
  import { BufferedSeekingInput } from "../BufferedSeekingInput.js";
3
2
  import { EFMedia } from "../../EFMedia.js";
4
3
  import { Task } from "@lit/task";
@@ -10,7 +9,6 @@ const makeScrubVideoInputTask = (host) => {
10
9
  },
11
10
  onComplete: (_value) => {},
12
11
  task: async () => {
13
- if (EF_RENDERING()) console.info("Scrub not available in rendering mode");
14
12
  const initSegment = await host.scrubVideoInitSegmentFetchTask.taskComplete;
15
13
  const segment = await host.scrubVideoSegmentFetchTask.taskComplete;
16
14
  if (!initSegment || !segment) throw new Error("Scrub init segment or segment is not available");
@@ -1,4 +1,3 @@
1
- import { EF_RENDERING } from "../../../EF_RENDERING.js";
2
1
  import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
3
2
  import { Task } from "@lit/task";
4
3
  const makeScrubVideoSegmentFetchTask = (host) => {
@@ -9,7 +8,6 @@ const makeScrubVideoSegmentFetchTask = (host) => {
9
8
  },
10
9
  onComplete: (_value) => {},
11
10
  task: async (_, { signal }) => {
12
- if (EF_RENDERING()) return /* @__PURE__ */ new ArrayBuffer(0);
13
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
14
12
  const segmentId = await host.scrubVideoSegmentIdTask.taskComplete;
15
13
  if (segmentId === void 0) throw new Error("Scrub segment ID is not available for video");
@@ -1,4 +1,3 @@
1
- import { EF_RENDERING } from "../../../EF_RENDERING.js";
2
1
  import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
3
2
  import { Task } from "@lit/task";
4
3
  const makeScrubVideoSegmentIdTask = (host) => {
@@ -9,7 +8,6 @@ const makeScrubVideoSegmentIdTask = (host) => {
9
8
  },
10
9
  onComplete: (_value) => {},
11
10
  task: async ([, targetSeekTimeMs], { signal }) => {
12
- if (EF_RENDERING()) return void 0;
13
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
14
12
  signal.throwIfAborted();
15
13
  const scrubRendition = mediaEngine.getScrubVideoRendition();
@@ -1,4 +1,3 @@
1
- import { EF_RENDERING } from "../../../EF_RENDERING.js";
2
1
  import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
3
2
  import { ScrubInputCache } from "./ScrubInputCache.js";
4
3
  import { Task } from "@lit/task";
@@ -14,7 +13,6 @@ const makeUnifiedVideoSeekTask = (host) => {
14
13
  task: async ([desiredSeekTimeMs], { signal }) => {
15
14
  const mediaEngine = await getLatestMediaEngine(host, signal);
16
15
  if (!mediaEngine) return void 0;
17
- if (EF_RENDERING()) return await getMainVideoSample(host, mediaEngine, desiredSeekTimeMs, signal);
18
16
  const mainRendition = mediaEngine.videoRendition;
19
17
  if (mainRendition) {
20
18
  const mainSegmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, mainRendition);
@@ -72,14 +70,15 @@ async function tryGetScrubSample(mediaEngine, desiredSeekTimeMs, signal) {
72
70
  */
73
71
  async function getMainVideoSample(_host, mediaEngine, desiredSeekTimeMs, signal) {
74
72
  try {
75
- const segmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, mediaEngine.getVideoRendition());
73
+ const videoRendition = mediaEngine.getVideoRendition();
74
+ if (!videoRendition) throw new Error("Video rendition unavailable after checking videoRendition exists");
75
+ const segmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, videoRendition);
76
76
  if (segmentId === void 0) return void 0;
77
- const [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(mediaEngine.getVideoRendition(), signal), mediaEngine.fetchMediaSegment(segmentId, mediaEngine.getVideoRendition(), signal)]);
77
+ const [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(videoRendition, signal), mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal)]);
78
78
  if (!initSegment || !mediaSegment) return void 0;
79
79
  signal.throwIfAborted();
80
80
  const { BufferedSeekingInput } = await import("../BufferedSeekingInput.js");
81
81
  const { EFMedia } = await import("../../EFMedia.js");
82
- const videoRendition = mediaEngine.videoRendition;
83
82
  const startTimeOffsetMs = videoRendition?.startTimeOffsetMs;
84
83
  const mainInput = new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
85
84
  videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
@@ -1,7 +1,7 @@
1
1
  import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
2
2
  import { EF_RENDERING } from "../../../EF_RENDERING.js";
3
3
  import { manageMediaBuffer } from "../shared/BufferUtils.js";
4
- import { getLatestMediaEngine, getVideoRendition } from "../tasks/makeMediaEngineTask.js";
4
+ import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
5
5
  import { Task } from "@lit/task";
6
6
  const makeVideoBufferTask = (host) => {
7
7
  let currentState = {
@@ -46,7 +46,7 @@ const makeVideoBufferTask = (host) => {
46
46
  },
47
47
  getRendition: async () => {
48
48
  const mediaEngine$1 = await getLatestMediaEngine(host, signal);
49
- return getVideoRendition(mediaEngine$1);
49
+ return mediaEngine$1.getVideoRendition();
50
50
  },
51
51
  logError: console.error
52
52
  });
@@ -81,7 +81,8 @@ export declare class EFMedia extends EFMedia_base {
81
81
  /**
82
82
  * Main integration method for EFTimegroup audio playback
83
83
  * Now powered by clean, testable utility functions
84
+ * Returns undefined if no audio rendition is available
84
85
  */
85
- fetchAudioSpanningTime(fromMs: number, toMs: number, signal?: AbortSignal): Promise<AudioSpan>;
86
+ fetchAudioSpanningTime(fromMs: number, toMs: number, signal?: AbortSignal): Promise<AudioSpan | undefined>;
86
87
  }
87
88
  export {};
@@ -145,6 +145,7 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
145
145
  /**
146
146
  * Main integration method for EFTimegroup audio playback
147
147
  * Now powered by clean, testable utility functions
148
+ * Returns undefined if no audio rendition is available
148
149
  */
149
150
  async fetchAudioSpanningTime(fromMs, toMs, signal = new AbortController().signal) {
150
151
  return fetchAudioSpanningTime(this, fromMs, toMs, signal);
@@ -373,7 +373,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
373
373
  if (error instanceof Error && error.message.includes("No audio track available")) return;
374
374
  throw error;
375
375
  }
376
- if (!audio) throw new Error("Failed to fetch audio");
376
+ if (!audio) return;
377
377
  const bufferSource = audioContext.createBufferSource();
378
378
  bufferSource.buffer = await audioContext.decodeAudioData(await audio.blob.arrayBuffer());
379
379
  bufferSource.connect(audioContext.destination);
@@ -207,13 +207,15 @@ export interface MediaEngine {
207
207
  }, signal?: AbortSignal) => Promise<ArrayBuffer>;
208
208
  computeSegmentId: (desiredSeekTimeMs: number, rendition: MediaRendition) => number | undefined;
209
209
  /**
210
- * Get the video rendition, or throws if no video rendition is available
210
+ * Get the video rendition if available, otherwise return undefined.
211
+ * Callers should handle undefined appropriately.
211
212
  */
212
- getVideoRendition: () => VideoRendition;
213
+ getVideoRendition: () => VideoRendition | undefined;
213
214
  /**
214
- * Get the audio rendition, or throws if no audio rendition is available
215
+ * Get the audio rendition if available, otherwise return undefined.
216
+ * Callers should handle undefined appropriately.
215
217
  */
216
- getAudioRendition: () => AudioRendition;
218
+ getAudioRendition: () => AudioRendition | undefined;
217
219
  /**
218
220
  * Check if a segment is cached for a given rendition
219
221
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.20.3-beta.0",
3
+ "version": "0.20.4-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.20.3-beta.0",
30
+ "@editframe/assets": "0.20.4-beta.0",
31
31
  "@lit/context": "^1.1.2",
32
32
  "@lit/task": "^1.0.1",
33
33
  "d3": "^7.9.0",
@@ -122,14 +122,16 @@ describe("AssetIdMediaEngine", () => {
122
122
 
123
123
  it("should return correct audio rendition", () => {
124
124
  const audioRendition = engine.audioRendition;
125
- expect(audioRendition.trackId).toBe(1);
126
- expect(audioRendition.src).toBe(mockAssetId);
125
+ expect(audioRendition).toBeDefined();
126
+ expect(audioRendition!.trackId).toBe(1);
127
+ expect(audioRendition!.src).toBe(mockAssetId);
127
128
  });
128
129
 
129
130
  it("should return correct video rendition", () => {
130
131
  const videoRendition = engine.videoRendition;
131
- expect(videoRendition.trackId).toBe(2);
132
- expect(videoRendition.src).toBe(mockAssetId);
132
+ expect(videoRendition).toBeDefined();
133
+ expect(videoRendition!.trackId).toBe(2);
134
+ expect(videoRendition!.src).toBe(mockAssetId);
133
135
  });
134
136
  });
135
137