@editframe/elements 0.18.21-beta.0 → 0.18.23-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/EFAudio.d.ts +1 -12
- package/dist/elements/EFAudio.js +3 -18
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +3 -3
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +15 -9
- package/dist/elements/EFMedia/BufferedSeekingInput.js +76 -78
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +12 -10
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +2 -18
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +12 -10
- package/dist/elements/EFTimegroup.d.ts +4 -4
- package/dist/elements/EFTimegroup.js +52 -39
- package/dist/elements/EFVideo.d.ts +1 -32
- package/dist/elements/EFVideo.js +13 -51
- package/dist/elements/SampleBuffer.js +1 -1
- package/package.json +2 -2
- package/src/elements/EFAudio.browsertest.ts +0 -3
- package/src/elements/EFAudio.ts +3 -22
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +39 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +5 -4
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +90 -185
- package/src/elements/EFMedia/BufferedSeekingInput.ts +119 -130
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +21 -21
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +10 -5
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +33 -34
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +22 -20
- package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +0 -3
- package/src/elements/EFMedia.browsertest.ts +72 -60
- package/src/elements/EFTimegroup.browsertest.ts +9 -4
- package/src/elements/EFTimegroup.ts +79 -55
- package/src/elements/EFVideo.browsertest.ts +172 -160
- package/src/elements/EFVideo.ts +17 -73
- package/src/elements/SampleBuffer.ts +1 -2
- package/test/EFVideo.framegen.browsertest.ts +0 -54
- 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
|
-
|
|
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
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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("
|
|
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("
|
|
953
|
+
test("seeks to 7975ms", async ({
|
|
970
954
|
barsNtone,
|
|
971
955
|
barsNtoneTimegroup,
|
|
956
|
+
expect,
|
|
972
957
|
}) => {
|
|
973
|
-
await
|
|
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
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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("
|
|
980
|
+
test("seeks to 8041.667ms in video track 1", async ({
|
|
987
981
|
barsNtone,
|
|
988
982
|
barsNtoneTimegroup,
|
|
983
|
+
expect,
|
|
989
984
|
}) => {
|
|
990
|
-
await
|
|
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
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
|
1033
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1019
|
+
await timegroup.seekTask.taskComplete;
|
|
1034
1020
|
|
|
1035
|
-
|
|
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
|
|
1046
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1029
|
+
await timegroup.seekTask.taskComplete;
|
|
1047
1030
|
|
|
1048
|
-
|
|
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
|
|
1059
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1039
|
+
await timegroup.seekTask.taskComplete;
|
|
1060
1040
|
|
|
1061
|
-
|
|
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
|
|
1072
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1049
|
+
await timegroup.seekTask.taskComplete;
|
|
1073
1050
|
|
|
1074
|
-
|
|
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
|
|
1085
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1059
|
+
await timegroup.seekTask.taskComplete;
|
|
1086
1060
|
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
|
1098
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1070
|
+
await timegroup.seekTask.taskComplete;
|
|
1099
1071
|
|
|
1100
|
-
|
|
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 ({
|
|
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
|
|
1111
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1084
|
+
await timegroup.seekTask.taskComplete;
|
|
1112
1085
|
|
|
1113
|
-
|
|
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 ({
|
|
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
|
|
1125
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1099
|
+
await timegroup.seekTask.taskComplete;
|
|
1126
1100
|
await waitForTaskIgnoringAborts(headMoov480p.videoSeekTask.taskComplete);
|
|
1127
|
-
|
|
1101
|
+
expect(headMoov480p.videoSeekTask.value?.timestamp).toBeCloseTo(7, 1);
|
|
1128
1102
|
|
|
1129
1103
|
// Then seek backward
|
|
1130
1104
|
timegroup.currentTimeMs = 2000;
|
|
1131
|
-
await
|
|
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 ({
|
|
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 (
|
|
1146
|
-
timegroup.currentTimeMs =
|
|
1147
|
-
await
|
|
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 ({
|
|
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 (
|
|
1166
|
-
timegroup.currentTimeMs =
|
|
1167
|
-
await
|
|
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
|
|
package/src/elements/EFVideo.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
*/
|