@aelionsdk/export 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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/audio-export.d.ts +23 -0
  4. package/dist/audio-export.d.ts.map +1 -0
  5. package/dist/audio-export.js +120 -0
  6. package/dist/checkpoint.d.ts +58 -0
  7. package/dist/checkpoint.d.ts.map +1 -0
  8. package/dist/checkpoint.js +119 -0
  9. package/dist/image-export.d.ts +40 -0
  10. package/dist/image-export.d.ts.map +1 -0
  11. package/dist/image-export.js +235 -0
  12. package/dist/index.d.ts +12 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +11 -0
  15. package/dist/memory-sink.d.ts +17 -0
  16. package/dist/memory-sink.d.ts.map +1 -0
  17. package/dist/memory-sink.js +60 -0
  18. package/dist/mux-export-worker.d.ts +2 -0
  19. package/dist/mux-export-worker.d.ts.map +1 -0
  20. package/dist/mux-export-worker.js +120 -0
  21. package/dist/opfs-sink.d.ts +20 -0
  22. package/dist/opfs-sink.d.ts.map +1 -0
  23. package/dist/opfs-sink.js +113 -0
  24. package/dist/profiles.d.ts +66 -0
  25. package/dist/profiles.d.ts.map +1 -0
  26. package/dist/profiles.js +284 -0
  27. package/dist/remote-export.d.ts +56 -0
  28. package/dist/remote-export.d.ts.map +1 -0
  29. package/dist/remote-export.js +83 -0
  30. package/dist/resumable-muxed-export.d.ts +84 -0
  31. package/dist/resumable-muxed-export.d.ts.map +1 -0
  32. package/dist/resumable-muxed-export.js +533 -0
  33. package/dist/session.d.ts +47 -0
  34. package/dist/session.d.ts.map +1 -0
  35. package/dist/session.js +483 -0
  36. package/dist/sink-completion.d.ts +8 -0
  37. package/dist/sink-completion.d.ts.map +1 -0
  38. package/dist/sink-completion.js +13 -0
  39. package/dist/webm-export.d.ts +97 -0
  40. package/dist/webm-export.d.ts.map +1 -0
  41. package/dist/webm-export.js +408 -0
  42. package/dist/worker-export.d.ts +25 -0
  43. package/dist/worker-export.d.ts.map +1 -0
  44. package/dist/worker-export.js +202 -0
  45. package/dist/worker-protocol.d.ts +66 -0
  46. package/dist/worker-protocol.d.ts.map +1 -0
  47. package/dist/worker-protocol.js +1 -0
  48. package/package.json +46 -0
