@editframe/elements 0.18.8-beta.0 → 0.18.20-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 (65) hide show
  1. package/dist/elements/EFMedia/AssetIdMediaEngine.js +4 -1
  2. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +3 -4
  3. package/dist/elements/EFMedia/AssetMediaEngine.js +16 -15
  4. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +30 -11
  5. package/dist/elements/EFMedia/BaseMediaEngine.js +83 -31
  6. package/dist/elements/EFMedia/JitMediaEngine.d.ts +2 -4
  7. package/dist/elements/EFMedia/JitMediaEngine.js +12 -12
  8. package/dist/elements/EFTemporal.d.ts +1 -0
  9. package/dist/elements/EFTemporal.js +8 -4
  10. package/dist/elements/EFTimegroup.js +21 -0
  11. package/dist/elements/EFVideo.d.ts +0 -1
  12. package/dist/elements/EFVideo.js +0 -9
  13. package/dist/elements/TargetController.js +3 -2
  14. package/dist/getRenderInfo.d.ts +2 -2
  15. package/dist/gui/ContextMixin.browsertest.d.ts +5 -0
  16. package/dist/gui/ContextMixin.js +96 -5
  17. package/package.json +2 -2
  18. package/src/elements/EFMedia/AssetIdMediaEngine.ts +10 -1
  19. package/src/elements/EFMedia/AssetMediaEngine.ts +25 -21
  20. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +311 -0
  21. package/src/elements/EFMedia/BaseMediaEngine.ts +168 -51
  22. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +2 -12
  23. package/src/elements/EFMedia/JitMediaEngine.ts +25 -16
  24. package/src/elements/EFTemporal.browsertest.ts +47 -0
  25. package/src/elements/EFTemporal.ts +15 -5
  26. package/src/elements/EFTimegroup.ts +40 -0
  27. package/src/elements/EFVideo.browsertest.ts +127 -281
  28. package/src/elements/EFVideo.ts +9 -9
  29. package/src/elements/TargetController.ts +6 -2
  30. package/src/gui/ContextMixin.browsertest.ts +565 -1
  31. package/src/gui/ContextMixin.ts +138 -5
  32. package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +3 -8
  33. package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +3 -8
  34. package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +3 -8
  35. package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +3 -8
  36. package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/metadata.json +3 -8
  37. package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +3 -8
  38. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
  39. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +4 -9
  40. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
  41. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +4 -9
  42. package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
  43. package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +4 -9
  44. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
  45. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +4 -9
  46. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
  47. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +4 -9
  48. package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
  49. package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +4 -9
  50. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +2 -4
  51. package/test/recordReplayProxyPlugin.js +46 -31
  52. package/test/setup.ts +16 -0
  53. package/test/useAssetMSW.ts +54 -0
  54. package/test/useMSW.ts +4 -11
  55. package/types.json +1 -1
  56. package/dist/elements/MediaController.d.ts +0 -30
  57. package/src/elements/EFMedia/BaseMediaEngine.test.ts +0 -164
  58. package/src/elements/MediaController.ts +0 -98
  59. package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/data.bin +0 -0
  60. package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/metadata.json +0 -22
  61. package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/data.bin +0 -0
  62. package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/metadata.json +0 -22
  63. package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/data.bin +0 -0
  64. package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/metadata.json +0 -22
  65. /package/dist/elements/EFMedia/{BaseMediaEngine.test.d.ts → BaseMediaEngine.browsertest.d.ts} +0 -0
@@ -7,9 +7,12 @@ var AssetIdMediaEngine = class AssetIdMediaEngine extends AssetMediaEngine {
7
7
  return new AssetIdMediaEngine(host, assetId, data, apiHost);
8
8
  }
9
9
  constructor(host, assetId, data, apiHost) {
10
- super(host, assetId, data);
10
+ super(host, assetId);
11
11
  this.assetId = assetId;
12
12
  this.apiHost = apiHost;
13
+ this.data = data;
14
+ const longestFragment = Object.values(this.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0);
15
+ this.durationMs = longestFragment * 1e3;
13
16
  }
