@editframe/elements 0.19.4-beta.0 → 0.20.1-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 (132) hide show
  1. package/dist/elements/ContextProxiesController.d.ts +40 -0
  2. package/dist/elements/ContextProxiesController.js +69 -0
  3. package/dist/elements/EFCaptions.d.ts +45 -6
  4. package/dist/elements/EFCaptions.js +220 -26
  5. package/dist/elements/EFImage.js +4 -1
  6. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
  7. package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
  8. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
  9. package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
  10. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
  11. package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
  12. package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
  13. package/dist/elements/EFMedia/JitMediaEngine.js +15 -0
  14. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +2 -1
  15. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +2 -0
  16. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +1 -1
  17. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +3 -1
  18. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +1 -1
  19. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +1 -1
  20. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +6 -5
  21. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +3 -1
  22. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +2 -0
  23. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +2 -2
  24. package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
  25. package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
  26. package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
  27. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
  28. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -1
  29. package/dist/elements/EFMedia.d.ts +2 -2
  30. package/dist/elements/EFMedia.js +25 -1
  31. package/dist/elements/EFSurface.browsertest.d.ts +0 -0
  32. package/dist/elements/EFSurface.d.ts +30 -0
  33. package/dist/elements/EFSurface.js +96 -0
  34. package/dist/elements/EFTemporal.js +7 -6
  35. package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
  36. package/dist/elements/EFThumbnailStrip.d.ts +86 -0
  37. package/dist/elements/EFThumbnailStrip.js +490 -0
  38. package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
  39. package/dist/elements/EFTimegroup.d.ts +6 -1
  40. package/dist/elements/EFTimegroup.js +53 -11
  41. package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
  42. package/dist/elements/updateAnimations.d.ts +5 -0
  43. package/dist/elements/updateAnimations.js +37 -13
  44. package/dist/getRenderInfo.js +1 -1
  45. package/dist/gui/ContextMixin.js +27 -14
  46. package/dist/gui/EFControls.browsertest.d.ts +0 -0
  47. package/dist/gui/EFControls.d.ts +38 -0
  48. package/dist/gui/EFControls.js +51 -0
  49. package/dist/gui/EFFilmstrip.d.ts +40 -1
  50. package/dist/gui/EFFilmstrip.js +240 -3
  51. package/dist/gui/EFPreview.js +2 -1
  52. package/dist/gui/EFScrubber.d.ts +6 -5
  53. package/dist/gui/EFScrubber.js +31 -21
  54. package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
  55. package/dist/gui/EFTimeDisplay.d.ts +2 -6
  56. package/dist/gui/EFTimeDisplay.js +13 -23
  57. package/dist/gui/TWMixin.js +1 -1
  58. package/dist/gui/currentTimeContext.d.ts +3 -0
  59. package/dist/gui/currentTimeContext.js +3 -0
  60. package/dist/gui/durationContext.d.ts +3 -0
  61. package/dist/gui/durationContext.js +3 -0
  62. package/dist/index.d.ts +3 -0
  63. package/dist/index.js +4 -1
  64. package/dist/style.css +1 -1
  65. package/dist/transcoding/types/index.d.ts +11 -0
  66. package/dist/utils/LRUCache.d.ts +46 -0
  67. package/dist/utils/LRUCache.js +382 -1
  68. package/dist/utils/LRUCache.test.d.ts +1 -0
  69. package/package.json +2 -2
  70. package/src/elements/ContextProxiesController.ts +124 -0
  71. package/src/elements/EFCaptions.browsertest.ts +1820 -0
  72. package/src/elements/EFCaptions.ts +373 -36
  73. package/src/elements/EFImage.ts +4 -1
  74. package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
  75. package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
  76. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
  77. package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
  78. package/src/elements/EFMedia/JitMediaEngine.ts +34 -0
  79. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +6 -5
  80. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +5 -0
  81. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +8 -5
  82. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +5 -5
  83. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +11 -12
  84. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +7 -4
  85. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +5 -0
  86. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +2 -2
  87. package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
  88. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +2 -2
  89. package/src/elements/EFMedia/shared/RenditionHelpers.ts +2 -2
  90. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
  91. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +1 -1
  92. package/src/elements/EFMedia.ts +38 -1
  93. package/src/elements/EFSurface.browsertest.ts +155 -0
  94. package/src/elements/EFSurface.ts +141 -0
  95. package/src/elements/EFTemporal.ts +14 -8
  96. package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
  97. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
  98. package/src/elements/EFThumbnailStrip.ts +905 -0
  99. package/src/elements/EFTimegroup.browsertest.ts +56 -7
  100. package/src/elements/EFTimegroup.ts +88 -16
  101. package/src/elements/updateAnimations.browsertest.ts +333 -11
  102. package/src/elements/updateAnimations.ts +68 -19
  103. package/src/gui/ContextMixin.browsertest.ts +0 -25
  104. package/src/gui/ContextMixin.ts +44 -20
  105. package/src/gui/EFControls.browsertest.ts +175 -0
  106. package/src/gui/EFControls.ts +84 -0
  107. package/src/gui/EFFilmstrip.ts +323 -4
  108. package/src/gui/EFPreview.ts +2 -1
  109. package/src/gui/EFScrubber.ts +29 -25
  110. package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
  111. package/src/gui/EFTimeDisplay.ts +12 -40
  112. package/src/gui/currentTimeContext.ts +5 -0
  113. package/src/gui/durationContext.ts +3 -0
  114. package/src/transcoding/types/index.ts +13 -0
  115. package/src/utils/LRUCache.test.ts +272 -0
  116. package/src/utils/LRUCache.ts +543 -0
  117. 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
  118. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +1 -1
  119. 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
  120. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +1 -1
  121. 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
  122. package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +1 -1
  123. 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
  124. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +1 -1
  125. 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
  126. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +1 -1
  127. 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
  128. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -1
  129. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +1 -1
  130. package/types.json +1 -1
  131. package/dist/transcoding/cache/CacheManager.d.ts +0 -73
  132. package/src/transcoding/cache/CacheManager.ts +0 -208
