@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
@@ -3,6 +3,7 @@ import { AudioRendition, InitSegmentPaths, MediaEngine, SegmentTimeRange } from
3
3
  import { UrlGenerator } from '../../transcoding/utils/UrlGenerator';
4
4
  import { EFMedia } from '../EFMedia';
5
5
  import { BaseMediaEngine } from './BaseMediaEngine';
6
+ import { MediaRendition } from './shared/MediaTaskUtils';
6
7
  export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
7
8
  host: EFMedia;
8
9
  src: string;
@@ -40,8 +41,5 @@ export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEn
40
41
  * Calculate audio segments for variable-duration segments using track fragment index
41
42
  */
42
43
  calculateAudioSegmentRange(fromMs: number, toMs: number, rendition: AudioRendition, _durationMs: number): SegmentTimeRange[];
43
- computeSegmentId(desiredSeekTimeMs: number, rendition: {
44
- trackId: number | undefined;
45
- src: string;
46
- }): number;
44
+ computeSegmentId(desiredSeekTimeMs: number, rendition: MediaRendition): number;
47
45
  }
@@ -1,4 +1,5 @@
1
1
  import { BaseMediaEngine } from "./BaseMediaEngine.js";
2
+ import { convertToScaledTime, roundToMilliseconds } from "./shared/PrecisionUtils.js";
2
3
  var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
3
4
  static async fetch(host, urlGenerator, src) {
4
5
  const url = urlGenerator.generateTrackFragmentIndexUrl(src);
@@ -105,12 +106,30 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
105
106
  const track = this.data[rendition.trackId];
106
107
  if (!track) throw new Error("Track not found");
107
108
  const { timescale, segments } = track;
108
- const desiredSeekTime = desiredSeekTimeMs / 1e3 * timescale;
109
+ const startTimeOffsetMs = "startTimeOffsetMs" in rendition && rendition.startTimeOffsetMs || 0;
110
+ const mediaTimeMs = roundToMilliseconds(desiredSeekTimeMs + startTimeOffsetMs);
111
+ const scaledSeekTime = convertToScaledTime(mediaTimeMs, timescale);
109
112
  for (let i = segments.length - 1; i >= 0; i--) {
110
113
  const segment = segments[i];
111
- if (segment.cts <= desiredSeekTime) return i;
114
+ const segmentEndTime = segment.cts + segment.duration;
115
+ if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) return i;
116
+ }
117
+ let nearestSegmentIndex = 0;
118
+ let nearestDistance = Number.MAX_SAFE_INTEGER;
119
+ for (let i = 0; i < segments.length; i++) {
120
+ const segment = segments[i];
121
+ const segmentStartTime = segment.cts;
122
+ const segmentEndTime = segment.cts + segment.duration;
123
+ let distance;
124
+ if (scaledSeekTime < segmentStartTime) distance = segmentStartTime - scaledSeekTime;
125
+ else if (scaledSeekTime >= segmentEndTime) distance = scaledSeekTime - segmentEndTime;
126
+ else return i;
127
+ if (distance < nearestDistance) {
128
+ nearestDistance = distance;
129
+ nearestSegmentIndex = i;
130
+ }
112
131
  }
113
- return 0;
132
+ return nearestSegmentIndex;
114
133
  }
115
134
  };
116
135
  export { AssetMediaEngine };
@@ -76,14 +76,33 @@ var BaseMediaEngine = class {
76
76
  */
77
77
  calculateAudioSegmentRange(fromMs, toMs, rendition, durationMs) {
78
78
  if (fromMs >= toMs) return [];
79
- const segmentDurationMs = rendition.segmentDurationMs || 1e3;
80
79
  const segments = [];
80
+ if (rendition.segmentDurationsMs && rendition.segmentDurationsMs.length > 0) {
81
+ let cumulativeTime = 0;
82
+ for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {
83
+ const segmentDuration = rendition.segmentDurationsMs[i];
84
+ if (segmentDuration === void 0) continue;
85
+ const segmentStartMs = cumulativeTime;
86
+ const segmentEndMs = Math.min(cumulativeTime + segmentDuration, durationMs);
87
+ if (segmentStartMs >= durationMs) break;
88
+ if (segmentStartMs < toMs && segmentEndMs > fromMs) segments.push({
89
+ segmentId: i + 1,
90
+ startMs: segmentStartMs,
91
+ endMs: segmentEndMs
92
+ });
93
+ cumulativeTime += segmentDuration;
94
+ if (cumulativeTime >= durationMs) break;
95
+ }
96
+ return segments;
97
+ }
98
+ const segmentDurationMs = rendition.segmentDurationMs || 1e3;
81
99
  const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);
82
100
  const endSegmentIndex = Math.floor(toMs / segmentDurationMs);
83
101
  for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
84
102
  const segmentId = i + 1;
85
103
  const segmentStartMs = i * segmentDurationMs;
86
104
  const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);
