@editframe/elements 0.26.2-beta.0 → 0.26.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 (135) hide show
  1. package/dist/elements/EFTimegroup.js +7 -2
  2. package/dist/elements/EFTimegroup.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/build-css.js +3 -3
  5. package/tsdown.config.ts +1 -1
  6. package/types.json +1 -1
  7. package/src/elements/ContextProxiesController.ts +0 -124
  8. package/src/elements/CrossUpdateController.ts +0 -22
  9. package/src/elements/EFAudio.browsertest.ts +0 -706
  10. package/src/elements/EFAudio.ts +0 -56
  11. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  12. package/src/elements/EFCaptions.ts +0 -823
  13. package/src/elements/EFImage.browsertest.ts +0 -120
  14. package/src/elements/EFImage.ts +0 -113
  15. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  16. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  17. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  18. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  19. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  20. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  21. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  22. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  23. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  24. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  25. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  26. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  27. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  28. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  29. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  30. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  31. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  32. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  33. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  34. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  35. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  36. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  37. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  38. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  39. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  40. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  41. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  42. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  43. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  44. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  45. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  46. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  47. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  48. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  49. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  51. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  52. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  53. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  54. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  55. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  56. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  57. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  58. package/src/elements/EFMedia.browsertest.ts +0 -872
  59. package/src/elements/EFMedia.ts +0 -341
  60. package/src/elements/EFSourceMixin.ts +0 -60
  61. package/src/elements/EFSurface.browsertest.ts +0 -151
  62. package/src/elements/EFSurface.ts +0 -142
  63. package/src/elements/EFTemporal.browsertest.ts +0 -215
  64. package/src/elements/EFTemporal.ts +0 -800
  65. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  66. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  67. package/src/elements/EFThumbnailStrip.ts +0 -906
  68. package/src/elements/EFTimegroup.browsertest.ts +0 -870
  69. package/src/elements/EFTimegroup.ts +0 -878
  70. package/src/elements/EFVideo.browsertest.ts +0 -1482
  71. package/src/elements/EFVideo.ts +0 -564
  72. package/src/elements/EFWaveform.ts +0 -547
  73. package/src/elements/FetchContext.browsertest.ts +0 -401
  74. package/src/elements/FetchMixin.ts +0 -38
  75. package/src/elements/SampleBuffer.ts +0 -94
  76. package/src/elements/TargetController.browsertest.ts +0 -230
  77. package/src/elements/TargetController.ts +0 -224
  78. package/src/elements/TimegroupController.ts +0 -26
  79. package/src/elements/durationConverter.ts +0 -35
  80. package/src/elements/parseTimeToMs.ts +0 -9
  81. package/src/elements/printTaskStatus.ts +0 -16
  82. package/src/elements/renderTemporalAudio.ts +0 -108
  83. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  84. package/src/elements/updateAnimations.ts +0 -217
  85. package/src/elements/util.ts +0 -24
  86. package/src/gui/ContextMixin.browsertest.ts +0 -860
  87. package/src/gui/ContextMixin.ts +0 -562
  88. package/src/gui/Controllable.browsertest.ts +0 -258
  89. package/src/gui/Controllable.ts +0 -41
  90. package/src/gui/EFConfiguration.ts +0 -40
  91. package/src/gui/EFControls.browsertest.ts +0 -389
  92. package/src/gui/EFControls.ts +0 -195
  93. package/src/gui/EFDial.browsertest.ts +0 -84
  94. package/src/gui/EFDial.ts +0 -172
  95. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  96. package/src/gui/EFFilmstrip.ts +0 -1349
  97. package/src/gui/EFFitScale.ts +0 -152
  98. package/src/gui/EFFocusOverlay.ts +0 -79
  99. package/src/gui/EFPause.browsertest.ts +0 -202
  100. package/src/gui/EFPause.ts +0 -73
  101. package/src/gui/EFPlay.browsertest.ts +0 -202
  102. package/src/gui/EFPlay.ts +0 -73
  103. package/src/gui/EFPreview.ts +0 -74
  104. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  105. package/src/gui/EFResizableBox.ts +0 -898
  106. package/src/gui/EFScrubber.ts +0 -151
  107. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  108. package/src/gui/EFTimeDisplay.ts +0 -55
  109. package/src/gui/EFToggleLoop.ts +0 -35
  110. package/src/gui/EFTogglePlay.ts +0 -70
  111. package/src/gui/EFWorkbench.ts +0 -115
  112. package/src/gui/PlaybackController.ts +0 -527
  113. package/src/gui/TWMixin.css +0 -6
  114. package/src/gui/TWMixin.ts +0 -61
  115. package/src/gui/TargetOrContextMixin.ts +0 -185
  116. package/src/gui/currentTimeContext.ts +0 -5
  117. package/src/gui/durationContext.ts +0 -3
  118. package/src/gui/efContext.ts +0 -6
  119. package/src/gui/fetchContext.ts +0 -5
  120. package/src/gui/focusContext.ts +0 -7
  121. package/src/gui/focusedElementContext.ts +0 -5
  122. package/src/gui/playingContext.ts +0 -5
  123. package/src/otel/BridgeSpanExporter.ts +0 -150
  124. package/src/otel/setupBrowserTracing.ts +0 -73
  125. package/src/otel/tracingHelpers.ts +0 -251
  126. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  127. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  128. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  129. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  130. package/src/transcoding/types/index.ts +0 -312
  131. package/src/transcoding/utils/MediaUtils.ts +0 -63
  132. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  133. package/src/transcoding/utils/constants.ts +0 -36
  134. package/src/utils/LRUCache.test.ts +0 -274
  135. package/src/utils/LRUCache.ts +0 -696