14
17
  get initSegmentPaths() {
15
18
  const paths = {};
@@ -5,12 +5,11 @@ import { EFMedia } from '../EFMedia';
5
5
  import { BaseMediaEngine } from './BaseMediaEngine';
6
6
  import { MediaRendition } from './shared/MediaTaskUtils';
7
7
  export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
8
- host: EFMedia;
9
8
  src: string;
10
9
  protected data: Record<number, TrackFragmentIndex>;
11
- static fetch(host: EFMedia, urlGenerator: UrlGenerator, src: string): Promise<AssetMediaEngine>;
12
10
  durationMs: number;
13
- constructor(host: EFMedia, src: string, data: Record<number, TrackFragmentIndex>);
11
+ constructor(host: EFMedia, src: string);
12
+ static fetch(host: EFMedia, urlGenerator: UrlGenerator, src: string): Promise<AssetMediaEngine>;
14
13
  get audioTrackIndex(): import('../../../../assets/src/index.ts').AudioTrackFragmentIndex | undefined;
15
14
  get videoTrackIndex(): import('../../../../assets/src/index.ts').VideoTrackFragmentIndex | undefined;
16
15
  get videoRendition(): {
@@ -33,7 +32,7 @@ export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEn
33
32
  trackId: number | undefined;
34
33
  src: string;
35
34
  }, signal: AbortSignal): Promise<ArrayBuffer>;