105
+ if (segmentStartMs >= durationMs) break;
87
106
  if (segmentStartMs < toMs && segmentEndMs > fromMs) segments.push({
88
107
  segmentId,
89
108
  startMs: segmentStartMs,
@@ -3,8 +3,8 @@ interface BufferedSeekingInputOptions {
3
3
  videoBufferSize?: number;
4
4
  audioBufferSize?: number;
5
5
  /**
6
- * FFmpeg start_time offset in milliseconds from the processed video.
7
- * Applied during seeking to correct for timing shifts introduced by FFmpeg processing.
6
+ * Timeline offset in milliseconds to map user timeline to media timeline.
7
+ * Applied during seeking to handle media that doesn't start at 0ms.
8
8
  */
9
9
  startTimeOffsetMs?: number;
10
10
  }
@@ -18,8 +18,8 @@ export declare class BufferedSeekingInput {
18
18
  private trackIteratorCreationPromises;
19
19
  private trackSeekPromises;
20
20
  /**
21
- * FFmpeg start_time offset in milliseconds from the processed video.
22
- * Applied during seeking to correct for timing shifts introduced by FFmpeg processing.
21
+ * Timeline offset in milliseconds to map user timeline to media timeline.
22
+ * Applied during seeking to handle media that doesn't start at 0ms.
23
23
  */
24
24
  private readonly startTimeOffsetMs;
25
25
  constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions);
@@ -36,7 +36,7 @@ export declare class BufferedSeekingInput {
36
36
  getTrackIterator(trackId: number): Promise<AsyncIterator<MediaSample, any, undefined>>;
37
37
  private createIteratorSafe;
38
38
  createTrackBuffer(trackId: number): Promise<void>;
39
- seek(trackId: number, timeMs: number): Promise<MediaSample>;
39
+ seek(trackId: number, timeMs: number): Promise<MediaSample | undefined>;
40
40
  private resetIterator;
41
41
  private seekSafe;
42
42
  }
@@ -1,3 +1,4 @@
1
+ import { roundToMilliseconds } from "./shared/PrecisionUtils.js";
1
2
  import { SampleBuffer } from "../SampleBuffer.js";
2
3
  import { AudioSampleSink, BufferSource, Input, MP4, VideoSampleSink } from "mediabunny";
3
4
  const defaultOptions = {
@@ -113,10 +114,11 @@ var BufferedSeekingInput = class {
113
114
  }
114
115
  }
115
116
  async seek(trackId, timeMs) {
116
- const correctedTimeMs = timeMs + this.startTimeOffsetMs;
117
+ const mediaTimeMs = timeMs + this.startTimeOffsetMs;
118
+ const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);
117
119
  const existingSeek = this.trackSeekPromises.get(trackId);
118
120
  if (existingSeek) await existingSeek;
119
- const seekPromise = this.seekSafe(trackId, correctedTimeMs);
121
+ const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);
120
122
  this.trackSeekPromises.set(trackId, seekPromise);
121
123
  try {
122
124
  return await seekPromise;
@@ -138,12 +140,24 @@ var BufferedSeekingInput = class {
138
140
  async seekSafe(trackId, timeMs) {
139
141
  if (!this.trackBuffers.has(trackId)) await this.createTrackBuffer(trackId);
140
142
  const trackBuffer = this.trackBuffers.get(trackId);
141
- if (timeMs < trackBuffer.firstTimestamp * 1e3) await this.resetIterator(trackId);
142
- const alreadyInBuffer = trackBuffer.find(timeMs);
143
143
  const track = await this.getTrack(trackId);
144
- const firstTimestampMs = await track.getFirstTimestamp() * 1e3;
145
- const lastSampleEndMs = await track.computeDuration() * 1e3;
146
- if (timeMs < firstTimestampMs || timeMs >= lastSampleEndMs) throw new NoSample(`Seek time ${timeMs}ms is outside track range [${firstTimestampMs}ms, ${lastSampleEndMs}ms]`);
144
+ const firstTimestampMs = roundToMilliseconds(await track.getFirstTimestamp() * 1e3);
145
+ let roundedTimeMs = roundToMilliseconds(timeMs);
146
+ if (roundedTimeMs < firstTimestampMs) {
147
+ const bufferContents$1 = trackBuffer.getContents();
148
+ if (bufferContents$1.length > 0) {
149
+ timeMs = firstTimestampMs;
150
+ roundedTimeMs = roundToMilliseconds(timeMs);
151
+ }
152
+ }
153
+ const bufferContents = trackBuffer.getContents();
154
+ if (bufferContents.length > 0) {
155
+ const bufferStartMs = roundToMilliseconds(trackBuffer.firstTimestamp * 1e3);
156
+ const lastSample = bufferContents[bufferContents.length - 1];
157
+ const bufferEndMs = lastSample ? roundToMilliseconds((lastSample.timestamp + (lastSample.duration || 0)) * 1e3) : bufferStartMs;
158
+ if (roundedTimeMs < bufferStartMs || roundedTimeMs > bufferEndMs) await this.resetIterator(trackId);
159
+ }
160
+ const alreadyInBuffer = trackBuffer.find(timeMs);
147
161
  if (alreadyInBuffer) return alreadyInBuffer;
148
162
  const iterator = await this.getTrackIterator(trackId);
149
163
  while (true) {
@@ -153,6 +167,12 @@ var BufferedSeekingInput = class {
153
167
  if (foundSample) return foundSample;
154
168
  if (done) break;
155
169
  }
170
+ const finalBufferContents = trackBuffer.getContents();
171
+ if (finalBufferContents.length > 0) {
172
+ const lastSample = finalBufferContents[finalBufferContents.length - 1];
173
+ const lastSampleEndMs = roundToMilliseconds(((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) * 1e3);
174
+ if (roundToMilliseconds(timeMs) >= lastSampleEndMs) return lastSample;
175
+ }
156
176
  throw new NoSample(`Sample not found for time ${timeMs} in ${track.type} track ${trackId}`);
157
177
  }
158
178
  };
@@ -27,5 +27,5 @@ export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngi
27
27
  trackId: number | undefined;
28
28
  src: string;
29
29
  }): Promise<ArrayBuffer>;
30
- computeSegmentId(desiredSeekTimeMs: number, rendition: VideoRendition | AudioRendition): number;
30
+ computeSegmentId(desiredSeekTimeMs: number, rendition: VideoRendition | AudioRendition): number | undefined;
31
31
  }
@@ -24,7 +24,8 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
24
24
  id: rendition.id,
25
25
  trackId: void 0,
26
26
  src: this.data.sourceUrl,
27
- segmentDurationMs: rendition.segmentDurationMs
27
+ segmentDurationMs: rendition.segmentDurationMs,
28
+ segmentDurationsMs: rendition.segmentDurationsMs
28
29
  };
