@editframe/elements 0.25.1-beta.0 → 0.26.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.
Files changed (57) hide show
  1. package/dist/elements/EFAudio.d.ts +4 -4
  2. package/dist/elements/EFCaptions.d.ts +12 -12
  3. package/dist/elements/EFImage.d.ts +4 -4
  4. package/dist/elements/EFMedia/AssetMediaEngine.js +2 -1
  5. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  6. package/dist/elements/EFMedia/BaseMediaEngine.js +13 -0
  7. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  8. package/dist/elements/EFMedia/JitMediaEngine.js +2 -1
  9. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  10. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +11 -4
  11. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
  12. package/dist/elements/EFMedia/shared/BufferUtils.js +16 -1
  13. package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
  14. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +11 -4
  15. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
  16. package/dist/elements/EFMedia.d.ts +2 -2
  17. package/dist/elements/EFSurface.d.ts +4 -4
  18. package/dist/elements/EFTemporal.js +16 -2
  19. package/dist/elements/EFTemporal.js.map +1 -1
  20. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  21. package/dist/elements/EFTimegroup.d.ts +22 -0
  22. package/dist/elements/EFTimegroup.js +35 -0
  23. package/dist/elements/EFTimegroup.js.map +1 -1
  24. package/dist/elements/EFVideo.d.ts +4 -4
  25. package/dist/elements/EFWaveform.d.ts +4 -4
  26. package/dist/elements/updateAnimations.js +3 -1
  27. package/dist/elements/updateAnimations.js.map +1 -1
  28. package/dist/gui/EFConfiguration.d.ts +4 -4
  29. package/dist/gui/EFControls.d.ts +2 -2
  30. package/dist/gui/EFDial.d.ts +4 -4
  31. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  32. package/dist/gui/EFPause.d.ts +2 -2
  33. package/dist/gui/EFPlay.d.ts +2 -2
  34. package/dist/gui/EFPreview.d.ts +4 -4
  35. package/dist/gui/EFResizableBox.d.ts +4 -4
  36. package/dist/gui/EFScrubber.d.ts +4 -4
  37. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  38. package/dist/gui/EFToggleLoop.d.ts +2 -2
  39. package/dist/gui/EFTogglePlay.d.ts +4 -4
  40. package/dist/gui/EFWorkbench.d.ts +6 -6
  41. package/dist/style.css +10 -0
  42. package/dist/transcoding/types/index.d.ts +1 -0
  43. package/package.json +2 -2
  44. package/src/elements/EFMedia/AssetMediaEngine.ts +1 -0
  45. package/src/elements/EFMedia/BaseMediaEngine.ts +20 -0
  46. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +68 -0
  47. package/src/elements/EFMedia/JitMediaEngine.ts +1 -0
  48. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +12 -0
  49. package/src/elements/EFMedia/shared/BufferUtils.ts +42 -0
  50. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +12 -0
  51. package/src/elements/EFTemporal.ts +20 -4
  52. package/src/elements/EFTimegroup.browsertest.ts +198 -0
  53. package/src/elements/EFTimegroup.ts +57 -0
  54. package/src/elements/updateAnimations.browsertest.ts +801 -0
  55. package/src/elements/updateAnimations.ts +12 -1
  56. package/src/transcoding/types/index.ts +1 -0
  57. package/types.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"updateAnimations.js","names":[],"sources":["../../src/elements/updateAnimations.ts"],"sourcesContent":["import {\n deepGetTemporalElements,\n type TemporalMixinInterface,\n} from \"./EFTemporal.ts\";\n\n// All animatable elements are temporal elements with HTMLElement interface\nexport type AnimatableElement = TemporalMixinInterface & HTMLElement;\n\n// Constants\nconst ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold\nconst DEFAULT_ANIMATION_ITERATIONS = 1;\nconst PROGRESS_PROPERTY = \"--ef-progress\";\nconst DURATION_PROPERTY = \"--ef-duration\";\nconst TRANSITION_DURATION_PROPERTY = \"--ef-transition-duration\";\nconst TRANSITION_OUT_START_PROPERTY = \"--ef-transition-out-start\";\n\n/**\n * Represents the temporal state of an element relative to the timeline\n */\ninterface TemporalState {\n progress: number;\n isVisible: boolean;\n timelineTimeMs: number;\n}\n\n/**\n * Evaluates what the element's state should be based on the timeline\n */\nexport const evaluateTemporalState = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // Root elements and elements aligned with composition end should remain visible at exact end time\n // Other elements use exclusive end for clean transitions\n const isRootElement = !(element as any).parentTimegroup;\n const isLastElementInComposition =\n element.endTimeMs === element.rootTimegroup?.endTimeMs;\n const useInclusiveEnd = isRootElement || isLastElementInComposition;\n\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n (useInclusiveEnd\n ? element.endTimeMs >= timelineTimeMs\n : element.endTimeMs > timelineTimeMs);\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Evaluates element visibility specifically for animation coordination\n * Uses inclusive end boundaries to prevent animation jumps at exact boundaries\n */\nexport const evaluateTemporalStateForAnimation = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // For animation coordination, use inclusive end for ALL elements to prevent visual jumps\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n element.endTimeMs >= timelineTimeMs;\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Updates the visual state (CSS + display) to match temporal state\n */\nconst updateVisualState = (\n element: AnimatableElement,\n state: TemporalState,\n): void => {\n // Always set progress (needed for many use cases)\n element.style.setProperty(PROGRESS_PROPERTY, `${state.progress * 100}%`);\n\n // Handle visibility\n if (!state.isVisible) {\n if (element.style.display !== \"none\") {\n element.style.display = \"none\";\n }\n return;\n }\n\n if (element.style.display === \"none\") {\n element.style.display = \"\";\n }\n\n // Set other CSS properties for visible elements only\n element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);\n element.style.setProperty(\n TRANSITION_DURATION_PROPERTY,\n `${element.parentTimegroup?.overlapMs ?? 0}ms`,\n );\n element.style.setProperty(\n TRANSITION_OUT_START_PROPERTY,\n `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,\n );\n};\n\n/**\n * Coordinates animations for a single element and its subtree, using the element as the time source\n */\nconst coordinateAnimationsForSingleElement = (\n element: AnimatableElement,\n): void => {\n const animations = element.getAnimations({ subtree: true });\n\n for (const animation of animations) {\n if (animation.playState === \"running\") {\n animation.pause();\n }\n\n const effect = animation.effect;\n if (!(effect && effect instanceof KeyframeEffect)) {\n continue;\n }\n\n const target = effect.target;\n if (!target) {\n continue;\n }\n\n // For animations in this element's subtree, always use this element as the time source\n // This handles both animations directly on the temporal element and on its non-temporal children\n const timing = effect.getTiming();\n const duration = Number(timing.duration) || 0;\n const delay = Number(timing.delay) || 0;\n const iterations =\n Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS;\n\n if (duration <= 0) {\n animation.currentTime = 0;\n continue;\n }\n\n // Use the element itself as the time source (it's guaranteed to be temporal)\n const currentTime = element.ownCurrentTimeMs ?? 0;\n\n if (currentTime < delay) {\n animation.currentTime = 0;\n continue;\n }\n\n const adjustedTime = currentTime - delay;\n const currentIteration = Math.floor(adjustedTime / duration);\n const currentIterationTime = adjustedTime % duration;\n\n // Calculate the total animation timeline length (delay + duration * iterations)\n const totalAnimationLength = delay + duration * iterations;\n\n // CRITICAL: Always keep currentTime below totalAnimationLength to prevent completion\n const maxSafeCurrentTime =\n totalAnimationLength - ANIMATION_PRECISION_OFFSET;\n\n if (currentIteration >= iterations) {\n // Animation would be complete - clamp to just before completion\n animation.currentTime = maxSafeCurrentTime;\n } else {\n // Animation in progress - clamp to safe value within current iteration\n const proposedCurrentTime =\n Math.min(currentIterationTime, duration - ANIMATION_PRECISION_OFFSET) +\n delay;\n animation.currentTime = Math.min(proposedCurrentTime, maxSafeCurrentTime);\n }\n }\n};\n\n/**\n * Main function: synchronizes DOM element with timeline\n */\nexport const updateAnimations = (element: AnimatableElement): void => {\n const temporalState = evaluateTemporalState(element);\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const temporalState = evaluateTemporalState(temporalElement);\n updateVisualState(temporalElement, temporalState);\n });\n updateVisualState(element, temporalState);\n\n // Coordinate animations - use animation-specific visibility to prevent jumps at exact boundaries\n const animationState = evaluateTemporalStateForAnimation(element);\n if (animationState.isVisible) {\n coordinateAnimationsForSingleElement(element);\n }\n\n // Coordinate animations for child elements using animation-specific visibility\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const childAnimationState =\n evaluateTemporalStateForAnimation(temporalElement);\n if (childAnimationState.isVisible) {\n coordinateAnimationsForSingleElement(temporalElement);\n }\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"updateAnimations.js","names":[],"sources":["../../src/elements/updateAnimations.ts"],"sourcesContent":["import {\n deepGetTemporalElements,\n type TemporalMixinInterface,\n} from \"./EFTemporal.ts\";\n\n// All animatable elements are temporal elements with HTMLElement interface\nexport type AnimatableElement = TemporalMixinInterface & HTMLElement;\n\n// Constants\nconst ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold\nconst DEFAULT_ANIMATION_ITERATIONS = 1;\nconst PROGRESS_PROPERTY = \"--ef-progress\";\nconst DURATION_PROPERTY = \"--ef-duration\";\nconst TRANSITION_DURATION_PROPERTY = \"--ef-transition-duration\";\nconst TRANSITION_OUT_START_PROPERTY = \"--ef-transition-out-start\";\n\n/**\n * Represents the temporal state of an element relative to the timeline\n */\ninterface TemporalState {\n progress: number;\n isVisible: boolean;\n timelineTimeMs: number;\n}\n\n/**\n * Evaluates what the element's state should be based on the timeline\n */\nexport const evaluateTemporalState = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // Root elements and elements aligned with composition end should remain visible at exact end time\n // Other elements use exclusive end for clean transitions\n const isRootElement = !(element as any).parentTimegroup;\n const isLastElementInComposition =\n element.endTimeMs === element.rootTimegroup?.endTimeMs;\n const useInclusiveEnd = isRootElement || isLastElementInComposition;\n\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n (useInclusiveEnd\n ? element.endTimeMs >= timelineTimeMs\n : element.endTimeMs > timelineTimeMs);\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Evaluates element visibility specifically for animation coordination\n * Uses inclusive end boundaries to prevent animation jumps at exact boundaries\n */\nexport const evaluateTemporalStateForAnimation = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // For animation coordination, use inclusive end for ALL elements to prevent visual jumps\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n element.endTimeMs >= timelineTimeMs;\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Updates the visual state (CSS + display) to match temporal state\n */\nconst updateVisualState = (\n element: AnimatableElement,\n state: TemporalState,\n): void => {\n // Always set progress (needed for many use cases)\n element.style.setProperty(PROGRESS_PROPERTY, `${state.progress * 100}%`);\n\n // Handle visibility\n if (!state.isVisible) {\n if (element.style.display !== \"none\") {\n element.style.display = \"none\";\n }\n return;\n }\n\n if (element.style.display === \"none\") {\n element.style.display = \"\";\n }\n\n // Set other CSS properties for visible elements only\n element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);\n element.style.setProperty(\n TRANSITION_DURATION_PROPERTY,\n `${element.parentTimegroup?.overlapMs ?? 0}ms`,\n );\n element.style.setProperty(\n TRANSITION_OUT_START_PROPERTY,\n `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,\n );\n};\n\n/**\n * Coordinates animations for a single element and its subtree, using the element as the time source\n */\nconst coordinateAnimationsForSingleElement = (\n element: AnimatableElement,\n): void => {\n const animations = element.getAnimations({ subtree: true });\n\n for (const animation of animations) {\n if (animation.playState === \"running\") {\n animation.pause();\n }\n\n const effect = animation.effect;\n if (!(effect && effect instanceof KeyframeEffect)) {\n continue;\n }\n\n const target = effect.target;\n if (!target) {\n continue;\n }\n\n // For animations in this element's subtree, always use this element as the time source\n // This handles both animations directly on the temporal element and on its non-temporal children\n const timing = effect.getTiming();\n const duration = Number(timing.duration) || 0;\n const delay = Number(timing.delay) || 0;\n const iterations =\n Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS;\n\n if (duration <= 0) {\n animation.currentTime = 0;\n continue;\n }\n\n // Use the element itself as the time source (it's guaranteed to be temporal)\n const currentTime = element.ownCurrentTimeMs ?? 0;\n\n if (currentTime < delay) {\n animation.currentTime = 0;\n continue;\n }\n\n const adjustedTime = currentTime - delay;\n const currentIteration = Math.floor(adjustedTime / duration);\n let currentIterationTime = adjustedTime % duration;\n\n // Handle animation-direction\n const direction = timing.direction || \"normal\";\n const shouldReverse =\n direction === \"reverse\" ||\n (direction === \"alternate\" && currentIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && currentIteration % 2 === 0);\n\n if (shouldReverse) {\n currentIterationTime = duration - currentIterationTime;\n }\n\n // Calculate the total animation timeline length (delay + duration * iterations)\n const totalAnimationLength = delay + duration * iterations;\n\n // CRITICAL: Always keep currentTime below totalAnimationLength to prevent completion\n const maxSafeCurrentTime =\n totalAnimationLength - ANIMATION_PRECISION_OFFSET;\n\n if (currentIteration >= iterations) {\n // Animation would be complete - clamp to just before completion\n animation.currentTime = maxSafeCurrentTime;\n } else {\n // Animation in progress - clamp to safe value within current iteration\n const proposedCurrentTime =\n Math.min(currentIterationTime, duration - ANIMATION_PRECISION_OFFSET) +\n delay;\n animation.currentTime = Math.min(proposedCurrentTime, maxSafeCurrentTime);\n }\n }\n};\n\n/**\n * Main function: synchronizes DOM element with timeline\n */\nexport const updateAnimations = (element: AnimatableElement): void => {\n const temporalState = evaluateTemporalState(element);\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const temporalState = evaluateTemporalState(temporalElement);\n updateVisualState(temporalElement, temporalState);\n });\n updateVisualState(element, temporalState);\n\n // Coordinate animations - use animation-specific visibility to prevent jumps at exact boundaries\n const animationState = evaluateTemporalStateForAnimation(element);\n if (animationState.isVisible) {\n coordinateAnimationsForSingleElement(element);\n }\n\n // Coordinate animations for child elements using animation-specific visibility\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const childAnimationState =\n evaluateTemporalStateForAnimation(temporalElement);\n if (childAnimationState.isVisible) {\n coordinateAnimationsForSingleElement(temporalElement);\n }\n });\n};\n"],"mappings":"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"}
