@aelionsdk/sdk 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/audio-controller.d.ts +25 -0
- package/dist/audio-controller.d.ts.map +1 -0
- package/dist/audio-controller.js +235 -0
- package/dist/audio-mastering.d.ts +27 -0
- package/dist/audio-mastering.d.ts.map +1 -0
- package/dist/audio-mastering.js +160 -0
- package/dist/composition.d.ts +75 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +146 -0
- package/dist/default-schemas.d.ts +7 -0
- package/dist/default-schemas.d.ts.map +1 -0
- package/dist/default-schemas.js +18 -0
- package/dist/export-job.d.ts +22 -0
- package/dist/export-job.d.ts.map +1 -0
- package/dist/export-job.js +126 -0
- package/dist/extension-host.d.ts +90 -0
- package/dist/extension-host.d.ts.map +1 -0
- package/dist/extension-host.js +367 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/media-provider.d.ts +49 -0
- package/dist/media-provider.d.ts.map +1 -0
- package/dist/media-provider.js +388 -0
- package/dist/migration-materials.d.ts +10 -0
- package/dist/migration-materials.d.ts.map +1 -0
- package/dist/migration-materials.js +148 -0
- package/dist/migration.d.ts +109 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +1232 -0
- package/dist/persistence.d.ts +73 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +245 -0
- package/dist/player.d.ts +22 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +522 -0
- package/dist/preview-controller.d.ts +62 -0
- package/dist/preview-controller.d.ts.map +1 -0
- package/dist/preview-controller.js +377 -0
- package/dist/preview-quality.d.ts +7 -0
- package/dist/preview-quality.d.ts.map +1 -0
- package/dist/preview-quality.js +8 -0
- package/dist/production-media-provider.d.ts +112 -0
- package/dist/production-media-provider.d.ts.map +1 -0
- package/dist/production-media-provider.js +749 -0
- package/dist/project-builder.d.ts +249 -0
- package/dist/project-builder.d.ts.map +1 -0
- package/dist/project-builder.js +953 -0
- package/dist/runtime-material-registry.d.ts +10 -0
- package/dist/runtime-material-registry.d.ts.map +1 -0
- package/dist/runtime-material-registry.js +23 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +1114 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import { throwIfAborted } from '@aelionsdk/core';
|
|
2
|
+
import { BlobRangeReader, FetchRangeReader, MemoryCacheStore, PageMediaResourceGovernor, createSampleIndexFromReader, createVideoFrameDecodeSessionFromReader, decodeAudioPcmRangeFromReader, decodeStillImageFromReader, decodeVideoFrameAtFromReader, proxyPresentationTimeUs, selectAssetRepresentation, } from '@aelionsdk/media';
|
|
3
|
+
const SAMPLE_INDEX_CACHE_VERSION = 'aelion-sample-index-v1';
|
|
4
|
+
function positiveSafeInteger(value, name) {
|
|
5
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
6
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function nonNegativeSafeInteger(value, name) {
|
|
11
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
12
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function assertIdentifier(value, name) {
|
|
17
|
+
if (value.trim().length === 0 || value.length > 512) {
|
|
18
|
+
throw new TypeError(`${name} must be a non-empty string of at most 512 characters`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function assertOptionalDimension(value, name) {
|
|
22
|
+
if (value !== undefined)
|
|
23
|
+
positiveSafeInteger(value, name);
|
|
24
|
+
}
|
|
25
|
+
function assertOptionalTime(value, name, allowZero) {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return;
|
|
28
|
+
if (!Number.isSafeInteger(value) || value < 0 || (!allowZero && value === 0)) {
|
|
29
|
+
throw new RangeError(`${name} must be ${allowZero ? 'a non-negative' : 'a positive'} safe integer`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function abortReason(signal) {
|
|
33
|
+
return signal.reason instanceof Error
|
|
34
|
+
? signal.reason
|
|
35
|
+
: new DOMException('Operation aborted', 'AbortError');
|
|
36
|
+
}
|
|
37
|
+
function asError(value, fallback) {
|
|
38
|
+
return value instanceof Error ? value : new Error(fallback, { cause: value });
|
|
39
|
+
}
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function serializeSampleIndex(index) {
|
|
44
|
+
return new TextEncoder().encode(JSON.stringify(index, (_key, value) => value instanceof Uint8Array ? { __aelionBytes: [...value] } : value));
|
|
45
|
+
}
|
|
46
|
+
function reviveBytes(value) {
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.map(entry => reviveBytes(entry));
|
|
49
|
+
if (!isRecord(value))
|
|
50
|
+
return value;
|
|
51
|
+
if (Array.isArray(value.__aelionBytes) &&
|
|
52
|
+
value.__aelionBytes.every(byte => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
|
53
|
+
return Uint8Array.from(value.__aelionBytes);
|
|
54
|
+
}
|
|
55
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, reviveBytes(entry)]));
|
|
56
|
+
}
|
|
57
|
+
function deserializeSampleIndex(bytes) {
|
|
58
|
+
let value;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
61
|
+
value = reviveBytes(parsed);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (!isRecord(value) ||
|
|
67
|
+
value.schemaVersion !== '1.0.0' ||
|
|
68
|
+
typeof value.durationUs !== 'number' ||
|
|
69
|
+
!Array.isArray(value.tracks) ||
|
|
70
|
+
!isRecord(value.samples) ||
|
|
71
|
+
!isRecord(value.presentationOrder) ||
|
|
72
|
+
!Array.isArray(value.diagnostics)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
function awaitWithSignal(promise, signal) {
|
|
78
|
+
if (signal === undefined)
|
|
79
|
+
return promise;
|
|
80
|
+
if (signal.aborted)
|
|
81
|
+
return Promise.reject(abortReason(signal));
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const onAbort = () => reject(abortReason(signal));
|
|
84
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
85
|
+
void promise.then(value => {
|
|
86
|
+
signal.removeEventListener('abort', onAbort);
|
|
87
|
+
resolve(value);
|
|
88
|
+
}, (error) => {
|
|
89
|
+
signal.removeEventListener('abort', onAbort);
|
|
90
|
+
reject(asError(error, 'Media operation failed'));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Range-backed media provider for File/Blob, URL and OPFS sources.
|
|
96
|
+
*
|
|
97
|
+
* It keeps whole media out of JavaScript memory, reuses content-addressed
|
|
98
|
+
* SampleIndexes, chooses preview proxies, and bounds decoder work across a page.
|
|
99
|
+
*/
|
|
100
|
+
export class ProductionMediaProvider {
|
|
101
|
+
#assets = new Map();
|
|
102
|
+
#residentIndexes = new Map();
|
|
103
|
+
#decodeSessions = new Map();
|
|
104
|
+
#residentImages = new Map();
|
|
105
|
+
#indexOperations = new Map();
|
|
106
|
+
#cache;
|
|
107
|
+
#governor;
|
|
108
|
+
#ownsGovernor;
|
|
109
|
+
#maxCachedIndexes;
|
|
110
|
+
#maxCachedIndexBytes;
|
|
111
|
+
#maxDecodeSessions;
|
|
112
|
+
#maxCachedVideoFramesPerSession;
|
|
113
|
+
#maxCachedVideoBytesPerSession;
|
|
114
|
+
#maxSequentialDecodeGapUs;
|
|
115
|
+
#maxCachedImages;
|
|
116
|
+
#maxCachedImageBytes;
|
|
117
|
+
#maxImageDecodeBytes;
|
|
118
|
+
#maxConcurrentOperations;
|
|
119
|
+
#maxPendingOperations;
|
|
120
|
+
#waiters = [];
|
|
121
|
+
#lifecycle = new AbortController();
|
|
122
|
+
#cachedIndexBytes = 0;
|
|
123
|
+
#cachedImageBytes = 0;
|
|
124
|
+
#imageCacheHits = 0;
|
|
125
|
+
#imageCacheMisses = 0;
|
|
126
|
+
#clock = 0;
|
|
127
|
+
#activeOperations = 0;
|
|
128
|
+
#operationSequence = 0;
|
|
129
|
+
#generation = 0;
|
|
130
|
+
#disposed = false;
|
|
131
|
+
constructor(options = {}) {
|
|
132
|
+
this.#maxCachedIndexes = positiveSafeInteger(options.maxCachedIndexes ?? 8, 'maxCachedIndexes');
|
|
133
|
+
this.#maxCachedIndexBytes = positiveSafeInteger(options.maxCachedIndexBytes ?? 64 * 1_024 * 1_024, 'maxCachedIndexBytes');
|
|
134
|
+
this.#maxConcurrentOperations = positiveSafeInteger(options.maxConcurrentOperations ?? 4, 'maxConcurrentOperations');
|
|
135
|
+
this.#maxPendingOperations = nonNegativeSafeInteger(options.maxPendingOperations ?? 64, 'maxPendingOperations');
|
|
136
|
+
this.#maxDecodeSessions = positiveSafeInteger(options.maxDecodeSessions ?? this.#maxConcurrentOperations, 'maxDecodeSessions');
|
|
137
|
+
this.#maxCachedVideoFramesPerSession = positiveSafeInteger(options.maxCachedVideoFramesPerSession ?? 24, 'maxCachedVideoFramesPerSession');
|
|
138
|
+
this.#maxCachedVideoBytesPerSession = positiveSafeInteger(options.maxCachedVideoBytesPerSession ?? 96 * 1_024 * 1_024, 'maxCachedVideoBytesPerSession');
|
|
139
|
+
this.#maxSequentialDecodeGapUs = positiveSafeInteger(options.maxSequentialDecodeGapUs ?? 3_000_000, 'maxSequentialDecodeGapUs');
|
|
140
|
+
this.#maxCachedImages = positiveSafeInteger(options.maxCachedImages ?? 16, 'maxCachedImages');
|
|
141
|
+
this.#maxCachedImageBytes = positiveSafeInteger(options.maxCachedImageBytes ?? 256 * 1_024 * 1_024, 'maxCachedImageBytes');
|
|
142
|
+
this.#maxImageDecodeBytes = positiveSafeInteger(options.maxImageDecodeBytes ?? 64 * 1_024 * 1_024, 'maxImageDecodeBytes');
|
|
143
|
+
this.#cache = options.cache ?? new MemoryCacheStore(this.#maxCachedIndexBytes);
|
|
144
|
+
this.#ownsGovernor = options.governor === undefined;
|
|
145
|
+
this.#governor =
|
|
146
|
+
options.governor ??
|
|
147
|
+
new PageMediaResourceGovernor({
|
|
148
|
+
decoderSlots: this.#maxConcurrentOperations,
|
|
149
|
+
gpuBytes: 1,
|
|
150
|
+
cacheBytes: this.#maxCachedIndexBytes,
|
|
151
|
+
}, Math.max(1, this.#maxPendingOperations));
|
|
152
|
+
}
|
|
153
|
+
registerReader(assetId, reader, options = {}) {
|
|
154
|
+
this.#assertActive();
|
|
155
|
+
assertIdentifier(assetId, 'assetId');
|
|
156
|
+
assertIdentifier(reader.id, 'RangeReader id');
|
|
157
|
+
const role = options.role ?? 'original';
|
|
158
|
+
const id = options.id ?? `${assetId}:${role}`;
|
|
159
|
+
assertIdentifier(id, 'representation id');
|
|
160
|
+
assertOptionalTime(options.durationUs, 'durationUs', false);
|
|
161
|
+
assertOptionalTime(options.sourceStartUs, 'sourceStartUs', true);
|
|
162
|
+
assertOptionalDimension(options.width, 'width');
|
|
163
|
+
assertOptionalDimension(options.height, 'height');
|
|
164
|
+
if (options.mimeType !== undefined && options.mimeType.trim().length === 0) {
|
|
165
|
+
throw new TypeError('mimeType must be a non-empty string');
|
|
166
|
+
}
|
|
167
|
+
if (options.contentHash !== undefined && !/^[0-9a-f]{64}$/u.test(options.contentHash)) {
|
|
168
|
+
throw new TypeError('contentHash must be a lowercase SHA-256 value');
|
|
169
|
+
}
|
|
170
|
+
let asset = this.#assets.get(assetId);
|
|
171
|
+
if (asset === undefined) {
|
|
172
|
+
asset = { representations: new Map() };
|
|
173
|
+
this.#assets.set(assetId, asset);
|
|
174
|
+
}
|
|
175
|
+
if (role === 'original') {
|
|
176
|
+
for (const [existingId, existing] of asset.representations) {
|
|
177
|
+
if (existing.role === 'original' && existingId !== id) {
|
|
178
|
+
asset.representations.delete(existingId);
|
|
179
|
+
this.#dropResidentIndex(this.#indexKey(assetId, existingId));
|
|
180
|
+
this.#dropDecodeSessions(assetId, existingId);
|
|
181
|
+
this.#dropResidentImage(this.#indexKey(assetId, existingId));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
asset.representations.set(id, {
|
|
186
|
+
id,
|
|
187
|
+
role,
|
|
188
|
+
reader,
|
|
189
|
+
kind: options.kind ?? 'media',
|
|
190
|
+
...(options.mimeType === undefined ? {} : { mimeType: options.mimeType }),
|
|
191
|
+
...(options.durationUs === undefined ? {} : { durationUs: options.durationUs }),
|
|
192
|
+
...(options.width === undefined ? {} : { width: options.width }),
|
|
193
|
+
...(options.height === undefined ? {} : { height: options.height }),
|
|
194
|
+
...(options.contentHash === undefined ? {} : { contentHash: options.contentHash }),
|
|
195
|
+
...(options.sourceStartUs === undefined ? {} : { sourceStartUs: options.sourceStartUs }),
|
|
196
|
+
});
|
|
197
|
+
this.#generation += 1;
|
|
198
|
+
this.#dropResidentIndex(this.#indexKey(assetId, id));
|
|
199
|
+
this.#dropDecodeSessions(assetId, id);
|
|
200
|
+
this.#dropResidentImage(this.#indexKey(assetId, id));
|
|
201
|
+
}
|
|
202
|
+
registerBlob(assetId, blob, options = {}) {
|
|
203
|
+
const role = options.role ?? 'original';
|
|
204
|
+
const id = options.id ?? `${assetId}:${role}`;
|
|
205
|
+
this.registerReader(assetId, new BlobRangeReader(id, blob), {
|
|
206
|
+
...options,
|
|
207
|
+
id,
|
|
208
|
+
role,
|
|
209
|
+
kind: options.kind ?? (blob.type.startsWith('image/') ? 'image' : 'media'),
|
|
210
|
+
...(options.mimeType === undefined && blob.type.length > 0 ? { mimeType: blob.type } : {}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
registerFile(assetId, file, options = {}) {
|
|
214
|
+
this.registerBlob(assetId, file, options);
|
|
215
|
+
}
|
|
216
|
+
registerImageBlob(assetId, blob, options = {}) {
|
|
217
|
+
this.registerBlob(assetId, blob, {
|
|
218
|
+
...options,
|
|
219
|
+
kind: 'image',
|
|
220
|
+
...(options.mimeType === undefined && blob.type.length > 0 ? { mimeType: blob.type } : {}),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
registerImageFile(assetId, file, options = {}) {
|
|
224
|
+
this.registerImageBlob(assetId, file, options);
|
|
225
|
+
}
|
|
226
|
+
registerUrl(assetId, url, options = {}) {
|
|
227
|
+
const role = options.role ?? 'original';
|
|
228
|
+
const id = options.id ?? `${assetId}:${role}`;
|
|
229
|
+
const readerOptions = {
|
|
230
|
+
...(options.headers === undefined ? {} : { headers: options.headers }),
|
|
231
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
232
|
+
};
|
|
233
|
+
this.registerReader(assetId, new FetchRangeReader(id, url, readerOptions), {
|
|
234
|
+
...options,
|
|
235
|
+
id,
|
|
236
|
+
role,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async registerOpfs(assetId, path, options = {}) {
|
|
240
|
+
this.#assertActive();
|
|
241
|
+
const navigatorValue = Reflect.get(globalThis, 'navigator');
|
|
242
|
+
const storage = isRecord(navigatorValue) ? navigatorValue.storage : undefined;
|
|
243
|
+
const getDirectory = isRecord(storage) ? storage.getDirectory : undefined;
|
|
244
|
+
if (typeof getDirectory !== 'function') {
|
|
245
|
+
throw new Error('Origin Private File System is unavailable');
|
|
246
|
+
}
|
|
247
|
+
const parts = path.split('/');
|
|
248
|
+
if (parts.length === 0 ||
|
|
249
|
+
parts.some(part => part.length === 0 || part === '.' || part === '..')) {
|
|
250
|
+
throw new TypeError('OPFS path must be a relative file path without empty or dot segments');
|
|
251
|
+
}
|
|
252
|
+
const fileName = parts.pop();
|
|
253
|
+
if (fileName === undefined)
|
|
254
|
+
throw new TypeError('OPFS path must name a file');
|
|
255
|
+
let directory = await Reflect.apply(getDirectory, storage, []);
|
|
256
|
+
for (const part of parts)
|
|
257
|
+
directory = await directory.getDirectoryHandle(part);
|
|
258
|
+
const handle = await directory.getFileHandle(fileName);
|
|
259
|
+
const file = await handle.getFile();
|
|
260
|
+
this.registerFile(assetId, file, options);
|
|
261
|
+
}
|
|
262
|
+
unregister(assetId) {
|
|
263
|
+
this.#assertActive();
|
|
264
|
+
const removed = this.#assets.delete(assetId);
|
|
265
|
+
if (!removed)
|
|
266
|
+
return false;
|
|
267
|
+
this.#generation += 1;
|
|
268
|
+
for (const key of [...this.#residentIndexes.keys()]) {
|
|
269
|
+
if (key.startsWith(`${assetId}\u0000`))
|
|
270
|
+
this.#dropResidentIndex(key);
|
|
271
|
+
}
|
|
272
|
+
this.#dropDecodeSessions(assetId);
|
|
273
|
+
for (const key of [...this.#residentImages.keys()]) {
|
|
274
|
+
if (key.startsWith(`${assetId}\u0000`))
|
|
275
|
+
this.#dropResidentImage(key);
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
representationFor(assetId, request = {}) {
|
|
280
|
+
const selected = this.#select(assetId, request);
|
|
281
|
+
return {
|
|
282
|
+
assetId,
|
|
283
|
+
representationId: selected.representation.id,
|
|
284
|
+
role: selected.representation.role,
|
|
285
|
+
usedProxy: selected.usedProxy,
|
|
286
|
+
diagnostics: selected.diagnostics,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async probe(assetId, options = {}) {
|
|
290
|
+
const selected = this.#select(assetId, options);
|
|
291
|
+
const index = await this.#sampleIndex(assetId, selected.representation, options.signal);
|
|
292
|
+
return {
|
|
293
|
+
assetId,
|
|
294
|
+
representationId: selected.representation.id,
|
|
295
|
+
role: selected.representation.role,
|
|
296
|
+
usedProxy: selected.usedProxy,
|
|
297
|
+
diagnostics: selected.diagnostics,
|
|
298
|
+
index,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
async frameAt(assetId, streamIndex, sourceTimeUs, signal, request) {
|
|
302
|
+
this.#assertActive();
|
|
303
|
+
const selected = this.#select(assetId, request ?? { purpose: 'export' });
|
|
304
|
+
if (selected.representation.kind === 'image') {
|
|
305
|
+
if (streamIndex !== 0)
|
|
306
|
+
throw new RangeError('Still images only expose stream index 0');
|
|
307
|
+
return this.#run(signal, operationSignal => this.#imageFrame(assetId, selected.representation, operationSignal));
|
|
308
|
+
}
|
|
309
|
+
const index = await this.#sampleIndex(assetId, selected.representation, signal);
|
|
310
|
+
const presentationTimeUs = proxyPresentationTimeUs(sourceTimeUs, {
|
|
311
|
+
id: selected.representation.id,
|
|
312
|
+
role: selected.representation.role,
|
|
313
|
+
locator: selected.representation.reader.id,
|
|
314
|
+
...(selected.representation.durationUs === undefined
|
|
315
|
+
? {}
|
|
316
|
+
: { durationUs: selected.representation.durationUs }),
|
|
317
|
+
...(selected.representation.sourceStartUs === undefined
|
|
318
|
+
? {}
|
|
319
|
+
: { sourceStartUs: selected.representation.sourceStartUs }),
|
|
320
|
+
});
|
|
321
|
+
return this.#run(signal, async (operationSignal) => {
|
|
322
|
+
const lease = await this.#governor.acquire({
|
|
323
|
+
ownerId: `aelion-media:${++this.#operationSequence}:${assetId}`,
|
|
324
|
+
priority: request?.purpose === 'preview' ? 'preview' : 'export',
|
|
325
|
+
decoderSlots: 1,
|
|
326
|
+
gpuBytes: 0,
|
|
327
|
+
cacheBytes: 0,
|
|
328
|
+
}, operationSignal);
|
|
329
|
+
try {
|
|
330
|
+
const resident = this.#acquireDecodeSession(assetId, selected.representation, streamIndex, index);
|
|
331
|
+
try {
|
|
332
|
+
const result = resident === undefined
|
|
333
|
+
? await decodeVideoFrameAtFromReader(selected.representation.reader, presentationTimeUs, {
|
|
334
|
+
sampleIndex: index,
|
|
335
|
+
streamIndex,
|
|
336
|
+
signal: operationSignal,
|
|
337
|
+
maxDecodeQueueSize: 8,
|
|
338
|
+
})
|
|
339
|
+
: await resident.session.frameAt(presentationTimeUs, operationSignal);
|
|
340
|
+
try {
|
|
341
|
+
throwIfAborted(operationSignal, 'production media video decode');
|
|
342
|
+
return result.frame.clone();
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
result.close();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
if (resident !== undefined) {
|
|
350
|
+
resident.active -= 1;
|
|
351
|
+
resident.access = ++this.#clock;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
await lease.dispose();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
async pcmRange(assetId, streamIndex, startUs, durationUs, signal) {
|
|
361
|
+
this.#assertActive();
|
|
362
|
+
const selected = this.#select(assetId, { purpose: 'export' });
|
|
363
|
+
return this.#run(signal, async (operationSignal) => {
|
|
364
|
+
const lease = await this.#governor.acquire({
|
|
365
|
+
ownerId: `aelion-media:${++this.#operationSequence}:${assetId}:audio`,
|
|
366
|
+
priority: 'preview',
|
|
367
|
+
decoderSlots: 1,
|
|
368
|
+
gpuBytes: 0,
|
|
369
|
+
cacheBytes: 0,
|
|
370
|
+
}, operationSignal);
|
|
371
|
+
try {
|
|
372
|
+
return await decodeAudioPcmRangeFromReader(selected.representation.reader, startUs, durationUs, { streamIndex, signal: operationSignal });
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
await lease.dispose();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
clear() {
|
|
380
|
+
this.#assertActive();
|
|
381
|
+
this.#generation += 1;
|
|
382
|
+
this.#residentIndexes.clear();
|
|
383
|
+
this.#cachedIndexBytes = 0;
|
|
384
|
+
for (const resident of this.#decodeSessions.values())
|
|
385
|
+
resident.session.dispose();
|
|
386
|
+
this.#decodeSessions.clear();
|
|
387
|
+
for (const resident of this.#residentImages.values())
|
|
388
|
+
resident.frame.close();
|
|
389
|
+
this.#residentImages.clear();
|
|
390
|
+
this.#cachedImageBytes = 0;
|
|
391
|
+
}
|
|
392
|
+
registerImageUrl(assetId, url, options = {}) {
|
|
393
|
+
this.registerUrl(assetId, url, { ...options, kind: 'image' });
|
|
394
|
+
}
|
|
395
|
+
snapshot() {
|
|
396
|
+
const decodeSnapshots = [...this.#decodeSessions.values()].map(value => value.session.snapshot());
|
|
397
|
+
return {
|
|
398
|
+
assets: this.#assets.size,
|
|
399
|
+
representations: [...this.#assets.values()].reduce((total, asset) => total + asset.representations.size, 0),
|
|
400
|
+
cachedIndexes: this.#residentIndexes.size,
|
|
401
|
+
cachedIndexBytes: this.#cachedIndexBytes,
|
|
402
|
+
maxCachedIndexes: this.#maxCachedIndexes,
|
|
403
|
+
maxCachedIndexBytes: this.#maxCachedIndexBytes,
|
|
404
|
+
decodeSessions: this.#decodeSessions.size,
|
|
405
|
+
activeDecodeSessions: [...this.#decodeSessions.values()].filter(value => value.active > 0)
|
|
406
|
+
.length,
|
|
407
|
+
maxDecodeSessions: this.#maxDecodeSessions,
|
|
408
|
+
cachedVideoFrames: decodeSnapshots.reduce((total, value) => total + value.cachedFrames, 0),
|
|
409
|
+
cachedVideoBytes: decodeSnapshots.reduce((total, value) => total + value.cachedBytes, 0),
|
|
410
|
+
maxCachedVideoFramesPerSession: this.#maxCachedVideoFramesPerSession,
|
|
411
|
+
maxCachedVideoBytesPerSession: this.#maxCachedVideoBytesPerSession,
|
|
412
|
+
maxSequentialDecodeGapUs: this.#maxSequentialDecodeGapUs,
|
|
413
|
+
decodeCacheHits: decodeSnapshots.reduce((total, value) => total + value.cacheHits, 0),
|
|
414
|
+
decodeCacheMisses: decodeSnapshots.reduce((total, value) => total + value.cacheMisses, 0),
|
|
415
|
+
decodeSeeks: decodeSnapshots.reduce((total, value) => total + value.seeks, 0),
|
|
416
|
+
sequentialDecodedFrames: decodeSnapshots.reduce((total, value) => total + value.sequentialFrames, 0),
|
|
417
|
+
cachedImages: this.#residentImages.size,
|
|
418
|
+
cachedImageBytes: this.#cachedImageBytes,
|
|
419
|
+
maxCachedImages: this.#maxCachedImages,
|
|
420
|
+
maxCachedImageBytes: this.#maxCachedImageBytes,
|
|
421
|
+
maxImageDecodeBytes: this.#maxImageDecodeBytes,
|
|
422
|
+
imageCacheHits: this.#imageCacheHits,
|
|
423
|
+
imageCacheMisses: this.#imageCacheMisses,
|
|
424
|
+
activeOperations: this.#activeOperations,
|
|
425
|
+
pendingOperations: this.#waiters.length,
|
|
426
|
+
maxConcurrentOperations: this.#maxConcurrentOperations,
|
|
427
|
+
maxPendingOperations: this.#maxPendingOperations,
|
|
428
|
+
governor: this.#governor.snapshot(),
|
|
429
|
+
cache: this.#cache.snapshot(),
|
|
430
|
+
disposed: this.#disposed,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
dispose() {
|
|
434
|
+
if (this.#disposed)
|
|
435
|
+
return;
|
|
436
|
+
this.#disposed = true;
|
|
437
|
+
this.#lifecycle.abort(new DOMException('ProductionMediaProvider was disposed', 'AbortError'));
|
|
438
|
+
for (const waiter of this.#waiters.splice(0)) {
|
|
439
|
+
waiter.signal?.removeEventListener('abort', waiter.onAbort);
|
|
440
|
+
waiter.reject(new ReferenceError('ProductionMediaProvider is disposed'));
|
|
441
|
+
}
|
|
442
|
+
this.#assets.clear();
|
|
443
|
+
this.#residentIndexes.clear();
|
|
444
|
+
this.#cachedIndexBytes = 0;
|
|
445
|
+
for (const resident of this.#decodeSessions.values())
|
|
446
|
+
resident.session.dispose();
|
|
447
|
+
this.#decodeSessions.clear();
|
|
448
|
+
for (const resident of this.#residentImages.values())
|
|
449
|
+
resident.frame.close();
|
|
450
|
+
this.#residentImages.clear();
|
|
451
|
+
this.#cachedImageBytes = 0;
|
|
452
|
+
if (this.#ownsGovernor)
|
|
453
|
+
this.#governor.dispose();
|
|
454
|
+
}
|
|
455
|
+
#select(assetId, request) {
|
|
456
|
+
this.#assertActive();
|
|
457
|
+
const asset = this.#assets.get(assetId);
|
|
458
|
+
if (asset === undefined)
|
|
459
|
+
throw new ReferenceError(`Unknown media asset: ${assetId}`);
|
|
460
|
+
const original = [...asset.representations.values()].find(value => value.role === 'original');
|
|
461
|
+
if (original === undefined)
|
|
462
|
+
throw new ReferenceError(`Media asset has no original: ${assetId}`);
|
|
463
|
+
const manifest = {
|
|
464
|
+
id: assetId,
|
|
465
|
+
locator: original.reader.id,
|
|
466
|
+
representations: [...asset.representations.values()].map(value => ({
|
|
467
|
+
id: value.id,
|
|
468
|
+
role: value.role,
|
|
469
|
+
locator: value.reader.id,
|
|
470
|
+
...(value.durationUs === undefined ? {} : { durationUs: value.durationUs }),
|
|
471
|
+
...(value.width === undefined ? {} : { width: value.width }),
|
|
472
|
+
...(value.height === undefined ? {} : { height: value.height }),
|
|
473
|
+
...(value.contentHash === undefined ? {} : { contentHash: value.contentHash }),
|
|
474
|
+
...(value.sourceStartUs === undefined ? {} : { sourceStartUs: value.sourceStartUs }),
|
|
475
|
+
})),
|
|
476
|
+
};
|
|
477
|
+
const selection = selectAssetRepresentation(manifest, {
|
|
478
|
+
purpose: request.purpose ?? 'export',
|
|
479
|
+
...(request.maxDimension === undefined ? {} : { maxDimension: request.maxDimension }),
|
|
480
|
+
...(original.durationUs === undefined ? {} : { sourceDurationUs: original.durationUs }),
|
|
481
|
+
});
|
|
482
|
+
const representation = asset.representations.get(selection.representation.id);
|
|
483
|
+
if (representation === undefined) {
|
|
484
|
+
throw new Error(`Selected media representation is not registered: ${selection.representation.id}`);
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
representation,
|
|
488
|
+
usedProxy: selection.usedProxy,
|
|
489
|
+
diagnostics: selection.diagnostics,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
#sampleIndex(assetId, representation, signal) {
|
|
493
|
+
throwIfAborted(signal, 'production media sample index');
|
|
494
|
+
const key = this.#indexKey(assetId, representation.id);
|
|
495
|
+
const resident = this.#residentIndexes.get(key);
|
|
496
|
+
if (resident !== undefined) {
|
|
497
|
+
resident.access = ++this.#clock;
|
|
498
|
+
return Promise.resolve(resident.index);
|
|
499
|
+
}
|
|
500
|
+
const current = this.#indexOperations.get(key);
|
|
501
|
+
if (current !== undefined)
|
|
502
|
+
return awaitWithSignal(current, signal);
|
|
503
|
+
const generation = this.#generation;
|
|
504
|
+
const operation = this.#run(this.#lifecycle.signal, async (operationSignal) => {
|
|
505
|
+
const address = this.#cacheAddress(representation);
|
|
506
|
+
if (address !== undefined) {
|
|
507
|
+
const cached = await this.#cache.get(address, operationSignal);
|
|
508
|
+
if (cached !== undefined) {
|
|
509
|
+
const restored = deserializeSampleIndex(cached);
|
|
510
|
+
if (restored !== undefined) {
|
|
511
|
+
this.#rememberIndex(key, restored, cached.byteLength, generation);
|
|
512
|
+
this.#hydrateRepresentation(representation, restored, generation);
|
|
513
|
+
return restored;
|
|
514
|
+
}
|
|
515
|
+
await this.#cache.delete(address, operationSignal);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const index = await createSampleIndexFromReader(representation.reader, {
|
|
519
|
+
signal: operationSignal,
|
|
520
|
+
});
|
|
521
|
+
const serialized = serializeSampleIndex(index);
|
|
522
|
+
this.#hydrateRepresentation(representation, index, generation);
|
|
523
|
+
this.#rememberIndex(key, index, serialized.byteLength, generation);
|
|
524
|
+
if (address !== undefined && serialized.byteLength <= this.#maxCachedIndexBytes) {
|
|
525
|
+
await this.#cache.put(address, serialized, operationSignal);
|
|
526
|
+
}
|
|
527
|
+
return index;
|
|
528
|
+
});
|
|
529
|
+
this.#indexOperations.set(key, operation);
|
|
530
|
+
void operation.finally(() => this.#indexOperations.delete(key)).catch(() => undefined);
|
|
531
|
+
return awaitWithSignal(operation, signal);
|
|
532
|
+
}
|
|
533
|
+
#hydrateRepresentation(representation, index, generation) {
|
|
534
|
+
if (generation !== this.#generation)
|
|
535
|
+
return;
|
|
536
|
+
representation.durationUs ??= index.durationUs;
|
|
537
|
+
const video = index.tracks.find(track => track.kind === 'video');
|
|
538
|
+
if (video !== undefined) {
|
|
539
|
+
representation.width ??= video.codedWidth;
|
|
540
|
+
representation.height ??= video.codedHeight;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
#rememberIndex(key, index, byteLength, generation) {
|
|
544
|
+
if (generation !== this.#generation || byteLength > this.#maxCachedIndexBytes)
|
|
545
|
+
return;
|
|
546
|
+
this.#dropResidentIndex(key);
|
|
547
|
+
this.#residentIndexes.set(key, { index, byteLength, access: ++this.#clock });
|
|
548
|
+
this.#cachedIndexBytes += byteLength;
|
|
549
|
+
while (this.#residentIndexes.size > this.#maxCachedIndexes ||
|
|
550
|
+
this.#cachedIndexBytes > this.#maxCachedIndexBytes) {
|
|
551
|
+
const oldest = [...this.#residentIndexes.entries()].sort((left, right) => left[1].access - right[1].access)[0];
|
|
552
|
+
if (oldest === undefined)
|
|
553
|
+
break;
|
|
554
|
+
this.#dropResidentIndex(oldest[0]);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
#dropResidentIndex(key) {
|
|
558
|
+
const resident = this.#residentIndexes.get(key);
|
|
559
|
+
if (resident !== undefined)
|
|
560
|
+
this.#cachedIndexBytes -= resident.byteLength;
|
|
561
|
+
this.#residentIndexes.delete(key);
|
|
562
|
+
}
|
|
563
|
+
#acquireDecodeSession(assetId, representation, streamIndex, index) {
|
|
564
|
+
const key = this.#decodeSessionKey(assetId, representation.id, streamIndex);
|
|
565
|
+
const existing = this.#decodeSessions.get(key);
|
|
566
|
+
if (existing !== undefined) {
|
|
567
|
+
existing.active += 1;
|
|
568
|
+
existing.access = ++this.#clock;
|
|
569
|
+
return existing;
|
|
570
|
+
}
|
|
571
|
+
while (this.#decodeSessions.size >= this.#maxDecodeSessions) {
|
|
572
|
+
const oldest = [...this.#decodeSessions.entries()]
|
|
573
|
+
.filter(([, value]) => value.active === 0)
|
|
574
|
+
.sort((left, right) => left[1].access - right[1].access)[0];
|
|
575
|
+
if (oldest === undefined)
|
|
576
|
+
return undefined;
|
|
577
|
+
oldest[1].session.dispose();
|
|
578
|
+
this.#decodeSessions.delete(oldest[0]);
|
|
579
|
+
}
|
|
580
|
+
const resident = {
|
|
581
|
+
session: createVideoFrameDecodeSessionFromReader(representation.reader, index, {
|
|
582
|
+
streamIndex,
|
|
583
|
+
maxCachedFrames: this.#maxCachedVideoFramesPerSession,
|
|
584
|
+
maxCachedBytes: this.#maxCachedVideoBytesPerSession,
|
|
585
|
+
maxSequentialGapUs: this.#maxSequentialDecodeGapUs,
|
|
586
|
+
}),
|
|
587
|
+
active: 1,
|
|
588
|
+
access: ++this.#clock,
|
|
589
|
+
};
|
|
590
|
+
this.#decodeSessions.set(key, resident);
|
|
591
|
+
return resident;
|
|
592
|
+
}
|
|
593
|
+
#dropDecodeSessions(assetId, representationId) {
|
|
594
|
+
const prefix = representationId === undefined
|
|
595
|
+
? `${assetId}\u0000`
|
|
596
|
+
: `${assetId}\u0000${representationId}\u0000`;
|
|
597
|
+
for (const [key, resident] of [...this.#decodeSessions]) {
|
|
598
|
+
if (!key.startsWith(prefix))
|
|
599
|
+
continue;
|
|
600
|
+
resident.session.dispose();
|
|
601
|
+
this.#decodeSessions.delete(key);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async #imageFrame(assetId, representation, signal) {
|
|
605
|
+
const key = this.#indexKey(assetId, representation.id);
|
|
606
|
+
const resident = this.#residentImages.get(key);
|
|
607
|
+
if (resident !== undefined) {
|
|
608
|
+
resident.access = ++this.#clock;
|
|
609
|
+
this.#imageCacheHits += 1;
|
|
610
|
+
return resident.frame.clone();
|
|
611
|
+
}
|
|
612
|
+
this.#imageCacheMisses += 1;
|
|
613
|
+
const generation = this.#generation;
|
|
614
|
+
const decoded = await decodeStillImageFromReader(representation.reader, {
|
|
615
|
+
maxBytes: this.#maxImageDecodeBytes,
|
|
616
|
+
...(representation.mimeType === undefined ? {} : { mimeType: representation.mimeType }),
|
|
617
|
+
signal,
|
|
618
|
+
});
|
|
619
|
+
const byteLength = (() => {
|
|
620
|
+
try {
|
|
621
|
+
return decoded.frame.allocationSize();
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return decoded.width * decoded.height * 4;
|
|
625
|
+
}
|
|
626
|
+
})();
|
|
627
|
+
const value = {
|
|
628
|
+
frame: decoded.frame,
|
|
629
|
+
byteLength,
|
|
630
|
+
access: ++this.#clock,
|
|
631
|
+
};
|
|
632
|
+
if (generation === this.#generation && byteLength <= this.#maxCachedImageBytes) {
|
|
633
|
+
this.#rememberImage(key, value);
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
return value.frame.clone();
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
if (this.#residentImages.get(key) !== value)
|
|
640
|
+
value.frame.close();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
#rememberImage(key, image) {
|
|
644
|
+
this.#dropResidentImage(key);
|
|
645
|
+
this.#residentImages.set(key, image);
|
|
646
|
+
this.#cachedImageBytes += image.byteLength;
|
|
647
|
+
while (this.#residentImages.size > this.#maxCachedImages ||
|
|
648
|
+
this.#cachedImageBytes > this.#maxCachedImageBytes) {
|
|
649
|
+
const oldest = [...this.#residentImages.entries()].sort((left, right) => left[1].access - right[1].access)[0];
|
|
650
|
+
if (oldest === undefined)
|
|
651
|
+
break;
|
|
652
|
+
this.#dropResidentImage(oldest[0]);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
#dropResidentImage(key) {
|
|
656
|
+
const resident = this.#residentImages.get(key);
|
|
657
|
+
if (resident === undefined)
|
|
658
|
+
return;
|
|
659
|
+
this.#residentImages.delete(key);
|
|
660
|
+
this.#cachedImageBytes -= resident.byteLength;
|
|
661
|
+
resident.frame.close();
|
|
662
|
+
}
|
|
663
|
+
#cacheAddress(representation) {
|
|
664
|
+
return representation.contentHash === undefined
|
|
665
|
+
? undefined
|
|
666
|
+
: {
|
|
667
|
+
namespace: 'sample-index',
|
|
668
|
+
contentHash: representation.contentHash,
|
|
669
|
+
version: SAMPLE_INDEX_CACHE_VERSION,
|
|
670
|
+
variant: representation.id,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
#indexKey(assetId, representationId) {
|
|
674
|
+
return `${assetId}\u0000${representationId}`;
|
|
675
|
+
}
|
|
676
|
+
#decodeSessionKey(assetId, representationId, streamIndex) {
|
|
677
|
+
return `${assetId}\u0000${representationId}\u0000${streamIndex.toString()}`;
|
|
678
|
+
}
|
|
679
|
+
async #run(signal, operation) {
|
|
680
|
+
this.#assertActive();
|
|
681
|
+
const release = await this.#acquireOperation(signal);
|
|
682
|
+
const controller = new AbortController();
|
|
683
|
+
const signals = signal === undefined ? [this.#lifecycle.signal] : [this.#lifecycle.signal, signal];
|
|
684
|
+
const listeners = signals.map(source => {
|
|
685
|
+
const abort = () => controller.abort(abortReason(source));
|
|
686
|
+
source.addEventListener('abort', abort, { once: true });
|
|
687
|
+
if (source.aborted)
|
|
688
|
+
abort();
|
|
689
|
+
return { source, abort };
|
|
690
|
+
});
|
|
691
|
+
try {
|
|
692
|
+
throwIfAborted(controller.signal, 'production media operation');
|
|
693
|
+
return await operation(controller.signal);
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
for (const listener of listeners) {
|
|
697
|
+
listener.source.removeEventListener('abort', listener.abort);
|
|
698
|
+
}
|
|
699
|
+
release();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
#acquireOperation(signal) {
|
|
703
|
+
if (signal?.aborted === true)
|
|
704
|
+
return Promise.reject(abortReason(signal));
|
|
705
|
+
if (this.#activeOperations < this.#maxConcurrentOperations && this.#waiters.length === 0) {
|
|
706
|
+
this.#activeOperations += 1;
|
|
707
|
+
return Promise.resolve(this.#releaseOperation());
|
|
708
|
+
}
|
|
709
|
+
if (this.#waiters.length >= this.#maxPendingOperations) {
|
|
710
|
+
return Promise.reject(new RangeError('MEDIA_RESOURCE_QUEUE_FULL'));
|
|
711
|
+
}
|
|
712
|
+
return new Promise((resolve, reject) => {
|
|
713
|
+
const onAbort = () => {
|
|
714
|
+
const index = this.#waiters.indexOf(waiter);
|
|
715
|
+
if (index < 0)
|
|
716
|
+
return;
|
|
717
|
+
this.#waiters.splice(index, 1);
|
|
718
|
+
reject(signal === undefined ? new Error('Operation aborted') : abortReason(signal));
|
|
719
|
+
};
|
|
720
|
+
const waiter = {
|
|
721
|
+
resolve,
|
|
722
|
+
reject,
|
|
723
|
+
...(signal === undefined ? {} : { signal }),
|
|
724
|
+
onAbort,
|
|
725
|
+
};
|
|
726
|
+
this.#waiters.push(waiter);
|
|
727
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
#releaseOperation() {
|
|
731
|
+
let released = false;
|
|
732
|
+
return () => {
|
|
733
|
+
if (released)
|
|
734
|
+
return;
|
|
735
|
+
released = true;
|
|
736
|
+
this.#activeOperations -= 1;
|
|
737
|
+
const waiter = this.#waiters.shift();
|
|
738
|
+
if (waiter === undefined)
|
|
739
|
+
return;
|
|
740
|
+
waiter.signal?.removeEventListener('abort', waiter.onAbort);
|
|
741
|
+
this.#activeOperations += 1;
|
|
742
|
+
waiter.resolve(this.#releaseOperation());
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
#assertActive() {
|
|
746
|
+
if (this.#disposed)
|
|
747
|
+
throw new ReferenceError('ProductionMediaProvider is disposed');
|
|
748
|
+
}
|
|
749
|
+
}
|