29
30
  }
30
31
  get videoRendition() {
@@ -34,7 +35,8 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
34
35
  id: rendition.id,
35
36
  trackId: void 0,
36
37
  src: this.data.sourceUrl,
37
- segmentDurationMs: rendition.segmentDurationMs
38
+ segmentDurationMs: rendition.segmentDurationMs,
39
+ segmentDurationsMs: rendition.segmentDurationsMs
38
40
  };
39
41
  }
40
42
  get templates() {
@@ -53,9 +55,26 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
53
55
  return this.fetchMediaCache(url);
54
56
  }
55
57
  computeSegmentId(desiredSeekTimeMs, rendition) {
58
+ if (desiredSeekTimeMs > this.durationMs) return void 0;
59
+ if (rendition.segmentDurationsMs && rendition.segmentDurationsMs.length > 0) {
60
+ let cumulativeTime = 0;
61
+ for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {
62
+ const segmentDuration = rendition.segmentDurationsMs[i];
63
+ if (segmentDuration === void 0) throw new Error("Segment duration is required for JIT metadata");
64
+ const segmentStartMs$1 = cumulativeTime;
65
+ const segmentEndMs = cumulativeTime + segmentDuration;
66
+ const isLastSegment = i === rendition.segmentDurationsMs.length - 1;
67
+ const includesEndTime = isLastSegment && desiredSeekTimeMs === this.durationMs;
68
+ if (desiredSeekTimeMs >= segmentStartMs$1 && (desiredSeekTimeMs < segmentEndMs || includesEndTime)) return i + 1;
69
+ cumulativeTime += segmentDuration;
70
+ if (cumulativeTime >= this.durationMs) break;
71
+ }
72
+ return void 0;
73
+ }
56
74
  if (!rendition.segmentDurationMs) throw new Error("Segment duration is required for JIT metadata");
57
75
  const segmentIndex = Math.floor(desiredSeekTimeMs / rendition.segmentDurationMs);
58
- if (segmentIndex * rendition.segmentDurationMs >= this.durationMs) return segmentIndex;
76
+ const segmentStartMs = segmentIndex * rendition.segmentDurationMs;
77
+ if (segmentStartMs >= this.durationMs) return void 0;
59
78
  return segmentIndex + 1;
60
79
  }
