@editframe/elements 0.18.3-beta.0 → 0.18.7-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 (107) hide show
  1. package/dist/elements/EFMedia/AssetMediaEngine.browsertest.d.ts +0 -0
  2. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +2 -4
  3. package/dist/elements/EFMedia/AssetMediaEngine.js +22 -3
  4. package/dist/elements/EFMedia/BaseMediaEngine.js +20 -1
  5. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +5 -5
  6. package/dist/elements/EFMedia/BufferedSeekingInput.js +27 -7
  7. package/dist/elements/EFMedia/JitMediaEngine.d.ts +1 -1
  8. package/dist/elements/EFMedia/JitMediaEngine.js +22 -3
  9. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +4 -1
  10. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +11 -3
  11. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.d.ts +0 -0
  12. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +10 -2
  13. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +11 -1
  14. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +3 -2
  15. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +4 -1
  16. package/dist/elements/EFMedia/shared/PrecisionUtils.d.ts +28 -0
  17. package/dist/elements/EFMedia/shared/PrecisionUtils.js +29 -0
  18. package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +11 -2
  19. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +11 -1
  20. package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +3 -2
  21. package/dist/elements/EFMedia.d.ts +0 -12
  22. package/dist/elements/EFMedia.js +4 -30
  23. package/dist/elements/EFTimegroup.js +12 -17
  24. package/dist/elements/EFVideo.d.ts +0 -9
  25. package/dist/elements/EFVideo.js +0 -7
  26. package/dist/elements/SampleBuffer.js +6 -6
  27. package/dist/getRenderInfo.d.ts +2 -2
  28. package/dist/gui/ContextMixin.js +71 -17
  29. package/dist/gui/TWMixin.js +1 -1
  30. package/dist/style.css +1 -1
  31. package/dist/transcoding/types/index.d.ts +9 -9
  32. package/package.json +2 -3
  33. package/src/elements/EFAudio.browsertest.ts +7 -7
  34. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +100 -0
  35. package/src/elements/EFMedia/AssetMediaEngine.ts +52 -7
  36. package/src/elements/EFMedia/BaseMediaEngine.ts +50 -1
  37. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +135 -54
  38. package/src/elements/EFMedia/BufferedSeekingInput.ts +74 -17
  39. package/src/elements/EFMedia/JitMediaEngine.ts +58 -2
  40. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +10 -1
  41. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +16 -8
  42. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +199 -0
  43. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +25 -3
  44. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +12 -1
  45. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +3 -2
  46. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +10 -1
  47. package/src/elements/EFMedia/shared/PrecisionUtils.ts +46 -0
  48. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +27 -3
  49. package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +12 -1
  50. package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +3 -2
  51. package/src/elements/EFMedia.browsertest.ts +73 -33
  52. package/src/elements/EFMedia.ts +11 -54
  53. package/src/elements/EFTimegroup.ts +21 -26
  54. package/src/elements/EFVideo.browsertest.ts +895 -162
  55. package/src/elements/EFVideo.ts +0 -16
  56. package/src/elements/SampleBuffer.ts +8 -10
  57. package/src/gui/ContextMixin.ts +104 -26
  58. package/src/transcoding/types/index.ts +10 -6
  59. package/test/EFVideo.framegen.browsertest.ts +1 -1
  60. package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +3 -3
  61. package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/data.bin +0 -0
  62. package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/metadata.json +22 -0
  63. package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +3 -3
  64. package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/data.bin +0 -0
  65. package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/metadata.json +22 -0
  66. package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +3 -3
  67. package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/data.bin +0 -0
  68. package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/metadata.json +22 -0
  69. package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +3 -3
  70. package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +3 -3
  71. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +3 -3
  72. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +3 -3
  73. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
  74. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +21 -0
  75. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
  76. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +21 -0
  77. package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +3 -3
  78. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -1
  79. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +4 -4
  80. package/test/recordReplayProxyPlugin.js +50 -0
  81. package/types.json +1 -1
  82. package/dist/DecoderResetFrequency.test.d.ts +0 -1
  83. package/dist/DecoderResetRecovery.test.d.ts +0 -1
  84. package/dist/ScrubTrackManager.d.ts +0 -96
  85. package/dist/elements/EFMedia/services/AudioElementFactory.browsertest.d.ts +0 -1
  86. package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +0 -22
  87. package/dist/elements/EFMedia/services/AudioElementFactory.js +0 -72
  88. package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +0 -1
  89. package/dist/elements/EFMedia/services/MediaSourceService.d.ts +0 -47
  90. package/dist/elements/EFMedia/services/MediaSourceService.js +0 -73
  91. package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +0 -1
  92. package/dist/gui/services/ElementConnectionManager.d.ts +0 -59
  93. package/dist/gui/services/ElementConnectionManager.js +0 -128
  94. package/dist/gui/services/PlaybackController.browsertest.d.ts +0 -1
  95. package/dist/gui/services/PlaybackController.d.ts +0 -103
  96. package/dist/gui/services/PlaybackController.js +0 -290
  97. package/dist/services/MediaSourceManager.d.ts +0 -62
  98. package/dist/services/MediaSourceManager.js +0 -211
  99. package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +0 -325
  100. package/src/elements/EFMedia/services/AudioElementFactory.ts +0 -119
  101. package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +0 -257
  102. package/src/elements/EFMedia/services/MediaSourceService.ts +0 -102
  103. package/src/gui/services/ElementConnectionManager.browsertest.ts +0 -263
  104. package/src/gui/services/ElementConnectionManager.ts +0 -224
  105. package/src/gui/services/PlaybackController.browsertest.ts +0 -437
  106. package/src/gui/services/PlaybackController.ts +0 -521
  107. package/src/services/MediaSourceManager.ts +0 -333