36
- fetchMediaSegmentImpl(segmentId: number, rendition: {
35
+ fetchMediaSegment(segmentId: number, rendition: {
37
36
  trackId: number | undefined;
38
37
  src: string;
39
38
  }, signal?: AbortSignal): Promise<ArrayBuffer>;
@@ -1,20 +1,21 @@
1
1
  import { BaseMediaEngine } from "./BaseMediaEngine.js";
2
2
  import { convertToScaledTime, roundToMilliseconds } from "./shared/PrecisionUtils.js";
3
3
  var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
4
+ constructor(host, src) {
5
+ super(host);
6
+ this.data = {};
7
+ this.durationMs = 0;
8
+ this.src = src;
9
+ }
4
10
  static async fetch(host, urlGenerator, src) {
11
+ const engine = new AssetMediaEngine(host, src);
5
12
  const url = urlGenerator.generateTrackFragmentIndexUrl(src);
6
- const response = await host.fetch(url);
7
- const data = await response.json();
8
- if (src.startsWith("/")) src = src.slice(1);
9
- return new AssetMediaEngine(host, src, data);
10
- }
11
- constructor(host, src, data) {
12
- super();
13
- this.host = host;
14
- this.src = src;
15
- this.data = data;
16
- const longestFragment = Object.values(data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0);
17
- this.durationMs = longestFragment * 1e3;
13
+ const data = await engine.fetchManifest(url);
14
+ engine.data = data;
15
+ const longestFragment = Object.values(engine.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0);
16
+ engine.durationMs = longestFragment * 1e3;
17
+ if (src.startsWith("/")) engine.src = src.slice(1);
18
+ return engine;
18
19
  }
19
20
  get audioTrackIndex() {
20
21
  return Object.values(this.data).find((track) => track.type === "audio");
@@ -67,16 +68,16 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
67
68
  const initSegment = this.data[rendition.trackId]?.initSegment;
68
69
  if (!initSegment) throw new Error("Init segment not found");
69
70
  const headers = { Range: `bytes=${initSegment.offset}-${initSegment.offset + initSegment.size - 1}` };
70
- return this.fetchMediaCacheWithHeaders(url, headers, signal);
71
+ return this.fetchMediaWithHeaders(url, headers, signal);
71
72
  }
72
- async fetchMediaSegmentImpl(segmentId, rendition, signal) {
73
+ async fetchMediaSegment(segmentId, rendition, signal) {
73
74
  if (!rendition.trackId) throw new Error("Track ID is required for asset metadata");
74
75
  if (segmentId === void 0) throw new Error("Segment ID is not available");
75
76
  const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);
76
77
  const mediaSegment = this.data[rendition.trackId]?.segments[segmentId];
77
78
  if (!mediaSegment) throw new Error("Media segment not found");
78
79
  const headers = { Range: `bytes=${mediaSegment.offset}-${mediaSegment.offset + mediaSegment.size - 1}` };
79
- return this.fetchMediaCacheWithHeaders(url, headers, signal);
80
+ return this.fetchMediaWithHeaders(url, headers, signal);
80
81
  }
81
82
  /**
82
83
  * Calculate audio segments for variable-duration segments using track fragment index
@@ -1,28 +1,53 @@
1
+ import { RequestDeduplicator } from '../../transcoding/cache/RequestDeduplicator.js';
1
2
  import { AudioRendition, SegmentTimeRange, VideoRendition } from '../../transcoding/types';
3
+ import { SizeAwareLRUCache } from '../../utils/LRUCache.js';
2
4
  import { EFMedia } from '../EFMedia.js';
5
+ export declare const mediaCache: SizeAwareLRUCache<string>;
6
+ export declare const globalRequestDeduplicator: RequestDeduplicator;
3
7
  export declare abstract class BaseMediaEngine {
4
- private requestDeduplicator;
8
+ protected host: EFMedia;
9
+ constructor(host: EFMedia);
5
10
  abstract get videoRendition(): VideoRendition | undefined;
6
11
  abstract get audioRendition(): AudioRendition | undefined;
7
- abstract get host(): EFMedia;
8
12
  getVideoRendition(): VideoRendition;
9
13
  getAudioRendition(): AudioRendition;
10
14
  /**
11
15
  * Generate cache key for segment requests
12
16
  */
13
17
  private getSegmentCacheKey;
18
+ /**
19
+ * Unified fetch method with caching and global deduplication
20
+ * All requests (media, manifest, init segments) go through this method
21
+ */
22
+ protected fetchWithCache(url: string, options: {
23
+ responseType: "arrayBuffer" | "json";
24
+ headers?: Record<string, string>;
25
+ signal?: AbortSignal;
26
+ }): Promise<any>;
27
+ /**
28
+ * Handles abort logic for a cached request without affecting the underlying fetch
29
+ * This allows multiple instances to share the same cached request while each
30
+ * manages their own abort behavior
31
+ */
32
+ private handleAbortForCachedRequest;
33
+ fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer>;
34
+ fetchManifest(url: string, signal?: AbortSignal): Promise<any>;
35
+ fetchMediaWithHeaders(url: string, headers: Record<string, string>, signal?: AbortSignal): Promise<ArrayBuffer>;
36
+ fetchMediaCache(url: string, signal?: AbortSignal): Promise<ArrayBuffer>;
37
+ fetchManifestCache(url: string, signal?: AbortSignal): Promise<any>;
38
+ fetchMediaCacheWithHeaders(url: string, headers: Record<string, string>, signal?: AbortSignal): Promise<ArrayBuffer>;
14
39
  /**
15
40
  * Abstract method for actual segment fetching - implemented by subclasses
16
41
  */
17
- abstract fetchMediaSegmentImpl(segmentId: number, rendition: {
42
+ abstract fetchMediaSegment(segmentId: number, rendition: {
18
43
  trackId: number | undefined;
19
44
  src: string;
20
45
  }): Promise<ArrayBuffer>;
21
46
  /**
22
47
  * Fetch media segment with built-in deduplication
23
- * Eliminates the need for separate coordinators - cleaner architecture
48
+ * Now uses global deduplication for all requests
24
49
  */
25
- fetchMediaSegment(segmentId: number, rendition: {
50
+ fetchMediaSegmentWithDeduplication(segmentId: number, rendition: {
26
51
  trackId: number | undefined;
27
52
  src: string;
28
53
  }, _signal?: AbortSignal): Promise<ArrayBuffer>;
@@ -41,12 +66,6 @@ export declare abstract class BaseMediaEngine {
41
66
  * Cancel all active segment requests (for cleanup)
42
67
  */
43
68
  cancelAllSegmentRequests(): void;
44
- fetchMediaCache(mediaUrl: string): Promise<ArrayBuffer>;
45
- /**
46
- * Enhanced caching method that supports custom headers (e.g., Range requests)
47
- * Cache key includes both URL and headers for proper cache isolation
48
- */
49
- fetchMediaCacheWithHeaders(mediaUrl: string, headers?: Record<string, string>, signal?: AbortSignal): Promise<ArrayBuffer>;
50
69
  /**
51
70
  * Calculate audio segments needed for a time range
52
71
  * Each media engine implements this based on their segment structure
@@ -1,9 +1,10 @@
1
1
  import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
2
2
  import { SizeAwareLRUCache } from "../../utils/LRUCache.js";
3
3
  const mediaCache = new SizeAwareLRUCache(100 * 1024 * 1024);
4
+ const globalRequestDeduplicator = new RequestDeduplicator();
4
5
  var BaseMediaEngine = class {
5
- constructor() {
6
- this.requestDeduplicator = new RequestDeduplicator();
6
+ constructor(host) {
7
+ this.host = host;
7
8
  }
8
9
  getVideoRendition() {
9
10
  if (!this.videoRendition) throw new Error("No video rendition available");
@@ -20,13 +21,86 @@ var BaseMediaEngine = class {
20
21
  return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;
21
22
  }
22
23
  /**
24
+ * Unified fetch method with caching and global deduplication
25
+ * All requests (media, manifest, init segments) go through this method
26
+ */
27
+ async fetchWithCache(url, options) {
28
+ const { responseType, headers, signal } = options;
29
+ const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
30
+ const cached = mediaCache.get(cacheKey);
31
+ if (cached) {
32
+ if (signal) return this.handleAbortForCachedRequest(cached, signal);
33
+ return cached;
34
+ }
35
+ const promise = globalRequestDeduplicator.executeRequest(cacheKey, async () => {
36
+ try {
37
+ const response = await this.host.fetch(url, { headers });
38
+ if (responseType === "json") return response.json();
39
+ return response.arrayBuffer();
40
+ } catch (error) {
41
+ if (error instanceof DOMException && error.name === "AbortError") mediaCache.delete(cacheKey);
42
+ throw error;
43
+ }
44
+ });
45
+ mediaCache.set(cacheKey, promise);
46
+ promise.catch((error) => {
47
+ if (error instanceof DOMException && error.name === "AbortError") mediaCache.delete(cacheKey);
48
+ });
49
+ if (signal) return this.handleAbortForCachedRequest(promise, signal);
50
+ return promise;
51
+ }
52
+ /**
53
+ * Handles abort logic for a cached request without affecting the underlying fetch
54
+ * This allows multiple instances to share the same cached request while each
55
+ * manages their own abort behavior
56
+ */
57
+ handleAbortForCachedRequest(promise, signal) {
58
+ if (signal.aborted) throw new DOMException("Aborted", "AbortError");
59
+ return Promise.race([promise, new Promise((_, reject) => {
60
+ signal.addEventListener("abort", () => {
61
+ reject(new DOMException("Aborted", "AbortError"));
62
+ });
63
+ })]);
64
+ }
65
+ async fetchMedia(url, signal) {
66
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
67
+ return this.fetchWithCache(url, {
68
+ responseType: "arrayBuffer",
69
+ signal
70
+ });
71
+ }
72
+ async fetchManifest(url, signal) {
73
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
74
+ return this.fetchWithCache(url, {
75
+ responseType: "json",
76
+ signal
77
+ });
78
+ }
79
+ async fetchMediaWithHeaders(url, headers, signal) {
80
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
81
+ return this.fetchWithCache(url, {
82
+ responseType: "arrayBuffer",
83
+ headers,
84
+ signal
85
+ });
86
+ }
87
+ async fetchMediaCache(url, signal) {
88
+ return this.fetchMedia(url, signal);
89
+ }
90
+ async fetchManifestCache(url, signal) {
91
+ return this.fetchManifest(url, signal);
92
+ }
93
+ async fetchMediaCacheWithHeaders(url, headers, signal) {
94
+ return this.fetchMediaWithHeaders(url, headers, signal);
95
+ }
96
+ /**
23
97
  * Fetch media segment with built-in deduplication
24
- * Eliminates the need for separate coordinators - cleaner architecture
98
+ * Now uses global deduplication for all requests
25
99
  */
26
- async fetchMediaSegment(segmentId, rendition, _signal) {
100
+ async fetchMediaSegmentWithDeduplication(segmentId, rendition, _signal) {
27
101
  const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
28
- return this.requestDeduplicator.executeRequest(cacheKey, async () => {
29
- return this.fetchMediaSegmentImpl(segmentId, rendition);
102
+ return globalRequestDeduplicator.executeRequest(cacheKey, async () => {
103
+ return this.fetchMediaSegment(segmentId, rendition);
30
104
  });
31
105
  }
32
106
  /**
@@ -34,41 +108,19 @@ var BaseMediaEngine = class {
34
108
  */
35
109
  isSegmentBeingFetched(segmentId, rendition) {
36
110
  const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
37
- return this.requestDeduplicator.isPending(cacheKey);
111
+ return globalRequestDeduplicator.isPending(cacheKey);
38
112
  }
39
113
  /**
40
114
  * Get count of active segment requests (for debugging/monitoring)
41
115
  */
42
116
  getActiveSegmentRequestCount() {
43
- return this.requestDeduplicator.getPendingCount();
117
+ return globalRequestDeduplicator.getPendingCount();
44
118
  }
45
119
  /**
46
120
  * Cancel all active segment requests (for cleanup)
47
121
  */
48
122
  cancelAllSegmentRequests() {
49
- this.requestDeduplicator.clear();
50
- }
51
- async fetchMediaCache(mediaUrl) {
52
- const cached = mediaCache.get(mediaUrl);
53
- if (cached) return cached;
54
- const promise = this.host.fetch(mediaUrl).then((response) => response.arrayBuffer());
55
- mediaCache.set(mediaUrl, promise);
56
- return promise;
57
- }
58
- /**
59
- * Enhanced caching method that supports custom headers (e.g., Range requests)
60
- * Cache key includes both URL and headers for proper cache isolation
61
- */
62
- async fetchMediaCacheWithHeaders(mediaUrl, headers, signal) {
63
- const cacheKey = headers ? `${mediaUrl}:${JSON.stringify(headers)}` : mediaUrl;
64
- const cached = mediaCache.get(cacheKey);
65
- if (cached) return cached;
66
- const promise = this.host.fetch(mediaUrl, {
67
- headers,
68
- signal
69
- }).then((response) => response.arrayBuffer());
70
- mediaCache.set(cacheKey, promise);
71
- return promise;
123
+ globalRequestDeduplicator.clear();
72
124
  }
73
125
  /**
74
126
  * Calculate audio segments needed for a time range
@@ -1,14 +1,12 @@
1
1
  import { AudioRendition, MediaEngine, RenditionId, VideoRendition } from '../../transcoding/types';
2
- import { ManifestResponse } from '../../transcoding/types/index.js';
3
2
  import { UrlGenerator } from '../../transcoding/utils/UrlGenerator';
4
3
  import { EFMedia } from '../EFMedia.js';
5
4
  import { BaseMediaEngine } from './BaseMediaEngine';
6
5
  export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
7
- host: EFMedia;
8
6
  private urlGenerator;
9
7
  private data;
10
8
  static fetch(host: EFMedia, urlGenerator: UrlGenerator, url: string): Promise<JitMediaEngine>;
11
- constructor(host: EFMedia, urlGenerator: UrlGenerator, data: ManifestResponse);
9
+ constructor(host: EFMedia, urlGenerator: UrlGenerator);
12
10
  get durationMs(): number;
13
11
  get src(): string;
14
12
  get audioRendition(): AudioRendition | undefined;
@@ -22,7 +20,7 @@ export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngi
22
20
  trackId: number | undefined;
23
21
  src: string;
24
22
  }, signal: AbortSignal): Promise<ArrayBuffer>;
25
- fetchMediaSegmentImpl(segmentId: number, rendition: {
23
+ fetchMediaSegment(segmentId: number, rendition: {
26
24
  id?: RenditionId;
27
25
  trackId: number | undefined;
28
26
  src: string;
@@ -1,15 +1,15 @@
1
1
  import { BaseMediaEngine } from "./BaseMediaEngine.js";
2
2
  var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
3
3
  static async fetch(host, urlGenerator, url) {
4
- const response = await host.fetch(url);
5
- const data = await response.json();
6
- return new JitMediaEngine(host, urlGenerator, data);
4
+ const engine = new JitMediaEngine(host, urlGenerator);
5
+ const data = await engine.fetchManifest(url);
6
+ engine.data = data;
7
+ return engine;
7
8
  }
8
- constructor(host, urlGenerator, data) {
9
- super();
10
- this.host = host;
9
+ constructor(host, urlGenerator) {
10
+ super(host);
11
+ this.data = {};
11
12
  this.urlGenerator = urlGenerator;
12
- this.data = data;
13
13
  }
14
14
  get durationMs() {
15
15
  return this.data.durationMs;
@@ -18,6 +18,7 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
18
18
  return this.data.sourceUrl;
19
19
  }
20
20
  get audioRendition() {
21
+ if (!this.data.audioRenditions || this.data.audioRenditions.length === 0) return void 0;
21
22
  const rendition = this.data.audioRenditions[0];
22
23
  if (!rendition) return void 0;
23
24
  return {
@@ -29,6 +30,7 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
29
30
  };
30
31
  }
31
32
  get videoRendition() {
33
+ if (!this.data.videoRenditions || this.data.videoRenditions.length === 0) return void 0;
32
34
  const rendition = this.data.videoRenditions[0];
33
35
  if (!rendition) return void 0;
34
36
  return {
@@ -45,14 +47,12 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
45
47
  async fetchInitSegment(rendition, signal) {
46
48
  if (!rendition.id) throw new Error("Rendition ID is required for JIT metadata");
47
49
  const url = this.urlGenerator.generateSegmentUrl("init", rendition.id, this);
48
- const response = await this.host.fetch(url, { signal });
49
- const arrayBuffer = await response.arrayBuffer();
50
- return arrayBuffer;
50
+ return this.fetchMedia(url, signal);
51
51
  }
52
- async fetchMediaSegmentImpl(segmentId, rendition) {
52
+ async fetchMediaSegment(segmentId, rendition) {
53
53
  if (!rendition.id) throw new Error("Rendition ID is required for JIT metadata");
54
54
  const url = this.urlGenerator.generateSegmentUrl(segmentId, rendition.id, this);
55
- return this.fetchMediaCache(url);
55
+ return this.fetchMedia(url);
56
56
  }
57
57
  computeSegmentId(desiredSeekTimeMs, rendition) {
58
58
  if (desiredSeekTimeMs > this.durationMs) return void 0;
@@ -187,6 +187,7 @@ export declare const deepGetElementsWithFrameTasks: (element: Element, elements?
187
187
  }>) => (HTMLElement & {
188
188
  frameTask: Task;
189
189
  })[];
190
+ export declare const clearTemporalCacheForElement: (element: Element) => void;
190
191
  export declare const shallowGetTemporalElements: (element: Element, temporals?: TemporalMixinInterface[]) => TemporalMixinInterface[];
191
192
  export declare class OwnCurrentTimeController implements ReactiveController {
192
193
  private host;
@@ -15,17 +15,21 @@ const deepGetElementsWithFrameTasks = (element, elements = []) => {
15
15
  return elements;
16
16
  };
17
17
  let temporalCache;
18
+ let modifiedElements = /* @__PURE__ */ new WeakSet();
18
19
  const resetTemporalCache = () => {
19
20
  temporalCache = /* @__PURE__ */ new Map();
21
+ modifiedElements = /* @__PURE__ */ new WeakSet();
20
22
  if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(resetTemporalCache);
21
23
  };
22
24
  resetTemporalCache();
25
+ const clearTemporalCacheForElement = (element) => {
26
+ temporalCache.delete(element);
27
+ modifiedElements.add(element);
28
+ };
23
29
  const shallowGetTemporalElements = (element, temporals = []) => {
24
- const cachedResult = temporalCache.get(element);
25
- if (cachedResult) return cachedResult;
30
+ temporals.length = 0;
26
31
  for (const child of element.children) if (isEFTemporal(child)) temporals.push(child);
27
32
  else shallowGetTemporalElements(child, temporals);
28
- temporalCache.set(element, temporals);
29
33
  return temporals;
30
34
  };
31
35
  var OwnCurrentTimeController = class {
@@ -281,4 +285,4 @@ const EFTemporal = (superClass) => {
281
285
  Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, { value: true });
282
286
  return TemporalMixinClass;
283
287
  };
284
- export { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, isEFTemporal, shallowGetTemporalElements, timegroupContext };
288
+ export { EFTemporal, clearTemporalCacheForElement, deepGetElementsWithFrameTasks, flushStartTimeMsCache, isEFTemporal, shallowGetTemporalElements, timegroupContext };
@@ -53,6 +53,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
53
53
  }
54
54
  #currentTime = 0;
55
55
  #resizeObserver;
56
+ #childObserver;
56
57
  set currentTime(time) {
57
58
  const newTime = Math.max(0, Math.min(time, this.durationMs / 1e3));
58
59
  if (this.isRootTimegroup && this.isFrameUpdateInProgress) {
@@ -128,6 +129,25 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
128
129
  });
129
130
  if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
130
131
  if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
132
+ this.#childObserver = new MutationObserver((mutations) => {
133
+ let shouldUpdate = false;
134
+ for (const mutation of mutations) if (mutation.type === "childList") shouldUpdate = true;
135
+ else if (mutation.type === "attributes") {
136
+ if (mutation.attributeName === "duration" || mutation.attributeName === "mode") shouldUpdate = true;
137
+ }
138
+ if (shouldUpdate) {
139
+ import("./EFTemporal.js").then(({ clearTemporalCacheForElement }) => {
140
+ clearTemporalCacheForElement(this);
141
+ });
142
+ this.requestUpdate();
143
+ }
144
+ });
145
+ this.#childObserver.observe(this, {
146
+ childList: true,
147
+ subtree: true,
148
+ attributes: true,
149
+ attributeFilter: ["duration", "mode"]
150
+ });
131
151
  requestAnimationFrame(() => {
132
152
  this.updateAnimations();
133
153
  });
@@ -135,6 +155,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
135
155
  disconnectedCallback() {
136
156
  super.disconnectedCallback();
137
157
  this.#resizeObserver?.disconnect();
158
+ this.#childObserver?.disconnect();
138
159
  }
139
160
  get storageKey() {
140
161
  if (!this.id) throw new Error("Timegroup must have an id to use localStorage.");
@@ -11,7 +11,6 @@ interface LoadingState {
11
11
  }
12
12
  declare const EFVideo_base: typeof EFMedia;
13
13
  export declare class EFVideo extends EFVideo_base {
14
- static get observedAttributes(): string[];
15
14
  static styles: import('lit').CSSResult[];
16
15
  canvasRef: import('lit-html/directives/ref').Ref<HTMLCanvasElement>;
17
16
  /**
@@ -15,15 +15,6 @@ import _decorate from "@oxc-project/runtime/helpers/decorate";
15
15
  import { createRef, ref } from "lit/directives/ref.js";
16
16
  const log = debug("ef:elements:EFVideo");
17
17
  let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
18
- static get observedAttributes() {
19
- const parentAttributes = EFMedia.observedAttributes || [];
20
- return [
21
- ...parentAttributes,
22
- "video-buffer-duration",
23
- "max-video-buffer-fetches",
24
- "enable-video-buffering"
25
- ];
26
- }
27
18
  static {
28
19
  this.styles = [css`
29
20
  :host {
@@ -21,7 +21,8 @@ var TargetRegistry = class {
21
21
  this.idMap.set(id, target);
22
22
  for (const callback of this.callbacks.get(id) ?? []) callback(target);
23
23
  }
24
- unregister(id) {
24
+ unregister(id, target) {
25
+ if (this.idMap.get(id) !== target) return;
25
26
  for (const callback of this.callbacks.get(id) ?? []) callback(void 0);
26
27
  this.idMap.delete(id);
27
28
  this.callbacks.delete(id);
@@ -46,7 +47,7 @@ const EFTargetable = (superClass) => {
46
47
  updateRegistry(oldValue, newValue) {
47
48
  if (!this.#registry) return;
48
49
  if (oldValue === newValue) return;
49
- if (oldValue) this.#registry.unregister(oldValue);
50
+ if (oldValue) this.#registry.unregister(oldValue, this);
50
51
  if (newValue) this.#registry.register(newValue, this);
51
52
  }
52
53
  connectedCallback() {
@@ -18,9 +18,9 @@ export declare const RenderInfo: z.ZodObject<{
18
18
  efImage: string[];
19
19
  }>;
20
20
  }, "strip", z.ZodTypeAny, {
21
+ durationMs: number;
21
22
  width: number;
22
23
  height: number;
23
- durationMs: number;
24
24
  fps: number;
25
25
  assets: {
26
26
  efMedia: Record<string, any>;
@@ -28,9 +28,9 @@ export declare const RenderInfo: z.ZodObject<{
28
28
  efImage: string[];
29
29
  };
30
30
  }, {
31
+ durationMs: number;
31
32
  width: number;
32
33
  height: number;
33
- durationMs: number;
34
34
  fps: number;
35
35
  assets: {
36
36
  efMedia: Record<string, any>;
@@ -2,9 +2,14 @@ import { LitElement } from 'lit';
2
2
  declare const TestContext_base: (new (...args: any[]) => import('./ContextMixin.js').ContextMixinInterface) & typeof LitElement;
3
3
  declare class TestContext extends TestContext_base {
4
4
  }
5
+ declare const TestContextElement_base: (new (...args: any[]) => import('./ContextMixin.js').ContextMixinInterface) & typeof LitElement;
6
+ declare class TestContextElement extends TestContextElement_base {
7
+ render(): import('../elements/EFTimegroup.js').EFTimegroup & Element;
8
+ }
5
9
  declare global {
6
10
  interface HTMLElementTagNameMap {
7
11
  "test-context": TestContext;
12
+ "test-context-reactivity": TestContextElement;
8
13
  }
9
14
  }
10
15
  export {};