61
80
  };
@@ -57,7 +57,10 @@ function makeAudioFrequencyAnalysisTask(element) {
57
57
  const currentTimeMs = element.currentSourceTimeMs;
58
58
  const analysisWindowMs = 5e3;
59
59
  const fromMs = Math.max(0, currentTimeMs);
60
- const toMs = fromMs + analysisWindowMs;
60
+ const maxToMs = fromMs + analysisWindowMs;
61
+ const videoDurationMs = element.intrinsicDurationMs || 0;
62
+ const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;
63
+ if (fromMs >= toMs) return null;
61
64
  const { fetchAudioSpanningTime: fetchAudioSpan } = await import("../shared/AudioSpanUtils.js");
62
65
  const audioSpan = await fetchAudioSpan(element, fromMs, toMs, new AbortController().signal);
63
66
  if (!audioSpan || !audioSpan.blob) {
@@ -8,13 +8,21 @@ const makeAudioInputTask = (host) => {
8
8
  console.error("audioInputTask error", error);
9
9
  },
10
10
  onComplete: (_value) => {},
11
- task: async () => {
11
+ task: async (_, { signal }) => {
12
12
  const initSegment = await host.audioInitSegmentFetchTask.taskComplete;
13
+ signal.throwIfAborted();
13
14
  const segment = await host.audioSegmentFetchTask.taskComplete;
15
+ signal.throwIfAborted();
14
16
  if (!initSegment || !segment) throw new Error("Init segment or segment is not available");
15
- return new BufferedSeekingInput(await new Blob([initSegment, segment]).arrayBuffer(), {
17
+ const mediaEngine = await host.mediaEngineTask.taskComplete;
18
+ const audioRendition = mediaEngine?.audioRendition;
19
+ const startTimeOffsetMs = audioRendition?.startTimeOffsetMs;
20
+ const arrayBuffer = await new Blob([initSegment, segment]).arrayBuffer();
21
+ signal.throwIfAborted();
22
+ return new BufferedSeekingInput(arrayBuffer, {
16
23
  videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
17
- audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE
24
+ audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
25
+ startTimeOffsetMs
18
26
  });
19
27
  }
20
28
  });
@@ -8,15 +8,23 @@ const makeAudioSeekTask = (host) => {
8
8
  console.error("audioSeekTask error", error);
9
9
  },
10
10
  onComplete: (_value) => {},
11
- task: async (_) => {
11
+ task: async ([targetSeekTimeMs], { signal }) => {
12
12
  await host.audioSegmentIdTask.taskComplete;
13
+ signal.throwIfAborted();
13
14
  await host.audioSegmentFetchTask.taskComplete;
15
+ signal.throwIfAborted();
14
16
  await host.audioInitSegmentFetchTask.taskComplete;
17
+ signal.throwIfAborted();
15
18
  const audioInput = await host.audioInputTask.taskComplete;
19
+ signal.throwIfAborted();
16
20
  if (!audioInput) throw new Error("Audio input is not available");
17
21
  const audioTrack = await audioInput.getFirstAudioTrack();
18
22
  if (!audioTrack) throw new Error("Audio track is not available");
19
- const sample = await audioInput.seek(audioTrack.id, host.desiredSeekTimeMs);
23
+ signal.throwIfAborted();
24
+ const sample = await audioInput.seek(audioTrack.id, targetSeekTimeMs);
25
+ signal.throwIfAborted();
26
+ if (sample === void 0 && signal.aborted) return void 0;
27
+ if (sample === void 0) throw new Error("Audio seek failed to find sample");
20
28
  return sample;
21
29
  }
22
30
  });
@@ -10,7 +10,17 @@ const makeAudioSegmentFetchTask = (host) => {
10
10
  task: async (_, { signal }) => {
11
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
12
12
  const segmentId = await host.audioSegmentIdTask.taskComplete;
13
- if (segmentId === void 0) throw new Error("Segment ID is not available");
13
+ if (segmentId === void 0) {
14
+ const rendition = mediaEngine.audioRendition;
15
+ const debugInfo = {
16
+ hasRendition: !!rendition,
17
+ segmentDurationMs: rendition?.segmentDurationMs,
18
+ segmentDurationsMs: rendition?.segmentDurationsMs?.length || 0,
19
+ desiredSeekTimeMs: host.desiredSeekTimeMs,
20
+ intrinsicDurationMs: host.intrinsicDurationMs
21
+ };
22
+ throw new Error(`Segment ID is not available for audio. Debug info: ${JSON.stringify(debugInfo)}`);
23
+ }
14
24
  return mediaEngine.fetchMediaSegment(segmentId, mediaEngine.getAudioRendition(), signal);
15
25
  }
16
26
  });
@@ -7,9 +7,10 @@ const makeAudioSegmentIdTask = (host) => {
7
7
  console.error("audioSegmentIdTask error", error);
8
8
  },
9
9
  onComplete: (_value) => {},
10
- task: async (_, { signal }) => {
10
+ task: async ([, targetSeekTimeMs], { signal }) => {
11
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
12
- return mediaEngine.computeSegmentId(host.desiredSeekTimeMs, mediaEngine.getAudioRendition());
12
+ signal.throwIfAborted();
13
+ return mediaEngine.computeSegmentId(targetSeekTimeMs, mediaEngine.getAudioRendition());
13
14
  }
14
15
  });
