@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.
@@ -0,0 +1,149 @@
1
+ import { throwIfAborted } from '@aelionsdk/core';
2
+ const PRIORITY = {
3
+ export: 0,
4
+ preview: 1,
5
+ background: 2,
6
+ };
7
+ function validateBudget(value, allowZero) {
8
+ for (const [name, amount] of [
9
+ ['decoderSlots', value.decoderSlots],
10
+ ['gpuBytes', value.gpuBytes],
11
+ ['cacheBytes', value.cacheBytes],
12
+ ]) {
13
+ if (!Number.isSafeInteger(amount) || amount < 0 || (!allowZero && amount === 0)) {
14
+ throw new RangeError(`MEDIA_RESOURCE_BUDGET_INVALID: ${name}`);
15
+ }
16
+ }
17
+ }
18
+ /** Shared page-level admission controller for decoder, GPU and cache allocations. */
19
+ export class PageMediaResourceGovernor {
20
+ #budget;
21
+ #used = { decoderSlots: 0, gpuBytes: 0, cacheBytes: 0 };
22
+ #leases = new Set();
23
+ #pending = [];
24
+ #maxPending;
25
+ #sequence = 0;
26
+ #disposed = false;
27
+ constructor(budget, maxPending = 128) {
28
+ validateBudget(budget, false);
29
+ if (!Number.isSafeInteger(maxPending) || maxPending <= 0) {
30
+ throw new RangeError('MEDIA_RESOURCE_PENDING_LIMIT_INVALID');
31
+ }
32
+ this.#budget = { ...budget };
33
+ this.#maxPending = maxPending;
34
+ }
35
+ get disposed() {
36
+ return this.#disposed;
37
+ }
38
+ snapshot() {
39
+ return {
40
+ budget: { ...this.#budget },
41
+ used: { ...this.#used },
42
+ activeLeases: this.#leases.size,
43
+ pendingRequests: this.#pending.length,
44
+ disposed: this.#disposed,
45
+ };
46
+ }
47
+ acquire(request, signal) {
48
+ if (this.#disposed)
49
+ return Promise.reject(new ReferenceError('Media resource governor is disposed'));
50
+ validateBudget(request, true);
51
+ if (request.ownerId.length === 0)
52
+ return Promise.reject(new TypeError('MEDIA_RESOURCE_OWNER_INVALID'));
53
+ if (request.decoderSlots > this.#budget.decoderSlots ||
54
+ request.gpuBytes > this.#budget.gpuBytes ||
55
+ request.cacheBytes > this.#budget.cacheBytes) {
56
+ return Promise.reject(new RangeError('MEDIA_RESOURCE_REQUEST_EXCEEDS_PAGE_BUDGET'));
57
+ }
58
+ try {
59
+ throwIfAborted(signal, 'Media resource admission');
60
+ }
61
+ catch (error) {
62
+ return Promise.reject(error instanceof Error ? error : new Error('Media admission failed'));
63
+ }
64
+ if (this.#pending.length === 0 && this.#fits(request)) {
65
+ return Promise.resolve(this.#grant(request));
66
+ }
67
+ if (this.#pending.length >= this.#maxPending) {
68
+ return Promise.reject(new RangeError('MEDIA_RESOURCE_QUEUE_FULL'));
69
+ }
70
+ return new Promise((resolve, reject) => {
71
+ const sequence = ++this.#sequence;
72
+ const onAbort = () => {
73
+ const index = this.#pending.findIndex(value => value.sequence === sequence);
74
+ if (index < 0)
75
+ return;
76
+ this.#pending.splice(index, 1);
77
+ reject(new DOMException('Media resource admission aborted', 'AbortError'));
78
+ };
79
+ this.#pending.push({ sequence, request, resolve, reject, signal, onAbort });
80
+ this.#sortPending();
81
+ signal?.addEventListener('abort', onAbort, { once: true });
82
+ this.#drain();
83
+ });
84
+ }
85
+ dispose() {
86
+ if (this.#disposed)
87
+ return;
88
+ this.#disposed = true;
89
+ for (const pending of this.#pending.splice(0)) {
90
+ pending.signal?.removeEventListener('abort', pending.onAbort);
91
+ pending.reject(new ReferenceError('Media resource governor was disposed'));
92
+ }
93
+ for (const lease of [...this.#leases])
94
+ void lease.dispose();
95
+ }
96
+ #fits(request) {
97
+ return (this.#used.decoderSlots + request.decoderSlots <= this.#budget.decoderSlots &&
98
+ this.#used.gpuBytes + request.gpuBytes <= this.#budget.gpuBytes &&
99
+ this.#used.cacheBytes + request.cacheBytes <= this.#budget.cacheBytes);
100
+ }
101
+ #grant(request) {
102
+ this.#used.decoderSlots += request.decoderSlots;
103
+ this.#used.gpuBytes += request.gpuBytes;
104
+ this.#used.cacheBytes += request.cacheBytes;
105
+ let disposed = false;
106
+ const lease = {
107
+ ownerId: request.ownerId,
108
+ allocation: {
109
+ decoderSlots: request.decoderSlots,
110
+ gpuBytes: request.gpuBytes,
111
+ cacheBytes: request.cacheBytes,
112
+ },
113
+ get disposed() {
114
+ return disposed;
115
+ },
116
+ dispose: () => {
117
+ if (disposed)
118
+ return;
119
+ disposed = true;
120
+ this.#leases.delete(lease);
121
+ this.#used.decoderSlots -= request.decoderSlots;
122
+ this.#used.gpuBytes -= request.gpuBytes;
123
+ this.#used.cacheBytes -= request.cacheBytes;
124
+ this.#drain();
125
+ },
126
+ };
127
+ this.#leases.add(lease);
128
+ return lease;
129
+ }
130
+ #sortPending() {
131
+ this.#pending.sort((left, right) => PRIORITY[left.request.priority] - PRIORITY[right.request.priority] ||
132
+ left.sequence - right.sequence);
133
+ }
134
+ #drain() {
135
+ if (this.#disposed)
136
+ return;
137
+ for (;;) {
138
+ const index = this.#pending.findIndex(value => this.#fits(value.request));
139
+ if (index < 0)
140
+ break;
141
+ const pending = this.#pending[index];
142
+ if (pending === undefined)
143
+ break;
144
+ this.#pending.splice(index, 1);
145
+ pending.signal?.removeEventListener('abort', pending.onAbort);
146
+ pending.resolve(this.#grant(pending.request));
147
+ }
148
+ }
149
+ }
package/dist/seek.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { SampleIndex, SeekPoint } from './types.js';
2
+ export declare function resolveVideoSeek(index: SampleIndex, trackId: number, targetUs: number): SeekPoint;
3
+ //# sourceMappingURL=seek.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seek.d.ts","sourceRoot":"","sources":["../src/seek.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsBtE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,CAqCjG"}
package/dist/seek.js ADDED
@@ -0,0 +1,55 @@
1
+ import { assertTimeUs } from '@aelionsdk/core';
2
+ function upperBound(samples, presentationOrder, targetUs) {
3
+ let low = 0;
4
+ let high = samples.length;
5
+ while (low < high) {
6
+ const middle = low + Math.floor((high - low) / 2);
7
+ const sampleIndex = presentationOrder[middle];
8
+ const timestamp = sampleIndex === undefined
9
+ ? Number.POSITIVE_INFINITY
10
+ : (samples[sampleIndex]?.presentationTimestampUs ?? Number.POSITIVE_INFINITY);
11
+ if (timestamp <= targetUs)
12
+ low = middle + 1;
13
+ else
14
+ high = middle;
15
+ }
16
+ return low;
17
+ }
18
+ export function resolveVideoSeek(index, trackId, targetUs) {
19
+ assertTimeUs(targetUs, 'targetUs');
20
+ const samples = index.samples[trackId];
21
+ const presentationOrder = index.presentationOrder[trackId];
22
+ if (samples === undefined || samples.length === 0) {
23
+ throw new RangeError(`Track ${trackId} has no indexed samples`);
24
+ }
25
+ const track = index.tracks.find(candidate => candidate.id === trackId);
26
+ if (track?.kind !== 'video') {
27
+ throw new TypeError(`Track ${trackId} is not a video track`);
28
+ }
29
+ if (presentationOrder === undefined || presentationOrder.length !== samples.length) {
30
+ throw new RangeError(`Track ${trackId} has an invalid presentation-order index`);
31
+ }
32
+ const insertion = upperBound(samples, presentationOrder, targetUs);
33
+ const presentationPosition = Math.max(0, Math.min(presentationOrder.length - 1, insertion - 1));
34
+ const presentationSample = presentationOrder[presentationPosition];
35
+ if (presentationSample === undefined) {
36
+ throw new RangeError('Seek resolution produced an invalid presentation sample');
37
+ }
38
+ let decodeStartSample = presentationSample;
39
+ while (decodeStartSample > 0 && !samples[decodeStartSample]?.isSync)
40
+ decodeStartSample -= 1;
41
+ const decodeStart = samples[decodeStartSample];
42
+ const presentation = samples[presentationSample];
43
+ if (decodeStart === undefined || presentation === undefined) {
44
+ throw new RangeError('Seek resolution produced an invalid sample index');
45
+ }
46
+ return {
47
+ trackId,
48
+ targetUs,
49
+ decodeStartSample,
50
+ presentationSample,
51
+ decodeStartUs: decodeStart.presentationTimestampUs,
52
+ presentationUs: presentation.presentationTimestampUs,
53
+ samplesToDecode: presentationSample - decodeStartSample + 1,
54
+ };
55
+ }
@@ -0,0 +1,93 @@
1
+ import type { Diagnostic, Rational } from '@aelionsdk/core';
2
+ export type TrackKind = 'video' | 'audio';
3
+ export interface ByteRange {
4
+ readonly offset: number;
5
+ readonly length: number;
6
+ }
7
+ export interface RangeRead {
8
+ readonly bytes: Uint8Array;
9
+ readonly range: ByteRange;
10
+ readonly totalSize: number;
11
+ readonly source: 'memory' | 'blob' | 'network';
12
+ }
13
+ export interface RangeReader {
14
+ readonly id: string;
15
+ readonly kind: 'memory' | 'blob' | 'network';
16
+ size(signal?: AbortSignal): Promise<number>;
17
+ read(range: ByteRange, signal?: AbortSignal): Promise<RangeRead>;
18
+ }
19
+ export interface SampleEntry {
20
+ readonly trackId: number;
21
+ readonly sampleIndex: number;
22
+ readonly kind: TrackKind;
23
+ readonly decodeOrder: number;
24
+ readonly presentationOrder: number;
25
+ readonly sourceSequenceNumber: number;
26
+ /** Presentation timestamp (PTS) on the source timeline. */
27
+ readonly presentationTimestampUs: number;
28
+ readonly durationUs: number;
29
+ /** Zero-origin monotonic decode timeline; not the raw container DTS. */
30
+ readonly normalizedDecodeTimeUs: number;
31
+ readonly isSync: boolean;
32
+ readonly byteOffset?: number;
33
+ readonly byteLength?: number;
34
+ }
35
+ export interface VideoTrackInfo {
36
+ readonly kind: 'video';
37
+ readonly id: number;
38
+ readonly codec: string;
39
+ readonly codecFamily: string;
40
+ readonly codedWidth: number;
41
+ readonly codedHeight: number;
42
+ readonly rotation: number;
43
+ readonly timeBase?: Rational;
44
+ readonly description?: Uint8Array;
45
+ }
46
+ export interface AudioTrackInfo {
47
+ readonly kind: 'audio';
48
+ readonly id: number;
49
+ readonly codec: string;
50
+ readonly codecFamily: string;
51
+ readonly sampleRate: number;
52
+ readonly channelCount: number;
53
+ readonly timeBase?: Rational;
54
+ readonly description?: Uint8Array;
55
+ }
56
+ export type TrackInfo = VideoTrackInfo | AudioTrackInfo;
57
+ export interface SampleIndex {
58
+ readonly schemaVersion: '1.0.0';
59
+ readonly container: 'mp4' | 'webm' | 'unknown';
60
+ readonly durationUs: number;
61
+ readonly tracks: readonly TrackInfo[];
62
+ readonly capabilities: {
63
+ /** PTS, duration, sync state, encoded size and decode order are exact. */
64
+ readonly timingAndSize: true;
65
+ /** Raw container DTS is adapter-dependent and is not exposed by Mediabunny 1.50.8. */
66
+ readonly rawDecodeTimestamps: boolean;
67
+ /** Physical sample offsets are adapter-dependent and are not exposed by Mediabunny 1.50.8. */
68
+ readonly byteOffsets: boolean;
69
+ };
70
+ /** Samples are stored in decode order for each track. */
71
+ readonly samples: Readonly<Record<number, readonly SampleEntry[]>>;
72
+ /** Decode-order sample indexes sorted into presentation order for exact lookup. */
73
+ readonly presentationOrder: Readonly<Record<number, readonly number[]>>;
74
+ readonly diagnostics: readonly Diagnostic[];
75
+ }
76
+ export interface SeekPoint {
77
+ readonly trackId: number;
78
+ readonly targetUs: number;
79
+ readonly decodeStartSample: number;
80
+ readonly presentationSample: number;
81
+ readonly decodeStartUs: number;
82
+ readonly presentationUs: number;
83
+ readonly samplesToDecode: number;
84
+ }
85
+ export interface MediaProbeOptions {
86
+ readonly signal?: AbortSignal;
87
+ readonly includeSamples?: boolean;
88
+ }
89
+ export interface VideoDecoderResourceSnapshot {
90
+ readonly activeDecoders: number;
91
+ readonly retainedFrames: number;
92
+ }
93
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@aelionsdk/media",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Range I/O, media indexing, decoding and exact seek for AelionSDK",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/FoyonaCZY/AelionSDK.git",
9
+ "directory": "packages/media"
10
+ },
11
+ "keywords": [
12
+ "aelion",
13
+ "video",
14
+ "webcodecs",
15
+ "seek"
16
+ ],
17
+ "sideEffects": false,
18
+ "type": "module",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "!dist/.tsbuildinfo"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20.19"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": true
35
+ },
36
+ "dependencies": {
37
+ "@aelionsdk/core": "0.1.0-beta.1",
38
+ "mediabunny": "1.50.8"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -b",
42
+ "typecheck": "tsc -b --pretty false"
43
+ }
44
+ }