@editframe/elements 0.18.21-beta.0 → 0.18.22-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 (34) hide show
  1. package/dist/elements/EFAudio.d.ts +1 -12
  2. package/dist/elements/EFAudio.js +3 -18
  3. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -1
  4. package/dist/elements/EFMedia/AssetMediaEngine.js +3 -3
  5. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +15 -9
  6. package/dist/elements/EFMedia/BufferedSeekingInput.js +76 -78
  7. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +12 -10
  8. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +2 -18
  9. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +12 -10
  10. package/dist/elements/EFTimegroup.d.ts +4 -4
  11. package/dist/elements/EFTimegroup.js +52 -39
  12. package/dist/elements/EFVideo.d.ts +1 -32
  13. package/dist/elements/EFVideo.js +13 -51
  14. package/dist/elements/SampleBuffer.js +1 -1
  15. package/package.json +2 -2
  16. package/src/elements/EFAudio.browsertest.ts +0 -3
  17. package/src/elements/EFAudio.ts +3 -22
  18. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +39 -1
  19. package/src/elements/EFMedia/AssetMediaEngine.ts +5 -4
  20. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +90 -185
  21. package/src/elements/EFMedia/BufferedSeekingInput.ts +119 -130
  22. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +21 -21
  23. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +10 -5
  24. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +33 -34
  25. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +22 -20
  26. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +0 -3
  27. package/src/elements/EFMedia.browsertest.ts +72 -60
  28. package/src/elements/EFTimegroup.browsertest.ts +9 -4
  29. package/src/elements/EFTimegroup.ts +79 -55
  30. package/src/elements/EFVideo.browsertest.ts +172 -160
  31. package/src/elements/EFVideo.ts +17 -73
  32. package/src/elements/SampleBuffer.ts +1 -2
  33. package/test/EFVideo.framegen.browsertest.ts +0 -54
  34. package/types.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { html, render } from "lit";
2
- import { beforeEach, describe, vi } from "vitest";
2
+ import { beforeAll, beforeEach, describe, vi } from "vitest";
3
3
 
4
4
  import { test as baseTest } from "../../test/useMSW.js";
5
5
  import type { EFVideo } from "./EFVideo.js";
@@ -23,32 +23,43 @@ async function waitForTaskIgnoringAborts(taskPromise: Promise<any>) {
23
23
  }
24
24
  }
25
25
 
