@aelionsdk/media 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FoyonaCZY and AelionSDK contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @aelionsdk/media
2
+
3
+ Range I/O, media indexing, decoding and exact seek for AelionSDK
4
+
5
+ Install with `npm install @aelionsdk/media@next`.
6
+
7
+ Version 0.1.0-beta.1 is a prerelease and its API may change before the first stable release. This package is part of [AelionSDK](https://github.com/FoyonaCZY/AelionSDK); see the repository README for supported browsers, examples and deployment requirements.
@@ -0,0 +1,65 @@
1
+ export interface CacheAddress {
2
+ readonly namespace: 'sample-index' | 'thumbnail' | 'waveform' | 'proxy' | 'derived';
3
+ readonly contentHash: string;
4
+ readonly version: string;
5
+ readonly variant?: string;
6
+ }
7
+ export interface CacheStoreSnapshot {
8
+ readonly entries: number;
9
+ readonly bytes: number;
10
+ readonly maxBytes: number;
11
+ readonly hits: number;
12
+ readonly misses: number;
13
+ readonly evictions: number;
14
+ }
15
+ export interface CacheStore {
16
+ get(address: CacheAddress, signal?: AbortSignal): Promise<Uint8Array | undefined>;
17
+ put(address: CacheAddress, value: Uint8Array, signal?: AbortSignal): Promise<void>;
18
+ delete(address: CacheAddress, signal?: AbortSignal): Promise<void>;
19
+ clear(signal?: AbortSignal): Promise<void>;
20
+ snapshot(): CacheStoreSnapshot;
21
+ }
22
+ export declare function cacheAddressKey(address: CacheAddress): string;
23
+ export declare class MemoryCacheStore implements CacheStore {
24
+ #private;
25
+ constructor(maxBytes?: number);
26
+ get(address: CacheAddress, signal?: AbortSignal): Promise<Uint8Array | undefined>;
27
+ put(address: CacheAddress, value: Uint8Array, signal?: AbortSignal): Promise<void>;
28
+ delete(address: CacheAddress, signal?: AbortSignal): Promise<void>;
29
+ clear(signal?: AbortSignal): Promise<void>;
30
+ snapshot(): CacheStoreSnapshot;
31
+ }
32
+ export declare class TieredCacheStore implements CacheStore {
33
+ readonly memory: CacheStore;
34
+ readonly persistent: CacheStore;
35
+ constructor(memory: CacheStore, persistent: CacheStore);
36
+ get(address: CacheAddress, signal?: AbortSignal): Promise<Uint8Array | undefined>;
37
+ put(address: CacheAddress, value: Uint8Array, signal?: AbortSignal): Promise<void>;
38
+ delete(address: CacheAddress, signal?: AbortSignal): Promise<void>;
39
+ clear(signal?: AbortSignal): Promise<void>;
40
+ snapshot(): CacheStoreSnapshot;
41
+ }
42
+ export interface SegmentedIndexChunk<T> {
43
+ readonly segment: number;
44
+ readonly startUs: number;
45
+ readonly durationUs: number;
46
+ readonly value: T;
47
+ }
48
+ export interface SegmentedIndexOptions<T> {
49
+ readonly durationUs: number;
50
+ readonly segmentDurationUs?: number;
51
+ readonly maxResidentSegments?: number;
52
+ readonly load: (startUs: number, durationUs: number, signal?: AbortSignal) => Promise<T>;
53
+ }
54
+ /** Lazy, bounded time segmentation for long-media indexes and derived data. */
55
+ export declare class SegmentedIndex<T> {
56
+ #private;
57
+ constructor(options: SegmentedIndexOptions<T>);
58
+ segmentAt(timeUs: number, signal?: AbortSignal): Promise<SegmentedIndexChunk<T>>;
59
+ clear(): void;
60
+ snapshot(): {
61
+ readonly residentSegments: number;
62
+ readonly inFlightSegments: number;
63
+ };
64
+ }
65
+ //# sourceMappingURL=cache-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-store.d.ts","sourceRoot":"","sources":["../src/cache-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,202 @@
1
+ import { throwIfAborted } from '@aelionsdk/core';
2
+ export function cacheAddressKey(address) {
3
+ if (!/^[0-9a-f]{64}$/u.test(address.contentHash) ||
4
+ address.version.length === 0 ||
5
+ address.version.length > 128) {
6
+ throw new TypeError('Cache address requires a SHA-256 content hash and bounded version');
7
+ }
8
+ return [address.namespace, address.contentHash, address.version, address.variant ?? ''].join(':');
9
+ }
10
+ export class MemoryCacheStore {
11
+ #entries = new Map();
12
+ #maxBytes;
13
+ #bytes = 0;
14
+ #clock = 0;
15
+ #hits = 0;
16
+ #misses = 0;
17
+ #evictions = 0;
18
+ constructor(maxBytes = 64 * 1_024 * 1_024) {
19
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
20
+ throw new RangeError('maxBytes must be a positive safe integer');
21
+ }
22
+ this.#maxBytes = maxBytes;
23
+ }
24
+ get(address, signal) {
25
+ throwIfAborted(signal, 'Cache read');
26
+ const entry = this.#entries.get(cacheAddressKey(address));
27
+ if (entry === undefined) {
28
+ this.#misses += 1;
29
+ return Promise.resolve(undefined);
30
+ }
31
+ this.#hits += 1;
32
+ entry.lastAccess = ++this.#clock;
33
+ return Promise.resolve(entry.value.slice());
34
+ }
35
+ put(address, value, signal) {
36
+ throwIfAborted(signal, 'Cache write');
37
+ if (value.byteLength > this.#maxBytes) {
38
+ throw new RangeError('Cache entry exceeds the store byte budget');
39
+ }
40
+ const key = cacheAddressKey(address);
41
+ const previous = this.#entries.get(key);
42
+ if (previous !== undefined)
43
+ this.#bytes -= previous.value.byteLength;
44
+ const copy = value.slice();
45
+ this.#entries.set(key, { value: copy, lastAccess: ++this.#clock });
46
+ this.#bytes += copy.byteLength;
47
+ while (this.#bytes > this.#maxBytes) {
48
+ let oldestKey;
49
+ let oldestAccess = Number.POSITIVE_INFINITY;
50
+ for (const [candidateKey, entry] of this.#entries) {
51
+ if (entry.lastAccess < oldestAccess) {
52
+ oldestKey = candidateKey;
53
+ oldestAccess = entry.lastAccess;
54
+ }
55
+ }
56
+ if (oldestKey === undefined)
57
+ break;
58
+ const oldest = this.#entries.get(oldestKey);
59
+ if (oldest !== undefined)
60
+ this.#bytes -= oldest.value.byteLength;
61
+ this.#entries.delete(oldestKey);
62
+ this.#evictions += 1;
63
+ }
64
+ return Promise.resolve();
65
+ }
66
+ delete(address, signal) {
67
+ throwIfAborted(signal, 'Cache delete');
68
+ const key = cacheAddressKey(address);
69
+ const entry = this.#entries.get(key);
70
+ if (entry !== undefined)
71
+ this.#bytes -= entry.value.byteLength;
72
+ this.#entries.delete(key);
73
+ return Promise.resolve();
74
+ }
75
+ clear(signal) {
76
+ throwIfAborted(signal, 'Cache clear');
77
+ this.#entries.clear();
78
+ this.#bytes = 0;
79
+ return Promise.resolve();
80
+ }
81
+ snapshot() {
82
+ return {
83
+ entries: this.#entries.size,
84
+ bytes: this.#bytes,
85
+ maxBytes: this.#maxBytes,
86
+ hits: this.#hits,
87
+ misses: this.#misses,
88
+ evictions: this.#evictions,
89
+ };
90
+ }
91
+ }
92
+ export class TieredCacheStore {
93
+ memory;
94
+ persistent;
95
+ constructor(memory, persistent) {
96
+ this.memory = memory;
97
+ this.persistent = persistent;
98
+ }
99
+ async get(address, signal) {
100
+ const hot = await this.memory.get(address, signal);
101
+ if (hot !== undefined)
102
+ return hot;
103
+ const cold = await this.persistent.get(address, signal);
104
+ if (cold !== undefined)
105
+ await this.memory.put(address, cold, signal);
106
+ return cold;
107
+ }
108
+ async put(address, value, signal) {
109
+ await this.persistent.put(address, value, signal);
110
+ await this.memory.put(address, value, signal);
111
+ }
112
+ async delete(address, signal) {
113
+ await Promise.all([
114
+ this.memory.delete(address, signal),
115
+ this.persistent.delete(address, signal),
116
+ ]);
117
+ }
118
+ async clear(signal) {
119
+ await Promise.all([this.memory.clear(signal), this.persistent.clear(signal)]);
120
+ }
121
+ snapshot() {
122
+ const memory = this.memory.snapshot();
123
+ const persistent = this.persistent.snapshot();
124
+ return {
125
+ entries: persistent.entries,
126
+ bytes: persistent.bytes,
127
+ maxBytes: persistent.maxBytes,
128
+ hits: memory.hits + persistent.hits,
129
+ misses: memory.misses + persistent.misses,
130
+ evictions: memory.evictions + persistent.evictions,
131
+ };
132
+ }
133
+ }
134
+ /** Lazy, bounded time segmentation for long-media indexes and derived data. */
135
+ export class SegmentedIndex {
136
+ #options;
137
+ #segmentDurationUs;
138
+ #maxResidentSegments;
139
+ #segments = new Map();
140
+ #inFlight = new Map();
141
+ #clock = 0;
142
+ constructor(options) {
143
+ const segmentDurationUs = options.segmentDurationUs ?? 60_000_000;
144
+ const maxResidentSegments = options.maxResidentSegments ?? 8;
145
+ if (!Number.isSafeInteger(options.durationUs) ||
146
+ !Number.isSafeInteger(segmentDurationUs) ||
147
+ !Number.isSafeInteger(maxResidentSegments) ||
148
+ options.durationUs <= 0 ||
149
+ segmentDurationUs <= 0 ||
150
+ maxResidentSegments <= 0) {
151
+ throw new RangeError('Invalid segmented index limits');
152
+ }
153
+ this.#options = { ...options, segmentDurationUs, maxResidentSegments };
154
+ this.#segmentDurationUs = segmentDurationUs;
155
+ this.#maxResidentSegments = maxResidentSegments;
156
+ }
157
+ async segmentAt(timeUs, signal) {
158
+ if (!Number.isSafeInteger(timeUs) || timeUs < 0 || timeUs >= this.#options.durationUs) {
159
+ throw new RangeError('Segment time is outside the media duration');
160
+ }
161
+ throwIfAborted(signal, 'Segmented index');
162
+ const segmentDurationUs = this.#segmentDurationUs;
163
+ const segment = Math.floor(timeUs / segmentDurationUs);
164
+ const resident = this.#segments.get(segment);
165
+ if (resident !== undefined) {
166
+ resident.access = ++this.#clock;
167
+ return resident.chunk;
168
+ }
169
+ const pending = this.#inFlight.get(segment);
170
+ if (pending !== undefined)
171
+ return pending;
172
+ const startUs = segment * segmentDurationUs;
173
+ const durationUs = Math.min(segmentDurationUs, this.#options.durationUs - startUs);
174
+ const task = this.#options
175
+ .load(startUs, durationUs, signal)
176
+ .then(value => {
177
+ throwIfAborted(signal, 'Segmented index');
178
+ const chunk = { segment, startUs, durationUs, value };
179
+ this.#segments.set(segment, { chunk, access: ++this.#clock });
180
+ this.#evict();
181
+ return chunk;
182
+ })
183
+ .finally(() => this.#inFlight.delete(segment));
184
+ this.#inFlight.set(segment, task);
185
+ return task;
186
+ }
187
+ clear() {
188
+ this.#segments.clear();
189
+ }
190
+ snapshot() {
191
+ return { residentSegments: this.#segments.size, inFlightSegments: this.#inFlight.size };
192
+ }
193
+ #evict() {
194
+ const limit = this.#maxResidentSegments;
195
+ while (this.#segments.size > limit) {
196
+ const oldest = [...this.#segments.entries()].sort((left, right) => left[1].access - right[1].access)[0];
197
+ if (oldest === undefined)
198
+ return;
199
+ this.#segments.delete(oldest[0]);
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,35 @@
1
+ import type { RangeReader } from './types.js';
2
+ export interface DecodedStillImage {
3
+ readonly frame: VideoFrame;
4
+ readonly width: number;
5
+ readonly height: number;
6
+ readonly colorSpace: VideoColorSpaceInit;
7
+ close(): void;
8
+ }
9
+ export declare function decodeStillImage(blob: Blob, signal?: AbortSignal): Promise<DecodedStillImage>;
10
+ export interface DecodeStillImageFromReaderOptions {
11
+ readonly mimeType?: string;
12
+ readonly maxBytes?: number;
13
+ readonly signal?: AbortSignal;
14
+ }
15
+ /** Decode a bounded still image from any registered range-backed source. */
16
+ export declare function decodeStillImageFromReader(reader: RangeReader, options?: DecodeStillImageFromReaderOptions): Promise<DecodedStillImage>;
17
+ export interface AnimatedImageInfo {
18
+ readonly mimeType: string;
19
+ readonly frameCount: number;
20
+ readonly repetitionCount: number;
21
+ readonly durationUs: number;
22
+ readonly frameDurationsUs: readonly number[];
23
+ }
24
+ export declare class AnimatedImageSource {
25
+ #private;
26
+ private constructor();
27
+ static open(blob: Blob, options?: {
28
+ readonly maxFrames?: number;
29
+ readonly signal?: AbortSignal;
30
+ }): Promise<AnimatedImageSource>;
31
+ get info(): AnimatedImageInfo;
32
+ frameAt(timeUs: number, signal?: AbortSignal): Promise<VideoFrame>;
33
+ dispose(): void;
34
+ }
35
+ //# sourceMappingURL=image-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-adapter.d.ts","sourceRoot":"","sources":["../src/image-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IACzC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAyB5B;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,4EAA4E;AAC5E,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,iBAAiB,CAAC,CAqB5B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED,qBAAa,mBAAmB;;IAK9B,OAAO;WAKa,IAAI,CACtB,IAAI,EAAE,IAAI,EACV,OAAO,GAAE;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GAC3E,OAAO,CAAC,mBAAmB,CAAC;IAyC/B,IAAW,IAAI,IAAI,iBAAiB,CAEnC;IAEY,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IA6BxE,OAAO,IAAI,IAAI;CAKvB"}
@@ -0,0 +1,138 @@
1
+ import { throwIfAborted } from '@aelionsdk/core';
2
+ export async function decodeStillImage(blob, signal) {
3
+ throwIfAborted(signal, 'Still image decode');
4
+ const bitmap = await createImageBitmap(blob, {
5
+ imageOrientation: 'from-image',
6
+ premultiplyAlpha: 'none',
7
+ colorSpaceConversion: 'default',
8
+ });
9
+ try {
10
+ throwIfAborted(signal, 'Still image decode');
11
+ const frame = new VideoFrame(bitmap, { timestamp: 0 });
12
+ return {
13
+ frame,
14
+ width: frame.displayWidth,
15
+ height: frame.displayHeight,
16
+ colorSpace: {
17
+ primaries: frame.colorSpace.primaries,
18
+ transfer: frame.colorSpace.transfer,
19
+ matrix: frame.colorSpace.matrix,
20
+ fullRange: frame.colorSpace.fullRange,
21
+ },
22
+ close: () => frame.close(),
23
+ };
24
+ }
25
+ finally {
26
+ bitmap.close();
27
+ }
28
+ }
29
+ /** Decode a bounded still image from any registered range-backed source. */
30
+ export async function decodeStillImageFromReader(reader, options = {}) {
31
+ const maxBytes = options.maxBytes ?? 64 * 1_024 * 1_024;
32
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
33
+ throw new RangeError('maxBytes must be a positive safe integer');
34
+ }
35
+ throwIfAborted(options.signal, 'Still image range decode');
36
+ const size = await reader.size(options.signal);
37
+ if (!Number.isSafeInteger(size) || size <= 0) {
38
+ throw new RangeError('Still image source must have a positive safe integer size');
39
+ }
40
+ if (size > maxBytes) {
41
+ throw new RangeError(`Still image exceeds the ${maxBytes.toString()} byte decode limit`);
42
+ }
43
+ const read = await reader.read({ offset: 0, length: size }, options.signal);
44
+ throwIfAborted(options.signal, 'Still image range decode');
45
+ return decodeStillImage(new Blob([read.bytes], {
46
+ type: options.mimeType ?? 'application/octet-stream',
47
+ }), options.signal);
48
+ }
49
+ export class AnimatedImageSource {
50
+ #decoder;
51
+ #info;
52
+ #disposed = false;
53
+ constructor(decoder, info) {
54
+ this.#decoder = decoder;
55
+ this.#info = info;
56
+ }
57
+ static async open(blob, options = {}) {
58
+ throwIfAborted(options.signal, 'Animated image open');
59
+ if (typeof ImageDecoder !== 'function') {
60
+ throw new DOMException('WebCodecs ImageDecoder is unavailable', 'NotSupportedError');
61
+ }
62
+ const supported = await ImageDecoder.isTypeSupported(blob.type);
63
+ if (!supported)
64
+ throw new DOMException(`Unsupported image type ${blob.type}`, 'NotSupportedError');
65
+ const decoder = new ImageDecoder({ type: blob.type, data: blob.stream() });
66
+ try {
67
+ await decoder.tracks.ready;
68
+ throwIfAborted(options.signal, 'Animated image open');
69
+ const track = decoder.tracks.selectedTrack;
70
+ if (track === null)
71
+ throw new Error('Animated image has no selected track');
72
+ const maxFrames = options.maxFrames ?? 10_000;
73
+ if (track.frameCount > maxFrames) {
74
+ throw new RangeError(`Animated image exceeds the ${maxFrames.toString()} frame limit`);
75
+ }
76
+ const frameDurationsUs = [];
77
+ for (let frameIndex = 0; frameIndex < track.frameCount; frameIndex += 1) {
78
+ throwIfAborted(options.signal, 'Animated image indexing');
79
+ const decoded = await decoder.decode({ frameIndex, completeFramesOnly: true });
80
+ try {
81
+ frameDurationsUs.push(Math.max(1, decoded.image.duration ?? 1));
82
+ }
83
+ finally {
84
+ decoded.image.close();
85
+ }
86
+ }
87
+ return new AnimatedImageSource(decoder, {
88
+ mimeType: blob.type,
89
+ frameCount: track.frameCount,
90
+ repetitionCount: track.repetitionCount,
91
+ durationUs: frameDurationsUs.reduce((sum, value) => sum + value, 0),
92
+ frameDurationsUs,
93
+ });
94
+ }
95
+ catch (error) {
96
+ decoder.close();
97
+ throw error;
98
+ }
99
+ }
100
+ get info() {
101
+ return this.#info;
102
+ }
103
+ async frameAt(timeUs, signal) {
104
+ if (this.#disposed)
105
+ throw new ReferenceError('AnimatedImageSource is disposed');
106
+ if (!Number.isSafeInteger(timeUs) || timeUs < 0) {
107
+ throw new RangeError('timeUs must be a non-negative safe integer');
108
+ }
109
+ throwIfAborted(signal, 'Animated image decode');
110
+ const loopDurationUs = this.#info.durationUs;
111
+ const localUs = this.#info.repetitionCount === 0
112
+ ? timeUs % loopDurationUs
113
+ : Math.min(timeUs, loopDurationUs * (this.#info.repetitionCount + 1) - 1) % loopDurationUs;
114
+ let accumulatedUs = 0;
115
+ let frameIndex = this.#info.frameCount - 1;
116
+ for (let index = 0; index < this.#info.frameDurationsUs.length; index += 1) {
117
+ accumulatedUs += this.#info.frameDurationsUs[index] ?? 0;
118
+ if (localUs < accumulatedUs) {
119
+ frameIndex = index;
120
+ break;
121
+ }
122
+ }
123
+ const decoded = await this.#decoder.decode({ frameIndex, completeFramesOnly: true });
124
+ try {
125
+ throwIfAborted(signal, 'Animated image decode');
126
+ return decoded.image.clone();
127
+ }
128
+ finally {
129
+ decoded.image.close();
130
+ }
131
+ }
132
+ dispose() {
133
+ if (this.#disposed)
134
+ return;
135
+ this.#disposed = true;
136
+ this.#decoder.close();
137
+ }
138
+ }
@@ -0,0 +1,10 @@
1
+ export * from './cache-store.js';
2
+ export * from './image-adapter.js';
3
+ export * from './opfs-cache.js';
4
+ export * from './proxy.js';
5
+ export * from './range-reader.js';
6
+ export * from './mediabunny-adapter.js';
7
+ export * from './seek.js';
8
+ export * from './types.js';
9
+ export * from './resource-governor.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from './cache-store.js';
2
+ export * from './image-adapter.js';
3
+ export * from './opfs-cache.js';
4
+ export * from './proxy.js';
5
+ export * from './range-reader.js';
6
+ export * from './mediabunny-adapter.js';
7
+ export * from './seek.js';
8
+ export * from './types.js';
9
+ export * from './resource-governor.js';
@@ -0,0 +1,95 @@
1
+ import { type Diagnostic } from '@aelionsdk/core';
2
+ import type { MediaProbeOptions, SampleIndex, RangeReader } from './types.js';
3
+ export declare function videoDecoderResourceSnapshot(): {
4
+ readonly activeDecoders: number;
5
+ readonly retainedFrames: number;
6
+ };
7
+ export declare function createSampleIndex(bytes: Uint8Array, options?: MediaProbeOptions): Promise<SampleIndex>;
8
+ export declare function createSampleIndexFromReader(reader: RangeReader, options?: MediaProbeOptions): Promise<SampleIndex>;
9
+ export declare function probeSampleIndex(bytes: Uint8Array, options?: MediaProbeOptions): Promise<{
10
+ readonly ok: true;
11
+ readonly index: SampleIndex;
12
+ } | {
13
+ readonly ok: false;
14
+ readonly diagnostics: readonly Diagnostic[];
15
+ }>;
16
+ export interface VideoDecodeResult {
17
+ readonly frame: VideoFrame;
18
+ readonly timestampUs: number;
19
+ readonly durationUs: number;
20
+ readonly decodedPackets: number;
21
+ readonly plannedPackets: number;
22
+ readonly decodeStartUs: number;
23
+ readonly targetUs: number;
24
+ close(): void;
25
+ }
26
+ export interface VideoDecodeOptions {
27
+ readonly signal?: AbortSignal;
28
+ readonly maxDecodeQueueSize?: number;
29
+ /** Zero-based index within the container's video tracks. */
30
+ readonly streamIndex?: number;
31
+ /** A caller-owned immutable index may be reused to avoid cold re-indexing on warm seeks. */
32
+ readonly sampleIndex?: SampleIndex;
33
+ }
34
+ export interface VideoFrameDecodeSessionOptions {
35
+ /** Zero-based index within the container's video tracks. */
36
+ readonly streamIndex?: number;
37
+ /** Maximum decoded frames retained by this session. */
38
+ readonly maxCachedFrames?: number;
39
+ /** Maximum decoded frame bytes retained by this session. */
40
+ readonly maxCachedBytes?: number;
41
+ /**
42
+ * Forward jumps larger than this restart at the nearest GOP instead of
43
+ * decoding every intervening frame.
44
+ */
45
+ readonly maxSequentialGapUs?: number;
46
+ }
47
+ export interface VideoFrameDecodeSessionSnapshot {
48
+ readonly streamIndex: number;
49
+ readonly cachedFrames: number;
50
+ readonly cachedBytes: number;
51
+ readonly maxCachedFrames: number;
52
+ readonly maxCachedBytes: number;
53
+ readonly maxSequentialGapUs: number;
54
+ readonly cacheHits: number;
55
+ readonly cacheMisses: number;
56
+ readonly seeks: number;
57
+ readonly sequentialFrames: number;
58
+ readonly resets: number;
59
+ readonly active: boolean;
60
+ readonly disposed: boolean;
61
+ }
62
+ export interface AudioPcmBlock {
63
+ readonly sampleRate: number;
64
+ readonly channelCount: number;
65
+ readonly startUs: number;
66
+ readonly durationUs: number;
67
+ readonly frameCount: number;
68
+ readonly interleaved: Float32Array;
69
+ }
70
+ export interface AudioDecodeOptions {
71
+ readonly signal?: AbortSignal;
72
+ readonly streamIndex?: number;
73
+ }
74
+ export declare function decodeAudioPcmRange(bytes: Uint8Array, startUs: number, durationUs: number, options?: AudioDecodeOptions): Promise<AudioPcmBlock>;
75
+ /** Decode an audio interval without first loading the complete media resource. */
76
+ export declare function decodeAudioPcmRangeFromReader(reader: RangeReader, startUs: number, durationUs: number, options?: AudioDecodeOptions): Promise<AudioPcmBlock>;
77
+ /**
78
+ * A bounded, persistent video decoder session.
79
+ *
80
+ * Sequential requests share one Input, decoder and packet iterator. Backward
81
+ * seeks and large forward jumps restart at the nearest verified key packet.
82
+ * Returned frames are caller-owned; cached frames remain session-owned.
83
+ */
84
+ export declare class VideoFrameDecodeSession {
85
+ #private;
86
+ constructor(reader: RangeReader, index: SampleIndex, options?: VideoFrameDecodeSessionOptions);
87
+ frameAt(targetUs: number, signal?: AbortSignal): Promise<VideoDecodeResult>;
88
+ snapshot(): VideoFrameDecodeSessionSnapshot;
89
+ dispose(): void;
90
+ }
91
+ export declare function createVideoFrameDecodeSessionFromReader(reader: RangeReader, index: SampleIndex, options?: VideoFrameDecodeSessionOptions): VideoFrameDecodeSession;
92
+ export declare function decodeVideoFrameAt(bytes: Uint8Array, targetUs: number, options?: VideoDecodeOptions): Promise<VideoDecodeResult>;
93
+ /** Decode an exact video frame using demand-driven byte-range reads. */
94
+ export declare function decodeVideoFrameAtFromReader(reader: RangeReader, targetUs: number, options?: VideoDecodeOptions): Promise<VideoDecodeResult>;
95
+ //# sourceMappingURL=mediabunny-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mediabunny-adapter.d.ts","sourceRoot":"","sources":["../src/mediabunny-adapter.ts"],"names":[],"mappings":"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"}