@@ -0,0 +1,40 @@
1
+ import { Context } from '@lit/context';
2
+ import { LitElement, ReactiveController } from 'lit';
3
+ /**
4
+ * Configuration for context proxying
5
+ */
6
+ export type ContextProxyConfig = {
7
+ target: () => HTMLElement | null;
8
+ contexts: Context<any, any>[];
9
+ };
10
+ /**
11
+ * A ReactiveController that proxies context requests to a target element.
12
+ *
13
+ * Usage:
14
+ * ```typescript
15
+ * @customElement('my-proxy')
16
+ * class MyProxy extends LitElement {
17
+ * @state()
18
+ * targetElement: HTMLElement | null = null;
19
+ *
20
+ * // @ts-expect-error controller is intentionally not referenced directly
21
+ * #contextProxyController = new ContextProxyController(this, {
22
+ * target: () => this.targetElement,
23
+ * contexts: [playingContext, loopContext, targetTimegroupContext]
24
+ * });
25
+ * }
26
+ * ```
27
+ */
28
+ export declare class ContextProxyController implements ReactiveController {
29
+ private host;
30
+ private proxyMap;
31
+ private boundHandler;
32
+ private pendingRequests;
33
+ constructor(host: LitElement, config: ContextProxyConfig);
34
+ hostConnected(): void;
35
+ hostDisconnected(): void;
36
+ hostUpdate(): void;
37
+ private processPendingRequests;
38
+ private handleContextRequest;
39
+ private processContextRequest;
40
+ }
@@ -0,0 +1,69 @@
1
+ import { ContextEvent } from "@lit/context";
2
+ /**
3
+ * A ReactiveController that proxies context requests to a target element.
4
+ *
5
+ * Usage:
6
+ * ```typescript
7
+ * @customElement('my-proxy')
8
+ * class MyProxy extends LitElement {
9
+ * @state()
10
+ * targetElement: HTMLElement | null = null;
11
+ *
12
+ * // @ts-expect-error controller is intentionally not referenced directly
13
+ * #contextProxyController = new ContextProxyController(this, {
14
+ * target: () => this.targetElement,
15
+ * contexts: [playingContext, loopContext, targetTimegroupContext]
16
+ * });
17
+ * }
18
+ * ```
19
+ */
20
+ var ContextProxyController = class {
21
+ constructor(host, config) {
22
+ this.proxyMap = /* @__PURE__ */ new Map();
23
+ this.pendingRequests = [];
24
+ this.host = host;
25
+ this.host.addController(this);
26
+ this.boundHandler = this.handleContextRequest.bind(this);
27
+ for (const context of config.contexts) this.proxyMap.set(context, config.target);
28
+ }
29
+ hostConnected() {
30
+ this.host.addEventListener("context-request", this.boundHandler);
31
+ }
32
+ hostDisconnected() {
33
+ this.host.removeEventListener("context-request", this.boundHandler);
34
+ }
35
+ hostUpdate() {
36
+ this.processPendingRequests();
37
+ }
38
+ processPendingRequests() {
39
+ if (this.pendingRequests.length === 0) return;
40
+ const requestsToProcess = [...this.pendingRequests];
41
+ this.pendingRequests = [];
42
+ for (const contextEvent of requestsToProcess) this.processContextRequest(contextEvent);
43
+ }
44
+ handleContextRequest(event) {
45
+ const contextEvent = event;
46
+ const targetGetter = this.proxyMap.get(contextEvent.context);
47
+ if (targetGetter) {
48
+ contextEvent.stopPropagation();
49
+ const processed = this.processContextRequest(contextEvent);
50
+ if (!processed) this.pendingRequests.push(contextEvent);
51
+ }
52
+ }
53
+ processContextRequest(contextEvent) {
54
+ const targetGetter = this.proxyMap.get(contextEvent.context);
55
+ if (!targetGetter) return false;
56
+ const targetElement = targetGetter();
57
+ if (!targetElement) return false;
58
+ const tempElement = document.createElement("div");
59
+ targetElement.appendChild(tempElement);
60
+ try {
61
+ const newEvent = new ContextEvent(contextEvent.context, contextEvent.callback, contextEvent.subscribe);
62
+ tempElement.dispatchEvent(newEvent);
63
+ return true;
64
+ } finally {
65
+ targetElement.removeChild(tempElement);
66
+ }
67
+ }
68
+ };
69
+ export { ContextProxyController };
@@ -1,7 +1,21 @@
1
- import { Task } from '@lit/task';
1
+ import { Task, TaskStatus } from '@lit/task';
2
2
  import { LitElement, PropertyValueMap } from 'lit';