15
16
  };
@@ -24,7 +24,10 @@ function makeAudioTimeDomainAnalysisTask(element) {
24
24
  const currentTimeMs = element.currentSourceTimeMs;
25
25
  const analysisWindowMs = 5e3;
26
26
  const fromMs = Math.max(0, currentTimeMs);
27
- const toMs = fromMs + analysisWindowMs;
27
+ const maxToMs = fromMs + analysisWindowMs;
28
+ const videoDurationMs = element.intrinsicDurationMs || 0;
29
+ const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;
30
+ if (fromMs >= toMs) return null;
28
31
  const { fetchAudioSpanningTime: fetchAudioSpan } = await import("../shared/AudioSpanUtils.js");
29
32
  const audioSpan = await fetchAudioSpan(element, fromMs, toMs, new AbortController().signal);
30
33
  if (!audioSpan || !audioSpan.blob) {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Centralized precision utilities for consistent timing calculations across the media pipeline.
3
+ *
4
+ * The key insight is that floating-point precision errors can cause inconsistencies between:
5
+ * 1. Segment selection logic (in AssetMediaEngine.computeSegmentId)
6
+ * 2. Sample finding logic (in SampleBuffer.find)
7
+ * 3. Timeline mapping (in BufferedSeekingInput.seek)
8
+ *
9
+ * All timing calculations must use the same rounding strategy to ensure consistency.
10
+ */
11
+ /**
12
+ * Round time to millisecond precision to handle floating-point precision issues.
13
+ * Uses Math.round for consistent behavior across the entire pipeline.
14
+ *
15
+ * This function should be used for ALL time-related calculations that need to be
16
+ * compared between different parts of the system.
17
+ */
18
+ export declare const roundToMilliseconds: (timeMs: number) => number;
19
+ /**
20
+ * Convert media time (in seconds) to scaled time units using consistent rounding.
21
+ * This is used in segment selection to convert from milliseconds to timescale units.
22
+ */
23
+ export declare const convertToScaledTime: (timeMs: number, timescale: number) => number;
24
+ /**
25
+ * Convert scaled time units back to media time (in milliseconds) using consistent rounding.
26
+ * This is the inverse of convertToScaledTime.
27
+ */
28
+ export declare const convertFromScaledTime: (scaledTime: number, timescale: number) => number;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Centralized precision utilities for consistent timing calculations across the media pipeline.
3
+ *
4
+ * The key insight is that floating-point precision errors can cause inconsistencies between:
5
+ * 1. Segment selection logic (in AssetMediaEngine.computeSegmentId)
6
+ * 2. Sample finding logic (in SampleBuffer.find)
7
+ * 3. Timeline mapping (in BufferedSeekingInput.seek)
8
+ *
9
+ * All timing calculations must use the same rounding strategy to ensure consistency.
10
+ */
11
+ /**
12
+ * Round time to millisecond precision to handle floating-point precision issues.
13
+ * Uses Math.round for consistent behavior across the entire pipeline.
14
+ *
15
+ * This function should be used for ALL time-related calculations that need to be
16
+ * compared between different parts of the system.
17
+ */
18
+ const roundToMilliseconds = (timeMs) => {
19
+ return Math.round(timeMs * 1e3) / 1e3;
20
+ };
21
+ /**
22
+ * Convert media time (in seconds) to scaled time units using consistent rounding.
23
+ * This is used in segment selection to convert from milliseconds to timescale units.
24
+ */
25
+ const convertToScaledTime = (timeMs, timescale) => {
26
+ const scaledTime = timeMs / 1e3 * timescale;
27
+ return Math.round(scaledTime);
28
+ };
29
+ export { convertToScaledTime, roundToMilliseconds };
@@ -8,16 +8,25 @@ const makeVideoSeekTask = (host) => {
8
8
  console.error("videoSeekTask error", error);
9
9
  },
10
10
  onComplete: (_value) => {},
11
- task: async (_) => {
11
+ task: async ([targetSeekTimeMs], { signal }) => {
12
12
  await host.mediaEngineTask.taskComplete;
13
+ signal.throwIfAborted();
13
14
  await host.videoSegmentIdTask.taskComplete;
15
+ signal.throwIfAborted();
14
16
  await host.videoSegmentFetchTask.taskComplete;
17
+ signal.throwIfAborted();
15
18
  await host.videoInitSegmentFetchTask.taskComplete;
19
+ signal.throwIfAborted();
16
20
  const videoInput = await host.videoInputTask.taskComplete;
21
+ signal.throwIfAborted();
17
22
  if (!videoInput) throw new Error("Video input is not available");
18
23
  const videoTrack = await videoInput.getFirstVideoTrack();
19
24
  if (!videoTrack) throw new Error("Video track is not available");
20
- const sample = await videoInput.seek(videoTrack.id, host.desiredSeekTimeMs);
25
+ signal.throwIfAborted();
26
+ const sample = await videoInput.seek(videoTrack.id, targetSeekTimeMs);
27
+ signal.throwIfAborted();
28
+ if (sample === void 0 && signal.aborted) return void 0;
29
+ if (sample === void 0) throw new Error("Video seek failed to find sample");
21
30
  return sample;
22
31
  }
23
32
  });
@@ -10,7 +10,17 @@ const makeVideoSegmentFetchTask = (host) => {
10
10
  task: async (_, { signal }) => {
11
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
12
12
  const segmentId = await host.videoSegmentIdTask.taskComplete;
13
- if (segmentId === void 0) throw new Error("Segment ID is not available");
13
+ if (segmentId === void 0) {
14
+ const rendition = mediaEngine.videoRendition;
15
+ const debugInfo = {
16
+ hasRendition: !!rendition,
17
+ segmentDurationMs: rendition?.segmentDurationMs,
18
+ segmentDurationsMs: rendition?.segmentDurationsMs?.length || 0,
19
+ desiredSeekTimeMs: host.desiredSeekTimeMs,
20
+ intrinsicDurationMs: host.intrinsicDurationMs
21
+ };
22
+ throw new Error(`Segment ID is not available for video. Debug info: ${JSON.stringify(debugInfo)}`);
23
+ }
14
24
  return mediaEngine.fetchMediaSegment(segmentId, mediaEngine.getVideoRendition(), signal);
15
25
  }
16
26
  });
