@editframe/elements 0.16.7-beta.0 → 0.17.6-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 (101) hide show
  1. package/README.md +30 -0
  2. package/dist/DecoderResetFrequency.test.d.ts +1 -0
  3. package/dist/DecoderResetRecovery.test.d.ts +1 -0
  4. package/dist/DelayedLoadingState.d.ts +48 -0
  5. package/dist/DelayedLoadingState.integration.test.d.ts +1 -0
  6. package/dist/DelayedLoadingState.js +113 -0
  7. package/dist/DelayedLoadingState.test.d.ts +1 -0
  8. package/dist/EF_FRAMEGEN.d.ts +10 -1
  9. package/dist/EF_FRAMEGEN.js +199 -179
  10. package/dist/EF_INTERACTIVE.js +2 -6
  11. package/dist/EF_RENDERING.js +1 -3
  12. package/dist/JitTranscodingClient.browsertest.d.ts +1 -0
  13. package/dist/JitTranscodingClient.d.ts +167 -0
  14. package/dist/JitTranscodingClient.js +373 -0
  15. package/dist/JitTranscodingClient.test.d.ts +1 -0
  16. package/dist/LoadingDebounce.test.d.ts +1 -0
  17. package/dist/LoadingIndicator.browsertest.d.ts +0 -0
  18. package/dist/ManualScrubTest.test.d.ts +1 -0
  19. package/dist/ScrubResolvedFlashing.test.d.ts +1 -0
  20. package/dist/ScrubTrackIntegration.test.d.ts +1 -0
  21. package/dist/ScrubTrackManager.d.ts +96 -0
  22. package/dist/ScrubTrackManager.js +216 -0
  23. package/dist/ScrubTrackManager.test.d.ts +1 -0
  24. package/dist/SegmentSwitchLoading.test.d.ts +1 -0
  25. package/dist/VideoSeekFlashing.browsertest.d.ts +0 -0
  26. package/dist/VideoStuckDiagnostic.test.d.ts +1 -0
  27. package/dist/elements/CrossUpdateController.js +13 -15
  28. package/dist/elements/EFAudio.browsertest.d.ts +0 -0
  29. package/dist/elements/EFAudio.d.ts +1 -1
  30. package/dist/elements/EFAudio.js +30 -43
  31. package/dist/elements/EFCaptions.js +337 -373
  32. package/dist/elements/EFImage.js +64 -90
  33. package/dist/elements/EFMedia.d.ts +98 -33
  34. package/dist/elements/EFMedia.js +1169 -678
  35. package/dist/elements/EFSourceMixin.js +31 -48
  36. package/dist/elements/EFTemporal.d.ts +1 -0
  37. package/dist/elements/EFTemporal.js +266 -360
  38. package/dist/elements/EFTimegroup.d.ts +3 -1
  39. package/dist/elements/EFTimegroup.js +262 -323
  40. package/dist/elements/EFVideo.browsertest.d.ts +0 -0
  41. package/dist/elements/EFVideo.d.ts +90 -2
  42. package/dist/elements/EFVideo.js +408 -111
  43. package/dist/elements/EFWaveform.js +375 -411
  44. package/dist/elements/FetchMixin.js +14 -24
  45. package/dist/elements/MediaController.d.ts +30 -0
  46. package/dist/elements/TargetController.js +130 -156
  47. package/dist/elements/TimegroupController.js +17 -19
  48. package/dist/elements/durationConverter.js +15 -4
  49. package/dist/elements/parseTimeToMs.js +4 -10
  50. package/dist/elements/printTaskStatus.d.ts +2 -0
  51. package/dist/elements/printTaskStatus.js +11 -0
  52. package/dist/elements/updateAnimations.js +39 -59
  53. package/dist/getRenderInfo.js +58 -67
  54. package/dist/gui/ContextMixin.js +203 -288
  55. package/dist/gui/EFConfiguration.js +27 -43
  56. package/dist/gui/EFFilmstrip.js +440 -620
  57. package/dist/gui/EFFitScale.js +112 -135
  58. package/dist/gui/EFFocusOverlay.js +45 -61
  59. package/dist/gui/EFPreview.js +30 -49
  60. package/dist/gui/EFScrubber.js +78 -99
  61. package/dist/gui/EFTimeDisplay.js +49 -70
  62. package/dist/gui/EFToggleLoop.js +17 -34
  63. package/dist/gui/EFTogglePlay.js +37 -58
  64. package/dist/gui/EFWorkbench.js +66 -88
  65. package/dist/gui/TWMixin.js +2 -48
  66. package/dist/gui/TWMixin2.js +31 -0
  67. package/dist/gui/efContext.js +2 -6
  68. package/dist/gui/fetchContext.js +1 -3
  69. package/dist/gui/focusContext.js +1 -3
  70. package/dist/gui/focusedElementContext.js +2 -6
  71. package/dist/gui/playingContext.js +1 -4
  72. package/dist/index.js +5 -30
  73. package/dist/msToTimeCode.js +11 -13
  74. package/dist/style.css +2 -1
  75. package/package.json +3 -3
  76. package/src/elements/EFAudio.browsertest.ts +569 -0
  77. package/src/elements/EFAudio.ts +4 -6
  78. package/src/elements/EFCaptions.browsertest.ts +0 -1
  79. package/src/elements/EFImage.browsertest.ts +0 -1
  80. package/src/elements/EFMedia.browsertest.ts +147 -115
  81. package/src/elements/EFMedia.ts +1339 -307
  82. package/src/elements/EFTemporal.browsertest.ts +0 -1
  83. package/src/elements/EFTemporal.ts +11 -0
  84. package/src/elements/EFTimegroup.ts +73 -10
  85. package/src/elements/EFVideo.browsertest.ts +680 -0
  86. package/src/elements/EFVideo.ts +729 -50
  87. package/src/elements/EFWaveform.ts +4 -4
  88. package/src/elements/MediaController.ts +108 -0
  89. package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
  90. package/src/elements/printTaskStatus.ts +16 -0
  91. package/src/elements/updateAnimations.ts +6 -0
  92. package/src/gui/TWMixin.ts +10 -3
  93. package/test/EFVideo.frame-tasks.browsertest.ts +524 -0
  94. package/test/EFVideo.framegen.browsertest.ts +118 -0
  95. package/test/createJitTestClips.ts +293 -0
  96. package/test/useAssetMSW.ts +49 -0
  97. package/test/useMSW.ts +31 -0
  98. package/types.json +1 -1
  99. package/dist/gui/TWMixin.css.js +0 -4
  100. /package/dist/elements/{TargetController.test.d.ts → TargetController.browsertest.d.ts} +0 -0
  101. /package/src/elements/{TargetController.test.ts → TargetController.browsertest.ts} +0 -0