@@ -1,823 +0,0 @@
1
- import { Task, TaskStatus } from "@lit/task";
2
- import { css, html, LitElement, type PropertyValueMap } from "lit";
3
- import { customElement, property } from "lit/decorators.js";
4
- import type { GetISOBMFFFileTranscriptionResult } from "../../../api/src/index.js";
5
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
6
- import { CrossUpdateController } from "./CrossUpdateController.js";
7
- import { EFAudio } from "./EFAudio.js";
8
- import { EFSourceMixin } from "./EFSourceMixin.js";
9
- import { EFTemporal, flushStartTimeMsCache } from "./EFTemporal.js";
10
- import { flushSequenceDurationCache } from "./EFTimegroup.js";
11
- import { EFVideo } from "./EFVideo.js";
12
- import { FetchMixin } from "./FetchMixin.js";
13
-
14
- export interface WordSegment {
15
- text: string;
16
- start: number;
17
- end: number;
18
- }
19
-
20
- export interface Segment {
21
- start: number;
22
- end: number;
23
- text: string;
24
- }
25
-
26
- export interface Caption {
27
- segments: Segment[];
28
- word_segments: WordSegment[];
29
- }
30
-
31
- const stopWords = new Set(["", ".", "!", "?", ","]);
32
-
33
- @customElement("ef-captions-active-word")
34
- export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
35
- static styles = [
36
- css`
37
- :host {
38
- display: inline-block;
39
- white-space: normal;
40
- line-height: 1;
41
- }
42
- :host([hidden]) {
43
- opacity: 0;
44
- pointer-events: none;
45
- }
46
- `,
47
- ];
48
-
49
- render() {
50
- if (stopWords.has(this.wordText)) {
51
- this.hidden = true;
52
- return undefined;
53
- }
54
- this.hidden = false;
55
-
56
- // Set deterministic --ef-word-seed value based on word index
57
- const seed = (this.wordIndex * 9007) % 233; // Prime numbers for better distribution
58
- const seedValue = seed / 233; // Normalize to 0-1 range
59
- this.style.setProperty("--ef-word-seed", seedValue.toString());
60
-
61
- return html`${this.wordText}`;
62
- }
63
-
64
- @property({ type: Number, attribute: false })
65
- wordStartMs = 0;
66
-
67
- @property({ type: Number, attribute: false })
68
- wordEndMs = 0;
69
-
70
- @property({ type: String, attribute: false })
71
- wordText = "";
72
-
73
- @property({ type: Number, attribute: false })
74
- wordIndex = 0;
75
-
76
- @property({ type: Boolean, reflect: true })
77
- hidden = false;
78
-
79
- get startTimeMs() {
80
- // Get parent captions element's absolute start time, then add our local offset
81
- const parentCaptions = this.closest("ef-captions") as EFCaptions;
82
- const parentStartTime = parentCaptions?.startTimeMs || 0;
83
- return parentStartTime + (this.wordStartMs || 0);
84
- }
85
-
86
- get endTimeMs() {
87
- const parentCaptions = this.closest("ef-captions") as EFCaptions;
88
- const parentStartTime = parentCaptions?.startTimeMs || 0;
89
- return parentStartTime + (this.wordEndMs || 0);
90
- }
91
-
92
- get durationMs(): number {
93
- return this.wordEndMs - this.wordStartMs;
94
- }
95
- }
96
-
97
- @customElement("ef-captions-segment")
98
- export class EFCaptionsSegment extends EFTemporal(LitElement) {
99
- static styles = [
100
- css`
101
- :host {
102
- display: inline-block;
103
- white-space: normal;
104
- line-height: 1;
105
- }
106
- `,
107
- ];
108
-
109
- render() {
110
- if (stopWords.has(this.segmentText)) {
111
- this.hidden = true;
112
- return undefined;
113
- }
114
- this.hidden = false;
115
- return html`${this.segmentText}`;
116
- }
117
-
118
- @property({ type: Number, attribute: false })
119
- segmentStartMs = 0;
120
-
121
- @property({ type: Number, attribute: false })
122
- segmentEndMs = 0;
123
-
124
- @property({ type: String, attribute: false })
125
- segmentText = "";
126
-
127
- @property({ type: Boolean, reflect: true })
128
- hidden = false;
129
-
130
- get startTimeMs() {
131
- // Get parent captions element's absolute start time, then add our local offset
132
- const parentCaptions = this.closest("ef-captions") as EFCaptions;
133
- const parentStartTime = parentCaptions?.startTimeMs || 0;
134
- return parentStartTime + (this.segmentStartMs || 0);
135
- }
136
-
137
- get endTimeMs() {
138
- const parentCaptions = this.closest("ef-captions") as EFCaptions;
139
- const parentStartTime = parentCaptions?.startTimeMs || 0;
140
- return parentStartTime + (this.segmentEndMs || 0);
141
- }
142
-
143
- get durationMs(): number {
144
- return this.segmentEndMs - this.segmentStartMs;
145
- }
146
- }
147
-
148
- @customElement("ef-captions-before-active-word")
149
- export class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
150
- static styles = [
151
- css`
152
- :host {
153
- display: inline-block;
154
- white-space: pre;
155
- line-height: 1;
156
- }
157
- :host([hidden]) {
158
- opacity: 0;
159
- pointer-events: none;
160
- }
161
- `,
162
- ];
163
-
164
- render() {
165
- if (stopWords.has(this.segmentText)) {
166
- this.hidden = true;
167
- return undefined;
168
- }
169
- this.hidden = false;
170
-
171
- // Check if there's an active word by looking for sibling active word element
172
- const activeWord = this.closest("ef-captions")?.querySelector(
173
- "ef-captions-active-word",
174
- );
175
- const hasActiveWord = activeWord?.wordText && !activeWord.hidden;
176
-
177
- return html`${this.segmentText}${hasActiveWord ? " " : ""}`;
178
- }
179
-
180
- @property({ type: Boolean, reflect: true })
181
- hidden = false;
182
-
183
- @property({ type: String, attribute: false })
184
- segmentText = "";
185
-
186
- @property({ type: Number, attribute: false })
187
- segmentStartMs = 0;
188
-
189
- @property({ type: Number, attribute: false })
190
- segmentEndMs = 0;
191
-
192
- get startTimeMs() {
193
- // Get parent captions element's absolute start time, then add our local offset
194
- const parentCaptions = this.closest("ef-captions") as EFCaptions;
195
- const parentStartTime = parentCaptions?.startTimeMs || 0;
196
- return parentStartTime + (this.segmentStartMs || 0);
197
- }
198
-
199
- get endTimeMs() {
200
- const parentCaptions = this.closest("ef-captions") as EFCaptions;
201
- const parentStartTime = parentCaptions?.startTimeMs || 0;
202
- return parentStartTime + (this.segmentEndMs || 0);
203
- }
204
-
205
- get durationMs(): number {
206
- return this.segmentEndMs - this.segmentStartMs;
207
- }
208
- }
209
-
210
- @customElement("ef-captions-after-active-word")
211
- export class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
212
- static styles = [
213
- css`
214
- :host {
215
- display: inline-block;
216
- white-space: pre;
217
- line-height: 1;
218
- }
219
- :host([hidden]) {
220
- opacity: 0;
221
- pointer-events: none;
222
- }
223
- `,
224
- ];
225
-
226
- render() {
227
- if (stopWords.has(this.segmentText)) {
228
- this.hidden = true;
229
- return undefined;
230
- }
231
- this.hidden = false;
232
-
233
- // Check if there's an active word by looking for sibling active word element
234
- const activeWord = this.closest("ef-captions")?.querySelector(
235
- "ef-captions-active-word",
236
- );
237
- const hasActiveWord = activeWord?.wordText && !activeWord.hidden;
238
-
239
- return html`${hasActiveWord ? " " : ""}${this.segmentText}`;
240
- }
241
-
242
- @property({ type: Boolean, reflect: true })
243
- hidden = false;
244
-
245
- @property({ type: String, attribute: false })
246
- segmentText = "";
247
-
248
- @property({ type: Number, attribute: false })
249
- segmentStartMs = 0;
250
-
251
- @property({ type: Number, attribute: false })
252
- segmentEndMs = 0;
253
-
254
- get startTimeMs() {
255
- // Get parent captions element's absolute start time, then add our local offset
256
- const parentCaptions = this.closest("ef-captions") as EFCaptions;
257
- const parentStartTime = parentCaptions?.startTimeMs || 0;
258
- return parentStartTime + (this.segmentStartMs || 0);
259
- }
260
-
261
- get endTimeMs() {
262
- const parentCaptions = this.closest("ef-captions") as EFCaptions;
263
- const parentStartTime = parentCaptions?.startTimeMs || 0;
264
- return parentStartTime + (this.segmentEndMs || 0);
265
- }
266
-
267
- get durationMs(): number {
268
- return this.segmentEndMs - this.segmentStartMs;
269
- }
270
- }
271
-
272
- @customElement("ef-captions")
273
- export class EFCaptions extends EFSourceMixin(
274
- EFTemporal(FetchMixin(LitElement)),
275
- { assetType: "caption_files" },
276
- ) {
277
- static styles = [
278
- css`
279
- :host {
280
- display: inline-flex;
281
- white-space: normal;
282
- line-height: 1;
283
- gap: 0;
284
- }
285
- ::slotted(*) {
286
- display: inline-block;
287
- margin: 0;
288
- padding: 0;
289
- }
290
- `,
291
- ];
292
-
293
- @property({ type: String, attribute: "target", reflect: true })
294
- targetSelector = "";
295
-
296
- set target(value: string) {
297
- this.targetSelector = value;
298
- }
299
-
300
- @property({ attribute: "word-style" })
301
- wordStyle = "";
302
-
303
- /**
304
- * URL or path to a JSON file containing custom captions data.
305
- * The JSON should conform to the Caption interface with 'segments' and 'word_segments' arrays.
306
- */
307
- @property({ type: String, attribute: "captions-src", reflect: true })
308
- captionsSrc = "";
309
-
310
- /**
311
- * Direct captions data object. Takes priority over captions-src and captions-script.
312
- * Should conform to the Caption interface with 'segments' and 'word_segments' arrays.
313
- */
314
- @property({ type: Object, attribute: false })
315
- captionsData: Caption | null = null;
316
-
317
- /**
318
- * ID of a <script> element containing JSON captions data.
319
- * The script's textContent should be valid JSON conforming to the Caption interface.
320
- */
321
- @property({ type: String, attribute: "captions-script", reflect: true })
322
- captionsScript = "";
323
-
324
- activeWordContainers = this.getElementsByTagName("ef-captions-active-word");
325
- segmentContainers = this.getElementsByTagName("ef-captions-segment");
326
- beforeActiveWordContainers = this.getElementsByTagName(
327
- "ef-captions-before-active-word",
328
- );
329
- afterActiveWordContainers = this.getElementsByTagName(
330
- "ef-captions-after-active-word",
331
- );
332
-
333
- render() {
334
- return html`<slot></slot>`;
335
- }
336
-
337
- transcriptionsPath() {
338
- if (!this.targetElement) {
339
- return null;
340
- }
341
- if (this.targetElement.assetId) {
342
- return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;
343
- }
344
- return null;
345
- }
346
-
347
- captionsPath() {
348
- if (!this.targetElement) {
349
- return null;
350
- }
351
- if (this.targetElement.assetId) {
352
- return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;
353
- }
354
- const targetSrc = this.targetElement.src;
355
- return `/@ef-captions/${targetSrc}`;
356
- }
357
-
358
- protected md5SumLoader = new Task(this, {
359
- autoRun: false,
360
- args: () => [this.target, this.fetch] as const,
361
- task: async ([_target, fetch], { signal }) => {
362
- if (!this.targetElement) {
363
- return null;
364
- }
365
- const md5Path = `/@ef-asset/${this.targetElement.src ?? ""}`;
366
- const response = await fetch(md5Path, { method: "HEAD", signal });
367
- return response.headers.get("etag") ?? undefined;
368
- },
369
- });
370
-
371
- private transcriptionDataTask = new Task(this, {
372
- autoRun: EF_INTERACTIVE,
373
- args: () =>
374
- [
375
- this.transcriptionsPath(),
376
- this.fetch,
377
- this.hasCustomCaptionsData,
378
- ] as const,
379
- task: async ([transcriptionsPath, fetch, hasCustomData], { signal }) => {
380
- // Skip transcription if we have custom captions data
381
- if (hasCustomData || !transcriptionsPath) {
382
- return null;
383
- }
384
- const response = await fetch(transcriptionsPath, { signal });
385
- return response.json() as any as GetISOBMFFFileTranscriptionResult;
386
- },
387
- });
388
-
389
- private transcriptionFragmentPath(
390
- transcriptionId: string,
391
- fragmentIndex: number,
392
- ) {
393
- return `${this.apiHost}/api/v1/transcriptions/${transcriptionId}/fragments/${fragmentIndex}`;
394
- }
395
-
396
- private fragmentIndexTask = new Task(this, {
397
- autoRun: EF_INTERACTIVE,
398
- args: () =>
399
- [this.transcriptionDataTask.value, this.ownCurrentTimeMs] as const,
400
- task: async ([transcription, ownCurrentTimeMs]) => {
401
- if (!transcription) {
402
- return null;
403
- }
404
- const fragmentIndex = Math.floor(
405
- ownCurrentTimeMs / transcription.work_slice_ms,
406
- );
407
- return fragmentIndex;
408
- },
409
- });
410
-
411
- private customCaptionsDataTask = new Task(this, {
412
- autoRun: EF_INTERACTIVE,
413
- args: () =>
414
- [
415
- this.captionsSrc,
416
- this.captionsData,
417
- this.captionsScript,
418
- this.fetch,
419
- ] as const,
420
- task: async (
421
- [captionsSrc, captionsData, captionsScript, fetch],
422
- { signal },
423
- ) => {
424
- // Priority: direct data > script reference > URL source
425
- if (captionsData) {
426
- return captionsData;
427
- }
428
-
429
- if (captionsScript) {
430
- const scriptElement = document.getElementById(captionsScript);
431
- if (scriptElement?.textContent) {
432
- try {
433
- return JSON.parse(scriptElement.textContent) as Caption;
434
- } catch (error) {
435
- console.error(
436
- `Failed to parse captions from script #${captionsScript}:`,
437
- error,
438
- );
439
- return null;
440
- }
441
- }
442
- }
443
-
444
- if (captionsSrc) {
445
- try {
446
- const response = await fetch(captionsSrc, { signal });
447
- return (await response.json()) as Caption;
448
- } catch (error) {
449
- console.error(`Failed to load captions from ${captionsSrc}:`, error);
450
- return null;
451
- }
452
- }
453
-
454
- return null;
455
- },
456
- });
457
-
458
- private transcriptionFragmentDataTask = new Task(this, {
459
- autoRun: EF_INTERACTIVE,
460
- args: () =>
461
- [
462
- this.transcriptionDataTask.value,
463
- this.fragmentIndexTask.value,
464
- this.fetch,
465
- ] as const,
466
- task: async ([transcription, fragmentIndex, fetch], { signal }) => {
467
- if (
468
- transcription === null ||
469
- transcription === undefined ||
470
- fragmentIndex === null ||
471
- fragmentIndex === undefined
472
- ) {
473
- return null;
474
- }
475
- const fragmentPath = this.transcriptionFragmentPath(
476
- transcription.id,
477
- fragmentIndex,
478
- );
479
- const response = await fetch(fragmentPath, { signal });
480
- return response.json() as any as Caption;
481
- },
482
- });
483
-
484
- unifiedCaptionsDataTask = new Task(this, {
485
- autoRun: EF_INTERACTIVE,
486
- args: () =>
487
- [
488
- this.customCaptionsDataTask.value,
489
- this.transcriptionFragmentDataTask.value,
490
- ] as const,
491
- task: async ([_customData, _transcriptionData]) => {
492
- if (this.customCaptionsDataTask.status === TaskStatus.PENDING) {
493
- await this.customCaptionsDataTask.taskComplete;
494
- }
495
- if (this.transcriptionFragmentDataTask.status === TaskStatus.PENDING) {
496
- await this.transcriptionFragmentDataTask.taskComplete;
497
- }
498
- return (
499
- this.customCaptionsDataTask.value ||
500
- this.transcriptionFragmentDataTask.value
501
- );
502
- },
503
- });
504
-
505
- frameTask = new Task(this, {
506
- autoRun: EF_INTERACTIVE,
507
- args: () => [this.unifiedCaptionsDataTask.status, this.ownCurrentTimeMs],
508
- task: async () => {
509
- await this.unifiedCaptionsDataTask.taskComplete;
510
- // Trigger updateTextContainers when data is ready or time changes
511
- this.updateTextContainers();
512
- },
513
- });
514
-
515
- connectedCallback() {
516
- super.connectedCallback();
517
-
518
- // Try to get target element safely
519
- const target = this.targetSelector
520
- ? document.getElementById(this.targetSelector)
521
- : null;
522
- if (target && (target instanceof EFAudio || target instanceof EFVideo)) {
523
- new CrossUpdateController(target, this);
524
- }
525
- // For standalone captions with custom data, ensure proper timeline sync
526
- else if (this.hasCustomCaptionsData && this.rootTimegroup) {
527
- new CrossUpdateController(this.rootTimegroup, this);
528
- }
529
-
530
- // Prevent display:none from being set on caption elements
531
- // This maintains constant width in the parent flex container
532
- const observer = new MutationObserver(() => {
533
- if (this.style.display === "none") {
534
- // Remove the display:none and use opacity instead
535
- this.style.removeProperty("display");
536
- this.style.opacity = "0";
537
- this.style.pointerEvents = "none";
538
- }
539
- });
540
- observer.observe(this, { attributes: true, attributeFilter: ["style"] });
541
- }
542
-
543
- protected updated(
544
- changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
545
- ): void {
546
- this.updateTextContainers();
547
-
548
- // Force duration recalculation when custom captions data changes
549
- if (
550
- changedProperties.has("captionsData") ||
551
- changedProperties.has("captionsSrc") ||
552
- changedProperties.has("captionsScript")
553
- ) {
554
- this.requestUpdate("intrinsicDurationMs");
555
-
556
- // Flush sequence duration cache and notify parent timegroups that child duration has changed
557
- flushSequenceDurationCache();
558
- flushStartTimeMsCache();
559
-
560
- // Notify parent timegroup to recalculate its duration
561
- if (this.parentTimegroup) {
562
- this.parentTimegroup.requestUpdate("durationMs");
563
- this.parentTimegroup.requestUpdate("currentTime");
564
- }
565
- }
566
-
567
- // Update captions when timeline position changes
568
- if (changedProperties.has("ownCurrentTimeMs")) {
569
- this.updateTextContainers();
570
- }
571
- }
572
-
573
- updateTextContainers() {
574
- const captionsData = this.unifiedCaptionsDataTask.value as Caption;
575
- if (!captionsData) {
576
- return;
577
- }
578
-
579
- // Use ownCurrentTimeMs which is synchronized with the timegroup
580
- const currentTimeMs = this.ownCurrentTimeMs;
581
- const currentTimeSec = currentTimeMs / 1000;
582
-
583
- // Find the current word from word_segments
584
- // Use exclusive end boundary to prevent overlap at exact boundaries
585
- const currentWord = captionsData.word_segments.find(
586
- (word) => currentTimeSec >= word.start && currentTimeSec < word.end,
587
- );
588
-
589
- // Find the current segment
590
- // Use exclusive end boundary to prevent overlap at exact boundaries
591
- const currentSegment = captionsData.segments.find(
592
- (segment) =>
593
- currentTimeSec >= segment.start && currentTimeSec < segment.end,
594
- );
595
-
596
- for (const wordContainer of this.activeWordContainers) {
597
- if (currentWord) {
598
- wordContainer.wordText = currentWord.text;
599
- wordContainer.wordStartMs = currentWord.start * 1000;
600
- wordContainer.wordEndMs = currentWord.end * 1000;
601
- // Set word index for deterministic animation variation
602
- const wordIndex = captionsData.word_segments.findIndex(
603
- (w) =>
604
- w.start === currentWord.start &&
605
- w.end === currentWord.end &&
606
- w.text === currentWord.text,
607
- );
608
- wordContainer.wordIndex = wordIndex >= 0 ? wordIndex : 0;
609
- // Force re-render to update hidden property
610
- wordContainer.requestUpdate();
611
- } else {
612
- // No active word - maintain layout with invisible placeholder
613
- wordContainer.wordText = ""; // Empty when no active word
614
- wordContainer.wordStartMs = 0;
615
- wordContainer.wordEndMs = 0;
616
- wordContainer.requestUpdate();
617
- }
618
- }
619
-
620
- for (const segmentContainer of this.segmentContainers) {
621
- if (currentSegment) {
622
- segmentContainer.segmentText = currentSegment.text;
623
- segmentContainer.segmentStartMs = currentSegment.start * 1000;
624
- segmentContainer.segmentEndMs = currentSegment.end * 1000;
625
- } else {
626
- // No active segment - clear the container
627
- segmentContainer.segmentText = "";
628
- segmentContainer.segmentStartMs = 0;
629
- segmentContainer.segmentEndMs = 0;
630
- }
631
- segmentContainer.requestUpdate();
632
- }
633
-
634
- // Process context for both word and segment cases
635
- if (currentWord && currentSegment) {
636
- // Find all word segments that fall within the current segment's time range
637
- const segmentWords = captionsData.word_segments.filter(
638
- (word) =>
639
- word.start >= currentSegment.start && word.end <= currentSegment.end,
640
- );
641
-
642
- // Find the index of the current word within the segment's word segments
643
- const currentWordIndex = segmentWords.findIndex(
644
- (word) =>
645
- word.start === currentWord.start && word.end === currentWord.end,
646
- );
647
-
648
- if (currentWordIndex !== -1) {
649
- // Get words before the current word
650
- const beforeWords = segmentWords
651
- .slice(0, currentWordIndex)
652
- .map((w) => w.text.trim())
653
- .join(" ");
654
-
655
- // Get words after the current word
656
- const afterWords = segmentWords
657
- .slice(currentWordIndex + 1)
658
- .map((w) => w.text.trim())
659
- .join(" ");
660
-
661
- // Update before containers - should be visible at the same time as active word
662
- for (const container of this.beforeActiveWordContainers) {
663
- container.segmentText = beforeWords;
664
- container.segmentStartMs = currentWord.start * 1000;
665
- container.segmentEndMs = currentWord.end * 1000;
666
- container.requestUpdate();
667
- }
668
-
669
- // Update after containers - should be visible at the same time as active word
670
- for (const container of this.afterActiveWordContainers) {
671
- container.segmentText = afterWords;
672
- container.segmentStartMs = currentWord.start * 1000;
673
- container.segmentEndMs = currentWord.end * 1000;
674
- container.requestUpdate();
675
- }
676
- }
677
- } else if (currentSegment) {
678
- // No active word but we have an active segment
679
- const segmentWords = captionsData.word_segments.filter(
680
- (word) =>
681
- word.start >= currentSegment.start && word.end <= currentSegment.end,
682
- );
683
-
684
- // Check if we're before the first word or after the last word
685
- const firstWord = segmentWords[0];
686
- const isBeforeFirstWord = firstWord && currentTimeSec < firstWord.start;
687
-
688
- if (isBeforeFirstWord) {
689
- // Before first word starts - show all words in "after" container (they're all upcoming)
690
- const allWords = segmentWords.map((w) => w.text.trim()).join(" ");
691
-
692
- for (const container of this.beforeActiveWordContainers) {
693
- container.segmentText = ""; // Nothing before yet
694
- container.segmentStartMs = currentSegment.start * 1000;
695
- container.segmentEndMs = currentSegment.end * 1000;
696
- container.requestUpdate();
697
- }
698
-
699
- for (const container of this.afterActiveWordContainers) {
700
- container.segmentText = allWords; // All words are upcoming
701
- container.segmentStartMs = currentSegment.start * 1000;
702
- container.segmentEndMs = currentSegment.end * 1000;
703
- container.requestUpdate();
704
- }
705
- } else {
706
- // After last word ends - show all completed words in "before" container
707
- const allCompletedWords = segmentWords
708
- .map((w) => w.text.trim())
709
- .join(" ");
710
-
711
- for (const container of this.beforeActiveWordContainers) {
712
- container.segmentText = allCompletedWords;
713
- container.segmentStartMs = currentSegment.start * 1000;
714
- container.segmentEndMs = currentSegment.end * 1000;
715
- container.requestUpdate();
716
- }
717
-
718
- for (const container of this.afterActiveWordContainers) {
719
- container.segmentText = "";
720
- container.segmentStartMs = currentSegment.start * 1000;
721
- container.segmentEndMs = currentSegment.end * 1000;
722
- container.requestUpdate();
723
- }
724
- }
725
- } else {
726
- // No active segment or word - clear all context containers
727
- for (const container of this.beforeActiveWordContainers) {
728
- container.segmentText = "";
729
- container.segmentStartMs = 0;
730
- container.segmentEndMs = 0;
731
- container.requestUpdate();
732
- }
733
-
734
- for (const container of this.afterActiveWordContainers) {
735
- container.segmentText = "";
736
- container.segmentStartMs = 0;
737
- container.segmentEndMs = 0;
738
- container.requestUpdate();
739
- }
740
- }
741
- }
742
-
743
- get targetElement() {
744
- const target = document.getElementById(this.targetSelector ?? "");
745
- if (target instanceof EFAudio || target instanceof EFVideo) {
746
- return target;
747
- }
748
- // When using custom captions data, a target is not required
749
- if (this.hasCustomCaptionsData) {
750
- return null;
751
- }
752
- return null;
753
- }
754
-
755
- get hasCustomCaptionsData(): boolean {
756
- return !!(this.captionsData || this.captionsSrc || this.captionsScript);
757
- }
758
-
759
- // Follow the exact EFMedia pattern for safe duration integration
760
- get intrinsicDurationMs(): number | undefined {
761
- // Direct access to custom captions data - avoiding complex task dependencies
762
- // Priority: direct data > script reference > external file
763
- let captionsData: Caption | null = null;
764
-
765
- if (this.captionsData) {
766
- captionsData = this.captionsData;
767
- } else if (this.captionsScript) {
768
- const scriptElement = document.getElementById(this.captionsScript);
769
- if (scriptElement?.textContent) {
770
- try {
771
- captionsData = JSON.parse(scriptElement.textContent) as Caption;
772
- } catch {
773
- // Invalid JSON, fall through to return undefined
774
- }
775
- }
776
- } else if (this.customCaptionsDataTask.value) {
777
- captionsData = this.customCaptionsDataTask.value as Caption;
778
- }
779
-
780
- if (!captionsData) {
781
- return undefined;
782
- }
783
-
784
- if (
785
- captionsData.segments.length === 0 &&
786
- captionsData.word_segments.length === 0
787
- ) {
788
- return 0;
789
- }
790
-
791
- // Find the maximum end time from both segments and word_segments
792
- const maxSegmentEnd =
793
- captionsData.segments.length > 0
794
- ? Math.max(...captionsData.segments.map((s) => s.end))
795
- : 0;
796
- const maxWordEnd =
797
- captionsData.word_segments.length > 0
798
- ? Math.max(...captionsData.word_segments.map((w) => w.end))
799
- : 0;
800
-
801
- return Math.max(maxSegmentEnd, maxWordEnd) * 1000; // Convert to milliseconds
802
- }
803
-
804
- // Follow the exact EFMedia pattern for safe duration integration
805
- get hasOwnDuration(): boolean {
806
- // Simple check - if we have any form of custom captions data, we have our own duration
807
- return !!(
808
- this.captionsData ||
809
- this.captionsScript ||
810
- this.customCaptionsDataTask.value
811
- );
812
- }
813
- }
814
-
815
- declare global {
816
- interface HTMLElementTagNameMap {
817
- "ef-captions": EFCaptions;
818
- "ef-captions-active-word": EFCaptionsActiveWord;
819
- "ef-captions-segment": EFCaptionsSegment;
820
- "ef-captions-before-active-word": EFCaptionsBeforeActiveWord;
821
- "ef-captions-after-active-word": EFCaptionsAfterActiveWord;
822
- }
823
- }