@@ -7,9 +7,10 @@ const makeVideoSegmentIdTask = (host) => {
7
7
  console.error("videoSegmentIdTask error", error);
8
8
  },
9
9
  onComplete: (_value) => {},
10
- task: async (_, { signal }) => {
10
+ task: async ([, targetSeekTimeMs], { signal }) => {
11
11
  const mediaEngine = await getLatestMediaEngine(host, signal);
12
- return mediaEngine.computeSegmentId(host.desiredSeekTimeMs, mediaEngine.getVideoRendition());
12
+ signal.throwIfAborted();
13
+ return mediaEngine.computeSegmentId(targetSeekTimeMs, mediaEngine.getVideoRendition());
13
14
  }
14
15
  });
15
16
  };
@@ -12,8 +12,6 @@ export declare class EFMedia extends EFMedia_base {
12
12
  static readonly VIDEO_SAMPLE_BUFFER_SIZE = 30;
13
13
  static readonly AUDIO_SAMPLE_BUFFER_SIZE = 120;
14
14
  static get observedAttributes(): string[];
15
- private mediaSourceService;
16
- private audioElementFactory;
17
15
  static styles: import('lit').CSSResult[];
18
16
  currentTimeMs: number;
19
17
  /**
@@ -86,10 +84,6 @@ export declare class EFMedia extends EFMedia_base {
86
84
  * Now powered by clean, testable utility functions
87
85
  */
88
86
  fetchAudioSpanningTime(fromMs: number, toMs: number, signal?: AbortSignal): Promise<AudioSpan>;
89
- /**
90
- * Get the HTML audio element for ContextMixin integration
91
- */
92
- get audioElement(): HTMLAudioElement | null;
93
87
  /**
94
88
  * Check if an audio segment is cached in the unified buffer system
95
89
  * Now uses the same caching approach as video for consistency
@@ -100,11 +94,5 @@ export declare class EFMedia extends EFMedia_base {
100
94
  * Now uses the same caching approach as video for consistency
101
95
  */
102
96
  getCachedAudioSegments(segmentIds: number[]): Set<number>;
103
- /**
104
- * Get MediaElementAudioSourceNode for ContextMixin integration
105
- * Uses AudioElementFactory for proper caching and lifecycle management
106
- */
107
- getMediaElementSource(audioContext: AudioContext): Promise<MediaElementAudioSourceNode>;
108
- disconnectedCallback(): void;
109
97
  }