3
3
  import { EFAudio } from './EFAudio.js';
4
4
  import { EFVideo } from './EFVideo.js';
5
+ export interface WordSegment {
6
+ text: string;
7
+ start: number;
8
+ end: number;
9
+ }
10
+ export interface Segment {
11
+ start: number;
12
+ end: number;
13
+ text: string;
14
+ }
15
+ export interface Caption {
16
+ segments: Segment[];
17
+ word_segments: WordSegment[];
18
+ }
5
19
  declare const EFCaptionsActiveWord_base: (new (...args: any[]) => import('./EFTemporal.js').TemporalMixinInterface) & typeof LitElement;
6
20
  export declare class EFCaptionsActiveWord extends EFCaptionsActiveWord_base {
7
21
  static styles: import('lit').CSSResult[];
@@ -9,8 +23,10 @@ export declare class EFCaptionsActiveWord extends EFCaptionsActiveWord_base {
9
23
  wordStartMs: number;
10
24
  wordEndMs: number;
11
25
  wordText: string;
26
+ wordIndex: number;
12
27
  hidden: boolean;
13
28
  get startTimeMs(): number;
29
+ get endTimeMs(): number;
14
30
  get durationMs(): number;
15
31
  }
16
32
  declare const EFCaptionsSegment_base: (new (...args: any[]) => import('./EFTemporal.js').TemporalMixinInterface) & typeof LitElement;
@@ -22,6 +38,7 @@ export declare class EFCaptionsSegment extends EFCaptionsSegment_base {
22
38
  segmentText: string;
23
39
  hidden: boolean;
24
40
  get startTimeMs(): number;
41
+ get endTimeMs(): number;
25
42
  get durationMs(): number;
26
43
  }
27
44
  export declare class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
@@ -32,6 +49,7 @@ export declare class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
32
49
  segmentStartMs: number;
33
50
  segmentEndMs: number;
34
51
  get startTimeMs(): number;
52
+ get endTimeMs(): number;
35
53
  get durationMs(): number;
36
54
  }
37
55
  export declare class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
@@ -42,6 +60,7 @@ export declare class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
42
60
  segmentStartMs: number;
43
61
  segmentEndMs: number;
44
62
  get startTimeMs(): number;
63
+ get endTimeMs(): number;
45
64
  get durationMs(): number;
46
65
  }
47
66
  declare const EFCaptions_base: (new (...args: any[]) => import('./EFSourceMixin.js').EFSourceMixinInterface) & (new (...args: any[]) => import('./EFTemporal.js').TemporalMixinInterface) & (new (...args: any[]) => import('./FetchMixin.js').FetchMixinInterface) & typeof LitElement;
@@ -52,23 +71,43 @@ export declare class EFCaptions extends EFCaptions_base {
52
71
  targetSelector: string;
53
72
  set target(value: string);
54
73
  wordStyle: string;
74
+ /**
75
+ * URL or path to a JSON file containing custom captions data.
76
+ * The JSON should conform to the Caption interface with 'segments' and 'word_segments' arrays.
77
+ */
78
+ captionsSrc: string;
79
+ /**
80
+ * Direct captions data object. Takes priority over captions-src and captions-script.
81
+ * Should conform to the Caption interface with 'segments' and 'word_segments' arrays.
82
+ */
83
+ captionsData: Caption | null;
84
+ /**
85
+ * ID of a <script> element containing JSON captions data.
86
+ * The script's textContent should be valid JSON conforming to the Caption interface.
87
+ */
88
+ captionsScript: string;
55
89
  activeWordContainers: HTMLCollectionOf<EFCaptionsActiveWord>;
56
90
  segmentContainers: HTMLCollectionOf<EFCaptionsSegment>;
57
91
  beforeActiveWordContainers: HTMLCollectionOf<EFCaptionsBeforeActiveWord>;
58
92
  afterActiveWordContainers: HTMLCollectionOf<EFCaptionsAfterActiveWord>;
59
93
  render(): import('lit-html').TemplateResult<1>;
60
94
  transcriptionsPath(): string | null;
61
- captionsPath(): string;
62
- protected md5SumLoader: Task<readonly [string, typeof fetch], string | undefined>;
95
+ captionsPath(): string | null;
96
+ protected md5SumLoader: Task<readonly [string, typeof fetch], string | null | undefined>;
63
97
  private transcriptionDataTask;
64
98
  private transcriptionFragmentPath;
65
99
  private fragmentIndexTask;
100
+ private customCaptionsDataTask;
66
101
  private transcriptionFragmentDataTask;
67
- frameTask: Task<import('@lit/task').TaskStatus[], void>;
102
+ unifiedCaptionsDataTask: Task<readonly [Caption | null | undefined, Caption | null | undefined], Caption | null | undefined>;
103
+ frameTask: Task<TaskStatus[], void>;
68
104
  connectedCallback(): void;
69
- protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
105
+ protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
70
106
  updateTextContainers(): void;
71
- get targetElement(): EFAudio | EFVideo;
107
+ get targetElement(): EFAudio | EFVideo | null;
108
+ get hasCustomCaptionsData(): boolean;
109
+ get intrinsicDurationMs(): number | undefined;
110
+ get hasOwnDuration(): boolean;
72
111
  }