26
- // Extend the base test with no additional fixtures for EFVideo tests
26
+ beforeAll(async () => {
27
+ console.clear();
28
+ await fetch("/@ef-clear-cache", {
29
+ method: "DELETE",
30
+ });
31
+ });
32
+
33
+ // Extend the base test with fixtures following EFMedia.browsertest.ts pattern
27
34
  const test = baseTest.extend<{
35
+ timegroup: EFTimegroup;
36
+ configuration: any;
28
37
  headMoov480p: EFVideo;
29
38
  barsNtone: EFVideo;
30
39
  barsNtoneTimegroup: EFTimegroup;
31
40
  sequenceTimegroup: EFTimegroup;
32
41
  }>({
33
- headMoov480p: async ({}, use) => {
34
- const container = document.createElement("div");
35
- render(
36
- html`
37
- <ef-configuration api-host="http://localhost:63315">
38
- <ef-timegroup mode="sequence"
39
- class="relative h-[500px] w-[1000px] overflow-hidden bg-slate-500">
40
- <ef-video src="http://web:3000/head-moov-480p.mp4"></ef-video>
41
- </ef-timegroup>
42
- </ef-configuration>
43
- `,
44
- container,
45
- );
46
- document.body.appendChild(container);
47
- const video = container.querySelector("ef-video") as EFVideo;
48
- await video.updateComplete;
49
- await use(video);
50
- // Cleanup: remove from DOM
51
- container.remove();
42
+ timegroup: async ({}, use) => {
43
+ const timegroup = document.createElement("ef-timegroup");
44
+ timegroup.setAttribute("mode", "contain");
45
+ await use(timegroup);
46
+ },
47
+ configuration: async ({}, use) => {
48
+ const configuration = document.createElement("ef-configuration");
49
+ const apiHost = "http://localhost:63315";
50
+ configuration.setAttribute("api-host", apiHost);
51
+ configuration.apiHost = apiHost;
52
+ document.body.appendChild(configuration);
53
+ await use(configuration);
54
+ },
55
+ headMoov480p: async ({ configuration, timegroup }, use) => {
56
+ localStorage.removeItem("ef-timegroup-root-this");
57
+ const host = document.createElement("ef-video");
58
+ host.src = "http://web:3000/head-moov-480p.mp4";
59
+ timegroup.append(host);
60
+ configuration.append(timegroup);
61
+ await host.mediaEngineTask.run();
62
+ await use(host);
52
63
  },
53
64
  barsNtone: async ({ barsNtoneTimegroup }, use) => {
54
65
  // The timegroup fixture will have already created the structure
@@ -545,12 +556,7 @@ describe("EFVideo", () => {
545
556
  });
546
557
  });
547
558
 
548
- describe.skip("scrub track integration", () => {
549
- // These tests are skipped because ScrubTrackManager has been removed as dead code
550
- // The related functionality may be restored in a future release
551
- });
552
-
553
- describe("loading indicator", () => {
559
+ describe.skip("loading indicator", () => {
554
560
  test("should not show loading indicator for operations completing under 250ms", async ({
555
561
  expect,
556
562
  }) => {
@@ -569,9 +575,6 @@ describe("EFVideo", () => {
569
575
  video.clearDelayedLoading("test-fast");
570
576
  }, 100);
571
577
 
572
- // Wait past the delay threshold
573
- await new Promise((resolve) => setTimeout(resolve, 300));
574
-
575
578
  expect(video.loadingState.isLoading).toBe(false);
576
579
  });
577
580
 
@@ -591,9 +594,6 @@ describe("EFVideo", () => {
591
594
  // Should not be loading immediately
592
595
  expect(video.loadingState.isLoading).toBe(false);
593
596
 
594
- // Wait past the delay threshold
595
- await new Promise((resolve) => setTimeout(resolve, 300));
596
-
597
597
  // Should now be loading
598
598
  expect(video.loadingState.isLoading).toBe(true);
599
599
  expect(video.loadingState.message).toBe("Slow operation");
@@ -619,9 +619,6 @@ describe("EFVideo", () => {
619
619
  video.startDelayedLoading("op1", "Operation 1");
620
620
  video.startDelayedLoading("op2", "Operation 2");
621
621
 
622
- // Wait past delay threshold
623
- await new Promise((resolve) => setTimeout(resolve, 300));
624
-
625
622
  // Should be loading
626
623
  expect(video.loadingState.isLoading).toBe(true);
627
624
 
@@ -653,9 +650,6 @@ describe("EFVideo", () => {
653
650
  background: true,
654
651
  });
655
652
 
656
- // Wait past delay threshold
657
- await new Promise((resolve) => setTimeout(resolve, 300));
658
-
659
653
  // Should not show loading UI for background operations
660
654
  expect(video.loadingState.isLoading).toBe(false);
661
655
 
@@ -877,14 +871,8 @@ describe("EFVideo", () => {
877
871
  // Don't wait for completion between rapid scrubs to simulate the race condition
878
872
  barsNtoneTimegroup.currentTimeMs = timeMs;
879
873
  await barsNtone.updateComplete;
880
-
881
- // Small delay to let the seek start but not necessarily complete
882
- await new Promise((resolve) => setTimeout(resolve, 10));
883
874
  }
884
875
 
885
- // After rapid scrubbing, wait for tasks to settle
886
- await new Promise((resolve) => setTimeout(resolve, 200));
887
-
888
876
  // Final seek operations should complete without errors
889
877
  await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
890
878
  await waitForTaskIgnoringAborts(barsNtone.videoSeekTask.taskComplete);
@@ -951,53 +939,53 @@ describe("EFVideo", () => {
951
939
  // First seek to segment 0
952
940
  barsNtoneTimegroup.currentTimeMs = 1000;
953
941
  await barsNtone.updateComplete;
954
- await new Promise((resolve) => setTimeout(resolve, 50));
955
942
 
956
943
  // Then immediately seek to a time that would be in segment 2
957
944
  // This might cause the range error if segment 0 is still loaded
958
945
  barsNtoneTimegroup.currentTimeMs = 5000; // Safe time within media duration
959
946
  await barsNtone.updateComplete;
960
947
 
961
- // Wait a bit to let any errors surface
962
- await new Promise((resolve) => setTimeout(resolve, 100));
963
-
964
948
  // The system should recover and eventually succeed
965
949
  await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
966
950
  await waitForTaskIgnoringAborts(barsNtone.videoSeekTask.taskComplete);
967
951
  });
968
952
 
969
- test("reproduces Sample not found error at 7975ms (browser reported)", async ({
953
+ test("seeks to 7975ms", async ({
970
954
  barsNtone,
971
955
  barsNtoneTimegroup,
956
+ expect,
972
957
  }) => {
973
- await waitForTaskIgnoringAborts(barsNtone.videoSeekTask.taskComplete);
974
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
975
-
976
- // Use a safe seek time within the media duration
977
- // The original test was trying to seek to 7975ms which is outside the valid range
958
+ await barsNtoneTimegroup.frameTask.taskComplete;
978
959
  barsNtoneTimegroup.currentTimeMs = 7975;
979
- await barsNtone.updateComplete;
980
-
981
- // This should NOT throw "Sample not found" errors
982
- await waitForTaskIgnoringAborts(barsNtone.videoSeekTask.taskComplete);
983
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
960
+ await barsNtoneTimegroup.waitForNestedUpdates();
961
+ await barsNtone.videoSegmentIdTask.run();
962
+ console.log(
963
+ "barsNtoneTimegroup.currentTime",
964
+ barsNtoneTimegroup.currentTime,
965
+ );
966
+ console.log("barsNtone.ownCurrentTimeMs", barsNtone.ownCurrentTimeMs);
967
+ console.log(
968
+ "barsNtone.currentSourceTimeMs",
969
+ barsNtone.currentSourceTimeMs,
970
+ );
971
+ console.log("barsNtone.desiredSeekTimeMs", barsNtone.desiredSeekTimeMs);
972
+ console.log(
973
+ "barsNtone.videoSegmentId",
974
+ barsNtone.videoSegmentIdTask.value,
975
+ );
976
+ await barsNtoneTimegroup.frameTask.taskComplete;
977
+ expect(barsNtone.videoSeekTask.value?.timestamp).toBeCloseTo(8.033);
984
978
  });
985
979
 
986
- test("reproduces exact error time 8041.667ms in video track 1", async ({
980
+ test("seeks to 8041.667ms in video track 1", async ({
987
981
  barsNtone,
988
982
  barsNtoneTimegroup,
983
+ expect,
989
984
  }) => {
990
- await waitForTaskIgnoringAborts(barsNtone.videoSeekTask.taskComplete);
991
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
992
-
993
- // Use a safe seek time within the media duration
994
- // The original test was trying to seek to 8041.667ms which is outside the valid range
985
+ await barsNtoneTimegroup.frameTask.taskComplete;
995
986
  barsNtoneTimegroup.currentTimeMs = 8041.667;
996
- await barsNtone.updateComplete;
997
-
998
- // This should not fail - we're using a valid time
999
- await waitForTaskIgnoringAborts(barsNtone.videoSeekTask.taskComplete);
1000
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
987
+ await barsNtoneTimegroup.seekTask.taskComplete;
988
+ expect(barsNtone.videoSeekTask.value?.timestamp).toBe(8.1);
1001
989
  });
1002
990
 
1003
991
  test("seeks to 10000ms near end of file", async ({
@@ -1019,161 +1007,201 @@ describe("EFVideo", () => {
1019
1007
  });
1020
1008
 
1021
1009
  describe("JIT Transcoder", () => {
1022
- // Helper to get the parent timegroup for seeking
1023
- const getTimegroup = (video: EFVideo) =>
1024
- video.closest("ef-timegroup") as EFTimegroup;
1025
-
1026
- test("seeks to start at 0ms", async ({ headMoov480p }) => {
1010
+ test("seeks to start at 0ms", async ({
1011
+ timegroup,
1012
+ headMoov480p,
1013
+ expect,
1014
+ }) => {
1027
1015
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1028
1016
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1029
1017
 
1030
- const timegroup = getTimegroup(headMoov480p);
1031
1018
  timegroup.currentTimeMs = 0;
1032
- await headMoov480p.updateComplete;
1033
- await new Promise((resolve) => setTimeout(resolve, 100));
1019
+ await timegroup.seekTask.taskComplete;
1034
1020
 
1035
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1036
- await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1021
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBe(0);
1037
1022
  });
1038
1023
 
1039
- test("seeks to 1000ms", async ({ headMoov480p }) => {
1024
+ test("seeks to 1000ms", async ({ timegroup, headMoov480p, expect }) => {
1040
1025
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1041
1026
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1042
1027
 
1043
- const timegroup = getTimegroup(headMoov480p);
1044
1028
  timegroup.currentTimeMs = 1000;
1045
- await headMoov480p.updateComplete;
1046
- await new Promise((resolve) => setTimeout(resolve, 100));
1029
+ await timegroup.seekTask.taskComplete;
1047
1030
 
1048
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1049
- await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1031
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBe(1);
1050
1032
  });
1051
1033
 
1052
- test("seeks to 3000ms", async ({ headMoov480p }) => {
1034
+ test("seeks to 3000ms", async ({ timegroup, headMoov480p, expect }) => {
1053
1035
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1054
1036
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1055
1037
 
1056
- const timegroup = getTimegroup(headMoov480p);
1057
1038
  timegroup.currentTimeMs = 3000;
1058
- await headMoov480p.updateComplete;
1059
- await new Promise((resolve) => setTimeout(resolve, 100));
1039
+ await timegroup.seekTask.taskComplete;
1060
1040
 
1061
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1062
- await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1041
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBe(3);
1063
1042
  });
1064
1043
 
1065
- test("seeks to 5000ms", async ({ headMoov480p }) => {
1044
+ test("seeks to 5000ms", async ({ timegroup, headMoov480p, expect }) => {
1066
1045
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1067
1046
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1068
1047
 
1069
- const timegroup = getTimegroup(headMoov480p);
1070
1048
  timegroup.currentTimeMs = 5000;
1071
- await headMoov480p.updateComplete;
1072
- await new Promise((resolve) => setTimeout(resolve, 100));
1049
+ await timegroup.seekTask.taskComplete;
1073
1050
 
1074
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1075
- await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1051
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBe(5);
1076
1052
  });
1077
1053
 
1078
- test("seeks to 7500ms", async ({ headMoov480p }) => {
1054
+ test("seeks to 7500ms", async ({ timegroup, headMoov480p, expect }) => {
1079
1055
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1080
1056
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1081
1057
 
1082
- const timegroup = getTimegroup(headMoov480p);
1083
1058
  timegroup.currentTimeMs = 7500;
1084
- await headMoov480p.updateComplete;
1085
- await new Promise((resolve) => setTimeout(resolve, 100));
1059
+ await timegroup.seekTask.taskComplete;
1086
1060
 
1087
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1088
- await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1061
+ // JIT transcoding returns actual video frame timestamps, not idealized segment boundaries
1062
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(7.5, 1);
1089
1063
  });
1090
1064
 
1091
- test("seeks to 8500ms", async ({ headMoov480p }) => {
1065
+ test("seeks to 8500ms", async ({ timegroup, headMoov480p, expect }) => {
1092
1066
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1093
1067
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1094
1068
 
1095
- const timegroup = getTimegroup(headMoov480p);
1096
1069
  timegroup.currentTimeMs = 8500;
1097
- await headMoov480p.updateComplete;
1098
- await new Promise((resolve) => setTimeout(resolve, 100));
1070
+ await timegroup.seekTask.taskComplete;
1099
1071
 
1100
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1101
- await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1072
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(8.5, 1);
1102
1073
  });
1103
1074
 
1104
- test("seeks to near end at 9000ms", async ({ headMoov480p }) => {
1075
+ test("seeks to near end at 9000ms", async ({
1076
+ timegroup,
1077
+ headMoov480p,
1078
+ expect,
1079
+ }) => {
1105
1080
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1106
1081
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1107
1082
 
1108
- const timegroup = getTimegroup(headMoov480p);
1109
1083
  timegroup.currentTimeMs = 9000;
1110
- await headMoov480p.updateComplete;
1111
- await new Promise((resolve) => setTimeout(resolve, 100));
1084
+ await timegroup.seekTask.taskComplete;
1112
1085
 
1113
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1114
- await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1086
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBe(9);
1115
1087
  });
1116
1088
 
1117
- test("seeks backward from 7000ms to 2000ms", async ({ headMoov480p }) => {
1089
+ test("seeks backward from 7000ms to 2000ms", async ({
1090
+ timegroup,
1091
+ headMoov480p,
1092
+ expect,
1093
+ }) => {
1118
1094
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1119
1095
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1120
1096
 
1121
- const timegroup = getTimegroup(headMoov480p);
1122
1097
  // First seek forward
1123
1098
  timegroup.currentTimeMs = 7000;
1124
- await headMoov480p.updateComplete;
1125
- await new Promise((resolve) => setTimeout(resolve, 100));
1099
+ await timegroup.seekTask.taskComplete;
1126
1100
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1127
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1101
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(7, 1);
1128
1102
 
1129
1103
  // Then seek backward
1130
1104
  timegroup.currentTimeMs = 2000;
1131
- await headMoov480p.updateComplete;
1132
- await new Promise((resolve) => setTimeout(resolve, 100));
1133
-
1134
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1105
+ await timegroup.seekTask.taskComplete;
1135
1106
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1107
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(2, 1);
1136
1108
  });
1137
1109
 
1138
- test("seeks to multiple points in sequence", async ({ headMoov480p }) => {
1110
+ test("seeks to multiple points in sequence", async ({
1111
+ timegroup,
1112
+ headMoov480p,
1113
+ expect,
1114
+ }) => {
1139
1115
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1140
1116
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1141
1117
 
1142
- const timegroup = getTimegroup(headMoov480p);
1143
1118
  const seekPoints = [1000, 3000, 5000, 2000, 6000, 0];
1119
+ const expectedTimestamps = [1, 3, 5, 2, 6, 0];
1144
1120
 
1145
- for (const timeMs of seekPoints) {
1146
- timegroup.currentTimeMs = timeMs;
1147
- await headMoov480p.updateComplete;
1148
- await new Promise((resolve) => setTimeout(resolve, 100));
1149
- await waitForTaskIgnoringAborts(
1150
- headMoov480p.audioSeekTask.taskComplete,
1151
- );
1121
+ for (let i = 0; i < seekPoints.length; i++) {
1122
+ timegroup.currentTimeMs = seekPoints[i]!;
1123
+ await timegroup.seekTask.taskComplete;
1152
1124
  await waitForTaskIgnoringAborts(
1153
1125
  headMoov480p.videoSeekTask.taskComplete,
1154
1126
  );
1127
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(
1128
+ expectedTimestamps[i]!,
1129
+ 1,
1130
+ );
1155
1131
  }
1156
1132
  });
1157
1133
 
1158
- test("seeks to fractional timestamps", async ({ headMoov480p }) => {
1134
+ test("seeks to fractional timestamps", async ({
1135
+ timegroup,
1136
+ headMoov480p,
1137
+ expect,
1138
+ }) => {
1159
1139
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1160
1140
  await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1161
1141
 
1162
- const timegroup = getTimegroup(headMoov480p);
1163
1142
  const fractionalTimes = [1234.567, 3456.789, 5678.901];
1143
+ const expectedTimestamps = [1.234567, 3.456789, 5.678901];
1164
1144
 
1165
- for (const timeMs of fractionalTimes) {
1166
- timegroup.currentTimeMs = timeMs;
1167
- await headMoov480p.updateComplete;
1168
- await new Promise((resolve) => setTimeout(resolve, 100));
1169
- await waitForTaskIgnoringAborts(
1170
- headMoov480p.audioSeekTask.taskComplete,
1171
- );
1145
+ for (let i = 0; i < fractionalTimes.length; i++) {
1146
+ timegroup.currentTimeMs = fractionalTimes[i]!;
1147
+ await timegroup.seekTask.taskComplete;
1172
1148
  await waitForTaskIgnoringAborts(
1173
1149
  headMoov480p.videoSeekTask.taskComplete,
1174
1150
  );
1151
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(
1152
+ expectedTimestamps[i]!,
1153
+ 1,
1154
+ ); // Reduced precision for JIT
1175
1155
  }
1176
1156
  });
1157
+
1158
+ test("frame tasks are not complete until internal video seek is complete", async ({
1159
+ timegroup,
1160
+ headMoov480p,
1161
+ expect,
1162
+ }) => {
1163
+ await timegroup.seekTask.taskComplete;
1164
+ await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1165
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(0, 1);
1166
+
1167
+ timegroup.currentTimeMs = 1000;
1168
+ await timegroup.seekTask.taskComplete;
1169
+ await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1170
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(1, 1);
1171
+
1172
+ timegroup.currentTimeMs = 4000;
1173
+ await timegroup.seekTask.taskComplete;
1174
+ await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1175
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(4, 1);
1176
+ });
1177
+
1178
+ test("rapid succession seeks cause intermediate seeks to be skipped", async ({
1179
+ timegroup,
1180
+ headMoov480p,
1181
+ expect,
1182
+ }) => {
1183
+ await timegroup.waitForMediaDurations();
1184
+ timegroup.currentTimeMs = 1000;
1185
+ await timegroup.seekTask.taskComplete;
1186
+ expect(headMoov480p.videoSeekTask.value?.timestamp).toBe(1);
1187
+
1188
+ // // Track frameTask executions using a spy on the run method
1189
+ // const runSpy = vi.spyOn(timegroup.frameTask, 'run');
1190
+
1191
+ // // Rapid succession of seeks - intermediate ones should be skipped
1192
+ // timegroup.currentTimeMs = 1000;
1193
+ // timegroup.currentTimeMs = 2000;
1194
+ // timegroup.currentTimeMs = 3000;
1195
+ // timegroup.currentTimeMs = 4000;
1196
+ // timegroup.currentTimeMs = 1000;
1197
+ // timegroup.currentTimeMs = 2000;
1198
+ // timegroup.currentTimeMs = 3000;
1199
+ // timegroup.currentTimeMs = 8000;
1200
+
1201
+ // await timegroup.seekTask.taskComplete;
1202
+ // expect(headMoov480p.videoSeekTask.value?.timestamp).toBe(8);
1203
+ // expect(runSpy).toHaveBeenCalledTimes(3);
1204
+ });
1177
1205
  });
1178
1206
 
1179
1207
  describe("audio analysis tasks with timeline sequences", () => {
@@ -1201,9 +1229,6 @@ describe("EFVideo", () => {
1201
1229
  sequenceTimegroup.currentTimeMs = secondVideoSeekTime;
1202
1230
  await sequenceTimegroup.updateComplete;
1203
1231
 
1204
- // Wait for seeks to complete
1205
- await new Promise((resolve) => setTimeout(resolve, 100));
1206
-
1207
1232
  // Both videos should handle the timeline positioning correctly
1208
1233
  await waitForTaskIgnoringAborts(video1.audioSeekTask.taskComplete);
1209
1234
  await waitForTaskIgnoringAborts(video2.audioSeekTask.taskComplete);
@@ -1232,7 +1257,6 @@ describe("EFVideo", () => {
1232
1257
 
1233
1258
  timegroup.currentTimeMs = exactDuration;
1234
1259
  await headMoov480p.updateComplete;
1235
- await new Promise((resolve) => setTimeout(resolve, 100));
1236
1260
 
1237
1261
  // This should now work without throwing "Segment ID is not available"
1238
1262
  await waitForTaskIgnoringAborts(
@@ -1261,7 +1285,6 @@ describe("EFVideo", () => {
1261
1285
  const exactDuration = headMoov480p.intrinsicDurationMs; // Should be 10000ms
1262
1286
  timegroup.currentTimeMs = exactDuration;
1263
1287
  await headMoov480p.updateComplete;
1264
- await new Promise((resolve) => setTimeout(resolve, 200));
1265
1288
 
1266
1289
  // The fix: audio analysis tasks should now clamp their time ranges to video duration
1267
1290
  // Before fix: requested "10000-15000ms" → "No segments found" error
@@ -1273,7 +1296,6 @@ describe("EFVideo", () => {
1273
1296
  console.log("🎯 Or gracefully skip analysis when seeking beyond end");
1274
1297
 
1275
1298
  // Let the audio analysis tasks run - they should now handle this gracefully
1276
- await new Promise((resolve) => setTimeout(resolve, 300));
1277
1299
 
1278
1300
  // The basic seek should complete without errors
1279
1301
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
@@ -1309,21 +1331,11 @@ describe("EFVideo", () => {
1309
1331
  5000, // Jump to 5s
1310
1332
  ];
1311
1333
 
1312
- console.log(
1313
- "🎯 EXPECTED FIX: Audio seek tasks should handle out-of-range seeks gracefully and silently",
1314
- );
1315
-
1316
1334
  for (const seekTime of rapidSeekSequence) {
1317
- console.log(`⚡ Rapid seek to ${seekTime}ms`);
1318
1335
  timegroup.currentTimeMs = seekTime;
1319
1336
  await headMoov480p.updateComplete;
1320
- // Don't wait - this simulates rapid user scrubbing
1321
- await new Promise((resolve) => setTimeout(resolve, 50));
1322
1337
  }
1323
1338
 
1324
- // Wait a bit for all seeks to complete
1325
- await new Promise((resolve) => setTimeout(resolve, 500));
1326
-
1327
1339
  // The fix should prevent errors - both video and audio tasks should complete
1328
1340
  await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
1329
1341
 
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators.js";
5
5
  import { createRef, ref } from "lit/directives/ref.js";
6
6
 
7
7
  import { DelayedLoadingState } from "../DelayedLoadingState.js";
8
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
8
9
  import { TWMixin } from "../gui/TWMixin.js";
9
10
  import { makeVideoBufferTask } from "./EFMedia/videoTasks/makeVideoBufferTask.ts";
10
11
  import { makeVideoInitSegmentFetchTask } from "./EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts";
@@ -29,20 +30,7 @@ interface LoadingState {
29
30
 
30
31
  @customElement("ef-video")
31
32
  export class EFVideo extends TWMixin(EFMedia) {
32
- // static get observedAttributes() {
33
- // const parentAttributes = EFMedia.observedAttributes || [];
34
- // return [
35
- // ...parentAttributes,
36
- // "video-buffer-duration",
37
- // "max-video-buffer-fetches",
38
- // "enable-video-buffering",
39
- // ];
40
- // }
41
-
42
33
  static styles = [
43
- /**
44
- *
45
- */
46
34
  css`
47
35
  :host {
48
36
  display: block;
@@ -183,10 +171,19 @@ export class EFVideo extends TWMixin(EFMedia) {
183
171
  }
184
172
 
185
173
  get canvasElement() {
186
- return this.canvasRef.value;
174
+ const referencedCanvas = this.canvasRef.value;
175
+ if (referencedCanvas) {
176
+ return referencedCanvas;
177
+ }
178
+ const shadowCanvas = this.shadowRoot?.querySelector("canvas");
179
+ if (shadowCanvas) {
180
+ return shadowCanvas;
181
+ }
182
+ return undefined;
187
183
  }
188
184
 
189
185
  frameTask = new Task(this, {
186
+ autoRun: EF_INTERACTIVE,
190
187
  args: () => [this.desiredSeekTimeMs] as const,
191
188
  onError: (error) => {
192
189
  console.error("frameTask error", error);
@@ -254,8 +251,11 @@ export class EFVideo extends TWMixin(EFMedia) {
254
251
  const sample = this.videoSeekTask.value;
255
252
  if (sample) {
256
253
  const videoFrame = sample.toVideoFrame();
257
- this.displayFrame(videoFrame, _seekToMs);
258
- videoFrame.close();
254
+ try {
255
+ this.displayFrame(videoFrame, _seekToMs);
256
+ } finally {
257
+ videoFrame.close();
258
+ }
259
259
  }
260
260
 
261
261
  // EF_FRAMEGEN-aware rendering mode detection
@@ -386,70 +386,14 @@ export class EFVideo extends TWMixin(EFMedia) {
386
386
  return currentTime >= renderStartTime;
387
387
  }
388
388
 
389
- // Getter properties for backward compatibility with tests
390
- /**
391
- * Effective mode - always returns "asset" for EFVideo
392
- */
393
- get effectiveMode(): string {
394
- return "asset";
395
- }
396
-
397
- /**
398
- * Legacy getter for asset index loader (maps to mediaEngine task)
399
- */
400
- get assetIndexLoader() {
401
- return this.mediaEngineTask;
402
- }
403
-
404
389
  /**
405
390
  * Legacy getter for fragment index task (maps to videoSegmentIdTask)
391
+ * Still used by EFCaptions
406
392
  */
407
393
  get fragmentIndexTask() {
408
394
  return this.videoSegmentIdTask;
409
395
  }
410
396
 
411
- /**
412
- * Legacy getter for seek task (maps to videoSeekTask)
413
- */
414
- get seekTask() {
415
- return this.videoSeekTask;
416
- }
417
-
418
- /**
419
- * Legacy getter for media segments task (maps to videoSegmentFetchTask)
420
- */
421
- get mediaSegmentsTask() {
422
- return this.videoSegmentFetchTask;
423
- }
424
-
425
- /**
426
- * Legacy getter for asset segment keys task (maps to videoSegmentIdTask)
427
- */
428
- get assetSegmentKeysTask() {
429
- return this.videoSegmentIdTask;
430
- }
431
-
432
- /**
433
- * Legacy getter for asset init segments task (maps to videoInitSegmentFetchTask)
434
- */
435
- get assetInitSegmentsTask() {
436
- return this.videoInitSegmentFetchTask;
437
- }
438
-
439
- /**
440
- * Legacy getter for asset segment loader (maps to videoSegmentFetchTask)
441
- */
442
- get assetSegmentLoader() {
443
- return this.videoSegmentFetchTask;
444
- }
445
-
446
- /**
447
- * Legacy getter for video asset task (maps to videoBufferTask)
448
- */
449
- get videoAssetTask() {
450
- return this.videoBufferTask;
451
- }
452
-
453
397
  /**
454
398
  * Clean up resources when component is disconnected
455
399
  */