@@ -1,15 +1,13 @@
1
1
  const msToTimeCode = (ms, subSecond = false) => {
2
- const seconds = Math.floor(ms / 1e3);
3
- const minutes = Math.floor(seconds / 60);
4
- const hours = Math.floor(minutes / 60);
5
- const pad = (num) => num.toString().padStart(2, "0");
6
- let timecode = `${pad(hours)}:${pad(minutes % 60)}:${pad(seconds % 60)}`;
7
- if (subSecond) {
8
- const subSeconds = Math.floor(ms % 1e3 / 10);
9
- timecode += `.${subSeconds.toString().padStart(2, "0")}`;
10
- }
11
- return timecode;
12
- };
13
- export {
14
- msToTimeCode
2
+ const seconds = Math.floor(ms / 1e3);
3
+ const minutes = Math.floor(seconds / 60);
4
+ const hours = Math.floor(minutes / 60);
5
+ const pad = (num) => num.toString().padStart(2, "0");
6
+ let timecode = `${pad(hours)}:${pad(minutes % 60)}:${pad(seconds % 60)}`;
7
+ if (subSecond) {
8
+ const subSeconds = Math.floor(ms % 1e3 / 10);
9
+ timecode += `.${subSeconds.toString().padStart(2, "0")}`;
10
+ }
11
+ return timecode;
15
12
  };