73
112
  declare global {
74
113
  interface HTMLElementTagNameMap {
@@ -1,11 +1,12 @@
1
1
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
2
2
  import { EFSourceMixin } from "./EFSourceMixin.js";
3
- import { EFTemporal } from "./EFTemporal.js";
3
+ import { EFTemporal, flushStartTimeMsCache } from "./EFTemporal.js";
4
4
  import { FetchMixin } from "./FetchMixin.js";
5
+ import { flushSequenceDurationCache } from "./EFTimegroup.js";
5
6
  import { EFAudio } from "./EFAudio.js";
6
7
  import { EFVideo } from "./EFVideo.js";
7
8
  import { CrossUpdateController } from "./CrossUpdateController.js";
8
- import { Task } from "@lit/task";
9
+ import { Task, TaskStatus } from "@lit/task";
9
10
  import { LitElement, css, html } from "lit";
10
11
  import { customElement, property } from "lit/decorators.js";
11
12
  import _decorate from "@oxc-project/runtime/helpers/decorate";
@@ -22,6 +23,7 @@ let EFCaptionsActiveWord = class EFCaptionsActiveWord$1 extends EFTemporal(LitEl
22
23
  this.wordStartMs = 0;
23
24
  this.wordEndMs = 0;
24
25
  this.wordText = "";
26
+ this.wordIndex = 0;
25
27
  this.hidden = false;
26
28
  }
27
29
  static {
@@ -29,6 +31,7 @@ let EFCaptionsActiveWord = class EFCaptionsActiveWord$1 extends EFTemporal(LitEl
29
31
  :host {
30
32
  display: inline-block;
31
33
  white-space: pre;
34
+ transform-origin: center;
32
35
  }
33
36
  :host([hidden]) {
34
37
  display: none;
@@ -41,10 +44,20 @@ let EFCaptionsActiveWord = class EFCaptionsActiveWord$1 extends EFTemporal(LitEl
41
44
  return void 0;
42
45
  }
43
46
  this.hidden = false;
47
+ const seed = this.wordIndex * 9007 % 233;
48
+ const seedValue = seed / 233;
49
+ this.style.setProperty("--ef-word-seed", seedValue.toString());
44
50
  return html` ${this.wordText.trim()} `;
45
51
  }
46
52
  get startTimeMs() {
47
- return this.wordStartMs || 0;
53
+ const parentCaptions = this.closest("ef-captions");
54
+ const parentStartTime = parentCaptions?.startTimeMs || 0;
55
+ return parentStartTime + (this.wordStartMs || 0);
56
+ }
57
+ get endTimeMs() {
58
+ const parentCaptions = this.closest("ef-captions");
59
+ const parentStartTime = parentCaptions?.startTimeMs || 0;
60
+ return parentStartTime + (this.wordEndMs || 0);
48
61
  }
49
62
  get durationMs() {
50
63
  return this.wordEndMs - this.wordStartMs;
@@ -62,6 +75,10 @@ _decorate([property({
62
75
  type: String,
63
76
  attribute: false
64
77
  })], EFCaptionsActiveWord.prototype, "wordText", void 0);
78
+ _decorate([property({
79
+ type: Number,
80
+ attribute: false
81
+ })], EFCaptionsActiveWord.prototype, "wordIndex", void 0);
65
82
  _decorate([property({
66
83
  type: Boolean,
67
84
  reflect: true
@@ -94,7 +111,14 @@ let EFCaptionsSegment = class EFCaptionsSegment$1 extends EFTemporal(LitElement)
94
111
  return html`${this.segmentText}`;
95
112
  }
96
113
  get startTimeMs() {
97
- return this.segmentStartMs || 0;
114
+ const parentCaptions = this.closest("ef-captions");
115
+ const parentStartTime = parentCaptions?.startTimeMs || 0;
116
+ return parentStartTime + (this.segmentStartMs || 0);
117
+ }
118
+ get endTimeMs() {
119
+ const parentCaptions = this.closest("ef-captions");
120
+ const parentStartTime = parentCaptions?.startTimeMs || 0;
121
+ return parentStartTime + (this.segmentEndMs || 0);
98
122
  }
99
123
  get durationMs() {
100
124
  return this.segmentEndMs - this.segmentStartMs;
@@ -145,7 +169,14 @@ let EFCaptionsBeforeActiveWord = class EFCaptionsBeforeActiveWord$1 extends EFCa
145
169
  return html` ${this.segmentText}`;
146
170
  }
147
171
  get startTimeMs() {
148
- return this.segmentStartMs || 0;
172
+ const parentCaptions = this.closest("ef-captions");
173
+ const parentStartTime = parentCaptions?.startTimeMs || 0;
174
+ return parentStartTime + (this.segmentStartMs || 0);
175
+ }
176
+ get endTimeMs() {
177
+ const parentCaptions = this.closest("ef-captions");
178
+ const parentStartTime = parentCaptions?.startTimeMs || 0;
179
+ return parentStartTime + (this.segmentEndMs || 0);
149
180
  }
150
181
  get durationMs() {
151
182
  return this.segmentEndMs - this.segmentStartMs;
@@ -196,7 +227,14 @@ let EFCaptionsAfterActiveWord = class EFCaptionsAfterActiveWord$1 extends EFCapt
196
227
  return html`${this.segmentText} `;
197
228
  }
198
229
  get startTimeMs() {
199
- return this.segmentStartMs || 0;
230
+ const parentCaptions = this.closest("ef-captions");
231
+ const parentStartTime = parentCaptions?.startTimeMs || 0;
232
+ return parentStartTime + (this.segmentStartMs || 0);
233
+ }
234
+ get endTimeMs() {
235
+ const parentCaptions = this.closest("ef-captions");
236
+ const parentStartTime = parentCaptions?.startTimeMs || 0;
237
+ return parentStartTime + (this.segmentEndMs || 0);
200
238
  }
201
239
  get durationMs() {
202
240
  return this.segmentEndMs - this.segmentStartMs;
@@ -226,6 +264,9 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
226
264
  this.contextWords = 3;
227
265
  this.targetSelector = "";
228
266
  this.wordStyle = "";
267
+ this.captionsSrc = "";
268
+ this.captionsData = null;
269
+ this.captionsScript = "";
229
270
  this.activeWordContainers = this.getElementsByTagName("ef-captions-active-word");
230
271
  this.segmentContainers = this.getElementsByTagName("ef-captions-segment");
231
272
  this.beforeActiveWordContainers = this.getElementsByTagName("ef-captions-before-active-word");
@@ -234,6 +275,7 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
234
275
  autoRun: false,
235
276
  args: () => [this.target, this.fetch],
236
277
  task: async ([_target, fetch], { signal }) => {
278
+ if (!this.targetElement) return null;
237
279
  const md5Path = `/@ef-asset/${this.targetElement.src ?? ""}`;
238
280
  const response = await fetch(md5Path, {
239
281
  method: "HEAD",
@@ -244,9 +286,13 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
244
286
  });
245
287
  this.transcriptionDataTask = new Task(this, {
246
288
  autoRun: EF_INTERACTIVE,
247
- args: () => [this.transcriptionsPath(), this.fetch],
248
- task: async ([transcriptionsPath, fetch], { signal }) => {
249
- if (!transcriptionsPath) return null;
289
+ args: () => [
290
+ this.transcriptionsPath(),
291
+ this.fetch,
292
+ this.hasCustomCaptionsData
293
+ ],
294
+ task: async ([transcriptionsPath, fetch, hasCustomData], { signal }) => {
295
+ if (hasCustomData || !transcriptionsPath) return null;
250
296
  const response = await fetch(transcriptionsPath, { signal });
251
297
  return response.json();
252
298
  }
@@ -260,6 +306,35 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
260
306
  return fragmentIndex;
261
307
  }
262
308
  });
309
+ this.customCaptionsDataTask = new Task(this, {
310
+ autoRun: EF_INTERACTIVE,
311
+ args: () => [
312
+ this.captionsSrc,
313
+ this.captionsData,
314
+ this.captionsScript,
315
+ this.fetch
316
+ ],
317
+ task: async ([captionsSrc, captionsData, captionsScript, fetch], { signal }) => {
318
+ if (captionsData) return captionsData;
319
+ if (captionsScript) {
320
+ const scriptElement = document.getElementById(captionsScript);
321
+ if (scriptElement?.textContent) try {
322
+ return JSON.parse(scriptElement.textContent);
323
+ } catch (error) {
324
+ console.error(`Failed to parse captions from script #${captionsScript}:`, error);
325
+ return null;
326
+ }
327
+ }
328
+ if (captionsSrc) try {
329
+ const response = await fetch(captionsSrc, { signal });
330
+ return await response.json();
331
+ } catch (error) {
332
+ console.error(`Failed to load captions from ${captionsSrc}:`, error);
333
+ return null;
334
+ }
335
+ return null;
336
+ }
337
+ });
263
338
  this.transcriptionFragmentDataTask = new Task(this, {
264
339
  autoRun: EF_INTERACTIVE,
265
340
  args: () => [
@@ -274,26 +349,43 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
274
349
  return response.json();
275
350
  }
276
351
  });
352
+ this.unifiedCaptionsDataTask = new Task(this, {
353
+ autoRun: EF_INTERACTIVE,
354
+ args: () => [this.customCaptionsDataTask.value, this.transcriptionFragmentDataTask.value],
355
+ task: async ([_customData, _transcriptionData]) => {
356
+ if (this.customCaptionsDataTask.status === TaskStatus.PENDING) await this.customCaptionsDataTask.taskComplete;
357
+ if (this.transcriptionFragmentDataTask.status === TaskStatus.PENDING) await this.transcriptionFragmentDataTask.taskComplete;
358
+ return this.customCaptionsDataTask.value || this.transcriptionFragmentDataTask.value;
359
+ }
360
+ });
277
361
  this.frameTask = new Task(this, {
278
362
  autoRun: EF_INTERACTIVE,
279
- args: () => [this.transcriptionFragmentDataTask.status],
363
+ args: () => [this.unifiedCaptionsDataTask.status],
280
364
  task: async () => {
281
- await this.transcriptionFragmentDataTask.taskComplete;
365
+ await this.unifiedCaptionsDataTask.taskComplete;
282
366
  }
283
367
  });
284
368
  }
285
369
  static {
286
370
  this.styles = [css`
287
371
  :host {
288
- display: flex;
289
- flex-wrap: wrap;
290
- align-items: baseline;
372
+ display: inline-flex;
291
373
  width: fit-content;
374
+ align-items: baseline;
292
375
  }
293
376
  ::slotted(*) {
294
377
  margin: 0;
295
378
  padding: 0;
296
379
  }
380
+ ::slotted(ef-captions-active-word) {
381
+ min-width: 0.5ch; /* Maintain minimum width when empty */
382
+ min-height: 1em; /* Maintain height for baseline alignment */
383
+ }
384
+ ::slotted(ef-captions-active-word[hidden]) {
385
+ opacity: 0; /* Hide when empty but maintain layout */
386
+ min-width: 0.5ch;
387
+ min-height: 1em;
388
+ }
297
389
  `];
298
390
  }
299
391
  set target(value) {
@@ -303,10 +395,12 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
303
395
  return html`<slot></slot>`;
304
396
  }
305
397
  transcriptionsPath() {
398
+ if (!this.targetElement) return null;
306
399
  if (this.targetElement.assetId) return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;
307
400
  return null;
308
401
  }
309
402
  captionsPath() {
403
+ if (!this.targetElement) return null;
310
404
  if (this.targetElement.assetId) return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;
311
405
  const targetSrc = this.targetElement.src;
312
406
  return `/@ef-captions/${targetSrc}`;
@@ -316,51 +410,137 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
316
410
  }
317
411
  connectedCallback() {
318
412
  super.connectedCallback();
319
- if (this.targetElement) new CrossUpdateController(this.targetElement, this);
413
+ const target = this.targetSelector ? document.getElementById(this.targetSelector) : null;
414
+ if (target && (target instanceof EFAudio || target instanceof EFVideo)) new CrossUpdateController(target, this);
415
+ else if (this.hasCustomCaptionsData && this.rootTimegroup) new CrossUpdateController(this.rootTimegroup, this);
320
416
  }
321
- updated(_changedProperties) {
417
+ updated(changedProperties) {
322
418
  this.updateTextContainers();
419
+ if (changedProperties.has("captionsData") || changedProperties.has("captionsSrc") || changedProperties.has("captionsScript")) {
420
+ this.requestUpdate("intrinsicDurationMs");
421
+ flushSequenceDurationCache();
422
+ flushStartTimeMsCache();
423
+ if (this.parentTimegroup) {
424
+ this.parentTimegroup.requestUpdate("durationMs");
425
+ this.parentTimegroup.requestUpdate("currentTime");
426
+ }
427
+ }
428
+ if (changedProperties.has("ownCurrentTimeMs")) this.updateTextContainers();
323
429
  }
324
430
  updateTextContainers() {
325
- const transcriptionFragment = this.transcriptionFragmentDataTask.value;
326
- if (!transcriptionFragment) return;
327
- const currentTimeMs = this.targetElement.currentSourceTimeMs;
431
+ const captionsData = this.unifiedCaptionsDataTask.value;
432
+ if (!captionsData) return;
433
+ const currentTimeMs = this.targetElement ? this.targetElement.currentSourceTimeMs : this.ownCurrentTimeMs;
328
434
  const currentTimeSec = currentTimeMs / 1e3;
329
- const currentWord = transcriptionFragment.word_segments.find((word) => currentTimeSec >= word.start && currentTimeSec <= word.end);
330
- const currentSegment = transcriptionFragment.segments.find((segment) => currentTimeSec >= segment.start && currentTimeSec <= segment.end);
435
+ const currentWord = captionsData.word_segments.find((word) => currentTimeSec >= word.start && currentTimeSec < word.end);
436
+ const currentSegment = captionsData.segments.find((segment) => currentTimeSec >= segment.start && currentTimeSec < segment.end);
331
437
  for (const wordContainer of this.activeWordContainers) if (currentWord) {
332
438
  wordContainer.wordText = currentWord.text;
333
439
  wordContainer.wordStartMs = currentWord.start * 1e3;
334
440
  wordContainer.wordEndMs = currentWord.end * 1e3;
441
+ const wordIndex = captionsData.word_segments.findIndex((w) => w.start === currentWord.start && w.end === currentWord.end && w.text === currentWord.text);
442
+ wordContainer.wordIndex = wordIndex >= 0 ? wordIndex : 0;
443
+ wordContainer.requestUpdate();
444
+ } else {
445
+ wordContainer.wordText = "";
446
+ wordContainer.wordStartMs = 0;
447
+ wordContainer.wordEndMs = 0;
448
+ wordContainer.requestUpdate();
335
449
  }
336
450
  for (const segmentContainer of this.segmentContainers) if (currentSegment) {
337
451
  segmentContainer.segmentText = currentSegment.text;
338
452
  segmentContainer.segmentStartMs = currentSegment.start * 1e3;
339
453
  segmentContainer.segmentEndMs = currentSegment.end * 1e3;
454
+ } else {
455
+ segmentContainer.segmentText = "";
456
+ segmentContainer.segmentStartMs = 0;
457
+ segmentContainer.segmentEndMs = 0;
340
458
  }
341
459
  if (currentWord && currentSegment) {
342
- const segmentWords = transcriptionFragment.word_segments.filter((word) => word.start >= currentSegment.start && word.end <= currentSegment.end);
460
+ const segmentWords = captionsData.word_segments.filter((word) => word.start >= currentSegment.start && word.end <= currentSegment.end);
343
461
  const currentWordIndex = segmentWords.findIndex((word) => word.start === currentWord.start && word.end === currentWord.end);
344
462
  if (currentWordIndex !== -1) {
345
463
  const beforeWords = segmentWords.slice(0, currentWordIndex).map((w) => w.text.trim()).join(" ");
346
464
  const afterWords = segmentWords.slice(currentWordIndex + 1).map((w) => w.text.trim()).join(" ");
347
465
  for (const container of this.beforeActiveWordContainers) {
348
466
  container.segmentText = beforeWords;
349
- container.segmentStartMs = currentSegment.start * 1e3;
350
- container.segmentEndMs = currentWord.start * 1e3;
467
+ container.segmentStartMs = currentWord.start * 1e3;
468
+ container.segmentEndMs = currentWord.end * 1e3;
351
469
  }
352
470
  for (const container of this.afterActiveWordContainers) {
353
471
  container.segmentText = afterWords;
354
- container.segmentStartMs = currentWord.end * 1e3;
472
+ container.segmentStartMs = currentWord.start * 1e3;
473
+ container.segmentEndMs = currentWord.end * 1e3;
474
+ }
475
+ }
476
+ } else if (currentSegment) {
477
+ const segmentWords = captionsData.word_segments.filter((word) => word.start >= currentSegment.start && word.end <= currentSegment.end);
478
+ const firstWord = segmentWords[0];
479
+ const isBeforeFirstWord = firstWord && currentTimeSec < firstWord.start;
480
+ if (isBeforeFirstWord) {
481
+ const allWords = segmentWords.map((w) => w.text.trim()).join(" ");
482
+ for (const container of this.beforeActiveWordContainers) {
483
+ container.segmentText = "";
484
+ container.segmentStartMs = currentSegment.start * 1e3;
485
+ container.segmentEndMs = currentSegment.end * 1e3;
486
+ }
487
+ for (const container of this.afterActiveWordContainers) {
488
+ container.segmentText = allWords;
489
+ container.segmentStartMs = currentSegment.start * 1e3;
490
+ container.segmentEndMs = currentSegment.end * 1e3;
491
+ }
492
+ } else {
493
+ const allCompletedWords = segmentWords.map((w) => w.text.trim()).join(" ");
494
+ for (const container of this.beforeActiveWordContainers) {
495
+ container.segmentText = allCompletedWords;
496
+ container.segmentStartMs = currentSegment.start * 1e3;
497
+ container.segmentEndMs = currentSegment.end * 1e3;
498
+ }
499
+ for (const container of this.afterActiveWordContainers) {
500
+ container.segmentText = "";
501
+ container.segmentStartMs = currentSegment.start * 1e3;
355
502
  container.segmentEndMs = currentSegment.end * 1e3;
356
503
  }
357
504
  }
505
+ } else {
506
+ for (const container of this.beforeActiveWordContainers) {
507
+ container.segmentText = "";
508
+ container.segmentStartMs = 0;
509
+ container.segmentEndMs = 0;
510
+ }
511
+ for (const container of this.afterActiveWordContainers) {
512
+ container.segmentText = "";
513
+ container.segmentStartMs = 0;
514
+ container.segmentEndMs = 0;
515
+ }
358
516
  }
359
517
  }
360
518
  get targetElement() {
361
519
  const target = document.getElementById(this.targetSelector ?? "");
362
520
  if (target instanceof EFAudio || target instanceof EFVideo) return target;
363
- throw new Error("Invalid target, must be an EFAudio or EFVideo element");
521
+ if (this.hasCustomCaptionsData) return null;
522
+ return null;
523
+ }
524
+ get hasCustomCaptionsData() {
525
+ return !!(this.captionsData || this.captionsSrc || this.captionsScript);
526
+ }
527
+ get intrinsicDurationMs() {
528
+ let captionsData = null;
529
+ if (this.captionsData) captionsData = this.captionsData;
530
+ else if (this.captionsScript) {
531
+ const scriptElement = document.getElementById(this.captionsScript);
532
+ if (scriptElement?.textContent) try {
533
+ captionsData = JSON.parse(scriptElement.textContent);
534
+ } catch {}
535
+ } else if (this.customCaptionsDataTask.value) captionsData = this.customCaptionsDataTask.value;
536
+ if (!captionsData) return void 0;
537
+ if (captionsData.segments.length === 0 && captionsData.word_segments.length === 0) return 0;
538
+ const maxSegmentEnd = captionsData.segments.length > 0 ? Math.max(...captionsData.segments.map((s) => s.end)) : 0;
539
+ const maxWordEnd = captionsData.word_segments.length > 0 ? Math.max(...captionsData.word_segments.map((w) => w.end)) : 0;
540
+ return Math.max(maxSegmentEnd, maxWordEnd) * 1e3;
541
+ }
542
+ get hasOwnDuration() {
543
+ return !!(this.captionsData || this.captionsScript || this.customCaptionsDataTask.value);
364
544
  }
365
545
  };
366
546
  _decorate([property({
@@ -379,5 +559,19 @@ _decorate([property({
379
559
  reflect: true
380
560
  })], EFCaptions.prototype, "targetSelector", void 0);
381
561
  _decorate([property({ attribute: "word-style" })], EFCaptions.prototype, "wordStyle", void 0);
562
+ _decorate([property({
563
+ type: String,
564
+ attribute: "captions-src",
565
+ reflect: true
566
+ })], EFCaptions.prototype, "captionsSrc", void 0);
567
+ _decorate([property({
568
+ type: Object,
569
+ attribute: false
570
+ })], EFCaptions.prototype, "captionsData", void 0);
571
+ _decorate([property({
572
+ type: String,
573
+ attribute: "captions-script",
574
+ reflect: true
575
+ })], EFCaptions.prototype, "captionsScript", void 0);
382
576
  EFCaptions = _decorate([customElement("ef-captions")], EFCaptions);
383
577
  export { EFCaptions, EFCaptionsActiveWord, EFCaptionsAfterActiveWord, EFCaptionsBeforeActiveWord, EFCaptionsSegment };
@@ -49,7 +49,10 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
49
49
  justify-content: center;
50
50
  }
51
51
  canvas, img {
52
- all: inherit;
52
+ position: static;
53
+ all: initial;
54
+ width: 100%;
55
+ height: 100%;
53
56
  }
54
57
  `];
55
58
  }
@@ -1,5 +1,5 @@
1
1
  import { TrackFragmentIndex } from '../../../../assets/src/index.ts';
2
- import { InitSegmentPaths, MediaEngine } from '../../transcoding/types';
2
+ import { InitSegmentPaths, MediaEngine, VideoRendition } from '../../transcoding/types';
3
3
  import { UrlGenerator } from '../../transcoding/utils/UrlGenerator';
4
4
  import { EFMedia } from '../EFMedia';
5
5
  import { AssetMediaEngine } from './AssetMediaEngine';
@@ -15,4 +15,5 @@ export declare class AssetIdMediaEngine extends AssetMediaEngine implements Medi
15
15
  };
16
16
  buildInitSegmentUrl(trackId: number): string;
17
17
  buildMediaSegmentUrl(trackId: number, _segmentId: number): string;
18
+ convertToSegmentRelativeTimestamps(globalTimestamps: number[], segmentId: number, rendition: VideoRendition): number[];
18
19
  }
@@ -40,5 +40,14 @@ var AssetIdMediaEngine = class AssetIdMediaEngine extends AssetMediaEngine {
40
40
  buildMediaSegmentUrl(trackId, _segmentId) {
41
41
  return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
42
42
  }
43
+ convertToSegmentRelativeTimestamps(globalTimestamps, segmentId, rendition) {
44
+ if (!rendition.trackId) throw new Error("Track ID is required for asset metadata");
45
+ const trackData = this.data[rendition.trackId];
46
+ if (!trackData) throw new Error("Track not found");
47
+ const segment = trackData.segments?.[segmentId];
48
+ if (!segment) throw new Error("Segment not found");
49
+ const segmentStartMs = segment.cts / trackData.timescale * 1e3;
50
+ return globalTimestamps.map((globalMs) => (globalMs - segmentStartMs) / 1e3);
51
+ }
43
52
  };
44
53
  export { AssetIdMediaEngine };
@@ -52,4 +52,5 @@ export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEn
52
52
  maxVideoBufferFetches: number;
53
53
  maxAudioBufferFetches: number;
54
54
  };
55
+ convertToSegmentRelativeTimestamps(globalTimestamps: number[], segmentId: number, rendition: VideoRendition): number[];
55
56
  }