@editframe/elements 0.20.4-beta.0 → 0.21.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DelayedLoadingState.js +0 -27
- package/dist/EF_FRAMEGEN.d.ts +5 -3
- package/dist/EF_FRAMEGEN.js +50 -11
- package/dist/_virtual/_@oxc-project_runtime@0.93.0/helpers/decorate.js +7 -0
- package/dist/elements/ContextProxiesController.js +2 -22
- package/dist/elements/EFAudio.js +4 -8
- package/dist/elements/EFCaptions.js +59 -84
- package/dist/elements/EFImage.js +5 -6
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -4
- package/dist/elements/EFMedia/AssetMediaEngine.js +35 -30
- package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
- package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -76
- package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +3 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +4 -16
- package/dist/elements/EFMedia/shared/BufferUtils.js +2 -15
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -7
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +7 -11
- package/dist/elements/EFMedia.js +26 -24
- package/dist/elements/EFSourceMixin.js +5 -7
- package/dist/elements/EFSurface.js +6 -9
- package/dist/elements/EFTemporal.js +19 -37
- package/dist/elements/EFThumbnailStrip.js +16 -59
- package/dist/elements/EFTimegroup.js +95 -90
- package/dist/elements/EFVideo.d.ts +6 -2
- package/dist/elements/EFVideo.js +142 -107
- package/dist/elements/EFWaveform.js +18 -27
- package/dist/elements/SampleBuffer.js +2 -5
- package/dist/elements/TargetController.js +3 -3
- package/dist/elements/durationConverter.js +4 -4
- package/dist/elements/updateAnimations.js +14 -35
- package/dist/gui/ContextMixin.js +23 -52
- package/dist/gui/EFConfiguration.js +7 -7
- package/dist/gui/EFControls.js +5 -5
- package/dist/gui/EFFilmstrip.js +77 -98
- package/dist/gui/EFFitScale.js +5 -6
- package/dist/gui/EFFocusOverlay.js +4 -4
- package/dist/gui/EFPreview.js +4 -4
- package/dist/gui/EFScrubber.js +9 -9
- package/dist/gui/EFTimeDisplay.js +5 -5
- package/dist/gui/EFToggleLoop.js +4 -4
- package/dist/gui/EFTogglePlay.js +5 -5
- package/dist/gui/EFWorkbench.js +5 -5
- package/dist/gui/TWMixin2.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/otel/BridgeSpanExporter.d.ts +13 -0
- package/dist/otel/BridgeSpanExporter.js +87 -0
- package/dist/otel/setupBrowserTracing.d.ts +12 -0
- package/dist/otel/setupBrowserTracing.js +30 -0
- package/dist/otel/tracingHelpers.d.ts +34 -0
- package/dist/otel/tracingHelpers.js +113 -0
- package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
- package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
- package/dist/transcoding/utils/UrlGenerator.js +2 -19
- package/dist/utils/LRUCache.js +6 -53
- package/package.json +10 -2
- package/src/elements/EFCaptions.browsertest.ts +2 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +65 -37
- package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
- package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
- package/src/elements/EFMedia.ts +16 -1
- package/src/elements/EFTimegroup.browsertest.ts +10 -8
- package/src/elements/EFTimegroup.ts +164 -76
- package/src/elements/EFVideo.browsertest.ts +19 -27
- package/src/elements/EFVideo.ts +203 -101
- package/src/otel/BridgeSpanExporter.ts +150 -0
- package/src/otel/setupBrowserTracing.ts +68 -0
- package/src/otel/tracingHelpers.ts +251 -0
- package/types.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TrackFragmentIndex } from "@editframe/assets";
|
|
2
2
|
|
|
3
|
+
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
3
4
|
import type {
|
|
4
5
|
AudioRendition,
|
|
5
6
|
InitSegmentPaths,
|
|
@@ -121,23 +122,36 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
121
122
|
rendition: { trackId: number | undefined; src: string },
|
|
122
123
|
signal: AbortSignal,
|
|
123
124
|
) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
125
|
+
return withSpan(
|
|
126
|
+
"assetEngine.fetchInitSegment",
|
|
127
|
+
{
|
|
128
|
+
trackId: rendition.trackId || -1,
|
|
129
|
+
src: rendition.src,
|
|
130
|
+
},
|
|
131
|
+
undefined,
|
|
132
|
+
async (span) => {
|
|
133
|
+
if (!rendition.trackId) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"[fetchInitSegment] Track ID is required for asset metadata",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const url = this.buildInitSegmentUrl(rendition.trackId);
|
|
139
|
+
const initSegment = this.data[rendition.trackId]?.initSegment;
|
|
140
|
+
if (!initSegment) {
|
|
141
|
+
throw new Error("Init segment not found");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
span.setAttribute("offset", initSegment.offset);
|
|
145
|
+
span.setAttribute("size", initSegment.size);
|
|
146
|
+
|
|
147
|
+
// Use unified fetch method with Range headers
|
|
148
|
+
const headers = {
|
|
149
|
+
Range: `bytes=${initSegment.offset}-${initSegment.offset + initSegment.size - 1}`,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
async fetchMediaSegment(
|
|
@@ -145,26 +159,40 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
145
159
|
rendition: { trackId: number | undefined; src: string },
|
|
146
160
|
signal?: AbortSignal,
|
|
147
161
|
) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
return withSpan(
|
|
163
|
+
"assetEngine.fetchMediaSegment",
|
|
164
|
+
{
|
|
165
|
+
segmentId,
|
|
166
|
+
trackId: rendition.trackId || -1,
|
|
167
|
+
src: rendition.src,
|
|
168
|
+
},
|
|
169
|
+
undefined,
|
|
170
|
+
async (span) => {
|
|
171
|
+
if (!rendition.trackId) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"[fetchMediaSegment] Track ID is required for asset metadata",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (segmentId === undefined) {
|
|
177
|
+
throw new Error("Segment ID is not available");
|
|
178
|
+
}
|
|
179
|
+
const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);
|
|
180
|
+
const mediaSegment = this.data[rendition.trackId]?.segments[segmentId];
|
|
181
|
+
if (!mediaSegment) {
|
|
182
|
+
throw new Error("Media segment not found");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
span.setAttribute("offset", mediaSegment.offset);
|
|
186
|
+
span.setAttribute("size", mediaSegment.size);
|
|
187
|
+
|
|
188
|
+
// Use unified fetch method with Range headers
|
|
189
|
+
const headers = {
|
|
190
|
+
Range: `bytes=${mediaSegment.offset}-${mediaSegment.offset + mediaSegment.size - 1}`,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
194
|
+
},
|
|
195
|
+
);
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
1
2
|
import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
|
|
2
3
|
import type {
|
|
3
4
|
AudioRendition,
|
|
@@ -61,65 +62,122 @@ export abstract class BaseMediaEngine {
|
|
|
61
62
|
signal?: AbortSignal;
|
|
62
63
|
},
|
|
63
64
|
): Promise<any> {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
65
|
+
return withSpan(
|
|
66
|
+
"mediaEngine.fetchWithCache",
|
|
67
|
+
{
|
|
68
|
+
url: url.length > 100 ? `${url.substring(0, 100)}...` : url,
|
|
69
|
+
responseType: options.responseType,
|
|
70
|
+
hasHeaders: !!options.headers,
|
|
71
|
+
},
|
|
72
|
+
undefined,
|
|
73
|
+
async (span) => {
|
|
74
|
+
const t0 = performance.now();
|
|
75
|
+
const { responseType, headers, signal } = options;
|
|
76
|
+
|
|
77
|
+
// Create cache key that includes URL and headers for proper isolation
|
|
78
|
+
// Note: We don't include signal in cache key as it would prevent proper deduplication
|
|
79
|
+
const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
|
|
80
|
+
|
|
81
|
+
// Check cache first
|
|
82
|
+
const t1 = performance.now();
|
|
83
|
+
const cached = mediaCache.get(cacheKey);
|
|
84
|
+
const t2 = performance.now();
|
|
85
|
+
span.setAttribute("cacheLookupMs", Math.round((t2 - t1) * 1000) / 1000);
|
|
86
|
+
|
|
87
|
+
if (cached) {
|
|
88
|
+
span.setAttribute("cacheHit", true);
|
|
89
|
+
// If we have a cached promise, we need to handle the caller's abort signal
|
|
90
|
+
// without affecting the underlying request that other instances might be using
|
|
91
|
+
if (signal) {
|
|
92
|
+
const t3 = performance.now();
|
|
93
|
+
const result = await this.handleAbortForCachedRequest(
|
|
94
|
+
cached,
|
|
95
|
+
signal,
|
|
96
|
+
);
|
|
97
|
+
const t4 = performance.now();
|
|
98
|
+
span.setAttribute(
|
|
99
|
+
"handleAbortMs",
|
|
100
|
+
Math.round((t4 - t3) * 100) / 100,
|
|
101
|
+
);
|
|
102
|
+
span.setAttribute(
|
|
103
|
+
"totalCacheHitMs",
|
|
104
|
+
Math.round((t4 - t0) * 100) / 100,
|
|
105
|
+
);
|
|
106
|
+
return result;
|
|
93
107
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
span.setAttribute(
|
|
109
|
+
"totalCacheHitMs",
|
|
110
|
+
Math.round((t2 - t0) * 100) / 100,
|
|
111
|
+
);
|
|
112
|
+
return cached;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
span.setAttribute("cacheHit", false);
|
|
116
|
+
|
|
117
|
+
// Use global deduplicator to prevent concurrent requests for the same resource
|
|
118
|
+
// Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort
|
|
119
|
+
const promise = globalRequestDeduplicator.executeRequest(
|
|
120
|
+
cacheKey,
|
|
121
|
+
async () => {
|
|
122
|
+
const fetchStart = performance.now();
|
|
123
|
+
try {
|
|
124
|
+
// Make the fetch request WITHOUT the signal - let each caller handle their own abort
|
|
125
|
+
// This prevents one instance's abort from affecting other instances using the shared cache
|
|
126
|
+
const response = await this.host.fetch(url, { headers });
|
|
127
|
+
const fetchEnd = performance.now();
|
|
128
|
+
span.setAttribute("fetchMs", fetchEnd - fetchStart);
|
|
129
|
+
|
|
130
|
+
if (responseType === "json") {
|
|
131
|
+
return response.json();
|
|
132
|
+
}
|
|
133
|
+
const buffer = await response.arrayBuffer();
|
|
134
|
+
span.setAttribute("sizeBytes", buffer.byteLength);
|
|
135
|
+
return buffer;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// If the request was aborted, don't cache the error
|
|
138
|
+
if (
|
|
139
|
+
error instanceof DOMException &&
|
|
140
|
+
error.name === "AbortError"
|
|
141
|
+
) {
|
|
142
|
+
// Remove from cache so other requests can retry
|
|
143
|
+
mediaCache.delete(cacheKey);
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Cache the promise (not the result) to handle concurrent requests
|
|
151
|
+
mediaCache.set(cacheKey, promise);
|
|
152
|
+
|
|
153
|
+
// Handle the case where the promise might be aborted
|
|
154
|
+
promise.catch((error) => {
|
|
155
|
+
// If the request was aborted, remove it from cache to prevent corrupted data
|
|
97
156
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
98
|
-
// Remove from cache so other requests can retry
|
|
99
157
|
mediaCache.delete(cacheKey);
|
|
100
158
|
}
|
|
101
|
-
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// If the caller has a signal, handle abort logic without affecting the underlying request
|
|
162
|
+
if (signal) {
|
|
163
|
+
const result = await this.handleAbortForCachedRequest(
|
|
164
|
+
promise,
|
|
165
|
+
signal,
|
|
166
|
+
);
|
|
167
|
+
const tEnd = performance.now();
|
|
168
|
+
span.setAttribute(
|
|
169
|
+
"totalFetchMs",
|
|
170
|
+
Math.round((tEnd - t0) * 100) / 100,
|
|
171
|
+
);
|
|
172
|
+
return result;
|
|
102
173
|
}
|
|
174
|
+
|
|
175
|
+
const result = await promise;
|
|
176
|
+
const tEnd = performance.now();
|
|
177
|
+
span.setAttribute("totalFetchMs", Math.round((tEnd - t0) * 100) / 100);
|
|
178
|
+
return result;
|
|
103
179
|
},
|
|
104
180
|
);
|
|
105
|
-
|
|
106
|
-
// Cache the promise (not the result) to handle concurrent requests
|
|
107
|
-
mediaCache.set(cacheKey, promise);
|
|
108
|
-
|
|
109
|
-
// Handle the case where the promise might be aborted
|
|
110
|
-
promise.catch((error) => {
|
|
111
|
-
// If the request was aborted, remove it from cache to prevent corrupted data
|
|
112
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
113
|
-
mediaCache.delete(cacheKey);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// If the caller has a signal, handle abort logic without affecting the underlying request
|
|
118
|
-
if (signal) {
|
|
119
|
-
return this.handleAbortForCachedRequest(promise, signal);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return promise;
|
|
123
181
|
}
|
|
124
182
|
|
|
125
183
|
/**
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
MP4,
|
|
9
9
|
VideoSampleSink,
|
|
10
10
|
} from "mediabunny";
|
|
11
|
+
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
11
12
|
import { type MediaSample, SampleBuffer } from "../SampleBuffer";
|
|
12
13
|
import { roundToMilliseconds } from "./shared/PrecisionUtils";
|
|
13
14
|
|
|
@@ -177,26 +178,39 @@ export class BufferedSeekingInput {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
async seek(trackId: number, timeMs: number) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
return withSpan(
|
|
182
|
+
"bufferedInput.seek",
|
|
183
|
+
{
|
|
184
|
+
trackId,
|
|
185
|
+
timeMs,
|
|
186
|
+
startTimeOffsetMs: this.startTimeOffsetMs,
|
|
187
|
+
},
|
|
188
|
+
undefined,
|
|
189
|
+
async (span) => {
|
|
190
|
+
// Apply timeline offset to map user timeline to media timeline
|
|
191
|
+
const mediaTimeMs = timeMs + this.startTimeOffsetMs;
|
|
192
|
+
|
|
193
|
+
// Round using consistent precision handling
|
|
194
|
+
const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);
|
|
195
|
+
span.setAttribute("roundedMediaTimeMs", roundedMediaTimeMs);
|
|
196
|
+
|
|
197
|
+
// Serialize seek operations per track (but don't block iterator creation)
|
|
198
|
+
const existingSeek = this.trackSeekPromises.get(trackId);
|
|
199
|
+
if (existingSeek) {
|
|
200
|
+
span.setAttribute("waitedForExistingSeek", true);
|
|
201
|
+
await existingSeek;
|
|
202
|
+
}
|
|
191
203
|
|
|
192
|
-
|
|
193
|
-
|
|
204
|
+
const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);
|
|
205
|
+
this.trackSeekPromises.set(trackId, seekPromise);
|
|
194
206
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
try {
|
|
208
|
+
return await seekPromise;
|
|
209
|
+
} finally {
|
|
210
|
+
this.trackSeekPromises.delete(trackId);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
);
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
private async resetIterator(track: InputTrack) {
|
|
@@ -224,90 +238,193 @@ export class BufferedSeekingInput {
|
|
|
224
238
|
#seekLock?: PromiseWithResolvers<void>;
|
|
225
239
|
|
|
226
240
|
private async seekSafe(trackId: number, timeMs: number) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const firstTimestampMs = roundToMilliseconds(
|
|
239
|
-
(await track.getFirstTimestamp()) * 1000,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
if (roundedTimeMs < firstTimestampMs) {
|
|
243
|
-
console.error("Seeking outside bounds of input", {
|
|
244
|
-
roundedTimeMs,
|
|
245
|
-
firstTimestampMs,
|
|
246
|
-
});
|
|
247
|
-
throw new NoSample(
|
|
248
|
-
`Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Check if we need to reset iterator for seeks outside current buffer range
|
|
253
|
-
const bufferContents = trackBuffer.getContents();
|
|
254
|
-
if (bufferContents.length > 0) {
|
|
255
|
-
const bufferStartMs = roundToMilliseconds(
|
|
256
|
-
trackBuffer.firstTimestamp * 1000,
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
if (roundedTimeMs < bufferStartMs) {
|
|
260
|
-
await this.resetIterator(track);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const alreadyInBuffer = trackBuffer.find(timeMs);
|
|
265
|
-
if (alreadyInBuffer) return alreadyInBuffer;
|
|
266
|
-
|
|
267
|
-
const iterator = this.getTrackIterator(track);
|
|
268
|
-
while (true) {
|
|
269
|
-
const { done, value: decodedSample } = await iterator.next();
|
|
270
|
-
|
|
271
|
-
if (decodedSample) {
|
|
272
|
-
trackBuffer.push(decodedSample);
|
|
273
|
-
}
|
|
274
|
-
const foundSample = trackBuffer.find(roundedTimeMs);
|
|
275
|
-
if (foundSample) {
|
|
276
|
-
return foundSample;
|
|
277
|
-
}
|
|
278
|
-
if (done) {
|
|
279
|
-
break;
|
|
241
|
+
return withSpan(
|
|
242
|
+
"bufferedInput.seekSafe",
|
|
243
|
+
{
|
|
244
|
+
trackId,
|
|
245
|
+
timeMs,
|
|
246
|
+
},
|
|
247
|
+
undefined,
|
|
248
|
+
async (span) => {
|
|
249
|
+
if (this.#seekLock) {
|
|
250
|
+
span.setAttribute("waitedForSeekLock", true);
|
|
251
|
+
await this.#seekLock.promise;
|
|
280
252
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
253
|
+
const seekLock = Promise.withResolvers<void>();
|
|
254
|
+
this.#seekLock = seekLock;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const track = await this.getTrack(trackId);
|
|
258
|
+
span.setAttribute("trackType", track.type);
|
|
259
|
+
|
|
260
|
+
const trackBuffer = this.getTrackBuffer(track);
|
|
261
|
+
|
|
262
|
+
const roundedTimeMs = roundToMilliseconds(timeMs);
|
|
263
|
+
const firstTimestampMs = roundToMilliseconds(
|
|
264
|
+
(await track.getFirstTimestamp()) * 1000,
|
|
265
|
+
);
|
|
266
|
+
span.setAttribute("firstTimestampMs", firstTimestampMs);
|
|
267
|
+
|
|
268
|
+
if (roundedTimeMs < firstTimestampMs) {
|
|
269
|
+
console.error("Seeking outside bounds of input", {
|
|
270
|
+
roundedTimeMs,
|
|
271
|
+
firstTimestampMs,
|
|
272
|
+
});
|
|
273
|
+
throw new NoSample(
|
|
274
|
+
`Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check if we need to reset iterator for seeks outside current buffer range
|
|
279
|
+
const bufferContents = trackBuffer.getContents();
|
|
280
|
+
span.setAttribute("bufferContentsLength", bufferContents.length);
|
|
281
|
+
|
|
282
|
+
if (bufferContents.length > 0) {
|
|
283
|
+
const bufferStartMs = roundToMilliseconds(
|
|
284
|
+
trackBuffer.firstTimestamp * 1000,
|
|
285
|
+
);
|
|
286
|
+
span.setAttribute("bufferStartMs", bufferStartMs);
|
|
287
|
+
|
|
288
|
+
if (roundedTimeMs < bufferStartMs) {
|
|
289
|
+
span.setAttribute("resetIterator", true);
|
|
290
|
+
await this.resetIterator(track);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const alreadyInBuffer = trackBuffer.find(timeMs);
|
|
295
|
+
if (alreadyInBuffer) {
|
|
296
|
+
span.setAttribute("foundInBuffer", true);
|
|
297
|
+
span.setAttribute("bufferSize", trackBuffer.length);
|
|
298
|
+
const contents = trackBuffer.getContents();
|
|
299
|
+
if (contents.length > 0) {
|
|
300
|
+
span.setAttribute(
|
|
301
|
+
"bufferTimestamps",
|
|
302
|
+
contents
|
|
303
|
+
.map((s) => Math.round((s.timestamp || 0) * 1000))
|
|
304
|
+
.slice(0, 10)
|
|
305
|
+
.join(","),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return alreadyInBuffer;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Buffer miss - record buffer state
|
|
312
|
+
span.setAttribute("foundInBuffer", false);
|
|
313
|
+
span.setAttribute("bufferSize", trackBuffer.length);
|
|
314
|
+
span.setAttribute("requestedTimeMs", Math.round(timeMs));
|
|
315
|
+
|
|
316
|
+
const contents = trackBuffer.getContents();
|
|
317
|
+
if (contents.length > 0) {
|
|
318
|
+
const firstSample = contents[0];
|
|
319
|
+
const lastSample = contents[contents.length - 1];
|
|
320
|
+
if (firstSample && lastSample) {
|
|
321
|
+
const bufferStartMs = Math.round(
|
|
322
|
+
(firstSample.timestamp || 0) * 1000,
|
|
323
|
+
);
|
|
324
|
+
const bufferEndMs = Math.round(
|
|
325
|
+
((lastSample.timestamp || 0) + (lastSample.duration || 0)) *
|
|
326
|
+
1000,
|
|
327
|
+
);
|
|
328
|
+
span.setAttribute("bufferStartMs", bufferStartMs);
|
|
329
|
+
span.setAttribute("bufferEndMs", bufferEndMs);
|
|
330
|
+
span.setAttribute(
|
|
331
|
+
"bufferRangeMs",
|
|
332
|
+
`${bufferStartMs}-${bufferEndMs}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const iterator = this.getTrackIterator(track);
|
|
338
|
+
let iterationCount = 0;
|
|
339
|
+
const decodeStart = performance.now();
|
|
340
|
+
|
|
341
|
+
while (true) {
|
|
342
|
+
iterationCount++;
|
|
343
|
+
const iterStart = performance.now();
|
|
344
|
+
const { done, value: decodedSample } = await iterator.next();
|
|
345
|
+
const iterEnd = performance.now();
|
|
346
|
+
|
|
347
|
+
// Record individual iteration timing for first 5 iterations
|
|
348
|
+
if (iterationCount <= 5) {
|
|
349
|
+
span.setAttribute(
|
|
350
|
+
`iter${iterationCount}Ms`,
|
|
351
|
+
Math.round((iterEnd - iterStart) * 100) / 100,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (decodedSample) {
|
|
356
|
+
trackBuffer.push(decodedSample);
|
|
357
|
+
if (iterationCount <= 5) {
|
|
358
|
+
span.setAttribute(
|
|
359
|
+
`iter${iterationCount}Timestamp`,
|
|
360
|
+
Math.round((decodedSample.timestamp || 0) * 1000),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const foundSample = trackBuffer.find(roundedTimeMs);
|
|
366
|
+
if (foundSample) {
|
|
367
|
+
const decodeEnd = performance.now();
|
|
368
|
+
span.setAttribute("iterationCount", iterationCount);
|
|
369
|
+
span.setAttribute(
|
|
370
|
+
"decodeMs",
|
|
371
|
+
Math.round((decodeEnd - decodeStart) * 100) / 100,
|
|
372
|
+
);
|
|
373
|
+
span.setAttribute(
|
|
374
|
+
"avgIterMs",
|
|
375
|
+
Math.round(((decodeEnd - decodeStart) / iterationCount) * 100) /
|
|
376
|
+
100,
|
|
377
|
+
);
|
|
378
|
+
span.setAttribute("foundSample", true);
|
|
379
|
+
span.setAttribute(
|
|
380
|
+
"foundTimestamp",
|
|
381
|
+
Math.round((foundSample.timestamp || 0) * 1000),
|
|
382
|
+
);
|
|
383
|
+
return foundSample;
|
|
384
|
+
}
|
|
385
|
+
if (done) {
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
span.setAttribute("iterationCount", iterationCount);
|
|
391
|
+
span.setAttribute("reachedEnd", true);
|
|
392
|
+
|
|
393
|
+
// Check if we're seeking to the exact end of the track (legitimate use case)
|
|
394
|
+
const finalBufferContents = trackBuffer.getContents();
|
|
395
|
+
if (finalBufferContents.length > 0) {
|
|
396
|
+
const lastSample =
|
|
397
|
+
finalBufferContents[finalBufferContents.length - 1];
|
|
398
|
+
const lastSampleEndMs = roundToMilliseconds(
|
|
399
|
+
((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) *
|
|
400
|
+
1000,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// Only return last sample if seeking to exactly the track duration
|
|
404
|
+
// (end of video) AND we have the final segment loaded
|
|
405
|
+
const trackDurationMs = (await track.computeDuration()) * 1000;
|
|
406
|
+
const isSeekingToTrackEnd =
|
|
407
|
+
roundToMilliseconds(timeMs) ===
|
|
408
|
+
roundToMilliseconds(trackDurationMs);
|
|
409
|
+
const isAtEndOfTrack =
|
|
410
|
+
roundToMilliseconds(timeMs) >= lastSampleEndMs;
|
|
411
|
+
|
|
412
|
+
if (isSeekingToTrackEnd && isAtEndOfTrack) {
|
|
413
|
+
span.setAttribute("returnedLastSample", true);
|
|
414
|
+
return lastSample;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// For all other cases (seeking within track but outside buffer range), throw error
|
|
419
|
+
// The caller should ensure the correct segment is loaded before seeking
|
|
420
|
+
throw new NoSample(
|
|
421
|
+
`Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,
|
|
422
|
+
);
|
|
423
|
+
} finally {
|
|
424
|
+
this.#seekLock = undefined;
|
|
425
|
+
seekLock.resolve();
|
|
300
426
|
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// For all other cases (seeking within track but outside buffer range), throw error
|
|
304
|
-
// The caller should ensure the correct segment is loaded before seeking
|
|
305
|
-
throw new NoSample(
|
|
306
|
-
`Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,
|
|
307
|
-
);
|
|
308
|
-
} finally {
|
|
309
|
-
this.#seekLock = undefined;
|
|
310
|
-
seekLock.resolve();
|
|
311
|
-
}
|
|
427
|
+
},
|
|
428
|
+
);
|
|
312
429
|
}
|
|
313
430
|
}
|
|
@@ -19,6 +19,8 @@ export const makeAudioInputTask = (host: EFMedia): InputTask => {
|
|
|
19
19
|
onComplete: (_value) => {},
|
|
20
20
|
task: async (_, { signal }) => {
|
|
21
21
|
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
22
|
+
if (signal.aborted) return undefined;
|
|
23
|
+
|
|
22
24
|
const audioRendition = mediaEngine?.audioRendition;
|
|
23
25
|
|
|
24
26
|
// Return undefined if no audio rendition available (video-only asset)
|
|
@@ -27,9 +29,10 @@ export const makeAudioInputTask = (host: EFMedia): InputTask => {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const initSegment = await host.audioInitSegmentFetchTask.taskComplete;
|
|
30
|
-
signal.
|
|
32
|
+
if (signal.aborted) return undefined;
|
|
33
|
+
|
|
31
34
|
const segment = await host.audioSegmentFetchTask.taskComplete;
|
|
32
|
-
signal.
|
|
35
|
+
if (signal.aborted) return undefined;
|
|
33
36
|
|
|
34
37
|
if (!initSegment || !segment) {
|
|
35
38
|
return undefined;
|
|
@@ -38,7 +41,8 @@ export const makeAudioInputTask = (host: EFMedia): InputTask => {
|
|
|
38
41
|
const startTimeOffsetMs = audioRendition.startTimeOffsetMs;
|
|
39
42
|
|
|
40
43
|
const arrayBuffer = await new Blob([initSegment, segment]).arrayBuffer();
|
|
41
|
-
signal.
|
|
44
|
+
if (signal.aborted) return undefined;
|
|
45
|
+
|
|
42
46
|
return new BufferedSeekingInput(arrayBuffer, {
|
|
43
47
|
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
44
48
|
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|