13
+ export { msToTimeCode };
package/dist/style.css CHANGED
@@ -1 +1,2 @@
1
- *,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.top-0{top:0}.z-10{z-index:10}.z-20{z-index:20}.col-span-2{grid-column:span 2 / span 2}.mx-2{margin-left:.5rem;margin-right:.5rem}.mb-\[1px\]{margin-bottom:1px}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-\[1\.1rem\]{height:1.1rem}.h-\[5px\]{height:5px}.h-full{height:100%}.w-1{width:.25rem}.w-\[2px\]{width:2px}.w-full{width:100%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-crosshair{cursor:crosshair}.resize{resize:both}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.items-center{align-items:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.text-nowrap{text-wrap:nowrap}.rounded{border-radius:.25rem}.border{border-width:1px}.border-r-2{border-right-width:2px}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.border-red-700{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity))}.border-slate-500{--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity))}.border-b-slate-600{--tw-border-opacity: 1;border-bottom-color:rgb(71 85 105 / var(--tw-border-opacity))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}.bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.bg-opacity-20{--tw-bg-opacity: .2}.p-\[1px\]{padding:1px}.pb-0{padding-bottom:0}.pl-1{padding-left:.25rem}.pl-2{padding-left:.5rem}.pr-0{padding-right:0}.pr-1{padding-right:.25rem}.pt-\[8px\]{padding-top:8px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.line-through{text-decoration-line:line-through}.opacity-50{opacity:.5}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-slate-300{--tw-shadow-color: #cbd5e1;--tw-shadow: var(--tw-shadow-colored)}.shadow-slate-600{--tw-shadow-color: #475569;--tw-shadow: var(--tw-shadow-colored)}.outline{outline-style:solid}.filter{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)}.hover\:bg-slate-400:hover{--tw-bg-opacity: 1;background-color:rgb(148 163 184 / var(--tw-bg-opacity))}.peer:hover~.peer-hover\:border-slate-400{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity))}.peer:hover~.peer-hover\:bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}.data-\[focused\]\:bg-slate-400[data-focused]{--tw-bg-opacity: 1;background-color:rgb(148 163 184 / var(--tw-bg-opacity))}.peer[data-focused]~.peer-data-\[focused\]\:border-slate-400{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity))}.peer[data-focused]~.peer-data-\[focused\]\:bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}
1
+ *,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.top-0{top:0}.isolate{isolation:isolate}.z-10{z-index:10}.z-20{z-index:20}.col-span-2{grid-column:span 2/span 2}.mx-2{margin-left:.5rem;margin-right:.5rem}.mb-\[1px\]{margin-bottom:1px}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-\[1\.1rem\]{height:1.1rem}.h-\[5px\]{height:5px}.h-full{height:100%}.w-1{width:.25rem}.w-\[2px\]{width:2px}.w-full{width:100%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y))rotate(var(--tw-rotate))skewX(var(--tw-skew-x))skewY(var(--tw-skew-y))scaleX(var(--tw-scale-x))scaleY(var(--tw-scale-y))}.cursor-crosshair{cursor:crosshair}.resize{resize:both}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.items-center{align-items:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.text-nowrap{text-wrap:nowrap}.rounded{border-radius:.25rem}.border{border-width:1px}.border-r-2{border-right-width:2px}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity))}.border-slate-500{--tw-border-opacity:1;border-color:rgb(100 116 139/var(--tw-border-opacity))}.border-b-slate-600{--tw-border-opacity:1;border-bottom-color:rgb(71 85 105/var(--tw-border-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity))}.bg-slate-200{--tw-bg-opacity:1;background-color:rgb(226 232 240/var(--tw-bg-opacity))}.bg-slate-300{--tw-bg-opacity:1;background-color:rgb(203 213 225/var(--tw-bg-opacity))}.bg-slate-800{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity))}.bg-opacity-20{--tw-bg-opacity:.2}.p-\[1px\]{padding:1px}.pb-0{padding-bottom:0}.pl-1{padding-left:.25rem}.pl-2{padding-left:.5rem}.pr-0{padding-right:0}.pr-1{padding-right:.25rem}.pt-\[8px\]{padding-top:8px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-5xl{font-size:3rem;line-height:1}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.line-through{text-decoration-line:line-through}.opacity-50{opacity:.5}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-slate-300{--tw-shadow-color:#cbd5e1;--tw-shadow:var(--tw-shadow-colored)}.shadow-slate-600{--tw-shadow-color:#475569;--tw-shadow:var(--tw-shadow-colored)}.outline{outline-style:solid}.blur{--tw-blur:blur(8px);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)}.filter{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)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur)var(--tw-backdrop-brightness)var(--tw-backdrop-contrast)var(--tw-backdrop-grayscale)var(--tw-backdrop-hue-rotate)var(--tw-backdrop-invert)var(--tw-backdrop-opacity)var(--tw-backdrop-saturate)var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur)var(--tw-backdrop-brightness)var(--tw-backdrop-contrast)var(--tw-backdrop-grayscale)var(--tw-backdrop-hue-rotate)var(--tw-backdrop-invert)var(--tw-backdrop-opacity)var(--tw-backdrop-saturate)var(--tw-backdrop-sepia)}.hover\:bg-slate-400:hover{--tw-bg-opacity:1;background-color:rgb(148 163 184/var(--tw-bg-opacity))}.peer:hover~.peer-hover\:border-slate-400{--tw-border-opacity:1;border-color:rgb(148 163 184/var(--tw-border-opacity))}.peer:hover~.peer-hover\:bg-slate-300{--tw-bg-opacity:1;background-color:rgb(203 213 225/var(--tw-bg-opacity))}.data-\[focused\]\:bg-slate-400[data-focused]{--tw-bg-opacity:1;background-color:rgb(148 163 184/var(--tw-bg-opacity))}.peer[data-focused]~.peer-data-\[focused\]\:border-slate-400{--tw-border-opacity:1;border-color:rgb(148 163 184/var(--tw-border-opacity))}.peer[data-focused]~.peer-data-\[focused\]\:bg-slate-300{--tw-bg-opacity:1;background-color:rgb(203 213 225/var(--tw-bg-opacity))}
2
+ /*$vite$:1*/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.16.7-beta.0",
3
+ "version": "0.17.6-beta.0",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -27,7 +27,7 @@
27
27
  "license": "UNLICENSED",
28
28
  "dependencies": {
29
29
  "@bramus/style-observer": "^1.3.0",
30
- "@editframe/assets": "0.16.7-beta.0",
30
+ "@editframe/assets": "0.17.6-beta.0",
31
31
  "@lit/context": "^1.1.2",
32
32
  "@lit/task": "^1.0.1",
33
33
  "d3": "^7.9.0",
@@ -43,7 +43,7 @@
43
43
  "autoprefixer": "^10.4.19",
44
44
  "rollup-plugin-tsconfig-paths": "^1.5.2",
45
45
  "typescript": "^5.5.4",
46
- "vite-plugin-dts": "^4.0.3",
46
+ "vite-plugin-dts": "^4.5.4",
47
47
  "vite-tsconfig-paths": "^4.3.2"
48
48
  }
49
49
  }
@@ -0,0 +1,569 @@
1
+ import { html, render } from "lit";
2
+ import { http, HttpResponse } from "msw";
3
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
4
+ import { assetMSWHandlers } from "../../test/useAssetMSW.js";
5
+ import { useMSW } from "../../test/useMSW.js";
6
+ import type { EFAudio } from "./EFAudio.js";
7
+ import "./EFAudio.js";
8
+ import "../gui/EFWorkbench.js";
9
+ import "../gui/EFPreview.js";
10
+ import "./EFTimegroup.js";
11
+
12
+ describe("EFAudio", () => {
13
+ const worker = useMSW();
14
+
15
+ beforeEach(() => {
16
+ // Clean up DOM and localStorage
17
+ while (document.body.children.length) {
18
+ document.body.children[0]?.remove();
19
+ }
20
+ localStorage.clear();
21
+
22
+ // Set up centralized MSW handlers to proxy requests to test assets
23
+ worker.use(...assetMSWHandlers);
24
+ });
25
+
26
+ afterEach(() => {
27
+ // Clean up any remaining elements
28
+ const audios = document.querySelectorAll("ef-audio");
29
+ for (const audio of audios) {
30
+ audio.remove();
31
+ }
32
+ });
33
+
34
+ describe("basic rendering", () => {
35
+ test("should be defined and render audio element", async () => {
36
+ const container = document.createElement("div");
37
+ render(
38
+ html`
39
+ <ef-workbench>
40
+ <ef-preview>
41
+ <ef-audio></ef-audio>
42
+ </ef-preview>
43
+ </ef-workbench>
44
+ `,
45
+ container,
46
+ );
47
+ document.body.appendChild(container);
48
+
49
+ const element = container.querySelector("ef-audio") as EFAudio;
50
+ // Wait for element to render
51
+ await element.updateComplete;
52
+
53
+ expect(element.tagName).toBe("EF-AUDIO");
54
+ expect(element.audioElement).toBeDefined();
55
+ expect(element.audioElement?.tagName).toBe("AUDIO");
56
+ });
57
+
58
+ test("audio element has correct default properties", async () => {
59
+ const container = document.createElement("div");
60
+ render(
61
+ html`
62
+ <ef-workbench>
63
+ <ef-preview>
64
+ <ef-audio></ef-audio>
65
+ </ef-preview>
66
+ </ef-workbench>
67
+ `,
68
+ container,
69
+ );
70
+ document.body.appendChild(container);
71
+
72
+ const audio = container.querySelector("ef-audio") as EFAudio;
73
+
74
+ // Wait for element to render
75
+ await audio.updateComplete;
76
+
77
+ const audioElement = audio.audioElement;
78
+
79
+ expect(audioElement).toBeDefined();
80
+ expect(audioElement?.controls).toBe(false); // Should not have controls by default
81
+ expect(audioElement?.preload).toBe("metadata");
82
+ });
83
+
84
+ test("inherits media properties from EFMedia", async () => {
85
+ const container = document.createElement("div");
86
+ render(
87
+ html`
88
+ <ef-workbench>
89
+ <ef-preview>
90
+ <ef-audio src="/test-audio.mp3"></ef-audio>
91
+ </ef-preview>
92
+ </ef-workbench>
93
+ `,
94
+ container,
95
+ );
96
+ document.body.appendChild(container);
97
+
98
+ const audio = container.querySelector("ef-audio") as EFAudio;
99
+
100
+ // Wait for element to render
101
+ await audio.updateComplete;
102
+
103
+ // Should inherit properties from EFMedia base class
104
+ expect(audio.src).toBe("/test-audio.mp3");
105
+ expect(audio.currentTimeMs).toBeDefined();
106
+ expect(audio.durationMs).toBeDefined();
107
+ });
108
+ });
109
+
110
+ describe("audio asset integration", () => {
111
+ test("integrates with audio asset loading", async () => {
112
+ const container = document.createElement("div");
113
+ render(
114
+ html`
115
+ <ef-preview>
116
+ <ef-audio src="/test-assets/media/bars-n-tone2.mp4" mode="asset"></ef-audio>
117
+ </ef-preview>
118
+ `,
119
+ container,
120
+ );
121
+ document.body.appendChild(container);
122
+
123
+ const audio = container.querySelector("ef-audio") as EFAudio;
124
+ await audio.updateComplete;
125
+ await audio.fragmentIndexTask.taskComplete;
126
+
127
+ expect(audio.src).toBe("/test-assets/media/bars-n-tone2.mp4");
128
+
129
+ // The audio should have loaded successfully and have a duration > 0
130
+ // We don't test for specific duration since real assets may vary
131
+ expect(audio.intrinsicDurationMs).toBeGreaterThan(0);
132
+ });
133
+
134
+ test("handles missing audio asset gracefully", async () => {
135
+ const container = document.createElement("div");
136
+ render(
137
+ html`
138
+ <ef-preview>
139
+ <ef-audio src="/nonexistent.mp3"></ef-audio>
140
+ </ef-preview>
141
+ `,
142
+ container,
143
+ );
144
+ document.body.appendChild(container);
145
+
146
+ const audio = container.querySelector("ef-audio") as EFAudio;
147
+
148
+ // Should not throw when audio asset is missing
149
+ expect(() => {
150
+ audio.frameTask.run();
151
+ }).not.toThrow();
152
+ });
153
+
154
+ test("handles audio loading errors gracefully", async () => {
155
+ // Mock 404 response for audio asset
156
+ worker.use(
157
+ http.get("/@ef-track-fragment-index//error-audio.mp3", () => {
158
+ return new HttpResponse(null, { status: 404 });
159
+ }),
160
+ );
161
+
162
+ const container = document.createElement("div");
163
+ render(
164
+ html`
165
+ <ef-preview>
166
+ <ef-audio src="/error-audio.mp3"></ef-audio>
167
+ </ef-preview>
168
+ `,
169
+ container,
170
+ );
171
+ document.body.appendChild(container);
172
+
173
+ const audio = container.querySelector("ef-audio") as EFAudio;
174
+
175
+ // Should handle loading errors gracefully
176
+ expect(() => {
177
+ audio.frameTask.run();
178
+ }).not.toThrow();
179
+ });
180
+ });
181
+
182
+ describe("frame task integration", () => {
183
+ test("frameTask coordinates all required media tasks", async () => {
184
+ const container = document.createElement("div");
185
+ render(
186
+ html`
187
+ <ef-preview>
188
+ <ef-audio src="/test-audio.mp3"></ef-audio>
189
+ </ef-preview>
190
+ `,
191
+ container,
192
+ );
193
+ document.body.appendChild(container);
194
+
195
+ const audio = container.querySelector("ef-audio") as EFAudio;
196
+
197
+ // frameTask should complete without errors
198
+ expect(() => {
199
+ audio.frameTask.run();
200
+ }).not.toThrow();
201
+
202
+ // Should coordinate the expected tasks
203
+ expect(audio.fragmentIndexTask).toBeDefined();
204
+ expect(audio.mediaSegmentsTask).toBeDefined();
205
+ expect(audio.seekTask).toBeDefined();
206
+ expect(audio.videoAssetTask).toBeDefined();
207
+ });
208
+
209
+ test("frameTask handles missing dependencies", () => {
210
+ const container = document.createElement("div");
211
+ render(
212
+ html`
213
+ <ef-workbench>
214
+ <ef-preview>
215
+ <ef-audio></ef-audio>
216
+ </ef-preview>
217
+ </ef-workbench>
218
+ `,
219
+ container,
220
+ );
221
+ document.body.appendChild(container);
222
+
223
+ const audio = container.querySelector("ef-audio") as EFAudio;
224
+
225
+ // Should handle missing dependencies gracefully
226
+ expect(() => {
227
+ audio.frameTask.run();
228
+ }).not.toThrow();
229
+ });
230
+
231
+ test("frameTask completion triggers timegroup updates", async () => {
232
+ const container = document.createElement("div");
233
+ render(
234
+ html`
235
+ <ef-preview>
236
+ <ef-timegroup mode="sequence">
237
+ <ef-audio src="/test-assets/media/bars-n-tone2.mp4" mode="asset"></ef-audio>
238
+ </ef-timegroup>
239
+ </ef-preview>
240
+ `,
241
+ container,
242
+ );
243
+ document.body.appendChild(container);
244
+
245
+ const audio = container.querySelector("ef-audio") as EFAudio;
246
+ const timegroup = container.querySelector("ef-timegroup");
247
+ await audio.updateComplete;
248
+
249
+ // Wait for fragment index to load
250
+ await new Promise((resolve) => setTimeout(resolve, 300));
251
+
252
+ // frameTask should trigger timegroup updates
253
+ await audio.frameTask.run();
254
+
255
+ expect(timegroup).toBeDefined();
256
+ // The frameTask should request updates on the root timegroup
257
+ });
258
+ });
259
+
260
+ describe("audio element behavior", () => {
261
+ test("audio element can be controlled programmatically", async () => {
262
+ const container = document.createElement("div");
263
+ render(
264
+ html`
265
+ <ef-workbench>
266
+ <ef-preview>
267
+ <ef-audio></ef-audio>
268
+ </ef-preview>
269
+ </ef-workbench>
270
+ `,
271
+ container,
272
+ );
273
+ document.body.appendChild(container);
274
+
275
+ const audio = container.querySelector("ef-audio") as EFAudio;
276
+ await audio.updateComplete;
277
+
278
+ const audioElement = audio.audioElement!;
279
+
280
+ // Should be able to control audio element properties
281
+ expect(() => {
282
+ audioElement.volume = 0.5;
283
+ audioElement.muted = true;
284
+ audioElement.loop = false;
285
+ }).not.toThrow();
286
+
287
+ expect(audioElement.volume).toBe(0.5);
288
+ expect(audioElement.muted).toBe(true);
289
+ expect(audioElement.loop).toBe(false);
290
+ });
291
+
292
+ test("audio element handles invalid src gracefully", async () => {
293
+ const container = document.createElement("div");
294
+ render(
295
+ html`
296
+ <ef-workbench>
297
+ <ef-preview>
298
+ <ef-audio></ef-audio>
299
+ </ef-preview>
300
+ </ef-workbench>
301
+ `,
302
+ container,
303
+ );
304
+ document.body.appendChild(container);
305
+
306
+ const audio = container.querySelector("ef-audio") as EFAudio;
307
+ await audio.updateComplete;
308
+
309
+ const audioElement = audio.audioElement!;
310
+
311
+ // Should handle invalid src without throwing
312
+ expect(() => {
313
+ audioElement.src = "invalid://url";
314
+ }).not.toThrow();
315
+
316
+ expect(() => {
317
+ audioElement.src = "";
318
+ }).not.toThrow();
319
+ });
320
+
321
+ test("audio element events can be handled", async () => {
322
+ const container = document.createElement("div");
323
+ render(
324
+ html`
325
+ <ef-workbench>
326
+ <ef-preview>
327
+ <ef-audio></ef-audio>
328
+ </ef-preview>
329
+ </ef-workbench>
330
+ `,
331
+ container,
332
+ );
333
+ document.body.appendChild(container);
334
+
335
+ const audio = container.querySelector("ef-audio") as EFAudio;
336
+ await audio.updateComplete;
337
+
338
+ const audioElement = audio.audioElement!;
339
+
340
+ let eventFired = false;
341
+
342
+ // Should be able to add event listeners
343
+ expect(() => {
344
+ audioElement.addEventListener("loadstart", () => {
345
+ eventFired = true;
346
+ });
347
+ }).not.toThrow();
348
+
349
+ // Trigger a loadstart event
350
+ audioElement.dispatchEvent(new Event("loadstart"));
351
+ expect(eventFired).toBe(true);
352
+ });
353
+ });
354
+
355
+ describe("error handling and edge cases", () => {
356
+ test("handles audio element removal during playback", () => {
357
+ const container = document.createElement("div");
358
+ render(
359
+ html`
360
+ <ef-workbench>
361
+ <ef-preview>
362
+ <ef-audio></ef-audio>
363
+ </ef-preview>
364
+ </ef-workbench>
365
+ `,
366
+ container,
367
+ );
368
+ document.body.appendChild(container);
369
+
370
+ const audio = container.querySelector("ef-audio") as EFAudio;
371
+
372
+ // Start some operations
373
+ audio.frameTask.run();
374
+
375
+ // Remove element
376
+ audio.remove();
377
+
378
+ // Should not cause errors
379
+ expect(() => {
380
+ audio.frameTask.run();
381
+ }).not.toThrow();
382
+ });
383
+
384
+ test("handles seek operations on audio", () => {
385
+ const container = document.createElement("div");
386
+ render(
387
+ html`
388
+ <ef-workbench>
389
+ <ef-preview>
390
+ <ef-audio></ef-audio>
391
+ </ef-preview>
392
+ </ef-workbench>
393
+ `,
394
+ container,
395
+ );
396
+ document.body.appendChild(container);
397
+
398
+ const audio = container.querySelector("ef-audio") as EFAudio;
399
+
400
+ // Should handle seek operations gracefully
401
+ expect(() => {
402
+ audio.desiredSeekTimeMs = 1000; // Seek to 1 second
403
+ audio.frameTask.run();
404
+ }).not.toThrow();
405
+
406
+ expect(() => {
407
+ audio.desiredSeekTimeMs = -500; // Invalid negative time
408
+ audio.frameTask.run();
409
+ }).not.toThrow();
410
+ });
411
+
412
+ test("handles audio without src gracefully", async () => {
413
+ const container = document.createElement("div");
414
+ render(
415
+ html`
416
+ <ef-workbench>
417
+ <ef-preview>
418
+ <ef-audio></ef-audio>
419
+ </ef-preview>
420
+ </ef-workbench>
421
+ `,
422
+ container,
423
+ );
424
+ document.body.appendChild(container);
425
+
426
+ const audio = container.querySelector("ef-audio") as EFAudio;
427
+ await audio.updateComplete;
428
+
429
+ // Should handle missing src gracefully - src defaults to empty string, not null
430
+ expect(audio.src).toBe("");
431
+
432
+ expect(() => {
433
+ audio.frameTask.run();
434
+ }).not.toThrow();
435
+ });
436
+
437
+ test("handles simultaneous frame task executions", async () => {
438
+ const container = document.createElement("div");
439
+ render(
440
+ html`
441
+ <ef-workbench>
442
+ <ef-preview>
443
+ <ef-audio></ef-audio>
444
+ </ef-preview>
445
+ </ef-workbench>
446
+ `,
447
+ container,
448
+ );
449
+ document.body.appendChild(container);
450
+
451
+ const audio = container.querySelector("ef-audio") as EFAudio;
452
+
453
+ // Should handle multiple simultaneous executions
454
+ const task1 = audio.frameTask.run();
455
+ const task2 = audio.frameTask.run();
456
+ const task3 = audio.frameTask.run();
457
+
458
+ // All should complete without throwing
459
+ await expect(
460
+ Promise.allSettled([task1, task2, task3]),
461
+ ).resolves.toBeDefined();
462
+ });
463
+ });
464
+
465
+ describe("integration with timegroups", () => {
466
+ test("integrates correctly within timegroup structure", async () => {
467
+ const container = document.createElement("div");
468
+ render(
469
+ html`
470
+ <ef-preview>
471
+ <ef-timegroup mode="sequence">
472
+ <ef-audio src="/test-assets/media/bars-n-tone2.mp4" mode="asset"></ef-audio>
473
+ </ef-timegroup>
474
+ </ef-preview>
475
+ `,
476
+ container,
477
+ );
478
+ document.body.appendChild(container);
479
+
480
+ const audio = container.querySelector("ef-audio") as EFAudio;
481
+ const timegroup = container.querySelector("ef-timegroup");
482
+ await audio.updateComplete;
483
+
484
+ // Wait for fragment index to load
485
+ await new Promise((resolve) => setTimeout(resolve, 300));
486
+
487
+ expect(timegroup).toBeDefined();
488
+
489
+ // The audio should have loaded successfully within the timegroup
490
+ // We test that it has a valid duration instead of a specific value
491
+ expect(audio.intrinsicDurationMs).toBeGreaterThan(0);
492
+ });
493
+
494
+ test("respects timegroup timing properties", async () => {
495
+ const container = document.createElement("div");
496
+ render(
497
+ html`
498
+ <ef-preview>
499
+ <ef-timegroup mode="contain">
500
+ <ef-audio src="/test-assets/media/bars-n-tone2.mp4" mode="asset" sourcein="1s" sourceout="4s"></ef-audio>
501
+ </ef-timegroup>
502
+ </ef-preview>
503
+ `,
504
+ container,
505
+ );
506
+ document.body.appendChild(container);
507
+
508
+ const audio = container.querySelector("ef-audio") as EFAudio;
509
+ await audio.updateComplete;
510
+
511
+ // Wait for fragment index to load
512
+ await new Promise((resolve) => setTimeout(resolve, 300));
513
+
514
+ // Should respect sourcein/sourceout timing
515
+ expect(audio.sourceInMs).toBe(1000);
516
+ expect(audio.sourceOutMs).toBe(4000);
517
+ expect(audio.durationMs).toBe(3000); // 4s - 1s
518
+ });
519
+ });
520
+
521
+ describe("audio-specific functionality", () => {
522
+ test("inherits audio analysis capabilities from EFMedia", () => {
523
+ const container = document.createElement("div");
524
+ render(
525
+ html`
526
+ <ef-workbench>
527
+ <ef-preview>
528
+ <ef-audio></ef-audio>
529
+ </ef-preview>
530
+ </ef-workbench>
531
+ `,
532
+ container,
533
+ );
534
+ document.body.appendChild(container);
535
+
536
+ const audio = container.querySelector("ef-audio") as EFAudio;
537
+
538
+ // Should inherit audio analysis from EFMedia
539
+ expect(audio.audioBufferTask).toBeDefined();
540
+ expect(audio.frequencyDataTask).toBeDefined();
541
+ expect(audio.byteTimeDomainTask).toBeDefined();
542
+ expect(audio.fftSize).toBeDefined();
543
+ expect(audio.fftGain).toBeDefined();
544
+ });
545
+
546
+ test("can access audio track information", async () => {
547
+ const container = document.createElement("div");
548
+ render(
549
+ html`
550
+ <ef-preview>
551
+ <ef-audio src="/test-assets/media/bars-n-tone2.mp4" mode="asset"></ef-audio>
552
+ </ef-preview>
553
+ `,
554
+ container,
555
+ );
556
+ document.body.appendChild(container);
557
+
558
+ const audio = container.querySelector("ef-audio") as EFAudio;
559
+ await audio.updateComplete;
560
+
561
+ // Wait for fragment index to load
562
+ await new Promise((resolve) => setTimeout(resolve, 300));
563
+
564
+ // Should be able to access default audio track
565
+ // We test that the audio loads successfully instead of checking specific track ID
566
+ expect(audio.intrinsicDurationMs).toBeGreaterThan(0);
567
+ });
568
+ });
569
+ });
@@ -19,17 +19,15 @@ export class EFAudio extends EFMedia {
19
19
  frameTask = new Task(this, {
20
20
  args: () =>
21
21
  [
22
- this.trackFragmentIndexLoader.status,
23
- this.initSegmentsLoader.status,
22
+ this.fragmentIndexTask.status,
24
23
  this.seekTask.status,
25
- this.fetchSeekTask.status,
24
+ this.mediaSegmentsTask.status,
26
25
  this.videoAssetTask.status,
27
26
  ] as const,
28
27
  task: async () => {
29
- await this.trackFragmentIndexLoader.taskComplete;
30
- await this.initSegmentsLoader.taskComplete;
28
+ await this.fragmentIndexTask.taskComplete;
31
29
  await this.seekTask.taskComplete;
32
- await this.fetchSeekTask.taskComplete;
30
+ await this.mediaSegmentsTask.taskComplete;
33
31
  await this.videoAssetTask.taskComplete;
34
32
  this.rootTimegroup?.requestUpdate();
35
33
  },