110
98
  export {};
@@ -8,8 +8,7 @@ import { makeAudioSeekTask } from "./EFMedia/audioTasks/makeAudioSeekTask.js";
8
8
  import { makeAudioSegmentFetchTask } from "./EFMedia/audioTasks/makeAudioSegmentFetchTask.js";
9
9
  import { makeAudioSegmentIdTask } from "./EFMedia/audioTasks/makeAudioSegmentIdTask.js";
10
10
  import { makeAudioTimeDomainAnalysisTask } from "./EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js";
11
- import { AudioElementFactory } from "./EFMedia/services/AudioElementFactory.js";
12
- import { MediaSourceService } from "./EFMedia/services/MediaSourceService.js";
11
+ import { fetchAudioSpanningTime } from "./EFMedia/shared/AudioSpanUtils.js";
13
12
  import { EFSourceMixin } from "./EFSourceMixin.js";
14
13
  import { EFTemporal } from "./EFTemporal.js";
15
14
  import { FetchMixin } from "./FetchMixin.js";
@@ -28,13 +27,6 @@ const deepGetMediaElements = (element, medias = []) => {
28
27
  var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(LitElement)), { assetType: "isobmff_files" })) {
29
28
  constructor(..._args) {
30
29
  super(..._args);
31
- this.mediaSourceService = new MediaSourceService({
32
- onError: (error) => {
33
- console.error("🎵 [EFMedia] MediaSourceService error:", error);
34
- },
35
- onReady: () => {}
36
- });
37
- this.audioElementFactory = new AudioElementFactory();
38
30
  this.currentTimeMs = 0;
39
31
  this.audioBufferDurationMs = 3e4;
40
32
  this.maxAudioBufferFetches = 2;
@@ -112,6 +104,8 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
112
104
  }
113
105
  updated(changedProperties) {
114
106
  super.updated(changedProperties);
107
+ const newCurrentSourceTimeMs = this.currentSourceTimeMs;
108
+ if (newCurrentSourceTimeMs !== this.desiredSeekTimeMs) this.executeSeek(newCurrentSourceTimeMs);
115
109
  if (changedProperties.has("ownCurrentTimeMs")) this.executeSeek(this.currentSourceTimeMs);
116
110
  if (changedProperties.has("currentTime") || changedProperties.has("ownCurrentTimeMs")) updateAnimations(this);
117
111
  }
@@ -132,15 +126,7 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
132
126
  * Now powered by clean, testable utility functions
133
127
  */
134
128
  async fetchAudioSpanningTime(fromMs, toMs, signal = new AbortController().signal) {
135
- await this.mediaSourceService.initialize();
136
- const { fetchAudioSpanningTime: fetchAudioSpan } = await import("./EFMedia/shared/AudioSpanUtils.js");
137
- return fetchAudioSpan(this, fromMs, toMs, signal);
138
- }
139
- /**
140
- * Get the HTML audio element for ContextMixin integration
141
- */
142
- get audioElement() {
143
- return this.mediaSourceService.getAudioElement();
129
+ return fetchAudioSpanningTime(this, fromMs, toMs, signal);
144
130
  }
145
131
  /**
146
132
  * Check if an audio segment is cached in the unified buffer system
@@ -158,18 +144,6 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
158
144
  if (!bufferState) return /* @__PURE__ */ new Set();
159
145
  return new Set(segmentIds.filter((id) => bufferState.cachedSegments.has(id)));
160
146
  }
