@editframe/elements 0.26.2-beta.0 → 0.26.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/EFTimegroup.js +7 -2
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/package.json +2 -2
- package/scripts/build-css.js +3 -3
- package/tsdown.config.ts +1 -1
- package/types.json +1 -1
- package/src/elements/ContextProxiesController.ts +0 -124
- package/src/elements/CrossUpdateController.ts +0 -22
- package/src/elements/EFAudio.browsertest.ts +0 -706
- package/src/elements/EFAudio.ts +0 -56
- package/src/elements/EFCaptions.browsertest.ts +0 -1960
- package/src/elements/EFCaptions.ts +0 -823
- package/src/elements/EFImage.browsertest.ts +0 -120
- package/src/elements/EFImage.ts +0 -113
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
- package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
- package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
- package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
- package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
- package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
- package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
- package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
- package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
- package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
- package/src/elements/EFMedia.browsertest.ts +0 -872
- package/src/elements/EFMedia.ts +0 -341
- package/src/elements/EFSourceMixin.ts +0 -60
- package/src/elements/EFSurface.browsertest.ts +0 -151
- package/src/elements/EFSurface.ts +0 -142
- package/src/elements/EFTemporal.browsertest.ts +0 -215
- package/src/elements/EFTemporal.ts +0 -800
- package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
- package/src/elements/EFThumbnailStrip.ts +0 -906
- package/src/elements/EFTimegroup.browsertest.ts +0 -870
- package/src/elements/EFTimegroup.ts +0 -878
- package/src/elements/EFVideo.browsertest.ts +0 -1482
- package/src/elements/EFVideo.ts +0 -564
- package/src/elements/EFWaveform.ts +0 -547
- package/src/elements/FetchContext.browsertest.ts +0 -401
- package/src/elements/FetchMixin.ts +0 -38
- package/src/elements/SampleBuffer.ts +0 -94
- package/src/elements/TargetController.browsertest.ts +0 -230
- package/src/elements/TargetController.ts +0 -224
- package/src/elements/TimegroupController.ts +0 -26
- package/src/elements/durationConverter.ts +0 -35
- package/src/elements/parseTimeToMs.ts +0 -9
- package/src/elements/printTaskStatus.ts +0 -16
- package/src/elements/renderTemporalAudio.ts +0 -108
- package/src/elements/updateAnimations.browsertest.ts +0 -1884
- package/src/elements/updateAnimations.ts +0 -217
- package/src/elements/util.ts +0 -24
- package/src/gui/ContextMixin.browsertest.ts +0 -860
- package/src/gui/ContextMixin.ts +0 -562
- package/src/gui/Controllable.browsertest.ts +0 -258
- package/src/gui/Controllable.ts +0 -41
- package/src/gui/EFConfiguration.ts +0 -40
- package/src/gui/EFControls.browsertest.ts +0 -389
- package/src/gui/EFControls.ts +0 -195
- package/src/gui/EFDial.browsertest.ts +0 -84
- package/src/gui/EFDial.ts +0 -172
- package/src/gui/EFFilmstrip.browsertest.ts +0 -712
- package/src/gui/EFFilmstrip.ts +0 -1349
- package/src/gui/EFFitScale.ts +0 -152
- package/src/gui/EFFocusOverlay.ts +0 -79
- package/src/gui/EFPause.browsertest.ts +0 -202
- package/src/gui/EFPause.ts +0 -73
- package/src/gui/EFPlay.browsertest.ts +0 -202
- package/src/gui/EFPlay.ts +0 -73
- package/src/gui/EFPreview.ts +0 -74
- package/src/gui/EFResizableBox.browsertest.ts +0 -79
- package/src/gui/EFResizableBox.ts +0 -898
- package/src/gui/EFScrubber.ts +0 -151
- package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
- package/src/gui/EFTimeDisplay.ts +0 -55
- package/src/gui/EFToggleLoop.ts +0 -35
- package/src/gui/EFTogglePlay.ts +0 -70
- package/src/gui/EFWorkbench.ts +0 -115
- package/src/gui/PlaybackController.ts +0 -527
- package/src/gui/TWMixin.css +0 -6
- package/src/gui/TWMixin.ts +0 -61
- package/src/gui/TargetOrContextMixin.ts +0 -185
- package/src/gui/currentTimeContext.ts +0 -5
- package/src/gui/durationContext.ts +0 -3
- package/src/gui/efContext.ts +0 -6
- package/src/gui/fetchContext.ts +0 -5
- package/src/gui/focusContext.ts +0 -7
- package/src/gui/focusedElementContext.ts +0 -5
- package/src/gui/playingContext.ts +0 -5
- package/src/otel/BridgeSpanExporter.ts +0 -150
- package/src/otel/setupBrowserTracing.ts +0 -73
- package/src/otel/tracingHelpers.ts +0 -251
- package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
- package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
- package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
- package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
- package/src/transcoding/types/index.ts +0 -312
- package/src/transcoding/utils/MediaUtils.ts +0 -63
- package/src/transcoding/utils/UrlGenerator.ts +0 -68
- package/src/transcoding/utils/constants.ts +0 -36
- package/src/utils/LRUCache.test.ts +0 -274
- package/src/utils/LRUCache.ts +0 -696
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { Task } from "@lit/task";
|
|
2
|
-
|
|
3
|
-
import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE";
|
|
4
|
-
import { EF_RENDERING } from "../../../EF_RENDERING";
|
|
5
|
-
import type { AudioRendition } from "../../../transcoding/types";
|
|
6
|
-
import type { EFMedia } from "../../EFMedia";
|
|
7
|
-
import {
|
|
8
|
-
type MediaBufferConfig,
|
|
9
|
-
type MediaBufferState,
|
|
10
|
-
manageMediaBuffer,
|
|
11
|
-
} from "../shared/BufferUtils";
|
|
12
|
-
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Configuration for audio buffering - extends the generic interface
|
|
16
|
-
*/
|
|
17
|
-
export interface AudioBufferConfig extends MediaBufferConfig {}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* State of the audio buffer - uses the generic interface
|
|
21
|
-
*/
|
|
22
|
-
export interface AudioBufferState extends MediaBufferState {}
|
|
23
|
-
|
|
24
|
-
type AudioBufferTask = Task<readonly [number], AudioBufferState>;
|
|
25
|
-
export const makeAudioBufferTask = (host: EFMedia): AudioBufferTask => {
|
|
26
|
-
let currentState: AudioBufferState = {
|
|
27
|
-
currentSeekTimeMs: 0,
|
|
28
|
-
requestedSegments: new Set(),
|
|
29
|
-
activeRequests: new Set(),
|
|
30
|
-
requestQueue: [],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return new Task(host, {
|
|
34
|
-
autoRun: EF_INTERACTIVE, // Make lazy - only run when element becomes timeline-active
|
|
35
|
-
args: () => [host.desiredSeekTimeMs] as const,
|
|
36
|
-
onError: (error) => {
|
|
37
|
-
console.error("audioBufferTask error", error);
|
|
38
|
-
},
|
|
39
|
-
onComplete: (value) => {
|
|
40
|
-
currentState = value;
|
|
41
|
-
},
|
|
42
|
-
task: async ([seekTimeMs], { signal }) => {
|
|
43
|
-
// Skip buffering entirely in rendering mode
|
|
44
|
-
if (EF_RENDERING()) {
|
|
45
|
-
return currentState; // Return existing state without any buffering activity
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Get media engine to potentially override buffer configuration
|
|
49
|
-
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
50
|
-
|
|
51
|
-
// Return existing state if no audio rendition available
|
|
52
|
-
if (!mediaEngine.audioRendition) {
|
|
53
|
-
return currentState;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Use media engine's buffer config, falling back to host properties
|
|
57
|
-
const engineConfig = mediaEngine.getBufferConfig();
|
|
58
|
-
const bufferDurationMs = engineConfig.audioBufferDurationMs;
|
|
59
|
-
const maxParallelFetches = engineConfig.maxAudioBufferFetches;
|
|
60
|
-
|
|
61
|
-
const currentConfig: AudioBufferConfig = {
|
|
62
|
-
bufferDurationMs,
|
|
63
|
-
maxParallelFetches,
|
|
64
|
-
enableBuffering: host.enableAudioBuffering,
|
|
65
|
-
bufferThresholdMs: engineConfig.bufferThresholdMs,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Timeline context for priority-based buffering
|
|
69
|
-
const timelineContext =
|
|
70
|
-
host.rootTimegroup?.currentTimeMs !== undefined
|
|
71
|
-
? {
|
|
72
|
-
elementStartMs: host.startTimeMs,
|
|
73
|
-
elementEndMs: host.endTimeMs,
|
|
74
|
-
playheadMs: host.rootTimegroup.currentTimeMs,
|
|
75
|
-
}
|
|
76
|
-
: undefined;
|
|
77
|
-
|
|
78
|
-
return manageMediaBuffer<AudioRendition>(
|
|
79
|
-
seekTimeMs,
|
|
80
|
-
currentConfig,
|
|
81
|
-
currentState,
|
|
82
|
-
(host as any).intrinsicDurationMs || 10000,
|
|
83
|
-
signal,
|
|
84
|
-
{
|
|
85
|
-
computeSegmentId: async (timeMs, rendition) => {
|
|
86
|
-
// Use media engine's computeSegmentId
|
|
87
|
-
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
88
|
-
return mediaEngine.computeSegmentId(timeMs, rendition);
|
|
89
|
-
},
|
|
90
|
-
prefetchSegment: async (segmentId, rendition) => {
|
|
91
|
-
// Trigger prefetch through BaseMediaEngine - let it handle caching
|
|
92
|
-
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
93
|
-
await mediaEngine.fetchMediaSegment(segmentId, rendition);
|
|
94
|
-
// Don't return data - just ensure it's cached in BaseMediaEngine
|
|
95
|
-
},
|
|
96
|
-
isSegmentCached: (segmentId, rendition) => {
|
|
97
|
-
// Check if segment is already cached in BaseMediaEngine
|
|
98
|
-
const mediaEngine = host.mediaEngineTask.value;
|
|
99
|
-
if (!mediaEngine) return false;
|
|
100
|
-
|
|
101
|
-
return mediaEngine.isSegmentCached(segmentId, rendition);
|
|
102
|
-
},
|
|
103
|
-
getRendition: async () => {
|
|
104
|
-
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
105
|
-
const audioRendition = mediaEngine.audioRendition;
|
|
106
|
-
if (!audioRendition) {
|
|
107
|
-
throw new Error("Audio rendition not available");
|
|
108
|
-
}
|
|
109
|
-
return audioRendition;
|
|
110
|
-
},
|
|
111
|
-
logError: console.error,
|
|
112
|
-
},
|
|
113
|
-
timelineContext,
|
|
114
|
-
);
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
};
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { Task } from "@lit/task";
|
|
2
|
-
import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
|
|
3
|
-
import { LRUCache } from "../../../utils/LRUCache.js";
|
|
4
|
-
import type { EFMedia } from "../../EFMedia.js";
|
|
5
|
-
|
|
6
|
-
// DECAY_WEIGHT constant - same as original
|
|
7
|
-
const DECAY_WEIGHT = 0.8;
|
|
8
|
-
|
|
9
|
-
function processFFTData(
|
|
10
|
-
fftData: Uint8Array,
|
|
11
|
-
zeroThresholdPercent = 0.1,
|
|
12
|
-
): Uint8Array {
|
|
13
|
-
// Step 1: Determine the threshold for zeros
|
|
14
|
-
const totalBins = fftData.length;
|
|
15
|
-
const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);
|
|
16
|
-
|
|
17
|
-
// Step 2: Interrogate the FFT output to find the cutoff point
|
|
18
|
-
let zeroCount = 0;
|
|
19
|
-
let cutoffIndex = totalBins; // Default to the end of the array
|
|
20
|
-
|
|
21
|
-
for (let i = totalBins - 1; i >= 0; i--) {
|
|
22
|
-
if (fftData[i] ?? 0 < 10) {
|
|
23
|
-
zeroCount++;
|
|
24
|
-
} else {
|
|
25
|
-
// If we encounter a non-zero value, we can stop
|
|
26
|
-
if (zeroCount >= zeroThresholdCount) {
|
|
27
|
-
cutoffIndex = i + 1; // Include this index
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (cutoffIndex < zeroThresholdCount) {
|
|
34
|
-
return fftData;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Step 3: Resample the "good" portion of the data
|
|
38
|
-
const goodData = fftData.slice(0, cutoffIndex);
|
|
39
|
-
const resampledData = interpolateData(goodData, fftData.length);
|
|
40
|
-
|
|
41
|
-
// Step 4: Attenuate the top 10% of interpolated samples
|
|
42
|
-
const attenuationStartIndex = Math.floor(totalBins * 0.9);
|
|
43
|
-
for (let i = attenuationStartIndex; i < totalBins; i++) {
|
|
44
|
-
// Calculate attenuation factor that goes from 1 to 0 over the top 10%
|
|
45
|
-
const attenuationProgress =
|
|
46
|
-
(i - attenuationStartIndex) / (totalBins - attenuationStartIndex) + 0.2;
|
|
47
|
-
const attenuationFactor = Math.max(0, 1 - attenuationProgress);
|
|
48
|
-
resampledData[i] = Math.floor((resampledData[i] ?? 0) * attenuationFactor);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return resampledData;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function interpolateData(data: Uint8Array, targetSize: number): Uint8Array {
|
|
55
|
-
const resampled = new Uint8Array(targetSize);
|
|
56
|
-
const dataLength = data.length;
|
|
57
|
-
|
|
58
|
-
for (let i = 0; i < targetSize; i++) {
|
|
59
|
-
// Calculate the corresponding index in the original data
|
|
60
|
-
const ratio = (i / (targetSize - 1)) * (dataLength - 1);
|
|
61
|
-
const index = Math.floor(ratio);
|
|
62
|
-
const fraction = ratio - index;
|
|
63
|
-
|
|
64
|
-
// Handle edge cases
|
|
65
|
-
if (index >= dataLength - 1) {
|
|
66
|
-
resampled[i] = data[dataLength - 1] ?? 0; // Last value
|
|
67
|
-
} else {
|
|
68
|
-
// Linear interpolation
|
|
69
|
-
resampled[i] = Math.round(
|
|
70
|
-
(data[index] ?? 0) * (1 - fraction) + (data[index + 1] ?? 0) * fraction,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return resampled;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function makeAudioFrequencyAnalysisTask(element: EFMedia) {
|
|
79
|
-
// Internal cache for this task instance (same as original #frequencyDataCache)
|
|
80
|
-
const cache = new LRUCache<string, Uint8Array>(100);
|
|
81
|
-
|
|
82
|
-
return new Task(element, {
|
|
83
|
-
autoRun: EF_INTERACTIVE,
|
|
84
|
-
onError: (error) => {
|
|
85
|
-
console.error("frequencyDataTask error", error);
|
|
86
|
-
},
|
|
87
|
-
args: () =>
|
|
88
|
-
[
|
|
89
|
-
element.currentSourceTimeMs,
|
|
90
|
-
element.fftSize,
|
|
91
|
-
element.fftDecay,
|
|
92
|
-
element.fftGain,
|
|
93
|
-
element.shouldInterpolateFrequencies,
|
|
94
|
-
] as const,
|
|
95
|
-
task: async (_, { signal }) => {
|
|
96
|
-
if (element.currentSourceTimeMs < 0) return null;
|
|
97
|
-
|
|
98
|
-
const currentTimeMs = element.currentSourceTimeMs;
|
|
99
|
-
|
|
100
|
-
// Calculate exact audio window needed based on fftDecay and frame timing
|
|
101
|
-
const frameIntervalMs = 1000 / 30; // 33.33ms per frame
|
|
102
|
-
|
|
103
|
-
// Need audio from earliest frame to current frame
|
|
104
|
-
const earliestFrameMs =
|
|
105
|
-
currentTimeMs - (element.fftDecay - 1) * frameIntervalMs;
|
|
106
|
-
const fromMs = Math.max(0, earliestFrameMs);
|
|
107
|
-
const maxToMs = currentTimeMs + frameIntervalMs; // Include current frame
|
|
108
|
-
const videoDurationMs = element.intrinsicDurationMs || 0;
|
|
109
|
-
const toMs =
|
|
110
|
-
videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;
|
|
111
|
-
|
|
112
|
-
// If the clamping results in an invalid range (seeking beyond the end), skip analysis silently
|
|
113
|
-
if (fromMs >= toMs) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Check cache early - before expensive audio fetching
|
|
118
|
-
// Use a preliminary cache key that doesn't depend on actual startOffsetMs from audio span
|
|
119
|
-
const preliminaryCacheKey = `${element.shouldInterpolateFrequencies}:${element.fftSize}:${element.fftDecay}:${element.fftGain}:${fromMs}:${currentTimeMs}`;
|
|
120
|
-
const cachedSmoothedData = cache.get(preliminaryCacheKey);
|
|
121
|
-
if (cachedSmoothedData) {
|
|
122
|
-
return cachedSmoothedData;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const { fetchAudioSpanningTime: fetchAudioSpan } = await import(
|
|
126
|
-
"../shared/AudioSpanUtils.ts"
|
|
127
|
-
);
|
|
128
|
-
const audioSpan = await fetchAudioSpan(element, fromMs, toMs, signal);
|
|
129
|
-
|
|
130
|
-
if (!audioSpan || !audioSpan.blob) {
|
|
131
|
-
console.warn("Frequency analysis skipped: no audio data available");
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Decode the real audio data
|
|
136
|
-
const tempAudioContext = new OfflineAudioContext(2, 48000, 48000);
|
|
137
|
-
const arrayBuffer = await audioSpan.blob.arrayBuffer();
|
|
138
|
-
const audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);
|
|
139
|
-
|
|
140
|
-
// Use actual startOffset from audioSpan (relative to requested time)
|
|
141
|
-
const startOffsetMs = audioSpan.startMs;
|
|
142
|
-
|
|
143
|
-
const framesData = await Promise.all(
|
|
144
|
-
Array.from({ length: element.fftDecay }, async (_, i) => {
|
|
145
|
-
const frameOffset = i * (1000 / 30);
|
|
146
|
-
const startTime = Math.max(
|
|
147
|
-
0,
|
|
148
|
-
(currentTimeMs - frameOffset - startOffsetMs) / 1000,
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
// Cache key for this specific frame
|
|
152
|
-
const cacheKey = `${element.shouldInterpolateFrequencies}:${element.fftSize}:${element.fftGain}:${startOffsetMs}:${startTime}`;
|
|
153
|
-
|
|
154
|
-
// Check cache for this specific frame
|
|
155
|
-
const cachedFrame = cache.get(cacheKey);
|
|
156
|
-
if (cachedFrame) {
|
|
157
|
-
return cachedFrame;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Running 48000 * (1 / 30) = 1600 broke something terrible, it came out as 0,
|
|
161
|
-
// I'm assuming weird floating point nonsense to do with running on rosetta
|
|
162
|
-
const SIZE = 48000 / 30;
|
|
163
|
-
let audioContext: OfflineAudioContext;
|
|
164
|
-
try {
|
|
165
|
-
audioContext = new OfflineAudioContext(2, SIZE, 48000);
|
|
166
|
-
} catch (error) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
`[EFMedia.frequencyDataTask] Failed to create OfflineAudioContext(2, ${SIZE}, 48000) for frame ${i} at time ${startTime}s: ${error instanceof Error ? error.message : String(error)}. This is for audio frequency analysis.`,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
const analyser = audioContext.createAnalyser();
|
|
172
|
-
analyser.fftSize = element.fftSize;
|
|
173
|
-
analyser.minDecibels = -90;
|
|
174
|
-
analyser.maxDecibels = -10;
|
|
175
|
-
|
|
176
|
-
const gainNode = audioContext.createGain();
|
|
177
|
-
gainNode.gain.value = element.fftGain;
|
|
178
|
-
|
|
179
|
-
const filter = audioContext.createBiquadFilter();
|
|
180
|
-
filter.type = "bandpass";
|
|
181
|
-
filter.frequency.value = 15000;
|
|
182
|
-
filter.Q.value = 0.05;
|
|
183
|
-
|
|
184
|
-
const audioBufferSource = audioContext.createBufferSource();
|
|
185
|
-
audioBufferSource.buffer = audioBuffer;
|
|
186
|
-
|
|
187
|
-
audioBufferSource.connect(filter);
|
|
188
|
-
filter.connect(gainNode);
|
|
189
|
-
gainNode.connect(analyser);
|
|
190
|
-
analyser.connect(audioContext.destination);
|
|
191
|
-
|
|
192
|
-
audioBufferSource.start(0, startTime, 1 / 30);
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
await audioContext.startRendering();
|
|
196
|
-
const frameData = new Uint8Array(element.fftSize / 2);
|
|
197
|
-
analyser.getByteFrequencyData(frameData);
|
|
198
|
-
|
|
199
|
-
// Cache this frame's analysis
|
|
200
|
-
cache.set(cacheKey, frameData);
|
|
201
|
-
return frameData;
|
|
202
|
-
} finally {
|
|
203
|
-
audioBufferSource.disconnect();
|
|
204
|
-
analyser.disconnect();
|
|
205
|
-
}
|
|
206
|
-
}),
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
const frameLength = framesData[0]?.length ?? 0;
|
|
210
|
-
|
|
211
|
-
// Combine frames with decay
|
|
212
|
-
const smoothedData = new Uint8Array(frameLength);
|
|
213
|
-
for (let i = 0; i < frameLength; i++) {
|
|
214
|
-
let weightedSum = 0;
|
|
215
|
-
let weightSum = 0;
|
|
216
|
-
|
|
217
|
-
framesData.forEach((frame: Uint8Array, frameIndex: number) => {
|
|
218
|
-
const decayWeight = DECAY_WEIGHT ** frameIndex;
|
|
219
|
-
weightedSum += (frame[i] ?? 0) * decayWeight;
|
|
220
|
-
weightSum += decayWeight;
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Apply frequency weights using instance FREQ_WEIGHTS
|
|
227
|
-
smoothedData.forEach((value, i) => {
|
|
228
|
-
const freqWeight = element.FREQ_WEIGHTS[i] ?? 0;
|
|
229
|
-
smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Only return the lower half of the frequency data
|
|
233
|
-
// The top half is zeroed out, which makes for aesthetically unpleasing waveforms
|
|
234
|
-
const slicedData = smoothedData.slice(
|
|
235
|
-
0,
|
|
236
|
-
Math.floor(smoothedData.length / 2),
|
|
237
|
-
);
|
|
238
|
-
const processedData = element.shouldInterpolateFrequencies
|
|
239
|
-
? processFFTData(slicedData)
|
|
240
|
-
: slicedData;
|
|
241
|
-
// Cache with the preliminary key so future requests can skip audio fetching
|
|
242
|
-
cache.set(preliminaryCacheKey, processedData);
|
|
243
|
-
return processedData;
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { TaskStatus } from "@lit/task";
|
|
2
|
-
import { customElement } from "lit/decorators.js";
|
|
3
|
-
import { afterEach, beforeEach, describe, vi } from "vitest";
|
|
4
|
-
import { test as baseTest } from "../../../../test/useMSW.js";
|
|
5
|
-
import { EFAudio } from "../../EFAudio";
|
|
6
|
-
import { makeAudioInitSegmentFetchTask } from "./makeAudioInitSegmentFetchTask";
|
|
7
|
-
|
|
8
|
-
@customElement("test-media-audio-init-segment-fetch")
|
|
9
|
-
class TestMediaAudioInitSegmentFetch extends EFAudio {}
|
|
10
|
-
|
|
11
|
-
declare global {
|
|
12
|
-
interface HTMLElementTagNameMap {
|
|
13
|
-
"test-media-audio-init-segment-fetch": TestMediaAudioInitSegmentFetch;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const test = baseTest.extend<{
|
|
18
|
-
element: TestMediaAudioInitSegmentFetch;
|
|
19
|
-
}>({
|
|
20
|
-
element: async ({}, use) => {
|
|
21
|
-
const element = document.createElement(
|
|
22
|
-
"test-media-audio-init-segment-fetch",
|
|
23
|
-
);
|
|
24
|
-
await use(element);
|
|
25
|
-
element.remove();
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("makeAudioInitSegmentFetchTask", () => {
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
// MSW setup is now handled by test fixtures
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
const elements = document.querySelectorAll(
|
|
36
|
-
"test-media-audio-init-segment-fetch",
|
|
37
|
-
);
|
|
38
|
-
for (const element of elements) {
|
|
39
|
-
element.remove();
|
|
40
|
-
}
|
|
41
|
-
vi.restoreAllMocks();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("creates task with correct initial state", ({ element, expect }) => {
|
|
45
|
-
const task = makeAudioInitSegmentFetchTask(element);
|
|
46
|
-
|
|
47
|
-
expect(task).toBeDefined();
|
|
48
|
-
expect(task.status).toBe(TaskStatus.INITIAL);
|
|
49
|
-
expect(task.value).toBeUndefined();
|
|
50
|
-
expect(task.error).toBeUndefined();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("task integrates with element properties", ({ element, expect }) => {
|
|
54
|
-
const task = makeAudioInitSegmentFetchTask(element);
|
|
55
|
-
|
|
56
|
-
expect(task).toBeDefined();
|
|
57
|
-
expect(task.status).toBe(TaskStatus.INITIAL);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Task } from "@lit/task";
|
|
2
|
-
import type { MediaEngine } from "../../../transcoding/types";
|
|
3
|
-
import type { EFMedia } from "../../EFMedia";
|
|
4
|
-
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
|
|
5
|
-
|
|
6
|
-
export const makeAudioInitSegmentFetchTask = (
|
|
7
|
-
host: EFMedia,
|
|
8
|
-
): Task<readonly [MediaEngine | undefined], ArrayBuffer | undefined> => {
|
|
9
|
-
return new Task(host, {
|
|
10
|
-
args: () => [host.mediaEngineTask.value] as const,
|
|
11
|
-
onError: (error) => {
|
|
12
|
-
console.error("audioInitSegmentFetchTask error", error);
|
|
13
|
-
},
|
|
14
|
-
onComplete: (_value) => {},
|
|
15
|
-
task: async ([_mediaEngine], { signal }) => {
|
|
16
|
-
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
17
|
-
const audioRendition = mediaEngine.getAudioRendition();
|
|
18
|
-
|
|
19
|
-
// Return undefined if no audio rendition available (video-only asset)
|
|
20
|
-
if (!audioRendition) {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return mediaEngine.fetchInitSegment(audioRendition, signal);
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { TaskStatus } from "@lit/task";
|
|
2
|
-
import { customElement } from "lit/decorators.js";
|
|
3
|
-
import { afterEach, beforeEach, describe, vi } from "vitest";
|
|
4
|
-
import { test as baseTest } from "../../../../test/useMSW.js";
|
|
5
|
-
import { EFAudio } from "../../EFAudio";
|
|
6
|
-
import { makeAudioInputTask } from "./makeAudioInputTask";
|
|
7
|
-
|
|
8
|
-
@customElement("test-media-audio-input")
|
|
9
|
-
class TestMediaAudioInput extends EFAudio {}
|
|
10
|
-
|
|
11
|
-
declare global {
|
|
12
|
-
interface HTMLElementTagNameMap {
|
|
13
|
-
"test-media-audio-input": TestMediaAudioInput;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const test = baseTest.extend<{
|
|
18
|
-
element: TestMediaAudioInput;
|
|
19
|
-
}>({
|
|
20
|
-
element: async ({}, use) => {
|
|
21
|
-
const element = document.createElement("test-media-audio-input");
|
|
22
|
-
await use(element);
|
|
23
|
-
element.remove();
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("makeAudioInputTask", () => {
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
// MSW setup is now handled by test fixtures
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
const elements = document.querySelectorAll("test-media-audio-input");
|
|
34
|
-
for (const element of elements) {
|
|
35
|
-
element.remove();
|
|
36
|
-
}
|
|
37
|
-
vi.restoreAllMocks();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("creates task with correct initial state", ({ element, expect }) => {
|
|
41
|
-
const task = makeAudioInputTask(element);
|
|
42
|
-
|
|
43
|
-
expect(task).toBeDefined();
|
|
44
|
-
expect(task.status).toBe(TaskStatus.INITIAL);
|
|
45
|
-
expect(task.value).toBeUndefined();
|
|
46
|
-
expect(task.error).toBeUndefined();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("task integrates with element properties", ({ element, expect }) => {
|
|
50
|
-
const task = makeAudioInputTask(element);
|
|
51
|
-
|
|
52
|
-
expect(task).toBeDefined();
|
|
53
|
-
expect(task.status).toBe(TaskStatus.INITIAL);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { Task } from "@lit/task";
|
|
2
|
-
import { EFMedia } from "../../EFMedia";
|
|
3
|
-
import { BufferedSeekingInput } from "../BufferedSeekingInput";
|
|
4
|
-
import type { InputTask } from "../shared/MediaTaskUtils";
|
|
5
|
-
|
|
6
|
-
export const makeAudioInputTask = (host: EFMedia): InputTask => {
|
|
7
|
-
return new Task<
|
|
8
|
-
readonly [ArrayBuffer | undefined, ArrayBuffer | undefined],
|
|
9
|
-
BufferedSeekingInput | undefined
|
|
10
|
-
>(host, {
|
|
11
|
-
args: () =>
|
|
12
|
-
[
|
|
13
|
-
host.audioInitSegmentFetchTask.value,
|
|
14
|
-
host.audioSegmentFetchTask.value,
|
|
15
|
-
] as const,
|
|
16
|
-
onError: (error) => {
|
|
17
|
-
console.error("audioInputTask error", error);
|
|
18
|
-
},
|
|
19
|
-
onComplete: (_value) => {},
|
|
20
|
-
task: async (_, { signal }) => {
|
|
21
|
-
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
22
|
-
if (signal.aborted) return undefined;
|
|
23
|
-
|
|
24
|
-
const audioRendition = mediaEngine?.audioRendition;
|
|
25
|
-
|
|
26
|
-
// Return undefined if no audio rendition available (video-only asset)
|
|
27
|
-
if (!audioRendition) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const initSegment = await host.audioInitSegmentFetchTask.taskComplete;
|
|
32
|
-
if (signal.aborted) return undefined;
|
|
33
|
-
|
|
34
|
-
const segment = await host.audioSegmentFetchTask.taskComplete;
|
|
35
|
-
if (signal.aborted) return undefined;
|
|
36
|
-
|
|
37
|
-
if (!initSegment || !segment) {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const startTimeOffsetMs = audioRendition.startTimeOffsetMs;
|
|
42
|
-
|
|
43
|
-
const arrayBuffer = await new Blob([initSegment, segment]).arrayBuffer();
|
|
44
|
-
if (signal.aborted) return undefined;
|
|
45
|
-
|
|
46
|
-
return new BufferedSeekingInput(arrayBuffer, {
|
|
47
|
-
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
48
|
-
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
49
|
-
startTimeOffsetMs,
|
|
50
|
-
});
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
};
|