@editframe/elements 0.19.4-beta.0 → 0.20.0-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.
- package/dist/elements/ContextProxiesController.d.ts +40 -0
- package/dist/elements/ContextProxiesController.js +69 -0
- package/dist/elements/EFCaptions.d.ts +45 -6
- package/dist/elements/EFCaptions.js +220 -26
- package/dist/elements/EFImage.js +4 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
- package/dist/elements/EFMedia.js +25 -1
- package/dist/elements/EFSurface.browsertest.d.ts +0 -0
- package/dist/elements/EFSurface.d.ts +30 -0
- package/dist/elements/EFSurface.js +96 -0
- package/dist/elements/EFTemporal.js +7 -6
- package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
- package/dist/elements/EFThumbnailStrip.d.ts +86 -0
- package/dist/elements/EFThumbnailStrip.js +490 -0
- package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
- package/dist/elements/EFTimegroup.d.ts +6 -1
- package/dist/elements/EFTimegroup.js +46 -10
- package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
- package/dist/elements/updateAnimations.d.ts +5 -0
- package/dist/elements/updateAnimations.js +37 -13
- package/dist/getRenderInfo.js +1 -1
- package/dist/gui/ContextMixin.js +27 -14
- package/dist/gui/EFControls.browsertest.d.ts +0 -0
- package/dist/gui/EFControls.d.ts +38 -0
- package/dist/gui/EFControls.js +51 -0
- package/dist/gui/EFFilmstrip.d.ts +40 -1
- package/dist/gui/EFFilmstrip.js +240 -3
- package/dist/gui/EFPreview.js +2 -1
- package/dist/gui/EFScrubber.d.ts +6 -5
- package/dist/gui/EFScrubber.js +31 -21
- package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
- package/dist/gui/EFTimeDisplay.d.ts +2 -6
- package/dist/gui/EFTimeDisplay.js +13 -23
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/currentTimeContext.d.ts +3 -0
- package/dist/gui/currentTimeContext.js +3 -0
- package/dist/gui/durationContext.d.ts +3 -0
- package/dist/gui/durationContext.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/style.css +1 -1
- package/dist/transcoding/types/index.d.ts +11 -0
- package/dist/utils/LRUCache.d.ts +46 -0
- package/dist/utils/LRUCache.js +382 -1
- package/dist/utils/LRUCache.test.d.ts +1 -0
- package/package.json +2 -2
- package/src/elements/ContextProxiesController.ts +123 -0
- package/src/elements/EFCaptions.browsertest.ts +1820 -0
- package/src/elements/EFCaptions.ts +373 -36
- package/src/elements/EFImage.ts +4 -1
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
- package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
- package/src/elements/EFMedia.ts +38 -1
- package/src/elements/EFSurface.browsertest.ts +155 -0
- package/src/elements/EFSurface.ts +141 -0
- package/src/elements/EFTemporal.ts +14 -8
- package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
- package/src/elements/EFThumbnailStrip.ts +905 -0
- package/src/elements/EFTimegroup.browsertest.ts +56 -7
- package/src/elements/EFTimegroup.ts +70 -11
- package/src/elements/updateAnimations.browsertest.ts +333 -11
- package/src/elements/updateAnimations.ts +68 -19
- package/src/gui/ContextMixin.browsertest.ts +0 -25
- package/src/gui/ContextMixin.ts +44 -20
- package/src/gui/EFControls.browsertest.ts +175 -0
- package/src/gui/EFControls.ts +84 -0
- package/src/gui/EFFilmstrip.ts +323 -4
- package/src/gui/EFPreview.ts +2 -1
- package/src/gui/EFScrubber.ts +29 -25
- package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
- package/src/gui/EFTimeDisplay.ts +12 -40
- package/src/gui/currentTimeContext.ts +5 -0
- package/src/gui/durationContext.ts +3 -0
- package/src/transcoding/types/index.ts +13 -0
- package/src/utils/LRUCache.test.ts +272 -0
- package/src/utils/LRUCache.ts +543 -0
- package/types.json +1 -1
- package/dist/transcoding/cache/CacheManager.d.ts +0 -73
- package/src/transcoding/cache/CacheManager.ts +0 -208
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Task } from "@lit/task";
|
|
1
|
+
import { Task, TaskStatus } from "@lit/task";
|
|
2
2
|
import { css, html, LitElement, type PropertyValueMap } from "lit";
|
|
3
3
|
import { customElement, property } from "lit/decorators.js";
|
|
4
4
|
import type { GetISOBMFFFileTranscriptionResult } from "../../../api/src/index.js";
|
|
@@ -6,23 +6,24 @@ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
|
|
|
6
6
|
import { CrossUpdateController } from "./CrossUpdateController.js";
|
|
7
7
|
import { EFAudio } from "./EFAudio.js";
|
|
8
8
|
import { EFSourceMixin } from "./EFSourceMixin.js";
|
|
9
|
-
import { EFTemporal } from "./EFTemporal.js";
|
|
9
|
+
import { EFTemporal, flushStartTimeMsCache } from "./EFTemporal.js";
|
|
10
|
+
import { flushSequenceDurationCache } from "./EFTimegroup.js";
|
|
10
11
|
import { EFVideo } from "./EFVideo.js";
|
|
11
12
|
import { FetchMixin } from "./FetchMixin.js";
|
|
12
13
|
|
|
13
|
-
interface WordSegment {
|
|
14
|
+
export interface WordSegment {
|
|
14
15
|
text: string;
|
|
15
16
|
start: number;
|
|
16
17
|
end: number;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
interface Segment {
|
|
20
|
+
export interface Segment {
|
|
20
21
|
start: number;
|
|
21
22
|
end: number;
|
|
22
23
|
text: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
interface Caption {
|
|
26
|
+
export interface Caption {
|
|
26
27
|
segments: Segment[];
|
|
27
28
|
word_segments: WordSegment[];
|
|
28
29
|
}
|
|
@@ -36,6 +37,7 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
|
|
|
36
37
|
:host {
|
|
37
38
|
display: inline-block;
|
|
38
39
|
white-space: pre;
|
|
40
|
+
transform-origin: center;
|
|
39
41
|
}
|
|
40
42
|
:host([hidden]) {
|
|
41
43
|
display: none;
|
|
@@ -49,6 +51,12 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
|
|
|
49
51
|
return undefined;
|
|
50
52
|
}
|
|
51
53
|
this.hidden = false;
|
|
54
|
+
|
|
55
|
+
// Set deterministic --ef-word-seed value based on word index
|
|
56
|
+
const seed = (this.wordIndex * 9007) % 233; // Prime numbers for better distribution
|
|
57
|
+
const seedValue = seed / 233; // Normalize to 0-1 range
|
|
58
|
+
this.style.setProperty("--ef-word-seed", seedValue.toString());
|
|
59
|
+
|
|
52
60
|
return html` ${this.wordText.trim()} `;
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -61,11 +69,23 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
|
|
|
61
69
|
@property({ type: String, attribute: false })
|
|
62
70
|
wordText = "";
|
|
63
71
|
|
|
72
|
+
@property({ type: Number, attribute: false })
|
|
73
|
+
wordIndex = 0;
|
|
74
|
+
|
|
64
75
|
@property({ type: Boolean, reflect: true })
|
|
65
76
|
hidden = false;
|
|
66
77
|
|
|
67
78
|
get startTimeMs() {
|
|
68
|
-
|
|
79
|
+
// Get parent captions element's absolute start time, then add our local offset
|
|
80
|
+
const parentCaptions = this.closest("ef-captions") as EFCaptions;
|
|
81
|
+
const parentStartTime = parentCaptions?.startTimeMs || 0;
|
|
82
|
+
return parentStartTime + (this.wordStartMs || 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get endTimeMs() {
|
|
86
|
+
const parentCaptions = this.closest("ef-captions") as EFCaptions;
|
|
87
|
+
const parentStartTime = parentCaptions?.startTimeMs || 0;
|
|
88
|
+
return parentStartTime + (this.wordEndMs || 0);
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
get durationMs(): number {
|
|
@@ -108,7 +128,16 @@ export class EFCaptionsSegment extends EFTemporal(LitElement) {
|
|
|
108
128
|
hidden = false;
|
|
109
129
|
|
|
110
130
|
get startTimeMs() {
|
|
111
|
-
|
|
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);
|
|
112
141
|
}
|
|
113
142
|
|
|
114
143
|
get durationMs(): number {
|
|
@@ -152,7 +181,16 @@ export class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
|
|
|
152
181
|
segmentEndMs = 0;
|
|
153
182
|
|
|
154
183
|
get startTimeMs() {
|
|
155
|
-
|
|
184
|
+
// Get parent captions element's absolute start time, then add our local offset
|
|
185
|
+
const parentCaptions = this.closest("ef-captions") as EFCaptions;
|
|
186
|
+
const parentStartTime = parentCaptions?.startTimeMs || 0;
|
|
187
|
+
return parentStartTime + (this.segmentStartMs || 0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get endTimeMs() {
|
|
191
|
+
const parentCaptions = this.closest("ef-captions") as EFCaptions;
|
|
192
|
+
const parentStartTime = parentCaptions?.startTimeMs || 0;
|
|
193
|
+
return parentStartTime + (this.segmentEndMs || 0);
|
|
156
194
|
}
|
|
157
195
|
|
|
158
196
|
get durationMs(): number {
|
|
@@ -196,7 +234,16 @@ export class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
|
|
|
196
234
|
segmentEndMs = 0;
|
|
197
235
|
|
|
198
236
|
get startTimeMs() {
|
|
199
|
-
|
|
237
|
+
// Get parent captions element's absolute start time, then add our local offset
|
|
238
|
+
const parentCaptions = this.closest("ef-captions") as EFCaptions;
|
|
239
|
+
const parentStartTime = parentCaptions?.startTimeMs || 0;
|
|
240
|
+
return parentStartTime + (this.segmentStartMs || 0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get endTimeMs() {
|
|
244
|
+
const parentCaptions = this.closest("ef-captions") as EFCaptions;
|
|
245
|
+
const parentStartTime = parentCaptions?.startTimeMs || 0;
|
|
246
|
+
return parentStartTime + (this.segmentEndMs || 0);
|
|
200
247
|
}
|
|
201
248
|
|
|
202
249
|
get durationMs(): number {
|
|
@@ -212,15 +259,23 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
212
259
|
static styles = [
|
|
213
260
|
css`
|
|
214
261
|
:host {
|
|
215
|
-
display: flex;
|
|
216
|
-
flex-wrap: wrap;
|
|
217
|
-
align-items: baseline;
|
|
262
|
+
display: inline-flex;
|
|
218
263
|
width: fit-content;
|
|
264
|
+
align-items: baseline;
|
|
219
265
|
}
|
|
220
266
|
::slotted(*) {
|
|
221
267
|
margin: 0;
|
|
222
268
|
padding: 0;
|
|
223
269
|
}
|
|
270
|
+
::slotted(ef-captions-active-word) {
|
|
271
|
+
min-width: 0.5ch; /* Maintain minimum width when empty */
|
|
272
|
+
min-height: 1em; /* Maintain height for baseline alignment */
|
|
273
|
+
}
|
|
274
|
+
::slotted(ef-captions-active-word[hidden]) {
|
|
275
|
+
opacity: 0; /* Hide when empty but maintain layout */
|
|
276
|
+
min-width: 0.5ch;
|
|
277
|
+
min-height: 1em;
|
|
278
|
+
}
|
|
224
279
|
`,
|
|
225
280
|
];
|
|
226
281
|
|
|
@@ -240,6 +295,27 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
240
295
|
@property({ attribute: "word-style" })
|
|
241
296
|
wordStyle = "";
|
|
242
297
|
|
|
298
|
+
/**
|
|
299
|
+
* URL or path to a JSON file containing custom captions data.
|
|
300
|
+
* The JSON should conform to the Caption interface with 'segments' and 'word_segments' arrays.
|
|
301
|
+
*/
|
|
302
|
+
@property({ type: String, attribute: "captions-src", reflect: true })
|
|
303
|
+
captionsSrc = "";
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Direct captions data object. Takes priority over captions-src and captions-script.
|
|
307
|
+
* Should conform to the Caption interface with 'segments' and 'word_segments' arrays.
|
|
308
|
+
*/
|
|
309
|
+
@property({ type: Object, attribute: false })
|
|
310
|
+
captionsData: Caption | null = null;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* ID of a <script> element containing JSON captions data.
|
|
314
|
+
* The script's textContent should be valid JSON conforming to the Caption interface.
|
|
315
|
+
*/
|
|
316
|
+
@property({ type: String, attribute: "captions-script", reflect: true })
|
|
317
|
+
captionsScript = "";
|
|
318
|
+
|
|
243
319
|
activeWordContainers = this.getElementsByTagName("ef-captions-active-word");
|
|
244
320
|
segmentContainers = this.getElementsByTagName("ef-captions-segment");
|
|
245
321
|
beforeActiveWordContainers = this.getElementsByTagName(
|
|
@@ -254,6 +330,9 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
254
330
|
}
|
|
255
331
|
|
|
256
332
|
transcriptionsPath() {
|
|
333
|
+
if (!this.targetElement) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
257
336
|
if (this.targetElement.assetId) {
|
|
258
337
|
return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;
|
|
259
338
|
}
|
|
@@ -261,6 +340,9 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
261
340
|
}
|
|
262
341
|
|
|
263
342
|
captionsPath() {
|
|
343
|
+
if (!this.targetElement) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
264
346
|
if (this.targetElement.assetId) {
|
|
265
347
|
return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;
|
|
266
348
|
}
|
|
@@ -272,6 +354,9 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
272
354
|
autoRun: false,
|
|
273
355
|
args: () => [this.target, this.fetch] as const,
|
|
274
356
|
task: async ([_target, fetch], { signal }) => {
|
|
357
|
+
if (!this.targetElement) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
275
360
|
const md5Path = `/@ef-asset/${this.targetElement.src ?? ""}`;
|
|
276
361
|
const response = await fetch(md5Path, { method: "HEAD", signal });
|
|
277
362
|
return response.headers.get("etag") ?? undefined;
|
|
@@ -280,9 +365,15 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
280
365
|
|
|
281
366
|
private transcriptionDataTask = new Task(this, {
|
|
282
367
|
autoRun: EF_INTERACTIVE,
|
|
283
|
-
args: () =>
|
|
284
|
-
|
|
285
|
-
|
|
368
|
+
args: () =>
|
|
369
|
+
[
|
|
370
|
+
this.transcriptionsPath(),
|
|
371
|
+
this.fetch,
|
|
372
|
+
this.hasCustomCaptionsData,
|
|
373
|
+
] as const,
|
|
374
|
+
task: async ([transcriptionsPath, fetch, hasCustomData], { signal }) => {
|
|
375
|
+
// Skip transcription if we have custom captions data
|
|
376
|
+
if (hasCustomData || !transcriptionsPath) {
|
|
286
377
|
return null;
|
|
287
378
|
}
|
|
288
379
|
const response = await fetch(transcriptionsPath, { signal });
|
|
@@ -312,6 +403,53 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
312
403
|
},
|
|
313
404
|
});
|
|
314
405
|
|
|
406
|
+
private customCaptionsDataTask = new Task(this, {
|
|
407
|
+
autoRun: EF_INTERACTIVE,
|
|
408
|
+
args: () =>
|
|
409
|
+
[
|
|
410
|
+
this.captionsSrc,
|
|
411
|
+
this.captionsData,
|
|
412
|
+
this.captionsScript,
|
|
413
|
+
this.fetch,
|
|
414
|
+
] as const,
|
|
415
|
+
task: async (
|
|
416
|
+
[captionsSrc, captionsData, captionsScript, fetch],
|
|
417
|
+
{ signal },
|
|
418
|
+
) => {
|
|
419
|
+
// Priority: direct data > script reference > URL source
|
|
420
|
+
if (captionsData) {
|
|
421
|
+
return captionsData;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (captionsScript) {
|
|
425
|
+
const scriptElement = document.getElementById(captionsScript);
|
|
426
|
+
if (scriptElement?.textContent) {
|
|
427
|
+
try {
|
|
428
|
+
return JSON.parse(scriptElement.textContent) as Caption;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(
|
|
431
|
+
`Failed to parse captions from script #${captionsScript}:`,
|
|
432
|
+
error,
|
|
433
|
+
);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (captionsSrc) {
|
|
440
|
+
try {
|
|
441
|
+
const response = await fetch(captionsSrc, { signal });
|
|
442
|
+
return (await response.json()) as Caption;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.error(`Failed to load captions from ${captionsSrc}:`, error);
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return null;
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
|
|
315
453
|
private transcriptionFragmentDataTask = new Task(this, {
|
|
316
454
|
autoRun: EF_INTERACTIVE,
|
|
317
455
|
args: () =>
|
|
@@ -338,46 +476,104 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
338
476
|
},
|
|
339
477
|
});
|
|
340
478
|
|
|
479
|
+
unifiedCaptionsDataTask = new Task(this, {
|
|
480
|
+
autoRun: EF_INTERACTIVE,
|
|
481
|
+
args: () =>
|
|
482
|
+
[
|
|
483
|
+
this.customCaptionsDataTask.value,
|
|
484
|
+
this.transcriptionFragmentDataTask.value,
|
|
485
|
+
] as const,
|
|
486
|
+
task: async ([_customData, _transcriptionData]) => {
|
|
487
|
+
if (this.customCaptionsDataTask.status === TaskStatus.PENDING) {
|
|
488
|
+
await this.customCaptionsDataTask.taskComplete;
|
|
489
|
+
}
|
|
490
|
+
if (this.transcriptionFragmentDataTask.status === TaskStatus.PENDING) {
|
|
491
|
+
await this.transcriptionFragmentDataTask.taskComplete;
|
|
492
|
+
}
|
|
493
|
+
return (
|
|
494
|
+
this.customCaptionsDataTask.value ||
|
|
495
|
+
this.transcriptionFragmentDataTask.value
|
|
496
|
+
);
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
341
500
|
frameTask = new Task(this, {
|
|
342
501
|
autoRun: EF_INTERACTIVE,
|
|
343
|
-
args: () => [this.
|
|
502
|
+
args: () => [this.unifiedCaptionsDataTask.status],
|
|
344
503
|
task: async () => {
|
|
345
|
-
await this.
|
|
504
|
+
await this.unifiedCaptionsDataTask.taskComplete;
|
|
346
505
|
},
|
|
347
506
|
});
|
|
348
507
|
|
|
349
508
|
connectedCallback() {
|
|
350
509
|
super.connectedCallback();
|
|
351
|
-
|
|
352
|
-
|
|
510
|
+
|
|
511
|
+
// Try to get target element safely
|
|
512
|
+
const target = this.targetSelector
|
|
513
|
+
? document.getElementById(this.targetSelector)
|
|
514
|
+
: null;
|
|
515
|
+
if (target && (target instanceof EFAudio || target instanceof EFVideo)) {
|
|
516
|
+
new CrossUpdateController(target, this);
|
|
517
|
+
}
|
|
518
|
+
// For standalone captions with custom data, ensure proper timeline sync
|
|
519
|
+
else if (this.hasCustomCaptionsData && this.rootTimegroup) {
|
|
520
|
+
new CrossUpdateController(this.rootTimegroup, this);
|
|
353
521
|
}
|
|
354
522
|
}
|
|
355
523
|
|
|
356
524
|
protected updated(
|
|
357
|
-
|
|
525
|
+
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
|
|
358
526
|
): void {
|
|
359
527
|
this.updateTextContainers();
|
|
528
|
+
|
|
529
|
+
// Force duration recalculation when custom captions data changes
|
|
530
|
+
if (
|
|
531
|
+
changedProperties.has("captionsData") ||
|
|
532
|
+
changedProperties.has("captionsSrc") ||
|
|
533
|
+
changedProperties.has("captionsScript")
|
|
534
|
+
) {
|
|
535
|
+
this.requestUpdate("intrinsicDurationMs");
|
|
536
|
+
|
|
537
|
+
// Flush sequence duration cache and notify parent timegroups that child duration has changed
|
|
538
|
+
flushSequenceDurationCache();
|
|
539
|
+
flushStartTimeMsCache();
|
|
540
|
+
|
|
541
|
+
// Notify parent timegroup to recalculate its duration
|
|
542
|
+
if (this.parentTimegroup) {
|
|
543
|
+
this.parentTimegroup.requestUpdate("durationMs");
|
|
544
|
+
this.parentTimegroup.requestUpdate("currentTime");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Update captions when timeline position changes
|
|
549
|
+
if (changedProperties.has("ownCurrentTimeMs")) {
|
|
550
|
+
this.updateTextContainers();
|
|
551
|
+
}
|
|
360
552
|
}
|
|
361
553
|
|
|
362
554
|
updateTextContainers() {
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
if (!transcriptionFragment) {
|
|
555
|
+
const captionsData = this.unifiedCaptionsDataTask.value as Caption;
|
|
556
|
+
if (!captionsData) {
|
|
366
557
|
return;
|
|
367
558
|
}
|
|
368
559
|
|
|
369
|
-
|
|
560
|
+
// Get current time from target element or parent timegroup
|
|
561
|
+
const currentTimeMs = this.targetElement
|
|
562
|
+
? this.targetElement.currentSourceTimeMs
|
|
563
|
+
: this.ownCurrentTimeMs;
|
|
370
564
|
const currentTimeSec = currentTimeMs / 1000;
|
|
371
565
|
|
|
372
566
|
// Find the current word from word_segments
|
|
373
|
-
|
|
374
|
-
|
|
567
|
+
// Use exclusive end boundary to prevent overlap at exact boundaries
|
|
568
|
+
const currentWord = captionsData.word_segments.find(
|
|
569
|
+
(word) => currentTimeSec >= word.start && currentTimeSec < word.end,
|
|
375
570
|
);
|
|
376
571
|
|
|
377
572
|
// Find the current segment
|
|
378
|
-
|
|
573
|
+
// Use exclusive end boundary to prevent overlap at exact boundaries
|
|
574
|
+
const currentSegment = captionsData.segments.find(
|
|
379
575
|
(segment) =>
|
|
380
|
-
currentTimeSec >= segment.start && currentTimeSec
|
|
576
|
+
currentTimeSec >= segment.start && currentTimeSec < segment.end,
|
|
381
577
|
);
|
|
382
578
|
|
|
383
579
|
for (const wordContainer of this.activeWordContainers) {
|
|
@@ -385,6 +581,22 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
385
581
|
wordContainer.wordText = currentWord.text;
|
|
386
582
|
wordContainer.wordStartMs = currentWord.start * 1000;
|
|
387
583
|
wordContainer.wordEndMs = currentWord.end * 1000;
|
|
584
|
+
// Set word index for deterministic animation variation
|
|
585
|
+
const wordIndex = captionsData.word_segments.findIndex(
|
|
586
|
+
(w) =>
|
|
587
|
+
w.start === currentWord.start &&
|
|
588
|
+
w.end === currentWord.end &&
|
|
589
|
+
w.text === currentWord.text,
|
|
590
|
+
);
|
|
591
|
+
wordContainer.wordIndex = wordIndex >= 0 ? wordIndex : 0;
|
|
592
|
+
// Force re-render to update hidden property
|
|
593
|
+
wordContainer.requestUpdate();
|
|
594
|
+
} else {
|
|
595
|
+
// No active word - maintain layout with invisible placeholder
|
|
596
|
+
wordContainer.wordText = ""; // Empty when no active word
|
|
597
|
+
wordContainer.wordStartMs = 0;
|
|
598
|
+
wordContainer.wordEndMs = 0;
|
|
599
|
+
wordContainer.requestUpdate();
|
|
388
600
|
}
|
|
389
601
|
}
|
|
390
602
|
|
|
@@ -393,13 +605,18 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
393
605
|
segmentContainer.segmentText = currentSegment.text;
|
|
394
606
|
segmentContainer.segmentStartMs = currentSegment.start * 1000;
|
|
395
607
|
segmentContainer.segmentEndMs = currentSegment.end * 1000;
|
|
608
|
+
} else {
|
|
609
|
+
// No active segment - clear the container
|
|
610
|
+
segmentContainer.segmentText = "";
|
|
611
|
+
segmentContainer.segmentStartMs = 0;
|
|
612
|
+
segmentContainer.segmentEndMs = 0;
|
|
396
613
|
}
|
|
397
614
|
}
|
|
398
615
|
|
|
399
|
-
//
|
|
616
|
+
// Process context for both word and segment cases
|
|
400
617
|
if (currentWord && currentSegment) {
|
|
401
618
|
// Find all word segments that fall within the current segment's time range
|
|
402
|
-
const segmentWords =
|
|
619
|
+
const segmentWords = captionsData.word_segments.filter(
|
|
403
620
|
(word) =>
|
|
404
621
|
word.start >= currentSegment.start && word.end <= currentSegment.end,
|
|
405
622
|
);
|
|
@@ -423,19 +640,76 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
423
640
|
.map((w) => w.text.trim())
|
|
424
641
|
.join(" ");
|
|
425
642
|
|
|
426
|
-
// Update before containers
|
|
643
|
+
// Update before containers - should be visible at the same time as active word
|
|
427
644
|
for (const container of this.beforeActiveWordContainers) {
|
|
428
645
|
container.segmentText = beforeWords;
|
|
429
|
-
container.segmentStartMs =
|
|
430
|
-
container.segmentEndMs = currentWord.
|
|
646
|
+
container.segmentStartMs = currentWord.start * 1000;
|
|
647
|
+
container.segmentEndMs = currentWord.end * 1000;
|
|
431
648
|
}
|
|
432
649
|
|
|
433
|
-
// Update after containers
|
|
650
|
+
// Update after containers - should be visible at the same time as active word
|
|
434
651
|
for (const container of this.afterActiveWordContainers) {
|
|
435
652
|
container.segmentText = afterWords;
|
|
436
|
-
container.segmentStartMs = currentWord.
|
|
653
|
+
container.segmentStartMs = currentWord.start * 1000;
|
|
654
|
+
container.segmentEndMs = currentWord.end * 1000;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} else if (currentSegment) {
|
|
658
|
+
// No active word but we have an active segment
|
|
659
|
+
const segmentWords = captionsData.word_segments.filter(
|
|
660
|
+
(word) =>
|
|
661
|
+
word.start >= currentSegment.start && word.end <= currentSegment.end,
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Check if we're before the first word or after the last word
|
|
665
|
+
const firstWord = segmentWords[0];
|
|
666
|
+
const isBeforeFirstWord = firstWord && currentTimeSec < firstWord.start;
|
|
667
|
+
|
|
668
|
+
if (isBeforeFirstWord) {
|
|
669
|
+
// Before first word starts - show all words in "after" container (they're all upcoming)
|
|
670
|
+
const allWords = segmentWords.map((w) => w.text.trim()).join(" ");
|
|
671
|
+
|
|
672
|
+
for (const container of this.beforeActiveWordContainers) {
|
|
673
|
+
container.segmentText = ""; // Nothing before yet
|
|
674
|
+
container.segmentStartMs = currentSegment.start * 1000;
|
|
675
|
+
container.segmentEndMs = currentSegment.end * 1000;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
for (const container of this.afterActiveWordContainers) {
|
|
679
|
+
container.segmentText = allWords; // All words are upcoming
|
|
680
|
+
container.segmentStartMs = currentSegment.start * 1000;
|
|
681
|
+
container.segmentEndMs = currentSegment.end * 1000;
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
// After last word ends - show all completed words in "before" container
|
|
685
|
+
const allCompletedWords = segmentWords
|
|
686
|
+
.map((w) => w.text.trim())
|
|
687
|
+
.join(" ");
|
|
688
|
+
|
|
689
|
+
for (const container of this.beforeActiveWordContainers) {
|
|
690
|
+
container.segmentText = allCompletedWords;
|
|
691
|
+
container.segmentStartMs = currentSegment.start * 1000;
|
|
437
692
|
container.segmentEndMs = currentSegment.end * 1000;
|
|
438
693
|
}
|
|
694
|
+
|
|
695
|
+
for (const container of this.afterActiveWordContainers) {
|
|
696
|
+
container.segmentText = "";
|
|
697
|
+
container.segmentStartMs = currentSegment.start * 1000;
|
|
698
|
+
container.segmentEndMs = currentSegment.end * 1000;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
// No active segment or word - clear all context containers
|
|
703
|
+
for (const container of this.beforeActiveWordContainers) {
|
|
704
|
+
container.segmentText = "";
|
|
705
|
+
container.segmentStartMs = 0;
|
|
706
|
+
container.segmentEndMs = 0;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
for (const container of this.afterActiveWordContainers) {
|
|
710
|
+
container.segmentText = "";
|
|
711
|
+
container.segmentStartMs = 0;
|
|
712
|
+
container.segmentEndMs = 0;
|
|
439
713
|
}
|
|
440
714
|
}
|
|
441
715
|
}
|
|
@@ -445,7 +719,70 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
445
719
|
if (target instanceof EFAudio || target instanceof EFVideo) {
|
|
446
720
|
return target;
|
|
447
721
|
}
|
|
448
|
-
|
|
722
|
+
// When using custom captions data, a target is not required
|
|
723
|
+
if (this.hasCustomCaptionsData) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
get hasCustomCaptionsData(): boolean {
|
|
730
|
+
return !!(this.captionsData || this.captionsSrc || this.captionsScript);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Follow the exact EFMedia pattern for safe duration integration
|
|
734
|
+
get intrinsicDurationMs(): number | undefined {
|
|
735
|
+
// Direct access to custom captions data - avoiding complex task dependencies
|
|
736
|
+
// Priority: direct data > script reference > external file
|
|
737
|
+
let captionsData: Caption | null = null;
|
|
738
|
+
|
|
739
|
+
if (this.captionsData) {
|
|
740
|
+
captionsData = this.captionsData;
|
|
741
|
+
} else if (this.captionsScript) {
|
|
742
|
+
const scriptElement = document.getElementById(this.captionsScript);
|
|
743
|
+
if (scriptElement?.textContent) {
|
|
744
|
+
try {
|
|
745
|
+
captionsData = JSON.parse(scriptElement.textContent) as Caption;
|
|
746
|
+
} catch {
|
|
747
|
+
// Invalid JSON, fall through to return undefined
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
} else if (this.customCaptionsDataTask.value) {
|
|
751
|
+
captionsData = this.customCaptionsDataTask.value as Caption;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (!captionsData) {
|
|
755
|
+
return undefined;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (
|
|
759
|
+
captionsData.segments.length === 0 &&
|
|
760
|
+
captionsData.word_segments.length === 0
|
|
761
|
+
) {
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Find the maximum end time from both segments and word_segments
|
|
766
|
+
const maxSegmentEnd =
|
|
767
|
+
captionsData.segments.length > 0
|
|
768
|
+
? Math.max(...captionsData.segments.map((s) => s.end))
|
|
769
|
+
: 0;
|
|
770
|
+
const maxWordEnd =
|
|
771
|
+
captionsData.word_segments.length > 0
|
|
772
|
+
? Math.max(...captionsData.word_segments.map((w) => w.end))
|
|
773
|
+
: 0;
|
|
774
|
+
|
|
775
|
+
return Math.max(maxSegmentEnd, maxWordEnd) * 1000; // Convert to milliseconds
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Follow the exact EFMedia pattern for safe duration integration
|
|
779
|
+
get hasOwnDuration(): boolean {
|
|
780
|
+
// Simple check - if we have any form of custom captions data, we have our own duration
|
|
781
|
+
return !!(
|
|
782
|
+
this.captionsData ||
|
|
783
|
+
this.captionsScript ||
|
|
784
|
+
this.customCaptionsDataTask.value
|
|
785
|
+
);
|
|
449
786
|
}
|
|
450
787
|
}
|
|
451
788
|
|
package/src/elements/EFImage.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { TrackFragmentIndex } from "@editframe/assets";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
InitSegmentPaths,
|
|
4
|
+
MediaEngine,
|
|
5
|
+
VideoRendition,
|
|
6
|
+
} from "../../transcoding/types";
|
|
3
7
|
import type { UrlGenerator } from "../../transcoding/utils/UrlGenerator";
|
|
4
8
|
import type { EFMedia } from "../EFMedia";
|
|
5
9
|
import { AssetMediaEngine } from "./AssetMediaEngine";
|
|
@@ -76,4 +80,29 @@ export class AssetIdMediaEngine
|
|
|
76
80
|
buildMediaSegmentUrl(trackId: number, _segmentId: number) {
|
|
77
81
|
return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
|
|
78
82
|
}
|
|
83
|
+
|
|
84
|
+
convertToSegmentRelativeTimestamps(
|
|
85
|
+
globalTimestamps: number[],
|
|
86
|
+
segmentId: number,
|
|
87
|
+
rendition: VideoRendition,
|
|
88
|
+
): number[] {
|
|
89
|
+
if (!rendition.trackId) {
|
|
90
|
+
throw new Error("Track ID is required for asset metadata");
|
|
91
|
+
}
|
|
92
|
+
// For AssetMediaEngine, we need to calculate the actual segment start time
|
|
93
|
+
// using the precise segment boundaries from the track fragment index
|
|
94
|
+
const trackData = this.data[rendition.trackId];
|
|
95
|
+
if (!trackData) {
|
|
96
|
+
throw new Error("Track not found");
|
|
97
|
+
}
|
|
98
|
+
const segment = trackData.segments?.[segmentId];
|
|
99
|
+
if (!segment) {
|
|
100
|
+
throw new Error("Segment not found");
|
|
101
|
+
}
|
|
102
|
+
const segmentStartMs = (segment.cts / trackData.timescale) * 1000;
|
|
103
|
+
|
|
104
|
+
return globalTimestamps.map(
|
|
105
|
+
(globalMs) => (globalMs - segmentStartMs) / 1000,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
79
108
|
}
|
|
@@ -297,4 +297,37 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
297
297
|
maxAudioBufferFetches: 1,
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
|
+
|
|
301
|
+
// AssetMediaEngine inherits the default extractThumbnails from BaseMediaEngine
|
|
302
|
+
// which provides a clear warning that this engine type is not supported
|
|
303
|
+
|
|
304
|
+
convertToSegmentRelativeTimestamps(
|
|
305
|
+
globalTimestamps: number[],
|
|
306
|
+
segmentId: number,
|
|
307
|
+
rendition: VideoRendition,
|
|
308
|
+
): number[] {
|
|
309
|
+
{
|
|
310
|
+
// Asset: MediaBunny expects segment-relative timestamps in seconds
|
|
311
|
+
// This is because Asset segments are independent timeline fragments
|
|
312
|
+
|
|
313
|
+
if (!rendition.trackId) {
|
|
314
|
+
throw new Error("Track ID is required for asset metadata");
|
|
315
|
+
}
|
|
316
|
+
// For AssetMediaEngine, we need to calculate the actual segment start time
|
|
317
|
+
// using the precise segment boundaries from the track fragment index
|
|
318
|
+
const trackData = this.data[rendition.trackId];
|
|
319
|
+
if (!trackData) {
|
|
320
|
+
throw new Error("Track not found");
|
|
321
|
+
}
|
|
322
|
+
const segment = trackData.segments?.[segmentId];
|
|
323
|
+
if (!segment) {
|
|
324
|
+
throw new Error("Segment not found");
|
|
325
|
+
}
|
|
326
|
+
const segmentStartMs = (segment.cts / trackData.timescale) * 1000;
|
|
327
|
+
|
|
328
|
+
return globalTimestamps.map(
|
|
329
|
+
(globalMs) => (globalMs - segmentStartMs) / 1000,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
300
333
|
}
|