@@ -1,15 +1,15 @@
1
- import * as lit11 from "lit";
1
+ import * as lit9 from "lit";
2
2
  import { LitElement } from "lit";
3
- import * as lit_html11 from "lit-html";
3
+ import * as lit_html9 from "lit-html";
4
4
 
5
5
  //#region src/gui/EFConfiguration.d.ts
6
6
  declare class EFConfiguration extends LitElement {
7
- static styles: lit11.CSSResult[];
7
+ static styles: lit9.CSSResult[];
8
8
  efConfiguration: this;
9
9
  apiHost?: string;
10
10
  signingURL: string;
11
11
  mediaEngine?: "cloud" | "local";
12
- render(): lit_html11.TemplateResult<1>;
12
+ render(): lit_html9.TemplateResult<1>;
13
13
  }
14
14
  declare global {
15
15
  interface HTMLElementTagNameMap {
@@ -1,7 +1,7 @@
1
1
  import { TemporalMixinInterface } from "../elements/EFTemporal.js";
2
2
  import { ControllableInterface } from "./Controllable.js";
3
3
  import { FocusContext } from "./focusContext.js";
4
- import * as lit14 from "lit";
4
+ import * as lit24 from "lit";
5
5
  import { LitElement, PropertyValueMap } from "lit";
6
6
 
7
7
  //#region src/gui/EFControls.d.ts
@@ -26,7 +26,7 @@ import { LitElement, PropertyValueMap } from "lit";
26
26
  */
27
27
  declare class EFControls extends LitElement {
28
28
  #private;
29
- static styles: lit14.CSSResult;
29
+ static styles: lit24.CSSResult;
30
30
  createRenderRoot(): this;
31
31
  /**
32
32
  * The ID of the ef-preview element to control
@@ -1,6 +1,6 @@
1
- import * as lit25 from "lit";
1
+ import * as lit23 from "lit";
2
2
  import { LitElement } from "lit";
3
- import * as lit_html22 from "lit-html";
3
+ import * as lit_html21 from "lit-html";
4
4
 
5
5
  //#region src/gui/EFDial.d.ts
6
6
  interface DialChangeDetail {
@@ -13,12 +13,12 @@ declare class EFDial extends LitElement {
13
13
  private isDragging;
14
14
  private dragStartAngle;
15
15
  private dragStartValue;
16
- static styles: lit25.CSSResult;
16
+ static styles: lit23.CSSResult;
17
17
  private getAngleFromPoint;
18
18
  private handlePointerDown;
19
19
  private handlePointerMove;
20
20
  private handlePointerUp;
21
- render(): lit_html22.TemplateResult<1>;
21
+ render(): lit_html21.TemplateResult<1>;
22
22
  }
23
23
  //#endregion
24
24
  export { DialChangeDetail, EFDial };
@@ -1,16 +1,16 @@
1
- import * as lit15 from "lit";
1
+ import * as lit25 from "lit";
2
2
  import { LitElement } from "lit";
3
- import * as lit_html12 from "lit-html";
3
+ import * as lit_html22 from "lit-html";
4
4
  import * as lit_html_directives_ref_js3 from "lit-html/directives/ref.js";
5
5
 
6
6
  //#region src/gui/EFFocusOverlay.d.ts
7
7
  declare class EFFocusOverlay extends LitElement {
8
- static styles: lit15.CSSResult;
8
+ static styles: lit25.CSSResult;
9
9
  focusedElement?: HTMLElement | null;
10
10
  overlay: lit_html_directives_ref_js3.Ref<HTMLDivElement>;
11
11
  private animationFrame?;
12
12
  drawOverlay: () => void;
13
- render(): lit_html12.TemplateResult<1>;
13
+ render(): lit_html22.TemplateResult<1>;
14
14
  connectedCallback(): void;
15
15
  disconnectedCallback(): void;
16
16
  protected updated(): void;
@@ -1,7 +1,7 @@
1
1
  import { ControllableInterface } from "./Controllable.js";
2
2
  import * as lit19 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html16 from "lit-html";
4
+ import * as lit_html17 from "lit-html";
5
5
 
6
6
  //#region src/gui/EFPause.d.ts
7
7
  declare const EFPause_base: (new (...args: any[]) => {
@@ -16,7 +16,7 @@ declare class EFPause extends EFPause_base {
16
16
  connectedCallback(): void;
17
17
  disconnectedCallback(): void;
18
18
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
19
- render(): lit_html16.TemplateResult<1>;
19
+ render(): lit_html17.TemplateResult<1>;
20
20
  handleClick: () => void;
21
21
  }
22
22
  declare global {
@@ -1,5 +1,5 @@
1
1
  import { ControllableInterface } from "./Controllable.js";
2
- import * as lit18 from "lit";
2
+ import * as lit17 from "lit";
3
3
  import { LitElement } from "lit";
4
4
  import * as lit_html15 from "lit-html";
5
5
 
@@ -10,7 +10,7 @@ declare const EFPlay_base: (new (...args: any[]) => {
10
10
  effectiveContext: ControllableInterface | null;
11
11
  }) & typeof LitElement;
12
12
  declare class EFPlay extends EFPlay_base {
13
- static styles: lit18.CSSResult[];
13
+ static styles: lit17.CSSResult[];
14
14
  playing: boolean;
15
15
  get efContext(): ControllableInterface | null;
16
16
  connectedCallback(): void;
@@ -1,19 +1,19 @@
1
1
  import { ContextMixinInterface } from "./ContextMixin.js";
2
- import * as lit10 from "lit";
2
+ import * as lit11 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html10 from "lit-html";
4
+ import * as lit_html11 from "lit-html";
5
5
 
6
6
  //#region src/gui/EFPreview.d.ts
7
7
  declare const EFPreview_base: (new (...args: any[]) => ContextMixinInterface) & typeof LitElement;
8
8
  declare class EFPreview extends EFPreview_base {
9
- static styles: lit10.CSSResult[];
9
+ static styles: lit11.CSSResult[];
10
10
  focusedElement?: HTMLElement;
11
11
  /**
12
12
  * Find the closest temporal element (timegroup, video, audio, etc.)
13
13
  */
14
14
  private findClosestTemporal;
15
15
  constructor();
16
- render(): lit_html10.TemplateResult<1>;
16
+ render(): lit_html11.TemplateResult<1>;
17
17
  }
18
18
  declare global {
19
19
  interface HTMLElementTagNameMap {
@@ -1,6 +1,6 @@
1
- import * as lit17 from "lit";
1
+ import * as lit15 from "lit";
2
2
  import { LitElement } from "lit";
3
- import * as lit_html14 from "lit-html";
3
+ import * as lit_html13 from "lit-html";
4
4
 
5
5
  //#region src/gui/EFResizableBox.d.ts
6
6
  interface BoxBounds {
@@ -18,7 +18,7 @@ declare class EFResizableBox extends LitElement {
18
18
  private dragMode;
19
19
  private interaction;
20
20
  private modifiers;
21
- static styles: lit17.CSSResult;
21
+ static styles: lit15.CSSResult;
22
22
  private resizeObserver?;
23
23
  connectedCallback(): void;
24
24
  private handlePointerDown;
@@ -33,7 +33,7 @@ declare class EFResizableBox extends LitElement {
33
33
  private simpleConstrainBounds;
34
34
  private constrainResizeDeltas;
35
35
  private dispatchBoundsChange;
36
- render(): lit_html14.TemplateResult<1>;
36
+ render(): lit_html13.TemplateResult<1>;
37
37
  private renderHandles;
38
38
  }
39
39
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { ControllableInterface } from "./Controllable.js";
2
- import * as lit23 from "lit";
2
+ import * as lit21 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html20 from "lit-html";
4
+ import * as lit_html19 from "lit-html";
5
5
 
6
6
  //#region src/gui/EFScrubber.d.ts
7
7
  declare const EFScrubber_base: (new (...args: any[]) => {
@@ -10,7 +10,7 @@ declare const EFScrubber_base: (new (...args: any[]) => {
10
10
  effectiveContext: ControllableInterface | null;
11
11
  }) & typeof LitElement;
12
12
  declare class EFScrubber extends EFScrubber_base {
13
- static styles: lit23.CSSResult[];
13
+ static styles: lit21.CSSResult[];
14
14
  playing: boolean;
15
15
  currentTimeMs: number;
16
16
  durationMs: number;
@@ -22,7 +22,7 @@ declare class EFScrubber extends EFScrubber_base {
22
22
  private boundHandlePointerDown;
23
23
  private boundHandlePointerMove;
24
24
  private boundHandlePointerUp;
25
- render(): lit_html20.TemplateResult<1>;
25
+ render(): lit_html19.TemplateResult<1>;
26
26
  connectedCallback(): void;
27
27
  disconnectedCallback(): void;
28
28
  }
@@ -1,7 +1,7 @@
1
1
  import { ControllableInterface } from "./Controllable.js";
2
- import * as lit24 from "lit";
2
+ import * as lit22 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html21 from "lit-html";
4
+ import * as lit_html20 from "lit-html";
5
5
 
6
6
  //#region src/gui/EFTimeDisplay.d.ts
7
7
  declare const EFTimeDisplay_base: (new (...args: any[]) => {
@@ -10,11 +10,11 @@ declare const EFTimeDisplay_base: (new (...args: any[]) => {
10
10
  effectiveContext: ControllableInterface | null;
11
11
  }) & typeof LitElement;
12
12
  declare class EFTimeDisplay extends EFTimeDisplay_base {
13
- static styles: lit24.CSSResult;
13
+ static styles: lit22.CSSResult;
14
14
  currentTimeMs: number;
15
15
  durationMs: number;
16
16
  private formatTime;
17
- render(): lit_html21.TemplateResult<1>;
17
+ render(): lit_html20.TemplateResult<1>;
18
18
  }
19
19
  declare global {
20
20
  interface HTMLElementTagNameMap {
@@ -1,5 +1,5 @@
1
1
  import { ControllableInterface } from "./Controllable.js";
2
- import * as lit21 from "lit";
2
+ import * as lit20 from "lit";
3
3
  import { LitElement } from "lit";
4
4
  import * as lit_html18 from "lit-html";
5
5
 
@@ -10,7 +10,7 @@ declare const EFToggleLoop_base: (new (...args: any[]) => {
10
10
  effectiveContext: ControllableInterface | null;
11
11
  }) & typeof LitElement;
12
12
  declare class EFToggleLoop extends EFToggleLoop_base {
13
- static styles: lit21.CSSResult[];
13
+ static styles: lit20.CSSResult[];
14
14
  get context(): ControllableInterface | null;
15
15
  render(): lit_html18.TemplateResult<1>;
16
16
  }
@@ -1,7 +1,7 @@
1
1
  import { ControllableInterface } from "./Controllable.js";
2
- import * as lit16 from "lit";
2
+ import * as lit14 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html13 from "lit-html";
4
+ import * as lit_html12 from "lit-html";
5
5
 
6
6
  //#region src/gui/EFTogglePlay.d.ts
7
7
  declare const EFTogglePlay_base: (new (...args: any[]) => {
@@ -10,12 +10,12 @@ declare const EFTogglePlay_base: (new (...args: any[]) => {
10
10
  effectiveContext: ControllableInterface | null;
11
11
  }) & typeof LitElement;
12
12
  declare class EFTogglePlay extends EFTogglePlay_base {
13
- static styles: lit16.CSSResult[];
13
+ static styles: lit14.CSSResult[];
14
14
  playing: boolean;
15
15
  get efContext(): ControllableInterface | null;
16
16
  connectedCallback(): void;
17
17
  disconnectedCallback(): void;
18
- render(): lit_html13.TemplateResult<1>;
18
+ render(): lit_html12.TemplateResult<1>;
19
19
  togglePlay: () => void;
20
20
  }
21
21
  declare global {
@@ -1,21 +1,21 @@
1
1
  import { ContextMixinInterface } from "./ContextMixin.js";
2
- import * as lit0 from "lit";
2
+ import * as lit10 from "lit";
3
3
  import { LitElement, PropertyValueMap } from "lit";
4
- import * as lit_html0 from "lit-html";
5
- import * as lit_html_directives_ref_js0 from "lit-html/directives/ref.js";
4
+ import * as lit_html10 from "lit-html";
5
+ import * as lit_html_directives_ref_js2 from "lit-html/directives/ref.js";
6
6
 
7
7
  //#region src/gui/EFWorkbench.d.ts
8
8
  declare const EFWorkbench_base: (new (...args: any[]) => ContextMixinInterface) & typeof LitElement;
9
9
  declare class EFWorkbench extends EFWorkbench_base {
10
- static styles: lit0.CSSResult[];
10
+ static styles: lit10.CSSResult[];
11
11
  rendering: boolean;
12
- focusOverlay: lit_html_directives_ref_js0.Ref<HTMLDivElement>;
12
+ focusOverlay: lit_html_directives_ref_js2.Ref<HTMLDivElement>;
13
13
  handleStageWheel(event: WheelEvent): void;
14
14
  connectedCallback(): void;
15
15
  disconnectedCallback(): void;
16
16
  update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
17
17
  drawOverlays: () => void;
18
- render(): lit_html0.TemplateResult<1>;
18
+ render(): lit_html10.TemplateResult<1>;
19
19
  }
20
20
  declare global {
21
21
  interface HTMLElementTagNameMap {
package/dist/style.css CHANGED
@@ -827,6 +827,10 @@ video {
827
827
  --tw-blur: blur(8px);
828
828
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
829
829
  }
830
+ .invert {
831
+ --tw-invert: invert(100%);
832
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
833
+ }
830
834
  .filter {
831
835
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
832
836
  }
@@ -838,6 +842,12 @@ video {
838
842
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
839
843
  transition-duration: 150ms;
840
844
  }
845
+ .ease-in {
846
+ transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
847
+ }
848
+ .ease-in-out {
849
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
850
+ }
841
851
  .ease-out {
842
852
  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
843
853
  }
@@ -74,6 +74,7 @@ interface MediaEngine {
74
74
  audioBufferDurationMs: number;
75
75
  maxVideoBufferFetches: number;
76
76
  maxAudioBufferFetches: number;
77
+ bufferThresholdMs: number;
77
78
  };
78
79
  /**
79
80
  * Extract thumbnail canvases at multiple timestamps efficiently
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.25.1-beta.0",
3
+ "version": "0.26.0-beta.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -13,7 +13,7 @@
13
13
  "license": "UNLICENSED",
14
14
  "dependencies": {
15
15
  "@bramus/style-observer": "^1.3.0",
16
- "@editframe/assets": "0.25.1-beta.0",
16
+ "@editframe/assets": "0.26.0-beta.0",
17
17
  "@lit/context": "^1.1.6",
18
18
  "@lit/task": "^1.0.3",
19
19
  "@opentelemetry/api": "^1.9.0",
@@ -346,6 +346,7 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
346
346
  audioBufferDurationMs: 2000,
347
347
  maxVideoBufferFetches: 1,
348
348
  maxAudioBufferFetches: 1,
349
+ bufferThresholdMs: 30000, // Timeline-aware buffering threshold
349
350
  };
350
351
  }
351
352
 
@@ -482,4 +482,24 @@ export abstract class BaseMediaEngine {
482
482
  segmentId: number,
483
483
  rendition: VideoRendition,
484
484
  ): number[];
485
+
486
+ /**
487
+ * Get buffer configuration for this media engine
488
+ * Can be overridden by subclasses to provide custom buffer settings
489
+ */
490
+ getBufferConfig(): {
491
+ videoBufferDurationMs: number;
492
+ audioBufferDurationMs: number;
493
+ maxVideoBufferFetches: number;
494
+ maxAudioBufferFetches: number;
495
+ bufferThresholdMs: number;
496
+ } {
497
+ return {
498
+ videoBufferDurationMs: 10000, // 10 seconds
499
+ audioBufferDurationMs: 10000, // 10 seconds
500
+ maxVideoBufferFetches: 3,
501
+ maxAudioBufferFetches: 3,
502
+ bufferThresholdMs: 30000, // 30 seconds - timeline-aware buffering threshold
503
+ };
504
+ }
485
505
  }
@@ -155,4 +155,72 @@ describe("JitMediaEngine", () => {
155
155
  "http://localhost:63315/api/v1/transcode/{rendition}/{segmentId}.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
156
156
  });
157
157
  });
158
+
159
+ test("calculatePlayheadDistance utility function", async ({ expect }) => {
160
+ const { calculatePlayheadDistance } = await import(
161
+ "./shared/BufferUtils.js"
162
+ );
163
+
164
+ // Element is currently active (playhead within element bounds)
165
+ expect(
166
+ calculatePlayheadDistance(
167
+ { startTimeMs: 0, endTimeMs: 2000 },
168
+ 1000, // playhead at 1s
169
+ ),
170
+ ).toBe(0);
171
+
172
+ // Element hasn't started yet (playhead before element)
173
+ expect(
174
+ calculatePlayheadDistance(
175
+ { startTimeMs: 2000, endTimeMs: 4000 },
176
+ 0, // playhead at 0s
177
+ ),
178
+ ).toBe(2000);
179
+
180
+ // Element already finished (playhead after element)
181
+ expect(
182
+ calculatePlayheadDistance(
183
+ { startTimeMs: 0, endTimeMs: 2000 },
184
+ 5000, // playhead at 5s
185
+ ),
186
+ ).toBe(3000);
187
+
188
+ // Playhead at element start boundary
189
+ expect(
190
+ calculatePlayheadDistance(
191
+ { startTimeMs: 2000, endTimeMs: 4000 },
192
+ 2000, // playhead exactly at start
193
+ ),
194
+ ).toBe(0);
195
+
196
+ // Playhead at element end boundary
197
+ expect(
198
+ calculatePlayheadDistance(
199
+ { startTimeMs: 2000, endTimeMs: 4000 },
200
+ 4000, // playhead exactly at end
201
+ ),
202
+ ).toBe(0);
203
+ });
204
+
205
+ test("buffer config includes timeline threshold", async ({ expect }) => {
206
+ const configuration = document.createElement("ef-configuration");
207
+ const apiHost = `${window.location.protocol}//${window.location.host}`;
208
+ configuration.setAttribute("api-host", apiHost);
209
+ configuration.apiHost = apiHost;
210
+ configuration.signingURL = "";
211
+
212
+ const video = document.createElement("ef-video");
213
+ video.src = "http://web:3000/head-moov-480p.mp4";
214
+ configuration.appendChild(video);
215
+ document.body.appendChild(configuration);
216
+
217
+ // Wait for media engine to initialize
218
+ const mediaEngine = await video.mediaEngineTask.taskComplete;
219
+
220
+ // Check that buffer config includes the threshold
221
+ const bufferConfig = mediaEngine.getBufferConfig();
222
+ expect(bufferConfig.bufferThresholdMs).toBe(30000);
223
+
224
+ configuration.remove();
225
+ });
158
226
  });
@@ -206,6 +206,7 @@ export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
206
206
  audioBufferDurationMs: 8000,
207
207
  maxVideoBufferFetches: 3,
208
208
  maxAudioBufferFetches: 3,
209
+ bufferThresholdMs: 30000, // Timeline-aware buffering threshold
209
210
  };
210
211
  }
211
212
 
@@ -62,8 +62,19 @@ export const makeAudioBufferTask = (host: EFMedia): AudioBufferTask => {
62
62
  bufferDurationMs,
63
63
  maxParallelFetches,
64
64
  enableBuffering: host.enableAudioBuffering,
65
+ bufferThresholdMs: engineConfig.bufferThresholdMs,
65
66
  };
66
67
 
68
+ // Timeline context for priority-based buffering
69
+ const timelineContext =
70
+ host.rootTimegroup?.currentTimeMs !== undefined
71
+ ? {
72
+ elementStartMs: host.startTimeMs,
73
+ elementEndMs: host.endTimeMs,
74
+ playheadMs: host.rootTimegroup.currentTimeMs,
75
+ }
76
+ : undefined;
77
+
67
78
  return manageMediaBuffer<AudioRendition>(
68
79
  seekTimeMs,
69
80
  currentConfig,
@@ -99,6 +110,7 @@ export const makeAudioBufferTask = (host: EFMedia): AudioBufferTask => {
99
110
  },
100
111
  logError: console.error,
101
112
  },
113
+ timelineContext,
102
114
  );
103
115
  },
104
116
  });
@@ -21,6 +21,7 @@ export interface MediaBufferConfig {
21
21
  maxParallelFetches: number;
22
22
  enableBuffering: boolean;
23
23
  enableContinuousBuffering?: boolean;
24
+ bufferThresholdMs?: number; // Timeline-aware buffering threshold (default: 30000ms)
24
25
  }
25
26
 
26
27
  /**
@@ -189,6 +190,26 @@ export const getUnrequestedSegments = (
189
190
  return segmentIds.filter((id) => !bufferState.requestedSegments.has(id));
190
191
  };
191
192
 
193
+ /**
194
+ * Calculate distance from element to playhead position
195
+ * Returns 0 if element is currently active, otherwise returns distance in milliseconds
196
+ */
197
+ export const calculatePlayheadDistance = (
198
+ element: { startTimeMs: number; endTimeMs: number },
199
+ playheadMs: number,
200
+ ): number => {
201
+ // Element hasn't started yet
202
+ if (playheadMs < element.startTimeMs) {
203
+ return element.startTimeMs - playheadMs;
204
+ }
205
+ // Element already finished
206
+ if (playheadMs > element.endTimeMs) {
207
+ return playheadMs - element.endTimeMs;
208
+ }
209
+ // Element is currently active
210
+ return 0;
211
+ };
212
+
192
213
  /**
193
214
  * Core media buffering orchestration logic - prefetch only, no data storage
194
215
  * Integrates with BaseMediaEngine's existing caching and request deduplication
@@ -202,11 +223,32 @@ export const manageMediaBuffer = async <
202
223
  durationMs: number,
203
224
  signal: AbortSignal,
204
225
  deps: MediaBufferDependencies<T>,
226
+ timelineContext?: {
227
+ elementStartMs: number;
228
+ elementEndMs: number;
229
+ playheadMs: number;
230
+ },
205
231
  ): Promise<MediaBufferState> => {
206
232
  if (!config.enableBuffering) {
207
233
  return currentState;
208
234
  }
209
235
 
236
+ // Timeline-aware buffering: skip if element is too far from playhead
237
+ if (timelineContext && config.bufferThresholdMs !== undefined) {
238
+ const distance = calculatePlayheadDistance(
239
+ {
240
+ startTimeMs: timelineContext.elementStartMs,
241
+ endTimeMs: timelineContext.elementEndMs,
242
+ },
243
+ timelineContext.playheadMs,
244
+ );
245
+
246
+ if (distance > config.bufferThresholdMs) {
247
+ // Element is too far from playhead, skip buffering
248
+ return currentState;
249
+ }
250
+ }
251
+
210
252
  const rendition = await deps.getRendition();
211
253
  if (!rendition) {
212
254
  // Cannot buffer without a rendition
@@ -57,8 +57,19 @@ export const makeVideoBufferTask = (host: EFVideo): VideoBufferTask => {
57
57
  bufferDurationMs,
58
58
  maxParallelFetches,
59
59
  enableBuffering: host.enableVideoBuffering,
60
+ bufferThresholdMs: engineConfig.bufferThresholdMs,
60
61
  };
61
62
 
63
+ // Timeline context for priority-based buffering
64
+ const timelineContext =
65
+ host.rootTimegroup?.currentTimeMs !== undefined
66
+ ? {
67
+ elementStartMs: host.startTimeMs,
68
+ elementEndMs: host.endTimeMs,
69
+ playheadMs: host.rootTimegroup.currentTimeMs,
70
+ }
71
+ : undefined;
72
+
62
73
  return manageMediaBuffer<VideoRendition>(
63
74
  seekTimeMs,
64
75
  currentConfig,
@@ -91,6 +102,7 @@ export const makeVideoBufferTask = (host: EFVideo): VideoBufferTask => {
91
102
  },
92
103
  logError: console.error,
93
104
  },
105
+ timelineContext,
94
106
  );
95
107
  },
96
108
  });