@@ -0,0 +1,284 @@
1
+ export const EXPORT_PROFILES = Object.freeze({
2
+ 'webm-vp9-opus': {
3
+ id: 'webm-vp9-opus',
4
+ kind: 'muxed-av',
5
+ mimeType: 'video/webm',
6
+ extension: '.webm',
7
+ videoCodec: 'vp09.00.10.08',
8
+ audioCodec: 'opus',
9
+ resumability: 'checkpointed-units',
10
+ },
11
+ 'mp4-h264-aac': {
12
+ id: 'mp4-h264-aac',
13
+ kind: 'muxed-av',
14
+ mimeType: 'video/mp4',
15
+ extension: '.mp4',
16
+ videoCodec: 'avc1.640028',
17
+ audioCodec: 'mp4a.40.2',
18
+ resumability: 'checkpointed-units',
19
+ },
20
+ 'mp4-av1-aac': {
21
+ id: 'mp4-av1-aac',
22
+ kind: 'muxed-av',
23
+ mimeType: 'video/mp4',
24
+ extension: '.mp4',
25
+ videoCodec: 'av01.0.08M.08',
26
+ audioCodec: 'mp4a.40.2',
27
+ resumability: 'checkpointed-units',
28
+ },
29
+ 'mp4-hevc-aac': {
30
+ id: 'mp4-hevc-aac',
31
+ kind: 'muxed-av',
32
+ mimeType: 'video/mp4',
33
+ extension: '.mp4',
34
+ videoCodec: 'hvc1.1.6.L120.B0',
35
+ audioCodec: 'mp4a.40.2',
36
+ resumability: 'checkpointed-units',
37
+ },
38
+ 'still-png': {
39
+ id: 'still-png',
40
+ kind: 'still',
41
+ mimeType: 'image/png',
42
+ extension: '.png',
43
+ resumability: 'checkpointed-units',
44
+ },
45
+ 'still-jpeg': {
46
+ id: 'still-jpeg',
47
+ kind: 'still',
48
+ mimeType: 'image/jpeg',
49
+ extension: '.jpg',
50
+ resumability: 'checkpointed-units',
51
+ },
52
+ 'still-webp': {
53
+ id: 'still-webp',
54
+ kind: 'still',
55
+ mimeType: 'image/webp',
56
+ extension: '.webp',
57
+ resumability: 'checkpointed-units',
58
+ },
59
+ 'animated-gif': {
60
+ id: 'animated-gif',
61
+ kind: 'animated-image',
62
+ mimeType: 'image/gif',
63
+ extension: '.gif',
64
+ resumability: 'checkpointed-units',
65
+ },
66
+ 'audio-wav': {
67
+ id: 'audio-wav',
68
+ kind: 'audio-only',
69
+ mimeType: 'audio/wav',
70
+ extension: '.wav',
71
+ resumability: 'checkpointed-units',
72
+ },
73
+ });
74
+ const AVC_LEVELS = Object.freeze([
75
+ { levelHex: '1e', maxMacroblocksPerFrame: 1_620, maxMacroblocksPerSecond: 40_500 },
76
+ { levelHex: '1f', maxMacroblocksPerFrame: 3_600, maxMacroblocksPerSecond: 108_000 },
77
+ { levelHex: '20', maxMacroblocksPerFrame: 5_120, maxMacroblocksPerSecond: 216_000 },
78
+ { levelHex: '28', maxMacroblocksPerFrame: 8_192, maxMacroblocksPerSecond: 245_760 },
79
+ { levelHex: '29', maxMacroblocksPerFrame: 8_192, maxMacroblocksPerSecond: 245_760 },
80
+ { levelHex: '2a', maxMacroblocksPerFrame: 8_704, maxMacroblocksPerSecond: 522_240 },
81
+ { levelHex: '32', maxMacroblocksPerFrame: 22_080, maxMacroblocksPerSecond: 589_824 },
82
+ { levelHex: '33', maxMacroblocksPerFrame: 36_864, maxMacroblocksPerSecond: 983_040 },
83
+ { levelHex: '34', maxMacroblocksPerFrame: 36_864, maxMacroblocksPerSecond: 2_073_600 },
84
+ ]);
85
+ function assertPositiveFinite(value, name) {
86
+ if (!Number.isFinite(value) || value <= 0) {
87
+ throw new RangeError(`${name} must be a positive finite number`);
88
+ }
89
+ }
90
+ /**
91
+ * Returns AVC codec strings from the smallest level that can represent the
92
+ * requested macroblock rate through progressively more permissive levels.
93
+ * Each level tries High, Main and Baseline profiles because platform encoders
94
+ * commonly expose only a subset even when they support the same dimensions.
95
+ */
96
+ export function avcCodecCandidates(width, height, framerate) {
97
+ assertPositiveFinite(width, 'width');
98
+ assertPositiveFinite(height, 'height');
99
+ assertPositiveFinite(framerate, 'framerate');
100
+ const macroblocksPerFrame = Math.ceil(width / 16) * Math.ceil(height / 16);
101
+ const macroblocksPerSecond = Math.ceil(macroblocksPerFrame * framerate);
102
+ const firstLevel = AVC_LEVELS.findIndex(level => macroblocksPerFrame <= level.maxMacroblocksPerFrame &&
103
+ macroblocksPerSecond <= level.maxMacroblocksPerSecond);
104
+ if (firstLevel < 0) {
105
+ throw new RangeError(`No bounded AVC level supports ${width.toString()}x${height.toString()} at ${framerate.toString()} fps`);
106
+ }
107
+ return AVC_LEVELS.slice(firstLevel).flatMap(level => [
108
+ `avc1.6400${level.levelHex}`,
109
+ `avc1.4d00${level.levelHex}`,
110
+ `avc1.4200${level.levelHex}`,
111
+ ]);
112
+ }
113
+ export function preferredAvcCodecString(width, height, framerate) {
114
+ const codec = avcCodecCandidates(width, height, framerate)[0];
115
+ if (codec === undefined)
116
+ throw new Error('AVC candidate generation returned no codecs');
117
+ return codec;
118
+ }
119
+ export function avcEncoderConfig(codec, width, height, framerate, bitrate) {
120
+ return {
121
+ codec,
122
+ width,
123
+ height,
124
+ bitrate,
125
+ bitrateMode: 'variable',
126
+ framerate,
127
+ latencyMode: 'quality',
128
+ alpha: 'discard',
129
+ avc: { format: 'avc' },
130
+ };
131
+ }
132
+ /** Main-profile AV1 level chosen conservatively for HD or UHD output. */
133
+ export function av1CodecString(width, height, framerate) {
134
+ assertPositiveFinite(width, 'width');
135
+ assertPositiveFinite(height, 'height');
136
+ assertPositiveFinite(framerate, 'framerate');
137
+ const samplesPerSecond = width * height * framerate;
138
+ return samplesPerSecond > 1920 * 1080 * 30 ? 'av01.0.12M.08' : 'av01.0.08M.08';
139
+ }
140
+ /** Main-tier HEVC level chosen conservatively for HD or UHD output. */
141
+ export function hevcCodecString(width, height, framerate) {
142
+ assertPositiveFinite(width, 'width');
143
+ assertPositiveFinite(height, 'height');
144
+ assertPositiveFinite(framerate, 'framerate');
145
+ const samplesPerSecond = width * height * framerate;
146
+ return samplesPerSecond > 1920 * 1080 * 30 ? 'hvc1.1.6.L153.B0' : 'hvc1.1.6.L120.B0';
147
+ }
148
+ export function negotiatedVideoCodecString(profile, width, height, framerate) {
149
+ if (profile.id === 'mp4-h264-aac') {
150
+ return preferredAvcCodecString(width, height, framerate);
151
+ }
152
+ if (profile.id === 'mp4-av1-aac')
153
+ return av1CodecString(width, height, framerate);
154
+ if (profile.id === 'mp4-hevc-aac')
155
+ return hevcCodecString(width, height, framerate);
156
+ if (profile.videoCodec === undefined)
157
+ throw new RangeError(`${profile.id} has no video codec`);
158
+ return profile.videoCodec;
159
+ }
160
+ const avcNegotiationCache = new Map();
161
+ export function negotiateAvcCodecString(options) {
162
+ const key = JSON.stringify(options);
163
+ let negotiation = avcNegotiationCache.get(key);
164
+ if (negotiation === undefined) {
165
+ negotiation = (async () => {
166
+ const attempts = [];
167
+ if (typeof VideoEncoder !== 'function')
168
+ return { attempts };
169
+ for (const codec of avcCodecCandidates(options.width, options.height, options.framerate)) {
170
+ try {
171
+ const support = await VideoEncoder.isConfigSupported(avcEncoderConfig(codec, options.width, options.height, options.framerate, options.bitrate));
172
+ attempts.push({ codec, supported: support.supported === true });
173
+ if (support.supported)
174
+ return { selected: codec, attempts };
175
+ }
176
+ catch (error) {
177
+ attempts.push({
178
+ codec,
179
+ supported: false,
180
+ error: error instanceof Error ? error.message : 'VideoEncoder probe failed',
181
+ });
182
+ }
183
+ }
184
+ return { attempts };
185
+ })();
186
+ avcNegotiationCache.set(key, negotiation);
187
+ void negotiation.catch(() => avcNegotiationCache.delete(key));
188
+ }
189
+ return negotiation;
190
+ }
191
+ async function encoderSupport(profile, options = {}) {
192
+ const reasons = [];
193
+ if (profile.videoCodec !== undefined) {
194
+ if (typeof VideoEncoder !== 'function')
195
+ reasons.push('EXPORT_VIDEO_ENCODER_UNAVAILABLE');
196
+ else {
197
+ try {
198
+ const width = options.width ?? 1_280;
199
+ const height = options.height ?? 720;
200
+ const framerate = options.framerate ?? 30;
201
+ const codecs = profile.id === 'mp4-h264-aac'
202
+ ? avcCodecCandidates(width, height, framerate)
203
+ : [negotiatedVideoCodecString(profile, width, height, framerate)];
204
+ let supported = false;
205
+ for (const codec of codecs) {
206
+ const result = await VideoEncoder.isConfigSupported(profile.id === 'mp4-h264-aac'
207
+ ? avcEncoderConfig(codec, width, height, framerate, options.videoBitrate ?? 4_000_000)
208
+ : {
209
+ codec,
210
+ width,
211
+ height,
212
+ bitrate: options.videoBitrate ?? 4_000_000,
213
+ bitrateMode: 'variable',
214
+ framerate,
215
+ latencyMode: 'quality',
216
+ alpha: 'discard',
217
+ ...(profile.id === 'mp4-hevc-aac' ? { hevc: { format: 'hevc' } } : {}),
218
+ });
219
+ if (result.supported) {
220
+ supported = true;
221
+ break;
222
+ }
223
+ }
224
+ if (!supported)
225
+ reasons.push('EXPORT_VIDEO_CONFIG_UNSUPPORTED');
226
+ }
227
+ catch {
228
+ reasons.push('EXPORT_VIDEO_CONFIG_PROBE_FAILED');
229
+ }
230
+ }
231
+ }
232
+ if (profile.audioCodec !== undefined) {
233
+ if (typeof AudioEncoder !== 'function')
234
+ reasons.push('EXPORT_AUDIO_ENCODER_UNAVAILABLE');
235
+ else {
236
+ try {
237
+ const supported = await AudioEncoder.isConfigSupported({
238
+ codec: profile.audioCodec,
239
+ sampleRate: options.sampleRate ?? 48_000,
240
+ numberOfChannels: options.numberOfChannels ?? 2,
241
+ bitrate: options.audioBitrate ?? 128_000,
242
+ bitrateMode: 'variable',
243
+ ...(profile.audioCodec === 'mp4a.40.2'
244
+ ? { aac: { format: 'aac' } }
245
+ : profile.audioCodec === 'opus'
246
+ ? { opus: { format: 'opus' } }
247
+ : {}),
248
+ });
249
+ if (!supported.supported)
250
+ reasons.push('EXPORT_AUDIO_CONFIG_UNSUPPORTED');
251
+ }
252
+ catch {
253
+ reasons.push('EXPORT_AUDIO_CONFIG_PROBE_FAILED');
254
+ }
255
+ }
256
+ }
257
+ if (profile.kind === 'still' && typeof OffscreenCanvas !== 'function') {
258
+ reasons.push('EXPORT_IMAGE_CANVAS_UNAVAILABLE');
259
+ }
260
+ if (profile.kind === 'animated-image' && typeof OffscreenCanvas !== 'function') {
261
+ reasons.push('EXPORT_IMAGE_CANVAS_UNAVAILABLE');
262
+ }
263
+ return { profile, supported: reasons.length === 0, reasons };
264
+ }
265
+ export async function probeExportProfiles(options = {}) {
266
+ return Promise.all(Object.values(EXPORT_PROFILES).map(profile => encoderSupport(profile, options)));
267
+ }
268
+ export async function selectExportProfile(options) {
269
+ const ids = [options.preferred, ...(options.fallbacks ?? [])].filter((id, index, values) => values.indexOf(id) === index);
270
+ const attempts = [];
271
+ for (const id of ids) {
272
+ const profile = EXPORT_PROFILES[id];
273
+ if (profile === undefined) {
274
+ throw new RangeError(`Unknown export profile ${String(id)}`);
275
+ }
276
+ const support = await encoderSupport(profile, options);
277
+ attempts.push(support);
278
+ if (support.supported)
279
+ return { selected: support.profile, execution: 'local', attempts };
280
+ }
281
+ return options.remoteAvailable === true
282
+ ? { selected: EXPORT_PROFILES[options.preferred], execution: 'remote', attempts }
283
+ : { execution: 'unsupported', attempts };
284
+ }
@@ -0,0 +1,56 @@
1
+ import { type JsonObject } from '@aelionsdk/core';
2
+ import type { ExportProfileId } from './profiles.js';
3
+ export interface RemoteExportAuthorization {
4
+ readonly scheme: string;
5
+ readonly token: string;
6
+ readonly expiresAtMs?: number;
7
+ }
8
+ export interface RemoteExportAuthorizer {
9
+ authorize(signal?: AbortSignal): Promise<RemoteExportAuthorization>;
10
+ }
11
+ export interface RemoteExportRequest {
12
+ readonly contentId: string;
13
+ readonly idempotencyKey: string;
14
+ readonly profileId: ExportProfileId;
15
+ readonly projectId: string;
16
+ readonly sequenceId: string;
17
+ readonly revision: string;
18
+ readonly manifest: JsonObject;
19
+ }
20
+ export type RemoteExportEvent = {
21
+ readonly type: 'progress';
22
+ readonly progress: number;
23
+ readonly stage?: string;
24
+ } | {
25
+ readonly type: 'completed';
26
+ readonly result: RemoteExportResult;
27
+ };
28
+ export interface RemoteExportResult {
29
+ readonly providerJobId: string;
30
+ readonly contentId: string;
31
+ readonly profileId: ExportProfileId;
32
+ readonly mimeType: string;
33
+ readonly byteLength: number;
34
+ readonly outputUrl?: string;
35
+ readonly outputToken?: string;
36
+ }
37
+ export interface RemoteExportSession {
38
+ readonly providerJobId: string;
39
+ readonly events: AsyncIterable<RemoteExportEvent>;
40
+ cancel(reason?: unknown): Promise<void>;
41
+ cleanup(reason?: unknown): Promise<void>;
42
+ }
43
+ export interface RemoteExportProvider {
44
+ readonly id: string;
45
+ start(request: RemoteExportRequest, authorization: RemoteExportAuthorization, signal?: AbortSignal): Promise<RemoteExportSession>;
46
+ }
47
+ export interface RunRemoteExportOptions {
48
+ readonly provider: RemoteExportProvider;
49
+ readonly authorizer: RemoteExportAuthorizer;
50
+ readonly request: RemoteExportRequest;
51
+ readonly signal?: AbortSignal;
52
+ readonly onProgress?: (progress: number, stage?: string) => void;
53
+ }
54
+ export declare function runRemoteExport(options: RunRemoteExportOptions): Promise<RemoteExportResult>;
55
+ export declare function createRemoteExportContentId(canonicalManifestBytes: Uint8Array, profileId: ExportProfileId, revision: string): Promise<string>;
56
+ //# sourceMappingURL=remote-export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-export.d.ts","sourceRoot":"","sources":["../src/remote-export.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import { AelionError, throwIfAborted } from '@aelionsdk/core';
2
+ function boundedProgress(value) {
3
+ if (!Number.isFinite(value))
4
+ return 0;
5
+ return Math.max(0, Math.min(1, value));
6
+ }
7
+ export async function runRemoteExport(options) {
8
+ throwIfAborted(options.signal, 'Remote export');
9
+ const authorization = await options.authorizer.authorize(options.signal);
10
+ throwIfAborted(options.signal, 'Remote export');
11
+ if (authorization.scheme.length === 0 || authorization.token.length === 0) {
12
+ throw new AelionError([
13
+ {
14
+ code: 'REMOTE_EXPORT_AUTH_INVALID',
15
+ severity: 'error',
16
+ message: 'Remote export authorization is empty',
17
+ recoverable: true,
18
+ },
19
+ ]);
20
+ }
21
+ if (authorization.expiresAtMs !== undefined && authorization.expiresAtMs <= Date.now()) {
22
+ throw new AelionError([
23
+ {
24
+ code: 'REMOTE_EXPORT_AUTH_EXPIRED',
25
+ severity: 'error',
26
+ message: 'Remote export authorization has expired',
27
+ recoverable: true,
28
+ },
29
+ ]);
30
+ }
31
+ let session;
32
+ let lastProgress = 0;
33
+ try {
34
+ session = await options.provider.start(options.request, authorization, options.signal);
35
+ for await (const event of session.events) {
36
+ throwIfAborted(options.signal, 'Remote export');
37
+ if (event.type === 'progress') {
38
+ const progress = boundedProgress(event.progress);
39
+ if (progress < lastProgress) {
40
+ throw new Error('Remote export progress must be monotonic');
41
+ }
42
+ lastProgress = progress;
43
+ options.onProgress?.(progress, event.stage);
44
+ continue;
45
+ }
46
+ if (event.result.providerJobId !== session.providerJobId ||
47
+ event.result.contentId !== options.request.contentId ||
48
+ event.result.profileId !== options.request.profileId) {
49
+ throw new Error('Remote export result identity does not match the request');
50
+ }
51
+ options.onProgress?.(1, 'completed');
52
+ return event.result;
53
+ }
54
+ throw new Error('Remote export event stream ended without a result');
55
+ }
56
+ catch (cause) {
57
+ await Promise.resolve(session?.cancel(cause)).catch(() => undefined);
58
+ await Promise.resolve(session?.cleanup(cause)).catch(() => undefined);
59
+ if (cause instanceof AelionError)
60
+ throw cause;
61
+ throw new AelionError([
62
+ {
63
+ code: options.signal?.aborted === true ? 'OPERATION_ABORTED' : 'REMOTE_EXPORT_FAILED',
64
+ severity: 'error',
65
+ message: options.signal?.aborted === true
66
+ ? 'Remote export was aborted'
67
+ : cause instanceof Error
68
+ ? cause.message
69
+ : 'Remote export failed',
70
+ recoverable: true,
71
+ cause,
72
+ },
73
+ ]);
74
+ }
75
+ }
76
+ export async function createRemoteExportContentId(canonicalManifestBytes, profileId, revision) {
77
+ const prefix = new TextEncoder().encode(`${profileId}\n${revision}\n`);
78
+ const bytes = new Uint8Array(prefix.byteLength + canonicalManifestBytes.byteLength);
79
+ bytes.set(prefix);
80
+ bytes.set(canonicalManifestBytes, prefix.byteLength);
81
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
82
+ return [...digest].map(value => value.toString(16).padStart(2, '0')).join('');
83
+ }
@@ -0,0 +1,84 @@
1
+ import { type MuxedEncoderConfiguration, type WebMExportOptions, type WebMExportResult } from './webm-export.js';
2
+ export type ResumableMuxedProfile = MuxedEncoderConfiguration['profile'];
3
+ export interface ResumableMuxedUnitMetadata {
4
+ readonly index: number;
5
+ readonly videoStartFrame: number;
6
+ readonly videoEndFrameExclusive: number;
7
+ readonly audioStartFrame: number;
8
+ readonly audioEndFrameExclusive: number;
9
+ readonly byteLength: number;
10
+ readonly sha256: string;
11
+ }
12
+ export interface ResumableMuxedExportManifest {
13
+ readonly version: 1;
14
+ readonly contentId: string;
15
+ readonly configurationId: string;
16
+ readonly profile: ResumableMuxedProfile;
17
+ readonly durationUs: number;
18
+ readonly totalUnits: number;
19
+ readonly completedUnits: number;
20
+ readonly units: readonly ResumableMuxedUnitMetadata[];
21
+ readonly mimeType?: string;
22
+ readonly encoderConfiguration?: MuxedEncoderConfiguration;
23
+ readonly updatedAtMs: number;
24
+ }
25
+ export interface ResumableMuxedExportUnit {
26
+ readonly index: number;
27
+ readonly init?: Uint8Array;
28
+ readonly media: Uint8Array;
29
+ }
30
+ export interface ResumableMuxedExportStore {
31
+ loadManifest(key: string, signal?: AbortSignal): Promise<ResumableMuxedExportManifest | undefined>;
32
+ loadUnit(key: string, index: number, signal?: AbortSignal): Promise<ResumableMuxedExportUnit | undefined>;
33
+ /** Commits unit bytes and the advanced manifest in one durable transaction. */
34
+ commitUnit(key: string, unit: ResumableMuxedExportUnit, manifest: ResumableMuxedExportManifest, signal?: AbortSignal): Promise<void>;
35
+ delete(key: string, signal?: AbortSignal): Promise<void>;
36
+ }
37
+ export declare class MemoryResumableMuxedExportStore implements ResumableMuxedExportStore {
38
+ #private;
39
+ loadManifest(key: string, signal?: AbortSignal): Promise<ResumableMuxedExportManifest | undefined>;
40
+ loadUnit(key: string, index: number, signal?: AbortSignal): Promise<ResumableMuxedExportUnit | undefined>;
41
+ commitUnit(key: string, unit: ResumableMuxedExportUnit, manifest: ResumableMuxedExportManifest, signal?: AbortSignal): Promise<void>;
42
+ delete(key: string, signal?: AbortSignal): Promise<void>;
43
+ }
44
+ /**
45
+ * IndexedDB-backed binary checkpoint store. Unit bytes and their prefix
46
+ * manifest advance in the same read/write transaction, so a reload observes
47
+ * either the previous committed prefix or the complete next unit.
48
+ */
49
+ export declare class IndexedDbResumableMuxedExportStore implements ResumableMuxedExportStore {
50
+ #private;
51
+ constructor(options?: {
52
+ readonly databaseName?: string;
53
+ readonly namespace?: string;
54
+ });
55
+ loadManifest(key: string, signal?: AbortSignal): Promise<ResumableMuxedExportManifest | undefined>;
56
+ loadUnit(key: string, index: number, signal?: AbortSignal): Promise<ResumableMuxedExportUnit | undefined>;
57
+ commitUnit(key: string, unit: ResumableMuxedExportUnit, manifest: ResumableMuxedExportManifest, signal?: AbortSignal): Promise<void>;
58
+ delete(key: string, signal?: AbortSignal): Promise<void>;
59
+ }
60
+ export interface ResumableMuxedExportOptions extends Omit<WebMExportOptions, 'onProgress'> {
61
+ readonly key: string;
62
+ readonly contentId: string;
63
+ readonly profile: ResumableMuxedProfile;
64
+ readonly store: ResumableMuxedExportStore;
65
+ /** Target unit duration. Actual boundaries are aligned to complete video frames. */
66
+ readonly segmentDurationUs?: number;
67
+ readonly onProgress?: (progress: number) => void;
68
+ readonly onUnitCommitted?: (completedUnits: number, totalUnits: number) => void;
69
+ readonly deleteCheckpointOnSuccess?: boolean;
70
+ readonly now?: () => number;
71
+ }
72
+ export interface ResumableMuxedExportResult extends WebMExportResult {
73
+ readonly checkpointKey: string;
74
+ readonly totalUnits: number;
75
+ readonly reusedUnits: number;
76
+ readonly encodedUnits: number;
77
+ }
78
+ /**
79
+ * Encodes frame-aligned WebM clusters or fMP4 fragments and checkpoints every
80
+ * complete unit. Resume reuses the committed prefix, verifies every unit hash,
81
+ * and re-encodes only the first missing unit onward.
82
+ */
83
+ export declare function exportResumableMuxed(options: ResumableMuxedExportOptions): Promise<ResumableMuxedExportResult>;
84
+ //# sourceMappingURL=resumable-muxed-export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumable-muxed-export.d.ts","sourceRoot":"","sources":["../src/resumable-muxed-export.ts"],"names":[],"mappings":"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"}