@@ -6,7 +6,6 @@ import { createRef, ref } from "lit/directives/ref.js";
6
6
 
7
7
  import { DelayedLoadingState } from "../DelayedLoadingState.js";
8
8
  import { TWMixin } from "../gui/TWMixin.js";
9
- import type { CacheStats, ScrubTrackManager } from "../ScrubTrackManager.js";
10
9
  import { makeVideoBufferTask } from "./EFMedia/videoTasks/makeVideoBufferTask.ts";
11
10
  import { makeVideoInitSegmentFetchTask } from "./EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts";
12
11
  import { makeVideoInputTask } from "./EFMedia/videoTasks/makeVideoInputTask.ts";
@@ -135,11 +134,6 @@ export class EFVideo extends TWMixin(EFMedia) {
135
134
  videoSeekTask = makeVideoSeekTask(this);
136
135
  videoBufferTask = makeVideoBufferTask(this);
137
136
 
138
- /**
139
- * Scrub track manager for fast timeline navigation
140
- */
141
- scrubTrackManager?: ScrubTrackManager;
142
-
143
137
  /**
144
138
  * Delayed loading state manager for user feedback
145
139
  */
@@ -392,13 +386,6 @@ export class EFVideo extends TWMixin(EFMedia) {
392
386
  return currentTime >= renderStartTime;
393
387
  }
394
388
 
395
- /**
396
- * Get scrub track performance statistics
397
- */
398
- getScrubTrackStats(): CacheStats | null {
399
- return this.scrubTrackManager?.getCacheStats() || null;
400
- }
401
-
402
389
  // Getter properties for backward compatibility with tests
403
390
  /**
404
391
  * Effective mode - always returns "asset" for EFVideo
@@ -469,9 +456,6 @@ export class EFVideo extends TWMixin(EFMedia) {
469
456
  disconnectedCallback(): void {
470
457
  super.disconnectedCallback();
471
458
 
472
- // Clean up scrub track manager
473
- this.scrubTrackManager?.cleanup();
474
-
475
459
  // Clean up delayed loading state
476
460
  this.delayedLoadingState.clearAllLoading();
477
461
  }
@@ -1,4 +1,5 @@
1
1
  import type { AudioSample, VideoSample } from "mediabunny";
2
+ import { roundToMilliseconds } from "./EFMedia/shared/PrecisionUtils";
2
3
  export type MediaSample = VideoSample | AudioSample;
3
4
 
4
5
  // Generic sample buffer that works with both VideoSample and AudioSample
@@ -57,22 +58,19 @@ export class SampleBuffer {
57
58
 
58
59
  if (currentBuffer.length === 0) return undefined;
59
60
 
60
- // Round to microsecond precision to handle floating point issues
61
- // without introducing timing aliasing problems
62
- const roundToMicroseconds = (timeMs: number) =>
63
- Math.round(timeMs * 1000) / 1000;
64
- const targetTimeMs = roundToMicroseconds(desiredSeekTimeMs);
61
+ // Use consistent precision handling across the entire pipeline
62
+ const targetTimeMs = roundToMilliseconds(desiredSeekTimeMs);
65
63
 
66
64
  // Find the sample that contains the target time
67
65
  for (const sample of currentBuffer) {
68
- const sampleStartMs = roundToMicroseconds((sample.timestamp || 0) * 1000);
69
- const sampleDurationMs = roundToMicroseconds(
66
+ const sampleStartMs = roundToMilliseconds((sample.timestamp || 0) * 1000);
67
+ const sampleDurationMs = roundToMilliseconds(
70
68
  (sample.duration || 0) * 1000,
71
69
  );
72
- const sampleEndMs = sampleStartMs + sampleDurationMs;
70
+ const sampleEndMs = roundToMilliseconds(sampleStartMs + sampleDurationMs);
73
71
 
74
- // Check if the desired time falls within this sample's time span [start, end)
75
- if (targetTimeMs >= sampleStartMs && targetTimeMs < sampleEndMs) {
72
+ // Check if the desired time falls within this sample's time span [start, end], inclusive of end
73
+ if (targetTimeMs >= sampleStartMs && targetTimeMs <= sampleEndMs) {
76
74
  return sample;
77
75
  }
78
76
  }
@@ -11,8 +11,6 @@ import { fetchContext } from "./fetchContext.js";
11
11
  import { type FocusContext, focusContext } from "./focusContext.js";
12
12
  import { focusedElementContext } from "./focusedElementContext.js";
13
13
  import { loopContext, playingContext } from "./playingContext.js";
14
- import { ElementConnectionManager } from "./services/ElementConnectionManager.js";
15
- import { PlaybackController } from "./services/PlaybackController.js";
16
14
 
17
15
  export const targetTimegroupContext = createContext<EFTimegroup | null>(
18
16
  "target-timegroup",
@@ -136,6 +134,9 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
136
134
  @state()
137
135
  currentTimeMs = 0;
138
136
 
137
+ #FPS = 30;
138
+ #MS_PER_FRAME = 1000 / this.#FPS;
139
+
139
140
  #timegroupObserver = new MutationObserver((mutations) => {
140
141
  for (const mutation of mutations) {
141
142
  if (mutation.type === "childList") {
@@ -159,9 +160,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
159
160
  attributes: true,
160
161
  });
161
162
 
162
- // Preferrably we would use a resizeObserver, but it is difficult to get the first resize
163
- // timed correctly. So we use requestAnimationFrame as a stop-gap.
164
- // requestAnimationFrame(this.setStageScale);
165
163
  if (this.playing) {
166
164
  this.startPlayback();
167
165
  }
@@ -208,37 +206,117 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
208
206
  this.playing = false;
209
207
  }
210
208
 
211
- // Services for clean playback management
212
- // Keep connection manager for potential future use
213
- // @ts-ignore - Keeping for future use
214
- private _elementConnectionManager = new ElementConnectionManager(3000);
215
- private playbackController = new PlaybackController({
216
- fps: 30,
217
- onTimeUpdate: (timeMs: number) => {
218
- this.currentTimeMs = timeMs;
219
- },
220
- onPlayStateChange: (playing: boolean) => {
221
- this.playing = playing;
222
- },
223
- onError: (error: Error) => {
224
- console.error("🎵 [PLAYBACK_ERROR]:", error);
225
- },
226
- });
209
+ #playbackAudioContext: AudioContext | null = null;
210
+ #playbackAnimationFrameRequest: number | null = null;
211
+ #AUDIO_PLAYBACK_SLICE_MS = ((47 * 1024) / 48000) * 1000; // AAC-aligned: ~1002.67ms
212
+
213
+ #syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
214
+ const rawTimeMs =
215
+ startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
216
+ const nextTimeMs =
217
+ Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
218
+ if (nextTimeMs !== this.currentTimeMs) {
219
+ this.currentTimeMs = nextTimeMs;
220
+ }
221
+ this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
222
+ this.#syncPlayheadToAudioContext(target, startMs);
223
+ });
224
+ }
227
225
 
228
226
  private async stopPlayback() {
229
- await this.playbackController.stopPlayback();
227
+ if (this.#playbackAudioContext) {
228
+ if (this.#playbackAudioContext.state !== "closed") {
229
+ await this.#playbackAudioContext.close();
230
+ }
231
+ }
232
+ if (this.#playbackAnimationFrameRequest) {
233
+ cancelAnimationFrame(this.#playbackAnimationFrameRequest);
234
+ }
235
+ this.#playbackAudioContext = null;
236
+ this.#playbackAnimationFrameRequest = null;
230
237
  }
231
238
 
232
239
  private async startPlayback() {
240
+ await this.stopPlayback();
233
241
  const timegroup = this.targetTimegroup;
234
242
  if (!timegroup) {
235
243
  return;
236
244
  }
237
245
 
238
- await this.playbackController.startPlayback(
239
- timegroup,
240
- timegroup.currentTimeMs,
241
- );
246
+ await timegroup.waitForMediaDurations();
247
+
248
+ let currentMs = timegroup.currentTimeMs;
249
+ const fromMs = currentMs;
250
+ const toMs = timegroup.endTimeMs;
251
+
252
+ if (fromMs >= toMs) {
253
+ this.pause();
254
+ return;
255
+ }
256
+
257
+ let bufferCount = 0;
258
+ this.#playbackAudioContext = new AudioContext({
259
+ latencyHint: "playback",
260
+ });
261
+
262
+ if (this.#playbackAnimationFrameRequest) {
263
+ cancelAnimationFrame(this.#playbackAnimationFrameRequest);
264
+ }
265
+ this.#syncPlayheadToAudioContext(timegroup, currentMs);
266
+ const playbackContext = this.#playbackAudioContext;
267
+ if (playbackContext.state === "suspended") {
268
+ console.warn(
269
+ "AudioContext is suspended, media playback will not work until user has interacted with page.",
270
+ );
271
+ this.playing = false;
272
+ return;
273
+ }
274
+ await playbackContext.suspend();
275
+
276
+ const fillBuffer = async () => {
277
+ if (bufferCount > 2) {
278
+ return;
279
+ }
280
+ const canFillBuffer = await queueBufferSource();
281
+ if (canFillBuffer) {
282
+ fillBuffer();
283
+ }
284
+ };
285
+
286
+ const queueBufferSource = async () => {
287
+ if (currentMs >= toMs) {
288
+ return false;
289
+ }
290
+ const startMs = currentMs;
291
+ const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
292
+ currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
293
+ const audioBuffer = await timegroup.renderAudio(startMs, endMs);
294
+ bufferCount++;
295
+ const source = playbackContext.createBufferSource();
296
+ source.buffer = audioBuffer;
297
+ source.connect(playbackContext.destination);
298
+ source.start((startMs - fromMs) / 1000);
299
+ source.onended = () => {
300
+ bufferCount--;
301
+ if (endMs >= toMs) {
302
+ this.pause();
303
+ if (this.loop) {
304
+ this.updateComplete.then(() => {
305
+ this.currentTimeMs = 0;
306
+ this.updateComplete.then(() => {
307
+ this.play();
308
+ });
309
+ });
310
+ }
311
+ } else {
312
+ fillBuffer();
313
+ }
314
+ };
315
+ return true;
316
+ };
317
+
318
+ await fillBuffer();
319
+ await playbackContext.resume();
242
320
  }
243
321
  }
244
322
 
@@ -2,6 +2,8 @@
2
2
  * Core types and interfaces for JIT Transcoding System
3
3
  */
4
4
 
5
+ import type { MediaRendition } from "../../elements/EFMedia/shared/MediaTaskUtils";
6
+
5
7
  export interface QualityPreset {
6
8
  name: string;
7
9
  width: number;
@@ -73,6 +75,8 @@ export interface ManifestVideoRendition {
73
75
  segmentDuration: number;
74
76
  /** Duration of each segment in milliseconds */
75
77
  segmentDurationMs: number;
78
+ /** Actual segment durations array (overrides fixed segmentDurationMs if provided) */
79
+ segmentDurationsMs?: number[];
76
80
  /** Video width in pixels */
77
81
  width: number;
78
82
  /** Video height in pixels */
@@ -100,6 +104,8 @@ export interface ManifestAudioRendition {
100
104
  segmentDuration: number;
101
105
  /** Duration of each segment in milliseconds */
102
106
  segmentDurationMs: number;
107
+ /** Actual segment durations array (overrides fixed segmentDurationMs if provided) */
108
+ segmentDurationsMs?: number[];
103
109
  /** Number of audio channels */
104
110
  channels: number;
105
111
  /** Sample rate in Hz */
@@ -183,12 +189,15 @@ export interface AudioRendition {
183
189
  trackId: number | undefined;
184
190
  src: string;
185
191
  segmentDurationMs?: number;
192
+ segmentDurationsMs?: number[];
193
+ startTimeOffsetMs?: number;
186
194
  }
187
195
  export interface VideoRendition {
188
196
  id?: RenditionId;
189
197
  trackId: number | undefined;
190
198
  src: string;
191
199
  segmentDurationMs?: number;
200
+ segmentDurationsMs?: number[];
192
201
  startTimeOffsetMs?: number;
193
202
  }
194
203
  export interface MediaEngine {
@@ -215,12 +224,7 @@ export interface MediaEngine {
215
224
  ) => Promise<ArrayBuffer>;
216
225
  computeSegmentId: (
217
226
  desiredSeekTimeMs: number,
218
- rendition: {
219
- id?: RenditionId;
220
- trackId: number | undefined;
221
- src: string;
222
- segmentDurationMs?: number;
223
- },
227
+ rendition: MediaRendition,
224
228
  ) => number | undefined;
225
229
 
226
230
  /**
@@ -33,7 +33,7 @@ describe("EFVideo Frame Generation", () => {
33
33
  render(
34
34
  html`
35
35
  <ef-video
36
- src="/test-assets/media/bars-n-tone2.mp4"
36
+ src="media/bars-n-tone2.mp4"
37
37
  mode="asset"
38
38
  current-time-ms="80"
39
39
  ></ef-video>
@@ -8,14 +8,14 @@
8
8
  "cache-control": "public, max-age=3600",
9
9
  "content-length": "32957",
10
10
  "content-type": "video/iso.segment",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
12
  "x-cache": "HIT",
13
13
  "x-powered-by": "Express",
14
- "x-total-server-time-ms": "6",
14
+ "x-total-server-time-ms": "0",
15
15
  "x-transcode-time-ms": "0"
16
16
  },
17
17
  "url": "/api/v1/transcode/audio/1.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
18
  "method": "GET",
19
19
  "range": null,
20
- "timestamp": "2025-08-04T18:16:50.153Z"
20
+ "timestamp": "2025-08-08T03:57:07.247Z"
21
21
  }
@@ -0,0 +1,22 @@
1
+ {
2
+ "statusCode": 200,
3
+ "headers": {
4
+ "access-control-allow-headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Range",
5
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
6
+ "access-control-allow-origin": "*",
7
+ "access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
8
+ "cache-control": "public, max-age=3600",
9
+ "connection": "close",
10
+ "content-length": "33661",
11
+ "content-type": "video/mp4",
12
+ "date": "Tue, 05 Aug 2025 17:32:32 GMT",
13
+ "x-cache": "HIT",
14
+ "x-powered-by": "Express",
15
+ "x-total-server-time-ms": "3",
16
+ "x-transcode-time-ms": "0"
17
+ },
18
+ "url": "/api/v1/transcode/audio/1.mp4?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
19
+ "method": "GET",
20
+ "range": "bytes=0-",
21
+ "timestamp": "2025-08-05T17:32:32.538Z"
22
+ }
@@ -8,14 +8,14 @@
8
8
  "cache-control": "public, max-age=3600",
9
9
  "content-length": "32502",
10
10
  "content-type": "video/iso.segment",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
12
  "x-cache": "HIT",
13
13
  "x-powered-by": "Express",
14
- "x-total-server-time-ms": "5",
14
+ "x-total-server-time-ms": "0",
15
15
  "x-transcode-time-ms": "0"
16
16
  },
17
17
  "url": "/api/v1/transcode/audio/2.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
18
  "method": "GET",
19
19
  "range": null,
20
- "timestamp": "2025-08-04T18:16:50.161Z"
20
+ "timestamp": "2025-08-08T03:57:07.247Z"
21
21
  }
@@ -0,0 +1,22 @@
1
+ {
2
+ "statusCode": 200,
3
+ "headers": {
4
+ "access-control-allow-headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Range",
5
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
6
+ "access-control-allow-origin": "*",
7
+ "access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
8
+ "cache-control": "public, max-age=3600",
9
+ "connection": "close",
10
+ "content-length": "33206",
11
+ "content-type": "video/mp4",
12
+ "date": "Tue, 05 Aug 2025 17:32:40 GMT",
13
+ "x-cache": "MISS",
14
+ "x-powered-by": "Express",
15
+ "x-total-server-time-ms": "215",
16
+ "x-transcode-time-ms": "209"
17
+ },
18
+ "url": "/api/v1/transcode/audio/2.mp4?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
19
+ "method": "GET",
20
+ "range": "bytes=0-",
21
+ "timestamp": "2025-08-05T17:32:40.581Z"
22
+ }
@@ -8,14 +8,14 @@
8
8
  "cache-control": "public, max-age=3600",
9
9
  "content-length": "32196",
10
10
  "content-type": "video/iso.segment",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
12
  "x-cache": "HIT",
13
13
  "x-powered-by": "Express",
14
- "x-total-server-time-ms": "9",
14
+ "x-total-server-time-ms": "0",
15
15
  "x-transcode-time-ms": "0"
16
16
  },
17
17
  "url": "/api/v1/transcode/audio/3.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
18
  "method": "GET",
19
19
  "range": null,
20
- "timestamp": "2025-08-04T18:16:50.184Z"
20
+ "timestamp": "2025-08-08T03:57:07.271Z"
21
21
  }
@@ -0,0 +1,22 @@
1
+ {
2
+ "statusCode": 200,
3
+ "headers": {
4
+ "access-control-allow-headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Range",
5
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
6
+ "access-control-allow-origin": "*",
7
+ "access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
8
+ "cache-control": "public, max-age=3600",
9
+ "connection": "close",
10
+ "content-length": "32900",
11
+ "content-type": "video/mp4",
12
+ "date": "Tue, 05 Aug 2025 18:58:23 GMT",
13
+ "x-cache": "MISS",
14
+ "x-powered-by": "Express",
15
+ "x-total-server-time-ms": "280",
16
+ "x-transcode-time-ms": "265"
17
+ },
18
+ "url": "/api/v1/transcode/audio/3.mp4?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
19
+ "method": "GET",
20
+ "range": "bytes=0-",
21
+ "timestamp": "2025-08-05T18:58:23.587Z"
22
+ }
@@ -8,14 +8,14 @@
8
8
  "cache-control": "public, max-age=3600",
9
9
  "content-length": "32570",
10
10
  "content-type": "video/iso.segment",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
12
  "x-cache": "HIT",
13
13
  "x-powered-by": "Express",
14
- "x-total-server-time-ms": "3",
14
+ "x-total-server-time-ms": "0",
15
15
  "x-transcode-time-ms": "0"
16
16
  },
17
17
  "url": "/api/v1/transcode/audio/4.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
18
  "method": "GET",
19
19
  "range": null,
20
- "timestamp": "2025-08-04T18:16:50.307Z"
20
+ "timestamp": "2025-08-08T03:57:07.247Z"
21
21
  }
@@ -8,14 +8,14 @@
8
8
  "cache-control": "public, max-age=3600",
9
9
  "content-length": "728",
10
10
  "content-type": "video/iso.segment",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
12
  "x-cache": "HIT",
13
13
  "x-powered-by": "Express",
14
- "x-total-server-time-ms": "8",
14
+ "x-total-server-time-ms": "0",
15
15
  "x-transcode-time-ms": "0"
16
16
  },
17
17
  "url": "/api/v1/transcode/audio/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
18
  "method": "GET",
19
19
  "range": null,
20
- "timestamp": "2025-08-04T18:16:50.138Z"
20
+ "timestamp": "2025-08-08T03:57:07.205Z"
21
21
  }
@@ -8,14 +8,14 @@
8
8
  "cache-control": "public, max-age=3600",
9
9
  "content-length": "827216",
10
10
  "content-type": "video/iso.segment",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
12
  "x-cache": "HIT",
13
13
  "x-powered-by": "Express",
14
- "x-total-server-time-ms": "12",
14
+ "x-total-server-time-ms": "0",
15
15
  "x-transcode-time-ms": "0"
16
16
  },
17
17
  "url": "/api/v1/transcode/high/1.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
18
  "method": "GET",
19
19
  "range": null,
20
- "timestamp": "2025-08-04T18:16:50.190Z"
20
+ "timestamp": "2025-08-08T03:57:07.256Z"
21
21
  }
@@ -8,14 +8,14 @@
8
8
  "cache-control": "public, max-age=3600",
9
9
  "content-length": "905893",
10
10
  "content-type": "video/iso.segment",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
12
  "x-cache": "HIT",
13
13
  "x-powered-by": "Express",
14
- "x-total-server-time-ms": "11",
14
+ "x-total-server-time-ms": "1",
15
15
  "x-transcode-time-ms": "0"
16
16
  },
17
17
  "url": "/api/v1/transcode/high/2.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
18
  "method": "GET",
19
19
  "range": null,
20
- "timestamp": "2025-08-04T18:16:50.197Z"
20
+ "timestamp": "2025-08-08T03:57:07.697Z"
21
21
  }
@@ -0,0 +1,21 @@
1
+ {
2
+ "statusCode": 200,
3
+ "headers": {
4
+ "access-control-allow-headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Range",
5
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
6
+ "access-control-allow-origin": "*",
7
+ "access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
8
+ "cache-control": "public, max-age=3600",
9
+ "content-length": "911925",
10
+ "content-type": "video/iso.segment",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
+ "x-cache": "HIT",
13
+ "x-powered-by": "Express",
14
+ "x-total-server-time-ms": "0",
15
+ "x-transcode-time-ms": "0"
16
+ },
17
+ "url": "/api/v1/transcode/high/4.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
+ "method": "GET",
19
+ "range": null,
20
+ "timestamp": "2025-08-08T03:57:07.247Z"
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "statusCode": 200,
3
+ "headers": {
4
+ "access-control-allow-headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Range",
5
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
6
+ "access-control-allow-origin": "*",
7
+ "access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
8
+ "cache-control": "public, max-age=3600",
9
+ "content-length": "829743",
10
+ "content-type": "video/iso.segment",
11
+ "date": "Tue, 05 Aug 2025 06:16:17 GMT",
12
+ "x-cache": "HIT",
13
+ "x-powered-by": "Express",
14
+ "x-total-server-time-ms": "16",
15
+ "x-transcode-time-ms": "0"
16
+ },
17
+ "url": "/api/v1/transcode/high/5.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
+ "method": "GET",
19
+ "range": null,
20
+ "timestamp": "2025-08-05T06:16:17.550Z"
21
+ }
@@ -8,14 +8,14 @@
8
8
  "cache-control": "public, max-age=3600",
9
9
  "content-length": "782",
10
10
  "content-type": "video/iso.segment",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
11
+ "date": "Fri, 08 Aug 2025 03:57:07 GMT",
12
12
  "x-cache": "HIT",
13
13
  "x-powered-by": "Express",
14
- "x-total-server-time-ms": "5",
14
+ "x-total-server-time-ms": "0",
15
15
  "x-transcode-time-ms": "0"
16
16
  },
17
17
  "url": "/api/v1/transcode/high/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
18
18
  "method": "GET",
19
19
  "range": null,
20
- "timestamp": "2025-08-04T18:16:50.142Z"
20
+ "timestamp": "2025-08-08T03:57:07.214Z"
21
21
  }
@@ -1 +1 @@
1
- {"version":"1.0","type":"jit-video","sourceUrl":"http://web:3000/head-moov-480p.mp4","duration":10,"durationMs":10000,"baseUrl":"http://localhost:63315","videoRenditions":[{"id":"high","width":1920,"height":1080,"bitrate":5000000,"codec":"avc1.640029","container":"video/mp4","mimeType":"video/mp4; codecs=\"avc1.640029,mp4a.40.2\"","segmentDuration":2,"segmentDurationMs":2000,"frameRate":30,"profile":"High","level":"4.1"},{"id":"medium","width":1280,"height":720,"bitrate":2500000,"codec":"avc1.640029","container":"video/mp4","mimeType":"video/mp4; codecs=\"avc1.640029,mp4a.40.2\"","segmentDuration":2,"segmentDurationMs":2000,"frameRate":30,"profile":"High","level":"4.1"},{"id":"low","width":854,"height":480,"bitrate":1000000,"codec":"avc1.640029","container":"video/mp4","mimeType":"video/mp4; codecs=\"avc1.640029,mp4a.40.2\"","segmentDuration":2,"segmentDurationMs":2000,"frameRate":30,"profile":"High","level":"4.1"},{"id":"scrub","width":320,"height":180,"bitrate":100000,"codec":"avc1.640029","container":"video/mp4","mimeType":"video/mp4; codecs=\"avc1.640029\"","segmentDuration":30,"segmentDurationMs":30000,"frameRate":15,"profile":"High","level":"4.1"}],"audioRenditions":[{"id":"audio","channels":1,"sampleRate":48000,"bitrate":128000,"codec":"mp4a.40.2","container":"audio/mp4","mimeType":"audio/mp4; codecs=\"mp4a.40.2\"","segmentDuration":2,"segmentDurationMs":2000,"language":"en"}],"endpoints":{"initSegment":"http://localhost:63315/api/v1/transcode/{rendition}/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4","mediaSegment":"http://localhost:63315/api/v1/transcode/{rendition}/{segmentId}.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4"},"jitInfo":{"parallelTranscodingSupported":true,"expectedTranscodeLatency":500,"segmentCount":5,"scrubSegmentCount":1}}
1
+ {"version":"1.0","type":"com.editframe/manifest","sourceUrl":"http://web:3000/head-moov-480p.mp4","duration":10,"durationMs":10000,"baseUrl":"http://localhost:63315","videoRenditions":[{"id":"high","width":1920,"height":1080,"bitrate":5000000,"codec":"avc1.640029","container":"video/mp4","mimeType":"video/mp4; codecs=\"avc1.640029,mp4a.40.2\"","segmentDuration":2,"segmentDurationMs":2000,"segmentDurationsMs":[2000,2000,2000,2000,2000],"frameRate":30,"profile":"High","level":"4.1"},{"id":"medium","width":1280,"height":720,"bitrate":2500000,"codec":"avc1.640029","container":"video/mp4","mimeType":"video/mp4; codecs=\"avc1.640029,mp4a.40.2\"","segmentDuration":2,"segmentDurationMs":2000,"segmentDurationsMs":[2000,2000,2000,2000,2000],"frameRate":30,"profile":"High","level":"4.1"},{"id":"low","width":854,"height":480,"bitrate":1000000,"codec":"avc1.640029","container":"video/mp4","mimeType":"video/mp4; codecs=\"avc1.640029,mp4a.40.2\"","segmentDuration":2,"segmentDurationMs":2000,"segmentDurationsMs":[2000,2000,2000,2000,2000],"frameRate":30,"profile":"High","level":"4.1"},{"id":"scrub","width":320,"height":180,"bitrate":100000,"codec":"avc1.640029","container":"video/mp4","mimeType":"video/mp4; codecs=\"avc1.640029\"","segmentDuration":30,"segmentDurationMs":30000,"segmentDurationsMs":[30000],"frameRate":15,"profile":"High","level":"4.1"}],"audioRenditions":[{"id":"audio","channels":1,"sampleRate":48000,"bitrate":128000,"codec":"mp4a.40.2","container":"audio/mp4","mimeType":"audio/mp4; codecs=\"mp4a.40.2\"","segmentDuration":2,"segmentDurationMs":2000,"segmentDurationsMs":[2026.66,2005.33,1984,2005.33,2026.66],"language":"en"}],"endpoints":{"initSegment":"http://localhost:63315/api/v1/transcode/{rendition}/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4","mediaSegment":"http://localhost:63315/api/v1/transcode/{rendition}/{segmentId}.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4"},"jitInfo":{"parallelTranscodingSupported":true,"expectedTranscodeLatency":500,"segmentCount":5,"scrubSegmentCount":1}}
@@ -6,14 +6,14 @@
6
6
  "access-control-allow-origin": "*",
7
7
  "access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
8
8
  "cache-control": "public, max-age=300",
9
- "content-length": "1799",
9
+ "content-length": "2045",
10
10
  "content-type": "application/json; charset=utf-8",
11
- "date": "Mon, 04 Aug 2025 18:16:50 GMT",
12
- "etag": "W/\"725-YLva1y8sO9GfMwcDHGQedQtKdV4\"",
11
+ "date": "Thu, 07 Aug 2025 20:55:37 GMT",
12
+ "etag": "W/\"81b-wi6z588RhWTgs57jivyDs3lEkkA\"",
13
13
  "x-powered-by": "Express"
14
14
  },
15
15
  "url": "/api/v1/transcode/manifest.json?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
16
16
  "method": "GET",
17
17
  "range": null,
18
- "timestamp": "2025-08-04T18:16:50.114Z"
18
+ "timestamp": "2025-08-07T20:55:37.385Z"
19
19
  }