@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
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Test utility to load pre-generated JIT transcoded clips for browser testing
3
+ * Reads segments created by scripts/generate-jit-test-clips.ts
4
+ */
5
+
6
+ export interface JitTestClip {
7
+ url: string;
8
+ startTimeMs: number;
9
+ durationMs: number;
10
+ quality: "low" | "medium" | "high";
11
+ data: Uint8Array;
12
+ actualStartTimeMs: number;
13
+ actualDurationMs: number;
14
+ }
15
+
16
+ export interface CreateJitTestClipsOptions {
17
+ sourceVideoUrl: string;
18
+ segments: Array<{
19
+ startTimeMs: number;
20
+ durationMs: number;
21
+ quality: "low" | "medium" | "high";
22
+ }>;
23
+ }
24
+
25
+ // Map segments to their pre-generated filenames
26
+ const SEGMENT_FILENAMES: Record<string, string> = {
27
+ "0:2000:low": "segment-0ms-2s-low.mp4",
28
+ "0:2000:medium": "segment-0ms-2s-medium.mp4",
29
+ "0:2000:high": "segment-0ms-2s-high.mp4",
30
+ "2000:2000:medium": "segment-2000ms-2s-medium.mp4",
31
+ "4000:2000:medium": "segment-4000ms-2s-medium.mp4",
32
+ "0:4000:medium": "segment-0ms-4s-medium.mp4",
33
+ "6000:1000:low": "segment-6000ms-1s-low.mp4",
34
+ };
35
+
36
+ /**
37
+ * Create a minimal MP4 file for testing when pre-generated files aren't available
38
+ * This creates a very basic MP4 structure that can be used for testing
39
+ */
40
+ function createMockMp4Segment(durationMs = 2000): Uint8Array {
41
+ // Minimal MP4 structure: ftyp box + mdat box with dummy data
42
+ const ftypBox = new Uint8Array([
43
+ 0x00,
44
+ 0x00,
45
+ 0x00,
46
+ 0x20, // box size (32 bytes)
47
+ 0x66,
48
+ 0x74,
49
+ 0x79,
50
+ 0x70, // 'ftyp'
51
+ 0x69,
52
+ 0x73,
53
+ 0x6f,
54
+ 0x6d, // major brand 'isom'
55
+ 0x00,
56
+ 0x00,
57
+ 0x02,
58
+ 0x00, // minor version
59
+ 0x69,
60
+ 0x73,
61
+ 0x6f,
62
+ 0x6d, // compatible brand 'isom'
63
+ 0x69,
64
+ 0x73,
65
+ 0x6f,
66
+ 0x32, // compatible brand 'iso2'
67
+ 0x61,
68
+ 0x76,
69
+ 0x63,
70
+ 0x31, // compatible brand 'avc1'
71
+ 0x6d,
72
+ 0x70,
73
+ 0x34,
74
+ 0x31, // compatible brand 'mp41'
75
+ ]);
76
+
77
+ // Simple mdat box with dummy data based on duration
78
+ const dataSize = Math.max(1000, durationMs / 10); // Rough size calculation
79
+ const mdatSize = dataSize + 8; // header size
80
+ const mdatBox = new Uint8Array(mdatSize);
81
+
82
+ // mdat header
83
+ const view = new DataView(mdatBox.buffer);
84
+ view.setUint32(0, mdatSize); // box size
85
+ mdatBox.set([0x6d, 0x64, 0x61, 0x74], 4); // 'mdat'
86
+
87
+ // Fill with pseudo-random data based on duration
88
+ for (let i = 8; i < mdatSize; i++) {
89
+ mdatBox[i] = (i * durationMs) % 256;
90
+ }
91
+
92
+ // Combine boxes
93
+ const result = new Uint8Array(ftypBox.length + mdatBox.length);
94
+ result.set(ftypBox, 0);
95
+ result.set(mdatBox, ftypBox.length);
96
+
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * Load a pre-generated segment from disk
102
+ */
103
+ async function loadPreGeneratedSegment(
104
+ startTimeMs: number,
105
+ durationMs: number,
106
+ quality: "low" | "medium" | "high",
107
+ ): Promise<Uint8Array> {
108
+ const segmentKey = `${startTimeMs}:${durationMs}:${quality}`;
109
+ const filename = SEGMENT_FILENAMES[segmentKey];
110
+
111
+ if (!filename) {
112
+ console.warn(`No pre-generated segment for ${segmentKey}, using mock data`);
113
+ return createMockMp4Segment(durationMs);
114
+ }
115
+
116
+ try {
117
+ // In browser environment, we'll need to fetch from a served location
118
+ // For now, use mock data as fallback
119
+ console.warn(
120
+ `Cannot load ${filename} in browser environment, using mock data`,
121
+ );
122
+ return createMockMp4Segment(durationMs);
123
+ } catch (error) {
124
+ console.warn(`Failed to load ${filename}, using mock data:`, error);
125
+ return createMockMp4Segment(durationMs);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Create JIT test clips using pre-generated segments when available
131
+ */
132
+ export async function createJitTestClips(
133
+ options: CreateJitTestClipsOptions,
134
+ ): Promise<JitTestClip[]> {
135
+ const clips: JitTestClip[] = [];
136
+
137
+ for (const segment of options.segments) {
138
+ try {
139
+ const data = await loadPreGeneratedSegment(
140
+ segment.startTimeMs,
141
+ segment.durationMs,
142
+ segment.quality,
143
+ );
144
+
145
+ clips.push({
146
+ url: options.sourceVideoUrl,
147
+ startTimeMs: segment.startTimeMs,
148
+ durationMs: segment.durationMs,
149
+ quality: segment.quality,
150
+ data,
151
+ actualStartTimeMs: segment.startTimeMs,
152
+ actualDurationMs: segment.durationMs,
153
+ });
154
+
155
+ console.log(`✅ Loaded ${segment.quality} clip: ${data.length} bytes`);
156
+ } catch (error) {
157
+ console.error(`Failed to load ${segment.quality} test clip:`, error);
158
+ // Create mock fallback
159
+ const mockData = createMockMp4Segment(segment.durationMs);
160
+ clips.push({
161
+ url: options.sourceVideoUrl,
162
+ startTimeMs: segment.startTimeMs,
163
+ durationMs: segment.durationMs,
164
+ quality: segment.quality,
165
+ data: mockData,
166
+ actualStartTimeMs: segment.startTimeMs,
167
+ actualDurationMs: segment.durationMs,
168
+ });
169
+ }
170
+ }
171
+
172
+ return clips;
173
+ }
174
+
175
+ /**
176
+ * Create a standard set of test clips for browser testing
177
+ */
178
+ export async function createStandardJitTestClips(
179
+ sourceVideoUrl: string,
180
+ ): Promise<JitTestClip[]> {
181
+ return createJitTestClips({
182
+ sourceVideoUrl,
183
+ segments: [
184
+ // Short clips for quick testing - these match our pre-generated segments
185
+ { startTimeMs: 0, durationMs: 2000, quality: "medium" }, // First 2 seconds
186
+ { startTimeMs: 0, durationMs: 2000, quality: "low" }, // Same segment, different quality
187
+ { startTimeMs: 0, durationMs: 2000, quality: "high" }, // Same segment, high quality
188
+
189
+ // Sequential segments for testing playback
190
+ { startTimeMs: 2000, durationMs: 2000, quality: "medium" }, // Next 2 seconds
191
+ { startTimeMs: 4000, durationMs: 2000, quality: "medium" }, // Next 2 seconds
192
+
193
+ // Edge cases
194
+ { startTimeMs: 0, durationMs: 4000, quality: "medium" }, // Longer segment
195
+ ],
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Convert JitTestClip to blob URL for use in browser tests
201
+ */
202
+ export function createBlobUrlFromJitClip(clip: JitTestClip): string {
203
+ const blob = new Blob([clip.data], { type: "video/mp4" });
204
+ return URL.createObjectURL(blob);
205
+ }
206
+
207
+ /**
208
+ * Mock MSW handler that serves pre-generated JIT test clips
209
+ */
210
+ export function createMockJitHandler(clips: JitTestClip[]) {
211
+ return {
212
+ clips,
213
+
214
+ // Mock metadata endpoint
215
+ getMetadata: (url: string) => ({
216
+ url,
217
+ durationMs: 10000, // 10 second test video
218
+ streams: [
219
+ {
220
+ index: 0,
221
+ type: "video" as const,
222
+ codecName: "h264",
223
+ duration: 10,
224
+ durationMs: 10000,
225
+ width: 1920,
226
+ height: 1080,
227
+ frameRate: { num: 30, den: 1 },
228
+ },
229
+ {
230
+ index: 1,
231
+ type: "audio" as const,
232
+ codecName: "aac",
233
+ duration: 10,
234
+ durationMs: 10000,
235
+ channels: 2,
236
+ sampleRate: 48000,
237
+ },
238
+ ],
239
+ presets: ["low", "medium", "high"],
240
+ segmentDuration: 2000,
241
+ supportedFormats: ["mp4"],
242
+ extractedAt: new Date().toISOString(),
243
+ }),
244
+
245
+ // Mock segment endpoint - returns pre-generated clip data
246
+ getSegment: (
247
+ url: string,
248
+ startTimeMs: number,
249
+ quality: "low" | "medium" | "high",
250
+ ) => {
251
+ // Align to 2s boundaries like the real service
252
+ const alignedStart = Math.floor(startTimeMs / 2000) * 2000;
253
+
254
+ const clip = clips.find(
255
+ (c) =>
256
+ c.url === url &&
257
+ c.startTimeMs === alignedStart &&
258
+ c.quality === quality,
259
+ );
260
+
261
+ if (!clip) {
262
+ throw new Error(
263
+ `No test clip found for ${url} at ${alignedStart}ms (${quality})`,
264
+ );
265
+ }
266
+
267
+ return clip.data;
268
+ },
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Cache for test clips to avoid regenerating them multiple times
274
+ */
275
+ const testClipCache = new Map<string, JitTestClip[]>();
276
+
277
+ /**
278
+ * Get or create cached test clips for a source URL
279
+ */
280
+ export async function getCachedJitTestClips(
281
+ sourceVideoUrl: string,
282
+ ): Promise<JitTestClip[]> {
283
+ let clips = testClipCache.get(sourceVideoUrl);
284
+
285
+ if (!clips) {
286
+ console.log(`Loading JIT test clips for ${sourceVideoUrl}...`);
287
+ clips = await createStandardJitTestClips(sourceVideoUrl);
288
+ testClipCache.set(sourceVideoUrl, clips);
289
+ console.log(`Loaded ${clips.length} test clips`);
290
+ }
291
+
292
+ return clips;
293
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Asset-specific MSW handlers for testing
3
+ * Provides pre-configured handlers for asset fragment indexes and track data
4
+ */
5
+
6
+ import { http, HttpResponse } from "msw";
7
+
8
+ /**
9
+ * Asset MSW handlers that redirect requests to real test assets
10
+ * Use with MSW worker.use() to proxy asset requests to /test-assets/asset-mode/
11
+ */
12
+ export const assetMSWHandlers = [
13
+ // Fragment index handler - rewrite to test asset
14
+ http.get("/@ef-track-fragment-index/*", async () => {
15
+ const response = await fetch("/asset-mode/index.json");
16
+ const data = await response.json();
17
+ return HttpResponse.json(data);
18
+ }),
19
+
20
+ // Track data handler - rewrite to test asset with proper range support
21
+ http.get("/@ef-track/*", async ({ request }) => {
22
+ const url = new URL(request.url);
23
+ const trackId = url.searchParams.get("trackId");
24
+ if (!trackId) {
25
+ return new HttpResponse(null, { status: 400 });
26
+ }
27
+
28
+ const rangeHeader = request.headers.get("range");
29
+ const response = await fetch(`/asset-mode/track-${trackId}.mp4`, {
30
+ headers: {
31
+ ...(rangeHeader && {
32
+ range: rangeHeader,
33
+ }),
34
+ },
35
+ });
36
+
37
+ const contentRangeHeader = response.headers.get("Content-Range");
38
+ return new HttpResponse(await response.arrayBuffer(), {
39
+ status: response.status,
40
+ headers: {
41
+ "Content-Type": "video/mp4",
42
+ "Accept-Ranges": "bytes",
43
+ ...(contentRangeHeader && {
44
+ "Content-Range": contentRangeHeader,
45
+ }),
46
+ },
47
+ });
48
+ }),
49
+ ];
package/test/useMSW.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * MSW server setup utility for testing
3
+ * Provides a properly configured MSW worker with lifecycle management for browser tests
4
+ */
5
+
6
+ import { setupWorker } from "msw/browser";
7
+ import { afterAll, afterEach, beforeAll } from "vitest";
8
+
9
+ /**
10
+ * Set up MSW worker for browser testing with proper lifecycle
11
+ */
12
+ export function useMSW() {
13
+ const worker = setupWorker();
14
+
15
+ beforeAll(async () => {
16
+ await worker.start({
17
+ onUnhandledRequest: "bypass", // Don't warn about unhandled requests
18
+ quiet: true, // Disable logging of matched requests
19
+ });
20
+ });
21
+
22
+ afterEach(() => {
23
+ worker.resetHandlers();
24
+ });
25
+
26
+ afterAll(() => {
27
+ worker.stop();
28
+ });
29
+
30
+ return worker;
31
+ }