161
- /**
162
- * Get MediaElementAudioSourceNode for ContextMixin integration
163
- * Uses AudioElementFactory for proper caching and lifecycle management
164
- */
165
- async getMediaElementSource(audioContext) {
166
- return this.audioElementFactory.createMediaElementSource(audioContext, this.mediaSourceService);
167
- }
168
- disconnectedCallback() {
169
- super.disconnectedCallback?.();
170
- this.mediaSourceService.cleanup();
171
- this.audioElementFactory.clearCache();
172
- }
173
147
  };
174
148
  _decorate([property({ type: Number })], EFMedia.prototype, "currentTimeMs", void 0);
175
149
  _decorate([property({
@@ -301,23 +301,18 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
301
301
  * Usage: timegroup.testPlayAudio(0, 5000) // Play first 5 seconds
302
302
  */
303
303
  async testPlayAudio(fromMs, toMs) {
304
- try {
305
- const renderedBuffer = await this.renderAudio(fromMs, toMs);
306
- const playbackContext = new AudioContext();
307
- const bufferSource = playbackContext.createBufferSource();
308
- bufferSource.buffer = renderedBuffer;
309
- bufferSource.connect(playbackContext.destination);
310
- bufferSource.start(0);
311
- return new Promise((resolve) => {
312
- bufferSource.onended = () => {
313
- playbackContext.close();
314
- resolve();
315
- };
316
- });
317
- } catch (error) {
318
- console.error("🎵 [TEST_PLAY_AUDIO] Error:", error);
319
- throw error;
320
- }
304
+ const renderedBuffer = await this.renderAudio(fromMs, toMs);
305
+ const playbackContext = new AudioContext();
306
+ const bufferSource = playbackContext.createBufferSource();
307
+ bufferSource.buffer = renderedBuffer;
308
+ bufferSource.connect(playbackContext.destination);
309
+ bufferSource.start(0);
310
+ return new Promise((resolve) => {
311
+ bufferSource.onended = () => {
312
+ playbackContext.close();
313
+ resolve();
314
+ };
315
+ });
321
316
  }
322
317
  async loadMd5Sums() {
323
318
  const efElements = this.efElements;
@@ -1,6 +1,5 @@
1
1
  import { Task } from '@lit/task';
2
2
  import { PropertyValueMap } from 'lit';
3
- import { CacheStats, ScrubTrackManager } from '../ScrubTrackManager.js';
4
3
  import { EFMedia } from './EFMedia.js';
5
4
  declare global {
6
5
  var EF_FRAMEGEN: import("../EF_FRAMEGEN.js").EFFramegen;
@@ -36,10 +35,6 @@ export declare class EFVideo extends EFVideo_base {
36
35
  videoInputTask: import('./EFMedia/shared/MediaTaskUtils.ts').InputTask;
37
36
  videoSeekTask: Task<readonly [number, import('./EFMedia/BufferedSeekingInput.ts').BufferedSeekingInput | undefined], import('mediabunny').VideoSample | undefined>;
38
37
  videoBufferTask: Task<readonly [number], import('./EFMedia/videoTasks/makeVideoBufferTask.ts').VideoBufferState>;
39
- /**
40
- * Scrub track manager for fast timeline navigation
41
- */
42
- scrubTrackManager?: ScrubTrackManager;
43
38
  /**
44
39
  * Delayed loading state manager for user feedback
45
40
  */
@@ -84,10 +79,6 @@ export declare class EFVideo extends EFVideo_base {
84
79
  * Check if EF_FRAMEGEN has explicitly started frame rendering (not just initialization)
85
80
  */
86
81
  private isFrameRenderingActive;
87
- /**
88
- * Get scrub track performance statistics
89
- */
90
- getScrubTrackStats(): CacheStats | null;
91
82
  /**
92
83
  * Effective mode - always returns "asset" for EFVideo
93
84
  */