@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,1349 +0,0 @@
1
- import { consume } from "@lit/context";
2
- import {
3
- css,
4
- html,
5
- LitElement,
6
- nothing,
7
- type PropertyValueMap,
8
- type ReactiveController,
9
- type TemplateResult,
10
- } from "lit";
11
- import {
12
- customElement,
13
- eventOptions,
14
- property,
15
- state,
16
- } from "lit/decorators.js";
17
- import { createRef, ref } from "lit/directives/ref.js";
18
- import { styleMap } from "lit/directives/style-map.js";
19
-
20
- import { EFAudio } from "../elements/EFAudio.js";
21
- import {
22
- type Caption,
23
- EFCaptions,
24
- EFCaptionsActiveWord,
25
- } from "../elements/EFCaptions.js";
26
- import { EFImage } from "../elements/EFImage.js";
27
- import {
28
- isEFTemporal,
29
- type TemporalMixinInterface,
30
- } from "../elements/EFTemporal.js";
31
- import { EFTimegroup } from "../elements/EFTimegroup.js";
32
- import { EFVideo } from "../elements/EFVideo.js";
33
- import { EFWaveform } from "../elements/EFWaveform.js";
34
- import { TargetController } from "../elements/TargetController.js";
35
- import { TimegroupController } from "../elements/TimegroupController.js";
36
- import { msToTimeCode } from "../msToTimeCode.js";
37
- import { targetTemporalContext } from "./ContextMixin.ts";
38
- import type { EFPreview } from "./EFPreview.js";
39
- import type { EFWorkbench } from "./EFWorkbench.js";
40
- import { type FocusContext, focusContext } from "./focusContext.js";
41
- import { focusedElementContext } from "./focusedElementContext.js";
42
- import { loopContext, playingContext } from "./playingContext.js";
43
- import { TWMixin } from "./TWMixin.js";
44
-
45
- class ElementFilmstripController implements ReactiveController {
46
- constructor(
47
- private host: LitElement,
48
- private filmstrip: FilmstripItem,
49
- ) {
50
- this.host.addController(this);
51
- }
52
-
53
- remove() {
54
- this.host.removeController(this);
55
- }
56
-
57
- hostDisconnected() {
58
- this.host.removeController(this);
59
- }
60
-
61
- hostUpdated(): void {
62
- this.filmstrip.requestUpdate();
63
- }
64
- }
65
-
66
- const CommonEffectKeys = new Set([
67
- "offset",
68
- "easing",
69
- "composite",
70
- "computedOffset",
71
- ]);
72
-
73
- class FilmstripItem extends TWMixin(LitElement) {
74
- static styles = [
75
- css`
76
- :host {
77
- display: block;
78
- }
79
- `,
80
- ];
81
-
82
- @consume({ context: focusContext, subscribe: true })
83
- focusContext?: FocusContext;
84
-
85
- @consume({ context: focusedElementContext, subscribe: true })
86
- focusedElement?: HTMLElement | null;
87
-
88
- get isFocused() {
89
- return this.element && this.focusContext?.focusedElement === this.element;
90
- }
91
-
92
- @property({ type: Object, attribute: false })
93
- element: TemporalMixinInterface & LitElement = new EFTimegroup();
94
-
95
- @property({ type: Number })
96
- pixelsPerMs = 0.04;
97
-
98
- // Gutter styles represent the entire source media.
99
- // If there is no trim, then the gutter and trim portion are the same.
100
- get gutterStyles() {
101
- return {
102
- position: "relative",
103
- left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.sourceStartMs)}px`,
104
- width: `${this.pixelsPerMs * (this.element.intrinsicDurationMs ?? this.element.durationMs)}px`,
105
- };
106
- }
107
-
108
- // Trim portion is the section of source that will be placed in the timeline
109
- // If there is no trim, then the gutter and trim portion are the same.
110
- get trimPortionStyles() {
111
- return {
112
- width: `${this.pixelsPerMs * this.element.durationMs}px`,
113
- left: `${this.pixelsPerMs * this.element.sourceStartMs}px`,
114
- };
115
- }
116
-
117
- render() {
118
- return html`<div style=${styleMap(this.gutterStyles)}>
119
- <div
120
- class="bg-slate-300"
121
- ?data-focused=${this.isFocused}
122
- @mouseenter=${() => {
123
- if (this.focusContext) {
124
- this.focusContext.focusedElement = this.element;
125
- }
126
- }}
127
- @mouseleave=${() => {
128
- if (this.focusContext) {
129
- this.focusContext.focusedElement = null;
130
- }
131
- }}
132
- >
133
- <div
134
- ?data-focused=${this.isFocused}
135
- class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 text-sm data-[focused]:bg-slate-400"
136
- style=${styleMap(this.trimPortionStyles)}
137
- >
138
- ${this.animations()}
139
- </div>
140
- </div>
141
- ${this.renderChildren()}
142
- </div>`;
143
- }
144
-
145
- renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {
146
- return renderFilmstripChildren(
147
- Array.from(this.element.children),
148
- this.pixelsPerMs,
149
- this.hideSelectors,
150
- this.showSelectors,
151
- );
152
- }
153
-
154
- @property({ type: Array, attribute: false })
155
- hideSelectors?: string[];
156
-
157
- @property({ type: Array, attribute: false })
158
- showSelectors?: string[];
159
-
160
- contents() {
161
- return html``;
162
- }
163
-
164
- animations() {
165
- const animations = this.element.getAnimations();
166
- return animations.map((animation) => {
167
- const effect = animation.effect;
168
- if (!(effect instanceof KeyframeEffect)) {
169
- return nothing;
170
- }
171
- const start = effect.getTiming().delay ?? 0;
172
- const duration = effect.getTiming().duration;
173
- if (duration === null) {
174
- return nothing;
175
- }
176
- const keyframes = effect.getKeyframes();
177
- const firstKeyframe = keyframes[0];
178
- if (!firstKeyframe) {
179
- return nothing;
180
- }
181
- const properties = new Set(Object.keys(firstKeyframe));
182
- for (const key of CommonEffectKeys) {
183
- properties.delete(key);
184
- }
185
-
186
- return html`<div
187
- class="relative h-[5px] bg-blue-500 opacity-50"
188
- label="animation"
189
- style=${styleMap({
190
- left: `${this.pixelsPerMs * start}px`,
191
- width: `${this.pixelsPerMs * Number(duration)}px`,
192
- })}
193
- >
194
- <!-- <div class="text-nowrap">${Array.from(properties).join(" ")}</div> -->
195
- ${effect.getKeyframes().map((keyframe) => {
196
- return html`<div
197
- class="absolute top-0 h-full w-1 bg-red-500"
198
- style=${styleMap({
199
- left: `${
200
- this.pixelsPerMs * keyframe.computedOffset * Number(duration)
201
- }px`,
202
- })}
203
- ></div>`;
204
- })}
205
- </div>`;
206
- });
207
- }
208
-
209
- protected filmstripController?: ElementFilmstripController;
210
-
211
- update(changedProperties: Map<string | number | symbol, unknown>) {
212
- if (
213
- changedProperties.has("element") &&
214
- this.element instanceof LitElement
215
- ) {
216
- this.filmstripController?.remove();
217
- this.filmstripController = new ElementFilmstripController(
218
- this.element,
219
- this,
220
- );
221
- }
222
- super.update(changedProperties);
223
- }
224
- }
225
-
226
- @customElement("ef-audio-filmstrip")
227
- export class EFAudioFilmstrip extends FilmstripItem {
228
- contents() {
229
- return html``;
230
- }
231
- }
232
-
233
- @customElement("ef-video-filmstrip")
234
- export class EFVideoFilmstrip extends FilmstripItem {
235
- contents() {
236
- return html` 📼 `;
237
- }
238
- }
239
-
240
- @customElement("ef-captions-filmstrip")
241
- export class EFCaptionsFilmstrip extends FilmstripItem {
242
- render() {
243
- const captions = this.element as EFCaptions;
244
- const captionsData = captions.unifiedCaptionsDataTask.value;
245
-
246
- return html`<div style=${styleMap(this.gutterStyles)}>
247
- <div
248
- class="bg-slate-300 relative"
249
- ?data-focused=${this.isFocused}
250
- @mouseenter=${() => {
251
- if (this.focusContext) {
252
- this.focusContext.focusedElement = this.element;
253
- }
254
- }}
255
- @mouseleave=${() => {
256
- if (this.focusContext) {
257
- this.focusContext.focusedElement = null;
258
- }
259
- }}
260
- >
261
- <div
262
- ?data-focused=${this.isFocused}
263
- class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 text-sm data-[focused]:bg-slate-400 overflow-hidden"
264
- style=${styleMap(this.trimPortionStyles)}
265
- >
266
- 📝 ${this.renderCaptionsData(captionsData)}
267
- </div>
268
- </div>
269
- ${this.renderChildren()}
270
- </div>`;
271
- }
272
-
273
- renderCaptionsData(captionsData: Caption | null | undefined) {
274
- if (!captionsData) {
275
- return html``;
276
- }
277
-
278
- // Get current time for highlighting active elements
279
- const captions = this.element as EFCaptions;
280
- const rootTimegroup = captions.rootTimegroup;
281
- const currentTimeMs = rootTimegroup?.currentTimeMs || 0;
282
- const captionsLocalTimeMs = currentTimeMs - captions.startTimeMs;
283
- const captionsLocalTimeSec = captionsLocalTimeMs / 1000;
284
-
285
- // Show all segments with text content, let them clip naturally
286
- const segmentElements = captionsData.segments.map((segment) => {
287
- const isActive =
288
- captionsLocalTimeSec >= segment.start &&
289
- captionsLocalTimeSec < segment.end;
290
-
291
- return html`<div
292
- class="absolute border border-slate-600 text-xs overflow-hidden flex items-center ${isActive ? "bg-green-200 border-green-500 font-bold z-[5]" : "bg-slate-100"}"
293
- style=${styleMap({
294
- left: `${this.pixelsPerMs * segment.start * 1000}px`,
295
- width: `${this.pixelsPerMs * (segment.end - segment.start) * 1000}px`,
296
- height: "100%",
297
- top: "0px",
298
- })}
299
- title="Segment: '${segment.text}' (${segment.start}s - ${segment.end}s)"
300
- >
301
- <span class="px-0.5 text-[8px] ${isActive ? "font-bold" : ""}">${segment.text}</span>
302
- </div>`;
303
- });
304
-
305
- return html`${segmentElements}`;
306
- }
307
-
308
- renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {
309
- // Also render normal DOM children (like ef-captions-active-word elements)
310
- return renderFilmstripChildren(
311
- Array.from(this.element.children),
312
- this.pixelsPerMs,
313
- this.hideSelectors,
314
- this.showSelectors,
315
- );
316
- }
317
- }
318
-
319
- @customElement("ef-captions-active-word-filmstrip")
320
- export class EFCaptionsActiveWordFilmstrip extends FilmstripItem {
321
- get captionsTrackStyles() {
322
- const parentCaptions = this.element.closest("ef-captions") as EFCaptions;
323
- return {
324
- position: "relative",
325
- left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,
326
- width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,
327
- };
328
- }
329
-
330
- render() {
331
- // Get parent captions element and its data
332
- const parentCaptions = this.element.closest("ef-captions") as EFCaptions;
333
- const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;
334
-
335
- if (!captionsData) {
336
- return html`<div style=${styleMap(this.captionsTrackStyles)}>
337
- <div class="bg-slate-300 border border-slate-500 h-[1.1rem] mb-[1px] text-xs">
338
- 🗣️ Active Word
339
- </div>
340
- </div>`;
341
- }
342
-
343
- // Get current time for highlighting
344
- const rootTimegroup = parentCaptions.rootTimegroup;
345
- const currentTimeMs = rootTimegroup?.currentTimeMs || 0;
346
- const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;
347
- const captionsLocalTimeSec = captionsLocalTimeMs / 1000;
348
-
349
- return html`<div style=${styleMap(this.captionsTrackStyles)}>
350
- <div class="bg-slate-300 relative border border-slate-500 h-[1.1rem] mb-[1px] w-full">
351
- ${captionsData.word_segments.map((word) => {
352
- const isCurrentlyActive =
353
- captionsLocalTimeSec >= word.start &&
354
- captionsLocalTimeSec < word.end;
355
-
356
- return html`<div
357
- class="absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? "bg-yellow-200 border-yellow-500 font-bold z-[5]" : "bg-blue-50 border-blue-200"}"
358
- style=${styleMap({
359
- left: `${this.pixelsPerMs * word.start * 1000}px`,
360
- width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,
361
- height: "100%",
362
- top: "0px",
363
- })}
364
- title="Word: '${word.text}' (${word.start}s - ${word.end}s)"
365
- >
366
- ${isCurrentlyActive ? html`<span class="px-0.5 text-[8px] font-bold whitespace-nowrap bg-yellow-200">${word.text.trim()}</span>` : ""}
367
- </div>`;
368
- })}
369
- </div>
370
- </div>`;
371
- }
372
- }
373
-
374
- @customElement("ef-captions-segment-filmstrip")
375
- export class EFCaptionsSegmentFilmstrip extends FilmstripItem {
376
- get captionsTrackStyles() {
377
- const parentCaptions = this.element.closest("ef-captions") as EFCaptions;
378
- return {
379
- position: "relative",
380
- left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,
381
- width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,
382
- };
383
- }
384
-
385
- render() {
386
- // Get parent captions element and its data
387
- const parentCaptions = this.element.closest("ef-captions") as EFCaptions;
388
- const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;
389
-
390
- if (!captionsData) {
391
- return html`<div style=${styleMap(this.captionsTrackStyles)}>
392
- <div class="bg-slate-300 border border-slate-500 h-[1.1rem] mb-[1px] text-xs">
393
- 📄 Segment
394
- </div>
395
- </div>`;
396
- }
397
-
398
- // Get current time for highlighting
399
- const rootTimegroup = parentCaptions.rootTimegroup;
400
- const currentTimeMs = rootTimegroup?.currentTimeMs || 0;
401
- const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;
402
- const captionsLocalTimeSec = captionsLocalTimeMs / 1000;
403
-
404
- return html`<div style=${styleMap(this.captionsTrackStyles)}>
405
- <div class="bg-slate-300 relative border border-slate-500 h-[1.1rem] mb-[1px] w-full">
406
- ${captionsData.segments.map((segment) => {
407
- const isCurrentlyActive =
408
- captionsLocalTimeSec >= segment.start &&
409
- captionsLocalTimeSec < segment.end;
410
-
411
- return html`<div
412
- class="absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? "bg-green-200 border-green-500 font-bold z-[5]" : "bg-green-50 border-green-200"}"
413
- style=${styleMap({
414
- left: `${this.pixelsPerMs * segment.start * 1000}px`,
415
- width: `${this.pixelsPerMs * (segment.end - segment.start) * 1000}px`,
416
- height: "100%",
417
- top: "0px",
418
- })}
419
- title="Segment: '${segment.text}' (${segment.start}s - ${segment.end}s)"
420
- >
421
- ${isCurrentlyActive ? html`<span class="px-0.5 text-[8px] font-bold whitespace-nowrap bg-green-200">${segment.text}</span>` : ""}
422
- </div>`;
423
- })}
424
- </div>
425
- </div>`;
426
- }
427
- }
428
-
429
- @customElement("ef-captions-before-word-filmstrip")
430
- export class EFCaptionsBeforeWordFilmstrip extends FilmstripItem {
431
- get captionsTrackStyles() {
432
- const parentCaptions = this.element.closest("ef-captions") as EFCaptions;
433
- return {
434
- position: "relative",
435
- left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,
436
- width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,
437
- };
438
- }
439
-
440
- render() {
441
- // Get parent captions element and its data
442
- const parentCaptions = this.element.closest("ef-captions") as EFCaptions;
443
- const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;
444
-
445
- if (!captionsData) {
446
- return html`<div style=${styleMap(this.captionsTrackStyles)}>
447
- <div class="bg-slate-300 border border-slate-500 h-[1.1rem] mb-[1px] text-xs">
448
- ⬅️ Before
449
- </div>
450
- </div>`;
451
- }
452
-
453
- // Get current time for highlighting
454
- const rootTimegroup = parentCaptions.rootTimegroup;
455
- const currentTimeMs = rootTimegroup?.currentTimeMs || 0;
456
- const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;
457
- const captionsLocalTimeSec = captionsLocalTimeMs / 1000;
458
-
459
- return html`<div style=${styleMap(this.captionsTrackStyles)}>
460
- <div class="bg-slate-300 relative border border-slate-500 h-[1.1rem] mb-[1px] w-full">
461
- ${captionsData.word_segments.map((word) => {
462
- const isCurrentlyActive =
463
- captionsLocalTimeSec >= word.start &&
464
- captionsLocalTimeSec < word.end;
465
-
466
- return html`<div
467
- class="absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? "bg-yellow-200 border-yellow-500 font-bold z-[5]" : "bg-purple-50 border-purple-200"}"
468
- style=${styleMap({
469
- left: `${this.pixelsPerMs * word.start * 1000}px`,
470
- width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,
471
- height: "100%",
472
- top: "0px",
473
- })}
474
- title="Word: '${word.text}' (${word.start}s - ${word.end}s)"
475
- >
476
- <!-- No text for before tracks - they're redundant -->
477
- </div>`;
478
- })}
479
- </div>
480
- </div>`;
481
- }
482
- }
483
-
484
- @customElement("ef-captions-after-word-filmstrip")
485
- export class EFCaptionsAfterWordFilmstrip extends FilmstripItem {
486
- get captionsTrackStyles() {
487
- const parentCaptions = this.element.closest("ef-captions") as EFCaptions;
488
- return {
489
- position: "relative",
490
- left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,
491
- width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,
492
- };
493
- }
494
-
495
- render() {
496
- // Get parent captions element and its data
497
- const parentCaptions = this.element.closest("ef-captions") as EFCaptions;
498
- const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;
499
-
500
- if (!captionsData) {
501
- return html`<div style=${styleMap(this.captionsTrackStyles)}>
502
- <div class="bg-slate-300 border border-slate-500 h-[1.1rem] mb-[1px] text-xs">
503
- ➡️ After
504
- </div>
505
- </div>`;
506
- }
507
-
508
- // Get current time for highlighting
509
- const rootTimegroup = parentCaptions.rootTimegroup;
510
- const currentTimeMs = rootTimegroup?.currentTimeMs || 0;
511
- const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;
512
- const captionsLocalTimeSec = captionsLocalTimeMs / 1000;
513
-
514
- return html`<div style=${styleMap(this.captionsTrackStyles)}>
515
- <div class="bg-slate-300 relative border border-slate-500 h-[1.1rem] mb-[1px] w-full">
516
- ${captionsData.word_segments.map((word) => {
517
- const isCurrentlyActive =
518
- captionsLocalTimeSec >= word.start &&
519
- captionsLocalTimeSec < word.end;
520
-
521
- return html`<div
522
- class="absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? "bg-yellow-200 border-yellow-500 font-bold z-[5]" : "bg-purple-50 border-purple-200"}"
523
- style=${styleMap({
524
- left: `${this.pixelsPerMs * word.start * 1000}px`,
525
- width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,
526
- height: "100%",
527
- top: "0px",
528
- })}
529
- title="Word: '${word.text}' (${word.start}s - ${word.end}s)"
530
- >
531
- <!-- No text for after tracks - they're redundant -->
532
- </div>`;
533
- })}
534
- </div>
535
- </div>`;
536
- }
537
- }
538
-
539
- @customElement("ef-waveform-filmstrip")
540
- export class EFWaveformFilmstrip extends FilmstripItem {
541
- contents() {
542
- return html` 🌊 `;
543
- }
544
-
545
- renderChildren(): typeof nothing {
546
- return nothing;
547
- }
548
- }
549
-
550
- @customElement("ef-image-filmstrip")
551
- export class EFImageFilmstrip extends FilmstripItem {
552
- contents() {
553
- return html` 🖼️ `;
554
- }
555
- }
556
-
557
- @customElement("ef-timegroup-filmstrip")
558
- export class EFTimegroupFilmstrip extends FilmstripItem {
559
- contents() {
560
- return html`
561
- <span>TIME GROUP</span>
562
- ${renderFilmstripChildren(
563
- Array.from(this.element.children || []),
564
- this.pixelsPerMs,
565
- this.hideSelectors,
566
- this.showSelectors,
567
- )}
568
- </div>
569
- `;
570
- }
571
- }
572
-
573
- @customElement("ef-html-filmstrip")
574
- export class EFHTMLFilmstrip extends FilmstripItem {
575
- contents() {
576
- return html`
577
- <span>${this.element.tagName}</span>
578
- ${renderFilmstripChildren(
579
- Array.from(this.element.children || []),
580
- this.pixelsPerMs,
581
- this.hideSelectors,
582
- this.showSelectors,
583
- )}
584
- `;
585
- }
586
- }
587
-
588
- @customElement("ef-hierarchy-item")
589
- class EFHierarchyItem<
590
- ElementType extends HTMLElement = HTMLElement,
591
- > extends TWMixin(LitElement) {
592
- @property({ type: Object, attribute: false })
593
- // @ts-expect-error This could be initialzed with any HTMLElement
594
- element: ElementType = new EFTimegroup();
595
-
596
- @consume({ context: focusContext })
597
- focusContext?: FocusContext;
598
-
599
- @consume({ context: focusedElementContext, subscribe: true })
600
- focusedElement?: HTMLElement | null;
601
-
602
- @property({ type: Array, attribute: false })
603
- hideSelectors?: string[];
604
-
605
- @property({ type: Array, attribute: false })
606
- showSelectors?: string[];
607
-
608
- get icon(): TemplateResult<1> | string {
609
- return "📼";
610
- }
611
-
612
- get isFocused() {
613
- return this.element && this.focusContext?.focusedElement === this.element;
614
- }
615
-
616
- displayLabel(): TemplateResult<1> | string | typeof nothing {
617
- return nothing;
618
- }
619
-
620
- render() {
621
- return html`
622
- <div>
623
- <div
624
- class="peer
625
- flex h-[1.1rem] items-center overflow-hidden text-nowrap border border-slate-500
626
- bg-slate-200 pl-2 text-xs font-mono hover:bg-slate-400 data-[focused]:bg-slate-400"
627
- ?data-focused=${this.isFocused}
628
- @mouseenter=${() => {
629
- if (this.focusContext) {
630
- this.focusContext.focusedElement = this.element;
631
- }
632
- }}
633
- @mouseleave=${() => {
634
- if (this.focusContext) {
635
- this.focusContext.focusedElement = null;
636
- }
637
- }}
638
- >
639
- ${this.icon} ${this.displayLabel()}
640
- </div>
641
- <div
642
- class="p-[1px] pb-0 pl-2 pr-0 peer-hover:bg-slate-300 peer-data-[focused]:bg-slate-300 peer-hover:border-slate-400 peer-data-[focused]:border-slate-400""
643
- >
644
- ${this.renderChildren()}
645
- </div>
646
- </div>`;
647
- }
648
-
649
- renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {
650
- return renderHierarchyChildren(
651
- Array.from(this.element.children),
652
- this.hideSelectors,
653
- this.showSelectors,
654
- );
655
- }
656
- }
657
-
658
- @customElement("ef-timegroup-hierarchy-item")
659
- class EFTimegroupHierarchyItem extends EFHierarchyItem<EFTimegroup> {
660
- get icon() {
661
- return "🕒";
662
- }
663
-
664
- displayLabel(): string | TemplateResult<1> | typeof nothing {
665
- return this.element.mode ?? "(no mode)";
666
- }
667
- }
668
-
669
- @customElement("ef-audio-hierarchy-item")
670
- class EFAudioHierarchyItem extends EFHierarchyItem<EFAudio> {
671
- get icon() {
672
- return "🔊";
673
- }
674
-
675
- displayLabel() {
676
- return this.element.src ?? "(no src)";
677
- }
678
- }
679
-
680
- @customElement("ef-video-hierarchy-item")
681
- class EFVideoHierarchyItem extends EFHierarchyItem<EFVideo> {
682
- get icon() {
683
- return "📼";
684
- }
685
-
686
- displayLabel() {
687
- return this.element.src ?? "(no src)";
688
- }
689
- }
690
-
691
- @customElement("ef-captions-hierarchy-item")
692
- class EFCaptionsHierarchyItem extends EFHierarchyItem {
693
- get icon() {
694
- return "📝 Captions";
695
- }
696
- }
697
-
698
- @customElement("ef-captions-active-word-hierarchy-item")
699
- class EFCaptionsActiveWordHierarchyItem extends EFHierarchyItem {
700
- get icon() {
701
- return "🗣️ Active Word";
702
- }
703
- }
704
-
705
- @customElement("ef-waveform-hierarchy-item")
706
- class EFWaveformHierarchyItem extends EFHierarchyItem {
707
- get icon() {
708
- return "🌊";
709
- }
710
-
711
- renderChildren(): typeof nothing {
712
- return nothing;
713
- }
714
- }
715
-
716
- @customElement("ef-image-hierarchy-item")
717
- class EFImageHierarchyItem extends EFHierarchyItem<EFImage> {
718
- get icon() {
719
- return "🖼️";
720
- }
721
-
722
- displayLabel() {
723
- return this.element.src ?? "(no src)";
724
- }
725
- }
726
-
727
- @customElement("ef-html-hierarchy-item")
728
- class EFHTMLHierarchyItem extends EFHierarchyItem {
729
- get icon() {
730
- return html`<code>${`<${this.element.tagName.toLowerCase()}>`}</code>`;
731
- }
732
- }
733
-
734
- const shouldRenderElement = (
735
- element: Element,
736
- hideSelectors?: string[],
737
- showSelectors?: string[],
738
- ): boolean => {
739
- if (element instanceof HTMLElement && element.dataset?.efHidden) {
740
- return false;
741
- }
742
-
743
- // If show selectors are provided (allowlist mode), only render if matches
744
- if (showSelectors && showSelectors.length > 0) {
745
- return showSelectors.some((selector) => {
746
- try {
747
- return element.matches(selector);
748
- } catch {
749
- return false;
750
- }
751
- });
752
- }
753
-
754
- // If hide selectors are provided, don't render if matches
755
- if (hideSelectors && hideSelectors.length > 0) {
756
- return !hideSelectors.some((selector) => {
757
- try {
758
- return element.matches(selector);
759
- } catch {
760
- return false;
761
- }
762
- });
763
- }
764
-
765
- // No filters, render everything
766
- return true;
767
- };
768
-
769
- const renderHierarchyChildren = (
770
- children: Element[],
771
- hideSelectors?: string[],
772
- showSelectors?: string[],
773
- skipRootFiltering = false,
774
- ): Array<TemplateResult<1> | typeof nothing> => {
775
- return children.map((child) => {
776
- if (
777
- !skipRootFiltering &&
778
- !shouldRenderElement(child, hideSelectors, showSelectors)
779
- ) {
780
- return nothing;
781
- }
782
-
783
- if (child instanceof EFTimegroup) {
784
- return html`<ef-timegroup-hierarchy-item
785
- .element=${child}
786
- .hideSelectors=${hideSelectors}
787
- .showSelectors=${showSelectors}
788
- ></ef-timegroup-hierarchy-item>`;
789
- }
790
- if (child instanceof EFImage) {
791
- return html`<ef-image-hierarchy-item
792
- .element=${child}
793
- .hideSelectors=${hideSelectors}
794
- .showSelectors=${showSelectors}
795
- ></ef-image-hierarchy-item>`;
796
- }
797
- if (child instanceof EFAudio) {
798
- return html`<ef-audio-hierarchy-item
799
- .element=${child}
800
- .hideSelectors=${hideSelectors}
801
- .showSelectors=${showSelectors}
802
- ></ef-audio-hierarchy-item>`;
803
- }
804
- if (child instanceof EFVideo) {
805
- return html`<ef-video-hierarchy-item
806
- .element=${child}
807
- .hideSelectors=${hideSelectors}
808
- .showSelectors=${showSelectors}
809
- ></ef-video-hierarchy-item>`;
810
- }
811
- if (child instanceof EFCaptions) {
812
- return html`<ef-captions-hierarchy-item
813
- .element=${child}
814
- .hideSelectors=${hideSelectors}
815
- .showSelectors=${showSelectors}
816
- ></ef-captions-hierarchy-item>`;
817
- }
818
- if (child instanceof EFCaptionsActiveWord) {
819
- return html`<ef-captions-active-word-hierarchy-item
820
- .element=${child}
821
- .hideSelectors=${hideSelectors}
822
- .showSelectors=${showSelectors}
823
- ></ef-captions-active-word-hierarchy-item>`;
824
- }
825
- if (child instanceof EFWaveform) {
826
- return html`<ef-waveform-hierarchy-item
827
- .element=${child}
828
- .hideSelectors=${hideSelectors}
829
- .showSelectors=${showSelectors}
830
- ></ef-waveform-hierarchy-item>`;
831
- }
832
- return html`<ef-html-hierarchy-item
833
- .element=${child}
834
- .hideSelectors=${hideSelectors}
835
- .showSelectors=${showSelectors}
836
- ></ef-html-hierarchy-item>`;
837
- });
838
- };
839
-
840
- const renderFilmstripChildren = (
841
- children: Element[],
842
- pixelsPerMs: number,
843
- hideSelectors?: string[],
844
- showSelectors?: string[],
845
- skipRootFiltering = false,
846
- ): Array<TemplateResult<1> | typeof nothing> => {
847
- return children.map((child) => {
848
- if (
849
- !skipRootFiltering &&
850
- !shouldRenderElement(child, hideSelectors, showSelectors)
851
- ) {
852
- return nothing;
853
- }
854
-
855
- if (child instanceof EFTimegroup) {
856
- return html`<ef-timegroup-filmstrip
857
- .element=${child}
858
- .pixelsPerMs=${pixelsPerMs}
859
- .hideSelectors=${hideSelectors}
860
- .showSelectors=${showSelectors}
861
- >
862
- </ef-timegroup-filmstrip>`;
863
- }
864
- if (child instanceof EFImage) {
865
- return html`<ef-image-filmstrip
866
- .element=${child}
867
- .pixelsPerMs=${pixelsPerMs}
868
- .hideSelectors=${hideSelectors}
869
- .showSelectors=${showSelectors}
870
- ></ef-image-filmstrip>`;
871
- }
872
- if (child instanceof EFAudio) {
873
- return html`<ef-audio-filmstrip
874
- .element=${child}
875
- .pixelsPerMs=${pixelsPerMs}
876
- .hideSelectors=${hideSelectors}
877
- .showSelectors=${showSelectors}
878
- ></ef-audio-filmstrip>`;
879
- }
880
- if (child instanceof EFVideo) {
881
- return html`<ef-video-filmstrip
882
- .element=${child}
883
- .pixelsPerMs=${pixelsPerMs}
884
- .hideSelectors=${hideSelectors}
885
- .showSelectors=${showSelectors}
886
- ></ef-video-filmstrip>`;
887
- }
888
- if (child instanceof EFCaptions) {
889
- return html`<ef-captions-filmstrip
890
- .element=${child}
891
- .pixelsPerMs=${pixelsPerMs}
892
- .hideSelectors=${hideSelectors}
893
- .showSelectors=${showSelectors}
894
- ></ef-captions-filmstrip>`;
895
- }
896
- if (child instanceof EFCaptionsActiveWord) {
897
- return html`<ef-captions-active-word-filmstrip
898
- .element=${child}
899
- .pixelsPerMs=${pixelsPerMs}
900
- .hideSelectors=${hideSelectors}
901
- .showSelectors=${showSelectors}
902
- ></ef-captions-active-word-filmstrip>`;
903
- }
904
- if (child.tagName === "EF-CAPTIONS-SEGMENT") {
905
- return html`<ef-captions-segment-filmstrip
906
- .element=${child}
907
- .pixelsPerMs=${pixelsPerMs}
908
- .hideSelectors=${hideSelectors}
909
- .showSelectors=${showSelectors}
910
- ></ef-captions-segment-filmstrip>`;
911
- }
912
- if (child.tagName === "EF-CAPTIONS-BEFORE-ACTIVE-WORD") {
913
- return html`<ef-captions-before-word-filmstrip
914
- .element=${child}
915
- .pixelsPerMs=${pixelsPerMs}
916
- .hideSelectors=${hideSelectors}
917
- .showSelectors=${showSelectors}
918
- ></ef-captions-before-word-filmstrip>`;
919
- }
920
- if (child.tagName === "EF-CAPTIONS-AFTER-ACTIVE-WORD") {
921
- return html`<ef-captions-after-word-filmstrip
922
- .element=${child}
923
- .pixelsPerMs=${pixelsPerMs}
924
- .hideSelectors=${hideSelectors}
925
- .showSelectors=${showSelectors}
926
- ></ef-captions-after-word-filmstrip>`;
927
- }
928
- if (child instanceof EFWaveform) {
929
- return html`<ef-waveform-filmstrip
930
- .element=${child}
931
- .pixelsPerMs=${pixelsPerMs}
932
- .hideSelectors=${hideSelectors}
933
- .showSelectors=${showSelectors}
934
- ></ef-waveform-filmstrip>`;
935
- }
936
- return html`<ef-html-filmstrip
937
- .element=${child}
938
- .pixelsPerMs=${pixelsPerMs}
939
- .hideSelectors=${hideSelectors}
940
- .showSelectors=${showSelectors}
941
- ></ef-html-filmstrip>`;
942
- });
943
- };
944
-
945
- @customElement("ef-filmstrip")
946
- export class EFFilmstrip extends TWMixin(LitElement) {
947
- static styles = [
948
- css`
949
- :host {
950
- display: block;
951
- overflow: hidden;
952
- width: 100%;
953
- height: 100%;
954
- }
955
- `,
956
- ];
957
- @property({ type: Number })
958
- pixelsPerMs = 0.04;
959
-
960
- @property({ type: String })
961
- hide = "";
962
-
963
- @property({ type: String })
964
- show = "";
965
-
966
- get hideSelectors(): string[] | undefined {
967
- if (!this.hide) return undefined;
968
- return this.hide
969
- .split(",")
970
- .map((s) => s.trim())
971
- .filter((s) => s.length > 0);
972
- }
973
-
974
- get showSelectors(): string[] | undefined {
975
- if (!this.show) return undefined;
976
- return this.show
977
- .split(",")
978
- .map((s) => s.trim())
979
- .filter((s) => s.length > 0);
980
- }
981
-
982
- @state()
983
- scrubbing = false;
984
-
985
- @state()
986
- timelineScrolltop = 0;
987
-
988
- @consume({ context: playingContext, subscribe: true })
989
- @state()
990
- playing?: boolean;
991
-
992
- @consume({ context: loopContext, subscribe: true })
993
- @state()
994
- loop?: boolean;
995
-
996
- timegroupController?: TimegroupController;
997
-
998
- @state()
999
- currentTimeMs = 0;
1000
-
1001
- @property({ type: Boolean, reflect: true, attribute: "auto-scale" })
1002
- autoScale = false;
1003
-
1004
- private resizeObserver = new ResizeObserver(() => {
1005
- if (this.autoScale) {
1006
- this.updatePixelsPerMs();
1007
- }
1008
- });
1009
-
1010
- connectedCallback(): void {
1011
- super.connectedCallback();
1012
- this.#bindToTargetTimegroup();
1013
- window.addEventListener("keypress", this.#handleKeyPress);
1014
-
1015
- this.resizeObserver.observe(this);
1016
-
1017
- if (this.target) {
1018
- this.#targetController = new TargetController(this);
1019
- }
1020
- }
1021
-
1022
- disconnectedCallback(): void {
1023
- super.disconnectedCallback();
1024
- window.removeEventListener("keypress", this.#handleKeyPress);
1025
- this.resizeObserver.disconnect();
1026
- }
1027
-
1028
- updatePixelsPerMs() {
1029
- const target = this.targetTemporal;
1030
- const gutter = this.gutterRef.value;
1031
- if (target && gutter && gutter.clientWidth > 0) {
1032
- this.pixelsPerMs = gutter.clientWidth / (target.durationMs || 1);
1033
- }
1034
- }
1035
-
1036
- #bindToTargetTimegroup() {
1037
- if (this.timegroupController) {
1038
- this.timegroupController.remove();
1039
- }
1040
- const target = this.targetTemporal;
1041
- if (target) {
1042
- this.timegroupController = new TimegroupController(
1043
- target as EFTimegroup,
1044
- this,
1045
- );
1046
- // Set the current time to the last saved time to avoid a cycle
1047
- // where the filmstrip clobbers the time loaded from localStorage
1048
- this.currentTimeMs = target.currentTimeMs;
1049
- }
1050
- }
1051
-
1052
- #handleKeyPress = (event: KeyboardEvent) => {
1053
- // On spacebar, toggle playback
1054
- if (event.key === " ") {
1055
- const [target] = event.composedPath();
1056
- // CSS selector to match all interactive elements
1057
- const interactiveSelector =
1058
- "input, textarea, button, select, a, [contenteditable]";
1059
-
1060
- // Check if the event target or its ancestor matches an interactive element
1061
- const closestInteractive = (target as HTMLElement | null)?.closest(
1062
- interactiveSelector,
1063
- );
1064
- if (closestInteractive) {
1065
- return;
1066
- }
1067
- event.preventDefault();
1068
- if (this.#contextElement) {
1069
- this.#contextElement.playing = !this.#contextElement.playing;
1070
- }
1071
- }
1072
- };
1073
-
1074
- @eventOptions({ passive: false })
1075
- syncGutterScroll() {
1076
- if (this.gutter && this.hierarchyRef.value) {
1077
- this.hierarchyRef.value.scrollTop = this.gutter.scrollTop;
1078
- this.timelineScrolltop = this.gutter.scrollTop;
1079
- }
1080
- }
1081
-
1082
- @eventOptions({ passive: false })
1083
- syncHierarchyScroll() {
1084
- if (this.gutter && this.hierarchyRef.value) {
1085
- this.gutter.scrollTop = this.hierarchyRef.value.scrollTop;
1086
- this.timelineScrolltop = this.hierarchyRef.value.scrollTop;
1087
- }
1088
- }
1089
-
1090
- @eventOptions({ capture: false })
1091
- scrub(e: MouseEvent) {
1092
- if (this.playing) {
1093
- return;
1094
- }
1095
- if (!this.scrubbing) {
1096
- return;
1097
- }
1098
- this.applyScrub(e);
1099
- }
1100
-
1101
- @eventOptions({ capture: false })
1102
- startScrub(e: MouseEvent) {
1103
- e.preventDefault();
1104
- this.scrubbing = true;
1105
- // Running scrub in the current microtask doesn't
1106
- // result in an actual update. Not sure why.
1107
- queueMicrotask(() => {
1108
- this.applyScrub(e);
1109
- });
1110
- addEventListener(
1111
- "mouseup",
1112
- () => {
1113
- this.scrubbing = false;
1114
- },
1115
- { once: true },
1116
- );
1117
- }
1118
-
1119
- applyScrub(e: MouseEvent) {
1120
- const gutter = this.shadowRoot?.querySelector("#gutter");
1121
- if (!gutter) {
1122
- return;
1123
- }
1124
- const rect = gutter.getBoundingClientRect();
1125
- if (this.targetTemporal) {
1126
- const layerX = e.pageX - rect.left + gutter.scrollLeft;
1127
- const scrubTimeMs = layerX / this.pixelsPerMs;
1128
- this.targetTemporal.currentTimeMs = scrubTimeMs;
1129
- }
1130
- }
1131
-
1132
- @eventOptions({ passive: false })
1133
- scrollScrub(e: WheelEvent) {
1134
- if (this.targetTemporal && this.gutter && !this.playing) {
1135
- if (e.deltaX !== 0) {
1136
- e.preventDefault(); // Prevent default side scroll behavior only
1137
- }
1138
- // Avoid over-scrolling to the left
1139
- if (
1140
- this.gutterRef.value &&
1141
- this.gutterRef.value.scrollLeft === 0 &&
1142
- e.deltaX < 0
1143
- ) {
1144
- this.gutter.scrollBy(0, e.deltaY);
1145
- return;
1146
- }
1147
-
1148
- // Avoid over-scrolling to the right
1149
- if (
1150
- this.gutter.scrollWidth - this.gutter.scrollLeft ===
1151
- this.gutter.clientWidth &&
1152
- e.deltaX > 0
1153
- ) {
1154
- this.gutter.scrollBy(0, e.deltaY);
1155
- return;
1156
- }
1157
-
1158
- if (this) {
1159
- this.gutter.scrollBy(e.deltaX, e.deltaY);
1160
- this.targetTemporal.currentTimeMs += e.deltaX / this.pixelsPerMs;
1161
- }
1162
- }
1163
- }
1164
-
1165
- gutterRef = createRef<HTMLDivElement>();
1166
- hierarchyRef = createRef<HTMLDivElement>();
1167
- playheadRef = createRef<HTMLDivElement>();
1168
-
1169
- get gutter() {
1170
- return this.gutterRef.value;
1171
- }
1172
-
1173
- render() {
1174
- const target = this.targetTemporal;
1175
-
1176
- return html` <div
1177
- class="grid h-full bg-slate-100"
1178
- style=${styleMap({
1179
- gridTemplateColumns: "200px 1fr",
1180
- gridTemplateRows: "1.5rem 1fr",
1181
- })}
1182
- >
1183
- <div
1184
- class="z-20 col-span-2 border-b-slate-600 bg-slate-100 shadow shadow-slate-300"
1185
- >
1186
- ${
1187
- !this.autoScale
1188
- ? html`<input
1189
- type="range"
1190
- .value=${this.pixelsPerMs}
1191
- min="0.01"
1192
- max="0.1"
1193
- step="0.001"
1194
- @input=${(e: Event) => {
1195
- const target = e.target as HTMLInputElement;
1196
- this.pixelsPerMs = Number.parseFloat(target.value);
1197
- }}
1198
- />`
1199
- : nothing
1200
- }
1201
- <code>${msToTimeCode(this.currentTimeMs, true)} </code> /
1202
- <code>${msToTimeCode(target?.durationMs ?? 0, true)}</code>
1203
- <ef-toggle-play class="inline-block mx-2">
1204
- <div slot="pause">
1205
- <button>⏸️</button>
1206
- </div>
1207
- <div slot="play">
1208
- <button>▶️</button>
1209
- </div>
1210
- </ef-toggle-play>
1211
- <ef-toggle-loop><button>${this.loop ? "🔁" : html`<span class="opacity-50 line-through">🔁</span>`}</button></ef-toggle-loop>
1212
- </div>
1213
- <div
1214
- class="z-10 pl-1 pr-1 pt-[8px] shadow shadow-slate-600 overflow-auto"
1215
- ${ref(this.hierarchyRef)}
1216
- @scroll=${this.syncHierarchyScroll}
1217
- >
1218
- ${renderHierarchyChildren(
1219
- target ? ([target] as unknown as Element[]) : [],
1220
- this.hideSelectors,
1221
- this.showSelectors,
1222
- true,
1223
- )}
1224
- </div>
1225
- <div
1226
- class="flex h-full w-full cursor-crosshair overflow-auto bg-slate-200 pt-[8px]"
1227
- id="gutter"
1228
- ${ref(this.gutterRef)}
1229
- @scroll=${this.syncGutterScroll}
1230
- @wheel=${this.scrollScrub}
1231
- >
1232
- <div
1233
- class="relative h-full w-full"
1234
- style="width: ${this.pixelsPerMs * (target?.durationMs ?? 0)}px;"
1235
- @mousemove=${this.scrub}
1236
- @mousedown=${this.startScrub}
1237
- >
1238
- <div
1239
- class="border-red pointer-events-none absolute z-[20] h-full w-[2px] border-r-2 border-red-700"
1240
- style=${styleMap({
1241
- left: `${this.pixelsPerMs * this.currentTimeMs}px`,
1242
- top: `${this.timelineScrolltop}px`,
1243
- })}
1244
- ${ref(this.playheadRef)}
1245
- ></div>
1246
-
1247
- ${renderFilmstripChildren(
1248
- target ? ([target] as unknown as Element[]) : [],
1249
- this.pixelsPerMs,
1250
- this.hideSelectors,
1251
- this.showSelectors,
1252
- true,
1253
- )}
1254
- </div>
1255
- </div>
1256
- </div>`;
1257
- }
1258
-
1259
- updated(changes: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
1260
- if (!this.targetTemporal) {
1261
- return;
1262
- }
1263
- if (changes.has("currentTimeMs")) {
1264
- if (this.targetTemporal.currentTimeMs !== this.currentTimeMs) {
1265
- this.targetTemporal.currentTimeMs = this.currentTimeMs;
1266
- }
1267
- }
1268
- }
1269
-
1270
- get #contextElement(): EFWorkbench | EFPreview | null {
1271
- return this.closest("ef-workbench, ef-preview") as EFWorkbench | EFPreview;
1272
- }
1273
-
1274
- @property({ type: String })
1275
- target = "";
1276
-
1277
- @state()
1278
- targetElement: Element | null = null;
1279
-
1280
- #targetController?: TargetController;
1281
- #lastTargetTemporal?: TemporalMixinInterface | null;
1282
-
1283
- @consume({ context: targetTemporalContext, subscribe: true })
1284
- @state()
1285
- private _contextProvidedTemporal?: TemporalMixinInterface | null;
1286
-
1287
- get targetTemporal(): TemporalMixinInterface | null {
1288
- const fromTarget =
1289
- this.targetElement && isEFTemporal(this.targetElement)
1290
- ? (this.targetElement as TemporalMixinInterface & HTMLElement)
1291
- : null;
1292
- const fromContext = this._contextProvidedTemporal;
1293
-
1294
- if (fromTarget && fromContext && fromTarget !== fromContext) {
1295
- console.warn(
1296
- "EFFilmstrip: Both target attribute and parent context found. Using target attribute.",
1297
- { target: this.target, fromTarget, fromContext },
1298
- );
1299
- }
1300
-
1301
- return fromTarget ?? fromContext ?? null;
1302
- }
1303
-
1304
- protected willUpdate(
1305
- changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
1306
- ) {
1307
- if (changedProperties.has("target")) {
1308
- if (this.target && !this.#targetController) {
1309
- this.#targetController = new TargetController(this);
1310
- }
1311
- }
1312
-
1313
- const currentTargetTemporal = this.targetTemporal;
1314
- if (this.#lastTargetTemporal !== currentTargetTemporal) {
1315
- this.#bindToTargetTimegroup();
1316
- this.#lastTargetTemporal = currentTargetTemporal;
1317
- }
1318
-
1319
- if (this.autoScale) {
1320
- this.updatePixelsPerMs();
1321
- }
1322
- super.willUpdate(changedProperties);
1323
- }
1324
- }
1325
-
1326
- declare global {
1327
- interface HTMLElementTagNameMap {
1328
- "ef-filmstrip": EFFilmstrip;
1329
- "ef-timegroup-hierarchy-item": EFTimegroupHierarchyItem;
1330
- "ef-audio-hierarchy-item": EFAudioHierarchyItem;
1331
- "ef-video-hierarchy-item": EFVideoHierarchyItem;
1332
- "ef-captions-hierarchy-item": EFCaptionsHierarchyItem;
1333
- "ef-captions-active-word-hierarchy-item": EFCaptionsActiveWordHierarchyItem;
1334
- "ef-waveform-hierarchy-item": EFWaveformHierarchyItem;
1335
- "ef-image-hierarchy-item": EFImageHierarchyItem;
1336
- "ef-html-hierarchy-item": EFHTMLHierarchyItem;
1337
- "ef-timegroup-filmstrip": EFTimegroupFilmstrip;
1338
- "ef-audio-filmstrip": EFAudioFilmstrip;
1339
- "ef-video-filmstrip": EFVideoFilmstrip;
1340
- "ef-captions-filmstrip": EFCaptionsFilmstrip;
1341
- "ef-captions-active-word-filmstrip": EFCaptionsActiveWordFilmstrip;
1342
- "ef-captions-segment-filmstrip": EFCaptionsSegmentFilmstrip;
1343
- "ef-captions-before-word-filmstrip": EFCaptionsBeforeWordFilmstrip;
1344
- "ef-captions-after-word-filmstrip": EFCaptionsAfterWordFilmstrip;
1345
- "ef-waveform-filmstrip": EFWaveformFilmstrip;
1346
- "ef-image-filmstrip": EFImageFilmstrip;
1347
- "ef-html-filmstrip": EFHTMLFilmstrip;
1348
- }
1349
- }